@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
@@ -18,13 +18,16 @@ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.j
18
18
  import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, sourceForCategory, } from "../observability.js";
19
19
  import { createScratchpad } from "../runtime/auth-flow.js";
20
20
  import { createProviderCache } from "../runtime/cache.js";
21
- import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
21
+ import { createHandleContext } from "../runtime/handle.js";
22
22
  import { createCredentialContext } from "../runtime/credential.js";
23
23
  import { bindDiagnosticSensitiveRegistry, compileProcessDiagnosticSensitiveValues, createDiagnosticRedactor, REDACTION_FAILED, isDiagnosticRedactionSuppressed, bindCriticalDiagnosticRedactor, redactCriticalDiagnosticText, redactDiagnosticText, registerDiagnosticSensitiveValues, } from "../runtime/diagnostic-redactor.js";
24
24
  import { createCeremonyEgressLeaseRuntime, ENGINE_CEREMONY_EGRESS_LEASE, } from "../runtime/egress-lease.js";
25
25
  import { createEnvContext } from "../runtime/env.js";
26
26
  import { executeOperation } from "../runtime/executor.js";
27
27
  import { createHttpClient } from "../runtime/http.js";
28
+ import { NativeTelemetryCollector } from "../runtime/native-telemetry.js";
29
+ import { bindHttpTelemetry, isSupportedHttpBinding, warnUnsupportedHttpBinding, } from "../runtime/http-telemetry-binding.js";
30
+ import { HttpTelemetryCollector } from "../runtime/http-telemetry.js";
28
31
  import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
29
32
  import { createOcrClientFromEnv } from "../runtime/ocr.js";
30
33
  import { getProviderBaseUrl } from "../runtime/provider.js";
@@ -82,16 +85,6 @@ function providerErrorCode(error) {
82
85
  }
83
86
  const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
84
87
  const retryResponseMeta = new WeakMap();
85
- const RETRY_LAST_ERROR_CODES = [
86
- "transport_cancelled",
87
- "transport_timeout",
88
- "transport_network_error",
89
- "upstream_http_error",
90
- "other",
91
- ];
92
- function tenantRetryLastErrorCode(value) {
93
- return RETRY_LAST_ERROR_CODES.find((candidate) => candidate === value) ?? "other";
94
- }
95
88
  const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
96
89
  const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
97
90
  const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
@@ -503,7 +496,6 @@ function createProviderContext(provider, request, operationId, options, state =
503
496
  };
504
497
  const resolverIdentityScope = resolveProviderResolverIdentityScope(provider, proxyClientOptions.affinityKey, request.requestId);
505
498
  const cache = createProviderCache({ providerId: provider.id });
506
- let wrappedContext;
507
499
  const { capabilityModules } = options;
508
500
  const resolverOptions = createResolverRuntimeOptions(provider, cache, resolverIdentityScope, proxyPolicy, proxyClientOptions, stealthProfile, scope.resolverTelemetry);
509
501
  const challengeRuntime = createStealthChallengeDetection(provider, capabilityModules.resolver, options.resolver, resolverOptions, scope.trace, signal);
@@ -525,10 +517,7 @@ function createProviderContext(provider, request, operationId, options, state =
525
517
  : {}),
526
518
  };
527
519
  const logStealthCleanupError = (error) => logProviderCleanupError(options.logger, provider, "operation", operationId, request.requestId, "stealth", error, scope.redact);
