@apifuse/provider-sdk 2.2.0-beta.48 → 2.2.0-beta.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/AUTHORING.md +91 -36
  2. package/CHANGELOG.md +4 -0
  3. package/README.md +11 -9
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-dev.ts +24 -13
  6. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  7. package/bin/apifuse-pack-smoke.ts +1 -1
  8. package/bin/apifuse-pack-types.ts +2 -1
  9. package/bin/apifuse-record.ts +30 -16
  10. package/bin/apifuse-submit-check.ts +20 -35
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  14. package/dist/cli/migrate-operation-declaration.js +1178 -0
  15. package/dist/cli/templates/provider/README.md.tpl +3 -3
  16. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -0
  17. package/dist/config/loader.d.ts +2 -0
  18. package/dist/config/loader.js +18 -7
  19. package/dist/contract-types.d.ts +11 -5
  20. package/dist/contract.js +21 -10
  21. package/dist/define.d.ts +25 -22
  22. package/dist/define.js +49 -75
  23. package/dist/dev.d.ts +3 -0
  24. package/dist/dev.js +1 -1
  25. package/dist/engine.d.ts +78 -0
  26. package/dist/engine.js +133 -0
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.js +2 -1
  29. package/dist/lint.d.ts +7 -15
  30. package/dist/lint.js +45 -70
  31. package/dist/provider.d.ts +3 -1
  32. package/dist/provider.js +1 -0
  33. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  34. package/dist/runtime/chrome149-header-order.js +289 -0
  35. package/dist/runtime/env.js +12 -0
  36. package/dist/runtime/executor.d.ts +2 -1
  37. package/dist/runtime/executor.js +3 -36
  38. package/dist/runtime/insights.js +2 -2
  39. package/dist/runtime/otlp.d.ts +71 -2
  40. package/dist/runtime/otlp.js +397 -16
  41. package/dist/runtime/resolver-vendors/capsolver.js +3 -3
  42. package/dist/runtime/resolver.js +3 -3
  43. package/dist/runtime/stealth.d.ts +13 -4
  44. package/dist/runtime/stealth.js +362 -85
  45. package/dist/runtime/trace-config.js +2 -1
  46. package/dist/runtime/trace.d.ts +5 -0
  47. package/dist/runtime/trace.js +43 -10
  48. package/dist/server/self-test.d.ts +1 -3
  49. package/dist/server/self-test.js +2 -12
  50. package/dist/server/serve-implementation.d.ts +6 -1
  51. package/dist/server/serve-implementation.js +55 -40
  52. package/dist/server/trace-output.d.ts +3 -1
  53. package/dist/server/trace-output.js +61 -2
  54. package/dist/stealth/profiles.d.ts +9 -8
  55. package/dist/stealth/profiles.js +123 -286
  56. package/dist/types.d.ts +111 -108
  57. package/package.json +2 -1
  58. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  59. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  60. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  61. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  62. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  63. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  64. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  65. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  66. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  67. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  68. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  69. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  70. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  71. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  72. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  73. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  74. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  75. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  76. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  77. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  78. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  79. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  80. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  81. package/src/cli/commands.ts +13 -0
  82. package/src/cli/migrate-operation-declaration.ts +1654 -0
  83. package/src/cli/templates/provider/README.md.tpl +3 -3
  84. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -0
  85. package/src/config/loader.ts +31 -6
  86. package/src/contract-types.ts +11 -5
  87. package/src/contract.ts +21 -10
  88. package/src/define.ts +107 -119
  89. package/src/dev.ts +4 -1
  90. package/src/engine.ts +279 -0
  91. package/src/index.ts +13 -5
  92. package/src/lint.ts +58 -92
  93. package/src/provider.ts +25 -3
  94. package/src/runtime/chrome149-header-order.ts +330 -0
  95. package/src/runtime/env.ts +13 -0
  96. package/src/runtime/executor.ts +7 -40
  97. package/src/runtime/insights.ts +2 -2
  98. package/src/runtime/otlp.ts +467 -21
  99. package/src/runtime/resolver-vendors/capsolver.ts +4 -3
  100. package/src/runtime/resolver.ts +3 -3
  101. package/src/runtime/stealth.ts +435 -103
  102. package/src/runtime/trace-config.ts +3 -2
  103. package/src/runtime/trace.ts +57 -17
  104. package/src/server/self-test.ts +2 -9
  105. package/src/server/serve-implementation.ts +89 -72
  106. package/src/server/trace-output.ts +99 -2
  107. package/src/stealth/profiles.ts +169 -327
  108. package/src/types.ts +109 -137
