@sguild/dispatcher 2.0.0

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.
Files changed (66) hide show
  1. package/README.md +354 -0
  2. package/contracts/credit-reservation-funding-state/schema/payloads/reservation.funded-v1.json +59 -0
  3. package/contracts/credit-reservation-funding-state/schema/payloads/reservation.refunded-v1.json +80 -0
  4. package/contracts/credit-reservation-funding-state/schema/payloads/reservation.refunding-v1.json +74 -0
  5. package/contracts/credit-reservation-lock/schema/payloads/credit.consumed-v1.json +33 -0
  6. package/contracts/credit-reservation-lock/schema/payloads/credit.forfeited-v1.json +41 -0
  7. package/contracts/credit-reservation-lock/schema/payloads/credit.funded-v1.json +31 -0
  8. package/contracts/credit-reservation-lock/schema/payloads/credit.locked-v1.json +42 -0
  9. package/contracts/credit-reservation-lock/schema/payloads/credit.purchased-v1.json +39 -0
  10. package/contracts/credit-reservation-lock/schema/payloads/credit.released-v1.json +61 -0
  11. package/contracts/credit-reservation-lock/schema/payloads/credit.released-v2.json +77 -0
  12. package/contracts/credit-reservation-lock/schema/payloads/credit.reserved-v1.json +60 -0
  13. package/contracts/credit-reservation-lock/schema/payloads/customer.handoff-v1.json +35 -0
  14. package/contracts/event-envelope/schema/envelope-v1.json +79 -0
  15. package/contracts/event-types-registry.json +541 -0
  16. package/contracts/identity/schema/payloads/intake.amended-v1.json +124 -0
  17. package/contracts/identity/schema/payloads/intake.captured-v2.json +114 -0
  18. package/contracts/identity/schema/payloads/person.updated-v1.json +99 -0
  19. package/contracts/lead-lifecycle/schema/payloads/lead.attempt.exhausted-v1.json +36 -0
  20. package/contracts/lead-lifecycle/schema/payloads/lead.callback.scheduled-v1.json +39 -0
  21. package/contracts/lead-lifecycle/schema/payloads/lead.created-v1.json +50 -0
  22. package/contracts/lead-lifecycle/schema/payloads/lead.reached-v1.json +39 -0
  23. package/contracts/lead-lifecycle/schema/payloads/lead.stage.changed-v1.json +44 -0
  24. package/contracts/payment-flow/schema/payloads/payment.failed-v1.json +88 -0
  25. package/contracts/payment-flow/schema/payloads/payment.received-v1.json +69 -0
  26. package/contracts/refund-flow/schema/payloads/refund.completed-v1.json +75 -0
  27. package/contracts/refund-flow/schema/payloads/refund.initiated-v1.json +69 -0
  28. package/dist/config.d.ts +67 -0
  29. package/dist/config.js +81 -0
  30. package/dist/dispatcher-errors.d.ts +20 -0
  31. package/dist/dispatcher-errors.js +42 -0
  32. package/dist/dispatcher.d.ts +123 -0
  33. package/dist/dispatcher.js +171 -0
  34. package/dist/dlq.d.ts +173 -0
  35. package/dist/dlq.js +391 -0
  36. package/dist/fanout-drain.d.ts +11 -0
  37. package/dist/fanout-drain.js +31 -0
  38. package/dist/fanout.d.ts +144 -0
  39. package/dist/fanout.js +321 -0
  40. package/dist/inbox.d.ts +125 -0
  41. package/dist/inbox.js +120 -0
  42. package/dist/index.d.ts +36 -0
  43. package/dist/index.js +70 -0
  44. package/dist/internal/id.d.ts +38 -0
  45. package/dist/internal/id.js +78 -0
  46. package/dist/internal/pg-search-path.d.ts +34 -0
  47. package/dist/internal/pg-search-path.js +55 -0
  48. package/dist/internal/resolve-contract-path.d.ts +41 -0
  49. package/dist/internal/resolve-contract-path.js +65 -0
  50. package/dist/observability.d.ts +24 -0
  51. package/dist/observability.js +37 -0
  52. package/dist/postgres-consumer.d.ts +175 -0
  53. package/dist/postgres-consumer.js +561 -0
  54. package/dist/postgres-transport.d.ts +70 -0
  55. package/dist/postgres-transport.js +144 -0
  56. package/dist/producer-db.d.ts +80 -0
  57. package/dist/producer-db.js +115 -0
  58. package/dist/registry.d.ts +94 -0
  59. package/dist/registry.js +99 -0
  60. package/dist/signature.d.ts +44 -0
  61. package/dist/signature.js +79 -0
  62. package/dist/types.d.ts +107 -0
  63. package/dist/types.js +13 -0
  64. package/dist/validator.d.ts +60 -0
  65. package/dist/validator.js +171 -0
  66. package/package.json +48 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Dispatcher SDK envelope types.
