Ctrl K
ComunidadGuías…

internal/pluginhost/adapters_interceptors.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pluginhost

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"reflect"
	"sort"
	"strings"

	coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
	coreexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginabi"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
	log "github.com/sirupsen/logrus"
)

func (h *Host) callRequestInterceptor(ctx context.Context, record capabilityRecord, method string, call func(context.Context, pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error), req pluginapi.RequestInterceptRequest) (out pluginapi.RequestInterceptResponse, ok bool) {
	if h == nil || call == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.RequestInterceptResponse{}, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, method, recovered)
			out = pluginapi.RequestInterceptResponse{}
			ok = false
		}
	}()
	resp, errIntercept := call(ctx, req)
	if errIntercept != nil {
		log.Warnf("pluginhost: request interceptor %s failed: %v", record.id, errIntercept)
		return pluginapi.RequestInterceptResponse{}, false
	}
	return resp, true
}

func (h *Host) callRequestBodyPatcher(ctx context.Context, record capabilityRecord, method string, patcher pluginapi.RequestBodyPatcher, body []byte) (out pluginapi.RequestBodyPatchResponse, ok bool) {
	if h == nil || patcher == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.RequestBodyPatchResponse{}, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, method, recovered)
			out = pluginapi.RequestBodyPatchResponse{}
			ok = false
		}
	}()
	resp, errPatch := patcher.PatchRequestBody(ctx, body)
	if errPatch != nil {
		log.Warnf("pluginhost: request body patcher %s failed: %v", record.id, errPatch)
		return pluginapi.RequestBodyPatchResponse{}, false
	}
	return resp, true
}

func (h *Host) callResponseInterceptor(ctx context.Context, record capabilityRecord, interceptor pluginapi.ResponseInterceptor, req pluginapi.ResponseInterceptRequest) (out pluginapi.ResponseInterceptResponse, ok bool) {
	if h == nil || interceptor == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.ResponseInterceptResponse{}, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "ResponseInterceptor.InterceptResponse", recovered)
			out = pluginapi.ResponseInterceptResponse{}
			ok = false
		}
	}()
	resp, errIntercept := interceptor.InterceptResponse(ctx, req)
	if errIntercept != nil {
		log.Warnf("pluginhost: response interceptor %s failed: %v", record.id, errIntercept)
		return pluginapi.ResponseInterceptResponse{}, false
	}
	return resp, true
}

func (h *Host) callStreamChunkInterceptor(ctx context.Context, record capabilityRecord, interceptor pluginapi.StreamChunkInterceptor, req pluginapi.StreamChunkInterceptRequest) (out pluginapi.StreamChunkInterceptResponse, ok bool) {
	if h == nil || interceptor == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.StreamChunkInterceptResponse{}, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "StreamChunkInterceptor.InterceptStreamChunk", recovered)
			out = pluginapi.StreamChunkInterceptResponse{}
			ok = false
		}
	}()
	resp, errIntercept := interceptor.InterceptStreamChunk(ctx, req)
	if errIntercept != nil {
		log.Warnf("pluginhost: stream chunk interceptor %s failed: %v", record.id, errIntercept)
		return pluginapi.StreamChunkInterceptResponse{}, false
	}
	return resp, true
}

func (h *Host) callWebSocketResponseObserver(ctx context.Context, record capabilityRecord, observer pluginapi.WebSocketResponseObserver, event pluginapi.WebSocketResponseEvent) {
	if h == nil || observer == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "WebSocketResponseObserver.ObserveWebSocketResponseEvent", recovered)
		}
	}()
	if errObserve := observer.ObserveWebSocketResponseEvent(ctx, event); errObserve != nil {
		log.Warnf("pluginhost: websocket response observer %s failed: %v", record.id, errObserve)
	}
}

func (h *Host) InterceptRequestBeforeAuth(ctx context.Context, req pluginapi.RequestInterceptRequest) pluginapi.RequestInterceptResponse {
	return h.InterceptRequestBeforeAuthExcept(ctx, req, "")
}

