@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.20
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 +85 -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 +1 -1
- package/dist/ceremonies/index.js +8 -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 +208 -125
- 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.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 +1 -1
- package/dist/runtime/auth-flow.js +3 -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 +15 -4
- 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 +115 -4
- package/dist/server/serve.js +792 -127
- package/dist/server/types.d.ts +30 -9
- package/dist/server/types.js +8 -7
- 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 +477 -33
- package/dist/types.d.ts +323 -11
- 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 +24 -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 +368 -504
- 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 +111 -42
- package/src/lint.ts +89 -159
- 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 +65 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -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 +41 -110
- 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 +1141 -324
- package/src/server/types.ts +12 -19
- 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 +652 -79
- package/src/types.ts +369 -11
- 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),
|
|
@@ -253,6 +306,18 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
253
306
|
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
254
307
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
255
308
|
: createStealthStub(),
|
|
309
|
+
...(provider.native
|
|
310
|
+
? {
|
|
311
|
+
native: {
|
|
312
|
+
network: createNativeNetworkClient({
|
|
313
|
+
egress: provider.native.network,
|
|
314
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
315
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
316
|
+
credentials: createEnvVendorCredentialResolver(createEnvContext(provider.secrets?.map((secret) => secret.name))),
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
256
321
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
257
322
|
credential,
|
|
258
323
|
context: flowContextStore.context,
|
|
@@ -292,16 +357,50 @@ function zodDetails(error) {
|
|
|
292
357
|
message: issue.message,
|
|
293
358
|
}));
|
|
294
359
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
360
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
361
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
362
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
363
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
364
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
365
|
+
// attribution even when the author left the category at the provider_error
|
|
366
|
+
// default.
|
|
367
|
+
function publicErrorSource(error, category) {
|
|
368
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
369
|
+
return "apifuse";
|
|
370
|
+
if (isProviderError(error)) {
|
|
371
|
+
if (error.code === MISSING_SECRET_CODE)
|
|
372
|
+
return "apifuse";
|
|
373
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
374
|
+
return "upstream_failure";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return sourceForCategory(category);
|
|
378
|
+
}
|
|
379
|
+
function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
380
|
+
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
381
|
+
const source = publicErrorSource(error, observability.category);
|
|
382
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
383
|
+
return {
|
|
384
|
+
error: {
|
|
385
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
386
|
+
message: "Stateful forwarding deadline expired.",
|
|
387
|
+
...(requestId ? { requestId } : {}),
|
|
388
|
+
retryable: observability.retryable,
|
|
389
|
+
source,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (isProviderError(error)) {
|
|
394
|
+
const details = error.details;
|
|
298
395
|
return {
|
|
299
396
|
error: {
|
|
300
397
|
code: error.code ?? "provider_error",
|
|
301
398
|
message: publicProviderErrorMessage(error),
|
|
302
399
|
...(requestId ? { requestId } : {}),
|
|
400
|
+
retryable: observability.retryable,
|
|
401
|
+
source,
|
|
303
402
|
...(error.fix ? { fix: error.fix } : {}),
|
|
304
|
-
...(details ? { details } : {}),
|
|
403
|
+
...(details !== undefined ? { details } : {}),
|
|
305
404
|
},
|
|
306
405
|
};
|
|
307
406
|
}
|
|
@@ -311,52 +410,67 @@ function toErrorResponse(error, requestId) {
|
|
|
311
410
|
code: "invalid_request",
|
|
312
411
|
message: "Invalid request body",
|
|
313
412
|
...(requestId ? { requestId } : {}),
|
|
413
|
+
retryable: observability.retryable,
|
|
414
|
+
source,
|
|
314
415
|
details: zodDetails(error),
|
|
315
416
|
},
|
|
316
417
|
};
|
|
317
418
|
}
|
|
419
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
420
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
421
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
422
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
423
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
424
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
318
425
|
return {
|
|
319
426
|
error: {
|
|
320
427
|
code: "internal_error",
|
|
321
428
|
message: "Internal error",
|
|
322
429
|
...(requestId ? { requestId } : {}),
|
|
430
|
+
retryable: observability.retryable,
|
|
431
|
+
source,
|
|
432
|
+
details: {
|
|
433
|
+
retryable: false,
|
|
434
|
+
category: "internal_error",
|
|
435
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
436
|
+
},
|
|
323
437
|
},
|
|
324
438
|
};
|
|
325
439
|
}
|
|
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) {
|
|
440
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
441
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
442
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
443
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
444
|
+
// from a duplicate SDK module instance.
|
|
445
|
+
function providerObservabilityDetails(error, declaredErrorCode) {
|
|
446
|
+
const declaredRetryable = sdkOwnsErrorResolution(error)
|
|
447
|
+
? undefined
|
|
448
|
+
: declaredErrorCode?.retryable;
|
|
347
449
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
348
450
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
349
451
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
350
452
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
351
453
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
352
|
-
if (error
|
|
454
|
+
if (isSessionExpiredError(error)) {
|
|
353
455
|
return {
|
|
354
456
|
category: error.options?.category ?? "credential_expired",
|
|
355
457
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
356
|
-
retryable: error.options?.retryable ?? false,
|
|
458
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
462
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
463
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
464
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
465
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
466
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
467
|
+
return {
|
|
468
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
469
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
470
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
357
471
|
};
|
|
358
472
|
}
|
|
359
|
-
if (!(error
|
|
473
|
+
if (!isTransportError(error)) {
|
|
360
474
|
return undefined;
|
|
361
475
|
}
|
|
362
476
|
const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
@@ -384,8 +498,69 @@ function providerObservabilityDetails(error) {
|
|
|
384
498
|
...(error.upstreamStatus ? { upstreamStatus: error.upstreamStatus } : {}),
|
|
385
499
|
};
|
|
386
500
|
}
|
|
501
|
+
function errorObservabilityDetails(error, declaredErrorCode) {
|
|
502
|
+
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
503
|
+
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
504
|
+
if (providerDetails)
|
|
505
|
+
return providerDetails;
|
|
506
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
507
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
508
|
+
return {
|
|
509
|
+
category: isProviderError(error) && error.options?.category
|
|
510
|
+
? error.options.category
|
|
511
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
512
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
513
|
+
? "upstream_rejected"
|
|
514
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
515
|
+
? "provider_error"
|
|
516
|
+
: "input_validation",
|
|
517
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
518
|
+
retryable: isProviderError(error)
|
|
519
|
+
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
520
|
+
: false,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
524
|
+
return {
|
|
525
|
+
category: "timeout",
|
|
526
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
527
|
+
retryable: false,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (isProviderError(error)) {
|
|
531
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
532
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
533
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
534
|
+
// author set an explicit category.
|
|
535
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
536
|
+
const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
|
|
537
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
538
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
539
|
+
? "upstream_rejected"
|
|
540
|
+
: "provider_error";
|
|
541
|
+
return {
|
|
542
|
+
category: error.options?.category ?? rejectionDefault,
|
|
543
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
544
|
+
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
category: "internal_error",
|
|
549
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
550
|
+
retryable: false,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function responseWithErrorObservability(response, error, declaredErrorCode) {
|
|
554
|
+
const headers = new Headers(response.headers);
|
|
555
|
+
headers.set(ERROR_OBSERVABILITY_HEADER, JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)));
|
|
556
|
+
return new Response(response.body, {
|
|
557
|
+
status: response.status,
|
|
558
|
+
statusText: response.statusText,
|
|
559
|
+
headers,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
387
562
|
function publicProviderErrorMessage(error) {
|
|
388
|
-
if (error
|
|
563
|
+
if (isTransportError(error)) {
|
|
389
564
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
390
565
|
return error.message;
|
|
391
566
|
}
|
|
@@ -409,18 +584,30 @@ function publicProviderErrorMessage(error) {
|
|
|
409
584
|
}
|
|
410
585
|
return error.message;
|
|
411
586
|
}
|
|
412
|
-
function
|
|
587
|
+
function isEmittableErrorStatus(value) {
|
|
588
|
+
return (typeof value === "number" &&
|
|
589
|
+
VALID_OPERATION_ERROR_STATUSES.some((status) => status === value));
|
|
590
|
+
}
|
|
591
|
+
function toStatusCode(error, declaredErrorCode) {
|
|
413
592
|
if (error instanceof z.ZodError) {
|
|
414
593
|
return 400;
|
|
415
594
|
}
|
|
416
|
-
if (error instanceof
|
|
417
|
-
return
|
|
595
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
596
|
+
return 504;
|
|
418
597
|
}
|
|
419
|
-
if (error
|
|
598
|
+
if (isProviderError(error)) {
|
|
599
|
+
if (!sdkOwnsErrorResolution(error) &&
|
|
600
|
+
isEmittableErrorStatus(declaredErrorCode?.status)) {
|
|
601
|
+
return declaredErrorCode.status;
|
|
602
|
+
}
|
|
420
603
|
switch (error.code) {
|
|
421
604
|
case "AUTH_REQUIRED":
|
|
422
605
|
case "reauth_required":
|
|
423
606
|
return 401;
|
|
607
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
608
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
609
|
+
case MISSING_SECRET_CODE:
|
|
610
|
+
return 400;
|
|
424
611
|
case "NOT_FOUND":
|
|
425
612
|
case "not_found":
|
|
426
613
|
case "NO_DATA":
|
|
@@ -429,17 +616,55 @@ function toStatusCode(error) {
|
|
|
429
616
|
case "UPSTREAM_RATE_LIMIT":
|
|
430
617
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
431
618
|
return 429;
|
|
619
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
620
|
+
// contract): the upstream evaluated the request and said no under
|
|
621
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
622
|
+
case "UPSTREAM_REJECTED":
|
|
623
|
+
return 409;
|
|
432
624
|
case "UPSTREAM_ERROR":
|
|
433
625
|
case "BLOCKED":
|
|
434
626
|
return 502;
|
|
435
627
|
case "STT_UNAVAILABLE":
|
|
436
628
|
case "UNSUPPORTED_STT_BACKEND":
|
|
629
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
437
630
|
return 503;
|
|
438
631
|
}
|
|
439
|
-
|
|
632
|
+
if (isTransportError(error)) {
|
|
633
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
634
|
+
}
|
|
635
|
+
if (isValidationError(error)) {
|
|
636
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
637
|
+
}
|
|
638
|
+
return 500;
|
|
440
639
|
}
|
|
441
640
|
return 500;
|
|
442
641
|
}
|
|
642
|
+
function sdkOwnsErrorResolution(error) {
|
|
643
|
+
if (isSessionExpiredError(error))
|
|
644
|
+
return true;
|
|
645
|
+
if (isTransportError(error))
|
|
646
|
+
return true;
|
|
647
|
+
if (error instanceof z.ZodError)
|
|
648
|
+
return true;
|
|
649
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
650
|
+
return true;
|
|
651
|
+
return (isProviderError(error) &&
|
|
652
|
+
typeof error.code === "string" &&
|
|
653
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code));
|
|
654
|
+
}
|
|
655
|
+
function buildOperationErrorCodeLookup(provider) {
|
|
656
|
+
return new Map(Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
657
|
+
const errorCodes = operation.docs?.errorCodes;
|
|
658
|
+
return errorCodes?.length
|
|
659
|
+
? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))]]
|
|
660
|
+
: [];
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
function declaredErrorCodeFor(error, operationId, lookup) {
|
|
664
|
+
if (!operationId || !isProviderError(error) || typeof error.code !== "string")
|
|
665
|
+
return undefined;
|
|
666
|
+
return lookup.get(operationId)?.get(error.code);
|
|
667
|
+
}
|
|
443
668
|
function extractRequestId(raw) {
|
|
444
669
|
if (!raw || typeof raw !== "object") {
|
|
445
670
|
return undefined;
|
|
@@ -447,17 +672,23 @@ function extractRequestId(raw) {
|
|
|
447
672
|
const value = Object.getOwnPropertyDescriptor(raw, "requestId")?.value;
|
|
448
673
|
return typeof value === "string" ? value : undefined;
|
|
449
674
|
}
|
|
450
|
-
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
451
|
-
const code = error
|
|
675
|
+
function logProviderError(logger, provider, kind, route, requestId, error, status, cost, declaredErrorCode) {
|
|
676
|
+
const code = isProviderError(error)
|
|
452
677
|
? (error.code ?? "provider_error")
|
|
453
678
|
: error instanceof z.ZodError
|
|
454
679
|
? "invalid_request"
|
|
455
|
-
:
|
|
680
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
681
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
682
|
+
: "internal_error";
|
|
456
683
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
457
684
|
const message = error instanceof Error ? error.message : String(error);
|
|
458
|
-
const details = error
|
|
459
|
-
|
|
460
|
-
|
|
685
|
+
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
686
|
+
const isUnregisteredProviderErrorCode = status === 500 &&
|
|
687
|
+
isProviderError(error) &&
|
|
688
|
+
!isValidationError(error) &&
|
|
689
|
+
typeof error.code === "string" &&
|
|
690
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
|
|
691
|
+
declaredErrorCode === undefined;
|
|
461
692
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
462
693
|
emit({
|
|
463
694
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -471,14 +702,14 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
471
702
|
code,
|
|
472
703
|
errorClass,
|
|
473
704
|
message,
|
|
474
|
-
...(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
705
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
706
|
+
errorCategory: details.category,
|
|
707
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
708
|
+
retryable: details.retryable,
|
|
709
|
+
...(isUnregisteredProviderErrorCode
|
|
478
710
|
? {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
retryable: details.retryable,
|
|
711
|
+
signal: "unregistered_provider_error_code",
|
|
712
|
+
signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
482
713
|
}
|
|
483
714
|
: {}),
|
|
484
715
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
@@ -594,8 +825,7 @@ async function validateSseEvent(operation, event) {
|
|
|
594
825
|
const transport = getSseTransport(operation);
|
|
595
826
|
const schema = transport?.events?.[event.event];
|
|
596
827
|
if (!schema) {
|
|
597
|
-
if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
598
|
-
event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
828
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
599
829
|
return event;
|
|
600
830
|
}
|
|
601
831
|
throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
|
|
@@ -714,8 +944,7 @@ function enforceStreamChunkLimit(body, maxChunkBytes) {
|
|
|
714
944
|
}
|
|
715
945
|
function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
716
946
|
const transport = operation.transport?.kind ?? "json";
|
|
717
|
-
if (transport === "sse" &&
|
|
718
|
-
(result instanceof Response || result instanceof ReadableStream)) {
|
|
947
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
719
948
|
void cleanup();
|
|
720
949
|
throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
|
|
721
950
|
code: "SSE_RESULT_UNSUPPORTED",
|
|
@@ -726,9 +955,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
726
955
|
}
|
|
727
956
|
if (result instanceof Response) {
|
|
728
957
|
const httpTransport = getHttpStreamTransport(operation);
|
|
729
|
-
if (httpTransport &&
|
|
730
|
-
result.body &&
|
|
731
|
-
httpTransport?.maxChunkBytes !== undefined) {
|
|
958
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
732
959
|
return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
733
960
|
headers: result.headers,
|
|
734
961
|
status: result.status,
|
|
@@ -747,8 +974,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
747
974
|
? { "Content-Type": "text/event-stream; charset=utf-8" }
|
|
748
975
|
: {
|
|
749
976
|
"Content-Type": operation.transport?.kind === "http-stream"
|
|
750
|
-
? (operation.transport.contentType ??
|
|
751
|
-
"application/octet-stream")
|
|
977
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
752
978
|
: "application/octet-stream",
|
|
753
979
|
},
|
|
754
980
|
}), cleanup);
|
|
@@ -766,9 +992,7 @@ function getSseTransport(operation) {
|
|
|
766
992
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
767
993
|
}
|
|
768
994
|
function getHttpStreamTransport(operation) {
|
|
769
|
-
return operation.transport?.kind === "http-stream"
|
|
770
|
-
? operation.transport
|
|
771
|
-
: undefined;
|
|
995
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
772
996
|
}
|
|
773
997
|
function toAuthFlowResponse(result, contextPatch) {
|
|
774
998
|
if (result instanceof Response) {
|
|
@@ -796,7 +1020,7 @@ function isAuthFlowLocale(value) {
|
|
|
796
1020
|
return value === "en" || value === "ko" || value === "ja";
|
|
797
1021
|
}
|
|
798
1022
|
function isAuthTurn(value) {
|
|
799
|
-
return
|
|
1023
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
800
1024
|
}
|
|
801
1025
|
function loadAuthFlowLocaleCatalogs(provider) {
|
|
802
1026
|
for (const providerDir of [
|
|
@@ -859,7 +1083,14 @@ async function handleOperation(provider, request, operationId, options = {}, sta
|
|
|
859
1083
|
}
|
|
860
1084
|
};
|
|
861
1085
|
try {
|
|
862
|
-
const result =
|
|
1086
|
+
const result = options.operationExecutor
|
|
1087
|
+
? await options.operationExecutor({
|
|
1088
|
+
provider,
|
|
1089
|
+
operationId,
|
|
1090
|
+
ctx,
|
|
1091
|
+
request,
|
|
1092
|
+
})
|
|
1093
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
863
1094
|
if (streaming && operation) {
|
|
864
1095
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
865
1096
|
}
|
|
@@ -893,8 +1124,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
893
1124
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
894
1125
|
});
|
|
895
1126
|
}
|
|
1127
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
1128
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
1129
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1130
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1131
|
+
// stranded flow even when provisioning is broken.
|
|
896
1132
|
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
897
1133
|
try {
|
|
1134
|
+
if (route !== "abort") {
|
|
1135
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
1136
|
+
}
|
|
898
1137
|
const result = route === "start"
|
|
899
1138
|
? await flow.start(context)
|
|
900
1139
|
: route === "continue"
|
|
@@ -933,25 +1172,268 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
933
1172
|
context.stealth.close?.();
|
|
934
1173
|
}
|
|
935
1174
|
}
|
|
1175
|
+
class StatefulForwardingReplayCache {
|
|
1176
|
+
maxEntries;
|
|
1177
|
+
#nonces = new Map();
|
|
1178
|
+
#expiryBuckets = new Map();
|
|
1179
|
+
#nextExpiryBucket;
|
|
1180
|
+
#latestExpiryBucket;
|
|
1181
|
+
constructor(maxEntries) {
|
|
1182
|
+
this.maxEntries = maxEntries;
|
|
1183
|
+
}
|
|
1184
|
+
claim(nonce, expiresAtMs, nowMs) {
|
|
1185
|
+
this.dropExpiredBuckets(nowMs);
|
|
1186
|
+
if (this.#nonces.has(nonce))
|
|
1187
|
+
return "replayed";
|
|
1188
|
+
if (this.#nonces.size >= this.maxEntries)
|
|
1189
|
+
return "full";
|
|
1190
|
+
const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1191
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1192
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1193
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
|
|
1194
|
+
bucket.add(nonce);
|
|
1195
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1196
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1197
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1198
|
+
return "accepted";
|
|
1199
|
+
}
|
|
1200
|
+
dropExpiredBuckets(nowMs) {
|
|
1201
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
|
|
1202
|
+
return;
|
|
1203
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1204
|
+
this.#nonces.clear();
|
|
1205
|
+
this.#expiryBuckets.clear();
|
|
1206
|
+
this.#nextExpiryBucket = undefined;
|
|
1207
|
+
this.#latestExpiryBucket = undefined;
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1211
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1212
|
+
if (bucket) {
|
|
1213
|
+
for (const cachedNonce of bucket)
|
|
1214
|
+
this.#nonces.delete(cachedNonce);
|
|
1215
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1216
|
+
}
|
|
1217
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function verifyStatefulForwardingRequest(input) {
|
|
1222
|
+
const config = input.options.statefulForwarding;
|
|
1223
|
+
if (!config?.secret) {
|
|
1224
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1225
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1229
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1230
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1231
|
+
if (!timestamp || !signature || !nonce) {
|
|
1232
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1233
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (nonce.length > 256) {
|
|
1237
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1238
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
const timestampMs = Date.parse(timestamp);
|
|
1242
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1243
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1244
|
+
throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
|
|
1245
|
+
}
|
|
1246
|
+
if (!verifyStatefulRequestSignature({
|
|
1247
|
+
secret: config.secret,
|
|
1248
|
+
timestamp,
|
|
1249
|
+
rawBody: input.rawBody,
|
|
1250
|
+
method: input.method,
|
|
1251
|
+
path: input.path,
|
|
1252
|
+
nonce,
|
|
1253
|
+
signature,
|
|
1254
|
+
})) {
|
|
1255
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1256
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1260
|
+
if (replayResult === "replayed") {
|
|
1261
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1262
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
if (replayResult === "full") {
|
|
1266
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1267
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function operationRequestFromForwardingEnvelope(envelope) {
|
|
1272
|
+
return {
|
|
1273
|
+
...envelope.operationRequest,
|
|
1274
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function parseStatefulForwardingEnvelope(rawBody) {
|
|
1278
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1279
|
+
if (parsed.success)
|
|
1280
|
+
return parsed.data;
|
|
1281
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1282
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1283
|
+
details: zodDetails(parsed.error),
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
936
1286
|
export function createServerApp(provider, options = {}) {
|
|
1287
|
+
validateStatefulServerConfig(options);
|
|
937
1288
|
const app = new Hono();
|
|
938
1289
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1290
|
+
const operationErrorCodes = buildOperationErrorCodeLookup(provider);
|
|
1291
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1292
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
|
|
939
1293
|
const state = options.state ??
|
|
940
1294
|
createProviderRuntimeStateFromEnv({
|
|
941
1295
|
providerId: provider.id,
|
|
942
1296
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
943
1297
|
});
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1298
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1299
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1300
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1301
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1302
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1303
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
|
|
1304
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1305
|
+
logger({
|
|
1306
|
+
level: "warn",
|
|
1307
|
+
event: "provider_secrets_missing",
|
|
1308
|
+
providerId: provider.id,
|
|
1309
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
app.notFound((c) => {
|
|
1313
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1314
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1315
|
+
});
|
|
950
1316
|
app.get("/health", (c) => c.json({
|
|
951
1317
|
status: "ok",
|
|
952
1318
|
provider: provider.id,
|
|
953
1319
|
version: provider.version,
|
|
954
1320
|
}));
|
|
1321
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1322
|
+
let rawBodyText = "";
|
|
1323
|
+
let rawBody;
|
|
1324
|
+
let operationId;
|
|
1325
|
+
const operation = "stateful-internal";
|
|
1326
|
+
const requestCost = startRequestCost();
|
|
1327
|
+
try {
|
|
1328
|
+
if (!options.internalOperationExecutor) {
|
|
1329
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1330
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1334
|
+
verifyStatefulForwardingRequest({
|
|
1335
|
+
options,
|
|
1336
|
+
rawBody: rawBodyText,
|
|
1337
|
+
headers: c.req.raw.headers,
|
|
1338
|
+
method: c.req.raw.method,
|
|
1339
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1340
|
+
replayCache: statefulForwardingReplayCache,
|
|
1341
|
+
});
|
|
1342
|
+
try {
|
|
1343
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1347
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1351
|
+
if (envelope.providerId !== provider.id) {
|
|
1352
|
+
throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
|
|
1353
|
+
}
|
|
1354
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1355
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1356
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
if (envelope.sourcePodId !==
|
|
1360
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
|
|
1361
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1362
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
|
|
1366
|
+
throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
|
|
1367
|
+
}
|
|
1368
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1369
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1370
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1371
|
+
}
|
|
1372
|
+
const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1373
|
+
const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1374
|
+
const signal = deadlineSignal
|
|
1375
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1376
|
+
: c.req.raw.signal;
|
|
1377
|
+
const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
|
|
1378
|
+
providerId: envelope.providerId,
|
|
1379
|
+
sessionKey: envelope.sessionKey,
|
|
1380
|
+
ownerPodId: envelope.ownerPodId,
|
|
1381
|
+
generation: envelope.generation,
|
|
1382
|
+
sourcePodId: envelope.sourcePodId,
|
|
1383
|
+
forwardedAt: envelope.forwardedAt,
|
|
1384
|
+
requestId: envelope.requestId,
|
|
1385
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1386
|
+
}, signal));
|
|
1387
|
+
let ownerFenceValid;
|
|
1388
|
+
try {
|
|
1389
|
+
ownerFenceValid = deadlineSignal
|
|
1390
|
+
? await Promise.race([
|
|
1391
|
+
ownerFenceValidation,
|
|
1392
|
+
new Promise((_resolve, reject) => {
|
|
1393
|
+
deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
|
|
1394
|
+
}),
|
|
1395
|
+
])
|
|
1396
|
+
: await ownerFenceValidation;
|
|
1397
|
+
}
|
|
1398
|
+
catch (error) {
|
|
1399
|
+
if (deadlineSignal?.aborted) {
|
|
1400
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1401
|
+
}
|
|
1402
|
+
throw error;
|
|
1403
|
+
}
|
|
1404
|
+
if (ownerFenceValid !== true) {
|
|
1405
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1406
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1410
|
+
operationId = envelope.operationId;
|
|
1411
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1412
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1413
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1414
|
+
}
|
|
1415
|
+
const output = await options.internalOperationExecutor({
|
|
1416
|
+
provider,
|
|
1417
|
+
operationId,
|
|
1418
|
+
ctx,
|
|
1419
|
+
request,
|
|
1420
|
+
internalStatefulForward: envelope,
|
|
1421
|
+
signal,
|
|
1422
|
+
});
|
|
1423
|
+
logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
|
|
1424
|
+
return c.json({ data: output });
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1428
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1429
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1430
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1431
|
+
}
|
|
1432
|
+
const requestId = extractRequestId(rawBody);
|
|
1433
|
+
logProviderError(logger, provider, "operation", operationId || operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
1434
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
955
1437
|
app.post("/v1/:operation", async (c) => {
|
|
956
1438
|
let rawBody;
|
|
957
1439
|
const operation = c.req.param("operation");
|
|
@@ -977,13 +1459,14 @@ export function createServerApp(provider, options = {}) {
|
|
|
977
1459
|
return c.json(response);
|
|
978
1460
|
}
|
|
979
1461
|
catch (error) {
|
|
980
|
-
const
|
|
1462
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
1463
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
981
1464
|
const requestId = extractRequestId(rawBody);
|
|
982
|
-
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1465
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost), declaredErrorCode);
|
|
983
1466
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
984
1467
|
if (telemetryHeader)
|
|
985
1468
|
c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
986
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1469
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId, declaredErrorCode), status), error, declaredErrorCode);
|
|
987
1470
|
}
|
|
988
1471
|
});
|
|
989
1472
|
app.post("/auth/start", async (c) => {
|
|
@@ -1003,7 +1486,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1003
1486
|
const status = toStatusCode(error);
|
|
1004
1487
|
const requestId = extractRequestId(rawBody);
|
|
1005
1488
|
logProviderError(logger, provider, "auth", "start", requestId, error, status, finishRequestCost(requestCost));
|
|
1006
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1489
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1007
1490
|
}
|
|
1008
1491
|
});
|
|
1009
1492
|
app.post("/auth/continue", async (c) => {
|
|
@@ -1023,7 +1506,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1023
1506
|
const status = toStatusCode(error);
|
|
1024
1507
|
const requestId = extractRequestId(rawBody);
|
|
1025
1508
|
logProviderError(logger, provider, "auth", "continue", requestId, error, status, finishRequestCost(requestCost));
|
|
1026
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1509
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1027
1510
|
}
|
|
1028
1511
|
});
|
|
1029
1512
|
app.post("/auth/poll", async (c) => {
|
|
@@ -1043,7 +1526,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1043
1526
|
const status = toStatusCode(error);
|
|
1044
1527
|
const requestId = extractRequestId(rawBody);
|
|
1045
1528
|
logProviderError(logger, provider, "auth", "poll", requestId, error, status, finishRequestCost(requestCost));
|
|
1046
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1529
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1047
1530
|
}
|
|
1048
1531
|
});
|
|
1049
1532
|
app.post("/auth/refresh", async (c) => {
|
|
@@ -1063,7 +1546,7 @@ export function createServerApp(provider, options = {}) {
|
|
|
1063
1546
|
const status = toStatusCode(error);
|
|
1064
1547
|
const requestId = extractRequestId(rawBody);
|
|
1065
1548
|
logProviderError(logger, provider, "auth", "refresh", requestId, error, status, finishRequestCost(requestCost));
|
|
1066
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1549
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1067
1550
|
}
|
|
1068
1551
|
});
|
|
1069
1552
|
app.post("/auth/disconnect", async (c) => {
|
|
@@ -1083,11 +1566,33 @@ export function createServerApp(provider, options = {}) {
|
|
|
1083
1566
|
const status = toStatusCode(error);
|
|
1084
1567
|
const requestId = extractRequestId(rawBody);
|
|
1085
1568
|
logProviderError(logger, provider, "auth", "disconnect", requestId, error, status, finishRequestCost(requestCost));
|
|
1086
|
-
return c.json(toErrorResponse(error, requestId), status);
|
|
1569
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error, requestId), status), error);
|
|
1087
1570
|
}
|
|
1088
1571
|
});
|
|
1089
1572
|
return app;
|
|
1090
1573
|
}
|
|
1574
|
+
function validateStatefulServerConfig(options) {
|
|
1575
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
1576
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
|
|
1577
|
+
}
|
|
1578
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
1579
|
+
throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
|
|
1580
|
+
}
|
|
1581
|
+
if (options.statefulForwarding &&
|
|
1582
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function") {
|
|
1583
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
|
|
1584
|
+
}
|
|
1585
|
+
if (options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
1586
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
1587
|
+
options.statefulForwarding.maxSkewMs <= 0)) {
|
|
1588
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
1589
|
+
}
|
|
1590
|
+
if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
1591
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
1592
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)) {
|
|
1593
|
+
throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1091
1596
|
function getBunServeRuntime() {
|
|
1092
1597
|
const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
|
|
1093
1598
|
if (!bunValue || typeof bunValue !== "object") {
|
|
@@ -1103,6 +1608,9 @@ function getBunServeRuntime() {
|
|
|
1103
1608
|
},
|
|
1104
1609
|
};
|
|
1105
1610
|
}
|
|
1611
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
1612
|
+
const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
1613
|
+
const processSignalCoordinators = new Map();
|
|
1106
1614
|
export async function serve(provider, options = {}) {
|
|
1107
1615
|
const bunRuntime = getBunServeRuntime();
|
|
1108
1616
|
if (bunRuntime === undefined) {
|
|
@@ -1110,29 +1618,186 @@ export async function serve(provider, options = {}) {
|
|
|
1110
1618
|
code: "RUNTIME_UNSUPPORTED",
|
|
1111
1619
|
});
|
|
1112
1620
|
}
|
|
1621
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1622
|
+
const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
|
|
1623
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1113
1624
|
const app = createServerApp(provider, {
|
|
1114
1625
|
logger: options.logger,
|
|
1115
1626
|
stt: options.stt,
|
|
1627
|
+
state: options.state,
|
|
1628
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
1629
|
+
operationExecutor: options.operationExecutor,
|
|
1630
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
1631
|
+
statefulForwarding: options.statefulForwarding,
|
|
1116
1632
|
});
|
|
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(),
|
|
1633
|
+
const servers = [];
|
|
1634
|
+
try {
|
|
1635
|
+
servers.push(bunRuntime.serve({
|
|
1636
|
+
port: options.port ?? DEFAULT_PORT,
|
|
1133
1637
|
hostname: options.host ?? DEFAULT_HOST,
|
|
1134
|
-
fetch:
|
|
1135
|
-
});
|
|
1638
|
+
fetch: app.fetch,
|
|
1639
|
+
}));
|
|
1640
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1641
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1642
|
+
// starts when the shared self-test master secret env is present.
|
|
1643
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1644
|
+
if (selfTestSecrets) {
|
|
1645
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
1646
|
+
secrets: selfTestSecrets,
|
|
1647
|
+
invoke: createSelfTestInvoke(app),
|
|
1648
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
1649
|
+
});
|
|
1650
|
+
servers.push(bunRuntime.serve({
|
|
1651
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1652
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
1653
|
+
fetch: selfTestApp.fetch,
|
|
1654
|
+
}));
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
const server = servers[0];
|
|
1662
|
+
if (!server)
|
|
1663
|
+
throw new Error("Provider server failed to create its primary listener.");
|
|
1664
|
+
let closePromise;
|
|
1665
|
+
let unregisterSignals = () => { };
|
|
1666
|
+
const close = (closeOptions = {}) => {
|
|
1667
|
+
if (closePromise)
|
|
1668
|
+
return closePromise;
|
|
1669
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
1670
|
+
closePromise = closeProviderServers({
|
|
1671
|
+
servers,
|
|
1672
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
1673
|
+
timeoutMs,
|
|
1674
|
+
logger,
|
|
1675
|
+
providerId: provider.id,
|
|
1676
|
+
}).finally(() => unregisterSignals());
|
|
1677
|
+
return closePromise;
|
|
1678
|
+
};
|
|
1679
|
+
try {
|
|
1680
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
|
|
1681
|
+
}
|
|
1682
|
+
catch (error) {
|
|
1683
|
+
unregisterSignals();
|
|
1684
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1685
|
+
throw error;
|
|
1686
|
+
}
|
|
1687
|
+
return { port: server.port, close };
|
|
1688
|
+
}
|
|
1689
|
+
function registerForProcessSignals(signals, close) {
|
|
1690
|
+
if (signals.length === 0)
|
|
1691
|
+
return () => { };
|
|
1692
|
+
const registration = {
|
|
1693
|
+
signals: new Set(signals),
|
|
1694
|
+
close,
|
|
1695
|
+
};
|
|
1696
|
+
let registered = true;
|
|
1697
|
+
const unregister = () => {
|
|
1698
|
+
if (!registered)
|
|
1699
|
+
return;
|
|
1700
|
+
registered = false;
|
|
1701
|
+
for (const signal of registration.signals) {
|
|
1702
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
1703
|
+
if (!coordinator)
|
|
1704
|
+
continue;
|
|
1705
|
+
coordinator.registrations.delete(registration);
|
|
1706
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
1707
|
+
process.removeListener(signal, coordinator.listener);
|
|
1708
|
+
processSignalCoordinators.delete(signal);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
try {
|
|
1713
|
+
for (const signal of signals) {
|
|
1714
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
1715
|
+
if (!coordinator) {
|
|
1716
|
+
const created = {
|
|
1717
|
+
registrations: new Set(),
|
|
1718
|
+
handling: false,
|
|
1719
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
1720
|
+
};
|
|
1721
|
+
coordinator = created;
|
|
1722
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
1723
|
+
process.on(signal, coordinator.listener);
|
|
1724
|
+
}
|
|
1725
|
+
coordinator.registrations.add(registration);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
catch (error) {
|
|
1729
|
+
unregister();
|
|
1730
|
+
throw error;
|
|
1731
|
+
}
|
|
1732
|
+
return unregister;
|
|
1733
|
+
}
|
|
1734
|
+
function handleCoordinatedSignal(signal, coordinator) {
|
|
1735
|
+
if (coordinator.handling)
|
|
1736
|
+
return;
|
|
1737
|
+
coordinator.handling = true;
|
|
1738
|
+
const registrations = [...coordinator.registrations];
|
|
1739
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
1740
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
1741
|
+
process.removeListener(signal, coordinator.listener);
|
|
1742
|
+
processSignalCoordinators.delete(signal);
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
process.kill(process.pid, signal);
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
process.exitCode = 1;
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
async function closeProviderServers(input) {
|
|
1753
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1754
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
1755
|
+
for (const gracefulStop of gracefulStops)
|
|
1756
|
+
gracefulStop.catch(() => undefined);
|
|
1757
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
1758
|
+
try {
|
|
1759
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
try {
|
|
1763
|
+
input.logger({
|
|
1764
|
+
level: "error",
|
|
1765
|
+
event: "provider_shutdown_hook_failed",
|
|
1766
|
+
providerId: input.providerId,
|
|
1767
|
+
hookIndex,
|
|
1768
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
1769
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
catch { }
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
1776
|
+
await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
|
|
1777
|
+
}
|
|
1778
|
+
async function withinShutdownBudget(promise, deadline) {
|
|
1779
|
+
promise.catch(() => undefined);
|
|
1780
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
1781
|
+
let timer;
|
|
1782
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1783
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1784
|
+
});
|
|
1785
|
+
try {
|
|
1786
|
+
return await Promise.race([promise, timeout]);
|
|
1787
|
+
}
|
|
1788
|
+
finally {
|
|
1789
|
+
if (timer)
|
|
1790
|
+
clearTimeout(timer);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function resolveShutdownSignals(signals) {
|
|
1794
|
+
if (signals === false)
|
|
1795
|
+
return [];
|
|
1796
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
1797
|
+
}
|
|
1798
|
+
function shutdownTimeout(value) {
|
|
1799
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1800
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1136
1801
|
}
|
|
1137
|
-
|
|
1802
|
+
return value;
|
|
1138
1803
|
}
|