528
- const env = createEnvContext([
529
- ...providerSecretNames(provider),
530
- PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
531
- ]);
520
+ const env = createEnvContext([...providerSecretNames(provider)]);
532
521
  const credential = createCredentialContext({
533
522
  allowedKeys: provider.credential?.keys,
534
523
  mode: request.connection?.mode,
@@ -548,11 +537,7 @@ function createProviderContext(provider, request, operationId, options, state =
548
537
  http: createHttpClient(baseUrl, {
549
538
  ...proxyClientOptions,
550
539
  ...(signal ? { signal } : {}),
551
- onRetrySummary: (summary) => {
552
- if (summary.attempts <= 1 || !wrappedContext)
553
- return;
554
- retryResponseMeta.set(wrappedContext, summary);
555
- },
540
+ httpTelemetry: scope.httpTelemetry,
556
541
  }),
557
542
  cache,
558
543
  state: requestState,
@@ -575,6 +560,8 @@ function createProviderContext(provider, request, operationId, options, state =
575
560
  ? {
576
561
  native: {
577
562
  network: capabilityModules.nativeNetwork.createNativeNetworkClient({
563
+ telemetry: scope.telemetry.proxy,
564
+ nativeTelemetry: scope.nativeTelemetry,
578
565
  egress: provider.native.network,
579
566
  proxyPolicy: resolveNativeProxyPolicy(provider),
580
567
  affinityKey: proxyClientOptions.affinityKey,
@@ -596,21 +583,41 @@ function createProviderContext(provider, request, operationId, options, state =
596
583
  ? bindResolverTelemetry(options.resolver, resolverOptions.telemetry)
597
584
  : undefined) ??
598
585
  createUnsupportedResolverClient("Provider does not declare resolver capability"), signal),
599
- choice: createProviderChoiceContext({
586
+ handle: createHandleContext({
600
587
  providerId: provider.id,
601
- env,
602
588
  request: requestContext,
603
- credential,
604
589
  state: requestState,
605
590
  onTelemetry: (event) => (options.logger ?? defaultProviderServerLogger)({
606
591
  level: "info",
607
- event: "provider_choice_token",
592
+ event: "provider_handle",
608
593
  ...event,
609
594
  }),
610
595
  }),
611
596
  };
612
- const context = wrapWithInstrumentation(options.engine.attach({ provider, bindings }));
613
- wrappedContext = context;
597
+ const candidateHttp = bindings.http;
598
+ const attached = options.engine.attach({ provider, bindings });
599
+ const hostHttp = provider.http ? attached.http : undefined;
600
+ const supportedHostHttp = provider.http && isSupportedHttpBinding(hostHttp);
601
+ if (provider.http && !supportedHostHttp)
602
+ warnUnsupportedHttpBinding();
603
+ const observedHttp = supportedHostHttp && hostHttp !== candidateHttp
604
+ ? bindHttpTelemetry(hostHttp, scope.httpTelemetry)
605
+ : undefined;
606
+ const contextTarget = observedHttp
607
+ ? new Proxy(attached, {
608
+ get(target, property, receiver) {
609
+ return property === "http" ? observedHttp : Reflect.get(target, property, receiver);
610
+ },
611
+ })
612
+ : provider.http && !supportedHostHttp
613
+ ? new Proxy(attached, {
614
+ get(target, property, receiver) {
615
+ return property === "http" ? undefined : Reflect.get(target, property, receiver);
616
+ },
617
+ })
618
+ : attached;
619
+ const context = wrapWithInstrumentation(contextTarget);
620
+ retryResponseMeta.set(context, scope.httpTelemetry);
614
621
  return context;
615
622
  }
616
623
  function createFlowContextStore(allowedKeys, initialContext = {}) {
@@ -721,6 +728,7 @@ function createAuthFlowContext(provider, request, route, options, state, scope,
721
728
  http: createHttpClient(baseUrl, {
722
729
  ...proxyClientOptions,
723
730
  ...(signal ? { signal } : {}),
731
+ httpTelemetry: scope.httpTelemetry,
724
732
  }),
725
733
  state: state.forConnection(resolveOperationConnectionId(request)),
726
734
  stealth: stealthBaseUrl
@@ -732,6 +740,8 @@ function createAuthFlowContext(provider, request, route, options, state, scope,
732
740
  ? {
733
741
  native: {
734
742
  network: capabilityModules.nativeNetwork.createNativeNetworkClient({
743
+ telemetry: scope.telemetry.proxy,
744
+ nativeTelemetry: scope.nativeTelemetry,
735
745
  egress: provider.native.network,
736
746
  proxyPolicy: resolveNativeProxyPolicy(provider),
737
747
  affinityKey: proxyClientOptions.affinityKey,
@@ -884,6 +894,52 @@ function toErrorResponse(error, requestId, observabilityDetails, redact) {
884
894
  // narrowing from a ProviderError-typed value would collapse the negative branch
885
895
  // to `never`. Narrowing from unknown avoids that while still recognizing errors
886
896
  // from a duplicate SDK module instance.
897
+ function ownErrorCode(error) {
898
+ if (error === null || (typeof error !== "object" && typeof error !== "function")) {
899
+ return undefined;
900
+ }
901
+ try {
902
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
903
+ return descriptor && Object.hasOwn(descriptor, "value") && typeof descriptor.value === "string"
904
+ ? descriptor.value
905
+ : undefined;
906
+ }
907
+ catch {
908
+ return undefined;
909
+ }
910
+ }
911
+ function errorChainHasCode(error, expected) {
912
+ const seen = new Set();
913
+ let current = error;
914
+ for (let depth = 0; depth < 5; depth++) {
915
+ if (current === null || (typeof current !== "object" && typeof current !== "function")) {
916
+ return false;
917
+ }
918
+ if (seen.has(current))
919
+ return false;
920
+ seen.add(current);
921
+ let code;
922
+ try {
923
+ code = providerErrorCode(current) ?? ownErrorCode(current);
924
+ }
925
+ catch {
926
+ code = ownErrorCode(current);
927
+ }
928
+ if (code === expected)
929
+ return true;
930
+ let cause;
931
+ try {
932
+ cause = Object.getOwnPropertyDescriptor(current, "cause");
933
+ }
934
+ catch {
935
+ return false;
936
+ }
937
+ if (!cause || !Object.hasOwn(cause, "value"))
938
+ return false;
939
+ current = cause.value;
940
+ }
941
+ return false;
942
+ }
887
943
  function providerObservabilityDetails(error, declaredErrorCode) {
888
944
  const declaredRetryable = sdkOwnsErrorResolution(error)
889
945
  ? undefined
@@ -909,24 +965,43 @@ function providerObservabilityDetails(error, declaredErrorCode) {
909
965
  retryable: providerErrorOption(error, "retryable") ?? declaredRetryable ?? false,
910
966
  };
911
967
  }
968
+ // Native proxy resolution throws ProxyResolutionError directly, while HTTP
969
+ // normalizes it beneath a transport_network_error cause and stealth retains
970
+ // the original code. Preserve proxy attribution across all three forms.
971
+ if (errorChainHasCode(error, "PROXY_REQUIRED")) {
972
+ return {
973
+ category: "proxy_pool",
974
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
975
+ retryable: true,
976
+ };
977
+ }
912
978
  if (!isTransportError(error)) {
913
979
  return undefined;
914
980
  }
915
- const isProxyPoolCode = providerErrorCode(error) === PROXY_POOL_EXHAUSTED_CODE ||
916
- providerErrorCode(error) === PROXY_EDGE_AUTH_REJECTED_CODE ||
917
- providerErrorCode(error) === "PROXY_ALLOCATION_FAILED";
981
+ const errorCode = providerErrorCode(error);
982
+ const isProxyPoolCode = errorCode === PROXY_POOL_EXHAUSTED_CODE ||
983
+ errorCode === PROXY_EDGE_AUTH_REJECTED_CODE ||
984
+ errorCode === "PROXY_ALLOCATION_FAILED" ||
985
+ errorCode === "native_proxy_expired" ||
986
+ errorCode === "native_proxy_invalid";
918
987
  const category = providerErrorOption(error, "category") ??
919
988
  (isProxyPoolCode
920
989
  ? "proxy_pool"
921
- : providerErrorCode(error) === PROXY_AUTH_IP_DENIED_CODE
990
+ : errorCode === PROXY_AUTH_IP_DENIED_CODE
922
991
  ? "anti_bot_blocked"
923
- : providerErrorCode(error) === "transport_timeout"
992
+ : errorCode === "transport_timeout" ||
993
+ errorCode === "native_connection_timeout" ||
994
+ errorCode === "native_connection_idle_timeout"
924
995
  ? "timeout"
925
- : providerErrorCode(error) === "transport_network_error"
996
+ : errorCode === "transport_network_error" ||
997
+ errorCode === "native_connection_failed" ||
998
+ errorCode === "native_connection_closed"
926
999
  ? "network"
927
- : error.upstreamStatus
928
- ? categoryForStatus(error.upstreamStatus)
929
- : "upstream_http");
1000
+ : errorCode === "native_connection_aborted"
1001
+ ? "client_cancelled"
1002
+ : error.upstreamStatus
1003
+ ? categoryForStatus(error.upstreamStatus)
1004
+ : "upstream_http");
930
1005
  return {
931
1006
  category,
932
1007
  taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
@@ -1415,9 +1490,13 @@ function createRequestScope(input) {
1415
1490
  bindDiagnosticSensitiveRegistry(trace, sensitiveRegistry);
1416
1491
  const proxyCollector = new ProxyTelemetryCollector();
1417
1492
  const resolverCollector = new ResolverTelemetryCollector({ redact });
1493
+ const nativeCollector = new NativeTelemetryCollector({ redact });
1494
+ const httpCollector = new HttpTelemetryCollector({ redact });
1418
1495
  const telemetry = new RequestTelemetry(trace);
1419
1496
  telemetry.register(proxyCollector);
1420
1497
  telemetry.register(resolverCollector);
1498
+ telemetry.register(nativeCollector);
1499
+ telemetry.register(httpCollector);
1421
1500
  let rootRunner = (fn) => fn();
1422
1501
  let resolveRoot;
1423
1502
  const rootTerminal = new Promise((resolve) => {
@@ -1494,6 +1573,8 @@ function createRequestScope(input) {
1494
1573
  trace,
1495
1574
  telemetry,
1496
1575
  resolverTelemetry: resolverCollector,
1576
+ nativeTelemetry: nativeCollector,
1577
+ httpTelemetry: httpCollector,
1497
1578
  seedCredentials(rawBody, kind) {
1498
1579
  const harvested = rawRequestCredentials(rawBody, kind);
1499
1580
  sensitiveRegistry.add(harvested.values);
@@ -1675,7 +1756,7 @@ function toJsonSuccessResponse(result, ctx) {
1675
1756
  return new Response(result);
1676
1757
  }
1677
1758
  const cacheMeta = ctx && "cache" in ctx ? ctx.cache.responseMeta() : undefined;
1678
- const retryMeta = ctx ? retryResponseMeta.get(ctx) : undefined;
1759
+ const retry = ctx ? retryResponseMeta.get(ctx)?.toTenantRetryPayload() : undefined;
1679
1760
  const cache = cacheMeta
1680
1761
  ? {
1681
1762
  hit: cacheMeta.hit,
@@ -1685,21 +1766,7 @@ function toJsonSuccessResponse(result, ctx) {
1685
1766
  ...(cacheMeta.source ? { source: closedEnum(cacheMeta.source) } : {}),
1686
1767
  }
1687
1768
  : undefined;
1688
- const retry = retryMeta
1689
- ? {
1690
- attempts: retryMeta.attempts,
1691
- retries: retryMeta.retries,
1692
- ...(retryMeta.preset ? { preset: closedEnum(retryMeta.preset) } : {}),
1693
- transport: closedEnum(retryMeta.transport),
1694
- ...(retryMeta.lastErrorCode
1695
- ? {
1696
- lastErrorCode: closedEnum(tenantRetryLastErrorCode(retryMeta.lastErrorCode)),
1697
- }
1698
- : {}),
1699
- ...(retryMeta.lastStatus ? { lastStatus: retryMeta.lastStatus } : {}),
1700
- }
1701
- : undefined;
1702
- const meta = cacheMeta || retryMeta
1769
+ const meta = cacheMeta || retry
1703
1770
  ? {
1704
1771
  ...(cache
1705
1772
  ? {
@@ -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
  };
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.60",
2
+ "version": "2.2.0-beta.62",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
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
@@ -14,6 +14,7 @@ import {
14
14
  OTEL_RESOURCE_ATTRIBUTES,
15
15
  OTEL_SERVICE_NAME,
16
16
  } from "./runtime/otlp.js";
17
+ import type { HandleContext } from "./handle.js";
17
18
  import type {
18
19
  AuthContext,
19
20
  BrowserClient,
@@ -23,7 +24,6 @@ import type {
23
24
  NativeContext,
24
25
  OcrContext,
25
26
  ProviderCache,
26
- ProviderChoiceContext,
27
27
  ProviderContext,
28
28
  ProviderDefinition,
29
29
  ProviderFilesContext,
@@ -118,8 +118,8 @@ function isEngineOwnedEngineEnvName(name: string): boolean {
118
118
  }
119
119
 
120
120
  /**
121
- * Hosted runtime settings the engine reads for OCR, STT, cache keying, choice
122
- * 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
123
123
  * and projections drop them, like the vendor credentials above.
124
124
  */
125
125
  export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
@@ -128,7 +128,6 @@ export const ENGINE_OWNED_RUNTIME_ENV_NAMES = [
128
128
  "APIFUSE__OCR__API_KEY",
129
129
  "APIFUSE__CLOUDFLARE__ACCOUNT_ID",
130
130
  "APIFUSE__CACHE__KEY_PEPPER",
131
- "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET",
132
131
  ] as const;
133
132
 
134
133
  const ENGINE_OWNED_RUNTIME_ENV_NAME_SET = new Set<string>(ENGINE_OWNED_RUNTIME_ENV_NAMES);
@@ -237,7 +236,7 @@ export const PROVIDER_CAPABILITY_KEYS = [
237
236
  "ocr",
238
237
  "stt",
239
238
  "resolver",
240
- "choice",
239
+ "handle",
241
240
  ] as const;
242
241
 
243
242
  export type ProviderCapabilityKey = (typeof PROVIDER_CAPABILITY_KEYS)[number];
@@ -249,7 +248,7 @@ export type ProviderEngineBindingCandidates = Partial<
249
248
  readonly credential: CredentialContext;
250
249
  readonly files: ProviderFilesContext;
251
250
  readonly auth: AuthContext;
252
- readonly choice: ProviderChoiceContext;
251
+ readonly handle: HandleContext;
253
252
  }
254
253
  > & {
255
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],
package/src/errors.ts CHANGED
@@ -12,6 +12,7 @@ const PROVIDER_ERROR_BRAND_VALUE = 1;
12
12
  const SESSION_EXPIRED_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/session-expired@1");
13
13
  const TRANSPORT_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/transport@1");
14
14
  const VALIDATION_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/validation@1");
15
+ const HANDLE_BRAND = Symbol.for("@apifuse/provider-sdk/error-kind/handle@1");
15
16
 
16
17
  // Defines a non-enumerable, non-writable, non-configurable own data property.
17
18
  // Immutable + own means a guard can trust it via a single descriptor read
@@ -208,6 +209,18 @@ export function isTransportError(value: unknown): value is TransportError {
208
209
  return isProviderError(value) && hasOwnBrand(value, TRANSPORT_BRAND, true);
209
210
  }
210
211
 
212
+ /** Brands a handle error (ADR-0012) so `isHandleError` works across duplicate SDK module copies. */
213
+ export function brandHandleError(target: object): void {
214
+ defineErrorBrand(target, HANDLE_BRAND, true);
215
+ }
216
+
217
+ export function hasHandleErrorBrand(value: unknown): boolean {
218
+ return (
219
+ hasOwnBrand(value, PROVIDER_ERROR_BRAND, PROVIDER_ERROR_BRAND_VALUE) &&
220
+ hasOwnBrand(value, HANDLE_BRAND, true)
221
+ );
222
+ }
223
+
211
224
  export function isValidationError(value: unknown): value is ValidationError {
212
225
  return (
213
226
  isProviderError(value) &&