func (h *Host) InterceptRequestBeforeAuthExcept(ctx context.Context, req pluginapi.RequestInterceptRequest, skipPluginID string) pluginapi.RequestInterceptResponse {
	return h.interceptRequest(ctx, req, true, "RequestInterceptor.InterceptRequestBeforeAuth", func(interceptor pluginapi.RequestInterceptor, ctx context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) {
		return interceptor.InterceptRequestBeforeAuth(ctx, req)
	}, skipPluginID)
}

func (h *Host) InterceptRequestAfterAuth(ctx context.Context, req pluginapi.RequestInterceptRequest) pluginapi.RequestInterceptResponse {
	return h.InterceptRequestAfterAuthExcept(ctx, req, "")
}

func (h *Host) InterceptRequestAfterAuthExcept(ctx context.Context, req pluginapi.RequestInterceptRequest, skipPluginID string) pluginapi.RequestInterceptResponse {
	return h.interceptRequest(ctx, req, false, "RequestInterceptor.InterceptRequestAfterAuth", func(interceptor pluginapi.RequestInterceptor, ctx context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) {
		return interceptor.InterceptRequestAfterAuth(ctx, req)
	}, skipPluginID)
}

func (h *Host) interceptRequest(ctx context.Context, req pluginapi.RequestInterceptRequest, beforeAuth bool, method string, invoke func(pluginapi.RequestInterceptor, context.Context, pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error), skipPluginID string) pluginapi.RequestInterceptResponse {
	current := pluginapi.RequestInterceptResponse{
		Headers: cloneHeader(req.Headers),
	}
	skipPluginID = strings.TrimSpace(skipPluginID)
	currentBase := req.Body
	bodyModified := false
	patchMethod := "RequestBodyPatcher.PatchRequestBodyAfterAuth"
	if beforeAuth {
		patchMethod = "RequestBodyPatcher.PatchRequestBodyBeforeAuth"
	}

	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) || record.id == skipPluginID {
			continue
		}
		patcher := requestRecordBodyPatcher(record, beforeAuth)
		interceptor := record.plugin.Capabilities.RequestInterceptor
		if patcher == nil && interceptor == nil {
			continue
		}
		if patcher != nil {
			if len(currentBase) > 0 {
				patchInput := requestPatcherInputBody(patcher, currentBase)
				if patchResp, ok := h.callRequestBodyPatcher(ctx, record, patchMethod, patcher, patchInput); ok && len(patchResp.Edits) > 0 {
					patched, modified, errPatch := applyRequestBodyEdits(currentBase, patchResp.Edits)
					if errPatch != nil {
						log.Warnf("pluginhost: request body patcher %s returned invalid edits: %v", record.id, errPatch)
					} else if modified {
						currentBase = patched
						bodyModified = true
					}
				}
			}
			// When a plugin opts into raw body patching for this phase without also requesting
			// metadata-only request interception (omit_body), skip the legacy full-body RPC call.
			if interceptor == nil || !requestInterceptorOmitsBody(record, beforeAuth) {
				continue
			}
		}
		nextReq := req
		nextReq.Headers = cloneHeader(current.Headers)
		if requestInterceptorOmitsBody(record, beforeAuth) {
			nextReq.Body = nil
		} else if len(currentBase) > 0 {
			nextReq.Body = requestInterceptorInputBody(interceptor, currentBase)
		} else {
			nextReq.Body = nil
		}
		nextReq.Metadata = cloneInterceptorMetadata(req.Metadata)
		if nextReq.CallerScope == "" {
			if scope, ok := nextReq.Metadata[coreexecutor.CallerScopeMetadataKey].(string); ok {
				nextReq.CallerScope = strings.TrimSpace(scope)
			}
		} else if _, ok := nextReq.Metadata[coreexecutor.CallerScopeMetadataKey]; !ok {
			if nextReq.Metadata == nil {
				nextReq.Metadata = make(map[string]any, 1)
			}
			nextReq.Metadata[coreexecutor.CallerScopeMetadataKey] = strings.TrimSpace(nextReq.CallerScope)
		}
		if current.Path != "" {
			if nextReq.Metadata == nil {
				nextReq.Metadata = make(map[string]any, 1)
			}
			nextReq.Metadata[coreexecutor.RequestPathMetadataKey] = current.Path
		}
		if resp, ok := h.callRequestInterceptor(ctx, record, method, func(callCtx context.Context, callReq pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) {
			return invoke(interceptor, callCtx, callReq)
		}, nextReq); ok {
			current.Headers = mergeHeaders(current.Headers, resp.Headers, resp.ClearHeaders)
			if len(resp.ClearHeaders) > 0 {
				current.ClearHeaders = append(current.ClearHeaders, resp.ClearHeaders...)
			}
			if len(resp.Body) > 0 {
				currentBase = requestInterceptorOutputBody(interceptor, resp.Body)
				bodyModified = true
			} else if edits := requestInterceptResponseEdits(resp); len(edits) > 0 && len(currentBase) > 0 {
				patched, modified, errPatch := applyRequestBodyEdits(currentBase, edits)
				if errPatch != nil {
					log.Warnf("pluginhost: request interceptor %s returned invalid edits: %v", record.id, errPatch)
				} else if modified {
					currentBase = patched
					bodyModified = true
				}
			}
			if strings.TrimSpace(resp.Path) != "" {
				current.Path = strings.TrimSpace(resp.Path)
			}
			if resp.Terminate {
				current.Terminate = true
				current.StatusCode = resp.StatusCode
				current.ResponseHeaders = cloneHeader(resp.ResponseHeaders)
				current.ResponseBody = bytes.Clone(resp.ResponseBody)
				break
			}
		}
	}
	if len(current.ClearHeaders) > 0 && len(current.Headers) > 0 {
		filtered := current.ClearHeaders[:0]
		for _, key := range current.ClearHeaders {
			if current.Headers.Values(key) == nil {
				filtered = append(filtered, key)
			}
		}
		if len(filtered) == 0 {
			current.ClearHeaders = nil
		} else {
			current.ClearHeaders = filtered
		}
	}
	if bodyModified {
		current.Body = currentBase
	}
	return current
}