@@ -6,9 +6,10 @@ export const APIFUSE__TRACE__EXPORTER = "APIFUSE__TRACE__EXPORTER";
6
6
 
7
7
  type EnvLike = Record<string, string | undefined>;
8
8
 
9
- const TRACE_EXPORTER_LOOKUP: Record<Exclude<NonNullable<TraceConfig["exporter"]>, "otlp">, true> = {
9
+ const TRACE_EXPORTER_LOOKUP: Record<NonNullable<TraceConfig["exporter"]>, true> = {
10
10
  console: true,
11
11
  json: true,
12
+ otlp: true,
12
13
  none: true,
13
14
  };
14
15
  const TRACE_EXPORTERS = new Set(Object.keys(TRACE_EXPORTER_LOOKUP));
@@ -66,7 +67,7 @@ export function resolveTraceConfigFromEnv(env: EnvLike = process.env): TraceConf
66
67
  if (exporterRaw !== undefined && exporter === "none" && normalizedExporter !== "none") {
67
68
  warnOnce(
68
69
  APIFUSE__TRACE__EXPORTER,
69
- `[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}" (OTLP is unsupported for server output); supported exporters are "console", "json", and "none"; falling back to exporter "none".`,
70
+ `[apifuse] Invalid APIFUSE__TRACE__EXPORTER value "${normalizedExporter ?? exporterRaw}"; supported exporters are "console", "json", "otlp", and "none"; falling back to exporter "none".`,
70
71
  );
71
72
  }
72
73
 
@@ -21,6 +21,11 @@ export interface CreateTraceContextOptions {
21
21
  onSpan?: (span: Span) => void;
22
22
  exportOptions?: OTLPExportOptions;
23
23
  resourceAttributes?: Record<string, string>;
24
+ /**
25
+ * Applied to a detached copy of each span immediately before OTLP export; never touches
26
+ * getSpans() or onSpan. Returning nothing (or throwing) drops that export batch.
27
+ */
28
+ sanitizeSpanForExport?: (span: Span) => Span | undefined;
24
29
  }
25
30
 
26
31
  type SpanHookOptions<T> = {
@@ -41,6 +46,8 @@ type PendingSpan = {
41
46
  type CompletedSpanEntry = {
42
47
  sequence: number;
43
48
  span: Span;
49
+ /** Set once the span has been handed to the exporter so no batch re-sends it. */
50
+ exported: boolean;
44
51
  };
45
52
 
46
53
  export interface TraceRecorder {
@@ -51,8 +58,6 @@ export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-re
51
58
 
52
59
  type InternalTraceContext = TraceContext & {
53
60
  [TRACE_RECORDER]: TraceRecorder;
54
- _exportOptions?: OTLPExportOptions;
55
- _resourceAttributes?: Record<string, string>;
56
61
  };
57
62
 
58
63
  function buildOTLPExportOptions(config?: TraceConfig): OTLPExportOptions | undefined {
@@ -124,6 +129,20 @@ function insertCompletedSpan(
124
129
  }
125
130
  }
126
131
 
132
+ /** Hands the hook a copy whose attributes are detached from the recorded span; a hook that returns nothing fails closed. */
133
+ function prepareSpanForExport(
134
+ span: Span,
135
+ sanitize: CreateTraceContextOptions["sanitizeSpanForExport"],
136
+ ): Span {
137
+ const copy: Span = { ...span, attributes: { ...span.attributes } };
138
+ if (!sanitize) return copy;
139
+ const sanitized = sanitize(copy);
140
+ if (!sanitized || typeof sanitized !== "object") {
141
+ throw new Error("sanitizeSpanForExport returned no span");
142
+ }
143
+ return sanitized;
144
+ }
145
+
127
146
  export function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null {
128
147
  return (trace as Partial<InternalTraceContext>)[TRACE_RECORDER] ?? null;
129
148
  }
@@ -133,20 +152,41 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
133
152
  const completed: CompletedSpanEntry[] = [];
134
153
  const activeSpanStorage = new AsyncLocalStorage<PendingSpan | undefined>();
135
154
  let sequence = 0;
136
- let traceContext!: InternalTraceContext;
137
-
155
+ // Export configuration (which can carry collector credentials) stays in this closure;
156
+ // the context object handed to provider code never exposes it.
157
+ const exportOptions = options.exportOptions;
158
+ const exportResourceAttributes = options.resourceAttributes
159
+ ? { ...options.resourceAttributes }
160
+ : undefined;
161
+ // One trace id per context so every export batch of this request shares it and
162
+ // two processes can never mint the same id.
163
+ const exportTraceId = crypto.randomUUID().replace(/-/g, "");
164
+ let exportScheduled = false;
165
+
166
+ // One pending batch per context: roots completing before the flush share it, and a span is
167
+ // handed to the exporter exactly once, so later roots never re-send earlier spans.
138
168
  const scheduleExport = () => {
139
- if (!traceContext._exportOptions) {
169
+ if (!exportOptions || exportScheduled) {
140
170
  return;
141
171
  }
172
+ exportScheduled = true;
142
173
 
143
- const spans = completed.map((entry) => ({ ...entry.span }));
144
174
  setImmediate(() => {
145
- void exportSpansOTLP(
146
- spans,
147
- traceContext._exportOptions as OTLPExportOptions,
148
- traceContext._resourceAttributes,
149
- );
175
+ exportScheduled = false;
176
+ const pending = completed.filter((entry) => !entry.exported);
177
+ for (const entry of pending) entry.exported = true;
178
+ if (pending.length === 0) return;
179
+ // Sanitization runs off the request path; a faulty sanitizer drops the batch, never the request.
180
+ let spans: Span[];
181
+ try {
182
+ spans = pending.map((entry) =>
183
+ prepareSpanForExport(entry.span, options.sanitizeSpanForExport),
184
+ );
185
+ } catch {
186
+ console.warn("[apifuse] OTLP export skipped; span sanitization failed.");
187
+ return;
188
+ }
189
+ void exportSpansOTLP(spans, exportOptions, exportResourceAttributes, exportTraceId);
150
190
  });
151
191
  };
152
192
 
@@ -189,7 +229,11 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
189
229
  ...(pendingSpan.parentId ? { parentId: pendingSpan.parentId } : {}),
190
230
  };
191
231
 
192
- insertCompletedSpan(completed, { sequence: pendingSpan.sequence, span }, maxSpans);
232
+ insertCompletedSpan(
233
+ completed,
234
+ { sequence: pendingSpan.sequence, span, exported: false },
235
+ maxSpans,
236
+ );
193
237
  options.onSpan?.(span);
194
238
 
195
239
  if (!pendingSpan.parentId) {
@@ -213,17 +257,13 @@ export function createTraceContext(options: CreateTraceContextOptions = {}): Tra
213
257
  },
214
258
  };
215
259
 
216
- traceContext = {
260
+ const traceContext: InternalTraceContext = {
217
261
  span(name, fn) {
218
262
  return recorder.runSpan(name, fn);
219
263
  },
220
264
  getSpans() {
221
265
  return completed.map((entry) => ({ ...entry.span }));
222
266
  },
223
- ...(options.exportOptions ? { _exportOptions: options.exportOptions } : {}),
224
- ...(options.resourceAttributes
225
- ? { _resourceAttributes: { ...options.resourceAttributes } }
226
- : {}),
227
267
  [TRACE_RECORDER]: recorder,
228
268
  };
229
269
 
@@ -245,17 +245,10 @@ export function computeSelfTestPlanDigest(provider: ProviderDefinition): string
245
245
 
246
246
  /**
247
247
  * Fail-closed read-only classification: an operation may self-test only when
248
- * its own metadata marks it read-only (annotations.readOnly or
249
- * toolRouter.riskClass "read") and nothing marks it side-effecting. Unclassified
250
- * operations are refused so the endpoint cannot become a mutation oracle.
248
+ * its authored safety source of truth classifies it as read-only.
251
249
  */
252
250
  export function isSelfTestReadOnlyOperation(operation: OperationDefinition): boolean {
253
- const annotations = operation.annotations;
254
- if (annotations?.readOnly === false) return false;
255
- if (annotations?.destructive === true) return false;
256
- const riskClass = operation.toolRouter?.riskClass;
257
- if (riskClass !== undefined && riskClass !== "read") return false;
258
- return annotations?.readOnly === true || riskClass === "read";
251
+ return operation.riskClass === "read";
259
252
  }
260
253
 
261
254
  /** Binds the self-test executor to a tenant app's /v1 pipeline in-process. */
@@ -8,6 +8,14 @@ import { z } from "zod";
8
8
  import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
9
9
  import { validateFailClosedDeclaration } from "../declaration-validation.js";
10
10
  import { safeProviderErrorObservability } from "../error-observability.js";
11
+ import {
12
+ createInProcessProviderEngine,
13
+ ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES,
14
+ isEngineOwnedEnvName,
15
+ readEngineProxyCredentials,
16
+ type ProviderEngine,
17
+ type ProviderEngineBindingCandidates,
18
+ } from "../engine.js";
11
19
  import {
12
20
  SDK_OWNED_PROVIDER_ERROR_CODES,
13
21
  SDK_RUNTIME_OWNED_ERROR_CODES,
@@ -112,6 +120,7 @@ import type {
112
120
  ProviderErrorStatus,
113
121
  ProviderContext,
114
122
  ProviderDefinition,
123
+ ProviderFilesContext,
115
124
  ProviderProxyPolicy,
116
125
  ProviderRuntimeState,
117
126
  ProviderStreamEvent,
@@ -262,6 +271,7 @@ type ProviderCapabilityModules = {
262
271
 
263
272
  type ProviderServerRuntimeOptions = ProviderServerOptions & {
264
273
  readonly capabilityModules: ProviderCapabilityModules;
274
+ readonly engine: ProviderEngine;
265
275
  };
266
276
 
267
277
  const require = createRequire(import.meta.url);
@@ -518,7 +528,7 @@ function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undef
518
528
  }
519
529
 
520
530
  function getProviderStealthProfile(provider: ProviderDefinition) {
521
- return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
531
+ return provider.stealth ? getStealthProfile(provider.stealth) : undefined;
522
532
  }
523
533
 
524
534
  function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
@@ -647,6 +657,12 @@ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPo
647
657
  return undefined;
648
658
  }
649
659
 
660
+ function providerSecretNames(provider: ProviderDefinition): string[] {
661
+ return (provider.secrets?.map((secret) => secret.name) ?? []).filter(
662
+ (name) => !isEngineOwnedEnvName(name),
663
+ );
664
+ }
665
+
650
666
  function createProviderContext(
651
667
  provider: ProviderDefinition,
652
668
  request: OperationRequest,
@@ -661,10 +677,12 @@ function createProviderContext(
661
677
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
662
678
  const stealthProfile = getProviderStealthProfile(provider);
663
679
  const proxyPolicy = resolveNativeProxyPolicy(provider);
680
+ const engineProxyCredentials = readEngineProxyCredentials();
664
681
  const proxyClientOptions = {
665
682
  upstream: { proxy: provider.proxy },
666
683
  affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
667
684
  telemetry: proxyTelemetry,
685
+ engineCredentials: engineProxyCredentials,
668
686
  };
669
687
  const resolverIdentityScope = resolveProviderResolverIdentityScope(
670
688
  provider,
@@ -676,7 +694,9 @@ function createProviderContext(
676
694
  upstream: proxyClientOptions.upstream,
677
695
  affinityKey: proxyClientOptions.affinityKey,
678
696
  telemetry: proxyTelemetry,
697
+ engineCredentials: engineProxyCredentials,
679
698
  ...(signal ? { signal } : {}),
699
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
680
700
  };
681
701
  const { capabilityModules } = options;
682
702
  const logStealthCleanupError = (error: unknown) =>
@@ -691,7 +711,7 @@ function createProviderContext(
691
711
  );
692
712
 
693
713
  const env = createEnvContext([
694
- ...(provider.secrets?.map((secret) => secret.name) ?? []),
714
+ ...providerSecretNames(provider),
695
715
  PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
696
716
  ]);
697
717
  const credential = createCredentialContext({
@@ -706,9 +726,10 @@ function createProviderContext(
706
726
  };
707
727
  const requestState = state.forConnection(requestContext.connectionId);
708
728
  const cache = createProviderCache({ providerId: provider.id });
709
- const context = wrapWithInstrumentation({
729
+ const bindings: ProviderEngineBindingCandidates = {
710
730
  env,
711
731
  credential,
732
+ ...(options.files ? { files: options.files } : {}),
712
733
  request: requestContext,
713
734
  http: createHttpClient(baseUrl, {
714
735
  ...proxyClientOptions,
@@ -722,21 +743,8 @@ function createProviderContext(
722
743
  state: requestState,
723
744
  stealth: stealthBaseUrl
724
745
  ? capabilityModules.stealth
725
- ? stealthProfile
726
- ? capabilityModules.stealth.createStealthClient(
727
- stealthBaseUrl,
728
- provider.stealth!.profile,
729
- stealthClientOptions,
730
- )
731
- : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
732
- : stealthProfile
733
- ? createLazyStealthClient(
734
- logStealthCleanupError,
735
- stealthBaseUrl,
736
- provider.stealth!.profile,
737
- stealthClientOptions,
738
- )
739
- : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
746
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
747
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
740
748
  : createStealthStub(),
741
749
  browser:
742
750
  provider.runtime === "browser"
@@ -756,7 +764,9 @@ function createProviderContext(
756
764
  egress: provider.native.network,
757
765
  proxyPolicy: resolveNativeProxyPolicy(provider),
758
766
  affinityKey: proxyClientOptions.affinityKey,
759
- credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(env),
767
+ credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(
768
+ createEnvContext([...ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES]),
769
+ ),
760
770
  }),
761
771
  },
762
772
  }
@@ -810,7 +820,10 @@ function createProviderContext(
810
820
  ...event,
811
821
  }),
812
822
  }),
813
- } as ProviderContext);
823
+ };
824
+ const context = wrapWithInstrumentation(
825
+ options.engine.attach({ provider, bindings }) as ProviderContext,
826
+ );
814
827
  wrappedContext = context;
815
828
  return context;
816
829
  }
@@ -882,6 +895,7 @@ function createAuthFlowContext(
882
895
  const stealthBaseUrl = getProviderStealthBaseUrl(provider);
883
896
  const stealthProfile = getProviderStealthProfile(provider);
884
897
  const proxyPolicy = resolveNativeProxyPolicy(provider);
898
+ const engineProxyCredentials = readEngineProxyCredentials();
885
899
  const contextData = request.context ?? {};
886
900
  const flowContextStore = createFlowContextStore(
887
901
  provider.context?.keys ?? Object.keys(contextData),
@@ -891,6 +905,7 @@ function createAuthFlowContext(
891
905
  upstream: { proxy: provider.proxy },
892
906
  affinityKey: resolveAuthFlowProxyAffinityKey(provider, request),
893
907
  telemetry: proxyTelemetry,
908
+ engineCredentials: engineProxyCredentials,
894
909
  };
895
910
  const resolverIdentityScope = resolveProviderResolverIdentityScope(
896
911
  provider,
@@ -901,7 +916,9 @@ function createAuthFlowContext(
901
916
  upstream: proxyClientOptions.upstream,
902
917
  affinityKey: proxyClientOptions.affinityKey,
903
918
  telemetry: proxyTelemetry,
919
+ engineCredentials: engineProxyCredentials,
904
920
  ...(signal ? { signal } : {}),
921
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
905
922
  };
906
923
  const { capabilityModules } = options;
907
924
  const logStealthCleanupError = (error: unknown) =>
@@ -938,21 +955,8 @@ function createAuthFlowContext(
938
955
  state: state.forConnection(resolveOperationConnectionId(request)),
939
956
  stealth: stealthBaseUrl
940
957
  ? capabilityModules.stealth
941
- ? stealthProfile
942
- ? capabilityModules.stealth.createStealthClient(
943
- stealthBaseUrl,
944
- provider.stealth!.profile,
945
- stealthClientOptions,
946
- )
947
- : capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
948
- : stealthProfile
949
- ? createLazyStealthClient(
950
- logStealthCleanupError,
951
- stealthBaseUrl,
952
- provider.stealth!.profile,
953
- stealthClientOptions,
954
- )
955
- : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
958
+ ? capabilityModules.stealth.createStealthClient(stealthBaseUrl, stealthClientOptions)
959
+ : createLazyStealthClient(logStealthCleanupError, stealthBaseUrl, stealthClientOptions)
956
960
  : createStealthStub(),
957
961
  ...(provider.native
958
962
  ? {
@@ -962,14 +966,14 @@ function createAuthFlowContext(
962
966
  proxyPolicy: resolveNativeProxyPolicy(provider),
963
967
  affinityKey: proxyClientOptions.affinityKey,
964
968
  credentials: capabilityModules.nativeNetwork!.createEnvVendorCredentialResolver(
965
- createEnvContext(provider.secrets?.map((secret) => secret.name)),
969
+ createEnvContext([...ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES]),
966
970
  ),
967
971
  }),
968
972
  },
969
973
  }
970
974
  : {}),
971
975
  env: createEnvContext([
972
- ...(provider.secrets?.map((secret) => secret.name) ?? []),
976
+ ...providerSecretNames(provider),
973
977
  ...(provider.auth?.mode === "oauth2_proxied" ? ["APIFUSE__AUTH_PROXY__URL"] : []),
974
978
  ]),
975
979
  credential,
@@ -1086,6 +1090,10 @@ export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
1086
1090
 
1087
1091
  export type ProviderServerOptions<TContext extends Partial<ProviderContext> = ProviderContext> = {
1088
1092
  logger?: ProviderServerLogger;
1093
+ /** Capability attachment boundary. Defaults to the local in-process engine. */
1094
+ engine?: ProviderEngine;
1095
+ /** Request-file resolver supplied by the engine host when `files` is declared. */
1096
+ files?: ProviderFilesContext;
1089
1097
  /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
1090
1098
  operationExecutor?: ProviderServerOperationExecutor<TContext>;
1091
1099
  /** Optional signed internal executor for stateful owner forwarding. */
@@ -1275,11 +1283,8 @@ function providerObservabilityDetails(
1275
1283
  const declaredRetryable = sdkOwnsErrorResolution(error)
1276
1284
  ? undefined
1277
1285
  : declaredErrorCode?.retryable;
1278
- // Session-expiry surfaces the credential_expired category + the opt-in
1279
- // retryable signal so Gateway/Credential Service can refresh and re-drive the
1280
- // operation (see design.md §4.3 D3). Without this branch the auth error would
1281
- // serialize as a bare 401 with no retryable/category, losing the refresh
1282
- // signal for exactly the retryOnAuthRefresh operations it is meant to enable.
1286
+ // Session-expiry surfaces the credential_expired category so Gateway and
1287
+ // Credential Service can distinguish an expired credential from other 401s.
1283
1288
  if (isSessionExpiredError(error)) {
1284
1289
  return {
1285
1290
  category: providerErrorOption(error, "category") ?? "credential_expired",
@@ -1496,7 +1501,7 @@ type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, Operatio
1496
1501
  function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
1497
1502
  return new Map(
1498
1503
  Object.entries(provider.operations).flatMap(([operationId, operation]) => {
1499
- const errorCodes = operation.docs?.errorCodes;
1504
+ const errorCodes = operation.errorCodes;
1500
1505
  return errorCodes?.length
1501
1506
  ? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
1502
1507
  : [];
@@ -1635,7 +1640,7 @@ function logProviderError(
1635
1640
  ? {
1636
1641
  signal: "unregistered_provider_error_code" as const,
1637
1642
  signalFix:
1638
- "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
1643
+ "Declare this code (with status and retryable) in the operation's errorCodes so it serves its intended status instead of 500.",
1639
1644
  }
1640
1645
  : {}),
1641
1646
  ...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
@@ -1704,7 +1709,7 @@ function toJsonSuccessResponse(
1704
1709
  return new Response(result);
1705
1710
  }
1706
1711
 
1707
- const cacheMeta = ctx?.cache.responseMeta();
1712
+ const cacheMeta = ctx && "cache" in ctx ? ctx.cache.responseMeta() : undefined;
1708
1713
  const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
1709
1714
  const meta =
1710
1715
  cacheMeta || retryMeta
@@ -2105,31 +2110,35 @@ async function handleOperation(
2105
2110
  const cleanup = async () => {
2106
2111
  if (cleanupCalled) return;
2107
2112
  cleanupCalled = true;
2108
- try {
2109
- ctx.stealth.close?.();
2110
- } catch (error) {
2111
- logProviderCleanupError(
2112
- options.logger,
2113
- provider,
2114
- "operation",
2115
- operationId,
2116
- request.requestId,
2117
- "stealth",
2118
- error,
2119
- );
2113
+ if ("stealth" in ctx) {
2114
+ try {
2115
+ ctx.stealth.close?.();
2116
+ } catch (error) {
2117
+ logProviderCleanupError(
2118
+ options.logger,
2119
+ provider,
2120
+ "operation",
2121
+ operationId,
2122
+ request.requestId,
2123
+ "stealth",
2124
+ error,
2125
+ );
2126
+ }
2120
2127
  }
2121
- try {
2122
- await ctx.browser.close?.();
2123
- } catch (error) {
2124
- logProviderCleanupError(
2125
- options.logger,
2126
- provider,
2127
- "operation",
2128
- operationId,
2129
- request.requestId,
2130
- "browser",
2131
- error,
2132
- );
2128
+ if ("browser" in ctx) {
2129
+ try {
2130
+ await ctx.browser.close?.();
2131
+ } catch (error) {
2132
+ logProviderCleanupError(
2133
+ options.logger,
2134
+ provider,
2135
+ "operation",
2136
+ operationId,
2137
+ request.requestId,
2138
+ "browser",
2139
+ error,
2140
+ );
2141
+ }
2133
2142
  }
2134
2143
  };
2135
2144
  try {
@@ -2141,7 +2150,9 @@ async function handleOperation(
2141
2150
  request,
2142
2151
  signal,
2143
2152
  })
2144
- : await executeOperation(provider, operationId, ctx, request.input);
2153
+ : await executeOperation(provider, operationId, ctx, request.input, {
2154
+ env: createEnvContext(providerSecretNames(provider)),
2155
+ });
2145
2156
  if (streaming && operation) {
2146
2157
  return toStreamingResponse(operation, result, cleanup, request.requestId);
2147
2158
  }
@@ -2427,7 +2438,11 @@ function createServerAppWithCapabilityModules(
2427
2438
  serverOptions: ProviderServerOptions,
2428
2439
  capabilityModules: ProviderCapabilityModules,
2429
2440
  ): Hono {
2430
- const options: ProviderServerRuntimeOptions = { ...serverOptions, capabilityModules };
2441
+ const options: ProviderServerRuntimeOptions = {
2442
+ ...serverOptions,
2443
+ capabilityModules,
2444
+ engine: serverOptions.engine ?? createInProcessProviderEngine(),
2445
+ };
2431
2446
  const app = new Hono();
2432
2447
  const logger = options.logger ?? defaultProviderServerLogger;
2433
2448
  const operationErrorCodes = buildOperationErrorCodeLookup(provider);
@@ -2449,7 +2464,7 @@ function createServerAppWithCapabilityModules(
2449
2464
  // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
2450
2465
  const missingSecretsAtBoot = listMissingRequiredSecrets(
2451
2466
  provider,
2452
- createEnvContext(provider.secrets?.map((secret) => secret.name)),
2467
+ createEnvContext(providerSecretNames(provider)),
2453
2468
  );
2454
2469
  if (missingSecretsAtBoot.length > 0) {
2455
2470
  logger({
@@ -3155,6 +3170,8 @@ export async function serve<TContext extends Partial<ProviderContext> = Provider
3155
3170
  const selfTestSecrets = resolveSelfTestMasterSecrets();
3156
3171
  const serverAppOptions: ProviderServerOptions<TContext> = {
3157
3172
  logger: options.logger,
3173
+ engine: options.engine,
3174
+ files: options.files,
3158
3175
  ocr: options.ocr,
3159
3176
  stt: options.stt,
3160
3177
  resolver: options.resolver,
@@ -1,15 +1,101 @@
1
+ import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
2
+ import {
3
+ OTEL_EXPORTER_OTLP_ENDPOINT,
4
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
5
+ type OTLPExportOptions,
6
+ type OTLPExportResolution,
7
+ type OTLPResourceResolution,
8
+ resolveOTLPExportOptions,
9
+ resolveOTLPResourceAttributes,
10
+ } from "../runtime/otlp.js";
1
11
  import {
2
- resolveTraceContextOptions,
3
12
  type CreateTraceContextOptions,
13
+ resolveTraceContextOptions,
4
14
  type Span,
5
15
  } from "../runtime/trace.js";
6
- import { sanitizeSpanForOutput } from "../trace-sanitization.js";
16
+ import {
17
+ sanitizeSpanForOutput,
18
+ sanitizeSpanNameForOutput,
19
+ sanitizeTraceAttributes,
20
+ } from "../trace-sanitization.js";
7
21
  import type { TraceConfig } from "../types.js";
8
22
 
23
+ type EnvLike = Record<string, string | undefined>;
24
+
25
+ // Warn once per environment object: process.env in production, each injected env in tests.
26
+ const warnedEnvironments = new WeakSet<EnvLike>();
27
+ const warnedResourceEnvironments = new WeakSet<EnvLike>();
28
+
29
+ function warnDiscardedResourceAttributes(env: EnvLike, resolution: OTLPResourceResolution): void {
30
+ if (resolution.discarded.length === 0 || warnedResourceEnvironments.has(env)) return;
31
+ warnedResourceEnvironments.add(env);
32
+ console.warn(
33
+ `[apifuse] ${resolution.discarded.join(", ")} could not be parsed as an OTel key=value list and was ignored.`,
34
+ );
35
+ }
36
+
37
+ function warnExportDisabled(
38
+ env: EnvLike,
39
+ resolution: Exclude<OTLPExportResolution, { status: "resolved" }>,
40
+ ): void {
41
+ if (warnedEnvironments.has(env)) return;
42
+ warnedEnvironments.add(env);
43
+ console.warn(
44
+ resolution.status === "unconfigured"
45
+ ? `[apifuse] OTLP trace export is enabled but no endpoint is configured; set ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT} or ${OTEL_EXPORTER_OTLP_ENDPOINT}. Trace export is disabled.`
46
+ : `[apifuse] OTLP trace export is enabled but ${resolution.source} ${resolution.reason}. Trace export is disabled.`,
47
+ );
48
+ }
49
+
50
+ function resolveServerOTLPExportOptions(
51
+ config: TraceConfig,
52
+ env: EnvLike,
53
+ ): OTLPExportOptions | undefined {
54
+ const resolution = resolveOTLPExportOptions(
55
+ {
56
+ endpoint: config.otlp?.endpoint ?? config.endpoint,
57
+ headers: config.otlp?.headers,
58
+ timeout: config.otlp?.timeout,
59
+ },
60
+ env,
61
+ );
62
+ if (resolution.status === "resolved") return resolution.options;
63
+ warnExportDisabled(env, resolution);
64
+ return undefined;
65
+ }
66
+
67
+ /**
68
+ * Per-request attributes carry client-supplied values (request_id) and get the full attribute
69
+ * sanitizer, exactly as on the console path. Operator-configured attributes from the process
70
+ * environment are identifiers rather than request data: their values are kept, with control
71
+ * characters neutralized, length bounded, and secret-named keys redacted.
72
+ */
73
+ function resolveExportResourceAttributes(
74
+ requestAttributes: Record<string, string>,
75
+ env: EnvLike,
76
+ ): Record<string, string> {
77
+ const resolution = resolveOTLPResourceAttributes({}, env);
78
+ warnDiscardedResourceAttributes(env, resolution);
79
+ const operatorAttributes = Object.fromEntries(
80
+ Object.entries(resolution.attributes).map(([key, value]) => [
81
+ sanitizeSpanNameForOutput(key),
82
+ isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeSpanNameForOutput(value),
83
+ ]),
84
+ );
85
+ const sanitizedRequestAttributes = Object.fromEntries(
86
+ Object.entries(sanitizeTraceAttributes(requestAttributes)).map(([key, value]) => [
87
+ key,
88
+ String(value),
89
+ ]),
90
+ );
91
+ return { ...operatorAttributes, ...sanitizedRequestAttributes };
92
+ }
93
+
9
94
  /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
10
95
  export function resolveServerTraceContextOptions(
11
96
  config: TraceConfig,
12
97
  resourceAttributes: Record<string, string>,
98
+ env: EnvLike = process.env,
13
99
  ): CreateTraceContextOptions {
14
100
  const resolved = resolveTraceContextOptions(config);
15
101
  const outputEnabled = config.enabled !== false && config.exporter !== "none";
@@ -24,9 +110,20 @@ export function resolveServerTraceContextOptions(
24
110
  resolved.onSpan?.(span);
25
111
  }
26
112
  : (consoleHook ?? resolved.onSpan);
113
+ const exportOptions =
114
+ outputEnabled && config.exporter === "otlp"
115
+ ? resolveServerOTLPExportOptions(config, env)
116
+ : undefined;
27
117
 
28
118
  return {
29
119
  maxSpans: resolved.maxSpans,
30
120
  onSpan,
121
+ ...(exportOptions
122
+ ? {
123
+ exportOptions,
124
+ resourceAttributes: resolveExportResourceAttributes(resourceAttributes, env),
125
+ sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span),
126
+ }
127
+ : {}),
31
128
  };
32
129
  }