@shware/workflow 2.0.0 → 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/config.cjs +2 -2
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- 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-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.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.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/reporting/tracking.cjs +5 -1
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +5 -1
- package/dist/reporting/tracking.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-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/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/templates.cjs.map +1 -1
- package/dist/templates.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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let node_http2 = require("node:http2");
|
|
3
|
+
//#region src/http2-client.ts
|
|
4
|
+
function http2Sessions() {
|
|
5
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
6
|
+
return {
|
|
7
|
+
session(origin) {
|
|
8
|
+
const existing = sessions.get(origin);
|
|
9
|
+
if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
|
|
10
|
+
const created = (0, node_http2.connect)(origin);
|
|
11
|
+
const forget = () => {
|
|
12
|
+
if (sessions.get(origin) === created) sessions.delete(origin);
|
|
13
|
+
};
|
|
14
|
+
created.on("error", forget);
|
|
15
|
+
created.on("close", forget);
|
|
16
|
+
created.on("goaway", forget);
|
|
17
|
+
sessions.set(origin, created);
|
|
18
|
+
return created;
|
|
19
|
+
},
|
|
20
|
+
discard(origin) {
|
|
21
|
+
sessions.get(origin)?.destroy();
|
|
22
|
+
sessions.delete(origin);
|
|
23
|
+
},
|
|
24
|
+
close() {
|
|
25
|
+
for (const open of sessions.values()) open.close();
|
|
26
|
+
sessions.clear();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a
|
|
32
|
+
* JSON API call needs: a method, string headers, a string or byte body, the status, headers and
|
|
33
|
+
* body back.
|
|
34
|
+
*/
|
|
35
|
+
function http2Fetch(pool = http2Sessions(), options = {}) {
|
|
36
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
37
|
+
return async (input, init) => {
|
|
38
|
+
const request = new Request(input, init);
|
|
39
|
+
const url = new URL(request.url);
|
|
40
|
+
const body = request.body === null ? void 0 : Buffer.from(await request.arrayBuffer());
|
|
41
|
+
const headers = {
|
|
42
|
+
":method": request.method,
|
|
43
|
+
":path": `${url.pathname}${url.search}`
|
|
44
|
+
};
|
|
45
|
+
request.headers.forEach((value, name) => {
|
|
46
|
+
headers[name] = value;
|
|
47
|
+
});
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let stream;
|
|
50
|
+
try {
|
|
51
|
+
stream = pool.session(url.origin).request(headers);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let status = 502;
|
|
57
|
+
const responseHeaders = new Headers();
|
|
58
|
+
const chunks = [];
|
|
59
|
+
stream.on("response", (received) => {
|
|
60
|
+
status = received[":status"] ?? 502;
|
|
61
|
+
for (const [name, value] of Object.entries(received)) {
|
|
62
|
+
if (name.startsWith(":") || value === void 0) continue;
|
|
63
|
+
responseHeaders.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
67
|
+
stream.on("end", () => {
|
|
68
|
+
const bytes = Buffer.concat(chunks);
|
|
69
|
+
resolve(new Response(status === 204 || status === 304 || bytes.length === 0 ? null : bytes, {
|
|
70
|
+
status,
|
|
71
|
+
headers: responseHeaders
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
stream.on("error", reject);
|
|
75
|
+
stream.setTimeout(timeoutMs, () => {
|
|
76
|
+
stream.close();
|
|
77
|
+
pool.discard(url.origin);
|
|
78
|
+
reject(/* @__PURE__ */ new Error(`HTTP/2 request to ${url.origin} timed out after ${timeoutMs} ms`));
|
|
79
|
+
});
|
|
80
|
+
stream.end(body);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
exports.http2Fetch = http2Fetch;
|
|
86
|
+
exports.http2Sessions = http2Sessions;
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=http2-client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http2-client.cjs","names":["connect"],"sources":["../src/http2-client.ts"],"sourcesContent":["import { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * A Node HTTP/2 client: one session per origin, reused while it lives. APNs speaks HTTP/2 only\n * and Node's `fetch` does not negotiate it, so a Node host (a Lambda, the development relay)\n * pushes through this instead. Node-only — a Worker's own `fetch` already speaks HTTP/2.\n *\n * Why it exists, and when the Lambda side can go (as of 2026-10):\n * - Node 24 (Lambda's `nodejs24.x`, the newest GA runtime) bundles undici 7, whose `fetch`\n * leaves HTTP/2 off (`allowH2: false`). A POST to APNs fails there with \"Response does not\n * match the HTTP/1.1 protocol\". The only switch is an undici `Agent({ allowH2: true })`\n * dispatcher, which needs the npm `undici` package (and, set globally, would change every\n * `fetch` in the host's process) — this client needs no dependency and touches nothing global.\n * - undici 8 negotiates HTTP/2 by default (nodejs/undici#4828), and Node 26 bundles it. Lambda's\n * `nodejs26.x` is in public preview only, outside the SLA; it goes GA once Node 26 reaches\n * Active LTS.\n * - Once the function runs on a GA runtime whose built-in `fetch` speaks HTTP/2 (check that\n * durable functions support it, and that a POST to api.push.apple.com answers JSON rather\n * than a protocol error), drop `apnsFetch: http2Fetch()` from `journeyLambda` (aws/index.ts):\n * APNs then uses the platform `fetch` like the other channels. The development relay\n * (`apns-relay.ts`, for workerd, which cannot make HTTP/2 subrequests) keeps using this pool.\n */\n\nexport interface Http2Sessions {\n session(origin: string): ClientHttp2Session;\n /** Drop a session that stopped answering, so the next request reconnects. */\n discard(origin: string): void;\n close(): void;\n}\n\nexport function http2Sessions(): Http2Sessions {\n const sessions = new Map<string, ClientHttp2Session>();\n return {\n session(origin) {\n const existing = sessions.get(origin);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(origin);\n const forget = () => {\n if (sessions.get(origin) === created) sessions.delete(origin);\n };\n created.on('error', forget);\n created.on('close', forget);\n created.on('goaway', forget);\n sessions.set(origin, created);\n return created;\n },\n discard(origin) {\n sessions.get(origin)?.destroy();\n sessions.delete(origin);\n },\n close() {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n },\n };\n}\n\n/**\n * `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a\n * JSON API call needs: a method, string headers, a string or byte body, the status, headers and\n * body back.\n */\nexport function http2Fetch(\n pool: Http2Sessions = http2Sessions(),\n options: {\n /**\n * How long a request may go without activity: a session a frozen Lambda left half-dead\n * answers nothing and closes nothing, so without a limit the request hangs until the\n * function's timeout. 10 seconds by default.\n */\n timeoutMs?: number;\n } = {}\n): typeof fetch {\n const timeoutMs = options.timeoutMs ?? 10_000;\n return async (input: string | URL | Request, init?: RequestInit) => {\n const request = new Request(input, init);\n const url = new URL(request.url);\n const body = request.body === null ? undefined : Buffer.from(await request.arrayBuffer());\n const headers: Record<string, string> = {\n ':method': request.method,\n ':path': `${url.pathname}${url.search}`,\n };\n request.headers.forEach((value, name) => {\n headers[name] = value;\n });\n return new Promise<Response>((resolve, reject) => {\n let stream;\n try {\n stream = pool.session(url.origin).request(headers);\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n let status = 502;\n const responseHeaders = new Headers();\n const chunks: Buffer[] = [];\n stream.on('response', (received) => {\n status = received[':status'] ?? 502;\n for (const [name, value] of Object.entries(received)) {\n if (name.startsWith(':') || value === undefined) continue;\n responseHeaders.set(name, Array.isArray(value) ? value.join(', ') : value);\n }\n });\n stream.on('data', (chunk: Buffer) => chunks.push(chunk));\n stream.on('end', () => {\n const bytes = Buffer.concat(chunks);\n resolve(\n new Response(status === 204 || status === 304 || bytes.length === 0 ? null : bytes, {\n status,\n headers: responseHeaders,\n })\n );\n });\n stream.on('error', reject);\n stream.setTimeout(timeoutMs, () => {\n stream.close();\n pool.discard(url.origin);\n reject(new Error(`HTTP/2 request to ${url.origin} timed out after ${timeoutMs} ms`));\n });\n stream.end(body);\n });\n };\n}\n"],"mappings":";;;AA8BA,SAAgB,gBAA+B;CAC7C,MAAM,2BAAW,IAAI,IAAgC;CACrD,OAAO;EACL,QAAQ,QAAQ;GACd,MAAM,WAAW,SAAS,IAAI,MAAM;GACpC,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,UAAU,CAAC,SAAS,WAAW,OAAO;GAC9E,MAAM,WAAA,GAAUA,WAAAA,QAAAA,CAAQ,MAAM;GAC9B,MAAM,eAAe;IACnB,IAAI,SAAS,IAAI,MAAM,MAAM,SAAS,SAAS,OAAO,MAAM;GAC9D;GACA,QAAQ,GAAG,SAAS,MAAM;GAC1B,QAAQ,GAAG,SAAS,MAAM;GAC1B,QAAQ,GAAG,UAAU,MAAM;GAC3B,SAAS,IAAI,QAAQ,OAAO;GAC5B,OAAO;EACT;EACA,QAAQ,QAAQ;GACd,SAAS,IAAI,MAAM,CAAC,EAAE,QAAQ;GAC9B,SAAS,OAAO,MAAM;EACxB;EACA,QAAQ;GACN,KAAK,MAAM,QAAQ,SAAS,OAAO,GAAG,KAAK,MAAM;GACjD,SAAS,MAAM;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,WACd,OAAsB,cAAc,GACpC,UAOI,CAAC,GACS;CACd,MAAM,YAAY,QAAQ,aAAa;CACvC,OAAO,OAAO,OAA+B,SAAuB;EAClE,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;EACvC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;EAC/B,MAAM,OAAO,QAAQ,SAAS,OAAO,KAAA,IAAY,OAAO,KAAK,MAAM,QAAQ,YAAY,CAAC;EACxF,MAAM,UAAkC;GACtC,WAAW,QAAQ;GACnB,SAAS,GAAG,IAAI,WAAW,IAAI;EACjC;EACA,QAAQ,QAAQ,SAAS,OAAO,SAAS;GACvC,QAAQ,QAAQ;EAClB,CAAC;EACD,OAAO,IAAI,SAAmB,SAAS,WAAW;GAChD,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,QAAQ,OAAO;GACnD,SAAS,OAAO;IACd,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;IAChE;GACF;GACA,IAAI,SAAS;GACb,MAAM,kBAAkB,IAAI,QAAQ;GACpC,MAAM,SAAmB,CAAC;GAC1B,OAAO,GAAG,aAAa,aAAa;IAClC,SAAS,SAAS,cAAc;IAChC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;KACpD,IAAI,KAAK,WAAW,GAAG,KAAK,UAAU,KAAA,GAAW;KACjD,gBAAgB,IAAI,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK;IAC3E;GACF,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;GACvD,OAAO,GAAG,aAAa;IACrB,MAAM,QAAQ,OAAO,OAAO,MAAM;IAClC,QACE,IAAI,SAAS,WAAW,OAAO,WAAW,OAAO,MAAM,WAAW,IAAI,OAAO,OAAO;KAClF;KACA,SAAS;IACX,CAAC,CACH;GACF,CAAC;GACD,OAAO,GAAG,SAAS,MAAM;GACzB,OAAO,WAAW,iBAAiB;IACjC,OAAO,MAAM;IACb,KAAK,QAAQ,IAAI,MAAM;IACvB,uBAAO,IAAI,MAAM,qBAAqB,IAAI,OAAO,mBAAmB,UAAU,IAAI,CAAC;GACrF,CAAC;GACD,OAAO,IAAI,IAAI;EACjB,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ClientHttp2Session } from "node:http2";
|
|
2
|
+
//#region src/http2-client.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A Node HTTP/2 client: one session per origin, reused while it lives. APNs speaks HTTP/2 only
|
|
5
|
+
* and Node's `fetch` does not negotiate it, so a Node host (a Lambda, the development relay)
|
|
6
|
+
* pushes through this instead. Node-only — a Worker's own `fetch` already speaks HTTP/2.
|
|
7
|
+
*
|
|
8
|
+
* Why it exists, and when the Lambda side can go (as of 2026-10):
|
|
9
|
+
* - Node 24 (Lambda's `nodejs24.x`, the newest GA runtime) bundles undici 7, whose `fetch`
|
|
10
|
+
* leaves HTTP/2 off (`allowH2: false`). A POST to APNs fails there with "Response does not
|
|
11
|
+
* match the HTTP/1.1 protocol". The only switch is an undici `Agent({ allowH2: true })`
|
|
12
|
+
* dispatcher, which needs the npm `undici` package (and, set globally, would change every
|
|
13
|
+
* `fetch` in the host's process) — this client needs no dependency and touches nothing global.
|
|
14
|
+
* - undici 8 negotiates HTTP/2 by default (nodejs/undici#4828), and Node 26 bundles it. Lambda's
|
|
15
|
+
* `nodejs26.x` is in public preview only, outside the SLA; it goes GA once Node 26 reaches
|
|
16
|
+
* Active LTS.
|
|
17
|
+
* - Once the function runs on a GA runtime whose built-in `fetch` speaks HTTP/2 (check that
|
|
18
|
+
* durable functions support it, and that a POST to api.push.apple.com answers JSON rather
|
|
19
|
+
* than a protocol error), drop `apnsFetch: http2Fetch()` from `journeyLambda` (aws/index.ts):
|
|
20
|
+
* APNs then uses the platform `fetch` like the other channels. The development relay
|
|
21
|
+
* (`apns-relay.ts`, for workerd, which cannot make HTTP/2 subrequests) keeps using this pool.
|
|
22
|
+
*/
|
|
23
|
+
export interface Http2Sessions {
|
|
24
|
+
session(origin: string): ClientHttp2Session;
|
|
25
|
+
/** Drop a session that stopped answering, so the next request reconnects. */
|
|
26
|
+
discard(origin: string): void;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function http2Sessions(): Http2Sessions;
|
|
30
|
+
/**
|
|
31
|
+
* `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a
|
|
32
|
+
* JSON API call needs: a method, string headers, a string or byte body, the status, headers and
|
|
33
|
+
* body back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function http2Fetch(pool?: Http2Sessions, options?: {
|
|
36
|
+
/**
|
|
37
|
+
* How long a request may go without activity: a session a frozen Lambda left half-dead
|
|
38
|
+
* answers nothing and closes nothing, so without a limit the request hangs until the
|
|
39
|
+
* function's timeout. 10 seconds by default.
|
|
40
|
+
*/
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}): typeof fetch;
|
|
43
|
+
//#endregion
|
|
44
|
+
//# sourceMappingURL=http2-client.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http2-client.d.cts","names":[],"sources":["../src/http2-client.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAuBiB;EACf,QAAQ,iBAAiB;;EAEzB,QAAQ;EACR;;wBAGc,iBAAiB;;;;;;wBAgCjB,WACd,OAAM,eACN;;;;;;EAME;WAEM"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ClientHttp2Session } from "node:http2";
|
|
2
|
+
//#region src/http2-client.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A Node HTTP/2 client: one session per origin, reused while it lives. APNs speaks HTTP/2 only
|
|
5
|
+
* and Node's `fetch` does not negotiate it, so a Node host (a Lambda, the development relay)
|
|
6
|
+
* pushes through this instead. Node-only — a Worker's own `fetch` already speaks HTTP/2.
|
|
7
|
+
*
|
|
8
|
+
* Why it exists, and when the Lambda side can go (as of 2026-10):
|
|
9
|
+
* - Node 24 (Lambda's `nodejs24.x`, the newest GA runtime) bundles undici 7, whose `fetch`
|
|
10
|
+
* leaves HTTP/2 off (`allowH2: false`). A POST to APNs fails there with "Response does not
|
|
11
|
+
* match the HTTP/1.1 protocol". The only switch is an undici `Agent({ allowH2: true })`
|
|
12
|
+
* dispatcher, which needs the npm `undici` package (and, set globally, would change every
|
|
13
|
+
* `fetch` in the host's process) — this client needs no dependency and touches nothing global.
|
|
14
|
+
* - undici 8 negotiates HTTP/2 by default (nodejs/undici#4828), and Node 26 bundles it. Lambda's
|
|
15
|
+
* `nodejs26.x` is in public preview only, outside the SLA; it goes GA once Node 26 reaches
|
|
16
|
+
* Active LTS.
|
|
17
|
+
* - Once the function runs on a GA runtime whose built-in `fetch` speaks HTTP/2 (check that
|
|
18
|
+
* durable functions support it, and that a POST to api.push.apple.com answers JSON rather
|
|
19
|
+
* than a protocol error), drop `apnsFetch: http2Fetch()` from `journeyLambda` (aws/index.ts):
|
|
20
|
+
* APNs then uses the platform `fetch` like the other channels. The development relay
|
|
21
|
+
* (`apns-relay.ts`, for workerd, which cannot make HTTP/2 subrequests) keeps using this pool.
|
|
22
|
+
*/
|
|
23
|
+
export interface Http2Sessions {
|
|
24
|
+
session(origin: string): ClientHttp2Session;
|
|
25
|
+
/** Drop a session that stopped answering, so the next request reconnects. */
|
|
26
|
+
discard(origin: string): void;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function http2Sessions(): Http2Sessions;
|
|
30
|
+
/**
|
|
31
|
+
* `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a
|
|
32
|
+
* JSON API call needs: a method, string headers, a string or byte body, the status, headers and
|
|
33
|
+
* body back.
|
|
34
|
+
*/
|
|
35
|
+
export declare function http2Fetch(pool?: Http2Sessions, options?: {
|
|
36
|
+
/**
|
|
37
|
+
* How long a request may go without activity: a session a frozen Lambda left half-dead
|
|
38
|
+
* answers nothing and closes nothing, so without a limit the request hangs until the
|
|
39
|
+
* function's timeout. 10 seconds by default.
|
|
40
|
+
*/
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}): typeof fetch;
|
|
43
|
+
//#endregion
|
|
44
|
+
//# sourceMappingURL=http2-client.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http2-client.d.mts","names":[],"sources":["../src/http2-client.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAuBiB;EACf,QAAQ,iBAAiB;;EAEzB,QAAQ;EACR;;wBAGc,iBAAiB;;;;;;wBAgCjB,WACd,OAAM,eACN;;;;;;EAME;WAEM"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { connect } from "node:http2";
|
|
2
|
+
//#region src/http2-client.ts
|
|
3
|
+
function http2Sessions() {
|
|
4
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
5
|
+
return {
|
|
6
|
+
session(origin) {
|
|
7
|
+
const existing = sessions.get(origin);
|
|
8
|
+
if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
|
|
9
|
+
const created = connect(origin);
|
|
10
|
+
const forget = () => {
|
|
11
|
+
if (sessions.get(origin) === created) sessions.delete(origin);
|
|
12
|
+
};
|
|
13
|
+
created.on("error", forget);
|
|
14
|
+
created.on("close", forget);
|
|
15
|
+
created.on("goaway", forget);
|
|
16
|
+
sessions.set(origin, created);
|
|
17
|
+
return created;
|
|
18
|
+
},
|
|
19
|
+
discard(origin) {
|
|
20
|
+
sessions.get(origin)?.destroy();
|
|
21
|
+
sessions.delete(origin);
|
|
22
|
+
},
|
|
23
|
+
close() {
|
|
24
|
+
for (const open of sessions.values()) open.close();
|
|
25
|
+
sessions.clear();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a
|
|
31
|
+
* JSON API call needs: a method, string headers, a string or byte body, the status, headers and
|
|
32
|
+
* body back.
|
|
33
|
+
*/
|
|
34
|
+
function http2Fetch(pool = http2Sessions(), options = {}) {
|
|
35
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
36
|
+
return async (input, init) => {
|
|
37
|
+
const request = new Request(input, init);
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
const body = request.body === null ? void 0 : Buffer.from(await request.arrayBuffer());
|
|
40
|
+
const headers = {
|
|
41
|
+
":method": request.method,
|
|
42
|
+
":path": `${url.pathname}${url.search}`
|
|
43
|
+
};
|
|
44
|
+
request.headers.forEach((value, name) => {
|
|
45
|
+
headers[name] = value;
|
|
46
|
+
});
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
let stream;
|
|
49
|
+
try {
|
|
50
|
+
stream = pool.session(url.origin).request(headers);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let status = 502;
|
|
56
|
+
const responseHeaders = new Headers();
|
|
57
|
+
const chunks = [];
|
|
58
|
+
stream.on("response", (received) => {
|
|
59
|
+
status = received[":status"] ?? 502;
|
|
60
|
+
for (const [name, value] of Object.entries(received)) {
|
|
61
|
+
if (name.startsWith(":") || value === void 0) continue;
|
|
62
|
+
responseHeaders.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
66
|
+
stream.on("end", () => {
|
|
67
|
+
const bytes = Buffer.concat(chunks);
|
|
68
|
+
resolve(new Response(status === 204 || status === 304 || bytes.length === 0 ? null : bytes, {
|
|
69
|
+
status,
|
|
70
|
+
headers: responseHeaders
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
stream.on("error", reject);
|
|
74
|
+
stream.setTimeout(timeoutMs, () => {
|
|
75
|
+
stream.close();
|
|
76
|
+
pool.discard(url.origin);
|
|
77
|
+
reject(/* @__PURE__ */ new Error(`HTTP/2 request to ${url.origin} timed out after ${timeoutMs} ms`));
|
|
78
|
+
});
|
|
79
|
+
stream.end(body);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { http2Fetch, http2Sessions };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=http2-client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http2-client.mjs","names":[],"sources":["../src/http2-client.ts"],"sourcesContent":["import { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * A Node HTTP/2 client: one session per origin, reused while it lives. APNs speaks HTTP/2 only\n * and Node's `fetch` does not negotiate it, so a Node host (a Lambda, the development relay)\n * pushes through this instead. Node-only — a Worker's own `fetch` already speaks HTTP/2.\n *\n * Why it exists, and when the Lambda side can go (as of 2026-10):\n * - Node 24 (Lambda's `nodejs24.x`, the newest GA runtime) bundles undici 7, whose `fetch`\n * leaves HTTP/2 off (`allowH2: false`). A POST to APNs fails there with \"Response does not\n * match the HTTP/1.1 protocol\". The only switch is an undici `Agent({ allowH2: true })`\n * dispatcher, which needs the npm `undici` package (and, set globally, would change every\n * `fetch` in the host's process) — this client needs no dependency and touches nothing global.\n * - undici 8 negotiates HTTP/2 by default (nodejs/undici#4828), and Node 26 bundles it. Lambda's\n * `nodejs26.x` is in public preview only, outside the SLA; it goes GA once Node 26 reaches\n * Active LTS.\n * - Once the function runs on a GA runtime whose built-in `fetch` speaks HTTP/2 (check that\n * durable functions support it, and that a POST to api.push.apple.com answers JSON rather\n * than a protocol error), drop `apnsFetch: http2Fetch()` from `journeyLambda` (aws/index.ts):\n * APNs then uses the platform `fetch` like the other channels. The development relay\n * (`apns-relay.ts`, for workerd, which cannot make HTTP/2 subrequests) keeps using this pool.\n */\n\nexport interface Http2Sessions {\n session(origin: string): ClientHttp2Session;\n /** Drop a session that stopped answering, so the next request reconnects. */\n discard(origin: string): void;\n close(): void;\n}\n\nexport function http2Sessions(): Http2Sessions {\n const sessions = new Map<string, ClientHttp2Session>();\n return {\n session(origin) {\n const existing = sessions.get(origin);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(origin);\n const forget = () => {\n if (sessions.get(origin) === created) sessions.delete(origin);\n };\n created.on('error', forget);\n created.on('close', forget);\n created.on('goaway', forget);\n sessions.set(origin, created);\n return created;\n },\n discard(origin) {\n sessions.get(origin)?.destroy();\n sessions.delete(origin);\n },\n close() {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n },\n };\n}\n\n/**\n * `fetch` over HTTP/2, for the senders that take one (`ApnsPushSender({ fetch })`). Only what a\n * JSON API call needs: a method, string headers, a string or byte body, the status, headers and\n * body back.\n */\nexport function http2Fetch(\n pool: Http2Sessions = http2Sessions(),\n options: {\n /**\n * How long a request may go without activity: a session a frozen Lambda left half-dead\n * answers nothing and closes nothing, so without a limit the request hangs until the\n * function's timeout. 10 seconds by default.\n */\n timeoutMs?: number;\n } = {}\n): typeof fetch {\n const timeoutMs = options.timeoutMs ?? 10_000;\n return async (input: string | URL | Request, init?: RequestInit) => {\n const request = new Request(input, init);\n const url = new URL(request.url);\n const body = request.body === null ? undefined : Buffer.from(await request.arrayBuffer());\n const headers: Record<string, string> = {\n ':method': request.method,\n ':path': `${url.pathname}${url.search}`,\n };\n request.headers.forEach((value, name) => {\n headers[name] = value;\n });\n return new Promise<Response>((resolve, reject) => {\n let stream;\n try {\n stream = pool.session(url.origin).request(headers);\n } catch (error) {\n reject(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n let status = 502;\n const responseHeaders = new Headers();\n const chunks: Buffer[] = [];\n stream.on('response', (received) => {\n status = received[':status'] ?? 502;\n for (const [name, value] of Object.entries(received)) {\n if (name.startsWith(':') || value === undefined) continue;\n responseHeaders.set(name, Array.isArray(value) ? value.join(', ') : value);\n }\n });\n stream.on('data', (chunk: Buffer) => chunks.push(chunk));\n stream.on('end', () => {\n const bytes = Buffer.concat(chunks);\n resolve(\n new Response(status === 204 || status === 304 || bytes.length === 0 ? null : bytes, {\n status,\n headers: responseHeaders,\n })\n );\n });\n stream.on('error', reject);\n stream.setTimeout(timeoutMs, () => {\n stream.close();\n pool.discard(url.origin);\n reject(new Error(`HTTP/2 request to ${url.origin} timed out after ${timeoutMs} ms`));\n });\n stream.end(body);\n });\n };\n}\n"],"mappings":";;AA8BA,SAAgB,gBAA+B;CAC7C,MAAM,2BAAW,IAAI,IAAgC;CACrD,OAAO;EACL,QAAQ,QAAQ;GACd,MAAM,WAAW,SAAS,IAAI,MAAM;GACpC,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,UAAU,CAAC,SAAS,WAAW,OAAO;GAC9E,MAAM,UAAU,QAAQ,MAAM;GAC9B,MAAM,eAAe;IACnB,IAAI,SAAS,IAAI,MAAM,MAAM,SAAS,SAAS,OAAO,MAAM;GAC9D;GACA,QAAQ,GAAG,SAAS,MAAM;GAC1B,QAAQ,GAAG,SAAS,MAAM;GAC1B,QAAQ,GAAG,UAAU,MAAM;GAC3B,SAAS,IAAI,QAAQ,OAAO;GAC5B,OAAO;EACT;EACA,QAAQ,QAAQ;GACd,SAAS,IAAI,MAAM,CAAC,EAAE,QAAQ;GAC9B,SAAS,OAAO,MAAM;EACxB;EACA,QAAQ;GACN,KAAK,MAAM,QAAQ,SAAS,OAAO,GAAG,KAAK,MAAM;GACjD,SAAS,MAAM;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,WACd,OAAsB,cAAc,GACpC,UAOI,CAAC,GACS;CACd,MAAM,YAAY,QAAQ,aAAa;CACvC,OAAO,OAAO,OAA+B,SAAuB;EAClE,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;EACvC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;EAC/B,MAAM,OAAO,QAAQ,SAAS,OAAO,KAAA,IAAY,OAAO,KAAK,MAAM,QAAQ,YAAY,CAAC;EACxF,MAAM,UAAkC;GACtC,WAAW,QAAQ;GACnB,SAAS,GAAG,IAAI,WAAW,IAAI;EACjC;EACA,QAAQ,QAAQ,SAAS,OAAO,SAAS;GACvC,QAAQ,QAAQ;EAClB,CAAC;EACD,OAAO,IAAI,SAAmB,SAAS,WAAW;GAChD,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,QAAQ,OAAO;GACnD,SAAS,OAAO;IACd,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;IAChE;GACF;GACA,IAAI,SAAS;GACb,MAAM,kBAAkB,IAAI,QAAQ;GACpC,MAAM,SAAmB,CAAC;GAC1B,OAAO,GAAG,aAAa,aAAa;IAClC,SAAS,SAAS,cAAc;IAChC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,GAAG;KACpD,IAAI,KAAK,WAAW,GAAG,KAAK,UAAU,KAAA,GAAW;KACjD,gBAAgB,IAAI,MAAM,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK;IAC3E;GACF,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;GACvD,OAAO,GAAG,aAAa;IACrB,MAAM,QAAQ,OAAO,OAAO,MAAM;IAClC,QACE,IAAI,SAAS,WAAW,OAAO,WAAW,OAAO,MAAM,WAAW,IAAI,OAAO,OAAO;KAClF;KACA,SAAS;IACX,CAAC,CACH;GACF,CAAC;GACD,OAAO,GAAG,SAAS,MAAM;GACzB,OAAO,WAAW,iBAAiB;IACjC,OAAO,MAAM;IACb,KAAK,QAAQ,IAAI,MAAM;IACvB,uBAAO,IAAI,MAAM,qBAAqB,IAAI,OAAO,mBAAmB,UAAU,IAAI,CAAC;GACrF,CAAC;GACD,OAAO,IAAI,IAAI;EACjB,CAAC;CACH;AACF"}
|
package/dist/index.cjs
CHANGED
|
@@ -80,6 +80,7 @@ exports.isUnsubscribeLink = require_email.isUnsubscribeLink;
|
|
|
80
80
|
exports.lookupTemplate = require_templates.lookupTemplate;
|
|
81
81
|
exports.lt = require_dsl_condition.lt;
|
|
82
82
|
exports.lte = require_dsl_condition.lte;
|
|
83
|
+
exports.mimeAddress = require_email.mimeAddress;
|
|
83
84
|
exports.ne = require_dsl_condition.ne;
|
|
84
85
|
exports.not = require_dsl_condition.not;
|
|
85
86
|
exports.notBetween = require_dsl_condition.notBetween;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.cjs";
|
|
2
|
-
import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.cjs";
|
|
2
|
+
import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, resolveEmail } from "./email.cjs";
|
|
3
3
|
import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
|
|
4
4
|
import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.cjs";
|
|
5
5
|
import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.cjs";
|
|
@@ -16,4 +16,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
|
|
|
16
16
|
import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.cjs";
|
|
17
17
|
import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
|
|
18
18
|
import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
|
|
19
|
-
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
|
19
|
+
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.mjs";
|
|
2
|
-
import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.mjs";
|
|
2
|
+
import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, resolveEmail } from "./email.mjs";
|
|
3
3
|
import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
|
|
4
4
|
import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.mjs";
|
|
5
5
|
import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.mjs";
|
|
@@ -16,4 +16,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
|
|
|
16
16
|
import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.mjs";
|
|
17
17
|
import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
|
|
18
18
|
import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.mjs";
|
|
19
|
-
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
|
19
|
+
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { action } from "./dsl/action.mjs";
|
|
|
3
3
|
import { event, user } from "./dsl/refs.mjs";
|
|
4
4
|
import { template } from "./dsl/template.mjs";
|
|
5
5
|
import { and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
|
|
6
|
-
import { assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.mjs";
|
|
6
|
+
import { assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, mimeAddress, resolveEmail } from "./email.mjs";
|
|
7
7
|
import { CHANNEL_OF, TemplateValidationError, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
|
|
8
8
|
import { flow } from "./dsl/flow.mjs";
|
|
9
9
|
import { trigger } from "./dsl/trigger.mjs";
|
|
@@ -15,4 +15,4 @@ import { ConfigCompileError, apnsEnvironment, compileConfig, defineConfig, isCon
|
|
|
15
15
|
import { ContractValidationError, checkEvent, defineContract, isContract } from "./contract.mjs";
|
|
16
16
|
import { isSemanticChange, plan } from "./plan.mjs";
|
|
17
17
|
import { resolveRuntime } from "./runtime.mjs";
|
|
18
|
-
export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
|
18
|
+
export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, mimeAddress, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/react-email.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.cjs","names":["indexTemplates","lookupTemplate","parseTemplateProps","resolveEmail","createElement","EmailProvider","renderToReadableStream"],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type
|
|
1
|
+
{"version":3,"file":"react-email.cjs","names":["indexTemplates","lookupTemplate","parseTemplateProps","resolveEmail","createElement","EmailProvider","renderToReadableStream"],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { type EmailConfig, resolveEmail } from './email';\nimport { EmailProvider } from './email-context';\nimport type { EmailRenderer } from './email-sender';\nimport {\n type EmailTemplate,\n indexTemplates,\n lookupTemplate,\n parseTemplateProps,\n} from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element inside an `EmailProvider` carrying the\n * template's resolved metadata (`resolveEmail` against the config's `email`),\n * so hooks and context work and a layout's `useEmailContext()` reads the\n * unsubscribe link the email is sent with; the render returns that metadata\n * with the subject and HTML, for the sender. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(\n emails: Readonly<Record<string, EmailTemplate>>,\n config?: EmailConfig\n): EmailRenderer {\n const index = indexTemplates('email', emails);\n return async (id, props) => {\n const template = lookupTemplate(index, 'email', id);\n const parsed = parseTemplateProps(template, props);\n const email = resolveEmail(template, config);\n const element = createElement(\n EmailProvider,\n { email },\n createElement(template.component as ComponentType<object>, parsed)\n );\n return { ...email, subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,UACJ;AAEF,SAAgB,iBACd,QACA,QACe;CACf,MAAM,QAAQA,kBAAAA,eAAe,SAAS,MAAM;CAC5C,OAAO,OAAO,IAAI,UAAU;EAC1B,MAAM,WAAWC,kBAAAA,eAAe,OAAO,SAAS,EAAE;EAClD,MAAM,SAASC,kBAAAA,mBAAmB,UAAU,KAAK;EACjD,MAAM,QAAQC,cAAAA,aAAa,UAAU,MAAM;EAC3C,MAAM,WAAA,GAAUC,MAAAA,cAAAA,CACdC,sBAAAA,eACA,EAAE,MAAM,IAAA,GACRD,MAAAA,cAAAA,CAAc,SAAS,WAAoC,MAAM,CACnE;EACA,OAAO;GAAE,GAAG;GAAO,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CAC9F;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,OAAA,GAAME,iBAAAA,uBAAAA,CAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
|
package/dist/react-email.d.cts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EmailConfig } from "./email.cjs";
|
|
2
2
|
import { EmailTemplate } from "./templates.cjs";
|
|
3
|
-
import { EmailRenderer } from "./
|
|
4
|
-
import "./cloudflare/index.cjs";
|
|
3
|
+
import { EmailRenderer } from "./email-sender.cjs";
|
|
5
4
|
//#region src/react-email.d.ts
|
|
6
5
|
export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>, config?: EmailConfig): EmailRenderer;
|
|
7
6
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBAmCgB,iBACd,QAAQ,SAAS,eAAe,iBAChC,SAAS,cACR"}
|
package/dist/react-email.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EmailConfig } from "./email.mjs";
|
|
2
2
|
import { EmailTemplate } from "./templates.mjs";
|
|
3
|
-
import { EmailRenderer } from "./
|
|
4
|
-
import "./cloudflare/index.mjs";
|
|
3
|
+
import { EmailRenderer } from "./email-sender.mjs";
|
|
5
4
|
//#region src/react-email.d.ts
|
|
6
5
|
export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>, config?: EmailConfig): EmailRenderer;
|
|
7
6
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;;wBAmCgB,iBACd,QAAQ,SAAS,eAAe,iBAChC,SAAS,cACR"}
|
package/dist/react-email.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type
|
|
1
|
+
{"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { type EmailConfig, resolveEmail } from './email';\nimport { EmailProvider } from './email-context';\nimport type { EmailRenderer } from './email-sender';\nimport {\n type EmailTemplate,\n indexTemplates,\n lookupTemplate,\n parseTemplateProps,\n} from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element inside an `EmailProvider` carrying the\n * template's resolved metadata (`resolveEmail` against the config's `email`),\n * so hooks and context work and a layout's `useEmailContext()` reads the\n * unsubscribe link the email is sent with; the render returns that metadata\n * with the subject and HTML, for the sender. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(\n emails: Readonly<Record<string, EmailTemplate>>,\n config?: EmailConfig\n): EmailRenderer {\n const index = indexTemplates('email', emails);\n return async (id, props) => {\n const template = lookupTemplate(index, 'email', id);\n const parsed = parseTemplateProps(template, props);\n const email = resolveEmail(template, config);\n const element = createElement(\n EmailProvider,\n { email },\n createElement(template.component as ComponentType<object>, parsed)\n );\n return { ...email, subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,UACJ;AAEF,SAAgB,iBACd,QACA,QACe;CACf,MAAM,QAAQ,eAAe,SAAS,MAAM;CAC5C,OAAO,OAAO,IAAI,UAAU;EAC1B,MAAM,WAAW,eAAe,OAAO,SAAS,EAAE;EAClD,MAAM,SAAS,mBAAmB,UAAU,KAAK;EACjD,MAAM,QAAQ,aAAa,UAAU,MAAM;EAC3C,MAAM,UAAU,cACd,eACA,EAAE,MAAM,GACR,cAAc,SAAS,WAAoC,MAAM,CACnE;EACA,OAAO;GAAE,GAAG;GAAO,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CAC9F;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,MAAM,uBAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
|
|
@@ -16,7 +16,11 @@ function requester(request) {
|
|
|
16
16
|
}
|
|
17
17
|
/** A `/t/...` request's response, or undefined when the path is not a tracking route. */
|
|
18
18
|
async function handleTracking(request, reporting, options = {}) {
|
|
19
|
-
const
|
|
19
|
+
const path = new URL(request.url).pathname.split("/");
|
|
20
|
+
const root = path.at(1);
|
|
21
|
+
const kind = path.at(2);
|
|
22
|
+
const id = path.at(3);
|
|
23
|
+
const position = path.at(4);
|
|
20
24
|
if (root !== "t" || id === void 0 || !UUID.test(id)) return void 0;
|
|
21
25
|
const who = requester(request);
|
|
22
26
|
const read = who.method === "GET" || who.method === "HEAD";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.cjs","names":["isUnsubscribeLink","classifyOpen","BURST_WINDOW_MS","classifyClick"],"sources":["../../src/reporting/tracking.ts"],"sourcesContent":["import { isUnsubscribeLink } from '../email';\nimport type { JourneyReporting } from '../store';\nimport { BURST_WINDOW_MS, classifyClick, classifyOpen } from './classify';\n\n/**\n * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker\n * without a bearer — recipients open them:\n *\n * GET /t/o/<message> the open pixel: records the open, answers a 1×1 GIF\n * GET /t/l/<message>/<position> a link: records the click, redirects to the recorded URL\n * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm\n * POST /t/u/<message> the unsubscribe confirmation\n * GET /t/c/<message>/<position> a link of an email sent before links carried their purpose\n * POST /t/d/<message> a push the device received (iOS's notification service\n * extension, a browser's service worker)\n * POST /t/p/<message> a push opened, reported by a browser's service worker\n *\n * Every open and click is recorded with its verdict (./classify.ts), and the message keeps its\n * first open and click of each kind. The redirect goes only to the URL recorded for that position,\n * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An\n * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was\n * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,\n * which no link scanner sends — counts. Changing a template's or the config's unsubscribe URL, or\n * removing the template, changes nothing for emails already sent.\n *\n * Emails sent before that carry `/t/c/` links, which say nothing of their purpose: one is the\n * unsubscribe link when its recorded URL is `legacyUnsubscribeUrl` (the global unsubscribe URL\n * those emails were all sent with), as it was then.\n */\n\nexport interface TrackingOptions {\n /**\n * The unsubscribe URL emails sent with `/t/c/` links carried (the config's `email.unsubscribeUrl`\n * by default): following such a link to it asks for confirmation instead of redirecting.\n */\n legacyUnsubscribeUrl?: string | undefined;\n /** The name the confirmation pages show (the config's `title`). */\n product?: string | undefined;\n /** `email.tracking.quickClickMs`: how soon after delivery an open or click is a weak machine signal. */\n quickMs?: number | undefined;\n /** A browser reported a push tapped (`/t/p`): the host records it as an event too (`webPush.openedEvent`). */\n pushOpened?:\n | ((open: { messageId: string; userId: string; at: number }) => Promise<void>)\n | undefined;\n}\n\nconst GIF = Uint8Array.from(\n atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),\n (char) => char.charCodeAt(0)\n);\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Requester {\n method: string;\n userAgent: string | null;\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\nfunction requester(request: Request): Requester {\n const cf = (\n request as Request & { cf?: { asn?: number; asOrganization?: string; country?: string } }\n ).cf;\n return {\n method: request.method,\n userAgent: request.headers.get('user-agent'),\n asn: cf?.asn ?? null,\n organization: cf?.asOrganization ?? null,\n country: cf?.country ?? null,\n };\n}\n\n/** A `/t/...` request's response, or undefined when the path is not a tracking route. */\nexport async function handleTracking(\n request: Request,\n reporting: JourneyReporting,\n options: TrackingOptions = {}\n): Promise<Response | undefined> {\n const url = new URL(request.url);\n const [, root, kind, id, position] = url.pathname.split('/');\n if (root !== 't' || id === undefined || !UUID.test(id)) return undefined;\n const who = requester(request);\n const read = who.method === 'GET' || who.method === 'HEAD';\n if (kind === 'o' && read) {\n await recordOpen(reporting, id, who, options);\n return new Response(who.method === 'HEAD' ? null : GIF, {\n headers: { 'content-type': 'image/gif', 'cache-control': 'no-store, max-age=0' },\n });\n }\n if ((kind === 'l' || kind === 'u' || kind === 'c') && position !== undefined && read) {\n const target = await recordClick(reporting, id, Number(position), who, options);\n if (target === undefined) return page(options, 'This link has expired.', 404);\n const unsubscribe =\n kind === 'u' || (kind === 'c' && isUnsubscribeLink(target, options.legacyUnsubscribeUrl));\n if (unsubscribe) {\n return page(\n options,\n `Unsubscribe from ${escape(options.product ?? 'these')} emails?`,\n 200,\n `<form method=\"post\" action=\"/t/u/${id}\"><button type=\"submit\">Unsubscribe</button></form>`\n );\n }\n return Response.redirect(target, 302);\n }\n if (kind === 'd' && who.method === 'POST') {\n const known = await reporting.recordDeviceDelivered(id, Date.now());\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'p' && who.method === 'POST') {\n // A notification tap: a person, as the message id is what proves it is theirs\n const at = Date.now();\n let userId: string | undefined;\n const known = await reporting.recordOpen(id, (message) => {\n userId = message.userId;\n return { at, machine: false, reason: null, ...network(who) };\n });\n if (known && userId !== undefined && options.pushOpened !== undefined) {\n try {\n await options.pushOpened({ messageId: id, userId, at });\n } catch (error) {\n // The open is recorded; the event is the host's extra, never a reason to fail the beacon\n console.error(`[workflow] recording push open ${id} as an event failed`, error);\n }\n }\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'u' && who.method === 'POST') {\n const known = await reporting.recordUnsubscribe(id, Date.now());\n if (!known) return page(options, 'This link has expired.', 404);\n return page(\n options,\n options.product === undefined\n ? \"You won't get these emails anymore.\"\n : `You won't get these emails from ${escape(options.product)} anymore.`\n );\n }\n return undefined;\n}\n\n/**\n * A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on\n * the copy — only the recipient's own, and never a machine, since no proxy or scanner taps a\n * notification. False when the message is unknown or not the user's.\n */\nexport async function recordPushOpen(\n reporting: JourneyReporting,\n userId: string,\n messageId: string,\n at = Date.now()\n): Promise<boolean> {\n if (!UUID.test(messageId)) return false;\n let recorded = false;\n await reporting.recordOpen(messageId, (message) => {\n if (message.userId !== userId) return undefined;\n recorded = true;\n return {\n at,\n machine: false,\n reason: null,\n userAgent: null,\n asn: null,\n organization: null,\n country: null,\n };\n });\n return recorded;\n}\n\nasync function recordOpen(\n reporting: JourneyReporting,\n id: string,\n who: Requester,\n options: TrackingOptions\n) {\n const at = Date.now();\n await reporting.recordOpen(id, (message) => ({\n at,\n ...classifyOpen(\n { ...who, sinceDeliveredMs: at - message.deliveredAt },\n { quickMs: options.quickMs }\n ),\n ...network(who),\n }));\n}\n\nconst network = (who: Requester) => ({\n userAgent: who.userAgent,\n asn: who.asn,\n organization: who.organization,\n country: who.country,\n});\n\n/** Records the click and answers the URL to send the visitor to, or undefined for an unknown link. */\nasync function recordClick(\n reporting: JourneyReporting,\n id: string,\n position: number,\n who: Requester,\n options: TrackingOptions\n): Promise<string | undefined> {\n if (!Number.isInteger(position) || position < 0) return undefined;\n const at = Date.now();\n const recorded = await reporting.recordClick(id, position, at, BURST_WINDOW_MS, (link) => {\n const linksInBurst = new Set([position, ...link.recent.map((click) => click.position)]).size;\n const verdict = classifyClick(\n { ...who, sinceDeliveredMs: at - link.deliveredAt, linksInBurst, linkCount: link.linkCount },\n { quickMs: options.quickMs }\n );\n return {\n engagement: { at, ...verdict, ...network(who) },\n backdate: verdict.reason === 'link_burst' || verdict.reason === 'quick+link_pair',\n };\n });\n return recorded?.url;\n}\n\nfunction escape(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\nfunction page(options: TrackingOptions, message: string, status = 200, extra = ''): Response {\n const title = escape(options.product ?? 'Email');\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;\n return new Response(html, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });\n}\n"],"mappings":";;;;AA8CA,MAAM,MAAM,WAAW,KACrB,KAAK,0DAA0D,IAC9D,SAAS,KAAK,WAAW,CAAC,CAC7B;AACA,MAAM,OAAO;AAUb,SAAS,UAAU,SAA6B;CAC9C,MAAM,KACJ,QACA;CACF,OAAO;EACL,QAAQ,QAAQ;EAChB,WAAW,QAAQ,QAAQ,IAAI,YAAY;EAC3C,KAAK,IAAI,OAAO;EAChB,cAAc,IAAI,kBAAkB;EACpC,SAAS,IAAI,WAAW;CAC1B;AACF;;AAGA,eAAsB,eACpB,SACA,WACA,UAA2B,CAAC,GACG;CAE/B,MAAM,GAAG,MAAM,MAAM,IAAI,YAAY,IADrB,IAAI,QAAQ,GACW,CAAC,CAAC,SAAS,MAAM,GAAG;CAC3D,IAAI,SAAS,OAAO,OAAO,KAAA,KAAa,CAAC,KAAK,KAAK,EAAE,GAAG,OAAO,KAAA;CAC/D,MAAM,MAAM,UAAU,OAAO;CAC7B,MAAM,OAAO,IAAI,WAAW,SAAS,IAAI,WAAW;CACpD,IAAI,SAAS,OAAO,MAAM;EACxB,MAAM,WAAW,WAAW,IAAI,KAAK,OAAO;EAC5C,OAAO,IAAI,SAAS,IAAI,WAAW,SAAS,OAAO,KAAK,EACtD,SAAS;GAAE,gBAAgB;GAAa,iBAAiB;EAAsB,EACjF,CAAC;CACH;CACA,KAAK,SAAS,OAAO,SAAS,OAAO,SAAS,QAAQ,aAAa,KAAA,KAAa,MAAM;EACpF,MAAM,SAAS,MAAM,YAAY,WAAW,IAAI,OAAO,QAAQ,GAAG,KAAK,OAAO;EAC9E,IAAI,WAAW,KAAA,GAAW,OAAO,KAAK,SAAS,0BAA0B,GAAG;EAG5E,IADE,SAAS,OAAQ,SAAS,OAAOA,cAAAA,kBAAkB,QAAQ,QAAQ,oBAAoB,GAEvF,OAAO,KACL,SACA,oBAAoB,OAAO,QAAQ,WAAW,OAAO,EAAE,WACvD,KACA,oCAAoC,GAAG,oDACzC;EAEF,OAAO,SAAS,SAAS,QAAQ,GAAG;CACtC;CACA,IAAI,SAAS,OAAO,IAAI,WAAW,QAAQ;EACzC,MAAM,QAAQ,MAAM,UAAU,sBAAsB,IAAI,KAAK,IAAI,CAAC;EAClE,OAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,QAAQ,MAAM,IAAI,CAAC;CACzD;CACA,IAAI,SAAS,OAAO,IAAI,WAAW,QAAQ;EAEzC,MAAM,KAAK,KAAK,IAAI;EACpB,IAAI;EACJ,MAAM,QAAQ,MAAM,UAAU,WAAW,KAAK,YAAY;GACxD,SAAS,QAAQ;GACjB,OAAO;IAAE;IAAI,SAAS;IAAO,QAAQ;IAAM,GAAG,QAAQ,GAAG;GAAE;EAC7D,CAAC;EACD,IAAI,SAAS,WAAW,KAAA,KAAa,QAAQ,eAAe,KAAA,GAC1D,IAAI;GACF,MAAM,QAAQ,WAAW;IAAE,WAAW;IAAI;IAAQ;GAAG,CAAC;EACxD,SAAS,OAAO;GAEd,QAAQ,MAAM,kCAAkC,GAAG,sBAAsB,KAAK;EAChF;EAEF,OAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,QAAQ,MAAM,IAAI,CAAC;CACzD;CACA,IAAI,SAAS,OAAO,IAAI,WAAW,QAAQ;EAEzC,IAAI,CAAC,MADe,UAAU,kBAAkB,IAAI,KAAK,IAAI,CAAC,GAClD,OAAO,KAAK,SAAS,0BAA0B,GAAG;EAC9D,OAAO,KACL,SACA,QAAQ,YAAY,KAAA,IAChB,wCACA,mCAAmC,OAAO,QAAQ,OAAO,EAAE,UACjE;CACF;AAEF;;;;;;AAOA,eAAsB,eACpB,WACA,QACA,WACA,KAAK,KAAK,IAAI,GACI;CAClB,IAAI,CAAC,KAAK,KAAK,SAAS,GAAG,OAAO;CAClC,IAAI,WAAW;CACf,MAAM,UAAU,WAAW,YAAY,YAAY;EACjD,IAAI,QAAQ,WAAW,QAAQ,OAAO,KAAA;EACtC,WAAW;EACX,OAAO;GACL;GACA,SAAS;GACT,QAAQ;GACR,WAAW;GACX,KAAK;GACL,cAAc;GACd,SAAS;EACX;CACF,CAAC;CACD,OAAO;AACT;AAEA,eAAe,WACb,WACA,IACA,KACA,SACA;CACA,MAAM,KAAK,KAAK,IAAI;CACpB,MAAM,UAAU,WAAW,KAAK,aAAa;EAC3C;EACA,GAAGC,2BAAAA,aACD;GAAE,GAAG;GAAK,kBAAkB,KAAK,QAAQ;EAAY,GACrD,EAAE,SAAS,QAAQ,QAAQ,CAC7B;EACA,GAAG,QAAQ,GAAG;CAChB,EAAE;AACJ;AAEA,MAAM,WAAW,SAAoB;CACnC,WAAW,IAAI;CACf,KAAK,IAAI;CACT,cAAc,IAAI;CAClB,SAAS,IAAI;AACf;;AAGA,eAAe,YACb,WACA,IACA,UACA,KACA,SAC6B;CAC7B,IAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,GAAG,OAAO,KAAA;CACxD,MAAM,KAAK,KAAK,IAAI;CAYpB,QAAO,MAXgB,UAAU,YAAY,IAAI,UAAU,IAAIC,2BAAAA,kBAAkB,SAAS;EACxF,MAAM,gCAAe,IAAI,IAAI,CAAC,UAAU,GAAG,KAAK,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC,EAAA,CAAE;EACxF,MAAM,UAAUC,2BAAAA,cACd;GAAE,GAAG;GAAK,kBAAkB,KAAK,KAAK;GAAa;GAAc,WAAW,KAAK;EAAU,GAC3F,EAAE,SAAS,QAAQ,QAAQ,CAC7B;EACA,OAAO;GACL,YAAY;IAAE;IAAI,GAAG;IAAS,GAAG,QAAQ,GAAG;GAAE;GAC9C,UAAU,QAAQ,WAAW,gBAAgB,QAAQ,WAAW;EAClE;CACF,CAAC,EAAA,EACgB;AACnB;AAEA,SAAS,OAAO,MAAsB;CACpC,OAAO,KAAK,QAAQ,aAAa,MAAM,KAAK,EAAE,WAAW,CAAC,EAAE,EAAE;AAChE;AAEA,SAAS,KAAK,SAA0B,SAAiB,SAAS,KAAK,QAAQ,IAAc;CAE3F,MAAM,OAAO,8HADC,OAAO,QAAQ,WAAW,OACuG,EAAE,wTAAwT,QAAQ,MAAM,MAAM;CAC7d,OAAO,IAAI,SAAS,MAAM;EAAE;EAAQ,SAAS,EAAE,gBAAgB,2BAA2B;CAAE,CAAC;AAC/F"}
|
|
1
|
+
{"version":3,"file":"tracking.cjs","names":["isUnsubscribeLink","classifyOpen","BURST_WINDOW_MS","classifyClick"],"sources":["../../src/reporting/tracking.ts"],"sourcesContent":["import { isUnsubscribeLink } from '../email';\nimport type { JourneyReporting } from '../store';\nimport { BURST_WINDOW_MS, classifyClick, classifyOpen } from './classify';\n\n/**\n * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker\n * without a bearer — recipients open them:\n *\n * GET /t/o/<message> the open pixel: records the open, answers a 1×1 GIF\n * GET /t/l/<message>/<position> a link: records the click, redirects to the recorded URL\n * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm\n * POST /t/u/<message> the unsubscribe confirmation\n * GET /t/c/<message>/<position> a link of an email sent before links carried their purpose\n * POST /t/d/<message> a push the device received (iOS's notification service\n * extension, a browser's service worker)\n * POST /t/p/<message> a push opened, reported by a browser's service worker\n *\n * Every open and click is recorded with its verdict (./classify.ts), and the message keeps its\n * first open and click of each kind. The redirect goes only to the URL recorded for that position,\n * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An\n * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was\n * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,\n * which no link scanner sends — counts. Changing a template's or the config's unsubscribe URL, or\n * removing the template, changes nothing for emails already sent.\n *\n * Emails sent before that carry `/t/c/` links, which say nothing of their purpose: one is the\n * unsubscribe link when its recorded URL is `legacyUnsubscribeUrl` (the global unsubscribe URL\n * those emails were all sent with), as it was then.\n */\n\nexport interface TrackingOptions {\n /**\n * The unsubscribe URL emails sent with `/t/c/` links carried (the config's `email.unsubscribeUrl`\n * by default): following such a link to it asks for confirmation instead of redirecting.\n */\n legacyUnsubscribeUrl?: string | undefined;\n /** The name the confirmation pages show (the config's `title`). */\n product?: string | undefined;\n /** `email.tracking.quickClickMs`: how soon after delivery an open or click is a weak machine signal. */\n quickMs?: number | undefined;\n /** A browser reported a push tapped (`/t/p`): the host records it as an event too (`webPush.openedEvent`). */\n pushOpened?:\n | ((open: { messageId: string; userId: string; at: number }) => Promise<void>)\n | undefined;\n}\n\nconst GIF = Uint8Array.from(\n atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),\n (char) => char.charCodeAt(0)\n);\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Requester {\n method: string;\n userAgent: string | null;\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\nfunction requester(request: Request): Requester {\n const cf = (\n request as Request & { cf?: { asn?: number; asOrganization?: string; country?: string } }\n ).cf;\n return {\n method: request.method,\n userAgent: request.headers.get('user-agent'),\n asn: cf?.asn ?? null,\n organization: cf?.asOrganization ?? null,\n country: cf?.country ?? null,\n };\n}\n\n/** A `/t/...` request's response, or undefined when the path is not a tracking route. */\nexport async function handleTracking(\n request: Request,\n reporting: JourneyReporting,\n options: TrackingOptions = {}\n): Promise<Response | undefined> {\n const url = new URL(request.url);\n const path = url.pathname.split('/');\n const root = path.at(1);\n const kind = path.at(2);\n const id = path.at(3);\n const position = path.at(4);\n if (root !== 't' || id === undefined || !UUID.test(id)) return undefined;\n const who = requester(request);\n const read = who.method === 'GET' || who.method === 'HEAD';\n if (kind === 'o' && read) {\n await recordOpen(reporting, id, who, options);\n return new Response(who.method === 'HEAD' ? null : GIF, {\n headers: { 'content-type': 'image/gif', 'cache-control': 'no-store, max-age=0' },\n });\n }\n if ((kind === 'l' || kind === 'u' || kind === 'c') && position !== undefined && read) {\n const target = await recordClick(reporting, id, Number(position), who, options);\n if (target === undefined) return page(options, 'This link has expired.', 404);\n const unsubscribe =\n kind === 'u' || (kind === 'c' && isUnsubscribeLink(target, options.legacyUnsubscribeUrl));\n if (unsubscribe) {\n return page(\n options,\n `Unsubscribe from ${escape(options.product ?? 'these')} emails?`,\n 200,\n `<form method=\"post\" action=\"/t/u/${id}\"><button type=\"submit\">Unsubscribe</button></form>`\n );\n }\n return Response.redirect(target, 302);\n }\n if (kind === 'd' && who.method === 'POST') {\n const known = await reporting.recordDeviceDelivered(id, Date.now());\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'p' && who.method === 'POST') {\n // A notification tap: a person, as the message id is what proves it is theirs\n const at = Date.now();\n let userId: string | undefined;\n const known = await reporting.recordOpen(id, (message) => {\n userId = message.userId;\n return { at, machine: false, reason: null, ...network(who) };\n });\n if (known && userId !== undefined && options.pushOpened !== undefined) {\n try {\n await options.pushOpened({ messageId: id, userId, at });\n } catch (error) {\n // The open is recorded; the event is the host's extra, never a reason to fail the beacon\n console.error(`[workflow] recording push open ${id} as an event failed`, error);\n }\n }\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'u' && who.method === 'POST') {\n const known = await reporting.recordUnsubscribe(id, Date.now());\n if (!known) return page(options, 'This link has expired.', 404);\n return page(\n options,\n options.product === undefined\n ? \"You won't get these emails anymore.\"\n : `You won't get these emails from ${escape(options.product)} anymore.`\n );\n }\n return undefined;\n}\n\n/**\n * A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on\n * the copy — only the recipient's own, and never a machine, since no proxy or scanner taps a\n * notification. False when the message is unknown or not the user's.\n */\nexport async function recordPushOpen(\n reporting: JourneyReporting,\n userId: string,\n messageId: string,\n at = Date.now()\n): Promise<boolean> {\n if (!UUID.test(messageId)) return false;\n let recorded = false;\n await reporting.recordOpen(messageId, (message) => {\n if (message.userId !== userId) return undefined;\n recorded = true;\n return {\n at,\n machine: false,\n reason: null,\n userAgent: null,\n asn: null,\n organization: null,\n country: null,\n };\n });\n return recorded;\n}\n\nasync function recordOpen(\n reporting: JourneyReporting,\n id: string,\n who: Requester,\n options: TrackingOptions\n) {\n const at = Date.now();\n await reporting.recordOpen(id, (message) => ({\n at,\n ...classifyOpen(\n { ...who, sinceDeliveredMs: at - message.deliveredAt },\n { quickMs: options.quickMs }\n ),\n ...network(who),\n }));\n}\n\nconst network = (who: Requester) => ({\n userAgent: who.userAgent,\n asn: who.asn,\n organization: who.organization,\n country: who.country,\n});\n\n/** Records the click and answers the URL to send the visitor to, or undefined for an unknown link. */\nasync function recordClick(\n reporting: JourneyReporting,\n id: string,\n position: number,\n who: Requester,\n options: TrackingOptions\n): Promise<string | undefined> {\n if (!Number.isInteger(position) || position < 0) return undefined;\n const at = Date.now();\n const recorded = await reporting.recordClick(id, position, at, BURST_WINDOW_MS, (link) => {\n const linksInBurst = new Set([position, ...link.recent.map((click) => click.position)]).size;\n const verdict = classifyClick(\n { ...who, sinceDeliveredMs: at - link.deliveredAt, linksInBurst, linkCount: link.linkCount },\n { quickMs: options.quickMs }\n );\n return {\n engagement: { at, ...verdict, ...network(who) },\n backdate: verdict.reason === 'link_burst' || verdict.reason === 'quick+link_pair',\n };\n });\n return recorded?.url;\n}\n\nfunction escape(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\nfunction page(options: TrackingOptions, message: string, status = 200, extra = ''): Response {\n const title = escape(options.product ?? 'Email');\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;\n return new Response(html, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.d.cts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;;;;EAKf;;EAEA;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;wBAgC9C,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;
|
|
1
|
+
{"version":3,"file":"tracking.d.cts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;;;;EAKf;;EAEA;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;wBAgC9C,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAuEW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.d.mts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;;;;EAKf;;EAEA;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;wBAgC9C,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;
|
|
1
|
+
{"version":3,"file":"tracking.d.mts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;;;;EAKf;;EAEA;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;wBAgC9C,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAuEW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
|
|
@@ -15,7 +15,11 @@ function requester(request) {
|
|
|
15
15
|
}
|
|
16
16
|
/** A `/t/...` request's response, or undefined when the path is not a tracking route. */
|
|
17
17
|
async function handleTracking(request, reporting, options = {}) {
|
|
18
|
-
const
|
|
18
|
+
const path = new URL(request.url).pathname.split("/");
|
|
19
|
+
const root = path.at(1);
|
|
20
|
+
const kind = path.at(2);
|
|
21
|
+
const id = path.at(3);
|
|
22
|
+
const position = path.at(4);
|
|
19
23
|
if (root !== "t" || id === void 0 || !UUID.test(id)) return void 0;
|
|
20
24
|
const who = requester(request);
|
|
21
25
|
const read = who.method === "GET" || who.method === "HEAD";
|