// CompleteRequest schedules terminal notifications without blocking response delivery.
func (h *Host) CompleteRequest(ctx context.Context, completion pluginapi.RequestCompletion) {
	h.CompleteRequestExcept(ctx, completion, "")
}

func (h *Host) HasWebSocketResponseObservers() bool {
	if h == nil {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) {
			continue
		}
		if record.plugin.Capabilities.WebSocketResponseObserver != nil {
			return true
		}
	}
	return false
}

func (h *Host) ObserveWebSocketResponseEvent(ctx context.Context, event pluginapi.WebSocketResponseEvent) {
	h.ObserveWebSocketResponseEventExcept(ctx, event, "")
}

func (h *Host) ObserveWebSocketResponseEventExcept(ctx context.Context, event pluginapi.WebSocketResponseEvent, skipPluginID string) {
	if h == nil {
		return
	}
	if ctx == nil {
		ctx = context.Background()
	} else {
		ctx = context.WithoutCancel(ctx)
	}
	skipPluginID = strings.TrimSpace(skipPluginID)
	for _, record := range h.activeRecords() {
		observer := record.plugin.Capabilities.WebSocketResponseObserver
		if h.isPluginFused(record.id) || observer == nil || record.id == skipPluginID || !h.recordCurrent(record) {
			continue
		}
		next := event
		next.Payload = bytes.Clone(event.Payload)
		next.Metadata = cloneInterceptorMetadata(event.Metadata)
		h.callWebSocketResponseObserver(ctx, record, observer, next)
	}
}