3
+ *
4
+ * Mirrors the Sguild Event Envelope JSON Schema at
5
+ * coordination/contracts/event-envelope/schema/envelope-v1.json
6
+ * (sourced from contracts/event-envelope/README.md v1.0.2).
7
+ *
8
+ * Per §8.2 of the envelope contract, payload schema versioning is per-event-type
9
+ * and independent of envelope versioning. The generic `TPayload` parameter on
10
+ * `EventEnvelope` lets callers narrow the payload type at the call site.
11
+ */
12
+ /**
13
+ * Closed set of producer domains per the closed-domain rule in
14
+ * coordination/CONVENTIONS.md and coordination/domains/README.md.
15
+ *
16
+ * Coaching was added on 2026-05-01 by ADR-0008. `operations` was the legacy
17
+ * value during the 2026-04-27 to 2026-05-11 deprecation window per the v1.0.1
18
+ * changelog of contracts/event-envelope/README.md and is included here so the
19
+ * type accepts events emitted under the legacy name; consumers MUST treat
20
+ * `operations` and `delivery` as the same producer per §4.1.
21
+ */
22
+ export type ProducerDomain = "platform" | "growth" | "sales" | "delivery" | "revenue" | "coaching" | "operations";
23
+ /**
24
+ * Live producer domains. Excludes the `operations` deprecation alias. New code
25
+ * SHOULD constrain to this type rather than the broader `ProducerDomain` to
26
+ * avoid emitting the deprecated value.
27
+ */
28
+ export type LiveProducerDomain = Exclude<ProducerDomain, "operations">;
29
+ /**
30
+ * Actor reference per §4.2 of the envelope contract. Either a Person ID
31
+ * (human-triggered) or `system:<domain>` (automated).
32
+ */
33
+ export type ActorRef = `per_${string}` | `system:${LiveProducerDomain}`;
34
+ /**
35
+ * Canonical entity ID prefix-and-UUID-v7 shape per ADR-0002. Used for the
36
+ * `subject` field. Specific role-record prefixes (per_, lead_, par_, coa_,
37
+ * crr_, crd_acct_, les_, etc.) are validated by the JSON Schema's regex; the
38
+ * TS type stays loose because the closed-set of prefixes is broader than the
39
+ * type system can usefully express here.
40
+ */
41
+ export type CanonicalEntityId = `${string}_${string}`;
42
+ /**
43
+ * The envelope every cross-domain event rides on. Required and optional fields
44
+ * per §4.1 and §4.2 of the contract.
45
+ *
46
+ * @typeParam TPayload - the event-specific payload shape, narrowed per
47
+ * (event_type, schema_version) by the consumer at the call site.
48
+ */
49
+ export type EventEnvelope<TPayload = unknown> = {
50
+ /** evt_<UUID v7 canonical>. Globally unique per ADR-0002. */
51
+ event_id: string;
52
+ /** Dotted lowercase noun.verb. Must be registered in the event-type registry. */
53
+ event_type: string;
54
+ /** ISO 8601 UTC, producer wall-clock at the moment the event happened. */
55
+ occurred_at: string;
56
+ /** Tenant scoping per ADR-0001. */
57
+ tenant_id: string;
58
+ /** Which domain emitted the event. */
59
+ producer: ProducerDomain;
60
+ /** Per-event-type schema version. Independent of envelope version per §8.2. */
61
+ schema_version: number;
62
+ /** Event-specific data; shape determined by (event_type, schema_version). */
63
+ payload: TPayload;
64
+ /** Optional. The primary entity the event is about. Usually a person_id. */
65
+ subject?: CanonicalEntityId;
66
+ /** Optional. The human or system that caused the event. */
67
+ actor?: ActorRef;
68
+ /** Optional. The originating event in a chain. */
69
+ correlation_id?: string;
70
+ };
71
+ /**
72
+ * The minimum a producer supplies to `dispatcher.publish`. The SDK fills in
73
+ * `event_id`, `occurred_at`, `tenant_id`, `producer`, and `schema_version`
74
+ * automatically per §10.2 of the envelope contract; producers populate
75
+ * `event_type`, `payload`, and the optional fields.
76
+ *
77
+ * Note: `schema_version` defaults to the active version registered in the
78
+ * event-type registry for `event_type`. Producers emitting at a non-default
79
+ * version (during a transition window) supply it explicitly via `EventEmit`.
80
+ */
81
+ export type EventEmit<TPayload = unknown> = {
82
+ event_type: string;
83
+ payload: TPayload;
84
+ schema_version?: number;
85
+ subject?: CanonicalEntityId;
86
+ actor?: ActorRef;
87
+ correlation_id?: string;
88
+ };
89
+ /**
90
+ * Subscriber handler signature. Per §9.2 of the envelope contract, the SDK
91
+ * dedupes over `(consumer, event_id)` so handlers are invoked at-most-once
92
+ * per (consumer, event_id) pair (when the SDK ships; today the SDK is in
93
+ * Phase 0 per memos/2026/2026-05-01-platform-dispatcher-sdk-build-plan).
94
+ *
95
+ * Handlers MUST throw on processing failure to invoke the SDK's retry path.
96
+ * Swallowing exceptions silently bypasses the dead-letter machinery.
97
+ */
98
+ export type SubscribeHandler<TPayload = unknown> = (envelope: EventEnvelope<TPayload>) => Promise<void>;
99
+ /**
100
+ * Result of a successful `dispatcher.publish` call. Returns the canonical
101
+ * `event_id` so the producer can correlate retries (per §10.6 of the envelope
102
+ * contract: producers SHALL reuse the same event_id on retry so consumer dedup
103
+ * works).
104
+ */
105
+ export type PublishResult = {
106
+ event_id: string;
107
+ };
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * Dispatcher SDK envelope types.
4
+ *
5
+ * Mirrors the Sguild Event Envelope JSON Schema at
6
+ * coordination/contracts/event-envelope/schema/envelope-v1.json
7
+ * (sourced from contracts/event-envelope/README.md v1.0.2).
8
+ *
9
+ * Per §8.2 of the envelope contract, payload schema versioning is per-event-type
10
+ * and independent of envelope versioning. The generic `TPayload` parameter on
11
+ * `EventEnvelope` lets callers narrow the payload type at the call site.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Dispatcher payload and envelope validation.
3
+ *
4
+ * ADR-0009 action item 8: every emit validates payload against the
5
+ * registered JSON Schema for `(event_type, schema_version)` before the
6
+ * row hits dispatcher_event. The validator runs producer-side at publish
7
+ * time so unschema'd payloads never reach consumers; consumers also
8
+ * re-validate at subscribe time per envelope contract §5.
9
+ *
10
+ * Schemas live in two places per the event-envelope contract:
11
+ *
12
+ * - The envelope schema at
13
+ * `coordination/contracts/event-envelope/schema/envelope-v1.json`
14
+ * applies to every envelope the SDK builds. Validated once per
15
+ * publish; cached after first compile.
16
+ *
17
+ * - Per-event-type payload schemas at
18
+ * `coordination/contracts/<contract>/schema/payloads/<event_type>-v<schema_version>.json`
19
+ * (path resolved from the registry's `payload_schema` field). One
20
+ * compiled validator per (event_type, schema_version), cached on
21
+ * first use.
22
+ *
23
+ * Validation failures throw `PayloadValidationError` or
24
+ * `EnvelopeValidationError` with the ajv-shaped error list attached so
25
+ * callers can surface useful messages.
26
+ */
27
+ import type { ErrorObject } from "ajv";
28
+ export declare class EnvelopeValidationError extends Error {
29
+ readonly errors: ErrorObject[];
30
+ constructor(errors: ErrorObject[]);
31
+ }
32
+ export declare class PayloadValidationError extends Error {
33
+ readonly eventType: string;
34
+ readonly schemaVersion: number;
35
+ readonly errors: ErrorObject[];
36
+ constructor(eventType: string, schemaVersion: number, errors: ErrorObject[]);
37
+ }
38
+ export declare class PayloadSchemaUnavailableError extends Error {
39
+ readonly eventType: string;
40
+ readonly schemaVersion: number;
41
+ constructor(eventType: string, schemaVersion: number);
42
+ }
43
+ /**
44
+ * Validate an envelope against the published JSON Schema. Throws
45
+ * EnvelopeValidationError on failure with the ajv error list attached.
46
+ * Idempotent: the compiled validator caches across calls.
47
+ */
48
+ export declare function validateEnvelope(envelope: unknown): void;
49
+ /**
50
+ * Validate a payload against the registered JSON Schema for
51
+ * (event_type, schema_version). Throws PayloadValidationError on
52
+ * validation failure or PayloadSchemaUnavailableError when the registry
53
+ * has no `payload_schema` recorded for the version (which is itself a
54
+ * registry-hygiene problem the producer should fix before emitting).
55
+ */
56
+ export declare function validatePayload(eventType: string, schemaVersion: number, payload: unknown): void;
57
+ /**
58
+ * Reset the compiled-validator caches. Test-only.
59
+ */
60
+ export declare function __resetValidatorCachesForTests(): void;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ /**
3
+ * Dispatcher payload and envelope validation.
4
+ *
5
+ * ADR-0009 action item 8: every emit validates payload against the
6
+ * registered JSON Schema for `(event_type, schema_version)` before the
7
+ * row hits dispatcher_event. The validator runs producer-side at publish
8
+ * time so unschema'd payloads never reach consumers; consumers also
9
+ * re-validate at subscribe time per envelope contract §5.
10
+ *
11
+ * Schemas live in two places per the event-envelope contract:
12
+ *
13
+ * - The envelope schema at
14
+ * `coordination/contracts/event-envelope/schema/envelope-v1.json`
15
+ * applies to every envelope the SDK builds. Validated once per
16
+ * publish; cached after first compile.
17
+ *
18
+ * - Per-event-type payload schemas at
19
+ * `coordination/contracts/<contract>/schema/payloads/<event_type>-v<schema_version>.json`
20
+ * (path resolved from the registry's `payload_schema` field). One
21
+ * compiled validator per (event_type, schema_version), cached on
22
+ * first use.
23
+ *
24
+ * Validation failures throw `PayloadValidationError` or
25
+ * `EnvelopeValidationError` with the ajv-shaped error list attached so
26
+ * callers can surface useful messages.
27
+ */
28
+ var __importDefault = (this && this.__importDefault) || function (mod) {
29
+ return (mod && mod.__esModule) ? mod : { "default": mod };
30
+ };
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.PayloadSchemaUnavailableError = exports.PayloadValidationError = exports.EnvelopeValidationError = void 0;
33
+ exports.validateEnvelope = validateEnvelope;
34
+ exports.validatePayload = validatePayload;
35
+ exports.__resetValidatorCachesForTests = __resetValidatorCachesForTests;
36
+ const node_fs_1 = require("node:fs");
37
+ const _2020_1 = __importDefault(require("ajv/dist/2020"));
38
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
39
+ const registry_1 = require("./registry");
40
+ const resolve_contract_path_1 = require("./internal/resolve-contract-path");
41
+ // =============================================================================
42
+ // ajv setup. Single instance, shared across the process. `strict: false`
43
+ // because the contract schemas use a few patterns ajv's strict mode
44
+ // rejects (notably draft-2020-12 features used loosely); the contracts
45
+ // pass schema linting separately so loose validation here is acceptable.
46
+ // addFormats wires up ISO 8601 date-time, uri, etc. for schema fields
47
+ // that use `format`.
48
+ // =============================================================================
49
+ const ajv = new _2020_1.default({ allErrors: true, strict: false });
50
+ (0, ajv_formats_1.default)(ajv);
51
+ let cachedEnvelopeValidator = null;
52
+ const payloadValidatorCache = new Map();
53
+ // The envelope schema ships bundled inside the package's contracts tree
54
+ // (rooted at `contracts/` exactly as the coordination repo stores it).
55
+ // `resolveContractPath` handles the module-relative and cwd-relative
56
+ // layouts, including the sibling coordination repo fallback.
57
+ const ENVELOPE_SCHEMA_REL_PATH = "contracts/event-envelope/schema/envelope-v1.json";
58
+ // =============================================================================
59
+ // Errors
60
+ // =============================================================================
61
+ class EnvelopeValidationError extends Error {
62
+ errors;
63
+ constructor(errors) {
64
+ super(`Envelope failed JSON Schema validation: ${errors
65
+ .map((e) => `${e.instancePath || "/"} ${e.message}`)
66
+ .join("; ")}`);
67
+ this.errors = errors;
68
+ this.name = "EnvelopeValidationError";
69
+ }
70
+ }
71
+ exports.EnvelopeValidationError = EnvelopeValidationError;
72
+ class PayloadValidationError extends Error {
73
+ eventType;
74
+ schemaVersion;
75
+ errors;
76
+ constructor(eventType, schemaVersion, errors) {
77
+ super(`Payload for event_type='${eventType}' schema_version=${schemaVersion} failed JSON Schema validation: ${errors
78
+ .map((e) => `${e.instancePath || "/"} ${e.message}`)
79
+ .join("; ")}`);
80
+ this.eventType = eventType;
81
+ this.schemaVersion = schemaVersion;
82
+ this.errors = errors;
83
+ this.name = "PayloadValidationError";
84
+ }
85
+ }
86
+ exports.PayloadValidationError = PayloadValidationError;
87
+ class PayloadSchemaUnavailableError extends Error {
88
+ eventType;
89
+ schemaVersion;
90
+ constructor(eventType, schemaVersion) {
91
+ super(`No payload schema registered for event_type='${eventType}' schema_version=${schemaVersion}. ` +
92
+ `Author the schema at coordination/contracts/<contract>/schema/payloads/<event_type>-v<schema_version>.json ` +
93
+ `and update the registry's payload_schema field before emitting.`);
94
+ this.eventType = eventType;
95
+ this.schemaVersion = schemaVersion;
96
+ this.name = "PayloadSchemaUnavailableError";
97
+ }
98
+ }
99
+ exports.PayloadSchemaUnavailableError = PayloadSchemaUnavailableError;
100
+ // =============================================================================
101
+ // Public API
102
+ // =============================================================================
103
+ /**
104
+ * Validate an envelope against the published JSON Schema. Throws
105
+ * EnvelopeValidationError on failure with the ajv error list attached.
106
+ * Idempotent: the compiled validator caches across calls.
107
+ */
108
+ function validateEnvelope(envelope) {
109
+ const validator = getEnvelopeValidator();
110
+ const ok = validator(envelope);
111
+ if (!ok) {
112
+ throw new EnvelopeValidationError(validator.errors ?? []);
113
+ }
114
+ }
115
+ /**
116
+ * Validate a payload against the registered JSON Schema for
117
+ * (event_type, schema_version). Throws PayloadValidationError on
118
+ * validation failure or PayloadSchemaUnavailableError when the registry
119
+ * has no `payload_schema` recorded for the version (which is itself a
120
+ * registry-hygiene problem the producer should fix before emitting).
121
+ */
122
+ function validatePayload(eventType, schemaVersion, payload) {
123
+ const validator = getPayloadValidator(eventType, schemaVersion);
124
+ const ok = validator(payload);
125
+ if (!ok) {
126
+ throw new PayloadValidationError(eventType, schemaVersion, validator.errors ?? []);
127
+ }
128
+ }
129
+ /**
130
+ * Reset the compiled-validator caches. Test-only.
131
+ */
132
+ function __resetValidatorCachesForTests() {
133
+ cachedEnvelopeValidator = null;
134
+ payloadValidatorCache.clear();
135
+ }
136
+ // =============================================================================
137
+ // Internal
138
+ // =============================================================================
139
+ function getEnvelopeValidator() {
140
+ if (cachedEnvelopeValidator)
141
+ return cachedEnvelopeValidator;
142
+ const schemaPath = (0, resolve_contract_path_1.resolveContractPath)(__dirname, ENVELOPE_SCHEMA_REL_PATH);
143
+ const raw = (0, node_fs_1.readFileSync)(schemaPath, "utf8");
144
+ const schema = JSON.parse(raw);
145
+ cachedEnvelopeValidator = ajv.compile(schema);
146
+ return cachedEnvelopeValidator;
147
+ }
148
+ function getPayloadValidator(eventType, schemaVersion) {
149
+ const cacheKey = `${eventType}@${schemaVersion}`;
150
+ const cached = payloadValidatorCache.get(cacheKey);
151
+ if (cached)
152
+ return cached;
153
+ const registration = (0, registry_1.getEventTypeRegistration)(eventType, (0, registry_1.loadRegistry)());
154
+ if (!registration) {
155
+ throw new PayloadSchemaUnavailableError(eventType, schemaVersion);
156
+ }
157
+ const versionEntry = registration.schema_versions.find((v) => v.version === schemaVersion);
158
+ if (!versionEntry || !versionEntry.payload_schema) {
159
+ throw new PayloadSchemaUnavailableError(eventType, schemaVersion);
160
+ }
161
+ // The registry stores the payload-schema path rooted at `contracts/`
162
+ // exactly as the coordination repo does. The dispatcher reads it from the
163
+ // bundled contracts tree (resolveContractPath handles the layout
164
+ // waterfall, including the sibling coordination repo fallback).
165
+ const schemaPath = (0, resolve_contract_path_1.resolveContractPath)(__dirname, versionEntry.payload_schema);
166
+ const raw = (0, node_fs_1.readFileSync)(schemaPath, "utf8");
167
+ const schema = JSON.parse(raw);
168
+ const validator = ajv.compile(schema);
169
+ payloadValidatorCache.set(cacheKey, validator);
170
+ return validator;
171
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@sguild/dispatcher",
3
+ "version": "2.0.0",
4
+ "description": "Cross-domain event dispatcher SDK for Sguild domains, per the Sguild Event Envelope contract and ADR-0009. Producer-side transactional emit, consumer-side polling worker with dedup/retry/dead-letter, envelope and payload validation against the bundled contracts tree.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/sguild-admin/platform.git",
9
+ "directory": "lib/dispatcher"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "contracts",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "ajv": "^8.17.1",
34
+ "ajv-formats": "^3.0.1",
35
+ "pg": "^8.17.2"
36
+ },
37
+ "peerDependencies": {
38
+ "@prisma/client": ">=7"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20",
42
+ "@types/pg": "^8.16.0",
43
+ "typescript": "^5.2.2"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }