hazo_notify 5.0.0 → 5.2.1

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 (113) hide show
  1. package/README.md +5 -5
  2. package/dist/components/notification_bell/index.d.ts +11 -1
  3. package/dist/components/notification_bell/index.d.ts.map +1 -1
  4. package/dist/components/notification_bell/index.js +33 -1
  5. package/dist/components/notification_bell/index.js.map +1 -1
  6. package/dist/lib/adapters/email/adapter.d.ts +2 -0
  7. package/dist/lib/adapters/email/adapter.d.ts.map +1 -1
  8. package/dist/lib/adapters/email/adapter.js +53 -22
  9. package/dist/lib/adapters/email/adapter.js.map +1 -1
  10. package/dist/lib/adapters/webhook/adapter.d.ts +10 -0
  11. package/dist/lib/adapters/webhook/adapter.d.ts.map +1 -0
  12. package/dist/lib/adapters/webhook/adapter.js +90 -0
  13. package/dist/lib/adapters/webhook/adapter.js.map +1 -0
  14. package/dist/lib/adapters/webhook/index.d.ts +8 -0
  15. package/dist/lib/adapters/webhook/index.d.ts.map +1 -0
  16. package/dist/lib/adapters/webhook/index.js +3 -0
  17. package/dist/lib/adapters/webhook/index.js.map +1 -0
  18. package/dist/lib/adapters/webhook/types.d.ts +45 -0
  19. package/dist/lib/adapters/webhook/types.d.ts.map +1 -0
  20. package/dist/lib/adapters/webhook/types.js +12 -0
  21. package/dist/lib/adapters/webhook/types.js.map +1 -0
  22. package/dist/lib/api/inbox.d.ts +32 -0
  23. package/dist/lib/api/inbox.d.ts.map +1 -1
  24. package/dist/lib/api/inbox.js +68 -0
  25. package/dist/lib/api/inbox.js.map +1 -1
  26. package/dist/lib/dispatcher/index.d.ts +2 -2
  27. package/dist/lib/dispatcher/index.d.ts.map +1 -1
  28. package/dist/lib/dispatcher/index.js +28 -8
  29. package/dist/lib/dispatcher/index.js.map +1 -1
  30. package/dist/lib/inbox/broadcaster.d.ts +44 -0
  31. package/dist/lib/inbox/broadcaster.d.ts.map +1 -0
  32. package/dist/lib/inbox/broadcaster.js +62 -0
  33. package/dist/lib/inbox/broadcaster.js.map +1 -0
  34. package/dist/lib/inbox/index.d.ts +2 -0
  35. package/dist/lib/inbox/index.d.ts.map +1 -1
  36. package/dist/lib/inbox/index.js +1 -0
  37. package/dist/lib/inbox/index.js.map +1 -1
  38. package/dist/lib/inbox/storage.d.ts +11 -32
  39. package/dist/lib/inbox/storage.d.ts.map +1 -1
  40. package/dist/lib/inbox/storage.js +22 -38
  41. package/dist/lib/inbox/storage.js.map +1 -1
  42. package/dist/lib/inbox/types.d.ts +7 -0
  43. package/dist/lib/inbox/types.d.ts.map +1 -1
  44. package/dist/lib/jobs/submit.d.ts.map +1 -1
  45. package/dist/lib/jobs/submit.js +1 -0
  46. package/dist/lib/jobs/submit.js.map +1 -1
  47. package/dist/lib/jobs/types.d.ts +1 -0
  48. package/dist/lib/jobs/types.d.ts.map +1 -1
  49. package/dist/lib/lifecycle/default_templates.d.ts +9 -0
  50. package/dist/lib/lifecycle/default_templates.d.ts.map +1 -0
  51. package/dist/lib/lifecycle/default_templates.js +160 -0
  52. package/dist/lib/lifecycle/default_templates.js.map +1 -0
  53. package/dist/lib/lifecycle/dispatch.d.ts +37 -0
  54. package/dist/lib/lifecycle/dispatch.d.ts.map +1 -0
  55. package/dist/lib/lifecycle/dispatch.js +32 -0
  56. package/dist/lib/lifecycle/dispatch.js.map +1 -0
  57. package/dist/lib/lifecycle/events.d.ts +34 -0
  58. package/dist/lib/lifecycle/events.d.ts.map +1 -0
  59. package/dist/lib/lifecycle/events.js +118 -0
  60. package/dist/lib/lifecycle/events.js.map +1 -0
  61. package/dist/lib/lifecycle/handler.d.ts +29 -0
  62. package/dist/lib/lifecycle/handler.d.ts.map +1 -0
  63. package/dist/lib/lifecycle/handler.js +145 -0
  64. package/dist/lib/lifecycle/handler.js.map +1 -0
  65. package/dist/lib/lifecycle/index.d.ts +23 -0
  66. package/dist/lib/lifecycle/index.d.ts.map +1 -0
  67. package/dist/lib/lifecycle/index.js +21 -0
  68. package/dist/lib/lifecycle/index.js.map +1 -0
  69. package/dist/lib/lifecycle/register.d.ts +29 -0
  70. package/dist/lib/lifecycle/register.d.ts.map +1 -0
  71. package/dist/lib/lifecycle/register.js +29 -0
  72. package/dist/lib/lifecycle/register.js.map +1 -0
  73. package/dist/lib/lifecycle/resolver.d.ts +22 -0
  74. package/dist/lib/lifecycle/resolver.d.ts.map +1 -0
  75. package/dist/lib/lifecycle/resolver.js +105 -0
  76. package/dist/lib/lifecycle/resolver.js.map +1 -0
  77. package/dist/lib/lifecycle/scheduler.d.ts +27 -0
  78. package/dist/lib/lifecycle/scheduler.d.ts.map +1 -0
  79. package/dist/lib/lifecycle/scheduler.js +108 -0
  80. package/dist/lib/lifecycle/scheduler.js.map +1 -0
  81. package/dist/lib/lifecycle/status.d.ts +39 -0
  82. package/dist/lib/lifecycle/status.d.ts.map +1 -0
  83. package/dist/lib/lifecycle/status.js +67 -0
  84. package/dist/lib/lifecycle/status.js.map +1 -0
  85. package/dist/lib/lifecycle/types.d.ts +165 -0
  86. package/dist/lib/lifecycle/types.d.ts.map +1 -0
  87. package/dist/lib/lifecycle/types.js +11 -0
  88. package/dist/lib/lifecycle/types.js.map +1 -0
  89. package/dist/lib/preferences/index.d.ts +3 -0
  90. package/dist/lib/preferences/index.d.ts.map +1 -0
  91. package/dist/lib/preferences/index.js +3 -0
  92. package/dist/lib/preferences/index.js.map +1 -0
  93. package/dist/lib/preferences/storage.d.ts +33 -0
  94. package/dist/lib/preferences/storage.d.ts.map +1 -0
  95. package/dist/lib/preferences/storage.js +125 -0
  96. package/dist/lib/preferences/storage.js.map +1 -0
  97. package/dist/lib/preferences/types.d.ts +59 -0
  98. package/dist/lib/preferences/types.d.ts.map +1 -0
  99. package/dist/lib/preferences/types.js +20 -0
  100. package/dist/lib/preferences/types.js.map +1 -0
  101. package/dist/lib/template_manager/db/template_repository.d.ts +4 -1
  102. package/dist/lib/template_manager/db/template_repository.d.ts.map +1 -1
  103. package/dist/lib/template_manager/db/template_repository.js +17 -1
  104. package/dist/lib/template_manager/db/template_repository.js.map +1 -1
  105. package/dist/lib/template_manager/types.d.ts +1 -0
  106. package/dist/lib/template_manager/types.d.ts.map +1 -1
  107. package/migrations/008_lifecycle_status.pg.sql +27 -0
  108. package/migrations/008_lifecycle_status.sqlite.sql +24 -0
  109. package/migrations/009_templates_locale.pg.sql +6 -0
  110. package/migrations/009_templates_locale.sqlite.sql +4 -0
  111. package/migrations/010_preferences.pg.sql +26 -0
  112. package/migrations/010_preferences.sqlite.sql +32 -0
  113. package/package.json +17 -5
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Default system template manifests for the lifecycle module.
3
+ *
4
+ * Consumers call `sync_system_templates(hazo_connect, lifecycleDefaultTemplates)`
5
+ * at boot to upsert these templates into the database.
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // Shared HTML/text body templates
9
+ // ---------------------------------------------------------------------------
10
+ const MAIN_HTML = `<!doctype html>
11
+ <html lang="en">
12
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>{{headline}}</title></head>
13
+ <body style="margin:0;background:#f9f9f9;font-family:Arial,sans-serif;color:#1a1a1a;">
14
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f9f9f9;padding:32px 16px;">
15
+ <tr><td align="center">
16
+ <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="background:#ffffff;border:1px solid #e5e5e5;border-radius:12px;padding:32px;max-width:520px;">
17
+ <tr><td>
18
+ <div style="font-size:20px;font-weight:600;color:#333;margin-bottom:16px;">{{app_name}}</div>
19
+ <h1 style="font-size:22px;line-height:1.25;color:#1a1a1a;font-weight:600;margin:0 0 16px;">{{headline}}</h1>
20
+ <p style="font-size:16px;line-height:1.6;color:#1a1a1a;margin:0 0 24px;">{{body}}</p>
21
+ <p style="margin:0 0 24px;"><a href="{{{cta_url}}}" style="display:inline-block;background:#3730a3;color:#ffffff;text-decoration:none;font-weight:600;font-size:15px;padding:12px 22px;border-radius:8px;">{{cta_label}}</a></p>
22
+ <p style="font-size:14px;line-height:1.6;color:#1a1a1a;margin:0;">{{signoff}}</p>
23
+ </td></tr>
24
+ </table>
25
+ <p style="font-size:12px;line-height:1.5;color:#888;max-width:520px;margin:18px auto 0;text-align:center;">{{footer}}</p>
26
+ <p style="font-size:12px;line-height:1.5;color:#888;max-width:520px;margin:8px auto 0;text-align:center;">{{{stop_tips_url_html}}}</p>
27
+ </td></tr>
28
+ </table>
29
+ </body>
30
+ </html>`;
31
+ const MAIN_TEXT = `{{headline}}
32
+
33
+ {{body}}
34
+
35
+ {{cta_label}}: {{{cta_url}}}
36
+
37
+ {{signoff}}
38
+
39
+ {{footer}}
40
+
41
+ ---
42
+ Don't want these tips? Unsubscribe: {{stop_tips_url}}`;
43
+ // ---------------------------------------------------------------------------
44
+ // Shared variable lists
45
+ // ---------------------------------------------------------------------------
46
+ const MAIN_VARIABLES = [
47
+ { variable_name: 'first_name', variable_description: "Recipient's first name" },
48
+ { variable_name: 'app_name', variable_description: 'Application name' },
49
+ { variable_name: 'headline', variable_description: 'Email headline' },
50
+ { variable_name: 'body', variable_description: 'Email body paragraph' },
51
+ { variable_name: 'cta_url', variable_description: 'Call-to-action URL' },
52
+ { variable_name: 'cta_label', variable_description: 'Call-to-action button label' },
53
+ { variable_name: 'signoff', variable_description: 'Closing salutation' },
54
+ { variable_name: 'footer', variable_description: 'Footer legal/contact text' },
55
+ { variable_name: 'stop_tips_url', variable_description: 'Signed URL for opt-out' },
56
+ { variable_name: 'stop_tips_url_html', variable_description: 'Pre-rendered HTML opt-out link fragment' },
57
+ ];
58
+ // ---------------------------------------------------------------------------
59
+ // Template manifests
60
+ // ---------------------------------------------------------------------------
61
+ export const lifecycleDefaultTemplates = [
62
+ // 1. Welcome
63
+ {
64
+ template_name: 'lifecycle-welcome',
65
+ template_label: 'Welcome',
66
+ category: 'Lifecycle',
67
+ bodies: {
68
+ html: MAIN_HTML,
69
+ text: MAIN_TEXT,
70
+ },
71
+ variables: [
72
+ ...MAIN_VARIABLES,
73
+ { variable_name: 'subject_a', variable_description: 'A/B subject line variant A' },
74
+ { variable_name: 'subject_b', variable_description: 'A/B subject line variant B' },
75
+ ],
76
+ },
77
+ // 2. Tree waiting
78
+ {
79
+ template_name: 'lifecycle-tree-waiting',
80
+ template_label: 'Your space is waiting',
81
+ category: 'Lifecycle',
82
+ bodies: {
83
+ html: MAIN_HTML,
84
+ text: MAIN_TEXT,
85
+ },
86
+ variables: MAIN_VARIABLES,
87
+ },
88
+ // 3. Bring existing tree
89
+ {
90
+ template_name: 'lifecycle-bring-existing-tree',
91
+ template_label: 'Already have a tree elsewhere?',
92
+ category: 'Lifecycle',
93
+ bodies: {
94
+ html: MAIN_HTML,
95
+ text: MAIN_TEXT,
96
+ },
97
+ variables: MAIN_VARIABLES,
98
+ },
99
+ // 4. Invite family
100
+ {
101
+ template_name: 'lifecycle-invite-family',
102
+ template_label: "Don't keep it to yourself",
103
+ category: 'Lifecycle',
104
+ bodies: {
105
+ html: MAIN_HTML,
106
+ text: MAIN_TEXT,
107
+ },
108
+ variables: MAIN_VARIABLES,
109
+ },
110
+ // 5. Milestone — map unlocked
111
+ {
112
+ template_name: 'lifecycle-milestone-map-unlocked',
113
+ template_label: 'Your family is on the map',
114
+ category: 'Lifecycle',
115
+ bodies: {
116
+ html: MAIN_HTML,
117
+ text: MAIN_TEXT,
118
+ },
119
+ variables: MAIN_VARIABLES,
120
+ },
121
+ // 6. Milestone — first photo
122
+ {
123
+ template_name: 'lifecycle-milestone-first-photo',
124
+ template_label: 'First photo is up',
125
+ category: 'Lifecycle',
126
+ bodies: {
127
+ html: MAIN_HTML,
128
+ text: MAIN_TEXT,
129
+ },
130
+ variables: MAIN_VARIABLES,
131
+ },
132
+ // 7. Milestone — first invite accepted
133
+ {
134
+ template_name: 'lifecycle-milestone-first-invite-accepted',
135
+ template_label: "You've got family with you",
136
+ category: 'Lifecycle',
137
+ bodies: {
138
+ html: MAIN_HTML,
139
+ text: MAIN_TEXT,
140
+ },
141
+ variables: [
142
+ ...MAIN_VARIABLES,
143
+ { variable_name: 'invitee_name', variable_description: 'Name of the invited user who joined' },
144
+ ],
145
+ },
146
+ // 8. Footer fragment — stop tips
147
+ {
148
+ template_name: 'lifecycle-footer-stop-tips',
149
+ template_label: 'Stop lifecycle tips (footer fragment)',
150
+ category: 'Lifecycle',
151
+ bodies: {
152
+ html: `<p style="font-size:12px;color:#888;">Don't want these tips? <a href="{{{stop_tips_url}}}">Stop</a>.</p>`,
153
+ text: `Don't want these tips? Stop: {{stop_tips_url}}`,
154
+ },
155
+ variables: [
156
+ { variable_name: 'stop_tips_url', variable_description: 'Signed URL for opt-out' },
157
+ ],
158
+ },
159
+ ];
160
+ //# sourceMappingURL=default_templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default_templates.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/default_templates.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Dispatch wrapper for lifecycle email steps.
3
+ *
4
+ * Wraps `send_template_email` with lifecycle-specific context. In v1,
5
+ * locale resolution happens at the template-lookup level via
6
+ * `get_template_by_name` (which accepts an optional `locale` param).
7
+ * `send_template_email` does not currently expose a `locale` parameter, so
8
+ * the `locale` option here is noted for future use — template resolution will
9
+ * fall back to the default locale until that param is threaded through.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import type { HazoConnectInstance } from '../template_manager/types.js';
14
+ export interface DispatchStepOptions {
15
+ hazo_connect: HazoConnectInstance;
16
+ user_id: string;
17
+ template_name: string;
18
+ variables: Record<string, string>;
19
+ to: string;
20
+ scope_id?: string | null;
21
+ /**
22
+ * Locale hint. v1: not yet forwarded to send_template_email (which does not
23
+ * accept locale). Template resolution defaults to the global locale.
24
+ * TODO: thread through once send_template_email exposes a locale param.
25
+ */
26
+ locale?: string | null;
27
+ /** Override the template's stored subject line (used for A/B testing). */
28
+ subject_override?: string | null;
29
+ }
30
+ /**
31
+ * Dispatch a lifecycle email step.
32
+ *
33
+ * Throws if `send_template_email` returns a non-success response so callers
34
+ * can catch and mark the status row as failed.
35
+ */
36
+ export declare function dispatchStep(opts: DispatchStepOptions): Promise<void>;
37
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,mBAAmB,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB3E"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Dispatch wrapper for lifecycle email steps.
3
+ *
4
+ * Wraps `send_template_email` with lifecycle-specific context. In v1,
5
+ * locale resolution happens at the template-lookup level via
6
+ * `get_template_by_name` (which accepts an optional `locale` param).
7
+ * `send_template_email` does not currently expose a `locale` parameter, so
8
+ * the `locale` option here is noted for future use — template resolution will
9
+ * fall back to the default locale until that param is threaded through.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import { send_template_email } from '../template_manager/template_manager.js';
14
+ /**
15
+ * Dispatch a lifecycle email step.
16
+ *
17
+ * Throws if `send_template_email` returns a non-success response so callers
18
+ * can catch and mark the status row as failed.
19
+ */
20
+ export async function dispatchStep(opts) {
21
+ const result = await send_template_email({
22
+ template_name: opts.template_name,
23
+ variables: opts.variables,
24
+ to: opts.to,
25
+ scope_id: opts.scope_id ?? null,
26
+ ...(opts.subject_override ? { subject: opts.subject_override } : {}),
27
+ }, opts.hazo_connect);
28
+ if (!result.success) {
29
+ throw new Error(result.error ?? `send_template_email failed for template '${opts.template_name}'`);
30
+ }
31
+ }
32
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAoB9E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAyB;IAC1D,MAAM,MAAM,GAAG,MAAM,mBAAmB,CACtC;QACE,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;QAC/B,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrE,EACD,IAAI,CAAC,YAAY,CAClB,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,MAAM,CAAC,KAAK,IAAI,4CAA4C,IAAI,CAAC,aAAa,GAAG,CAClF,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `recordEvent` implementation for lifecycle sequences.
3
+ *
4
+ * When a product event fires (e.g. "user_added_first_person"), this module
5
+ * scans all sequences for matching event-triggered steps, loads their status
6
+ * rows, and dispatches any that are still in `scheduled` state.
7
+ *
8
+ * Deduplication contract:
9
+ * - If the status row is 'sent' → no-op (already dispatched).
10
+ * - If the status row is not found → no-op (user never started the sequence).
11
+ * - If the status row is 'skipped' or 'failed' → no-op (terminal state).
12
+ * - Only 'scheduled' rows trigger dispatch.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ import type { LifecycleSchedulerOptions } from './types.js';
17
+ /**
18
+ * Fire all event-triggered steps whose `event_kind` matches the emitted event.
19
+ *
20
+ * Scans every sequence in `opts.sequences` (or only `sequence_id` if supplied)
21
+ * for steps with `trigger.kind === 'event' && trigger.event_kind === event_kind`.
22
+ * For each match, loads the status row and dispatches if status is `scheduled`.
23
+ *
24
+ * @param opts - Scheduler options (hazo_connect, resolvers, sequences, etc.)
25
+ * @param get_user_email - Async function that resolves the email address for user_id.
26
+ * @param params - Event parameters from the caller.
27
+ */
28
+ export declare function recordEvent(opts: LifecycleSchedulerOptions, get_user_email: (user_id: string) => Promise<string>, params: {
29
+ user_id: string;
30
+ event_kind: string;
31
+ sequence_id?: string;
32
+ payload?: Record<string, unknown>;
33
+ }): Promise<void>;
34
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,yBAAyB,EAC/B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EACpD,MAAM,EAAE;IACN,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,GACA,OAAO,CAAC,IAAI,CAAC,CAgHf"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `recordEvent` implementation for lifecycle sequences.
3
+ *
4
+ * When a product event fires (e.g. "user_added_first_person"), this module
5
+ * scans all sequences for matching event-triggered steps, loads their status
6
+ * rows, and dispatches any that are still in `scheduled` state.
7
+ *
8
+ * Deduplication contract:
9
+ * - If the status row is 'sent' → no-op (already dispatched).
10
+ * - If the status row is not found → no-op (user never started the sequence).
11
+ * - If the status row is 'skipped' or 'failed' → no-op (terminal state).
12
+ * - Only 'scheduled' rows trigger dispatch.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ import { createHash } from 'node:crypto';
17
+ import { evaluateConditions } from './resolver.js';
18
+ import { dispatchStep } from './dispatch.js';
19
+ import { findEventStepRow, updateStatusRow } from './status.js';
20
+ /**
21
+ * Fire all event-triggered steps whose `event_kind` matches the emitted event.
22
+ *
23
+ * Scans every sequence in `opts.sequences` (or only `sequence_id` if supplied)
24
+ * for steps with `trigger.kind === 'event' && trigger.event_kind === event_kind`.
25
+ * For each match, loads the status row and dispatches if status is `scheduled`.
26
+ *
27
+ * @param opts - Scheduler options (hazo_connect, resolvers, sequences, etc.)
28
+ * @param get_user_email - Async function that resolves the email address for user_id.
29
+ * @param params - Event parameters from the caller.
30
+ */
31
+ export async function recordEvent(opts, get_user_email, params) {
32
+ const { user_id, event_kind } = params;
33
+ // Determine which sequence IDs to scan.
34
+ const sequence_ids = params.sequence_id
35
+ ? [params.sequence_id]
36
+ : Object.keys(opts.sequences);
37
+ for (const seq_id of sequence_ids) {
38
+ const steps = opts.sequences[seq_id];
39
+ if (!steps)
40
+ continue;
41
+ for (const step of steps) {
42
+ // Only process event-triggered steps matching the event_kind.
43
+ if (step.trigger.kind !== 'event' ||
44
+ step.trigger.event_kind !== event_kind) {
45
+ continue;
46
+ }
47
+ // Load the status row for this (app_id, user_id, sequence_id, step_id).
48
+ const row = await findEventStepRow(opts.hazo_connect, opts.app_id, user_id, seq_id, step.id);
49
+ if (!row) {
50
+ // User never started this sequence — no-op.
51
+ continue;
52
+ }
53
+ if (row.status === 'sent') {
54
+ // Already dispatched — dedupe.
55
+ continue;
56
+ }
57
+ if (row.status !== 'scheduled') {
58
+ // Skipped or failed — don't re-trigger.
59
+ continue;
60
+ }
61
+ // Opt-out check.
62
+ if (opts.is_opted_out) {
63
+ const opted_out = await opts.is_opted_out(user_id);
64
+ if (opted_out) {
65
+ await updateStatusRow(opts.hazo_connect, row.id, {
66
+ status: 'skipped',
67
+ skip_reason: 'opted_out',
68
+ attempt_count: row.attempt_count + 1,
69
+ });
70
+ continue;
71
+ }
72
+ }
73
+ // Evaluate conditions.
74
+ const condition_result = await evaluateConditions(step.conditions, opts.resolvers, user_id);
75
+ if (!condition_result.pass) {
76
+ await updateStatusRow(opts.hazo_connect, row.id, {
77
+ status: 'skipped',
78
+ skip_reason: `condition_failed:${condition_result.reason ?? 'unknown'}`,
79
+ attempt_count: row.attempt_count + 1,
80
+ });
81
+ continue;
82
+ }
83
+ // Resolve recipient, extra variables, and dispatch.
84
+ const to = await get_user_email(user_id);
85
+ const raw_vars = opts.get_variables
86
+ ? await opts.get_variables(user_id, step.id)
87
+ : {};
88
+ let subject_override;
89
+ let ab_variant;
90
+ const { subject_a, subject_b, ...extra_vars } = raw_vars;
91
+ if (subject_a && subject_b) {
92
+ const hash_byte = createHash('sha256')
93
+ .update(user_id + step.template)
94
+ .digest()[0];
95
+ ab_variant = (hash_byte & 1) === 0 ? 'a' : 'b';
96
+ subject_override = ab_variant === 'a' ? subject_a : subject_b;
97
+ }
98
+ await dispatchStep({
99
+ hazo_connect: opts.hazo_connect,
100
+ user_id,
101
+ template_name: step.template,
102
+ variables: extra_vars,
103
+ to,
104
+ locale: opts.default_locale ?? null,
105
+ subject_override,
106
+ });
107
+ await updateStatusRow(opts.hazo_connect, row.id, {
108
+ status: 'sent',
109
+ sent_at: new Date().toISOString(),
110
+ attempt_count: row.attempt_count + 1,
111
+ ...(ab_variant
112
+ ? { dispatch_meta: { ab_variant, subject: subject_override } }
113
+ : {}),
114
+ });
115
+ }
116
+ }
117
+ }
118
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * hazo_jobs handler for `hazo_notify:lifecycle_step` jobs.
3
+ *
4
+ * Each time-triggered lifecycle step is deferred via hazo_jobs. When the job
5
+ * fires, this handler re-evaluates conditions and either dispatches the email
6
+ * or marks the row as skipped/failed.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import type { HazoJobHandler, LifecycleSchedulerOptions, LifecycleStepPayload } from './types.js';
11
+ /**
12
+ * Factory that produces the hazo_jobs handler for `lifecycle_step` jobs.
13
+ *
14
+ * Handler flow:
15
+ * 1. Load status row — guard on status !== 'scheduled' (idempotency).
16
+ * 2. Increment attempt_count.
17
+ * 3. Check opt-out via opts.is_opted_out (if provided).
18
+ * 4. Evaluate ConditionNode graph via evaluateConditions.
19
+ * 5. If pass: dispatch via dispatchStep, mark 'sent'.
20
+ * 6. If fail: mark 'skipped' with reason.
21
+ * 7. On exception: if attempts exhausted, mark 'failed'; else leave 'scheduled'
22
+ * so hazo_jobs retries.
23
+ *
24
+ * @param opts - Same options passed to `createLifecycleScheduler`.
25
+ * @param get_user_email - Async function that resolves an email address for a
26
+ * given user_id. Required to know where to send the step email.
27
+ */
28
+ export declare function createLifecycleStepHandler(opts: LifecycleSchedulerOptions, get_user_email: (user_id: string) => Promise<string>): HazoJobHandler<LifecycleStepPayload>;
29
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAGV,cAAc,EACd,yBAAyB,EACzB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,yBAAyB,EAC/B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACnD,cAAc,CAAC,oBAAoB,CAAC,CA2ItC"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * hazo_jobs handler for `hazo_notify:lifecycle_step` jobs.
3
+ *
4
+ * Each time-triggered lifecycle step is deferred via hazo_jobs. When the job
5
+ * fires, this handler re-evaluates conditions and either dispatches the email
6
+ * or marks the row as skipped/failed.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { evaluateConditions } from './resolver.js';
12
+ import { dispatchStep } from './dispatch.js';
13
+ import { loadStatusRow, updateStatusRow } from './status.js';
14
+ /**
15
+ * Factory that produces the hazo_jobs handler for `lifecycle_step` jobs.
16
+ *
17
+ * Handler flow:
18
+ * 1. Load status row — guard on status !== 'scheduled' (idempotency).
19
+ * 2. Increment attempt_count.
20
+ * 3. Check opt-out via opts.is_opted_out (if provided).
21
+ * 4. Evaluate ConditionNode graph via evaluateConditions.
22
+ * 5. If pass: dispatch via dispatchStep, mark 'sent'.
23
+ * 6. If fail: mark 'skipped' with reason.
24
+ * 7. On exception: if attempts exhausted, mark 'failed'; else leave 'scheduled'
25
+ * so hazo_jobs retries.
26
+ *
27
+ * @param opts - Same options passed to `createLifecycleScheduler`.
28
+ * @param get_user_email - Async function that resolves an email address for a
29
+ * given user_id. Required to know where to send the step email.
30
+ */
31
+ export function createLifecycleStepHandler(opts, get_user_email) {
32
+ const max_attempts = opts.max_attempts ?? 3;
33
+ return async function lifecycle_step_handler(job, log) {
34
+ // hazo_jobs (SQLite backend) may deliver payload as a JSON string.
35
+ const payload = typeof job.payload === 'string'
36
+ ? JSON.parse(job.payload)
37
+ : job.payload;
38
+ const { status_row_id, user_id, sequence_id, step_id } = payload;
39
+ log?.info?.(`[lifecycle] step start row=${status_row_id} user=${user_id} seq=${sequence_id} step=${step_id}`);
40
+ // 1. Load the status row.
41
+ const row = await loadStatusRow(opts.hazo_connect, status_row_id);
42
+ // 2. Guard: already processed or gone.
43
+ if (!row || row.status !== 'scheduled') {
44
+ log?.info?.(`[lifecycle] step skip (already handled) row=${status_row_id} status=${row?.status ?? 'not_found'}`);
45
+ return;
46
+ }
47
+ // 3. Increment attempt counter.
48
+ const attempt_count = row.attempt_count + 1;
49
+ try {
50
+ // 4. Opt-out check.
51
+ if (opts.is_opted_out) {
52
+ const opted_out = await opts.is_opted_out(user_id);
53
+ if (opted_out) {
54
+ await updateStatusRow(opts.hazo_connect, status_row_id, {
55
+ status: 'skipped',
56
+ skip_reason: 'opted_out',
57
+ attempt_count,
58
+ });
59
+ log?.info?.(`[lifecycle] step skipped (opted_out) row=${status_row_id}`);
60
+ return;
61
+ }
62
+ }
63
+ // 5. Locate the step definition.
64
+ const sequence = opts.sequences[sequence_id];
65
+ if (!sequence) {
66
+ throw new Error(`sequence_not_found:${sequence_id}`);
67
+ }
68
+ const step = sequence.find((s) => s.id === step_id);
69
+ if (!step) {
70
+ throw new Error(`step_not_found:${sequence_id}/${step_id}`);
71
+ }
72
+ // 6. Evaluate conditions.
73
+ const condition_result = await evaluateConditions(step.conditions, opts.resolvers, user_id);
74
+ if (!condition_result.pass) {
75
+ const skip_reason = `condition_failed:${condition_result.reason ?? 'unknown'}`;
76
+ await updateStatusRow(opts.hazo_connect, status_row_id, {
77
+ status: 'skipped',
78
+ skip_reason,
79
+ attempt_count,
80
+ });
81
+ log?.info?.(`[lifecycle] step skipped row=${status_row_id} reason=${skip_reason}`);
82
+ return;
83
+ }
84
+ // 7. Resolve recipient email address.
85
+ const to = await get_user_email(user_id);
86
+ // 8. Resolve extra template variables (e.g. opt-out link, A/B subjects).
87
+ const raw_vars = opts.get_variables
88
+ ? await opts.get_variables(user_id, step_id)
89
+ : {};
90
+ // 8a. A/B subject selection: if both subject_a and subject_b are provided,
91
+ // pick one deterministically by hashing user_id + template_name.
92
+ // Remove both from template variables so they don't appear in the body.
93
+ let subject_override;
94
+ let ab_variant;
95
+ const { subject_a, subject_b, ...extra_vars } = raw_vars;
96
+ if (subject_a && subject_b) {
97
+ const hash_byte = createHash('sha256')
98
+ .update(user_id + step.template)
99
+ .digest()[0];
100
+ ab_variant = (hash_byte & 1) === 0 ? 'a' : 'b';
101
+ subject_override = ab_variant === 'a' ? subject_a : subject_b;
102
+ }
103
+ // 9. Dispatch the email.
104
+ await dispatchStep({
105
+ hazo_connect: opts.hazo_connect,
106
+ user_id,
107
+ template_name: step.template,
108
+ variables: extra_vars,
109
+ to,
110
+ locale: opts.default_locale ?? null,
111
+ subject_override,
112
+ });
113
+ // 10. Mark as sent (include dispatch_meta for A/B tracking).
114
+ await updateStatusRow(opts.hazo_connect, status_row_id, {
115
+ status: 'sent',
116
+ sent_at: new Date().toISOString(),
117
+ attempt_count,
118
+ ...(ab_variant
119
+ ? { dispatch_meta: { ab_variant, subject: subject_override } }
120
+ : {}),
121
+ });
122
+ log?.info?.(`[lifecycle] step sent row=${status_row_id} template=${step.template}`);
123
+ }
124
+ catch (err) {
125
+ const msg = err instanceof Error ? err.message : String(err);
126
+ log?.error?.(`[lifecycle] step error row=${status_row_id} err=${msg}`);
127
+ if (attempt_count >= max_attempts) {
128
+ await updateStatusRow(opts.hazo_connect, status_row_id, {
129
+ status: 'failed',
130
+ failure_message: msg,
131
+ attempt_count,
132
+ });
133
+ log?.error?.(`[lifecycle] step failed permanently row=${status_row_id}`);
134
+ }
135
+ else {
136
+ // Leave status as 'scheduled' so hazo_jobs retries the job.
137
+ await updateStatusRow(opts.hazo_connect, status_row_id, {
138
+ attempt_count,
139
+ failure_message: msg,
140
+ });
141
+ }
142
+ }
143
+ };
144
+ }
145
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * hazo_notify/lifecycle — public API surface.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```ts
7
+ * import {
8
+ * createLifecycleScheduler,
9
+ * registerLifecycleHandlers,
10
+ * } from 'hazo_notify/lifecycle';
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ export { createLifecycleScheduler, LIFECYCLE_STEP_JOB_TYPE } from './scheduler.js';
16
+ export { registerLifecycleHandlers } from './register.js';
17
+ export { evaluateConditions } from './resolver.js';
18
+ export { insertStatusRow, loadStatusRow, updateStatusRow, queryStatusRows, findEventStepRow, } from './status.js';
19
+ export { dispatchStep } from './dispatch.js';
20
+ export type { LifecycleTrigger, Condition, ConditionNode, LifecycleStep, LifecycleStatusRow, LifecycleSchedulerOptions, LifecycleScheduler, LifecycleStepPayload, HazoJobsSubmitter, } from './types.js';
21
+ export type { HazoConnectInstance, HazoJob, HazoJobLogger, HazoJobHandler, } from './types.js';
22
+ export { lifecycleDefaultTemplates } from './default_templates.js';
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,eAAe,EACf,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,mBAAmB,EACnB,OAAO,EACP,aAAa,EACb,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * hazo_notify/lifecycle — public API surface.
3
+ *
4
+ * Usage:
5
+ *
6
+ * ```ts
7
+ * import {
8
+ * createLifecycleScheduler,
9
+ * registerLifecycleHandlers,
10
+ * } from 'hazo_notify/lifecycle';
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ export { createLifecycleScheduler, LIFECYCLE_STEP_JOB_TYPE } from './scheduler.js';
16
+ export { registerLifecycleHandlers } from './register.js';
17
+ export { evaluateConditions } from './resolver.js';
18
+ export { insertStatusRow, loadStatusRow, updateStatusRow, queryStatusRows, findEventStepRow, } from './status.js';
19
+ export { dispatchStep } from './dispatch.js';
20
+ export { lifecycleDefaultTemplates } from './default_templates.js';
21
+ //# sourceMappingURL=index.js.map