// CompleteRequestExcept notifies lifecycle plugins except the plugin that initiated a nested host execution.
func (h *Host) CompleteRequestExcept(ctx context.Context, completion pluginapi.RequestCompletion, skipPluginID string) {
	if h == nil {
		return
	}
	if ctx == nil {
		ctx = context.Background()
	} else {
		ctx = context.WithoutCancel(ctx)
	}
	skipPluginID = strings.TrimSpace(skipPluginID)
	for _, record := range h.activeRecords() {
		plugin := record.plugin.Capabilities.RequestLifecyclePlugin
		if h.isPluginFused(record.id) || plugin == nil || record.id == skipPluginID || !h.recordCurrent(record) {
			continue
		}
		next := completion
		next.Metadata = cloneInterceptorMetadata(completion.Metadata)
		go func(record capabilityRecord, plugin pluginapi.RequestLifecyclePlugin, completion pluginapi.RequestCompletion) {
			defer func() {
				if recovered := recover(); recovered != nil {
					h.fusePlugin(record.id, "RequestLifecyclePlugin.HandleRequestComplete", recovered)
				}
			}()
			if errComplete := plugin.HandleRequestComplete(ctx, completion); errComplete != nil {
				log.Warnf("pluginhost: request lifecycle plugin %s failed: %v", record.id, errComplete)
			}
		}(record, plugin, next)
	}
}

func (h *Host) InterceptResponse(ctx context.Context, req pluginapi.ResponseInterceptRequest) pluginapi.ResponseInterceptResponse {
	return h.InterceptResponseExcept(ctx, req, "")
}

func (h *Host) InterceptResponseExcept(ctx context.Context, req pluginapi.ResponseInterceptRequest, skipPluginID string) pluginapi.ResponseInterceptResponse {
	current := pluginapi.ResponseInterceptResponse{
		Headers: cloneHeader(req.ResponseHeaders),
		Body:    bytes.Clone(req.Body),
	}
	skipPluginID = strings.TrimSpace(skipPluginID)
	for _, record := range h.activeRecords() {
		interceptor := record.plugin.Capabilities.ResponseInterceptor
		if h.isPluginFused(record.id) || interceptor == nil || record.id == skipPluginID {
			continue
		}
		nextReq := req
		nextReq.RequestHeaders = cloneHeader(req.RequestHeaders)
		nextReq.ResponseHeaders = cloneHeader(current.Headers)
		nextReq.OriginalRequest = bytes.Clone(req.OriginalRequest)
		nextReq.RequestBody = bytes.Clone(req.RequestBody)
		nextReq.Body = bytes.Clone(current.Body)
		nextReq.Metadata = cloneInterceptorMetadata(req.Metadata)
		if resp, ok := h.callResponseInterceptor(ctx, record, interceptor, nextReq); ok {
			current.Headers = mergeHeaders(current.Headers, resp.Headers, resp.ClearHeaders)
			if len(resp.Body) > 0 {
				current.Body = bytes.Clone(resp.Body)
			}
		}
	}
	return current
}

func (h *Host) InterceptStreamChunk(ctx context.Context, req pluginapi.StreamChunkInterceptRequest) pluginapi.StreamChunkInterceptResponse {
	return h.InterceptStreamChunkExcept(ctx, req, "")
}

func (h *Host) InterceptStreamChunkExcept(ctx context.Context, req pluginapi.StreamChunkInterceptRequest, skipPluginID string) pluginapi.StreamChunkInterceptResponse {
	current := pluginapi.StreamChunkInterceptResponse{
		Headers: cloneHeader(req.ResponseHeaders),
		Body:    bytes.Clone(req.Body),
	}
	skipPluginID = strings.TrimSpace(skipPluginID)
	for _, record := range h.activeRecords() {
		interceptor := record.plugin.Capabilities.StreamChunkInterceptor
		if h.isPluginFused(record.id) || interceptor == nil || current.DropChunk || record.id == skipPluginID {
			continue
		}
		nextReq := req
		nextReq.RequestHeaders = cloneHeader(req.RequestHeaders)
		nextReq.ResponseHeaders = cloneHeader(current.Headers)
		// Schema v3+ omits request bodies on payload chunks to avoid re-sending multi-MB
		// prompts across cgo/JSON for every frame. Legacy plugins still receive them.
		if req.ChunkIndex != pluginapi.StreamChunkHeaderInitIndex && streamChunkOmitsRequestBodies(record.plugin.SchemaVersion) {
			nextReq.OriginalRequest = nil
			nextReq.RequestBody = nil
		} else {
			nextReq.OriginalRequest = bytes.Clone(req.OriginalRequest)
			nextReq.RequestBody = bytes.Clone(req.RequestBody)
		}
		nextReq.Body = bytes.Clone(current.Body)
		// Schema v5+ omits HistoryChunks on payload chunks to avoid cloning and re-encoding
		// recent chunk windows across cgo/JSON for every frame. Legacy plugins still receive them.
		if req.ChunkIndex != pluginapi.StreamChunkHeaderInitIndex && streamChunkOmitsHistory(record.plugin.SchemaVersion) {
			nextReq.HistoryChunks = nil
		} else {
			nextReq.HistoryChunks = cloneByteSlices(req.HistoryChunks)
		}
		nextReq.Metadata = cloneInterceptorMetadata(req.Metadata)
		if resp, ok := h.callStreamChunkInterceptor(ctx, record, interceptor, nextReq); ok {
			current.Headers = mergeHeaders(current.Headers, resp.Headers, resp.ClearHeaders)
			if len(resp.Body) > 0 {
				current.Body = bytes.Clone(resp.Body)
			}
			if resp.DropChunk {
				current.DropChunk = true
			}
		}
	}
	return current
}

