@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/src/server/serve.ts
CHANGED
|
@@ -3,62 +3,83 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { AuthAbortError, createAuthFlowHelpers } from "../auth";
|
|
6
|
+
import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
7
|
+
import {
|
|
8
|
+
SDK_OWNED_PROVIDER_ERROR_CODES,
|
|
9
|
+
SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
10
|
+
} from "../error-resolution.js";
|
|
7
11
|
import {
|
|
8
12
|
AuthError,
|
|
13
|
+
isProviderError,
|
|
14
|
+
isSessionExpiredError,
|
|
15
|
+
isTransportError,
|
|
16
|
+
isValidationError,
|
|
9
17
|
ProviderError,
|
|
10
|
-
|
|
11
|
-
TransportError,
|
|
12
|
-
} from "../errors";
|
|
18
|
+
} from "../errors.js";
|
|
13
19
|
import {
|
|
14
20
|
loadProviderLocaleCatalogs,
|
|
15
21
|
localizeAuthTurn,
|
|
16
22
|
type ProviderLocaleCatalogMap,
|
|
17
|
-
} from "../i18n/catalog";
|
|
18
|
-
import type { ProviderLocale } from "../i18n/keys";
|
|
23
|
+
} from "../i18n/catalog.js";
|
|
24
|
+
import type { ProviderLocale } from "../i18n/keys.js";
|
|
19
25
|
import {
|
|
20
26
|
categoryForStatus,
|
|
27
|
+
type ProviderErrorSource,
|
|
28
|
+
sourceForCategory,
|
|
21
29
|
isRetryableCategory,
|
|
22
30
|
PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
23
31
|
type ProviderErrorCategory,
|
|
24
|
-
} from "../observability";
|
|
25
|
-
import { createScratchpad } from "../runtime/auth-flow";
|
|
26
|
-
import { createBrowserClient } from "../runtime/browser";
|
|
27
|
-
import { createProviderCache } from "../runtime/cache";
|
|
32
|
+
} from "../observability.js";
|
|
33
|
+
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
34
|
+
import { createBrowserClient } from "../runtime/browser.js";
|
|
35
|
+
import { createProviderCache } from "../runtime/cache.js";
|
|
28
36
|
import {
|
|
29
37
|
createProviderChoiceContext,
|
|
30
38
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
31
|
-
} from "../runtime/choice";
|
|
32
|
-
import { createCredentialContext } from "../runtime/credential";
|
|
33
|
-
import { createEnvContext } from "../runtime/env";
|
|
34
|
-
import { executeOperation } from "../runtime/executor";
|
|
35
|
-
import { createHttpClient } from "../runtime/http";
|
|
36
|
-
import { wrapWithInstrumentation } from "../runtime/instrumentation";
|
|
37
|
-
import {
|
|
39
|
+
} from "../runtime/choice.js";
|
|
40
|
+
import { createCredentialContext } from "../runtime/credential.js";
|
|
41
|
+
import { createEnvContext } from "../runtime/env.js";
|
|
42
|
+
import { executeOperation } from "../runtime/executor.js";
|
|
43
|
+
import { createHttpClient } from "../runtime/http.js";
|
|
44
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
45
|
+
import {
|
|
46
|
+
createEnvVendorCredentialResolver,
|
|
47
|
+
createNativeNetworkClient,
|
|
48
|
+
} from "../runtime/native-network.js";
|
|
49
|
+
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
38
50
|
import {
|
|
39
51
|
PROXY_AUTH_IP_DENIED_CODE,
|
|
40
52
|
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
41
53
|
PROXY_POOL_EXHAUSTED_CODE,
|
|
42
|
-
} from "../runtime/proxy-errors";
|
|
54
|
+
} from "../runtime/proxy-errors.js";
|
|
55
|
+
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
43
56
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
assertRequiredSecretsPresent,
|
|
58
|
+
listMissingRequiredSecrets,
|
|
59
|
+
MISSING_SECRET_CODE,
|
|
60
|
+
} from "../runtime/secrets.js";
|
|
47
61
|
import {
|
|
48
62
|
createProviderRuntimeStateFromEnv,
|
|
49
63
|
createUnsupportedProviderRuntimeState,
|
|
50
|
-
} from "../runtime/state";
|
|
51
|
-
import { createStealthClient } from "../runtime/stealth";
|
|
52
|
-
import { createSttClientFromEnv } from "../runtime/stt";
|
|
53
|
-
import { createTraceContext } from "../runtime/trace";
|
|
54
|
-
import { parseSchema } from "../schema";
|
|
55
|
-
import {
|
|
64
|
+
} from "../runtime/state.js";
|
|
65
|
+
import { createStealthClient } from "../runtime/stealth.js";
|
|
66
|
+
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
67
|
+
import { createTraceContext } from "../runtime/trace.js";
|
|
68
|
+
import { parseSchema } from "../schema.js";
|
|
69
|
+
import {
|
|
70
|
+
STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
|
|
71
|
+
STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER,
|
|
72
|
+
STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER,
|
|
73
|
+
verifyStatefulRequestSignature,
|
|
74
|
+
} from "../stateful-signing.js";
|
|
75
|
+
import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
|
|
76
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
56
77
|
import {
|
|
57
78
|
APIFUSE_STREAM_DONE_EVENT,
|
|
58
79
|
APIFUSE_STREAM_ERROR_EVENT,
|
|
59
80
|
encodeSseEvent,
|
|
60
81
|
error as streamError,
|
|
61
|
-
} from "../stream";
|
|
82
|
+
} from "../stream.js";
|
|
62
83
|
import type {
|
|
63
84
|
AuthContext,
|
|
64
85
|
AuthTurn,
|
|
@@ -67,37 +88,118 @@ import type {
|
|
|
67
88
|
FlowContextStore,
|
|
68
89
|
HttpRetrySummary,
|
|
69
90
|
OperationDefinition,
|
|
91
|
+
OperationErrorCode,
|
|
70
92
|
OperationHttpStreamTransport,
|
|
71
93
|
OperationSseTransport,
|
|
94
|
+
ProviderErrorStatus,
|
|
72
95
|
ProviderContext,
|
|
73
96
|
ProviderDefinition,
|
|
97
|
+
ProviderProxyPolicy,
|
|
74
98
|
ProviderRuntimeState,
|
|
75
99
|
ProviderStreamEvent,
|
|
76
100
|
StealthClient,
|
|
77
101
|
SttContext,
|
|
78
|
-
} from "../types";
|
|
102
|
+
} from "../types.js";
|
|
103
|
+
import { VALID_OPERATION_ERROR_STATUSES } from "../types.js";
|
|
79
104
|
import {
|
|
80
105
|
createSelfTestApp,
|
|
106
|
+
createSelfTestAuthFlowInvoke,
|
|
81
107
|
createSelfTestInvoke,
|
|
82
108
|
resolveSelfTestPort,
|
|
83
|
-
} from "./self-test";
|
|
84
|
-
import { resolveSelfTestMasterSecrets } from "./self-test-token";
|
|
109
|
+
} from "./self-test.js";
|
|
110
|
+
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
85
111
|
import {
|
|
86
112
|
type AuthFlowRequest,
|
|
87
113
|
AuthFlowRequestSchema,
|
|
88
114
|
type AuthFlowResponse,
|
|
89
115
|
type AuthFlowSuccessResponse,
|
|
116
|
+
OperationConnectionSchema,
|
|
90
117
|
type OperationErrorResponse,
|
|
91
118
|
type OperationRequest,
|
|
92
119
|
OperationRequestSchema,
|
|
93
120
|
type OperationResponse,
|
|
94
121
|
type OperationSuccessResponse,
|
|
95
|
-
} from "./types";
|
|
122
|
+
} from "./types.js";
|
|
96
123
|
|
|
97
124
|
const DEFAULT_HOST = "0.0.0.0";
|
|
98
125
|
const DEFAULT_PORT = 3000;
|
|
126
|
+
/** Compact SDK-owned error classification emitted separately from the public response body. */
|
|
127
|
+
export const ERROR_OBSERVABILITY_HEADER = "X-ApiFuse-Error-Observability";
|
|
128
|
+
export type ErrorObservabilityDetails = {
|
|
129
|
+
category: ProviderErrorCategory;
|
|
130
|
+
taxonomyVersion: string;
|
|
131
|
+
retryable: boolean;
|
|
132
|
+
upstreamStatus?: number;
|
|
133
|
+
};
|
|
99
134
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
|
|
100
135
|
const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
|
|
136
|
+
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
137
|
+
const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
|
|
138
|
+
const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
|
|
139
|
+
const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
|
|
140
|
+
const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
|
|
141
|
+
const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(
|
|
142
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export const ProviderServerStatefulForwardEnvelopeSchema = z
|
|
146
|
+
.object({
|
|
147
|
+
requestId: z.string().min(1),
|
|
148
|
+
providerId: z.string().min(1),
|
|
149
|
+
operationId: z.string().min(1),
|
|
150
|
+
sessionKey: z.string().min(1),
|
|
151
|
+
connectionId: z.string().min(1),
|
|
152
|
+
serviceAccountId: z.string().min(1),
|
|
153
|
+
ownerPodId: z.string().min(1),
|
|
154
|
+
generation: z.number().int().positive(),
|
|
155
|
+
sourcePodId: z.string().min(1),
|
|
156
|
+
forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
157
|
+
deadlineAt: z
|
|
158
|
+
.string()
|
|
159
|
+
.refine((value) => Number.isFinite(Date.parse(value)))
|
|
160
|
+
.optional(),
|
|
161
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
162
|
+
operationRequest: OperationRequestSchema.extend({
|
|
163
|
+
connection: OperationConnectionSchema.strict().optional(),
|
|
164
|
+
}).strict(),
|
|
165
|
+
})
|
|
166
|
+
.strict();
|
|
167
|
+
|
|
168
|
+
export type ProviderServerStatefulForwardEnvelope = Readonly<
|
|
169
|
+
z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>
|
|
170
|
+
>;
|
|
171
|
+
|
|
172
|
+
export type ProviderServerStatefulOwnerFence = Readonly<
|
|
173
|
+
Pick<
|
|
174
|
+
ProviderServerStatefulForwardEnvelope,
|
|
175
|
+
| "providerId"
|
|
176
|
+
| "sessionKey"
|
|
177
|
+
| "ownerPodId"
|
|
178
|
+
| "generation"
|
|
179
|
+
| "sourcePodId"
|
|
180
|
+
| "forwardedAt"
|
|
181
|
+
| "requestId"
|
|
182
|
+
| "idempotencyKey"
|
|
183
|
+
>
|
|
184
|
+
>;
|
|
185
|
+
|
|
186
|
+
export type ProviderServerStatefulOwnerFenceValidator = (
|
|
187
|
+
fence: ProviderServerStatefulOwnerFence,
|
|
188
|
+
signal: AbortSignal,
|
|
189
|
+
) => boolean | Promise<boolean>;
|
|
190
|
+
|
|
191
|
+
export type ProviderServerOperationExecutorInput = {
|
|
192
|
+
readonly provider: ProviderDefinition;
|
|
193
|
+
readonly operationId: string;
|
|
194
|
+
readonly ctx: ProviderContext;
|
|
195
|
+
readonly request: OperationRequest & { readonly deadlineAt?: string };
|
|
196
|
+
readonly signal?: AbortSignal;
|
|
197
|
+
readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type ProviderServerOperationExecutor = (
|
|
201
|
+
input: ProviderServerOperationExecutorInput,
|
|
202
|
+
) => Promise<unknown>;
|
|
101
203
|
|
|
102
204
|
type RequestCleanup = () => void | Promise<void>;
|
|
103
205
|
|
|
@@ -156,9 +258,7 @@ function createStealthStub(): StealthClient {
|
|
|
156
258
|
};
|
|
157
259
|
}
|
|
158
260
|
|
|
159
|
-
function getProviderStealthBaseUrl(
|
|
160
|
-
provider: ProviderDefinition,
|
|
161
|
-
): string | undefined {
|
|
261
|
+
function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
|
|
162
262
|
const baseUrl = getProviderBaseUrl(provider);
|
|
163
263
|
if (baseUrl) {
|
|
164
264
|
return baseUrl;
|
|
@@ -168,15 +268,10 @@ function getProviderStealthBaseUrl(
|
|
|
168
268
|
}
|
|
169
269
|
|
|
170
270
|
function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
171
|
-
return provider.stealth?.profile
|
|
172
|
-
? getStealthProfile(provider.stealth.profile)
|
|
173
|
-
: undefined;
|
|
271
|
+
return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
|
|
174
272
|
}
|
|
175
273
|
|
|
176
|
-
function isProductionProviderBrowserMode(
|
|
177
|
-
provider: ProviderDefinition,
|
|
178
|
-
env = process.env,
|
|
179
|
-
): boolean {
|
|
274
|
+
function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
|
|
180
275
|
if (provider.runtime !== "browser") {
|
|
181
276
|
return false;
|
|
182
277
|
}
|
|
@@ -185,9 +280,7 @@ function isProductionProviderBrowserMode(
|
|
|
185
280
|
return true;
|
|
186
281
|
}
|
|
187
282
|
|
|
188
|
-
return
|
|
189
|
-
env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id
|
|
190
|
-
);
|
|
283
|
+
return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
|
|
191
284
|
}
|
|
192
285
|
|
|
193
286
|
export function resolveProviderProxyAffinityKey(
|
|
@@ -195,24 +288,26 @@ export function resolveProviderProxyAffinityKey(
|
|
|
195
288
|
request: OperationRequest,
|
|
196
289
|
operationId: string,
|
|
197
290
|
): string {
|
|
198
|
-
const connectionKey =
|
|
199
|
-
resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
291
|
+
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
200
292
|
const affinity =
|
|
201
|
-
typeof provider.proxy === "object"
|
|
202
|
-
? provider.proxy.session?.affinity
|
|
203
|
-
: undefined;
|
|
293
|
+
typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
|
|
204
294
|
if (affinity === "operation") {
|
|
205
295
|
return `${provider.id}/${operationId}`;
|
|
206
296
|
}
|
|
207
297
|
return connectionKey ?? provider.id;
|
|
208
298
|
}
|
|
209
299
|
|
|
210
|
-
function resolveOperationConnectionId(
|
|
211
|
-
request: OperationRequest,
|
|
212
|
-
): string | undefined {
|
|
300
|
+
function resolveOperationConnectionId(request: OperationRequest): string | undefined {
|
|
213
301
|
return request.connection?.id ?? request.connectionId;
|
|
214
302
|
}
|
|
215
303
|
|
|
304
|
+
function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
|
|
305
|
+
if (typeof provider.proxy === "object") return provider.proxy;
|
|
306
|
+
if (provider.proxy === true) return { mode: "optional" };
|
|
307
|
+
if (provider.proxy === false) return { mode: "disabled" };
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
216
311
|
function createProviderContext(
|
|
217
312
|
provider: ProviderDefinition,
|
|
218
313
|
request: OperationRequest,
|
|
@@ -226,11 +321,7 @@ function createProviderContext(
|
|
|
226
321
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
227
322
|
const proxyClientOptions = {
|
|
228
323
|
upstream: { proxy: provider.proxy },
|
|
229
|
-
affinityKey: resolveProviderProxyAffinityKey(
|
|
230
|
-
provider,
|
|
231
|
-
request,
|
|
232
|
-
operationId,
|
|
233
|
-
),
|
|
324
|
+
affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
|
|
234
325
|
telemetry: proxyTelemetry,
|
|
235
326
|
};
|
|
236
327
|
let wrappedContext: ProviderContext | undefined;
|
|
@@ -269,11 +360,7 @@ function createProviderContext(
|
|
|
269
360
|
state,
|
|
270
361
|
stealth: stealthBaseUrl
|
|
271
362
|
? stealthProfile
|
|
272
|
-
? createStealthClient(
|
|
273
|
-
stealthBaseUrl,
|
|
274
|
-
stealthProfile.name,
|
|
275
|
-
stealthClientOptions,
|
|
276
|
-
)
|
|
363
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
277
364
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
278
365
|
: createStealthStub(),
|
|
279
366
|
browser:
|
|
@@ -287,6 +374,18 @@ function createProviderContext(
|
|
|
287
374
|
engine: provider.browser?.engine,
|
|
288
375
|
})
|
|
289
376
|
: createBrowserStub(),
|
|
377
|
+
...(provider.native
|
|
378
|
+
? {
|
|
379
|
+
native: {
|
|
380
|
+
network: createNativeNetworkClient({
|
|
381
|
+
egress: provider.native.network,
|
|
382
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
383
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
384
|
+
credentials: createEnvVendorCredentialResolver(env),
|
|
385
|
+
}),
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
: {}),
|
|
290
389
|
trace: createTraceContext(),
|
|
291
390
|
auth: createAuthStub(),
|
|
292
391
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -379,6 +478,7 @@ function createAuthFlowContext(
|
|
|
379
478
|
|
|
380
479
|
return {
|
|
381
480
|
context: {
|
|
481
|
+
flowId: request.flowId,
|
|
382
482
|
connectionId: request.connectionId,
|
|
383
483
|
externalRef: request.externalRef,
|
|
384
484
|
tenantId: request.tenantId ?? "",
|
|
@@ -386,14 +486,29 @@ function createAuthFlowContext(
|
|
|
386
486
|
http: createHttpClient(baseUrl, proxyClientOptions),
|
|
387
487
|
stealth: stealthBaseUrl
|
|
388
488
|
? stealthProfile
|
|
389
|
-
? createStealthClient(
|
|
390
|
-
stealthBaseUrl,
|
|
391
|
-
stealthProfile.name,
|
|
392
|
-
stealthClientOptions,
|
|
393
|
-
)
|
|
489
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
394
490
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
395
491
|
: createStealthStub(),
|
|
396
|
-
|
|
492
|
+
...(provider.native
|
|
493
|
+
? {
|
|
494
|
+
native: {
|
|
495
|
+
network: createNativeNetworkClient({
|
|
496
|
+
egress: provider.native.network,
|
|
497
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
498
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
499
|
+
credentials: createEnvVendorCredentialResolver(
|
|
500
|
+
createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
501
|
+
),
|
|
502
|
+
}),
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
: {}),
|
|
506
|
+
env: createEnvContext([
|
|
507
|
+
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
508
|
+
...(provider.auth?.mode === "oauth2_proxied"
|
|
509
|
+
? ["APIFUSE__AUTH_PROXY__URL"]
|
|
510
|
+
: []),
|
|
511
|
+
]),
|
|
397
512
|
credential,
|
|
398
513
|
context: flowContextStore.context,
|
|
399
514
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -422,7 +537,7 @@ export type ProviderServerLogEvent =
|
|
|
422
537
|
| (ProviderServerLogEventBase & {
|
|
423
538
|
level: "info";
|
|
424
539
|
event: "provider_request_completed";
|
|
425
|
-
|
|
540
|
+
})
|
|
426
541
|
| (ProviderServerLogEventBase & {
|
|
427
542
|
level: "warn" | "error";
|
|
428
543
|
event: "provider_request_failed";
|
|
@@ -433,8 +548,16 @@ export type ProviderServerLogEvent =
|
|
|
433
548
|
errorCategory?: ProviderErrorCategory;
|
|
434
549
|
taxonomyVersion?: string;
|
|
435
550
|
retryable?: boolean;
|
|
551
|
+
signal?: "unregistered_provider_error_code";
|
|
552
|
+
signalFix?: string;
|
|
436
553
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
437
|
-
|
|
554
|
+
})
|
|
555
|
+
| {
|
|
556
|
+
level: "warn";
|
|
557
|
+
event: "provider_secrets_missing";
|
|
558
|
+
providerId: string;
|
|
559
|
+
missingSecrets: string[];
|
|
560
|
+
}
|
|
438
561
|
| {
|
|
439
562
|
level: "warn";
|
|
440
563
|
event: "provider_cleanup_failed";
|
|
@@ -445,18 +568,59 @@ export type ProviderServerLogEvent =
|
|
|
445
568
|
resource: "browser" | "stealth";
|
|
446
569
|
errorClass: string;
|
|
447
570
|
message: string;
|
|
448
|
-
|
|
571
|
+
}
|
|
572
|
+
| {
|
|
573
|
+
level: "error";
|
|
574
|
+
event: "provider_shutdown_hook_failed";
|
|
575
|
+
providerId: string;
|
|
576
|
+
hookIndex: number;
|
|
577
|
+
errorClass: string;
|
|
578
|
+
message: string;
|
|
579
|
+
};
|
|
449
580
|
|
|
450
581
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
451
582
|
|
|
452
583
|
export type ProviderServerOptions = {
|
|
453
584
|
logger?: ProviderServerLogger;
|
|
585
|
+
/** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
|
|
586
|
+
operationExecutor?: ProviderServerOperationExecutor;
|
|
587
|
+
/** Optional signed internal executor for stateful owner forwarding. */
|
|
588
|
+
internalOperationExecutor?: ProviderServerOperationExecutor;
|
|
589
|
+
statefulForwarding?: {
|
|
590
|
+
readonly secret: string;
|
|
591
|
+
readonly maxSkewMs?: number;
|
|
592
|
+
readonly replayCacheMaxEntries?: number;
|
|
593
|
+
/** Required fail-closed check against the SDK/runtime owner registry. */
|
|
594
|
+
readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
|
|
595
|
+
};
|
|
454
596
|
/** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
|
|
455
597
|
stt?: SttContext;
|
|
456
598
|
/** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
|
|
457
599
|
state?: ProviderRuntimeState;
|
|
458
600
|
/** Allow process-local runtime state only for local development and tests. */
|
|
459
601
|
allowMemoryStateFallback?: boolean;
|
|
602
|
+
/**
|
|
603
|
+
* Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* await serve(provider, {
|
|
608
|
+
* shutdown: {
|
|
609
|
+
* hooks: [
|
|
610
|
+
* async () => { await emitter.flush(); },
|
|
611
|
+
* async () => { await sessionManager.closeAll("server-shutdown"); },
|
|
612
|
+
* async () => { await lease.release(); },
|
|
613
|
+
* async () => { await router.close(); },
|
|
614
|
+
* ],
|
|
615
|
+
* },
|
|
616
|
+
* });
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
shutdown?: {
|
|
620
|
+
readonly hooks?: Array<() => Promise<void>>;
|
|
621
|
+
readonly signals?: boolean | NodeJS.Signals[];
|
|
622
|
+
readonly timeoutMs?: number;
|
|
623
|
+
};
|
|
460
624
|
};
|
|
461
625
|
|
|
462
626
|
const defaultProviderServerLogger: ProviderServerLogger = (event) => {
|
|
@@ -503,19 +667,57 @@ function zodDetails(error: z.ZodError): Array<{
|
|
|
503
667
|
}));
|
|
504
668
|
}
|
|
505
669
|
|
|
670
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
671
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
672
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
673
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
674
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
675
|
+
// attribution even when the author left the category at the provider_error
|
|
676
|
+
// default.
|
|
677
|
+
function publicErrorSource(
|
|
678
|
+
error: unknown,
|
|
679
|
+
category: ProviderErrorCategory,
|
|
680
|
+
): ProviderErrorSource {
|
|
681
|
+
if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
|
|
682
|
+
if (isProviderError(error)) {
|
|
683
|
+
if (error.code === MISSING_SECRET_CODE) return "apifuse";
|
|
684
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
685
|
+
return "upstream_failure";
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return sourceForCategory(category);
|
|
689
|
+
}
|
|
690
|
+
|
|
506
691
|
function toErrorResponse(
|
|
507
692
|
error: unknown,
|
|
508
693
|
requestId?: string,
|
|
694
|
+
declaredErrorCode?: OperationErrorCode,
|
|
509
695
|
): OperationErrorResponse {
|
|
510
|
-
|
|
511
|
-
|
|
696
|
+
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
697
|
+
const source = publicErrorSource(error, observability.category);
|
|
698
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
699
|
+
return {
|
|
700
|
+
error: {
|
|
701
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
702
|
+
message: "Stateful forwarding deadline expired.",
|
|
703
|
+
...(requestId ? { requestId } : {}),
|
|
704
|
+
retryable: observability.retryable,
|
|
705
|
+
source,
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (isProviderError(error)) {
|
|
711
|
+
const details = error.details;
|
|
512
712
|
return {
|
|
513
713
|
error: {
|
|
514
714
|
code: error.code ?? "provider_error",
|
|
515
715
|
message: publicProviderErrorMessage(error),
|
|
516
716
|
...(requestId ? { requestId } : {}),
|
|
717
|
+
retryable: observability.retryable,
|
|
718
|
+
source,
|
|
517
719
|
...(error.fix ? { fix: error.fix } : {}),
|
|
518
|
-
...(details ? { details } : {}),
|
|
720
|
+
...(details !== undefined ? { details } : {}),
|
|
519
721
|
},
|
|
520
722
|
};
|
|
521
723
|
}
|
|
@@ -526,64 +728,72 @@ function toErrorResponse(
|
|
|
526
728
|
code: "invalid_request",
|
|
527
729
|
message: "Invalid request body",
|
|
528
730
|
...(requestId ? { requestId } : {}),
|
|
731
|
+
retryable: observability.retryable,
|
|
732
|
+
source,
|
|
529
733
|
details: zodDetails(error),
|
|
530
734
|
},
|
|
531
735
|
};
|
|
532
736
|
}
|
|
533
737
|
|
|
738
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
739
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
740
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
741
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
742
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
743
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
534
744
|
return {
|
|
535
745
|
error: {
|
|
536
746
|
code: "internal_error",
|
|
537
747
|
message: "Internal error",
|
|
538
748
|
...(requestId ? { requestId } : {}),
|
|
749
|
+
retryable: observability.retryable,
|
|
750
|
+
source,
|
|
751
|
+
details: {
|
|
752
|
+
retryable: false,
|
|
753
|
+
category: "internal_error",
|
|
754
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
755
|
+
},
|
|
539
756
|
},
|
|
540
757
|
};
|
|
541
758
|
}
|
|
542
759
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
return {
|
|
557
|
-
provider: providerDetails,
|
|
558
|
-
observability: observabilityDetails,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
563
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function providerObservabilityDetails(error: ProviderError):
|
|
567
|
-
| {
|
|
568
|
-
category: ProviderErrorCategory;
|
|
569
|
-
taxonomyVersion: string;
|
|
570
|
-
retryable: boolean;
|
|
571
|
-
upstreamStatus?: number;
|
|
572
|
-
}
|
|
573
|
-
| undefined {
|
|
760
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
761
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
762
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
763
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
764
|
+
// from a duplicate SDK module instance.
|
|
765
|
+
function providerObservabilityDetails(
|
|
766
|
+
error: unknown,
|
|
767
|
+
declaredErrorCode?: OperationErrorCode,
|
|
768
|
+
): ErrorObservabilityDetails | undefined {
|
|
769
|
+
const declaredRetryable = sdkOwnsErrorResolution(error)
|
|
770
|
+
? undefined
|
|
771
|
+
: declaredErrorCode?.retryable;
|
|
574
772
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
575
773
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
576
774
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
577
775
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
578
776
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
579
|
-
if (error
|
|
777
|
+
if (isSessionExpiredError(error)) {
|
|
580
778
|
return {
|
|
581
779
|
category: error.options?.category ?? "credential_expired",
|
|
582
780
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
583
|
-
retryable: error.options?.retryable ?? false,
|
|
781
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
785
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
786
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
787
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
788
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
789
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
790
|
+
return {
|
|
791
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
792
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
793
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
584
794
|
};
|
|
585
795
|
}
|
|
586
|
-
if (!(error
|
|
796
|
+
if (!isTransportError(error)) {
|
|
587
797
|
return undefined;
|
|
588
798
|
}
|
|
589
799
|
const isProxyPoolCode =
|
|
@@ -615,8 +825,86 @@ function providerObservabilityDetails(error: ProviderError):
|
|
|
615
825
|
};
|
|
616
826
|
}
|
|
617
827
|
|
|
828
|
+
function errorObservabilityDetails(
|
|
829
|
+
error: unknown,
|
|
830
|
+
declaredErrorCode?: OperationErrorCode,
|
|
831
|
+
): ErrorObservabilityDetails {
|
|
832
|
+
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
833
|
+
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
834
|
+
if (providerDetails) return providerDetails;
|
|
835
|
+
|
|
836
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
837
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
838
|
+
return {
|
|
839
|
+
category:
|
|
840
|
+
isProviderError(error) && error.options?.category
|
|
841
|
+
? error.options.category
|
|
842
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
843
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
844
|
+
? "upstream_rejected"
|
|
845
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
846
|
+
? "provider_error"
|
|
847
|
+
: "input_validation",
|
|
848
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
849
|
+
retryable: isProviderError(error)
|
|
850
|
+
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
851
|
+
: false,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
856
|
+
return {
|
|
857
|
+
category: "timeout",
|
|
858
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
859
|
+
retryable: false,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (isProviderError(error)) {
|
|
864
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
865
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
866
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
867
|
+
// author set an explicit category.
|
|
868
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
869
|
+
const rejectionDefault =
|
|
870
|
+
error.code === "UPSTREAM_REJECTED" ||
|
|
871
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
872
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
873
|
+
? ("upstream_rejected" as const)
|
|
874
|
+
: ("provider_error" as const);
|
|
875
|
+
return {
|
|
876
|
+
category: error.options?.category ?? rejectionDefault,
|
|
877
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
878
|
+
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
category: "internal_error",
|
|
884
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
885
|
+
retryable: false,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function responseWithErrorObservability(
|
|
890
|
+
response: Response,
|
|
891
|
+
error: unknown,
|
|
892
|
+
declaredErrorCode?: OperationErrorCode,
|
|
893
|
+
): Response {
|
|
894
|
+
const headers = new Headers(response.headers);
|
|
895
|
+
headers.set(
|
|
896
|
+
ERROR_OBSERVABILITY_HEADER,
|
|
897
|
+
JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
|
|
898
|
+
);
|
|
899
|
+
return new Response(response.body, {
|
|
900
|
+
status: response.status,
|
|
901
|
+
statusText: response.statusText,
|
|
902
|
+
headers,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
618
906
|
function publicProviderErrorMessage(error: ProviderError): string {
|
|
619
|
-
if (error
|
|
907
|
+
if (isTransportError(error)) {
|
|
620
908
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
621
909
|
return error.message;
|
|
622
910
|
}
|
|
@@ -639,22 +927,35 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
639
927
|
return error.message;
|
|
640
928
|
}
|
|
641
929
|
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
930
|
+
function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
|
|
931
|
+
return (
|
|
932
|
+
typeof value === "number" &&
|
|
933
|
+
VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
|
|
645
938
|
if (error instanceof z.ZodError) {
|
|
646
939
|
return 400;
|
|
647
940
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return error.code === "transport_timeout" ? 504 : 502;
|
|
941
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
942
|
+
return 504;
|
|
651
943
|
}
|
|
652
|
-
|
|
653
|
-
|
|
944
|
+
if (isProviderError(error)) {
|
|
945
|
+
if (
|
|
946
|
+
!sdkOwnsErrorResolution(error) &&
|
|
947
|
+
isEmittableErrorStatus(declaredErrorCode?.status)
|
|
948
|
+
) {
|
|
949
|
+
return declaredErrorCode.status;
|
|
950
|
+
}
|
|
654
951
|
switch (error.code) {
|
|
655
952
|
case "AUTH_REQUIRED":
|
|
656
953
|
case "reauth_required":
|
|
657
954
|
return 401;
|
|
955
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
956
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
957
|
+
case MISSING_SECRET_CODE:
|
|
958
|
+
return 400;
|
|
658
959
|
case "NOT_FOUND":
|
|
659
960
|
case "not_found":
|
|
660
961
|
case "NO_DATA":
|
|
@@ -663,20 +964,66 @@ function toStatusCode(
|
|
|
663
964
|
case "UPSTREAM_RATE_LIMIT":
|
|
664
965
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
665
966
|
return 429;
|
|
967
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
968
|
+
// contract): the upstream evaluated the request and said no under
|
|
969
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
970
|
+
case "UPSTREAM_REJECTED":
|
|
971
|
+
return 409;
|
|
666
972
|
case "UPSTREAM_ERROR":
|
|
667
973
|
case "BLOCKED":
|
|
668
974
|
return 502;
|
|
669
975
|
case "STT_UNAVAILABLE":
|
|
670
976
|
case "UNSUPPORTED_STT_BACKEND":
|
|
977
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
671
978
|
return 503;
|
|
672
979
|
}
|
|
980
|
+
if (isTransportError(error)) {
|
|
981
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
982
|
+
}
|
|
983
|
+
if (isValidationError(error)) {
|
|
984
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
985
|
+
}
|
|
673
986
|
|
|
674
|
-
return
|
|
987
|
+
return 500;
|
|
675
988
|
}
|
|
676
989
|
|
|
677
990
|
return 500;
|
|
678
991
|
}
|
|
679
992
|
|
|
993
|
+
function sdkOwnsErrorResolution(error: unknown): boolean {
|
|
994
|
+
if (isSessionExpiredError(error)) return true;
|
|
995
|
+
if (isTransportError(error)) return true;
|
|
996
|
+
if (error instanceof z.ZodError) return true;
|
|
997
|
+
if (error instanceof StatefulRoutingDeadlineError) return true;
|
|
998
|
+
return (
|
|
999
|
+
isProviderError(error) &&
|
|
1000
|
+
typeof error.code === "string" &&
|
|
1001
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
|
|
1006
|
+
|
|
1007
|
+
function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
|
|
1008
|
+
return new Map(
|
|
1009
|
+
Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
1010
|
+
const errorCodes = operation.docs?.errorCodes;
|
|
1011
|
+
return errorCodes?.length
|
|
1012
|
+
? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
|
|
1013
|
+
: [];
|
|
1014
|
+
}),
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function declaredErrorCodeFor(
|
|
1019
|
+
error: unknown,
|
|
1020
|
+
operationId: string | undefined,
|
|
1021
|
+
lookup: OperationErrorCodeLookup,
|
|
1022
|
+
): OperationErrorCode | undefined {
|
|
1023
|
+
if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
|
|
1024
|
+
return lookup.get(operationId)?.get(error.code);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
680
1027
|
function extractRequestId(raw: unknown): string | undefined {
|
|
681
1028
|
if (!raw || typeof raw !== "object") {
|
|
682
1029
|
return undefined;
|
|
@@ -695,21 +1042,26 @@ function logProviderError(
|
|
|
695
1042
|
error: unknown,
|
|
696
1043
|
status: number,
|
|
697
1044
|
cost: ProviderRequestCost,
|
|
1045
|
+
declaredErrorCode?: OperationErrorCode,
|
|
698
1046
|
): void {
|
|
699
|
-
const code =
|
|
700
|
-
error
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1047
|
+
const code = isProviderError(error)
|
|
1048
|
+
? (error.code ?? "provider_error")
|
|
1049
|
+
: error instanceof z.ZodError
|
|
1050
|
+
? "invalid_request"
|
|
1051
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
1052
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
704
1053
|
: "internal_error";
|
|
705
1054
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
706
1055
|
const message = error instanceof Error ? error.message : String(error);
|
|
707
|
-
const details =
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
typeof
|
|
1056
|
+
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
1057
|
+
const isUnregisteredProviderErrorCode =
|
|
1058
|
+
status === 500 &&
|
|
1059
|
+
isProviderError(error) &&
|
|
1060
|
+
!isValidationError(error) &&
|
|
1061
|
+
typeof error.code === "string" &&
|
|
1062
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
|
|
1063
|
+
declaredErrorCode === undefined;
|
|
1064
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
713
1065
|
emit({
|
|
714
1066
|
level: status >= 500 ? "error" : "warn",
|
|
715
1067
|
event: "provider_request_failed",
|
|
@@ -722,14 +1074,15 @@ function logProviderError(
|
|
|
722
1074
|
code,
|
|
723
1075
|
errorClass,
|
|
724
1076
|
message,
|
|
725
|
-
...(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1077
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
1078
|
+
errorCategory: details.category,
|
|
1079
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
1080
|
+
retryable: details.retryable,
|
|
1081
|
+
...(isUnregisteredProviderErrorCode
|
|
729
1082
|
? {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1083
|
+
signal: "unregistered_provider_error_code" as const,
|
|
1084
|
+
signalFix:
|
|
1085
|
+
"Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
733
1086
|
}
|
|
734
1087
|
: {}),
|
|
735
1088
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
@@ -744,8 +1097,7 @@ function logProviderCleanupError(
|
|
|
744
1097
|
resource: "browser" | "stealth",
|
|
745
1098
|
error: unknown,
|
|
746
1099
|
): void {
|
|
747
|
-
const emit =
|
|
748
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1100
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
749
1101
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
750
1102
|
const message = error instanceof Error ? error.message : String(error);
|
|
751
1103
|
emit({
|
|
@@ -770,8 +1122,7 @@ function logProviderSuccess(
|
|
|
770
1122
|
status: number,
|
|
771
1123
|
cost: ProviderRequestCost,
|
|
772
1124
|
): void {
|
|
773
|
-
const emit =
|
|
774
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1125
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
775
1126
|
emit({
|
|
776
1127
|
level: "info",
|
|
777
1128
|
event: "provider_request_completed",
|
|
@@ -817,18 +1168,13 @@ function toJsonSuccessResponse(
|
|
|
817
1168
|
};
|
|
818
1169
|
}
|
|
819
1170
|
|
|
820
|
-
function isAsyncIterable<T = unknown>(
|
|
821
|
-
value: unknown,
|
|
822
|
-
): value is AsyncIterable<T> {
|
|
1171
|
+
function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
|
|
823
1172
|
if (!value || typeof value !== "object") return false;
|
|
824
1173
|
const iterator = Reflect.get(value, Symbol.asyncIterator);
|
|
825
1174
|
return typeof iterator === "function";
|
|
826
1175
|
}
|
|
827
1176
|
|
|
828
|
-
function responseWithCleanup(
|
|
829
|
-
response: Response,
|
|
830
|
-
cleanup: RequestCleanup,
|
|
831
|
-
): Response {
|
|
1177
|
+
function responseWithCleanup(response: Response, cleanup: RequestCleanup): Response {
|
|
832
1178
|
if (!response.body) {
|
|
833
1179
|
void cleanup();
|
|
834
1180
|
return response;
|
|
@@ -877,10 +1223,7 @@ async function validateSseEvent(
|
|
|
877
1223
|
const transport = getSseTransport(operation);
|
|
878
1224
|
const schema = transport?.events?.[event.event];
|
|
879
1225
|
if (!schema) {
|
|
880
|
-
if (
|
|
881
|
-
event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
882
|
-
event.event === APIFUSE_STREAM_DONE_EVENT
|
|
883
|
-
) {
|
|
1226
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
884
1227
|
return event;
|
|
885
1228
|
}
|
|
886
1229
|
throw new ProviderError(
|
|
@@ -893,11 +1236,7 @@ async function validateSseEvent(
|
|
|
893
1236
|
},
|
|
894
1237
|
);
|
|
895
1238
|
}
|
|
896
|
-
const data = await parseSchema(
|
|
897
|
-
schema,
|
|
898
|
-
event.data,
|
|
899
|
-
`transport.events.${event.event}`,
|
|
900
|
-
);
|
|
1239
|
+
const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
|
|
901
1240
|
return { ...event, data };
|
|
902
1241
|
}
|
|
903
1242
|
|
|
@@ -917,8 +1256,7 @@ function assertStreamPayloadWithinLimit(
|
|
|
917
1256
|
throw new ProviderError(
|
|
918
1257
|
`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
|
|
919
1258
|
{
|
|
920
|
-
code:
|
|
921
|
-
kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
1259
|
+
code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
922
1260
|
retryable: false,
|
|
923
1261
|
category: "input_validation",
|
|
924
1262
|
fix:
|
|
@@ -963,15 +1301,10 @@ function toSseResponse(
|
|
|
963
1301
|
const validated = await validateSseEvent(operation, next.value);
|
|
964
1302
|
const encodedEvent = encodeSseEvent(validated);
|
|
965
1303
|
const encodedBytes = encoder.encode(encodedEvent);
|
|
966
|
-
assertStreamPayloadWithinLimit(
|
|
967
|
-
encodedBytes.byteLength,
|
|
968
|
-
transport?.maxEventBytes,
|
|
969
|
-
"event",
|
|
970
|
-
);
|
|
1304
|
+
assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
|
|
971
1305
|
controller.enqueue(encodedBytes);
|
|
972
1306
|
} catch (error) {
|
|
973
|
-
const message =
|
|
974
|
-
error instanceof Error ? error.message : "Stream failed";
|
|
1307
|
+
const message = error instanceof Error ? error.message : "Stream failed";
|
|
975
1308
|
controller.enqueue(
|
|
976
1309
|
encoder.encode(
|
|
977
1310
|
encodeSseEvent(
|
|
@@ -1018,11 +1351,7 @@ function enforceStreamChunkLimit(
|
|
|
1018
1351
|
return;
|
|
1019
1352
|
}
|
|
1020
1353
|
if (value) {
|
|
1021
|
-
assertStreamPayloadWithinLimit(
|
|
1022
|
-
byteLength(value),
|
|
1023
|
-
maxChunkBytes,
|
|
1024
|
-
"chunk",
|
|
1025
|
-
);
|
|
1354
|
+
assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
|
|
1026
1355
|
controller.enqueue(value);
|
|
1027
1356
|
}
|
|
1028
1357
|
} catch (error) {
|
|
@@ -1042,10 +1371,7 @@ function toStreamingResponse(
|
|
|
1042
1371
|
requestId?: string,
|
|
1043
1372
|
): Response {
|
|
1044
1373
|
const transport = operation.transport?.kind ?? "json";
|
|
1045
|
-
if (
|
|
1046
|
-
transport === "sse" &&
|
|
1047
|
-
(result instanceof Response || result instanceof ReadableStream)
|
|
1048
|
-
) {
|
|
1374
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
1049
1375
|
void cleanup();
|
|
1050
1376
|
throw new ProviderError(
|
|
1051
1377
|
"SSE operations must return an AsyncIterable of typed stream.event(...) values.",
|
|
@@ -1059,20 +1385,13 @@ function toStreamingResponse(
|
|
|
1059
1385
|
}
|
|
1060
1386
|
if (result instanceof Response) {
|
|
1061
1387
|
const httpTransport = getHttpStreamTransport(operation);
|
|
1062
|
-
if (
|
|
1063
|
-
httpTransport &&
|
|
1064
|
-
result.body &&
|
|
1065
|
-
httpTransport?.maxChunkBytes !== undefined
|
|
1066
|
-
) {
|
|
1388
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
1067
1389
|
return responseWithCleanup(
|
|
1068
|
-
new Response(
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
statusText: result.statusText,
|
|
1074
|
-
},
|
|
1075
|
-
),
|
|
1390
|
+
new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
1391
|
+
headers: result.headers,
|
|
1392
|
+
status: result.status,
|
|
1393
|
+
statusText: result.statusText,
|
|
1394
|
+
}),
|
|
1076
1395
|
cleanup,
|
|
1077
1396
|
);
|
|
1078
1397
|
}
|
|
@@ -1092,8 +1411,7 @@ function toStreamingResponse(
|
|
|
1092
1411
|
: {
|
|
1093
1412
|
"Content-Type":
|
|
1094
1413
|
operation.transport?.kind === "http-stream"
|
|
1095
|
-
? (operation.transport.contentType ??
|
|
1096
|
-
"application/octet-stream")
|
|
1414
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
1097
1415
|
: "application/octet-stream",
|
|
1098
1416
|
},
|
|
1099
1417
|
}),
|
|
@@ -1113,18 +1431,14 @@ function toStreamingResponse(
|
|
|
1113
1431
|
);
|
|
1114
1432
|
}
|
|
1115
1433
|
|
|
1116
|
-
function getSseTransport(
|
|
1117
|
-
operation: OperationDefinition,
|
|
1118
|
-
): OperationSseTransport | undefined {
|
|
1434
|
+
function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
|
|
1119
1435
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
1120
1436
|
}
|
|
1121
1437
|
|
|
1122
1438
|
function getHttpStreamTransport(
|
|
1123
1439
|
operation: OperationDefinition,
|
|
1124
1440
|
): OperationHttpStreamTransport | undefined {
|
|
1125
|
-
return operation.transport?.kind === "http-stream"
|
|
1126
|
-
? operation.transport
|
|
1127
|
-
: undefined;
|
|
1441
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
1128
1442
|
}
|
|
1129
1443
|
|
|
1130
1444
|
function toAuthFlowResponse(
|
|
@@ -1145,9 +1459,7 @@ function toAuthFlowResponse(
|
|
|
1145
1459
|
};
|
|
1146
1460
|
}
|
|
1147
1461
|
|
|
1148
|
-
function authFlowLocaleFromHeaders(
|
|
1149
|
-
headers?: Record<string, string>,
|
|
1150
|
-
): ProviderLocale {
|
|
1462
|
+
function authFlowLocaleFromHeaders(headers?: Record<string, string>): ProviderLocale {
|
|
1151
1463
|
const header = Object.entries(headers ?? {}).find(
|
|
1152
1464
|
([key]) => key.toLowerCase() === "accept-language",
|
|
1153
1465
|
)?.[1];
|
|
@@ -1165,9 +1477,7 @@ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
|
|
|
1165
1477
|
}
|
|
1166
1478
|
|
|
1167
1479
|
function isAuthTurn(value: unknown): value is AuthTurn {
|
|
1168
|
-
return
|
|
1169
|
-
!!value && typeof value === "object" && "kind" in value && "turnId" in value
|
|
1170
|
-
);
|
|
1480
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
1171
1481
|
}
|
|
1172
1482
|
|
|
1173
1483
|
function loadAuthFlowLocaleCatalogs(
|
|
@@ -1204,10 +1514,7 @@ function materializeAuthFlowTurn(
|
|
|
1204
1514
|
});
|
|
1205
1515
|
}
|
|
1206
1516
|
|
|
1207
|
-
function withAuthRequestHeaders(
|
|
1208
|
-
request: AuthFlowRequest,
|
|
1209
|
-
headers: Headers,
|
|
1210
|
-
): AuthFlowRequest {
|
|
1517
|
+
function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
|
|
1211
1518
|
return {
|
|
1212
1519
|
...request,
|
|
1213
1520
|
headers: {
|
|
@@ -1225,17 +1532,9 @@ async function handleOperation(
|
|
|
1225
1532
|
state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
|
|
1226
1533
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
1227
1534
|
): Promise<Response | OperationResponse> {
|
|
1228
|
-
const ctx = createProviderContext(
|
|
1229
|
-
provider,
|
|
1230
|
-
request,
|
|
1231
|
-
operationId,
|
|
1232
|
-
options,
|
|
1233
|
-
state,
|
|
1234
|
-
proxyTelemetry,
|
|
1235
|
-
);
|
|
1535
|
+
const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
|
|
1236
1536
|
const operation = provider.operations[operationId];
|
|
1237
|
-
const streaming =
|
|
1238
|
-
operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1537
|
+
const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1239
1538
|
let cleanupCalled = false;
|
|
1240
1539
|
const cleanup = async () => {
|
|
1241
1540
|
if (cleanupCalled) return;
|
|
@@ -1266,12 +1565,14 @@ async function handleOperation(
|
|
|
1266
1565
|
}
|
|
1267
1566
|
};
|
|
1268
1567
|
try {
|
|
1269
|
-
const result =
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1568
|
+
const result = options.operationExecutor
|
|
1569
|
+
? await options.operationExecutor({
|
|
1570
|
+
provider,
|
|
1571
|
+
operationId,
|
|
1572
|
+
ctx,
|
|
1573
|
+
request,
|
|
1574
|
+
})
|
|
1575
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
1275
1576
|
if (streaming && operation) {
|
|
1276
1577
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
1277
1578
|
}
|
|
@@ -1315,13 +1616,16 @@ async function handleAuthFlow(
|
|
|
1315
1616
|
});
|
|
1316
1617
|
}
|
|
1317
1618
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
);
|
|
1619
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
1620
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
1621
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1622
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1623
|
+
// stranded flow even when provisioning is broken.
|
|
1624
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
1324
1625
|
try {
|
|
1626
|
+
if (route !== "abort") {
|
|
1627
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
1628
|
+
}
|
|
1325
1629
|
const result =
|
|
1326
1630
|
route === "start"
|
|
1327
1631
|
? await flow.start(context)
|
|
@@ -1363,12 +1667,143 @@ async function handleAuthFlow(
|
|
|
1363
1667
|
}
|
|
1364
1668
|
}
|
|
1365
1669
|
|
|
1670
|
+
class StatefulForwardingReplayCache {
|
|
1671
|
+
readonly #nonces = new Map<string, number>();
|
|
1672
|
+
readonly #expiryBuckets = new Map<number, Set<string>>();
|
|
1673
|
+
#nextExpiryBucket?: number;
|
|
1674
|
+
#latestExpiryBucket?: number;
|
|
1675
|
+
|
|
1676
|
+
constructor(private readonly maxEntries: number) {}
|
|
1677
|
+
|
|
1678
|
+
claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
|
|
1679
|
+
this.dropExpiredBuckets(nowMs);
|
|
1680
|
+
if (this.#nonces.has(nonce)) return "replayed";
|
|
1681
|
+
if (this.#nonces.size >= this.maxEntries) return "full";
|
|
1682
|
+
const expiryBucket =
|
|
1683
|
+
Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1684
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1685
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1686
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
|
|
1687
|
+
bucket.add(nonce);
|
|
1688
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1689
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1690
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1691
|
+
return "accepted";
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
private dropExpiredBuckets(nowMs: number): void {
|
|
1695
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
|
|
1696
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1697
|
+
this.#nonces.clear();
|
|
1698
|
+
this.#expiryBuckets.clear();
|
|
1699
|
+
this.#nextExpiryBucket = undefined;
|
|
1700
|
+
this.#latestExpiryBucket = undefined;
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1704
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1705
|
+
if (bucket) {
|
|
1706
|
+
for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
|
|
1707
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1708
|
+
}
|
|
1709
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function verifyStatefulForwardingRequest(input: {
|
|
1715
|
+
readonly options: ProviderServerOptions;
|
|
1716
|
+
readonly rawBody: string;
|
|
1717
|
+
readonly headers: Headers;
|
|
1718
|
+
readonly method: string;
|
|
1719
|
+
readonly path: string;
|
|
1720
|
+
readonly replayCache: StatefulForwardingReplayCache;
|
|
1721
|
+
}): void {
|
|
1722
|
+
const config = input.options.statefulForwarding;
|
|
1723
|
+
if (!config?.secret) {
|
|
1724
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1725
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1729
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1730
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1731
|
+
if (!timestamp || !signature || !nonce) {
|
|
1732
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1733
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
if (nonce.length > 256) {
|
|
1737
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1738
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
const timestampMs = Date.parse(timestamp);
|
|
1742
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1743
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1744
|
+
throw new ProviderError(
|
|
1745
|
+
"Stateful forwarding signature timestamp is outside the allowed skew.",
|
|
1746
|
+
{ code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
if (
|
|
1750
|
+
!verifyStatefulRequestSignature({
|
|
1751
|
+
secret: config.secret,
|
|
1752
|
+
timestamp,
|
|
1753
|
+
rawBody: input.rawBody,
|
|
1754
|
+
method: input.method,
|
|
1755
|
+
path: input.path,
|
|
1756
|
+
nonce,
|
|
1757
|
+
signature,
|
|
1758
|
+
})
|
|
1759
|
+
) {
|
|
1760
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1761
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1765
|
+
if (replayResult === "replayed") {
|
|
1766
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1767
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
if (replayResult === "full") {
|
|
1771
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1772
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function operationRequestFromForwardingEnvelope(
|
|
1778
|
+
envelope: ProviderServerStatefulForwardEnvelope,
|
|
1779
|
+
): OperationRequest & { readonly deadlineAt?: string } {
|
|
1780
|
+
return {
|
|
1781
|
+
...envelope.operationRequest,
|
|
1782
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function parseStatefulForwardingEnvelope(rawBody: unknown): ProviderServerStatefulForwardEnvelope {
|
|
1787
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1788
|
+
if (parsed.success) return parsed.data;
|
|
1789
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1790
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1791
|
+
details: zodDetails(parsed.error),
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1366
1795
|
export function createServerApp(
|
|
1367
1796
|
provider: ProviderDefinition,
|
|
1368
1797
|
options: ProviderServerOptions = {},
|
|
1369
1798
|
): Hono {
|
|
1799
|
+
validateStatefulServerConfig(options);
|
|
1370
1800
|
const app = new Hono();
|
|
1371
1801
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1802
|
+
const operationErrorCodes = buildOperationErrorCodeLookup(provider);
|
|
1803
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
|
|
1804
|
+
options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1805
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
|
|
1806
|
+
);
|
|
1372
1807
|
const state =
|
|
1373
1808
|
options.state ??
|
|
1374
1809
|
createProviderRuntimeStateFromEnv({
|
|
@@ -1376,17 +1811,28 @@ export function createServerApp(
|
|
|
1376
1811
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
1377
1812
|
});
|
|
1378
1813
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
404,
|
|
1388
|
-
),
|
|
1814
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1815
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1816
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1817
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1818
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1819
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(
|
|
1820
|
+
provider,
|
|
1821
|
+
createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
1389
1822
|
);
|
|
1823
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1824
|
+
logger({
|
|
1825
|
+
level: "warn",
|
|
1826
|
+
event: "provider_secrets_missing",
|
|
1827
|
+
providerId: provider.id,
|
|
1828
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
app.notFound((c) => {
|
|
1833
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1834
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1835
|
+
});
|
|
1390
1836
|
|
|
1391
1837
|
app.get("/health", (c) =>
|
|
1392
1838
|
c.json({
|
|
@@ -1396,6 +1842,169 @@ export function createServerApp(
|
|
|
1396
1842
|
}),
|
|
1397
1843
|
);
|
|
1398
1844
|
|
|
1845
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1846
|
+
let rawBodyText = "";
|
|
1847
|
+
let rawBody: unknown;
|
|
1848
|
+
let operationId: string | undefined;
|
|
1849
|
+
const operation = "stateful-internal";
|
|
1850
|
+
const requestCost = startRequestCost();
|
|
1851
|
+
try {
|
|
1852
|
+
if (!options.internalOperationExecutor) {
|
|
1853
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1854
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1858
|
+
verifyStatefulForwardingRequest({
|
|
1859
|
+
options,
|
|
1860
|
+
rawBody: rawBodyText,
|
|
1861
|
+
headers: c.req.raw.headers,
|
|
1862
|
+
method: c.req.raw.method,
|
|
1863
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1864
|
+
replayCache: statefulForwardingReplayCache,
|
|
1865
|
+
});
|
|
1866
|
+
try {
|
|
1867
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1868
|
+
} catch {
|
|
1869
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1870
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1874
|
+
if (envelope.providerId !== provider.id) {
|
|
1875
|
+
throw new ProviderError(
|
|
1876
|
+
"Stateful forwarding envelope providerId does not match the served provider.",
|
|
1877
|
+
{ code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1881
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1882
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
if (
|
|
1886
|
+
envelope.sourcePodId !==
|
|
1887
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
|
|
1888
|
+
) {
|
|
1889
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1890
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
if (
|
|
1894
|
+
envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
|
|
1895
|
+
) {
|
|
1896
|
+
throw new ProviderError(
|
|
1897
|
+
"Stateful forwarding forwardedAt does not match its signature timestamp.",
|
|
1898
|
+
{ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1902
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1903
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1904
|
+
}
|
|
1905
|
+
const remainingDeadlineMs =
|
|
1906
|
+
deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1907
|
+
const deadlineSignal =
|
|
1908
|
+
remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1909
|
+
const signal = deadlineSignal
|
|
1910
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1911
|
+
: c.req.raw.signal;
|
|
1912
|
+
const ownerFenceValidation = Promise.resolve(
|
|
1913
|
+
options.statefulForwarding?.validateOwnerFence(
|
|
1914
|
+
{
|
|
1915
|
+
providerId: envelope.providerId,
|
|
1916
|
+
sessionKey: envelope.sessionKey,
|
|
1917
|
+
ownerPodId: envelope.ownerPodId,
|
|
1918
|
+
generation: envelope.generation,
|
|
1919
|
+
sourcePodId: envelope.sourcePodId,
|
|
1920
|
+
forwardedAt: envelope.forwardedAt,
|
|
1921
|
+
requestId: envelope.requestId,
|
|
1922
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1923
|
+
},
|
|
1924
|
+
signal,
|
|
1925
|
+
),
|
|
1926
|
+
);
|
|
1927
|
+
let ownerFenceValid: boolean | undefined;
|
|
1928
|
+
try {
|
|
1929
|
+
ownerFenceValid = deadlineSignal
|
|
1930
|
+
? await Promise.race([
|
|
1931
|
+
ownerFenceValidation,
|
|
1932
|
+
new Promise<never>((_resolve, reject) => {
|
|
1933
|
+
deadlineSignal.addEventListener(
|
|
1934
|
+
"abort",
|
|
1935
|
+
() =>
|
|
1936
|
+
reject(
|
|
1937
|
+
new StatefulRoutingDeadlineError(
|
|
1938
|
+
envelope.requestId,
|
|
1939
|
+
envelope.deadlineAt as string,
|
|
1940
|
+
),
|
|
1941
|
+
),
|
|
1942
|
+
{ once: true },
|
|
1943
|
+
);
|
|
1944
|
+
}),
|
|
1945
|
+
])
|
|
1946
|
+
: await ownerFenceValidation;
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
if (deadlineSignal?.aborted) {
|
|
1949
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1950
|
+
}
|
|
1951
|
+
throw error;
|
|
1952
|
+
}
|
|
1953
|
+
if (ownerFenceValid !== true) {
|
|
1954
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1955
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1959
|
+
operationId = envelope.operationId;
|
|
1960
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1961
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1962
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1963
|
+
}
|
|
1964
|
+
const output = await options.internalOperationExecutor({
|
|
1965
|
+
provider,
|
|
1966
|
+
operationId,
|
|
1967
|
+
ctx,
|
|
1968
|
+
request,
|
|
1969
|
+
internalStatefulForward: envelope,
|
|
1970
|
+
signal,
|
|
1971
|
+
});
|
|
1972
|
+
logProviderSuccess(
|
|
1973
|
+
logger,
|
|
1974
|
+
provider,
|
|
1975
|
+
"operation",
|
|
1976
|
+
operationId || operation,
|
|
1977
|
+
request.requestId,
|
|
1978
|
+
200,
|
|
1979
|
+
finishRequestCost(requestCost),
|
|
1980
|
+
);
|
|
1981
|
+
return c.json({ data: output });
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1984
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1985
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1986
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1987
|
+
}
|
|
1988
|
+
const requestId = extractRequestId(rawBody);
|
|
1989
|
+
logProviderError(
|
|
1990
|
+
logger,
|
|
1991
|
+
provider,
|
|
1992
|
+
"operation",
|
|
1993
|
+
operationId || operation,
|
|
1994
|
+
requestId,
|
|
1995
|
+
error,
|
|
1996
|
+
status,
|
|
1997
|
+
finishRequestCost(requestCost),
|
|
1998
|
+
declaredErrorCode,
|
|
1999
|
+
);
|
|
2000
|
+
return responseWithErrorObservability(
|
|
2001
|
+
c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
|
|
2002
|
+
error,
|
|
2003
|
+
declaredErrorCode,
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
|
|
1399
2008
|
app.post("/v1/:operation", async (c) => {
|
|
1400
2009
|
let rawBody: unknown;
|
|
1401
2010
|
const operation = c.req.param("operation");
|
|
@@ -1442,7 +2051,8 @@ export function createServerApp(
|
|
|
1442
2051
|
);
|
|
1443
2052
|
return c.json(response);
|
|
1444
2053
|
} catch (error) {
|
|
1445
|
-
const
|
|
2054
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
2055
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1446
2056
|
const requestId = extractRequestId(rawBody);
|
|
1447
2057
|
logProviderError(
|
|
1448
2058
|
logger,
|
|
@@ -1453,10 +2063,15 @@ export function createServerApp(
|
|
|
1453
2063
|
error,
|
|
1454
2064
|
status,
|
|
1455
2065
|
finishRequestCost(requestCost),
|
|
2066
|
+
declaredErrorCode,
|
|
1456
2067
|
);
|
|
1457
2068
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1458
2069
|
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1459
|
-
return
|
|
2070
|
+
return responseWithErrorObservability(
|
|
2071
|
+
c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
|
|
2072
|
+
error,
|
|
2073
|
+
declaredErrorCode,
|
|
2074
|
+
);
|
|
1460
2075
|
}
|
|
1461
2076
|
});
|
|
1462
2077
|
|
|
@@ -1468,17 +2083,8 @@ export function createServerApp(
|
|
|
1468
2083
|
.clone()
|
|
1469
2084
|
.json()
|
|
1470
2085
|
.catch(() => undefined);
|
|
1471
|
-
const body = withAuthRequestHeaders(
|
|
1472
|
-
|
|
1473
|
-
c.req.raw.headers,
|
|
1474
|
-
);
|
|
1475
|
-
const response = await handleAuthFlow(
|
|
1476
|
-
provider,
|
|
1477
|
-
body,
|
|
1478
|
-
"start",
|
|
1479
|
-
options,
|
|
1480
|
-
c.req.raw.signal,
|
|
1481
|
-
);
|
|
2086
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2087
|
+
const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
|
|
1482
2088
|
logProviderSuccess(
|
|
1483
2089
|
logger,
|
|
1484
2090
|
provider,
|
|
@@ -1502,7 +2108,10 @@ export function createServerApp(
|
|
|
1502
2108
|
status,
|
|
1503
2109
|
finishRequestCost(requestCost),
|
|
1504
2110
|
);
|
|
1505
|
-
return
|
|
2111
|
+
return responseWithErrorObservability(
|
|
2112
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2113
|
+
error,
|
|
2114
|
+
);
|
|
1506
2115
|
}
|
|
1507
2116
|
});
|
|
1508
2117
|
|
|
@@ -1514,17 +2123,8 @@ export function createServerApp(
|
|
|
1514
2123
|
.clone()
|
|
1515
2124
|
.json()
|
|
1516
2125
|
.catch(() => undefined);
|
|
1517
|
-
const body = withAuthRequestHeaders(
|
|
1518
|
-
|
|
1519
|
-
c.req.raw.headers,
|
|
1520
|
-
);
|
|
1521
|
-
const response = await handleAuthFlow(
|
|
1522
|
-
provider,
|
|
1523
|
-
body,
|
|
1524
|
-
"continue",
|
|
1525
|
-
options,
|
|
1526
|
-
c.req.raw.signal,
|
|
1527
|
-
);
|
|
2126
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2127
|
+
const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
|
|
1528
2128
|
logProviderSuccess(
|
|
1529
2129
|
logger,
|
|
1530
2130
|
provider,
|
|
@@ -1548,7 +2148,10 @@ export function createServerApp(
|
|
|
1548
2148
|
status,
|
|
1549
2149
|
finishRequestCost(requestCost),
|
|
1550
2150
|
);
|
|
1551
|
-
return
|
|
2151
|
+
return responseWithErrorObservability(
|
|
2152
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2153
|
+
error,
|
|
2154
|
+
);
|
|
1552
2155
|
}
|
|
1553
2156
|
});
|
|
1554
2157
|
|
|
@@ -1560,17 +2163,8 @@ export function createServerApp(
|
|
|
1560
2163
|
.clone()
|
|
1561
2164
|
.json()
|
|
1562
2165
|
.catch(() => undefined);
|
|
1563
|
-
const body = withAuthRequestHeaders(
|
|
1564
|
-
|
|
1565
|
-
c.req.raw.headers,
|
|
1566
|
-
);
|
|
1567
|
-
const response = await handleAuthFlow(
|
|
1568
|
-
provider,
|
|
1569
|
-
body,
|
|
1570
|
-
"poll",
|
|
1571
|
-
options,
|
|
1572
|
-
c.req.raw.signal,
|
|
1573
|
-
);
|
|
2166
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2167
|
+
const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
|
|
1574
2168
|
logProviderSuccess(
|
|
1575
2169
|
logger,
|
|
1576
2170
|
provider,
|
|
@@ -1594,7 +2188,10 @@ export function createServerApp(
|
|
|
1594
2188
|
status,
|
|
1595
2189
|
finishRequestCost(requestCost),
|
|
1596
2190
|
);
|
|
1597
|
-
return
|
|
2191
|
+
return responseWithErrorObservability(
|
|
2192
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2193
|
+
error,
|
|
2194
|
+
);
|
|
1598
2195
|
}
|
|
1599
2196
|
});
|
|
1600
2197
|
|
|
@@ -1606,17 +2203,8 @@ export function createServerApp(
|
|
|
1606
2203
|
.clone()
|
|
1607
2204
|
.json()
|
|
1608
2205
|
.catch(() => undefined);
|
|
1609
|
-
const body = withAuthRequestHeaders(
|
|
1610
|
-
|
|
1611
|
-
c.req.raw.headers,
|
|
1612
|
-
);
|
|
1613
|
-
const response = await handleAuthFlow(
|
|
1614
|
-
provider,
|
|
1615
|
-
body,
|
|
1616
|
-
"refresh",
|
|
1617
|
-
options,
|
|
1618
|
-
c.req.raw.signal,
|
|
1619
|
-
);
|
|
2206
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2207
|
+
const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
|
|
1620
2208
|
logProviderSuccess(
|
|
1621
2209
|
logger,
|
|
1622
2210
|
provider,
|
|
@@ -1640,7 +2228,10 @@ export function createServerApp(
|
|
|
1640
2228
|
status,
|
|
1641
2229
|
finishRequestCost(requestCost),
|
|
1642
2230
|
);
|
|
1643
|
-
return
|
|
2231
|
+
return responseWithErrorObservability(
|
|
2232
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2233
|
+
error,
|
|
2234
|
+
);
|
|
1644
2235
|
}
|
|
1645
2236
|
});
|
|
1646
2237
|
|
|
@@ -1652,17 +2243,8 @@ export function createServerApp(
|
|
|
1652
2243
|
.clone()
|
|
1653
2244
|
.json()
|
|
1654
2245
|
.catch(() => undefined);
|
|
1655
|
-
const body = withAuthRequestHeaders(
|
|
1656
|
-
|
|
1657
|
-
c.req.raw.headers,
|
|
1658
|
-
);
|
|
1659
|
-
const response = await handleAuthFlow(
|
|
1660
|
-
provider,
|
|
1661
|
-
body,
|
|
1662
|
-
"abort",
|
|
1663
|
-
options,
|
|
1664
|
-
c.req.raw.signal,
|
|
1665
|
-
);
|
|
2246
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2247
|
+
const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
|
|
1666
2248
|
logProviderSuccess(
|
|
1667
2249
|
logger,
|
|
1668
2250
|
provider,
|
|
@@ -1686,19 +2268,64 @@ export function createServerApp(
|
|
|
1686
2268
|
status,
|
|
1687
2269
|
finishRequestCost(requestCost),
|
|
1688
2270
|
);
|
|
1689
|
-
return
|
|
2271
|
+
return responseWithErrorObservability(
|
|
2272
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2273
|
+
error,
|
|
2274
|
+
);
|
|
1690
2275
|
}
|
|
1691
2276
|
});
|
|
1692
2277
|
|
|
1693
2278
|
return app;
|
|
1694
2279
|
}
|
|
1695
2280
|
|
|
2281
|
+
function validateStatefulServerConfig(options: ProviderServerOptions): void {
|
|
2282
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
"Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
2288
|
+
throw new Error(
|
|
2289
|
+
"Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
if (
|
|
2293
|
+
options.statefulForwarding &&
|
|
2294
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function"
|
|
2295
|
+
) {
|
|
2296
|
+
throw new Error(
|
|
2297
|
+
"Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
if (
|
|
2301
|
+
options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
2302
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
2303
|
+
options.statefulForwarding.maxSkewMs <= 0)
|
|
2304
|
+
) {
|
|
2305
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
2306
|
+
}
|
|
2307
|
+
if (
|
|
2308
|
+
options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
2309
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
2310
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)
|
|
2311
|
+
) {
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
"Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1696
2318
|
type BunServeRuntime = {
|
|
1697
2319
|
serve: (options: {
|
|
1698
2320
|
port: number;
|
|
1699
2321
|
hostname: string;
|
|
1700
2322
|
fetch: (request: Request) => Response | Promise<Response>;
|
|
1701
|
-
}) =>
|
|
2323
|
+
}) => BunServerHandle;
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
type BunServerHandle = {
|
|
2327
|
+
readonly port: number;
|
|
2328
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
1702
2329
|
};
|
|
1703
2330
|
|
|
1704
2331
|
function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
@@ -1714,11 +2341,20 @@ function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
|
1714
2341
|
|
|
1715
2342
|
return {
|
|
1716
2343
|
serve(options) {
|
|
1717
|
-
return serve(options);
|
|
2344
|
+
return serve(options) as BunServerHandle;
|
|
1718
2345
|
},
|
|
1719
2346
|
};
|
|
1720
2347
|
}
|
|
1721
2348
|
|
|
2349
|
+
export type ProviderServerCloseOptions = {
|
|
2350
|
+
readonly timeoutMs?: number;
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
export type ProviderServerHandle = {
|
|
2354
|
+
readonly port: number;
|
|
2355
|
+
close(options?: ProviderServerCloseOptions): Promise<void>;
|
|
2356
|
+
};
|
|
2357
|
+
|
|
1722
2358
|
export interface ServeOptions extends ProviderServerOptions {
|
|
1723
2359
|
host?: string;
|
|
1724
2360
|
port?: number;
|
|
@@ -1730,46 +2366,233 @@ export interface ServeOptions extends ProviderServerOptions {
|
|
|
1730
2366
|
selfTestPort?: number;
|
|
1731
2367
|
}
|
|
1732
2368
|
|
|
2369
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
2370
|
+
const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
|
|
2371
|
+
|
|
2372
|
+
type SignalServerRegistration = {
|
|
2373
|
+
readonly signals: ReadonlySet<NodeJS.Signals>;
|
|
2374
|
+
readonly close: () => Promise<void>;
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
type ProcessSignalCoordinator = {
|
|
2378
|
+
readonly registrations: Set<SignalServerRegistration>;
|
|
2379
|
+
readonly listener: () => void;
|
|
2380
|
+
handling: boolean;
|
|
2381
|
+
};
|
|
2382
|
+
|
|
2383
|
+
const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
|
|
2384
|
+
|
|
1733
2385
|
export async function serve(
|
|
1734
2386
|
provider: ProviderDefinition,
|
|
1735
2387
|
options: ServeOptions = {},
|
|
1736
|
-
): Promise<
|
|
2388
|
+
): Promise<ProviderServerHandle> {
|
|
1737
2389
|
const bunRuntime = getBunServeRuntime();
|
|
1738
2390
|
|
|
1739
2391
|
if (bunRuntime === undefined) {
|
|
1740
|
-
throw new ProviderError(
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
code: "RUNTIME_UNSUPPORTED",
|
|
1744
|
-
},
|
|
1745
|
-
);
|
|
2392
|
+
throw new ProviderError("Bun runtime is required to start the provider server", {
|
|
2393
|
+
code: "RUNTIME_UNSUPPORTED",
|
|
2394
|
+
});
|
|
1746
2395
|
}
|
|
2396
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
2397
|
+
const configuredTimeoutMs = shutdownTimeout(
|
|
2398
|
+
options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
2399
|
+
);
|
|
2400
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1747
2401
|
|
|
1748
2402
|
const app = createServerApp(provider, {
|
|
1749
2403
|
logger: options.logger,
|
|
1750
2404
|
stt: options.stt,
|
|
2405
|
+
state: options.state,
|
|
2406
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
2407
|
+
operationExecutor: options.operationExecutor,
|
|
2408
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
2409
|
+
statefulForwarding: options.statefulForwarding,
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
const servers: BunServerHandle[] = [];
|
|
2413
|
+
try {
|
|
2414
|
+
servers.push(
|
|
2415
|
+
bunRuntime.serve({
|
|
2416
|
+
port: options.port ?? DEFAULT_PORT,
|
|
2417
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2418
|
+
fetch: app.fetch,
|
|
2419
|
+
}),
|
|
2420
|
+
);
|
|
2421
|
+
|
|
2422
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
2423
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
2424
|
+
// starts when the shared self-test master secret env is present.
|
|
2425
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
2426
|
+
if (selfTestSecrets) {
|
|
2427
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
2428
|
+
secrets: selfTestSecrets,
|
|
2429
|
+
invoke: createSelfTestInvoke(app),
|
|
2430
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
2431
|
+
});
|
|
2432
|
+
servers.push(
|
|
2433
|
+
bunRuntime.serve({
|
|
2434
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
2435
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2436
|
+
fetch: selfTestApp.fetch,
|
|
2437
|
+
}),
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2442
|
+
throw error;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
const server = servers[0];
|
|
2446
|
+
if (!server) throw new Error("Provider server failed to create its primary listener.");
|
|
2447
|
+
let closePromise: Promise<void> | undefined;
|
|
2448
|
+
let unregisterSignals = () => {};
|
|
2449
|
+
|
|
2450
|
+
const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
|
|
2451
|
+
if (closePromise) return closePromise;
|
|
2452
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
2453
|
+
closePromise = closeProviderServers({
|
|
2454
|
+
servers,
|
|
2455
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
2456
|
+
timeoutMs,
|
|
2457
|
+
logger,
|
|
2458
|
+
providerId: provider.id,
|
|
2459
|
+
}).finally(() => unregisterSignals());
|
|
2460
|
+
return closePromise;
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
try {
|
|
2464
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () =>
|
|
2465
|
+
close({ timeoutMs: configuredTimeoutMs }),
|
|
2466
|
+
);
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
unregisterSignals();
|
|
2469
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2470
|
+
throw error;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
return { port: server.port, close };
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function registerForProcessSignals(
|
|
2477
|
+
signals: NodeJS.Signals[],
|
|
2478
|
+
close: () => Promise<void>,
|
|
2479
|
+
): () => void {
|
|
2480
|
+
if (signals.length === 0) return () => {};
|
|
2481
|
+
const registration: SignalServerRegistration = {
|
|
2482
|
+
signals: new Set(signals),
|
|
2483
|
+
close,
|
|
2484
|
+
};
|
|
2485
|
+
let registered = true;
|
|
2486
|
+
const unregister = () => {
|
|
2487
|
+
if (!registered) return;
|
|
2488
|
+
registered = false;
|
|
2489
|
+
for (const signal of registration.signals) {
|
|
2490
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
2491
|
+
if (!coordinator) continue;
|
|
2492
|
+
coordinator.registrations.delete(registration);
|
|
2493
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
2494
|
+
process.removeListener(signal, coordinator.listener);
|
|
2495
|
+
processSignalCoordinators.delete(signal);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
try {
|
|
2500
|
+
for (const signal of signals) {
|
|
2501
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
2502
|
+
if (!coordinator) {
|
|
2503
|
+
const created: ProcessSignalCoordinator = {
|
|
2504
|
+
registrations: new Set(),
|
|
2505
|
+
handling: false,
|
|
2506
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
2507
|
+
};
|
|
2508
|
+
coordinator = created;
|
|
2509
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
2510
|
+
process.on(signal, coordinator.listener);
|
|
2511
|
+
}
|
|
2512
|
+
coordinator.registrations.add(registration);
|
|
2513
|
+
}
|
|
2514
|
+
} catch (error) {
|
|
2515
|
+
unregister();
|
|
2516
|
+
throw error;
|
|
2517
|
+
}
|
|
2518
|
+
return unregister;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function handleCoordinatedSignal(
|
|
2522
|
+
signal: NodeJS.Signals,
|
|
2523
|
+
coordinator: ProcessSignalCoordinator,
|
|
2524
|
+
): void {
|
|
2525
|
+
if (coordinator.handling) return;
|
|
2526
|
+
coordinator.handling = true;
|
|
2527
|
+
const registrations = [...coordinator.registrations];
|
|
2528
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
2529
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
2530
|
+
process.removeListener(signal, coordinator.listener);
|
|
2531
|
+
processSignalCoordinators.delete(signal);
|
|
2532
|
+
}
|
|
2533
|
+
try {
|
|
2534
|
+
process.kill(process.pid, signal);
|
|
2535
|
+
} catch {
|
|
2536
|
+
process.exitCode = 1;
|
|
2537
|
+
}
|
|
1751
2538
|
});
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
async function closeProviderServers(input: {
|
|
2542
|
+
readonly servers: BunServerHandle[];
|
|
2543
|
+
readonly hooks: Array<() => Promise<void>>;
|
|
2544
|
+
readonly timeoutMs: number;
|
|
2545
|
+
readonly logger: ProviderServerLogger;
|
|
2546
|
+
readonly providerId: string;
|
|
2547
|
+
}): Promise<void> {
|
|
2548
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
2549
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
2550
|
+
for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
|
|
2551
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
2552
|
+
try {
|
|
2553
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
try {
|
|
2556
|
+
input.logger({
|
|
2557
|
+
level: "error",
|
|
2558
|
+
event: "provider_shutdown_hook_failed",
|
|
2559
|
+
providerId: input.providerId,
|
|
2560
|
+
hookIndex,
|
|
2561
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
2562
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
2563
|
+
});
|
|
2564
|
+
} catch {}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
2568
|
+
await withinShutdownBudget(
|
|
2569
|
+
Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
|
|
2570
|
+
deadline,
|
|
2571
|
+
).catch(() => undefined);
|
|
2572
|
+
}
|
|
1752
2573
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2574
|
+
async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
|
|
2575
|
+
promise.catch(() => undefined);
|
|
2576
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
2577
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2578
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
2579
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1757
2580
|
});
|
|
2581
|
+
try {
|
|
2582
|
+
return await Promise.race([promise, timeout]);
|
|
2583
|
+
} finally {
|
|
2584
|
+
if (timer) clearTimeout(timer);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
1758
2587
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
});
|
|
1768
|
-
bunRuntime.serve({
|
|
1769
|
-
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1770
|
-
hostname: options.host ?? DEFAULT_HOST,
|
|
1771
|
-
fetch: selfTestApp.fetch,
|
|
1772
|
-
});
|
|
2588
|
+
function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
|
|
2589
|
+
if (signals === false) return [];
|
|
2590
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
function shutdownTimeout(value: number): number {
|
|
2594
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2595
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1773
2596
|
}
|
|
1774
|
-
|
|
2597
|
+
return value;
|
|
1775
2598
|
}
|