@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/dist/server/serve.js
CHANGED
|
@@ -2,36 +2,67 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { AuthAbortError, createAuthFlowHelpers } from "../auth";
|
|
6
|
-
import { AuthError,
|
|
7
|
-
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog";
|
|
8
|
-
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability";
|
|
9
|
-
import { createScratchpad } from "../runtime/auth-flow";
|
|
10
|
-
import { createBrowserClient } from "../runtime/browser";
|
|
11
|
-
import { createProviderCache } from "../runtime/cache";
|
|
12
|
-
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice";
|
|
13
|
-
import { createCredentialContext } from "../runtime/credential";
|
|
14
|
-
import { createEnvContext } from "../runtime/env";
|
|
15
|
-
import { executeOperation } from "../runtime/executor";
|
|
16
|
-
import { createHttpClient } from "../runtime/http";
|
|
17
|
-
import { wrapWithInstrumentation } from "../runtime/instrumentation";
|
|
18
|
-
import { getProviderBaseUrl } from "../runtime/provider";
|
|
19
|
-
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors";
|
|
20
|
-
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
5
|
+
import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
6
|
+
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, } from "../errors.js";
|
|
7
|
+
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
8
|
+
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
9
|
+
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
10
|
+
import { createBrowserClient } from "../runtime/browser.js";
|
|
11
|
+
import { createProviderCache } from "../runtime/cache.js";
|
|
12
|
+
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
|
|
13
|
+
import { createCredentialContext } from "../runtime/credential.js";
|
|
14
|
+
import { createEnvContext } from "../runtime/env.js";
|
|
15
|
+
import { executeOperation } from "../runtime/executor.js";
|
|
16
|
+
import { createHttpClient } from "../runtime/http.js";
|
|
17
|
+
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
18
|
+
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
19
|
+
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
20
|
+
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
21
|
+
import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
|
|
22
|
+
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
|
|
23
|
+
import { createStealthClient } from "../runtime/stealth.js";
|
|
24
|
+
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
25
|
+
import { createTraceContext } from "../runtime/trace.js";
|
|
26
|
+
import { parseSchema } from "../schema.js";
|
|
27
|
+
import { STATEFUL_NONCE_HEADER as STATEFUL_FORWARDING_NONCE_HEADER, STATEFUL_SIGNATURE_HEADER as STATEFUL_FORWARDING_SIGNATURE_HEADER, STATEFUL_TIMESTAMP_HEADER as STATEFUL_FORWARDING_TIMESTAMP_HEADER, verifyStatefulRequestSignature, } from "../stateful-signing.js";
|
|
28
|
+
import { StatefulRoutingDeadlineError } from "../stateful/stateful-provider-session-routing.js";
|
|
29
|
+
import { getStealthProfile } from "../stealth/profiles.js";
|
|
30
|
+
import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
|
|
31
|
+
import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
|
|
32
|
+
import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
|
|
33
|
+
import { AuthFlowRequestSchema, OperationConnectionSchema, OperationRequestSchema, } from "./types.js";
|
|
31
34
|
const DEFAULT_HOST = "0.0.0.0";
|
|
32
35
|
const DEFAULT_PORT = 3000;
|
|
33
36
|
const AUTH_FLOW_LOCALES = ["en", "ko", "ja"];
|
|
34
37
|
const retryResponseMeta = new WeakMap();
|
|
38
|
+
const STATEFUL_INTERNAL_OPERATIONS_ROUTE = "/__apifuse/stateful/operations";
|
|
39
|
+
const STATEFUL_FORWARDING_SOURCE_POD_HEADER = "x-apifuse-stateful-source-pod";
|
|
40
|
+
const DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS = 5 * 60_000;
|
|
41
|
+
const DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES = 10_000;
|
|
42
|
+
const STATEFUL_FORWARDING_REPLAY_BUCKET_MS = 10_000;
|
|
43
|
+
const STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS = Math.ceil(STATEFUL_FORWARDING_REPLAY_BUCKET_MS / 1_000);
|
|
44
|
+
export const ProviderServerStatefulForwardEnvelopeSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
requestId: z.string().min(1),
|
|
47
|
+
providerId: z.string().min(1),
|
|
48
|
+
operationId: z.string().min(1),
|
|
49
|
+
sessionKey: z.string().min(1),
|
|
50
|
+
connectionId: z.string().min(1),
|
|
51
|
+
serviceAccountId: z.string().min(1),
|
|
52
|
+
ownerPodId: z.string().min(1),
|
|
53
|
+
generation: z.number().int().positive(),
|
|
54
|
+
sourcePodId: z.string().min(1),
|
|
55
|
+
forwardedAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
56
|
+
deadlineAt: z
|
|
57
|
+
.string()
|
|
58
|
+
.refine((value) => Number.isFinite(Date.parse(value)))
|
|
59
|
+
.optional(),
|
|
60
|
+
idempotencyKey: z.string().min(1).optional(),
|
|
61
|
+
operationRequest: OperationRequestSchema.extend({
|
|
62
|
+
connection: OperationConnectionSchema.strict().optional(),
|
|
63
|
+
}).strict(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
35
66
|
function createAuthStub() {
|
|
36
67
|
return {
|
|
37
68
|
async requestField(name) {
|
|
@@ -93,9 +124,7 @@ function getProviderStealthBaseUrl(provider) {
|
|
|
93
124
|
return firstHost ? `https://${firstHost}` : undefined;
|
|
94
125
|
}
|
|
95
126
|
function getProviderStealthProfile(provider) {
|
|
96
|
-
return provider.stealth?.profile
|
|
97
|
-
? getStealthProfile(provider.stealth.profile)
|
|
98
|
-
: undefined;
|
|
127
|
+
return provider.stealth?.profile ? getStealthProfile(provider.stealth.profile) : undefined;
|
|
99
128
|
}
|
|
100
129
|
function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
101
130
|
if (provider.runtime !== "browser") {
|
|
@@ -104,18 +133,19 @@ function isProductionProviderBrowserMode(provider, env = process.env) {
|
|
|
104
133
|
if (env.APIFUSE__PROVIDER__RUNTIME === "browser") {
|
|
105
134
|
return true;
|
|
106
135
|
}
|
|
107
|
-
return
|
|
136
|
+
return env.NODE_ENV === "production" && env.APIFUSE__PROVIDER__ID === provider.id;
|
|
108
137
|
}
|
|
109
138
|
export function resolveProviderProxyAffinityKey(provider, request, operationId) {
|
|
110
|
-
const connectionKey = request
|
|
111
|
-
const affinity = typeof provider.proxy === "object"
|
|
112
|
-
? provider.proxy.session?.affinity
|
|
113
|
-
: undefined;
|
|
139
|
+
const connectionKey = resolveOperationConnectionId(request) ?? request.connection?.externalRef;
|
|
140
|
+
const affinity = typeof provider.proxy === "object" ? provider.proxy.session?.affinity : undefined;
|
|
114
141
|
if (affinity === "operation") {
|
|
115
142
|
return `${provider.id}/${operationId}`;
|
|
116
143
|
}
|
|
117
144
|
return connectionKey ?? provider.id;
|
|
118
145
|
}
|
|
146
|
+
function resolveOperationConnectionId(request) {
|
|
147
|
+
return request.connection?.id ?? request.connectionId;
|
|
148
|
+
}
|
|
119
149
|
function createProviderContext(provider, request, operationId, options = {}, state = createUnsupportedProviderRuntimeState(), proxyTelemetry) {
|
|
120
150
|
const baseUrl = getProviderBaseUrl(provider);
|
|
121
151
|
const stealthBaseUrl = getProviderStealthBaseUrl(provider);
|
|
@@ -142,7 +172,7 @@ function createProviderContext(provider, request, operationId, options = {}, sta
|
|
|
142
172
|
values: request.connection?.secrets,
|
|
143
173
|
});
|
|
144
174
|
const requestContext = {
|
|
145
|
-
connectionId: request
|
|
175
|
+
connectionId: resolveOperationConnectionId(request),
|
|
146
176
|
headers: request.headers ?? {},
|
|
147
177
|
};
|
|
148
178
|
const context = wrapWithInstrumentation({
|
|
@@ -290,7 +320,17 @@ function zodDetails(error) {
|
|
|
290
320
|
}));
|
|
291
321
|
}
|
|
292
322
|
function toErrorResponse(error, requestId) {
|
|
293
|
-
if (error instanceof
|
|
323
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
324
|
+
return {
|
|
325
|
+
error: {
|
|
326
|
+
code: "STATEFUL_FORWARDING_DEADLINE_EXPIRED",
|
|
327
|
+
message: "Stateful forwarding deadline expired.",
|
|
328
|
+
...(requestId ? { requestId } : {}),
|
|
329
|
+
details: { retryable: false },
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (isProviderError(error)) {
|
|
294
334
|
const details = publicProviderErrorDetails(error);
|
|
295
335
|
return {
|
|
296
336
|
error: {
|
|
@@ -312,11 +352,22 @@ function toErrorResponse(error, requestId) {
|
|
|
312
352
|
},
|
|
313
353
|
};
|
|
314
354
|
}
|
|
355
|
+
// A masked internal error MUST NOT be advertised as retryable: without an
|
|
356
|
+
// explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
|
|
357
|
+
// to retryable:true, which turns a deterministic pre-upstream crash into an
|
|
358
|
+
// infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
|
|
359
|
+
// We still refuse to leak message/stack — only the error class name (or the
|
|
360
|
+
// primitive type for non-Error throwables) is surfaced for ops triage.
|
|
315
361
|
return {
|
|
316
362
|
error: {
|
|
317
363
|
code: "internal_error",
|
|
318
364
|
message: "Internal error",
|
|
319
365
|
...(requestId ? { requestId } : {}),
|
|
366
|
+
details: {
|
|
367
|
+
retryable: false,
|
|
368
|
+
category: "internal_error",
|
|
369
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
370
|
+
},
|
|
320
371
|
},
|
|
321
372
|
};
|
|
322
373
|
}
|
|
@@ -340,20 +391,37 @@ function publicProviderErrorDetails(error) {
|
|
|
340
391
|
function isPlainRecord(value) {
|
|
341
392
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
342
393
|
}
|
|
394
|
+
// Accepts `unknown` so the branded guards narrow cleanly from the top: the
|
|
395
|
+
// subtype error classes are structurally compatible with ProviderError, so
|
|
396
|
+
// narrowing from a ProviderError-typed value would collapse the negative branch
|
|
397
|
+
// to `never`. Narrowing from unknown avoids that while still recognizing errors
|
|
398
|
+
// from a duplicate SDK module instance.
|
|
343
399
|
function providerObservabilityDetails(error) {
|
|
344
400
|
// Session-expiry surfaces the credential_expired category + the opt-in
|
|
345
401
|
// retryable signal so Gateway/Credential Service can refresh and re-drive the
|
|
346
402
|
// operation (see design.md §4.3 D3). Without this branch the auth error would
|
|
347
403
|
// serialize as a bare 401 with no retryable/category, losing the refresh
|
|
348
404
|
// signal for exactly the retryOnAuthRefresh operations it is meant to enable.
|
|
349
|
-
if (error
|
|
405
|
+
if (isSessionExpiredError(error)) {
|
|
350
406
|
return {
|
|
351
407
|
category: error.options?.category ?? "credential_expired",
|
|
352
408
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
353
409
|
retryable: error.options?.retryable ?? false,
|
|
354
410
|
};
|
|
355
411
|
}
|
|
356
|
-
|
|
412
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
413
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
414
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
415
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
416
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
417
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
418
|
+
return {
|
|
419
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
420
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
421
|
+
retryable: error.options?.retryable ?? false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (!isTransportError(error)) {
|
|
357
425
|
return undefined;
|
|
358
426
|
}
|
|
359
427
|
const isProxyPoolCode = error.code === PROXY_POOL_EXHAUSTED_CODE ||
|
|
@@ -382,7 +450,7 @@ function providerObservabilityDetails(error) {
|
|
|
382
450
|
};
|
|
383
451
|
}
|
|
384
452
|
function publicProviderErrorMessage(error) {
|
|
385
|
-
if (error
|
|
453
|
+
if (isTransportError(error)) {
|
|
386
454
|
if (error.code === PROXY_AUTH_IP_DENIED_CODE) {
|
|
387
455
|
return error.message;
|
|
388
456
|
}
|
|
@@ -410,14 +478,21 @@ function toStatusCode(error) {
|
|
|
410
478
|
if (error instanceof z.ZodError) {
|
|
411
479
|
return 400;
|
|
412
480
|
}
|
|
413
|
-
if (error instanceof
|
|
481
|
+
if (error instanceof StatefulRoutingDeadlineError) {
|
|
482
|
+
return 504;
|
|
483
|
+
}
|
|
484
|
+
if (isTransportError(error)) {
|
|
414
485
|
return error.code === "transport_timeout" ? 504 : 502;
|
|
415
486
|
}
|
|
416
|
-
if (error
|
|
487
|
+
if (isProviderError(error)) {
|
|
417
488
|
switch (error.code) {
|
|
418
489
|
case "AUTH_REQUIRED":
|
|
419
490
|
case "reauth_required":
|
|
420
491
|
return 401;
|
|
492
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
493
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
494
|
+
case MISSING_SECRET_CODE:
|
|
495
|
+
return 400;
|
|
421
496
|
case "NOT_FOUND":
|
|
422
497
|
case "not_found":
|
|
423
498
|
case "NO_DATA":
|
|
@@ -431,6 +506,7 @@ function toStatusCode(error) {
|
|
|
431
506
|
return 502;
|
|
432
507
|
case "STT_UNAVAILABLE":
|
|
433
508
|
case "UNSUPPORTED_STT_BACKEND":
|
|
509
|
+
case "STATEFUL_FORWARDING_REPLAY_CACHE_FULL":
|
|
434
510
|
return 503;
|
|
435
511
|
}
|
|
436
512
|
return 400;
|
|
@@ -445,16 +521,16 @@ function extractRequestId(raw) {
|
|
|
445
521
|
return typeof value === "string" ? value : undefined;
|
|
446
522
|
}
|
|
447
523
|
function logProviderError(logger, provider, kind, route, requestId, error, status, cost) {
|
|
448
|
-
const code = error
|
|
524
|
+
const code = isProviderError(error)
|
|
449
525
|
? (error.code ?? "provider_error")
|
|
450
526
|
: error instanceof z.ZodError
|
|
451
527
|
? "invalid_request"
|
|
452
|
-
:
|
|
528
|
+
: error instanceof StatefulRoutingDeadlineError
|
|
529
|
+
? "STATEFUL_FORWARDING_DEADLINE_EXPIRED"
|
|
530
|
+
: "internal_error";
|
|
453
531
|
const errorClass = error instanceof Error ? error.name : typeof error;
|
|
454
532
|
const message = error instanceof Error ? error.message : String(error);
|
|
455
|
-
const details = error
|
|
456
|
-
? providerObservabilityDetails(error)
|
|
457
|
-
: undefined;
|
|
533
|
+
const details = isProviderError(error) ? providerObservabilityDetails(error) : undefined;
|
|
458
534
|
const emit = typeof logger === "function" ? logger : defaultProviderServerLogger;
|
|
459
535
|
emit({
|
|
460
536
|
level: status >= 500 ? "error" : "warn",
|
|
@@ -468,7 +544,7 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
468
544
|
code,
|
|
469
545
|
errorClass,
|
|
470
546
|
message,
|
|
471
|
-
...(error
|
|
547
|
+
...(isTransportError(error) && error.upstreamStatus
|
|
472
548
|
? { upstreamStatus: error.upstreamStatus }
|
|
473
549
|
: {}),
|
|
474
550
|
...(details
|
|
@@ -591,8 +667,7 @@ async function validateSseEvent(operation, event) {
|
|
|
591
667
|
const transport = getSseTransport(operation);
|
|
592
668
|
const schema = transport?.events?.[event.event];
|
|
593
669
|
if (!schema) {
|
|
594
|
-
if (event.event === APIFUSE_STREAM_ERROR_EVENT ||
|
|
595
|
-
event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
670
|
+
if (event.event === APIFUSE_STREAM_ERROR_EVENT || event.event === APIFUSE_STREAM_DONE_EVENT) {
|
|
596
671
|
return event;
|
|
597
672
|
}
|
|
598
673
|
throw new ProviderError(`SSE event "${event.event}" is not declared in operation transport.events.`, {
|
|
@@ -711,8 +786,7 @@ function enforceStreamChunkLimit(body, maxChunkBytes) {
|
|
|
711
786
|
}
|
|
712
787
|
function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
713
788
|
const transport = operation.transport?.kind ?? "json";
|
|
714
|
-
if (transport === "sse" &&
|
|
715
|
-
(result instanceof Response || result instanceof ReadableStream)) {
|
|
789
|
+
if (transport === "sse" && (result instanceof Response || result instanceof ReadableStream)) {
|
|
716
790
|
void cleanup();
|
|
717
791
|
throw new ProviderError("SSE operations must return an AsyncIterable of typed stream.event(...) values.", {
|
|
718
792
|
code: "SSE_RESULT_UNSUPPORTED",
|
|
@@ -723,9 +797,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
723
797
|
}
|
|
724
798
|
if (result instanceof Response) {
|
|
725
799
|
const httpTransport = getHttpStreamTransport(operation);
|
|
726
|
-
if (httpTransport &&
|
|
727
|
-
result.body &&
|
|
728
|
-
httpTransport?.maxChunkBytes !== undefined) {
|
|
800
|
+
if (httpTransport && result.body && httpTransport?.maxChunkBytes !== undefined) {
|
|
729
801
|
return responseWithCleanup(new Response(enforceStreamChunkLimit(result.body, httpTransport.maxChunkBytes), {
|
|
730
802
|
headers: result.headers,
|
|
731
803
|
status: result.status,
|
|
@@ -744,8 +816,7 @@ function toStreamingResponse(operation, result, cleanup, requestId) {
|
|
|
744
816
|
? { "Content-Type": "text/event-stream; charset=utf-8" }
|
|
745
817
|
: {
|
|
746
818
|
"Content-Type": operation.transport?.kind === "http-stream"
|
|
747
|
-
? (operation.transport.contentType ??
|
|
748
|
-
"application/octet-stream")
|
|
819
|
+
? (operation.transport.contentType ?? "application/octet-stream")
|
|
749
820
|
: "application/octet-stream",
|
|
750
821
|
},
|
|
751
822
|
}), cleanup);
|
|
@@ -763,9 +834,7 @@ function getSseTransport(operation) {
|
|
|
763
834
|
return operation.transport?.kind === "sse" ? operation.transport : undefined;
|
|
764
835
|
}
|
|
765
836
|
function getHttpStreamTransport(operation) {
|
|
766
|
-
return operation.transport?.kind === "http-stream"
|
|
767
|
-
? operation.transport
|
|
768
|
-
: undefined;
|
|
837
|
+
return operation.transport?.kind === "http-stream" ? operation.transport : undefined;
|
|
769
838
|
}
|
|
770
839
|
function toAuthFlowResponse(result, contextPatch) {
|
|
771
840
|
if (result instanceof Response) {
|
|
@@ -793,7 +862,7 @@ function isAuthFlowLocale(value) {
|
|
|
793
862
|
return value === "en" || value === "ko" || value === "ja";
|
|
794
863
|
}
|
|
795
864
|
function isAuthTurn(value) {
|
|
796
|
-
return
|
|
865
|
+
return !!value && typeof value === "object" && "kind" in value && "turnId" in value;
|
|
797
866
|
}
|
|
798
867
|
function loadAuthFlowLocaleCatalogs(provider) {
|
|
799
868
|
for (const providerDir of [
|
|
@@ -856,7 +925,14 @@ async function handleOperation(provider, request, operationId, options = {}, sta
|
|
|
856
925
|
}
|
|
857
926
|
};
|
|
858
927
|
try {
|
|
859
|
-
const result =
|
|
928
|
+
const result = options.operationExecutor
|
|
929
|
+
? await options.operationExecutor({
|
|
930
|
+
provider,
|
|
931
|
+
operationId,
|
|
932
|
+
ctx,
|
|
933
|
+
request,
|
|
934
|
+
})
|
|
935
|
+
: await executeOperation(provider, operationId, ctx, request.input);
|
|
860
936
|
if (streaming && operation) {
|
|
861
937
|
return toStreamingResponse(operation, result, cleanup, request.requestId);
|
|
862
938
|
}
|
|
@@ -890,8 +966,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
890
966
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
891
967
|
});
|
|
892
968
|
}
|
|
969
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
970
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
971
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
972
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
973
|
+
// stranded flow even when provisioning is broken.
|
|
893
974
|
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
894
975
|
try {
|
|
976
|
+
if (route !== "abort") {
|
|
977
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
978
|
+
}
|
|
895
979
|
const result = route === "start"
|
|
896
980
|
? await flow.start(context)
|
|
897
981
|
: route === "continue"
|
|
@@ -930,14 +1014,142 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
930
1014
|
context.stealth.close?.();
|
|
931
1015
|
}
|
|
932
1016
|
}
|
|
1017
|
+
class StatefulForwardingReplayCache {
|
|
1018
|
+
maxEntries;
|
|
1019
|
+
#nonces = new Map();
|
|
1020
|
+
#expiryBuckets = new Map();
|
|
1021
|
+
#nextExpiryBucket;
|
|
1022
|
+
#latestExpiryBucket;
|
|
1023
|
+
constructor(maxEntries) {
|
|
1024
|
+
this.maxEntries = maxEntries;
|
|
1025
|
+
}
|
|
1026
|
+
claim(nonce, expiresAtMs, nowMs) {
|
|
1027
|
+
this.dropExpiredBuckets(nowMs);
|
|
1028
|
+
if (this.#nonces.has(nonce))
|
|
1029
|
+
return "replayed";
|
|
1030
|
+
if (this.#nonces.size >= this.maxEntries)
|
|
1031
|
+
return "full";
|
|
1032
|
+
const expiryBucket = Math.ceil(expiresAtMs / STATEFUL_FORWARDING_REPLAY_BUCKET_MS) *
|
|
1033
|
+
STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1034
|
+
this.#nonces.set(nonce, expiryBucket);
|
|
1035
|
+
const bucket = this.#expiryBuckets.get(expiryBucket) ?? new Set();
|
|
1036
|
+
bucket.add(nonce);
|
|
1037
|
+
this.#expiryBuckets.set(expiryBucket, bucket);
|
|
1038
|
+
this.#nextExpiryBucket = Math.min(this.#nextExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1039
|
+
this.#latestExpiryBucket = Math.max(this.#latestExpiryBucket ?? expiryBucket, expiryBucket);
|
|
1040
|
+
return "accepted";
|
|
1041
|
+
}
|
|
1042
|
+
dropExpiredBuckets(nowMs) {
|
|
1043
|
+
if (this.#nextExpiryBucket === undefined || this.#latestExpiryBucket === undefined)
|
|
1044
|
+
return;
|
|
1045
|
+
if (nowMs >= this.#latestExpiryBucket) {
|
|
1046
|
+
this.#nonces.clear();
|
|
1047
|
+
this.#expiryBuckets.clear();
|
|
1048
|
+
this.#nextExpiryBucket = undefined;
|
|
1049
|
+
this.#latestExpiryBucket = undefined;
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
while (this.#nextExpiryBucket <= nowMs) {
|
|
1053
|
+
const bucket = this.#expiryBuckets.get(this.#nextExpiryBucket);
|
|
1054
|
+
if (bucket) {
|
|
1055
|
+
for (const cachedNonce of bucket)
|
|
1056
|
+
this.#nonces.delete(cachedNonce);
|
|
1057
|
+
this.#expiryBuckets.delete(this.#nextExpiryBucket);
|
|
1058
|
+
}
|
|
1059
|
+
this.#nextExpiryBucket += STATEFUL_FORWARDING_REPLAY_BUCKET_MS;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function verifyStatefulForwardingRequest(input) {
|
|
1064
|
+
const config = input.options.statefulForwarding;
|
|
1065
|
+
if (!config?.secret) {
|
|
1066
|
+
throw new ProviderError("Stateful forwarding is not configured.", {
|
|
1067
|
+
code: "STATEFUL_FORWARDING_NOT_CONFIGURED",
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
const timestamp = input.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "";
|
|
1071
|
+
const signature = input.headers.get(STATEFUL_FORWARDING_SIGNATURE_HEADER) ?? "";
|
|
1072
|
+
const nonce = input.headers.get(STATEFUL_FORWARDING_NONCE_HEADER) ?? "";
|
|
1073
|
+
if (!timestamp || !signature || !nonce) {
|
|
1074
|
+
throw new ProviderError("Stateful forwarding signature headers are missing.", {
|
|
1075
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_MISSING",
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
if (nonce.length > 256) {
|
|
1079
|
+
throw new ProviderError("Stateful forwarding nonce is invalid.", {
|
|
1080
|
+
code: "STATEFUL_FORWARDING_NONCE_INVALID",
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
const timestampMs = Date.parse(timestamp);
|
|
1084
|
+
const maxSkewMs = config.maxSkewMs ?? DEFAULT_STATEFUL_FORWARDING_MAX_SKEW_MS;
|
|
1085
|
+
if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > maxSkewMs) {
|
|
1086
|
+
throw new ProviderError("Stateful forwarding signature timestamp is outside the allowed skew.", { code: "STATEFUL_FORWARDING_TIMESTAMP_INVALID" });
|
|
1087
|
+
}
|
|
1088
|
+
if (!verifyStatefulRequestSignature({
|
|
1089
|
+
secret: config.secret,
|
|
1090
|
+
timestamp,
|
|
1091
|
+
rawBody: input.rawBody,
|
|
1092
|
+
method: input.method,
|
|
1093
|
+
path: input.path,
|
|
1094
|
+
nonce,
|
|
1095
|
+
signature,
|
|
1096
|
+
})) {
|
|
1097
|
+
throw new ProviderError("Stateful forwarding signature is invalid.", {
|
|
1098
|
+
code: "STATEFUL_FORWARDING_SIGNATURE_INVALID",
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
const replayResult = input.replayCache.claim(nonce, timestampMs + maxSkewMs, Date.now());
|
|
1102
|
+
if (replayResult === "replayed") {
|
|
1103
|
+
throw new ProviderError("Stateful forwarding nonce has already been used.", {
|
|
1104
|
+
code: "STATEFUL_FORWARDING_REPLAY_DETECTED",
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (replayResult === "full") {
|
|
1108
|
+
throw new ProviderError("Stateful forwarding replay cache is at capacity.", {
|
|
1109
|
+
code: "STATEFUL_FORWARDING_REPLAY_CACHE_FULL",
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function operationRequestFromForwardingEnvelope(envelope) {
|
|
1114
|
+
return {
|
|
1115
|
+
...envelope.operationRequest,
|
|
1116
|
+
...(envelope.deadlineAt !== undefined ? { deadlineAt: envelope.deadlineAt } : {}),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function parseStatefulForwardingEnvelope(rawBody) {
|
|
1120
|
+
const parsed = ProviderServerStatefulForwardEnvelopeSchema.safeParse(rawBody);
|
|
1121
|
+
if (parsed.success)
|
|
1122
|
+
return parsed.data;
|
|
1123
|
+
throw new ProviderError("Stateful forwarding envelope is invalid.", {
|
|
1124
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1125
|
+
details: zodDetails(parsed.error),
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
933
1128
|
export function createServerApp(provider, options = {}) {
|
|
1129
|
+
validateStatefulServerConfig(options);
|
|
934
1130
|
const app = new Hono();
|
|
935
1131
|
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1132
|
+
const statefulForwardingReplayCache = new StatefulForwardingReplayCache(options.statefulForwarding?.replayCacheMaxEntries ??
|
|
1133
|
+
DEFAULT_STATEFUL_FORWARDING_REPLAY_CACHE_MAX_ENTRIES);
|
|
936
1134
|
const state = options.state ??
|
|
937
1135
|
createProviderRuntimeStateFromEnv({
|
|
938
1136
|
providerId: provider.id,
|
|
939
1137
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
940
1138
|
});
|
|
1139
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
1140
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
1141
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
1142
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
1143
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
1144
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
|
|
1145
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
1146
|
+
logger({
|
|
1147
|
+
level: "warn",
|
|
1148
|
+
event: "provider_secrets_missing",
|
|
1149
|
+
providerId: provider.id,
|
|
1150
|
+
missingSecrets: missingSecretsAtBoot,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
941
1153
|
app.notFound((c) => c.json({
|
|
942
1154
|
error: {
|
|
943
1155
|
code: "not_found",
|
|
@@ -949,6 +1161,120 @@ export function createServerApp(provider, options = {}) {
|
|
|
949
1161
|
provider: provider.id,
|
|
950
1162
|
version: provider.version,
|
|
951
1163
|
}));
|
|
1164
|
+
app.post(STATEFUL_INTERNAL_OPERATIONS_ROUTE, async (c) => {
|
|
1165
|
+
let rawBodyText = "";
|
|
1166
|
+
let rawBody;
|
|
1167
|
+
const operation = "stateful-internal";
|
|
1168
|
+
const requestCost = startRequestCost();
|
|
1169
|
+
try {
|
|
1170
|
+
if (!options.internalOperationExecutor) {
|
|
1171
|
+
throw new ProviderError("Stateful internal operation executor is not configured.", {
|
|
1172
|
+
code: "STATEFUL_INTERNAL_EXECUTOR_NOT_CONFIGURED",
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
rawBodyText = await c.req.raw.clone().text();
|
|
1176
|
+
verifyStatefulForwardingRequest({
|
|
1177
|
+
options,
|
|
1178
|
+
rawBody: rawBodyText,
|
|
1179
|
+
headers: c.req.raw.headers,
|
|
1180
|
+
method: c.req.raw.method,
|
|
1181
|
+
path: STATEFUL_INTERNAL_OPERATIONS_ROUTE,
|
|
1182
|
+
replayCache: statefulForwardingReplayCache,
|
|
1183
|
+
});
|
|
1184
|
+
try {
|
|
1185
|
+
rawBody = JSON.parse(rawBodyText);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
throw new ProviderError("Stateful forwarding envelope is not valid JSON.", {
|
|
1189
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
const envelope = parseStatefulForwardingEnvelope(rawBody);
|
|
1193
|
+
if (envelope.providerId !== provider.id) {
|
|
1194
|
+
throw new ProviderError("Stateful forwarding envelope providerId does not match the served provider.", { code: "STATEFUL_FORWARDING_PROVIDER_MISMATCH" });
|
|
1195
|
+
}
|
|
1196
|
+
if (envelope.requestId !== envelope.operationRequest.requestId) {
|
|
1197
|
+
throw new ProviderError("Stateful forwarding requestId values do not match.", {
|
|
1198
|
+
code: "STATEFUL_FORWARDING_ENVELOPE_INVALID",
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
if (envelope.sourcePodId !==
|
|
1202
|
+
(c.req.raw.headers.get(STATEFUL_FORWARDING_SOURCE_POD_HEADER) ?? "")) {
|
|
1203
|
+
throw new ProviderError("Stateful forwarding source pod does not match its header.", {
|
|
1204
|
+
code: "STATEFUL_FORWARDING_SOURCE_POD_MISMATCH",
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
if (envelope.forwardedAt !== (c.req.raw.headers.get(STATEFUL_FORWARDING_TIMESTAMP_HEADER) ?? "")) {
|
|
1208
|
+
throw new ProviderError("Stateful forwarding forwardedAt does not match its signature timestamp.", { code: "STATEFUL_FORWARDING_ENVELOPE_INVALID" });
|
|
1209
|
+
}
|
|
1210
|
+
const deadlineAtMs = envelope.deadlineAt ? Date.parse(envelope.deadlineAt) : undefined;
|
|
1211
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1212
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1213
|
+
}
|
|
1214
|
+
const remainingDeadlineMs = deadlineAtMs === undefined ? undefined : deadlineAtMs - Date.now();
|
|
1215
|
+
const deadlineSignal = remainingDeadlineMs === undefined ? undefined : AbortSignal.timeout(remainingDeadlineMs);
|
|
1216
|
+
const signal = deadlineSignal
|
|
1217
|
+
? AbortSignal.any([c.req.raw.signal, deadlineSignal])
|
|
1218
|
+
: c.req.raw.signal;
|
|
1219
|
+
const ownerFenceValidation = Promise.resolve(options.statefulForwarding?.validateOwnerFence({
|
|
1220
|
+
providerId: envelope.providerId,
|
|
1221
|
+
sessionKey: envelope.sessionKey,
|
|
1222
|
+
ownerPodId: envelope.ownerPodId,
|
|
1223
|
+
generation: envelope.generation,
|
|
1224
|
+
sourcePodId: envelope.sourcePodId,
|
|
1225
|
+
forwardedAt: envelope.forwardedAt,
|
|
1226
|
+
requestId: envelope.requestId,
|
|
1227
|
+
...(envelope.idempotencyKey ? { idempotencyKey: envelope.idempotencyKey } : {}),
|
|
1228
|
+
}, signal));
|
|
1229
|
+
let ownerFenceValid;
|
|
1230
|
+
try {
|
|
1231
|
+
ownerFenceValid = deadlineSignal
|
|
1232
|
+
? await Promise.race([
|
|
1233
|
+
ownerFenceValidation,
|
|
1234
|
+
new Promise((_resolve, reject) => {
|
|
1235
|
+
deadlineSignal.addEventListener("abort", () => reject(new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt)), { once: true });
|
|
1236
|
+
}),
|
|
1237
|
+
])
|
|
1238
|
+
: await ownerFenceValidation;
|
|
1239
|
+
}
|
|
1240
|
+
catch (error) {
|
|
1241
|
+
if (deadlineSignal?.aborted) {
|
|
1242
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1243
|
+
}
|
|
1244
|
+
throw error;
|
|
1245
|
+
}
|
|
1246
|
+
if (ownerFenceValid !== true) {
|
|
1247
|
+
throw new ProviderError("Stateful forwarding owner fence is no longer current.", {
|
|
1248
|
+
code: "STATEFUL_FORWARDING_OWNER_FENCE_INVALID",
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
const request = operationRequestFromForwardingEnvelope(envelope);
|
|
1252
|
+
const operationId = envelope.operationId;
|
|
1253
|
+
const ctx = createProviderContext(provider, request, operationId, options, state);
|
|
1254
|
+
if (deadlineAtMs !== undefined && deadlineAtMs <= Date.now()) {
|
|
1255
|
+
throw new StatefulRoutingDeadlineError(envelope.requestId, envelope.deadlineAt);
|
|
1256
|
+
}
|
|
1257
|
+
const output = await options.internalOperationExecutor({
|
|
1258
|
+
provider,
|
|
1259
|
+
operationId,
|
|
1260
|
+
ctx,
|
|
1261
|
+
request,
|
|
1262
|
+
internalStatefulForward: envelope,
|
|
1263
|
+
signal,
|
|
1264
|
+
});
|
|
1265
|
+
logProviderSuccess(logger, provider, "operation", operationId || operation, request.requestId, 200, finishRequestCost(requestCost));
|
|
1266
|
+
return c.json({ data: output });
|
|
1267
|
+
}
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
const status = toStatusCode(error);
|
|
1270
|
+
if (isProviderError(error) && error.code === "STATEFUL_FORWARDING_REPLAY_CACHE_FULL") {
|
|
1271
|
+
c.header("Retry-After", String(STATEFUL_FORWARDING_REPLAY_RETRY_AFTER_SECONDS));
|
|
1272
|
+
}
|
|
1273
|
+
const requestId = extractRequestId(rawBody);
|
|
1274
|
+
logProviderError(logger, provider, "operation", operation, requestId, error, status, finishRequestCost(requestCost));
|
|
1275
|
+
return c.json(toErrorResponse(error, requestId), status);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
952
1278
|
app.post("/v1/:operation", async (c) => {
|
|
953
1279
|
let rawBody;
|
|
954
1280
|
const operation = c.req.param("operation");
|
|
@@ -1085,6 +1411,28 @@ export function createServerApp(provider, options = {}) {
|
|
|
1085
1411
|
});
|
|
1086
1412
|
return app;
|
|
1087
1413
|
}
|
|
1414
|
+
function validateStatefulServerConfig(options) {
|
|
1415
|
+
if (options.statefulForwarding && !options.internalOperationExecutor) {
|
|
1416
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires internalOperationExecutor; missing option internalOperationExecutor.");
|
|
1417
|
+
}
|
|
1418
|
+
if (options.internalOperationExecutor && !options.statefulForwarding?.secret) {
|
|
1419
|
+
throw new Error("Invalid provider server configuration: internalOperationExecutor requires statefulForwarding.secret; missing option statefulForwarding.secret.");
|
|
1420
|
+
}
|
|
1421
|
+
if (options.statefulForwarding &&
|
|
1422
|
+
typeof options.statefulForwarding.validateOwnerFence !== "function") {
|
|
1423
|
+
throw new Error("Invalid provider server configuration: statefulForwarding requires validateOwnerFence.");
|
|
1424
|
+
}
|
|
1425
|
+
if (options.statefulForwarding?.maxSkewMs !== undefined &&
|
|
1426
|
+
(!Number.isFinite(options.statefulForwarding.maxSkewMs) ||
|
|
1427
|
+
options.statefulForwarding.maxSkewMs <= 0)) {
|
|
1428
|
+
throw new Error("Invalid provider server configuration: maxSkewMs must be positive.");
|
|
1429
|
+
}
|
|
1430
|
+
if (options.statefulForwarding?.replayCacheMaxEntries !== undefined &&
|
|
1431
|
+
(!Number.isInteger(options.statefulForwarding.replayCacheMaxEntries) ||
|
|
1432
|
+
options.statefulForwarding.replayCacheMaxEntries <= 0)) {
|
|
1433
|
+
throw new Error("Invalid provider server configuration: replayCacheMaxEntries must be a positive integer.");
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1088
1436
|
function getBunServeRuntime() {
|
|
1089
1437
|
const bunValue = Object.getOwnPropertyDescriptor(globalThis, "Bun")?.value;
|
|
1090
1438
|
if (!bunValue || typeof bunValue !== "object") {
|
|
@@ -1100,6 +1448,9 @@ function getBunServeRuntime() {
|
|
|
1100
1448
|
},
|
|
1101
1449
|
};
|
|
1102
1450
|
}
|
|
1451
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
1452
|
+
const DEFAULT_SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
|
|
1453
|
+
const processSignalCoordinators = new Map();
|
|
1103
1454
|
export async function serve(provider, options = {}) {
|
|
1104
1455
|
const bunRuntime = getBunServeRuntime();
|
|
1105
1456
|
if (bunRuntime === undefined) {
|
|
@@ -1107,29 +1458,186 @@ export async function serve(provider, options = {}) {
|
|
|
1107
1458
|
code: "RUNTIME_UNSUPPORTED",
|
|
1108
1459
|
});
|
|
1109
1460
|
}
|
|
1461
|
+
const logger = options.logger ?? defaultProviderServerLogger;
|
|
1462
|
+
const configuredTimeoutMs = shutdownTimeout(options.shutdown?.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS);
|
|
1463
|
+
const configuredSignals = resolveShutdownSignals(options.shutdown?.signals ?? true);
|
|
1110
1464
|
const app = createServerApp(provider, {
|
|
1111
1465
|
logger: options.logger,
|
|
1112
1466
|
stt: options.stt,
|
|
1467
|
+
state: options.state,
|
|
1468
|
+
allowMemoryStateFallback: options.allowMemoryStateFallback,
|
|
1469
|
+
operationExecutor: options.operationExecutor,
|
|
1470
|
+
internalOperationExecutor: options.internalOperationExecutor,
|
|
1471
|
+
statefulForwarding: options.statefulForwarding,
|
|
1113
1472
|
});
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
});
|
|
1119
|
-
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1120
|
-
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1121
|
-
// starts when the shared self-test master secret env is present.
|
|
1122
|
-
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1123
|
-
if (selfTestSecrets) {
|
|
1124
|
-
const selfTestApp = createSelfTestApp(provider, {
|
|
1125
|
-
secrets: selfTestSecrets,
|
|
1126
|
-
invoke: createSelfTestInvoke(app),
|
|
1127
|
-
});
|
|
1128
|
-
bunRuntime.serve({
|
|
1129
|
-
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1473
|
+
const servers = [];
|
|
1474
|
+
try {
|
|
1475
|
+
servers.push(bunRuntime.serve({
|
|
1476
|
+
port: options.port ?? DEFAULT_PORT,
|
|
1130
1477
|
hostname: options.host ?? DEFAULT_HOST,
|
|
1131
|
-
fetch:
|
|
1132
|
-
});
|
|
1478
|
+
fetch: app.fetch,
|
|
1479
|
+
}));
|
|
1480
|
+
// Internal self-test listener (health dependency inversion): a SEPARATE
|
|
1481
|
+
// socket the tenant-facing gateway never dials. Off by default — it only
|
|
1482
|
+
// starts when the shared self-test master secret env is present.
|
|
1483
|
+
const selfTestSecrets = resolveSelfTestMasterSecrets();
|
|
1484
|
+
if (selfTestSecrets) {
|
|
1485
|
+
const selfTestApp = createSelfTestApp(provider, {
|
|
1486
|
+
secrets: selfTestSecrets,
|
|
1487
|
+
invoke: createSelfTestInvoke(app),
|
|
1488
|
+
authFlow: createSelfTestAuthFlowInvoke(app),
|
|
1489
|
+
});
|
|
1490
|
+
servers.push(bunRuntime.serve({
|
|
1491
|
+
port: options.selfTestPort ?? resolveSelfTestPort(),
|
|
1492
|
+
hostname: options.host ?? DEFAULT_HOST,
|
|
1493
|
+
fetch: selfTestApp.fetch,
|
|
1494
|
+
}));
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1499
|
+
throw error;
|
|
1500
|
+
}
|
|
1501
|
+
const server = servers[0];
|
|
1502
|
+
if (!server)
|
|
1503
|
+
throw new Error("Provider server failed to create its primary listener.");
|
|
1504
|
+
let closePromise;
|
|
1505
|
+
let unregisterSignals = () => { };
|
|
1506
|
+
const close = (closeOptions = {}) => {
|
|
1507
|
+
if (closePromise)
|
|
1508
|
+
return closePromise;
|
|
1509
|
+
const timeoutMs = shutdownTimeout(closeOptions.timeoutMs ?? configuredTimeoutMs);
|
|
1510
|
+
closePromise = closeProviderServers({
|
|
1511
|
+
servers,
|
|
1512
|
+
hooks: options.shutdown?.hooks ?? [],
|
|
1513
|
+
timeoutMs,
|
|
1514
|
+
logger,
|
|
1515
|
+
providerId: provider.id,
|
|
1516
|
+
}).finally(() => unregisterSignals());
|
|
1517
|
+
return closePromise;
|
|
1518
|
+
};
|
|
1519
|
+
try {
|
|
1520
|
+
unregisterSignals = registerForProcessSignals(configuredSignals, () => close({ timeoutMs: configuredTimeoutMs }));
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
unregisterSignals();
|
|
1524
|
+
await Promise.allSettled(servers.map((startedServer) => startedServer.stop(true)));
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
return { port: server.port, close };
|
|
1528
|
+
}
|
|
1529
|
+
function registerForProcessSignals(signals, close) {
|
|
1530
|
+
if (signals.length === 0)
|
|
1531
|
+
return () => { };
|
|
1532
|
+
const registration = {
|
|
1533
|
+
signals: new Set(signals),
|
|
1534
|
+
close,
|
|
1535
|
+
};
|
|
1536
|
+
let registered = true;
|
|
1537
|
+
const unregister = () => {
|
|
1538
|
+
if (!registered)
|
|
1539
|
+
return;
|
|
1540
|
+
registered = false;
|
|
1541
|
+
for (const signal of registration.signals) {
|
|
1542
|
+
const coordinator = processSignalCoordinators.get(signal);
|
|
1543
|
+
if (!coordinator)
|
|
1544
|
+
continue;
|
|
1545
|
+
coordinator.registrations.delete(registration);
|
|
1546
|
+
if (coordinator.registrations.size === 0 && !coordinator.handling) {
|
|
1547
|
+
process.removeListener(signal, coordinator.listener);
|
|
1548
|
+
processSignalCoordinators.delete(signal);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
try {
|
|
1553
|
+
for (const signal of signals) {
|
|
1554
|
+
let coordinator = processSignalCoordinators.get(signal);
|
|
1555
|
+
if (!coordinator) {
|
|
1556
|
+
const created = {
|
|
1557
|
+
registrations: new Set(),
|
|
1558
|
+
handling: false,
|
|
1559
|
+
listener: () => handleCoordinatedSignal(signal, created),
|
|
1560
|
+
};
|
|
1561
|
+
coordinator = created;
|
|
1562
|
+
processSignalCoordinators.set(signal, coordinator);
|
|
1563
|
+
process.on(signal, coordinator.listener);
|
|
1564
|
+
}
|
|
1565
|
+
coordinator.registrations.add(registration);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
unregister();
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
return unregister;
|
|
1573
|
+
}
|
|
1574
|
+
function handleCoordinatedSignal(signal, coordinator) {
|
|
1575
|
+
if (coordinator.handling)
|
|
1576
|
+
return;
|
|
1577
|
+
coordinator.handling = true;
|
|
1578
|
+
const registrations = [...coordinator.registrations];
|
|
1579
|
+
void Promise.allSettled(registrations.map((registration) => registration.close())).finally(() => {
|
|
1580
|
+
if (processSignalCoordinators.get(signal) === coordinator) {
|
|
1581
|
+
process.removeListener(signal, coordinator.listener);
|
|
1582
|
+
processSignalCoordinators.delete(signal);
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
process.kill(process.pid, signal);
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
process.exitCode = 1;
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
async function closeProviderServers(input) {
|
|
1593
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1594
|
+
const gracefulStops = input.servers.map((server) => server.stop(false));
|
|
1595
|
+
for (const gracefulStop of gracefulStops)
|
|
1596
|
+
gracefulStop.catch(() => undefined);
|
|
1597
|
+
for (const [hookIndex, hook] of input.hooks.entries()) {
|
|
1598
|
+
try {
|
|
1599
|
+
await withinShutdownBudget(Promise.resolve().then(hook), deadline);
|
|
1600
|
+
}
|
|
1601
|
+
catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
input.logger({
|
|
1604
|
+
level: "error",
|
|
1605
|
+
event: "provider_shutdown_hook_failed",
|
|
1606
|
+
providerId: input.providerId,
|
|
1607
|
+
hookIndex,
|
|
1608
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
1609
|
+
message: error instanceof Error ? error.message : "Shutdown hook failed.",
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
catch { }
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const forcedStops = input.servers.map((server) => server.stop(true));
|
|
1616
|
+
await withinShutdownBudget(Promise.allSettled([...gracefulStops, ...forcedStops]).then(() => undefined), deadline).catch(() => undefined);
|
|
1617
|
+
}
|
|
1618
|
+
async function withinShutdownBudget(promise, deadline) {
|
|
1619
|
+
promise.catch(() => undefined);
|
|
1620
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
1621
|
+
let timer;
|
|
1622
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1623
|
+
timer = setTimeout(() => reject(new Error("Provider server shutdown timed out.")), remainingMs);
|
|
1624
|
+
});
|
|
1625
|
+
try {
|
|
1626
|
+
return await Promise.race([promise, timeout]);
|
|
1627
|
+
}
|
|
1628
|
+
finally {
|
|
1629
|
+
if (timer)
|
|
1630
|
+
clearTimeout(timer);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function resolveShutdownSignals(signals) {
|
|
1634
|
+
if (signals === false)
|
|
1635
|
+
return [];
|
|
1636
|
+
return [...new Set(signals === true ? DEFAULT_SHUTDOWN_SIGNALS : signals)];
|
|
1637
|
+
}
|
|
1638
|
+
function shutdownTimeout(value) {
|
|
1639
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1640
|
+
throw new Error("Provider server shutdown timeoutMs must be a non-negative finite number.");
|
|
1133
1641
|
}
|
|
1134
|
-
|
|
1642
|
+
return value;
|
|
1135
1643
|
}
|