@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,462 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
4
|
+
const REDACTED_VALUE = "[REDACTED]";
|
|
5
|
+
const CREDENTIAL_KEY_PATTERN = /(?:token|password|secret|credential|cookie|authorization)/i;
|
|
6
|
+
|
|
7
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
8
|
+
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
|
9
|
+
|
|
10
|
+
export type ProviderEventSubject = {
|
|
11
|
+
/** Provider-defined resource category, for example `message`, `mailbox`, or `browser-context`. */
|
|
12
|
+
kind: string;
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ProviderEventSession = {
|
|
17
|
+
/** Canonical logical session identity shared by every stateful provider. */
|
|
18
|
+
sessionKey: string;
|
|
19
|
+
/** Ownership generation shared by every stateful provider for stale-event fencing. */
|
|
20
|
+
generation: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface ProviderEvent<TPayload = Record<string, unknown>> {
|
|
24
|
+
/** Universal stable identity used for transport retries and deduplication. */
|
|
25
|
+
eventId: string;
|
|
26
|
+
/** Universal provider namespace needed to route and interpret the event. */
|
|
27
|
+
providerId: string;
|
|
28
|
+
/** Optional because mailbox/browser protocols may not expose an API connection identity. */
|
|
29
|
+
connectionId?: string;
|
|
30
|
+
/** Optional because not every provider session maps to a platform service account. */
|
|
31
|
+
serviceAccountId?: string;
|
|
32
|
+
/** Universal provider-defined event classification. */
|
|
33
|
+
eventType: string;
|
|
34
|
+
/** Optional for protocol-level events without a single resource subject. */
|
|
35
|
+
subject?: ProviderEventSubject;
|
|
36
|
+
/** Optional when the upstream protocol does not provide an occurrence timestamp. */
|
|
37
|
+
occurredAt?: string;
|
|
38
|
+
/** Universal SDK observation time used for ordering and lag measurement. */
|
|
39
|
+
observedAt: string;
|
|
40
|
+
/** Universal JSON data envelope; protocols without data use an empty object. */
|
|
41
|
+
payload: TPayload;
|
|
42
|
+
/** Universal stateful ownership fence for rejecting events from stale sessions. */
|
|
43
|
+
session: ProviderEventSession;
|
|
44
|
+
providerCursor?: string;
|
|
45
|
+
dedupeKey?: string;
|
|
46
|
+
rawRef?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ProviderEventIdFactory = () => string;
|
|
50
|
+
export type ProviderEventClock = () => Date;
|
|
51
|
+
|
|
52
|
+
export type BuildProviderEventInput<
|
|
53
|
+
TPayload extends Record<string, unknown> = Record<string, unknown>,
|
|
54
|
+
> = Omit<ProviderEvent<TPayload>, "eventId" | "observedAt"> & {
|
|
55
|
+
eventId?: string;
|
|
56
|
+
observedAt?: string;
|
|
57
|
+
clock?: ProviderEventClock;
|
|
58
|
+
idFactory?: ProviderEventIdFactory;
|
|
59
|
+
maxPayloadBytes?: number;
|
|
60
|
+
shouldRedactPayload?: boolean;
|
|
61
|
+
/** Provider-specific credential key patterns added to the conservative SDK defaults. */
|
|
62
|
+
redactionPatterns?: readonly RegExp[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ProviderEventListOptions = {
|
|
66
|
+
providerId?: string;
|
|
67
|
+
connectionId?: string;
|
|
68
|
+
eventType?: string;
|
|
69
|
+
subjectKind?: string;
|
|
70
|
+
subjectId?: string;
|
|
71
|
+
afterObservedAt?: string;
|
|
72
|
+
limit?: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ProviderEventAppendResult = {
|
|
76
|
+
event: ProviderEvent;
|
|
77
|
+
appended: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export interface ProviderEventLog {
|
|
81
|
+
append(event: ProviderEvent): Promise<ProviderEventAppendResult>;
|
|
82
|
+
get(eventId: string): Promise<ProviderEvent | null>;
|
|
83
|
+
list(options?: ProviderEventListOptions): Promise<ProviderEvent[]>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ProviderEventEmitterDefaults = {
|
|
87
|
+
providerId: string;
|
|
88
|
+
connectionId?: string;
|
|
89
|
+
serviceAccountId?: string;
|
|
90
|
+
session: ProviderEventSession;
|
|
91
|
+
clock?: ProviderEventClock;
|
|
92
|
+
idFactory?: ProviderEventIdFactory;
|
|
93
|
+
maxPayloadBytes?: number;
|
|
94
|
+
shouldRedactPayload?: boolean;
|
|
95
|
+
redactionPatterns?: readonly RegExp[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type ProviderEventEmitterInput<
|
|
99
|
+
TPayload extends Record<string, unknown> = Record<string, unknown>,
|
|
100
|
+
> = Omit<
|
|
101
|
+
BuildProviderEventInput<TPayload>,
|
|
102
|
+
"providerId" | "connectionId" | "serviceAccountId" | "session"
|
|
103
|
+
> & {
|
|
104
|
+
providerId?: string;
|
|
105
|
+
connectionId?: string;
|
|
106
|
+
serviceAccountId?: string;
|
|
107
|
+
session?: ProviderEventSession;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export class ProviderEventValidationError extends Error {
|
|
111
|
+
constructor(message: string) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "ProviderEventValidationError";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildProviderEvent<
|
|
118
|
+
TPayload extends Record<string, unknown> = Record<string, unknown>,
|
|
119
|
+
>(input: BuildProviderEventInput<TPayload>): ProviderEvent<TPayload> {
|
|
120
|
+
validatePayload(input.payload, input.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES);
|
|
121
|
+
const observedAt = input.observedAt ?? input.clock?.().toISOString() ?? new Date().toISOString();
|
|
122
|
+
const payload =
|
|
123
|
+
input.shouldRedactPayload === false
|
|
124
|
+
? input.payload
|
|
125
|
+
: redactProviderEventPayload(input.payload, input.redactionPatterns);
|
|
126
|
+
|
|
127
|
+
const event: ProviderEvent<TPayload> = {
|
|
128
|
+
eventId: input.eventId ?? buildEventId({ ...input, observedAt }),
|
|
129
|
+
providerId: input.providerId,
|
|
130
|
+
eventType: input.eventType,
|
|
131
|
+
observedAt,
|
|
132
|
+
payload,
|
|
133
|
+
session: input.session,
|
|
134
|
+
...(input.connectionId ? { connectionId: input.connectionId } : {}),
|
|
135
|
+
...(input.serviceAccountId ? { serviceAccountId: input.serviceAccountId } : {}),
|
|
136
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
137
|
+
...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
|
|
138
|
+
...(input.providerCursor ? { providerCursor: input.providerCursor } : {}),
|
|
139
|
+
...(input.dedupeKey ? { dedupeKey: input.dedupeKey } : {}),
|
|
140
|
+
...(input.rawRef ? { rawRef: input.rawRef } : {}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
validateProviderEvent(event, input.maxPayloadBytes);
|
|
144
|
+
return event;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function redactProviderEventPayload<TPayload>(
|
|
148
|
+
payload: TPayload,
|
|
149
|
+
extraPatterns: readonly RegExp[] = [],
|
|
150
|
+
): TPayload {
|
|
151
|
+
const cloned = structuredClone(payload);
|
|
152
|
+
redactValueInPlace(cloned, [CREDENTIAL_KEY_PATTERN, ...extraPatterns]);
|
|
153
|
+
return cloned;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function isStaleProviderEvent(event: ProviderEvent, currentGeneration: number): boolean {
|
|
157
|
+
return event.session.generation < currentGeneration;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class InMemoryProviderEventLog implements ProviderEventLog {
|
|
161
|
+
readonly #events: ProviderEvent[] = [];
|
|
162
|
+
readonly #eventIds = new Set<string>();
|
|
163
|
+
readonly #dedupeKeys = new Set<string>();
|
|
164
|
+
|
|
165
|
+
async append(event: ProviderEvent): Promise<ProviderEventAppendResult> {
|
|
166
|
+
validateProviderEvent(event);
|
|
167
|
+
|
|
168
|
+
if (this.#eventIds.has(event.eventId)) {
|
|
169
|
+
return { event, appended: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const dedupeTuple = event.dedupeKey ? buildDedupeTuple(event) : undefined;
|
|
173
|
+
if (dedupeTuple && this.#dedupeKeys.has(dedupeTuple)) {
|
|
174
|
+
return { event, appended: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.#events.push(event);
|
|
178
|
+
this.#eventIds.add(event.eventId);
|
|
179
|
+
if (dedupeTuple) this.#dedupeKeys.add(dedupeTuple);
|
|
180
|
+
|
|
181
|
+
return { event, appended: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async get(eventId: string): Promise<ProviderEvent | null> {
|
|
185
|
+
validateRequiredString("eventId", eventId);
|
|
186
|
+
return this.#events.find((event) => event.eventId === eventId) ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async list(options: ProviderEventListOptions = {}): Promise<ProviderEvent[]> {
|
|
190
|
+
validateListOptions(options);
|
|
191
|
+
let events = this.#events;
|
|
192
|
+
|
|
193
|
+
if (options.providerId) {
|
|
194
|
+
events = events.filter((event) => event.providerId === options.providerId);
|
|
195
|
+
}
|
|
196
|
+
if (options.connectionId) {
|
|
197
|
+
events = events.filter((event) => event.connectionId === options.connectionId);
|
|
198
|
+
}
|
|
199
|
+
if (options.eventType) {
|
|
200
|
+
events = events.filter((event) => event.eventType === options.eventType);
|
|
201
|
+
}
|
|
202
|
+
if (options.subjectKind) {
|
|
203
|
+
events = events.filter((event) => event.subject?.kind === options.subjectKind);
|
|
204
|
+
}
|
|
205
|
+
if (options.subjectId) {
|
|
206
|
+
events = events.filter((event) => event.subject?.id === options.subjectId);
|
|
207
|
+
}
|
|
208
|
+
if (options.afterObservedAt) {
|
|
209
|
+
const afterMs = Date.parse(options.afterObservedAt);
|
|
210
|
+
events = events.filter((event) => Date.parse(event.observedAt) > afterMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return typeof options.limit === "number" ? events.slice(0, options.limit) : [...events];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class ProviderEventEmitter {
|
|
218
|
+
readonly #log: ProviderEventLog;
|
|
219
|
+
readonly #defaults: ProviderEventEmitterDefaults;
|
|
220
|
+
|
|
221
|
+
constructor(log: ProviderEventLog, defaults: ProviderEventEmitterDefaults) {
|
|
222
|
+
validateRequiredString("providerId", defaults.providerId);
|
|
223
|
+
if (defaults.connectionId !== undefined) {
|
|
224
|
+
validateRequiredString("connectionId", defaults.connectionId);
|
|
225
|
+
}
|
|
226
|
+
if (defaults.serviceAccountId !== undefined) {
|
|
227
|
+
validateRequiredString("serviceAccountId", defaults.serviceAccountId);
|
|
228
|
+
}
|
|
229
|
+
validateSession(defaults.session);
|
|
230
|
+
this.#log = log;
|
|
231
|
+
this.#defaults = defaults;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
emit<TPayload extends Record<string, unknown> = Record<string, unknown>>(
|
|
235
|
+
input: ProviderEventEmitterInput<TPayload>,
|
|
236
|
+
): Promise<ProviderEventAppendResult> {
|
|
237
|
+
const event = buildProviderEvent({
|
|
238
|
+
...input,
|
|
239
|
+
providerId: input.providerId ?? this.#defaults.providerId,
|
|
240
|
+
connectionId: input.connectionId ?? this.#defaults.connectionId,
|
|
241
|
+
serviceAccountId: input.serviceAccountId ?? this.#defaults.serviceAccountId,
|
|
242
|
+
session: input.session ?? this.#defaults.session,
|
|
243
|
+
clock: input.clock ?? this.#defaults.clock,
|
|
244
|
+
idFactory: input.idFactory ?? this.#defaults.idFactory,
|
|
245
|
+
maxPayloadBytes: input.maxPayloadBytes ?? this.#defaults.maxPayloadBytes,
|
|
246
|
+
shouldRedactPayload: input.shouldRedactPayload ?? this.#defaults.shouldRedactPayload,
|
|
247
|
+
redactionPatterns: input.redactionPatterns ?? this.#defaults.redactionPatterns,
|
|
248
|
+
});
|
|
249
|
+
return this.#log.append(event);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function buildEventId<TPayload extends Record<string, unknown>>(
|
|
254
|
+
input: Omit<BuildProviderEventInput<TPayload>, "eventId"> & {
|
|
255
|
+
observedAt: string;
|
|
256
|
+
},
|
|
257
|
+
): string {
|
|
258
|
+
if (input.dedupeKey) {
|
|
259
|
+
return deterministicProviderEventId([
|
|
260
|
+
input.providerId,
|
|
261
|
+
input.connectionId ?? "",
|
|
262
|
+
input.eventType,
|
|
263
|
+
input.dedupeKey,
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
input.idFactory?.() ??
|
|
269
|
+
deterministicProviderEventId([
|
|
270
|
+
input.providerId,
|
|
271
|
+
input.connectionId ?? "",
|
|
272
|
+
input.eventType,
|
|
273
|
+
input.subject?.kind ?? "",
|
|
274
|
+
input.subject?.id ?? "",
|
|
275
|
+
input.observedAt,
|
|
276
|
+
])
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function deterministicProviderEventId(parts: readonly string[]): string {
|
|
281
|
+
return `pevt_${createHash("sha256").update(parts.join("\u001f")).digest("hex")}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validateProviderEvent(
|
|
285
|
+
event: ProviderEvent,
|
|
286
|
+
maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES,
|
|
287
|
+
): void {
|
|
288
|
+
validateRequiredString("eventId", event.eventId);
|
|
289
|
+
validateRequiredString("providerId", event.providerId);
|
|
290
|
+
if (event.connectionId !== undefined) validateRequiredString("connectionId", event.connectionId);
|
|
291
|
+
if (event.serviceAccountId !== undefined) {
|
|
292
|
+
validateRequiredString("serviceAccountId", event.serviceAccountId);
|
|
293
|
+
}
|
|
294
|
+
validateRequiredString("eventType", event.eventType);
|
|
295
|
+
if (event.subject) {
|
|
296
|
+
validateRequiredString("subject.kind", event.subject.kind);
|
|
297
|
+
validateRequiredString("subject.id", event.subject.id);
|
|
298
|
+
}
|
|
299
|
+
if (event.occurredAt !== undefined) {
|
|
300
|
+
validateRequiredString("occurredAt", event.occurredAt);
|
|
301
|
+
validateTimestamp("occurredAt", event.occurredAt);
|
|
302
|
+
}
|
|
303
|
+
validateRequiredString("observedAt", event.observedAt);
|
|
304
|
+
validateTimestamp("observedAt", event.observedAt);
|
|
305
|
+
validateSession(event.session);
|
|
306
|
+
if (event.providerCursor !== undefined) {
|
|
307
|
+
validateRequiredString("providerCursor", event.providerCursor);
|
|
308
|
+
}
|
|
309
|
+
if (event.dedupeKey !== undefined) {
|
|
310
|
+
validateRequiredString("dedupeKey", event.dedupeKey);
|
|
311
|
+
}
|
|
312
|
+
if (event.rawRef !== undefined) {
|
|
313
|
+
validateRequiredString("rawRef", event.rawRef);
|
|
314
|
+
}
|
|
315
|
+
validatePayload(event.payload, maxPayloadBytes);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateSession(session: ProviderEventSession): void {
|
|
319
|
+
validateRequiredString("session.sessionKey", session?.sessionKey);
|
|
320
|
+
if (!Number.isInteger(session?.generation) || session.generation < 0) {
|
|
321
|
+
throw new ProviderEventValidationError(
|
|
322
|
+
"ProviderEvent session.generation must be a non-negative integer.",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function validateRequiredString(name: string, value: unknown): void {
|
|
328
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
329
|
+
throw new ProviderEventValidationError(`ProviderEvent ${name} must be a non-empty string.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateTimestamp(name: string, value: string): void {
|
|
334
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
335
|
+
throw new ProviderEventValidationError(
|
|
336
|
+
`ProviderEvent ${name} must be a parseable ISO timestamp string.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function validatePayload(payload: unknown, maxPayloadBytes: number): void {
|
|
342
|
+
if (
|
|
343
|
+
typeof maxPayloadBytes !== "number" ||
|
|
344
|
+
!Number.isFinite(maxPayloadBytes) ||
|
|
345
|
+
maxPayloadBytes <= 0
|
|
346
|
+
) {
|
|
347
|
+
throw new ProviderEventValidationError(
|
|
348
|
+
"ProviderEvent maxPayloadBytes must be a positive finite number.",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!isPlainRecord(payload)) {
|
|
353
|
+
throw new ProviderEventValidationError(
|
|
354
|
+
"ProviderEvent payload must be a JSON-serializable object.",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
assertJsonValue(payload, "payload");
|
|
359
|
+
const encoded = JSON.stringify(payload);
|
|
360
|
+
if (encoded === undefined) {
|
|
361
|
+
throw new ProviderEventValidationError("ProviderEvent payload must be JSON-serializable.");
|
|
362
|
+
}
|
|
363
|
+
const payloadBytes = Buffer.byteLength(encoded, "utf8");
|
|
364
|
+
if (payloadBytes > maxPayloadBytes) {
|
|
365
|
+
throw new ProviderEventValidationError(
|
|
366
|
+
`ProviderEvent payload exceeds max size of ${maxPayloadBytes} bytes.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function assertJsonValue(
|
|
372
|
+
value: unknown,
|
|
373
|
+
path: string,
|
|
374
|
+
seen = new WeakSet<object>(),
|
|
375
|
+
): asserts value is JsonValue {
|
|
376
|
+
if (value === null) return;
|
|
377
|
+
if (typeof value === "string" || typeof value === "boolean") return;
|
|
378
|
+
if (typeof value === "number") {
|
|
379
|
+
if (Number.isFinite(value)) return;
|
|
380
|
+
throw new ProviderEventValidationError(
|
|
381
|
+
`ProviderEvent ${path} must not contain non-finite numbers.`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (Array.isArray(value)) {
|
|
385
|
+
if (seen.has(value)) {
|
|
386
|
+
throw new ProviderEventValidationError(
|
|
387
|
+
`ProviderEvent ${path} must not contain circular references.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
seen.add(value);
|
|
391
|
+
for (const [index, entry] of value.entries()) {
|
|
392
|
+
assertJsonValue(entry, `${path}[${index}]`, seen);
|
|
393
|
+
}
|
|
394
|
+
seen.delete(value);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (isPlainRecord(value)) {
|
|
398
|
+
if (seen.has(value)) {
|
|
399
|
+
throw new ProviderEventValidationError(
|
|
400
|
+
`ProviderEvent ${path} must not contain circular references.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
seen.add(value);
|
|
404
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
405
|
+
assertJsonValue(entry, `${path}.${key}`, seen);
|
|
406
|
+
}
|
|
407
|
+
seen.delete(value);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
throw new ProviderEventValidationError(`ProviderEvent ${path} must be JSON-serializable.`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function redactValueInPlace(value: unknown, patterns: readonly RegExp[]): void {
|
|
414
|
+
if (Array.isArray(value)) {
|
|
415
|
+
for (const entry of value) redactValueInPlace(entry, patterns);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!isPlainRecord(value)) return;
|
|
419
|
+
|
|
420
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
421
|
+
if (patterns.some((pattern) => matchesPattern(pattern, key))) value[key] = REDACTED_VALUE;
|
|
422
|
+
else redactValueInPlace(entry, patterns);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
427
|
+
return (
|
|
428
|
+
typeof value === "object" &&
|
|
429
|
+
value !== null &&
|
|
430
|
+
!Array.isArray(value) &&
|
|
431
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function buildDedupeTuple(event: ProviderEvent): string {
|
|
436
|
+
return [
|
|
437
|
+
event.serviceAccountId ?? "",
|
|
438
|
+
event.providerId,
|
|
439
|
+
event.connectionId ?? "",
|
|
440
|
+
event.dedupeKey,
|
|
441
|
+
].join("\u001f");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function matchesPattern(pattern: RegExp, value: string): boolean {
|
|
445
|
+
pattern.lastIndex = 0;
|
|
446
|
+
const matched = pattern.test(value);
|
|
447
|
+
pattern.lastIndex = 0;
|
|
448
|
+
return matched;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function validateListOptions(options: ProviderEventListOptions): void {
|
|
452
|
+
if (options.afterObservedAt) {
|
|
453
|
+
validateTimestamp("afterObservedAt", options.afterObservedAt);
|
|
454
|
+
}
|
|
455
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
|
|
456
|
+
throw new ProviderEventValidationError("ProviderEvent list limit must be a positive integer.");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function randomProviderEventId(): string {
|
|
461
|
+
return `pevt_${randomUUID().replaceAll("-", "")}`;
|
|
462
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
declare const SESSION_KEY_BRAND: unique symbol;
|
|
2
|
+
|
|
3
|
+
/** Canonical, opaque identity for a stateful provider session. */
|
|
4
|
+
export type SessionKey = string & { readonly [SESSION_KEY_BRAND]: "SessionKey" };
|
|
5
|
+
|
|
6
|
+
export interface SessionKeyParts {
|
|
7
|
+
readonly providerId: string;
|
|
8
|
+
readonly serviceAccountId: string;
|
|
9
|
+
readonly connectionId: string;
|
|
10
|
+
/** Optional extra session axes (e.g. device, mailbox, persona). Defaults to none. */
|
|
11
|
+
readonly dimensions?: Readonly<Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PREFIX = "stateful:v1";
|
|
15
|
+
const REQUIRED_DIMENSIONS = ["providerId", "serviceAccountId", "connectionId"] as const;
|
|
16
|
+
const REQUIRED_DIMENSION_SET = new Set<string>(REQUIRED_DIMENSIONS);
|
|
17
|
+
|
|
18
|
+
export function buildSessionKey(parts: SessionKeyParts): SessionKey {
|
|
19
|
+
validateRequiredPart(parts.providerId, "providerId");
|
|
20
|
+
validateRequiredPart(parts.serviceAccountId, "serviceAccountId");
|
|
21
|
+
validateRequiredPart(parts.connectionId, "connectionId");
|
|
22
|
+
|
|
23
|
+
const dimensions = Object.entries(parts.dimensions ?? {}).sort(([left], [right]) =>
|
|
24
|
+
left < right ? -1 : left > right ? 1 : 0,
|
|
25
|
+
);
|
|
26
|
+
for (const [name, value] of dimensions) {
|
|
27
|
+
validateDimension(name, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const segments: Array<readonly [string, string]> = [
|
|
31
|
+
["providerId", parts.providerId],
|
|
32
|
+
["serviceAccountId", parts.serviceAccountId],
|
|
33
|
+
["connectionId", parts.connectionId],
|
|
34
|
+
...dimensions,
|
|
35
|
+
];
|
|
36
|
+
return `${PREFIX}/${segments
|
|
37
|
+
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
38
|
+
.join("/")}` as SessionKey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseSessionKey(key: string): SessionKeyParts {
|
|
42
|
+
const prefix = `${PREFIX}/`;
|
|
43
|
+
if (!key.startsWith(prefix)) {
|
|
44
|
+
throw new Error(`Invalid session key: expected canonical prefix "${PREFIX}".`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parsed = new Map<string, string>();
|
|
48
|
+
for (const segment of key.slice(prefix.length).split("/")) {
|
|
49
|
+
const separator = segment.indexOf("=");
|
|
50
|
+
if (separator <= 0) {
|
|
51
|
+
throw new Error(`Invalid session key segment "${segment}"; expected name=value.`);
|
|
52
|
+
}
|
|
53
|
+
const name = decodeSegment(segment.slice(0, separator), "dimension name");
|
|
54
|
+
const value = decodeSegment(segment.slice(separator + 1), `dimension "${name}"`);
|
|
55
|
+
if (parsed.has(name)) {
|
|
56
|
+
throw new Error(`Invalid session key: duplicate dimension "${name}".`);
|
|
57
|
+
}
|
|
58
|
+
if (name.trim().length === 0) {
|
|
59
|
+
throw new Error("Invalid session key: dimension name must not be empty.");
|
|
60
|
+
}
|
|
61
|
+
if (value.trim().length === 0) {
|
|
62
|
+
throw new Error(`Invalid session key: dimension "${name}" must not be empty.`);
|
|
63
|
+
}
|
|
64
|
+
parsed.set(name, value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const required of REQUIRED_DIMENSIONS) {
|
|
68
|
+
if (!parsed.has(required)) {
|
|
69
|
+
throw new Error(`Invalid session key: missing required dimension "${required}".`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dimensions: Record<string, string> = {};
|
|
74
|
+
for (const [name, value] of parsed) {
|
|
75
|
+
if (!REQUIRED_DIMENSION_SET.has(name)) dimensions[name] = value;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
providerId: parsed.get("providerId") as string,
|
|
79
|
+
serviceAccountId: parsed.get("serviceAccountId") as string,
|
|
80
|
+
connectionId: parsed.get("connectionId") as string,
|
|
81
|
+
dimensions,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateRequiredPart(value: string, name: string): void {
|
|
86
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
87
|
+
throw new Error(`Cannot build session key: required dimension "${name}" is missing or empty.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateDimension(name: string, value: string): void {
|
|
92
|
+
if (name.trim().length === 0) {
|
|
93
|
+
throw new Error("Cannot build session key: extra dimension name must not be empty.");
|
|
94
|
+
}
|
|
95
|
+
if (REQUIRED_DIMENSION_SET.has(name)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Cannot build session key: extra dimension "${name}" duplicates a required dimension.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
101
|
+
throw new Error(`Cannot build session key: extra dimension "${name}" is missing or empty.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decodeSegment(value: string, label: string): string {
|
|
106
|
+
try {
|
|
107
|
+
return decodeURIComponent(value);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`Invalid session key: ${label} is not valid URI encoding.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StatefulProviderSessionContext } from "./stateful-provider-adapter.js";
|
|
2
|
+
import type { StatefulOperationRequest } from "./stateful-provider-session-routing.js";
|
|
3
|
+
import type { ManagedSession, SessionOwnerRecord } from "./stateful-provider-session-runtime.js";
|
|
4
|
+
|
|
5
|
+
export function makeStatefulProviderSessionContext(
|
|
6
|
+
providerId: string,
|
|
7
|
+
owner: SessionOwnerRecord,
|
|
8
|
+
request: StatefulOperationRequest,
|
|
9
|
+
signal: AbortSignal,
|
|
10
|
+
): StatefulProviderSessionContext {
|
|
11
|
+
const ctx: StatefulProviderSessionContext = {
|
|
12
|
+
sessionKey: owner.sessionKey,
|
|
13
|
+
providerId,
|
|
14
|
+
connectionId: request.connectionId,
|
|
15
|
+
serviceAccountId: request.serviceAccountId,
|
|
16
|
+
ownerPodId: owner.ownerPodId,
|
|
17
|
+
ownerEndpoint: owner.ownerEndpoint,
|
|
18
|
+
ownerStatus: owner.status,
|
|
19
|
+
generation: owner.generation,
|
|
20
|
+
signal,
|
|
21
|
+
requestId: request.requestId,
|
|
22
|
+
operationId: request.operationId,
|
|
23
|
+
...(request.runtimeContext !== undefined ? { runtimeContext: request.runtimeContext } : {}),
|
|
24
|
+
};
|
|
25
|
+
return withOptionalRequestMetadata(ctx, request);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function makeStatefulProviderCloseContext<TSession>(
|
|
29
|
+
providerId: string,
|
|
30
|
+
session: ManagedSession<TSession>,
|
|
31
|
+
): StatefulProviderSessionContext {
|
|
32
|
+
const identity = session.identity;
|
|
33
|
+
return {
|
|
34
|
+
sessionKey: session.sessionKey,
|
|
35
|
+
providerId,
|
|
36
|
+
...(identity
|
|
37
|
+
? {
|
|
38
|
+
connectionId: identity.connectionId,
|
|
39
|
+
serviceAccountId: identity.serviceAccountId,
|
|
40
|
+
ownerPodId: identity.ownerPodId,
|
|
41
|
+
ownerEndpoint: identity.ownerEndpoint,
|
|
42
|
+
ownerStatus: identity.ownerStatus,
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
generation: session.generation,
|
|
46
|
+
signal: new AbortController().signal,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withOptionalRequestMetadata(
|
|
51
|
+
ctx: StatefulProviderSessionContext,
|
|
52
|
+
request: StatefulOperationRequest,
|
|
53
|
+
): StatefulProviderSessionContext {
|
|
54
|
+
return {
|
|
55
|
+
...ctx,
|
|
56
|
+
...(request.idempotencyKey ? { idempotencyKey: request.idempotencyKey } : {}),
|
|
57
|
+
...(request.deadlineAt ? { deadlineAt: request.deadlineAt } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StatefulProviderMetricEmitter } from "./stateful-provider-observability.js";
|
|
2
|
+
import type { StatefulOperationRequest } from "./stateful-provider-session-routing.js";
|
|
3
|
+
import type { SessionOwnerRecord } from "./stateful-provider-session-runtime.js";
|
|
4
|
+
|
|
5
|
+
export function observeStatefulSessionOperationDuration(input: {
|
|
6
|
+
readonly metricEmitter: StatefulProviderMetricEmitter;
|
|
7
|
+
readonly request: StatefulOperationRequest;
|
|
8
|
+
readonly owner: SessionOwnerRecord;
|
|
9
|
+
readonly durationMs: number;
|
|
10
|
+
}): void {
|
|
11
|
+
input.metricEmitter.observe(
|
|
12
|
+
"apifuse_stateful_provider_session_operation_duration_ms",
|
|
13
|
+
statefulSessionMetricLabels(input.request, input.owner),
|
|
14
|
+
Math.max(0, input.durationMs),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emitStatefulSessionInvalidatedMetric(input: {
|
|
19
|
+
readonly metricEmitter: StatefulProviderMetricEmitter;
|
|
20
|
+
readonly request: StatefulOperationRequest;
|
|
21
|
+
readonly owner: SessionOwnerRecord;
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
}): void {
|
|
24
|
+
input.metricEmitter.increment(
|
|
25
|
+
"apifuse_stateful_provider_session_invalidations_total",
|
|
26
|
+
statefulSessionMetricLabels(input.request, input.owner, {
|
|
27
|
+
redactedErrorCode: input.reason,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statefulSessionMetricLabels(
|
|
33
|
+
request: StatefulOperationRequest,
|
|
34
|
+
owner: SessionOwnerRecord,
|
|
35
|
+
extra: { readonly redactedErrorCode?: string } = {},
|
|
36
|
+
) {
|
|
37
|
+
return {
|
|
38
|
+
requestId: request.requestId,
|
|
39
|
+
providerId: request.providerId,
|
|
40
|
+
connectionId: request.connectionId,
|
|
41
|
+
serviceAccountId: request.serviceAccountId,
|
|
42
|
+
operationId: request.operationId,
|
|
43
|
+
sessionKey: request.sessionKey,
|
|
44
|
+
ownerPodId: owner.ownerPodId,
|
|
45
|
+
generation: owner.generation,
|
|
46
|
+
...extra,
|
|
47
|
+
};
|
|
48
|
+
}
|