@offerkit/sdk 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1585 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { createORPCClient } from "@orpc/client";
3
+ import { OpenAPILink } from "@orpc/openapi-client/fetch";
4
+ import { oc } from "@orpc/contract";
5
+ import { z } from "zod";
6
+ //#region ../contract/src/schemas/api-key.ts
7
+ const apiKeyOutput = z.object({
8
+ id: z.string(),
9
+ name: z.string(),
10
+ prefix: z.string(),
11
+ scopes: z.array(z.string()),
12
+ rateLimitRps: z.number().int(),
13
+ lastUsedAt: z.string().datetime().nullable(),
14
+ disabledAt: z.string().datetime().nullable(),
15
+ createdAt: z.string().datetime()
16
+ });
17
+ const apiKeyCreateInput = z.object({
18
+ name: z.string().min(1).max(100),
19
+ scopes: z.array(z.string()).optional(),
20
+ rateLimitRps: z.number().int().min(1).max(1e4).optional()
21
+ });
22
+ const apiKeyCreateOutput = apiKeyOutput.extend({
23
+ /** The plaintext token. Shown once at creation; never returned again. */
24
+ token: z.string() });
25
+ //#endregion
26
+ //#region ../contract/src/routes/api-keys.ts
27
+ const apiKeys = {
28
+ list: oc.route({
29
+ method: "GET",
30
+ path: "/api-keys",
31
+ summary: "List API keys"
32
+ }).output(z.object({ data: z.array(apiKeyOutput) })),
33
+ create: oc.route({
34
+ method: "POST",
35
+ path: "/api-keys",
36
+ summary: "Mint a new API key"
37
+ }).input(apiKeyCreateInput).output(apiKeyCreateOutput),
38
+ revoke: oc.route({
39
+ method: "DELETE",
40
+ path: "/api-keys/{id}",
41
+ summary: "Disable an API key (cannot be re-enabled)"
42
+ }).input(z.object({ id: z.string() })).output(z.object({ ok: z.literal(true) }))
43
+ };
44
+ //#endregion
45
+ //#region ../contract/src/mcp.ts
46
+ /**
47
+ * Returns a `meta` object that attaches MCP exposure to a procedure.
48
+ * Wrapping in a helper keeps the call sites typed and uniform.
49
+ */
50
+ function mcpMeta(meta) {
51
+ return { mcp: meta };
52
+ }
53
+ //#endregion
54
+ //#region ../contract/src/schemas/campaign.ts
55
+ const campaignType = z.enum([
56
+ "DISCOUNT",
57
+ "GIFT_VOUCHERS",
58
+ "LOYALTY_PROGRAM",
59
+ "REFERRAL_PROGRAM",
60
+ "PROMOTION"
61
+ ]);
62
+ const campaignStatus = z.enum([
63
+ "draft",
64
+ "active",
65
+ "paused",
66
+ "ended"
67
+ ]);
68
+ const codeConfig = z.object({
69
+ length: z.number().int().min(4).max(32).optional(),
70
+ prefix: z.string().max(20).optional(),
71
+ suffix: z.string().max(20).optional(),
72
+ charset: z.enum([
73
+ "alphanumeric",
74
+ "uppercase",
75
+ "lowercase",
76
+ "numeric"
77
+ ]).optional(),
78
+ excludeConfusable: z.boolean().optional()
79
+ });
80
+ const campaignOutput = z.object({
81
+ id: z.string().uuid(),
82
+ name: z.string(),
83
+ description: z.string().nullable(),
84
+ type: campaignType,
85
+ status: campaignStatus,
86
+ currency: z.string().length(3),
87
+ timezone: z.string(),
88
+ startDate: z.string().datetime().nullable(),
89
+ endDate: z.string().datetime().nullable(),
90
+ codeConfig,
91
+ validationRuleId: z.string().uuid().nullable(),
92
+ autoApply: z.boolean(),
93
+ voucherCount: z.number().int(),
94
+ metadata: z.record(z.string(), z.unknown()),
95
+ createdAt: z.string().datetime(),
96
+ updatedAt: z.string().datetime()
97
+ });
98
+ const campaignCreateInput = z.object({
99
+ name: z.string().min(1).max(100),
100
+ description: z.string().max(500).optional(),
101
+ type: campaignType,
102
+ currency: z.string().length(3),
103
+ timezone: z.string().optional(),
104
+ startDate: z.string().datetime().optional(),
105
+ endDate: z.string().datetime().optional(),
106
+ codeConfig: codeConfig.optional(),
107
+ validationRuleId: z.string().uuid().nullable().optional(),
108
+ autoApply: z.boolean().optional(),
109
+ metadata: z.record(z.string(), z.unknown()).optional()
110
+ });
111
+ const campaignUpdateInput = campaignCreateInput.omit({ type: true }).partial().extend({ status: campaignStatus.optional() });
112
+ //#endregion
113
+ //#region ../contract/src/schemas/pagination.ts
114
+ const limit = z.union([z.number(), z.string().regex(/^\d+$/)]).transform((v) => typeof v === "string" ? Number.parseInt(v, 10) : v).pipe(z.number().int().min(1).max(100)).default(20);
115
+ const paginationInput = z.object({
116
+ cursor: z.string().optional(),
117
+ limit
118
+ });
119
+ function paginatedOutput(item) {
120
+ return z.object({
121
+ data: z.array(item),
122
+ next: z.string().optional(),
123
+ prev: z.string().optional()
124
+ });
125
+ }
126
+ //#endregion
127
+ //#region ../contract/src/routes/campaigns.ts
128
+ const campaigns = {
129
+ list: oc.meta(mcpMeta({
130
+ expose: true,
131
+ riskLevel: "safe"
132
+ })).route({
133
+ method: "GET",
134
+ path: "/campaigns",
135
+ summary: "List campaigns"
136
+ }).input(paginationInput.extend({ search: z.string().optional() })).output(paginatedOutput(campaignOutput)),
137
+ get: oc.meta(mcpMeta({
138
+ expose: true,
139
+ riskLevel: "safe"
140
+ })).route({
141
+ method: "GET",
142
+ path: "/campaigns/{id}",
143
+ summary: "Get campaign"
144
+ }).input(z.object({ id: z.string().uuid() })).output(campaignOutput),
145
+ create: oc.route({
146
+ method: "POST",
147
+ path: "/campaigns",
148
+ summary: "Create campaign"
149
+ }).input(campaignCreateInput).output(campaignOutput),
150
+ update: oc.route({
151
+ method: "PATCH",
152
+ path: "/campaigns/{id}",
153
+ summary: "Update campaign"
154
+ }).input(z.object({
155
+ id: z.string().uuid(),
156
+ patch: campaignUpdateInput
157
+ })).output(campaignOutput),
158
+ delete: oc.route({
159
+ method: "DELETE",
160
+ path: "/campaigns/{id}",
161
+ summary: "Soft-delete campaign"
162
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
163
+ };
164
+ //#endregion
165
+ //#region ../contract/src/schemas/customer.ts
166
+ const customerAddress = z.object({
167
+ line1: z.string().optional(),
168
+ line2: z.string().optional(),
169
+ city: z.string().optional(),
170
+ state: z.string().optional(),
171
+ postalCode: z.string().optional(),
172
+ country: z.string().length(2).optional()
173
+ });
174
+ const customerSummary = z.object({
175
+ totalSpent: z.number().int().nonnegative().optional(),
176
+ redemptionCount: z.number().int().nonnegative().optional(),
177
+ lastRedeemedAt: z.string().datetime().optional()
178
+ });
179
+ const customerOutput = z.object({
180
+ id: z.string().uuid(),
181
+ email: z.string().email().nullable(),
182
+ name: z.string().nullable(),
183
+ phone: z.string().nullable(),
184
+ address: customerAddress.nullable(),
185
+ metadata: z.record(z.string(), z.unknown()),
186
+ summary: customerSummary,
187
+ createdAt: z.string().datetime(),
188
+ updatedAt: z.string().datetime()
189
+ });
190
+ const customerCreateInput = z.object({
191
+ email: z.string().email().optional(),
192
+ name: z.string().optional(),
193
+ phone: z.string().optional(),
194
+ address: customerAddress.optional(),
195
+ metadata: z.record(z.string(), z.unknown()).optional()
196
+ });
197
+ const customerUpdateInput = customerCreateInput.partial();
198
+ //#endregion
199
+ //#region ../contract/src/routes/customers.ts
200
+ const customers = {
201
+ list: oc.meta(mcpMeta({
202
+ expose: true,
203
+ riskLevel: "safe"
204
+ })).route({
205
+ method: "GET",
206
+ path: "/customers",
207
+ summary: "List customers"
208
+ }).input(paginationInput.extend({ search: z.string().optional() })).output(paginatedOutput(customerOutput)),
209
+ get: oc.meta(mcpMeta({
210
+ expose: true,
211
+ riskLevel: "safe"
212
+ })).route({
213
+ method: "GET",
214
+ path: "/customers/{id}",
215
+ summary: "Get customer"
216
+ }).input(z.object({ id: z.string().uuid() })).output(customerOutput),
217
+ create: oc.route({
218
+ method: "POST",
219
+ path: "/customers",
220
+ summary: "Create customer"
221
+ }).input(customerCreateInput).output(customerOutput),
222
+ update: oc.route({
223
+ method: "PATCH",
224
+ path: "/customers/{id}",
225
+ summary: "Update customer"
226
+ }).input(z.object({
227
+ id: z.string().uuid(),
228
+ patch: customerUpdateInput
229
+ })).output(customerOutput),
230
+ delete: oc.route({
231
+ method: "DELETE",
232
+ path: "/customers/{id}",
233
+ summary: "Soft-delete customer"
234
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
235
+ };
236
+ //#endregion
237
+ //#region ../contract/src/schemas/loyalty.ts
238
+ const loyaltyProgramOutput = z.object({
239
+ id: z.string().uuid(),
240
+ campaignId: z.string().uuid(),
241
+ pointsExpiryDays: z.number().int().nullable(),
242
+ metadata: z.record(z.string(), z.unknown()),
243
+ createdAt: z.string().datetime(),
244
+ updatedAt: z.string().datetime()
245
+ });
246
+ const loyaltyProgramCreateInput = z.object({
247
+ campaignId: z.string().uuid(),
248
+ pointsExpiryDays: z.number().int().min(1).optional(),
249
+ metadata: z.record(z.string(), z.unknown()).optional()
250
+ });
251
+ const loyaltyProgramUpdateInput = z.object({
252
+ pointsExpiryDays: z.number().int().min(1).nullable().optional(),
253
+ metadata: z.record(z.string(), z.unknown()).optional()
254
+ }).partial();
255
+ const loyaltyTierOutput = z.object({
256
+ id: z.string().uuid(),
257
+ programId: z.string().uuid(),
258
+ name: z.string(),
259
+ threshold: z.number().int(),
260
+ earnMultiplier: z.number().int(),
261
+ sortOrder: z.number().int()
262
+ });
263
+ const loyaltyTierCreateInput = z.object({
264
+ programId: z.string().uuid(),
265
+ name: z.string().min(1).max(100),
266
+ threshold: z.number().int().min(0),
267
+ earnMultiplier: z.number().int().min(0).default(1e4),
268
+ sortOrder: z.number().int().default(0)
269
+ });
270
+ const loyaltyTierUpdateInput = loyaltyTierCreateInput.omit({ programId: true }).partial();
271
+ const loyaltyEarningRuleFormula = z.object({
272
+ kind: z.enum([
273
+ "fixed",
274
+ "per_cents",
275
+ "custom"
276
+ ]),
277
+ value: z.number().int().min(0).optional(),
278
+ divisor: z.number().int().min(1).optional()
279
+ });
280
+ const loyaltyEarningRuleOutput = z.object({
281
+ id: z.string().uuid(),
282
+ programId: z.string().uuid(),
283
+ name: z.string(),
284
+ event: z.string(),
285
+ validationRuleId: z.string().uuid().nullable(),
286
+ formula: loyaltyEarningRuleFormula,
287
+ active: z.enum(["yes", "no"])
288
+ });
289
+ const loyaltyEarningRuleCreateInput = z.object({
290
+ programId: z.string().uuid(),
291
+ name: z.string().min(1).max(100),
292
+ event: z.string().min(1).max(100),
293
+ validationRuleId: z.string().uuid().optional(),
294
+ formula: loyaltyEarningRuleFormula,
295
+ active: z.enum(["yes", "no"]).optional()
296
+ });
297
+ const loyaltyEarningRuleUpdateInput = loyaltyEarningRuleCreateInput.omit({ programId: true }).partial();
298
+ const loyaltyRewardPayload = z.object({
299
+ kind: z.enum([
300
+ "discount",
301
+ "gift_card",
302
+ "custom"
303
+ ]),
304
+ discount: z.object({
305
+ type: z.enum(["AMOUNT", "PERCENTAGE"]),
306
+ amount: z.number().int().min(0).optional(),
307
+ percent: z.number().int().min(0).max(1e4).optional(),
308
+ maxDiscountAmount: z.number().int().min(0).optional()
309
+ }).optional(),
310
+ creditCents: z.number().int().min(0).optional(),
311
+ typeKey: z.string().optional(),
312
+ payload: z.record(z.string(), z.unknown()).optional()
313
+ });
314
+ const loyaltyRewardOutput = z.object({
315
+ id: z.string().uuid(),
316
+ programId: z.string().uuid(),
317
+ name: z.string(),
318
+ description: z.string().nullable(),
319
+ cost: z.number().int(),
320
+ payload: loyaltyRewardPayload
321
+ });
322
+ const loyaltyRewardCreateInput = z.object({
323
+ programId: z.string().uuid(),
324
+ name: z.string().min(1).max(100),
325
+ description: z.string().max(500).optional(),
326
+ cost: z.number().int().min(1),
327
+ payload: loyaltyRewardPayload
328
+ });
329
+ const loyaltyRewardUpdateInput = loyaltyRewardCreateInput.omit({ programId: true }).partial();
330
+ const loyaltyMemberOutput = z.object({
331
+ id: z.string().uuid(),
332
+ customerId: z.string().uuid(),
333
+ programId: z.string().uuid(),
334
+ balance: z.number().int(),
335
+ lifetimePoints: z.number().int(),
336
+ currentTierId: z.string().uuid().nullable(),
337
+ enrolledAt: z.string().datetime()
338
+ });
339
+ const loyaltyMemberEnrollInput = z.object({
340
+ programId: z.string().uuid(),
341
+ customerId: z.string().uuid()
342
+ });
343
+ const loyaltyEarnInput = z.object({
344
+ memberId: z.string().uuid(),
345
+ basePoints: z.number().int().min(1),
346
+ earningRuleId: z.string().uuid().optional(),
347
+ eventId: z.string().optional(),
348
+ note: z.string().max(500).optional(),
349
+ expiresAt: z.string().datetime().optional(),
350
+ applyMultiplier: z.boolean().optional()
351
+ });
352
+ const loyaltyAdjustInput = z.object({
353
+ memberId: z.string().uuid(),
354
+ delta: z.number().int(),
355
+ note: z.string().max(500).optional()
356
+ });
357
+ const loyaltyRedeemInput = z.object({
358
+ memberId: z.string().uuid(),
359
+ rewardId: z.string().uuid(),
360
+ note: z.string().max(500).optional()
361
+ });
362
+ const loyaltyTransactionOutput = z.object({
363
+ id: z.string().uuid(),
364
+ memberId: z.string().uuid(),
365
+ delta: z.number().int(),
366
+ balanceAfter: z.number().int(),
367
+ reason: z.enum([
368
+ "EARN",
369
+ "REDEEM",
370
+ "ADJUSTMENT",
371
+ "EXPIRY",
372
+ "ROLLBACK"
373
+ ]),
374
+ rewardId: z.string().uuid().nullable(),
375
+ earningRuleId: z.string().uuid().nullable(),
376
+ eventId: z.string().nullable(),
377
+ note: z.string().nullable(),
378
+ expiresAt: z.string().datetime().nullable(),
379
+ expiredAt: z.string().datetime().nullable(),
380
+ createdAt: z.string().datetime()
381
+ });
382
+ const loyalty = {
383
+ programs: {
384
+ list: oc.route({
385
+ method: "GET",
386
+ path: "/loyalty/programs",
387
+ summary: "List loyalty programs"
388
+ }).input(paginationInput).output(paginatedOutput(loyaltyProgramOutput)),
389
+ get: oc.route({
390
+ method: "GET",
391
+ path: "/loyalty/programs/{id}",
392
+ summary: "Get loyalty program"
393
+ }).input(z.object({ id: z.string().uuid() })).output(loyaltyProgramOutput),
394
+ create: oc.route({
395
+ method: "POST",
396
+ path: "/loyalty/programs",
397
+ summary: "Create loyalty program"
398
+ }).input(loyaltyProgramCreateInput).output(loyaltyProgramOutput),
399
+ update: oc.route({
400
+ method: "PATCH",
401
+ path: "/loyalty/programs/{id}",
402
+ summary: "Update loyalty program"
403
+ }).input(z.object({
404
+ id: z.string().uuid(),
405
+ patch: loyaltyProgramUpdateInput
406
+ })).output(loyaltyProgramOutput),
407
+ delete: oc.route({
408
+ method: "DELETE",
409
+ path: "/loyalty/programs/{id}",
410
+ summary: "Soft-delete program"
411
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
412
+ },
413
+ tiers: {
414
+ list: oc.route({
415
+ method: "GET",
416
+ path: "/loyalty/programs/{programId}/tiers",
417
+ summary: "List tiers"
418
+ }).input(z.object({ programId: z.string().uuid() })).output(z.object({ data: z.array(loyaltyTierOutput) })),
419
+ create: oc.route({
420
+ method: "POST",
421
+ path: "/loyalty/tiers",
422
+ summary: "Create tier"
423
+ }).input(loyaltyTierCreateInput).output(loyaltyTierOutput),
424
+ update: oc.route({
425
+ method: "PATCH",
426
+ path: "/loyalty/tiers/{id}",
427
+ summary: "Update tier"
428
+ }).input(z.object({
429
+ id: z.string().uuid(),
430
+ patch: loyaltyTierUpdateInput
431
+ })).output(loyaltyTierOutput),
432
+ delete: oc.route({
433
+ method: "DELETE",
434
+ path: "/loyalty/tiers/{id}",
435
+ summary: "Delete tier"
436
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
437
+ },
438
+ earningRules: {
439
+ list: oc.route({
440
+ method: "GET",
441
+ path: "/loyalty/programs/{programId}/earning-rules",
442
+ summary: "List earning rules"
443
+ }).input(z.object({ programId: z.string().uuid() })).output(z.object({ data: z.array(loyaltyEarningRuleOutput) })),
444
+ create: oc.route({
445
+ method: "POST",
446
+ path: "/loyalty/earning-rules",
447
+ summary: "Create earning rule"
448
+ }).input(loyaltyEarningRuleCreateInput).output(loyaltyEarningRuleOutput),
449
+ update: oc.route({
450
+ method: "PATCH",
451
+ path: "/loyalty/earning-rules/{id}",
452
+ summary: "Update earning rule"
453
+ }).input(z.object({
454
+ id: z.string().uuid(),
455
+ patch: loyaltyEarningRuleUpdateInput
456
+ })).output(loyaltyEarningRuleOutput),
457
+ delete: oc.route({
458
+ method: "DELETE",
459
+ path: "/loyalty/earning-rules/{id}",
460
+ summary: "Delete earning rule"
461
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
462
+ },
463
+ rewards: {
464
+ list: oc.route({
465
+ method: "GET",
466
+ path: "/loyalty/programs/{programId}/rewards",
467
+ summary: "List rewards"
468
+ }).input(z.object({ programId: z.string().uuid() })).output(z.object({ data: z.array(loyaltyRewardOutput) })),
469
+ create: oc.route({
470
+ method: "POST",
471
+ path: "/loyalty/rewards",
472
+ summary: "Create reward"
473
+ }).input(loyaltyRewardCreateInput).output(loyaltyRewardOutput),
474
+ update: oc.route({
475
+ method: "PATCH",
476
+ path: "/loyalty/rewards/{id}",
477
+ summary: "Update reward"
478
+ }).input(z.object({
479
+ id: z.string().uuid(),
480
+ patch: loyaltyRewardUpdateInput
481
+ })).output(loyaltyRewardOutput),
482
+ delete: oc.route({
483
+ method: "DELETE",
484
+ path: "/loyalty/rewards/{id}",
485
+ summary: "Soft-delete reward"
486
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
487
+ },
488
+ members: {
489
+ list: oc.route({
490
+ method: "GET",
491
+ path: "/loyalty/programs/{programId}/members",
492
+ summary: "List members of a program"
493
+ }).input(paginationInput.extend({ programId: z.string().uuid() })).output(paginatedOutput(loyaltyMemberOutput)),
494
+ get: oc.route({
495
+ method: "GET",
496
+ path: "/loyalty/members/{id}",
497
+ summary: "Get member"
498
+ }).input(z.object({ id: z.string().uuid() })).output(loyaltyMemberOutput),
499
+ enroll: oc.route({
500
+ method: "POST",
501
+ path: "/loyalty/members",
502
+ summary: "Enroll a customer"
503
+ }).input(loyaltyMemberEnrollInput).output(loyaltyMemberOutput),
504
+ earn: oc.route({
505
+ method: "POST",
506
+ path: "/loyalty/members/earn",
507
+ summary: "Earn points"
508
+ }).input(loyaltyEarnInput).output(z.object({
509
+ ok: z.boolean(),
510
+ transactionId: z.string().uuid().optional(),
511
+ delta: z.number().int().optional(),
512
+ balance: z.number().int().optional(),
513
+ lifetimePoints: z.number().int().optional(),
514
+ tierId: z.string().uuid().nullable().optional(),
515
+ code: z.string().optional(),
516
+ message: z.string().optional()
517
+ })),
518
+ adjust: oc.route({
519
+ method: "POST",
520
+ path: "/loyalty/members/adjust",
521
+ summary: "Manual adjustment"
522
+ }).input(loyaltyAdjustInput).output(z.object({
523
+ ok: z.boolean(),
524
+ transactionId: z.string().uuid().optional(),
525
+ balance: z.number().int().optional(),
526
+ code: z.string().optional(),
527
+ message: z.string().optional()
528
+ })),
529
+ redeem: oc.route({
530
+ method: "POST",
531
+ path: "/loyalty/members/redeem",
532
+ summary: "Redeem a reward"
533
+ }).input(loyaltyRedeemInput).output(z.object({
534
+ ok: z.boolean(),
535
+ transactionId: z.string().uuid().optional(),
536
+ rewardId: z.string().uuid().optional(),
537
+ cost: z.number().int().optional(),
538
+ balance: z.number().int().optional(),
539
+ payload: loyaltyRewardPayload.optional(),
540
+ code: z.string().optional(),
541
+ message: z.string().optional()
542
+ })),
543
+ history: oc.meta(mcpMeta({
544
+ expose: true,
545
+ riskLevel: "safe",
546
+ description: "List a loyalty member's transaction history (earn / redeem / adjust / expiry)."
547
+ })).route({
548
+ method: "GET",
549
+ path: "/loyalty/members/{id}/transactions",
550
+ summary: "Member transaction ledger"
551
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ data: z.array(loyaltyTransactionOutput) }))
552
+ }
553
+ };
554
+ //#endregion
555
+ //#region ../contract/src/schemas/referral.ts
556
+ const referralReward = z.object({
557
+ kind: z.enum([
558
+ "discount",
559
+ "gift_card",
560
+ "loyalty_points",
561
+ "custom"
562
+ ]),
563
+ discount: z.object({
564
+ type: z.enum(["AMOUNT", "PERCENTAGE"]),
565
+ amount: z.number().int().min(0).optional(),
566
+ percent: z.number().int().min(0).max(1e4).optional(),
567
+ maxDiscountAmount: z.number().int().min(0).optional()
568
+ }).optional(),
569
+ creditCents: z.number().int().min(0).optional(),
570
+ loyaltyProgramId: z.string().uuid().optional(),
571
+ loyaltyPoints: z.number().int().min(0).optional(),
572
+ typeKey: z.string().optional(),
573
+ payload: z.record(z.string(), z.unknown()).optional()
574
+ });
575
+ const referralProgramOutput = z.object({
576
+ id: z.string().uuid(),
577
+ campaignId: z.string().uuid(),
578
+ referrerReward: referralReward,
579
+ refereeReward: referralReward,
580
+ codeLength: z.number().int(),
581
+ metadata: z.record(z.string(), z.unknown()),
582
+ createdAt: z.string().datetime(),
583
+ updatedAt: z.string().datetime()
584
+ });
585
+ const referralProgramCreateInput = z.object({
586
+ campaignId: z.string().uuid(),
587
+ referrerReward: referralReward,
588
+ refereeReward: referralReward,
589
+ codeLength: z.number().int().min(4).max(32).optional(),
590
+ metadata: z.record(z.string(), z.unknown()).optional()
591
+ });
592
+ const referralProgramUpdateInput = referralProgramCreateInput.omit({ campaignId: true }).partial();
593
+ const referralOutcome = z.object({
594
+ kind: z.enum([
595
+ "discount",
596
+ "gift_card",
597
+ "loyalty_points",
598
+ "custom"
599
+ ]),
600
+ voucherCode: z.string().optional(),
601
+ loyaltyTransactionId: z.string().uuid().optional(),
602
+ payload: z.record(z.string(), z.unknown()).optional()
603
+ });
604
+ const referralCodeOutput = z.object({
605
+ id: z.string().uuid(),
606
+ programId: z.string().uuid(),
607
+ referrerCustomerId: z.string().uuid(),
608
+ code: z.string(),
609
+ metadata: z.record(z.string(), z.unknown()),
610
+ createdAt: z.string().datetime(),
611
+ updatedAt: z.string().datetime()
612
+ });
613
+ const referralConversionStatus = z.enum(["converted", "rejected"]);
614
+ const referralConversionOutput = z.object({
615
+ id: z.string().uuid(),
616
+ codeId: z.string().uuid(),
617
+ refereeCustomerId: z.string().uuid(),
618
+ status: referralConversionStatus,
619
+ convertedAt: z.string().datetime(),
620
+ conversionEventId: z.string().nullable(),
621
+ referrerOutcome: referralOutcome,
622
+ refereeOutcome: referralOutcome,
623
+ createdAt: z.string().datetime(),
624
+ updatedAt: z.string().datetime()
625
+ });
626
+ const referralIssueInput = z.object({
627
+ programId: z.string().uuid(),
628
+ referrerCustomerId: z.string().uuid(),
629
+ prefix: z.string().min(1).max(12).optional()
630
+ });
631
+ const referralConvertInput = z.object({
632
+ code: z.string().min(1),
633
+ refereeCustomerId: z.string().uuid(),
634
+ conversionEventId: z.string().optional()
635
+ });
636
+ const referralIssueOutput = z.object({
637
+ ok: z.boolean(),
638
+ codeId: z.string().uuid().optional(),
639
+ code: z.string().optional(),
640
+ errorCode: z.string().optional(),
641
+ message: z.string().optional()
642
+ });
643
+ const referralConvertOutput = z.object({
644
+ ok: z.boolean(),
645
+ conversionId: z.string().uuid().optional(),
646
+ codeId: z.string().uuid().optional(),
647
+ code: z.string().optional(),
648
+ referrerCustomerId: z.string().uuid().optional(),
649
+ refereeCustomerId: z.string().uuid().optional(),
650
+ referrerReward: referralOutcome.optional(),
651
+ refereeReward: referralOutcome.optional(),
652
+ idempotent: z.boolean().optional(),
653
+ errorCode: z.string().optional(),
654
+ message: z.string().optional()
655
+ });
656
+ const referrals = {
657
+ programs: {
658
+ list: oc.route({
659
+ method: "GET",
660
+ path: "/referral-programs",
661
+ summary: "List referral programs"
662
+ }).input(paginationInput).output(paginatedOutput(referralProgramOutput)),
663
+ get: oc.route({
664
+ method: "GET",
665
+ path: "/referral-programs/{id}",
666
+ summary: "Get referral program"
667
+ }).input(z.object({ id: z.string().uuid() })).output(referralProgramOutput),
668
+ create: oc.route({
669
+ method: "POST",
670
+ path: "/referral-programs",
671
+ summary: "Create referral program"
672
+ }).input(referralProgramCreateInput).output(referralProgramOutput),
673
+ update: oc.route({
674
+ method: "PATCH",
675
+ path: "/referral-programs/{id}",
676
+ summary: "Update referral program"
677
+ }).input(z.object({
678
+ id: z.string().uuid(),
679
+ patch: referralProgramUpdateInput
680
+ })).output(referralProgramOutput),
681
+ delete: oc.route({
682
+ method: "DELETE",
683
+ path: "/referral-programs/{id}",
684
+ summary: "Soft-delete referral program"
685
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
686
+ },
687
+ listCodes: oc.route({
688
+ method: "GET",
689
+ path: "/referral-programs/{programId}/codes",
690
+ summary: "List referral codes in a program"
691
+ }).input(paginationInput.extend({ programId: z.string().uuid() })).output(paginatedOutput(referralCodeOutput)),
692
+ listConversions: oc.route({
693
+ method: "GET",
694
+ path: "/referral-codes/{codeId}/conversions",
695
+ summary: "List conversions for a referral code"
696
+ }).input(paginationInput.extend({ codeId: z.string().uuid() })).output(paginatedOutput(referralConversionOutput)),
697
+ getByCode: oc.route({
698
+ method: "GET",
699
+ path: "/referrals/{code}",
700
+ summary: "Look up a referral code"
701
+ }).input(z.object({ code: z.string() })).output(referralCodeOutput),
702
+ issue: oc.route({
703
+ method: "POST",
704
+ path: "/referrals/issue",
705
+ summary: "Issue (or fetch) a referral code for a customer"
706
+ }).input(referralIssueInput).output(referralIssueOutput),
707
+ convert: oc.route({
708
+ method: "POST",
709
+ path: "/referrals/convert",
710
+ summary: "Convert a referral and issue both rewards"
711
+ }).input(referralConvertInput).output(referralConvertOutput)
712
+ };
713
+ //#endregion
714
+ //#region ../contract/src/schemas/reward-type.ts
715
+ const rewardTypeOutput = z.object({
716
+ id: z.string().uuid(),
717
+ key: z.string(),
718
+ name: z.string(),
719
+ description: z.string().nullable(),
720
+ payloadSchema: z.record(z.string(), z.unknown()),
721
+ createdAt: z.string().datetime(),
722
+ updatedAt: z.string().datetime()
723
+ });
724
+ const rewardTypeCreateInput = z.object({
725
+ key: z.string().min(2).max(50).regex(/^[A-Z][A-Z0-9_]*$/, "Use SCREAMING_SNAKE_CASE: e.g. FREE_SHIPPING"),
726
+ name: z.string().min(1).max(100),
727
+ description: z.string().max(500).optional(),
728
+ payloadSchema: z.record(z.string(), z.unknown())
729
+ });
730
+ const rewardTypeUpdateInput = rewardTypeCreateInput.omit({ key: true }).partial();
731
+ //#endregion
732
+ //#region ../contract/src/routes/reward-types.ts
733
+ const rewardTypes = {
734
+ list: oc.route({
735
+ method: "GET",
736
+ path: "/reward-types",
737
+ summary: "List reward types"
738
+ }).input(paginationInput.extend({ search: z.string().optional() })).output(paginatedOutput(rewardTypeOutput)),
739
+ get: oc.route({
740
+ method: "GET",
741
+ path: "/reward-types/{id}",
742
+ summary: "Get reward type"
743
+ }).input(z.object({ id: z.string().uuid() })).output(rewardTypeOutput),
744
+ create: oc.route({
745
+ method: "POST",
746
+ path: "/reward-types",
747
+ summary: "Create reward type"
748
+ }).input(rewardTypeCreateInput).output(rewardTypeOutput),
749
+ update: oc.route({
750
+ method: "PATCH",
751
+ path: "/reward-types/{id}",
752
+ summary: "Update reward type"
753
+ }).input(z.object({
754
+ id: z.string().uuid(),
755
+ patch: rewardTypeUpdateInput
756
+ })).output(rewardTypeOutput),
757
+ delete: oc.route({
758
+ method: "DELETE",
759
+ path: "/reward-types/{id}",
760
+ summary: "Soft-delete reward type"
761
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
762
+ };
763
+ //#endregion
764
+ //#region ../contract/src/schemas/segment.ts
765
+ const segmentRule = z.record(z.string(), z.unknown());
766
+ const segmentOutput = z.object({
767
+ id: z.string().uuid(),
768
+ name: z.string(),
769
+ description: z.string().nullable(),
770
+ rule: segmentRule,
771
+ createdAt: z.string().datetime(),
772
+ updatedAt: z.string().datetime()
773
+ });
774
+ const segmentCreateInput = z.object({
775
+ name: z.string().min(1).max(100),
776
+ description: z.string().max(500).optional(),
777
+ rule: segmentRule
778
+ });
779
+ const segmentUpdateInput = segmentCreateInput.partial();
780
+ //#endregion
781
+ //#region ../contract/src/routes/segments.ts
782
+ const segments = {
783
+ list: oc.route({
784
+ method: "GET",
785
+ path: "/segments",
786
+ summary: "List segments"
787
+ }).input(paginationInput.extend({ search: z.string().optional() })).output(paginatedOutput(segmentOutput)),
788
+ get: oc.route({
789
+ method: "GET",
790
+ path: "/segments/{id}",
791
+ summary: "Get segment"
792
+ }).input(z.object({ id: z.string().uuid() })).output(segmentOutput),
793
+ create: oc.route({
794
+ method: "POST",
795
+ path: "/segments",
796
+ summary: "Create segment"
797
+ }).input(segmentCreateInput).output(segmentOutput),
798
+ update: oc.route({
799
+ method: "PATCH",
800
+ path: "/segments/{id}",
801
+ summary: "Update segment"
802
+ }).input(z.object({
803
+ id: z.string().uuid(),
804
+ patch: segmentUpdateInput
805
+ })).output(segmentOutput),
806
+ delete: oc.route({
807
+ method: "DELETE",
808
+ path: "/segments/{id}",
809
+ summary: "Soft-delete segment"
810
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) })),
811
+ preview: oc.meta(mcpMeta({
812
+ expose: true,
813
+ riskLevel: "safe",
814
+ description: "Run a JSON Logic rule against existing customers and report match count plus a sample."
815
+ })).route({
816
+ method: "POST",
817
+ path: "/segments/preview",
818
+ summary: "Preview rule against customers"
819
+ }).input(z.object({
820
+ rule: segmentRule,
821
+ sampleSize: z.number().int().min(1).max(50).default(10)
822
+ })).output(z.object({
823
+ matchedCount: z.number().int().nonnegative(),
824
+ sample: z.array(customerOutput)
825
+ }))
826
+ };
827
+ //#endregion
828
+ //#region ../contract/src/schemas/validation-rule.ts
829
+ const validationRuleAppliesTo = z.enum([
830
+ "voucher",
831
+ "promotion",
832
+ "earn",
833
+ "reward"
834
+ ]);
835
+ const validationRuleOutput = z.object({
836
+ id: z.string().uuid(),
837
+ name: z.string(),
838
+ description: z.string().nullable(),
839
+ rule: z.record(z.string(), z.unknown()),
840
+ appliesTo: validationRuleAppliesTo,
841
+ createdAt: z.string().datetime(),
842
+ updatedAt: z.string().datetime()
843
+ });
844
+ const validationRuleCreateInput = z.object({
845
+ name: z.string().min(1).max(100),
846
+ description: z.string().max(500).optional(),
847
+ rule: z.record(z.string(), z.unknown()),
848
+ appliesTo: validationRuleAppliesTo.optional()
849
+ });
850
+ const validationRuleUpdateInput = validationRuleCreateInput.partial();
851
+ //#endregion
852
+ //#region ../contract/src/routes/validation-rules.ts
853
+ const validationRules = {
854
+ list: oc.route({
855
+ method: "GET",
856
+ path: "/validation-rules",
857
+ summary: "List validation rules"
858
+ }).input(paginationInput.extend({ search: z.string().optional() })).output(paginatedOutput(validationRuleOutput)),
859
+ get: oc.route({
860
+ method: "GET",
861
+ path: "/validation-rules/{id}",
862
+ summary: "Get validation rule"
863
+ }).input(z.object({ id: z.string().uuid() })).output(validationRuleOutput),
864
+ create: oc.route({
865
+ method: "POST",
866
+ path: "/validation-rules",
867
+ summary: "Create validation rule"
868
+ }).input(validationRuleCreateInput).output(validationRuleOutput),
869
+ update: oc.route({
870
+ method: "PATCH",
871
+ path: "/validation-rules/{id}",
872
+ summary: "Update validation rule"
873
+ }).input(z.object({
874
+ id: z.string().uuid(),
875
+ patch: validationRuleUpdateInput
876
+ })).output(validationRuleOutput),
877
+ delete: oc.route({
878
+ method: "DELETE",
879
+ path: "/validation-rules/{id}",
880
+ summary: "Soft-delete validation rule"
881
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
882
+ };
883
+ //#endregion
884
+ //#region ../contract/src/schemas/voucher.ts
885
+ const voucherType = z.enum(["DISCOUNT", "GIFT_CARD"]);
886
+ const voucherDiscount = z.object({
887
+ type: z.enum(["AMOUNT", "PERCENTAGE"]),
888
+ amount: z.number().int().min(0).optional(),
889
+ percent: z.number().int().min(0).max(1e4).optional(),
890
+ maxDiscountAmount: z.number().int().min(0).optional(),
891
+ appliesTo: z.object({
892
+ productIds: z.array(z.string()).optional(),
893
+ collectionIds: z.array(z.string()).optional()
894
+ }).optional()
895
+ });
896
+ const customReward = z.object({
897
+ typeKey: z.string().min(1),
898
+ payload: z.record(z.string(), z.unknown())
899
+ });
900
+ const voucherOutput = z.object({
901
+ id: z.string().uuid(),
902
+ code: z.string(),
903
+ campaignId: z.string().uuid().nullable(),
904
+ type: voucherType,
905
+ discount: voucherDiscount.nullable(),
906
+ customRewards: z.array(customReward),
907
+ giftBalance: z.number().int().nullable(),
908
+ redemptionLimit: z.number().int().nullable(),
909
+ redemptionCount: z.number().int(),
910
+ priority: z.number().int(),
911
+ exclusive: z.boolean(),
912
+ active: z.boolean(),
913
+ startDate: z.string().datetime().nullable(),
914
+ endDate: z.string().datetime().nullable(),
915
+ customerId: z.string().uuid().nullable(),
916
+ metadata: z.record(z.string(), z.unknown()),
917
+ createdAt: z.string().datetime(),
918
+ updatedAt: z.string().datetime()
919
+ });
920
+ const voucherCreateInput = z.object({
921
+ code: z.string().min(1).optional(),
922
+ campaignId: z.string().uuid().optional(),
923
+ type: voucherType,
924
+ discount: voucherDiscount.optional(),
925
+ customRewards: z.array(customReward).optional(),
926
+ giftBalance: z.number().int().min(0).optional(),
927
+ redemptionLimit: z.number().int().min(1).optional(),
928
+ priority: z.number().int().optional(),
929
+ exclusive: z.boolean().optional(),
930
+ startDate: z.string().datetime().optional(),
931
+ endDate: z.string().datetime().optional(),
932
+ customerId: z.string().uuid().optional(),
933
+ metadata: z.record(z.string(), z.unknown()).optional()
934
+ });
935
+ const voucherUpdateInput = voucherCreateInput.omit({
936
+ type: true,
937
+ code: true
938
+ }).partial().extend({ active: z.boolean().optional() });
939
+ const voucherBulkCreateInput = z.object({
940
+ campaignId: z.string().uuid(),
941
+ count: z.number().int().min(1).max(1e5)
942
+ });
943
+ //#endregion
944
+ //#region ../contract/src/schemas/redemption.ts
945
+ const orderItem$1 = z.object({
946
+ productId: z.string(),
947
+ collectionId: z.string().optional(),
948
+ quantity: z.number().int().min(1),
949
+ unitPrice: z.number().int().min(0)
950
+ });
951
+ const orderInput = z.object({
952
+ amount: z.number().int().min(0),
953
+ currency: z.string().length(3),
954
+ items: z.array(orderItem$1).optional()
955
+ });
956
+ const validateInput = z.object({
957
+ code: z.string().min(1),
958
+ customerId: z.string().uuid().optional(),
959
+ order: orderInput.optional()
960
+ });
961
+ const redeemInput = validateInput.extend({
962
+ /** uuid of an `order` row created via the orders API. */
963
+ orderId: z.string().uuid().optional(),
964
+ /** Free-form integrator order reference (Shopify id, etc). */
965
+ externalOrderId: z.string().min(1).max(120).optional(),
966
+ idempotencyKey: z.string().min(1).max(128).optional()
967
+ });
968
+ const breakdownEntry = z.object({
969
+ voucherId: z.string().uuid(),
970
+ code: z.string(),
971
+ amount: z.number().int(),
972
+ type: z.enum(["AMOUNT", "PERCENTAGE"]).optional(),
973
+ reason: z.enum(["exclusivity_lost", "zero_after_running_total"]).optional()
974
+ });
975
+ const validateOutput = z.object({
976
+ valid: z.boolean(),
977
+ code: z.string().optional(),
978
+ message: z.string().optional(),
979
+ preview: z.object({
980
+ amount: z.number().int(),
981
+ finalOrder: z.object({
982
+ amount: z.number().int(),
983
+ currency: z.string()
984
+ }),
985
+ breakdown: z.array(breakdownEntry)
986
+ }).optional()
987
+ });
988
+ const redeemOutput = z.object({
989
+ ok: z.boolean(),
990
+ redemptionId: z.string().uuid().optional(),
991
+ amount: z.number().int().optional(),
992
+ finalOrder: z.object({
993
+ amount: z.number().int(),
994
+ currency: z.string()
995
+ }).optional(),
996
+ breakdown: z.array(breakdownEntry).optional(),
997
+ idempotent: z.boolean().optional(),
998
+ code: z.string().optional(),
999
+ message: z.string().optional()
1000
+ });
1001
+ const stackRedeemInput = z.object({
1002
+ codes: z.array(z.string().min(1)).min(1).max(20),
1003
+ customerId: z.string().uuid().optional(),
1004
+ orderId: z.string().uuid().optional(),
1005
+ externalOrderId: z.string().min(1).max(120).optional(),
1006
+ order: orderInput,
1007
+ idempotencyKey: z.string().min(1).max(128).optional()
1008
+ });
1009
+ const stackEntry = z.object({
1010
+ voucherCode: z.string(),
1011
+ voucherId: z.string().uuid(),
1012
+ redemptionId: z.string().uuid(),
1013
+ amount: z.number().int()
1014
+ });
1015
+ const stackRedeemOutput = z.object({
1016
+ ok: z.boolean(),
1017
+ batchId: z.string().uuid().optional(),
1018
+ amount: z.number().int().optional(),
1019
+ finalOrder: z.object({
1020
+ amount: z.number().int(),
1021
+ currency: z.string()
1022
+ }).optional(),
1023
+ breakdown: z.array(breakdownEntry).optional(),
1024
+ entries: z.array(stackEntry).optional(),
1025
+ idempotent: z.boolean().optional(),
1026
+ code: z.string().optional(),
1027
+ message: z.string().optional()
1028
+ });
1029
+ //#endregion
1030
+ //#region ../contract/src/routes/vouchers.ts
1031
+ const vouchers = {
1032
+ list: oc.meta(mcpMeta({
1033
+ expose: true,
1034
+ riskLevel: "safe"
1035
+ })).route({
1036
+ method: "GET",
1037
+ path: "/vouchers",
1038
+ summary: "List vouchers"
1039
+ }).input(paginationInput.extend({
1040
+ search: z.string().optional(),
1041
+ campaignId: z.string().uuid().optional(),
1042
+ active: z.boolean().optional(),
1043
+ customerId: z.string().uuid().optional()
1044
+ })).output(paginatedOutput(voucherOutput)),
1045
+ get: oc.meta(mcpMeta({
1046
+ expose: true,
1047
+ riskLevel: "safe"
1048
+ })).route({
1049
+ method: "GET",
1050
+ path: "/vouchers/{code}",
1051
+ summary: "Get voucher by code"
1052
+ }).input(z.object({ code: z.string() })).output(voucherOutput),
1053
+ create: oc.route({
1054
+ method: "POST",
1055
+ path: "/vouchers",
1056
+ summary: "Create voucher"
1057
+ }).input(voucherCreateInput).output(voucherOutput),
1058
+ update: oc.route({
1059
+ method: "PATCH",
1060
+ path: "/vouchers/{code}",
1061
+ summary: "Update voucher"
1062
+ }).input(z.object({
1063
+ code: z.string(),
1064
+ patch: voucherUpdateInput
1065
+ })).output(voucherOutput),
1066
+ delete: oc.route({
1067
+ method: "DELETE",
1068
+ path: "/vouchers/{code}",
1069
+ summary: "Soft-delete voucher"
1070
+ }).input(z.object({ code: z.string() })).output(z.object({ ok: z.literal(true) })),
1071
+ bulk: oc.route({
1072
+ method: "POST",
1073
+ path: "/vouchers/bulk",
1074
+ summary: "Generate vouchers in bulk"
1075
+ }).input(voucherBulkCreateInput).output(z.object({
1076
+ campaignId: z.string().uuid(),
1077
+ generated: z.number().int(),
1078
+ jobId: z.string().uuid().optional()
1079
+ })),
1080
+ validate: oc.meta(mcpMeta({
1081
+ expose: true,
1082
+ riskLevel: "safe"
1083
+ })).route({
1084
+ method: "POST",
1085
+ path: "/vouchers/{code}/validate",
1086
+ summary: "Validate a voucher against an optional order context"
1087
+ }).input(validateInput).output(validateOutput),
1088
+ redeem: oc.meta(mcpMeta({
1089
+ expose: true,
1090
+ riskLevel: "mutating",
1091
+ description: "Commit a redemption against an order. Confirm with the user before calling. Use idempotencyKey to safely retry."
1092
+ })).route({
1093
+ method: "POST",
1094
+ path: "/vouchers/{code}/redemption",
1095
+ summary: "Redeem a voucher"
1096
+ }).input(redeemInput).output(redeemOutput),
1097
+ stackRedeem: oc.meta(mcpMeta({
1098
+ expose: true,
1099
+ riskLevel: "mutating",
1100
+ description: "Apply N codes to one order atomically. Either every voucher commits or none. Confirm with the user before calling."
1101
+ })).route({
1102
+ method: "POST",
1103
+ path: "/redemptions/stack",
1104
+ summary: "Redeem multiple vouchers against one order in a single transaction"
1105
+ }).input(stackRedeemInput).output(stackRedeemOutput),
1106
+ transactions: oc.route({
1107
+ method: "GET",
1108
+ path: "/vouchers/{code}/transactions",
1109
+ summary: "Gift card balance ledger"
1110
+ }).input(z.object({ code: z.string() })).output(z.object({ data: z.array(z.object({
1111
+ id: z.string().uuid(),
1112
+ redemptionId: z.string().uuid().nullable(),
1113
+ delta: z.number().int(),
1114
+ balanceAfter: z.number().int(),
1115
+ reason: z.enum([
1116
+ "CREDIT",
1117
+ "REDEMPTION",
1118
+ "ROLLBACK",
1119
+ "ADJUSTMENT"
1120
+ ]),
1121
+ createdAt: z.string().datetime()
1122
+ })) }))
1123
+ };
1124
+ //#endregion
1125
+ //#region ../contract/src/schemas/webhook.ts
1126
+ const webhookOutput = z.object({
1127
+ id: z.string().uuid(),
1128
+ name: z.string(),
1129
+ url: z.string().url(),
1130
+ secretPrefix: z.string(),
1131
+ events: z.array(z.string()),
1132
+ active: z.boolean(),
1133
+ createdAt: z.string().datetime(),
1134
+ updatedAt: z.string().datetime()
1135
+ });
1136
+ const webhookCreateInput = z.object({
1137
+ name: z.string().min(1).max(100),
1138
+ url: z.string().url(),
1139
+ events: z.array(z.string()).min(1).default(["*"]),
1140
+ active: z.boolean().default(true)
1141
+ });
1142
+ const webhookCreateOutput = webhookOutput.extend({
1143
+ /** Plaintext signing secret. Shown once. */
1144
+ secret: z.string() });
1145
+ const webhookUpdateInput = z.object({
1146
+ name: z.string().min(1).max(100).optional(),
1147
+ url: z.string().url().optional(),
1148
+ events: z.array(z.string()).optional(),
1149
+ active: z.boolean().optional()
1150
+ });
1151
+ const webhookDeliveryOutput = z.object({
1152
+ id: z.string().uuid(),
1153
+ webhookId: z.string().uuid(),
1154
+ eventId: z.string().uuid(),
1155
+ eventType: z.string(),
1156
+ status: z.enum([
1157
+ "pending",
1158
+ "succeeded",
1159
+ "failed",
1160
+ "dead"
1161
+ ]),
1162
+ attempts: z.number().int(),
1163
+ responseStatus: z.number().int().nullable(),
1164
+ responseBody: z.string().nullable(),
1165
+ error: z.string().nullable(),
1166
+ nextRetryAt: z.string().datetime().nullable(),
1167
+ createdAt: z.string().datetime(),
1168
+ updatedAt: z.string().datetime()
1169
+ });
1170
+ const eventOutput = z.object({
1171
+ id: z.string().uuid(),
1172
+ type: z.string(),
1173
+ payload: z.record(z.string(), z.unknown()),
1174
+ entityId: z.string().nullable(),
1175
+ createdAt: z.string().datetime()
1176
+ });
1177
+ //#endregion
1178
+ //#region ../contract/src/routes/webhooks.ts
1179
+ const webhooks$1 = {
1180
+ list: oc.route({
1181
+ method: "GET",
1182
+ path: "/webhooks",
1183
+ summary: "List webhooks"
1184
+ }).output(z.object({ data: z.array(webhookOutput) })),
1185
+ get: oc.route({
1186
+ method: "GET",
1187
+ path: "/webhooks/{id}",
1188
+ summary: "Get webhook"
1189
+ }).input(z.object({ id: z.string().uuid() })).output(webhookOutput),
1190
+ create: oc.route({
1191
+ method: "POST",
1192
+ path: "/webhooks",
1193
+ summary: "Create webhook"
1194
+ }).input(webhookCreateInput).output(webhookCreateOutput),
1195
+ update: oc.route({
1196
+ method: "PATCH",
1197
+ path: "/webhooks/{id}",
1198
+ summary: "Update webhook"
1199
+ }).input(z.object({
1200
+ id: z.string().uuid(),
1201
+ patch: webhookUpdateInput
1202
+ })).output(webhookOutput),
1203
+ delete: oc.route({
1204
+ method: "DELETE",
1205
+ path: "/webhooks/{id}",
1206
+ summary: "Soft-delete webhook"
1207
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) })),
1208
+ deliveries: oc.route({
1209
+ method: "GET",
1210
+ path: "/webhooks/{id}/deliveries",
1211
+ summary: "Recent deliveries for a webhook"
1212
+ }).input(z.object({
1213
+ id: z.string().uuid(),
1214
+ limit: z.number().int().min(1).max(100).default(50)
1215
+ })).output(z.object({ data: z.array(webhookDeliveryOutput) })),
1216
+ replay: oc.route({
1217
+ method: "POST",
1218
+ path: "/webhooks/deliveries/{id}/replay",
1219
+ summary: "Re-enqueue a delivery (succeeded or otherwise)"
1220
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) }))
1221
+ };
1222
+ const events = {
1223
+ list: oc.route({
1224
+ method: "GET",
1225
+ path: "/events",
1226
+ summary: "List events"
1227
+ }).input(paginationInput.extend({ type: z.string().optional() })).output(paginatedOutput(eventOutput)),
1228
+ get: oc.route({
1229
+ method: "GET",
1230
+ path: "/events/{id}",
1231
+ summary: "Get event"
1232
+ }).input(z.object({ id: z.string().uuid() })).output(eventOutput)
1233
+ };
1234
+ //#endregion
1235
+ //#region ../contract/src/routes/insights.ts
1236
+ const counters = z.object({
1237
+ redemptionsToday: z.number().int(),
1238
+ redemptions7d: z.number().int(),
1239
+ redemptions30d: z.number().int()
1240
+ });
1241
+ const daily = z.array(z.object({
1242
+ day: z.string(),
1243
+ total: z.number().int()
1244
+ }));
1245
+ const topCampaigns = z.array(z.object({
1246
+ campaignId: z.string().uuid(),
1247
+ campaignName: z.string(),
1248
+ redemptions: z.number().int()
1249
+ }));
1250
+ const failures = z.array(z.object({
1251
+ reason: z.string(),
1252
+ total: z.number().int()
1253
+ }));
1254
+ const webhooks = z.array(z.object({
1255
+ status: z.enum([
1256
+ "pending",
1257
+ "succeeded",
1258
+ "failed",
1259
+ "dead"
1260
+ ]),
1261
+ total: z.number().int()
1262
+ }));
1263
+ const insights = { summary: oc.route({
1264
+ method: "GET",
1265
+ path: "/insights/summary",
1266
+ summary: "Headline metrics for the dashboard insights page"
1267
+ }).output(z.object({
1268
+ sinceDays: z.number().int(),
1269
+ counters,
1270
+ daily,
1271
+ topCampaigns,
1272
+ failures,
1273
+ webhooks
1274
+ })) };
1275
+ //#endregion
1276
+ //#region ../contract/src/schemas/user.ts
1277
+ const userRole = z.enum(["admin", "member"]);
1278
+ const userOutput = z.object({
1279
+ id: z.string(),
1280
+ email: z.string().email(),
1281
+ name: z.string().nullable(),
1282
+ role: userRole,
1283
+ mustChangePassword: z.boolean(),
1284
+ disabledAt: z.string().datetime().nullable(),
1285
+ createdAt: z.string().datetime()
1286
+ });
1287
+ const userCreateInput = z.object({
1288
+ email: z.string().email(),
1289
+ name: z.string().max(120).optional(),
1290
+ role: userRole.default("member")
1291
+ });
1292
+ const userCreateOutput = userOutput.extend({
1293
+ /** Generated password. Shown once at creation. */
1294
+ password: z.string() });
1295
+ //#endregion
1296
+ //#region ../contract/src/routes/users.ts
1297
+ const users = {
1298
+ list: oc.route({
1299
+ method: "GET",
1300
+ path: "/users",
1301
+ summary: "List staff users (admin)"
1302
+ }).output(z.object({ data: z.array(userOutput) })),
1303
+ create: oc.route({
1304
+ method: "POST",
1305
+ path: "/users",
1306
+ summary: "Create staff user (admin)"
1307
+ }).input(userCreateInput).output(userCreateOutput),
1308
+ resetPassword: oc.route({
1309
+ method: "POST",
1310
+ path: "/users/{id}/reset-password",
1311
+ summary: "Reset a staff user's password (admin)"
1312
+ }).input(z.object({ id: z.string() })).output(userCreateOutput),
1313
+ setRole: oc.route({
1314
+ method: "PATCH",
1315
+ path: "/users/{id}/role",
1316
+ summary: "Change a staff user's role (admin)"
1317
+ }).input(z.object({
1318
+ id: z.string(),
1319
+ role: userRole
1320
+ })).output(userOutput),
1321
+ disable: oc.route({
1322
+ method: "POST",
1323
+ path: "/users/{id}/disable",
1324
+ summary: "Disable a staff user (admin)"
1325
+ }).input(z.object({ id: z.string() })).output(userOutput),
1326
+ enable: oc.route({
1327
+ method: "POST",
1328
+ path: "/users/{id}/enable",
1329
+ summary: "Re-enable a staff user (admin)"
1330
+ }).input(z.object({ id: z.string() })).output(userOutput)
1331
+ };
1332
+ //#endregion
1333
+ //#region ../contract/src/schemas/order.ts
1334
+ const orderStatus = z.enum([
1335
+ "CREATED",
1336
+ "PAID",
1337
+ "CANCELED",
1338
+ "FULFILLED"
1339
+ ]);
1340
+ const orderItem = z.object({
1341
+ productId: z.string().optional(),
1342
+ sku: z.string().optional(),
1343
+ name: z.string().min(1),
1344
+ quantity: z.number().int().positive(),
1345
+ unitPrice: z.number().int().nonnegative()
1346
+ });
1347
+ const orderOutput = z.object({
1348
+ id: z.string().uuid(),
1349
+ externalId: z.string().nullable(),
1350
+ customerId: z.string().uuid().nullable(),
1351
+ items: z.array(orderItem),
1352
+ amount: z.number().int(),
1353
+ discountAmount: z.number().int(),
1354
+ currency: z.string(),
1355
+ status: orderStatus,
1356
+ metadata: z.record(z.string(), z.unknown()),
1357
+ createdAt: z.string().datetime(),
1358
+ updatedAt: z.string().datetime()
1359
+ });
1360
+ const orderCreateInput = z.object({
1361
+ externalId: z.string().min(1).max(120).optional(),
1362
+ customerId: z.string().uuid().optional(),
1363
+ items: z.array(orderItem).min(1),
1364
+ amount: z.number().int().nonnegative(),
1365
+ discountAmount: z.number().int().nonnegative().optional(),
1366
+ currency: z.string().length(3),
1367
+ status: orderStatus.optional(),
1368
+ metadata: z.record(z.string(), z.unknown()).optional()
1369
+ });
1370
+ const orderUpdateInput = z.object({
1371
+ id: z.string().uuid(),
1372
+ patch: z.object({
1373
+ status: orderStatus.optional(),
1374
+ discountAmount: z.number().int().nonnegative().optional(),
1375
+ metadata: z.record(z.string(), z.unknown()).optional()
1376
+ })
1377
+ });
1378
+ const orderListInput = z.object({
1379
+ limit: z.number().int().min(1).max(100).default(20),
1380
+ cursor: z.string().optional(),
1381
+ customerId: z.string().uuid().optional(),
1382
+ status: orderStatus.optional(),
1383
+ search: z.string().optional()
1384
+ });
1385
+ //#endregion
1386
+ //#region ../contract/src/routes/orders.ts
1387
+ const orders = {
1388
+ list: oc.route({
1389
+ method: "GET",
1390
+ path: "/orders",
1391
+ summary: "List orders"
1392
+ }).input(orderListInput).output(paginatedOutput(orderOutput)),
1393
+ get: oc.route({
1394
+ method: "GET",
1395
+ path: "/orders/{id}",
1396
+ summary: "Fetch one order"
1397
+ }).input(z.object({ id: z.string().uuid() })).output(orderOutput),
1398
+ create: oc.route({
1399
+ method: "POST",
1400
+ path: "/orders",
1401
+ summary: "Create an order"
1402
+ }).input(orderCreateInput).output(orderOutput),
1403
+ update: oc.route({
1404
+ method: "PATCH",
1405
+ path: "/orders/{id}",
1406
+ summary: "Update an order"
1407
+ }).input(orderUpdateInput).output(orderOutput),
1408
+ cancel: oc.route({
1409
+ method: "POST",
1410
+ path: "/orders/{id}/cancel",
1411
+ summary: "Cancel an order"
1412
+ }).input(z.object({ id: z.string().uuid() })).output(orderOutput),
1413
+ fulfill: oc.route({
1414
+ method: "POST",
1415
+ path: "/orders/{id}/fulfill",
1416
+ summary: "Mark order fulfilled"
1417
+ }).input(z.object({ id: z.string().uuid() })).output(orderOutput),
1418
+ delete: oc.route({
1419
+ method: "DELETE",
1420
+ path: "/orders/{id}",
1421
+ summary: "Soft-delete an order"
1422
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ ok: z.literal(true) })),
1423
+ redemptions: oc.route({
1424
+ method: "GET",
1425
+ path: "/orders/{id}/redemptions",
1426
+ summary: "List redemptions attached to an order"
1427
+ }).input(z.object({ id: z.string().uuid() })).output(z.object({ data: z.array(z.object({
1428
+ id: z.string().uuid(),
1429
+ voucherCode: z.string(),
1430
+ voucherId: z.string().uuid(),
1431
+ customerId: z.string().uuid().nullable(),
1432
+ result: z.enum([
1433
+ "SUCCESS",
1434
+ "FAILURE",
1435
+ "ROLLBACK"
1436
+ ]),
1437
+ failureReason: z.string().nullable(),
1438
+ amount: z.number().int().nullable(),
1439
+ createdAt: z.string().datetime()
1440
+ })) }))
1441
+ };
1442
+ //#endregion
1443
+ //#region ../contract/src/schemas/audit-log.ts
1444
+ const auditActor = z.enum([
1445
+ "user",
1446
+ "api_key",
1447
+ "system"
1448
+ ]);
1449
+ const auditLogOutput = z.object({
1450
+ id: z.string().uuid(),
1451
+ actor: auditActor,
1452
+ actorId: z.string().nullable(),
1453
+ action: z.string(),
1454
+ entity: z.string(),
1455
+ entityId: z.string().nullable(),
1456
+ before: z.unknown().nullable(),
1457
+ after: z.unknown().nullable(),
1458
+ ip: z.string().nullable(),
1459
+ userAgent: z.string().nullable(),
1460
+ createdAt: z.string().datetime()
1461
+ });
1462
+ const auditLogListInput = z.object({
1463
+ limit: z.number().int().min(1).max(100).default(50),
1464
+ cursor: z.string().optional(),
1465
+ actor: auditActor.optional(),
1466
+ entity: z.string().optional(),
1467
+ action: z.string().optional(),
1468
+ entityId: z.string().optional()
1469
+ });
1470
+ //#endregion
1471
+ //#region ../contract/src/routes/audit-log.ts
1472
+ const auditLog = { list: oc.route({
1473
+ method: "GET",
1474
+ path: "/audit-log",
1475
+ summary: "List audit log entries (admin)"
1476
+ }).input(auditLogListInput).output(z.object({
1477
+ data: z.array(auditLogOutput),
1478
+ next: z.string().optional()
1479
+ })) };
1480
+ //#endregion
1481
+ //#region ../contract/src/routes/workspace.ts
1482
+ const workspaceOutput = z.object({
1483
+ name: z.string(),
1484
+ defaultCurrency: z.string(),
1485
+ defaultTimezone: z.string(),
1486
+ emailProvider: z.enum(["resend", "log"]),
1487
+ updatedAt: z.string().datetime()
1488
+ });
1489
+ const workspaceUpdateInput = z.object({
1490
+ name: z.string().min(1).max(120).optional(),
1491
+ defaultCurrency: z.string().length(3).optional(),
1492
+ defaultTimezone: z.string().min(1).max(64).optional()
1493
+ });
1494
+ const workspace = {
1495
+ get: oc.route({
1496
+ method: "GET",
1497
+ path: "/workspace",
1498
+ summary: "Workspace settings"
1499
+ }).output(workspaceOutput),
1500
+ update: oc.route({
1501
+ method: "PATCH",
1502
+ path: "/workspace",
1503
+ summary: "Update workspace settings (admin)"
1504
+ }).input(workspaceUpdateInput).output(workspaceOutput)
1505
+ };
1506
+ //#endregion
1507
+ //#region ../contract/src/router.ts
1508
+ const healthOutput = z.object({
1509
+ status: z.literal("ok"),
1510
+ version: z.string()
1511
+ });
1512
+ const contract = {
1513
+ health: oc.route({
1514
+ method: "GET",
1515
+ path: "/health",
1516
+ summary: "Liveness probe"
1517
+ }).output(healthOutput),
1518
+ ready: oc.route({
1519
+ method: "GET",
1520
+ path: "/ready",
1521
+ summary: "Readiness probe (db + worker)"
1522
+ }).output(z.object({
1523
+ status: z.enum(["ok", "degraded"]),
1524
+ checks: z.object({
1525
+ db: z.boolean(),
1526
+ worker: z.boolean()
1527
+ })
1528
+ })),
1529
+ customers,
1530
+ segments,
1531
+ campaigns,
1532
+ vouchers,
1533
+ validationRules,
1534
+ rewardTypes,
1535
+ loyalty,
1536
+ referrals,
1537
+ apiKeys,
1538
+ webhooks: webhooks$1,
1539
+ events,
1540
+ insights,
1541
+ users,
1542
+ orders,
1543
+ auditLog,
1544
+ workspace
1545
+ };
1546
+ //#endregion
1547
+ //#region src/index.ts
1548
+ function createClient(options) {
1549
+ return createORPCClient(new OpenAPILink(contract, {
1550
+ url: `${options.baseUrl.replace(/\/$/, "")}/api/v1`,
1551
+ headers: () => {
1552
+ const headers = {};
1553
+ if (options.apiKey) headers["Authorization"] = `Bearer ${options.apiKey}`;
1554
+ return headers;
1555
+ },
1556
+ ...options.fetch ? { fetch: options.fetch } : {}
1557
+ }));
1558
+ }
1559
+ /**
1560
+ * Verify the X-Offerkit-Signature header against the raw request body.
1561
+ *
1562
+ * Format: `t=<unix-seconds>,v1=<hex>`. v1 = HMAC-SHA256(secret, "${t}.${rawBody}").
1563
+ * Returns true on match within the tolerance window.
1564
+ */
1565
+ function verifyWebhook(rawBody, signature, secret, options = {}) {
1566
+ const tolerance = options.toleranceSeconds ?? 300;
1567
+ const now = options.now ?? Date.now();
1568
+ const parts = {};
1569
+ for (const segment of signature.split(",")) {
1570
+ const idx = segment.indexOf("=");
1571
+ if (idx <= 0) continue;
1572
+ parts[segment.slice(0, idx)] = segment.slice(idx + 1);
1573
+ }
1574
+ const t = Number(parts["t"]);
1575
+ const v1 = parts["v1"];
1576
+ if (!Number.isFinite(t) || !v1) return false;
1577
+ if (Math.abs(now / 1e3 - t) > tolerance) return false;
1578
+ const expected = createHmac("sha256", secret).update(`${String(t)}.${rawBody}`).digest("hex");
1579
+ if (expected.length !== v1.length) return false;
1580
+ return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
1581
+ }
1582
+ //#endregion
1583
+ export { createClient, verifyWebhook };
1584
+
1585
+ //# sourceMappingURL=index.mjs.map