@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11
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 +106 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -1
- 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 +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +510 -45
- 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 +149 -8
- package/dist/config/loader.js +378 -89
- 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 +8 -1
- package/dist/define.js +119 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -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 +44 -42
- package/dist/index.js +39 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -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 +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +144 -348
- 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 +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- 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 +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- 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 +2 -2
- package/dist/runtime/stealth.js +108 -212
- 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-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 +105 -4
- package/dist/server/serve.js +588 -80
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -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 +207 -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.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 +1 -1
- package/dist/testing/run.js +7 -14
- package/dist/types.d.ts +87 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +14 -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 +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +247 -505
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- 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 +49 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +15 -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 +23 -20
- package/src/runtime/http.ts +195 -559
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- 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 +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +162 -377
- 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 +24 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +859 -266
- package/src/server/types.ts +7 -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 +279 -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.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +19 -67
- package/src/types.ts +88 -1
- 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,72 @@ 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
7
|
import {
|
|
8
8
|
AuthError,
|
|
9
|
+
isProviderError,
|
|
10
|
+
isSessionExpiredError,
|
|
11
|
+
isTransportError,
|
|
9
12
|
ProviderError,
|
|
10
|
-
|
|
11
|
-
TransportError,
|
|
12
|
-
} from "../errors";
|
|
13
|
+
} from "../errors.js";
|
|
13
14
|
import {
|
|
14
15
|
loadProviderLocaleCatalogs,
|
|
15
16
|
localizeAuthTurn,
|
|
16
17
|
type ProviderLocaleCatalogMap,
|
|
17
|
-
} from "../i18n/catalog";
|
|
18
|
-
import type { ProviderLocale } from "../i18n/keys";
|
|
18
|
+
} from "../i18n/catalog.js";
|
|
19
|
+
import type { ProviderLocale } from "../i18n/keys.js";
|
|
19
20
|
import {
|
|
20
21
|
categoryForStatus,
|
|
21
22
|
isRetryableCategory,
|
|
22
23
|
PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
23
24
|
type ProviderErrorCategory,
|
|
24
|
-
} from "../observability";
|
|
25
|
-
import { createScratchpad } from "../runtime/auth-flow";
|
|
26
|
-
import { createBrowserClient } from "../runtime/browser";
|
|
27
|
-
import { createProviderCache } from "../runtime/cache";
|
|
25
|
+
} from "../observability.js";
|
|
26
|
+
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
27
|
+
import { createBrowserClient } from "../runtime/browser.js";
|
|
28
|
+
import { createProviderCache } from "../runtime/cache.js";
|
|
28
29
|
import {
|
|
29
30
|
createProviderChoiceContext,
|
|
30
31
|
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 { getProviderBaseUrl } from "../runtime/provider";
|
|
32
|
+
} from "../runtime/choice.js";
|
|
33
|
+
import { createCredentialContext } from "../runtime/credential.js";
|
|
34
|
+
import { createEnvContext } from "../runtime/env.js";
|
|
35
|
+
import { executeOperation } from "../runtime/executor.js";
|
|
36
|
+
import { createHttpClient } from "../runtime/http.js";
|
|
37
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
38
|
+
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
38
39
|
import {
|
|
39
40
|
PROXY_AUTH_IP_DENIED_CODE,
|
|
40
41
|
PROXY_EDGE_AUTH_REJECTED_CODE,
|
|
41
42
|
PROXY_POOL_EXHAUSTED_CODE,
|
|
42
|
-
} from "../runtime/proxy-errors";
|
|
43
|
+
} from "../runtime/proxy-errors.js";
|
|
44
|
+
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
43
45
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
assertRequiredSecretsPresent,
|
|
47
|
+
listMissingRequiredSecrets,
|
|
48
|
+
MISSING_SECRET_CODE,
|
|
49
|
+
} from "../runtime/secrets.js";
|
|
47
50
|
import {
|
|
48
51
|
createProviderRuntimeStateFromEnv,
|
|
49
52
|
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 {
|
|
53
|
+
} from "../runtime/state.js";
|
|
54
|
+
import { createStealthClient } from "../runtime/stealth.js";
|
|
55
|
+
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
56
|
+
import { createTraceContext } from "../runtime/trace.js";
|
|
57
|
+
import { parseSchema } from "../schema.js";
|
|
58
|
+
import {
|
|
59
|
+
STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER,
|
|
60
|
+
STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER,
|
|
61
|
+
STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER,
|
|
62
|
+
verifyStatefulRequestSignature,
|
|
63
|
+
} from "../stateful-signing.js";
|
|
64
|
+
import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
|
|
65
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
56
66
|
import {
|
|
57
67
|
APIFUSE_STREAM_DONE_EVENT,
|
|
58
68
|
APIFUSE_STREAM_ERROR_EVENT,
|
|
59
69
|
encodeSseEvent,
|
|
60
70
|
error as streamError,
|
|
61
|
-
} from "../stream";
|
|
71
|
+
} from "../stream.js";
|
|
62
72
|
import type {
|
|
63
73
|
AuthContext,
|
|
64
74
|
AuthTurn,
|
|
@@ -75,29 +85,98 @@ import type {
|
|
|
75
85
|
ProviderStreamEvent,
|
|
76
86
|
StealthClient,
|
|
77
87
|
SttContext,
|
|
78
|
-
} from "../types";
|
|
88
|
+
} from "../types.js";
|
|
79
89
|
import {
|
|
80
90
|
createSelfTestApp,
|
|
91
|
+
createSelfTestAuthFlowInvoke,
|
|
81
92
|
createSelfTestInvoke,
|
|
82
93
|
resolveSelfTestPort,
|
|
83
|
-
} from "./self-test";
|
|
84
|
-
import { resolveSelfTestMasterSecrets } from "./self-test-token";
|
|
94
|
+
} from "./self-test.js";
|
|
95
|
+
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
85
96
|
import {
|
|
86
97
|
type AuthFlowRequest,
|
|
87
98
|
AuthFlowRequestSchema,
|
|
88
99
|
type AuthFlowResponse,
|
|
89
100
|
type AuthFlowSuccessResponse,
|
|
101
|
+
OperationConnectionSchema,
|
|
90
102
|
type OperationErrorResponse,
|
|
91
103
|
type OperationRequest,
|
|
92
104
|
OperationRequestSchema,
|
|
93
105
|
type OperationResponse,
|
|
94
106
|
type OperationSuccessResponse,
|
|
95
|
-
} from "./types";
|
|
107
|
+
} from "./types.js";
|
|
96
108
|
|
|
97
109
|
const DEFAULT_HOST = "0.0.0.0";
|
|
98
110
|
const DEFAULT_PORT = 3000;
|
|
99
111
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"] as const;
|
|
100
112
|
const retryResponseMeta = new WeakMap<ProviderContext, HttpRetrySummary>();
|
|
113
|
+
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
114
|
+
const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
|
|
115
|
+
const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
|
|
116
|
+
const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
|
|
117
|
+
const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
|
|
118
|
+
const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(
|
|
119
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
export const ProviderServerStatefulForwardEnvelopeSchema = z
|
|
123
|
+
.object({
|
|
124
|
+
requestId: z.string().min(1),
|
|
125
|
+
providerId: z.string().min(1),
|
|
126
|
+
operationId: z.string().min(1),
|
|
127
|
+
sessionKey: z.string().min(1),
|
|
128
|
+
connectionId: z.string().min(1),
|
|
129
|
+
serviceAccountId: z.string().min(1),
|
|
130
|
+
ownerPodId: z.string().min(1),
|
|
131
|
+
generation: z.number().int().positive(),
|
|
132
|
+
sourcePodId: z.string().min(1),
|
|
133
|
+
forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
134
|
+
deadlineAt: z
|
|
135
|
+
.string()
|
|
136
|
+
.refine((value) => Number.isFinite(Date.parse(value)))
|
|
137
|
+
.optional(),
|
|
138
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
139
|
+
operationRequest: OperationRequestSchema.extend({
|
|
140
|
+
connection: OperationConnectionSchema.strict().optional(),
|
|
141
|
+
}).strict(),
|
|
142
|
+
})
|
|
143
|
+
.strict();
|
|
144
|
+
|
|
145
|
+
export type ProviderServerStatefulForwardEnvelope = Readonly<
|
|
146
|
+
z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>
|
|
147
|
+
>;
|
|
148
|
+
|
|
149
|
+
export type ProviderServerStatefulOwnerFence = Readonly<
|
|
150
|
+
Pick<
|
|
151
|
+
ProviderServerStatefulForwardEnvelope,
|
|
152
|
+
| "providerId"
|
|
153
|
+
| "sessionKey"
|
|
154
|
+
| "ownerPodId"
|
|
155
|
+
| "generation"
|
|
156
|
+
| "sourcePodId"
|
|
157
|
+
| "forwardedAt"
|
|
158
|
+
| "requestId"
|
|
159
|
+
| "idempotencyKey"
|
|
160
|
+
>
|
|
161
|
+
>;
|
|
162
|
+
|
|
163
|
+
export type ProviderServerStatefulOwnerFenceValidator = (
|
|
164
|
+
fence: ProviderServerStatefulOwnerFence,
|
|
165
|
+
signal: AbortSignal,
|
|
166
|
+
) => boolean | Promise<boolean>;
|
|
167
|
+
|
|
168
|
+
export type ProviderServerOperationExecutorInput = {
|
|
169
|
+
readonly provider: ProviderDefinition;
|
|
170
|
+
readonly operationId: string;
|
|
171
|
+
readonly ctx: ProviderContext;
|
|
172
|
+
readonly request: OperationRequest & { readonly deadlineAt?: string };
|
|
173
|
+
readonly signal?: AbortSignal;
|
|
174
|
+
readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export type ProviderServerOperationExecutor = (
|
|
178
|
+
input: ProviderServerOperationExecutorInput,
|
|
179
|
+
) => Promise<unknown>;
|
|
101
180
|
|
|
102
181
|
type RequestCleanup = () => void | Promise<void>;
|
|
103
182
|
|
|
@@ -156,9 +235,7 @@ function createStealthStub(): StealthClient {
|
|
|
156
235
|
};
|
|
157
236
|
}
|
|
158
237
|
|
|
159
|
-
function getProviderStealthBaseUrl(
|
|
160
|
-
provider: ProviderDefinition,
|
|
161
|
-
): string | undefined {
|
|
238
|
+
function getProviderStealthBaseUrl(provider: ProviderDefinition): string | undefined {
|
|
162
239
|
const baseUrl = getProviderBaseUrl(provider);
|
|
163
240
|
if (baseUrl) {
|
|
164
241
|
return baseUrl;
|
|
@@ -168,15 +245,10 @@ function getProviderStealthBaseUrl(
|
|
|
168
245
|
}
|
|
169
246
|
|
|
170
247
|
function getProviderStealthProfile(provider: ProviderDefinition) {
|
|
171
|
-
return provider.stealth?.profile
|
|
172
|
-
? getStealthProfile(provider.stealth.profile)
|
|
173
|
-
: undefined;
|
|
248
|
+
return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
|
|
174
249
|
}
|
|
175
250
|
|
|
176
|
-
function isProductionProviderBrowserMode(
|
|
177
|
-
provider: ProviderDefinition,
|
|
178
|
-
env = process.env,
|
|
179
|
-
): boolean {
|
|
251
|
+
function isProductionProviderBrowserMode(provider: ProviderDefinition, env = process.env): boolean {
|
|
180
252
|
if (provider.runtime !== "browser") {
|
|
181
253
|
return false;
|
|
182
254
|
}
|
|
@@ -185,9 +257,7 @@ function isProductionProviderBrowserMode(
|
|
|
185
257
|
return true;
|
|
186
258
|
}
|
|
187
259
|
|
|
188
|
-
return
|
|
189
|
-
env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id
|
|
190
|
-
);
|
|
260
|
+
return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
|
|
191
261
|
}
|
|
192
262
|
|
|
193
263
|
export function resolveProviderProxyAffinityKey(
|
|
@@ -195,18 +265,19 @@ export function resolveProviderProxyAffinityKey(
|
|
|
195
265
|
request: OperationRequest,
|
|
196
266
|
operationId: string,
|
|
197
267
|
): string {
|
|
198
|
-
const connectionKey =
|
|
199
|
-
request.connection?.id ?? request.connection?.externalRef;
|
|
268
|
+
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
200
269
|
const affinity =
|
|
201
|
-
typeof provider.proxy === "object"
|
|
202
|
-
? provider.proxy.session?.affinity
|
|
203
|
-
: undefined;
|
|
270
|
+
typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
|
|
204
271
|
if (affinity === "operation") {
|
|
205
272
|
return `${provider.id}/${operationId}`;
|
|
206
273
|
}
|
|
207
274
|
return connectionKey ?? provider.id;
|
|
208
275
|
}
|
|
209
276
|
|
|
277
|
+
function resolveOperationConnectionId(request: OperationRequest): string | undefined {
|
|
278
|
+
return request.connection?.id ?? request.connectionId;
|
|
279
|
+
}
|
|
280
|
+
|
|
210
281
|
function createProviderContext(
|
|
211
282
|
provider: ProviderDefinition,
|
|
212
283
|
request: OperationRequest,
|
|
@@ -220,11 +291,7 @@ function createProviderContext(
|
|
|
220
291
|
const stealthProfile = getProviderStealthProfile(provider);
|
|
221
292
|
const proxyClientOptions = {
|
|
222
293
|
upstream: { proxy: provider.proxy },
|
|
223
|
-
affinityKey: resolveProviderProxyAffinityKey(
|
|
224
|
-
provider,
|
|
225
|
-
request,
|
|
226
|
-
operationId,
|
|
227
|
-
),
|
|
294
|
+
affinityKey: resolveProviderProxyAffinityKey(provider, request, operationId),
|
|
228
295
|
telemetry: proxyTelemetry,
|
|
229
296
|
};
|
|
230
297
|
let wrappedContext: ProviderContext | undefined;
|
|
@@ -245,7 +312,7 @@ function createProviderContext(
|
|
|
245
312
|
values: request.connection?.secrets,
|
|
246
313
|
});
|
|
247
314
|
const requestContext = {
|
|
248
|
-
connectionId: request
|
|
315
|
+
connectionId: resolveOperationConnectionId(request),
|
|
249
316
|
headers: request.headers ?? {},
|
|
250
317
|
};
|
|
251
318
|
const context = wrapWithInstrumentation({
|
|
@@ -263,11 +330,7 @@ function createProviderContext(
|
|
|
263
330
|
state,
|
|
264
331
|
stealth: stealthBaseUrl
|
|
265
332
|
? stealthProfile
|
|
266
|
-
? createStealthClient(
|
|
267
|
-
stealthBaseUrl,
|
|
268
|
-
stealthProfile.name,
|
|
269
|
-
stealthClientOptions,
|
|
270
|
-
)
|
|
333
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
271
334
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
272
335
|
: createStealthStub(),
|
|
273
336
|
browser:
|
|
@@ -380,11 +443,7 @@ function createAuthFlowContext(
|
|
|
380
443
|
http: createHttpClient(baseUrl, proxyClientOptions),
|
|
381
444
|
stealth: stealthBaseUrl
|
|
382
445
|
? stealthProfile
|
|
383
|
-
? createStealthClient(
|
|
384
|
-
stealthBaseUrl,
|
|
385
|
-
stealthProfile.name,
|
|
386
|
-
stealthClientOptions,
|
|
387
|
-
)
|
|
446
|
+
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
388
447
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
389
448
|
: createStealthStub(),
|
|
390
449
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
@@ -429,6 +488,12 @@ export type ProviderServerLogEvent =
|
|
|
429
488
|
retryable?: boolean;
|
|
430
489
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
431
490
|
})
|
|
491
|
+
| {
|
|
492
|
+
level: "warn";
|
|
493
|
+
event: "provider_secrets_missing";
|
|
494
|
+
providerId: string;
|
|
495
|
+
missingSecrets: string[];
|
|
496
|
+
}
|
|
432
497
|
| {
|
|
433
498
|
level: "warn";
|
|
434
499
|
event: "provider_cleanup_failed";
|
|
@@ -439,18 +504,59 @@ export type ProviderServerLogEvent =
|
|
|
439
504
|
resource: "browser" | "stealth";
|
|
440
505
|
errorClass: string;
|
|
441
506
|
message: string;
|
|
507
|
+
}
|
|
508
|
+
| {
|
|
509
|
+
level: "error";
|
|
510
|
+
event: "provider_shutdown_hook_failed";
|
|
511
|
+
providerId: string;
|
|
512
|
+
hookIndex: number;
|
|
513
|
+
errorClass: string;
|
|
514
|
+
message: string;
|
|
442
515
|
};
|
|
443
516
|
|
|
444
517
|
export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
|
|
445
518
|
|
|
446
519
|
export type ProviderServerOptions = {
|
|
447
520
|
logger?: ProviderServerLogger;
|
|
521
|
+
/** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
|
|
522
|
+
operationExecutor?: ProviderServerOperationExecutor;
|
|
523
|
+
/** Optional signed internal executor for stateful owner forwarding. */
|
|
524
|
+
internalOperationExecutor?: ProviderServerOperationExecutor;
|
|
525
|
+
statefulForwarding?: {
|
|
526
|
+
readonly secret: string;
|
|
527
|
+
readonly maxSkewMs?: number;
|
|
528
|
+
readonly replayCacheMaxEntries?: number;
|
|
529
|
+
/** Required fail-closed check against the SDK/runtime owner registry. */
|
|
530
|
+
readonly validateOwnerFence: ProviderServerStatefulOwnerFenceValidator;
|
|
531
|
+
};
|
|
448
532
|
/** Optional STT override for tests or custom hosts; local/prod normally resolves from env. */
|
|
449
533
|
stt?: SttContext;
|
|
450
534
|
/** Optional runtime state override for tests or custom hosts. Production resolves Redis from env and fails closed when unavailable. */
|
|
451
535
|
state?: ProviderRuntimeState;
|
|
452
536
|
/** Allow process-local runtime state only for local development and tests. */
|
|
453
537
|
allowMemoryStateFallback?: boolean;
|
|
538
|
+
/**
|
|
539
|
+
* Graceful process shutdown. Hooks run in declaration order after listeners stop accepting work.
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* await serve(provider, {
|
|
544
|
+
* shutdown: {
|
|
545
|
+
* hooks: [
|
|
546
|
+
* async () => { await emitter.flush(); },
|
|
547
|
+
* async () => { await sessionManager.closeAll("server-shutdown"); },
|
|
548
|
+
* async () => { await lease.release(); },
|
|
549
|
+
* async () => { await router.close(); },
|
|
550
|
+
* ],
|
|
551
|
+
* },
|
|
552
|
+
* });
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
shutdown?: {
|
|
556
|
+
readonly hooks?: Array<() => Promise<void>>;
|
|
557
|
+
readonly signals?: boolean | NodeJS.Signals[];
|
|
558
|
+
readonly timeoutMs?: number;
|
|
559
|
+
};
|
|
454
560
|
};
|
|
455
561
|
|
|
456
562
|
const defaultProviderServerLogger: ProviderServerLogger = (event) => {
|
|
@@ -497,11 +603,19 @@ function zodDetails(error: z.ZodError): Array<{
|
|
|
497
603
|
}));
|
|
498
604
|
}
|
|
499
605
|
|
|
500
|
-
function toErrorResponse(
|
|
501
|
-
error
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
606
|
+
function toErrorResponse(error: unknown, requestId?: string): OperationErrorResponse {
|
|
607
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
608
|
+
return {
|
|
609
|
+
error: {
|
|
610
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
611
|
+
message: "Stateful forwarding deadline expired.",
|
|
612
|
+
...(requestId ? { requestId } : {}),
|
|
613
|
+
details: { retryable: false },
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (isProviderError(error)) {
|
|
505
619
|
const details = publicProviderErrorDetails(error);
|
|
506
620
|
return {
|
|
507
621
|
error: {
|
|
@@ -525,11 +639,22 @@ function toErrorResponse(
|
|
|
525
639
|
};
|
|
526
640
|
}
|
|
527
641
|
|
|
642
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
643
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
644
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
645
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
646
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
647
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
528
648
|
return {
|
|
529
649
|
error: {
|
|
530
650
|
code: "internal_error",
|
|
531
651
|
message: "Internal error",
|
|
532
652
|
...(requestId ? { requestId } : {}),
|
|
653
|
+
details: {
|
|
654
|
+
retryable: false,
|
|
655
|
+
category: "internal_error",
|
|
656
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
657
|
+
},
|
|
533
658
|
},
|
|
534
659
|
};
|
|
535
660
|
}
|
|
@@ -557,7 +682,12 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
557
682
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
558
683
|
}
|
|
559
684
|
|
|
560
|
-
|
|
685
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
686
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
687
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
688
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
689
|
+
// from a duplicate SDK module instance.
|
|
690
|
+
function providerObservabilityDetails(error: unknown):
|
|
561
691
|
| {
|
|
562
692
|
category: ProviderErrorCategory;
|
|
563
693
|
taxonomyVersion: string;
|
|
@@ -570,14 +700,26 @@ function providerObservabilityDetails(error: ProviderError):
|
|
|
570
700
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
571
701
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
572
702
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
573
|
-
if (error
|
|
703
|
+
if (isSessionExpiredError(error)) {
|
|
574
704
|
return {
|
|
575
705
|
category: error.options?.category ?? "credential_expired",
|
|
576
706
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
577
707
|
retryable: error.options?.retryable ?? false,
|
|
578
708
|
};
|
|
579
709
|
}
|
|
580
|
-
|
|
710
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
711
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
712
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
713
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
714
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
715
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
716
|
+
return {
|
|
717
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
718
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
719
|
+
retryable: error.options?.retryable ?? false,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (!isTransportError(error)) {
|
|
581
723
|
return undefined;
|
|
582
724
|
}
|
|
583
725
|
const isProxyPoolCode =
|
|
@@ -610,7 +752,7 @@ function providerObservabilityDetails(error: ProviderError):
|
|
|
610
752
|
}
|
|
611
753
|
|
|
612
754
|
function publicProviderErrorMessage(error: ProviderError): string {
|
|
613
|
-
if (error
|
|
755
|
+
if (isTransportError(error)) {
|
|
614
756
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
615
757
|
return error.message;
|
|
616
758
|
}
|
|
@@ -633,22 +775,27 @@ function publicProviderErrorMessage(error: ProviderError): string {
|
|
|
633
775
|
return error.message;
|
|
634
776
|
}
|
|
635
777
|
|
|
636
|
-
function toStatusCode(
|
|
637
|
-
error: unknown,
|
|
638
|
-
): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
778
|
+
function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 | 504 {
|
|
639
779
|
if (error instanceof z.ZodError) {
|
|
640
780
|
return 400;
|
|
641
781
|
}
|
|
782
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
783
|
+
return 504;
|
|
784
|
+
}
|
|
642
785
|
|
|
643
|
-
if (error
|
|
786
|
+
if (isTransportError(error)) {
|
|
644
787
|
return error.code === "transport_timeout" ? 504 : 502;
|
|
645
788
|
}
|
|
646
789
|
|
|
647
|
-
if (error
|
|
790
|
+
if (isProviderError(error)) {
|
|
648
791
|
switch (error.code) {
|
|
649
792
|
case "AUTH_REQUIRED":
|
|
650
793
|
case "reauth_required":
|
|
651
794
|
return 401;
|
|
795
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
796
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
797
|
+
case MISSING_SECRET_CODE:
|
|
798
|
+
return 400;
|
|
652
799
|
case "NOT_FOUND":
|
|
653
800
|
case "not_found":
|
|
654
801
|
case "NO_DATA":
|
|
@@ -662,6 +809,7 @@ function toStatusCode(
|
|
|
662
809
|
return 502;
|
|
663
810
|
case "STT_UNAVAILABLE":
|
|
664
811
|
case "UNSUPPORTED_STT_BACKEND":
|
|
812
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
665
813
|
return 503;
|
|
666
814
|
}
|
|
667
815
|
|
|
@@ -690,20 +838,17 @@ function logProviderError(
|
|
|
690
838
|
status: number,
|
|
691
839
|
cost: ProviderRequestCost,
|
|
692
840
|
): void {
|
|
693
|
-
const code =
|
|
694
|
-
error
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
841
|
+
const code = isProviderError(error)
|
|
842
|
+
? (error.code ?? "provider_error")
|
|
843
|
+
: error instanceof z.ZodError
|
|
844
|
+
? "invalid_request"
|
|
845
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
846
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
698
847
|
: "internal_error";
|
|
699
848
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
700
849
|
const message = error instanceof Error ? error.message : String(error);
|
|
701
|
-
const details =
|
|
702
|
-
|
|
703
|
-
? providerObservabilityDetails(error)
|
|
704
|
-
: undefined;
|
|
705
|
-
const emit =
|
|
706
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
850
|
+
const details = isProviderError(error) ? providerObservabilityDetails(error) : undefined;
|
|
851
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
707
852
|
emit({
|
|
708
853
|
level: status >= 500 ? "error" : "warn",
|
|
709
854
|
event: "provider_request_failed",
|
|
@@ -716,7 +861,7 @@ function logProviderError(
|
|
|
716
861
|
code,
|
|
717
862
|
errorClass,
|
|
718
863
|
message,
|
|
719
|
-
...(error
|
|
864
|
+
...(isTransportError(error) && error.upstreamStatus
|
|
720
865
|
? { upstreamStatus: error.upstreamStatus }
|
|
721
866
|
: {}),
|
|
722
867
|
...(details
|
|
@@ -738,8 +883,7 @@ function logProviderCleanupError(
|
|
|
738
883
|
resource: "browser" | "stealth",
|
|
739
884
|
error: unknown,
|
|
740
885
|
): void {
|
|
741
|
-
const emit =
|
|
742
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
886
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
743
887
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
744
888
|
const message = error instanceof Error ? error.message : String(error);
|
|
745
889
|
emit({
|
|
@@ -764,8 +908,7 @@ function logProviderSuccess(
|
|
|
764
908
|
status: number,
|
|
765
909
|
cost: ProviderRequestCost,
|
|
766
910
|
): void {
|
|
767
|
-
const emit =
|
|
768
|
-
typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
911
|
+
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
769
912
|
emit({
|
|
770
913
|
level: "info",
|
|
771
914
|
event: "provider_request_completed",
|
|
@@ -811,18 +954,13 @@ function toJsonSuccessResponse(
|
|
|
811
954
|
};
|
|
812
955
|
}
|
|
813
956
|
|
|
814
|
-
function isAsyncIterable<T = unknown>(
|
|
815
|
-
value: unknown,
|
|
816
|
-
): value is AsyncIterable<T> {
|
|
957
|
+
function isAsyncIterable<T = unknown>(value: unknown): value is AsyncIterable<T> {
|
|
817
958
|
if (!value || typeof value !== "object") return false;
|
|
818
959
|
const iterator = Reflect.get(value, Symbol.asyncIterator);
|
|
819
960
|
return typeof iterator === "function";
|
|
820
961
|
}
|
|
821
962
|
|
|
822
|
-
function responseWithCleanup(
|
|
823
|
-
response: Response,
|
|
824
|
-
cleanup: RequestCleanup,
|
|
825
|
-
): Response {
|
|
963
|
+
function responseWithCleanup(response: Response, cleanup: RequestCleanup): Response {
|
|
826
964
|
if (!response.body) {
|
|
827
965
|
void cleanup();
|
|
828
966
|
return response;
|
|
@@ -871,10 +1009,7 @@ async function validateSseEvent(
|
|
|
871
1009
|
const transport = getSseTransport(operation);
|
|
872
1010
|
const schema = transport?.events?.[event.event];
|
|
873
1011
|
if (!schema) {
|
|
874
|
-
if (
|
|
875
|
-
event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
876
|
-
event.event === APIFUSE_STREAM_DONE_EVENT
|
|
877
|
-
) {
|
|
1012
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
878
1013
|
return event;
|
|
879
1014
|
}
|
|
880
1015
|
throw new ProviderError(
|
|
@@ -887,11 +1022,7 @@ async function validateSseEvent(
|
|
|
887
1022
|
},
|
|
888
1023
|
);
|
|
889
1024
|
}
|
|
890
|
-
const data = await parseSchema(
|
|
891
|
-
schema,
|
|
892
|
-
event.data,
|
|
893
|
-
`transport.events.${event.event}`,
|
|
894
|
-
);
|
|
1025
|
+
const data = await parseSchema(schema, event.data, `transport.events.${event.event}`);
|
|
895
1026
|
return { ...event, data };
|
|
896
1027
|
}
|
|
897
1028
|
|
|
@@ -911,8 +1042,7 @@ function assertStreamPayloadWithinLimit(
|
|
|
911
1042
|
throw new ProviderError(
|
|
912
1043
|
`Stream ${kind} exceeded declared byte limit (${actualBytes} > ${maxBytes}).`,
|
|
913
1044
|
{
|
|
914
|
-
code:
|
|
915
|
-
kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
1045
|
+
code: kind === "event" ? "STREAM_EVENT_TOO_LARGE" : "STREAM_CHUNK_TOO_LARGE",
|
|
916
1046
|
retryable: false,
|
|
917
1047
|
category: "input_validation",
|
|
918
1048
|
fix:
|
|
@@ -957,15 +1087,10 @@ function toSseResponse(
|
|
|
957
1087
|
const validated = await validateSseEvent(operation, next.value);
|
|
958
1088
|
const encodedEvent = encodeSseEvent(validated);
|
|
959
1089
|
const encodedBytes = encoder.encode(encodedEvent);
|
|
960
|
-
assertStreamPayloadWithinLimit(
|
|
961
|
-
encodedBytes.byteLength,
|
|
962
|
-
transport?.maxEventBytes,
|
|
963
|
-
"event",
|
|
964
|
-
);
|
|
1090
|
+
assertStreamPayloadWithinLimit(encodedBytes.byteLength, transport?.maxEventBytes, "event");
|
|
965
1091
|
controller.enqueue(encodedBytes);
|
|
966
1092
|
} catch (error) {
|
|
967
|
-
const message =
|
|
968
|
-
error instanceof Error ? error.message : "Stream failed";
|
|
1093
|
+
const message = error instanceof Error ? error.message : "Stream failed";
|
|
969
1094
|
controller.enqueue(
|
|
970
1095
|
encoder.encode(
|
|
971
1096
|
encodeSseEvent(
|
|
@@ -1012,11 +1137,7 @@ function enforceStreamChunkLimit(
|
|
|
1012
1137
|
return;
|
|
1013
1138
|
}
|
|
1014
1139
|
if (value) {
|
|
1015
|
-
assertStreamPayloadWithinLimit(
|
|
1016
|
-
byteLength(value),
|
|
1017
|
-
maxChunkBytes,
|
|
1018
|
-
"chunk",
|
|
1019
|
-
);
|
|
1140
|
+
assertStreamPayloadWithinLimit(byteLength(value), maxChunkBytes, "chunk");
|
|
1020
1141
|
controller.enqueue(value);
|
|
1021
1142
|
}
|
|
1022
1143
|
} catch (error) {
|
|
@@ -1036,10 +1157,7 @@ function toStreamingResponse(
|
|
|
1036
1157
|
requestId?: string,
|
|
1037
1158
|
): Response {
|
|
1038
1159
|
const transport = operation.transport?.kind ?? "json";
|
|
1039
|
-
if (
|
|
1040
|
-
transport === "sse" &&
|
|
1041
|
-
(result instanceof Response || result instanceof ReadableStream)
|
|
1042
|
-
) {
|
|
1160
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
1043
1161
|
void cleanup();
|
|
1044
1162
|
throw new ProviderError(
|
|
1045
1163
|
"SSE operations must return an AsyncIterable of typed stream.event(...) values.",
|
|
@@ -1053,20 +1171,13 @@ function toStreamingResponse(
|
|
|
1053
1171
|
}
|
|
1054
1172
|
if (result instanceof Response) {
|
|
1055
1173
|
const httpTransport = getHttpStreamTransport(operation);
|
|
1056
|
-
if (
|
|
1057
|
-
httpTransport &&
|
|
1058
|
-
result.body &&
|
|
1059
|
-
httpTransport?.maxChunkBytes !== undefined
|
|
1060
|
-
) {
|
|
1174
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
1061
1175
|
return responseWithCleanup(
|
|
1062
|
-
new Response(
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
statusText: result.statusText,
|
|
1068
|
-
},
|
|
1069
|
-
),
|
|
1176
|
+
new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
1177
|
+
headers: result.headers,
|
|
1178
|
+
status: result.status,
|
|
1179
|
+
statusText: result.statusText,
|
|
1180
|
+
}),
|
|
1070
1181
|
cleanup,
|
|
1071
1182
|
);
|
|
1072
1183
|
}
|
|
@@ -1086,8 +1197,7 @@ function toStreamingResponse(
|
|
|
1086
1197
|
: {
|
|
1087
1198
|
"Content-Type":
|
|
1088
1199
|
operation.transport?.kind === "http-stream"
|
|
1089
|
-
? (operation.transport.contentType ??
|
|
1090
|
-
"application/octet-stream")
|
|
1200
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
1091
1201
|
: "application/octet-stream",
|
|
1092
1202
|
},
|
|
1093
1203
|
}),
|
|
@@ -1107,18 +1217,14 @@ function toStreamingResponse(
|
|
|
1107
1217
|
);
|
|
1108
1218
|
}
|
|
1109
1219
|
|
|
1110
|
-
function getSseTransport(
|
|
1111
|
-
operation: OperationDefinition,
|
|
1112
|
-
): OperationSseTransport | undefined {
|
|
1220
|
+
function getSseTransport(operation: OperationDefinition): OperationSseTransport | undefined {
|
|
1113
1221
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
1114
1222
|
}
|
|
1115
1223
|
|
|
1116
1224
|
function getHttpStreamTransport(
|
|
1117
1225
|
operation: OperationDefinition,
|
|
1118
1226
|
): OperationHttpStreamTransport | undefined {
|
|
1119
|
-
return operation.transport?.kind === "http-stream"
|
|
1120
|
-
? operation.transport
|
|
1121
|
-
: undefined;
|
|
1227
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
1122
1228
|
}
|
|
1123
1229
|
|
|
1124
1230
|
function toAuthFlowResponse(
|
|
@@ -1139,9 +1245,7 @@ function toAuthFlowResponse(
|
|
|
1139
1245
|
};
|
|
1140
1246
|
}
|
|
1141
1247
|
|
|
1142
|
-
function authFlowLocaleFromHeaders(
|
|
1143
|
-
headers?: Record<string, string>,
|
|
1144
|
-
): ProviderLocale {
|
|
1248
|
+
function authFlowLocaleFromHeaders(headers?: Record<string, string>): ProviderLocale {
|
|
1145
1249
|
const header = Object.entries(headers ?? {}).find(
|
|
1146
1250
|
([key]) => key.toLowerCase() === "accept-language",
|
|
1147
1251
|
)?.[1];
|
|
@@ -1159,9 +1263,7 @@ function isAuthFlowLocale(value: string | undefined): value is ProviderLocale {
|
|
|
1159
1263
|
}
|
|
1160
1264
|
|
|
1161
1265
|
function isAuthTurn(value: unknown): value is AuthTurn {
|
|
1162
|
-
return
|
|
1163
|
-
!!value && typeof value === "object" && "kind" in value && "turnId" in value
|
|
1164
|
-
);
|
|
1266
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
1165
1267
|
}
|
|
1166
1268
|
|
|
1167
1269
|
function loadAuthFlowLocaleCatalogs(
|
|
@@ -1198,10 +1300,7 @@ function materializeAuthFlowTurn(
|
|
|
1198
1300
|
});
|
|
1199
1301
|
}
|
|
1200
1302
|
|
|
1201
|
-
function withAuthRequestHeaders(
|
|
1202
|
-
request: AuthFlowRequest,
|
|
1203
|
-
headers: Headers,
|
|
1204
|
-
): AuthFlowRequest {
|
|
1303
|
+
function withAuthRequestHeaders(request: AuthFlowRequest, headers: Headers): AuthFlowRequest {
|
|
1205
1304
|
return {
|
|
1206
1305
|
...request,
|
|
1207
1306
|
headers: {
|
|
@@ -1219,17 +1318,9 @@ async function handleOperation(
|
|
|
1219
1318
|
state: ProviderRuntimeState = createUnsupportedProviderRuntimeState(),
|
|
1220
1319
|
proxyTelemetry?: ProxyTelemetryCollector,
|
|
1221
1320
|
): Promise<Response | OperationResponse> {
|
|
1222
|
-
const ctx = createProviderContext(
|
|
1223
|
-
provider,
|
|
1224
|
-
request,
|
|
1225
|
-
operationId,
|
|
1226
|
-
options,
|
|
1227
|
-
state,
|
|
1228
|
-
proxyTelemetry,
|
|
1229
|
-
);
|
|
1321
|
+
const ctx = createProviderContext(provider, request, operationId, options, state, proxyTelemetry);
|
|
1230
1322
|
const operation = provider.operations[operationId];
|
|
1231
|
-
const streaming =
|
|
1232
|
-
operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1323
|
+
const streaming = operation?.transport?.kind && operation.transport.kind !== "json";
|
|
1233
1324
|
let cleanupCalled = false;
|
|
1234
1325
|
const cleanup = async () => {
|
|
1235
1326
|
if (cleanupCalled) return;
|
|
@@ -1260,12 +1351,14 @@ async function handleOperation(
|
|
|
1260
1351
|
}
|
|
1261
1352
|
};
|
|
1262
1353
|
try {
|
|
1263
|
-
const result =
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1354
|
+
const result = options.operationExecutor
|
|
1355
|
+
? await options.operationExecutor({
|
|
1356
|
+
provider,
|
|
1357
|
+
operationId,
|
|
1358
|
+
ctx,
|
|
1359
|
+
request,
|
|
1360
|
+
})
|
|
1361
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
1269
1362
|
if (streaming && operation) {
|
|
1270
1363
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
1271
1364
|
}
|
|
@@ -1309,13 +1402,16 @@ async function handleAuthFlow(
|
|
|
1309
1402
|
});
|
|
1310
1403
|
}
|
|
1311
1404
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
);
|
|
1405
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
1406
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
1407
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
1408
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
1409
|
+
// stranded flow even when provisioning is broken.
|
|
1410
|
+
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
1318
1411
|
try {
|
|
1412
|
+
if (route !== "abort") {
|
|
1413
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
1414
|
+
}
|
|
1319
1415
|
const result =
|
|
1320
1416
|
route === "start"
|
|
1321
1417
|
? await flow.start(context)
|
|
@@ -1357,12 +1453,142 @@ async function handleAuthFlow(
|
|
|
1357
1453
|
}
|
|
1358
1454
|
}
|
|
1359
1455
|
|
|
1456
|
+
class StatefulForwardingReplayCache {
|
|
1457
|
+
readonly #nonces = new Map<string, number>();
|
|
1458
|
+
readonly #expiryBuckets = new Map<number, Set<string>>();
|
|
1459
|
+
#nextExpiryBucket?: number;
|
|
1460
|
+
#latestExpiryBucket?: number;
|
|
1461
|
+
|
|
1462
|
+
constructor(private readonly maxEntries: number) {}
|
|
1463
|
+
|
|
1464
|
+
claim(nonce: string, expiresAtMs: number, nowMs: number): "accepted" | "replayed" | "full" {
|
|
1465
|
+
this.dropExpiredBuckets(nowMs);
|
|
1466
|
+
if (this.#nonces.has(nonce)) return "replayed";
|
|
1467
|
+
if (this.#nonces.size >= this.maxEntries) return "full";
|
|
1468
|
+
const expiryBucket =
|
|
1469
|
+
Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1470
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1471
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1472
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set<string>();
|
|
1473
|
+
bucket.add(nonce);
|
|
1474
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1475
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1476
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1477
|
+
return "accepted";
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private dropExpiredBuckets(nowMs: number): void {
|
|
1481
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined) return;
|
|
1482
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1483
|
+
this.#nonces.clear();
|
|
1484
|
+
this.#expiryBuckets.clear();
|
|
1485
|
+
this.#nextExpiryBucket = undefined;
|
|
1486
|
+
this.#latestExpiryBucket = undefined;
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1490
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1491
|
+
if (bucket) {
|
|
1492
|
+
for (const cachedNonce of bucket) this.#nonces.delete(cachedNonce);
|
|
1493
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1494
|
+
}
|
|
1495
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function verifyStatefulForwardingRequest(input: {
|
|
1501
|
+
readonly options: ProviderServerOptions;
|
|
1502
|
+
readonly rawBody: string;
|
|
1503
|
+
readonly headers: Headers;
|
|
1504
|
+
readonly method: string;
|
|
1505
|
+
readonly path: string;
|
|
1506
|
+
readonly replayCache: StatefulForwardingReplayCache;
|
|
1507
|
+
}): void {
|
|
1508
|
+
const config = input.options.statefulForwarding;
|
|
1509
|
+
if (!config?.secret) {
|
|
1510
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1511
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1515
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1516
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1517
|
+
if (!timestamp || !signature || !nonce) {
|
|
1518
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1519
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
if (nonce.length > 256) {
|
|
1523
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1524
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
const timestampMs = Date.parse(timestamp);
|
|
1528
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1529
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1530
|
+
throw new ProviderError(
|
|
1531
|
+
"Stateful forwarding signature timestamp is outside the allowed skew.",
|
|
1532
|
+
{ code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" },
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (
|
|
1536
|
+
!verifyStatefulRequestSignature({
|
|
1537
|
+
secret: config.secret,
|
|
1538
|
+
timestamp,
|
|
1539
|
+
rawBody: input.rawBody,
|
|
1540
|
+
method: input.method,
|
|
1541
|
+
path: input.path,
|
|
1542
|
+
nonce,
|
|
1543
|
+
signature,
|
|
1544
|
+
})
|
|
1545
|
+
) {
|
|
1546
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1547
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1551
|
+
if (replayResult === "replayed") {
|
|
1552
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1553
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (replayResult === "full") {
|
|
1557
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1558
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function operationRequestFromForwardingEnvelope(
|
|
1564
|
+
envelope: ProviderServerStatefulForwardEnvelope,
|
|
1565
|
+
): OperationRequest & { readonly deadlineAt?: string } {
|
|
1566
|
+
return {
|
|
1567
|
+
...envelope.operationRequest,
|
|
1568
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function parseStatefulForwardingEnvelope(rawBody: unknown): ProviderServerStatefulForwardEnvelope {
|
|
1573
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1574
|
+
if (parsed.success) return parsed.data;
|
|
1575
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1576
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1577
|
+
details: zodDetails(parsed.error),
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1360
1581
|
export function createServerApp(
|
|
1361
1582
|
provider: ProviderDefinition,
|
|
1362
1583
|
options: ProviderServerOptions = {},
|
|
1363
1584
|
): Hono {
|
|
1585
|
+
validateStatefulServerConfig(options);
|
|
1364
1586
|
const app = new Hono();
|
|
1365
1587
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1588
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(
|
|
1589
|
+
options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1590
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES,
|
|
1591
|
+
);
|
|
1366
1592
|
const state =
|
|
1367
1593
|
options.state ??
|
|
1368
1594
|
createProviderRuntimeStateFromEnv({
|
|
@@ -1370,6 +1596,24 @@ export function createServerApp(
|
|
|
1370
1596
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
1371
1597
|
});
|
|
1372
1598
|
|
|
1599
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1600
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1601
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1602
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1603
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1604
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(
|
|
1605
|
+
provider,
|
|
1606
|
+
createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
1607
|
+
);
|
|
1608
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1609
|
+
logger({
|
|
1610
|
+
level: "warn",
|
|
1611
|
+
event: "provider_secrets_missing",
|
|
1612
|
+
providerId: provider.id,
|
|
1613
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1373
1617
|
app.notFound((c) =>
|
|
1374
1618
|
c.json(
|
|
1375
1619
|
{
|
|
@@ -1390,6 +1634,162 @@ export function createServerApp(
|
|
|
1390
1634
|
}),
|
|
1391
1635
|
);
|
|
1392
1636
|
|
|
1637
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1638
|
+
let rawBodyText = "";
|
|
1639
|
+
let rawBody: unknown;
|
|
1640
|
+
const operation = "stateful-internal";
|
|
1641
|
+
const requestCost = startRequestCost();
|
|
1642
|
+
try {
|
|
1643
|
+
if (!options.internalOperationExecutor) {
|
|
1644
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1645
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1649
|
+
verifyStatefulForwardingRequest({
|
|
1650
|
+
options,
|
|
1651
|
+
rawBody: rawBodyText,
|
|
1652
|
+
headers: c.req.raw.headers,
|
|
1653
|
+
method: c.req.raw.method,
|
|
1654
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1655
|
+
replayCache: statefulForwardingReplayCache,
|
|
1656
|
+
});
|
|
1657
|
+
try {
|
|
1658
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1659
|
+
} catch {
|
|
1660
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1661
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1665
|
+
if (envelope.providerId !== provider.id) {
|
|
1666
|
+
throw new ProviderError(
|
|
1667
|
+
"Stateful forwarding envelope providerId does not match the served provider.",
|
|
1668
|
+
{ code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" },
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1672
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1673
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
if (
|
|
1677
|
+
envelope.sourcePodId !==
|
|
1678
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")
|
|
1679
|
+
) {
|
|
1680
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1681
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
if (
|
|
1685
|
+
envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")
|
|
1686
|
+
) {
|
|
1687
|
+
throw new ProviderError(
|
|
1688
|
+
"Stateful forwarding forwardedAt does not match its signature timestamp.",
|
|
1689
|
+
{ code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" },
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1693
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1694
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1695
|
+
}
|
|
1696
|
+
const remainingDeadlineMs =
|
|
1697
|
+
deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1698
|
+
const deadlineSignal =
|
|
1699
|
+
remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1700
|
+
const signal = deadlineSignal
|
|
1701
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1702
|
+
: c.req.raw.signal;
|
|
1703
|
+
const ownerFenceValidation = Promise.resolve(
|
|
1704
|
+
options.statefulForwarding?.validateOwnerFence(
|
|
1705
|
+
{
|
|
1706
|
+
providerId: envelope.providerId,
|
|
1707
|
+
sessionKey: envelope.sessionKey,
|
|
1708
|
+
ownerPodId: envelope.ownerPodId,
|
|
1709
|
+
generation: envelope.generation,
|
|
1710
|
+
sourcePodId: envelope.sourcePodId,
|
|
1711
|
+
forwardedAt: envelope.forwardedAt,
|
|
1712
|
+
requestId: envelope.requestId,
|
|
1713
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1714
|
+
},
|
|
1715
|
+
signal,
|
|
1716
|
+
),
|
|
1717
|
+
);
|
|
1718
|
+
let ownerFenceValid: boolean | undefined;
|
|
1719
|
+
try {
|
|
1720
|
+
ownerFenceValid = deadlineSignal
|
|
1721
|
+
? await Promise.race([
|
|
1722
|
+
ownerFenceValidation,
|
|
1723
|
+
new Promise<never>((_resolve, reject) => {
|
|
1724
|
+
deadlineSignal.addEventListener(
|
|
1725
|
+
"abort",
|
|
1726
|
+
() =>
|
|
1727
|
+
reject(
|
|
1728
|
+
new StatefulRoutingDeadlineError(
|
|
1729
|
+
envelope.requestId,
|
|
1730
|
+
envelope.deadlineAt as string,
|
|
1731
|
+
),
|
|
1732
|
+
),
|
|
1733
|
+
{ once: true },
|
|
1734
|
+
);
|
|
1735
|
+
}),
|
|
1736
|
+
])
|
|
1737
|
+
: await ownerFenceValidation;
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
if (deadlineSignal?.aborted) {
|
|
1740
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1741
|
+
}
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
if (ownerFenceValid !== true) {
|
|
1745
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1746
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1750
|
+
const operationId = envelope.operationId;
|
|
1751
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1752
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1753
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt as string);
|
|
1754
|
+
}
|
|
1755
|
+
const output = await options.internalOperationExecutor({
|
|
1756
|
+
provider,
|
|
1757
|
+
operationId,
|
|
1758
|
+
ctx,
|
|
1759
|
+
request,
|
|
1760
|
+
internalStatefulForward: envelope,
|
|
1761
|
+
signal,
|
|
1762
|
+
});
|
|
1763
|
+
logProviderSuccess(
|
|
1764
|
+
logger,
|
|
1765
|
+
provider,
|
|
1766
|
+
"operation",
|
|
1767
|
+
operationId || operation,
|
|
1768
|
+
request.requestId,
|
|
1769
|
+
200,
|
|
1770
|
+
finishRequestCost(requestCost),
|
|
1771
|
+
);
|
|
1772
|
+
return c.json({ data: output });
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
const status = toStatusCode(error);
|
|
1775
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1776
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1777
|
+
}
|
|
1778
|
+
const requestId = extractRequestId(rawBody);
|
|
1779
|
+
logProviderError(
|
|
1780
|
+
logger,
|
|
1781
|
+
provider,
|
|
1782
|
+
"operation",
|
|
1783
|
+
operation,
|
|
1784
|
+
requestId,
|
|
1785
|
+
error,
|
|
1786
|
+
status,
|
|
1787
|
+
finishRequestCost(requestCost),
|
|
1788
|
+
);
|
|
1789
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1393
1793
|
app.post("/v1/:operation", async (c) => {
|
|
1394
1794
|
let rawBody: unknown;
|
|
1395
1795
|
const operation = c.req.param("operation");
|
|
@@ -1462,17 +1862,8 @@ export function createServerApp(
|
|
|
1462
1862
|
.clone()
|
|
1463
1863
|
.json()
|
|
1464
1864
|
.catch(() => undefined);
|
|
1465
|
-
const body = withAuthRequestHeaders(
|
|
1466
|
-
|
|
1467
|
-
c.req.raw.headers,
|
|
1468
|
-
);
|
|
1469
|
-
const response = await handleAuthFlow(
|
|
1470
|
-
provider,
|
|
1471
|
-
body,
|
|
1472
|
-
"start",
|
|
1473
|
-
options,
|
|
1474
|
-
c.req.raw.signal,
|
|
1475
|
-
);
|
|
1865
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1866
|
+
const response = await handleAuthFlow(provider, body, "start", options, c.req.raw.signal);
|
|
1476
1867
|
logProviderSuccess(
|
|
1477
1868
|
logger,
|
|
1478
1869
|
provider,
|
|
@@ -1508,17 +1899,8 @@ export function createServerApp(
|
|
|
1508
1899
|
.clone()
|
|
1509
1900
|
.json()
|
|
1510
1901
|
.catch(() => undefined);
|
|
1511
|
-
const body = withAuthRequestHeaders(
|
|
1512
|
-
|
|
1513
|
-
c.req.raw.headers,
|
|
1514
|
-
);
|
|
1515
|
-
const response = await handleAuthFlow(
|
|
1516
|
-
provider,
|
|
1517
|
-
body,
|
|
1518
|
-
"continue",
|
|
1519
|
-
options,
|
|
1520
|
-
c.req.raw.signal,
|
|
1521
|
-
);
|
|
1902
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1903
|
+
const response = await handleAuthFlow(provider, body, "continue", options, c.req.raw.signal);
|
|
1522
1904
|
logProviderSuccess(
|
|
1523
1905
|
logger,
|
|
1524
1906
|
provider,
|
|
@@ -1554,17 +1936,8 @@ export function createServerApp(
|
|
|
1554
1936
|
.clone()
|
|
1555
1937
|
.json()
|
|
1556
1938
|
.catch(() => undefined);
|
|
1557
|
-
const body = withAuthRequestHeaders(
|
|
1558
|
-
|
|
1559
|
-
c.req.raw.headers,
|
|
1560
|
-
);
|
|
1561
|
-
const response = await handleAuthFlow(
|
|
1562
|
-
provider,
|
|
1563
|
-
body,
|
|
1564
|
-
"poll",
|
|
1565
|
-
options,
|
|
1566
|
-
c.req.raw.signal,
|
|
1567
|
-
);
|
|
1939
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1940
|
+
const response = await handleAuthFlow(provider, body, "poll", options, c.req.raw.signal);
|
|
1568
1941
|
logProviderSuccess(
|
|
1569
1942
|
logger,
|
|
1570
1943
|
provider,
|
|
@@ -1600,17 +1973,8 @@ export function createServerApp(
|
|
|
1600
1973
|
.clone()
|
|
1601
1974
|
.json()
|
|
1602
1975
|
.catch(() => undefined);
|
|
1603
|
-
const body = withAuthRequestHeaders(
|
|
1604
|
-
|
|
1605
|
-
c.req.raw.headers,
|
|
1606
|
-
);
|
|
1607
|
-
const response = await handleAuthFlow(
|
|
1608
|
-
provider,
|
|
1609
|
-
body,
|
|
1610
|
-
"refresh",
|
|
1611
|
-
options,
|
|
1612
|
-
c.req.raw.signal,
|
|
1613
|
-
);
|
|
1976
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
1977
|
+
const response = await handleAuthFlow(provider, body, "refresh", options, c.req.raw.signal);
|
|
1614
1978
|
logProviderSuccess(
|
|
1615
1979
|
logger,
|
|
1616
1980
|
provider,
|
|
@@ -1646,17 +2010,8 @@ export function createServerApp(
|
|
|
1646
2010
|
.clone()
|
|
1647
2011
|
.json()
|
|
1648
2012
|
.catch(() => undefined);
|
|
1649
|
-
const body = withAuthRequestHeaders(
|
|
1650
|
-
|
|
1651
|
-
c.req.raw.headers,
|
|
1652
|
-
);
|
|
1653
|
-
const response = await handleAuthFlow(
|
|
1654
|
-
provider,
|
|
1655
|
-
body,
|
|
1656
|
-
"abort",
|
|
1657
|
-
options,
|
|
1658
|
-
c.req.raw.signal,
|
|
1659
|
-
);
|
|
2013
|
+
const body = withAuthRequestHeaders(AuthFlowRequestSchema.parse(rawBody), c.req.raw.headers);
|
|
2014
|
+
const response = await handleAuthFlow(provider, body, "abort", options, c.req.raw.signal);
|
|
1660
2015
|
logProviderSuccess(
|
|
1661
2016
|
logger,
|
|
1662
2017
|
provider,
|
|
@@ -1687,12 +2042,54 @@ export function createServerApp(
|
|
|
1687
2042
|
return app;
|
|
1688
2043
|
}
|
|
1689
2044
|
|
|
2045
|
+
function validateStatefulServerConfig(options: ProviderServerOptions): void {
|
|
2046
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
2047
|
+
throw new Error(
|
|
2048
|
+
"Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.",
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
"Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.",
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
if (
|
|
2057
|
+
options.statefulForwarding &&
|
|
2058
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function"
|
|
2059
|
+
) {
|
|
2060
|
+
throw new Error(
|
|
2061
|
+
"Invalid provider server configuration: statefulForwarding requires validateOwnerFence.",
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
if (
|
|
2065
|
+
options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
2066
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
2067
|
+
options.statefulForwarding.maxSkewMs <= 0)
|
|
2068
|
+
) {
|
|
2069
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
2070
|
+
}
|
|
2071
|
+
if (
|
|
2072
|
+
options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
2073
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
2074
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)
|
|
2075
|
+
) {
|
|
2076
|
+
throw new Error(
|
|
2077
|
+
"Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.",
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1690
2082
|
type BunServeRuntime = {
|
|
1691
2083
|
serve: (options: {
|
|
1692
2084
|
port: number;
|
|
1693
2085
|
hostname: string;
|
|
1694
2086
|
fetch: (request: Request) => Response | Promise<Response>;
|
|
1695
|
-
}) =>
|
|
2087
|
+
}) => BunServerHandle;
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
type BunServerHandle = {
|
|
2091
|
+
readonly port: number;
|
|
2092
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
1696
2093
|
};
|
|
1697
2094
|
|
|
1698
2095
|
function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
@@ -1708,11 +2105,20 @@ function getBunServeRuntime(): BunServeRuntime | undefined {
|
|
|
1708
2105
|
|
|
1709
2106
|
return {
|
|
1710
2107
|
serve(options) {
|
|
1711
|
-
return serve(options);
|
|
2108
|
+
return serve(options) as BunServerHandle;
|
|
1712
2109
|
},
|
|
1713
2110
|
};
|
|
1714
2111
|
}
|
|
1715
2112
|
|
|
2113
|
+
export type ProviderServerCloseOptions = {
|
|
2114
|
+
readonly timeoutMs?: number;
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
export type ProviderServerHandle = {
|
|
2118
|
+
readonly port: number;
|
|
2119
|
+
close(options?: ProviderServerCloseOptions): Promise<void>;
|
|
2120
|
+
};
|
|
2121
|
+
|
|
1716
2122
|
export interface ServeOptions extends ProviderServerOptions {
|
|
1717
2123
|
host?: string;
|
|
1718
2124
|
port?: number;
|
|
@@ -1724,46 +2130,233 @@ export interface ServeOptions extends ProviderServerOptions {
|
|
|
1724
2130
|
selfTestPort?: number;
|
|
1725
2131
|
}
|
|
1726
2132
|
|
|
2133
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
2134
|
+
const DEFAULT_SHUTDOWN_SIGNALS: NodeJS.Signals[] = ["SIGTERM", "SIGINT"];
|
|
2135
|
+
|
|
2136
|
+
type SignalServerRegistration = {
|
|
2137
|
+
readonly signals: ReadonlySet<NodeJS.Signals>;
|
|
2138
|
+
readonly close: () => Promise<void>;
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
type ProcessSignalCoordinator = {
|
|
2142
|
+
readonly registrations: Set<SignalServerRegistration>;
|
|
2143
|
+
readonly listener: () => void;
|
|
2144
|
+
handling: boolean;
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
const processSignalCoordinators = new Map<NodeJS.Signals, ProcessSignalCoordinator>();
|
|
2148
|
+
|
|
1727
2149
|
export async function serve(
|
|
1728
2150
|
provider: ProviderDefinition,
|
|
1729
2151
|
options: ServeOptions = {},
|
|
1730
|
-
): Promise<
|
|
2152
|
+
): Promise<ProviderServerHandle> {
|
|
1731
2153
|
const bunRuntime = getBunServeRuntime();
|
|
1732
2154
|
|
|
1733
2155
|
if (bunRuntime === undefined) {
|
|
1734
|
-
throw new ProviderError(
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
code: "RUNTIME_UNSUPPORTED",
|
|
1738
|
-
},
|
|
1739
|
-
);
|
|
2156
|
+
throw new ProviderError("Bun runtime is required to start the provider server", {
|
|
2157
|
+
code: "RUNTIME_UNSUPPORTED",
|
|
2158
|
+
});
|
|
1740
2159
|
}
|
|
2160
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
2161
|
+
const configuredTimeoutMs = shutdownTimeout(
|
|
2162
|
+
options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
2163
|
+
);
|
|
2164
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1741
2165
|
|
|
1742
2166
|
const app = createServerApp(provider, {
|
|
1743
2167
|
logger: options.logger,
|
|
1744
2168
|
stt: options.stt,
|
|
2169
|
+
state: options.state,
|
|
2170
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
2171
|
+
operationExecutor: options.operationExecutor,
|
|
2172
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
2173
|
+
statefulForwarding: options.statefulForwarding,
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
const servers: BunServerHandle[] = [];
|
|
2177
|
+
try {
|
|
2178
|
+
servers.push(
|
|
2179
|
+
bunRuntime.serve({
|
|
2180
|
+
port: options.port ?? DEFAULT_PORT,
|
|
2181
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2182
|
+
fetch: app.fetch,
|
|
2183
|
+
}),
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
2187
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
2188
|
+
// starts when the shared self-test master secret env is present.
|
|
2189
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
2190
|
+
if (selfTestSecrets) {
|
|
2191
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
2192
|
+
secrets: selfTestSecrets,
|
|
2193
|
+
invoke: createSelfTestInvoke(app),
|
|
2194
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
2195
|
+
});
|
|
2196
|
+
servers.push(
|
|
2197
|
+
bunRuntime.serve({
|
|
2198
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
2199
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
2200
|
+
fetch: selfTestApp.fetch,
|
|
2201
|
+
}),
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2206
|
+
throw error;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
const server = servers[0];
|
|
2210
|
+
if (!server) throw new Error("Provider server failed to create its primary listener.");
|
|
2211
|
+
let closePromise: Promise<void> | undefined;
|
|
2212
|
+
let unregisterSignals = () => {};
|
|
2213
|
+
|
|
2214
|
+
const close = (closeOptions: ProviderServerCloseOptions = {}): Promise<void> => {
|
|
2215
|
+
if (closePromise) return closePromise;
|
|
2216
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
2217
|
+
closePromise = closeProviderServers({
|
|
2218
|
+
servers,
|
|
2219
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
2220
|
+
timeoutMs,
|
|
2221
|
+
logger,
|
|
2222
|
+
providerId: provider.id,
|
|
2223
|
+
}).finally(() => unregisterSignals());
|
|
2224
|
+
return closePromise;
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
try {
|
|
2228
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () =>
|
|
2229
|
+
close({ timeoutMs: configuredTimeoutMs }),
|
|
2230
|
+
);
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
unregisterSignals();
|
|
2233
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
2234
|
+
throw error;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
return { port: server.port, close };
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function registerForProcessSignals(
|
|
2241
|
+
signals: NodeJS.Signals[],
|
|
2242
|
+
close: () => Promise<void>,
|
|
2243
|
+
): () => void {
|
|
2244
|
+
if (signals.length === 0) return () => {};
|
|
2245
|
+
const registration: SignalServerRegistration = {
|
|
2246
|
+
signals: new Set(signals),
|
|
2247
|
+
close,
|
|
2248
|
+
};
|
|
2249
|
+
let registered = true;
|
|
2250
|
+
const unregister = () => {
|
|
2251
|
+
if (!registered) return;
|
|
2252
|
+
registered = false;
|
|
2253
|
+
for (const signal of registration.signals) {
|
|
2254
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
2255
|
+
if (!coordinator) continue;
|
|
2256
|
+
coordinator.registrations.delete(registration);
|
|
2257
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
2258
|
+
process.removeListener(signal, coordinator.listener);
|
|
2259
|
+
processSignalCoordinators.delete(signal);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
try {
|
|
2264
|
+
for (const signal of signals) {
|
|
2265
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
2266
|
+
if (!coordinator) {
|
|
2267
|
+
const created: ProcessSignalCoordinator = {
|
|
2268
|
+
registrations: new Set(),
|
|
2269
|
+
handling: false,
|
|
2270
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
2271
|
+
};
|
|
2272
|
+
coordinator = created;
|
|
2273
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
2274
|
+
process.on(signal, coordinator.listener);
|
|
2275
|
+
}
|
|
2276
|
+
coordinator.registrations.add(registration);
|
|
2277
|
+
}
|
|
2278
|
+
} catch (error) {
|
|
2279
|
+
unregister();
|
|
2280
|
+
throw error;
|
|
2281
|
+
}
|
|
2282
|
+
return unregister;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function handleCoordinatedSignal(
|
|
2286
|
+
signal: NodeJS.Signals,
|
|
2287
|
+
coordinator: ProcessSignalCoordinator,
|
|
2288
|
+
): void {
|
|
2289
|
+
if (coordinator.handling) return;
|
|
2290
|
+
coordinator.handling = true;
|
|
2291
|
+
const registrations = [...coordinator.registrations];
|
|
2292
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
2293
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
2294
|
+
process.removeListener(signal, coordinator.listener);
|
|
2295
|
+
processSignalCoordinators.delete(signal);
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
process.kill(process.pid, signal);
|
|
2299
|
+
} catch {
|
|
2300
|
+
process.exitCode = 1;
|
|
2301
|
+
}
|
|
1745
2302
|
});
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
async function closeProviderServers(input: {
|
|
2306
|
+
readonly servers: BunServerHandle[];
|
|
2307
|
+
readonly hooks: Array<() => Promise<void>>;
|
|
2308
|
+
readonly timeoutMs: number;
|
|
2309
|
+
readonly logger: ProviderServerLogger;
|
|
2310
|
+
readonly providerId: string;
|
|
2311
|
+
}): Promise<void> {
|
|
2312
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
2313
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
2314
|
+
for (const gracefulStop of gracefulStops) gracefulStop.catch(() => undefined);
|
|
2315
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
2316
|
+
try {
|
|
2317
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
try {
|
|
2320
|
+
input.logger({
|
|
2321
|
+
level: "error",
|
|
2322
|
+
event: "provider_shutdown_hook_failed",
|
|
2323
|
+
providerId: input.providerId,
|
|
2324
|
+
hookIndex,
|
|
2325
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
2326
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
2327
|
+
});
|
|
2328
|
+
} catch {}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
2332
|
+
await withinShutdownBudget(
|
|
2333
|
+
Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined),
|
|
2334
|
+
deadline,
|
|
2335
|
+
).catch(() => undefined);
|
|
2336
|
+
}
|
|
1746
2337
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2338
|
+
async function withinShutdownBudget<T>(promise: Promise<T>, deadline: number): Promise<T> {
|
|
2339
|
+
promise.catch(() => undefined);
|
|
2340
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
2341
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2342
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
2343
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1751
2344
|
});
|
|
2345
|
+
try {
|
|
2346
|
+
return await Promise.race([promise, timeout]);
|
|
2347
|
+
} finally {
|
|
2348
|
+
if (timer) clearTimeout(timer);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
1752
2351
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
});
|
|
1762
|
-
bunRuntime.serve({
|
|
1763
|
-
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1764
|
-
hostname: options.host ?? DEFAULT_HOST,
|
|
1765
|
-
fetch: selfTestApp.fetch,
|
|
1766
|
-
});
|
|
2352
|
+
function resolveShutdownSignals(signals: boolean | NodeJS.Signals[]): NodeJS.Signals[] {
|
|
2353
|
+
if (signals === false) return [];
|
|
2354
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function shutdownTimeout(value: number): number {
|
|
2358
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2359
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1767
2360
|
}
|
|
1768
|
-
|
|
2361
|
+
return value;
|
|
1769
2362
|
}
|