@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +106 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +510 -45
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +149 -8
- package/dist/config/loader.js +378 -89
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +8 -1
- package/dist/define.js +119 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +44 -42
- package/dist/index.js +39 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +144 -348
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +2 -2
- package/dist/runtime/stealth.js +108 -212
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +105 -4
- package/dist/server/serve.js +588 -80
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -7
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +207 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +1 -1
- package/dist/testing/run.js +7 -14
- package/dist/types.d.ts +87 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +14 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +247 -505
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +49 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +15 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +23 -20
- package/src/runtime/http.ts +195 -559
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +162 -377
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +24 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +859 -266
- package/src/server/types.ts +7 -19
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +279 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +19 -67
- package/src/types.ts +88 -1
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "../stateful-signing.js";
|
|
2
|
+
export * from "./errors.js";
|
|
3
|
+
export * from "./http-provider-event-emitter.js";
|
|
4
|
+
export * from "./http-session-owner-registry.js";
|
|
5
|
+
export * from "./provider-event-delivery-failures.js";
|
|
6
|
+
export * from "./provider-event-pipeline-metrics.js";
|
|
7
|
+
export * from "./provider-event-pipeline.js";
|
|
8
|
+
export * from "./provider-events.js";
|
|
9
|
+
export * from "./session-key.js";
|
|
10
|
+
export * from "./stateful-provider-adapter-context.js";
|
|
11
|
+
export * from "./stateful-provider-adapter-metrics.js";
|
|
12
|
+
export * from "./stateful-provider-adapter.js";
|
|
13
|
+
export * from "./stateful-provider-observability.js";
|
|
14
|
+
export * from "./stateful-provider-owner-forwarder.js";
|
|
15
|
+
export * from "./stateful-provider-runtime-context.js";
|
|
16
|
+
export * from "./stateful-provider-runtime-executor.js";
|
|
17
|
+
export * from "./stateful-provider-session-routing.js";
|
|
18
|
+
export * from "./stateful-provider-session-runtime.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ProviderWebhookFailureTotals = {
|
|
2
|
+
retryingCount: number;
|
|
3
|
+
failedCount: number;
|
|
4
|
+
deadLetteredCount: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function failureForStatus(statusCode: number): {
|
|
7
|
+
readonly statusCode: number;
|
|
8
|
+
readonly error?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function failureForError(error: unknown): {
|
|
11
|
+
readonly statusCode?: number;
|
|
12
|
+
readonly error: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function countProviderWebhookFailure(totals: ProviderWebhookFailureTotals, status: string): void;
|
|
15
|
+
export type ProviderEventDeliveryFailureReason = "buffer_overflow" | "attempts_exhausted";
|
|
16
|
+
export type ProviderEventDeliveryFailure = {
|
|
17
|
+
readonly eventId: string;
|
|
18
|
+
readonly reason: ProviderEventDeliveryFailureReason;
|
|
19
|
+
readonly attempts: number;
|
|
20
|
+
readonly failedAt: string;
|
|
21
|
+
readonly error?: string;
|
|
22
|
+
};
|
|
23
|
+
export interface ProviderEventDeliveryFailureRecorder {
|
|
24
|
+
record(failure: ProviderEventDeliveryFailure): void | Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare const NOOP_PROVIDER_EVENT_DELIVERY_FAILURE_RECORDER: {
|
|
27
|
+
record(): void;
|
|
28
|
+
};
|
|
29
|
+
export declare class RecordingProviderEventDeliveryFailureRecorder implements ProviderEventDeliveryFailureRecorder {
|
|
30
|
+
readonly failures: ProviderEventDeliveryFailure[];
|
|
31
|
+
record(failure: ProviderEventDeliveryFailure): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function failureForStatus(statusCode) {
|
|
2
|
+
if (statusCode >= 300 && statusCode < 400) {
|
|
3
|
+
return { statusCode, error: "Webhook redirects are not followed." };
|
|
4
|
+
}
|
|
5
|
+
return { statusCode };
|
|
6
|
+
}
|
|
7
|
+
export function failureForError(error) {
|
|
8
|
+
if (isPublicValidationError(error)) {
|
|
9
|
+
return { statusCode: 400, error: error.message };
|
|
10
|
+
}
|
|
11
|
+
return { error: publicWorkerError(error) };
|
|
12
|
+
}
|
|
13
|
+
export function countProviderWebhookFailure(totals, status) {
|
|
14
|
+
if (status === "retrying") {
|
|
15
|
+
totals.retryingCount += 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (status === "dead_lettered") {
|
|
19
|
+
totals.deadLetteredCount += 1;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
totals.failedCount += 1;
|
|
23
|
+
}
|
|
24
|
+
export const NOOP_PROVIDER_EVENT_DELIVERY_FAILURE_RECORDER = {
|
|
25
|
+
record() { },
|
|
26
|
+
};
|
|
27
|
+
export class RecordingProviderEventDeliveryFailureRecorder {
|
|
28
|
+
failures = [];
|
|
29
|
+
record(failure) {
|
|
30
|
+
this.failures.push(failure);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function publicWorkerError(error) {
|
|
34
|
+
if (isPublicValidationError(error))
|
|
35
|
+
return error.message;
|
|
36
|
+
if (error instanceof Error && error.message) {
|
|
37
|
+
return "Webhook delivery request failed.";
|
|
38
|
+
}
|
|
39
|
+
return "Webhook delivery failed.";
|
|
40
|
+
}
|
|
41
|
+
function isPublicValidationError(error) {
|
|
42
|
+
return error instanceof Error && error.name.endsWith("ValidationError");
|
|
43
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProviderEvent } from "./provider-events.js";
|
|
2
|
+
export declare const PROVIDER_EVENT_METRIC_NAMES: readonly ["apifuse_stateful_provider_event_append_total", "apifuse_stateful_provider_event_dedupe_total", "apifuse_stateful_provider_event_drop_total", "apifuse_stateful_provider_webhook_enqueue_total", "apifuse_stateful_provider_webhook_fanout_recovered_total", "apifuse_stateful_provider_webhook_delivered_total", "apifuse_stateful_provider_webhook_retry_total", "apifuse_stateful_provider_webhook_dead_letter_total", "apifuse_stateful_provider_webhook_canceled_total", "apifuse_stateful_provider_webhook_latency_ms"];
|
|
3
|
+
export type ProviderEventMetricName = (typeof PROVIDER_EVENT_METRIC_NAMES)[number];
|
|
4
|
+
export type ProviderEventMetricLabelInput = {
|
|
5
|
+
readonly eventId?: string;
|
|
6
|
+
readonly deliveryId?: string;
|
|
7
|
+
readonly providerId?: string;
|
|
8
|
+
readonly connectionId?: string;
|
|
9
|
+
readonly serviceAccountId?: string;
|
|
10
|
+
readonly eventType?: string;
|
|
11
|
+
readonly subjectKind?: string;
|
|
12
|
+
readonly subjectId?: string;
|
|
13
|
+
readonly sessionKey?: string;
|
|
14
|
+
readonly generation?: number | string;
|
|
15
|
+
};
|
|
16
|
+
export interface ProviderEventMetricEmitter {
|
|
17
|
+
increment(name: ProviderEventMetricName, labels?: ProviderEventMetricLabelInput, value?: number): void;
|
|
18
|
+
observe(name: ProviderEventMetricName, labels: ProviderEventMetricLabelInput, value: number): void;
|
|
19
|
+
}
|
|
20
|
+
export declare const NOOP_PROVIDER_EVENT_METRIC_EMITTER: {
|
|
21
|
+
increment(): void;
|
|
22
|
+
observe(): void;
|
|
23
|
+
};
|
|
24
|
+
export declare function providerEventMetricLabels(event: ProviderEvent): {
|
|
25
|
+
eventId: string;
|
|
26
|
+
providerId: string;
|
|
27
|
+
connectionId: string | undefined;
|
|
28
|
+
serviceAccountId: string | undefined;
|
|
29
|
+
eventType: string;
|
|
30
|
+
subjectKind: string | undefined;
|
|
31
|
+
subjectId: string | undefined;
|
|
32
|
+
sessionKey: string;
|
|
33
|
+
generation: number;
|
|
34
|
+
};
|
|
35
|
+
export declare function providerWebhookDeliveryMetricLabels(delivery: {
|
|
36
|
+
readonly deliveryId: string;
|
|
37
|
+
}): {
|
|
38
|
+
deliveryId: string;
|
|
39
|
+
};
|
|
40
|
+
export declare function emitProviderWebhookDeliveryFailureMetric(metricEmitter: ProviderEventMetricEmitter, delivery: {
|
|
41
|
+
readonly deliveryId: string;
|
|
42
|
+
}, status: string): void;
|
|
43
|
+
export declare function providerWebhookDeliveryLatencyMs(delivery: {
|
|
44
|
+
readonly lastAttemptAt?: string;
|
|
45
|
+
readonly nextAttemptAt: string;
|
|
46
|
+
}, now: string): number;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const PROVIDER_EVENT_METRIC_NAMES = [
|
|
2
|
+
"apifuse_stateful_provider_event_append_total",
|
|
3
|
+
"apifuse_stateful_provider_event_dedupe_total",
|
|
4
|
+
"apifuse_stateful_provider_event_drop_total",
|
|
5
|
+
"apifuse_stateful_provider_webhook_enqueue_total",
|
|
6
|
+
"apifuse_stateful_provider_webhook_fanout_recovered_total",
|
|
7
|
+
"apifuse_stateful_provider_webhook_delivered_total",
|
|
8
|
+
"apifuse_stateful_provider_webhook_retry_total",
|
|
9
|
+
"apifuse_stateful_provider_webhook_dead_letter_total",
|
|
10
|
+
"apifuse_stateful_provider_webhook_canceled_total",
|
|
11
|
+
"apifuse_stateful_provider_webhook_latency_ms",
|
|
12
|
+
];
|
|
13
|
+
export const NOOP_PROVIDER_EVENT_METRIC_EMITTER = {
|
|
14
|
+
increment() { },
|
|
15
|
+
observe() { },
|
|
16
|
+
};
|
|
17
|
+
export function providerEventMetricLabels(event) {
|
|
18
|
+
return {
|
|
19
|
+
eventId: event.eventId,
|
|
20
|
+
providerId: event.providerId,
|
|
21
|
+
connectionId: event.connectionId,
|
|
22
|
+
serviceAccountId: event.serviceAccountId,
|
|
23
|
+
eventType: event.eventType,
|
|
24
|
+
subjectKind: event.subject?.kind,
|
|
25
|
+
subjectId: event.subject?.id,
|
|
26
|
+
sessionKey: event.session.sessionKey,
|
|
27
|
+
generation: event.session.generation,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function providerWebhookDeliveryMetricLabels(delivery) {
|
|
31
|
+
return { deliveryId: delivery.deliveryId };
|
|
32
|
+
}
|
|
33
|
+
export function emitProviderWebhookDeliveryFailureMetric(metricEmitter, delivery, status) {
|
|
34
|
+
if (status === "retrying") {
|
|
35
|
+
metricEmitter.increment("apifuse_stateful_provider_webhook_retry_total", providerWebhookDeliveryMetricLabels(delivery));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (status === "dead_lettered") {
|
|
39
|
+
metricEmitter.increment("apifuse_stateful_provider_webhook_dead_letter_total", providerWebhookDeliveryMetricLabels(delivery));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function providerWebhookDeliveryLatencyMs(delivery, now) {
|
|
43
|
+
const startedAt = Date.parse(delivery.lastAttemptAt ?? delivery.nextAttemptAt);
|
|
44
|
+
const completedAt = Date.parse(now);
|
|
45
|
+
if (!Number.isFinite(startedAt) || !Number.isFinite(completedAt))
|
|
46
|
+
return 0;
|
|
47
|
+
return Math.max(0, completedAt - startedAt);
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ProviderEventMetricEmitter } from "./provider-event-pipeline-metrics.js";
|
|
2
|
+
import type { ProviderEvent } from "./provider-events.js";
|
|
3
|
+
import type { SessionKey } from "./session-key.js";
|
|
4
|
+
export type { ProviderEventMetricEmitter } from "./provider-event-pipeline-metrics.js";
|
|
5
|
+
export type ProviderEventOwnerFence = {
|
|
6
|
+
readonly ownerPodId: string;
|
|
7
|
+
readonly ownerEndpoint: string;
|
|
8
|
+
readonly sessionKey: SessionKey;
|
|
9
|
+
readonly generation: number;
|
|
10
|
+
};
|
|
11
|
+
export type ProviderEventPublishOptions = {
|
|
12
|
+
/** Required ownership proof forwarded to the durable platform write boundary. */
|
|
13
|
+
readonly ownerFence: ProviderEventOwnerFence;
|
|
14
|
+
/** Overrides the transport idempotency key; defaults to the event id. */
|
|
15
|
+
readonly idempotencyKey?: string;
|
|
16
|
+
};
|
|
17
|
+
export type PublishAck = {
|
|
18
|
+
/** Whether this event was accepted into the pod-local retry buffer. */
|
|
19
|
+
readonly accepted: boolean;
|
|
20
|
+
/** Number of events currently held in the pod-local retry buffer. */
|
|
21
|
+
readonly queued: number;
|
|
22
|
+
};
|
|
23
|
+
/** Fire-and-forget publication from a provider pod into the platform ingest boundary. */
|
|
24
|
+
export interface ProviderEventPublisher {
|
|
25
|
+
publish(event: ProviderEvent, options: ProviderEventPublishOptions): PublishAck;
|
|
26
|
+
}
|
|
27
|
+
export type AppendProviderEventAndEnqueueWebhooksResult = {
|
|
28
|
+
readonly appended: boolean;
|
|
29
|
+
readonly appendedCount: number;
|
|
30
|
+
readonly matchedSubscriptionCount: number;
|
|
31
|
+
readonly enqueuedCount: number;
|
|
32
|
+
readonly recoveredFanoutCount?: number;
|
|
33
|
+
readonly fenced?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type ProviderEventPipelineAppendOptions = {
|
|
36
|
+
readonly now?: string;
|
|
37
|
+
readonly idFactory?: () => string;
|
|
38
|
+
readonly metricEmitter?: ProviderEventMetricEmitter;
|
|
39
|
+
readonly beforeAppend?: () => Promise<boolean> | boolean;
|
|
40
|
+
readonly ownerFence?: ProviderEventOwnerFence;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Durable append-and-fanout contract implemented on the platform side.
|
|
44
|
+
*
|
|
45
|
+
* Provider-pod code must depend on ProviderEventPublisher, never ProviderEventPipeline: a pod-local
|
|
46
|
+
* publisher cannot truthfully report a durable append or downstream webhook fanout.
|
|
47
|
+
*/
|
|
48
|
+
export interface ProviderEventPipeline {
|
|
49
|
+
appendAndFanout(event: ProviderEvent, options?: ProviderEventPipelineAppendOptions): Promise<AppendProviderEventAndEnqueueWebhooksResult>;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type ProviderEventSubject = {
|
|
2
|
+
/** Provider-defined resource category, for example `message`, `mailbox`, or `browser-context`. */
|
|
3
|
+
kind: string;
|
|
4
|
+
id: string;
|
|
5
|
+
};
|
|
6
|
+
export type ProviderEventSession = {
|
|
7
|
+
/** Canonical logical session identity shared by every stateful provider. */
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
/** Ownership generation shared by every stateful provider for stale-event fencing. */
|
|
10
|
+
generation: number;
|
|
11
|
+
};
|
|
12
|
+
export interface ProviderEvent<TPayload = Record<string, unknown>> {
|
|
13
|
+
/** Universal stable identity used for transport retries and deduplication. */
|
|
14
|
+
eventId: string;
|
|
15
|
+
/** Universal provider namespace needed to route and interpret the event. */
|
|
16
|
+
providerId: string;
|
|
17
|
+
/** Optional because mailbox/browser protocols may not expose an API connection identity. */
|
|
18
|
+
connectionId?: string;
|
|
19
|
+
/** Optional because not every provider session maps to a platform service account. */
|
|
20
|
+
serviceAccountId?: string;
|
|
21
|
+
/** Universal provider-defined event classification. */
|
|
22
|
+
eventType: string;
|
|
23
|
+
/** Optional for protocol-level events without a single resource subject. */
|
|
24
|
+
subject?: ProviderEventSubject;
|
|
25
|
+
/** Optional when the upstream protocol does not provide an occurrence timestamp. */
|
|
26
|
+
occurredAt?: string;
|
|
27
|
+
/** Universal SDK observation time used for ordering and lag measurement. */
|
|
28
|
+
observedAt: string;
|
|
29
|
+
/** Universal JSON data envelope; protocols without data use an empty object. */
|
|
30
|
+
payload: TPayload;
|
|
31
|
+
/** Universal stateful ownership fence for rejecting events from stale sessions. */
|
|
32
|
+
session: ProviderEventSession;
|
|
33
|
+
providerCursor?: string;
|
|
34
|
+
dedupeKey?: string;
|
|
35
|
+
rawRef?: string;
|
|
36
|
+
}
|
|
37
|
+
export type ProviderEventIdFactory = () => string;
|
|
38
|
+
export type ProviderEventClock = () => Date;
|
|
39
|
+
export type BuildProviderEventInput<TPayload extends Record<string, unknown> = Record<string, unknown>> = Omit<ProviderEvent<TPayload>, "eventId" | "observedAt"> & {
|
|
40
|
+
eventId?: string;
|
|
41
|
+
observedAt?: string;
|
|
42
|
+
clock?: ProviderEventClock;
|
|
43
|
+
idFactory?: ProviderEventIdFactory;
|
|
44
|
+
maxPayloadBytes?: number;
|
|
45
|
+
shouldRedactPayload?: boolean;
|
|
46
|
+
/** Provider-specific credential key patterns added to the conservative SDK defaults. */
|
|
47
|
+
redactionPatterns?: readonly RegExp[];
|
|
48
|
+
};
|
|
49
|
+
export type ProviderEventListOptions = {
|
|
50
|
+
providerId?: string;
|
|
51
|
+
connectionId?: string;
|
|
52
|
+
eventType?: string;
|
|
53
|
+
subjectKind?: string;
|
|
54
|
+
subjectId?: string;
|
|
55
|
+
afterObservedAt?: string;
|
|
56
|
+
limit?: number;
|
|
57
|
+
};
|
|
58
|
+
export type ProviderEventAppendResult = {
|
|
59
|
+
event: ProviderEvent;
|
|
60
|
+
appended: boolean;
|
|
61
|
+
};
|
|
62
|
+
export interface ProviderEventLog {
|
|
63
|
+
append(event: ProviderEvent): Promise<ProviderEventAppendResult>;
|
|
64
|
+
get(eventId: string): Promise<ProviderEvent | null>;
|
|
65
|
+
list(options?: ProviderEventListOptions): Promise<ProviderEvent[]>;
|
|
66
|
+
}
|
|
67
|
+
export type ProviderEventEmitterDefaults = {
|
|
68
|
+
providerId: string;
|
|
69
|
+
connectionId?: string;
|
|
70
|
+
serviceAccountId?: string;
|
|
71
|
+
session: ProviderEventSession;
|
|
72
|
+
clock?: ProviderEventClock;
|
|
73
|
+
idFactory?: ProviderEventIdFactory;
|
|
74
|
+
maxPayloadBytes?: number;
|
|
75
|
+
shouldRedactPayload?: boolean;
|
|
76
|
+
redactionPatterns?: readonly RegExp[];
|
|
77
|
+
};
|
|
78
|
+
export type ProviderEventEmitterInput<TPayload extends Record<string, unknown> = Record<string, unknown>> = Omit<BuildProviderEventInput<TPayload>, "providerId" | "connectionId" | "serviceAccountId" | "session"> & {
|
|
79
|
+
providerId?: string;
|
|
80
|
+
connectionId?: string;
|
|
81
|
+
serviceAccountId?: string;
|
|
82
|
+
session?: ProviderEventSession;
|
|
83
|
+
};
|
|
84
|
+
export declare class ProviderEventValidationError extends Error {
|
|
85
|
+
constructor(message: string);
|
|
86
|
+
}
|
|
87
|
+
export declare function buildProviderEvent<TPayload extends Record<string, unknown> = Record<string, unknown>>(input: BuildProviderEventInput<TPayload>): ProviderEvent<TPayload>;
|
|
88
|
+
export declare function redactProviderEventPayload<TPayload>(payload: TPayload, extraPatterns?: readonly RegExp[]): TPayload;
|
|
89
|
+
export declare function isStaleProviderEvent(event: ProviderEvent, currentGeneration: number): boolean;
|
|
90
|
+
export declare class InMemoryProviderEventLog implements ProviderEventLog {
|
|
91
|
+
#private;
|
|
92
|
+
append(event: ProviderEvent): Promise<ProviderEventAppendResult>;
|
|
93
|
+
get(eventId: string): Promise<ProviderEvent | null>;
|
|
94
|
+
list(options?: ProviderEventListOptions): Promise<ProviderEvent[]>;
|
|
95
|
+
}
|
|
96
|
+
export declare class ProviderEventEmitter {
|
|
97
|
+
#private;
|
|
98
|
+
constructor(log: ProviderEventLog, defaults: ProviderEventEmitterDefaults);
|
|
99
|
+
emit<TPayload extends Record<string, unknown> = Record<string, unknown>>(input: ProviderEventEmitterInput<TPayload>): Promise<ProviderEventAppendResult>;
|
|
100
|
+
}
|
|
101
|
+
export declare function randomProviderEventId(): string;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
const DEFAULT_MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
3
|
+
const REDACTED_VALUE = "[REDACTED]";
|
|
4
|
+
const CREDENTIAL_KEY_PATTERN = /(?:token|password|secret|credential|cookie|authorization)/i;
|
|
5
|
+
export class ProviderEventValidationError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ProviderEventValidationError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function buildProviderEvent(input) {
|
|
12
|
+
validatePayload(input.payload, input.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES);
|
|
13
|
+
const observedAt = input.observedAt ?? input.clock?.().toISOString() ?? new Date().toISOString();
|
|
14
|
+
const payload = input.shouldRedactPayload === false
|
|
15
|
+
? input.payload
|
|
16
|
+
: redactProviderEventPayload(input.payload, input.redactionPatterns);
|
|
17
|
+
const event = {
|
|
18
|
+
eventId: input.eventId ?? buildEventId({ ...input, observedAt }),
|
|
19
|
+
providerId: input.providerId,
|
|
20
|
+
eventType: input.eventType,
|
|
21
|
+
observedAt,
|
|
22
|
+
payload,
|
|
23
|
+
session: input.session,
|
|
24
|
+
...(input.connectionId ? { connectionId: input.connectionId } : {}),
|
|
25
|
+
...(input.serviceAccountId ? { serviceAccountId: input.serviceAccountId } : {}),
|
|
26
|
+
...(input.subject ? { subject: input.subject } : {}),
|
|
27
|
+
...(input.occurredAt ? { occurredAt: input.occurredAt } : {}),
|
|
28
|
+
...(input.providerCursor ? { providerCursor: input.providerCursor } : {}),
|
|
29
|
+
...(input.dedupeKey ? { dedupeKey: input.dedupeKey } : {}),
|
|
30
|
+
...(input.rawRef ? { rawRef: input.rawRef } : {}),
|
|
31
|
+
};
|
|
32
|
+
validateProviderEvent(event, input.maxPayloadBytes);
|
|
33
|
+
return event;
|
|
34
|
+
}
|
|
35
|
+
export function redactProviderEventPayload(payload, extraPatterns = []) {
|
|
36
|
+
const cloned = structuredClone(payload);
|
|
37
|
+
redactValueInPlace(cloned, [CREDENTIAL_KEY_PATTERN, ...extraPatterns]);
|
|
38
|
+
return cloned;
|
|
39
|
+
}
|
|
40
|
+
export function isStaleProviderEvent(event, currentGeneration) {
|
|
41
|
+
return event.session.generation < currentGeneration;
|
|
42
|
+
}
|
|
43
|
+
export class InMemoryProviderEventLog {
|
|
44
|
+
#events = [];
|
|
45
|
+
#eventIds = new Set();
|
|
46
|
+
#dedupeKeys = new Set();
|
|
47
|
+
async append(event) {
|
|
48
|
+
validateProviderEvent(event);
|
|
49
|
+
if (this.#eventIds.has(event.eventId)) {
|
|
50
|
+
return { event, appended: false };
|
|
51
|
+
}
|
|
52
|
+
const dedupeTuple = event.dedupeKey ? buildDedupeTuple(event) : undefined;
|
|
53
|
+
if (dedupeTuple && this.#dedupeKeys.has(dedupeTuple)) {
|
|
54
|
+
return { event, appended: false };
|
|
55
|
+
}
|
|
56
|
+
this.#events.push(event);
|
|
57
|
+
this.#eventIds.add(event.eventId);
|
|
58
|
+
if (dedupeTuple)
|
|
59
|
+
this.#dedupeKeys.add(dedupeTuple);
|
|
60
|
+
return { event, appended: true };
|
|
61
|
+
}
|
|
62
|
+
async get(eventId) {
|
|
63
|
+
validateRequiredString("eventId", eventId);
|
|
64
|
+
return this.#events.find((event) => event.eventId === eventId) ?? null;
|
|
65
|
+
}
|
|
66
|
+
async list(options = {}) {
|
|
67
|
+
validateListOptions(options);
|
|
68
|
+
let events = this.#events;
|
|
69
|
+
if (options.providerId) {
|
|
70
|
+
events = events.filter((event) => event.providerId === options.providerId);
|
|
71
|
+
}
|
|
72
|
+
if (options.connectionId) {
|
|
73
|
+
events = events.filter((event) => event.connectionId === options.connectionId);
|
|
74
|
+
}
|
|
75
|
+
if (options.eventType) {
|
|
76
|
+
events = events.filter((event) => event.eventType === options.eventType);
|
|
77
|
+
}
|
|
78
|
+
if (options.subjectKind) {
|
|
79
|
+
events = events.filter((event) => event.subject?.kind === options.subjectKind);
|
|
80
|
+
}
|
|
81
|
+
if (options.subjectId) {
|
|
82
|
+
events = events.filter((event) => event.subject?.id === options.subjectId);
|
|
83
|
+
}
|
|
84
|
+
if (options.afterObservedAt) {
|
|
85
|
+
const afterMs = Date.parse(options.afterObservedAt);
|
|
86
|
+
events = events.filter((event) => Date.parse(event.observedAt) > afterMs);
|
|
87
|
+
}
|
|
88
|
+
return typeof options.limit === "number" ? events.slice(0, options.limit) : [...events];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export class ProviderEventEmitter {
|
|
92
|
+
#log;
|
|
93
|
+
#defaults;
|
|
94
|
+
constructor(log, defaults) {
|
|
95
|
+
validateRequiredString("providerId", defaults.providerId);
|
|
96
|
+
if (defaults.connectionId !== undefined) {
|
|
97
|
+
validateRequiredString("connectionId", defaults.connectionId);
|
|
98
|
+
}
|
|
99
|
+
if (defaults.serviceAccountId !== undefined) {
|
|
100
|
+
validateRequiredString("serviceAccountId", defaults.serviceAccountId);
|
|
101
|
+
}
|
|
102
|
+
validateSession(defaults.session);
|
|
103
|
+
this.#log = log;
|
|
104
|
+
this.#defaults = defaults;
|
|
105
|
+
}
|
|
106
|
+
emit(input) {
|
|
107
|
+
const event = buildProviderEvent({
|
|
108
|
+
...input,
|
|
109
|
+
providerId: input.providerId ?? this.#defaults.providerId,
|
|
110
|
+
connectionId: input.connectionId ?? this.#defaults.connectionId,
|
|
111
|
+
serviceAccountId: input.serviceAccountId ?? this.#defaults.serviceAccountId,
|
|
112
|
+
session: input.session ?? this.#defaults.session,
|
|
113
|
+
clock: input.clock ?? this.#defaults.clock,
|
|
114
|
+
idFactory: input.idFactory ?? this.#defaults.idFactory,
|
|
115
|
+
maxPayloadBytes: input.maxPayloadBytes ?? this.#defaults.maxPayloadBytes,
|
|
116
|
+
shouldRedactPayload: input.shouldRedactPayload ?? this.#defaults.shouldRedactPayload,
|
|
117
|
+
redactionPatterns: input.redactionPatterns ?? this.#defaults.redactionPatterns,
|
|
118
|
+
});
|
|
119
|
+
return this.#log.append(event);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function buildEventId(input) {
|
|
123
|
+
if (input.dedupeKey) {
|
|
124
|
+
return deterministicProviderEventId([
|
|
125
|
+
input.providerId,
|
|
126
|
+
input.connectionId ?? "",
|
|
127
|
+
input.eventType,
|
|
128
|
+
input.dedupeKey,
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
return (input.idFactory?.() ??
|
|
132
|
+
deterministicProviderEventId([
|
|
133
|
+
input.providerId,
|
|
134
|
+
input.connectionId ?? "",
|
|
135
|
+
input.eventType,
|
|
136
|
+
input.subject?.kind ?? "",
|
|
137
|
+
input.subject?.id ?? "",
|
|
138
|
+
input.observedAt,
|
|
139
|
+
]));
|
|
140
|
+
}
|
|
141
|
+
function deterministicProviderEventId(parts) {
|
|
142
|
+
return `pevt_${createHash("sha256").update(parts.join("\u001f")).digest("hex")}`;
|
|
143
|
+
}
|
|
144
|
+
function validateProviderEvent(event, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES) {
|
|
145
|
+
validateRequiredString("eventId", event.eventId);
|
|
146
|
+
validateRequiredString("providerId", event.providerId);
|
|
147
|
+
if (event.connectionId !== undefined)
|
|
148
|
+
validateRequiredString("connectionId", event.connectionId);
|
|
149
|
+
if (event.serviceAccountId !== undefined) {
|
|
150
|
+
validateRequiredString("serviceAccountId", event.serviceAccountId);
|
|
151
|
+
}
|
|
152
|
+
validateRequiredString("eventType", event.eventType);
|
|
153
|
+
if (event.subject) {
|
|
154
|
+
validateRequiredString("subject.kind", event.subject.kind);
|
|
155
|
+
validateRequiredString("subject.id", event.subject.id);
|
|
156
|
+
}
|
|
157
|
+
if (event.occurredAt !== undefined) {
|
|
158
|
+
validateRequiredString("occurredAt", event.occurredAt);
|
|
159
|
+
validateTimestamp("occurredAt", event.occurredAt);
|
|
160
|
+
}
|
|
161
|
+
validateRequiredString("observedAt", event.observedAt);
|
|
162
|
+
validateTimestamp("observedAt", event.observedAt);
|
|
163
|
+
validateSession(event.session);
|
|
164
|
+
if (event.providerCursor !== undefined) {
|
|
165
|
+
validateRequiredString("providerCursor", event.providerCursor);
|
|
166
|
+
}
|
|
167
|
+
if (event.dedupeKey !== undefined) {
|
|
168
|
+
validateRequiredString("dedupeKey", event.dedupeKey);
|
|
169
|
+
}
|
|
170
|
+
if (event.rawRef !== undefined) {
|
|
171
|
+
validateRequiredString("rawRef", event.rawRef);
|
|
172
|
+
}
|
|
173
|
+
validatePayload(event.payload, maxPayloadBytes);
|
|
174
|
+
}
|
|
175
|
+
function validateSession(session) {
|
|
176
|
+
validateRequiredString("session.sessionKey", session?.sessionKey);
|
|
177
|
+
if (!Number.isInteger(session?.generation) || session.generation < 0) {
|
|
178
|
+
throw new ProviderEventValidationError("ProviderEvent session.generation must be a non-negative integer.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function validateRequiredString(name, value) {
|
|
182
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
183
|
+
throw new ProviderEventValidationError(`ProviderEvent ${name} must be a non-empty string.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateTimestamp(name, value) {
|
|
187
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
188
|
+
throw new ProviderEventValidationError(`ProviderEvent ${name} must be a parseable ISO timestamp string.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function validatePayload(payload, maxPayloadBytes) {
|
|
192
|
+
if (typeof maxPayloadBytes !== "number" ||
|
|
193
|
+
!Number.isFinite(maxPayloadBytes) ||
|
|
194
|
+
maxPayloadBytes <= 0) {
|
|
195
|
+
throw new ProviderEventValidationError("ProviderEvent maxPayloadBytes must be a positive finite number.");
|
|
196
|
+
}
|
|
197
|
+
if (!isPlainRecord(payload)) {
|
|
198
|
+
throw new ProviderEventValidationError("ProviderEvent payload must be a JSON-serializable object.");
|
|
199
|
+
}
|
|
200
|
+
assertJsonValue(payload, "payload");
|
|
201
|
+
const encoded = JSON.stringify(payload);
|
|
202
|
+
if (encoded === undefined) {
|
|
203
|
+
throw new ProviderEventValidationError("ProviderEvent payload must be JSON-serializable.");
|
|
204
|
+
}
|
|
205
|
+
const payloadBytes = Buffer.byteLength(encoded, "utf8");
|
|
206
|
+
if (payloadBytes > maxPayloadBytes) {
|
|
207
|
+
throw new ProviderEventValidationError(`ProviderEvent payload exceeds max size of ${maxPayloadBytes} bytes.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function assertJsonValue(value, path, seen = new WeakSet()) {
|
|
211
|
+
if (value === null)
|
|
212
|
+
return;
|
|
213
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
214
|
+
return;
|
|
215
|
+
if (typeof value === "number") {
|
|
216
|
+
if (Number.isFinite(value))
|
|
217
|
+
return;
|
|
218
|
+
throw new ProviderEventValidationError(`ProviderEvent ${path} must not contain non-finite numbers.`);
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
if (seen.has(value)) {
|
|
222
|
+
throw new ProviderEventValidationError(`ProviderEvent ${path} must not contain circular references.`);
|
|
223
|
+
}
|
|
224
|
+
seen.add(value);
|
|
225
|
+
for (const [index, entry] of value.entries()) {
|
|
226
|
+
assertJsonValue(entry, `${path}[${index}]`, seen);
|
|
227
|
+
}
|
|
228
|
+
seen.delete(value);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (isPlainRecord(value)) {
|
|
232
|
+
if (seen.has(value)) {
|
|
233
|
+
throw new ProviderEventValidationError(`ProviderEvent ${path} must not contain circular references.`);
|
|
234
|
+
}
|
|
235
|
+
seen.add(value);
|
|
236
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
237
|
+
assertJsonValue(entry, `${path}.${key}`, seen);
|
|
238
|
+
}
|
|
239
|
+
seen.delete(value);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw new ProviderEventValidationError(`ProviderEvent ${path} must be JSON-serializable.`);
|
|
243
|
+
}
|
|
244
|
+
function redactValueInPlace(value, patterns) {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
for (const entry of value)
|
|
247
|
+
redactValueInPlace(entry, patterns);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!isPlainRecord(value))
|
|
251
|
+
return;
|
|
252
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
253
|
+
if (patterns.some((pattern) => matchesPattern(pattern, key)))
|
|
254
|
+
value[key] = REDACTED_VALUE;
|
|
255
|
+
else
|
|
256
|
+
redactValueInPlace(entry, patterns);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function isPlainRecord(value) {
|
|
260
|
+
return (typeof value === "object" &&
|
|
261
|
+
value !== null &&
|
|
262
|
+
!Array.isArray(value) &&
|
|
263
|
+
Object.getPrototypeOf(value) === Object.prototype);
|
|
264
|
+
}
|
|
265
|
+
function buildDedupeTuple(event) {
|
|
266
|
+
return [
|
|
267
|
+
event.serviceAccountId ?? "",
|
|
268
|
+
event.providerId,
|
|
269
|
+
event.connectionId ?? "",
|
|
270
|
+
event.dedupeKey,
|
|
271
|
+
].join("\u001f");
|
|
272
|
+
}
|
|
273
|
+
function matchesPattern(pattern, value) {
|
|
274
|
+
pattern.lastIndex = 0;
|
|
275
|
+
const matched = pattern.test(value);
|
|
276
|
+
pattern.lastIndex = 0;
|
|
277
|
+
return matched;
|
|
278
|
+
}
|
|
279
|
+
function validateListOptions(options) {
|
|
280
|
+
if (options.afterObservedAt) {
|
|
281
|
+
validateTimestamp("afterObservedAt", options.afterObservedAt);
|
|
282
|
+
}
|
|
283
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
|
|
284
|
+
throw new ProviderEventValidationError("ProviderEvent list limit must be a positive integer.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export function randomProviderEventId() {
|
|
288
|
+
return `pevt_${randomUUID().replaceAll("-", "")}`;
|
|
289
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const SESSION_KEY_BRAND: unique symbol;
|
|
2
|
+
/** Canonical, opaque identity for a stateful provider session. */
|
|
3
|
+
export type SessionKey = string & {
|
|
4
|
+
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
|
+
export declare function buildSessionKey(parts: SessionKeyParts): SessionKey;
|
|
14
|
+
export declare function parseSessionKey(key: string): SessionKeyParts;
|
|
15
|
+
export {};
|