@campfire-interactive/platform-events 0.1.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.
@@ -0,0 +1,19 @@
1
+ import { Envelope } from './envelope';
2
+ import { MasterDataEventType, MasterDataPayload } from './events/master-data';
3
+ /**
4
+ * Consumer-side helpers (ADR §10: consumers need the types as much as
5
+ * producers). The eventId dedup helper and replay-drop default land in
6
+ * plan Phase 1b.
7
+ */
8
+ /** Parse + validate the envelope of any message off the bus (payload untyped). */
9
+ export declare function parseEnvelope(detail: unknown): Envelope;
10
+ export interface MasterDataEvent<T extends MasterDataEventType = MasterDataEventType> {
11
+ type: T;
12
+ envelope: Envelope<MasterDataPayload<T>>;
13
+ }
14
+ export declare function isMasterDataEventType(type: string): type is MasterDataEventType;
15
+ /**
16
+ * Parse a message known to come from the master-data catalog; validates
17
+ * both envelope and payload and narrows the payload type on `type`.
18
+ */
19
+ export declare function parseMasterDataEvent(detail: unknown): MasterDataEvent;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseEnvelope = parseEnvelope;
4
+ exports.isMasterDataEventType = isMasterDataEventType;
5
+ exports.parseMasterDataEvent = parseMasterDataEvent;
6
+ const envelope_1 = require("./envelope");
7
+ const master_data_1 = require("./events/master-data");
8
+ /**
9
+ * Consumer-side helpers (ADR §10: consumers need the types as much as
10
+ * producers). The eventId dedup helper and replay-drop default land in
11
+ * plan Phase 1b.
12
+ */
13
+ /** Parse + validate the envelope of any message off the bus (payload untyped). */
14
+ function parseEnvelope(detail) {
15
+ const raw = typeof detail === 'string' ? JSON.parse(detail) : detail;
16
+ return envelope_1.envelopeSchema.parse(raw);
17
+ }
18
+ function isMasterDataEventType(type) {
19
+ return type in master_data_1.masterDataEvents;
20
+ }
21
+ /**
22
+ * Parse a message known to come from the master-data catalog; validates
23
+ * both envelope and payload and narrows the payload type on `type`.
24
+ */
25
+ function parseMasterDataEvent(detail) {
26
+ const envelope = parseEnvelope(detail);
27
+ if (!isMasterDataEventType(envelope.type)) {
28
+ throw new Error(`platform-events: '${envelope.type}' is not a catalogued master-data event`);
29
+ }
30
+ const def = master_data_1.masterDataEvents[envelope.type];
31
+ const payload = def.payload.parse(envelope.payload);
32
+ return {
33
+ type: envelope.type,
34
+ envelope: { ...envelope, payload },
35
+ };
36
+ }
@@ -0,0 +1,59 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { Envelope } from './envelope';
3
+ /**
4
+ * Consumer-side eventId dedup (ADR open problem 1, resolution per plan
5
+ * decision 6.1): a shared store claims `consumerApp#eventId` BEFORE side
6
+ * effects run, so at-least-once delivery doesn't double-execute them.
7
+ *
8
+ * Apps with natural state-machine idempotency (e.g. OMSF's
9
+ * processRoutedQuote status guard) may skip this — the obligation is
10
+ * idempotency, not this table.
11
+ */
12
+ export interface DedupStore {
13
+ /**
14
+ * Atomically claim an eventId for a consumer. Returns true if this is
15
+ * the first claim (proceed), false if already claimed (skip).
16
+ */
17
+ claim(consumerApp: string, eventId: string): Promise<boolean>;
18
+ }
19
+ export interface DynamoDbDedupStoreOptions {
20
+ /** e.g. 'platform-events-processed' — provisioned per environment in Phase 2 infra */
21
+ tableName: string;
22
+ client?: DynamoDBClient;
23
+ /** Retention for claims; default 14 days. */
24
+ ttlSeconds?: number;
25
+ /** Test seam; defaults to Date.now. */
26
+ now?: () => number;
27
+ }
28
+ /**
29
+ * DynamoDB-backed store: conditional put on `pk = <consumerApp>#<eventId>`
30
+ * with a TTL attribute. Table schema: pk (S, partition key), ttl (N,
31
+ * enabled as the TTL attribute).
32
+ */
33
+ export declare class DynamoDbDedupStore implements DedupStore {
34
+ private readonly client;
35
+ private readonly tableName;
36
+ private readonly ttlSeconds;
37
+ private readonly now;
38
+ constructor(opts: DynamoDbDedupStoreOptions);
39
+ claim(consumerApp: string, eventId: string): Promise<boolean>;
40
+ }
41
+ /** True when the message was re-emitted by the replay runbook (Phase 6). */
42
+ export declare function isReplay(envelope: Envelope): boolean;
43
+ export type HandleResult = 'handled' | 'duplicate' | 'replay-dropped';
44
+ export interface WithIdempotencyOptions {
45
+ /**
46
+ * Whether replayed messages run the handler. Default false (ADR open
47
+ * problem 2: non-idempotent side effects must not double-execute on
48
+ * archive replay) — opt in per handler for replay-safe event types.
49
+ */
50
+ allowReplay?: boolean;
51
+ }
52
+ /**
53
+ * Claim-then-handle wrapper. Skips duplicates and (by default) replays.
54
+ * If the handler throws AFTER the claim, the message redelivers via SQS
55
+ * but the claim stands — so handlers must put their own side effects
56
+ * behind the claim only when they are all-or-nothing; otherwise rely on
57
+ * state-machine idempotency for the partial-failure window.
58
+ */
59
+ export declare function withIdempotency(store: DedupStore, consumerApp: string, envelope: Envelope, handler: () => Promise<void>, opts?: WithIdempotencyOptions): Promise<HandleResult>;
package/dist/dedup.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DynamoDbDedupStore = void 0;
4
+ exports.isReplay = isReplay;
5
+ exports.withIdempotency = withIdempotency;
6
+ const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
7
+ const DEFAULT_TTL_SECONDS = 14 * 24 * 60 * 60;
8
+ /**
9
+ * DynamoDB-backed store: conditional put on `pk = <consumerApp>#<eventId>`
10
+ * with a TTL attribute. Table schema: pk (S, partition key), ttl (N,
11
+ * enabled as the TTL attribute).
12
+ */
13
+ class DynamoDbDedupStore {
14
+ client;
15
+ tableName;
16
+ ttlSeconds;
17
+ now;
18
+ constructor(opts) {
19
+ this.client = opts.client ?? new client_dynamodb_1.DynamoDBClient({});
20
+ this.tableName = opts.tableName;
21
+ this.ttlSeconds = opts.ttlSeconds ?? DEFAULT_TTL_SECONDS;
22
+ this.now = opts.now ?? Date.now;
23
+ }
24
+ async claim(consumerApp, eventId) {
25
+ try {
26
+ await this.client.send(new client_dynamodb_1.PutItemCommand({
27
+ TableName: this.tableName,
28
+ Item: {
29
+ pk: { S: `${consumerApp}#${eventId}` },
30
+ ttl: { N: String(Math.floor(this.now() / 1000) + this.ttlSeconds) },
31
+ },
32
+ ConditionExpression: 'attribute_not_exists(pk)',
33
+ }));
34
+ return true;
35
+ }
36
+ catch (err) {
37
+ if (err instanceof client_dynamodb_1.ConditionalCheckFailedException)
38
+ return false;
39
+ throw err;
40
+ }
41
+ }
42
+ }
43
+ exports.DynamoDbDedupStore = DynamoDbDedupStore;
44
+ /** True when the message was re-emitted by the replay runbook (Phase 6). */
45
+ function isReplay(envelope) {
46
+ return envelope.replay === true;
47
+ }
48
+ /**
49
+ * Claim-then-handle wrapper. Skips duplicates and (by default) replays.
50
+ * If the handler throws AFTER the claim, the message redelivers via SQS
51
+ * but the claim stands — so handlers must put their own side effects
52
+ * behind the claim only when they are all-or-nothing; otherwise rely on
53
+ * state-machine idempotency for the partial-failure window.
54
+ */
55
+ async function withIdempotency(store, consumerApp, envelope, handler, opts = {}) {
56
+ if (isReplay(envelope) && !opts.allowReplay)
57
+ return 'replay-dropped';
58
+ const firstClaim = await store.claim(consumerApp, envelope.eventId);
59
+ if (!firstClaim)
60
+ return 'duplicate';
61
+ await handler();
62
+ return 'handled';
63
+ }
@@ -0,0 +1,231 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Envelope v2 — ADR 2026-06-05 §6 ("A mandatory event/command envelope").
4
+ *
5
+ * schemaVersion 1 is the shape master-data publishes on the legacy
6
+ * `master-data-events` bus ({ schemaVersion: 1, eventId, tenantId,
7
+ * entityType, action, actor, timestamp, payload }). It is NOT modeled
8
+ * here: v1 keeps flowing untouched on the legacy bus during the
9
+ * dual-publish window, and consumers migrate envelope shape at the same
10
+ * moment they migrate buses (plan decision 2).
11
+ */
12
+ /**
13
+ * Known producers. Matches master-data's deployed `EventCallerService`
14
+ * union — hyphenated `master-data`, settled at ratification (plan
15
+ * decision 3; the ADR uses this spelling).
16
+ */
17
+ export declare const PRODUCERS: readonly ["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"];
18
+ export declare const producerSchema: z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>;
19
+ export type Producer = z.infer<typeof producerSchema>;
20
+ export declare const ENVELOPE_SCHEMA_VERSION: 2;
21
+ /** Envelope with an unvalidated payload — use `envelopeFor(schema)` when the payload shape is known. */
22
+ export declare const envelopeSchema: z.ZodObject<{
23
+ schemaVersion: z.ZodLiteral<2>;
24
+ /** uuid — the idempotency/dedupe key */
25
+ eventId: z.ZodString;
26
+ /** ISO-8601 instant the state change committed at the producer */
27
+ occurredAt: z.ZodString;
28
+ producer: z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>;
29
+ /** = EventBridge detail-type, `<entity>.<action>` (or `<verb>.<noun>` for commands) */
30
+ type: z.ZodString;
31
+ entityType: z.ZodString;
32
+ /** id of the affected entity (the survivor id for merge events) */
33
+ entityId: z.ZodString;
34
+ /**
35
+ * Monotonic per-entity version for consumer-side ordering (§5).
36
+ * Optional in 0.x because master-data does not yet track one; becomes
37
+ * required before 1.0.0 — producers derive it (e.g. updatedAt epoch ms)
38
+ * if they have no version column. See plan Phase 2.
39
+ */
40
+ entityVersion: z.ZodOptional<z.ZodNumber>;
41
+ /** null only for platform-global entities (region, part_family) */
42
+ tenantId: z.ZodNullable<z.ZodString>;
43
+ /** acting user, when the change was user-initiated */
44
+ userId: z.ZodOptional<z.ZodString>;
45
+ /** commands only: the app whose inbox this is routed to */
46
+ recipient: z.ZodOptional<z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>>;
47
+ /** commands only: async request/reply correlation */
48
+ correlationId: z.ZodOptional<z.ZodString>;
49
+ /** commands only: sender's inbox identifier for the reply */
50
+ replyTo: z.ZodOptional<z.ZodString>;
51
+ /** saga correlation — auto-propagated by the SDK across a choreographed chain */
52
+ sagaId: z.ZodOptional<z.ZodString>;
53
+ /** eventId of the message whose handling caused this one */
54
+ causationId: z.ZodOptional<z.ZodString>;
55
+ /** marks a saga compensating ("undo") event */
56
+ compensation: z.ZodOptional<z.ZodBoolean>;
57
+ /** stamped by the replay runbook's transformer; non-idempotent handlers drop these */
58
+ replay: z.ZodOptional<z.ZodBoolean>;
59
+ /** distributed-tracing id, producer → bus → queue → consumer → browser */
60
+ traceId: z.ZodString;
61
+ } & {
62
+ payload: z.ZodUnknown;
63
+ }, "strip", z.ZodTypeAny, {
64
+ type: string;
65
+ schemaVersion: 2;
66
+ eventId: string;
67
+ occurredAt: string;
68
+ producer: "master-data" | "cpq" | "omsf" | "pim" | "erfq" | "forecast" | "identity";
69
+ entityType: string;
70
+ entityId: string;
71
+ tenantId: string | null;
72
+ traceId: string;
73
+ entityVersion?: number | undefined;
74
+ userId?: string | undefined;
75
+ recipient?: "master-data" | "cpq" | "omsf" | "pim" | "erfq" | "forecast" | "identity" | undefined;
76
+ correlationId?: string | undefined;
77
+ replyTo?: string | undefined;
78
+ sagaId?: string | undefined;
79
+ causationId?: string | undefined;
80
+ compensation?: boolean | undefined;
81
+ replay?: boolean | undefined;
82
+ payload?: unknown;
83
+ }, {
84
+ type: string;
85
+ schemaVersion: 2;
86
+ eventId: string;
87
+ occurredAt: string;
88
+ producer: "master-data" | "cpq" | "omsf" | "pim" | "erfq" | "forecast" | "identity";
89
+ entityType: string;
90
+ entityId: string;
91
+ tenantId: string | null;
92
+ traceId: string;
93
+ entityVersion?: number | undefined;
94
+ userId?: string | undefined;
95
+ recipient?: "master-data" | "cpq" | "omsf" | "pim" | "erfq" | "forecast" | "identity" | undefined;
96
+ correlationId?: string | undefined;
97
+ replyTo?: string | undefined;
98
+ sagaId?: string | undefined;
99
+ causationId?: string | undefined;
100
+ compensation?: boolean | undefined;
101
+ replay?: boolean | undefined;
102
+ payload?: unknown;
103
+ }>;
104
+ export type Envelope<TPayload = unknown> = Omit<z.infer<typeof envelopeSchema>, 'payload'> & {
105
+ payload: TPayload;
106
+ };
107
+ /** Envelope schema with a typed, validated payload. */
108
+ export declare function envelopeFor<TSchema extends z.ZodTypeAny>(payload: TSchema): z.ZodObject<{
109
+ schemaVersion: z.ZodLiteral<2>;
110
+ /** uuid — the idempotency/dedupe key */
111
+ eventId: z.ZodString;
112
+ /** ISO-8601 instant the state change committed at the producer */
113
+ occurredAt: z.ZodString;
114
+ producer: z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>;
115
+ /** = EventBridge detail-type, `<entity>.<action>` (or `<verb>.<noun>` for commands) */
116
+ type: z.ZodString;
117
+ entityType: z.ZodString;
118
+ /** id of the affected entity (the survivor id for merge events) */
119
+ entityId: z.ZodString;
120
+ /**
121
+ * Monotonic per-entity version for consumer-side ordering (§5).
122
+ * Optional in 0.x because master-data does not yet track one; becomes
123
+ * required before 1.0.0 — producers derive it (e.g. updatedAt epoch ms)
124
+ * if they have no version column. See plan Phase 2.
125
+ */
126
+ entityVersion: z.ZodOptional<z.ZodNumber>;
127
+ /** null only for platform-global entities (region, part_family) */
128
+ tenantId: z.ZodNullable<z.ZodString>;
129
+ /** acting user, when the change was user-initiated */
130
+ userId: z.ZodOptional<z.ZodString>;
131
+ /** commands only: the app whose inbox this is routed to */
132
+ recipient: z.ZodOptional<z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>>;
133
+ /** commands only: async request/reply correlation */
134
+ correlationId: z.ZodOptional<z.ZodString>;
135
+ /** commands only: sender's inbox identifier for the reply */
136
+ replyTo: z.ZodOptional<z.ZodString>;
137
+ /** saga correlation — auto-propagated by the SDK across a choreographed chain */
138
+ sagaId: z.ZodOptional<z.ZodString>;
139
+ /** eventId of the message whose handling caused this one */
140
+ causationId: z.ZodOptional<z.ZodString>;
141
+ /** marks a saga compensating ("undo") event */
142
+ compensation: z.ZodOptional<z.ZodBoolean>;
143
+ /** stamped by the replay runbook's transformer; non-idempotent handlers drop these */
144
+ replay: z.ZodOptional<z.ZodBoolean>;
145
+ /** distributed-tracing id, producer → bus → queue → consumer → browser */
146
+ traceId: z.ZodString;
147
+ } & {
148
+ payload: TSchema;
149
+ }, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<{
150
+ schemaVersion: z.ZodLiteral<2>;
151
+ /** uuid — the idempotency/dedupe key */
152
+ eventId: z.ZodString;
153
+ /** ISO-8601 instant the state change committed at the producer */
154
+ occurredAt: z.ZodString;
155
+ producer: z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>;
156
+ /** = EventBridge detail-type, `<entity>.<action>` (or `<verb>.<noun>` for commands) */
157
+ type: z.ZodString;
158
+ entityType: z.ZodString;
159
+ /** id of the affected entity (the survivor id for merge events) */
160
+ entityId: z.ZodString;
161
+ /**
162
+ * Monotonic per-entity version for consumer-side ordering (§5).
163
+ * Optional in 0.x because master-data does not yet track one; becomes
164
+ * required before 1.0.0 — producers derive it (e.g. updatedAt epoch ms)
165
+ * if they have no version column. See plan Phase 2.
166
+ */
167
+ entityVersion: z.ZodOptional<z.ZodNumber>;
168
+ /** null only for platform-global entities (region, part_family) */
169
+ tenantId: z.ZodNullable<z.ZodString>;
170
+ /** acting user, when the change was user-initiated */
171
+ userId: z.ZodOptional<z.ZodString>;
172
+ /** commands only: the app whose inbox this is routed to */
173
+ recipient: z.ZodOptional<z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>>;
174
+ /** commands only: async request/reply correlation */
175
+ correlationId: z.ZodOptional<z.ZodString>;
176
+ /** commands only: sender's inbox identifier for the reply */
177
+ replyTo: z.ZodOptional<z.ZodString>;
178
+ /** saga correlation — auto-propagated by the SDK across a choreographed chain */
179
+ sagaId: z.ZodOptional<z.ZodString>;
180
+ /** eventId of the message whose handling caused this one */
181
+ causationId: z.ZodOptional<z.ZodString>;
182
+ /** marks a saga compensating ("undo") event */
183
+ compensation: z.ZodOptional<z.ZodBoolean>;
184
+ /** stamped by the replay runbook's transformer; non-idempotent handlers drop these */
185
+ replay: z.ZodOptional<z.ZodBoolean>;
186
+ /** distributed-tracing id, producer → bus → queue → consumer → browser */
187
+ traceId: z.ZodString;
188
+ } & {
189
+ payload: TSchema;
190
+ }>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<{
191
+ schemaVersion: z.ZodLiteral<2>;
192
+ /** uuid — the idempotency/dedupe key */
193
+ eventId: z.ZodString;
194
+ /** ISO-8601 instant the state change committed at the producer */
195
+ occurredAt: z.ZodString;
196
+ producer: z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>;
197
+ /** = EventBridge detail-type, `<entity>.<action>` (or `<verb>.<noun>` for commands) */
198
+ type: z.ZodString;
199
+ entityType: z.ZodString;
200
+ /** id of the affected entity (the survivor id for merge events) */
201
+ entityId: z.ZodString;
202
+ /**
203
+ * Monotonic per-entity version for consumer-side ordering (§5).
204
+ * Optional in 0.x because master-data does not yet track one; becomes
205
+ * required before 1.0.0 — producers derive it (e.g. updatedAt epoch ms)
206
+ * if they have no version column. See plan Phase 2.
207
+ */
208
+ entityVersion: z.ZodOptional<z.ZodNumber>;
209
+ /** null only for platform-global entities (region, part_family) */
210
+ tenantId: z.ZodNullable<z.ZodString>;
211
+ /** acting user, when the change was user-initiated */
212
+ userId: z.ZodOptional<z.ZodString>;
213
+ /** commands only: the app whose inbox this is routed to */
214
+ recipient: z.ZodOptional<z.ZodEnum<["master-data", "cpq", "omsf", "pim", "erfq", "forecast", "identity"]>>;
215
+ /** commands only: async request/reply correlation */
216
+ correlationId: z.ZodOptional<z.ZodString>;
217
+ /** commands only: sender's inbox identifier for the reply */
218
+ replyTo: z.ZodOptional<z.ZodString>;
219
+ /** saga correlation — auto-propagated by the SDK across a choreographed chain */
220
+ sagaId: z.ZodOptional<z.ZodString>;
221
+ /** eventId of the message whose handling caused this one */
222
+ causationId: z.ZodOptional<z.ZodString>;
223
+ /** marks a saga compensating ("undo") event */
224
+ compensation: z.ZodOptional<z.ZodBoolean>;
225
+ /** stamped by the replay runbook's transformer; non-idempotent handlers drop these */
226
+ replay: z.ZodOptional<z.ZodBoolean>;
227
+ /** distributed-tracing id, producer → bus → queue → consumer → browser */
228
+ traceId: z.ZodString;
229
+ } & {
230
+ payload: TSchema;
231
+ }> extends infer T_1 ? { [k_1 in keyof T_1]: T_1[k_1]; } : never>;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.envelopeSchema = exports.ENVELOPE_SCHEMA_VERSION = exports.producerSchema = exports.PRODUCERS = void 0;
4
+ exports.envelopeFor = envelopeFor;
5
+ const zod_1 = require("zod");
6
+ /**
7
+ * Envelope v2 — ADR 2026-06-05 §6 ("A mandatory event/command envelope").
8
+ *
9
+ * schemaVersion 1 is the shape master-data publishes on the legacy
10
+ * `master-data-events` bus ({ schemaVersion: 1, eventId, tenantId,
11
+ * entityType, action, actor, timestamp, payload }). It is NOT modeled
12
+ * here: v1 keeps flowing untouched on the legacy bus during the
13
+ * dual-publish window, and consumers migrate envelope shape at the same
14
+ * moment they migrate buses (plan decision 2).
15
+ */
16
+ /**
17
+ * Known producers. Matches master-data's deployed `EventCallerService`
18
+ * union — hyphenated `master-data`, settled at ratification (plan
19
+ * decision 3; the ADR uses this spelling).
20
+ */
21
+ exports.PRODUCERS = [
22
+ 'master-data',
23
+ 'cpq',
24
+ 'omsf',
25
+ 'pim',
26
+ 'erfq',
27
+ 'forecast',
28
+ 'identity',
29
+ ];
30
+ exports.producerSchema = zod_1.z.enum(exports.PRODUCERS);
31
+ exports.ENVELOPE_SCHEMA_VERSION = 2;
32
+ const envelopeBase = zod_1.z.object({
33
+ schemaVersion: zod_1.z.literal(exports.ENVELOPE_SCHEMA_VERSION),
34
+ /** uuid — the idempotency/dedupe key */
35
+ eventId: zod_1.z.string().uuid(),
36
+ /** ISO-8601 instant the state change committed at the producer */
37
+ occurredAt: zod_1.z.string().datetime(),
38
+ producer: exports.producerSchema,
39
+ /** = EventBridge detail-type, `<entity>.<action>` (or `<verb>.<noun>` for commands) */
40
+ type: zod_1.z.string().min(1),
41
+ entityType: zod_1.z.string().min(1),
42
+ /** id of the affected entity (the survivor id for merge events) */
43
+ entityId: zod_1.z.string().min(1),
44
+ /**
45
+ * Monotonic per-entity version for consumer-side ordering (§5).
46
+ * Optional in 0.x because master-data does not yet track one; becomes
47
+ * required before 1.0.0 — producers derive it (e.g. updatedAt epoch ms)
48
+ * if they have no version column. See plan Phase 2.
49
+ */
50
+ entityVersion: zod_1.z.number().int().nonnegative().optional(),
51
+ /** null only for platform-global entities (region, part_family) */
52
+ tenantId: zod_1.z.string().nullable(),
53
+ /** acting user, when the change was user-initiated */
54
+ userId: zod_1.z.string().optional(),
55
+ /** commands only: the app whose inbox this is routed to */
56
+ recipient: exports.producerSchema.optional(),
57
+ /** commands only: async request/reply correlation */
58
+ correlationId: zod_1.z.string().optional(),
59
+ /** commands only: sender's inbox identifier for the reply */
60
+ replyTo: zod_1.z.string().optional(),
61
+ /** saga correlation — auto-propagated by the SDK across a choreographed chain */
62
+ sagaId: zod_1.z.string().optional(),
63
+ /** eventId of the message whose handling caused this one */
64
+ causationId: zod_1.z.string().optional(),
65
+ /** marks a saga compensating ("undo") event */
66
+ compensation: zod_1.z.boolean().optional(),
67
+ /** stamped by the replay runbook's transformer; non-idempotent handlers drop these */
68
+ replay: zod_1.z.boolean().optional(),
69
+ /** distributed-tracing id, producer → bus → queue → consumer → browser */
70
+ traceId: zod_1.z.string().min(1),
71
+ });
72
+ /** Envelope with an unvalidated payload — use `envelopeFor(schema)` when the payload shape is known. */
73
+ exports.envelopeSchema = envelopeBase.extend({ payload: zod_1.z.unknown() });
74
+ /** Envelope schema with a typed, validated payload. */
75
+ function envelopeFor(payload) {
76
+ return envelopeBase.extend({ payload });
77
+ }