@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.
- package/AUTHORING.md +17 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +5 -7
- package/bin/apifuse-pack-types.ts +26 -0
- package/bin/apifuse-perf.ts +4 -6
- package/bin/apifuse-record.ts +4 -7
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +3 -4
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +55 -0
- package/dist/handle-meta.js +57 -0
- package/dist/handle.d.ts +208 -0
- package/dist/handle.js +299 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +372 -47
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +4 -2
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +900 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +0 -3
- package/dist/server/serve-implementation.d.ts +6 -11
- package/dist/server/serve-implementation.js +119 -52
- package/dist/testing/run.js +10 -7
- package/dist/types.d.ts +4 -107
- package/package.json +1 -1
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +5 -6
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +93 -0
- package/src/handle.ts +612 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +45 -13
- package/src/lint.ts +437 -55
- package/src/provider.ts +19 -20
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1172 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/state.ts +44 -16
- package/src/server/index.ts +30 -0
- package/src/server/sensitive-values.ts +0 -3
- package/src/server/serve-implementation.ts +127 -89
- package/src/testing/run.ts +11 -7
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
592
|
+
event: "provider_handle",
|
|
608
593
|
...event,
|
|
609
594
|
}),
|
|
610
595
|
}),
|
|
611
596
|
};
|
|
612
|
-
const
|
|
613
|
-
|
|
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
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
:
|
|
990
|
+
: errorCode === PROXY_AUTH_IP_DENIED_CODE
|
|
922
991
|
? "anti_bot_blocked"
|
|
923
|
-
:
|
|
992
|
+
: errorCode === "transport_timeout" ||
|
|
993
|
+
errorCode === "native_connection_timeout" ||
|
|
994
|
+
errorCode === "native_connection_idle_timeout"
|
|
924
995
|
? "timeout"
|
|
925
|
-
:
|
|
996
|
+
: errorCode === "transport_network_error" ||
|
|
997
|
+
errorCode === "native_connection_failed" ||
|
|
998
|
+
errorCode === "native_connection_closed"
|
|
926
999
|
? "network"
|
|
927
|
-
:
|
|
928
|
-
?
|
|
929
|
-
:
|
|
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
|
|
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
|
|
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
|
? {
|
package/dist/testing/run.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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>) & ("
|
|
2001
|
-
|
|
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
|
-
|
|
2059
|
+
handle?: readonly HandleKind[];
|
|
2163
2060
|
reviewed?: ProviderReviewed;
|
|
2164
2061
|
access?: ProviderAccessConfig;
|
|
2165
2062
|
secrets?: ProviderSecretDeclaration[];
|
package/package.json
CHANGED
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
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
122
|
-
*
|
|
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
|
-
"
|
|
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
|
|
251
|
+
readonly handle: HandleContext;
|
|
253
252
|
}
|
|
254
253
|
> & {
|
|
255
254
|
readonly request?: ProviderRequestContext;
|
package/src/error-resolution.ts
CHANGED
|
@@ -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
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
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) &&
|