@keemakr/event-sdk 0.2.0-dev-20261012160211
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/README.md +41 -0
- package/dist/apps/channels.d.ts +400 -0
- package/dist/apps/channels.js +180 -0
- package/dist/apps/define.d.ts +29 -0
- package/dist/apps/define.js +41 -0
- package/dist/apps/index.d.ts +16 -0
- package/dist/apps/index.js +27 -0
- package/dist/apps/smartlead.d.ts +52 -0
- package/dist/apps/smartlead.js +45 -0
- package/dist/index.d.ts +342 -0
- package/dist/index.js +576 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @keemakr/event-sdk
|
|
2
|
+
|
|
3
|
+
Declare, publish, and consume typed Keemakr Events without accepting provider traffic directly in a
|
|
4
|
+
Pack Runtime.
|
|
5
|
+
|
|
6
|
+
An Event key is owned by its Pack namespace and one positive integer contract version. Core remains
|
|
7
|
+
the authoritative validator and delivery ledger. Delivery is at least once, so direct external
|
|
8
|
+
effects must use the stable delivery identity for idempotency.
|
|
9
|
+
|
|
10
|
+
Consumer builds call `fetchEventContractArtifact` with a short-lived, tenant-scoped native access
|
|
11
|
+
token in CI. Core applies Pack visibility, and the SDK rejects any artifact whose owner, key,
|
|
12
|
+
version, or fingerprint differs from the build pin. Do not persist the access token in generated
|
|
13
|
+
artifacts.
|
|
14
|
+
|
|
15
|
+
## Runtime delivery endpoint
|
|
16
|
+
|
|
17
|
+
A consuming Pack mounts one SDK-owned HTTP adapter at `/events/v1/deliver`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { handleEventDeliveryRequest } from '@keemakr/event-sdk';
|
|
21
|
+
import { startReplenishment, retryIsolationProbe } from './consumers';
|
|
22
|
+
|
|
23
|
+
export function POST(request: Request) {
|
|
24
|
+
return handleEventDeliveryRequest(request, [startReplenishment, retryIsolationProbe], {
|
|
25
|
+
audience: requireEnv('EVENT_RUNTIME_ORIGIN'),
|
|
26
|
+
coreUrl: requireEnv('KEEMAKR_CORE_URL'),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The release manifest must set `runtime.event_url` to the full public endpoint, for example
|
|
32
|
+
`https://inventory-consumer-v1.example/events/v1/deliver`. The URL is an immutable release pin and
|
|
33
|
+
must be unique to that Pack release. A later release uses a different URL. The general
|
|
34
|
+
`runtime.url` remains mutable and does not retarget accepted Event deliveries.
|
|
35
|
+
|
|
36
|
+
`EVENT_RUNTIME_ORIGIN` is the URL origin used as the signed grant audience.
|
|
37
|
+
`KEE_CORE_JWKS_URL` locates Core's JWKS for grant verification. `KEEMAKR_CORE_URL` is the Core API
|
|
38
|
+
base used when a handler publishes an allowed child Event. The SDK compares tenant installation,
|
|
39
|
+
Pack version, and deployment revision values from the signed grant with the delivery envelope. A
|
|
40
|
+
release endpoint can therefore serve several tenant installations without tenant-specific runtime
|
|
41
|
+
configuration.
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A customer message that reached a Pack's inbound handler through a Channel
|
|
4
|
+
* Binding. Core owns these contracts: a Pack subscribes to them and never
|
|
5
|
+
* redefines a provider's own events (PRD R9).
|
|
6
|
+
*
|
|
7
|
+
* The payload is what the handler may see, already routed and checked. The
|
|
8
|
+
* provider's raw fields are not here: tenant, Contact, handler and authority
|
|
9
|
+
* come from Core configuration, never from a provider payload (PRD R14).
|
|
10
|
+
*/
|
|
11
|
+
declare const message: z.ZodObject<{
|
|
12
|
+
conversation_id: z.ZodUUID;
|
|
13
|
+
owner_epoch: z.ZodNumber;
|
|
14
|
+
message: z.ZodObject<{
|
|
15
|
+
id: z.ZodUUID;
|
|
16
|
+
text: z.ZodString;
|
|
17
|
+
received_at: z.ZodISODateTime;
|
|
18
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
19
|
+
media_type: z.ZodString;
|
|
20
|
+
byte_size: z.ZodNumber;
|
|
21
|
+
name: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strict>>>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
identity: z.ZodEnum<{
|
|
25
|
+
anonymous: "anonymous";
|
|
26
|
+
identified: "identified";
|
|
27
|
+
verified: "verified";
|
|
28
|
+
conflict: "conflict";
|
|
29
|
+
}>;
|
|
30
|
+
contact_id: z.ZodNullable<z.ZodString>;
|
|
31
|
+
new_conversation: z.ZodBoolean;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export type ChannelMessageReceived = z.infer<typeof message>;
|
|
34
|
+
/**
|
|
35
|
+
* An agent reply the provider accepted. Informational: the delivery that carries
|
|
36
|
+
* it permits no action, so a reply can never trigger another reply. Staff replies
|
|
37
|
+
* are not delivered, because a handler hears nothing while a person owns the
|
|
38
|
+
* conversation.
|
|
39
|
+
*/
|
|
40
|
+
declare const sentMessage: z.ZodObject<{
|
|
41
|
+
conversation_id: z.ZodUUID;
|
|
42
|
+
owner_epoch: z.ZodNumber;
|
|
43
|
+
message: z.ZodObject<{
|
|
44
|
+
id: z.ZodUUID;
|
|
45
|
+
text: z.ZodString;
|
|
46
|
+
sent_at: z.ZodISODateTime;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
identity: z.ZodEnum<{
|
|
49
|
+
anonymous: "anonymous";
|
|
50
|
+
identified: "identified";
|
|
51
|
+
verified: "verified";
|
|
52
|
+
conflict: "conflict";
|
|
53
|
+
}>;
|
|
54
|
+
contact_id: z.ZodNullable<z.ZodString>;
|
|
55
|
+
}, z.core.$strict>;
|
|
56
|
+
export type ChannelMessageSent = z.infer<typeof sentMessage>;
|
|
57
|
+
export declare const widgetEvents: {
|
|
58
|
+
readonly 'message.received': import("../index.js").EventContract<{
|
|
59
|
+
conversation_id: string;
|
|
60
|
+
owner_epoch: number;
|
|
61
|
+
message: {
|
|
62
|
+
id: string;
|
|
63
|
+
text: string;
|
|
64
|
+
received_at: string;
|
|
65
|
+
attachments?: {
|
|
66
|
+
media_type: string;
|
|
67
|
+
byte_size: number;
|
|
68
|
+
name?: string | undefined;
|
|
69
|
+
}[] | undefined;
|
|
70
|
+
};
|
|
71
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
72
|
+
contact_id: string | null;
|
|
73
|
+
new_conversation: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
readonly 'message.sent': import("../index.js").EventContract<{
|
|
76
|
+
conversation_id: string;
|
|
77
|
+
owner_epoch: number;
|
|
78
|
+
message: {
|
|
79
|
+
id: string;
|
|
80
|
+
text: string;
|
|
81
|
+
sent_at: string;
|
|
82
|
+
};
|
|
83
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
84
|
+
contact_id: string | null;
|
|
85
|
+
}>;
|
|
86
|
+
};
|
|
87
|
+
export declare const whatsappEvents: {
|
|
88
|
+
readonly 'message.received': import("../index.js").EventContract<{
|
|
89
|
+
conversation_id: string;
|
|
90
|
+
owner_epoch: number;
|
|
91
|
+
message: {
|
|
92
|
+
id: string;
|
|
93
|
+
text: string;
|
|
94
|
+
received_at: string;
|
|
95
|
+
attachments?: {
|
|
96
|
+
media_type: string;
|
|
97
|
+
byte_size: number;
|
|
98
|
+
name?: string | undefined;
|
|
99
|
+
}[] | undefined;
|
|
100
|
+
};
|
|
101
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
102
|
+
contact_id: string | null;
|
|
103
|
+
new_conversation: boolean;
|
|
104
|
+
}>;
|
|
105
|
+
readonly 'message.sent': import("../index.js").EventContract<{
|
|
106
|
+
conversation_id: string;
|
|
107
|
+
owner_epoch: number;
|
|
108
|
+
message: {
|
|
109
|
+
id: string;
|
|
110
|
+
text: string;
|
|
111
|
+
sent_at: string;
|
|
112
|
+
};
|
|
113
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
114
|
+
contact_id: string | null;
|
|
115
|
+
}>;
|
|
116
|
+
};
|
|
117
|
+
export declare const emailEvents: {
|
|
118
|
+
readonly 'message.received': import("../index.js").EventContract<{
|
|
119
|
+
conversation_id: string;
|
|
120
|
+
owner_epoch: number;
|
|
121
|
+
message: {
|
|
122
|
+
id: string;
|
|
123
|
+
text: string;
|
|
124
|
+
received_at: string;
|
|
125
|
+
attachments?: {
|
|
126
|
+
media_type: string;
|
|
127
|
+
byte_size: number;
|
|
128
|
+
name?: string | undefined;
|
|
129
|
+
}[] | undefined;
|
|
130
|
+
};
|
|
131
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
132
|
+
contact_id: string | null;
|
|
133
|
+
new_conversation: boolean;
|
|
134
|
+
}>;
|
|
135
|
+
readonly 'message.sent': import("../index.js").EventContract<{
|
|
136
|
+
conversation_id: string;
|
|
137
|
+
owner_epoch: number;
|
|
138
|
+
message: {
|
|
139
|
+
id: string;
|
|
140
|
+
text: string;
|
|
141
|
+
sent_at: string;
|
|
142
|
+
};
|
|
143
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
144
|
+
contact_id: string | null;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
export declare const smsEvents: {
|
|
148
|
+
readonly 'message.received': import("../index.js").EventContract<{
|
|
149
|
+
conversation_id: string;
|
|
150
|
+
owner_epoch: number;
|
|
151
|
+
message: {
|
|
152
|
+
id: string;
|
|
153
|
+
text: string;
|
|
154
|
+
received_at: string;
|
|
155
|
+
attachments?: {
|
|
156
|
+
media_type: string;
|
|
157
|
+
byte_size: number;
|
|
158
|
+
name?: string | undefined;
|
|
159
|
+
}[] | undefined;
|
|
160
|
+
};
|
|
161
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
162
|
+
contact_id: string | null;
|
|
163
|
+
new_conversation: boolean;
|
|
164
|
+
}>;
|
|
165
|
+
readonly 'message.sent': import("../index.js").EventContract<{
|
|
166
|
+
conversation_id: string;
|
|
167
|
+
owner_epoch: number;
|
|
168
|
+
message: {
|
|
169
|
+
id: string;
|
|
170
|
+
text: string;
|
|
171
|
+
sent_at: string;
|
|
172
|
+
};
|
|
173
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
174
|
+
contact_id: string | null;
|
|
175
|
+
}>;
|
|
176
|
+
};
|
|
177
|
+
/** The contract a binding on each channel delivers for a customer message. */
|
|
178
|
+
export declare const CHANNEL_MESSAGE_RECEIVED: {
|
|
179
|
+
readonly widget: import("../index.js").EventContract<{
|
|
180
|
+
conversation_id: string;
|
|
181
|
+
owner_epoch: number;
|
|
182
|
+
message: {
|
|
183
|
+
id: string;
|
|
184
|
+
text: string;
|
|
185
|
+
received_at: string;
|
|
186
|
+
attachments?: {
|
|
187
|
+
media_type: string;
|
|
188
|
+
byte_size: number;
|
|
189
|
+
name?: string | undefined;
|
|
190
|
+
}[] | undefined;
|
|
191
|
+
};
|
|
192
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
193
|
+
contact_id: string | null;
|
|
194
|
+
new_conversation: boolean;
|
|
195
|
+
}>;
|
|
196
|
+
readonly whatsapp: import("../index.js").EventContract<{
|
|
197
|
+
conversation_id: string;
|
|
198
|
+
owner_epoch: number;
|
|
199
|
+
message: {
|
|
200
|
+
id: string;
|
|
201
|
+
text: string;
|
|
202
|
+
received_at: string;
|
|
203
|
+
attachments?: {
|
|
204
|
+
media_type: string;
|
|
205
|
+
byte_size: number;
|
|
206
|
+
name?: string | undefined;
|
|
207
|
+
}[] | undefined;
|
|
208
|
+
};
|
|
209
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
210
|
+
contact_id: string | null;
|
|
211
|
+
new_conversation: boolean;
|
|
212
|
+
}>;
|
|
213
|
+
readonly email: import("../index.js").EventContract<{
|
|
214
|
+
conversation_id: string;
|
|
215
|
+
owner_epoch: number;
|
|
216
|
+
message: {
|
|
217
|
+
id: string;
|
|
218
|
+
text: string;
|
|
219
|
+
received_at: string;
|
|
220
|
+
attachments?: {
|
|
221
|
+
media_type: string;
|
|
222
|
+
byte_size: number;
|
|
223
|
+
name?: string | undefined;
|
|
224
|
+
}[] | undefined;
|
|
225
|
+
};
|
|
226
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
227
|
+
contact_id: string | null;
|
|
228
|
+
new_conversation: boolean;
|
|
229
|
+
}>;
|
|
230
|
+
readonly sms: import("../index.js").EventContract<{
|
|
231
|
+
conversation_id: string;
|
|
232
|
+
owner_epoch: number;
|
|
233
|
+
message: {
|
|
234
|
+
id: string;
|
|
235
|
+
text: string;
|
|
236
|
+
received_at: string;
|
|
237
|
+
attachments?: {
|
|
238
|
+
media_type: string;
|
|
239
|
+
byte_size: number;
|
|
240
|
+
name?: string | undefined;
|
|
241
|
+
}[] | undefined;
|
|
242
|
+
};
|
|
243
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
244
|
+
contact_id: string | null;
|
|
245
|
+
new_conversation: boolean;
|
|
246
|
+
}>;
|
|
247
|
+
};
|
|
248
|
+
/** The contract a binding on each channel delivers for an agent reply that went out. */
|
|
249
|
+
export declare const CHANNEL_MESSAGE_SENT: {
|
|
250
|
+
readonly widget: import("../index.js").EventContract<{
|
|
251
|
+
conversation_id: string;
|
|
252
|
+
owner_epoch: number;
|
|
253
|
+
message: {
|
|
254
|
+
id: string;
|
|
255
|
+
text: string;
|
|
256
|
+
sent_at: string;
|
|
257
|
+
};
|
|
258
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
259
|
+
contact_id: string | null;
|
|
260
|
+
}>;
|
|
261
|
+
readonly whatsapp: import("../index.js").EventContract<{
|
|
262
|
+
conversation_id: string;
|
|
263
|
+
owner_epoch: number;
|
|
264
|
+
message: {
|
|
265
|
+
id: string;
|
|
266
|
+
text: string;
|
|
267
|
+
sent_at: string;
|
|
268
|
+
};
|
|
269
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
270
|
+
contact_id: string | null;
|
|
271
|
+
}>;
|
|
272
|
+
readonly email: import("../index.js").EventContract<{
|
|
273
|
+
conversation_id: string;
|
|
274
|
+
owner_epoch: number;
|
|
275
|
+
message: {
|
|
276
|
+
id: string;
|
|
277
|
+
text: string;
|
|
278
|
+
sent_at: string;
|
|
279
|
+
};
|
|
280
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
281
|
+
contact_id: string | null;
|
|
282
|
+
}>;
|
|
283
|
+
readonly sms: import("../index.js").EventContract<{
|
|
284
|
+
conversation_id: string;
|
|
285
|
+
owner_epoch: number;
|
|
286
|
+
message: {
|
|
287
|
+
id: string;
|
|
288
|
+
text: string;
|
|
289
|
+
sent_at: string;
|
|
290
|
+
};
|
|
291
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
292
|
+
contact_id: string | null;
|
|
293
|
+
}>;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The person in a conversation proved a phone number or email address with a
|
|
297
|
+
* one-time code or link. The Contact is always included: proving the handle is
|
|
298
|
+
* what makes it visible under every identity rule.
|
|
299
|
+
*/
|
|
300
|
+
declare const verified: z.ZodObject<{
|
|
301
|
+
conversation_id: z.ZodUUID;
|
|
302
|
+
owner_epoch: z.ZodNumber;
|
|
303
|
+
contact_id: z.ZodString;
|
|
304
|
+
handle_kind: z.ZodEnum<{
|
|
305
|
+
email: "email";
|
|
306
|
+
phone: "phone";
|
|
307
|
+
}>;
|
|
308
|
+
method: z.ZodEnum<{
|
|
309
|
+
email_code: "email_code";
|
|
310
|
+
sms_code: "sms_code";
|
|
311
|
+
whatsapp_code: "whatsapp_code";
|
|
312
|
+
email_link: "email_link";
|
|
313
|
+
sms_link: "sms_link";
|
|
314
|
+
whatsapp_link: "whatsapp_link";
|
|
315
|
+
}>;
|
|
316
|
+
verified_at: z.ZodISODateTime;
|
|
317
|
+
}, z.core.$strict>;
|
|
318
|
+
export type ContactVerified = z.infer<typeof verified>;
|
|
319
|
+
export declare const contactEvents: {
|
|
320
|
+
readonly verified: import("../index.js").EventContract<{
|
|
321
|
+
conversation_id: string;
|
|
322
|
+
owner_epoch: number;
|
|
323
|
+
contact_id: string;
|
|
324
|
+
handle_kind: "email" | "phone";
|
|
325
|
+
method: "email_code" | "sms_code" | "whatsapp_code" | "email_link" | "sms_link" | "whatsapp_link";
|
|
326
|
+
verified_at: string;
|
|
327
|
+
}>;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* The conversation came back to the agent: a person gave it back, or a handoff
|
|
331
|
+
* the agent asked for ran out. Changes to a person are not delivered: a handler
|
|
332
|
+
* hears nothing while a person owns the conversation.
|
|
333
|
+
*/
|
|
334
|
+
declare const ownerChanged: z.ZodObject<{
|
|
335
|
+
conversation_id: z.ZodUUID;
|
|
336
|
+
owner_epoch: z.ZodNumber;
|
|
337
|
+
owner: z.ZodLiteral<"agent">;
|
|
338
|
+
reason: z.ZodEnum<{
|
|
339
|
+
given_back: "given_back";
|
|
340
|
+
expired: "expired";
|
|
341
|
+
}>;
|
|
342
|
+
changed_at: z.ZodISODateTime;
|
|
343
|
+
identity: z.ZodEnum<{
|
|
344
|
+
anonymous: "anonymous";
|
|
345
|
+
identified: "identified";
|
|
346
|
+
verified: "verified";
|
|
347
|
+
conflict: "conflict";
|
|
348
|
+
}>;
|
|
349
|
+
contact_id: z.ZodNullable<z.ZodString>;
|
|
350
|
+
}, z.core.$strict>;
|
|
351
|
+
export type ConversationOwnerChanged = z.infer<typeof ownerChanged>;
|
|
352
|
+
/**
|
|
353
|
+
* A staff member clicked one of the Pack's declared buttons in the inbox, such
|
|
354
|
+
* as "Send booking link". The turn that carries it acts as whoever owns the
|
|
355
|
+
* conversation at the click: the agent, or that staff member, whose replies
|
|
356
|
+
* then go out under their name.
|
|
357
|
+
*/
|
|
358
|
+
declare const actionRequested: z.ZodObject<{
|
|
359
|
+
conversation_id: z.ZodUUID;
|
|
360
|
+
owner_epoch: z.ZodNumber;
|
|
361
|
+
action_key: z.ZodString;
|
|
362
|
+
choice: z.ZodNullable<z.ZodString>;
|
|
363
|
+
requested_by: z.ZodUUID;
|
|
364
|
+
acting_as: z.ZodEnum<{
|
|
365
|
+
agent: "agent";
|
|
366
|
+
staff: "staff";
|
|
367
|
+
}>;
|
|
368
|
+
requested_at: z.ZodISODateTime;
|
|
369
|
+
identity: z.ZodEnum<{
|
|
370
|
+
anonymous: "anonymous";
|
|
371
|
+
identified: "identified";
|
|
372
|
+
verified: "verified";
|
|
373
|
+
conflict: "conflict";
|
|
374
|
+
}>;
|
|
375
|
+
contact_id: z.ZodNullable<z.ZodString>;
|
|
376
|
+
}, z.core.$strict>;
|
|
377
|
+
export type ConversationActionRequested = z.infer<typeof actionRequested>;
|
|
378
|
+
export declare const conversationEvents: {
|
|
379
|
+
readonly 'owner.changed': import("../index.js").EventContract<{
|
|
380
|
+
conversation_id: string;
|
|
381
|
+
owner_epoch: number;
|
|
382
|
+
owner: "agent";
|
|
383
|
+
reason: "given_back" | "expired";
|
|
384
|
+
changed_at: string;
|
|
385
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
386
|
+
contact_id: string | null;
|
|
387
|
+
}>;
|
|
388
|
+
readonly 'action.requested': import("../index.js").EventContract<{
|
|
389
|
+
conversation_id: string;
|
|
390
|
+
owner_epoch: number;
|
|
391
|
+
action_key: string;
|
|
392
|
+
choice: string | null;
|
|
393
|
+
requested_by: string;
|
|
394
|
+
acting_as: "agent" | "staff";
|
|
395
|
+
requested_at: string;
|
|
396
|
+
identity: "anonymous" | "identified" | "verified" | "conflict";
|
|
397
|
+
contact_id: string | null;
|
|
398
|
+
}>;
|
|
399
|
+
};
|
|
400
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineAppEvents } from './define.js';
|
|
3
|
+
/**
|
|
4
|
+
* A customer message that reached a Pack's inbound handler through a Channel
|
|
5
|
+
* Binding. Core owns these contracts: a Pack subscribes to them and never
|
|
6
|
+
* redefines a provider's own events (PRD R9).
|
|
7
|
+
*
|
|
8
|
+
* The payload is what the handler may see, already routed and checked. The
|
|
9
|
+
* provider's raw fields are not here: tenant, Contact, handler and authority
|
|
10
|
+
* come from Core configuration, never from a provider payload (PRD R14).
|
|
11
|
+
*/
|
|
12
|
+
const message = z.strictObject({
|
|
13
|
+
/** The Core conversation; the Pack's Eve session address. */
|
|
14
|
+
conversation_id: z.uuid(),
|
|
15
|
+
/** The ownership version the delivery was made under. */
|
|
16
|
+
owner_epoch: z.number().int().positive(),
|
|
17
|
+
message: z.strictObject({
|
|
18
|
+
/** Core's timeline entry id for the message. */
|
|
19
|
+
id: z.uuid(),
|
|
20
|
+
text: z.string().max(20000),
|
|
21
|
+
received_at: z.iso.datetime(),
|
|
22
|
+
attachments: z
|
|
23
|
+
.array(z.strictObject({
|
|
24
|
+
media_type: z.string().min(1).max(200),
|
|
25
|
+
byte_size: z.number().int().positive(),
|
|
26
|
+
name: z.string().max(500).optional(),
|
|
27
|
+
}))
|
|
28
|
+
.max(10)
|
|
29
|
+
.optional(),
|
|
30
|
+
}),
|
|
31
|
+
/** What Core knows about who is writing; the handler applies its own rule. */
|
|
32
|
+
identity: z.enum(['anonymous', 'identified', 'verified', 'conflict']),
|
|
33
|
+
contact_id: z.string().min(1).max(200).nullable(),
|
|
34
|
+
/** True for the first message of a new conversation, including after a rollover. */
|
|
35
|
+
new_conversation: z.boolean(),
|
|
36
|
+
});
|
|
37
|
+
const received = (channel) => ({
|
|
38
|
+
version: 1,
|
|
39
|
+
description: `A customer message arrived on a ${channel} channel bound to this handler.`,
|
|
40
|
+
schema: message,
|
|
41
|
+
sensitivity: 'personal',
|
|
42
|
+
subject: ['conversation_id'],
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* An agent reply the provider accepted. Informational: the delivery that carries
|
|
46
|
+
* it permits no action, so a reply can never trigger another reply. Staff replies
|
|
47
|
+
* are not delivered, because a handler hears nothing while a person owns the
|
|
48
|
+
* conversation.
|
|
49
|
+
*/
|
|
50
|
+
const sentMessage = z.strictObject({
|
|
51
|
+
conversation_id: z.uuid(),
|
|
52
|
+
owner_epoch: z.number().int().positive(),
|
|
53
|
+
message: z.strictObject({
|
|
54
|
+
/** Core's timeline entry id for the message. */
|
|
55
|
+
id: z.uuid(),
|
|
56
|
+
text: z.string().max(20000),
|
|
57
|
+
sent_at: z.iso.datetime(),
|
|
58
|
+
}),
|
|
59
|
+
identity: z.enum(['anonymous', 'identified', 'verified', 'conflict']),
|
|
60
|
+
contact_id: z.string().min(1).max(200).nullable(),
|
|
61
|
+
});
|
|
62
|
+
const sent = (channel) => ({
|
|
63
|
+
version: 1,
|
|
64
|
+
description: `An agent reply went out on a ${channel} channel bound to this handler.`,
|
|
65
|
+
schema: sentMessage,
|
|
66
|
+
sensitivity: 'personal',
|
|
67
|
+
subject: ['conversation_id'],
|
|
68
|
+
});
|
|
69
|
+
export const widgetEvents = defineAppEvents('widget', {
|
|
70
|
+
'message.received': received('website widget'),
|
|
71
|
+
'message.sent': sent('website widget'),
|
|
72
|
+
});
|
|
73
|
+
export const whatsappEvents = defineAppEvents('whatsapp', {
|
|
74
|
+
'message.received': received('WhatsApp'),
|
|
75
|
+
'message.sent': sent('WhatsApp'),
|
|
76
|
+
});
|
|
77
|
+
export const emailEvents = defineAppEvents('email', {
|
|
78
|
+
'message.received': received('email'),
|
|
79
|
+
'message.sent': sent('email'),
|
|
80
|
+
});
|
|
81
|
+
export const smsEvents = defineAppEvents('sms', {
|
|
82
|
+
'message.received': received('SMS'),
|
|
83
|
+
'message.sent': sent('SMS'),
|
|
84
|
+
});
|
|
85
|
+
/** The contract a binding on each channel delivers for a customer message. */
|
|
86
|
+
export const CHANNEL_MESSAGE_RECEIVED = {
|
|
87
|
+
widget: widgetEvents['message.received'],
|
|
88
|
+
whatsapp: whatsappEvents['message.received'],
|
|
89
|
+
email: emailEvents['message.received'],
|
|
90
|
+
sms: smsEvents['message.received'],
|
|
91
|
+
};
|
|
92
|
+
/** The contract a binding on each channel delivers for an agent reply that went out. */
|
|
93
|
+
export const CHANNEL_MESSAGE_SENT = {
|
|
94
|
+
widget: widgetEvents['message.sent'],
|
|
95
|
+
whatsapp: whatsappEvents['message.sent'],
|
|
96
|
+
email: emailEvents['message.sent'],
|
|
97
|
+
sms: smsEvents['message.sent'],
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The person in a conversation proved a phone number or email address with a
|
|
101
|
+
* one-time code or link. The Contact is always included: proving the handle is
|
|
102
|
+
* what makes it visible under every identity rule.
|
|
103
|
+
*/
|
|
104
|
+
const verified = z.strictObject({
|
|
105
|
+
conversation_id: z.uuid(),
|
|
106
|
+
owner_epoch: z.number().int().positive(),
|
|
107
|
+
contact_id: z.string().min(1).max(200),
|
|
108
|
+
handle_kind: z.enum(['email', 'phone']),
|
|
109
|
+
method: z.enum([
|
|
110
|
+
'email_code',
|
|
111
|
+
'sms_code',
|
|
112
|
+
'whatsapp_code',
|
|
113
|
+
'email_link',
|
|
114
|
+
'sms_link',
|
|
115
|
+
'whatsapp_link',
|
|
116
|
+
]),
|
|
117
|
+
verified_at: z.iso.datetime(),
|
|
118
|
+
});
|
|
119
|
+
export const contactEvents = defineAppEvents('contact', {
|
|
120
|
+
verified: {
|
|
121
|
+
version: 1,
|
|
122
|
+
description: 'The person in a conversation proved a phone number or email address.',
|
|
123
|
+
schema: verified,
|
|
124
|
+
sensitivity: 'personal',
|
|
125
|
+
subject: ['conversation_id'],
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* The conversation came back to the agent: a person gave it back, or a handoff
|
|
130
|
+
* the agent asked for ran out. Changes to a person are not delivered: a handler
|
|
131
|
+
* hears nothing while a person owns the conversation.
|
|
132
|
+
*/
|
|
133
|
+
const ownerChanged = z.strictObject({
|
|
134
|
+
conversation_id: z.uuid(),
|
|
135
|
+
/** The new ownership version; the turn that carries this event acts under it. */
|
|
136
|
+
owner_epoch: z.number().int().positive(),
|
|
137
|
+
owner: z.literal('agent'),
|
|
138
|
+
reason: z.enum(['given_back', 'expired']),
|
|
139
|
+
changed_at: z.iso.datetime(),
|
|
140
|
+
identity: z.enum(['anonymous', 'identified', 'verified', 'conflict']),
|
|
141
|
+
contact_id: z.string().min(1).max(200).nullable(),
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* A staff member clicked one of the Pack's declared buttons in the inbox, such
|
|
145
|
+
* as "Send booking link". The turn that carries it acts as whoever owns the
|
|
146
|
+
* conversation at the click: the agent, or that staff member, whose replies
|
|
147
|
+
* then go out under their name.
|
|
148
|
+
*/
|
|
149
|
+
const actionRequested = z.strictObject({
|
|
150
|
+
conversation_id: z.uuid(),
|
|
151
|
+
owner_epoch: z.number().int().positive(),
|
|
152
|
+
/** The key the Pack declared for the button. */
|
|
153
|
+
action_key: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/),
|
|
154
|
+
/** The option the staff member picked, when the button declares options. */
|
|
155
|
+
choice: z
|
|
156
|
+
.string()
|
|
157
|
+
.regex(/^[a-z][a-z0-9-]{0,39}$/)
|
|
158
|
+
.nullable(),
|
|
159
|
+
requested_by: z.uuid(),
|
|
160
|
+
acting_as: z.enum(['agent', 'staff']),
|
|
161
|
+
requested_at: z.iso.datetime(),
|
|
162
|
+
identity: z.enum(['anonymous', 'identified', 'verified', 'conflict']),
|
|
163
|
+
contact_id: z.string().min(1).max(200).nullable(),
|
|
164
|
+
});
|
|
165
|
+
export const conversationEvents = defineAppEvents('conversation', {
|
|
166
|
+
'owner.changed': {
|
|
167
|
+
version: 1,
|
|
168
|
+
description: 'The conversation came back to the agent.',
|
|
169
|
+
schema: ownerChanged,
|
|
170
|
+
sensitivity: 'personal',
|
|
171
|
+
subject: ['conversation_id'],
|
|
172
|
+
},
|
|
173
|
+
'action.requested': {
|
|
174
|
+
version: 1,
|
|
175
|
+
description: "A staff member clicked one of the Pack's buttons in the inbox.",
|
|
176
|
+
schema: actionRequested,
|
|
177
|
+
sensitivity: 'personal',
|
|
178
|
+
subject: ['conversation_id'],
|
|
179
|
+
},
|
|
180
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type EventContract, type EventSensitivity, type JsonSchema } from '../index.js';
|
|
3
|
+
export interface AppEventInput {
|
|
4
|
+
version: number;
|
|
5
|
+
description: string;
|
|
6
|
+
schema: z.ZodType;
|
|
7
|
+
sensitivity?: EventSensitivity;
|
|
8
|
+
/** Payload fields that name what the event is about; they order its deliveries. */
|
|
9
|
+
subject?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The JSON Schema a contract is published and fingerprinted with.
|
|
13
|
+
*
|
|
14
|
+
* Raw `z.toJSONSchema` output is not stable enough to fingerprint: it carries a
|
|
15
|
+
* `$schema` dialect line, and for a string format it adds the library's own
|
|
16
|
+
* validation pattern, both of which a zod release may change without the
|
|
17
|
+
* contract changing. They are dropped, so a fingerprint moves only when a field
|
|
18
|
+
* does. apps/contracts.lock.json turns any other drift into a failed check.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stableJsonSchema(schema: z.ZodType): JsonSchema;
|
|
21
|
+
/**
|
|
22
|
+
* Declares the normalized events of one Core App. The App name is the event
|
|
23
|
+
* family, so `defineAppEvents('smartlead', { 'email.opened': … })` declares
|
|
24
|
+
* `smartlead.email.opened`. Two events may share a schema: the name carries the
|
|
25
|
+
* meaning, the schema only the shape.
|
|
26
|
+
*/
|
|
27
|
+
export declare function defineAppEvents<const Events extends Record<string, AppEventInput>>(app: string, events: Events): {
|
|
28
|
+
readonly [Name in keyof Events]: EventContract<z.infer<Events[Name]['schema']>>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineEvent, } from '../index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The JSON Schema a contract is published and fingerprinted with.
|
|
5
|
+
*
|
|
6
|
+
* Raw `z.toJSONSchema` output is not stable enough to fingerprint: it carries a
|
|
7
|
+
* `$schema` dialect line, and for a string format it adds the library's own
|
|
8
|
+
* validation pattern, both of which a zod release may change without the
|
|
9
|
+
* contract changing. They are dropped, so a fingerprint moves only when a field
|
|
10
|
+
* does. apps/contracts.lock.json turns any other drift into a failed check.
|
|
11
|
+
*/
|
|
12
|
+
export function stableJsonSchema(schema) {
|
|
13
|
+
const { $schema: _dialect, ...jsonSchema } = z.toJSONSchema(schema, {
|
|
14
|
+
override: ({ jsonSchema: node }) => {
|
|
15
|
+
if (node.format)
|
|
16
|
+
delete node.pattern;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
return jsonSchema;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Declares the normalized events of one Core App. The App name is the event
|
|
23
|
+
* family, so `defineAppEvents('smartlead', { 'email.opened': … })` declares
|
|
24
|
+
* `smartlead.email.opened`. Two events may share a schema: the name carries the
|
|
25
|
+
* meaning, the schema only the shape.
|
|
26
|
+
*/
|
|
27
|
+
export function defineAppEvents(app, events) {
|
|
28
|
+
const contracts = Object.entries(events).map(([name, event]) => [
|
|
29
|
+
name,
|
|
30
|
+
defineEvent({
|
|
31
|
+
owner: app,
|
|
32
|
+
key: `${app}.${name}`,
|
|
33
|
+
version: event.version,
|
|
34
|
+
description: event.description,
|
|
35
|
+
payloadSchema: stableJsonSchema(event.schema),
|
|
36
|
+
...(event.sensitivity ? { sensitivity: event.sensitivity } : {}),
|
|
37
|
+
...(event.subject ? { subjects: { primary: event.subject } } : {}),
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
40
|
+
return Object.freeze(Object.fromEntries(contracts));
|
|
41
|
+
}
|