func (h *Host) HasStreamInterceptors() bool {
	if h == nil {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) {
			continue
		}
		if record.plugin.Capabilities.StreamChunkInterceptor != nil {
			return true
		}
	}
	return false
}

// StreamChunkPayloadIncludesRequestBody reports whether any active stream chunk
// interceptor still requires OriginalRequest/RequestBody on payload chunks
// (schema_version < SchemaVersionStreamChunkOmitRequestBody).
func (h *Host) StreamChunkPayloadIncludesRequestBody() bool {
	if h == nil {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) || record.plugin.Capabilities.StreamChunkInterceptor == nil {
			continue
		}
		if !streamChunkOmitsRequestBodies(record.plugin.SchemaVersion) {
			return true
		}
	}
	return false
}

func streamChunkOmitsRequestBodies(schemaVersion uint32) bool {
	return schemaVersion >= pluginabi.SchemaVersionStreamChunkOmitRequestBody
}

// StreamChunkPayloadIncludesHistory reports whether any active stream chunk
// interceptor still requires HistoryChunks on payload chunks
// (schema_version < SchemaVersionStreamChunkOmitHistory).
func (h *Host) StreamChunkPayloadIncludesHistory() bool {
	if h == nil {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) || record.plugin.Capabilities.StreamChunkInterceptor == nil {
			continue
		}
		if !streamChunkOmitsHistory(record.plugin.SchemaVersion) {
			return true
		}
	}
	return false
}

func requestInterceptorOmitsBody(record capabilityRecord, beforeAuth bool) bool {
	if record.plugin.SchemaVersion < pluginabi.SchemaVersionRequestInterceptorOmitBody {
		return false
	}
	caps := record.plugin.Capabilities
	if caps.RequestInterceptor == nil {
		return false
	}
	return requestInterceptorWantsOmitBody(caps, beforeAuth)
}

func streamChunkOmitsHistory(schemaVersion uint32) bool {
	return schemaVersion >= pluginabi.SchemaVersionStreamChunkOmitHistory
}

func (h *Host) HasRequestInterceptors() bool {
	if h == nil {
		return false
	}
	for _, record := range h.activeRecords() {
		if h.isPluginFused(record.id) {
			continue
		}
		if recordHasRequestInterceptor(record) {
			return true
		}
	}
	return false
}

func recordHasRequestInterceptor(record capabilityRecord) bool {
	caps := record.plugin.Capabilities
	if caps.RequestInterceptor != nil {
		return true
	}
	return requestRecordBodyPatcher(record, true) != nil || requestRecordBodyPatcher(record, false) != nil
}

type funcBodyPatcher func(context.Context, []byte) (pluginapi.RequestBodyPatchResponse, error)

func (f funcBodyPatcher) PatchRequestBody(ctx context.Context, body []byte) (pluginapi.RequestBodyPatchResponse, error) {
	return f(ctx, body)
}

