@apifuse/provider-sdk 2.2.0-beta.59 → 2.2.0-beta.61

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 (124) hide show
  1. package/AUTHORING.md +33 -16
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-dev.ts +7 -8
  4. package/bin/apifuse-pack-types.ts +22 -0
  5. package/bin/apifuse-perf.ts +45 -10
  6. package/bin/apifuse-record.ts +6 -8
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/config/loader.js +13 -12
  9. package/dist/define.d.ts +3 -2
  10. package/dist/define.js +1 -1
  11. package/dist/dev.js +3 -1
  12. package/dist/engine.d.ts +7 -6
  13. package/dist/engine.js +6 -6
  14. package/dist/error-resolution.js +21 -4
  15. package/dist/errors.d.ts +3 -0
  16. package/dist/errors.js +9 -0
  17. package/dist/handle-meta.d.ts +39 -0
  18. package/dist/handle-meta.js +29 -0
  19. package/dist/handle.d.ts +187 -0
  20. package/dist/handle.js +254 -0
  21. package/dist/i18n/catalog.js +21 -1
  22. package/dist/index.d.ts +6 -4
  23. package/dist/index.js +4 -3
  24. package/dist/lint.d.ts +3 -0
  25. package/dist/lint.js +388 -62
  26. package/dist/provider.d.ts +4 -4
  27. package/dist/provider.js +3 -3
  28. package/dist/runtime/browser.js +2 -1
  29. package/dist/runtime/cache.js +3 -2
  30. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  31. package/dist/runtime/diagnostic-env-values.js +57 -0
  32. package/dist/runtime/diagnostic-env.d.ts +11 -0
  33. package/dist/runtime/diagnostic-env.js +35 -0
  34. package/dist/runtime/diagnostic-redactor.d.ts +49 -0
  35. package/dist/runtime/diagnostic-redactor.js +569 -0
  36. package/dist/runtime/egress-lease.d.ts +2 -0
  37. package/dist/runtime/egress-lease.js +4 -1
  38. package/dist/runtime/env.js +2 -1
  39. package/dist/runtime/handle-wordlist.d.ts +43 -0
  40. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  41. package/dist/runtime/handle.d.ts +71 -0
  42. package/dist/runtime/handle.js +895 -0
  43. package/dist/runtime/instrumentation.js +3 -0
  44. package/dist/runtime/native-network.d.ts +10 -1
  45. package/dist/runtime/native-network.js +230 -29
  46. package/dist/runtime/native-telemetry.d.ts +142 -0
  47. package/dist/runtime/native-telemetry.js +231 -0
  48. package/dist/runtime/ocr.js +2 -1
  49. package/dist/runtime/otlp.js +5 -4
  50. package/dist/runtime/proxy-nodemaven.js +3 -2
  51. package/dist/runtime/request-options.d.ts +7 -0
  52. package/dist/runtime/request-options.js +62 -22
  53. package/dist/runtime/resolver-vendors/browser.js +0 -1
  54. package/dist/runtime/resolver.js +3 -2
  55. package/dist/runtime/state.d.ts +4 -1
  56. package/dist/runtime/state.js +32 -17
  57. package/dist/runtime/stt.js +3 -2
  58. package/dist/runtime/trace.d.ts +5 -0
  59. package/dist/runtime/trace.js +115 -20
  60. package/dist/schema.d.ts +11 -0
  61. package/dist/schema.js +56 -2
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1 -0
  64. package/dist/server/self-test-redaction.js +8 -33
  65. package/dist/server/sensitive-values.d.ts +26 -0
  66. package/dist/server/sensitive-values.js +128 -0
  67. package/dist/server/serve-implementation.d.ts +4 -11
  68. package/dist/server/serve-implementation.js +305 -96
  69. package/dist/server/trace-output.d.ts +2 -1
  70. package/dist/server/trace-output.js +46 -14
  71. package/dist/testing/run.js +10 -7
  72. package/dist/trace-sanitization.d.ts +4 -3
  73. package/dist/trace-sanitization.js +45 -22
  74. package/dist/types.d.ts +4 -107
  75. package/package.json +2 -1
  76. package/src/config/loader.ts +13 -12
  77. package/src/define.ts +4 -3
  78. package/src/dev.ts +3 -1
  79. package/src/engine.ts +8 -8
  80. package/src/error-resolution.ts +21 -4
  81. package/src/errors.ts +13 -0
  82. package/src/handle-meta.ts +58 -0
  83. package/src/handle.ts +529 -0
  84. package/src/i18n/catalog.ts +23 -1
  85. package/src/index.ts +28 -13
  86. package/src/lint.ts +456 -71
  87. package/src/provider.ts +10 -20
  88. package/src/runtime/browser.ts +2 -1
  89. package/src/runtime/cache.ts +3 -2
  90. package/src/runtime/diagnostic-env-values.ts +56 -0
  91. package/src/runtime/diagnostic-env.ts +55 -0
  92. package/src/runtime/diagnostic-redactor.ts +662 -0
  93. package/src/runtime/egress-lease.ts +6 -1
  94. package/src/runtime/env.ts +2 -1
  95. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  96. package/src/runtime/handle.ts +1162 -0
  97. package/src/runtime/instrumentation.ts +3 -0
  98. package/src/runtime/native-network.ts +281 -33
  99. package/src/runtime/native-telemetry.ts +366 -0
  100. package/src/runtime/ocr.ts +2 -1
  101. package/src/runtime/otlp.ts +5 -4
  102. package/src/runtime/proxy-nodemaven.ts +3 -2
  103. package/src/runtime/request-options.ts +76 -28
  104. package/src/runtime/resolver-vendors/browser.ts +0 -1
  105. package/src/runtime/resolver.ts +3 -2
  106. package/src/runtime/state.ts +44 -16
  107. package/src/runtime/stt.ts +3 -2
  108. package/src/runtime/trace.ts +168 -26
  109. package/src/schema.ts +63 -2
  110. package/src/server/index.ts +17 -0
  111. package/src/server/self-test-redaction.ts +10 -40
  112. package/src/server/sensitive-values.ts +197 -0
  113. package/src/server/serve-implementation.ts +388 -128
  114. package/src/server/trace-output.ts +77 -17
  115. package/src/testing/run.ts +11 -7
  116. package/src/trace-sanitization.ts +70 -23
  117. package/src/types.ts +4 -137
  118. package/dist/choice-token.d.ts +0 -24
  119. package/dist/choice-token.js +0 -74
  120. package/dist/runtime/choice-wordlist.d.ts +0 -9
  121. package/dist/runtime/choice.d.ts +0 -27
  122. package/dist/runtime/choice.js +0 -801
  123. package/src/choice-token.ts +0 -165
  124. package/src/runtime/choice.ts +0 -1197
