@shware/workflow 1.0.0 → 2.0.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.
- package/README.md +42 -11
- 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 +1 -4
- package/dist/cloudflare/channels.cjs.map +1 -1
- package/dist/cloudflare/channels.d.cts +3 -3
- package/dist/cloudflare/channels.d.mts +3 -3
- package/dist/cloudflare/channels.mjs +1 -4
- 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 +10 -11
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +10 -33
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +10 -33
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +11 -11
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +1 -1
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +1 -1
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +2 -2
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +2 -2
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +1 -1
- package/dist/dsl/bundle.d.mts +1 -1
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +1 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +1 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/refs.cjs.map +1 -1
- package/dist/dsl/refs.d.cts +5 -4
- package/dist/dsl/refs.d.cts.map +1 -1
- package/dist/dsl/refs.d.mts +5 -4
- package/dist/dsl/refs.d.mts.map +1 -1
- package/dist/dsl/refs.mjs.map +1 -1
- package/dist/dsl/template.cjs.map +1 -1
- package/dist/dsl/template.d.cts +2 -2
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +2 -2
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.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/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- 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 +8 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +3 -2
- package/dist/react-email.cjs +11 -3
- 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 +11 -3
- 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 +8 -13
- 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 +8 -13
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +5 -5
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.mjs +5 -5
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/templates.cjs +6 -1
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.d.cts +21 -2
- package/dist/templates.d.cts.map +1 -1
- package/dist/templates.d.mts +21 -2
- package/dist/templates.d.mts.map +1 -1
- package/dist/templates.mjs +6 -1
- package/dist/templates.mjs.map +1 -1
- package/package.json +11 -1
package/dist/dsl/refs.d.mts
CHANGED
|
@@ -47,10 +47,11 @@ export type EventRefs<E> = { readonly [K in keyof E]-?: EventRef<E[K]>; };
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function user<U extends UserPropertyBase>(): UserRefs<U>;
|
|
49
49
|
export declare function event<E extends EventMap>(): EventRefs<E>;
|
|
50
|
-
|
|
50
|
+
/** An impossible field cannot be supplied indirectly through a profile reference either. */
|
|
51
|
+
export type PropInput<T> = [T] extends [never] ? never : T | UserPropertyRef<T>;
|
|
51
52
|
export type PropsInput<P> = { [K in keyof P]: PropInput<P[K]>; };
|
|
52
53
|
/** Props may be omitted when a template declares none, and are required when it does. */
|
|
53
|
-
export type MessageArgs<P> = Record<
|
|
54
|
+
export type MessageArgs<P> = Record<string, never> extends P ? [props?: PropsInput<P>] : [props: PropsInput<P>];
|
|
54
55
|
/**
|
|
55
56
|
* Where a payload ref keeps its own state. A payload ref doubles as the table
|
|
56
57
|
* of its sub-fields, so every *string* key it answered for itself shadowed an
|
|
@@ -74,9 +75,9 @@ export interface PayloadRef<T> {
|
|
|
74
75
|
readonly __t?: T;
|
|
75
76
|
}
|
|
76
77
|
/** A payload field ref that is itself an object also acts as a table of its sub-field refs. */
|
|
77
|
-
export type PayloadRefNode<T> = [NonNullable<T>] extends [object] ? PayloadRef<T> & { readonly [K in keyof NonNullable<T> & string]-?: PayloadRefNode<NonNullable<T>[K]>; } : PayloadRef<T>;
|
|
78
|
+
export type PayloadRefNode<T> = [NonNullable<T>] extends [never] ? PayloadRef<T> : [NonNullable<T>] extends [object] ? PayloadRef<T> & { readonly [K in keyof NonNullable<T> & string as [NonNullable<T>[K]] extends [never] ? never : K]-?: PayloadRefNode<NonNullable<T>[K]>; } : PayloadRef<T>;
|
|
78
79
|
/** Payload reference table for an event's payload P — the `p` in `where: (p) => eq(p.platform, 'web')`. */
|
|
79
|
-
export type PayloadRefs<P> = [P] extends [object] ? { readonly [K in keyof P & string]-?: PayloadRefNode<P[K]>; } : Record<string, PayloadRef<unknown>>;
|
|
80
|
+
export type PayloadRefs<P> = [P] extends [object] ? { readonly [K in keyof P & string as [P[K]] extends [never] ? never : K]-?: PayloadRefNode<P[K]>; } : Record<string, PayloadRef<unknown>>;
|
|
80
81
|
/**
|
|
81
82
|
* Nested Proxy: every string key extends the dotted path, so an author's field
|
|
82
83
|
* may be named anything — including `path` or `type`. The ref's own path reads
|
package/dist/dsl/refs.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/dsl/refs.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAmBiB,gBAAgB;WACtB;WACA;WACA,MAAM;;;iBAIA,SAAS;WACf;WACA;WACA,MAAM;;;YAIL,SAAS,iBAAiB,WAAW,MAAM,gBAAgB,EAAE;;YAG7D,UAAU,iBAAiB,WAAW,MAAM,SAAS,EAAE;;;;;;;;;;;;;;;wBAgBnD,KAAK,UAAU,qBAAqB,SAAS;wBAM7C,MAAM,UAAU,aAAa,UAAU
|
|
1
|
+
{"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/dsl/refs.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAmBiB,gBAAgB;WACtB;WACA;WACA,MAAM;;;iBAIA,SAAS;WACf;WACA;WACA,MAAM;;;YAIL,SAAS,iBAAiB,WAAW,MAAM,gBAAgB,EAAE;;YAG7D,UAAU,iBAAiB,WAAW,MAAM,SAAS,EAAE;;;;;;;;;;;;;;;wBAgBnD,KAAK,UAAU,qBAAqB,SAAS;wBAM7C,MAAM,UAAU,aAAa,UAAU;;YAO3C,UAAU,MAAM,6BAA6B,IAAI,gBAAgB;YACjE,WAAW,QAAQ,WAAW,IAAI,UAAU,EAAE;;YAG9C,YAAY,KACtB,8BAA8B,KAAK,QAAQ,WAAW,OAAO,OAAO,WAAW;;;;;;;;;;;qBAcpE;;;;;;;;iBASI,WAAW;YAChB;WACD,MAAM;;;YAIL,eAAe,MAAM,YAAY,sBACzC,WAAW,MACV,YAAY,uBACX,WAAW,iBAEP,WAAW,YAAY,gBAAgB,YAAY,GAAG,8BAA8B,MACjF,eAAe,YAAY,GAAG,SAErC,WAAW;;YAGL,YAAY,MAAM,kCAEd,WAAW,eAAe,EAAE,8BAA8B,MAAM,eACxE,EAAE,SAGN,eAAe;;;;;;;wBAQH,YAAY,GAAG,kBAAc,YAAY"}
|
package/dist/dsl/refs.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refs.mjs","names":[],"sources":["../../src/dsl/refs.ts"],"sourcesContent":["import type { EventMap, UserPropertyBase } from './base';\n\n/**\n * Reference tables — the DSL's only two type-injection points (drizzle's\n * column-object pattern): `u = user<UserProperty>()` and `e = event<Event>()`.\n * Every other function in the DSL is generic-free and reads types off these\n * references.\n *\n * Type technique: phantom types. `UserPropertyRef<T>` carries the property's\n * type in an optional, never-assigned `__t` — purely so free functions like\n * `eq` can infer T from the reference. At runtime a ref is just\n * `{ type, path }`, produced by a Proxy on property access.\n */\n\n/**\n * User-property reference (drizzle's column equivalent), shared by predicates\n * (eq/gt/…) and message-prop personalization. The phantom type __t is how a\n * free function reads the property's type off the reference.\n */\nexport interface UserPropertyRef<T> {\n readonly type: 'user_property';\n readonly path: string;\n readonly __t?: T;\n}\n\n/** Event reference: the argument to `performed`; future payload where-clauses and value refs hang off it. */\nexport interface EventRef<P = unknown> {\n readonly type: 'event_ref';\n readonly name: string;\n readonly __p?: P;\n}\n\n/** Property reference table: `u.subscription_status` is a UserPropertyRef (drizzle's `users.email`). */\nexport type UserRefs<U> = { readonly [K in keyof U]-?: UserPropertyRef<U[K]> };\n\n/** Event reference table: `e.purchase` is an EventRef. */\nexport type EventRefs<E> = { readonly [K in keyof E]-?: EventRef<E[K]> };\n\n/**\n * Build the reference tables: the app calls each once in its schema module and\n * exports the result (implemented with a Proxy — property access *is* the reference).\n *\n * export const u = user<UserProperty>();\n * export const e = event<Event>();\n *\n * From then on predicates and personalization are all property access:\n * eq(u.subscription_status, 'active'), performed(e.purchase, { within: '30 days' }),\n * { plan: u.subscription_plan }.\n *\n * Properties are deliberately single-level: the data source is a database\n * table and is naturally flat, so nested paths are out of scope.\n */\nexport function user<U extends UserPropertyBase>(): UserRefs<U> {\n return new Proxy({} as UserRefs<U>, {\n get: (_target, prop) => ({ type: 'user_property', path: String(prop) }),\n });\n}\n\nexport function event<E extends EventMap>(): EventRefs<E> {\n return new Proxy({} as EventRefs<E>, {\n get: (_target, prop) => ({ type: 'event_ref', name: String(prop) }),\n });\n}\n\nexport type PropInput<T> = T | UserPropertyRef<T>;\nexport type PropsInput<P> = { [K in keyof P]: PropInput<P[K]> };\n\n/** Props may be omitted when a template declares none, and are required when it does. */\nexport type MessageArgs<P> =\n Record<
|
|
1
|
+
{"version":3,"file":"refs.mjs","names":[],"sources":["../../src/dsl/refs.ts"],"sourcesContent":["import type { EventMap, UserPropertyBase } from './base';\n\n/**\n * Reference tables — the DSL's only two type-injection points (drizzle's\n * column-object pattern): `u = user<UserProperty>()` and `e = event<Event>()`.\n * Every other function in the DSL is generic-free and reads types off these\n * references.\n *\n * Type technique: phantom types. `UserPropertyRef<T>` carries the property's\n * type in an optional, never-assigned `__t` — purely so free functions like\n * `eq` can infer T from the reference. At runtime a ref is just\n * `{ type, path }`, produced by a Proxy on property access.\n */\n\n/**\n * User-property reference (drizzle's column equivalent), shared by predicates\n * (eq/gt/…) and message-prop personalization. The phantom type __t is how a\n * free function reads the property's type off the reference.\n */\nexport interface UserPropertyRef<T> {\n readonly type: 'user_property';\n readonly path: string;\n readonly __t?: T;\n}\n\n/** Event reference: the argument to `performed`; future payload where-clauses and value refs hang off it. */\nexport interface EventRef<P = unknown> {\n readonly type: 'event_ref';\n readonly name: string;\n readonly __p?: P;\n}\n\n/** Property reference table: `u.subscription_status` is a UserPropertyRef (drizzle's `users.email`). */\nexport type UserRefs<U> = { readonly [K in keyof U]-?: UserPropertyRef<U[K]> };\n\n/** Event reference table: `e.purchase` is an EventRef. */\nexport type EventRefs<E> = { readonly [K in keyof E]-?: EventRef<E[K]> };\n\n/**\n * Build the reference tables: the app calls each once in its schema module and\n * exports the result (implemented with a Proxy — property access *is* the reference).\n *\n * export const u = user<UserProperty>();\n * export const e = event<Event>();\n *\n * From then on predicates and personalization are all property access:\n * eq(u.subscription_status, 'active'), performed(e.purchase, { within: '30 days' }),\n * { plan: u.subscription_plan }.\n *\n * Properties are deliberately single-level: the data source is a database\n * table and is naturally flat, so nested paths are out of scope.\n */\nexport function user<U extends UserPropertyBase>(): UserRefs<U> {\n return new Proxy({} as UserRefs<U>, {\n get: (_target, prop) => ({ type: 'user_property', path: String(prop) }),\n });\n}\n\nexport function event<E extends EventMap>(): EventRefs<E> {\n return new Proxy({} as EventRefs<E>, {\n get: (_target, prop) => ({ type: 'event_ref', name: String(prop) }),\n });\n}\n\n/** An impossible field cannot be supplied indirectly through a profile reference either. */\nexport type PropInput<T> = [T] extends [never] ? never : T | UserPropertyRef<T>;\nexport type PropsInput<P> = { [K in keyof P]: PropInput<P[K]> };\n\n/** Props may be omitted when a template declares none, and are required when it does. */\nexport type MessageArgs<P> =\n Record<string, never> extends P ? [props?: PropsInput<P>] : [props: PropsInput<P>];\n\n/* ------------------------------ payload references ----------------------------- */\n\n/**\n * Where a payload ref keeps its own state. A payload ref doubles as the table\n * of its sub-fields, so every *string* key it answered for itself shadowed an\n * author's field of that name: with a payload field called `path`,\n * `performed(e.page_view, (p) => eq(p.path, '/pricing'))` type-checked, handed\n * `prop()` the ref's own prefix instead of a sub-ref, and compiled to a\n * *profile* predicate on a property named 'path' — a mistake that only\n * surfaced later, as a zod error nowhere near the line that caused it. A symbol\n * key cannot be reached by `p.<field>`, so no field name can collide with it.\n */\nexport const PAYLOAD_PATH: unique symbol = Symbol('workflow.payloadPath');\n\n/**\n * Event-payload reference: what a `where` callback's parameter hands out. The\n * path it carries is dotted for nested fields ('tags.utm_source'). Predicates\n * accept it alongside UserPropertyRef and compile to a `payload` condition —\n * which is only valid inside `performed({ where })` / the event trigger's\n * `where` (compileBundle enforces the placement).\n */\nexport interface PayloadRef<T> {\n readonly [PAYLOAD_PATH]: string;\n readonly __t?: T;\n}\n\n/** A payload field ref that is itself an object also acts as a table of its sub-field refs. */\nexport type PayloadRefNode<T> = [NonNullable<T>] extends [never]\n ? PayloadRef<T>\n : [NonNullable<T>] extends [object]\n ? PayloadRef<T> & {\n readonly [\n K in keyof NonNullable<T> & string as [NonNullable<T>[K]] extends [never] ? never : K\n ]-?: PayloadRefNode<NonNullable<T>[K]>;\n }\n : PayloadRef<T>;\n\n/** Payload reference table for an event's payload P — the `p` in `where: (p) => eq(p.platform, 'web')`. */\nexport type PayloadRefs<P> = [P] extends [object]\n ? {\n readonly [K in keyof P & string as [P[K]] extends [never] ? never : K]-?: PayloadRefNode<\n P[K]\n >;\n }\n : Record<string, PayloadRef<unknown>>;\n\n/**\n * Nested Proxy: every string key extends the dotted path, so an author's field\n * may be named anything — including `path` or `type`. The ref's own path reads\n * off PAYLOAD_PATH, the one key a field name cannot spell.\n * @internal used by performed() and trigger.event() to hand `p` to a where callback\n */\nexport function payloadRefs<P>(prefix = ''): PayloadRefs<P> {\n return new Proxy({} as PayloadRefs<P>, {\n get: (_target, prop) => {\n if (prop === PAYLOAD_PATH) return prefix;\n // Other symbols are the runtime's own probes (inspection, iteration, thenable checks), never fields\n if (typeof prop === 'symbol') return undefined;\n return payloadRefs(prefix === '' ? prop : `${prefix}.${prop}`);\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoDA,SAAgB,OAAgD;CAC9D,OAAO,IAAI,MAAM,CAAC,GAAkB,EAClC,MAAM,SAAS,UAAU;EAAE,MAAM;EAAiB,MAAM,OAAO,IAAI;CAAE,GACvE,CAAC;AACH;AAEA,SAAgB,QAA0C;CACxD,OAAO,IAAI,MAAM,CAAC,GAAmB,EACnC,MAAM,SAAS,UAAU;EAAE,MAAM;EAAa,MAAM,OAAO,IAAI;CAAE,GACnE,CAAC;AACH;;;;;;;;;;;AAsBA,MAAa,eAA8B,OAAO,sBAAsB;;;;;;;AAwCxE,SAAgB,YAAe,SAAS,IAAoB;CAC1D,OAAO,IAAI,MAAM,CAAC,GAAqB,EACrC,MAAM,SAAS,SAAS;EACtB,IAAI,SAAS,cAAc,OAAO;EAElC,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;EACrC,OAAO,YAAY,WAAW,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM;CAC/D,EACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.cjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates in the DSL. The four channels with template definitions (email, push, discord,\n * slack) take the definition itself — `.email(welcome, props)` — so its Zod `props` schema types\n * the call (`PropsOf`) and its `id` lands in the IR. The channels rendered by app-supplied\n * senders (sms, in-app, survey) have no definition; they take a hand-typed reference by key.\n */\n\nimport type * as core from 'zod/v4/core';\nimport type { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from '../templates';\n\nexport type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';\n\nexport type EmptyProps = Record<
|
|
1
|
+
{"version":3,"file":"template.cjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates in the DSL. The four channels with template definitions (email, push, discord,\n * slack) take the definition itself — `.email(welcome, props)` — so its Zod `props` schema types\n * the call (`PropsOf`) and its `id` lands in the IR. The channels rendered by app-supplied\n * senders (sms, in-app, survey) have no definition; they take a hand-typed reference by key.\n */\n\nimport type * as core from 'zod/v4/core';\nimport type { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from '../templates';\n\nexport type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';\n\nexport type EmptyProps = Record<string, never>;\n\n/** A template by key, its props typed by hand: for the channels without template definitions. */\nexport interface TemplateRef<C extends Channel = Channel, P extends object = object> {\n readonly channel: C;\n readonly key: string;\n readonly __props?: P;\n}\n\nexport interface TemplateFactory {\n sms<P extends object = EmptyProps>(key: string): TemplateRef<'sms', P>;\n inApp<P extends object = EmptyProps>(key: string): TemplateRef<'in_app', P>;\n survey<P extends object = EmptyProps>(key: string): TemplateRef<'survey', P>;\n}\n\nconst makeTemplate =\n <C extends Channel>(channel: C) =>\n <P extends object = EmptyProps>(key: string): TemplateRef<C, P> => ({ channel, key });\n\n/**\n * Template references for the channels an app-supplied sender renders:\n *\n * const code = template.sms<{ code: string }>('login_code');\n */\nexport const template: TemplateFactory = {\n sms: makeTemplate('sms'),\n inApp: makeTemplate('in_app'),\n survey: makeTemplate('survey'),\n};\n\n/** Template props: the input type of the template's Zod `props` schema — what the DSL passes. */\nexport type PropsOf<M> = M extends\n | EmailTemplate<infer S>\n | PushTemplate<infer S>\n | DiscordTemplate<infer S, never>\n | SlackTemplate<infer S, never>\n ? core.input<S> extends object\n ? core.input<S>\n : EmptyProps\n : never;\n"],"mappings":";;AA2BA,MAAM,gBACgB,aACY,SAAoC;CAAE;CAAS;AAAI;;;;;;AAOrF,MAAa,WAA4B;CACvC,KAAK,aAAa,KAAK;CACvB,OAAO,aAAa,QAAQ;CAC5B,QAAQ,aAAa,QAAQ;AAC/B"}
|
package/dist/dsl/template.d.cts
CHANGED
|
@@ -2,9 +2,9 @@ import { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from "../
|
|
|
2
2
|
import * as core from "zod/v4/core";
|
|
3
3
|
//#region src/dsl/template.d.ts
|
|
4
4
|
export type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';
|
|
5
|
-
export type EmptyProps = Record<
|
|
5
|
+
export type EmptyProps = Record<string, never>;
|
|
6
6
|
/** A template by key, its props typed by hand: for the channels without template definitions. */
|
|
7
|
-
export interface TemplateRef<C extends Channel = Channel, P extends object =
|
|
7
|
+
export interface TemplateRef<C extends Channel = Channel, P extends object = object> {
|
|
8
8
|
readonly channel: C;
|
|
9
9
|
readonly key: string;
|
|
10
10
|
readonly __props?: P;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.cts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;YAUY;YAEA,aAAa;;iBAGR,YAAY,UAAU,UAAU,SAAS
|
|
1
|
+
{"version":3,"file":"template.d.cts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;YAUY;YAEA,aAAa;;iBAGR,YAAY,UAAU,UAAU,SAAS;WAC/C,SAAS;WACT;WACA,UAAU;;iBAGJ;EACf,IAAI,mBAAmB,YAAY,cAAc,mBAAmB;EACpE,MAAM,mBAAmB,YAAY,cAAc,sBAAsB;EACzE,OAAO,mBAAmB,YAAY,cAAc,sBAAsB;;;;;;;qBAY/D,UAAU;;YAOX,QAAQ,KAAK,UACrB,oBAAoB,KACpB,mBAAmB,KACnB,sBAAsB,YACtB,oBAAoB,YACpB,KAAK,MAAM,oBACT,KAAK,MAAM,KACX"}
|
package/dist/dsl/template.d.mts
CHANGED
|
@@ -2,9 +2,9 @@ import { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from "../
|
|
|
2
2
|
import * as core from "zod/v4/core";
|
|
3
3
|
//#region src/dsl/template.d.ts
|
|
4
4
|
export type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';
|
|
5
|
-
export type EmptyProps = Record<
|
|
5
|
+
export type EmptyProps = Record<string, never>;
|
|
6
6
|
/** A template by key, its props typed by hand: for the channels without template definitions. */
|
|
7
|
-
export interface TemplateRef<C extends Channel = Channel, P extends object =
|
|
7
|
+
export interface TemplateRef<C extends Channel = Channel, P extends object = object> {
|
|
8
8
|
readonly channel: C;
|
|
9
9
|
readonly key: string;
|
|
10
10
|
readonly __props?: P;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.mts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;YAUY;YAEA,aAAa;;iBAGR,YAAY,UAAU,UAAU,SAAS
|
|
1
|
+
{"version":3,"file":"template.d.mts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;YAUY;YAEA,aAAa;;iBAGR,YAAY,UAAU,UAAU,SAAS;WAC/C,SAAS;WACT;WACA,UAAU;;iBAGJ;EACf,IAAI,mBAAmB,YAAY,cAAc,mBAAmB;EACpE,MAAM,mBAAmB,YAAY,cAAc,sBAAsB;EACzE,OAAO,mBAAmB,YAAY,cAAc,sBAAsB;;;;;;;qBAY/D,UAAU;;YAOX,QAAQ,KAAK,UACrB,oBAAoB,KACpB,mBAAmB,KACnB,sBAAsB,YACtB,oBAAoB,YACpB,KAAK,MAAM,oBACT,KAAK,MAAM,KACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.mjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates in the DSL. The four channels with template definitions (email, push, discord,\n * slack) take the definition itself — `.email(welcome, props)` — so its Zod `props` schema types\n * the call (`PropsOf`) and its `id` lands in the IR. The channels rendered by app-supplied\n * senders (sms, in-app, survey) have no definition; they take a hand-typed reference by key.\n */\n\nimport type * as core from 'zod/v4/core';\nimport type { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from '../templates';\n\nexport type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';\n\nexport type EmptyProps = Record<
|
|
1
|
+
{"version":3,"file":"template.mjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates in the DSL. The four channels with template definitions (email, push, discord,\n * slack) take the definition itself — `.email(welcome, props)` — so its Zod `props` schema types\n * the call (`PropsOf`) and its `id` lands in the IR. The channels rendered by app-supplied\n * senders (sms, in-app, survey) have no definition; they take a hand-typed reference by key.\n */\n\nimport type * as core from 'zod/v4/core';\nimport type { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from '../templates';\n\nexport type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';\n\nexport type EmptyProps = Record<string, never>;\n\n/** A template by key, its props typed by hand: for the channels without template definitions. */\nexport interface TemplateRef<C extends Channel = Channel, P extends object = object> {\n readonly channel: C;\n readonly key: string;\n readonly __props?: P;\n}\n\nexport interface TemplateFactory {\n sms<P extends object = EmptyProps>(key: string): TemplateRef<'sms', P>;\n inApp<P extends object = EmptyProps>(key: string): TemplateRef<'in_app', P>;\n survey<P extends object = EmptyProps>(key: string): TemplateRef<'survey', P>;\n}\n\nconst makeTemplate =\n <C extends Channel>(channel: C) =>\n <P extends object = EmptyProps>(key: string): TemplateRef<C, P> => ({ channel, key });\n\n/**\n * Template references for the channels an app-supplied sender renders:\n *\n * const code = template.sms<{ code: string }>('login_code');\n */\nexport const template: TemplateFactory = {\n sms: makeTemplate('sms'),\n inApp: makeTemplate('in_app'),\n survey: makeTemplate('survey'),\n};\n\n/** Template props: the input type of the template's Zod `props` schema — what the DSL passes. */\nexport type PropsOf<M> = M extends\n | EmailTemplate<infer S>\n | PushTemplate<infer S>\n | DiscordTemplate<infer S, never>\n | SlackTemplate<infer S, never>\n ? core.input<S> extends object\n ? core.input<S>\n : EmptyProps\n : never;\n"],"mappings":";AA2BA,MAAM,gBACgB,aACY,SAAoC;CAAE;CAAS;AAAI;;;;;;AAOrF,MAAa,WAA4B;CACvC,KAAK,aAAa,KAAK;CACvB,OAAO,aAAa,QAAQ;CAC5B,QAAQ,aAAa,QAAQ;AAC/B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let react = require("react");
|
|
3
|
+
//#region src/email-context.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@shware/workflow/email-context` — the email being rendered, for its components: the resolved
|
|
6
|
+
* metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the
|
|
7
|
+
* template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)
|
|
8
|
+
* and the studio's preview both provide it; the props stay the template's own.
|
|
9
|
+
*
|
|
10
|
+
* const { unsubscribeUrl } = useEmailContext();
|
|
11
|
+
* {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe: React only — no react-dom/server, no Worker bindings.
|
|
14
|
+
*/
|
|
15
|
+
const EmailContext = (0, react.createContext)(void 0);
|
|
16
|
+
function EmailProvider({ email, children }) {
|
|
17
|
+
return (0, react.createElement)(EmailContext.Provider, { value: email }, children);
|
|
18
|
+
}
|
|
19
|
+
/** The email being rendered; throws outside a render the SDK or the studio provides. */
|
|
20
|
+
function useEmailContext() {
|
|
21
|
+
const email = (0, react.useContext)(EmailContext);
|
|
22
|
+
if (email === void 0) throw new Error("useEmailContext() is called outside an email render: render the template with @shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>");
|
|
23
|
+
return email;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.EmailProvider = EmailProvider;
|
|
27
|
+
exports.useEmailContext = useEmailContext;
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=email-context.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-context.cjs","names":["createContext","createElement","useContext"],"sources":["../src/email-context.ts"],"sourcesContent":["import { type ReactNode, createContext, createElement, useContext } from 'react';\nimport type { ResolvedEmail } from './email';\n\n/**\n * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved\n * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the\n * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)\n * and the studio's preview both provide it; the props stay the template's own.\n *\n * const { unsubscribeUrl } = useEmailContext();\n * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}\n *\n * Browser-safe: React only — no react-dom/server, no Worker bindings.\n */\n\nconst EmailContext = createContext<ResolvedEmail | undefined>(undefined);\n\nexport function EmailProvider({\n email,\n children,\n}: {\n email: ResolvedEmail;\n children?: ReactNode;\n}): ReactNode {\n return createElement(EmailContext.Provider, { value: email }, children);\n}\n\n/** The email being rendered; throws outside a render the SDK or the studio provides. */\nexport function useEmailContext(): ResolvedEmail {\n const email = useContext(EmailContext);\n if (email === undefined) {\n throw new Error(\n 'useEmailContext() is called outside an email render: render the template with ' +\n \"@shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>\"\n );\n }\n return email;\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,gBAAA,GAAeA,MAAAA,cAAAA,CAAyC,KAAA,CAAS;AAEvE,SAAgB,cAAc,EAC5B,OACA,YAIY;CACZ,QAAA,GAAOC,MAAAA,cAAAA,CAAc,aAAa,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AACxE;;AAGA,SAAgB,kBAAiC;CAC/C,MAAM,SAAA,GAAQC,MAAAA,WAAAA,CAAW,YAAY;CACrC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,2MAEF;CAEF,OAAO;AACT"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolvedEmail } from "./email.cjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/email-context.d.ts
|
|
4
|
+
export declare function EmailProvider({ email, children }: {
|
|
5
|
+
email: ResolvedEmail;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}): ReactNode;
|
|
8
|
+
/** The email being rendered; throws outside a render the SDK or the studio provides. */
|
|
9
|
+
export declare function useEmailContext(): ResolvedEmail;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=email-context.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-context.d.cts","names":[],"sources":["../src/email-context.ts"],"mappings":";;;wBAiBgB,gBACd,OACA;EAEA,OAAO;EACP,WAAW;IACT;;wBAKY,mBAAmB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolvedEmail } from "./email.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/email-context.d.ts
|
|
4
|
+
export declare function EmailProvider({ email, children }: {
|
|
5
|
+
email: ResolvedEmail;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}): ReactNode;
|
|
8
|
+
/** The email being rendered; throws outside a render the SDK or the studio provides. */
|
|
9
|
+
export declare function useEmailContext(): ResolvedEmail;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=email-context.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-context.d.mts","names":[],"sources":["../src/email-context.ts"],"mappings":";;;wBAiBgB,gBACd,OACA;EAEA,OAAO;EACP,WAAW;IACT;;wBAKY,mBAAmB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, createElement, useContext } from "react";
|
|
2
|
+
//#region src/email-context.ts
|
|
3
|
+
/**
|
|
4
|
+
* `@shware/workflow/email-context` — the email being rendered, for its components: the resolved
|
|
5
|
+
* metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the
|
|
6
|
+
* template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)
|
|
7
|
+
* and the studio's preview both provide it; the props stay the template's own.
|
|
8
|
+
*
|
|
9
|
+
* const { unsubscribeUrl } = useEmailContext();
|
|
10
|
+
* {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}
|
|
11
|
+
*
|
|
12
|
+
* Browser-safe: React only — no react-dom/server, no Worker bindings.
|
|
13
|
+
*/
|
|
14
|
+
const EmailContext = createContext(void 0);
|
|
15
|
+
function EmailProvider({ email, children }) {
|
|
16
|
+
return createElement(EmailContext.Provider, { value: email }, children);
|
|
17
|
+
}
|
|
18
|
+
/** The email being rendered; throws outside a render the SDK or the studio provides. */
|
|
19
|
+
function useEmailContext() {
|
|
20
|
+
const email = useContext(EmailContext);
|
|
21
|
+
if (email === void 0) throw new Error("useEmailContext() is called outside an email render: render the template with @shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>");
|
|
22
|
+
return email;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { EmailProvider, useEmailContext };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=email-context.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-context.mjs","names":[],"sources":["../src/email-context.ts"],"sourcesContent":["import { type ReactNode, createContext, createElement, useContext } from 'react';\nimport type { ResolvedEmail } from './email';\n\n/**\n * `@shware/workflow/email-context` — the email being rendered, for its components: the resolved\n * metadata (`resolveEmail`) of the template, so a shared layout renders the unsubscribe link the\n * template resolves to instead of a URL of its own. The renderer (`@shware/workflow/react-email`)\n * and the studio's preview both provide it; the props stay the template's own.\n *\n * const { unsubscribeUrl } = useEmailContext();\n * {unsubscribeUrl !== undefined && <Link href={unsubscribeUrl}>Unsubscribe</Link>}\n *\n * Browser-safe: React only — no react-dom/server, no Worker bindings.\n */\n\nconst EmailContext = createContext<ResolvedEmail | undefined>(undefined);\n\nexport function EmailProvider({\n email,\n children,\n}: {\n email: ResolvedEmail;\n children?: ReactNode;\n}): ReactNode {\n return createElement(EmailContext.Provider, { value: email }, children);\n}\n\n/** The email being rendered; throws outside a render the SDK or the studio provides. */\nexport function useEmailContext(): ResolvedEmail {\n const email = useContext(EmailContext);\n if (email === undefined) {\n throw new Error(\n 'useEmailContext() is called outside an email render: render the template with ' +\n \"@shware/workflow/react-email's registryRenderer, or wrap it in <EmailProvider email={resolveEmail(template, config.email)}>\"\n );\n }\n return email;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,eAAe,cAAyC,KAAA,CAAS;AAEvE,SAAgB,cAAc,EAC5B,OACA,YAIY;CACZ,OAAO,cAAc,aAAa,UAAU,EAAE,OAAO,MAAM,GAAG,QAAQ;AACxE;;AAGA,SAAgB,kBAAiC;CAC/C,MAAM,QAAQ,WAAW,YAAY;CACrC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,2MAEF;CAEF,OAAO;AACT"}
|
package/dist/email.cjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/email.ts
|
|
3
|
+
/** `Name <email>`, or the bare address. */
|
|
4
|
+
function formatEmailAddress(address) {
|
|
5
|
+
if (typeof address === "string") return address;
|
|
6
|
+
return address.name === void 0 ? address.email : `${address.name} <${address.email}>`;
|
|
7
|
+
}
|
|
8
|
+
/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
|
|
9
|
+
function resolveEmail(template, config) {
|
|
10
|
+
const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;
|
|
11
|
+
return {
|
|
12
|
+
from: template.from,
|
|
13
|
+
...template.replyTo === void 0 ? {} : { replyTo: template.replyTo },
|
|
14
|
+
...unsubscribeUrl === void 0 ? {} : { unsubscribeUrl }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
|
|
18
|
+
/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
|
|
19
|
+
function assertEmailAddress(address, where) {
|
|
20
|
+
const email = typeof address === "object" && address !== null ? address.email : address;
|
|
21
|
+
const name = typeof address === "object" && address !== null ? address.name : void 0;
|
|
22
|
+
if (typeof email !== "string" || !ADDRESS.test(email)) throw new TypeError(`${where} must be an email address or { email, name? }`);
|
|
23
|
+
if (name !== void 0 && typeof name !== "string") throw new TypeError(`${where}.name must be a string`);
|
|
24
|
+
}
|
|
25
|
+
/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
|
|
26
|
+
function assertUnsubscribeUrl(url, where) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = typeof url === "string" ? new URL(url) : void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
parsed = void 0;
|
|
32
|
+
}
|
|
33
|
+
if (parsed === void 0 || parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);
|
|
34
|
+
}
|
|
35
|
+
/** The config's `email` and every registered template's metadata; throws on the first invalid value. */
|
|
36
|
+
function assertEmailConfig(config, templates) {
|
|
37
|
+
if (Object.hasOwn(config, "emails")) throw new TypeError("the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, and the rest moved to `email: { unsubscribeUrl, tracking }`");
|
|
38
|
+
const email = config.email;
|
|
39
|
+
if (email !== void 0) {
|
|
40
|
+
if (email.unsubscribeUrl !== void 0) assertUnsubscribeUrl(email.unsubscribeUrl, "email.unsubscribeUrl");
|
|
41
|
+
const quick = email.tracking?.quickClickMs;
|
|
42
|
+
if (quick !== void 0 && !(typeof quick === "number" && Number.isFinite(quick) && quick >= 0)) throw new TypeError("email.tracking.quickClickMs must be a non-negative number");
|
|
43
|
+
}
|
|
44
|
+
for (const template of templates) assertEmailMetadata(template, `email template '${template.id}'`);
|
|
45
|
+
}
|
|
46
|
+
/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
|
|
47
|
+
function assertEmailMetadata(template, where) {
|
|
48
|
+
assertEmailAddress(template.from, `${where}: from`);
|
|
49
|
+
if (template.replyTo !== void 0) assertEmailAddress(template.replyTo, `${where}: replyTo`);
|
|
50
|
+
if (template.unsubscribeUrl !== void 0) assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);
|
|
51
|
+
}
|
|
52
|
+
/** The unsubscribe link may carry its own query (a token, a list): the path names it. */
|
|
53
|
+
function isUnsubscribeLink(url, unsubscribeUrl) {
|
|
54
|
+
if (unsubscribeUrl === void 0) return false;
|
|
55
|
+
try {
|
|
56
|
+
const a = new URL(url);
|
|
57
|
+
const b = new URL(unsubscribeUrl);
|
|
58
|
+
return a.origin === b.origin && a.pathname === b.pathname;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
exports.assertEmailAddress = assertEmailAddress;
|
|
65
|
+
exports.assertEmailConfig = assertEmailConfig;
|
|
66
|
+
exports.assertEmailMetadata = assertEmailMetadata;
|
|
67
|
+
exports.assertUnsubscribeUrl = assertUnsubscribeUrl;
|
|
68
|
+
exports.formatEmailAddress = formatEmailAddress;
|
|
69
|
+
exports.isUnsubscribeLink = isUnsubscribeLink;
|
|
70
|
+
exports.resolveEmail = resolveEmail;
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=email.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.cjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["import type { EmailTemplate } from './templates';\n\n/**\n * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link\n * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should\n * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's\n * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,\n * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser\n * can import it.\n */\n\n/** An address as the transports take it: bare, or with a display name. */\nexport type EmailAddress = string | { email: string; name?: string };\n\n/** `Name <email>`, or the bare address. */\nexport function formatEmailAddress(address: EmailAddress): string {\n if (typeof address === 'string') return address;\n return address.name === undefined ? address.email : `${address.name} <${address.email}>`;\n}\n\n/** The config's `email`: what every email template shares. */\nexport interface EmailConfig {\n /**\n * The unsubscribe link of every email template that names none of its own. The layout reads it\n * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a\n * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.\n */\n unsubscribeUrl?: string;\n /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */\n tracking?: {\n /**\n * An open or click this many milliseconds after delivery is a weak machine signal — machine\n * only together with another (a second link within seconds); 1000 by default. A person\n * tapping a mail notification can click within a few seconds, so keep it short.\n */\n quickClickMs?: number;\n };\n}\n\n/** One template's email metadata, resolved against the config. */\nexport interface ResolvedEmail {\n from: EmailAddress;\n /** Absent when the template names none: the email goes out without a Reply-To. */\n replyTo?: EmailAddress;\n /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */\n unsubscribeUrl?: string;\n}\n\n/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */\nexport function resolveEmail(\n template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>,\n config?: EmailConfig\n): ResolvedEmail {\n const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;\n return {\n from: template.from,\n ...(template.replyTo === undefined ? {} : { replyTo: template.replyTo }),\n ...(unsubscribeUrl === undefined ? {} : { unsubscribeUrl }),\n };\n}\n\nconst ADDRESS = /^[^\\s@<>]+@[^\\s@<>]+$/;\n\n/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */\nexport function assertEmailAddress(address: unknown, where: string): void {\n const email =\n typeof address === 'object' && address !== null\n ? (address as { email?: unknown }).email\n : address;\n const name =\n typeof address === 'object' && address !== null\n ? (address as { name?: unknown }).name\n : undefined;\n if (typeof email !== 'string' || !ADDRESS.test(email)) {\n throw new TypeError(`${where} must be an email address or { email, name? }`);\n }\n if (name !== undefined && typeof name !== 'string') {\n throw new TypeError(`${where}.name must be a string`);\n }\n}\n\n/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */\nexport function assertUnsubscribeUrl(url: unknown, where: string): void {\n let parsed: URL | undefined;\n try {\n parsed = typeof url === 'string' ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')) {\n throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);\n }\n}\n\n/** The config's `email` and every registered template's metadata; throws on the first invalid value. */\nexport function assertEmailConfig(\n config: { email?: EmailConfig },\n templates: Iterable<EmailTemplate>\n): void {\n if (Object.hasOwn(config, 'emails')) {\n throw new TypeError(\n \"the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, \" +\n 'and the rest moved to `email: { unsubscribeUrl, tracking }`'\n );\n }\n const email = config.email;\n if (email !== undefined) {\n if (email.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(email.unsubscribeUrl, 'email.unsubscribeUrl');\n }\n const quick = email.tracking?.quickClickMs;\n if (\n quick !== undefined &&\n !(typeof quick === 'number' && Number.isFinite(quick) && quick >= 0)\n ) {\n throw new TypeError('email.tracking.quickClickMs must be a non-negative number');\n }\n }\n for (const template of templates)\n assertEmailMetadata(template, `email template '${template.id}'`);\n}\n\n/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */\nexport function assertEmailMetadata(\n template: { from?: unknown; replyTo?: unknown; unsubscribeUrl?: unknown },\n where: string\n): void {\n assertEmailAddress(template.from, `${where}: from`);\n if (template.replyTo !== undefined) assertEmailAddress(template.replyTo, `${where}: replyTo`);\n if (template.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);\n }\n}\n\n/** The unsubscribe link may carry its own query (a token, a list): the path names it. */\nexport function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean {\n if (unsubscribeUrl === undefined) return false;\n try {\n const a = new URL(url);\n const b = new URL(unsubscribeUrl);\n return a.origin === b.origin && a.pathname === b.pathname;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;AAeA,SAAgB,mBAAmB,SAA+B;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QAAQ,SAAS,KAAA,IAAY,QAAQ,QAAQ,GAAG,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACxF;;AA+BA,SAAgB,aACd,UACA,QACe;CACf,MAAM,iBAAiB,SAAS,kBAAkB,QAAQ;CAC1D,OAAO;EACL,MAAM,SAAS;EACf,GAAI,SAAS,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,SAAS,QAAQ;EACtE,GAAI,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe;CAC3D;AACF;AAEA,MAAM,UAAU;;AAGhB,SAAgB,mBAAmB,SAAkB,OAAqB;CACxE,MAAM,QACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAAgC,QACjC;CACN,MAAM,OACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAA+B,OAChC,KAAA;CACN,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,KAAK,GAClD,MAAM,IAAI,UAAU,GAAG,MAAM,8CAA8C;CAE7E,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UACxC,MAAM,IAAI,UAAU,GAAG,MAAM,uBAAuB;AAExD;;AAGA,SAAgB,qBAAqB,KAAc,OAAqB;CACtE,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,QAAQ,WAAW,IAAI,IAAI,GAAG,IAAI,KAAA;CACpD,QAAQ;EACN,SAAS,KAAA;CACX;CACA,IAAI,WAAW,KAAA,KAAc,OAAO,aAAa,YAAY,OAAO,aAAa,SAC/E,MAAM,IAAI,UAAU,GAAG,MAAM,gDAAgD;AAEjF;;AAGA,SAAgB,kBACd,QACA,WACM;CACN,IAAI,OAAO,OAAO,QAAQ,QAAQ,GAChC,MAAM,IAAI,UACR,qJAEF;CAEF,MAAM,QAAQ,OAAO;CACrB,IAAI,UAAU,KAAA,GAAW;EACvB,IAAI,MAAM,mBAAmB,KAAA,GAC3B,qBAAqB,MAAM,gBAAgB,sBAAsB;EAEnE,MAAM,QAAQ,MAAM,UAAU;EAC9B,IACE,UAAU,KAAA,KACV,EAAE,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,IAElE,MAAM,IAAI,UAAU,2DAA2D;CAEnF;CACA,KAAK,MAAM,YAAY,WACrB,oBAAoB,UAAU,mBAAmB,SAAS,GAAG,EAAE;AACnE;;AAGA,SAAgB,oBACd,UACA,OACM;CACN,mBAAmB,SAAS,MAAM,GAAG,MAAM,OAAO;CAClD,IAAI,SAAS,YAAY,KAAA,GAAW,mBAAmB,SAAS,SAAS,GAAG,MAAM,UAAU;CAC5F,IAAI,SAAS,mBAAmB,KAAA,GAC9B,qBAAqB,SAAS,gBAAgB,GAAG,MAAM,iBAAiB;AAE5E;;AAGA,SAAgB,kBAAkB,KAAa,gBAA6C;CAC1F,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,GAAG;EACrB,MAAM,IAAI,IAAI,IAAI,cAAc;EAChC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE;CACnD,QAAQ;EACN,OAAO;CACT;AACF"}
|
package/dist/email.d.cts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EmailTemplate } from "./templates.cjs";
|
|
2
|
+
//#region src/email.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Email metadata: who a template's email comes from, where replies go, and the unsubscribe link
|
|
5
|
+
* its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should
|
|
6
|
+
* go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's
|
|
7
|
+
* `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,
|
|
8
|
+
* the Worker's preview and the studio all go through it. No React and no I/O here, so a browser
|
|
9
|
+
* can import it.
|
|
10
|
+
*/
|
|
11
|
+
/** An address as the transports take it: bare, or with a display name. */
|
|
12
|
+
export type EmailAddress = string | {
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
/** `Name <email>`, or the bare address. */
|
|
17
|
+
export declare function formatEmailAddress(address: EmailAddress): string;
|
|
18
|
+
/** The config's `email`: what every email template shares. */
|
|
19
|
+
export interface EmailConfig {
|
|
20
|
+
/**
|
|
21
|
+
* The unsubscribe link of every email template that names none of its own. The layout reads it
|
|
22
|
+
* with `useEmailContext()`; sent, it is tracked like every link, but following it shows a
|
|
23
|
+
* confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.
|
|
24
|
+
*/
|
|
25
|
+
unsubscribeUrl?: string;
|
|
26
|
+
/** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */
|
|
27
|
+
tracking?: {
|
|
28
|
+
/**
|
|
29
|
+
* An open or click this many milliseconds after delivery is a weak machine signal — machine
|
|
30
|
+
* only together with another (a second link within seconds); 1000 by default. A person
|
|
31
|
+
* tapping a mail notification can click within a few seconds, so keep it short.
|
|
32
|
+
*/
|
|
33
|
+
quickClickMs?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** One template's email metadata, resolved against the config. */
|
|
37
|
+
export interface ResolvedEmail {
|
|
38
|
+
from: EmailAddress;
|
|
39
|
+
/** Absent when the template names none: the email goes out without a Reply-To. */
|
|
40
|
+
replyTo?: EmailAddress;
|
|
41
|
+
/** Absent when neither the template nor the config names one: the email has no unsubscribe link. */
|
|
42
|
+
unsubscribeUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
|
|
45
|
+
export declare function resolveEmail(template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>, config?: EmailConfig): ResolvedEmail;
|
|
46
|
+
/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
|
|
47
|
+
export declare function assertEmailAddress(address: unknown, where: string): void;
|
|
48
|
+
/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
|
|
49
|
+
export declare function assertUnsubscribeUrl(url: unknown, where: string): void;
|
|
50
|
+
/** The config's `email` and every registered template's metadata; throws on the first invalid value. */
|
|
51
|
+
export declare function assertEmailConfig(config: {
|
|
52
|
+
email?: EmailConfig;
|
|
53
|
+
}, templates: Iterable<EmailTemplate>): void;
|
|
54
|
+
/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
|
|
55
|
+
export declare function assertEmailMetadata(template: {
|
|
56
|
+
from?: unknown;
|
|
57
|
+
replyTo?: unknown;
|
|
58
|
+
unsubscribeUrl?: unknown;
|
|
59
|
+
}, where: string): void;
|
|
60
|
+
/** The unsubscribe link may carry its own query (a token, a list): the path names it. */
|
|
61
|
+
export declare function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean;
|
|
62
|
+
//#endregion
|
|
63
|
+
//# sourceMappingURL=email.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.cts","names":[],"sources":["../src/email.ts"],"mappings":";;;;;;;;;;;YAYY;EAA0B;EAAe;;;wBAGrC,mBAAmB,SAAS;;iBAM3B;;;;;;EAMf;;EAEA;;;;;;IAME;;;;iBAKa;EACf,MAAM;;EAEN,UAAU;;EAEV;;;wBAIc,aACd,UAAU,KAAK,uDACf,SAAS,cACR;;wBAYa,mBAAmB,kBAAkB;;wBAkBrC,qBAAqB,cAAc;;wBAanC,kBACd;EAAU,QAAQ;GAClB,WAAW,SAAS;;wBA0BN,oBACd;EAAY;EAAgB;EAAmB;GAC/C;;wBAUc,kBAAkB,aAAa"}
|
package/dist/email.d.mts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EmailTemplate } from "./templates.mjs";
|
|
2
|
+
//#region src/email.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Email metadata: who a template's email comes from, where replies go, and the unsubscribe link
|
|
5
|
+
* its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should
|
|
6
|
+
* go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's
|
|
7
|
+
* `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,
|
|
8
|
+
* the Worker's preview and the studio all go through it. No React and no I/O here, so a browser
|
|
9
|
+
* can import it.
|
|
10
|
+
*/
|
|
11
|
+
/** An address as the transports take it: bare, or with a display name. */
|
|
12
|
+
export type EmailAddress = string | {
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
/** `Name <email>`, or the bare address. */
|
|
17
|
+
export declare function formatEmailAddress(address: EmailAddress): string;
|
|
18
|
+
/** The config's `email`: what every email template shares. */
|
|
19
|
+
export interface EmailConfig {
|
|
20
|
+
/**
|
|
21
|
+
* The unsubscribe link of every email template that names none of its own. The layout reads it
|
|
22
|
+
* with `useEmailContext()`; sent, it is tracked like every link, but following it shows a
|
|
23
|
+
* confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.
|
|
24
|
+
*/
|
|
25
|
+
unsubscribeUrl?: string;
|
|
26
|
+
/** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */
|
|
27
|
+
tracking?: {
|
|
28
|
+
/**
|
|
29
|
+
* An open or click this many milliseconds after delivery is a weak machine signal — machine
|
|
30
|
+
* only together with another (a second link within seconds); 1000 by default. A person
|
|
31
|
+
* tapping a mail notification can click within a few seconds, so keep it short.
|
|
32
|
+
*/
|
|
33
|
+
quickClickMs?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** One template's email metadata, resolved against the config. */
|
|
37
|
+
export interface ResolvedEmail {
|
|
38
|
+
from: EmailAddress;
|
|
39
|
+
/** Absent when the template names none: the email goes out without a Reply-To. */
|
|
40
|
+
replyTo?: EmailAddress;
|
|
41
|
+
/** Absent when neither the template nor the config names one: the email has no unsubscribe link. */
|
|
42
|
+
unsubscribeUrl?: string;
|
|
43
|
+
}
|
|
44
|
+
/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
|
|
45
|
+
export declare function resolveEmail(template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>, config?: EmailConfig): ResolvedEmail;
|
|
46
|
+
/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
|
|
47
|
+
export declare function assertEmailAddress(address: unknown, where: string): void;
|
|
48
|
+
/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
|
|
49
|
+
export declare function assertUnsubscribeUrl(url: unknown, where: string): void;
|
|
50
|
+
/** The config's `email` and every registered template's metadata; throws on the first invalid value. */
|
|
51
|
+
export declare function assertEmailConfig(config: {
|
|
52
|
+
email?: EmailConfig;
|
|
53
|
+
}, templates: Iterable<EmailTemplate>): void;
|
|
54
|
+
/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
|
|
55
|
+
export declare function assertEmailMetadata(template: {
|
|
56
|
+
from?: unknown;
|
|
57
|
+
replyTo?: unknown;
|
|
58
|
+
unsubscribeUrl?: unknown;
|
|
59
|
+
}, where: string): void;
|
|
60
|
+
/** The unsubscribe link may carry its own query (a token, a list): the path names it. */
|
|
61
|
+
export declare function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean;
|
|
62
|
+
//#endregion
|
|
63
|
+
//# sourceMappingURL=email.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.mts","names":[],"sources":["../src/email.ts"],"mappings":";;;;;;;;;;;YAYY;EAA0B;EAAe;;;wBAGrC,mBAAmB,SAAS;;iBAM3B;;;;;;EAMf;;EAEA;;;;;;IAME;;;;iBAKa;EACf,MAAM;;EAEN,UAAU;;EAEV;;;wBAIc,aACd,UAAU,KAAK,uDACf,SAAS,cACR;;wBAYa,mBAAmB,kBAAkB;;wBAkBrC,qBAAqB,cAAc;;wBAanC,kBACd;EAAU,QAAQ;GAClB,WAAW,SAAS;;wBA0BN,oBACd;EAAY;EAAgB;EAAmB;GAC/C;;wBAUc,kBAAkB,aAAa"}
|
package/dist/email.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/email.ts
|
|
2
|
+
/** `Name <email>`, or the bare address. */
|
|
3
|
+
function formatEmailAddress(address) {
|
|
4
|
+
if (typeof address === "string") return address;
|
|
5
|
+
return address.name === void 0 ? address.email : `${address.name} <${address.email}>`;
|
|
6
|
+
}
|
|
7
|
+
/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */
|
|
8
|
+
function resolveEmail(template, config) {
|
|
9
|
+
const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;
|
|
10
|
+
return {
|
|
11
|
+
from: template.from,
|
|
12
|
+
...template.replyTo === void 0 ? {} : { replyTo: template.replyTo },
|
|
13
|
+
...unsubscribeUrl === void 0 ? {} : { unsubscribeUrl }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
|
|
17
|
+
/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */
|
|
18
|
+
function assertEmailAddress(address, where) {
|
|
19
|
+
const email = typeof address === "object" && address !== null ? address.email : address;
|
|
20
|
+
const name = typeof address === "object" && address !== null ? address.name : void 0;
|
|
21
|
+
if (typeof email !== "string" || !ADDRESS.test(email)) throw new TypeError(`${where} must be an email address or { email, name? }`);
|
|
22
|
+
if (name !== void 0 && typeof name !== "string") throw new TypeError(`${where}.name must be a string`);
|
|
23
|
+
}
|
|
24
|
+
/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */
|
|
25
|
+
function assertUnsubscribeUrl(url, where) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = typeof url === "string" ? new URL(url) : void 0;
|
|
29
|
+
} catch {
|
|
30
|
+
parsed = void 0;
|
|
31
|
+
}
|
|
32
|
+
if (parsed === void 0 || parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);
|
|
33
|
+
}
|
|
34
|
+
/** The config's `email` and every registered template's metadata; throws on the first invalid value. */
|
|
35
|
+
function assertEmailConfig(config, templates) {
|
|
36
|
+
if (Object.hasOwn(config, "emails")) throw new TypeError("the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, and the rest moved to `email: { unsubscribeUrl, tracking }`");
|
|
37
|
+
const email = config.email;
|
|
38
|
+
if (email !== void 0) {
|
|
39
|
+
if (email.unsubscribeUrl !== void 0) assertUnsubscribeUrl(email.unsubscribeUrl, "email.unsubscribeUrl");
|
|
40
|
+
const quick = email.tracking?.quickClickMs;
|
|
41
|
+
if (quick !== void 0 && !(typeof quick === "number" && Number.isFinite(quick) && quick >= 0)) throw new TypeError("email.tracking.quickClickMs must be a non-negative number");
|
|
42
|
+
}
|
|
43
|
+
for (const template of templates) assertEmailMetadata(template, `email template '${template.id}'`);
|
|
44
|
+
}
|
|
45
|
+
/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */
|
|
46
|
+
function assertEmailMetadata(template, where) {
|
|
47
|
+
assertEmailAddress(template.from, `${where}: from`);
|
|
48
|
+
if (template.replyTo !== void 0) assertEmailAddress(template.replyTo, `${where}: replyTo`);
|
|
49
|
+
if (template.unsubscribeUrl !== void 0) assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);
|
|
50
|
+
}
|
|
51
|
+
/** The unsubscribe link may carry its own query (a token, a list): the path names it. */
|
|
52
|
+
function isUnsubscribeLink(url, unsubscribeUrl) {
|
|
53
|
+
if (unsubscribeUrl === void 0) return false;
|
|
54
|
+
try {
|
|
55
|
+
const a = new URL(url);
|
|
56
|
+
const b = new URL(unsubscribeUrl);
|
|
57
|
+
return a.origin === b.origin && a.pathname === b.pathname;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=email.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.mjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["import type { EmailTemplate } from './templates';\n\n/**\n * Email metadata: who a template's email comes from, where replies go, and the unsubscribe link\n * its layout carries. Each `emailTemplate` declares its own `from` (and `replyTo`, if replies should\n * go elsewhere); the unsubscribe link is the template's `unsubscribeUrl`, else the config's\n * `email.unsubscribeUrl`, else none. `resolveEmail` is that rule, and the sender, the renderer,\n * the Worker's preview and the studio all go through it. No React and no I/O here, so a browser\n * can import it.\n */\n\n/** An address as the transports take it: bare, or with a display name. */\nexport type EmailAddress = string | { email: string; name?: string };\n\n/** `Name <email>`, or the bare address. */\nexport function formatEmailAddress(address: EmailAddress): string {\n if (typeof address === 'string') return address;\n return address.name === undefined ? address.email : `${address.name} <${address.email}>`;\n}\n\n/** The config's `email`: what every email template shares. */\nexport interface EmailConfig {\n /**\n * The unsubscribe link of every email template that names none of its own. The layout reads it\n * with `useEmailContext()`; sent, it is tracked like every link, but following it shows a\n * confirmation page, and only the confirmation (a POST no link scanner sends) unsubscribes.\n */\n unsubscribeUrl?: string;\n /** How the Worker tells a person's opens and clicks from a machine's (see the reporting classifier). */\n tracking?: {\n /**\n * An open or click this many milliseconds after delivery is a weak machine signal — machine\n * only together with another (a second link within seconds); 1000 by default. A person\n * tapping a mail notification can click within a few seconds, so keep it short.\n */\n quickClickMs?: number;\n };\n}\n\n/** One template's email metadata, resolved against the config. */\nexport interface ResolvedEmail {\n from: EmailAddress;\n /** Absent when the template names none: the email goes out without a Reply-To. */\n replyTo?: EmailAddress;\n /** Absent when neither the template nor the config names one: the email has no unsubscribe link. */\n unsubscribeUrl?: string;\n}\n\n/** The template's metadata, its `unsubscribeUrl` falling back to the config's. Leaves both untouched. */\nexport function resolveEmail(\n template: Pick<EmailTemplate, 'from' | 'replyTo' | 'unsubscribeUrl'>,\n config?: EmailConfig\n): ResolvedEmail {\n const unsubscribeUrl = template.unsubscribeUrl ?? config?.unsubscribeUrl;\n return {\n from: template.from,\n ...(template.replyTo === undefined ? {} : { replyTo: template.replyTo }),\n ...(unsubscribeUrl === undefined ? {} : { unsubscribeUrl }),\n };\n}\n\nconst ADDRESS = /^[^\\s@<>]+@[^\\s@<>]+$/;\n\n/** Throws naming `where` when `address` is neither `user@host` nor `{ email: 'user@host', name? }`. */\nexport function assertEmailAddress(address: unknown, where: string): void {\n const email =\n typeof address === 'object' && address !== null\n ? (address as { email?: unknown }).email\n : address;\n const name =\n typeof address === 'object' && address !== null\n ? (address as { name?: unknown }).name\n : undefined;\n if (typeof email !== 'string' || !ADDRESS.test(email)) {\n throw new TypeError(`${where} must be an email address or { email, name? }`);\n }\n if (name !== undefined && typeof name !== 'string') {\n throw new TypeError(`${where}.name must be a string`);\n }\n}\n\n/** Throws naming `where` unless `url` is an absolute http(s) URL; '' and null are not a way to leave it out. */\nexport function assertUnsubscribeUrl(url: unknown, where: string): void {\n let parsed: URL | undefined;\n try {\n parsed = typeof url === 'string' ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')) {\n throw new TypeError(`${where} must be an http(s) URL (leave it out for none)`);\n }\n}\n\n/** The config's `email` and every registered template's metadata; throws on the first invalid value. */\nexport function assertEmailConfig(\n config: { email?: EmailConfig },\n templates: Iterable<EmailTemplate>\n): void {\n if (Object.hasOwn(config, 'emails')) {\n throw new TypeError(\n \"the config's `emails` is gone: `from` and `replyTo` are declared on each emailTemplate, \" +\n 'and the rest moved to `email: { unsubscribeUrl, tracking }`'\n );\n }\n const email = config.email;\n if (email !== undefined) {\n if (email.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(email.unsubscribeUrl, 'email.unsubscribeUrl');\n }\n const quick = email.tracking?.quickClickMs;\n if (\n quick !== undefined &&\n !(typeof quick === 'number' && Number.isFinite(quick) && quick >= 0)\n ) {\n throw new TypeError('email.tracking.quickClickMs must be a non-negative number');\n }\n }\n for (const template of templates)\n assertEmailMetadata(template, `email template '${template.id}'`);\n}\n\n/** A template's own `from` / `replyTo` / `unsubscribeUrl`, as `emailTemplate` checks them. */\nexport function assertEmailMetadata(\n template: { from?: unknown; replyTo?: unknown; unsubscribeUrl?: unknown },\n where: string\n): void {\n assertEmailAddress(template.from, `${where}: from`);\n if (template.replyTo !== undefined) assertEmailAddress(template.replyTo, `${where}: replyTo`);\n if (template.unsubscribeUrl !== undefined) {\n assertUnsubscribeUrl(template.unsubscribeUrl, `${where}: unsubscribeUrl`);\n }\n}\n\n/** The unsubscribe link may carry its own query (a token, a list): the path names it. */\nexport function isUnsubscribeLink(url: string, unsubscribeUrl: string | undefined): boolean {\n if (unsubscribeUrl === undefined) return false;\n try {\n const a = new URL(url);\n const b = new URL(unsubscribeUrl);\n return a.origin === b.origin && a.pathname === b.pathname;\n } catch {\n return false;\n }\n}\n"],"mappings":";;AAeA,SAAgB,mBAAmB,SAA+B;CAChE,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,OAAO,QAAQ,SAAS,KAAA,IAAY,QAAQ,QAAQ,GAAG,QAAQ,KAAK,IAAI,QAAQ,MAAM;AACxF;;AA+BA,SAAgB,aACd,UACA,QACe;CACf,MAAM,iBAAiB,SAAS,kBAAkB,QAAQ;CAC1D,OAAO;EACL,MAAM,SAAS;EACf,GAAI,SAAS,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,SAAS,QAAQ;EACtE,GAAI,mBAAmB,KAAA,IAAY,CAAC,IAAI,EAAE,eAAe;CAC3D;AACF;AAEA,MAAM,UAAU;;AAGhB,SAAgB,mBAAmB,SAAkB,OAAqB;CACxE,MAAM,QACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAAgC,QACjC;CACN,MAAM,OACJ,OAAO,YAAY,YAAY,YAAY,OACtC,QAA+B,OAChC,KAAA;CACN,IAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,KAAK,GAClD,MAAM,IAAI,UAAU,GAAG,MAAM,8CAA8C;CAE7E,IAAI,SAAS,KAAA,KAAa,OAAO,SAAS,UACxC,MAAM,IAAI,UAAU,GAAG,MAAM,uBAAuB;AAExD;;AAGA,SAAgB,qBAAqB,KAAc,OAAqB;CACtE,IAAI;CACJ,IAAI;EACF,SAAS,OAAO,QAAQ,WAAW,IAAI,IAAI,GAAG,IAAI,KAAA;CACpD,QAAQ;EACN,SAAS,KAAA;CACX;CACA,IAAI,WAAW,KAAA,KAAc,OAAO,aAAa,YAAY,OAAO,aAAa,SAC/E,MAAM,IAAI,UAAU,GAAG,MAAM,gDAAgD;AAEjF;;AAGA,SAAgB,kBACd,QACA,WACM;CACN,IAAI,OAAO,OAAO,QAAQ,QAAQ,GAChC,MAAM,IAAI,UACR,qJAEF;CAEF,MAAM,QAAQ,OAAO;CACrB,IAAI,UAAU,KAAA,GAAW;EACvB,IAAI,MAAM,mBAAmB,KAAA,GAC3B,qBAAqB,MAAM,gBAAgB,sBAAsB;EAEnE,MAAM,QAAQ,MAAM,UAAU;EAC9B,IACE,UAAU,KAAA,KACV,EAAE,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,KAAK,SAAS,IAElE,MAAM,IAAI,UAAU,2DAA2D;CAEnF;CACA,KAAK,MAAM,YAAY,WACrB,oBAAoB,UAAU,mBAAmB,SAAS,GAAG,EAAE;AACnE;;AAGA,SAAgB,oBACd,UACA,OACM;CACN,mBAAmB,SAAS,MAAM,GAAG,MAAM,OAAO;CAClD,IAAI,SAAS,YAAY,KAAA,GAAW,mBAAmB,SAAS,SAAS,GAAG,MAAM,UAAU;CAC5F,IAAI,SAAS,mBAAmB,KAAA,GAC9B,qBAAqB,SAAS,gBAAgB,GAAG,MAAM,iBAAiB;AAE5E;;AAGA,SAAgB,kBAAkB,KAAa,gBAA6C;CAC1F,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,GAAG;EACrB,MAAM,IAAI,IAAI,IAAI,cAAc;EAChC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE;CACnD,QAAQ;EACN,OAAO;CACT;AACF"}
|
package/dist/engine/index.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.cjs";
|
|
1
|
+
import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, LinkPurpose, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.cjs";
|
|
2
2
|
import { ActionVersionPolicy, RegisteredAction, RegistryActionInvoker } from "./actions.cjs";
|
|
3
3
|
import { hashToBucket, murmur3 } from "./bucket.cjs";
|
|
4
4
|
import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.cjs";
|
|
5
5
|
import { runJourney } from "./interpreter.cjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.cjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.cjs";
|
|
8
|
-
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
8
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
package/dist/engine/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.mjs";
|
|
1
|
+
import { ActionInvocation, ActionInvoker, EngineStep, EventSink, FactSource, JourneyContext, JourneyObserver, JourneyOutcome, LinkPurpose, MessageSender, MessageTracking, OutboundMessage, PushDevice, SendResult } from "./ports.mjs";
|
|
2
2
|
import { ActionVersionPolicy, RegisteredAction, RegistryActionInvoker } from "./actions.mjs";
|
|
3
3
|
import { hashToBucket, murmur3 } from "./bucket.mjs";
|
|
4
4
|
import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.mjs";
|
|
5
5
|
import { runJourney } from "./interpreter.mjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
|
|
8
|
-
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
8
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|