func requestRecordBodyPatcher(record capabilityRecord, beforeAuth bool) pluginapi.RequestBodyPatcher {
	if record.plugin.SchemaVersion < pluginabi.SchemaVersionRequestInterceptorBodyPatch {
		return nil
	}
	caps := record.plugin.Capabilities
	if beforeAuth {
		if caps.RequestBodyPatcherBefore != nil {
			return caps.RequestBodyPatcherBefore
		}
		if caps.RequestInterceptorPatchBodyBefore && caps.RequestInterceptor != nil {
			if p, ok := caps.RequestInterceptor.(interface {
				PatchRequestBodyBeforeAuth(context.Context, []byte) (pluginapi.RequestBodyPatchResponse, error)
			}); ok && p != nil {
				return funcBodyPatcher(p.PatchRequestBodyBeforeAuth)
			}
			if p, ok := caps.RequestInterceptor.(pluginapi.RequestBodyPatcher); ok && p != nil {
				return p
			}
		}
		return nil
	}
	if caps.RequestBodyPatcherAfter != nil {
		return caps.RequestBodyPatcherAfter
	}
	if caps.RequestInterceptorPatchBodyAfter && caps.RequestInterceptor != nil {
		if p, ok := caps.RequestInterceptor.(interface {
			PatchRequestBodyAfterAuth(context.Context, []byte) (pluginapi.RequestBodyPatchResponse, error)
		}); ok && p != nil {
			return funcBodyPatcher(p.PatchRequestBodyAfterAuth)
		}
		if p, ok := caps.RequestInterceptor.(pluginapi.RequestBodyPatcher); ok && p != nil {
			return p
		}
	}
	return nil
}

func requestPatcherInputBody(patcher pluginapi.RequestBodyPatcher, body []byte) []byte {
	if _, isRPC := patcher.(rpcRequestBodyPatcher); isRPC {
		return body
	}
	return bytes.Clone(body)
}

func requestInterceptorInputBody(interceptor pluginapi.RequestInterceptor, body []byte) []byte {
	if _, isRPC := interceptor.(*rpcPluginAdapter); isRPC {
		return body
	}
	return bytes.Clone(body)
}

func requestInterceptorOutputBody(interceptor pluginapi.RequestInterceptor, body []byte) []byte {
	if _, isRPC := interceptor.(*rpcPluginAdapter); isRPC {
		return body
	}
	return bytes.Clone(body)
}

func requestInterceptResponseEdits(resp pluginapi.RequestInterceptResponse) []pluginapi.RequestBodyEdit {
	if len(resp.BodyEdits) > 0 {
		return resp.BodyEdits
	}
	return resp.Edits
}

func requestBodyEditReplacementLen(edit pluginapi.RequestBodyEdit) int {
	if len(edit.ReplacementBytes) > 0 {
		return len(edit.ReplacementBytes)
	}
	return len(edit.Replacement)
}

func appendRequestBodyEditReplacement(dst []byte, edit pluginapi.RequestBodyEdit) []byte {
	if len(edit.ReplacementBytes) > 0 {
		return append(dst, edit.ReplacementBytes...)
	}
	return append(dst, edit.Replacement...)
}