@@ -1,6 +1,7 @@
1
+ import { type DiagnosticRedactor } from "../runtime/diagnostic-redactor.js";
1
2
  import { type CreateTraceContextOptions } from "../runtime/trace.js";
2
3
  import type { TraceConfig } from "../types.js";
3
4
  type EnvLike = Record<string, string | undefined>;
4
5
  /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
5
- export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>, env?: EnvLike): CreateTraceContextOptions;
6
+ export declare function resolveServerTraceContextOptions(config: TraceConfig, resourceAttributes: Record<string, string>, env?: EnvLike, redact?: DiagnosticRedactor): CreateTraceContextOptions;
6
7
  export {};
@@ -1,7 +1,9 @@
1
1
  import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
2
+ import { redactedKeyAllocator, isRedactedKey, REDACTION_FAILED, redactDiagnosticText, } from "../runtime/diagnostic-redactor.js";
2
3
  import { OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, resolveOTLPExportOptions, resolveOTLPResourceAttributes, } from "../runtime/otlp.js";
3
4
  import { resolveTraceContextOptions, } from "../runtime/trace.js";
4
5
  import { sanitizeSpanForOutput, sanitizeSpanNameForOutput, sanitizeTraceAttributes, } from "../trace-sanitization.js";
6
+ const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for("@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer");
5
7
  // Warn once per environment object: process.env in production, each injected env in tests.
6
8
  const warnedEnvironments = new WeakSet();
7
9
  const warnedResourceEnvironments = new WeakSet();
