@shware/workflow 0.4.0 → 2.0.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/README.md +90 -49
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +1 -1
- package/dist/cloudflare/bindings.d.mts +1 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/channels.cjs +13 -18
- package/dist/cloudflare/channels.cjs.map +1 -1
- package/dist/cloudflare/channels.d.cts +3 -3
- package/dist/cloudflare/channels.d.cts.map +1 -1
- package/dist/cloudflare/channels.d.mts +3 -3
- package/dist/cloudflare/channels.d.mts.map +1 -1
- package/dist/cloudflare/channels.mjs +14 -19
- package/dist/cloudflare/channels.mjs.map +1 -1
- package/dist/cloudflare/index.d.cts +4 -4
- package/dist/cloudflare/index.d.mts +4 -4
- package/dist/cloudflare/router.d.cts +1 -1
- package/dist/cloudflare/router.d.mts +1 -1
- package/dist/cloudflare/runner.d.cts +2 -2
- package/dist/cloudflare/runner.d.mts +2 -2
- package/dist/cloudflare/senders.cjs +15 -8
- package/dist/cloudflare/senders.cjs.map +1 -1
- package/dist/cloudflare/senders.d.cts +15 -11
- package/dist/cloudflare/senders.d.cts.map +1 -1
- package/dist/cloudflare/senders.d.mts +15 -11
- package/dist/cloudflare/senders.d.mts.map +1 -1
- package/dist/cloudflare/senders.mjs +15 -8
- package/dist/cloudflare/senders.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +7 -4
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +8 -5
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +66 -42
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +26 -45
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +26 -45
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +67 -42
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +1 -1
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +1 -1
- package/dist/dsl/action.d.mts +1 -1
- package/dist/dsl/action.mjs +1 -1
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +3 -2
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +3 -2
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts +3 -2
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +3 -2
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +1 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +1 -1
- package/dist/dsl/condition.d.mts +1 -1
- package/dist/dsl/condition.mjs +1 -1
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +45 -17
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +30 -17
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +30 -17
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +45 -18
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/index.cjs +0 -1
- package/dist/dsl/index.d.cts +2 -2
- package/dist/dsl/index.d.mts +2 -2
- package/dist/dsl/index.mjs +2 -2
- package/dist/dsl/template.cjs +2 -47
- package/dist/dsl/template.cjs.map +1 -1
- package/dist/dsl/template.d.cts +4 -54
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +4 -54
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.mjs +3 -47
- package/dist/dsl/template.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +1 -1
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts +1 -1
- package/dist/dsl/workflow.d.mts +1 -1
- package/dist/dsl/workflow.mjs +1 -1
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/email-context.cjs +29 -0
- package/dist/email-context.cjs.map +1 -0
- package/dist/email-context.d.cts +11 -0
- package/dist/email-context.d.cts.map +1 -0
- package/dist/email-context.d.mts +11 -0
- package/dist/email-context.d.mts.map +1 -0
- package/dist/email-context.mjs +27 -0
- package/dist/email-context.mjs.map +1 -0
- package/dist/email.cjs +72 -0
- package/dist/email.cjs.map +1 -0
- package/dist/email.d.cts +63 -0
- package/dist/email.d.cts.map +1 -0
- package/dist/email.d.mts +63 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +65 -0
- package/dist/email.mjs.map +1 -0
- package/dist/engine/index.d.cts +2 -2
- package/dist/engine/index.d.mts +2 -2
- package/dist/engine/ports.d.cts +7 -2
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +7 -2
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/index.cjs +13 -3
- package/dist/index.d.cts +5 -4
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +5 -4
- package/dist/push.cjs +3 -5
- package/dist/push.cjs.map +1 -1
- package/dist/push.d.cts +2 -2
- package/dist/push.d.cts.map +1 -1
- package/dist/push.d.mts +2 -2
- package/dist/push.d.mts.map +1 -1
- package/dist/push.mjs +4 -6
- package/dist/push.mjs.map +1 -1
- package/dist/react-email.cjs +16 -8
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +2 -1
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +2 -1
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs +17 -9
- package/dist/react-email.mjs.map +1 -1
- package/dist/reporting/classify.cjs.map +1 -1
- package/dist/reporting/classify.d.cts +1 -1
- package/dist/reporting/classify.d.mts +1 -1
- package/dist/reporting/classify.mjs.map +1 -1
- package/dist/reporting/observer.cjs +5 -4
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.d.cts +3 -2
- package/dist/reporting/observer.d.cts.map +1 -1
- package/dist/reporting/observer.d.mts +3 -2
- package/dist/reporting/observer.d.mts.map +1 -1
- package/dist/reporting/observer.mjs +5 -4
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +3 -12
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +18 -7
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +18 -7
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +3 -12
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/templates.cjs +90 -30
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.d.cts +70 -13
- package/dist/templates.d.cts.map +1 -1
- package/dist/templates.d.mts +70 -13
- package/dist/templates.d.mts.map +1 -1
- package/dist/templates.mjs +87 -31
- package/dist/templates.mjs.map +1 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@ data in D1 or in your own Postgres.
|
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
11
|
const welcome = workflow({ id: 'welcome', trigger: trigger.event(e.sign_up) })
|
|
12
|
-
.email(
|
|
12
|
+
.email(email.getting_started, { name: u.name })
|
|
13
13
|
.delay('3 days')
|
|
14
14
|
.waitUntil(performed(e.activated), {
|
|
15
15
|
timeout: '7 days',
|
|
16
|
-
onTimeout: (w) => w.email(
|
|
16
|
+
onTimeout: (w) => w.email(email.nudge, { name: u.name }),
|
|
17
17
|
});
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -68,9 +68,11 @@ the contract is defined, because input and output must mean the same JSON.
|
|
|
68
68
|
|
|
69
69
|
### 2. Templates
|
|
70
70
|
|
|
71
|
-
One object per template
|
|
72
|
-
the
|
|
73
|
-
|
|
71
|
+
One object per template, identified by its own `id` (unique per channel): the
|
|
72
|
+
IR, the manifest, every send and the studio name it by that id, so keep it
|
|
73
|
+
stable and rename `title` instead. Its strict Zod `props` schema is the only
|
|
74
|
+
source of the props type: the DSL's arguments, the `preview` and the component
|
|
75
|
+
are all derived from it, and every send parses the props against it first.
|
|
74
76
|
|
|
75
77
|
```tsx
|
|
76
78
|
// src/email/welcome.tsx
|
|
@@ -79,6 +81,8 @@ import { emailTemplate } from '@shware/workflow';
|
|
|
79
81
|
import { strictObject, string } from 'zod/mini';
|
|
80
82
|
|
|
81
83
|
export default emailTemplate({
|
|
84
|
+
id: 'welcome',
|
|
85
|
+
from: { email: 'noreply@acme.test', name: 'Acme' },
|
|
82
86
|
title: 'Welcome',
|
|
83
87
|
props: strictObject({ name: string() }),
|
|
84
88
|
subject: 'Welcome aboard',
|
|
@@ -91,30 +95,37 @@ export default emailTemplate({
|
|
|
91
95
|
});
|
|
92
96
|
```
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
Gather them per channel; the workflows send the definitions and the config
|
|
99
|
+
registers the whole:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// src/email/index.ts
|
|
103
|
+
import tip from './tip';
|
|
104
|
+
import welcome from './welcome';
|
|
105
|
+
|
|
106
|
+
export const email = { welcome, tip };
|
|
107
|
+
```
|
|
96
108
|
|
|
97
109
|
```ts
|
|
98
110
|
// src/templates.ts
|
|
99
|
-
import {
|
|
100
|
-
import welcome from './email/welcome';
|
|
101
|
-
import tip from './email/tip';
|
|
111
|
+
import { email } from './email';
|
|
102
112
|
|
|
103
|
-
export const
|
|
104
|
-
export const t = templates(registry);
|
|
113
|
+
export const templates = { email };
|
|
105
114
|
```
|
|
106
115
|
|
|
107
|
-
`tip.tsx` is written the same way. The
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
`tip.tsx` is written the same way. The property names are JavaScript names
|
|
117
|
+
only. `.email(email.welcome, props)` checks `props` against that template's
|
|
118
|
+
schema, and a push template is not accepted where an email is. The compile
|
|
119
|
+
refuses an id registered twice in a channel, and a workflow sending a
|
|
120
|
+
definition other than the one registered under its id.
|
|
110
121
|
|
|
111
122
|
### 3. A workflow
|
|
112
123
|
|
|
113
124
|
```ts
|
|
114
125
|
// src/workflows/welcome.ts
|
|
115
126
|
import { not, performed, trigger, workflow } from '@shware/workflow';
|
|
116
|
-
import { t } from '../templates';
|
|
117
127
|
import { e, u } from '../contract';
|
|
128
|
+
import { email } from '../email';
|
|
118
129
|
|
|
119
130
|
export const welcomeSeries = workflow({
|
|
120
131
|
id: 'welcome_series',
|
|
@@ -122,9 +133,9 @@ export const welcomeSeries = workflow({
|
|
|
122
133
|
trigger: trigger.event(e.login),
|
|
123
134
|
goal: { condition: performed(e.purchase), within: '7 days' },
|
|
124
135
|
})
|
|
125
|
-
.email(
|
|
136
|
+
.email(email.welcome, { name: u.name })
|
|
126
137
|
.delay('2 days')
|
|
127
|
-
.branch([not(performed(e.purchase)), (w) => w.email(
|
|
138
|
+
.branch([not(performed(e.purchase)), (w) => w.email(email.tip, { name: u.name })]);
|
|
128
139
|
```
|
|
129
140
|
|
|
130
141
|
`id` is wire identity (instances, the entry ledger and stats are keyed on it),
|
|
@@ -136,15 +147,15 @@ so rename `name` instead.
|
|
|
136
147
|
// workflow.config.ts
|
|
137
148
|
import { defineConfig } from '@shware/workflow';
|
|
138
149
|
import { contract } from './src/contract';
|
|
139
|
-
import {
|
|
150
|
+
import { templates } from './src/templates';
|
|
140
151
|
import { welcomeSeries } from './src/workflows/welcome';
|
|
141
152
|
|
|
142
153
|
export default defineConfig({
|
|
143
154
|
title: 'Workflow Studio · Acme',
|
|
144
155
|
contract,
|
|
145
156
|
workflows: [welcomeSeries],
|
|
146
|
-
templates
|
|
147
|
-
|
|
157
|
+
templates,
|
|
158
|
+
email: { unsubscribeUrl: 'https://acme.test/unsubscribe' },
|
|
148
159
|
runtime: {
|
|
149
160
|
development: { timeScale: 600, logMessages: true },
|
|
150
161
|
production: { timeScale: 1, logMessages: false },
|
|
@@ -212,8 +223,8 @@ the routing table. Run it after every deploy that changes a journey.
|
|
|
212
223
|
From the service that knows what users do:
|
|
213
224
|
|
|
214
225
|
```ts
|
|
215
|
-
import { JourneyClient } from '@shware/workflow/client';
|
|
216
226
|
import { contract } from '@acme/workflow-contract';
|
|
227
|
+
import { JourneyClient } from '@shware/workflow/client';
|
|
217
228
|
|
|
218
229
|
export const journeys = new JourneyClient({
|
|
219
230
|
contract,
|
|
@@ -296,10 +307,10 @@ trigger.webhook();
|
|
|
296
307
|
|
|
297
308
|
| Step | What it does |
|
|
298
309
|
| --------------------------------------------- | ------------------------------------------------------------------------- |
|
|
299
|
-
| `.email(
|
|
300
|
-
| `.push(
|
|
301
|
-
| `.slack(
|
|
302
|
-
| `.discord(
|
|
310
|
+
| `.email(email.k, props)` | Send an email |
|
|
311
|
+
| `.push(push.k, props, { devices? })` | Push to every device of the user, or only the `'latest'`; skipped if none |
|
|
312
|
+
| `.slack(slack.k, props)` | Post to a Slack incoming webhook |
|
|
313
|
+
| `.discord(discord.k, props)` | Post to a Discord webhook |
|
|
303
314
|
| `.sms()`, `.inApp()`, `.survey()` | Message nodes with no built-in sender (see [Channels](#channels)) |
|
|
304
315
|
| `.prefer([first, second], props, { label? })` | One push and one email: the first the user is reachable on |
|
|
305
316
|
| `.delay('1 day')` | Sleep; `{ min, max }` sleeps a random time in between |
|
|
@@ -323,9 +334,9 @@ Both templates must declare the same props type.
|
|
|
323
334
|
|
|
324
335
|
```ts
|
|
325
336
|
workflow({ id: 'welcome_series', trigger: trigger.event(e.login) })
|
|
326
|
-
.email(
|
|
337
|
+
.email(email.welcome, { name: u.name })
|
|
327
338
|
.delay('1 day')
|
|
328
|
-
.prefer([
|
|
339
|
+
.prefer([push.tip_push, email.tip], { name: u.name }, { label: 'tip by channel' });
|
|
329
340
|
```
|
|
330
341
|
|
|
331
342
|
**timeWindow** takes `between: ['09:00', '17:00']` (24-hour, leading zeros, not
|
|
@@ -345,7 +356,7 @@ workflow({ id: 'notification_optin', trigger: trigger.event(e.login) }).waitUnti
|
|
|
345
356
|
),
|
|
346
357
|
{
|
|
347
358
|
timeout: '1 day',
|
|
348
|
-
onTimeout: (w) => w.email(
|
|
359
|
+
onTimeout: (w) => w.email(email.enable_notifications, { name: u.name }),
|
|
349
360
|
}
|
|
350
361
|
);
|
|
351
362
|
```
|
|
@@ -359,11 +370,11 @@ workflow({ id: 'nps_followup', trigger: trigger.event(e.nps_sent) }).branch(
|
|
|
359
370
|
'by score',
|
|
360
371
|
[
|
|
361
372
|
performed(e.nps_sent, (p) => gte(p.score, 9)),
|
|
362
|
-
(w) => w.delay('1 day').email(
|
|
373
|
+
(w) => w.delay('1 day').email(email.review_ask, { name: u.name }),
|
|
363
374
|
],
|
|
364
375
|
[
|
|
365
376
|
performed(e.nps_sent, (p) => lte(p.score, 6)),
|
|
366
|
-
(w) => w.slack(
|
|
377
|
+
(w) => w.slack(slack.nps_detractor, { name: u.name }).exit('handed to support'),
|
|
367
378
|
]
|
|
368
379
|
);
|
|
369
380
|
```
|
|
@@ -378,8 +389,8 @@ workflow({ id: 'checkout_recovery', trigger: trigger.event(e.begin_checkout) })
|
|
|
378
389
|
.delay('1 day')
|
|
379
390
|
.cohort(
|
|
380
391
|
{
|
|
381
|
-
discount: { weight: 50, flow: (w) => w.email(
|
|
382
|
-
social_proof: { weight: 50, flow: (w) => w.email(
|
|
392
|
+
discount: { weight: 50, flow: (w) => w.email(email.offer_discount, { name: u.name }) },
|
|
393
|
+
social_proof: { weight: 50, flow: (w) => w.email(email.offer_reviews, { name: u.name }) },
|
|
383
394
|
},
|
|
384
395
|
{ key: 'checkout_offer_v1' }
|
|
385
396
|
);
|
|
@@ -450,32 +461,61 @@ the compile fails.
|
|
|
450
461
|
|
|
451
462
|
### Email
|
|
452
463
|
|
|
453
|
-
`emailTemplate({ title, description?, props, subject, preview, component })`
|
|
464
|
+
`emailTemplate({ id, from, replyTo?, unsubscribeUrl?, title, description?, props, subject, preview, component })`
|
|
454
465
|
declares one. The Worker renders it with `@shware/workflow/react-email` (React's
|
|
455
466
|
streaming renderer, which waits for react-email's `<Tailwind>`) and sends it
|
|
456
|
-
through the `EMAIL` send_email binding
|
|
467
|
+
through the `EMAIL` send_email binding, from the template's own `from`:
|
|
457
468
|
|
|
458
469
|
```ts
|
|
459
|
-
|
|
470
|
+
// src/email/welcome.tsx
|
|
471
|
+
export default emailTemplate({
|
|
472
|
+
id: 'welcome',
|
|
460
473
|
from: { email: 'noreply@acme.test', name: 'Acme' },
|
|
461
|
-
replyTo: { email: 'support@acme.test', name: 'Acme Support' },
|
|
462
|
-
|
|
463
|
-
|
|
474
|
+
replyTo: { email: 'support@acme.test', name: 'Acme Support' }, // left out: no Reply-To
|
|
475
|
+
title: 'Welcome',
|
|
476
|
+
props: strictObject({ name: string() }),
|
|
477
|
+
subject: 'Welcome to Acme',
|
|
478
|
+
preview: { name: 'John Doe' },
|
|
479
|
+
component: ({ name }) => <Layout name={name}>…</Layout>,
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
There is no global sender: share addresses through a module of your own. The
|
|
484
|
+
unsubscribe link is the template's `unsubscribeUrl`, else the config's
|
|
485
|
+
`email.unsubscribeUrl`, else none (`resolveEmail` is the rule). A layout reads
|
|
486
|
+
it from the render, never from a constant, so a template's own URL reaches
|
|
487
|
+
its HTML:
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
import { useEmailContext } from '@shware/workflow/email-context';
|
|
491
|
+
|
|
492
|
+
const { unsubscribeUrl } = useEmailContext();
|
|
493
|
+
{
|
|
494
|
+
unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>;
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
// workflow.config.ts
|
|
500
|
+
email: {
|
|
501
|
+
unsubscribeUrl: 'https://acme.test/unsubscribe', // templates without their own
|
|
502
|
+
tracking: { quickClickMs: 1_000 }, // the default
|
|
464
503
|
},
|
|
465
504
|
```
|
|
466
505
|
|
|
467
506
|
An email step refuses to send when the profile has no `email`.
|
|
468
|
-
`GET /preview/<
|
|
507
|
+
`GET /preview/<id>?props=<JSON>` renders a template, with the subject in the
|
|
469
508
|
`x-subject` header.
|
|
470
509
|
|
|
471
510
|
### Push
|
|
472
511
|
|
|
473
|
-
`pushTemplate({ title, description?, props, preview, content })`, where
|
|
512
|
+
`pushTemplate({ id, title, description?, props, preview, content })`, where
|
|
474
513
|
`content(props)` returns `{ title, body, image?, url? }`: final text, never
|
|
475
514
|
placeholder-expanded. `url` is the page a browser push opens.
|
|
476
515
|
|
|
477
516
|
```ts
|
|
478
517
|
export default pushTemplate({
|
|
518
|
+
id: 'onboarding_tip_push',
|
|
479
519
|
title: 'Onboarding tip (push)',
|
|
480
520
|
props: strictObject({ name: string() }),
|
|
481
521
|
preview: { name: 'John Doe' },
|
|
@@ -528,6 +568,7 @@ import { optional, strictObject, string } from 'zod/mini';
|
|
|
528
568
|
import type { Env } from '../env';
|
|
529
569
|
|
|
530
570
|
export default slackTemplate({
|
|
571
|
+
id: 'nps_detractor',
|
|
531
572
|
title: 'NPS detractor (support)',
|
|
532
573
|
props: strictObject({ name: string(), email: optional(string()) }),
|
|
533
574
|
preview: { name: 'John Doe', email: 'john@example.com' },
|
|
@@ -544,9 +585,8 @@ post never holds up the messages after it.
|
|
|
544
585
|
|
|
545
586
|
### Other channels
|
|
546
587
|
|
|
547
|
-
`.sms()`, `.inApp()` and `.survey()` compile, and
|
|
548
|
-
|
|
549
|
-
hand-typed reference. The config's channels deliver only email, push, Slack and
|
|
588
|
+
`.sms()`, `.inApp()` and `.survey()` compile, and take a hand-typed reference
|
|
589
|
+
by key: `template.sms<P>(key)` (also `inApp`, `survey`). The config's channels deliver only email, push, Slack and
|
|
550
590
|
Discord; supply anything else with `journeyWorker({ messages })`.
|
|
551
591
|
|
|
552
592
|
## Configuration
|
|
@@ -561,7 +601,7 @@ Discord; supply anything else with `journeyWorker({ messages })`.
|
|
|
561
601
|
| `segments` | Named segments |
|
|
562
602
|
| `actions` | Custom actions `.run()` uses |
|
|
563
603
|
| `templates` | The registries: `{ email?, push?, discord?, slack? }` |
|
|
564
|
-
| `
|
|
604
|
+
| `email` | `{ unsubscribeUrl?, tracking? }`: what every email template shares |
|
|
565
605
|
| `runtime` | Per mode (`development`, `production`, …): `{ timeScale?, logMessages? }` |
|
|
566
606
|
| `apns` | `{ topic, keyId, teamId, environment? }` |
|
|
567
607
|
| `fcm` | `{ androidChannelId? }` |
|
|
@@ -651,8 +691,8 @@ workflows actually changed. Deploy recomputes every declared hash and refuses a
|
|
|
651
691
|
bundle whose hashes do not describe its contents.
|
|
652
692
|
|
|
653
693
|
Node ids are structural paths, so inserting a node shifts the ids of the nodes
|
|
654
|
-
after it. Template
|
|
655
|
-
backward-compatible changes, so a new required prop means a new
|
|
694
|
+
after it. Template ids are stable identities too: an id in flight takes only
|
|
695
|
+
backward-compatible changes, so a new required prop means a new id.
|
|
656
696
|
|
|
657
697
|
## Cloudflare setup
|
|
658
698
|
|
|
@@ -665,7 +705,7 @@ The default store needs three bindings, named as in `JourneyEnv`:
|
|
|
665
705
|
| `JOURNEY` | Workflow | The `JourneyRunner` entrypoint |
|
|
666
706
|
|
|
667
707
|
A Worker on Postgres needs only `JOURNEY`. Add the `EMAIL` send_email binding
|
|
668
|
-
when the config
|
|
708
|
+
when the config registers email templates, and a `triggers.crons` entry with `scheduled`
|
|
669
709
|
exported to run the segment sweep.
|
|
670
710
|
|
|
671
711
|
Set `API_TOKEN` in production. Without it every mutating endpoint is open, and
|
|
@@ -742,7 +782,8 @@ On Postgres the engine also keeps:
|
|
|
742
782
|
by the default observer. With `WORKFLOW_ORIGIN` set, email links and an open
|
|
743
783
|
pixel are tracked through the Worker's `/t/...` routes. Each open and click
|
|
744
784
|
is classified as a person's or a machine's (Apple Mail Privacy Protection,
|
|
745
|
-
link scanners, prefetches).
|
|
785
|
+
link scanners, prefetches). An email's unsubscribe link gets a confirmation
|
|
786
|
+
page; which link that is was fixed when the email was sent.
|
|
746
787
|
A push carries `message_id`; report the tap with `messageOpened`.
|
|
747
788
|
- **Segments.** The cron trigger runs `sweepSegments`, which keeps every
|
|
748
789
|
segment's membership and size and starts the journeys of segments entered by
|
package/dist/client.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Contract, ContractIssue, ProfilePatch } from "./contract.cjs";
|
|
2
1
|
import { ProfileProps } from "./store/index.cjs";
|
|
2
|
+
import { Contract, ContractIssue, ProfilePatch } from "./contract.cjs";
|
|
3
3
|
import { DeployResult, IdentifyResult, IngestResult } from "./cloudflare/router.cjs";
|
|
4
4
|
import "./cloudflare/index.cjs";
|
|
5
5
|
import * as core from "zod/v4/core";
|
package/dist/client.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Contract, ContractIssue, ProfilePatch } from "./contract.mjs";
|
|
2
1
|
import { ProfileProps } from "./store/index.mjs";
|
|
2
|
+
import { Contract, ContractIssue, ProfilePatch } from "./contract.mjs";
|
|
3
3
|
import { DeployResult, IdentifyResult, IngestResult } from "./cloudflare/router.mjs";
|
|
4
4
|
import "./cloudflare/index.mjs";
|
|
5
5
|
import * as core from "zod/v4/core";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../
|
|
1
|
+
{"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;;AAqHA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../
|
|
1
|
+
{"version":3,"file":"bindings.mjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;AAqHA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
|
|
@@ -58,12 +58,9 @@ function channelsFromConfig(config, env, options = {}) {
|
|
|
58
58
|
const registry = config.templates?.email;
|
|
59
59
|
if (registry === void 0) return unavailable("no email templates are registered");
|
|
60
60
|
if (env.EMAIL === void 0) return unavailable("no EMAIL (send_email) binding");
|
|
61
|
-
if (config.emails === void 0) return unavailable("emails.from is not configured");
|
|
62
61
|
return new require_cloudflare_senders.CfEmailSender({
|
|
63
62
|
binding: env.EMAIL,
|
|
64
|
-
|
|
65
|
-
...config.emails.replyTo === void 0 ? {} : { replyTo: config.emails.replyTo },
|
|
66
|
-
render: require_react_email.registryRenderer(registry)
|
|
63
|
+
render: require_react_email.registryRenderer(registry, config.email)
|
|
67
64
|
});
|
|
68
65
|
};
|
|
69
66
|
const push = () => {
|
|
@@ -112,24 +109,21 @@ function channelsFromConfig(config, env, options = {}) {
|
|
|
112
109
|
* A webhook template's URL, read from the environment the Worker validated at startup (its
|
|
113
110
|
* `env` schema): a template whose variable is unset fails the step, naming the template.
|
|
114
111
|
*/
|
|
115
|
-
const webhookUrl = (channel,
|
|
112
|
+
const webhookUrl = (channel, template) => {
|
|
116
113
|
const url = template.webhook(env);
|
|
117
|
-
if (typeof url !== "string" || url === "") throw new Error(`${channel} template '${
|
|
114
|
+
if (typeof url !== "string" || url === "") throw new Error(`${channel} template '${template.id}': its webhook URL is not set in the environment`);
|
|
118
115
|
return url;
|
|
119
116
|
};
|
|
120
|
-
const lookupTemplate = (registry, channel, key) => {
|
|
121
|
-
if (!Object.hasOwn(registry, key)) throw new Error(`unknown ${channel} template: ${key}`);
|
|
122
|
-
return registry[key];
|
|
123
|
-
};
|
|
124
117
|
const discord = () => {
|
|
125
118
|
const registry = config.templates?.discord;
|
|
126
119
|
if (registry === void 0) return unavailable("no discord templates are registered");
|
|
120
|
+
const index = require_templates.indexTemplates("discord", registry);
|
|
127
121
|
return new require_webhooks.DiscordWebhookSender({
|
|
128
|
-
render: (
|
|
129
|
-
const template = lookupTemplate(
|
|
122
|
+
render: (id, props) => {
|
|
123
|
+
const template = require_templates.lookupTemplate(index, "discord", id);
|
|
130
124
|
return {
|
|
131
|
-
url: webhookUrl("discord",
|
|
132
|
-
content: require_templates.renderDiscordContent(template,
|
|
125
|
+
url: webhookUrl("discord", template),
|
|
126
|
+
content: require_templates.renderDiscordContent(template, props)
|
|
133
127
|
};
|
|
134
128
|
},
|
|
135
129
|
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
@@ -138,12 +132,13 @@ function channelsFromConfig(config, env, options = {}) {
|
|
|
138
132
|
const slack = () => {
|
|
139
133
|
const registry = config.templates?.slack;
|
|
140
134
|
if (registry === void 0) return unavailable("no slack templates are registered");
|
|
135
|
+
const index = require_templates.indexTemplates("slack", registry);
|
|
141
136
|
return new require_webhooks.SlackWebhookSender({
|
|
142
|
-
render: (
|
|
143
|
-
const template = lookupTemplate(
|
|
137
|
+
render: (id, props) => {
|
|
138
|
+
const template = require_templates.lookupTemplate(index, "slack", id);
|
|
144
139
|
return {
|
|
145
|
-
url: webhookUrl("slack",
|
|
146
|
-
content: require_templates.renderSlackContent(template,
|
|
140
|
+
url: webhookUrl("slack", template),
|
|
141
|
+
content: require_templates.renderSlackContent(template, props)
|
|
147
142
|
};
|
|
148
143
|
},
|
|
149
144
|
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.cjs","names":["resolveRuntime","CfEmailSender","registryRenderer","registryPushRenderer","removeDevice","ApnsPushSender","apnsKeyFromBase64","apnsEnvironment","FcmPushSender","serviceAccountFromBase64","WebPushSender","routeByPlatform","DiscordWebhookSender","renderDiscordContent","SlackWebhookSender","renderSlackContent","routeByChannel"],"sources":["../../src/cloudflare/channels.ts"],"sourcesContent":["import { ApnsPushSender, apnsKeyFromBase64 } from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport type { MessageSender, OutboundMessage } from '../engine/ports';\nimport { FcmPushSender, serviceAccountFromBase64 } from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport { WebPushSender } from '../webpush';\nimport type { JourneyEnv } from './bindings';\nimport { removeDevice } from './router';\nimport { CfEmailSender, routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the `EMAIL` binding with the configured addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/** A sender that fails every message with the reason the channel cannot deliver. */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new Error(`cannot send '${message.template}' on ${message.channel}: ${reason}`);\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${name} is not set: the environment must hold it for the configured channel`);\n }\n return value;\n}\n\n/**\n * A sender built on its first message. A platform's secret is only required\n * when something is actually sent there: a journey that only emails must not\n * fail because the APNs key is missing from a local `.dev.vars`.\n */\nfunction lazy(build: () => MessageSender): MessageSender {\n let sender: MessageSender | undefined;\n return {\n async send(message) {\n sender ??= build();\n return sender.send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'emails' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n fetch?: typeof fetch;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n if (env.EMAIL === undefined) return unavailable('no EMAIL (send_email) binding');\n if (config.emails === undefined) return unavailable('emails.from is not configured');\n return new CfEmailSender({\n binding: env.EMAIL,\n from: config.emails.from,\n ...(config.emails.replyTo === undefined ? {} : { replyTo: config.emails.replyTo }),\n render: registryRenderer(registry),\n });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n // A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n // waits that read devices, as a removal through the API does\n const onUnregistered = async (message: OutboundMessage) => {\n if (message.device !== undefined && options.store !== undefined) {\n await removeDevice(env, message.device.token, options.store);\n }\n };\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n platforms.ios = lazy(\n () =>\n new ApnsPushSender({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(requireSecret(env, 'APNS_PRIVATE_KEY_BASE64')),\n topic: apns.topic,\n environment: apnsEnvironment(apns, mode),\n render,\n onUnregistered,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = lazy(\n () =>\n new FcmPushSender({\n serviceAccount: serviceAccountFromBase64(\n requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64')\n ),\n render,\n onUnregistered,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = lazy(\n () =>\n new WebPushSender({\n vapid: {\n subject: webPush.subject,\n publicKey: requireSecret(env, 'VAPID_PUBLIC_KEY'),\n privateKey: requireSecret(env, 'VAPID_PRIVATE_KEY'),\n },\n render,\n onUnregistered,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (\n channel: 'discord' | 'slack',\n key: string,\n template: DiscordTemplate | SlackTemplate\n ) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new Error(\n `${channel} template '${key}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n const lookupTemplate = <T>(\n registry: Readonly<Record<string, T>>,\n channel: string,\n key: string\n ) => {\n if (!Object.hasOwn(registry, key)) throw new Error(`unknown ${channel} template: ${key}`);\n return registry[key];\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n return new DiscordWebhookSender({\n render: (key, props) => {\n const template = lookupTemplate(registry, 'discord', key) as DiscordTemplate;\n return {\n url: webhookUrl('discord', key, template),\n content: renderDiscordContent(template, key, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n return new SlackWebhookSender({\n render: (key, props) => {\n const template = lookupTemplate(registry, 'slack', key) as SlackTemplate;\n return {\n url: webhookUrl('slack', key, template),\n content: renderSlackContent(template, key, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"channels.cjs","names":["resolveRuntime","CfEmailSender","registryRenderer","registryPushRenderer","removeDevice","ApnsPushSender","apnsKeyFromBase64","apnsEnvironment","FcmPushSender","serviceAccountFromBase64","WebPushSender","routeByPlatform","indexTemplates","DiscordWebhookSender","lookupTemplate","renderDiscordContent","SlackWebhookSender","renderSlackContent","routeByChannel"],"sources":["../../src/cloudflare/channels.ts"],"sourcesContent":["import { ApnsPushSender, apnsKeyFromBase64 } from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport type { MessageSender, OutboundMessage } from '../engine/ports';\nimport { FcmPushSender, serviceAccountFromBase64 } from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport { WebPushSender } from '../webpush';\nimport type { JourneyEnv } from './bindings';\nimport { removeDevice } from './router';\nimport { CfEmailSender, routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the `EMAIL` binding with the configured addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/** A sender that fails every message with the reason the channel cannot deliver. */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new Error(`cannot send '${message.template}' on ${message.channel}: ${reason}`);\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${name} is not set: the environment must hold it for the configured channel`);\n }\n return value;\n}\n\n/**\n * A sender built on its first message. A platform's secret is only required\n * when something is actually sent there: a journey that only emails must not\n * fail because the APNs key is missing from a local `.dev.vars`.\n */\nfunction lazy(build: () => MessageSender): MessageSender {\n let sender: MessageSender | undefined;\n return {\n async send(message) {\n sender ??= build();\n return sender.send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n fetch?: typeof fetch;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n if (env.EMAIL === undefined) return unavailable('no EMAIL (send_email) binding');\n // Each template names its own sender: there is no global one to require\n return new CfEmailSender({\n binding: env.EMAIL,\n render: registryRenderer(registry, config.email),\n });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n // A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n // waits that read devices, as a removal through the API does\n const onUnregistered = async (message: OutboundMessage) => {\n if (message.device !== undefined && options.store !== undefined) {\n await removeDevice(env, message.device.token, options.store);\n }\n };\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n platforms.ios = lazy(\n () =>\n new ApnsPushSender({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(requireSecret(env, 'APNS_PRIVATE_KEY_BASE64')),\n topic: apns.topic,\n environment: apnsEnvironment(apns, mode),\n render,\n onUnregistered,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = lazy(\n () =>\n new FcmPushSender({\n serviceAccount: serviceAccountFromBase64(\n requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64')\n ),\n render,\n onUnregistered,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = lazy(\n () =>\n new WebPushSender({\n vapid: {\n subject: webPush.subject,\n publicKey: requireSecret(env, 'VAPID_PUBLIC_KEY'),\n privateKey: requireSecret(env, 'VAPID_PRIVATE_KEY'),\n },\n render,\n onUnregistered,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new Error(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { MessageSender } from "../engine/ports.cjs";
|
|
2
|
-
import { JourneyMode, JourneyRuntimeOptions } from "../runtime.cjs";
|
|
3
|
-
import { JourneyConfig } from "../config.cjs";
|
|
4
2
|
import { JourneyEnv } from "./bindings.cjs";
|
|
5
3
|
import { JourneyStore } from "../store/index.cjs";
|
|
4
|
+
import { JourneyMode, JourneyRuntimeOptions } from "../runtime.cjs";
|
|
5
|
+
import { JourneyConfig } from "../config.cjs";
|
|
6
6
|
//#region src/cloudflare/channels.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The outlets a `JourneyConfig` describes, assembled against the environment:
|
|
@@ -19,7 +19,7 @@ export declare function journeyMode(env: JourneyEnv): JourneyMode;
|
|
|
19
19
|
/** The runtime options for this environment's mode. */
|
|
20
20
|
export declare function runtimeFromConfig(config: Pick<JourneyConfig, 'runtime'> | undefined, env: JourneyEnv): Required<JourneyRuntimeOptions>;
|
|
21
21
|
/** The channels slice of the config — what `channelsFromConfig` reads. */
|
|
22
|
-
export type ChannelsConfig = Pick<JourneyConfig, 'templates' | '
|
|
22
|
+
export type ChannelsConfig = Pick<JourneyConfig, 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'>;
|
|
23
23
|
export declare function channelsFromConfig(config: ChannelsConfig, env: JourneyEnv, options?: {
|
|
24
24
|
/** The store a token APNs / FCM / a push service reports as gone is removed from. */
|
|
25
25
|
store?: JourneyStore | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.d.cts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"channels.d.cts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;wBAkCgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;YA4CA,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;EACR,eAAe;IAEhB"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { MessageSender } from "../engine/ports.mjs";
|
|
2
|
-
import { JourneyMode, JourneyRuntimeOptions } from "../runtime.mjs";
|
|
3
|
-
import { JourneyConfig } from "../config.mjs";
|
|
4
2
|
import { JourneyEnv } from "./bindings.mjs";
|
|
5
3
|
import { JourneyStore } from "../store/index.mjs";
|
|
4
|
+
import { JourneyMode, JourneyRuntimeOptions } from "../runtime.mjs";
|
|
5
|
+
import { JourneyConfig } from "../config.mjs";
|
|
6
6
|
//#region src/cloudflare/channels.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The outlets a `JourneyConfig` describes, assembled against the environment:
|
|
@@ -19,7 +19,7 @@ export declare function journeyMode(env: JourneyEnv): JourneyMode;
|
|
|
19
19
|
/** The runtime options for this environment's mode. */
|
|
20
20
|
export declare function runtimeFromConfig(config: Pick<JourneyConfig, 'runtime'> | undefined, env: JourneyEnv): Required<JourneyRuntimeOptions>;
|
|
21
21
|
/** The channels slice of the config — what `channelsFromConfig` reads. */
|
|
22
|
-
export type ChannelsConfig = Pick<JourneyConfig, 'templates' | '
|
|
22
|
+
export type ChannelsConfig = Pick<JourneyConfig, 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'>;
|
|
23
23
|
export declare function channelsFromConfig(config: ChannelsConfig, env: JourneyEnv, options?: {
|
|
24
24
|
/** The store a token APNs / FCM / a push service reports as gone is removed from. */
|
|
25
25
|
store?: JourneyStore | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.d.mts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"channels.d.mts","names":[],"sources":["../../src/cloudflare/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;wBAkCgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;YA4CA,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;EACR,eAAe;IAEhB"}
|