func applyRequestBodyEdits(base []byte, edits []pluginapi.RequestBodyEdit) ([]byte, bool, error) {
	if len(edits) == 0 {
		return base, false, nil
	}
	type indexedEdit struct {
		edit pluginapi.RequestBodyEdit
	}
	sorted := make([]indexedEdit, len(edits))
	for i, edit := range edits {
		sorted[i] = indexedEdit{edit: edit}
	}
	if len(sorted) > 1 {
		sort.SliceStable(sorted, func(i, j int) bool {
			if sorted[i].edit.Start != sorted[j].edit.Start {
				return sorted[i].edit.Start < sorted[j].edit.Start
			}
			return sorted[i].edit.End < sorted[j].edit.End
		})
	}
	baseLen := len(base)
	outLen := baseLen
	prevEnd := 0
	anyChange := false
	for i, item := range sorted {
		e := item.edit
		if e.Start < 0 || e.End < e.Start || e.End > baseLen {
			return base, false, fmt.Errorf("edit[%d] range [%d:%d] out of bounds for body length %d", i, e.Start, e.End, baseLen)
		}
		if i > 0 && e.Start < prevEnd {
			return base, false, fmt.Errorf("edit[%d] range [%d:%d] overlaps previous end %d", i, e.Start, e.End, prevEnd)
		}
		replLen := requestBodyEditReplacementLen(e)
		if e.Start != e.End || replLen > 0 {
			if len(e.ReplacementBytes) > 0 {
				if !bytes.Equal(base[e.Start:e.End], e.ReplacementBytes) {
					anyChange = true
				}
			} else if string(base[e.Start:e.End]) != e.Replacement {
				anyChange = true
			}
		}
		outLen += replLen - (e.End - e.Start)
		prevEnd = e.End
	}
	if !anyChange {
		return base, false, nil
	}
	out := make([]byte, 0, outLen)
	cursor := 0
	for _, item := range sorted {
		e := item.edit
		if e.Start > cursor {
			out = append(out, base[cursor:e.Start]...)
		}
		out = appendRequestBodyEditReplacement(out, e)
		cursor = e.End
	}
	if cursor < baseLen {
		out = append(out, base[cursor:]...)
	}
	if !json.Valid(out) {
		return base, false, fmt.Errorf("patched request body is not valid JSON")
	}
	return out, true, nil
}

func readAndRestoreRequestBody(r *http.Request) ([]byte, error) {
	if r == nil || r.Body == nil {
		return nil, nil
	}
	origBody := r.Body
	body, errReadAll := io.ReadAll(origBody)
	if errReadAll != nil {
		r.Body = struct {
			io.Reader
			io.Closer
		}{
			Reader: io.MultiReader(bytes.NewReader(body), origBody),
			Closer: origBody,
		}
		return nil, errReadAll
	}
	r.Body = io.NopCloser(bytes.NewReader(body))
	return body, nil
}

func authID(auth *coreauth.Auth) string {
	if auth == nil {
		return ""
	}
	return auth.ID
}

func authProvider(auth *coreauth.Auth) string {
	if auth == nil {
		return ""
	}
	return auth.Provider
}

func authMetadata(auth *coreauth.Auth) map[string]any {
	if auth == nil {
		return nil
	}
	return auth.Metadata
}

func cloneHeader(in http.Header) http.Header {
	if len(in) == 0 {
		return nil
	}
	out := make(http.Header, len(in))
	for key, values := range in {
		out[key] = append([]string(nil), values...)
	}
	return out
}

func mergeHeaders(current, updates http.Header, clear []string) http.Header {
	out := cloneHeader(current)
	if out == nil {
		out = make(http.Header)
	}
	for _, key := range clear {
		out.Del(key)
	}
	for key, values := range updates {
		out.Del(key)
		for _, value := range values {
			out.Add(key, value)
		}
	}
	return out
}

func cloneByteSlices(in [][]byte) [][]byte {
	if len(in) == 0 {
		return nil
	}
	out := make([][]byte, 0, len(in))
	for _, item := range in {
		out = append(out, bytes.Clone(item))
	}
	return out
}

func cloneValues(in url.Values) url.Values {
	if len(in) == 0 {
		return nil
	}
	out := make(url.Values, len(in))
	for key, values := range in {
		out[key] = append([]string(nil), values...)
	}
	return out
}

func cloneAnyMap(in map[string]any) map[string]any {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]any, len(in))
	for key, value := range in {
		out[key] = value
	}
	return out
}

func cloneInterceptorMetadata(in map[string]any) map[string]any {
	if len(in) == 0 {
		return nil
	}
	visited := make(map[metadataCloneVisit]reflect.Value)
	out := make(map[string]any, len(in))
	for key, value := range in {
		out[key] = cloneInterceptorMetadataAny(reflect.ValueOf(value), visited)
	}
	return out
}

type metadataCloneVisit struct {
	typ reflect.Type
	ptr uintptr
}

func cloneInterceptorMetadataAny(value reflect.Value, visited map[metadataCloneVisit]reflect.Value) any {
	cloned := cloneInterceptorMetadataReflectValue(value, visited)
	if !cloned.IsValid() {
		return nil
	}
	return cloned.Interface()
}

