notifkit 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +130 -122
  2. package/dist/index.d.mts +196 -132
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/{main-DtHWhueo.mjs → main-40zwq6b0.mjs} +28 -3
  7. package/dist/{main-DtHWhueo.mjs.map → main-40zwq6b0.mjs.map} +1 -1
  8. package/dist/{main-DyfbnJc3.mjs → main-BFre2-HQ.mjs} +2 -2
  9. package/dist/{main-DyfbnJc3.mjs.map → main-BFre2-HQ.mjs.map} +1 -1
  10. package/dist/{main-CAH0_Q6d.mjs → main-BNJtzY61.mjs} +3 -3
  11. package/dist/main-BNJtzY61.mjs.map +1 -0
  12. package/dist/{main-B561M1d3.mjs → main-BOPMYqsW.mjs} +2 -2
  13. package/dist/{main-B561M1d3.mjs.map → main-BOPMYqsW.mjs.map} +1 -1
  14. package/dist/{main-CCfc45ev.mjs → main-CiigNpsP.mjs} +7 -4
  15. package/dist/main-CiigNpsP.mjs.map +1 -0
  16. package/dist/{main-Ce9dcrsg.mjs → main-DeNFQ-UL.mjs} +6 -3
  17. package/dist/{main-Ce9dcrsg.mjs.map → main-DeNFQ-UL.mjs.map} +1 -1
  18. package/dist/{main-B-jwm8ED.mjs → main-DmCPcxOc.mjs} +2 -2
  19. package/dist/{main-B-jwm8ED.mjs.map → main-DmCPcxOc.mjs.map} +1 -1
  20. package/dist/{main-C45e7grq.mjs → main-DvgJSm11.mjs} +2 -2
  21. package/dist/{main-C45e7grq.mjs.map → main-DvgJSm11.mjs.map} +1 -1
  22. package/dist/{src-C-PfEDMY.mjs → src-vG79L-8m.mjs} +57 -26
  23. package/dist/src-vG79L-8m.mjs.map +1 -0
  24. package/drizzle/0002_wide_colleen_wing.sql +2 -0
  25. package/drizzle/0003_skinny_daimon_hellstrom.sql +1 -0
  26. package/drizzle/0004_pretty_bruce_banner.sql +1 -0
  27. package/drizzle/meta/0002_snapshot.json +1460 -0
  28. package/drizzle/meta/0003_snapshot.json +1460 -0
  29. package/drizzle/meta/0004_snapshot.json +1470 -0
  30. package/drizzle/meta/_journal.json +21 -0
  31. package/package.json +3 -2
  32. package/src/client.ts +412 -0
  33. package/src/config/index.ts +107 -0
  34. package/src/contracts/common.ts +28 -0
  35. package/src/contracts/envelope.ts +31 -0
  36. package/src/contracts/events/notification-ai-pending.ts +18 -0
  37. package/src/contracts/events/notification-canceled.ts +7 -0
  38. package/src/contracts/events/notification-created.ts +14 -0
  39. package/src/contracts/events/notification-delivered.ts +17 -0
  40. package/src/contracts/events/notification-dispatched.ts +45 -0
  41. package/src/contracts/events/notification-enriched.ts +46 -0
  42. package/src/contracts/events/notification-failed.ts +19 -0
  43. package/src/contracts/events/notification-requested.ts +36 -0
  44. package/src/contracts/events/notification-scheduled.ts +9 -0
  45. package/src/contracts/events/notification-skipped.ts +9 -0
  46. package/src/contracts/helpers.ts +21 -0
  47. package/src/contracts/index.ts +46 -0
  48. package/src/contracts/metadata.ts +10 -0
  49. package/src/contracts/registry.ts +88 -0
  50. package/src/contracts/sdk.ts +242 -0
  51. package/src/contracts/streams.ts +62 -0
  52. package/src/db/index.ts +69 -0
  53. package/src/db/schema.ts +412 -0
  54. package/src/idempotency/index.ts +50 -0
  55. package/src/index.ts +19 -0
  56. package/src/logger/index.ts +60 -0
  57. package/src/metrics/index.ts +53 -0
  58. package/src/queue/index.ts +501 -0
  59. package/src/rate-limiter/index.ts +210 -0
  60. package/src/redis/index.ts +89 -0
  61. package/src/repositories/index.ts +1246 -0
  62. package/src/server.ts +277 -0
  63. package/src/services/ai/main.ts +404 -0
  64. package/src/services/api/handlers.ts +1734 -0
  65. package/src/services/api/http.ts +64 -0
  66. package/src/services/api/main.ts +693 -0
  67. package/src/services/api/router.ts +82 -0
  68. package/src/services/delivery/main.ts +842 -0
  69. package/src/services/delivery/throttle.ts +71 -0
  70. package/src/services/engine/main.ts +827 -0
  71. package/src/services/enricher/main.ts +594 -0
  72. package/src/services/events/main.ts +365 -0
  73. package/src/services/scheduler/main.ts +319 -0
  74. package/src/services/workflow/main.ts +627 -0
  75. package/src/shared/batch-processor.ts +67 -0
  76. package/src/shared/cache.ts +47 -0
  77. package/src/shared/circuit-breaker.ts +74 -0
  78. package/src/shared/dataloader.ts +41 -0
  79. package/src/shared/events.ts +3 -0
  80. package/src/shared/index.ts +39 -0
  81. package/src/shared/semaphore.ts +33 -0
  82. package/src/shared/utils.ts +64 -0
  83. package/src/templates/cache.ts +32 -0
  84. package/src/templates/index.ts +69 -0
  85. package/src/templates/render.ts +128 -0
  86. package/src/transport/index.ts +96 -0
  87. package/src/unsubscribe/index.ts +127 -0
  88. package/src/workers/health.ts +31 -0
  89. package/src/workers/index.ts +266 -0
  90. package/src/workflows/index.ts +2 -0
  91. package/src/workflows/registry.ts +21 -0
  92. package/src/workflows/sdk.ts +106 -0
  93. package/dist/main-CAH0_Q6d.mjs.map +0 -1
  94. package/dist/main-CCfc45ev.mjs.map +0 -1
  95. package/dist/src-C-PfEDMY.mjs.map +0 -1
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema } from "@/contracts/common.js";
3
+
4
+ export const NotificationDeliveredPayloadSchema = z.object({
5
+ projectId: z.string().uuid(),
6
+ taskId: z.string().min(1),
7
+ enrichedEventId: z.string().uuid(),
8
+ channel: NotificationChannelSchema,
9
+ deliveredAt: z.string().datetime(),
10
+ providerMessageId: z.string().optional(),
11
+ providerResponse: z.record(z.string(), z.unknown()).optional(),
12
+ templateId: z.string().uuid().optional(),
13
+ workflowInstanceId: z.string().uuid().optional(),
14
+ /** Campaign this message belongs to, carried from the originating request. */
15
+ campaignId: z.string().min(1).max(128).optional(),
16
+ });
17
+ export type NotificationDeliveredPayload = z.infer<typeof NotificationDeliveredPayloadSchema>;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema, NotificationPrioritySchema } from "@/contracts/common.js";
3
+ import { RecipientProfileSchema } from "./notification-enriched.js";
4
+
5
+ export const RenderedContentSchema = z.object({
6
+ content: z.record(z.string(), z.unknown()),
7
+ attachments: z
8
+ .array(
9
+ z.object({
10
+ name: z.string(),
11
+ contentType: z.string(),
12
+ url: z.string().url(),
13
+ }),
14
+ )
15
+ .optional(),
16
+ });
17
+ export type RenderedContent = z.infer<typeof RenderedContentSchema>;
18
+
19
+ export const DeliveryOptionsSchema = z.object({
20
+ maxAttempts: z.number().int().positive().default(3),
21
+ timeoutMs: z.number().int().positive().default(10_000),
22
+ headers: z.record(z.string(), z.string()).optional(),
23
+ });
24
+ export type DeliveryOptions = z.infer<typeof DeliveryOptionsSchema>;
25
+
26
+ export const NotificationDispatchedPayloadSchema = z.object({
27
+ projectId: z.string().uuid(),
28
+ taskId: z.string().min(1),
29
+ enrichedEventId: z.string().uuid(),
30
+ recipientId: z.string().min(1),
31
+ channel: NotificationChannelSchema,
32
+ priority: NotificationPrioritySchema,
33
+ templateId: z.string().min(1).optional(),
34
+ templateVariables: z.record(z.string(), z.unknown()).default({}),
35
+ aiPrompts: z.record(z.string(), z.string()).optional(),
36
+ recipient: RecipientProfileSchema.optional(),
37
+ renderedContent: RenderedContentSchema,
38
+ destination: z.string().min(1).optional(),
39
+ deliveryOptions: DeliveryOptionsSchema,
40
+ fallbackChain: z.array(NotificationChannelSchema).optional(),
41
+ throttleAttemptCount: z.number().int().nonnegative().optional(),
42
+ /** Campaign this message belongs to, carried from the originating request. */
43
+ campaignId: z.string().min(1).max(128).optional(),
44
+ });
45
+ export type NotificationDispatchedPayload = z.infer<typeof NotificationDispatchedPayloadSchema>;
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema, NotificationPrioritySchema } from "@/contracts/common.js";
3
+
4
+ export const RecipientProfileSchema = z.object({
5
+ id: z.string(),
6
+ email: z.string().email().optional(),
7
+ phone: z.string().optional(),
8
+ webhook: z.string().url().optional(),
9
+ pushTokens: z.array(z.string()).optional(),
10
+ pushToken: z.string().optional(),
11
+ telegram: z.string().optional(),
12
+ discord: z.string().url().optional(),
13
+ slack: z.string().optional(),
14
+ locale: z.string().default("en"),
15
+ timezone: z.string().default("UTC"),
16
+ preferences: z.object({
17
+ optedOut: z.boolean().default(false),
18
+ channels: z.array(NotificationChannelSchema).default([]),
19
+ quietHours: z
20
+ .array(
21
+ z.object({
22
+ start: z.string(),
23
+ end: z.string(),
24
+ }),
25
+ )
26
+ .optional(),
27
+ }),
28
+ });
29
+ export type RecipientProfile = z.infer<typeof RecipientProfileSchema>;
30
+
31
+ export const NotificationEnrichedPayloadSchema = z.object({
32
+ projectId: z.string().uuid(),
33
+ rawEventId: z.string().uuid(),
34
+ recipientId: z.string().min(1),
35
+ channel: NotificationChannelSchema,
36
+ priority: NotificationPrioritySchema,
37
+ templateId: z.string().min(1).optional(),
38
+ templateVariables: z.record(z.string(), z.unknown()),
39
+ recipient: RecipientProfileSchema,
40
+ aiPrompts: z.record(z.string(), z.string()).optional(),
41
+ scheduledAt: z.string().datetime().optional(),
42
+ fallbackChain: z.array(NotificationChannelSchema).optional(),
43
+ /** Campaign this message belongs to, carried from the originating request. */
44
+ campaignId: z.string().min(1).max(128).optional(),
45
+ });
46
+ export type NotificationEnrichedPayload = z.infer<typeof NotificationEnrichedPayloadSchema>;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema } from "@/contracts/common.js";
3
+
4
+ export const NotificationFailedPayloadSchema = z.object({
5
+ projectId: z.string().uuid(),
6
+ taskId: z.string().min(1),
7
+ enrichedEventId: z.string().uuid(),
8
+ channel: NotificationChannelSchema,
9
+ failureReason: z.string(),
10
+ failureCode: z.string(),
11
+ retryable: z.boolean(),
12
+ attempt: z.number().int().positive(),
13
+ providerResponse: z.record(z.string(), z.unknown()).optional(),
14
+ templateId: z.string().uuid().optional(),
15
+ workflowInstanceId: z.string().uuid().optional(),
16
+ /** Campaign this message belongs to, carried from the originating request. */
17
+ campaignId: z.string().min(1).max(128).optional(),
18
+ });
19
+ export type NotificationFailedPayload = z.infer<typeof NotificationFailedPayloadSchema>;
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema, NotificationPrioritySchema } from "@/contracts/common.js";
3
+
4
+ /**
5
+ * A high-level notification request as issued by the SDK's `notify()` call.
6
+ *
7
+ * Unlike `notification.created` (which targets a single resolved recipient),
8
+ * this event carries the *unresolved* target — a user id, a segment, or a
9
+ * topic. A downstream resolver stage fans it out into one
10
+ * `notification.created` per matching recipient, applying preference filters.
11
+ */
12
+ export const NotificationTargetSchema = z.discriminatedUnion("type", [
13
+ z.object({ type: z.literal("user"), userId: z.string().min(1) }),
14
+ z.object({ type: z.literal("segment"), segment: z.string().min(1) }),
15
+ z.object({ type: z.literal("topic"), topic: z.string().min(1) }),
16
+ ]);
17
+ export type NotificationTarget = z.infer<typeof NotificationTargetSchema>;
18
+
19
+ export const NotificationRequestedPayloadSchema = z.object({
20
+ projectId: z.string().uuid(),
21
+ target: NotificationTargetSchema,
22
+ templateId: z.string().min(1),
23
+ priority: NotificationPrioritySchema.default("normal"),
24
+ channels: z.array(NotificationChannelSchema).nonempty().optional(),
25
+ data: z.record(z.string(), z.unknown()).default({}),
26
+ fallback: z.boolean().default(false),
27
+ aiPrompts: z.record(z.string(), z.string()).optional(),
28
+ scheduledAt: z.string().datetime().optional(),
29
+ idempotencyKey: z.string().optional(),
30
+ /**
31
+ * Groups every message this request fans out into, so the send can be
32
+ * reported on afterwards. Carried unchanged to the delivery log.
33
+ */
34
+ campaignId: z.string().min(1).max(128).optional(),
35
+ });
36
+ export type NotificationRequestedPayload = z.infer<typeof NotificationRequestedPayloadSchema>;
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const NotificationScheduledPayloadSchema = z.object({
4
+ projectId: z.string().uuid(),
5
+ enrichedEventId: z.string().uuid(),
6
+ taskId: z.string().min(1),
7
+ scheduledAt: z.string().datetime(),
8
+ });
9
+ export type NotificationScheduledPayload = z.infer<typeof NotificationScheduledPayloadSchema>;
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const NotificationSkippedPayloadSchema = z.object({
4
+ projectId: z.string().uuid(),
5
+ eventId: z.string().uuid(),
6
+ recipientId: z.string(),
7
+ reason: z.string(),
8
+ });
9
+ export type NotificationSkippedPayload = z.infer<typeof NotificationSkippedPayloadSchema>;
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { type EventEnvelope } from "./envelope.js";
3
+
4
+ // Legacy factory helpers — backward compatible with the queue package's publish() API,
5
+ // which accepts Omit<StreamEvent, "id" | "timestamp">.
6
+ export function buildStreamEvent(
7
+ type: string,
8
+ payload: Record<string, unknown>,
9
+ source: string,
10
+ traceId?: string,
11
+ ): Omit<EventEnvelope, "id" | "timestamp"> {
12
+ return {
13
+ type,
14
+ payload,
15
+ metadata: {
16
+ traceId: traceId ?? randomUUID(),
17
+ source,
18
+ retryCount: 0,
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,46 @@
1
+ // Core building blocks
2
+ export * from "./common.js";
3
+ export * from "./metadata.js";
4
+ export * from "./registry.js";
5
+ export * from "./envelope.js";
6
+ export * from "./streams.js";
7
+ export * from "./helpers.js";
8
+ export * from "./sdk.js";
9
+
10
+ // Event payload schemas and types
11
+ export * from "./events/notification-requested.js";
12
+ export * from "./events/notification-created.js";
13
+ export * from "./events/notification-enriched.js";
14
+ export * from "./events/notification-scheduled.js";
15
+ export * from "./events/notification-dispatched.js";
16
+ export * from "./events/notification-delivered.js";
17
+ export * from "./events/notification-failed.js";
18
+ export * from "./events/notification-skipped.js";
19
+ export * from "./events/notification-canceled.js";
20
+ export * from "./events/notification-ai-pending.js";
21
+
22
+ // Register all built-in event schemas in the global registry.
23
+ // This runs once at module load time; call registry.define() in your own
24
+ // module to add new event types without modifying this file.
25
+ import { registry } from "./registry.js";
26
+ import { NotificationRequestedPayloadSchema } from "./events/notification-requested.js";
27
+ import { NotificationCreatedPayloadSchema } from "./events/notification-created.js";
28
+ import { NotificationEnrichedPayloadSchema } from "./events/notification-enriched.js";
29
+ import { NotificationScheduledPayloadSchema } from "./events/notification-scheduled.js";
30
+ import { NotificationDispatchedPayloadSchema } from "./events/notification-dispatched.js";
31
+ import { NotificationDeliveredPayloadSchema } from "./events/notification-delivered.js";
32
+ import { NotificationFailedPayloadSchema } from "./events/notification-failed.js";
33
+ import { NotificationSkippedPayloadSchema } from "./events/notification-skipped.js";
34
+ import { NotificationCanceledPayloadSchema } from "./events/notification-canceled.js";
35
+ import { NotificationAiPendingPayloadSchema } from "./events/notification-ai-pending.js";
36
+
37
+ registry.define("notification.requested", NotificationRequestedPayloadSchema);
38
+ registry.define("notification.created", NotificationCreatedPayloadSchema);
39
+ registry.define("notification.enriched", NotificationEnrichedPayloadSchema);
40
+ registry.define("notification.scheduled", NotificationScheduledPayloadSchema);
41
+ registry.define("notification.dispatched", NotificationDispatchedPayloadSchema);
42
+ registry.define("notification.delivered", NotificationDeliveredPayloadSchema);
43
+ registry.define("notification.failed", NotificationFailedPayloadSchema);
44
+ registry.define("notification.skipped", NotificationSkippedPayloadSchema);
45
+ registry.define("notification.canceled", NotificationCanceledPayloadSchema);
46
+ registry.define("notification.ai_pending", NotificationAiPendingPayloadSchema);
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+
3
+ export const EventMetadataSchema = z.object({
4
+ traceId: z.string(),
5
+ source: z.string(),
6
+ retryCount: z.number().int().nonnegative().default(0),
7
+ correlationId: z.string().optional(),
8
+ causationId: z.string().optional(),
9
+ });
10
+ export type EventMetadata = z.infer<typeof EventMetadataSchema>;
@@ -0,0 +1,88 @@
1
+ import { z, type ZodTypeAny, type ZodError } from "zod";
2
+ import type { NotificationRequestedPayload } from "./events/notification-requested.js";
3
+ import type { NotificationCreatedPayload } from "./events/notification-created.js";
4
+ import type { NotificationEnrichedPayload } from "./events/notification-enriched.js";
5
+ import type { NotificationScheduledPayload } from "./events/notification-scheduled.js";
6
+ import type { NotificationDispatchedPayload } from "./events/notification-dispatched.js";
7
+ import type { NotificationDeliveredPayload } from "./events/notification-delivered.js";
8
+ import type { NotificationFailedPayload } from "./events/notification-failed.js";
9
+ import type { NotificationSkippedPayload } from "./events/notification-skipped.js";
10
+ import type { NotificationCanceledPayload } from "./events/notification-canceled.js";
11
+ import type { NotificationAiPendingPayload } from "./events/notification-ai-pending.js";
12
+ /**
13
+ * Built-in event payload types.
14
+ * External packages extend this interface via declaration merging:
15
+ *
16
+ * declare module "../index.js" {
17
+ * interface EventPayloadMap {
18
+ * "my.custom.event": { field: string };
19
+ * }
20
+ * }
21
+ *
22
+ * Then call registry.define("my.custom.event", MySchema) at app startup.
23
+ */
24
+ export interface EventPayloadMap {
25
+ "notification.requested": NotificationRequestedPayload;
26
+ "notification.created": NotificationCreatedPayload;
27
+ "notification.enriched": NotificationEnrichedPayload;
28
+ "notification.scheduled": NotificationScheduledPayload;
29
+ "notification.dispatched": NotificationDispatchedPayload;
30
+ "notification.delivered": NotificationDeliveredPayload;
31
+ "notification.failed": NotificationFailedPayload;
32
+ "notification.skipped": NotificationSkippedPayload;
33
+ "notification.canceled": NotificationCanceledPayload;
34
+ "notification.ai_pending": NotificationAiPendingPayload;
35
+ }
36
+
37
+ export type KnownEventType = keyof EventPayloadMap & string;
38
+
39
+ export type ParseResult<T> = { success: true; data: T } | { success: false; error: ZodError };
40
+
41
+ export class EventRegistry {
42
+ private readonly schemas = new Map<string, ZodTypeAny>();
43
+
44
+ define(type: string, schema: ZodTypeAny): void {
45
+ if (this.schemas.has(type)) {
46
+ throw new Error(`Event type "${type}" is already registered`);
47
+ }
48
+ this.schemas.set(type, schema);
49
+ }
50
+
51
+ getSchema(type: string): ZodTypeAny | undefined {
52
+ return this.schemas.get(type);
53
+ }
54
+
55
+ has(type: string): boolean {
56
+ return this.schemas.has(type);
57
+ }
58
+
59
+ types(): string[] {
60
+ return [...this.schemas.keys()];
61
+ }
62
+
63
+ parsePayload<K extends KnownEventType>(type: K, payload: unknown): EventPayloadMap[K] {
64
+ const schema = this.schemas.get(type);
65
+ if (!schema) throw new Error(`Unknown event type: "${type}"`);
66
+ return schema.parse(payload) as EventPayloadMap[K];
67
+ }
68
+
69
+ safeParsePayload<K extends KnownEventType>(
70
+ type: K,
71
+ payload: unknown,
72
+ ): ParseResult<EventPayloadMap[K]> {
73
+ const schema = this.schemas.get(type);
74
+ if (!schema) {
75
+ return {
76
+ success: false,
77
+ error: new z.ZodError([
78
+ { code: "custom", message: `Unknown event type: "${type}"`, path: [] },
79
+ ]),
80
+ };
81
+ }
82
+ const result = schema.safeParse(payload);
83
+ if (result.success) return { success: true, data: result.data as EventPayloadMap[K] };
84
+ return { success: false, error: result.error };
85
+ }
86
+ }
87
+
88
+ export const registry = new EventRegistry();
@@ -0,0 +1,242 @@
1
+ import { z } from "zod";
2
+ import { NotificationChannelSchema, NotificationPrioritySchema } from "./common.js";
3
+
4
+ // ─── Preferences ──────────────────────────────────────────────────────────────
5
+ //
6
+ // Preferences are stored as JSONB and are intentionally open-ended so new
7
+ // channels/topics can be added without a migration. `channels` and `topics`
8
+ // are boolean opt-in maps; `quietHours` is a list of UTC HH:MM windows.
9
+
10
+ export const QuietHoursSchema = z.object({
11
+ start: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:MM (24h, UTC)"),
12
+ end: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:MM (24h, UTC)"),
13
+ });
14
+ export type QuietHours = z.infer<typeof QuietHoursSchema>;
15
+
16
+ export const PreferencesSchema = z.object({
17
+ channels: z.record(z.string(), z.boolean()).optional(),
18
+ topics: z.record(z.string(), z.boolean()).optional(),
19
+ quietHours: z.array(QuietHoursSchema).optional(),
20
+ });
21
+ export type Preferences = z.infer<typeof PreferencesSchema>;
22
+
23
+ // ─── Contacts ─────────────────────────────────────────────────────────────────
24
+
25
+ /** Channels that carry an addressable target (email address, phone, push token, url, chat id). */
26
+ export const ContactChannelSchema = z.enum([
27
+ "email",
28
+ "sms",
29
+ "push",
30
+ "webhook",
31
+ "telegram",
32
+ "discord",
33
+ "whatsapp",
34
+ "slack",
35
+ ]);
36
+ export type ContactChannel = z.infer<typeof ContactChannelSchema>;
37
+
38
+ /** Accept a single value or an array; always normalise to a non-empty array. */
39
+ const stringOrArray = z
40
+ .union([z.string().min(1), z.array(z.string().min(1))])
41
+ .transform((v) => (Array.isArray(v) ? v : [v]));
42
+
43
+ // ─── Users ────────────────────────────────────────────────────────────────────
44
+
45
+ /**
46
+ * addUser({ id, email, phone, pushToken, segments, preferences })
47
+ * email / phone / pushToken accept a single string or an array.
48
+ */
49
+ export const AddUserSchema = z.object({
50
+ id: z.string().min(1),
51
+ language: z.string().optional(),
52
+ timezone: z.string().optional(),
53
+ email: stringOrArray.optional(),
54
+ phone: stringOrArray.optional(),
55
+ pushToken: stringOrArray.optional(),
56
+ segments: z.array(z.string().min(1)).optional(),
57
+ preferences: PreferencesSchema.optional(),
58
+ });
59
+ export type AddUserInput = z.input<typeof AddUserSchema>;
60
+
61
+ /** updateUser(id, patch) — every field optional; id comes from the path. */
62
+ export const UpdateUserSchema = z.object({
63
+ language: z.string().optional(),
64
+ timezone: z.string().optional(),
65
+ email: stringOrArray.optional(),
66
+ phone: stringOrArray.optional(),
67
+ pushToken: stringOrArray.optional(),
68
+ segments: z.array(z.string().min(1)).optional(),
69
+ preferences: PreferencesSchema.optional(),
70
+ });
71
+ export type UpdateUserInput = z.input<typeof UpdateUserSchema>;
72
+
73
+ /** addUserContact(userId, channel, { target, preferences }) — channel carried in body. */
74
+ export const AddContactSchema = z.object({
75
+ channel: ContactChannelSchema,
76
+ target: z.string().min(1),
77
+ preferences: PreferencesSchema.optional(),
78
+ });
79
+ export type AddContactInput = z.infer<typeof AddContactSchema>;
80
+
81
+ // ─── Templates ────────────────────────────────────────────────────────────────
82
+
83
+ export const TemplateSchema = z.object({
84
+ id: z.string().min(1),
85
+ channel: NotificationChannelSchema,
86
+ topic: z
87
+ .union([z.string().min(1), z.array(z.string().min(1))])
88
+ .transform((v) => (Array.isArray(v) ? v : [v]))
89
+ .optional(),
90
+ content: z.record(z.string(), z.unknown()),
91
+ aiPrompts: z.record(z.string(), z.string()).optional(),
92
+ });
93
+ export type TemplateInput = z.infer<typeof TemplateSchema>;
94
+
95
+ export const SyncTemplatesSchema = z.object({
96
+ templates: z.array(TemplateSchema).min(1),
97
+ });
98
+ export type SyncTemplatesInput = z.infer<typeof SyncTemplatesSchema>;
99
+
100
+ // ─── notify() ─────────────────────────────────────────────────────────────────
101
+
102
+ /** Inline user object accepted by notify({ user: {...} }). */
103
+ export const InlineUserSchema = z.object({
104
+ id: z.string().min(1),
105
+ language: z.string().optional(),
106
+ timezone: z.string().optional(),
107
+ email: stringOrArray.optional(),
108
+ phone: stringOrArray.optional(),
109
+ pushToken: stringOrArray.optional(),
110
+ segments: z.array(z.string().min(1)).optional(),
111
+ preferences: PreferencesSchema.optional(),
112
+ });
113
+ export type InlineUser = z.infer<typeof InlineUserSchema>;
114
+
115
+ /**
116
+ * notify(...) request body.
117
+ *
118
+ * Exactly one of `user` / `segment` / `topic` must be provided.
119
+ */
120
+ /**
121
+ * The field set shared by `notify()` and a workflow's `notify` step. The two
122
+ * differ only in whether naming a target is mandatory, so the fields live here
123
+ * once and each schema layers its own target rule on top.
124
+ */
125
+ const NotifyRequestFields = z.object({
126
+ user: z
127
+ .union([
128
+ z.string().min(1),
129
+ InlineUserSchema,
130
+ z.array(z.union([z.string().min(1), InlineUserSchema])).nonempty(),
131
+ ])
132
+ .optional(),
133
+ segment: z.string().min(1).optional(),
134
+ topic: z.string().min(1).optional(),
135
+ template: z.string().min(1),
136
+ data: z.record(z.string(), z.unknown()).optional(),
137
+ aiPrompts: z.record(z.string(), z.string()).optional(),
138
+ priority: NotificationPrioritySchema.optional(),
139
+ channels: z.array(NotificationChannelSchema).nonempty().optional(),
140
+ fallback: z.boolean().optional(),
141
+ sendAt: z.string().datetime().optional(),
142
+ /**
143
+ * A label grouping every message this call produces, so the send can be
144
+ * reported on later via `/v1/campaigns/:id/stats`. Free-form, but reusing one
145
+ * label across calls merges them into a single campaign — which is either
146
+ * what you want (a send split into batches) or a reporting bug.
147
+ */
148
+ campaign: z.string().min(1).max(128).optional(),
149
+ });
150
+
151
+ function countTargets(val: { user?: unknown; segment?: unknown; topic?: unknown }): number {
152
+ return [val.user, val.segment, val.topic].filter((t) => t !== undefined).length;
153
+ }
154
+
155
+ export const NotifyRequestSchema = NotifyRequestFields.superRefine((val, ctx) => {
156
+ const targets = countTargets(val);
157
+ if (targets === 0) {
158
+ ctx.addIssue({
159
+ code: z.ZodIssueCode.custom,
160
+ message: "one of `user`, `segment`, or `topic` is required",
161
+ path: ["user"],
162
+ });
163
+ } else if (targets > 1) {
164
+ ctx.addIssue({
165
+ code: z.ZodIssueCode.custom,
166
+ message: "provide exactly one of `user`, `segment`, or `topic`",
167
+ path: ["user"],
168
+ });
169
+ }
170
+ });
171
+ export type NotifyRequestInput = z.input<typeof NotifyRequestSchema>;
172
+
173
+ /**
174
+ * The payload of a workflow `notify` step — every field `notify()` takes.
175
+ *
176
+ * The one difference is that a target is optional here: a step naming none
177
+ * inherits the instance's own user, which is the ordinary case. Naming one
178
+ * overrides that, so a step can notify a different user, a segment, or a topic.
179
+ */
180
+ export const WorkflowNotifyPayloadSchema = NotifyRequestFields.superRefine((val, ctx) => {
181
+ if (countTargets(val) > 1) {
182
+ ctx.addIssue({
183
+ code: z.ZodIssueCode.custom,
184
+ message: "provide at most one of `user`, `segment`, or `topic`",
185
+ path: ["user"],
186
+ });
187
+ }
188
+ });
189
+ export type WorkflowNotifyInput = z.input<typeof WorkflowNotifyPayloadSchema>;
190
+
191
+ // ─── Workflows ────────────────────────────────────────────────────────────────
192
+
193
+ export const TriggerWorkflowSchema = z.object({
194
+ name: z.string().min(1),
195
+ input: z.record(z.string(), z.unknown()).optional(),
196
+ user: z.union([z.string().min(1), InlineUserSchema]).optional(),
197
+ });
198
+ export type TriggerWorkflowInput = z.infer<typeof TriggerWorkflowSchema>;
199
+
200
+ // ─── Events ───────────────────────────────────────────────────────────────────
201
+
202
+ export const IngestEventSchema = z.object({
203
+ name: z.string().min(1),
204
+ properties: z.record(z.string(), z.unknown()),
205
+ });
206
+ export type IngestEventInput = z.infer<typeof IngestEventSchema>;
207
+
208
+ export const WorkflowStepSchema = z.discriminatedUnion("action", [
209
+ z.object({
210
+ action: z.literal("notify"),
211
+ payload: WorkflowNotifyPayloadSchema,
212
+ }),
213
+ z.object({
214
+ action: z.literal("wait"),
215
+ duration: z.string(), // e.g. "1h", "5m"
216
+ }),
217
+ z.object({
218
+ action: z.literal("waitForEvent"),
219
+ event: z.string(),
220
+ options: z
221
+ .object({
222
+ timeout: z.string().optional(), // e.g. "24h"
223
+ })
224
+ .optional(),
225
+ }),
226
+ ]);
227
+ export type WorkflowStepDef = z.infer<typeof WorkflowStepSchema>;
228
+
229
+ export const CreateWorkflowSchema = z.object({
230
+ name: z.string().min(1),
231
+ steps: z.array(WorkflowStepSchema).min(1),
232
+ });
233
+ export type CreateWorkflowInput = z.infer<typeof CreateWorkflowSchema>;
234
+
235
+ // ─── Projects ─────────────────────────────────────────────────────────────────
236
+
237
+ export const UpdateProjectSchema = z.object({
238
+ rateLimitRpm: z.number().nullable().optional(),
239
+ throttleLimit: z.number().nullable().optional(),
240
+ throttleWindowHours: z.number().nullable().optional(),
241
+ });
242
+ export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>;
@@ -0,0 +1,62 @@
1
+ export const STREAMS = {
2
+ INBOUND_CRITICAL: "notifkit:stream:inbound:critical",
3
+ INBOUND_NORMAL: "notifkit:stream:inbound:normal",
4
+ INBOUND_LOW: "notifkit:stream:inbound:low",
5
+ ENRICHED_CRITICAL: "notifkit:stream:enriched:critical",
6
+ ENRICHED_NORMAL: "notifkit:stream:enriched:normal",
7
+ ENRICHED_LOW: "notifkit:stream:enriched:low",
8
+ AI_PENDING: "notifkit:stream:ai:pending",
9
+ SCHEDULED: "notifkit:stream:scheduled",
10
+ OUTBOUND_CRITICAL: "notifkit:stream:outbound:critical",
11
+ OUTBOUND_NORMAL: "notifkit:stream:outbound:normal",
12
+ OUTBOUND_LOW: "notifkit:stream:outbound:low",
13
+ DEAD_LETTER: "notifkit:stream:dlq",
14
+ WORKFLOW_INBOUND: "notifkit:stream:workflow:inbound",
15
+ EVENTS_INBOUND: "notifkit:stream:events:inbound",
16
+ } as const;
17
+
18
+ export const INBOUND_STREAMS = [
19
+ STREAMS.INBOUND_CRITICAL,
20
+ STREAMS.INBOUND_NORMAL,
21
+ STREAMS.INBOUND_LOW,
22
+ ] as const;
23
+
24
+ export const ENRICHED_STREAMS = [
25
+ STREAMS.ENRICHED_CRITICAL,
26
+ STREAMS.ENRICHED_NORMAL,
27
+ STREAMS.ENRICHED_LOW,
28
+ ] as const;
29
+
30
+ export const OUTBOUND_STREAMS = [
31
+ STREAMS.OUTBOUND_CRITICAL,
32
+ STREAMS.OUTBOUND_NORMAL,
33
+ STREAMS.OUTBOUND_LOW,
34
+ ] as const;
35
+ export type StreamName = (typeof STREAMS)[keyof typeof STREAMS];
36
+
37
+ /**
38
+ * Redis pub/sub channels used to drop cached state across every process.
39
+ *
40
+ * Each cache also carries a TTL, so these only shorten the window in which a
41
+ * worker can act on stale data — they are not the sole correctness mechanism.
42
+ */
43
+ export const PUBSUB_CHANNELS = {
44
+ /** Payload: `{projectId}:{templateId}`. */
45
+ TEMPLATE_INVALIDATED: "template.invalidated",
46
+ /** Payload: `{projectId}`. Published when project settings change. */
47
+ PROJECT_INVALIDATED: "project.invalidated",
48
+ /** Payload: a token hash, or `*` for the whole cache. */
49
+ API_KEY_INVALIDATED: "apikey.invalidated",
50
+ } as const;
51
+ export type PubSubChannel = (typeof PUBSUB_CHANNELS)[keyof typeof PUBSUB_CHANNELS];
52
+
53
+ export const CONSUMER_GROUPS = {
54
+ ENRICHER: "notifkit:group:enricher",
55
+ ENGINE: "notifkit:group:engine",
56
+ DELIVERY: "notifkit:group:delivery",
57
+ SCHEDULER: "notifkit:group:scheduler",
58
+ AI: "notifkit:group:ai",
59
+ WORKFLOW: "notifkit:group:workflow",
60
+ EVENTS: "notifkit:group:events",
61
+ } as const;
62
+ export type ConsumerGroup = (typeof CONSUMER_GROUPS)[keyof typeof CONSUMER_GROUPS];