@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,4 +1,11 @@
1
1
  import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
2
+ import {
3
+ type DiagnosticRedactor,
4
+ redactedKeyAllocator,
5
+ isRedactedKey,
6
+ REDACTION_FAILED,
7
+ redactDiagnosticText,
8
+ } from "../runtime/diagnostic-redactor.js";
2
9
  import {
3
10
  OTEL_EXPORTER_OTLP_ENDPOINT,
4
11
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
@@ -22,6 +29,16 @@ import type { TraceConfig } from "../types.js";
22
29
 
23
30
  type EnvLike = Record<string, string | undefined>;
24
31
 
32
+ const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for(
33
+ "@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer",
34
+ );
35
+
36
+ type ResourceAttributesWithSanitizer = Record<string, string> & {
37
+ [TRACE_RESOURCE_ATTRIBUTE_SANITIZER]?: (
38
+ attributes: Record<string, string>,
39
+ ) => Record<string, string>;
40
+ };
41
+
25
42
  // Warn once per environment object: process.env in production, each injected env in tests.
26
43
  const warnedEnvironments = new WeakSet<EnvLike>();
27
44
  const warnedResourceEnvironments = new WeakSet<EnvLike>();
@@ -73,22 +90,52 @@ function resolveServerOTLPExportOptions(
73
90
  function resolveExportResourceAttributes(
74
91
  requestAttributes: Record<string, string>,
75
92
  env: EnvLike,
76
- ): Record<string, string> {
93
+ redact?: DiagnosticRedactor,
94
+ ): {
95
+ attributes: Record<string, string>;
96
+ sanitize: (attributes: Record<string, string>) => Record<string, string>;
97
+ } {
77
98
  const resolution = resolveOTLPResourceAttributes({}, env);
78
99
  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 };
100
+ const operatorAttributes = resolution.attributes;
101
+ const requestKeys = new Set(Object.keys(requestAttributes));
102
+ return {
103
+ // Keep the raw snapshot private until export. A credential registered after
104
+ // trace creation must be matched before any key or value is bounded.
105
+ attributes: { ...operatorAttributes, ...requestAttributes },
106
+ sanitize(attributes) {
107
+ const nextKey = redactedKeyAllocator(Object.keys(attributes));
108
+ const sanitizedOperatorAttributes: Record<string, string> = {};
109
+ const rawRequestAttributes: Record<string, string> = {};
110
+ for (const [key, value] of Object.entries(attributes)) {
111
+ const unchangedOperatorValue = !requestKeys.has(key) && operatorAttributes[key] === value;
112
+ if (unchangedOperatorValue) {
113
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticText(key, redact);
114
+ const keyChanged = redactedKey !== key;
115
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
116
+ sanitizedOperatorAttributes[sanitizedKey] = keyChanged
117
+ ? redactedKey === REDACTION_FAILED
118
+ ? REDACTION_FAILED
119
+ : REDACTED_FIXTURE_VALUE
120
+ : isSensitiveFixtureKey(key)
121
+ ? REDACTED_FIXTURE_VALUE
122
+ : sanitizeSpanNameForOutput(value, redact);
123
+ } else {
124
+ rawRequestAttributes[key] = value;
125
+ }
126
+ }
127
+ const sanitizedRequestAttributes = Object.fromEntries(
128
+ Object.entries(
129
+ sanitizeTraceAttributes(
130
+ rawRequestAttributes,
131
+ redact,
132
+ Object.keys(sanitizedOperatorAttributes),
133
+ ),
134
+ ).map(([key, value]) => [key, String(value)]),
135
+ );
136
+ return { ...sanitizedOperatorAttributes, ...sanitizedRequestAttributes };
137
+ },
138
+ };
92
139
  }
93
140
 
94
141
  /** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
@@ -96,12 +143,14 @@ export function resolveServerTraceContextOptions(
96
143
  config: TraceConfig,
97
144
  resourceAttributes: Record<string, string>,
98
145
  env: EnvLike = process.env,
146
+ redact?: DiagnosticRedactor,
99
147
  ): CreateTraceContextOptions {
100
148
  const resolved = resolveTraceContextOptions(config);
101
149
  const outputEnabled = config.enabled !== false && config.exporter !== "none";
102
150
  const consoleHook =
103
151
  outputEnabled && (config.exporter === "console" || config.exporter === "json")
104
- ? (span: Span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
152
+ ? (span: Span) =>
153
+ console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes, redact)))
105
154
  : undefined;
106
155
  const onSpan =
107
156
  consoleHook && resolved.onSpan
@@ -114,15 +163,26 @@ export function resolveServerTraceContextOptions(
114
163
  outputEnabled && config.exporter === "otlp"
115
164
  ? resolveServerOTLPExportOptions(config, env)
116
165
  : undefined;
166
+ const exportResources = exportOptions
167
+ ? resolveExportResourceAttributes(resourceAttributes, env, redact)
168
+ : undefined;
169
+ if (exportResources) {
170
+ Object.defineProperty(
171
+ exportResources.attributes as ResourceAttributesWithSanitizer,
172
+ TRACE_RESOURCE_ATTRIBUTE_SANITIZER,
173
+ { value: exportResources.sanitize },
174
+ );
175
+ }
117
176
 
118
177
  return {
119
178
  maxSpans: resolved.maxSpans,
120
179
  onSpan,
180
+ ...(redact ? { redact } : {}),
121
181
  ...(exportOptions
122
182
  ? {
123
183
  exportOptions,
124
- resourceAttributes: resolveExportResourceAttributes(resourceAttributes, env),
125
- sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span),
184
+ resourceAttributes: exportResources?.attributes,
185
+ sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span, undefined, redact),
126
186
  }
127
187
  : {}),
128
188
  };
@@ -2,7 +2,13 @@ import { describe, expect, it } from "bun:test";
2
2
 
3
3
  import { ProviderError } from "../errors.js";
4
4
  import { createProviderCache } from "../runtime/cache.js";
5
- import { createTestProviderChoiceContext } from "../runtime/choice.js";
5
+ import { createTestHandleContext } from "../runtime/handle.js";
6
+
7
+ /**
8
+ * Stable connection scope for the local test harness so bound handles (drafts,
9
+ * default cursors) can be exercised exactly as in a connected production call.
10
+ */
11
+ const STANDARD_TEST_CONNECTION_ID = "standard-test-connection";
6
12
  import { createMemoryProviderRuntimeState } from "../runtime/state.js";
7
13
  import { createUnsupportedOcrClient } from "../runtime/ocr.js";
8
14
  import { createUnsupportedSttClient } from "../runtime/stt.js";
@@ -319,7 +325,7 @@ function createUpstreamContext(
319
325
  getAccessToken: () => undefined,
320
326
  getScopes: () => [],
321
327
  };
322
- const request = { headers: {} };
328
+ const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
323
329
  const state = createMemoryProviderRuntimeState();
324
330
  const nativeEgress = provider.native
325
331
  ? createNativeEgressAuthorization({ egress: provider.native.network })
@@ -578,10 +584,9 @@ function createUpstreamContext(
578
584
  resolver: {
579
585
  solve: async () => unsupported("ctx.resolver.solve"),
580
586
  },
581
- choice: createTestProviderChoiceContext({
587
+ handle: createTestHandleContext({
582
588
  providerId: `standard-test-${operationName}`,
583
589
  request,
584
- credential,
585
590
  state,
586
591
  }),
587
592
  } as ProviderContext;
@@ -622,7 +627,7 @@ export function createSnapshotContext(rawFixture: unknown): ProviderContext {
622
627
  getAccessToken: () => undefined,
623
628
  getScopes: () => [],
624
629
  };
625
- const request = { headers: {} };
630
+ const request = { headers: {}, connectionId: STANDARD_TEST_CONNECTION_ID };
626
631
  const state = createMemoryProviderRuntimeState();
627
632
  const streamCaptureGroup = findStreamCaptureGroup(rawFixture);
628
633
  let nextCaptureItem = 0;
@@ -709,10 +714,9 @@ export function createSnapshotContext(rawFixture: unknown): ProviderContext {
709
714
  resolver: {
710
715
  solve: async () => unsupported("ctx.resolver.solve"),
711
716
  },
712
- choice: createTestProviderChoiceContext({
717
+ handle: createTestHandleContext({
713
718
  providerId: "standard-test",
714
719
  request,
715
- credential,
716
720
  state,
717
721
  }),
718
722
  } satisfies Omit<ProviderContext, "native"> as unknown as ProviderContext;
@@ -4,20 +4,33 @@ import {
4
4
  REDACTED_FIXTURE_VALUE,
5
5
  sanitizeDiagnosticText,
6
6
  } from "./fixture-sanitization.js";
7
+ import {
8
+ type DiagnosticRedactor,
9
+ diagnosticStructuredRedactor,
10
+ redactDiagnosticAttributeKey,
11
+ copyDiagnosticAttributeTypes,
12
+ isDiagnosticBigintAttribute,
13
+ redactedKeyAllocator,
14
+ isRedactedKey,
15
+ redactCriticalDiagnosticText,
16
+ diagnosticAttributeRedactor,
17
+ REDACTION_FAILED,
18
+ redactDiagnosticText,
19
+ } from "./runtime/diagnostic-redactor.js";
7
20
  import type { TraceSpan } from "./types.js";
8
21
 
9
22
  const MAX_TRACE_TEXT_LENGTH = 300;
10
23
 
11
- function sanitizeTraceText(value: string): string {
12
- const encoded = sanitizeDiagnosticText(value);
24
+ function sanitizeTraceText(value: string, redact?: DiagnosticRedactor): string {
25
+ const encoded = sanitizeDiagnosticText(redactDiagnosticText(value, redact));
13
26
  return encoded.length > MAX_TRACE_TEXT_LENGTH
14
27
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
15
28
  : encoded;
16
29
  }
17
30
 
18
31
  /** Span names are SDK-authored identifiers; retain them while preventing log injection. */
19
- export function sanitizeSpanNameForOutput(value: string): string {
20
- const encoded = encodeDiagnosticControls(value);
32
+ export function sanitizeSpanNameForOutput(value: string, redact?: DiagnosticRedactor): string {
33
+ const encoded = encodeDiagnosticControls(redactDiagnosticText(value, redact));
21
34
  return encoded.length > MAX_TRACE_TEXT_LENGTH
22
35
  ? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
23
36
  : encoded;
@@ -25,39 +38,73 @@ export function sanitizeSpanNameForOutput(value: string): string {
25
38
 
26
39
  export function sanitizeTraceAttributes(
27
40
  attributes: Record<string, unknown>,
41
+ redact?: DiagnosticRedactor,
42
+ reservedKeys: Iterable<string> = [],
28
43
  ): Record<string, string | number | boolean> {
29
- return Object.fromEntries(
30
- Object.entries(attributes).map(([key, value]) => [
31
- sanitizeTraceText(key),
32
- isSensitiveFixtureKey(key)
33
- ? REDACTED_FIXTURE_VALUE
34
- : typeof value === "string"
35
- ? sanitizeTraceText(value)
36
- : typeof value === "number" || typeof value === "boolean"
37
- ? value
38
- : sanitizeTraceText(String(value)),
39
- ]),
44
+ const nextKey = redactedKeyAllocator([...Object.keys(attributes), ...reservedKeys]);
45
+ return copyDiagnosticAttributeTypes(
46
+ attributes,
47
+ Object.fromEntries(
48
+ Object.entries(attributes).map(([key, value]) => {
49
+ const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
50
+ const keyChanged = redactedKey !== key;
51
+ const sanitizedKey = keyChanged ? nextKey() : sanitizeTraceText(key);
52
+ const valueRedactor =
53
+ typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
54
+ ? diagnosticStructuredRedactor(redact)
55
+ : diagnosticAttributeRedactor(key, redact);
56
+ const sanitizedValue = keyChanged
57
+ ? redactedKey === REDACTION_FAILED
58
+ ? REDACTION_FAILED
59
+ : REDACTED_FIXTURE_VALUE
60
+ : isSensitiveFixtureKey(key)
61
+ ? REDACTED_FIXTURE_VALUE
62
+ : typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
63
+ ? String(value).length < 8
64
+ ? String(value)
65
+ : sanitizeTraceText(String(value), valueRedactor)
66
+ : typeof value === "string"
67
+ ? sanitizeTraceText(value, valueRedactor)
68
+ : typeof value === "number" || typeof value === "boolean"
69
+ ? (() => {
70
+ if (String(value).length < 8) return value;
71
+ const redacted = redactDiagnosticText(
72
+ String(value),
73
+ diagnosticStructuredRedactor(valueRedactor),
74
+ );
75
+ return redacted === String(value) ? value : sanitizeTraceText(redacted);
76
+ })()
77
+ : sanitizeTraceText(String(value), valueRedactor);
78
+ return [sanitizedKey, sanitizedValue];
79
+ }),
80
+ ),
40
81
  );
41
82
  }
42
83
 
43
84
  export function sanitizeSpanForOutput(
44
85
  span: TraceSpan,
45
86
  additionalAttributes?: Record<string, string>,
87
+ redact?: DiagnosticRedactor,
46
88
  ): TraceSpan {
47
89
  // Keep this schema explicit so future fields are not silently added to a
48
90
  // process output path before their trust boundary has been reviewed.
49
91
  return {
50
- id: span.id,
51
- name: sanitizeSpanNameForOutput(span.name),
92
+ id: redactCriticalDiagnosticText(span.id, redact),
93
+ name: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(span.name, redact)),
52
94
  startedAt: span.startedAt,
53
95
  endedAt: span.endedAt,
54
96
  duration_ms: span.duration_ms,
55
97
  status: span.status,
56
- attributes: sanitizeTraceAttributes({
57
- ...span.attributes,
58
- ...(additionalAttributes ?? {}),
59
- }),
60
- ...(span.error !== undefined ? { error: sanitizeTraceText(span.error) } : {}),
61
- ...(span.parentId !== undefined ? { parentId: span.parentId } : {}),
98
+ attributes: sanitizeTraceAttributes(
99
+ copyDiagnosticAttributeTypes(span.attributes, {
100
+ ...span.attributes,
101
+ ...(additionalAttributes ?? {}),
102
+ }),
103
+ redact,
104
+ ),
105
+ ...(span.error !== undefined ? { error: sanitizeTraceText(span.error, redact) } : {}),
106
+ ...(span.parentId !== undefined
107
+ ? { parentId: redactCriticalDiagnosticText(span.parentId, redact) }
108
+ : {}),
62
109
  };
63
110
  }
package/src/types.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
 
@@ -1997,140 +1998,6 @@ export interface ProviderRequestContext {
1997
1998
  headers: Record<string, string>;
1998
1999
  }
1999
2000
 
2000
- export interface ProviderChoiceBindingOptions {
2001
- connection?: boolean;
2002
- credentialKeys?: readonly string[];
2003
- }
2004
-
2005
- export type ProviderChoiceConsumeMode = "never" | "on-parse" | "explicit";
2006
-
2007
- export type ProviderChoiceConsumeResult =
2008
- | { readonly status: "consumed" }
2009
- | { readonly status: "already-consumed" }
2010
- | { readonly status: "unsupported" };
2011
-
2012
- export type ProviderChoiceExplicitParseResult =
2013
- | {
2014
- readonly status: "active";
2015
- readonly payload: Record<string, unknown>;
2016
- /** Stable, opaque key for provider-owned idempotency records. */
2017
- readonly replayKey: string;
2018
- /** Atomically claims a word token. Inline tokens report unsupported. */
2019
- consume(): Promise<ProviderChoiceConsumeResult>;
2020
- }
2021
- | {
2022
- readonly status: "consumed";
2023
- /** Use this key to read the provider-owned result before returning an error. */
2024
- readonly replayKey: string;
2025
- };
2026
-
2027
- export type ProviderChoiceStorageOptions =
2028
- | {
2029
- readonly mode: "inline";
2030
- }
2031
- | {
2032
- readonly mode: "server";
2033
- readonly namespace: string;
2034
- readonly state?: ProviderRuntimeState;
2035
- readonly ttl?: ProviderStateDurationString;
2036
- readonly maxEntries: number;
2037
- readonly maxValueBytes: number;
2038
- readonly unavailable?: "reject";
2039
- }
2040
- | {
2041
- readonly mode: "auto";
2042
- readonly namespace: string;
2043
- readonly state?: ProviderRuntimeState;
2044
- readonly ttl?: ProviderStateDurationString;
2045
- readonly maxInlineBytes: number;
2046
- readonly maxEntries: number;
2047
- readonly maxValueBytes: number;
2048
- readonly unavailable?: "reject";
2049
- };
2050
-
2051
- export interface ProviderChoiceIssueOptions<
2052
- TPayload extends Record<string, unknown>,
2053
- > {
2054
- prefix: string;
2055
- purpose: string;
2056
- payload: TPayload;
2057
- ttlMs: number;
2058
- nowMs?: number;
2059
- bind?: ProviderChoiceBindingOptions;
2060
- /** Server storage only: standard emits four words; high emits five. */
2061
- strength?: "standard" | "high";
2062
- storage?: ProviderChoiceStorageOptions;
2063
- }
2064
-
2065
- export interface ProviderChoiceParseOptions {
2066
- token: string;
2067
- prefix: string;
2068
- purpose: string;
2069
- ttlMs?: number;
2070
- nowMs?: number;
2071
- futureToleranceMs?: number;
2072
- bind?: ProviderChoiceBindingOptions;
2073
- storage?: ProviderChoiceStorageOptions;
2074
- /** Defaults to never, preserving reusable choice-token parse semantics. */
2075
- consume?: ProviderChoiceConsumeMode;
2076
- }
2077
-
2078
- export interface ProviderChoiceContext {
2079
- issue<TPayload extends Record<string, unknown>>(
2080
- options: ProviderChoiceIssueOptions<TPayload> & {
2081
- readonly storage?: { readonly mode: "inline" };
2082
- },
2083
- ): string;
2084
- issue<TPayload extends Record<string, unknown>>(
2085
- options: ProviderChoiceIssueOptions<TPayload> & {
2086
- readonly storage: Extract<
2087
- ProviderChoiceStorageOptions,
2088
- { readonly mode: "server" }
2089
- >;
2090
- },
2091
- ): Promise<string>;
2092
- issue<TPayload extends Record<string, unknown>>(
2093
- options: ProviderChoiceIssueOptions<TPayload> & {
2094
- readonly storage: Extract<
2095
- ProviderChoiceStorageOptions,
2096
- { readonly mode: "auto" }
2097
- >;
2098
- },
2099
- ): string | Promise<string>;
2100
- issue<TPayload extends Record<string, unknown>>(
2101
- options: ProviderChoiceIssueOptions<TPayload>,
2102
- ): string | Promise<string>;
2103
- parse(
2104
- options: ProviderChoiceParseOptions & { readonly consume: "explicit" },
2105
- ): Promise<ProviderChoiceExplicitParseResult>;
2106
- parse(
2107
- options: ProviderChoiceParseOptions & {
2108
- readonly storage?: { readonly mode: "inline" };
2109
- },
2110
- ): Record<string, unknown>;
2111
- parse(
2112
- options: ProviderChoiceParseOptions & {
2113
- readonly storage: Extract<
2114
- ProviderChoiceStorageOptions,
2115
- { readonly mode: "server" }
2116
- >;
2117
- },
2118
- ): Promise<Record<string, unknown>>;
2119
- parse(
2120
- options: ProviderChoiceParseOptions & {
2121
- readonly storage: Extract<
2122
- ProviderChoiceStorageOptions,
2123
- { readonly mode: "auto" }
2124
- >;
2125
- },
2126
- ): Record<string, unknown> | Promise<Record<string, unknown>>;
2127
- parse(
2128
- options: ProviderChoiceParseOptions,
2129
- ):
2130
- | Record<string, unknown>
2131
- | Promise<Record<string, unknown> | ProviderChoiceExplicitParseResult>;
2132
- }
2133
-
2134
2001
  export interface ContextScratchpad {
2135
2002
  get(key: string): unknown;
2136
2003
  set(key: string, value: unknown): void;
@@ -2402,8 +2269,8 @@ export type ProviderContext<TConfig = Record<string, unknown>> = {
2402
2269
  & ("ocr" extends keyof TConfig ? { ocr: OcrContext } : Record<never, never>)
2403
2270
  & ("stt" extends keyof TConfig ? { stt: SttContext } : Record<never, never>)
2404
2271
  & ("resolver" extends keyof TConfig ? { resolver: ResolverContext } : Record<never, never>)
2405
- & ("choice" extends keyof TConfig
2406
- ? { choice: ProviderChoiceContext }
2272
+ & ("handle" extends keyof TConfig
2273
+ ? { handle: HandleContext }
2407
2274
  : Record<never, never>);
2408
2275
 
2409
2276
  /** Backwards-compatible name for a declaration-derived provider context. */
@@ -2588,7 +2455,7 @@ export interface ProviderDefinition<TContext = ProviderContext> {
2588
2455
  engine: BrowserEngine;
2589
2456
  };
2590
2457
  auth?: AuthConfig;
2591
- choice?: Record<string, never> | true;
2458
+ handle?: readonly HandleKind[];
2592
2459
  reviewed?: ProviderReviewed;
2593
2460
  access?: ProviderAccessConfig;
2594
2461
  secrets?: ProviderSecretDeclaration[];
@@ -1,24 +0,0 @@
1
- export type ProviderChoiceTokenPayload = Record<string, unknown>;
2
- export type ProviderChoiceTokenErrorReason = "invalid_shape" | "invalid_signature" | "invalid_payload" | "invalid_binding" | "stale";
3
- export declare class ProviderChoiceTokenError extends Error {
4
- readonly reason: ProviderChoiceTokenErrorReason;
5
- constructor(reason: ProviderChoiceTokenErrorReason, message: string);
6
- }
7
- export interface CreateProviderChoiceTokenOptions<TPayload extends ProviderChoiceTokenPayload> {
8
- prefix: string;
9
- payload: TPayload;
10
- secret: string;
11
- }
12
- export interface ParseProviderChoiceTokenOptions {
13
- token: string;
14
- prefix: string;
15
- secret: string;
16
- }
17
- export interface FreshProviderChoiceIssuedAtOptions {
18
- ttlMs: number;
19
- nowMs?: number;
20
- futureToleranceMs?: number;
21
- }
22
- export declare function createProviderChoiceToken<TPayload extends ProviderChoiceTokenPayload>(options: CreateProviderChoiceTokenOptions<TPayload>): string;
23
- export declare function parseProviderChoiceToken(options: ParseProviderChoiceTokenOptions): ProviderChoiceTokenPayload;
24
- export declare function assertFreshProviderChoiceIssuedAt(issuedAtMs: unknown, options: FreshProviderChoiceIssuedAtOptions): number;
@@ -1,74 +0,0 @@
1
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
- export class ProviderChoiceTokenError extends Error {
3
- reason;
4
- constructor(reason, message) {
5
- super(message);
6
- this.name = "ProviderChoiceTokenError";
7
- this.reason = reason;
8
- }
9
- }
10
- export function createProviderChoiceToken(options) {
11
- const iv = randomBytes(12);
12
- const cipher = createCipheriv("aes-256-gcm", choiceEncryptionKey(options.secret), iv);
13
- const encryptedPayload = Buffer.concat([
14
- cipher.update(JSON.stringify(options.payload), "utf8"),
15
- cipher.final(),
16
- ]).toString("base64url");
17
- const authTag = cipher.getAuthTag().toString("base64url");
18
- const encodedIv = iv.toString("base64url");
19
- const signature = signProviderChoiceTokenBody(`${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}`, options.secret);
20
- return `${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}.${signature}`;
21
- }
22
- export function parseProviderChoiceToken(options) {
23
- const [actualPrefix, encodedIv, encryptedPayload, authTag, signature, ...extra] = options.token.split(".");
24
- if (actualPrefix !== options.prefix ||
25
- !encodedIv ||
26
- !encryptedPayload ||
27
- !authTag ||
28
- !signature ||
29
- extra.length > 0) {
30
- throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
31
- }
32
- const signedBody = `${options.prefix}.${encodedIv}.${encryptedPayload}.${authTag}`;
33
- const expectedSignature = signProviderChoiceTokenBody(signedBody, options.secret);
34
- const actualBuffer = Buffer.from(signature);
35
- const expectedBuffer = Buffer.from(expectedSignature);
36
- if (actualBuffer.length !== expectedBuffer.length ||
37
- !timingSafeEqual(actualBuffer, expectedBuffer)) {
38
- throw new ProviderChoiceTokenError("invalid_signature", "Provider choice token signature is invalid.");
39
- }
40
- try {
41
- const decipher = createDecipheriv("aes-256-gcm", choiceEncryptionKey(options.secret), Buffer.from(encodedIv, "base64url"));
42
- decipher.setAuthTag(Buffer.from(authTag, "base64url"));
43
- const decrypted = Buffer.concat([
44
- decipher.update(Buffer.from(encryptedPayload, "base64url")),
45
- decipher.final(),
46
- ]).toString("utf8");
47
- const parsed = JSON.parse(decrypted);
48
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
49
- throw new Error("payload is not an object");
50
- }
51
- return Object.fromEntries(Object.entries(parsed));
52
- }
53
- catch {
54
- throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
55
- }
56
- }
57
- export function assertFreshProviderChoiceIssuedAt(issuedAtMs, options) {
58
- const parsed = typeof issuedAtMs === "number" ? issuedAtMs : Number(issuedAtMs);
59
- const nowMs = options.nowMs ?? Date.now();
60
- const futureToleranceMs = options.futureToleranceMs ?? 30_000;
61
- if (!Number.isFinite(parsed) ||
62
- parsed <= 0 ||
63
- nowMs - parsed > options.ttlMs ||
64
- parsed - nowMs > futureToleranceMs) {
65
- throw new ProviderChoiceTokenError("stale", "Provider choice token is stale.");
66
- }
67
- return parsed;
68
- }
69
- function choiceEncryptionKey(secret) {
70
- return createHash("sha256").update(secret).digest();
71
- }
72
- function signProviderChoiceTokenBody(body, secret) {
73
- return createHmac("sha256", secret).update(body).digest("base64url");
74
- }
@@ -1,9 +0,0 @@
1
- export declare const CHOICE_WORDLIST_SIZE = 1296;
2
- export declare const CHOICE_WORD_ENTROPY_BITS: number;
3
- export declare const STANDARD_CHOICE_WORD_COUNT = 4;
4
- export declare const HIGH_CHOICE_WORD_COUNT = 5;
5
- export declare const STANDARD_CHOICE_ENTROPY_BITS: number;
6
- export declare const HIGH_CHOICE_ENTROPY_BITS: number;
7
- export declare const EFF_SHORT_WORDLIST_2: readonly string[];
8
- export declare function choiceWordAt(index: number): string;
9
- export declare function isChoiceWord(value: string): boolean;
@@ -1,27 +0,0 @@
1
- import type { CredentialContext, EnvContext, ProviderChoiceConsumeMode, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types.js";
2
- export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
3
- export type ProviderChoiceTelemetryEvent = {
4
- readonly providerId: string;
5
- readonly purpose: string;
6
- readonly operation: "parse" | "consume";
7
- readonly format: "word" | "legacy";
8
- readonly outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
9
- readonly consumeMode: ProviderChoiceConsumeMode;
10
- readonly consumed: boolean;
11
- readonly replay: boolean;
12
- };
13
- export type CreateProviderChoiceContextOptions = {
14
- readonly providerId: string;
15
- readonly env?: EnvContext;
16
- readonly request?: ProviderRequestContext;
17
- readonly credential?: CredentialContext;
18
- readonly state?: ProviderRuntimeState;
19
- readonly masterSecret?: string;
20
- readonly kid?: string;
21
- /** Receives allowlisted metadata only; token and payload values are never included. */
22
- readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
23
- };
24
- export declare function createProviderChoiceContext(options: CreateProviderChoiceContextOptions): ProviderChoiceContext;
25
- export declare function createTestProviderChoiceContext(options: Omit<CreateProviderChoiceContextOptions, "masterSecret"> & {
26
- readonly masterSecret?: string;
27
- }): ProviderChoiceContext;