func cloneInterceptorMetadataReflectValue(value reflect.Value, visited map[metadataCloneVisit]reflect.Value) reflect.Value {
	if !value.IsValid() {
		return reflect.Value{}
	}

	switch value.Kind() {
	case reflect.Interface:
		if value.IsNil() {
			return reflect.Zero(value.Type())
		}
		return cloneInterceptorMetadataReflectValue(value.Elem(), visited)
	case reflect.Pointer:
		if value.IsNil() {
			return reflect.Zero(value.Type())
		}
		visit := metadataCloneVisit{typ: value.Type(), ptr: value.Pointer()}
		if existing, okExisting := visited[visit]; okExisting {
			return existing
		}
		out := reflect.New(value.Type().Elem())
		visited[visit] = out
		clonedElem := cloneInterceptorMetadataReflectValue(value.Elem(), visited)
		if clonedElem.IsValid() {
			outElem := out.Elem()
			if clonedElem.Type().AssignableTo(outElem.Type()) {
				outElem.Set(clonedElem)
			} else if clonedElem.Type().ConvertibleTo(outElem.Type()) {
				outElem.Set(clonedElem.Convert(outElem.Type()))
			}
		}
		return out
	case reflect.Map:
		if value.IsNil() {
			return reflect.Zero(value.Type())
		}
		visit := metadataCloneVisit{typ: value.Type(), ptr: value.Pointer()}
		if existing, okExisting := visited[visit]; okExisting {
			return existing
		}
		out := reflect.MakeMapWithSize(value.Type(), value.Len())
		visited[visit] = out
		iter := value.MapRange()
		for iter.Next() {
			keyValue := adaptClonedValue(iter.Key(), cloneInterceptorMetadataReflectValue(iter.Key(), visited))
			valValue := adaptClonedValue(iter.Value(), cloneInterceptorMetadataReflectValue(iter.Value(), visited))
			out.SetMapIndex(keyValue, valValue)
		}
		return out
	case reflect.Slice:
		if value.IsNil() {
			return reflect.Zero(value.Type())
		}
		if value.Type().Elem().Kind() == reflect.Uint8 {
			out := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
			reflect.Copy(out, value)
			return out
		}
		visit := metadataCloneVisit{typ: value.Type(), ptr: value.Pointer()}
		if existing, okExisting := visited[visit]; okExisting {
			return existing
		}
		out := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
		visited[visit] = out
		for i := 0; i < value.Len(); i++ {
			clonedItem := cloneInterceptorMetadataReflectValue(value.Index(i), visited)
			if !clonedItem.IsValid() {
				continue
			}
			out.Index(i).Set(adaptClonedValue(value.Index(i), clonedItem))
		}
		return out
	case reflect.Array:
		out := reflect.New(value.Type()).Elem()
		for i := 0; i < value.Len(); i++ {
			clonedItem := cloneInterceptorMetadataReflectValue(value.Index(i), visited)
			if !clonedItem.IsValid() {
				continue
			}
			out.Index(i).Set(adaptClonedValue(value.Index(i), clonedItem))
		}
		return out
	case reflect.Struct:
		out := reflect.New(value.Type()).Elem()
		// Preserve unexported fields and deep-clone exported fields on a best-effort basis.
		out.Set(value)
		for i := 0; i < value.NumField(); i++ {
			field := value.Field(i)
			if !out.Field(i).CanSet() {
				continue
			}
			fieldClone := cloneInterceptorMetadataReflectValue(field, visited)
			if !fieldClone.IsValid() {
				continue
			}
			out.Field(i).Set(adaptClonedValue(field, fieldClone))
		}
		return out
	default:
		return value
	}
}

func adaptClonedValue(original, cloned reflect.Value) reflect.Value {
	if !cloned.IsValid() {
		return original
	}
	if cloned.Type().AssignableTo(original.Type()) {
		return cloned
	}
	if cloned.Type().ConvertibleTo(original.Type()) {
		return cloned.Convert(original.Type())
	}
	return original
}

func cloneStringMap(in map[string]string) map[string]string {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]string, len(in))
	for key, value := range in {
		out[key] = value
	}
	return out
}