@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51
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 +537 -33
- package/CHANGELOG.md +205 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1249 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- 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/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +179 -16
- package/dist/config/loader.js +435 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +43 -10
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -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 +9 -4
- package/dist/stealth/profiles.js +160 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1751 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +690 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +202 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- 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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
|
|
2
|
+
import {
|
|
3
|
+
OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
4
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
|
|
5
|
+
type OTLPExportOptions,
|
|
6
|
+
type OTLPExportResolution,
|
|
7
|
+
type OTLPResourceResolution,
|
|
8
|
+
resolveOTLPExportOptions,
|
|
9
|
+
resolveOTLPResourceAttributes,
|
|
10
|
+
} from "../runtime/otlp.js";
|
|
11
|
+
import {
|
|
12
|
+
type CreateTraceContextOptions,
|
|
13
|
+
resolveTraceContextOptions,
|
|
14
|
+
type Span,
|
|
15
|
+
} from "../runtime/trace.js";
|
|
16
|
+
import {
|
|
17
|
+
sanitizeSpanForOutput,
|
|
18
|
+
sanitizeSpanNameForOutput,
|
|
19
|
+
sanitizeTraceAttributes,
|
|
20
|
+
} from "../trace-sanitization.js";
|
|
21
|
+
import type { TraceConfig } from "../types.js";
|
|
22
|
+
|
|
23
|
+
type EnvLike = Record<string, string | undefined>;
|
|
24
|
+
|
|
25
|
+
// Warn once per environment object: process.env in production, each injected env in tests.
|
|
26
|
+
const warnedEnvironments = new WeakSet<EnvLike>();
|
|
27
|
+
const warnedResourceEnvironments = new WeakSet<EnvLike>();
|
|
28
|
+
|
|
29
|
+
function warnDiscardedResourceAttributes(env: EnvLike, resolution: OTLPResourceResolution): void {
|
|
30
|
+
if (resolution.discarded.length === 0 || warnedResourceEnvironments.has(env)) return;
|
|
31
|
+
warnedResourceEnvironments.add(env);
|
|
32
|
+
console.warn(
|
|
33
|
+
`[apifuse] ${resolution.discarded.join(", ")} could not be parsed as an OTel key=value list and was ignored.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function warnExportDisabled(
|
|
38
|
+
env: EnvLike,
|
|
39
|
+
resolution: Exclude<OTLPExportResolution, { status: "resolved" }>,
|
|
40
|
+
): void {
|
|
41
|
+
if (warnedEnvironments.has(env)) return;
|
|
42
|
+
warnedEnvironments.add(env);
|
|
43
|
+
console.warn(
|
|
44
|
+
resolution.status === "unconfigured"
|
|
45
|
+
? `[apifuse] OTLP trace export is enabled but no endpoint is configured; set ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT} or ${OTEL_EXPORTER_OTLP_ENDPOINT}. Trace export is disabled.`
|
|
46
|
+
: `[apifuse] OTLP trace export is enabled but ${resolution.source} ${resolution.reason}. Trace export is disabled.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveServerOTLPExportOptions(
|
|
51
|
+
config: TraceConfig,
|
|
52
|
+
env: EnvLike,
|
|
53
|
+
): OTLPExportOptions | undefined {
|
|
54
|
+
const resolution = resolveOTLPExportOptions(
|
|
55
|
+
{
|
|
56
|
+
endpoint: config.otlp?.endpoint ?? config.endpoint,
|
|
57
|
+
headers: config.otlp?.headers,
|
|
58
|
+
timeout: config.otlp?.timeout,
|
|
59
|
+
},
|
|
60
|
+
env,
|
|
61
|
+
);
|
|
62
|
+
if (resolution.status === "resolved") return resolution.options;
|
|
63
|
+
warnExportDisabled(env, resolution);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Per-request attributes carry client-supplied values (request_id) and get the full attribute
|
|
69
|
+
* sanitizer, exactly as on the console path. Operator-configured attributes from the process
|
|
70
|
+
* environment are identifiers rather than request data: their values are kept, with control
|
|
71
|
+
* characters neutralized, length bounded, and secret-named keys redacted.
|
|
72
|
+
*/
|
|
73
|
+
function resolveExportResourceAttributes(
|
|
74
|
+
requestAttributes: Record<string, string>,
|
|
75
|
+
env: EnvLike,
|
|
76
|
+
): Record<string, string> {
|
|
77
|
+
const resolution = resolveOTLPResourceAttributes({}, env);
|
|
78
|
+
warnDiscardedResourceAttributes(env, resolution);
|
|
79
|
+
const operatorAttributes = Object.fromEntries(
|
|
80
|
+
Object.entries(resolution.attributes).map(([key, value]) => [
|
|
81
|
+
sanitizeSpanNameForOutput(key),
|
|
82
|
+
isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeSpanNameForOutput(value),
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
const sanitizedRequestAttributes = Object.fromEntries(
|
|
86
|
+
Object.entries(sanitizeTraceAttributes(requestAttributes)).map(([key, value]) => [
|
|
87
|
+
key,
|
|
88
|
+
String(value),
|
|
89
|
+
]),
|
|
90
|
+
);
|
|
91
|
+
return { ...operatorAttributes, ...sanitizedRequestAttributes };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
95
|
+
export function resolveServerTraceContextOptions(
|
|
96
|
+
config: TraceConfig,
|
|
97
|
+
resourceAttributes: Record<string, string>,
|
|
98
|
+
env: EnvLike = process.env,
|
|
99
|
+
): CreateTraceContextOptions {
|
|
100
|
+
const resolved = resolveTraceContextOptions(config);
|
|
101
|
+
const outputEnabled = config.enabled !== false && config.exporter !== "none";
|
|
102
|
+
const consoleHook =
|
|
103
|
+
outputEnabled && (config.exporter === "console" || config.exporter === "json")
|
|
104
|
+
? (span: Span) => console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes)))
|
|
105
|
+
: undefined;
|
|
106
|
+
const onSpan =
|
|
107
|
+
consoleHook && resolved.onSpan
|
|
108
|
+
? (span: Span) => {
|
|
109
|
+
consoleHook(span);
|
|
110
|
+
resolved.onSpan?.(span);
|
|
111
|
+
}
|
|
112
|
+
: (consoleHook ?? resolved.onSpan);
|
|
113
|
+
const exportOptions =
|
|
114
|
+
outputEnabled && config.exporter === "otlp"
|
|
115
|
+
? resolveServerOTLPExportOptions(config, env)
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
maxSpans: resolved.maxSpans,
|
|
120
|
+
onSpan,
|
|
121
|
+
...(exportOptions
|
|
122
|
+
? {
|
|
123
|
+
exportOptions,
|
|
124
|
+
resourceAttributes: resolveExportResourceAttributes(resourceAttributes, env),
|
|
125
|
+
sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span),
|
|
126
|
+
}
|
|
127
|
+
: {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/server/types.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PROVIDER_ERROR_SOURCES } from "../observability.js";
|
|
2
3
|
|
|
3
4
|
import { HttpRetryPreset } from "../types.js";
|
|
4
5
|
|
|
5
|
-
export const ConnectionModeSchema = z.enum([
|
|
6
|
+
export const ConnectionModeSchema = z.enum([
|
|
7
|
+
"oauth2",
|
|
8
|
+
"oauth2_proxied",
|
|
9
|
+
"credentials",
|
|
10
|
+
"platform-managed",
|
|
11
|
+
"none",
|
|
12
|
+
]);
|
|
6
13
|
|
|
7
14
|
export const OperationConnectionSchema = z.object({
|
|
8
15
|
id: z.string(),
|
|
@@ -26,6 +33,11 @@ export const ErrorEnvelopeSchema = z.object({
|
|
|
26
33
|
code: z.string(),
|
|
27
34
|
message: z.string(),
|
|
28
35
|
requestId: z.string().optional(),
|
|
36
|
+
retryable: z.boolean(),
|
|
37
|
+
// Who stopped the request (honest-provider-error-contract); optional so
|
|
38
|
+
// older peers (stateful forwarding owners) remain parseable. Derived from
|
|
39
|
+
// the PROVIDER_ERROR_SOURCES registry so the enum cannot drift.
|
|
40
|
+
source: z.enum(PROVIDER_ERROR_SOURCES).optional(),
|
|
29
41
|
fix: z.string().optional(),
|
|
30
42
|
details: z.unknown().optional(),
|
|
31
43
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Stateful module compatibility note
|
|
2
|
+
|
|
3
|
+
The `@apifuse/provider-sdk/stateful` entry point is new and unreleased. Its initial contract includes
|
|
4
|
+
the following compatibility-sensitive behavior:
|
|
5
|
+
|
|
6
|
+
- Session identifiers use the branded `SessionKey` returned by `buildSessionKey`.
|
|
7
|
+
`parseSessionKey` returns `SessionKeyParts`; validate and brand an external string with
|
|
8
|
+
`buildSessionKey(parseSessionKey(value))`. Registry inputs, routing requests, event owner fences,
|
|
9
|
+
and release APIs no longer accept arbitrary strings. A decoded `SessionOwnerRecord.sessionKey`
|
|
10
|
+
remains a string and must be checked where external data enters trusted SDK code (a trust
|
|
11
|
+
boundary).
|
|
12
|
+
- Adapters declare `policy.concurrency` (`serialize` or bounded/unbounded `parallel`) and
|
|
13
|
+
`policy.reconnect` (`resume`, `recreate`, or `unsupported`). `restore`, `snapshot`, and `health`
|
|
14
|
+
are optional; a declared `snapshot` must be paired with the manager's `checkpointStore`.
|
|
15
|
+
- Code in the process hosting the adapter (the provider pod) publishes through
|
|
16
|
+
`ProviderEventPublisher`; persistent services that survive provider-pod restarts (durable
|
|
17
|
+
platform code) implement `ProviderEventPipeline`. An adapter's
|
|
18
|
+
`subscribe(ctx, session, publish)` callback accepts only an event. The session manager, not the
|
|
19
|
+
adapter, binds the registry's current write authorization (the authoritative
|
|
20
|
+
`{ sessionKey, generation, ownerPodId, ownerEndpoint }` fence) before calling the publisher.
|
|
21
|
+
Adapters must not construct or cache owner fences themselves.
|
|
22
|
+
- `subscribe` must return a disposer. The manager runs it before checkpointing and closing a
|
|
23
|
+
session. `health()` and `reconcileWrite()` use the same per-session concurrency scheduler and
|
|
24
|
+
optional ownership validation semantics as invocation.
|
|
25
|
+
- `HttpProviderEventEmitter.flush(timeoutMs)` resolves to
|
|
26
|
+
`{ delivered: number, failed: number, pending: number }`. Counts are cumulative for the emitter's
|
|
27
|
+
lifetime; `pending` is the buffer remaining when the flush returns.
|
|
28
|
+
- `serve()` resolves to a `ProviderServerHandle` with `port` and idempotent `close({ timeoutMs? })`.
|
|
29
|
+
Close stops listeners, runs shutdown hooks in declaration order, and enforces the configured
|
|
30
|
+
timeout. Signal-enabled handles share one process handler per signal; a signal closes every
|
|
31
|
+
registered server before it is re-raised. Closing the last handle removes the shared handlers.
|
|
32
|
+
- `ProviderServerStatefulForwardEnvelope` is strict. Its owner fence is flattened into
|
|
33
|
+
`sessionKey`, `ownerPodId`, and `generation`; a nested `owner` object is not accepted. The
|
|
34
|
+
envelope also carries operation/session identity, `sourcePodId`, `forwardedAt`, optional
|
|
35
|
+
`idempotencyKey`, optional ISO `deadlineAt`, and the nested `operationRequest`. Unknown or
|
|
36
|
+
malformed fields fail closed, and the owner-side executor receives `deadlineAt` on its request.
|
|
37
|
+
- Stateful forwarding is configured as a pair: `internalOperationExecutor` requires
|
|
38
|
+
`statefulForwarding`, and `statefulForwarding` requires an `internalOperationExecutor`.
|
|
39
|
+
`statefulForwarding` supplies the signing secret and `validateOwnerFence`; it may also set
|
|
40
|
+
`maxSkewMs` and `replayCacheMaxEntries`. Replay-cache capacity returns HTTP 503 with
|
|
41
|
+
`Retry-After`; fail-closed nonce handling means invalid, replayed, or untrackable nonces are
|
|
42
|
+
rejected instead of allowing an unverifiable request.
|
|
43
|
+
- Stateful signatures bind the method, route, timestamp, nonce, and exact body. Forwarding also
|
|
44
|
+
rejects provider/source mismatches, stale owner fences, nonce replay, and request-scoped files.
|
|
45
|
+
|
|
46
|
+
## Minimal composition
|
|
47
|
+
|
|
48
|
+
Inside the module that defines your `provider`, the following wires an adapter, session manager,
|
|
49
|
+
router, signed owner forwarding, and graceful shutdown. Replace the environment variable names and
|
|
50
|
+
the service-account mapping with your deployment's values.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import {
|
|
54
|
+
buildSessionKey,
|
|
55
|
+
createStatefulOwnerFenceValidator,
|
|
56
|
+
HttpSessionOwnerRegistry,
|
|
57
|
+
HttpStatefulOwnerForwarder,
|
|
58
|
+
parseSessionKey,
|
|
59
|
+
statefulForwardingContextFromProviderRequest,
|
|
60
|
+
StatefulProviderSessionManager,
|
|
61
|
+
StatefulSessionRouter,
|
|
62
|
+
type StatefulProviderAdapter,
|
|
63
|
+
withStatefulLocalProviderContext,
|
|
64
|
+
} from "@apifuse/provider-sdk/stateful";
|
|
65
|
+
import { serve, type ProviderServerOperationExecutor } from "@apifuse/provider-sdk/server";
|
|
66
|
+
|
|
67
|
+
type Session = { closed: boolean };
|
|
68
|
+
|
|
69
|
+
const adapter = {
|
|
70
|
+
providerId: provider.id,
|
|
71
|
+
policy: { concurrency: { mode: "serialize" }, reconnect: "recreate" },
|
|
72
|
+
connect: async () => ({ closed: false }),
|
|
73
|
+
invoke: async (_ctx, _session, request) => ({ output: request.input }),
|
|
74
|
+
close: async (_ctx, session) => {
|
|
75
|
+
session.closed = true;
|
|
76
|
+
},
|
|
77
|
+
} satisfies StatefulProviderAdapter<Session>;
|
|
78
|
+
|
|
79
|
+
const registry = new HttpSessionOwnerRegistry({
|
|
80
|
+
baseUrl: process.env.STATEFUL_CONTROL_PLANE_URL as string,
|
|
81
|
+
secret: process.env.STATEFUL_CONTROL_PLANE_SECRET as string,
|
|
82
|
+
});
|
|
83
|
+
const manager = new StatefulProviderSessionManager({
|
|
84
|
+
adapter,
|
|
85
|
+
poolPolicy: { maxSessions: 100, idleTimeoutMs: "unlimited", maxLifetimeMs: "unlimited" },
|
|
86
|
+
});
|
|
87
|
+
const currentPod = {
|
|
88
|
+
podId: process.env.POD_ID as string,
|
|
89
|
+
endpoint: process.env.POD_ENDPOINT as string,
|
|
90
|
+
};
|
|
91
|
+
const router = new StatefulSessionRouter({
|
|
92
|
+
currentPod,
|
|
93
|
+
registry,
|
|
94
|
+
forwarder: new HttpStatefulOwnerForwarder({
|
|
95
|
+
currentPodId: currentPod.podId,
|
|
96
|
+
secret: process.env.STATEFUL_FORWARDING_SECRET as string,
|
|
97
|
+
}),
|
|
98
|
+
executor: manager,
|
|
99
|
+
leaseDurationMs: 30_000,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const executeStateful: ProviderServerOperationExecutor = async ({
|
|
103
|
+
ctx,
|
|
104
|
+
request,
|
|
105
|
+
operationId,
|
|
106
|
+
internalStatefulForward,
|
|
107
|
+
}) => {
|
|
108
|
+
const keyParts = internalStatefulForward
|
|
109
|
+
? parseSessionKey(internalStatefulForward.sessionKey)
|
|
110
|
+
: {
|
|
111
|
+
providerId: provider.id,
|
|
112
|
+
serviceAccountId: request.connection?.externalRef as string,
|
|
113
|
+
connectionId: (request.connection?.id ?? request.connectionId) as string,
|
|
114
|
+
};
|
|
115
|
+
const result = await router.route({
|
|
116
|
+
requestId: request.requestId,
|
|
117
|
+
sessionKey: buildSessionKey(keyParts),
|
|
118
|
+
providerId: provider.id,
|
|
119
|
+
operationId,
|
|
120
|
+
connectionId: keyParts.connectionId,
|
|
121
|
+
serviceAccountId: keyParts.serviceAccountId,
|
|
122
|
+
input: request.input,
|
|
123
|
+
...(request.deadlineAt ? { deadlineAt: request.deadlineAt } : {}),
|
|
124
|
+
runtimeContext: withStatefulLocalProviderContext(
|
|
125
|
+
ctx,
|
|
126
|
+
statefulForwardingContextFromProviderRequest(request),
|
|
127
|
+
),
|
|
128
|
+
});
|
|
129
|
+
return result.output;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await serve(provider, {
|
|
133
|
+
operationExecutor: executeStateful,
|
|
134
|
+
internalOperationExecutor: executeStateful,
|
|
135
|
+
statefulForwarding: {
|
|
136
|
+
secret: process.env.STATEFUL_FORWARDING_SECRET as string,
|
|
137
|
+
validateOwnerFence: createStatefulOwnerFenceValidator(registry),
|
|
138
|
+
},
|
|
139
|
+
shutdown: {
|
|
140
|
+
hooks: [
|
|
141
|
+
() => manager.closeAll("server-shutdown"),
|
|
142
|
+
() => router.release(),
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type StatefulControlPlaneOperation = "resolve" | "acquire" | "renew" | "release";
|
|
2
|
+
|
|
3
|
+
export class StatefulRoutingDeadlineError extends Error {
|
|
4
|
+
readonly requestId: string;
|
|
5
|
+
readonly deadlineAt: string;
|
|
6
|
+
|
|
7
|
+
constructor(requestId: string, deadlineAt: string) {
|
|
8
|
+
super(`Stateful operation request ${requestId} deadline has expired.`);
|
|
9
|
+
this.name = "StatefulRoutingDeadlineError";
|
|
10
|
+
this.requestId = requestId;
|
|
11
|
+
this.deadlineAt = deadlineAt;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class StatefulControlPlaneError extends Error {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly operation: StatefulControlPlaneOperation;
|
|
18
|
+
readonly status?: number;
|
|
19
|
+
readonly cause?: unknown;
|
|
20
|
+
|
|
21
|
+
constructor(input: {
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly operation: StatefulControlPlaneOperation;
|
|
25
|
+
readonly status?: number;
|
|
26
|
+
readonly cause?: unknown;
|
|
27
|
+
}) {
|
|
28
|
+
super(input.message);
|
|
29
|
+
this.name = "StatefulControlPlaneError";
|
|
30
|
+
this.code = input.code;
|
|
31
|
+
this.operation = input.operation;
|
|
32
|
+
this.status = input.status;
|
|
33
|
+
this.cause = input.cause;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { statefulSignedHeaders } from "../stateful-signing.js";
|
|
2
|
+
import type {
|
|
3
|
+
ProviderEventDeliveryFailureReason,
|
|
4
|
+
ProviderEventDeliveryFailureRecorder,
|
|
5
|
+
} from "./provider-event-delivery-failures.js";
|
|
6
|
+
import type {
|
|
7
|
+
ProviderEventPublisher,
|
|
8
|
+
ProviderEventPublishOptions,
|
|
9
|
+
PublishAck,
|
|
10
|
+
} from "./provider-event-pipeline.js";
|
|
11
|
+
import {
|
|
12
|
+
NOOP_PROVIDER_EVENT_METRIC_EMITTER,
|
|
13
|
+
providerEventMetricLabels,
|
|
14
|
+
type ProviderEventMetricEmitter,
|
|
15
|
+
} from "./provider-event-pipeline-metrics.js";
|
|
16
|
+
import type { ProviderEvent } from "./provider-events.js";
|
|
17
|
+
|
|
18
|
+
type FetchTransport = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
19
|
+
|
|
20
|
+
export type HttpProviderEventEmitterOptions = {
|
|
21
|
+
readonly baseUrl: string;
|
|
22
|
+
readonly secret: string;
|
|
23
|
+
readonly fetch?: FetchTransport;
|
|
24
|
+
readonly clock?: () => Date;
|
|
25
|
+
readonly random?: () => number;
|
|
26
|
+
readonly metricEmitter?: ProviderEventMetricEmitter;
|
|
27
|
+
readonly failureRecorder?: ProviderEventDeliveryFailureRecorder;
|
|
28
|
+
readonly maxBufferedEvents?: number;
|
|
29
|
+
readonly retryBaseMs?: number;
|
|
30
|
+
readonly retryMaxMs?: number;
|
|
31
|
+
readonly maxAttempts?: number;
|
|
32
|
+
readonly jitterRatio?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type BufferedEvent = {
|
|
36
|
+
readonly event: ProviderEvent;
|
|
37
|
+
readonly rawBody: string;
|
|
38
|
+
readonly idempotencyKey: string;
|
|
39
|
+
attempts: number;
|
|
40
|
+
dropped: boolean;
|
|
41
|
+
abortController?: AbortController;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const DEFAULT_MAX_BUFFERED_EVENTS = 1_000;
|
|
45
|
+
const DEFAULT_RETRY_BASE_MS = 500;
|
|
46
|
+
const DEFAULT_RETRY_MAX_MS = 30_000;
|
|
47
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
48
|
+
const DEFAULT_JITTER_RATIO = 0.2;
|
|
49
|
+
|
|
50
|
+
export type ProviderEventFlushReport = {
|
|
51
|
+
/** Events delivered during this emitter's lifetime. */
|
|
52
|
+
readonly delivered: number;
|
|
53
|
+
/** Events permanently dropped during this emitter's lifetime. */
|
|
54
|
+
readonly failed: number;
|
|
55
|
+
/** Events still buffered when flush returned. */
|
|
56
|
+
readonly pending: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const CONSOLE_PROVIDER_EVENT_DELIVERY_FAILURE_RECORDER = {
|
|
60
|
+
record(failure) {
|
|
61
|
+
console.error(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
event: "provider_event_delivery_failed",
|
|
64
|
+
eventId: failure.eventId,
|
|
65
|
+
reason: failure.reason,
|
|
66
|
+
attempts: failure.attempts,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
} satisfies ProviderEventDeliveryFailureRecorder;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Provider-pod HTTP publication with a bounded best-effort delivery guarantee and at-least-once
|
|
74
|
+
* transport retries. This is not durable at-least-once delivery: buffered events can be lost if
|
|
75
|
+
* the process is killed. Graceful shutdown should drain the in-memory buffer with flush().
|
|
76
|
+
*/
|
|
77
|
+
export class HttpProviderEventEmitter implements ProviderEventPublisher {
|
|
78
|
+
readonly #baseUrl: string;
|
|
79
|
+
readonly #secret: string;
|
|
80
|
+
readonly #fetch: FetchTransport;
|
|
81
|
+
readonly #clock: () => Date;
|
|
82
|
+
readonly #random: () => number;
|
|
83
|
+
readonly #metricEmitter: ProviderEventMetricEmitter;
|
|
84
|
+
readonly #failureRecorder: ProviderEventDeliveryFailureRecorder;
|
|
85
|
+
readonly #maxBufferedEvents: number;
|
|
86
|
+
readonly #retryBaseMs: number;
|
|
87
|
+
readonly #retryMaxMs: number;
|
|
88
|
+
readonly #maxAttempts: number;
|
|
89
|
+
readonly #jitterRatio: number;
|
|
90
|
+
readonly #queue: BufferedEvent[] = [];
|
|
91
|
+
#drainPromise?: Promise<void>;
|
|
92
|
+
#deliveredCount = 0;
|
|
93
|
+
#failedCount = 0;
|
|
94
|
+
|
|
95
|
+
constructor(options: HttpProviderEventEmitterOptions) {
|
|
96
|
+
if (options.baseUrl.trim().length === 0) {
|
|
97
|
+
throw new Error("Provider event ingest baseUrl is required.");
|
|
98
|
+
}
|
|
99
|
+
if (options.secret.trim().length === 0) {
|
|
100
|
+
throw new Error("Provider event ingest secret is required.");
|
|
101
|
+
}
|
|
102
|
+
this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
103
|
+
this.#secret = options.secret;
|
|
104
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
105
|
+
this.#clock = options.clock ?? (() => new Date());
|
|
106
|
+
this.#random = options.random ?? Math.random;
|
|
107
|
+
this.#metricEmitter = options.metricEmitter ?? NOOP_PROVIDER_EVENT_METRIC_EMITTER;
|
|
108
|
+
this.#failureRecorder =
|
|
109
|
+
options.failureRecorder ?? CONSOLE_PROVIDER_EVENT_DELIVERY_FAILURE_RECORDER;
|
|
110
|
+
this.#maxBufferedEvents = positiveInteger(
|
|
111
|
+
options.maxBufferedEvents ?? DEFAULT_MAX_BUFFERED_EVENTS,
|
|
112
|
+
"maxBufferedEvents",
|
|
113
|
+
);
|
|
114
|
+
this.#retryBaseMs = nonnegativeFinite(
|
|
115
|
+
options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS,
|
|
116
|
+
"retryBaseMs",
|
|
117
|
+
);
|
|
118
|
+
this.#retryMaxMs = nonnegativeFinite(options.retryMaxMs ?? DEFAULT_RETRY_MAX_MS, "retryMaxMs");
|
|
119
|
+
this.#maxAttempts = positiveInteger(options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS, "maxAttempts");
|
|
120
|
+
this.#jitterRatio = boundedRatio(options.jitterRatio ?? DEFAULT_JITTER_RATIO, "jitterRatio");
|
|
121
|
+
if (this.#retryMaxMs < this.#retryBaseMs) {
|
|
122
|
+
throw new Error("Provider event retryMaxMs must be greater than or equal to retryBaseMs.");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
publish(event: ProviderEvent, options: ProviderEventPublishOptions): PublishAck {
|
|
127
|
+
const accepted = this.#enqueue(event, options);
|
|
128
|
+
return { accepted, queued: this.#queue.length };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pendingCount(): number {
|
|
132
|
+
return this.#queue.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async flush(timeoutMs = 30_000): Promise<ProviderEventFlushReport> {
|
|
136
|
+
nonnegativeFinite(timeoutMs, "flush timeout");
|
|
137
|
+
this.#ensureDrain();
|
|
138
|
+
if (this.#queue.length === 0 || timeoutMs === 0) return this.#flushReport();
|
|
139
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
140
|
+
const timedOut = new Promise<void>((resolve) => {
|
|
141
|
+
timeout = setTimeout(resolve, timeoutMs);
|
|
142
|
+
});
|
|
143
|
+
await Promise.race([this.#drainPromise ?? Promise.resolve(), timedOut]);
|
|
144
|
+
if (timeout) clearTimeout(timeout);
|
|
145
|
+
return this.#flushReport();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#flushReport(): ProviderEventFlushReport {
|
|
149
|
+
return {
|
|
150
|
+
delivered: this.#deliveredCount,
|
|
151
|
+
failed: this.#failedCount,
|
|
152
|
+
pending: this.#queue.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#enqueue(event: ProviderEvent, options: ProviderEventPublishOptions): boolean {
|
|
157
|
+
let rawBody: string;
|
|
158
|
+
try {
|
|
159
|
+
rawBody = JSON.stringify({
|
|
160
|
+
...event,
|
|
161
|
+
ownerFence: options.ownerFence,
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.#recordFailure(event, "attempts_exhausted", 0, error);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (this.#queue.length >= this.#maxBufferedEvents) {
|
|
168
|
+
const oldest = this.#queue.shift();
|
|
169
|
+
if (oldest) {
|
|
170
|
+
oldest.dropped = true;
|
|
171
|
+
oldest.abortController?.abort();
|
|
172
|
+
this.#recordFailure(oldest.event, "buffer_overflow", oldest.attempts);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.#queue.push({
|
|
176
|
+
event,
|
|
177
|
+
rawBody,
|
|
178
|
+
idempotencyKey: options.idempotencyKey ?? event.eventId,
|
|
179
|
+
attempts: 0,
|
|
180
|
+
dropped: false,
|
|
181
|
+
});
|
|
182
|
+
this.#ensureDrain();
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#ensureDrain(): void {
|
|
187
|
+
if (this.#drainPromise || this.#queue.length === 0) return;
|
|
188
|
+
this.#drainPromise = Promise.resolve()
|
|
189
|
+
.then(() => this.#drain())
|
|
190
|
+
.finally(() => {
|
|
191
|
+
this.#drainPromise = undefined;
|
|
192
|
+
if (this.#queue.length > 0) this.#ensureDrain();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #drain(): Promise<void> {
|
|
197
|
+
while (this.#queue.length > 0) {
|
|
198
|
+
const current = this.#queue[0];
|
|
199
|
+
if (!current) return;
|
|
200
|
+
current.attempts += 1;
|
|
201
|
+
const delivered = await this.#send(current);
|
|
202
|
+
if (current.dropped) continue;
|
|
203
|
+
if (delivered) {
|
|
204
|
+
if (this.#queue[0] === current) this.#queue.shift();
|
|
205
|
+
this.#deliveredCount += 1;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (current.attempts >= this.#maxAttempts) {
|
|
209
|
+
if (this.#queue[0] === current) this.#queue.shift();
|
|
210
|
+
this.#recordFailure(current.event, "attempts_exhausted", current.attempts);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
await delay(this.#retryDelay(current.attempts));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async #send(buffered: BufferedEvent): Promise<boolean> {
|
|
218
|
+
const timestamp = this.#clock().toISOString();
|
|
219
|
+
const path = "/v1/stateful/events";
|
|
220
|
+
const abortController = new AbortController();
|
|
221
|
+
buffered.abortController = abortController;
|
|
222
|
+
try {
|
|
223
|
+
const response = await this.#fetch(`${this.#baseUrl}${path}`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
"content-type": "application/json",
|
|
227
|
+
"x-apifuse-event-id": buffered.idempotencyKey,
|
|
228
|
+
...statefulSignedHeaders({
|
|
229
|
+
secret: this.#secret,
|
|
230
|
+
timestamp,
|
|
231
|
+
rawBody: buffered.rawBody,
|
|
232
|
+
method: "POST",
|
|
233
|
+
path,
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
body: buffered.rawBody,
|
|
237
|
+
signal: abortController.signal,
|
|
238
|
+
});
|
|
239
|
+
return response.ok;
|
|
240
|
+
} catch {
|
|
241
|
+
return false;
|
|
242
|
+
} finally {
|
|
243
|
+
if (buffered.abortController === abortController) {
|
|
244
|
+
buffered.abortController = undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#retryDelay(attempts: number): number {
|
|
250
|
+
const exponential = Math.min(
|
|
251
|
+
this.#retryMaxMs,
|
|
252
|
+
this.#retryBaseMs * 2 ** Math.max(0, attempts - 1),
|
|
253
|
+
);
|
|
254
|
+
const jitter = 1 + (this.#random() * 2 - 1) * this.#jitterRatio;
|
|
255
|
+
return Math.max(0, Math.round(exponential * jitter));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#recordFailure(
|
|
259
|
+
event: ProviderEvent,
|
|
260
|
+
reason: ProviderEventDeliveryFailureReason,
|
|
261
|
+
attempts: number,
|
|
262
|
+
error?: unknown,
|
|
263
|
+
): void {
|
|
264
|
+
this.#failedCount += 1;
|
|
265
|
+
this.#incrementDropMetric(event);
|
|
266
|
+
try {
|
|
267
|
+
const recorded = this.#failureRecorder.record({
|
|
268
|
+
eventId: event.eventId,
|
|
269
|
+
reason,
|
|
270
|
+
attempts,
|
|
271
|
+
failedAt: this.#clock().toISOString(),
|
|
272
|
+
...(error ? { error: "Provider event could not be serialized." } : {}),
|
|
273
|
+
});
|
|
274
|
+
void Promise.resolve(recorded).catch(() => {});
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#incrementDropMetric(
|
|
279
|
+
event: ProviderEvent,
|
|
280
|
+
emitter: ProviderEventMetricEmitter = this.#metricEmitter,
|
|
281
|
+
): void {
|
|
282
|
+
try {
|
|
283
|
+
emitter.increment(
|
|
284
|
+
"apifuse_stateful_provider_event_drop_total",
|
|
285
|
+
providerEventMetricLabels(event),
|
|
286
|
+
);
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function delay(ms: number): Promise<void> {
|
|
292
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function positiveInteger(value: number, name: string): number {
|
|
296
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
297
|
+
throw new Error(`Provider event ${name} must be a positive integer.`);
|
|
298
|
+
}
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function nonnegativeFinite(value: number, name: string): number {
|
|
303
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
304
|
+
throw new Error(`Provider event ${name} must be a non-negative finite number.`);
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function boundedRatio(value: number, name: string): number {
|
|
310
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
311
|
+
throw new Error(`Provider event ${name} must be between 0 and 1.`);
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|