@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
package/dist/inbox.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Domain-side dispatcher inbox helpers.
4
+ *
5
+ * Per the 2026-05-12 topology decision (memos/2026-05-12-platform-cross-db-
6
+ * consumer-dsn-upstream): Platform's fanout worker POSTs events to each
7
+ * consumer domain's webhook endpoint. The endpoint pattern is roughly:
8
+ *
9
+ * POST /api/dispatcher/inbox
10
+ *
11
+ * with these headers from Platform's fanout:
12
+ *
13
+ * x-sguild-event-id: evt_<UUIDv7>
14
+ * x-sguild-event-type: <event_type>
15
+ * x-sguild-signature: sha256=<hex>
16
+ *
17
+ * and the JSON-serialized envelope as the request body. This module
18
+ * carries the two pieces every domain inbox needs:
19
+ *
20
+ * 1. `verifyInboxRequest(...)` — validates the HMAC signature against
21
+ * the raw request body using the domain's shared secret, returns the
22
+ * parsed envelope on success or a typed error on failure.
23
+ *
24
+ * 2. `dedupOnEventId(...)` — wraps an idempotency check in the domain's
25
+ * own database. Domains track which event_ids they have already
26
+ * processed; a duplicate hit returns early without re-running the
27
+ * handler. The dedup table is domain-owned, not Platform's.
28
+ *
29
+ * Domains import these helpers from `@/lib/dispatcher/inbox` once they
30
+ * vendor Platform's dispatcher SDK. The handler shape is the domain's
31
+ * own concern — this module does not own routing; it owns signature
32
+ * verification and the dedup contract.
33
+ *
34
+ * The reference route at `app/api/dispatcher/inbox/route.ts` in this
35
+ * repo shows the full assembly: verify, parse, dedup, dispatch, ack.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.verifyInboxRequest = verifyInboxRequest;
39
+ exports.tryInsertOrDetectConflict = tryInsertOrDetectConflict;
40
+ const signature_1 = require("./signature");
41
+ /**
42
+ * Verify a Platform-fanout-style POST request against the domain's
43
+ * shared HMAC secret. The signature is computed over the raw request
44
+ * body, so callers MUST pass the exact bytes received (not a re-
45
+ * stringified JSON object). The standard pattern in a Next.js route
46
+ * handler is:
47
+ *
48
+ * const rawBody = await req.text();
49
+ * const result = verifyInboxRequest(rawBody, req.headers, secret);
50
+ *
51
+ * Returns the parsed envelope on success. Failures map to the right
52
+ * HTTP status: 401 for signature mismatch, 400 for missing header or
53
+ * malformed body. The route handler returns that status verbatim.
54
+ *
55
+ * On 5xx-shaped failures (database errors during dedup, handler
56
+ * throws, etc.), the route handler returns 500 separately; Platform's
57
+ * fanout retries on 5xx and dead-letters on permanent failure. 4xx
58
+ * responses also retry once per Platform's ConsumerLoop retry budget
59
+ * (4xx may indicate a deploy-in-progress secret rotation that is
60
+ * transient).
61
+ */
62
+ function verifyInboxRequest(rawBody, headers, secret) {
63
+ const signature = headers.get(signature_1.SIGNATURE_HEADER);
64
+ if (!signature) {
65
+ return {
66
+ ok: false,
67
+ status: 400,
68
+ reason: `missing ${signature_1.SIGNATURE_HEADER} header`,
69
+ };
70
+ }
71
+ if (!(0, signature_1.verifyEnvelopeSignature)(rawBody, secret, signature)) {
72
+ return { ok: false, status: 401, reason: "invalid HMAC signature" };
73
+ }
74
+ let envelope;
75
+ try {
76
+ envelope = JSON.parse(rawBody);
77
+ }
78
+ catch (e) {
79
+ return {
80
+ ok: false,
81
+ status: 400,
82
+ reason: `request body is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
83
+ };
84
+ }
85
+ if (typeof envelope.event_id !== "string" || envelope.event_id.length === 0) {
86
+ return { ok: false, status: 400, reason: "envelope missing event_id" };
87
+ }
88
+ if (typeof envelope.event_type !== "string" || envelope.event_type.length === 0) {
89
+ return { ok: false, status: 400, reason: "envelope missing event_type" };
90
+ }
91
+ return { ok: true, envelope, rawBody };
92
+ }
93
+ /**
94
+ * Helper for the common case where the dedup INSERT throws on conflict
95
+ * with a recognizable Prisma error code. Wrap a Prisma insert and pass
96
+ * the throwing function in. The helper inspects the thrown error for
97
+ * Prisma's unique-violation code (P2002) and translates to
98
+ * `already_processed`; any other error rethrows.
99
+ *
100
+ * Usage:
101
+ *
102
+ * const outcome = await tryInsertOrDetectConflict(async () => {
103
+ * await prisma.dispatcherInboxDedup.create({
104
+ * data: { eventId, eventType }
105
+ * });
106
+ * });
107
+ * if (outcome === "already_processed") return new Response(null, { status: 200 });
108
+ */
109
+ async function tryInsertOrDetectConflict(insert) {
110
+ try {
111
+ await insert();
112
+ return "first_time";
113
+ }
114
+ catch (e) {
115
+ const code = e?.code;
116
+ if (code === "P2002")
117
+ return "already_processed";
118
+ throw e;
119
+ }
120
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Dispatcher SDK public surface.
3
+ *
4
+ * Domain code imports from `@/lib/dispatcher`, never from the internal
5
+ * files directly. The barrel re-exports the singleton dispatcher, the
6
+ * envelope types, the registry query functions, the runtime config
7
+ * helpers, the publish/consumer option shapes, and the error classes
8
+ * callers may need to handle.
9
+ *
10
+ * Internal files (transport implementations, dedup tracking, registry
11
+ * cache management, validator internals) are not re-exported. The narrow
12
+ * public surface keeps domains from depending on internal layout that
13
+ * may change as the remaining Slice 3b through Slice 5 land.
14
+ *
15
+ * Phase 2 status: see ./README.md for what's implemented and what's
16
+ * pending. publish wires to the Postgres-queue transport; subscribe +
17
+ * start + stop wire the polling worker; LISTEN/NOTIFY wake-up
18
+ * (Slice 3b) and the per-consumer DLQ read API (Slice 5) remain.
19
+ */
20
+ export { dispatcher } from "./dispatcher";
21
+ export { DispatcherNotImplementedError, UnregisteredEventTypeError, UnregisteredSchemaVersionError, } from "./dispatcher";
22
+ export { configureDispatcher, type DispatcherConfig, } from "./config";
23
+ export type { PublishOptions } from "./postgres-transport";
24
+ export type { ConsumerLoopOptions } from "./postgres-consumer";
25
+ export { EnvelopeValidationError, PayloadValidationError, PayloadSchemaUnavailableError, } from "./validator";
26
+ export { DeadLetterAlreadyResolvedError, DeadLetterNotFoundError, ProducerDbNotConfiguredError, ReplayConsumerUnknownError, ReplayDeliveryFailedError, getDeadLetter, knownProducers, listDeadLetters, replayDeadLetter, resolveDeadLetter, } from "./dlq";
27
+ export type { DeadLetter, GetDeadLetterOptions, ListDeadLettersFilter, ReplayDeadLetterInput, ReplayDeadLetterOutcome, ResolveDeadLetterInput, } from "./dlq";
28
+ export { configureDispatcherObservability, resetDispatcherObservabilityForTests, } from "./observability";
29
+ export type { DispatcherHistogramName, DispatcherMetricLabels, DispatcherMetricName, DispatcherObservabilityHooks, } from "./observability";
30
+ export type { ActorRef, CanonicalEntityId, EventEmit, EventEnvelope, LiveProducerDomain, ProducerDomain, PublishResult, SubscribeHandler, } from "./types";
31
+ export { getActiveSchemaVersion, getEventTypeRegistration, loadRegistry, } from "./registry";
32
+ export type { EventTypeRegistration, RegistryManifest, SchemaVersionEntry, SchemaVersionStatus, } from "./registry";
33
+ export { tryInsertOrDetectConflict, verifyInboxRequest, } from "./inbox";
34
+ export type { DedupAttempt, DedupOutcome, InboxVerificationResult, } from "./inbox";
35
+ export { EVENT_ID_HEADER, EVENT_TYPE_HEADER, SIGNATURE_HEADER, signEnvelope, verifyEnvelopeSignature, } from "./signature";
36
+ export { WebhookDispatchError } from "./fanout";
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Dispatcher SDK public surface.
4
+ *
5
+ * Domain code imports from `@/lib/dispatcher`, never from the internal
6
+ * files directly. The barrel re-exports the singleton dispatcher, the
7
+ * envelope types, the registry query functions, the runtime config
8
+ * helpers, the publish/consumer option shapes, and the error classes
9
+ * callers may need to handle.
10
+ *
11
+ * Internal files (transport implementations, dedup tracking, registry
12
+ * cache management, validator internals) are not re-exported. The narrow
13
+ * public surface keeps domains from depending on internal layout that
14
+ * may change as the remaining Slice 3b through Slice 5 land.
15
+ *
16
+ * Phase 2 status: see ./README.md for what's implemented and what's
17
+ * pending. publish wires to the Postgres-queue transport; subscribe +
18
+ * start + stop wire the polling worker; LISTEN/NOTIFY wake-up
19
+ * (Slice 3b) and the per-consumer DLQ read API (Slice 5) remain.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.WebhookDispatchError = exports.verifyEnvelopeSignature = exports.signEnvelope = exports.SIGNATURE_HEADER = exports.EVENT_TYPE_HEADER = exports.EVENT_ID_HEADER = exports.verifyInboxRequest = exports.tryInsertOrDetectConflict = exports.loadRegistry = exports.getEventTypeRegistration = exports.getActiveSchemaVersion = exports.resetDispatcherObservabilityForTests = exports.configureDispatcherObservability = exports.resolveDeadLetter = exports.replayDeadLetter = exports.listDeadLetters = exports.knownProducers = exports.getDeadLetter = exports.ReplayDeliveryFailedError = exports.ReplayConsumerUnknownError = exports.ProducerDbNotConfiguredError = exports.DeadLetterNotFoundError = exports.DeadLetterAlreadyResolvedError = exports.PayloadSchemaUnavailableError = exports.PayloadValidationError = exports.EnvelopeValidationError = exports.configureDispatcher = exports.UnregisteredSchemaVersionError = exports.UnregisteredEventTypeError = exports.DispatcherNotImplementedError = exports.dispatcher = void 0;
23
+ var dispatcher_1 = require("./dispatcher");
24
+ Object.defineProperty(exports, "dispatcher", { enumerable: true, get: function () { return dispatcher_1.dispatcher; } });
25
+ var dispatcher_2 = require("./dispatcher");
26
+ Object.defineProperty(exports, "DispatcherNotImplementedError", { enumerable: true, get: function () { return dispatcher_2.DispatcherNotImplementedError; } });
27
+ Object.defineProperty(exports, "UnregisteredEventTypeError", { enumerable: true, get: function () { return dispatcher_2.UnregisteredEventTypeError; } });
28
+ Object.defineProperty(exports, "UnregisteredSchemaVersionError", { enumerable: true, get: function () { return dispatcher_2.UnregisteredSchemaVersionError; } });
29
+ var config_1 = require("./config");
30
+ Object.defineProperty(exports, "configureDispatcher", { enumerable: true, get: function () { return config_1.configureDispatcher; } });
31
+ var validator_1 = require("./validator");
32
+ Object.defineProperty(exports, "EnvelopeValidationError", { enumerable: true, get: function () { return validator_1.EnvelopeValidationError; } });
33
+ Object.defineProperty(exports, "PayloadValidationError", { enumerable: true, get: function () { return validator_1.PayloadValidationError; } });
34
+ Object.defineProperty(exports, "PayloadSchemaUnavailableError", { enumerable: true, get: function () { return validator_1.PayloadSchemaUnavailableError; } });
35
+ var dlq_1 = require("./dlq");
36
+ Object.defineProperty(exports, "DeadLetterAlreadyResolvedError", { enumerable: true, get: function () { return dlq_1.DeadLetterAlreadyResolvedError; } });
37
+ Object.defineProperty(exports, "DeadLetterNotFoundError", { enumerable: true, get: function () { return dlq_1.DeadLetterNotFoundError; } });
38
+ Object.defineProperty(exports, "ProducerDbNotConfiguredError", { enumerable: true, get: function () { return dlq_1.ProducerDbNotConfiguredError; } });
39
+ Object.defineProperty(exports, "ReplayConsumerUnknownError", { enumerable: true, get: function () { return dlq_1.ReplayConsumerUnknownError; } });
40
+ Object.defineProperty(exports, "ReplayDeliveryFailedError", { enumerable: true, get: function () { return dlq_1.ReplayDeliveryFailedError; } });
41
+ Object.defineProperty(exports, "getDeadLetter", { enumerable: true, get: function () { return dlq_1.getDeadLetter; } });
42
+ Object.defineProperty(exports, "knownProducers", { enumerable: true, get: function () { return dlq_1.knownProducers; } });
43
+ Object.defineProperty(exports, "listDeadLetters", { enumerable: true, get: function () { return dlq_1.listDeadLetters; } });
44
+ Object.defineProperty(exports, "replayDeadLetter", { enumerable: true, get: function () { return dlq_1.replayDeadLetter; } });
45
+ Object.defineProperty(exports, "resolveDeadLetter", { enumerable: true, get: function () { return dlq_1.resolveDeadLetter; } });
46
+ var observability_1 = require("./observability");
47
+ Object.defineProperty(exports, "configureDispatcherObservability", { enumerable: true, get: function () { return observability_1.configureDispatcherObservability; } });
48
+ Object.defineProperty(exports, "resetDispatcherObservabilityForTests", { enumerable: true, get: function () { return observability_1.resetDispatcherObservabilityForTests; } });
49
+ var registry_1 = require("./registry");
50
+ Object.defineProperty(exports, "getActiveSchemaVersion", { enumerable: true, get: function () { return registry_1.getActiveSchemaVersion; } });
51
+ Object.defineProperty(exports, "getEventTypeRegistration", { enumerable: true, get: function () { return registry_1.getEventTypeRegistration; } });
52
+ Object.defineProperty(exports, "loadRegistry", { enumerable: true, get: function () { return registry_1.loadRegistry; } });
53
+ // Inbox helpers consumed by domain webhook endpoints (verify HMAC,
54
+ // dedup on event_id). Platform-side fanout machinery is internal and
55
+ // not re-exported here; instrumentation.ts imports it directly from
56
+ // ./fanout.
57
+ var inbox_1 = require("./inbox");
58
+ Object.defineProperty(exports, "tryInsertOrDetectConflict", { enumerable: true, get: function () { return inbox_1.tryInsertOrDetectConflict; } });
59
+ Object.defineProperty(exports, "verifyInboxRequest", { enumerable: true, get: function () { return inbox_1.verifyInboxRequest; } });
60
+ // HMAC sign/verify primitives live in signature.ts (low-dep, vendorable
61
+ // stand-alone). The fanout module re-exports them for legacy imports;
62
+ // new code imports from the canonical here.
63
+ var signature_1 = require("./signature");
64
+ Object.defineProperty(exports, "EVENT_ID_HEADER", { enumerable: true, get: function () { return signature_1.EVENT_ID_HEADER; } });
65
+ Object.defineProperty(exports, "EVENT_TYPE_HEADER", { enumerable: true, get: function () { return signature_1.EVENT_TYPE_HEADER; } });
66
+ Object.defineProperty(exports, "SIGNATURE_HEADER", { enumerable: true, get: function () { return signature_1.SIGNATURE_HEADER; } });
67
+ Object.defineProperty(exports, "signEnvelope", { enumerable: true, get: function () { return signature_1.signEnvelope; } });
68
+ Object.defineProperty(exports, "verifyEnvelopeSignature", { enumerable: true, get: function () { return signature_1.verifyEnvelopeSignature; } });
69
+ var fanout_1 = require("./fanout");
70
+ Object.defineProperty(exports, "WebhookDispatchError", { enumerable: true, get: function () { return fanout_1.WebhookDispatchError; } });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Canonical entity ID generation per ADR-0002.
3
+ *
4
+ * Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
5
+ * package carries no dependency on the platform repo's `lib/id.ts`. Kept
6
+ * byte-for-byte in step with that source; if the canonical implementation
7
+ * changes, re-sync this copy. The implementation is dependency-free (only
8
+ * `node:crypto`) per RFC 9562 §5.7, so vendoring is a literal copy.
9
+ *
10
+ * The dispatcher uses `uuidv7()` for envelope `event_id` values and
11
+ * dead-letter row ids; `prefixedId` and `isUuidV7` are carried for parity
12
+ * with the source module and may be used by future SDK code.
13
+ */
14
+ /**
15
+ * Generate a UUID v7 in canonical form per RFC 9562 §5.7.
16
+ *
17
+ * Layout:
18
+ * - 48 bits: Unix timestamp in milliseconds, big-endian (bytes 0-5)
19
+ * - 4 bits: version (set to `0111` = 7) (high nibble of byte 6)
20
+ * - 12 bits: random data (low nibble of byte 6 + byte 7)
21
+ * - 2 bits: variant (set to `10`) (high two bits of byte 8)
22
+ * - 62 bits: random data (remaining 62 bits)
23
+ *
24
+ * The timestamp prefix gives lexicographic sort order matching insertion
25
+ * order, which is the property that makes v7 valuable for database keys.
26
+ *
27
+ * @returns A 36-character UUID string in the canonical 8-4-4-4-12 form.
28
+ */
29
+ export declare function uuidv7(): string;
30
+ /**
31
+ * Generate a prefixed canonical entity ID per ADR-0002. Convenience wrapper
32
+ * that combines `uuidv7()` with the prefix.
33
+ *
34
+ * @example
35
+ * prefixedId("per_") // "per_018f3c2d-4a5b-7c8d-9e0f-1a2b3c4d5e6f"
36
+ */
37
+ export declare function prefixedId(prefix: string): string;
38
+ export declare function isUuidV7(value: string): boolean;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical entity ID generation per ADR-0002.
4
+ *
5
+ * Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
6
+ * package carries no dependency on the platform repo's `lib/id.ts`. Kept
7
+ * byte-for-byte in step with that source; if the canonical implementation
8
+ * changes, re-sync this copy. The implementation is dependency-free (only
9
+ * `node:crypto`) per RFC 9562 §5.7, so vendoring is a literal copy.
10
+ *
11
+ * The dispatcher uses `uuidv7()` for envelope `event_id` values and
12
+ * dead-letter row ids; `prefixedId` and `isUuidV7` are carried for parity
13
+ * with the source module and may be used by future SDK code.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.uuidv7 = uuidv7;
17
+ exports.prefixedId = prefixedId;
18
+ exports.isUuidV7 = isUuidV7;
19
+ const node_crypto_1 = require("node:crypto");
20
+ /**
21
+ * Generate a UUID v7 in canonical form per RFC 9562 §5.7.
22
+ *
23
+ * Layout:
24
+ * - 48 bits: Unix timestamp in milliseconds, big-endian (bytes 0-5)
25
+ * - 4 bits: version (set to `0111` = 7) (high nibble of byte 6)
26
+ * - 12 bits: random data (low nibble of byte 6 + byte 7)
27
+ * - 2 bits: variant (set to `10`) (high two bits of byte 8)
28
+ * - 62 bits: random data (remaining 62 bits)
29
+ *
30
+ * The timestamp prefix gives lexicographic sort order matching insertion
31
+ * order, which is the property that makes v7 valuable for database keys.
32
+ *
33
+ * @returns A 36-character UUID string in the canonical 8-4-4-4-12 form.
34
+ */
35
+ function uuidv7() {
36
+ const timestamp = BigInt(Date.now());
37
+ const bytes = new Uint8Array(16);
38
+ // Bytes 0-5: 48-bit timestamp, big-endian.
39
+ bytes[0] = Number((timestamp >> 40n) & 0xffn);
40
+ bytes[1] = Number((timestamp >> 32n) & 0xffn);
41
+ bytes[2] = Number((timestamp >> 24n) & 0xffn);
42
+ bytes[3] = Number((timestamp >> 16n) & 0xffn);
43
+ bytes[4] = Number((timestamp >> 8n) & 0xffn);
44
+ bytes[5] = Number(timestamp & 0xffn);
45
+ // Bytes 6-15: 80 bits of random data (we'll then overlay version + variant).
46
+ const rand = (0, node_crypto_1.randomBytes)(10);
47
+ for (let i = 0; i < 10; i++) {
48
+ bytes[6 + i] = rand[i];
49
+ }
50
+ // Set version bits (high nibble of byte 6 to 0111 = 7).
51
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
52
+ // Set variant bits (high two bits of byte 8 to 10).
53
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
54
+ // Format as canonical UUID string (8-4-4-4-12).
55
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
56
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
57
+ }
58
+ /**
59
+ * Generate a prefixed canonical entity ID per ADR-0002. Convenience wrapper
60
+ * that combines `uuidv7()` with the prefix.
61
+ *
62
+ * @example
63
+ * prefixedId("per_") // "per_018f3c2d-4a5b-7c8d-9e0f-1a2b3c4d5e6f"
64
+ */
65
+ function prefixedId(prefix) {
66
+ if (!prefix.endsWith("_")) {
67
+ throw new Error(`prefixedId: prefix '${prefix}' must end with '_' per ADR-0002`);
68
+ }
69
+ return `${prefix}${uuidv7()}`;
70
+ }
71
+ /**
72
+ * Validate the canonical UUID v7 form (lowercase hex, dashed, version
73
+ * nibble = 7, variant nibble = 8/9/a/b). Useful at storage boundaries.
74
+ */
75
+ const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
76
+ function isUuidV7(value) {
77
+ return UUID_V7_PATTERN.test(value);
78
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Postgres search_path config helper.
3
+ *
4
+ * Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
5
+ * package carries no dependency on the platform repo's
6
+ * `lib/db/pg-search-path.ts`. Kept byte-for-byte in step with that source;
7
+ * the implementation is dependency-free (only the WHATWG `URL`), so
8
+ * vendoring is a literal copy.
9
+ *
10
+ * Prisma treats `?schema=<name>` in a Postgres URL as the target schema.
11
+ * node-postgres does not. Dispatcher consumers use raw pg pools so they can
12
+ * poll producer schemas without Prisma's generated schema qualification, so
13
+ * they must translate Prisma's URL convention into Postgres search_path.
14
+ *
15
+ * Caveat: Supabase's pgbouncer at port 6543 (transaction-mode pooling) silently
16
+ * drops the `options` startup parameter. Setting search_path via the libpq
17
+ * `options=-c search_path=...` mechanism does not survive the pooler. The
18
+ * options field stays here for direct-connection (port 5432) callers, but the
19
+ * caller MUST NOT rely on it alone — for pooler DSNs, schema-qualify the SQL
20
+ * queries explicitly using the `schema` field returned here.
21
+ */
22
+ export type PgSearchPathConfig = {
23
+ connectionString: string | undefined;
24
+ /** libpq startup options. Effective on direct connections; dropped by pgbouncer. */
25
+ options?: string;
26
+ /**
27
+ * The schema name parsed from the DSN's `?schema=<name>` query param.
28
+ * Null if absent or `public`. Callers schema-qualify their SQL with this
29
+ * value (e.g., `"${schema}".dispatcher_cursor`) so the query works regardless
30
+ * of whether the pooler dropped the search_path option.
31
+ */
32
+ schema: string | null;
33
+ };
34
+ export declare function pgConfigWithSearchPath(connectionString: string | undefined): PgSearchPathConfig;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Postgres search_path config helper.
4
+ *
5
+ * Vendored into the dispatcher SDK so the published `@sguild/dispatcher`
6
+ * package carries no dependency on the platform repo's
7
+ * `lib/db/pg-search-path.ts`. Kept byte-for-byte in step with that source;
8
+ * the implementation is dependency-free (only the WHATWG `URL`), so
9
+ * vendoring is a literal copy.
10
+ *
11
+ * Prisma treats `?schema=<name>` in a Postgres URL as the target schema.
12
+ * node-postgres does not. Dispatcher consumers use raw pg pools so they can
13
+ * poll producer schemas without Prisma's generated schema qualification, so
14
+ * they must translate Prisma's URL convention into Postgres search_path.
15
+ *
16
+ * Caveat: Supabase's pgbouncer at port 6543 (transaction-mode pooling) silently
17
+ * drops the `options` startup parameter. Setting search_path via the libpq
18
+ * `options=-c search_path=...` mechanism does not survive the pooler. The
19
+ * options field stays here for direct-connection (port 5432) callers, but the
20
+ * caller MUST NOT rely on it alone — for pooler DSNs, schema-qualify the SQL
21
+ * queries explicitly using the `schema` field returned here.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.pgConfigWithSearchPath = pgConfigWithSearchPath;
25
+ function pgConfigWithSearchPath(connectionString) {
26
+ if (!connectionString) {
27
+ return { connectionString, schema: null };
28
+ }
29
+ const schema = readSchemaParam(connectionString);
30
+ if (!schema) {
31
+ return { connectionString, schema: null };
32
+ }
33
+ return {
34
+ connectionString,
35
+ options: `-c search_path=${[schema, "public", "extensions"].join(",")}`,
36
+ schema,
37
+ };
38
+ }
39
+ function readSchemaParam(connectionString) {
40
+ let url;
41
+ try {
42
+ url = new URL(connectionString);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ const schema = url.searchParams.get("schema")?.trim();
48
+ if (!schema || schema === "public") {
49
+ return null;
50
+ }
51
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {
52
+ throw new Error(`Invalid Postgres schema name in connection URL: ${schema}`);
53
+ }
54
+ return schema;
55
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Resolve a path inside the bundled contracts tree to an absolute path.
3
+ *
4
+ * The dispatcher SDK ships the contracts tree (`contracts/`) bundled inside
5
+ * the `@sguild/dispatcher` package so the registry manifest and the JSON
6
+ * Schemas are available at runtime with no dependency on a sibling
7
+ * coordination repo.
8
+ *
9
+ * `contractsRelPath` is a path rooted at `contracts/` exactly as the
10
+ * coordination repo stores it (e.g. `contracts/event-types-registry.json`,
11
+ * or a registry `payload_schema` value like
12
+ * `contracts/<contract>/schema/payloads/<event_type>-v<n>.json`).
13
+ *
14
+ * Resolution order (first existing path wins):
15
+ *
16
+ * 1. `priorityPaths` — explicit caller overrides and test fixtures,
17
+ * resolved against the process cwd (absolute paths pass through).
18
+ * 2. Module-relative — `moduleDir` (the caller's `__dirname`) plus the
19
+ * relative path, and one level up. Reliable under plain Node / tsx,
20
+ * which is how the consumer-side subscriber worker runs.
21
+ * 3. cwd-relative defaults — covers the cases where `__dirname` is
22
+ * unreliable because a bundler (e.g. Next.js) rewrote it in a server
23
+ * bundle. `lib/dispatcher/<rel>` is the in-platform-repo source
24
+ * layout; `node_modules/@sguild/dispatcher/<rel>` is the published
25
+ * package; `../coordination/<rel>` is a sibling coordination repo.
26
+ * 4. `fallbackPaths` — last-resort caller overrides.
27
+ *
28
+ * Note for consumers whose producer path runs inside a bundled server
29
+ * (Next.js route handlers): ensure the deploy ships the package's
30
+ * `contracts/` directory. Next.js does not trace `readFileSync` data files
31
+ * automatically; add `node_modules/@sguild/dispatcher/contracts/**` to
32
+ * `outputFileTracingIncludes` (or equivalent) so the cwd-relative
33
+ * `node_modules/@sguild/dispatcher` candidate resolves at runtime.
34
+ */
35
+ export type ResolveContractPathOptions = {
36
+ /** Tried before any default location. Explicit caller overrides, test fixtures. */
37
+ priorityPaths?: string[];
38
+ /** Tried after all default locations. Last-resort caller overrides. */
39
+ fallbackPaths?: string[];
40
+ };
41
+ export declare function resolveContractPath(moduleDir: string, contractsRelPath: string, options?: ResolveContractPathOptions): string;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Resolve a path inside the bundled contracts tree to an absolute path.
4
+ *
5
+ * The dispatcher SDK ships the contracts tree (`contracts/`) bundled inside
6
+ * the `@sguild/dispatcher` package so the registry manifest and the JSON
7
+ * Schemas are available at runtime with no dependency on a sibling
8
+ * coordination repo.
9
+ *
10
+ * `contractsRelPath` is a path rooted at `contracts/` exactly as the
11
+ * coordination repo stores it (e.g. `contracts/event-types-registry.json`,
12
+ * or a registry `payload_schema` value like
13
+ * `contracts/<contract>/schema/payloads/<event_type>-v<n>.json`).
14
+ *
15
+ * Resolution order (first existing path wins):
16
+ *
17
+ * 1. `priorityPaths` — explicit caller overrides and test fixtures,
18
+ * resolved against the process cwd (absolute paths pass through).
19
+ * 2. Module-relative — `moduleDir` (the caller's `__dirname`) plus the
20
+ * relative path, and one level up. Reliable under plain Node / tsx,
21
+ * which is how the consumer-side subscriber worker runs.
22
+ * 3. cwd-relative defaults — covers the cases where `__dirname` is
23
+ * unreliable because a bundler (e.g. Next.js) rewrote it in a server
24
+ * bundle. `lib/dispatcher/<rel>` is the in-platform-repo source
25
+ * layout; `node_modules/@sguild/dispatcher/<rel>` is the published
26
+ * package; `../coordination/<rel>` is a sibling coordination repo.
27
+ * 4. `fallbackPaths` — last-resort caller overrides.
28
+ *
29
+ * Note for consumers whose producer path runs inside a bundled server
30
+ * (Next.js route handlers): ensure the deploy ships the package's
31
+ * `contracts/` directory. Next.js does not trace `readFileSync` data files
32
+ * automatically; add `node_modules/@sguild/dispatcher/contracts/**` to
33
+ * `outputFileTracingIncludes` (or equivalent) so the cwd-relative
34
+ * `node_modules/@sguild/dispatcher` candidate resolves at runtime.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.resolveContractPath = resolveContractPath;
38
+ const node_fs_1 = require("node:fs");
39
+ const node_path_1 = require("node:path");
40
+ /**
41
+ * cwd-relative base directories the bundled contracts tree may live under,
42
+ * tried after the module-relative candidates.
43
+ */
44
+ const CWD_RELATIVE_BASES = [
45
+ "lib/dispatcher", // in-platform-repo source consumption
46
+ "node_modules/@sguild/dispatcher", // published-package consumption
47
+ "../coordination", // checkout with a sibling coordination repo
48
+ ];
49
+ function resolveContractPath(moduleDir, contractsRelPath, options = {}) {
50
+ const priorityPaths = options.priorityPaths ?? [];
51
+ const candidates = [
52
+ ...priorityPaths.map((p) => (0, node_path_1.resolve)(process.cwd(), p)),
53
+ (0, node_path_1.join)(moduleDir, contractsRelPath), // in-repo source layout
54
+ (0, node_path_1.join)(moduleDir, "..", contractsRelPath), // published dist/ layout
55
+ ...CWD_RELATIVE_BASES.map((base) => (0, node_path_1.resolve)(process.cwd(), base, contractsRelPath)),
56
+ ...(options.fallbackPaths ?? []).map((p) => (0, node_path_1.resolve)(process.cwd(), p)),
57
+ ];
58
+ for (const candidate of candidates) {
59
+ if ((0, node_fs_1.existsSync)(candidate))
60
+ return candidate;
61
+ }
62
+ // Nothing matched; return the first module-relative candidate so the
63
+ // caller's readFileSync throws an error naming the expected location.
64
+ return candidates[priorityPaths.length];
65
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Dispatcher observability hooks.
3
+ *
4
+ * Phase 3 intentionally exposes a small adapter rather than binding the
5
+ * SDK to a metrics vendor. Deployments can bridge these hooks to
6
+ * Prometheus, OpenTelemetry, Datadog, logs, or a domain-local test probe.
7
+ * The dispatcher never lets hook failures affect delivery.
8
+ */
9
+ export type DispatcherMetricName = "dispatcher.publish.count" | "dispatcher.consume.count" | "dispatcher.dedup_hit.count" | "dispatcher.dead_letter.count";
10
+ export type DispatcherHistogramName = "dispatcher.end_to_end_latency_ms" | "dispatcher.handler_latency_ms";
11
+ export type DispatcherMetricLabels = {
12
+ event_type: string;
13
+ consumer?: string;
14
+ producer?: string;
15
+ schema_version?: string;
16
+ };
17
+ export type DispatcherObservabilityHooks = {
18
+ increment?: (name: DispatcherMetricName, value: number, labels: DispatcherMetricLabels) => void;
19
+ observe?: (name: DispatcherHistogramName, valueMs: number, labels: DispatcherMetricLabels) => void;
20
+ };
21
+ export declare function configureDispatcherObservability(next: DispatcherObservabilityHooks): void;
22
+ export declare function resetDispatcherObservabilityForTests(): void;
23
+ export declare function recordDispatcherIncrement(name: DispatcherMetricName, labels: DispatcherMetricLabels, value?: number): void;
24
+ export declare function recordDispatcherObservation(name: DispatcherHistogramName, valueMs: number, labels: DispatcherMetricLabels): void;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Dispatcher observability hooks.
4
+ *
5
+ * Phase 3 intentionally exposes a small adapter rather than binding the
6
+ * SDK to a metrics vendor. Deployments can bridge these hooks to
7
+ * Prometheus, OpenTelemetry, Datadog, logs, or a domain-local test probe.
8
+ * The dispatcher never lets hook failures affect delivery.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.configureDispatcherObservability = configureDispatcherObservability;
12
+ exports.resetDispatcherObservabilityForTests = resetDispatcherObservabilityForTests;
13
+ exports.recordDispatcherIncrement = recordDispatcherIncrement;
14
+ exports.recordDispatcherObservation = recordDispatcherObservation;
15
+ let hooks = {};
16
+ function configureDispatcherObservability(next) {
17
+ hooks = next;
18
+ }
19
+ function resetDispatcherObservabilityForTests() {
20
+ hooks = {};
21
+ }
22
+ function recordDispatcherIncrement(name, labels, value = 1) {
23
+ try {
24
+ hooks.increment?.(name, value, labels);
25
+ }
26
+ catch (e) {
27
+ console.warn(`dispatcher observability increment hook failed for ${name}: ${e instanceof Error ? e.message : String(e)}`);
28
+ }
29
+ }
30
+ function recordDispatcherObservation(name, valueMs, labels) {
31
+ try {
32
+ hooks.observe?.(name, valueMs, labels);
33
+ }
34
+ catch (e) {
35
+ console.warn(`dispatcher observability observe hook failed for ${name}: ${e instanceof Error ? e.message : String(e)}`);
36
+ }
37
+ }