@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.62

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 (85) hide show
  1. package/AUTHORING.md +17 -16
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-dev.ts +5 -7
  4. package/bin/apifuse-pack-types.ts +26 -0
  5. package/bin/apifuse-perf.ts +4 -6
  6. package/bin/apifuse-record.ts +4 -7
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/define.d.ts +3 -2
  9. package/dist/define.js +1 -1
  10. package/dist/dev.js +3 -1
  11. package/dist/engine.d.ts +7 -6
  12. package/dist/engine.js +3 -4
  13. package/dist/error-resolution.js +21 -4
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +9 -0
  16. package/dist/handle-meta.d.ts +55 -0
  17. package/dist/handle-meta.js +57 -0
  18. package/dist/handle.d.ts +208 -0
  19. package/dist/handle.js +299 -0
  20. package/dist/i18n/catalog.js +21 -1
  21. package/dist/index.d.ts +7 -4
  22. package/dist/index.js +6 -3
  23. package/dist/lint.d.ts +3 -0
  24. package/dist/lint.js +372 -47
  25. package/dist/provider.d.ts +5 -3
  26. package/dist/provider.js +4 -2
  27. package/dist/runtime/handle-wordlist.d.ts +43 -0
  28. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  29. package/dist/runtime/handle.d.ts +71 -0
  30. package/dist/runtime/handle.js +900 -0
  31. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  32. package/dist/runtime/http-telemetry-binding.js +152 -0
  33. package/dist/runtime/http-telemetry-guard.d.ts +6 -0
  34. package/dist/runtime/http-telemetry-guard.js +150 -0
  35. package/dist/runtime/http-telemetry.d.ts +104 -0
  36. package/dist/runtime/http-telemetry.js +190 -0
  37. package/dist/runtime/http.d.ts +31 -0
  38. package/dist/runtime/http.js +222 -146
  39. package/dist/runtime/native-network.d.ts +10 -1
  40. package/dist/runtime/native-network.js +230 -29
  41. package/dist/runtime/native-telemetry.d.ts +142 -0
  42. package/dist/runtime/native-telemetry.js +231 -0
  43. package/dist/runtime/state.d.ts +4 -1
  44. package/dist/runtime/state.js +32 -17
  45. package/dist/server/index.d.ts +3 -0
  46. package/dist/server/index.js +2 -0
  47. package/dist/server/sensitive-values.d.ts +1 -1
  48. package/dist/server/sensitive-values.js +0 -3
  49. package/dist/server/serve-implementation.d.ts +6 -11
  50. package/dist/server/serve-implementation.js +119 -52
  51. package/dist/testing/run.js +10 -7
  52. package/dist/types.d.ts +4 -107
  53. package/package.json +1 -1
  54. package/src/define.ts +4 -3
  55. package/src/dev.ts +3 -1
  56. package/src/engine.ts +5 -6
  57. package/src/error-resolution.ts +21 -4
  58. package/src/errors.ts +13 -0
  59. package/src/handle-meta.ts +93 -0
  60. package/src/handle.ts +612 -0
  61. package/src/i18n/catalog.ts +23 -1
  62. package/src/index.ts +45 -13
  63. package/src/lint.ts +437 -55
  64. package/src/provider.ts +19 -20
  65. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  66. package/src/runtime/handle.ts +1172 -0
  67. package/src/runtime/http-telemetry-binding.ts +162 -0
  68. package/src/runtime/http-telemetry-guard.ts +155 -0
  69. package/src/runtime/http-telemetry.ts +292 -0
  70. package/src/runtime/http.ts +295 -173
  71. package/src/runtime/native-network.ts +281 -33
  72. package/src/runtime/native-telemetry.ts +366 -0
  73. package/src/runtime/state.ts +44 -16
  74. package/src/server/index.ts +30 -0
  75. package/src/server/sensitive-values.ts +0 -3
  76. package/src/server/serve-implementation.ts +127 -89
  77. package/src/testing/run.ts +11 -7
  78. package/src/types.ts +4 -137
  79. package/dist/choice-token.d.ts +0 -24
  80. package/dist/choice-token.js +0 -74
  81. package/dist/runtime/choice-wordlist.d.ts +0 -9
  82. package/dist/runtime/choice.d.ts +0 -27
  83. package/dist/runtime/choice.js +0 -801
  84. package/src/choice-token.ts +0 -165
  85. package/src/runtime/choice.ts +0 -1197
@@ -53,10 +53,8 @@ import {
53
53
  import { createScratchpad } from "../runtime/auth-flow.js";
54
54
  import type * as BrowserRuntimeModule from "../runtime/browser.js";
55
55
  import { createProviderCache } from "../runtime/cache.js";
56
- import {
57
- createProviderChoiceContext,
58
- PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
59
- } from "../runtime/choice.js";
56
+ import type { HandleTelemetryEvent } from "../handle.js";
57
+ import { createHandleContext } from "../runtime/handle.js";
60
58
  import { createCredentialContext } from "../runtime/credential.js";
61
59
  import {
62
60
  bindDiagnosticSensitiveRegistry,
@@ -78,6 +76,13 @@ import {
78
76
  import { createEnvContext } from "../runtime/env.js";
79
77
  import { executeOperation } from "../runtime/executor.js";
80
78
  import { createHttpClient } from "../runtime/http.js";
79
+ import { NativeTelemetryCollector } from "../runtime/native-telemetry.js";
80
+ import {
81
+ bindHttpTelemetry,
82
+ isSupportedHttpBinding,
83
+ warnUnsupportedHttpBinding,
84
+ } from "../runtime/http-telemetry-binding.js";
85
+ import { HttpTelemetryCollector, type HttpTelemetryLogPayload } from "../runtime/http-telemetry.js";
81
86
  import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
82
87
  import type * as NativeNetworkRuntimeModule from "../runtime/native-network.js";
83
88
  import { createOcrClientFromEnv } from "../runtime/ocr.js";
@@ -154,7 +159,6 @@ import type {
154
159
  BrowserClient,
155
160
  FlowContext,
156
161
  FlowContextStore,
157
- HttpRetrySummary,
158
162
  OcrContext,
159
163
  OperationDefinition,
160
164
  OperationErrorCode,
@@ -235,29 +239,7 @@ function providerErrorCode(error: unknown): string | undefined {
235
239
  }
236
240
 
237
241
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
238
- const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
239
- type RetryLastErrorCode =
240
- // runtime/http.ts:178-179 preserves ambient cancellation.
241
- | "transport_cancelled"
242
- // runtime/http.ts:180-196 normalizes timeout aborts and timeout-shaped errors.
243
- | "transport_timeout"
244
- // runtime/http.ts:198-225 normalizes other native fetch failures.
245
- | "transport_network_error"
246
- // runtime/http.ts:119-126,763-771,829-837 creates upstream status errors.
247
- | "upstream_http_error"
248
- // runtime/proxy-retry-policy.ts:64-84 forwards other coded TransportErrors; the builder closes them.
249
- | "other";
250
- const RETRY_LAST_ERROR_CODES = [
251
- "transport_cancelled",
252
- "transport_timeout",
253
- "transport_network_error",
254
- "upstream_http_error",
255
- "other",
256
- ] as const satisfies readonly RetryLastErrorCode[];
257
-
258
- function tenantRetryLastErrorCode(value: string): RetryLastErrorCode {
259
- return RETRY_LAST_ERROR_CODES.find((candidate) => candidate === value) ?? "other";
260
- }
242
+ const retryResponseMeta = new WeakMap<ProviderContext, HttpTelemetryCollector>();
261
243
  const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
262
244
  const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
263
245
  const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
@@ -845,6 +827,8 @@ type RequestScopeContext = {
845
827
  trace: RuntimeTraceContext;
846
828
  telemetry: RequestTelemetry;
847
829
  resolverTelemetry: ResolverTelemetryCollector;
830
+ nativeTelemetry: NativeTelemetryCollector;
831
+ httpTelemetry: HttpTelemetryCollector;
848
832
  };
849
833
 
850
834
  function createProviderContext(
@@ -873,7 +857,6 @@ function createProviderContext(
873
857
  request.requestId,
874
858
  );
875
859
  const cache = createProviderCache({ providerId: provider.id });
876
- let wrappedContext: ProviderContext | undefined;
877
860
  const { capabilityModules } = options;
878
861
  const resolverOptions = createResolverRuntimeOptions(
879
862
  provider,
@@ -921,10 +904,7 @@ function createProviderContext(
921
904
  scope.redact,
922
905
  );
923
906
 
924
- const env = createEnvContext([
925
- ...providerSecretNames(provider),
926
- PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
927
- ]);
907
+ const env = createEnvContext([...providerSecretNames(provider)]);
928
908
  const credential = createCredentialContext({
929
909
  allowedKeys: provider.credential?.keys,
930
910
  mode: request.connection?.mode,
@@ -944,10 +924,7 @@ function createProviderContext(
944
924
  http: createHttpClient(baseUrl, {
945
925
  ...proxyClientOptions,
946
926
  ...(signal ? { signal } : {}),
947
- onRetrySummary: (summary) => {
948
- if (summary.attempts <= 1 || !wrappedContext) return;
949
- retryResponseMeta.set(wrappedContext, summary);
950
- },
927
+ httpTelemetry: scope.httpTelemetry,
951
928
  }),
952
929
  cache,
953
930
  state: requestState,
@@ -971,6 +948,8 @@ function createProviderContext(
971
948
  ? {
972
949
  native: {
973
950
  network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
951
+ telemetry: scope.telemetry.proxy,
952
+ nativeTelemetry: scope.nativeTelemetry,
974
953
  egress: provider.native.network,
975
954
  proxyPolicy: resolveNativeProxyPolicy(provider),
976
955
  affinityKey: proxyClientOptions.affinityKey,
@@ -1004,24 +983,42 @@ function createProviderContext(
1004
983
  createUnsupportedResolverClient("Provider does not declare resolver capability"),
1005
984
  signal,
1006
985
  ),
1007
- choice: createProviderChoiceContext({
986
+ handle: createHandleContext({
1008
987
  providerId: provider.id,
1009
- env,
1010
988
  request: requestContext,
1011
- credential,
1012
989
  state: requestState,
1013
990
  onTelemetry: (event) =>
1014
991
  (options.logger ?? defaultProviderServerLogger)({
1015
992
  level: "info",
1016
- event: "provider_choice_token",
993
+ event: "provider_handle",
1017
994
  ...event,
1018
995
  }),
1019
996
  }),
1020
997
  };
1021
- const context = wrapWithInstrumentation(
1022
- options.engine.attach({ provider, bindings }) as ProviderContext,
1023
- );
1024
- wrappedContext = context;
998
+ const candidateHttp = bindings.http;
999
+ const attached = options.engine.attach({ provider, bindings }) as ProviderContext;
1000
+ const hostHttp = provider.http ? attached.http : undefined;
1001
+ const supportedHostHttp = provider.http && isSupportedHttpBinding(hostHttp);
1002
+ if (provider.http && !supportedHostHttp) warnUnsupportedHttpBinding();
1003
+ const observedHttp =
1004
+ supportedHostHttp && hostHttp !== candidateHttp
1005
+ ? bindHttpTelemetry(hostHttp, scope.httpTelemetry)
1006
+ : undefined;
1007
+ const contextTarget = observedHttp
1008
+ ? new Proxy(attached, {
1009
+ get(target, property, receiver) {
1010
+ return property === "http" ? observedHttp : Reflect.get(target, property, receiver);
1011
+ },
1012
+ })
1013
+ : provider.http && !supportedHostHttp
1014
+ ? new Proxy(attached, {
1015
+ get(target, property, receiver) {
1016
+ return property === "http" ? undefined : Reflect.get(target, property, receiver);
1017
+ },
1018
+ })
1019
+ : attached;
1020
+ const context = wrapWithInstrumentation(contextTarget);
1021
+ retryResponseMeta.set(context, scope.httpTelemetry);
1025
1022
  return context;
1026
1023
  }
1027
1024
 
@@ -1202,6 +1199,7 @@ function createAuthFlowContext(
1202
1199
  http: createHttpClient(baseUrl, {
1203
1200
  ...proxyClientOptions,
1204
1201
  ...(signal ? { signal } : {}),
1202
+ httpTelemetry: scope.httpTelemetry,
1205
1203
  }),
1206
1204
  state: state.forConnection(resolveOperationConnectionId(request)),
1207
1205
  stealth: stealthBaseUrl
@@ -1213,6 +1211,8 @@ function createAuthFlowContext(
1213
1211
  ? {
1214
1212
  native: {
1215
1213
  network: capabilityModules.nativeNetwork!.createNativeNetworkClient({
1214
+ telemetry: scope.telemetry.proxy,
1215
+ nativeTelemetry: scope.nativeTelemetry,
1216
1216
  egress: provider.native.network,
1217
1217
  proxyPolicy: resolveNativeProxyPolicy(provider),
1218
1218
  affinityKey: proxyClientOptions.affinityKey,
@@ -1289,6 +1289,7 @@ type ProviderServerLogEventBase = ProviderRequestCost & {
1289
1289
  status: number;
1290
1290
  proxy?: ProxyTelemetryLogPayload;
1291
1291
  resolver?: ResolverTelemetryLogPayload;
1292
+ http?: HttpTelemetryLogPayload;
1292
1293
  };
1293
1294
 
1294
1295
  export type ProviderServerLogEvent =
@@ -1318,18 +1319,10 @@ export type ProviderServerLogEvent =
1318
1319
  providerId: string;
1319
1320
  missingSecrets: string[];
1320
1321
  }
1321
- | {
1322
+ | ({
1322
1323
  level: "info";
1323
- event: "provider_choice_token";
1324
- providerId: string;
1325
- purpose: string;
1326
- operation: "parse" | "consume";
1327
- format: "word" | "legacy";
1328
- outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
1329
- consumeMode: "never" | "on-parse" | "explicit";
1330
- consumed: boolean;
1331
- replay: boolean;
1332
- }
1324
+ event: "provider_handle";
1325
+ } & HandleTelemetryEvent)
1333
1326
  | {
1334
1327
  level: "warn";
1335
1328
  event: "provider_cleanup_failed";
@@ -1549,6 +1542,48 @@ function toErrorResponse(
1549
1542
  // narrowing from a ProviderError-typed value would collapse the negative branch
1550
1543
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
1551
1544
  // from a duplicate SDK module instance.
1545
+ function ownErrorCode(error: unknown): string | undefined {
1546
+ if (error === null || (typeof error !== "object" && typeof error !== "function")) {
1547
+ return undefined;
1548
+ }
1549
+ try {
1550
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
1551
+ return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string"
1552
+ ? descriptor.value
1553
+ : undefined;
1554
+ } catch {
1555
+ return undefined;
1556
+ }
1557
+ }
1558
+
1559
+ function errorChainHasCode(error: unknown, expected: string): boolean {
1560
+ const seen = new Set<object>();
1561
+ let current = error;
1562
+ for (let depth = 0; depth < 5; depth++) {
1563
+ if (current === null || (typeof current !== "object" && typeof current !== "function")) {
1564
+ return false;
1565
+ }
1566
+ if (seen.has(current)) return false;
1567
+ seen.add(current);
1568
+ let code: string | undefined;
1569
+ try {
1570
+ code = providerErrorCode(current) ?? ownErrorCode(current);
1571
+ } catch {
1572
+ code = ownErrorCode(current);
1573
+ }
1574
+ if (code === expected) return true;
1575
+ let cause: PropertyDescriptor | undefined;
1576
+ try {
1577
+ cause = Object.getOwnPropertyDescriptor(current, "cause");
1578
+ } catch {
1579
+ return false;
1580
+ }
1581
+ if (!cause || !Object.hasOwn(cause, "value")) return false;
1582
+ current = cause.value;
1583
+ }
1584
+ return false;
1585
+ }
1586
+
1552
1587
  function providerObservabilityDetails(
1553
1588
  error: unknown,
1554
1589
  declaredErrorCode?: OperationErrorCode,
@@ -1577,26 +1612,45 @@ function providerObservabilityDetails(
1577
1612
  retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
1578
1613
  };
1579
1614
  }
1615
+ // Native proxy resolution throws ProxyResolutionError directly, while HTTP
1616
+ // normalizes it beneath a transport_network_error cause and stealth retains
1617
+ // the original code. Preserve proxy attribution across all three forms.
1618
+ if (errorChainHasCode(error, "PROXY_REQUIRED")) {
1619
+ return {
1620
+ category: "proxy_pool",
1621
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
1622
+ retryable: true,
1623
+ };
1624
+ }
1580
1625
  if (!isTransportError(error)) {
1581
1626
  return undefined;
1582
1627
  }
1628
+ const errorCode = providerErrorCode(error);
1583
1629
  const isProxyPoolCode =
1584
- providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE ||
1585
- providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE ||
1586
- providerErrorCode(error) === "PROXY_ALLOCATION_FAILED";
1630
+ errorCode === PROXY_POOL_EXHAUSTED_CODE ||
1631
+ errorCode === PROXY_EDGE_AUTH_REJECTED_CODE ||
1632
+ errorCode === "PROXY_ALLOCATION_FAILED" ||
1633
+ errorCode === "native_proxy_expired" ||
1634
+ errorCode === "native_proxy_invalid";
1587
1635
  const category =
1588
1636
  providerErrorOption(error, "category") ??
1589
1637
  (isProxyPoolCode
1590
1638
  ? "proxy_pool"
1591
- : providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE
1639
+ : errorCode === PROXY_AUTH_IP_DENIED_CODE
1592
1640
  ? "anti_bot_blocked"
1593
- : providerErrorCode(error) === "transport_timeout"
1641
+ : errorCode === "transport_timeout" ||
1642
+ errorCode === "native_connection_timeout" ||
1643
+ errorCode === "native_connection_idle_timeout"
1594
1644
  ? "timeout"
1595
- : providerErrorCode(error) === "transport_network_error"
1645
+ : errorCode === "transport_network_error" ||
1646
+ errorCode === "native_connection_failed" ||
1647
+ errorCode === "native_connection_closed"
1596
1648
  ? "network"
1597
- : error.upstreamStatus
1598
- ? categoryForStatus(error.upstreamStatus)
1599
- : "upstream_http");
1649
+ : errorCode === "native_connection_aborted"
1650
+ ? "client_cancelled"
1651
+ : error.upstreamStatus
1652
+ ? categoryForStatus(error.upstreamStatus)
1653
+ : "upstream_http");
1600
1654
  return {
1601
1655
  category,
1602
1656
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
@@ -2223,9 +2277,13 @@ function createRequestScope(input: {
2223
2277
  bindDiagnosticSensitiveRegistry(trace, sensitiveRegistry);
2224
2278
  const proxyCollector = new ProxyTelemetryCollector();
2225
2279
  const resolverCollector = new ResolverTelemetryCollector({ redact });
2280
+ const nativeCollector = new NativeTelemetryCollector({ redact });
2281
+ const httpCollector = new HttpTelemetryCollector({ redact });
2226
2282
  const telemetry = new RequestTelemetry(trace);
2227
2283
  telemetry.register(proxyCollector);
2228
2284
  telemetry.register(resolverCollector);
2285
+ telemetry.register(nativeCollector);
2286
+ telemetry.register(httpCollector);
2229
2287
  let rootRunner: <T>(fn: () => Promise<T>) => Promise<T> = (fn) => fn();
2230
2288
  let resolveRoot!: (outcome: RequestTerminalOutcome) => void;
2231
2289
  const rootTerminal = new Promise<RequestTerminalOutcome>((resolve) => {
@@ -2307,6 +2365,8 @@ function createRequestScope(input: {
2307
2365
  trace,
2308
2366
  telemetry,
2309
2367
  resolverTelemetry: resolverCollector,
2368
+ nativeTelemetry: nativeCollector,
2369
+ httpTelemetry: httpCollector,
2310
2370
  seedCredentials(rawBody, kind): void {
2311
2371
  const harvested = rawRequestCredentials(rawBody, kind);
2312
2372
  sensitiveRegistry.add(harvested.values);
@@ -2526,21 +2586,13 @@ function toJsonSuccessResponse(
2526
2586
  }
2527
2587
 
2528
2588
  const cacheMeta = ctx && "cache" in ctx ? ctx.cache.responseMeta() : undefined;
2529
- const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
2589
+ const retry = ctx ? retryResponseMeta.get(ctx)?.toTenantRetryPayload() : undefined;
2530
2590
  type TenantCacheMeta = {
2531
2591
  hit: boolean;
2532
2592
  stale: boolean;
2533
2593
  keys: TenantOpaqueKeys;
2534
2594
  source?: ClosedEnum<NonNullable<ProviderCacheResponseMeta["source"]>>;
2535
2595
  };
2536
- type TenantRetryMeta = {
2537
- attempts: number;
2538
- retries: number;
2539
- preset?: ClosedEnum<NonNullable<HttpRetrySummary["preset"]>>;
2540
- transport: ClosedEnum<HttpRetrySummary["transport"]>;
2541
- lastErrorCode?: ClosedEnum<RetryLastErrorCode>;
2542
- lastStatus?: number;
2543
- };
2544
2596
  const cache: TenantNeutral<TenantCacheMeta> | undefined = cacheMeta
2545
2597
  ? {
2546
2598
  hit: cacheMeta.hit,
@@ -2550,22 +2602,8 @@ function toJsonSuccessResponse(
2550
2602
  ...(cacheMeta.source ? { source: closedEnum(cacheMeta.source) } : {}),
2551
2603
  }
2552
2604
  : undefined;
2553
- const retry: TenantNeutral<TenantRetryMeta> | undefined = retryMeta
2554
- ? {
2555
- attempts: retryMeta.attempts,
2556
- retries: retryMeta.retries,
2557
- ...(retryMeta.preset ? { preset: closedEnum(retryMeta.preset) } : {}),
2558
- transport: closedEnum(retryMeta.transport),
2559
- ...(retryMeta.lastErrorCode
2560
- ? {
2561
- lastErrorCode: closedEnum(tenantRetryLastErrorCode(retryMeta.lastErrorCode)),
2562
- }
2563
- : {}),
2564
- ...(retryMeta.lastStatus ? { lastStatus: retryMeta.lastStatus } : {}),
2565
- }
2566
- : undefined;
2567
2605
  const meta =
2568
- cacheMeta || retryMeta
2606
+ cacheMeta || retry
2569
2607
  ? {
2570
2608
  ...(cache
2571
2609
  ? {
@@ -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;
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;