@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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 +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +112 -42
- package/src/lint.ts +88 -152
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/server/serve.js
CHANGED
|
@@ -2,36 +2,72 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { AuthAbortError, createAuthFlowHelpers } from "../auth";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
5
|
+
import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
6
|
+
import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
|
|
7
|
+
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
8
|
+
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
9
|
+
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
10
|
+
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
11
|
+
import { createBrowserClient } from "../runtime/browser.js";
|
|
12
|
+
import { createProviderCache } from "../runtime/cache.js";
|
|
13
|
+
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
|
|
14
|
+
import { createCredentialContext } from "../runtime/credential.js";
|
|
15
|
+
import { createEnvContext } from "../runtime/env.js";
|
|
16
|
+
import { executeOperation } from "../runtime/executor.js";
|
|
17
|
+
import { createHttpClient } from "../runtime/http.js";
|
|
18
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
19
|
+
import { createEnvVendorCredentialResolver, createNativeNetworkClient, } from "../runtime/native-network.js";
|
|
20
|
+
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
21
|
+
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
22
|
+
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
23
|
+
import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
|
|
24
|
+
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
|
|
25
|
+
import { createStealthClient } from "../runtime/stealth.js";
|
|
26
|
+
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
27
|
+
import { createTraceContext } from "../runtime/trace.js";
|
|
28
|
+
import { parseSchema } from "../schema.js";
|
|
29
|
+
import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
|
|
30
|
+
import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
|
|
31
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
32
|
+
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
|
|
33
|
+
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
34
|
+
import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
|
|
35
|
+
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
36
|
+
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
31
37
|
const DEFAULT_HOST = "0.0.0.0";
|
|
32
38
|
const DEFAULT_PORT = 3000;
|
|
39
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
40
|
+
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
33
41
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
34
42
|
const retryResponseMeta = new WeakMap();
|
|
43
|
+
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
44
|
+
const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
|
|
45
|
+
const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
|
|
46
|
+
const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
|
|
47
|
+
const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
|
|
48
|
+
const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000);
|
|
49
|
+
export const ProviderServerStatefulForwardEnvelopeSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
requestId: z.string().min(1),
|
|
52
|
+
providerId: z.string().min(1),
|
|
53
|
+
operationId: z.string().min(1),
|
|
54
|
+
sessionKey: z.string().min(1),
|
|
55
|
+
connectionId: z.string().min(1),
|
|
56
|
+
serviceAccountId: z.string().min(1),
|
|
57
|
+
ownerPodId: z.string().min(1),
|
|
58
|
+
generation: z.number().int().positive(),
|
|
59
|
+
sourcePodId: z.string().min(1),
|
|
60
|
+
forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
61
|
+
deadlineAt: z
|
|
62
|
+
.string()
|
|
63
|
+
.refine((value) => Number.isFinite(Date.parse(value)))
|
|
64
|
+
.optional(),
|
|
65
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
66
|
+
operationRequest: OperationRequestSchema.extend({
|
|
67
|
+
connection: OperationConnectionSchema.strict().optional(),
|
|
68
|
+
}).strict(),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
35
71
|
function createAuthStub() {
|
|
36
72
|
return {
|
|
37
73
|
async requestField(name) {
|
|
@@ -93,9 +129,7 @@ function getProviderStealthBaseUrl(provider) {
|
|
|
93
129
|
return firstHost ? `https://${firstHost}` : undefined;
|
|
94
130
|
}
|
|
95
131
|
function getProviderStealthProfile(provider) {
|
|
96
|
-
return provider.stealth?.profile
|
|
97
|
-
? getStealthProfile(provider.stealth.profile)
|
|
98
|
-
: undefined;
|
|
132
|
+
return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
|
|
99
133
|
}
|
|
100
134
|
function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
101
135
|
if (provider.runtime !== "browser") {
|
|
@@ -104,13 +138,11 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
|
104
138
|
if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
|
|
105
139
|
return true;
|
|
106
140
|
}
|
|
107
|
-
return
|
|
141
|
+
return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
|
|
108
142
|
}
|
|
109
143
|
export function resolveProviderProxyAffinityKey(provider, request, operationId) {
|
|
110
144
|
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
111
|
-
const affinity = typeof provider.proxy === "object"
|
|
112
|
-
? provider.proxy.session?.affinity
|
|
113
|
-
: undefined;
|
|
145
|
+
const affinity = typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
|
|
114
146
|
if (affinity === "operation") {
|
|
115
147
|
return `${provider.id}/${operationId}`;
|
|
116
148
|
}
|
|
@@ -119,6 +151,15 @@ export function resolveProviderProxyAffinityKey(provider, request, operationId)
|
|
|
119
151
|
function resolveOperationConnectionId(request) {
|
|
120
152
|
return request.connection?.id ?? request.connectionId;
|
|
121
153
|
}
|
|
154
|
+
function resolveNativeProxyPolicy(provider) {
|
|
155
|
+
if (typeof provider.proxy === "object")
|
|
156
|
+
return provider.proxy;
|
|
157
|
+
if (provider.proxy === true)
|
|
158
|
+
return { mode: "optional" };
|
|
159
|
+
if (provider.proxy === false)
|
|
160
|
+
return { mode: "disabled" };
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
122
163
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
123
164
|
const baseUrl = getProviderBaseUrl(provider);
|
|
124
165
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -177,6 +218,18 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
177
218
|
engine: provider.browser?.engine,
|
|
178
219
|
})
|
|
179
220
|
: createBrowserStub(),
|
|
221
|
+
...(provider.native
|
|
222
|
+
? {
|
|
223
|
+
native: {
|
|
224
|
+
network: createNativeNetworkClient({
|
|
225
|
+
egress: provider.native.network,
|
|
226
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
227
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
228
|
+
credentials: createEnvVendorCredentialResolver(env),
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
180
233
|
trace: createTraceContext(),
|
|
181
234
|
auth: createAuthStub(),
|
|
182
235
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -243,6 +296,7 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
243
296
|
: undefined;
|
|
244
297
|
return {
|
|
245
298
|
context: {
|
|
299
|
+
flowId: request.flowId,
|
|
246
300
|
connectionId: request.connectionId,
|
|
247
301
|
externalRef: request.externalRef,
|
|
248
302
|
tenantId: request.tenantId ?? "",
|
|
@@ -253,7 +307,24 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
253
307
|
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
254
308
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
255
309
|
: createStealthStub(),
|
|
256
|
-
|
|
310
|
+
...(provider.native
|
|
311
|
+
? {
|
|
312
|
+
native: {
|
|
313
|
+
network: createNativeNetworkClient({
|
|
314
|
+
egress: provider.native.network,
|
|
315
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
316
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
317
|
+
credentials: createEnvVendorCredentialResolver(createEnvContext(provider.secrets?.map((secret) => secret.name))),
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
322
|
+
env: createEnvContext([
|
|
323
|
+
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
324
|
+
...(provider.auth?.mode === "oauth2_proxied"
|
|
325
|
+
? ["APIFUSE__AUTH_PROXY__URL"]
|
|
326
|
+
: []),
|
|
327
|
+
]),
|
|
257
328
|
credential,
|
|
258
329
|
context: flowContextStore.context,
|
|
259
330
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -292,16 +363,50 @@ function zodDetails(error) {
|
|
|
292
363
|
message: issue.message,
|
|
293
364
|
}));
|
|
294
365
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
366
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
367
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
368
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
369
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
370
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
371
|
+
// attribution even when the author left the category at the provider_error
|
|
372
|
+
// default.
|
|
373
|
+
function publicErrorSource(error, category) {
|
|
374
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
375
|
+
return "apifuse";
|
|
376
|
+
if (isProviderError(error)) {
|
|
377
|
+
if (error.code === MISSING_SECRET_CODE)
|
|
378
|
+
return "apifuse";
|
|
379
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
380
|
+
return "upstream_failure";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return sourceForCategory(category);
|
|
384
|
+
}
|
|
385
|
+
function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
386
|
+
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
387
|
+
const source = publicErrorSource(error, observability.category);
|
|
388
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
389
|
+
return {
|
|
390
|
+
error: {
|
|
391
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
392
|
+
message: "Stateful forwarding deadline expired.",
|
|
393
|
+
...(requestId ? { requestId } : {}),
|
|
394
|
+
retryable: observability.retryable,
|
|
395
|
+
source,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (isProviderError(error)) {
|
|
400
|
+
const details = error.details;
|
|
298
401
|
return {
|
|
299
402
|
error: {
|
|
300
403
|
code: error.code ?? "provider_error",
|
|
301
404
|
message: publicProviderErrorMessage(error),
|
|
302
405
|
...(requestId ? { requestId } : {}),
|
|
406
|
+
retryable: observability.retryable,
|
|
407
|
+
source,
|
|
303
408
|
...(error.fix ? { fix: error.fix } : {}),
|
|
304
|
-
...(details ? { details } : {}),
|
|
409
|
+
...(details !== undefined ? { details } : {}),
|
|
305
410
|
},
|
|
306
411
|
};
|
|
307
412
|
}
|
|
@@ -311,52 +416,67 @@ function toErrorResponse(error, requestId) {
|
|
|
311
416
|
code: "invalid_request",
|
|
312
417
|
message: "Invalid request body",
|
|
313
418
|
...(requestId ? { requestId } : {}),
|
|
419
|
+
retryable: observability.retryable,
|
|
420
|
+
source,
|
|
314
421
|
details: zodDetails(error),
|
|
315
422
|
},
|
|
316
423
|
};
|
|
317
424
|
}
|
|
425
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
426
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
427
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
428
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
429
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
430
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
318
431
|
return {
|
|
319
432
|
error: {
|
|
320
433
|
code: "internal_error",
|
|
321
434
|
message: "Internal error",
|
|
322
435
|
...(requestId ? { requestId } : {}),
|
|
436
|
+
retryable: observability.retryable,
|
|
437
|
+
source,
|
|
438
|
+
details: {
|
|
439
|
+
retryable: false,
|
|
440
|
+
category: "internal_error",
|
|
441
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
442
|
+
},
|
|
323
443
|
},
|
|
324
444
|
};
|
|
325
445
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (isPlainRecord(providerDetails) && isPlainRecord(observabilityDetails)) {
|
|
336
|
-
return { ...providerDetails, ...observabilityDetails };
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
provider: providerDetails,
|
|
340
|
-
observability: observabilityDetails,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function isPlainRecord(value) {
|
|
344
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
345
|
-
}
|
|
346
|
-
function providerObservabilityDetails(error) {
|
|
446
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
447
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
448
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
449
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
450
|
+
// from a duplicate SDK module instance.
|
|
451
|
+
function providerObservabilityDetails(error, declaredErrorCode) {
|
|
452
|
+
const declaredRetryable = sdkOwnsErrorResolution(error)
|
|
453
|
+
? undefined
|
|
454
|
+
: declaredErrorCode?.retryable;
|
|
347
455
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
348
456
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
349
457
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
350
458
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
351
459
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
352
|
-
if (error
|
|
460
|
+
if (isSessionExpiredError(error)) {
|
|
353
461
|
return {
|
|
354
462
|
category: error.options?.category ?? "credential_expired",
|
|
355
463
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
356
|
-
retryable: error.options?.retryable ?? false,
|
|
464
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
468
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
469
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
470
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
471
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
472
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
473
|
+
return {
|
|
474
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
475
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
476
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
357
477
|
};
|
|
358
478
|
}
|
|
359
|
-
if (!(error
|
|
479
|
+
if (!isTransportError(error)) {
|
|
360
480
|
return undefined;
|
|
361
481
|
}
|
|
362
482
|
const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
@@ -384,8 +504,69 @@ function providerObservabilityDetails(error) {
|
|
|
384
504
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
385
505
|
};
|
|
386
506
|
}
|
|
507
|
+
function errorObservabilityDetails(error, declaredErrorCode) {
|
|
508
|
+
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
509
|
+
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
510
|
+
if (providerDetails)
|
|
511
|
+
return providerDetails;
|
|
512
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
513
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
514
|
+
return {
|
|
515
|
+
category: isProviderError(error) && error.options?.category
|
|
516
|
+
? error.options.category
|
|
517
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
518
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
519
|
+
? "upstream_rejected"
|
|
520
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
521
|
+
? "provider_error"
|
|
522
|
+
: "input_validation",
|
|
523
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
524
|
+
retryable: isProviderError(error)
|
|
525
|
+
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
526
|
+
: false,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
530
|
+
return {
|
|
531
|
+
category: "timeout",
|
|
532
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
533
|
+
retryable: false,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (isProviderError(error)) {
|
|
537
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
538
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
539
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
540
|
+
// author set an explicit category.
|
|
541
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
542
|
+
const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
|
|
543
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
544
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
545
|
+
? "upstream_rejected"
|
|
546
|
+
: "provider_error";
|
|
547
|
+
return {
|
|
548
|
+
category: error.options?.category ?? rejectionDefault,
|
|
549
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
550
|
+
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
category: "internal_error",
|
|
555
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
556
|
+
retryable: false,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function responseWithErrorObservability(response, error, declaredErrorCode) {
|
|
560
|
+
const headers = new Headers(response.headers);
|
|
561
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
|
|
562
|
+
return new Response(response.body, {
|
|
563
|
+
status: response.status,
|
|
564
|
+
statusText: response.statusText,
|
|
565
|
+
headers,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
387
568
|
function publicProviderErrorMessage(error) {
|
|
388
|
-
if (error
|
|
569
|
+
if (isTransportError(error)) {
|
|
389
570
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
390
571
|
return error.message;
|
|
391
572
|
}
|
|
@@ -409,18 +590,30 @@ function publicProviderErrorMessage(error) {
|
|
|
409
590
|
}
|
|
410
591
|
return error.message;
|
|
411
592
|
}
|
|
412
|
-
function
|
|
593
|
+
function isEmittableErrorStatus(value) {
|
|
594
|
+
return (typeof value === "number" &&
|
|
595
|
+
VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
|
|
596
|
+
}
|
|
597
|
+
function toStatusCode(error, declaredErrorCode) {
|
|
413
598
|
if (error instanceof z.ZodError) {
|
|
414
599
|
return 400;
|
|
415
600
|
}
|
|
416
|
-
if (error instanceof
|
|
417
|
-
return
|
|
601
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
602
|
+
return 504;
|
|
418
603
|
}
|
|
419
|
-
if (error
|
|
604
|
+
if (isProviderError(error)) {
|
|
605
|
+
if (!sdkOwnsErrorResolution(error) &&
|
|
606
|
+
isEmittableErrorStatus(declaredErrorCode?.status)) {
|
|
607
|
+
return declaredErrorCode.status;
|
|
608
|
+
}
|
|
420
609
|
switch (error.code) {
|
|
421
610
|
case "AUTH_REQUIRED":
|
|
422
611
|
case "reauth_required":
|
|
423
612
|
return 401;
|
|
613
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
614
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
615
|
+
case MISSING_SECRET_CODE:
|
|
616
|
+
return 400;
|
|
424
617
|
case "NOT_FOUND":
|
|
425
618
|
case "not_found":
|
|
426
619
|
case "NO_DATA":
|
|
@@ -429,17 +622,55 @@ function toStatusCode(error) {
|
|
|
429
622
|
case "UPSTREAM_RATE_LIMIT":
|
|
430
623
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
431
624
|
return 429;
|
|
625
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
626
|
+
// contract): the upstream evaluated the request and said no under
|
|
627
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
628
|
+
case "UPSTREAM_REJECTED":
|
|
629
|
+
return 409;
|
|
432
630
|
case "UPSTREAM_ERROR":
|
|
433
631
|
case "BLOCKED":
|
|
434
632
|
return 502;
|
|
435
633
|
case "STT_UNAVAILABLE":
|
|
436
634
|
case "UNSUPPORTED_STT_BACKEND":
|
|
635
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
437
636
|
return 503;
|
|
438
637
|
}
|
|
439
|
-
|
|
638
|
+
if (isTransportError(error)) {
|
|
639
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
640
|
+
}
|
|
641
|
+
if (isValidationError(error)) {
|
|
642
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
643
|
+
}
|
|
644
|
+
return 500;
|
|
440
645
|
}
|
|
441
646
|
return 500;
|
|
442
647
|
}
|
|
648
|
+
function sdkOwnsErrorResolution(error) {
|
|
649
|
+
if (isSessionExpiredError(error))
|
|
650
|
+
return true;
|
|
651
|
+
if (isTransportError(error))
|
|
652
|
+
return true;
|
|
653
|
+
if (error instanceof z.ZodError)
|
|
654
|
+
return true;
|
|
655
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
656
|
+
return true;
|
|
657
|
+
return (isProviderError(error) &&
|
|
658
|
+
typeof error.code === "string" &&
|
|
659
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
|
|
660
|
+
}
|
|
661
|
+
function buildOperationErrorCodeLookup(provider) {
|
|
662
|
+
return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
663
|
+
const errorCodes = operation.docs?.errorCodes;
|
|
664
|
+
return errorCodes?.length
|
|
665
|
+
? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
|
|
666
|
+
: [];
|
|
667
|
+
}));
|
|
668
|
+
}
|
|
669
|
+
function declaredErrorCodeFor(error, operationId, lookup) {
|
|
670
|
+
if (!operationId || !isProviderError(error) || typeof error.code !== "string")
|
|
671
|
+
return undefined;
|
|
672
|
+
return lookup.get(operationId)?.get(error.code);
|
|
673
|
+
}
|
|
443
674
|
function extractRequestId(raw) {
|
|
444
675
|
if (!raw || typeof raw !== "object") {
|
|
445
676
|
return undefined;
|
|
@@ -447,17 +678,23 @@ function extractRequestId(raw) {
|
|
|
447
678
|
const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
|
|
448
679
|
return typeof value === "string" ? value : undefined;
|
|
449
680
|
}
|
|
450
|
-
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
451
|
-
const code = error
|
|
681
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
|
|
682
|
+
const code = isProviderError(error)
|
|
452
683
|
? (error.code ?? "provider_error")
|
|
453
684
|
: error instanceof z.ZodError
|
|
454
685
|
? "invalid_request"
|
|
455
|
-
:
|
|
686
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
687
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
688
|
+
: "internal_error";
|
|
456
689
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
457
690
|
const message = error instanceof Error ? error.message : String(error);
|
|
458
|
-
const details = error
|
|
459
|
-
|
|
460
|
-
|
|
691
|
+
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
692
|
+
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
693
|
+
isProviderError(error) &&
|
|
694
|
+
!isValidationError(error) &&
|
|
695
|
+
typeof error.code === "string" &&
|
|
696
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
|
|
697
|
+
declaredErrorCode === undefined;
|
|
461
698
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
462
699
|
emit({
|
|
463
700
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -471,14 +708,14 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
471
708
|
code,
|
|
472
709
|
errorClass,
|
|
473
710
|
message,
|
|
474
|
-
...(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
711
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
712
|
+
errorCategory: details.category,
|
|
713
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
714
|
+
retryable: details.retryable,
|
|
715
|
+
...(isUnregisteredProviderErrorCode
|
|
478
716
|
? {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
retryable: details.retryable,
|
|
717
|
+
signal: "unregistered_provider_error_code",
|
|
718
|
+
signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
482
719
|
}
|
|
483
720
|
: {}),
|
|
484
721
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
@@ -594,8 +831,7 @@ async function validateSseEvent(operation, event) {
|
|
|
594
831
|
const transport = getSseTransport(operation);
|
|
595
832
|
const schema = transport?.events?.[event.event];
|
|
596
833
|
if (!schema) {
|
|
597
|
-
if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
598
|
-
event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
834
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
599
835
|
return event;
|
|
600
836
|
}
|
|
601
837
|
throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
|
|
@@ -714,8 +950,7 @@ function enforceStreamChunkLimit(body, maxChunkBytes) {
|
|
|
714
950
|
}
|
|
715
951
|
function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
716
952
|
const transport = operation.transport?.kind ?? "json";
|
|
717
|
-
if (transport === "sse" &&
|
|
718
|
-
(result instanceof Response || result instanceof ReadableStream)) {
|
|
953
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
719
954
|
void cleanup();
|
|
720
955
|
throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
|
|
721
956
|
code: "SSE_RESULT_UNSUPPORTED",
|
|
@@ -726,9 +961,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
726
961
|
}
|
|
727
962
|
if (result instanceof Response) {
|
|
728
963
|
const httpTransport = getHttpStreamTransport(operation);
|
|
729
|
-
if (httpTransport &&
|
|
730
|
-
result.body &&
|
|
731
|
-
httpTransport?.maxChunkBytes !== undefined) {
|
|
964
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
732
965
|
return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
733
966
|
headers: result.headers,
|
|
734
967
|
status: result.status,
|
|
@@ -747,8 +980,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
747
980
|
? { "Content-Type": "text/event-stream; charset=utf-8" }
|
|
748
981
|
: {
|
|
749
982
|
"Content-Type": operation.transport?.kind === "http-stream"
|
|
750
|
-
? (operation.transport.contentType ??
|
|
751
|
-
"application/octet-stream")
|
|
983
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
752
984
|
: "application/octet-stream",
|
|
753
985
|
},
|
|
754
986
|
}), cleanup);
|
|
@@ -766,9 +998,7 @@ function getSseTransport(operation) {
|
|
|
766
998
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
767
999
|
}
|
|
768
1000
|
function getHttpStreamTransport(operation) {
|
|
769
|
-
return operation.transport?.kind === "http-stream"
|
|
770
|
-
? operation.transport
|
|
771
|
-
: undefined;
|
|
1001
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
772
1002
|
}
|
|
773
1003
|
function toAuthFlowResponse(result, contextPatch) {
|
|
774
1004
|
if (result instanceof Response) {
|
|
@@ -796,7 +1026,7 @@ function isAuthFlowLocale(value) {
|
|
|
796
1026
|
return value === "en" || value === "ko" || value === "ja";
|
|
797
1027
|
}
|
|
798
1028
|
function isAuthTurn(value) {
|
|
799
|
-
return
|
|
1029
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
800
1030
|
}
|
|
801
1031
|
function loadAuthFlowLocaleCatalogs(provider) {
|
|
802
1032
|
for (const providerDir of [
|
|
@@ -859,7 +1089,14 @@ async function handleOperation(provider, request, operationId, options = {}, sta
|
|
|
859
1089
|
}
|
|
860
1090
|
};
|
|
861
1091
|
try {
|
|
862
|
-
const result =
|
|
1092
|
+
const result = options.operationExecutor
|
|
1093
|
+
? await options.operationExecutor({
|
|
1094
|
+
provider,
|
|
1095
|
+
operationId,
|
|
1096
|
+
ctx,
|
|
1097
|
+
request,
|
|
1098
|
+
})
|
|
1099
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
863
1100
|
if (streaming && operation) {
|
|
864
1101
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
865
1102
|
}
|
|
@@ -893,8 +1130,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
893
1130
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
894
1131
|
});
|
|
895
1132
|
}
|
|
1133
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
1134
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
1135
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1136
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1137
|
+
// stranded flow even when provisioning is broken.
|
|
896
1138
|
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
897
1139
|
try {
|
|
1140
|
+
if (route !== "abort") {
|
|
1141
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
1142
|
+
}
|
|
898
1143
|
const result = route === "start"
|
|
899
1144
|
? await flow.start(context)
|
|
900
1145
|
: route === "continue"
|
|
@@ -933,25 +1178,268 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
933
1178
|
context.stealth.close?.();
|
|
934
1179
|
}
|
|
935
1180
|
}
|
|
1181
|
+
class StatefulForwardingReplayCache {
|
|
1182
|
+
maxEntries;
|
|
1183
|
+
#nonces = new Map();
|
|
1184
|
+
#expiryBuckets = new Map();
|
|
1185
|
+
#nextExpiryBucket;
|
|
1186
|
+
#latestExpiryBucket;
|
|
1187
|
+
constructor(maxEntries) {
|
|
1188
|
+
this.maxEntries = maxEntries;
|
|
1189
|
+
}
|
|
1190
|
+
claim(nonce, expiresAtMs, nowMs) {
|
|
1191
|
+
this.dropExpiredBuckets(nowMs);
|
|
1192
|
+
if (this.#nonces.has(nonce))
|
|
1193
|
+
return "replayed";
|
|
1194
|
+
if (this.#nonces.size >= this.maxEntries)
|
|
1195
|
+
return "full";
|
|
1196
|
+
const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1197
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1198
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1199
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
|
|
1200
|
+
bucket.add(nonce);
|
|
1201
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1202
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1203
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1204
|
+
return "accepted";
|
|
1205
|
+
}
|
|
1206
|
+
dropExpiredBuckets(nowMs) {
|
|
1207
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
|
|
1208
|
+
return;
|
|
1209
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1210
|
+
this.#nonces.clear();
|
|
1211
|
+
this.#expiryBuckets.clear();
|
|
1212
|
+
this.#nextExpiryBucket = undefined;
|
|
1213
|
+
this.#latestExpiryBucket = undefined;
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1217
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1218
|
+
if (bucket) {
|
|
1219
|
+
for (const cachedNonce of bucket)
|
|
1220
|
+
this.#nonces.delete(cachedNonce);
|
|
1221
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1222
|
+
}
|
|
1223
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function verifyStatefulForwardingRequest(input) {
|
|
1228
|
+
const config = input.options.statefulForwarding;
|
|
1229
|
+
if (!config?.secret) {
|
|
1230
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1231
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1235
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1236
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1237
|
+
if (!timestamp || !signature || !nonce) {
|
|
1238
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1239
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
if (nonce.length > 256) {
|
|
1243
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1244
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const timestampMs = Date.parse(timestamp);
|
|
1248
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1249
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1250
|
+
throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
|
|
1251
|
+
}
|
|
1252
|
+
if (!verifyStatefulRequestSignature({
|
|
1253
|
+
secret: config.secret,
|
|
1254
|
+
timestamp,
|
|
1255
|
+
rawBody: input.rawBody,
|
|
1256
|
+
method: input.method,
|
|
1257
|
+
path: input.path,
|
|
1258
|
+
nonce,
|
|
1259
|
+
signature,
|
|
1260
|
+
})) {
|
|
1261
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1262
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1266
|
+
if (replayResult === "replayed") {
|
|
1267
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1268
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
if (replayResult === "full") {
|
|
1272
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1273
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function operationRequestFromForwardingEnvelope(envelope) {
|
|
1278
|
+
return {
|
|
1279
|
+
...envelope.operationRequest,
|
|
1280
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function parseStatefulForwardingEnvelope(rawBody) {
|
|
1284
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1285
|
+
if (parsed.success)
|
|
1286
|
+
return parsed.data;
|
|
1287
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1288
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1289
|
+
details: zodDetails(parsed.error),
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
936
1292
|
export function createServerApp(provider, options = {}) {
|
|
1293
|
+
validateStatefulServerConfig(options);
|
|
937
1294
|
const app = new Hono();
|
|
938
1295
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1296
|
+
const operationErrorCodes = buildOperationErrorCodeLookup(provider);
|
|
1297
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1298
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
|
|
939
1299
|
const state = options.state ??
|
|
940
1300
|
createProviderRuntimeStateFromEnv({
|
|
941
1301
|
providerId: provider.id,
|
|
942
1302
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
943
1303
|
});
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1304
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1305
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1306
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1307
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1308
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1309
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
|
|
1310
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1311
|
+
logger({
|
|
1312
|
+
level: "warn",
|
|
1313
|
+
event: "provider_secrets_missing",
|
|
1314
|
+
providerId: provider.id,
|
|
1315
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
app.notFound((c) => {
|
|
1319
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1320
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1321
|
+
});
|
|
950
1322
|
app.get("/health", (c) => c.json({
|
|
951
1323
|
status: "ok",
|
|
952
1324
|
provider: provider.id,
|
|
953
1325
|
version: provider.version,
|
|
954
1326
|
}));
|
|
1327
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1328
|
+
let rawBodyText = "";
|
|
1329
|
+
let rawBody;
|
|
1330
|
+
let operationId;
|
|
1331
|
+
const operation = "stateful-internal";
|
|
1332
|
+
const requestCost = startRequestCost();
|
|
1333
|
+
try {
|
|
1334
|
+
if (!options.internalOperationExecutor) {
|
|
1335
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1336
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1340
|
+
verifyStatefulForwardingRequest({
|
|
1341
|
+
options,
|
|
1342
|
+
rawBody: rawBodyText,
|
|
1343
|
+
headers: c.req.raw.headers,
|
|
1344
|
+
method: c.req.raw.method,
|
|
1345
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1346
|
+
replayCache: statefulForwardingReplayCache,
|
|
1347
|
+
});
|
|
1348
|
+
try {
|
|
1349
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1353
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1357
|
+
if (envelope.providerId !== provider.id) {
|
|
1358
|
+
throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
|
|
1359
|
+
}
|
|
1360
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1361
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1362
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
if (envelope.sourcePodId !==
|
|
1366
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
|
|
1367
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1368
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
|
|
1372
|
+
throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
|
|
1373
|
+
}
|
|
1374
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1375
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1376
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1377
|
+
}
|
|
1378
|
+
const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1379
|
+
const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1380
|
+
const signal = deadlineSignal
|
|
1381
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1382
|
+
: c.req.raw.signal;
|
|
1383
|
+
const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
|
|
1384
|
+
providerId: envelope.providerId,
|
|
1385
|
+
sessionKey: envelope.sessionKey,
|
|
1386
|
+
ownerPodId: envelope.ownerPodId,
|
|
1387
|
+
generation: envelope.generation,
|
|
1388
|
+
sourcePodId: envelope.sourcePodId,
|
|
1389
|
+
forwardedAt: envelope.forwardedAt,
|
|
1390
|
+
requestId: envelope.requestId,
|
|
1391
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1392
|
+
}, signal));
|
|
1393
|
+
let ownerFenceValid;
|
|
1394
|
+
try {
|
|
1395
|
+
ownerFenceValid = deadlineSignal
|
|
1396
|
+
? await Promise.race([
|
|
1397
|
+
ownerFenceValidation,
|
|
1398
|
+
new Promise((_resolve, reject) => {
|
|
1399
|
+
deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
|
|
1400
|
+
}),
|
|
1401
|
+
])
|
|
1402
|
+
: await ownerFenceValidation;
|
|
1403
|
+
}
|
|
1404
|
+
catch (error) {
|
|
1405
|
+
if (deadlineSignal?.aborted) {
|
|
1406
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1407
|
+
}
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
if (ownerFenceValid !== true) {
|
|
1411
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1412
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1416
|
+
operationId = envelope.operationId;
|
|
1417
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1418
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1419
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1420
|
+
}
|
|
1421
|
+
const output = await options.internalOperationExecutor({
|
|
1422
|
+
provider,
|
|
1423
|
+
operationId,
|
|
1424
|
+
ctx,
|
|
1425
|
+
request,
|
|
1426
|
+
internalStatefulForward: envelope,
|
|
1427
|
+
signal,
|
|
1428
|
+
});
|
|
1429
|
+
logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
|
|
1430
|
+
return c.json({ data: output });
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1434
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1435
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1436
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1437
|
+
}
|
|
1438
|
+
const requestId = extractRequestId(rawBody);
|
|
1439
|
+
logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
1440
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
955
1443
|
app.post("/v1/:operation", async (c) => {
|
|
956
1444
|
let rawBody;
|
|
957
1445
|
const operation = c.req.param("operation");
|
|
@@ -977,13 +1465,14 @@ export function createServerApp(provider, options = {}) {
|
|
|
977
1465
|
return c.json(response);
|
|
978
1466
|
}
|
|
979
1467
|
catch (error) {
|
|
980
|
-
const
|
|
1468
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
1469
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
981
1470
|
const requestId = extractRequestId(rawBody);
|
|
982
|
-
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1471
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
983
1472
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
984
1473
|
if (telemetryHeader)
|
|
985
1474
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
986
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1475
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
987
1476
|
}
|
|
988
1477
|
});
|
|
989
1478
|
app.post("/auth/start", async (c) => {
|
|
@@ -1003,7 +1492,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1003
1492
|
const status = toStatusCode(error);
|
|
1004
1493
|
const requestId = extractRequestId(rawBody);
|
|
1005
1494
|
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
|
|
1006
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1495
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1007
1496
|
}
|
|
1008
1497
|
});
|
|
1009
1498
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1023,7 +1512,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1023
1512
|
const status = toStatusCode(error);
|
|
1024
1513
|
const requestId = extractRequestId(rawBody);
|
|
1025
1514
|
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
|
|
1026
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1515
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1027
1516
|
}
|
|
1028
1517
|
});
|
|
1029
1518
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1043,7 +1532,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1043
1532
|
const status = toStatusCode(error);
|
|
1044
1533
|
const requestId = extractRequestId(rawBody);
|
|
1045
1534
|
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
|
|
1046
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1535
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1047
1536
|
}
|
|
1048
1537
|
});
|
|
1049
1538
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1063,7 +1552,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1063
1552
|
const status = toStatusCode(error);
|
|
1064
1553
|
const requestId = extractRequestId(rawBody);
|
|
1065
1554
|
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
|
|
1066
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1555
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1067
1556
|
}
|
|
1068
1557
|
});
|
|
1069
1558
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1083,11 +1572,33 @@ export function createServerApp(provider, options = {}) {
|
|
|
1083
1572
|
const status = toStatusCode(error);
|
|
1084
1573
|
const requestId = extractRequestId(rawBody);
|
|
1085
1574
|
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
|
|
1086
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1575
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1087
1576
|
}
|
|
1088
1577
|
});
|
|
1089
1578
|
return app;
|
|
1090
1579
|
}
|
|
1580
|
+
function validateStatefulServerConfig(options) {
|
|
1581
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
1582
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
|
|
1583
|
+
}
|
|
1584
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
1585
|
+
throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
|
|
1586
|
+
}
|
|
1587
|
+
if (options.statefulForwarding &&
|
|
1588
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function") {
|
|
1589
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
|
|
1590
|
+
}
|
|
1591
|
+
if (options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
1592
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
1593
|
+
options.statefulForwarding.maxSkewMs <= 0)) {
|
|
1594
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
1595
|
+
}
|
|
1596
|
+
if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
1597
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
1598
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)) {
|
|
1599
|
+
throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1091
1602
|
function getBunServeRuntime() {
|
|
1092
1603
|
const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
|
|
1093
1604
|
if (!bunValue || typeof bunValue !== "object") {
|
|
@@ -1103,6 +1614,9 @@ function getBunServeRuntime() {
|
|
|
1103
1614
|
},
|
|
1104
1615
|
};
|
|
1105
1616
|
}
|
|
1617
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
1618
|
+
const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
1619
|
+
const processSignalCoordinators = new Map();
|
|
1106
1620
|
export async function serve(provider, options = {}) {
|
|
1107
1621
|
const bunRuntime = getBunServeRuntime();
|
|
1108
1622
|
if (bunRuntime === undefined) {
|
|
@@ -1110,29 +1624,186 @@ export async function serve(provider, options = {}) {
|
|
|
1110
1624
|
code: "RUNTIME_UNSUPPORTED",
|
|
1111
1625
|
});
|
|
1112
1626
|
}
|
|
1627
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1628
|
+
const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
|
|
1629
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1113
1630
|
const app = createServerApp(provider, {
|
|
1114
1631
|
logger: options.logger,
|
|
1115
1632
|
stt: options.stt,
|
|
1633
|
+
state: options.state,
|
|
1634
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
1635
|
+
operationExecutor: options.operationExecutor,
|
|
1636
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
1637
|
+
statefulForwarding: options.statefulForwarding,
|
|
1116
1638
|
});
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
});
|
|
1122
|
-
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1123
|
-
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1124
|
-
// starts when the shared self-test master secret env is present.
|
|
1125
|
-
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1126
|
-
if (selfTestSecrets) {
|
|
1127
|
-
const selfTestApp = createSelfTestApp(provider, {
|
|
1128
|
-
secrets: selfTestSecrets,
|
|
1129
|
-
invoke: createSelfTestInvoke(app),
|
|
1130
|
-
});
|
|
1131
|
-
bunRuntime.serve({
|
|
1132
|
-
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1639
|
+
const servers = [];
|
|
1640
|
+
try {
|
|
1641
|
+
servers.push(bunRuntime.serve({
|
|
1642
|
+
port: options.port ?? DEFAULT_PORT,
|
|
1133
1643
|
hostname: options.host ?? DEFAULT_HOST,
|
|
1134
|
-
fetch:
|
|
1135
|
-
});
|
|
1644
|
+
fetch: app.fetch,
|
|
1645
|
+
}));
|
|
1646
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1647
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1648
|
+
// starts when the shared self-test master secret env is present.
|
|
1649
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1650
|
+
if (selfTestSecrets) {
|
|
1651
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
1652
|
+
secrets: selfTestSecrets,
|
|
1653
|
+
invoke: createSelfTestInvoke(app),
|
|
1654
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
1655
|
+
});
|
|
1656
|
+
servers.push(bunRuntime.serve({
|
|
1657
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1658
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
1659
|
+
fetch: selfTestApp.fetch,
|
|
1660
|
+
}));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1665
|
+
throw error;
|
|
1666
|
+
}
|
|
1667
|
+
const server = servers[0];
|
|
1668
|
+
if (!server)
|
|
1669
|
+
throw new Error("Provider server failed to create its primary listener.");
|
|
1670
|
+
let closePromise;
|
|
1671
|
+
let unregisterSignals = () => { };
|
|
1672
|
+
const close = (closeOptions = {}) => {
|
|
1673
|
+
if (closePromise)
|
|
1674
|
+
return closePromise;
|
|
1675
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
1676
|
+
closePromise = closeProviderServers({
|
|
1677
|
+
servers,
|
|
1678
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
1679
|
+
timeoutMs,
|
|
1680
|
+
logger,
|
|
1681
|
+
providerId: provider.id,
|
|
1682
|
+
}).finally(() => unregisterSignals());
|
|
1683
|
+
return closePromise;
|
|
1684
|
+
};
|
|
1685
|
+
try {
|
|
1686
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
|
|
1687
|
+
}
|
|
1688
|
+
catch (error) {
|
|
1689
|
+
unregisterSignals();
|
|
1690
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1691
|
+
throw error;
|
|
1692
|
+
}
|
|
1693
|
+
return { port: server.port, close };
|
|
1694
|
+
}
|
|
1695
|
+
function registerForProcessSignals(signals, close) {
|
|
1696
|
+
if (signals.length === 0)
|
|
1697
|
+
return () => { };
|
|
1698
|
+
const registration = {
|
|
1699
|
+
signals: new Set(signals),
|
|
1700
|
+
close,
|
|
1701
|
+
};
|
|
1702
|
+
let registered = true;
|
|
1703
|
+
const unregister = () => {
|
|
1704
|
+
if (!registered)
|
|
1705
|
+
return;
|
|
1706
|
+
registered = false;
|
|
1707
|
+
for (const signal of registration.signals) {
|
|
1708
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
1709
|
+
if (!coordinator)
|
|
1710
|
+
continue;
|
|
1711
|
+
coordinator.registrations.delete(registration);
|
|
1712
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
1713
|
+
process.removeListener(signal, coordinator.listener);
|
|
1714
|
+
processSignalCoordinators.delete(signal);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
try {
|
|
1719
|
+
for (const signal of signals) {
|
|
1720
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
1721
|
+
if (!coordinator) {
|
|
1722
|
+
const created = {
|
|
1723
|
+
registrations: new Set(),
|
|
1724
|
+
handling: false,
|
|
1725
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
1726
|
+
};
|
|
1727
|
+
coordinator = created;
|
|
1728
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
1729
|
+
process.on(signal, coordinator.listener);
|
|
1730
|
+
}
|
|
1731
|
+
coordinator.registrations.add(registration);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
catch (error) {
|
|
1735
|
+
unregister();
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1738
|
+
return unregister;
|
|
1739
|
+
}
|
|
1740
|
+
function handleCoordinatedSignal(signal, coordinator) {
|
|
1741
|
+
if (coordinator.handling)
|
|
1742
|
+
return;
|
|
1743
|
+
coordinator.handling = true;
|
|
1744
|
+
const registrations = [...coordinator.registrations];
|
|
1745
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
1746
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
1747
|
+
process.removeListener(signal, coordinator.listener);
|
|
1748
|
+
processSignalCoordinators.delete(signal);
|
|
1749
|
+
}
|
|
1750
|
+
try {
|
|
1751
|
+
process.kill(process.pid, signal);
|
|
1752
|
+
}
|
|
1753
|
+
catch {
|
|
1754
|
+
process.exitCode = 1;
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
async function closeProviderServers(input) {
|
|
1759
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1760
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
1761
|
+
for (const gracefulStop of gracefulStops)
|
|
1762
|
+
gracefulStop.catch(() => undefined);
|
|
1763
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
1764
|
+
try {
|
|
1765
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
try {
|
|
1769
|
+
input.logger({
|
|
1770
|
+
level: "error",
|
|
1771
|
+
event: "provider_shutdown_hook_failed",
|
|
1772
|
+
providerId: input.providerId,
|
|
1773
|
+
hookIndex,
|
|
1774
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
1775
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
catch { }
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
1782
|
+
await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
|
|
1783
|
+
}
|
|
1784
|
+
async function withinShutdownBudget(promise, deadline) {
|
|
1785
|
+
promise.catch(() => undefined);
|
|
1786
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
1787
|
+
let timer;
|
|
1788
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1789
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1790
|
+
});
|
|
1791
|
+
try {
|
|
1792
|
+
return await Promise.race([promise, timeout]);
|
|
1793
|
+
}
|
|
1794
|
+
finally {
|
|
1795
|
+
if (timer)
|
|
1796
|
+
clearTimeout(timer);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function resolveShutdownSignals(signals) {
|
|
1800
|
+
if (signals === false)
|
|
1801
|
+
return [];
|
|
1802
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
1803
|
+
}
|
|
1804
|
+
function shutdownTimeout(value) {
|
|
1805
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1806
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1136
1807
|
}
|
|
1137
|
-
|
|
1808
|
+
return value;
|
|
1138
1809
|
}
|