@@ -36,25 +38,48 @@ function resolveServerOTLPExportOptions(config, env) {
36
38
  * environment are identifiers rather than request data: their values are kept, with control
37
39
  * characters neutralized, length bounded, and secret-named keys redacted.
38
40
  */
39
- function resolveExportResourceAttributes(requestAttributes, env) {
41
+ function resolveExportResourceAttributes(requestAttributes, env, redact) {
40
42
  const resolution = resolveOTLPResourceAttributes({}, env);
41
43
  warnDiscardedResourceAttributes(env, resolution);
42
- const operatorAttributes = Object.fromEntries(Object.entries(resolution.attributes).map(([key, value]) => [
43
- sanitizeSpanNameForOutput(key),
44
- isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeSpanNameForOutput(value),
45
- ]));
46
- const sanitizedRequestAttributes = Object.fromEntries(Object.entries(sanitizeTraceAttributes(requestAttributes)).map(([key, value]) => [
47
- key,
48
- String(value),
49
- ]));
50
- return { ...operatorAttributes, ...sanitizedRequestAttributes };
44
+ const operatorAttributes = resolution.attributes;
45
+ const requestKeys = new Set(Object.keys(requestAttributes));
46
+ return {
47
+ // Keep the raw snapshot private until export. A credential registered after
48
+ // trace creation must be matched before any key or value is bounded.
49
+ attributes: { ...operatorAttributes, ...requestAttributes },
50
+ sanitize(attributes) {
51
+ const nextKey = redactedKeyAllocator(Object.keys(attributes));
52
+ const sanitizedOperatorAttributes = {};
53
+ const rawRequestAttributes = {};
54
+ for (const [key, value] of Object.entries(attributes)) {
55
+ const unchangedOperatorValue = !requestKeys.has(key) && operatorAttributes[key] === value;
56
+ if (unchangedOperatorValue) {
57
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticText(key, redact);
58
+ const keyChanged = redactedKey !== key;
59
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
60
+ sanitizedOperatorAttributes[sanitizedKey] = keyChanged
61
+ ? redactedKey === REDACTION_FAILED
62
+ ? REDACTION_FAILED
63
+ : REDACTED_FIXTURE_VALUE
64
+ : isSensitiveFixtureKey(key)
65
+ ? REDACTED_FIXTURE_VALUE
66
+ : sanitizeSpanNameForOutput(value, redact);
67
+ }
68
+ else {
69
+ rawRequestAttributes[key] = value;
70
+ }
71
+ }
72
+ const sanitizedRequestAttributes = Object.fromEntries(Object.entries(sanitizeTraceAttributes(rawRequestAttributes, redact, Object.keys(sanitizedOperatorAttributes))).map(([key, value]) => [key, String(value)]));
73
+ return { ...sanitizedOperatorAttributes, ...sanitizedRequestAttributes };
74
+ },
75
+ };
51
76
  }
52
77
  /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
53
- export function resolveServerTraceContextOptions(config, resourceAttributes, env = process.env) {
78
+ export function resolveServerTraceContextOptions(config, resourceAttributes, env = process.env, redact) {
54
79
  const resolved = resolveTraceContextOptions(config);
55
80
  const outputEnabled = config.enabled !== false && config.exporter !== "none";
56
81
  const consoleHook = outputEnabled && (config.exporter === "console" || config.exporter === "json")
57
- ? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
82
+ ? (span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes, redact)))
58
83
  : undefined;
59
84
  const onSpan = consoleHook && resolved.onSpan
60
85
  ? (span) => {
@@ -65,14 +90,21 @@ export function resolveServerTraceContextOptions(config, resourceAttributes, env
65
90
  const exportOptions = outputEnabled && config.exporter === "otlp"
66
91
  ? resolveServerOTLPExportOptions(config, env)
67
92
  : undefined;
93
+ const exportResources = exportOptions
94
+ ? resolveExportResourceAttributes(resourceAttributes, env, redact)
95
+ : undefined;
96
+ if (exportResources) {
97
+ Object.defineProperty(exportResources.attributes, TRACE_RESOURCE_ATTRIBUTE_SANITIZER, { value: exportResources.sanitize });
98
+ }
68
99
  return {
69
100
  maxSpans: resolved.maxSpans,
70
101
  onSpan,
102
+ ...(redact ? { redact } : {}),
71
103
  ...(exportOptions
72
104
  ? {
73
105
  exportOptions,
74
- resourceAttributes: resolveExportResourceAttributes(resourceAttributes, env),
75
- sanitizeSpanForExport: (span) => sanitizeSpanForOutput(span),
106
+ resourceAttributes: exportResources?.attributes,
107
+ sanitizeSpanForExport: (span) => sanitizeSpanForOutput(span, undefined, redact),
76
108
  }
77
109
  : {}),
78
110
  };
@@ -1,7 +1,12 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { ProviderError } from "../errors.js";
3
3
  import { createProviderCache } from "../runtime/cache.js";
4
- import { createTestProviderChoiceContext } from "../runtime/choice.js";
4
+ import { createTestHandleContext } from "../runtime/handle.js";
5
+ /**
6
+ * Stable connection scope for the local test harness so bound handles (drafts,
7
+ * default cursors) can be exercised exactly as in a connected production call.
8
+ */
9
+ const STANDARD_TEST_CONNECTION_ID = "standard-test-connection";
5
10
  import { createMemoryProviderRuntimeState } from "../runtime/state.js";
6
11
  import { createUnsupportedOcrClient } from "../runtime/ocr.js";
7
12
  import { createUnsupportedSttClient } from "../runtime/stt.js";
@@ -190,7 +195,7 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
190
195
  getAccessToken: () => undefined,
191
196
  getScopes: () => [],
192
197
  };
193
- const request = { headers: {} };
198
+ const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
194
199
  const state = createMemoryProviderRuntimeState();
195
200
  const nativeEgress = provider.native
196
201
  ? createNativeEgressAuthorization({ egress: provider.native.network })
@@ -411,10 +416,9 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
411
416
  resolver: {
412
417
  solve: async () => unsupported("ctx.resolver.solve"),
413
418
  },
414
- choice: createTestProviderChoiceContext({
419
+ handle: createTestHandleContext({
415
420
  providerId: `standard-test-${operationName}`,
416
421
  request,
417
- credential,
418
422
  state,
419
423
  }),
420
424
  };
@@ -447,7 +451,7 @@ export function createSnapshotContext(rawFixture) {
447
451
  getAccessToken: () => undefined,
448
452
  getScopes: () => [],
449
453
  };
450
- const request = { headers: {} };
454
+ const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
451
455
  const state = createMemoryProviderRuntimeState();
452
456
  const streamCaptureGroup = findStreamCaptureGroup(rawFixture);
453
457
  let nextCaptureItem = 0;
@@ -521,10 +525,9 @@ export function createSnapshotContext(rawFixture) {
521
525
  resolver: {
522
526
  solve: async () => unsupported("ctx.resolver.solve"),
523
527
  },
524
- choice: createTestProviderChoiceContext({
528
+ handle: createTestHandleContext({
525
529
  providerId: "standard-test",
526
530
  request,
527
- credential,
528
531
  state,
529
532
  }),
530
533
  };
@@ -1,5 +1,6 @@
1
+ import { type DiagnosticRedactor } from "./runtime/diagnostic-redactor.js";
1
2
  import type { TraceSpan } from "./types.js";
2
3
  /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
3
- export declare function sanitizeSpanNameForOutput(value: string): string;
4
- export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>): Record<string, string | number | boolean>;
5
- export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string>): TraceSpan;
4
+ export declare function sanitizeSpanNameForOutput(value: string, redact?: DiagnosticRedactor): string;
5
+ export declare function sanitizeTraceAttributes(attributes: Record<string, unknown>, redact?: DiagnosticRedactor, reservedKeys?: Iterable<string>): Record<string, string | number | boolean>;
6
+ export declare function sanitizeSpanForOutput(span: TraceSpan, additionalAttributes?: Record<string, string>, redact?: DiagnosticRedactor): TraceSpan;
@@ -1,45 +1,68 @@
1
1
  import { encodeDiagnosticControls, isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "./fixture-sanitization.js";
2
+ import { diagnosticStructuredRedactor, redactDiagnosticAttributeKey, copyDiagnosticAttributeTypes, isDiagnosticBigintAttribute, redactedKeyAllocator, isRedactedKey, redactCriticalDiagnosticText, diagnosticAttributeRedactor, REDACTION_FAILED, redactDiagnosticText, } from "./runtime/diagnostic-redactor.js";
2
3
  const MAX_TRACE_TEXT_LENGTH = 300;
3
- function sanitizeTraceText(value) {
4
- const encoded = sanitizeDiagnosticText(value);
4
+ function sanitizeTraceText(value, redact) {
5
+ const encoded = sanitizeDiagnosticText(redactDiagnosticText(value, redact));
5
6
  return encoded.length > MAX_TRACE_TEXT_LENGTH
6
7
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
7
8
  : encoded;
8
9
  }
9
10
  /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
10
- export function sanitizeSpanNameForOutput(value) {
11
- const encoded = encodeDiagnosticControls(value);
11
+ export function sanitizeSpanNameForOutput(value, redact) {
12
+ const encoded = encodeDiagnosticControls(redactDiagnosticText(value, redact));
12
13
  return encoded.length > MAX_TRACE_TEXT_LENGTH
13
14
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
14
15
  : encoded;
15
16
  }
16
- export function sanitizeTraceAttributes(attributes) {
17
- return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
18
- sanitizeTraceText(key),
19
- isSensitiveFixtureKey(key)
20
- ? REDACTED_FIXTURE_VALUE
21
- : typeof value === "string"
22
- ? sanitizeTraceText(value)
23
- : typeof value === "number" || typeof value === "boolean"
24
- ? value
25
- : sanitizeTraceText(String(value)),
26
- ]));
17
+ export function sanitizeTraceAttributes(attributes, redact, reservedKeys = []) {
18
+ const nextKey = redactedKeyAllocator([...Object.keys(attributes), ...reservedKeys]);
19
+ return copyDiagnosticAttributeTypes(attributes, Object.fromEntries(Object.entries(attributes).map(([key, value]) => {
20
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
21
+ const keyChanged = redactedKey !== key;
22
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeTraceText(key);
23
+ const valueRedactor = typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
24
+ ? diagnosticStructuredRedactor(redact)
25
+ : diagnosticAttributeRedactor(key, redact);
26
+ const sanitizedValue = keyChanged
27
+ ? redactedKey === REDACTION_FAILED
28
+ ? REDACTION_FAILED
29
+ : REDACTED_FIXTURE_VALUE
30
+ : isSensitiveFixtureKey(key)
31
+ ? REDACTED_FIXTURE_VALUE
32
+ : typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
33
+ ? String(value).length < 8
34
+ ? String(value)
35
+ : sanitizeTraceText(String(value), valueRedactor)
36
+ : typeof value === "string"
37
+ ? sanitizeTraceText(value, valueRedactor)
38
+ : typeof value === "number" || typeof value === "boolean"
39
+ ? (() => {
40
+ if (String(value).length < 8)
41
+ return value;
42
+ const redacted = redactDiagnosticText(String(value), diagnosticStructuredRedactor(valueRedactor));
43
+ return redacted === String(value) ? value : sanitizeTraceText(redacted);
44
+ })()
45
+ : sanitizeTraceText(String(value), valueRedactor);
46
+ return [sanitizedKey, sanitizedValue];
47
+ })));
27
48
  }
28
- export function sanitizeSpanForOutput(span, additionalAttributes) {
49
+ export function sanitizeSpanForOutput(span, additionalAttributes, redact) {
29
50
  // Keep this schema explicit so future fields are not silently added to a
30
51
  // process output path before their trust boundary has been reviewed.
31
52
  return {
32
- id: span.id,
33
- name: sanitizeSpanNameForOutput(span.name),
53
+ id: redactCriticalDiagnosticText(span.id, redact),
54
+ name: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(span.name, redact)),
34
55
  startedAt: span.startedAt,
35
56
  endedAt: span.endedAt,
36
57
  duration_ms: span.duration_ms,
37
58
  status: span.status,
38
- attributes: sanitizeTraceAttributes({
59
+ attributes: sanitizeTraceAttributes(copyDiagnosticAttributeTypes(span.attributes, {
39
60
  ...span.attributes,
40
61
  ...(additionalAttributes ?? {}),
41
- }),
42
- ...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
43
- ...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
62
+ }), redact),
63
+ ...(span.error !== undefined ? { error: sanitizeTraceText(span.error, redact) } : {}),
64
+ ...(span.parentId !== undefined
65
+ ? { parentId: redactCriticalDiagnosticText(span.parentId, redact) }
66
+ : {}),
44
67
  };
45
68
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type ms from "ms";
2
+ import type { HandleContext, HandleKind } from "./handle.js";
2
3
  import type { HealthScenario } from "./health-scenario.js";
3
4
  import type { SerializedCookieJar } from "tough-cookie";
4
5
  import type { infer as ZodInfer, ZodType } from "zod";
@@ -1660,110 +1661,6 @@ export interface ProviderRequestContext {
1660
1661
  connectionId?: string;
1661
1662
  headers: Record<string, string>;
1662
1663
  }
1663
- export interface ProviderChoiceBindingOptions {
1664
- connection?: boolean;
1665
- credentialKeys?: readonly string[];
1666
- }
1667
- export type ProviderChoiceConsumeMode = "never" | "on-parse" | "explicit";
1668
- export type ProviderChoiceConsumeResult = {
1669
- readonly status: "consumed";
1670
- } | {
1671
- readonly status: "already-consumed";
1672
- } | {
1673
- readonly status: "unsupported";
1674
- };
1675
- export type ProviderChoiceExplicitParseResult = {
1676
- readonly status: "active";
1677
- readonly payload: Record<string, unknown>;
1678
- /** Stable, opaque key for provider-owned idempotency records. */
1679
- readonly replayKey: string;
1680
- /** Atomically claims a word token. Inline tokens report unsupported. */
1681
- consume(): Promise<ProviderChoiceConsumeResult>;
1682
- } | {
1683
- readonly status: "consumed";
1684
- /** Use this key to read the provider-owned result before returning an error. */
1685
- readonly replayKey: string;
1686
- };
1687
- export type ProviderChoiceStorageOptions = {
1688
- readonly mode: "inline";
1689
- } | {
1690
- readonly mode: "server";
1691
- readonly namespace: string;
1692
- readonly state?: ProviderRuntimeState;
1693
- readonly ttl?: ProviderStateDurationString;
1694
- readonly maxEntries: number;
1695
- readonly maxValueBytes: number;
1696
- readonly unavailable?: "reject";
1697
- } | {
1698
- readonly mode: "auto";
1699
- readonly namespace: string;
1700
- readonly state?: ProviderRuntimeState;
1701
- readonly ttl?: ProviderStateDurationString;
1702
- readonly maxInlineBytes: number;
1703
- readonly maxEntries: number;
1704
- readonly maxValueBytes: number;
1705
- readonly unavailable?: "reject";
1706
- };
1707
- export interface ProviderChoiceIssueOptions<TPayload extends Record<string, unknown>> {
1708
- prefix: string;
1709
- purpose: string;
1710
- payload: TPayload;
1711
- ttlMs: number;
1712
- nowMs?: number;
1713
- bind?: ProviderChoiceBindingOptions;
1714
- /** Server storage only: standard emits four words; high emits five. */
1715
- strength?: "standard" | "high";
1716
- storage?: ProviderChoiceStorageOptions;
1717
- }
1718
- export interface ProviderChoiceParseOptions {
1719
- token: string;
1720
- prefix: string;
1721
- purpose: string;
1722
- ttlMs?: number;
1723
- nowMs?: number;
1724
- futureToleranceMs?: number;
1725
- bind?: ProviderChoiceBindingOptions;
1726
- storage?: ProviderChoiceStorageOptions;
1727
- /** Defaults to never, preserving reusable choice-token parse semantics. */
1728
- consume?: ProviderChoiceConsumeMode;
1729
- }
1730
- export interface ProviderChoiceContext {
1731
- issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
1732
- readonly storage?: {
1733
- readonly mode: "inline";
1734
- };
1735
- }): string;
1736
- issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
1737
- readonly storage: Extract<ProviderChoiceStorageOptions, {
1738
- readonly mode: "server";
1739
- }>;
1740
- }): Promise<string>;
1741
- issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload> & {
1742
- readonly storage: Extract<ProviderChoiceStorageOptions, {
1743
- readonly mode: "auto";
1744
- }>;
1745
- }): string | Promise<string>;
1746
- issue<TPayload extends Record<string, unknown>>(options: ProviderChoiceIssueOptions<TPayload>): string | Promise<string>;
1747
- parse(options: ProviderChoiceParseOptions & {
1748
- readonly consume: "explicit";
1749
- }): Promise<ProviderChoiceExplicitParseResult>;
1750
- parse(options: ProviderChoiceParseOptions & {
1751
- readonly storage?: {
1752
- readonly mode: "inline";
1753
- };
1754
- }): Record<string, unknown>;
1755
- parse(options: ProviderChoiceParseOptions & {
1756
- readonly storage: Extract<ProviderChoiceStorageOptions, {
1757
- readonly mode: "server";
1758
- }>;
1759
- }): Promise<Record<string, unknown>>;
1760
- parse(options: ProviderChoiceParseOptions & {
1761
- readonly storage: Extract<ProviderChoiceStorageOptions, {
1762
- readonly mode: "auto";
1763
- }>;
1764
- }): Record<string, unknown> | Promise<Record<string, unknown>>;
1765
- parse(options: ProviderChoiceParseOptions): Record<string, unknown> | Promise<Record<string, unknown> | ProviderChoiceExplicitParseResult>;
1766
- }
1767
1664
  export interface ContextScratchpad {
1768
1665
  get(key: string): unknown;
1769
1666
  set(key: string, value: unknown): void;
@@ -1997,8 +1894,8 @@ export type ProviderContext<TConfig = Record<string, unknown>> = {
1997
1894
  stt: SttContext;
1998
1895
  } : Record<never, never>) & ("resolver" extends keyof TConfig ? {
1999
1896
  resolver: ResolverContext;
2000
- } : Record<never, never>) & ("choice" extends keyof TConfig ? {
2001
- choice: ProviderChoiceContext;
1897
+ } : Record<never, never>) & ("handle" extends keyof TConfig ? {
1898
+ handle: HandleContext;
2002
1899
  } : Record<never, never>);
2003
1900
  /** Backwards-compatible name for a declaration-derived provider context. */
2004
1901
  export type ProviderContextFor<TConfig> = ProviderContext<TConfig>;
@@ -2159,7 +2056,7 @@ export interface ProviderDefinition<TContext = ProviderContext> {
2159
2056
  engine: BrowserEngine;
2160
2057
  };
2161
2058
  auth?: AuthConfig;
2162
- choice?: Record<string, never> | true;
2059
+ handle?: readonly HandleKind[];
2163
2060
  reviewed?: ProviderReviewed;
2164
2061
  access?: ProviderAccessConfig;
2165
2062
  secrets?: ProviderSecretDeclaration[];
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.59",
2
+ "version": "2.2.0-beta.61",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -110,6 +110,7 @@
110
110
  "format": "biome format --write",
111
111
  "type-check": "tsc --noEmit",
112
112
  "test": "bun test",
113
+ "bench:redaction": "bun scripts/benchmark-request-redaction.ts",
113
114
  "check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run lint:test-typesafety && bun run build",
114
115
  "api:update": "bun run build && bun scripts/api-reports.ts update",
115
116
  "api:check": "bun run build && bun scripts/api-reports.ts check",
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "../runtime/diagnostic-env.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { existsSync } from "node:fs";
3
4
  import { createRequire } from "node:module";
@@ -256,8 +257,8 @@ const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
256
257
 
257
258
  function redisUrlFromEnv(): string | undefined {
258
259
  return (
259
- process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
260
- process.env[REDIS_URL_ENV]?.trim() ||
260
+ readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
261
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
261
262
  undefined
262
263
  );
263
264
  }
@@ -268,9 +269,9 @@ export function providerCacheRedisUrlFromEnv(): string | undefined {
268
269
 
269
270
  export function providerStateRedisUrlFromEnv(): string | undefined {
270
271
  return (
271
- process.env[PROVIDER_STATE_REDIS_URL_ENV]?.trim() ||
272
- process.env[PROVIDER_CACHE_REDIS_URL_ENV]?.trim() ||
273
- process.env[REDIS_URL_ENV]?.trim() ||
272
+ readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
273
+ readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
274
+ readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
274
275
  undefined
275
276
  );
276
277
  }
@@ -693,12 +694,12 @@ export async function resolveWithVendor(
693
694
  const startedAt = Date.now();
694
695
  const username = (
695
696
  context.credentials === undefined
696
- ? process.env[NODEMAVEN_USERNAME_ENV]
697
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
697
698
  : context.credentials[NODEMAVEN_USERNAME_ENV]
698
699
  )?.trim();
699
700
  const password = (
700
701
  context.credentials === undefined
701
- ? process.env[NODEMAVEN_PASSWORD_ENV]
702
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
702
703
  : context.credentials[NODEMAVEN_PASSWORD_ENV]
703
704
  )?.trim();
704
705
  const filter =
@@ -749,7 +750,7 @@ export async function resolveWithVendor(
749
750
  // smartproxy allocation-style vendor.
750
751
  const appKey = (
751
752
  context.credentials === undefined
752
- ? process.env[SMARTPROXY_APP_KEY_ENV]
753
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
753
754
  : context.credentials[SMARTPROXY_APP_KEY_ENV]
754
755
  )?.trim();
755
756
  if (!appKey) {
@@ -838,18 +839,18 @@ function vendorHasCredentials(
838
839
  if (vendor === "nodemaven") {
839
840
  return Boolean(
840
841
  (credentials === undefined
841
- ? process.env[NODEMAVEN_USERNAME_ENV]
842
+ ? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
842
843
  : credentials[NODEMAVEN_USERNAME_ENV]
843
844
  )?.trim() &&
844
845
  (credentials === undefined
845
- ? process.env[NODEMAVEN_PASSWORD_ENV]
846
+ ? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
846
847
  : credentials[NODEMAVEN_PASSWORD_ENV]
847
848
  )?.trim(),
848
849
  );
849
850
  }
850
851
  return Boolean(
851
852
  (credentials === undefined
852
- ? process.env[SMARTPROXY_APP_KEY_ENV]
853
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
853
854
  : credentials[SMARTPROXY_APP_KEY_ENV]
854
855
  )?.trim(),
855
856
  );
@@ -1757,7 +1758,7 @@ function markSmartproxyCacheInvalidated(options: ProxyResolutionOptions = {}): s
1757
1758
  const lifetimeMinutes = resolveSmartproxyLifetime(policy);
1758
1759
  const appKey = (
1759
1760
  options.engineCredentials === undefined
1760
- ? process.env[SMARTPROXY_APP_KEY_ENV]
1761
+ ? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
1761
1762
  : options.engineCredentials[SMARTPROXY_APP_KEY_ENV]
1762
1763
  )?.trim();
1763
1764
  if (!appKey) return undefined;
package/src/define.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  } from "./native-egress-policy.js";
22
22
  import { safeParseSchemaSync } from "./schema.js";
23
23
  import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
24
+ import type { HandleKind } from "./handle.js";
24
25
  import type {
25
26
  AuthConfig,
26
27
  BrowserEngine,
@@ -601,8 +602,8 @@ export interface ProviderDeclaration {
601
602
  resolver?: ProviderResolverConfig;
602
603
  browser?: { engine: BrowserEngine };
603
604
  auth?: AuthConfig;
604
- /** Declares the choice capability binding. A bare object states use without configuration. */
605
- choice?: Record<string, never> | true;
605
+ /** Declares the handle kinds this provider issues and accepts (ADR-0012). */
606
+ handle?: readonly HandleKind[];
606
607
  reviewed?: ProviderReviewed;
607
608
  access?: ProviderAccessConfig;
608
609
  /** Declares secret requirements; this does not add a `ctx.secrets` member. */
@@ -3042,7 +3043,7 @@ function finalizeProvider<
3042
3043
  resolver: config.resolver,
3043
3044
  browser: config.browser,
3044
3045
  auth: config.auth,
3045
- choice: config.choice,
3046
+ handle: config.handle,
3046
3047
  reviewed: config.reviewed,
3047
3048
  access: config.access,
3048
3049
  secrets: config.secrets,
package/src/dev.ts CHANGED
@@ -17,7 +17,9 @@ export function createDevServer(
17
17
 
18
18
  return {
19
19
  start: () => {
20
- void serve(provider, { port, engine: options?.engine });
20
+ // Local development has no Redis by default; handles (ADR-0012) need a
21
+ // state backend, so fall back to memory state when none is configured.
22
+ void serve(provider, { port, engine: options?.engine, allowMemoryStateFallback: true });
21
23
  console.log(
22
24
  `[apifuse dev] ${provider.id}@${provider.version} running at http://localhost:${port}`,
23
25
  );
package/src/engine.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readDiagnosticEnv } from "./runtime/diagnostic-env.js";
1
2
  import { ProviderError } from "./errors.js";
2
3
  import {
3
4
  APIFUSE__RESOLVER__2CAPTCHA__API_KEY,
@@ -13,6 +14,7 @@ import {
13
14
  OTEL_RESOURCE_ATTRIBUTES,
14
15
  OTEL_SERVICE_NAME,
15
16
  } from "./runtime/otlp.js";
17
+ import type { HandleContext } from "./handle.js";
16
18
  import type {
17
19
  AuthContext,
18
20
  BrowserClient,
@@ -22,7 +24,6 @@ import type {
22
24
  NativeContext,
23
25
  OcrContext,
24
26
  ProviderCache,
25
- ProviderChoiceContext,
26
27
  ProviderContext,
27
28
  ProviderDefinition,
28
29
  ProviderFilesContext,
@@ -117,8 +118,8 @@ function isEngineOwnedEngineEnvName(name: string): boolean {
117
118
  }
118
119
 
119
120
  /**
120
- * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
121
- * tokens, and the CDP pool. Providers never own them: declarations are rejected
121
+ * Hosted runtime settings the engine reads for OCR, STT, cache keying, and
122
+ * the CDP pool. Providers never own them: declarations are rejected
122
123
  * and projections drop them, like the vendor credentials above.
123
124
  */
124
125
  export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
@@ -127,7 +128,6 @@ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
127
128
  "APIFUSE__OCR__API_KEY",
128
129
  "APIFUSE__CLOUDFLARE__ACCOUNT_ID",
129
130
  "APIFUSE__CACHE__KEY_PEPPER",
130
- "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
131
131
  ] as const;
132
132
 
133
133
  const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
@@ -160,7 +160,7 @@ export function readEngineProxyCredentials(
160
160
  ): Readonly<Record<string, string>> {
161
161
  return Object.fromEntries(
162
162
  ENGINE_OWNED_PROXY_CREDENTIAL_ENV_NAMES.flatMap((name) => {
163
- const value = environment[name]?.trim();
163
+ const value = readDiagnosticEnv(name, environment)?.trim();
164
164
  return value ? [[name, value] as const] : [];
165
165
  }),
166
166
  );
@@ -174,7 +174,7 @@ export function createProviderEnvironment(
174
174
  return Object.fromEntries(
175
175
  declaredNames.flatMap((name) => {
176
176
  if (isEngineOwnedEnvName(name)) return [];
177
- const value = environment[name];
177
+ const value = readDiagnosticEnv(name, environment);
178
178
  return value === undefined ? [] : [[name, value] as const];
179
179
  }),
180
180
  );
@@ -236,7 +236,7 @@ export const PROVIDER_CAPABILITY_KEYS = [
236
236
  "ocr",
237
237
  "stt",
238
238
  "resolver",
239
- "choice",
239
+ "handle",
240
240
  ] as const;
241
241
 
242
242
  export type ProviderCapabilityKey = (typeof PROVIDER_CAPABILITY_KEYS)[number];
@@ -248,7 +248,7 @@ export type ProviderEngineBindingCandidates = Partial<
248
248
  readonly credential: CredentialContext;
249
249
  readonly files: ProviderFilesContext;
250
250
  readonly auth: AuthContext;
251
- readonly choice: ProviderChoiceContext;
251
+ readonly handle: HandleContext;
252
252
  }
253
253
  > & {
254
254
  readonly request?: ProviderRequestContext;
@@ -20,10 +20,17 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
20
20
  "refresh_not_supported",
21
21
  "RUNTIME_UNSUPPORTED",
22
22
  "PROVIDER_STATE_UNSUPPORTED",
23
- "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
24
- "CHOICE_STATE_PAYLOAD_TOO_LARGE",
25
- "CHOICE_STATE_UNAVAILABLE",
26
- "CHOICE_CONTEXT_REQUIRED",
23
+ "HANDLE_INVALID",
24
+ "HANDLE_NOT_FOUND",
25
+ "HANDLE_EXPIRED",
26
+ "HANDLE_KIND_MISMATCH",
27
+ "HANDLE_BUSY",
28
+ "HANDLE_COMMITTED",
29
+ "HANDLE_CONNECTION_REQUIRED",
30
+ "HANDLE_STORAGE_UNAVAILABLE",
31
+ "HANDLE_TOO_LARGE",
32
+ "HANDLE_INVALID_DATA",
33
+ "PICK_NOT_OFFERED",
27
34
  "unsupported_stealth_cookie_store_version",
28
35
  "provider_secret_error",
29
36
  "credential_key_error",
@@ -130,6 +137,16 @@ export const SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES: ReadonlyMap<string, Provide
130
137
  ["EGRESS_LEASE_INVALID", 409],
131
138
  ["REPLAY_SESSION_MISMATCH", 409],
132
139
  ["REPLAY_ALREADY_ATTEMPTED", 409],
140
+ // Handles (ADR-0012): only caller-side faults are mapped. Storage,
141
+ // connection-scope, size, and schema faults stay 500 because the provider
142
+ // or host owns them.
143
+ ["HANDLE_INVALID", 400],
144
+ ["HANDLE_KIND_MISMATCH", 400],
145
+ ["PICK_NOT_OFFERED", 400],
146
+ ["HANDLE_NOT_FOUND", 404],
147
+ ["HANDLE_EXPIRED", 410],
148
+ ["HANDLE_COMMITTED", 409],
149
+ ["HANDLE_BUSY", 409],
133
150
  ["UPSTREAM_ERROR", 502],
134
151
  ["BLOCKED", 502],
135
152
  ["OCR_UNAVAILABLE", 503],