@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +487 -0
- package/CHANGELOG.md +85 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +208 -125
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +115 -4
- package/dist/server/serve.js +792 -127
- package/dist/server/types.d.ts +30 -9
- package/dist/server/types.js +8 -7
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +477 -33
- package/dist/types.d.ts +323 -11
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +368 -504
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +111 -42
- package/src/lint.ts +89 -159
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +65 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1141 -324
- package/src/server/types.ts +12 -19
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +652 -79
- package/src/types.ts +369 -11
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/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),
|
|
@@ -386,13 +485,23 @@ function createAuthFlowContext(
|
|
|
386
485
|
http: createHttpClient(baseUrl, proxyClientOptions),
|
|
387
486
|
stealth: stealthBaseUrl
|
|
388
487
|
? stealthProfile
|
|
389
|
-
? createStealthClient(
|
|
390
|
-
stealthBaseUrl,
|
|
391
|
-
stealthProfile.name,
|
|
392
|
-
stealthClientOptions,
|
|
393
|
-
)
|
|
488
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
394
489
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
395
490
|
: createStealthStub(),
|
|
491
|
+
...(provider.native
|
|
492
|
+
? {
|
|
493
|
+
native: {
|
|
494
|
+
network: createNativeNetworkClient({
|
|
495
|
+
egress: provider.native.network,
|
|
496
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
497
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
498
|
+
credentials: createEnvVendorCredentialResolver(
|
|
499
|
+
createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
500
|
+
),
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
: {}),
|
|
396
505
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
397
506
|
credential,
|
|
398
507
|
context: flowContextStore.context,
|
|
@@ -433,8 +542,16 @@ export type ProviderServerLogEvent =
|
|
|
433
542
|
errorCategory?: ProviderErrorCategory;
|
|
434
543
|
taxonomyVersion?: string;
|
|
435
544
|
retryable?: boolean;
|
|
545
|
+
signal?: "unregistered_provider_error_code";
|
|
546
|
+
signalFix?: string;
|
|
436
547
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
437
548
|
})
|
|
549
|
+
| {
|
|
550
|
+
level: "warn";
|
|
551
|
+
event: "provider_secrets_missing";
|
|
552
|
+
providerId: string;
|
|
553
|
+
missingSecrets: string[];
|
|
554
|
+
}
|
|
438
555
|
| {
|
|
439
556
|
level: "warn";
|
|
440
557
|
event: "provider_cleanup_failed";
|
|
@@ -445,18 +562,59 @@ export type ProviderServerLogEvent =
|
|
|
445
562
|
resource: "browser" | "stealth";
|
|
446
563
|
errorClass: string;
|
|
447
564
|
message: string;
|
|
565
|
+
}
|
|
566
|
+
| {
|
|
567
|
+
level: "error";
|
|
568
|
+
event: "provider_shutdown_hook_failed";
|
|
569
|
+
providerId: string;
|
|
570
|
+
hookIndex: number;
|
|
571
|
+
errorClass: string;
|
|
572
|
+
message: string;
|
|
448
573
|
};
|
|
449
574
|
|
|
450
575
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
451
576
|
|
|
452
577
|
export type ProviderServerOptions = {
|
|
453
578
|
logger?: ProviderServerLogger;
|
|
579
|
+
/** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
|
|
580
|
+
operationExecutor?: ProviderServerOperationExecutor;
|
|
581
|
+
/** Optional signed internal executor for stateful owner forwarding. */
|
|
582
|
+
internalOperationExecutor?: ProviderServerOperationExecutor;
|
|
583
|
+
statefulForwarding?: {
|
|
584
|
+
readonly secret: string;
|
|
585
|
+
readonly maxSkewMs?: number;
|
|
586
|
+
readonly replayCacheMaxEntries?: number;
|
|
587
|
+
/** Required fail-closed check against the SDK/runtime owner registry. */
|
|
588
|
+
readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
|
|
589
|
+
};
|
|
454
590
|
/** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
|
|
455
591
|
stt?: SttContext;
|
|
456
592
|
/** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
|
|
457
593
|
state?: ProviderRuntimeState;
|
|
458
594
|
/** Allow process-local runtime state only for local development and tests. */
|
|
459
595
|
allowMemoryStateFallback?: boolean;
|
|
596
|
+
/**
|
|
597
|
+
* Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```ts
|
|
601
|
+
* await serve(provider, {
|
|
602
|
+
* shutdown: {
|
|
603
|
+
* hooks: [
|
|
604
|
+
* async () => { await emitter.flush(); },
|
|
605
|
+
* async () => { await sessionManager.closeAll("server-shutdown"); },
|
|
606
|
+
* async () => { await lease.release(); },
|
|
607
|
+
* async () => { await router.close(); },
|
|
608
|
+
* ],
|
|
609
|
+
* },
|
|
610
|
+
* });
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
shutdown?: {
|
|
614
|
+
readonly hooks?: Array<() => Promise<void>>;
|
|
615
|
+
readonly signals?: boolean | NodeJS.Signals[];
|
|
616
|
+
readonly timeoutMs?: number;
|
|
617
|
+
};
|
|
460
618
|
};
|
|
461
619
|
|
|
462
620
|
const defaultProviderServerLogger: ProviderServerLogger = (event) => {
|
|
@@ -503,19 +661,57 @@ function zodDetails(error: z.ZodError): Array<{
|
|
|
503
661
|
}));
|
|
504
662
|
}
|
|
505
663
|
|
|
664
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
665
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
666
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
667
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
668
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
669
|
+
// attribution even when the author left the category at the provider_error
|
|
670
|
+
// default.
|
|
671
|
+
function publicErrorSource(
|
|
672
|
+
error: unknown,
|
|
673
|
+
category: ProviderErrorCategory,
|
|
674
|
+
): ProviderErrorSource {
|
|
675
|
+
if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
|
|
676
|
+
if (isProviderError(error)) {
|
|
677
|
+
if (error.code === MISSING_SECRET_CODE) return "apifuse";
|
|
678
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
679
|
+
return "upstream_failure";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return sourceForCategory(category);
|
|
683
|
+
}
|
|
684
|
+
|
|
506
685
|
function toErrorResponse(
|
|
507
686
|
error: unknown,
|
|
508
687
|
requestId?: string,
|
|
688
|
+
declaredErrorCode?: OperationErrorCode,
|
|
509
689
|
): OperationErrorResponse {
|
|
510
|
-
|
|
511
|
-
|
|
690
|
+
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
691
|
+
const source = publicErrorSource(error, observability.category);
|
|
692
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
693
|
+
return {
|
|
694
|
+
error: {
|
|
695
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
696
|
+
message: "Stateful forwarding deadline expired.",
|
|
697
|
+
...(requestId ? { requestId } : {}),
|
|
698
|
+
retryable: observability.retryable,
|
|
699
|
+
source,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (isProviderError(error)) {
|
|
705
|
+
const details = error.details;
|
|
512
706
|
return {
|
|
513
707
|
error: {
|
|
514
708
|
code: error.code ?? "provider_error",
|
|
515
709
|
message: publicProviderErrorMessage(error),
|
|
516
710
|
...(requestId ? { requestId } : {}),
|
|
711
|
+
retryable: observability.retryable,
|
|
712
|
+
source,
|
|
517
713
|
...(error.fix ? { fix: error.fix } : {}),
|
|
518
|
-
...(details ? { details } : {}),
|
|
714
|
+
...(details !== undefined ? { details } : {}),
|
|
519
715
|
},
|
|
520
716
|
};
|
|
521
717
|
}
|
|
@@ -526,64 +722,72 @@ function toErrorResponse(
|
|
|
526
722
|
code: "invalid_request",
|
|
527
723
|
message: "Invalid request body",
|
|
528
724
|
...(requestId ? { requestId } : {}),
|
|
725
|
+
retryable: observability.retryable,
|
|
726
|
+
source,
|
|
529
727
|
details: zodDetails(error),
|
|
530
728
|
},
|
|
531
729
|
};
|
|
532
730
|
}
|
|
533
731
|
|
|
732
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
733
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
734
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
735
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
736
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
737
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
534
738
|
return {
|
|
535
739
|
error: {
|
|
536
740
|
code: "internal_error",
|
|
537
741
|
message: "Internal error",
|
|
538
742
|
...(requestId ? { requestId } : {}),
|
|
743
|
+
retryable: observability.retryable,
|
|
744
|
+
source,
|
|
745
|
+
details: {
|
|
746
|
+
retryable: false,
|
|
747
|
+
category: "internal_error",
|
|
748
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
749
|
+
},
|
|
539
750
|
},
|
|
540
751
|
};
|
|
541
752
|
}
|
|
542
753
|
|
|
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 {
|
|
754
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
755
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
756
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
757
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
758
|
+
// from a duplicate SDK module instance.
|
|
759
|
+
function providerObservabilityDetails(
|
|
760
|
+
error: unknown,
|
|
761
|
+
declaredErrorCode?: OperationErrorCode,
|
|
762
|
+
): ErrorObservabilityDetails | undefined {
|
|
763
|
+
const declaredRetryable = sdkOwnsErrorResolution(error)
|
|
764
|
+
? undefined
|
|
765
|
+
: declaredErrorCode?.retryable;
|
|
574
766
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
575
767
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
576
768
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
577
769
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
578
770
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
579
|
-
if (error
|
|
771
|
+
if (isSessionExpiredError(error)) {
|
|
580
772
|
return {
|
|
581
773
|
category: error.options?.category ?? "credential_expired",
|
|
582
774
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
583
|
-
retryable: error.options?.retryable ?? false,
|
|
775
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
779
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
780
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
781
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
782
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
783
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
784
|
+
return {
|
|
785
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
786
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
787
|
+
retryable: error.options?.retryable ?? declaredRetryable ?? false,
|
|
584
788
|
};
|
|
585
789
|
}
|
|
586
|
-
if (!(error
|
|
790
|
+
if (!isTransportError(error)) {
|
|
587
791
|
return undefined;
|
|
588
792
|
}
|
|
589
793
|
const isProxyPoolCode =
|
|
@@ -615,8 +819,86 @@ function providerObservabilityDetails(error: ProviderError):
|
|
|
615
819
|
};
|
|
616
820
|
}
|
|
617
821
|
|
|
822
|
+
function errorObservabilityDetails(
|
|
823
|
+
error: unknown,
|
|
824
|
+
declaredErrorCode?: OperationErrorCode,
|
|
825
|
+
): ErrorObservabilityDetails {
|
|
826
|
+
const effectiveDeclaration = sdkOwnsErrorResolution(error) ? undefined : declaredErrorCode;
|
|
827
|
+
const providerDetails = providerObservabilityDetails(error, effectiveDeclaration);
|
|
828
|
+
if (providerDetails) return providerDetails;
|
|
829
|
+
|
|
830
|
+
if (error instanceof z.ZodError || isValidationError(error)) {
|
|
831
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
832
|
+
return {
|
|
833
|
+
category:
|
|
834
|
+
isProviderError(error) && error.options?.category
|
|
835
|
+
? error.options.category
|
|
836
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
837
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
838
|
+
? "upstream_rejected"
|
|
839
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
840
|
+
? "provider_error"
|
|
841
|
+
: "input_validation",
|
|
842
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
843
|
+
retryable: isProviderError(error)
|
|
844
|
+
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
845
|
+
: false,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
850
|
+
return {
|
|
851
|
+
category: "timeout",
|
|
852
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
853
|
+
retryable: false,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (isProviderError(error)) {
|
|
858
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
859
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
860
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
861
|
+
// author set an explicit category.
|
|
862
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
863
|
+
const rejectionDefault =
|
|
864
|
+
error.code === "UPSTREAM_REJECTED" ||
|
|
865
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
866
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
867
|
+
? ("upstream_rejected" as const)
|
|
868
|
+
: ("provider_error" as const);
|
|
869
|
+
return {
|
|
870
|
+
category: error.options?.category ?? rejectionDefault,
|
|
871
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
872
|
+
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return {
|
|
877
|
+
category: "internal_error",
|
|
878
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
879
|
+
retryable: false,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function responseWithErrorObservability(
|
|
884
|
+
response: Response,
|
|
885
|
+
error: unknown,
|
|
886
|
+
declaredErrorCode?: OperationErrorCode,
|
|
887
|
+
): Response {
|
|
888
|
+
const headers = new Headers(response.headers);
|
|
889
|
+
headers.set(
|
|
890
|
+
ERROR_OBSERVABILITY_HEADER,
|
|
891
|
+
JSON.stringify(errorObservabilityDetails(error, declaredErrorCode)),
|
|
892
|
+
);
|
|
893
|
+
return new Response(response.body, {
|
|
894
|
+
status: response.status,
|
|
895
|
+
statusText: response.statusText,
|
|
896
|
+
headers,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
618
900
|
function publicProviderErrorMessage(error: ProviderError): string {
|
|
619
|
-
if (error
|
|
901
|
+
if (isTransportError(error)) {
|
|
620
902
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
621
903
|
return error.message;
|
|
622
904
|
}
|
|
@@ -639,22 +921,35 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
639
921
|
return error.message;
|
|
640
922
|
}
|
|
641
923
|
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
924
|
+
function isEmittableErrorStatus(value: unknown): value is ProviderErrorStatus {
|
|
925
|
+
return (
|
|
926
|
+
typeof value === "number" &&
|
|
927
|
+
VALID_OPERATION_ERROR_STATUSES.some((status) => status === value)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): ProviderErrorStatus {
|
|
645
932
|
if (error instanceof z.ZodError) {
|
|
646
933
|
return 400;
|
|
647
934
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return error.code === "transport_timeout" ? 504 : 502;
|
|
935
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
936
|
+
return 504;
|
|
651
937
|
}
|
|
652
|
-
|
|
653
|
-
|
|
938
|
+
if (isProviderError(error)) {
|
|
939
|
+
if (
|
|
940
|
+
!sdkOwnsErrorResolution(error) &&
|
|
941
|
+
isEmittableErrorStatus(declaredErrorCode?.status)
|
|
942
|
+
) {
|
|
943
|
+
return declaredErrorCode.status;
|
|
944
|
+
}
|
|
654
945
|
switch (error.code) {
|
|
655
946
|
case "AUTH_REQUIRED":
|
|
656
947
|
case "reauth_required":
|
|
657
948
|
return 401;
|
|
949
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
950
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
951
|
+
case MISSING_SECRET_CODE:
|
|
952
|
+
return 400;
|
|
658
953
|
case "NOT_FOUND":
|
|
659
954
|
case "not_found":
|
|
660
955
|
case "NO_DATA":
|
|
@@ -663,20 +958,66 @@ function toStatusCode(
|
|
|
663
958
|
case "UPSTREAM_RATE_LIMIT":
|
|
664
959
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
665
960
|
return 429;
|
|
961
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
962
|
+
// contract): the upstream evaluated the request and said no under
|
|
963
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
964
|
+
case "UPSTREAM_REJECTED":
|
|
965
|
+
return 409;
|
|
666
966
|
case "UPSTREAM_ERROR":
|
|
667
967
|
case "BLOCKED":
|
|
668
968
|
return 502;
|
|
669
969
|
case "STT_UNAVAILABLE":
|
|
670
970
|
case "UNSUPPORTED_STT_BACKEND":
|
|
971
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
671
972
|
return 503;
|
|
672
973
|
}
|
|
974
|
+
if (isTransportError(error)) {
|
|
975
|
+
return error.code === "transport_timeout" ? 504 : 502;
|
|
976
|
+
}
|
|
977
|
+
if (isValidationError(error)) {
|
|
978
|
+
return error.options?.category === "output_validation" ? 500 : 400;
|
|
979
|
+
}
|
|
673
980
|
|
|
674
|
-
return
|
|
981
|
+
return 500;
|
|
675
982
|
}
|
|
676
983
|
|
|
677
984
|
return 500;
|
|
678
985
|
}
|
|
679
986
|
|
|
987
|
+
function sdkOwnsErrorResolution(error: unknown): boolean {
|
|
988
|
+
if (isSessionExpiredError(error)) return true;
|
|
989
|
+
if (isTransportError(error)) return true;
|
|
990
|
+
if (error instanceof z.ZodError) return true;
|
|
991
|
+
if (error instanceof StatefulRoutingDeadlineError) return true;
|
|
992
|
+
return (
|
|
993
|
+
isProviderError(error) &&
|
|
994
|
+
typeof error.code === "string" &&
|
|
995
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(error.code)
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
type OperationErrorCodeLookup = ReadonlyMap<string, ReadonlyMap<string, OperationErrorCode>>;
|
|
1000
|
+
|
|
1001
|
+
function buildOperationErrorCodeLookup(provider: ProviderDefinition): OperationErrorCodeLookup {
|
|
1002
|
+
return new Map(
|
|
1003
|
+
Object.entries(provider.operations).flatMap(([operationId, operation]) => {
|
|
1004
|
+
const errorCodes = operation.docs?.errorCodes;
|
|
1005
|
+
return errorCodes?.length
|
|
1006
|
+
? [[operationId, new Map(errorCodes.map((entry) => [entry.code, entry]))] as const]
|
|
1007
|
+
: [];
|
|
1008
|
+
}),
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function declaredErrorCodeFor(
|
|
1013
|
+
error: unknown,
|
|
1014
|
+
operationId: string | undefined,
|
|
1015
|
+
lookup: OperationErrorCodeLookup,
|
|
1016
|
+
): OperationErrorCode | undefined {
|
|
1017
|
+
if (!operationId || !isProviderError(error) || typeof error.code !== "string") return undefined;
|
|
1018
|
+
return lookup.get(operationId)?.get(error.code);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
680
1021
|
function extractRequestId(raw: unknown): string | undefined {
|
|
681
1022
|
if (!raw || typeof raw !== "object") {
|
|
682
1023
|
return undefined;
|
|
@@ -695,21 +1036,26 @@ function logProviderError(
|
|
|
695
1036
|
error: unknown,
|
|
696
1037
|
status: number,
|
|
697
1038
|
cost: ProviderRequestCost,
|
|
1039
|
+
declaredErrorCode?: OperationErrorCode,
|
|
698
1040
|
): void {
|
|
699
|
-
const code =
|
|
700
|
-
error
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1041
|
+
const code = isProviderError(error)
|
|
1042
|
+
? (error.code ?? "provider_error")
|
|
1043
|
+
: error instanceof z.ZodError
|
|
1044
|
+
? "invalid_request"
|
|
1045
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
1046
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
704
1047
|
: "internal_error";
|
|
705
1048
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
706
1049
|
const message = error instanceof Error ? error.message : String(error);
|
|
707
|
-
const details =
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
typeof
|
|
1050
|
+
const details = errorObservabilityDetails(error, declaredErrorCode);
|
|
1051
|
+
const isUnregisteredProviderErrorCode =
|
|
1052
|
+
status === 500 &&
|
|
1053
|
+
isProviderError(error) &&
|
|
1054
|
+
!isValidationError(error) &&
|
|
1055
|
+
typeof error.code === "string" &&
|
|
1056
|
+
!SDK_OWNED_PROVIDER_ERROR_CODES.has(error.code) &&
|
|
1057
|
+
declaredErrorCode === undefined;
|
|
1058
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
713
1059
|
emit({
|
|
714
1060
|
level: status >= 500 ? "error" : "warn",
|
|
715
1061
|
event: "provider_request_failed",
|
|
@@ -722,14 +1068,15 @@ function logProviderError(
|
|
|
722
1068
|
code,
|
|
723
1069
|
errorClass,
|
|
724
1070
|
message,
|
|
725
|
-
...(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1071
|
+
...(details.upstreamStatus ? { upstreamStatus: details.upstreamStatus } : {}),
|
|
1072
|
+
errorCategory: details.category,
|
|
1073
|
+
taxonomyVersion: details.taxonomyVersion,
|
|
1074
|
+
retryable: details.retryable,
|
|
1075
|
+
...(isUnregisteredProviderErrorCode
|
|
729
1076
|
? {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1077
|
+
signal: "unregistered_provider_error_code" as const,
|
|
1078
|
+
signalFix:
|
|
1079
|
+
"Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
733
1080
|
}
|
|
734
1081
|
: {}),
|
|
735
1082
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
@@ -744,8 +1091,7 @@ function logProviderCleanupError(
|
|
|
744
1091
|
resource: "browser" | "stealth",
|
|
745
1092
|
error: unknown,
|
|
746
1093
|
): void {
|
|
747
|
-
const emit =
|
|
748
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1094
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
749
1095
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
750
1096
|
const message = error instanceof Error ? error.message : String(error);
|
|
751
1097
|
emit({
|
|
@@ -770,8 +1116,7 @@ function logProviderSuccess(
|
|
|
770
1116
|
status: number,
|
|
771
1117
|
cost: ProviderRequestCost,
|
|
772
1118
|
): void {
|
|
773
|
-
const emit =
|
|
774
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
1119
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
775
1120
|
emit({
|
|
776
1121
|
level: "info",
|
|
777
1122
|
event: "provider_request_completed",
|
|
@@ -817,18 +1162,13 @@ function toJsonSuccessResponse(
|
|
|
817
1162
|
};
|
|
818
1163
|
}
|
|
819
1164
|
|
|
820
|
-
function isAsyncIterable<T = unknown>(
|
|
821
|
-
value: unknown,
|
|
822
|
-
): value is AsyncIterable<T> {
|
|
1165
|
+
function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
|
|
823
1166
|
if (!value || typeof value !== "object") return false;
|
|
824
1167
|
const iterator = Reflect.get(value, Symbol.asyncIterator);
|
|
825
1168
|
return typeof iterator === "function";
|
|
826
1169
|
}
|
|
827
1170
|
|
|
828
|
-
function responseWithCleanup(
|
|
829
|
-
response: Response,
|
|
830
|
-
cleanup: RequestCleanup,
|
|
831
|
-
): Response {
|
|
1171
|
+
function responseWithCleanup(response: Response, cleanup: RequestCleanup): Response {
|
|
832
1172
|
if (!response.body) {
|
|
833
1173
|
void cleanup();
|
|
834
1174
|
return response;
|
|
@@ -877,10 +1217,7 @@ async function validateSseEvent(
|
|
|
877
1217
|
const transport = getSseTransport(operation);
|
|
878
1218
|
const schema = transport?.events?.[event.event];
|
|
879
1219
|
if (!schema) {
|
|
880
|
-
if (
|
|
881
|
-
event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
882
|
-
event.event === APIFUSE_STREAM_DONE_EVENT
|
|
883
|
-
) {
|
|
1220
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
884
1221
|
return event;
|
|
885
1222
|
}
|
|
886
1223
|
throw new ProviderError(
|
|
@@ -893,11 +1230,7 @@ async function validateSseEvent(
|
|
|
893
1230
|
},
|
|
894
1231
|
);
|
|
895
1232
|
}
|
|
896
|
-
const data = await parseSchema(
|
|
897
|
-
schema,
|
|
898
|
-
event.data,
|
|
899
|
-
`transport.events.${event.event}`,
|
|
900
|
-
);
|
|
1233
|
+
const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
|
|
901
1234
|
return { ...event, data };
|
|
902
1235
|
}
|
|
903
1236
|
|
|
@@ -917,8 +1250,7 @@ function assertStreamPayloadWithinLimit(
|
|
|
917
1250
|
throw new ProviderError(
|
|
918
1251
|
`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
|
|
919
1252
|
{
|
|
920
|
-
code:
|
|
921
|
-
kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
1253
|
+
code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
922
1254
|
retryable: false,
|
|
923
1255
|
category: "input_validation",
|
|
924
1256
|
fix:
|
|
@@ -963,15 +1295,10 @@ function toSseResponse(
|
|
|
963
1295
|
const validated = await validateSseEvent(operation, next.value);
|
|
964
1296
|
const encodedEvent = encodeSseEvent(validated);
|
|
965
1297
|
const encodedBytes = encoder.encode(encodedEvent);
|
|
966
|
-
assertStreamPayloadWithinLimit(
|
|
967
|
-
encodedBytes.byteLength,
|
|
968
|
-
transport?.maxEventBytes,
|
|
969
|
-
"event",
|
|
970
|
-
);
|
|
1298
|
+
assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
|
|
971
1299
|
controller.enqueue(encodedBytes);
|
|
972
1300
|
} catch (error) {
|
|
973
|
-
const message =
|
|
974
|
-
error instanceof Error ? error.message : "Stream failed";
|
|
1301
|
+
const message = error instanceof Error ? error.message : "Stream failed";
|
|
975
1302
|
controller.enqueue(
|
|
976
1303
|
encoder.encode(
|
|
977
1304
|
encodeSseEvent(
|
|
@@ -1018,11 +1345,7 @@ function enforceStreamChunkLimit(
|
|
|
1018
1345
|
return;
|
|
1019
1346
|
}
|
|
1020
1347
|
if (value) {
|
|
1021
|
-
assertStreamPayloadWithinLimit(
|
|
1022
|
-
byteLength(value),
|
|
1023
|
-
maxChunkBytes,
|
|
1024
|
-
"chunk",
|
|
1025
|
-
);
|
|
1348
|
+
assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
|
|
1026
1349
|
controller.enqueue(value);
|
|
1027
1350
|
}
|
|
1028
1351
|
} catch (error) {
|
|
@@ -1042,10 +1365,7 @@ function toStreamingResponse(
|
|
|
1042
1365
|
requestId?: string,
|
|
1043
1366
|
): Response {
|
|
1044
1367
|
const transport = operation.transport?.kind ?? "json";
|
|
1045
|
-
if (
|
|
1046
|
-
transport === "sse" &&
|
|
1047
|
-
(result instanceof Response || result instanceof ReadableStream)
|
|
1048
|
-
) {
|
|
1368
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
1049
1369
|
void cleanup();
|
|
1050
1370
|
throw new ProviderError(
|
|
1051
1371
|
"SSE operations must return an AsyncIterable of typed stream.event(...) values.",
|
|
@@ -1059,20 +1379,13 @@ function toStreamingResponse(
|
|
|
1059
1379
|
}
|
|
1060
1380
|
if (result instanceof Response) {
|
|
1061
1381
|
const httpTransport = getHttpStreamTransport(operation);
|
|
1062
|
-
if (
|
|
1063
|
-
httpTransport &&
|
|
1064
|
-
result.body &&
|
|
1065
|
-
httpTransport?.maxChunkBytes !== undefined
|
|
1066
|
-
) {
|
|
1382
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
1067
1383
|
return responseWithCleanup(
|
|
1068
|
-
new Response(
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
statusText: result.statusText,
|
|
1074
|
-
},
|
|
1075
|
-
),
|
|
1384
|
+
new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
1385
|
+
headers: result.headers,
|
|
1386
|
+
status: result.status,
|
|
1387
|
+
statusText: result.statusText,
|
|
1388
|
+
}),
|
|
1076
1389
|
cleanup,
|
|
1077
1390
|
);
|
|
1078
1391
|
}
|
|
@@ -1092,8 +1405,7 @@ function toStreamingResponse(
|
|
|
1092
1405
|
: {
|
|
1093
1406
|
"Content-Type":
|
|
1094
1407
|
operation.transport?.kind === "http-stream"
|
|
1095
|
-
? (operation.transport.contentType ??
|
|
1096
|
-
"application/octet-stream")
|
|
1408
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
1097
1409
|
: "application/octet-stream",
|
|
1098
1410
|
},
|
|
1099
1411
|
}),
|
|
@@ -1113,18 +1425,14 @@ function toStreamingResponse(
|
|
|
1113
1425
|
);
|
|
1114
1426
|
}
|
|
1115
1427
|
|
|
1116
|
-
function getSseTransport(
|
|
1117
|
-
operation: OperationDefinition,
|
|
1118
|
-
): OperationSseTransport | undefined {
|
|
1428
|
+
function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
|
|
1119
1429
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
1120
1430
|
}
|
|
1121
1431
|
|
|
1122
1432
|
function getHttpStreamTransport(
|
|
1123
1433
|
operation: OperationDefinition,
|
|
1124
1434
|
): OperationHttpStreamTransport | undefined {
|
|
1125
|
-
return operation.transport?.kind === "http-stream"
|
|
1126
|
-
? operation.transport
|
|
1127
|
-
: undefined;
|
|
1435
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
1128
1436
|
}
|
|
1129
1437
|
|
|
1130
1438
|
function toAuthFlowResponse(
|
|
@@ -1145,9 +1453,7 @@ function toAuthFlowResponse(
|
|
|
1145
1453
|
};
|
|
1146
1454
|
}
|
|
1147
1455
|
|
|
1148
|
-
function authFlowLocaleFromHeaders(
|
|
1149
|
-
headers?: Record<string, string>,
|
|
1150
|
-
): ProviderLocale {
|
|
1456
|
+
function authFlowLocaleFromHeaders(headers?: Record<string, string>): ProviderLocale {
|
|
1151
1457
|
const header = Object.entries(headers ?? {}).find(
|
|
1152
1458
|
([key]) => key.toLowerCase() === "accept-language",
|
|
1153
1459
|
)?.[1];
|
|
@@ -1165,9 +1471,7 @@ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
|
|
|
1165
1471
|
}
|
|
1166
1472
|
|
|
1167
1473
|
function isAuthTurn(value: unknown): value is AuthTurn {
|
|
1168
|
-
return
|
|
1169
|
-
!!value && typeof value === "object" && "kind" in value && "turnId" in value
|
|
1170
|
-
);
|
|
1474
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
1171
1475
|
}
|
|
1172
1476
|
|
|
1173
1477
|
function loadAuthFlowLocaleCatalogs(
|
|
@@ -1204,10 +1508,7 @@ function materializeAuthFlowTurn(
|
|
|
1204
1508
|
});
|
|
1205
1509
|
}
|
|
1206
1510
|
|
|
1207
|
-
function withAuthRequestHeaders(
|
|
1208
|
-
request: AuthFlowRequest,
|
|
1209
|
-
headers: Headers,
|
|
1210
|
-
): AuthFlowRequest {
|
|
1511
|
+
function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
|
|
1211
1512
|
return {
|
|
1212
1513
|
...request,
|
|
1213
1514
|
headers: {
|
|
@@ -1225,17 +1526,9 @@ async function handleOperation(
|
|
|
1225
1526
|
state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
|
|
1226
1527
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
1227
1528
|
): Promise<Response | OperationResponse> {
|
|
1228
|
-
const ctx = createProviderContext(
|
|
1229
|
-
provider,
|
|
1230
|
-
request,
|
|
1231
|
-
operationId,
|
|
1232
|
-
options,
|
|
1233
|
-
state,
|
|
1234
|
-
proxyTelemetry,
|
|
1235
|
-
);
|
|
1529
|
+
const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
|
|
1236
1530
|
const operation = provider.operations[operationId];
|
|
1237
|
-
const streaming =
|
|
1238
|
-
operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1531
|
+
const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1239
1532
|
let cleanupCalled = false;
|
|
1240
1533
|
const cleanup = async () => {
|
|
1241
1534
|
if (cleanupCalled) return;
|
|
@@ -1266,12 +1559,14 @@ async function handleOperation(
|
|
|
1266
1559
|
}
|
|
1267
1560
|
};
|
|
1268
1561
|
try {
|
|
1269
|
-
const result =
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1562
|
+
const result = options.operationExecutor
|
|
1563
|
+
? await options.operationExecutor({
|
|
1564
|
+
provider,
|
|
1565
|
+
operationId,
|
|
1566
|
+
ctx,
|
|
1567
|
+
request,
|
|
1568
|
+
})
|
|
1569
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
1275
1570
|
if (streaming && operation) {
|
|
1276
1571
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
1277
1572
|
}
|
|
@@ -1315,13 +1610,16 @@ async function handleAuthFlow(
|
|
|
1315
1610
|
});
|
|
1316
1611
|
}
|
|
1317
1612
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
);
|
|
1613
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
1614
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
1615
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1616
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1617
|
+
// stranded flow even when provisioning is broken.
|
|
1618
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
1324
1619
|
try {
|
|
1620
|
+
if (route !== "abort") {
|
|
1621
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
1622
|
+
}
|
|
1325
1623
|
const result =
|
|
1326
1624
|
route === "start"
|
|
1327
1625
|
? await flow.start(context)
|
|
@@ -1363,12 +1661,143 @@ async function handleAuthFlow(
|
|
|
1363
1661
|
}
|
|
1364
1662
|
}
|
|
1365
1663
|
|
|
1664
|
+
class StatefulForwardingReplayCache {
|
|
1665
|
+
readonly #nonces = new Map<string, number>();
|
|
1666
|
+
readonly #expiryBuckets = new Map<number, Set<string>>();
|
|
1667
|
+
#nextExpiryBucket?: number;
|
|
1668
|
+
#latestExpiryBucket?: number;
|
|
1669
|
+
|
|
1670
|
+
constructor(private readonly maxEntries: number) {}
|
|
1671
|
+
|
|
1672
|
+
claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
|
|
1673
|
+
this.dropExpiredBuckets(nowMs);
|
|
1674
|
+
if (this.#nonces.has(nonce)) return "replayed";
|
|
1675
|
+
if (this.#nonces.size >= this.maxEntries) return "full";
|
|
1676
|
+
const expiryBucket =
|
|
1677
|
+
Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1678
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1679
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1680
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
|
|
1681
|
+
bucket.add(nonce);
|
|
1682
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1683
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1684
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1685
|
+
return "accepted";
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
private dropExpiredBuckets(nowMs: number): void {
|
|
1689
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
|
|
1690
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1691
|
+
this.#nonces.clear();
|
|
1692
|
+
this.#expiryBuckets.clear();
|
|
1693
|
+
this.#nextExpiryBucket = undefined;
|
|
1694
|
+
this.#latestExpiryBucket = undefined;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1698
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1699
|
+
if (bucket) {
|
|
1700
|
+
for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
|
|
1701
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1702
|
+
}
|
|
1703
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function verifyStatefulForwardingRequest(input: {
|
|
1709
|
+
readonly options: ProviderServerOptions;
|
|
1710
|
+
readonly rawBody: string;
|
|
1711
|
+
readonly headers: Headers;
|
|
1712
|
+
readonly method: string;
|
|
1713
|
+
readonly path: string;
|
|
1714
|
+
readonly replayCache: StatefulForwardingReplayCache;
|
|
1715
|
+
}): void {
|
|
1716
|
+
const config = input.options.statefulForwarding;
|
|
1717
|
+
if (!config?.secret) {
|
|
1718
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1719
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1723
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1724
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1725
|
+
if (!timestamp || !signature || !nonce) {
|
|
1726
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1727
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
if (nonce.length > 256) {
|
|
1731
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1732
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
const timestampMs = Date.parse(timestamp);
|
|
1736
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1737
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1738
|
+
throw new ProviderError(
|
|
1739
|
+
"Stateful forwarding signature timestamp is outside the allowed skew.",
|
|
1740
|
+
{ code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (
|
|
1744
|
+
!verifyStatefulRequestSignature({
|
|
1745
|
+
secret: config.secret,
|
|
1746
|
+
timestamp,
|
|
1747
|
+
rawBody: input.rawBody,
|
|
1748
|
+
method: input.method,
|
|
1749
|
+
path: input.path,
|
|
1750
|
+
nonce,
|
|
1751
|
+
signature,
|
|
1752
|
+
})
|
|
1753
|
+
) {
|
|
1754
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1755
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1759
|
+
if (replayResult === "replayed") {
|
|
1760
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1761
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (replayResult === "full") {
|
|
1765
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1766
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function operationRequestFromForwardingEnvelope(
|
|
1772
|
+
envelope: ProviderServerStatefulForwardEnvelope,
|
|
1773
|
+
): OperationRequest & { readonly deadlineAt?: string } {
|
|
1774
|
+
return {
|
|
1775
|
+
...envelope.operationRequest,
|
|
1776
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function parseStatefulForwardingEnvelope(rawBody: unknown): ProviderServerStatefulForwardEnvelope {
|
|
1781
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1782
|
+
if (parsed.success) return parsed.data;
|
|
1783
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1784
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1785
|
+
details: zodDetails(parsed.error),
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1366
1789
|
export function createServerApp(
|
|
1367
1790
|
provider: ProviderDefinition,
|
|
1368
1791
|
options: ProviderServerOptions = {},
|
|
1369
1792
|
): Hono {
|
|
1793
|
+
validateStatefulServerConfig(options);
|
|
1370
1794
|
const app = new Hono();
|
|
1371
1795
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1796
|
+
const operationErrorCodes = buildOperationErrorCodeLookup(provider);
|
|
1797
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
|
|
1798
|
+
options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1799
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
|
|
1800
|
+
);
|
|
1372
1801
|
const state =
|
|
1373
1802
|
options.state ??
|
|
1374
1803
|
createProviderRuntimeStateFromEnv({
|
|
@@ -1376,17 +1805,28 @@ export function createServerApp(
|
|
|
1376
1805
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
1377
1806
|
});
|
|
1378
1807
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
404,
|
|
1388
|
-
),
|
|
1808
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1809
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1810
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1811
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1812
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1813
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(
|
|
1814
|
+
provider,
|
|
1815
|
+
createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
1389
1816
|
);
|
|
1817
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1818
|
+
logger({
|
|
1819
|
+
level: "warn",
|
|
1820
|
+
event: "provider_secrets_missing",
|
|
1821
|
+
providerId: provider.id,
|
|
1822
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
app.notFound((c) => {
|
|
1827
|
+
const error = new ProviderError("Not found", { code: "not_found", retryable: false });
|
|
1828
|
+
return responseWithErrorObservability(c.json(toErrorResponse(error), 404), error);
|
|
1829
|
+
});
|
|
1390
1830
|
|
|
1391
1831
|
app.get("/health", (c) =>
|
|
1392
1832
|
c.json({
|
|
@@ -1396,6 +1836,169 @@ export function createServerApp(
|
|
|
1396
1836
|
}),
|
|
1397
1837
|
);
|
|
1398
1838
|
|
|
1839
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1840
|
+
let rawBodyText = "";
|
|
1841
|
+
let rawBody: unknown;
|
|
1842
|
+
let operationId: string | undefined;
|
|
1843
|
+
const operation = "stateful-internal";
|
|
1844
|
+
const requestCost = startRequestCost();
|
|
1845
|
+
try {
|
|
1846
|
+
if (!options.internalOperationExecutor) {
|
|
1847
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1848
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1852
|
+
verifyStatefulForwardingRequest({
|
|
1853
|
+
options,
|
|
1854
|
+
rawBody: rawBodyText,
|
|
1855
|
+
headers: c.req.raw.headers,
|
|
1856
|
+
method: c.req.raw.method,
|
|
1857
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1858
|
+
replayCache: statefulForwardingReplayCache,
|
|
1859
|
+
});
|
|
1860
|
+
try {
|
|
1861
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1862
|
+
} catch {
|
|
1863
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1864
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1868
|
+
if (envelope.providerId !== provider.id) {
|
|
1869
|
+
throw new ProviderError(
|
|
1870
|
+
"Stateful forwarding envelope providerId does not match the served provider.",
|
|
1871
|
+
{ code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1875
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1876
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
if (
|
|
1880
|
+
envelope.sourcePodId !==
|
|
1881
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
|
|
1882
|
+
) {
|
|
1883
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1884
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (
|
|
1888
|
+
envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
|
|
1889
|
+
) {
|
|
1890
|
+
throw new ProviderError(
|
|
1891
|
+
"Stateful forwarding forwardedAt does not match its signature timestamp.",
|
|
1892
|
+
{ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1896
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1897
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1898
|
+
}
|
|
1899
|
+
const remainingDeadlineMs =
|
|
1900
|
+
deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1901
|
+
const deadlineSignal =
|
|
1902
|
+
remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1903
|
+
const signal = deadlineSignal
|
|
1904
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1905
|
+
: c.req.raw.signal;
|
|
1906
|
+
const ownerFenceValidation = Promise.resolve(
|
|
1907
|
+
options.statefulForwarding?.validateOwnerFence(
|
|
1908
|
+
{
|
|
1909
|
+
providerId: envelope.providerId,
|
|
1910
|
+
sessionKey: envelope.sessionKey,
|
|
1911
|
+
ownerPodId: envelope.ownerPodId,
|
|
1912
|
+
generation: envelope.generation,
|
|
1913
|
+
sourcePodId: envelope.sourcePodId,
|
|
1914
|
+
forwardedAt: envelope.forwardedAt,
|
|
1915
|
+
requestId: envelope.requestId,
|
|
1916
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1917
|
+
},
|
|
1918
|
+
signal,
|
|
1919
|
+
),
|
|
1920
|
+
);
|
|
1921
|
+
let ownerFenceValid: boolean | undefined;
|
|
1922
|
+
try {
|
|
1923
|
+
ownerFenceValid = deadlineSignal
|
|
1924
|
+
? await Promise.race([
|
|
1925
|
+
ownerFenceValidation,
|
|
1926
|
+
new Promise<never>((_resolve, reject) => {
|
|
1927
|
+
deadlineSignal.addEventListener(
|
|
1928
|
+
"abort",
|
|
1929
|
+
() =>
|
|
1930
|
+
reject(
|
|
1931
|
+
new StatefulRoutingDeadlineError(
|
|
1932
|
+
envelope.requestId,
|
|
1933
|
+
envelope.deadlineAt as string,
|
|
1934
|
+
),
|
|
1935
|
+
),
|
|
1936
|
+
{ once: true },
|
|
1937
|
+
);
|
|
1938
|
+
}),
|
|
1939
|
+
])
|
|
1940
|
+
: await ownerFenceValidation;
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
if (deadlineSignal?.aborted) {
|
|
1943
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1944
|
+
}
|
|
1945
|
+
throw error;
|
|
1946
|
+
}
|
|
1947
|
+
if (ownerFenceValid !== true) {
|
|
1948
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1949
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1953
|
+
operationId = envelope.operationId;
|
|
1954
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1955
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1956
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1957
|
+
}
|
|
1958
|
+
const output = await options.internalOperationExecutor({
|
|
1959
|
+
provider,
|
|
1960
|
+
operationId,
|
|
1961
|
+
ctx,
|
|
1962
|
+
request,
|
|
1963
|
+
internalStatefulForward: envelope,
|
|
1964
|
+
signal,
|
|
1965
|
+
});
|
|
1966
|
+
logProviderSuccess(
|
|
1967
|
+
logger,
|
|
1968
|
+
provider,
|
|
1969
|
+
"operation",
|
|
1970
|
+
operationId || operation,
|
|
1971
|
+
request.requestId,
|
|
1972
|
+
200,
|
|
1973
|
+
finishRequestCost(requestCost),
|
|
1974
|
+
);
|
|
1975
|
+
return c.json({ data: output });
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operationId, operationErrorCodes);
|
|
1978
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1979
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1980
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1981
|
+
}
|
|
1982
|
+
const requestId = extractRequestId(rawBody);
|
|
1983
|
+
logProviderError(
|
|
1984
|
+
logger,
|
|
1985
|
+
provider,
|
|
1986
|
+
"operation",
|
|
1987
|
+
operationId || operation,
|
|
1988
|
+
requestId,
|
|
1989
|
+
error,
|
|
1990
|
+
status,
|
|
1991
|
+
finishRequestCost(requestCost),
|
|
1992
|
+
declaredErrorCode,
|
|
1993
|
+
);
|
|
1994
|
+
return responseWithErrorObservability(
|
|
1995
|
+
c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
|
|
1996
|
+
error,
|
|
1997
|
+
declaredErrorCode,
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
|
|
1399
2002
|
app.post("/v1/:operation", async (c) => {
|
|
1400
2003
|
let rawBody: unknown;
|
|
1401
2004
|
const operation = c.req.param("operation");
|
|
@@ -1442,7 +2045,8 @@ export function createServerApp(
|
|
|
1442
2045
|
);
|
|
1443
2046
|
return c.json(response);
|
|
1444
2047
|
} catch (error) {
|
|
1445
|
-
const
|
|
2048
|
+
const declaredErrorCode = declaredErrorCodeFor(error, operation, operationErrorCodes);
|
|
2049
|
+
const status = toStatusCode(error, declaredErrorCode);
|
|
1446
2050
|
const requestId = extractRequestId(rawBody);
|
|
1447
2051
|
logProviderError(
|
|
1448
2052
|
logger,
|
|
@@ -1453,10 +2057,15 @@ export function createServerApp(
|
|
|
1453
2057
|
error,
|
|
1454
2058
|
status,
|
|
1455
2059
|
finishRequestCost(requestCost),
|
|
2060
|
+
declaredErrorCode,
|
|
1456
2061
|
);
|
|
1457
2062
|
const telemetryHeader = proxyTelemetry.toHeaderValue();
|
|
1458
2063
|
if (telemetryHeader) c.header(PROVIDER_TELEMETRY_HEADER, telemetryHeader);
|
|
1459
|
-
return
|
|
2064
|
+
return responseWithErrorObservability(
|
|
2065
|
+
c.json(toErrorResponse(error, requestId, declaredErrorCode), status),
|
|
2066
|
+
error,
|
|
2067
|
+
declaredErrorCode,
|
|
2068
|
+
);
|
|
1460
2069
|
}
|
|
1461
2070
|
});
|
|
1462
2071
|
|
|
@@ -1468,17 +2077,8 @@ export function createServerApp(
|
|
|
1468
2077
|
.clone()
|
|
1469
2078
|
.json()
|
|
1470
2079
|
.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
|
-
);
|
|
2080
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2081
|
+
const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
|
|
1482
2082
|
logProviderSuccess(
|
|
1483
2083
|
logger,
|
|
1484
2084
|
provider,
|
|
@@ -1502,7 +2102,10 @@ export function createServerApp(
|
|
|
1502
2102
|
status,
|
|
1503
2103
|
finishRequestCost(requestCost),
|
|
1504
2104
|
);
|
|
1505
|
-
return
|
|
2105
|
+
return responseWithErrorObservability(
|
|
2106
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2107
|
+
error,
|
|
2108
|
+
);
|
|
1506
2109
|
}
|
|
1507
2110
|
});
|
|
1508
2111
|
|
|
@@ -1514,17 +2117,8 @@ export function createServerApp(
|
|
|
1514
2117
|
.clone()
|
|
1515
2118
|
.json()
|
|
1516
2119
|
.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
|
-
);
|
|
2120
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2121
|
+
const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
|
|
1528
2122
|
logProviderSuccess(
|
|
1529
2123
|
logger,
|
|
1530
2124
|
provider,
|
|
@@ -1548,7 +2142,10 @@ export function createServerApp(
|
|
|
1548
2142
|
status,
|
|
1549
2143
|
finishRequestCost(requestCost),
|
|
1550
2144
|
);
|
|
1551
|
-
return
|
|
2145
|
+
return responseWithErrorObservability(
|
|
2146
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2147
|
+
error,
|
|
2148
|
+
);
|
|
1552
2149
|
}
|
|
1553
2150
|
});
|
|
1554
2151
|
|
|
@@ -1560,17 +2157,8 @@ export function createServerApp(
|
|
|
1560
2157
|
.clone()
|
|
1561
2158
|
.json()
|
|
1562
2159
|
.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
|
-
);
|
|
2160
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2161
|
+
const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
|
|
1574
2162
|
logProviderSuccess(
|
|
1575
2163
|
logger,
|
|
1576
2164
|
provider,
|
|
@@ -1594,7 +2182,10 @@ export function createServerApp(
|
|
|
1594
2182
|
status,
|
|
1595
2183
|
finishRequestCost(requestCost),
|
|
1596
2184
|
);
|
|
1597
|
-
return
|
|
2185
|
+
return responseWithErrorObservability(
|
|
2186
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2187
|
+
error,
|
|
2188
|
+
);
|
|
1598
2189
|
}
|
|
1599
2190
|
});
|
|
1600
2191
|
|
|
@@ -1606,17 +2197,8 @@ export function createServerApp(
|
|
|
1606
2197
|
.clone()
|
|
1607
2198
|
.json()
|
|
1608
2199
|
.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
|
-
);
|
|
2200
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2201
|
+
const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
|
|
1620
2202
|
logProviderSuccess(
|
|
1621
2203
|
logger,
|
|
1622
2204
|
provider,
|
|
@@ -1640,7 +2222,10 @@ export function createServerApp(
|
|
|
1640
2222
|
status,
|
|
1641
2223
|
finishRequestCost(requestCost),
|
|
1642
2224
|
);
|
|
1643
|
-
return
|
|
2225
|
+
return responseWithErrorObservability(
|
|
2226
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2227
|
+
error,
|
|
2228
|
+
);
|
|
1644
2229
|
}
|
|
1645
2230
|
});
|
|
1646
2231
|
|
|
@@ -1652,17 +2237,8 @@ export function createServerApp(
|
|
|
1652
2237
|
.clone()
|
|
1653
2238
|
.json()
|
|
1654
2239
|
.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
|
-
);
|
|
2240
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2241
|
+
const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
|
|
1666
2242
|
logProviderSuccess(
|
|
1667
2243
|
logger,
|
|
1668
2244
|
provider,
|
|
@@ -1686,19 +2262,64 @@ export function createServerApp(
|
|
|
1686
2262
|
status,
|
|
1687
2263
|
finishRequestCost(requestCost),
|
|
1688
2264
|
);
|
|
1689
|
-
return
|
|
2265
|
+
return responseWithErrorObservability(
|
|
2266
|
+
c.json(toErrorResponse(error, requestId), status),
|
|
2267
|
+
error,
|
|
2268
|
+
);
|
|
1690
2269
|
}
|
|
1691
2270
|
});
|
|
1692
2271
|
|
|
1693
2272
|
return app;
|
|
1694
2273
|
}
|
|
1695
2274
|
|
|
2275
|
+
function validateStatefulServerConfig(options: ProviderServerOptions): void {
|
|
2276
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
2277
|
+
throw new Error(
|
|
2278
|
+
"Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
"Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
if (
|
|
2287
|
+
options.statefulForwarding &&
|
|
2288
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function"
|
|
2289
|
+
) {
|
|
2290
|
+
throw new Error(
|
|
2291
|
+
"Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
if (
|
|
2295
|
+
options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
2296
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
2297
|
+
options.statefulForwarding.maxSkewMs <= 0)
|
|
2298
|
+
) {
|
|
2299
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
2300
|
+
}
|
|
2301
|
+
if (
|
|
2302
|
+
options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
2303
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
2304
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)
|
|
2305
|
+
) {
|
|
2306
|
+
throw new Error(
|
|
2307
|
+
"Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1696
2312
|
type BunServeRuntime = {
|
|
1697
2313
|
serve: (options: {
|
|
1698
2314
|
port: number;
|
|
1699
2315
|
hostname: string;
|
|
1700
2316
|
fetch: (request: Request) => Response | Promise<Response>;
|
|
1701
|
-
}) =>
|
|
2317
|
+
}) => BunServerHandle;
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
type BunServerHandle = {
|
|
2321
|
+
readonly port: number;
|
|
2322
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
1702
2323
|
};
|
|
1703
2324
|
|
|
1704
2325
|
function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
@@ -1714,11 +2335,20 @@ function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
|
1714
2335
|
|
|
1715
2336
|
return {
|
|
1716
2337
|
serve(options) {
|
|
1717
|
-
return serve(options);
|
|
2338
|
+
return serve(options) as BunServerHandle;
|
|
1718
2339
|
},
|
|
1719
2340
|
};
|
|
1720
2341
|
}
|
|
1721
2342
|
|
|
2343
|
+
export type ProviderServerCloseOptions = {
|
|
2344
|
+
readonly timeoutMs?: number;
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
export type ProviderServerHandle = {
|
|
2348
|
+
readonly port: number;
|
|
2349
|
+
close(options?: ProviderServerCloseOptions): Promise<void>;
|
|
2350
|
+
};
|
|
2351
|
+
|
|
1722
2352
|
export interface ServeOptions extends ProviderServerOptions {
|
|
1723
2353
|
host?: string;
|
|
1724
2354
|
port?: number;
|
|
@@ -1730,46 +2360,233 @@ export interface ServeOptions extends ProviderServerOptions {
|
|
|
1730
2360
|
selfTestPort?: number;
|
|
1731
2361
|
}
|
|
1732
2362
|
|
|
2363
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
2364
|
+
const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
|
|
2365
|
+
|
|
2366
|
+
type SignalServerRegistration = {
|
|
2367
|
+
readonly signals: ReadonlySet<NodeJS.Signals>;
|
|
2368
|
+
readonly close: () => Promise<void>;
|
|
2369
|
+
};
|
|
2370
|
+
|
|
2371
|
+
type ProcessSignalCoordinator = {
|
|
2372
|
+
readonly registrations: Set<SignalServerRegistration>;
|
|
2373
|
+
readonly listener: () => void;
|
|
2374
|
+
handling: boolean;
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
|
|
2378
|
+
|
|
1733
2379
|
export async function serve(
|
|
1734
2380
|
provider: ProviderDefinition,
|
|
1735
2381
|
options: ServeOptions = {},
|
|
1736
|
-
): Promise<
|
|
2382
|
+
): Promise<ProviderServerHandle> {
|
|
1737
2383
|
const bunRuntime = getBunServeRuntime();
|
|
1738
2384
|
|
|
1739
2385
|
if (bunRuntime === undefined) {
|
|
1740
|
-
throw new ProviderError(
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
code: "RUNTIME_UNSUPPORTED",
|
|
1744
|
-
},
|
|
1745
|
-
);
|
|
2386
|
+
throw new ProviderError("Bun runtime is required to start the provider server", {
|
|
2387
|
+
code: "RUNTIME_UNSUPPORTED",
|
|
2388
|
+
});
|
|
1746
2389
|
}
|
|
2390
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
2391
|
+
const configuredTimeoutMs = shutdownTimeout(
|
|
2392
|
+
options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
2393
|
+
);
|
|
2394
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1747
2395
|
|
|
1748
2396
|
const app = createServerApp(provider, {
|
|
1749
2397
|
logger: options.logger,
|
|
1750
2398
|
stt: options.stt,
|
|
2399
|
+
state: options.state,
|
|
2400
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
2401
|
+
operationExecutor: options.operationExecutor,
|
|
2402
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
2403
|
+
statefulForwarding: options.statefulForwarding,
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
const servers: BunServerHandle[] = [];
|
|
2407
|
+
try {
|
|
2408
|
+
servers.push(
|
|
2409
|
+
bunRuntime.serve({
|
|
2410
|
+
port: options.port ?? DEFAULT_PORT,
|
|
2411
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2412
|
+
fetch: app.fetch,
|
|
2413
|
+
}),
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2416
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
2417
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
2418
|
+
// starts when the shared self-test master secret env is present.
|
|
2419
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
2420
|
+
if (selfTestSecrets) {
|
|
2421
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
2422
|
+
secrets: selfTestSecrets,
|
|
2423
|
+
invoke: createSelfTestInvoke(app),
|
|
2424
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
2425
|
+
});
|
|
2426
|
+
servers.push(
|
|
2427
|
+
bunRuntime.serve({
|
|
2428
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
2429
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2430
|
+
fetch: selfTestApp.fetch,
|
|
2431
|
+
}),
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2436
|
+
throw error;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const server = servers[0];
|
|
2440
|
+
if (!server) throw new Error("Provider server failed to create its primary listener.");
|
|
2441
|
+
let closePromise: Promise<void> | undefined;
|
|
2442
|
+
let unregisterSignals = () => {};
|
|
2443
|
+
|
|
2444
|
+
const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
|
|
2445
|
+
if (closePromise) return closePromise;
|
|
2446
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
2447
|
+
closePromise = closeProviderServers({
|
|
2448
|
+
servers,
|
|
2449
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
2450
|
+
timeoutMs,
|
|
2451
|
+
logger,
|
|
2452
|
+
providerId: provider.id,
|
|
2453
|
+
}).finally(() => unregisterSignals());
|
|
2454
|
+
return closePromise;
|
|
2455
|
+
};
|
|
2456
|
+
|
|
2457
|
+
try {
|
|
2458
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () =>
|
|
2459
|
+
close({ timeoutMs: configuredTimeoutMs }),
|
|
2460
|
+
);
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
unregisterSignals();
|
|
2463
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2464
|
+
throw error;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
return { port: server.port, close };
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function registerForProcessSignals(
|
|
2471
|
+
signals: NodeJS.Signals[],
|
|
2472
|
+
close: () => Promise<void>,
|
|
2473
|
+
): () => void {
|
|
2474
|
+
if (signals.length === 0) return () => {};
|
|
2475
|
+
const registration: SignalServerRegistration = {
|
|
2476
|
+
signals: new Set(signals),
|
|
2477
|
+
close,
|
|
2478
|
+
};
|
|
2479
|
+
let registered = true;
|
|
2480
|
+
const unregister = () => {
|
|
2481
|
+
if (!registered) return;
|
|
2482
|
+
registered = false;
|
|
2483
|
+
for (const signal of registration.signals) {
|
|
2484
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
2485
|
+
if (!coordinator) continue;
|
|
2486
|
+
coordinator.registrations.delete(registration);
|
|
2487
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
2488
|
+
process.removeListener(signal, coordinator.listener);
|
|
2489
|
+
processSignalCoordinators.delete(signal);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
try {
|
|
2494
|
+
for (const signal of signals) {
|
|
2495
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
2496
|
+
if (!coordinator) {
|
|
2497
|
+
const created: ProcessSignalCoordinator = {
|
|
2498
|
+
registrations: new Set(),
|
|
2499
|
+
handling: false,
|
|
2500
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
2501
|
+
};
|
|
2502
|
+
coordinator = created;
|
|
2503
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
2504
|
+
process.on(signal, coordinator.listener);
|
|
2505
|
+
}
|
|
2506
|
+
coordinator.registrations.add(registration);
|
|
2507
|
+
}
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
unregister();
|
|
2510
|
+
throw error;
|
|
2511
|
+
}
|
|
2512
|
+
return unregister;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function handleCoordinatedSignal(
|
|
2516
|
+
signal: NodeJS.Signals,
|
|
2517
|
+
coordinator: ProcessSignalCoordinator,
|
|
2518
|
+
): void {
|
|
2519
|
+
if (coordinator.handling) return;
|
|
2520
|
+
coordinator.handling = true;
|
|
2521
|
+
const registrations = [...coordinator.registrations];
|
|
2522
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
2523
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
2524
|
+
process.removeListener(signal, coordinator.listener);
|
|
2525
|
+
processSignalCoordinators.delete(signal);
|
|
2526
|
+
}
|
|
2527
|
+
try {
|
|
2528
|
+
process.kill(process.pid, signal);
|
|
2529
|
+
} catch {
|
|
2530
|
+
process.exitCode = 1;
|
|
2531
|
+
}
|
|
1751
2532
|
});
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
async function closeProviderServers(input: {
|
|
2536
|
+
readonly servers: BunServerHandle[];
|
|
2537
|
+
readonly hooks: Array<() => Promise<void>>;
|
|
2538
|
+
readonly timeoutMs: number;
|
|
2539
|
+
readonly logger: ProviderServerLogger;
|
|
2540
|
+
readonly providerId: string;
|
|
2541
|
+
}): Promise<void> {
|
|
2542
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
2543
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
2544
|
+
for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
|
|
2545
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
2546
|
+
try {
|
|
2547
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
try {
|
|
2550
|
+
input.logger({
|
|
2551
|
+
level: "error",
|
|
2552
|
+
event: "provider_shutdown_hook_failed",
|
|
2553
|
+
providerId: input.providerId,
|
|
2554
|
+
hookIndex,
|
|
2555
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
2556
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
2557
|
+
});
|
|
2558
|
+
} catch {}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
2562
|
+
await withinShutdownBudget(
|
|
2563
|
+
Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
|
|
2564
|
+
deadline,
|
|
2565
|
+
).catch(() => undefined);
|
|
2566
|
+
}
|
|
1752
2567
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2568
|
+
async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
|
|
2569
|
+
promise.catch(() => undefined);
|
|
2570
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
2571
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2572
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
2573
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1757
2574
|
});
|
|
2575
|
+
try {
|
|
2576
|
+
return await Promise.race([promise, timeout]);
|
|
2577
|
+
} finally {
|
|
2578
|
+
if (timer) clearTimeout(timer);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
1758
2581
|
|
|
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
|
-
});
|
|
2582
|
+
function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
|
|
2583
|
+
if (signals === false) return [];
|
|
2584
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
function shutdownTimeout(value: number): number {
|
|
2588
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2589
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1773
2590
|
}
|
|
1774
|
-
|
|
2591
|
+
return value;
|
|
1775
2592
|
}
|