@shware/workflow 2.0.1 → 3.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 +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
|
@@ -1,59 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { SweepResult } from "./router.cjs";
|
|
5
|
-
import { JourneyRuntimeOptions } from "../runtime.cjs";
|
|
1
|
+
import { JourneyEnv } from "./bindings.cjs";
|
|
2
|
+
import { SweepResult } from "../host/router.cjs";
|
|
3
|
+
import { EnvValidationError, JourneyHostOptions } from "../host/journey-host.cjs";
|
|
6
4
|
import { JourneyRunner } from "./runner.cjs";
|
|
7
|
-
import { JourneyConfig } from "../config.cjs";
|
|
8
|
-
import * as core from "zod/v4/core";
|
|
9
5
|
//#region src/cloudflare/worker.d.ts
|
|
10
6
|
/** The fetch handler's execution context (the subset used). */
|
|
11
7
|
export interface ExecutionContextLike {
|
|
12
8
|
waitUntil(promise: Promise<unknown>): void;
|
|
13
9
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* envelope, actions, and the runtime / APNs / FCM settings. Secrets come
|
|
21
|
-
* from the env under the names `JourneyEnv` documents.
|
|
22
|
-
*/
|
|
23
|
-
config: JourneyConfig;
|
|
24
|
-
/**
|
|
25
|
-
* The data plane, opened per request and per workflow run. Return a
|
|
26
|
-
* `StoreLease` when the client must be released afterwards (a Postgres
|
|
27
|
-
* connection); defaults to D1 + KV from the env's bindings. `database` is
|
|
28
|
-
* the config's (`schema` defaulting to `workflow`), for a Postgres store:
|
|
29
|
-
*
|
|
30
|
-
* store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database)
|
|
31
|
-
*/
|
|
32
|
-
store?: (env: Env, database: {
|
|
33
|
-
schema: string;
|
|
34
|
-
}) => JourneyStore | StoreLease;
|
|
35
|
-
/**
|
|
36
|
-
* Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click
|
|
37
|
-
* links and an open pixel for email, payload data for a push), and how each instance ended.
|
|
38
|
-
* By default a store that keeps reporting (Postgres) records them itself, and the Worker
|
|
39
|
-
* serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report
|
|
40
|
-
* elsewhere.
|
|
41
|
-
*/
|
|
42
|
-
observer?: (env: Env, store: JourneyStore) => JourneyObserver;
|
|
43
|
-
/** Message outlet override (tests, a custom gateway); by default the channels the config describes. */
|
|
44
|
-
messages?: (env: Env, store: JourneyStore) => MessageSender;
|
|
45
|
-
/** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */
|
|
46
|
-
runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;
|
|
47
|
-
/**
|
|
48
|
-
* The host's environment, as a zod schema over its vars and secrets (bindings pass through
|
|
49
|
-
* untouched): checked the first time the Worker runs, so a missing or malformed variable fails
|
|
50
|
-
* there — every problem listed — not at the send that needs it. Webhook templates read their
|
|
51
|
-
* URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.
|
|
52
|
-
*/
|
|
53
|
-
env?: core.$ZodType<object>;
|
|
54
|
-
/** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
|
|
55
|
-
contractMode?: 'enforce' | 'report';
|
|
56
|
-
}
|
|
10
|
+
/**
|
|
11
|
+
* `journeyWorker`'s options: the host's (config, store, email transport, observer, ...). The
|
|
12
|
+
* email transport is the host's choice — `cloudflareEmailTransport({ binding: env.EMAIL })` for
|
|
13
|
+
* Cloudflare Email, `sesEmailTransport(...)` for SES.
|
|
14
|
+
*/
|
|
15
|
+
export type JourneyWorkerOptions<Env extends JourneyEnv> = JourneyHostOptions<Env>;
|
|
57
16
|
export interface JourneyWorker<Env extends JourneyEnv> {
|
|
58
17
|
/** Export this under the `class_name` of the wrangler `workflows` binding. */
|
|
59
18
|
Runner: new (...args: never[]) => JourneyRunner;
|
|
@@ -81,12 +40,7 @@ export interface JourneyWorker<Env extends JourneyEnv> {
|
|
|
81
40
|
* export const MyRunner = journeys.Runner;
|
|
82
41
|
* export default { fetch: journeys.fetch };
|
|
83
42
|
*/
|
|
84
|
-
/** The Worker's environment failed its schema (`journeyWorker({ env })`). */
|
|
85
|
-
export declare class EnvValidationError extends Error {
|
|
86
|
-
readonly issues: readonly string[];
|
|
87
|
-
readonly name = "EnvValidationError";
|
|
88
|
-
constructor(issues: readonly string[]);
|
|
89
|
-
}
|
|
90
43
|
export declare function journeyWorker<Env extends JourneyEnv>(options: JourneyWorkerOptions<Env>): JourneyWorker<Env>;
|
|
91
44
|
//#endregion
|
|
45
|
+
export { EnvValidationError };
|
|
92
46
|
//# sourceMappingURL=worker.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.cts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.d.cts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;iBAciB;EACf,UAAU,SAAS;;;;;;;YAQT,qBAAqB,YAAY,cAAc,mBAAmB;iBAE7D,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;wBAe3D,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
|
|
@@ -1,59 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { SweepResult } from "./router.mjs";
|
|
5
|
-
import { JourneyRuntimeOptions } from "../runtime.mjs";
|
|
1
|
+
import { JourneyEnv } from "./bindings.mjs";
|
|
2
|
+
import { SweepResult } from "../host/router.mjs";
|
|
3
|
+
import { EnvValidationError, JourneyHostOptions } from "../host/journey-host.mjs";
|
|
6
4
|
import { JourneyRunner } from "./runner.mjs";
|
|
7
|
-
import { JourneyConfig } from "../config.mjs";
|
|
8
|
-
import * as core from "zod/v4/core";
|
|
9
5
|
//#region src/cloudflare/worker.d.ts
|
|
10
6
|
/** The fetch handler's execution context (the subset used). */
|
|
11
7
|
export interface ExecutionContextLike {
|
|
12
8
|
waitUntil(promise: Promise<unknown>): void;
|
|
13
9
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* envelope, actions, and the runtime / APNs / FCM settings. Secrets come
|
|
21
|
-
* from the env under the names `JourneyEnv` documents.
|
|
22
|
-
*/
|
|
23
|
-
config: JourneyConfig;
|
|
24
|
-
/**
|
|
25
|
-
* The data plane, opened per request and per workflow run. Return a
|
|
26
|
-
* `StoreLease` when the client must be released afterwards (a Postgres
|
|
27
|
-
* connection); defaults to D1 + KV from the env's bindings. `database` is
|
|
28
|
-
* the config's (`schema` defaulting to `workflow`), for a Postgres store:
|
|
29
|
-
*
|
|
30
|
-
* store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database)
|
|
31
|
-
*/
|
|
32
|
-
store?: (env: Env, database: {
|
|
33
|
-
schema: string;
|
|
34
|
-
}) => JourneyStore | StoreLease;
|
|
35
|
-
/**
|
|
36
|
-
* Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click
|
|
37
|
-
* links and an open pixel for email, payload data for a push), and how each instance ended.
|
|
38
|
-
* By default a store that keeps reporting (Postgres) records them itself, and the Worker
|
|
39
|
-
* serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report
|
|
40
|
-
* elsewhere.
|
|
41
|
-
*/
|
|
42
|
-
observer?: (env: Env, store: JourneyStore) => JourneyObserver;
|
|
43
|
-
/** Message outlet override (tests, a custom gateway); by default the channels the config describes. */
|
|
44
|
-
messages?: (env: Env, store: JourneyStore) => MessageSender;
|
|
45
|
-
/** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */
|
|
46
|
-
runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;
|
|
47
|
-
/**
|
|
48
|
-
* The host's environment, as a zod schema over its vars and secrets (bindings pass through
|
|
49
|
-
* untouched): checked the first time the Worker runs, so a missing or malformed variable fails
|
|
50
|
-
* there — every problem listed — not at the send that needs it. Webhook templates read their
|
|
51
|
-
* URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.
|
|
52
|
-
*/
|
|
53
|
-
env?: core.$ZodType<object>;
|
|
54
|
-
/** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
|
|
55
|
-
contractMode?: 'enforce' | 'report';
|
|
56
|
-
}
|
|
10
|
+
/**
|
|
11
|
+
* `journeyWorker`'s options: the host's (config, store, email transport, observer, ...). The
|
|
12
|
+
* email transport is the host's choice — `cloudflareEmailTransport({ binding: env.EMAIL })` for
|
|
13
|
+
* Cloudflare Email, `sesEmailTransport(...)` for SES.
|
|
14
|
+
*/
|
|
15
|
+
export type JourneyWorkerOptions<Env extends JourneyEnv> = JourneyHostOptions<Env>;
|
|
57
16
|
export interface JourneyWorker<Env extends JourneyEnv> {
|
|
58
17
|
/** Export this under the `class_name` of the wrangler `workflows` binding. */
|
|
59
18
|
Runner: new (...args: never[]) => JourneyRunner;
|
|
@@ -81,12 +40,7 @@ export interface JourneyWorker<Env extends JourneyEnv> {
|
|
|
81
40
|
* export const MyRunner = journeys.Runner;
|
|
82
41
|
* export default { fetch: journeys.fetch };
|
|
83
42
|
*/
|
|
84
|
-
/** The Worker's environment failed its schema (`journeyWorker({ env })`). */
|
|
85
|
-
export declare class EnvValidationError extends Error {
|
|
86
|
-
readonly issues: readonly string[];
|
|
87
|
-
readonly name = "EnvValidationError";
|
|
88
|
-
constructor(issues: readonly string[]);
|
|
89
|
-
}
|
|
90
43
|
export declare function journeyWorker<Env extends JourneyEnv>(options: JourneyWorkerOptions<Env>): JourneyWorker<Env>;
|
|
91
44
|
//#endregion
|
|
45
|
+
export { EnvValidationError };
|
|
92
46
|
//# sourceMappingURL=worker.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.mts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"worker.d.mts","names":[],"sources":["../../src/cloudflare/worker.ts"],"mappings":";;;;;;iBAciB;EACf,UAAU,SAAS;;;;;;;YAQT,qBAAqB,YAAY,cAAc,mBAAmB;iBAE7D,cAAc,YAAY;;EAEzC,gBAAgB,kBAAkB;;EAElC,QAAQ,SAAS,SAAS,KAAK,KAAK,MAAM,yBAAyB,QAAQ;;;;;;;EAO3E,QAAQ,KAAK,QAAQ,QAAQ;;EAE7B,YAAY,qBAAqB,KAAK,KAAK,KAAK,yBAAyB;;;;;;;;;;;;;;wBAe3D,cAAc,YAAY,YACxC,SAAS,qBAAqB,OAC7B,cAAc"}
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { TemplateValidationError, indexTemplates, registryValidator } from "../templates.mjs";
|
|
3
|
-
import { compileConfig } from "../config.mjs";
|
|
4
|
-
import { ContractValidationError, checkEvent } from "../contract.mjs";
|
|
5
|
-
import "../store/tables.mjs";
|
|
6
|
-
import { registryRenderer } from "../react-email.mjs";
|
|
7
|
-
import { handleTracking } from "../reporting/tracking.mjs";
|
|
8
|
-
import { SchemaMismatchError } from "../store/index.mjs";
|
|
9
|
-
import { d1Store } from "./d1-env.mjs";
|
|
10
|
-
import { deployBundle, handleRequest, ingestEvent, sweepSegments } from "./router.mjs";
|
|
11
|
-
import { channelsFromConfig, runtimeFromConfig } from "./channels.mjs";
|
|
12
|
-
import { scaleSegmentWindows } from "../engine/time-scale.mjs";
|
|
1
|
+
import { EnvValidationError, createJourneyHost } from "../host/journey-host.mjs";
|
|
13
2
|
import { JourneyRunner } from "./runner.mjs";
|
|
14
|
-
import { reportingObserver } from "../reporting/observer.mjs";
|
|
15
|
-
import * as z from "zod/mini";
|
|
16
3
|
//#region src/cloudflare/worker.ts
|
|
17
4
|
/**
|
|
18
5
|
* A complete journey host in one call: the runner class with the app's store,
|
|
@@ -26,227 +13,61 @@ import * as z from "zod/mini";
|
|
|
26
13
|
* export const MyRunner = journeys.Runner;
|
|
27
14
|
* export default { fetch: journeys.fetch };
|
|
28
15
|
*/
|
|
29
|
-
/** The Worker's environment failed its schema (`journeyWorker({ env })`). */
|
|
30
|
-
var EnvValidationError = class extends Error {
|
|
31
|
-
issues;
|
|
32
|
-
name = "EnvValidationError";
|
|
33
|
-
constructor(issues) {
|
|
34
|
-
super(`the Worker's environment is invalid:\n ${issues.join("\n ")}`);
|
|
35
|
-
this.issues = issues;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
16
|
function journeyWorker(options) {
|
|
39
|
-
const
|
|
40
|
-
const emailIds = indexTemplates("email", config.templates?.email);
|
|
41
|
-
assertEmailConfig(config, emailIds.values());
|
|
42
|
-
const checked = /* @__PURE__ */ new WeakMap();
|
|
43
|
-
const checkEnv = (env) => {
|
|
44
|
-
if (options.env === void 0) return env;
|
|
45
|
-
const cached = checked.get(env);
|
|
46
|
-
if (cached !== void 0) return cached;
|
|
47
|
-
const result = z.safeParse(options.env, env);
|
|
48
|
-
if (!result.success) throw new EnvValidationError(result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`));
|
|
49
|
-
const merged = {
|
|
50
|
-
...env,
|
|
51
|
-
...result.data
|
|
52
|
-
};
|
|
53
|
-
checked.set(env, merged);
|
|
54
|
-
return merged;
|
|
55
|
-
};
|
|
56
|
-
const bundle = () => compileConfig(config);
|
|
57
|
-
const emails = config.templates?.email;
|
|
58
|
-
const database = { schema: config.database?.schema ?? "workflow" };
|
|
59
|
-
const lease = (env) => {
|
|
60
|
-
const opened = options.store === void 0 ? d1Store(env) : options.store(env, database);
|
|
61
|
-
const noop = async () => {};
|
|
62
|
-
const factor = runtimeFromConfig(config, env).timeScale;
|
|
63
|
-
return "store" in opened ? {
|
|
64
|
-
store: scaleSegmentWindows(opened.store, factor),
|
|
65
|
-
close: opened.close ?? noop
|
|
66
|
-
} : {
|
|
67
|
-
store: scaleSegmentWindows(opened, factor),
|
|
68
|
-
close: noop
|
|
69
|
-
};
|
|
70
|
-
};
|
|
17
|
+
const host = createJourneyHost(options);
|
|
71
18
|
class Runner extends JourneyRunner {
|
|
72
19
|
leases = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
current;
|
|
74
20
|
createStore() {
|
|
75
|
-
const { store, close } = lease(checkEnv(this.env));
|
|
21
|
+
const { store, close } = host.lease(host.checkEnv(this.env));
|
|
76
22
|
this.leases.set(store, close);
|
|
77
|
-
this.current = store;
|
|
78
23
|
return store;
|
|
79
24
|
}
|
|
80
25
|
async closeStore(store) {
|
|
81
26
|
await this.leases.get(store)?.();
|
|
82
27
|
}
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
createMessageValidator() {
|
|
87
|
-
return registryValidator(config.templates ?? {});
|
|
88
|
-
}
|
|
89
|
-
createMessageSender() {
|
|
90
|
-
if (this.current === void 0) return super.createMessageSender();
|
|
91
|
-
if (options.messages !== void 0) return options.messages(checkEnv(this.env), this.current);
|
|
92
|
-
return channelsFromConfig(config, checkEnv(this.env), { store: this.current });
|
|
93
|
-
}
|
|
94
|
-
createObserver(store) {
|
|
95
|
-
if (options.observer !== void 0) return options.observer(checkEnv(this.env), store);
|
|
96
|
-
return store.reporting === void 0 ? void 0 : reportingObserver(store.reporting, checkEnv(this.env).WORKFLOW_ORIGIN);
|
|
97
|
-
}
|
|
98
|
-
runtime(params) {
|
|
99
|
-
const env = checkEnv(this.env);
|
|
100
|
-
if (options.runtime !== void 0) return options.runtime(env, params);
|
|
101
|
-
return runtimeFromConfig(config, env);
|
|
102
|
-
}
|
|
103
|
-
actions() {
|
|
104
|
-
return config.actions ?? [];
|
|
28
|
+
createRunParts(store, params) {
|
|
29
|
+
return host.runParts(host.checkEnv(this.env), store, params);
|
|
105
30
|
}
|
|
106
31
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
headers: { "content-type": "application/json" }
|
|
110
|
-
});
|
|
111
|
-
const authorized = (request, env) => env.API_TOKEN === void 0 || request.headers.get("authorization") === `Bearer ${env.API_TOKEN}`;
|
|
112
|
-
const preview = emails === void 0 ? void 0 : registryRenderer(emails, config.email);
|
|
113
|
-
let applying;
|
|
114
|
-
let retryAt = 0;
|
|
115
|
-
const applyOwnBundle = (env) => {
|
|
32
|
+
/** CF's version metadata: the bundle compiled into each new version goes live by itself. */
|
|
33
|
+
const ownVersion = (env) => {
|
|
116
34
|
const metadata = env.CF_VERSION_METADATA;
|
|
117
|
-
if (metadata === void 0
|
|
118
|
-
|
|
35
|
+
if (metadata === void 0) return void 0;
|
|
36
|
+
return {
|
|
119
37
|
id: metadata.id,
|
|
120
38
|
createdAt: Date.parse(metadata.timestamp)
|
|
121
39
|
};
|
|
122
|
-
if (Number.isNaN(version.createdAt)) return void 0;
|
|
123
|
-
if (applying !== void 0 || Date.now() < retryAt) return void 0;
|
|
124
|
-
applying = (async () => {
|
|
125
|
-
const { store, close } = lease(env);
|
|
126
|
-
try {
|
|
127
|
-
if (store.liveBundleVersion === void 0) return;
|
|
128
|
-
const live = await store.liveBundleVersion();
|
|
129
|
-
if (live !== void 0 && (live.id === version.id || live.createdAt >= version.createdAt)) return;
|
|
130
|
-
if ((await deployBundle(env, bundle(), store, version)).applied) console.log(`[workflow] version ${version.id} is live`);
|
|
131
|
-
} finally {
|
|
132
|
-
await close();
|
|
133
|
-
}
|
|
134
|
-
})().catch((error) => {
|
|
135
|
-
applying = void 0;
|
|
136
|
-
retryAt = Date.now() + 6e4;
|
|
137
|
-
console.error(`[workflow] could not make version ${version.id} live`, error);
|
|
138
|
-
});
|
|
139
|
-
return applying;
|
|
140
40
|
};
|
|
141
41
|
const fetch = async (request, rawEnv, ctx) => {
|
|
142
42
|
let env;
|
|
143
43
|
try {
|
|
144
|
-
env = checkEnv(rawEnv);
|
|
44
|
+
env = host.checkEnv(rawEnv);
|
|
145
45
|
} catch (error) {
|
|
146
46
|
if (!(error instanceof EnvValidationError)) throw error;
|
|
147
47
|
console.error(`[workflow] ${error.message}`);
|
|
148
|
-
return
|
|
48
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
49
|
+
status: 500,
|
|
50
|
+
headers: { "content-type": "application/json" }
|
|
51
|
+
});
|
|
149
52
|
}
|
|
150
|
-
const pending = applyOwnBundle(env);
|
|
53
|
+
const pending = host.applyOwnBundle(env, ownVersion(env));
|
|
151
54
|
if (pending !== void 0) {
|
|
152
55
|
if (ctx === void 0) await pending;
|
|
153
56
|
else ctx.waitUntil(pending);
|
|
154
57
|
}
|
|
155
|
-
const
|
|
156
|
-
const { store, close } = lease(env);
|
|
58
|
+
const { store, close } = host.lease(env);
|
|
157
59
|
try {
|
|
158
|
-
|
|
159
|
-
const openedEvent = config.webPush?.openedEvent;
|
|
160
|
-
const tracked = await handleTracking(request, store.reporting, {
|
|
161
|
-
legacyUnsubscribeUrl: config.email?.unsubscribeUrl,
|
|
162
|
-
product: config.title,
|
|
163
|
-
quickMs: config.email?.tracking?.quickClickMs,
|
|
164
|
-
pushOpened: openedEvent === void 0 ? void 0 : async ({ messageId, userId, at }) => {
|
|
165
|
-
const payload = { message_id: messageId };
|
|
166
|
-
const contract = config.contract;
|
|
167
|
-
const verdict = contract === void 0 ? void 0 : checkEvent(contract, openedEvent, payload);
|
|
168
|
-
if (verdict !== void 0 && !verdict.ok) throw new ContractValidationError(openedEvent, verdict.issues);
|
|
169
|
-
await ingestEvent(env, {
|
|
170
|
-
id: `${openedEvent}:${messageId}`,
|
|
171
|
-
userId,
|
|
172
|
-
event: openedEvent,
|
|
173
|
-
payload: verdict?.data ?? payload,
|
|
174
|
-
ts: at
|
|
175
|
-
}, store);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
if (tracked !== void 0) return tracked;
|
|
179
|
-
}
|
|
180
|
-
if (url.pathname === "/segments/sweep") {
|
|
181
|
-
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
182
|
-
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
183
|
-
return json(await sweepSegments(env, store));
|
|
184
|
-
}
|
|
185
|
-
if (url.pathname === "/bundle") {
|
|
186
|
-
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
187
|
-
if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
188
|
-
return json(bundle());
|
|
189
|
-
}
|
|
190
|
-
if (url.pathname === "/bundle/deploy") {
|
|
191
|
-
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
192
|
-
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
193
|
-
try {
|
|
194
|
-
return json(await deployBundle(env, bundle(), store));
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (!(error instanceof SchemaMismatchError)) throw error;
|
|
197
|
-
return json({
|
|
198
|
-
error: error.message,
|
|
199
|
-
problems: error.problems
|
|
200
|
-
}, 503);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (emails !== void 0 && preview !== void 0 && url.pathname.startsWith("/preview/")) {
|
|
204
|
-
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
205
|
-
if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
206
|
-
const key = url.pathname.slice(9);
|
|
207
|
-
if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);
|
|
208
|
-
let props = {};
|
|
209
|
-
try {
|
|
210
|
-
props = JSON.parse(url.searchParams.get("props") ?? "{}");
|
|
211
|
-
} catch {
|
|
212
|
-
return json({ error: "props must be a JSON object" }, 400);
|
|
213
|
-
}
|
|
214
|
-
if (typeof props !== "object" || props === null || Array.isArray(props)) return json({ error: "props must be a JSON object" }, 400);
|
|
215
|
-
try {
|
|
216
|
-
const { subject, html } = await preview(key, props);
|
|
217
|
-
return new Response(html, { headers: {
|
|
218
|
-
"content-type": "text/html; charset=utf-8",
|
|
219
|
-
"x-subject": subject
|
|
220
|
-
} });
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error instanceof TemplateValidationError) return json({
|
|
223
|
-
error: error.message,
|
|
224
|
-
issues: error.issues
|
|
225
|
-
}, 400);
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return await handleRequest(request, env, store, {
|
|
230
|
-
contract: config.contract,
|
|
231
|
-
mode: options.contractMode
|
|
232
|
-
});
|
|
60
|
+
return await host.handle(request, env, store);
|
|
233
61
|
} finally {
|
|
234
62
|
if (ctx === void 0) await close();
|
|
235
63
|
else ctx.waitUntil(close());
|
|
236
64
|
}
|
|
237
65
|
};
|
|
238
|
-
const sweep =
|
|
239
|
-
const env = checkEnv(rawEnv);
|
|
240
|
-
const { store, close } = lease(env);
|
|
241
|
-
try {
|
|
242
|
-
return await sweepSegments(env, store);
|
|
243
|
-
} finally {
|
|
244
|
-
await close();
|
|
245
|
-
}
|
|
246
|
-
};
|
|
66
|
+
const sweep = (rawEnv) => host.sweep(host.checkEnv(rawEnv));
|
|
247
67
|
const scheduled = async (_controller, env, ctx) => {
|
|
248
68
|
ctx.waitUntil((async () => {
|
|
249
|
-
|
|
69
|
+
const checked = host.checkEnv(env);
|
|
70
|
+
await host.applyOwnBundle(checked, ownVersion(checked));
|
|
250
71
|
await sweep(env);
|
|
251
72
|
})());
|
|
252
73
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.mjs","names":[],"sources":["../../src/cloudflare/worker.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { assertEmailConfig } from '../email';\nimport type { RegisteredAction } from '../engine/actions';\nimport type { JourneyObserver, MessageSender, MessageValidator } from '../engine/ports';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { ScalarIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport { handleTracking } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport {\n type BundleVersion,\n type JourneyStore,\n SchemaMismatchError,\n type StoreLease,\n} from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { TemplateValidationError, indexTemplates, registryValidator } from '../templates';\nimport type { JourneyEnv, JourneyParams } from './bindings';\nimport { channelsFromConfig, runtimeFromConfig } from './channels';\nimport { d1Store } from './d1-env';\nimport {\n type SweepResult,\n deployBundle,\n handleRequest,\n ingestEvent,\n sweepSegments,\n} from './router';\nimport { JourneyRunner } from './runner';\n\n/** The fetch handler's execution context (the subset used). */\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport interface JourneyWorkerOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the Worker\n * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the\n * template registries the senders, the validator and `/preview` render from,\n * the contract every `/events` and `/identify` is checked against, the email\n * envelope, actions, and the runtime / APNs / FCM settings. Secrets come\n * from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a\n * `StoreLease` when the client must be released afterwards (a Postgres\n * connection); defaults to D1 + KV from the env's bindings. `database` is\n * the config's (`schema` defaulting to `workflow`), for a Postgres store:\n *\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the Worker\n * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report\n * elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /** Message outlet override (tests, a custom gateway); by default the channels the config describes. */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the Worker runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\nexport interface JourneyWorker<Env extends JourneyEnv> {\n /** Export this under the `class_name` of the wrangler `workflows` binding. */\n Runner: new (...args: never[]) => JourneyRunner;\n /** The Worker's fetch handler: `/health`, `/events`, `/identify`, `/devices`, `/messages/opened`, `/segments/sweep`, `/t/*` (email tracking), `/deploy`, `/bundle`, `/bundle/deploy` and `/preview/*`. */\n fetch: (request: Request, env: Env, ctx?: ExecutionContextLike) => Promise<Response>;\n /**\n * The segment sweep (`sweepSegments`): membership of every deployed segment for every known\n * user, the journeys of segments entered by the clock alone (`not(performed(e.login, { within:\n * '7 days' }))` has no event to ride on), and each segment's size. Also served as\n * `POST /segments/sweep`.\n */\n sweep: (env: Env) => Promise<SweepResult>;\n /** The cron trigger's handler: runs the sweep. Export it beside `fetch` with a `triggers.crons` entry. */\n scheduled: (controller: unknown, env: Env, ctx: ExecutionContextLike) => Promise<void>;\n}\n\n/**\n * A complete journey host in one call: the runner class with the app's store,\n * outlets and runtime plugged in, and the fetch handler that serves the ingest\n * API around them.\n *\n * const journeys = journeyWorker<Env>({\n * config, // workflow.config.ts (defineConfig)\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),\n * });\n * export const MyRunner = journeys.Runner;\n * export default { fetch: journeys.fetch };\n */\n/** The Worker's environment failed its schema (`journeyWorker({ env })`). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the Worker's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport function journeyWorker<Env extends JourneyEnv>(\n options: JourneyWorkerOptions<Env>\n): JourneyWorker<Env> {\n const { config } = options;\n const emailIds = indexTemplates('email', config.templates?.email);\n // A config still on `emails` (its sender would be silently dropped) fails here, not at a send\n assertEmailConfig(config, emailIds.values());\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n if (!result.success) {\n // Names and reasons only: the values are secrets\n throw new EnvValidationError(\n result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n );\n }\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n const bundle = () => compileConfig(config);\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n class Runner extends JourneyRunner {\n private readonly leases = new WeakMap<JourneyStore, () => Promise<void>>();\n private current: JourneyStore | undefined;\n\n protected override createStore(): JourneyStore {\n // A run fails at its start on an invalid environment, not at the send that needs it\n const { store, close } = lease(checkEnv(this.env as Env));\n this.leases.set(store, close);\n this.current = store;\n return store;\n }\n\n protected override async closeStore(store: JourneyStore): Promise<void> {\n await this.leases.get(store)?.();\n }\n\n protected override contract(): Contract | undefined {\n return config.contract;\n }\n\n protected override createMessageValidator(): MessageValidator {\n return registryValidator(config.templates ?? {});\n }\n\n protected override createMessageSender(): MessageSender {\n if (this.current === undefined) return super.createMessageSender();\n if (options.messages !== undefined) {\n return options.messages(checkEnv(this.env as Env), this.current);\n }\n return channelsFromConfig(config, checkEnv(this.env as Env), { store: this.current });\n }\n\n protected override createObserver(store: JourneyStore): JourneyObserver | undefined {\n if (options.observer !== undefined) return options.observer(checkEnv(this.env as Env), store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, checkEnv(this.env as Env).WORKFLOW_ORIGIN);\n }\n\n protected override runtime(params: JourneyParams): JourneyRuntimeOptions {\n const env = checkEnv(this.env as Env);\n if (options.runtime !== undefined) return options.runtime(env, params);\n return runtimeFromConfig(config, env);\n }\n\n protected override actions(): readonly RegisteredAction[] {\n // An action ref is a registered action structurally: the same object carries name, code and hash\n return config.actions ?? [];\n }\n }\n\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\n const authorized = (request: Request, env: Env): boolean =>\n env.API_TOKEN === undefined ||\n request.headers.get('authorization') === `Bearer ${env.API_TOKEN}`;\n\n const preview = emails === undefined ? undefined : registryRenderer(emails, config.email);\n\n // The bundle compiled into this version goes live on its own, once per isolate: the first\n // request or cron run reads which version's bundle is live (one row) and applies this one only\n // over an older version, so one still serving during a rollout can never put its routes back.\n // A failure (say the migrations have not run) is logged and retried a minute later. Only the\n // request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env): Promise<void> | undefined => {\n const metadata = env.CF_VERSION_METADATA;\n if (metadata === undefined || metadata.id === '') return undefined;\n const version: BundleVersion = { id: metadata.id, createdAt: Date.parse(metadata.timestamp) };\n if (Number.isNaN(version.createdAt)) return undefined;\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n const fetch = async (request: Request, rawEnv: Env, ctx?: ExecutionContextLike) => {\n // Everything below reads the validated environment (the schema's defaults applied)\n let env: Env;\n try {\n env = checkEnv(rawEnv);\n } catch (error) {\n if (!(error instanceof EnvValidationError)) throw error;\n console.error(`[workflow] ${error.message}`);\n return json({ error: error.message }, 500);\n }\n const pending = applyOwnBundle(env);\n // In the background where the runtime has waitUntil; inline otherwise (tests)\n if (pending !== undefined) {\n if (ctx === undefined) await pending;\n else ctx.waitUntil(pending);\n }\n const url = new URL(request.url);\n const { store, close } = lease(env);\n try {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n if (url.pathname.startsWith('/t/') && store.reporting !== undefined) {\n const openedEvent = config.webPush?.openedEvent;\n const tracked = await handleTracking(request, store.reporting, {\n legacyUnsubscribeUrl: config.email?.unsubscribeUrl,\n product: config.title,\n quickMs: config.email?.tracking?.quickClickMs,\n pushOpened:\n openedEvent === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload: Record<string, unknown> = { message_id: messageId };\n const contract = config.contract;\n const verdict =\n contract === undefined ? undefined : checkEvent(contract, openedEvent, payload);\n if (verdict !== undefined && !verdict.ok) {\n throw new ContractValidationError(openedEvent, verdict.issues);\n }\n // Keyed by the copy: a second tap of the same notification is the same event\n await ingestEvent(\n env,\n {\n id: `${openedEvent}:${messageId}`,\n userId,\n event: openedEvent,\n payload: verdict?.data ?? payload,\n ts: at,\n },\n store\n );\n },\n });\n if (tracked !== undefined) return tracked;\n }\n if (url.pathname === '/segments/sweep') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n return json(await sweepSegments(env, store));\n }\n if (url.pathname === '/bundle') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n return json(bundle());\n }\n if (url.pathname === '/bundle/deploy') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n try {\n return json(await deployBundle(env, bundle(), store));\n } catch (error) {\n if (!(error instanceof SchemaMismatchError)) throw error;\n return json({ error: error.message, problems: error.problems }, 503);\n }\n }\n if (emails !== undefined && preview !== undefined && url.pathname.startsWith('/preview/')) {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n const key = url.pathname.slice('/preview/'.length);\n if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown = {};\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n try {\n const { subject, html } = await preview(key, props as Record<string, ScalarIR>);\n return new Response(html, {\n headers: { 'content-type': 'text/html; charset=utf-8', 'x-subject': subject },\n });\n } catch (error) {\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n throw error;\n }\n }\n return await handleRequest(request, env, store, {\n contract: config.contract,\n mode: options.contractMode,\n });\n } finally {\n // After the response when the runtime offers waitUntil, inline otherwise (tests).\n if (ctx === undefined) await close();\n else ctx.waitUntil(close());\n }\n };\n\n const sweep = async (rawEnv: Env) => {\n const env = checkEnv(rawEnv);\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n const scheduled = async (_controller: unknown, env: Env, ctx: ExecutionContextLike) => {\n // A version with no traffic yet goes live here; the sweep then reads its segments\n ctx.waitUntil(\n (async () => {\n await applyOwnBundle(checkEnv(env));\n await sweep(env);\n })()\n );\n };\n\n return { Runner, fetch, sweep, scheduled };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"worker.mjs","names":[],"sources":["../../src/cloudflare/worker.ts"],"sourcesContent":["import type { JourneyRunParts } from '../host/execute';\nimport {\n EnvValidationError,\n type JourneyHostOptions,\n createJourneyHost,\n} from '../host/journey-host';\nimport type { SweepResult } from '../host/router';\nimport type { BundleVersion, JourneyStore } from '../store/index';\nimport type { JourneyEnv, JourneyParams } from './bindings';\nimport { JourneyRunner } from './runner';\n\nexport { EnvValidationError } from '../host/journey-host';\n\n/** The fetch handler's execution context (the subset used). */\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\n/**\n * `journeyWorker`'s options: the host's (config, store, email transport, observer, ...). The\n * email transport is the host's choice — `cloudflareEmailTransport({ binding: env.EMAIL })` for\n * Cloudflare Email, `sesEmailTransport(...)` for SES.\n */\nexport type JourneyWorkerOptions<Env extends JourneyEnv> = JourneyHostOptions<Env>;\n\nexport interface JourneyWorker<Env extends JourneyEnv> {\n /** Export this under the `class_name` of the wrangler `workflows` binding. */\n Runner: new (...args: never[]) => JourneyRunner;\n /** The Worker's fetch handler: `/health`, `/events`, `/identify`, `/devices`, `/messages/opened`, `/segments/sweep`, `/t/*` (email tracking), `/deploy`, `/bundle`, `/bundle/deploy` and `/preview/*`. */\n fetch: (request: Request, env: Env, ctx?: ExecutionContextLike) => Promise<Response>;\n /**\n * The segment sweep (`sweepSegments`): membership of every deployed segment for every known\n * user, the journeys of segments entered by the clock alone (`not(performed(e.login, { within:\n * '7 days' }))` has no event to ride on), and each segment's size. Also served as\n * `POST /segments/sweep`.\n */\n sweep: (env: Env) => Promise<SweepResult>;\n /** The cron trigger's handler: runs the sweep. Export it beside `fetch` with a `triggers.crons` entry. */\n scheduled: (controller: unknown, env: Env, ctx: ExecutionContextLike) => Promise<void>;\n}\n\n/**\n * A complete journey host in one call: the runner class with the app's store,\n * outlets and runtime plugged in, and the fetch handler that serves the ingest\n * API around them.\n *\n * const journeys = journeyWorker<Env>({\n * config, // workflow.config.ts (defineConfig)\n * store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),\n * });\n * export const MyRunner = journeys.Runner;\n * export default { fetch: journeys.fetch };\n */\nexport function journeyWorker<Env extends JourneyEnv>(\n options: JourneyWorkerOptions<Env>\n): JourneyWorker<Env> {\n const host = createJourneyHost(options);\n\n class Runner extends JourneyRunner {\n private readonly leases = new WeakMap<JourneyStore, () => Promise<void>>();\n\n protected override createStore(): JourneyStore {\n // A run fails at its start on an invalid environment, not at the send that needs it\n const { store, close } = host.lease(host.checkEnv(this.env as Env));\n this.leases.set(store, close);\n return store;\n }\n\n protected override async closeStore(store: JourneyStore): Promise<void> {\n await this.leases.get(store)?.();\n }\n\n protected override createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return host.runParts(host.checkEnv(this.env as Env), store, params);\n }\n }\n\n /** CF's version metadata: the bundle compiled into each new version goes live by itself. */\n const ownVersion = (env: Env): BundleVersion | undefined => {\n const metadata = env.CF_VERSION_METADATA;\n if (metadata === undefined) return undefined;\n return { id: metadata.id, createdAt: Date.parse(metadata.timestamp) };\n };\n\n const fetch = async (request: Request, rawEnv: Env, ctx?: ExecutionContextLike) => {\n // Everything below reads the validated environment (the schema's defaults applied)\n let env: Env;\n try {\n env = host.checkEnv(rawEnv);\n } catch (error) {\n if (!(error instanceof EnvValidationError)) throw error;\n console.error(`[workflow] ${error.message}`);\n return new Response(JSON.stringify({ error: error.message }), {\n status: 500,\n headers: { 'content-type': 'application/json' },\n });\n }\n const pending = host.applyOwnBundle(env, ownVersion(env));\n // In the background where the runtime has waitUntil; inline otherwise (tests)\n if (pending !== undefined) {\n if (ctx === undefined) await pending;\n else ctx.waitUntil(pending);\n }\n const { store, close } = host.lease(env);\n try {\n return await host.handle(request, env, store);\n } finally {\n // After the response when the runtime offers waitUntil, inline otherwise (tests).\n if (ctx === undefined) await close();\n else ctx.waitUntil(close());\n }\n };\n\n const sweep = (rawEnv: Env) => host.sweep(host.checkEnv(rawEnv));\n\n const scheduled = async (_controller: unknown, env: Env, ctx: ExecutionContextLike) => {\n // A version with no traffic yet goes live here; the sweep then reads its segments\n ctx.waitUntil(\n (async () => {\n const checked = host.checkEnv(env);\n await host.applyOwnBundle(checked, ownVersion(checked));\n await sweep(env);\n })()\n );\n };\n\n return { Runner, fetch, sweep, scheduled };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAqDA,SAAgB,cACd,SACoB;CACpB,MAAM,OAAO,kBAAkB,OAAO;CAEtC,MAAM,eAAe,cAAc;EACjC,yBAA0B,IAAI,QAA2C;EAEzE,cAA+C;GAE7C,MAAM,EAAE,OAAO,UAAU,KAAK,MAAM,KAAK,SAAS,KAAK,GAAU,CAAC;GAClE,KAAK,OAAO,IAAI,OAAO,KAAK;GAC5B,OAAO;EACT;EAEA,MAAyB,WAAW,OAAoC;GACtE,MAAM,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG;EACjC;EAEA,eAAkC,OAAqB,QAAwC;GAC7F,OAAO,KAAK,SAAS,KAAK,SAAS,KAAK,GAAU,GAAG,OAAO,MAAM;EACpE;CACF;;CAGA,MAAM,cAAc,QAAwC;EAC1D,MAAM,WAAW,IAAI;EACrB,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;EACnC,OAAO;GAAE,IAAI,SAAS;GAAI,WAAW,KAAK,MAAM,SAAS,SAAS;EAAE;CACtE;CAEA,MAAM,QAAQ,OAAO,SAAkB,QAAa,QAA+B;EAEjF,IAAI;EACJ,IAAI;GACF,MAAM,KAAK,SAAS,MAAM;EAC5B,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,qBAAqB,MAAM;GAClD,QAAQ,MAAM,cAAc,MAAM,SAAS;GAC3C,OAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,QAAQ,CAAC,GAAG;IAC5D,QAAQ;IACR,SAAS,EAAE,gBAAgB,mBAAmB;GAChD,CAAC;EACH;EACA,MAAM,UAAU,KAAK,eAAe,KAAK,WAAW,GAAG,CAAC;EAExD,IAAI,YAAY,KAAA,GAAW;GACzB,IAAI,QAAQ,KAAA,GAAW,MAAM;QACxB,IAAI,UAAU,OAAO;EAC5B;EACA,MAAM,EAAE,OAAO,UAAU,KAAK,MAAM,GAAG;EACvC,IAAI;GACF,OAAO,MAAM,KAAK,OAAO,SAAS,KAAK,KAAK;EAC9C,UAAU;GAER,IAAI,QAAQ,KAAA,GAAW,MAAM,MAAM;QAC9B,IAAI,UAAU,MAAM,CAAC;EAC5B;CACF;CAEA,MAAM,SAAS,WAAgB,KAAK,MAAM,KAAK,SAAS,MAAM,CAAC;CAE/D,MAAM,YAAY,OAAO,aAAsB,KAAU,QAA8B;EAErF,IAAI,WACD,YAAY;GACX,MAAM,UAAU,KAAK,SAAS,GAAG;GACjC,MAAM,KAAK,eAAe,SAAS,WAAW,OAAO,CAAC;GACtD,MAAM,MAAM,GAAG;EACjB,EAAA,CAAG,CACL;CACF;CAEA,OAAO;EAAE;EAAQ;EAAO;EAAO;CAAU;AAC3C"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_email = require("./email.cjs");
|
|
3
|
+
//#region src/email-sender.ts
|
|
4
|
+
/**
|
|
5
|
+
* The email channel over any transport: render → tracking applied to the HTML → the tracked
|
|
6
|
+
* links committed → the transport. The links resolve before the email exists anywhere a
|
|
7
|
+
* scanner could follow them.
|
|
8
|
+
*/
|
|
9
|
+
var EmailSender = class {
|
|
10
|
+
options;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async send(message) {
|
|
15
|
+
if (message.channel !== "email") throw new Error(`EmailSender: unsupported channel '${message.channel}'`);
|
|
16
|
+
if (message.recipient === void 0) throw new Error(`EmailSender: no recipient for user '${message.userId}'`);
|
|
17
|
+
const { subject, html, from, replyTo, unsubscribeUrl } = await this.options.render(message.template, message.props);
|
|
18
|
+
const tracked = message.tracking === void 0 ? html : trackEmail(html, message.tracking, unsubscribeUrl);
|
|
19
|
+
await message.tracking?.commit?.();
|
|
20
|
+
await this.options.transport.send({
|
|
21
|
+
from,
|
|
22
|
+
to: message.recipient,
|
|
23
|
+
...replyTo === void 0 ? {} : { replyTo },
|
|
24
|
+
subject,
|
|
25
|
+
html: tracked,
|
|
26
|
+
idempotencyKey: message.idempotencyKey
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const LINK = /(<a\b[^>]*?\shref\s*=\s*)(["'])(https?:\/\/[^"']+)\2/gi;
|
|
31
|
+
/**
|
|
32
|
+
* An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through
|
|
33
|
+
* `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —
|
|
34
|
+
* as the unsubscribe link when it is the one the email was rendered with, so what a click on it
|
|
35
|
+
* means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the
|
|
36
|
+
* end when there is none.
|
|
37
|
+
*/
|
|
38
|
+
function trackEmail(html, tracking, unsubscribeUrl) {
|
|
39
|
+
const { link, pixel } = tracking;
|
|
40
|
+
let out = html;
|
|
41
|
+
if (link !== void 0) out = out.replace(LINK, (_, start, quote, attribute) => {
|
|
42
|
+
const url = unescapeAttribute(attribute);
|
|
43
|
+
const purpose = require_email.isUnsubscribeLink(url, unsubscribeUrl) ? "unsubscribe" : "link";
|
|
44
|
+
return `${start}${quote}${escapeAttribute(link(url, purpose))}${quote}`;
|
|
45
|
+
});
|
|
46
|
+
if (pixel !== void 0) {
|
|
47
|
+
const image = `<img src="${escapeAttribute(pixel)}" width="1" height="1" alt="" style="display:block;border:0;width:1px;height:1px" />`;
|
|
48
|
+
out = /<\/body>/i.test(out) ? out.replace(/<\/body>/i, `${image}</body>`) : `${out}${image}`;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function unescapeAttribute(value) {
|
|
53
|
+
return value.replaceAll("&", "&").replaceAll(""", "\"").replaceAll("'", "'");
|
|
54
|
+
}
|
|
55
|
+
function escapeAttribute(value) {
|
|
56
|
+
return value.replaceAll("&", "&").replaceAll("\"", """);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
exports.EmailSender = EmailSender;
|
|
60
|
+
exports.trackEmail = trackEmail;
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=email-sender.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-sender.cjs","names":["isUnsubscribeLink"],"sources":["../src/email-sender.ts"],"sourcesContent":["import { type EmailAddress, type ResolvedEmail, isUnsubscribeLink } from './email';\nimport type { MessageSender, MessageTracking, OutboundMessage } from './engine/ports';\nimport type { ScalarIR } from './ir';\n\n/**\n * The email channel, split where the runtime stops mattering: `EmailSender` renders a copy,\n * applies its tracking and commits the tracked links — the same on every host — and hands the\n * finished email to an `EmailTransport`, the only part that knows a provider (SES, Cloudflare\n * Email, ...). Nothing here imports a platform module, so a Worker and a Lambda share it.\n */\n\n/** A rendered email: the template's resolved metadata (`resolveEmail`), its subject and its HTML. */\nexport interface RenderedEmail extends ResolvedEmail {\n subject: string;\n html: string;\n}\n\n/**\n * Template renderer, injected by the app — this package's core stays free of\n * any react-email dependency (`@shware/workflow/react-email` ships one over a\n * registry). The subject comes back final: it is sent exactly as written, from\n * the template's own sender, and the unsubscribe link is the one the HTML was\n * rendered with.\n */\nexport type EmailRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<RenderedEmail>;\n\n/** A finished email, as a transport takes it: rendered, tracked, addressed. */\nexport interface EmailMessage {\n from: EmailAddress;\n to: string;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /**\n * `${instanceId}:${nodeId}`: the copy's identity, stable across retries and replays. A provider\n * that de-duplicates on a key receives it; one that cannot (SES) still sends at least once —\n * the window between the provider accepting the email and the step's checkpoint stays open.\n */\n idempotencyKey: string;\n}\n\n/**\n * One email provider. Throwing hands the retry to the durable step; resolving means the provider\n * accepted the email (not that it was delivered).\n */\nexport interface EmailTransport {\n send(message: EmailMessage): Promise<{ providerMessageId?: string }>;\n}\n\nexport interface EmailSenderOptions {\n transport: EmailTransport;\n render: EmailRenderer;\n}\n\n/**\n * The email channel over any transport: render → tracking applied to the HTML → the tracked\n * links committed → the transport. The links resolve before the email exists anywhere a\n * scanner could follow them.\n */\nexport class EmailSender implements MessageSender {\n constructor(private readonly options: EmailSenderOptions) {}\n\n async send(message: OutboundMessage): Promise<void> {\n if (message.channel !== 'email') {\n throw new Error(`EmailSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`EmailSender: no recipient for user '${message.userId}'`);\n }\n const { subject, html, from, replyTo, unsubscribeUrl } = await this.options.render(\n message.template,\n message.props\n );\n const tracked =\n message.tracking === undefined ? html : trackEmail(html, message.tracking, unsubscribeUrl);\n await message.tracking?.commit?.();\n await this.options.transport.send({\n from,\n to: message.recipient,\n ...(replyTo === undefined ? {} : { replyTo }),\n subject,\n html: tracked,\n idempotencyKey: message.idempotencyKey,\n });\n }\n}\n\nconst LINK = /(<a\\b[^>]*?\\shref\\s*=\\s*)([\"'])(https?:\\/\\/[^\"']+)\\2/gi;\n\n/**\n * An email's tracking applied to its rendered HTML: every `http(s)` anchor routed through\n * `link` (the original URL decoded from its attribute escaping, the tracking URL escaped back) —\n * as the unsubscribe link when it is the one the email was rendered with, so what a click on it\n * means is fixed when the email is sent — and the open pixel appended before `</body>`, or at the\n * end when there is none.\n */\nexport function trackEmail(\n html: string,\n tracking: MessageTracking,\n unsubscribeUrl?: string\n): string {\n const { link, pixel } = tracking;\n let out = html;\n if (link !== undefined) {\n out = out.replace(LINK, (_, start: string, quote: string, attribute: string) => {\n const url = unescapeAttribute(attribute);\n const purpose = isUnsubscribeLink(url, unsubscribeUrl) ? 'unsubscribe' : 'link';\n return `${start}${quote}${escapeAttribute(link(url, purpose))}${quote}`;\n });\n }\n if (pixel !== undefined) {\n const image = `<img src=\"${escapeAttribute(pixel)}\" width=\"1\" height=\"1\" alt=\"\" style=\"display:block;border:0;width:1px;height:1px\" />`;\n out = /<\\/body>/i.test(out) ? out.replace(/<\\/body>/i, `${image}</body>`) : `${out}${image}`;\n }\n return out;\n}\n\nfunction unescapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('"', '\"').replaceAll(''', \"'\");\n}\n\nfunction escapeAttribute(value: string): string {\n return value.replaceAll('&', '&').replaceAll('\"', '"');\n}\n"],"mappings":";;;;;;;;AA8DA,IAAa,cAAb,MAAkD;CACnB;CAA7B,YAAY,SAA8C;EAA7B,KAAA,UAAA;CAA8B;CAE3D,MAAM,KAAK,SAAyC;EAClD,IAAI,QAAQ,YAAY,SACtB,MAAM,IAAI,MAAM,qCAAqC,QAAQ,QAAQ,EAAE;EAEzE,IAAI,QAAQ,cAAc,KAAA,GACxB,MAAM,IAAI,MAAM,uCAAuC,QAAQ,OAAO,EAAE;EAE1E,MAAM,EAAE,SAAS,MAAM,MAAM,SAAS,mBAAmB,MAAM,KAAK,QAAQ,OAC1E,QAAQ,UACR,QAAQ,KACV;EACA,MAAM,UACJ,QAAQ,aAAa,KAAA,IAAY,OAAO,WAAW,MAAM,QAAQ,UAAU,cAAc;EAC3F,MAAM,QAAQ,UAAU,SAAS;EACjC,MAAM,KAAK,QAAQ,UAAU,KAAK;GAChC;GACA,IAAI,QAAQ;GACZ,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ;GAC3C;GACA,MAAM;GACN,gBAAgB,QAAQ;EAC1B,CAAC;CACH;AACF;AAEA,MAAM,OAAO;;;;;;;;AASb,SAAgB,WACd,MACA,UACA,gBACQ;CACR,MAAM,EAAE,MAAM,UAAU;CACxB,IAAI,MAAM;CACV,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,QAAQ,OAAO,GAAG,OAAe,OAAe,cAAsB;EAC9E,MAAM,MAAM,kBAAkB,SAAS;EACvC,MAAM,UAAUA,cAAAA,kBAAkB,KAAK,cAAc,IAAI,gBAAgB;EACzE,OAAO,GAAG,QAAQ,QAAQ,gBAAgB,KAAK,KAAK,OAAO,CAAC,IAAI;CAClE,CAAC;CAEH,IAAI,UAAU,KAAA,GAAW;EACvB,MAAM,QAAQ,aAAa,gBAAgB,KAAK,EAAE;EAClD,MAAM,YAAY,KAAK,GAAG,IAAI,IAAI,QAAQ,aAAa,GAAG,MAAM,QAAQ,IAAI,GAAG,MAAM;CACvF;CACA,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAuB;CAChD,OAAO,MAAM,WAAW,SAAS,GAAG,CAAC,CAAC,WAAW,UAAU,IAAG,CAAC,CAAC,WAAW,UAAU,GAAG;AAC1F;AAEA,SAAS,gBAAgB,OAAuB;CAC9C,OAAO,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC,WAAW,MAAK,QAAQ;AAChE"}
|