@shware/workflow 4.0.0 → 4.1.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 +141 -13
- package/dist/aws/index.cjs +3 -0
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -1
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -1
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +2 -1
- package/dist/aws/index.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +1 -0
- package/dist/cloudflare/index.d.cts +2 -2
- package/dist/cloudflare/index.d.mts +2 -2
- package/dist/cloudflare/index.mjs +2 -2
- package/dist/host/auth.cjs +15 -15
- package/dist/host/auth.cjs.map +1 -1
- package/dist/host/auth.d.cts +16 -2
- package/dist/host/auth.d.cts.map +1 -1
- package/dist/host/auth.d.mts +16 -2
- package/dist/host/auth.d.mts.map +1 -1
- package/dist/host/auth.mjs +15 -16
- package/dist/host/auth.mjs.map +1 -1
- package/dist/host/http.cjs +37 -18
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +5 -1
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +5 -1
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +37 -18
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/journey-host.cjs +1 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +12 -0
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +12 -0
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +1 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/package.json +1 -1
package/dist/host/auth.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.cjs","names":[],"sources":["../../src/host/auth.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"auth.cjs","names":["timingSafeEqualBytes"],"sources":["../../src/host/auth.ts"],"sourcesContent":["import { timingSafeEqual as timingSafeEqualBytes } from 'node:crypto';\n\n/**\n * Who may call the host's protected routes. By default the env's `API_TOKEN` as a bearer; a host\n * replaces it with its own `Authorize` (an HMAC over the body, a JWT, a header its proxy adds).\n * The helpers here are what such an authorizer builds on.\n */\n\n/**\n * The host's authorization: receives a copy of the request (so it may read the body) and the\n * env. True lets the request through, false answers 401, a throw answers 500 and is logged — an\n * authorizer that fails never lets a request in. The tracking routes (`/t/...`) and `/health`\n * are public and never reach it.\n */\nexport type Authorize<Env> = (request: Request, env: Env) => boolean | Promise<boolean>;\n\nconst utf8 = new TextEncoder();\n\n/**\n * `node:crypto`'s constant-time compare — on Node, Lambda, and a Worker with `nodejs_compat`\n * (which the host needs anyway). It demands equal lengths, so callers hash first or check the\n * length; a mismatch on length answers at once, which reveals the length and nothing else.\n */\nfunction constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n return a.length === b.length && timingSafeEqualBytes(a, b);\n}\n\n/**\n * Whether two secrets are equal, in time that does not depend on where they differ: both are\n * hashed first, so the comparison runs over equal-length digests whatever their lengths — for an\n * authorizer comparing a signature or a token of its own.\n */\nexport async function timingSafeEqual(a: string, b: string): Promise<boolean> {\n const [x, y] = await Promise.all([\n crypto.subtle.digest('SHA-256', utf8.encode(a)),\n crypto.subtle.digest('SHA-256', utf8.encode(b)),\n ]);\n return constantTimeEqual(new Uint8Array(x), new Uint8Array(y));\n}\n\n/**\n * Whether the request carries `token` as a bearer credential (`Authorization: Bearer <token>`).\n * The scheme is matched case-insensitively, as RFC 9110 says schemes are; the token is compared\n * in constant time.\n */\nexport function bearerMatches(request: Request, token: string): boolean {\n const header = request.headers.get('authorization');\n if (header === null) return false;\n const space = header.indexOf(' ');\n if (space === -1) return false;\n if (header.slice(0, space).toLowerCase() !== 'bearer') return false;\n const presented = header.slice(space + 1).trim();\n return constantTimeEqual(utf8.encode(presented), utf8.encode(token));\n}\n\n/**\n * Whether the request may use the API: always when no token is configured (an open development\n * instance — production refuses to run that way, see `createJourneyHost`), otherwise only with\n * the matching bearer.\n */\nexport function authorized(request: Request, env: { API_TOKEN?: string | undefined }): boolean {\n return env.API_TOKEN === undefined || bearerMatches(request, env.API_TOKEN);\n}\n"],"mappings":";;;AAgBA,MAAM,OAAO,IAAI,YAAY;;;;;;AAO7B,SAAS,kBAAkB,GAAe,GAAwB;CAChE,OAAO,EAAE,WAAW,EAAE,WAAA,GAAUA,YAAAA,gBAAAA,CAAqB,GAAG,CAAC;AAC3D;;;;;;AAOA,eAAsB,gBAAgB,GAAW,GAA6B;CAC5E,MAAM,CAAC,GAAG,KAAK,MAAM,QAAQ,IAAI,CAC/B,OAAO,OAAO,OAAO,WAAW,KAAK,OAAO,CAAC,CAAC,GAC9C,OAAO,OAAO,OAAO,WAAW,KAAK,OAAO,CAAC,CAAC,CAChD,CAAC;CACD,OAAO,kBAAkB,IAAI,WAAW,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC;AAC/D;;;;;;AAOA,SAAgB,cAAc,SAAkB,OAAwB;CACtE,MAAM,SAAS,QAAQ,QAAQ,IAAI,eAAe;CAClD,IAAI,WAAW,MAAM,OAAO;CAC5B,MAAM,QAAQ,OAAO,QAAQ,GAAG;CAChC,IAAI,UAAU,IAAI,OAAO;CACzB,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY,MAAM,UAAU,OAAO;CAC9D,MAAM,YAAY,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK;CAC/C,OAAO,kBAAkB,KAAK,OAAO,SAAS,GAAG,KAAK,OAAO,KAAK,CAAC;AACrE;;;;;;AAOA,SAAgB,WAAW,SAAkB,KAAkD;CAC7F,OAAO,IAAI,cAAc,KAAA,KAAa,cAAc,SAAS,IAAI,SAAS;AAC5E"}
|
package/dist/host/auth.d.cts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
//#region src/host/auth.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Who may call the host's protected routes. By default the env's `API_TOKEN` as a bearer; a host
|
|
4
|
+
* replaces it with its own `Authorize` (an HMAC over the body, a JWT, a header its proxy adds).
|
|
5
|
+
* The helpers here are what such an authorizer builds on.
|
|
5
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* The host's authorization: receives a copy of the request (so it may read the body) and the
|
|
9
|
+
* env. True lets the request through, false answers 401, a throw answers 500 and is logged — an
|
|
10
|
+
* authorizer that fails never lets a request in. The tracking routes (`/t/...`) and `/health`
|
|
11
|
+
* are public and never reach it.
|
|
12
|
+
*/
|
|
13
|
+
export type Authorize<Env> = (request: Request, env: Env) => boolean | Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether two secrets are equal, in time that does not depend on where they differ: both are
|
|
16
|
+
* hashed first, so the comparison runs over equal-length digests whatever their lengths — for an
|
|
17
|
+
* authorizer comparing a signature or a token of its own.
|
|
18
|
+
*/
|
|
19
|
+
export declare function timingSafeEqual(a: string, b: string): Promise<boolean>;
|
|
6
20
|
/**
|
|
7
21
|
* Whether the request carries `token` as a bearer credential (`Authorization: Bearer <token>`).
|
|
8
22
|
* The scheme is matched case-insensitively, as RFC 9110 says schemes are; the token is compared
|
package/dist/host/auth.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.cts","names":[],"sources":["../../src/host/auth.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.cts","names":[],"sources":["../../src/host/auth.ts"],"mappings":";;;;;;;;;;;;YAcY,UAAU,QAAQ,SAAS,SAAS,KAAK,kBAAkB;;;;;;wBAkBjD,gBAAgB,WAAW,YAAY;;;;;;wBAa7C,cAAc,SAAS,SAAS;;;;;;wBAehC,WAAW,SAAS,SAAS;EAAO"}
|
package/dist/host/auth.d.mts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
//#region src/host/auth.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Who may call the host's protected routes. By default the env's `API_TOKEN` as a bearer; a host
|
|
4
|
+
* replaces it with its own `Authorize` (an HMAC over the body, a JWT, a header its proxy adds).
|
|
5
|
+
* The helpers here are what such an authorizer builds on.
|
|
5
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* The host's authorization: receives a copy of the request (so it may read the body) and the
|
|
9
|
+
* env. True lets the request through, false answers 401, a throw answers 500 and is logged — an
|
|
10
|
+
* authorizer that fails never lets a request in. The tracking routes (`/t/...`) and `/health`
|
|
11
|
+
* are public and never reach it.
|
|
12
|
+
*/
|
|
13
|
+
export type Authorize<Env> = (request: Request, env: Env) => boolean | Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether two secrets are equal, in time that does not depend on where they differ: both are
|
|
16
|
+
* hashed first, so the comparison runs over equal-length digests whatever their lengths — for an
|
|
17
|
+
* authorizer comparing a signature or a token of its own.
|
|
18
|
+
*/
|
|
19
|
+
export declare function timingSafeEqual(a: string, b: string): Promise<boolean>;
|
|
6
20
|
/**
|
|
7
21
|
* Whether the request carries `token` as a bearer credential (`Authorization: Bearer <token>`).
|
|
8
22
|
* The scheme is matched case-insensitively, as RFC 9110 says schemes are; the token is compared
|
package/dist/host/auth.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.mts","names":[],"sources":["../../src/host/auth.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth.d.mts","names":[],"sources":["../../src/host/auth.ts"],"mappings":";;;;;;;;;;;;YAcY,UAAU,QAAQ,SAAS,SAAS,KAAK,kBAAkB;;;;;;wBAkBjD,gBAAgB,WAAW,YAAY;;;;;;wBAa7C,cAAc,SAAS,SAAS;;;;;;wBAehC,WAAW,SAAS,SAAS;EAAO"}
|
package/dist/host/auth.mjs
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
+
import { timingSafeEqual as timingSafeEqual$1 } from "node:crypto";
|
|
1
2
|
//#region src/host/auth.ts
|
|
2
|
-
/**
|
|
3
|
-
* Bearer authentication for the host's HTTP API, shared by the ingest router and the journey
|
|
4
|
-
* host so the two cannot drift apart.
|
|
5
|
-
*/
|
|
6
3
|
const utf8 = new TextEncoder();
|
|
7
4
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* answers at once), which is accepted: it is not the secret.
|
|
12
|
-
*
|
|
13
|
-
* `crypto.subtle.timingSafeEqual` is not available on every runtime the host runs on, hence
|
|
14
|
-
* the XOR loop.
|
|
5
|
+
* `node:crypto`'s constant-time compare — on Node, Lambda, and a Worker with `nodejs_compat`
|
|
6
|
+
* (which the host needs anyway). It demands equal lengths, so callers hash first or check the
|
|
7
|
+
* length; a mismatch on length answers at once, which reveals the length and nothing else.
|
|
15
8
|
*/
|
|
16
9
|
function constantTimeEqual(a, b) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
return a.length === b.length && timingSafeEqual$1(a, b);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether two secrets are equal, in time that does not depend on where they differ: both are
|
|
14
|
+
* hashed first, so the comparison runs over equal-length digests whatever their lengths — for an
|
|
15
|
+
* authorizer comparing a signature or a token of its own.
|
|
16
|
+
*/
|
|
17
|
+
async function timingSafeEqual(a, b) {
|
|
18
|
+
const [x, y] = await Promise.all([crypto.subtle.digest("SHA-256", utf8.encode(a)), crypto.subtle.digest("SHA-256", utf8.encode(b))]);
|
|
19
|
+
return constantTimeEqual(new Uint8Array(x), new Uint8Array(y));
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
22
|
* Whether the request carries `token` as a bearer credential (`Authorization: Bearer <token>`).
|
|
@@ -42,6 +41,6 @@ function authorized(request, env) {
|
|
|
42
41
|
return env.API_TOKEN === void 0 || bearerMatches(request, env.API_TOKEN);
|
|
43
42
|
}
|
|
44
43
|
//#endregion
|
|
45
|
-
export { authorized, bearerMatches };
|
|
44
|
+
export { authorized, bearerMatches, timingSafeEqual };
|
|
46
45
|
|
|
47
46
|
//# sourceMappingURL=auth.mjs.map
|
package/dist/host/auth.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/host/auth.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":["timingSafeEqualBytes"],"sources":["../../src/host/auth.ts"],"sourcesContent":["import { timingSafeEqual as timingSafeEqualBytes } from 'node:crypto';\n\n/**\n * Who may call the host's protected routes. By default the env's `API_TOKEN` as a bearer; a host\n * replaces it with its own `Authorize` (an HMAC over the body, a JWT, a header its proxy adds).\n * The helpers here are what such an authorizer builds on.\n */\n\n/**\n * The host's authorization: receives a copy of the request (so it may read the body) and the\n * env. True lets the request through, false answers 401, a throw answers 500 and is logged — an\n * authorizer that fails never lets a request in. The tracking routes (`/t/...`) and `/health`\n * are public and never reach it.\n */\nexport type Authorize<Env> = (request: Request, env: Env) => boolean | Promise<boolean>;\n\nconst utf8 = new TextEncoder();\n\n/**\n * `node:crypto`'s constant-time compare — on Node, Lambda, and a Worker with `nodejs_compat`\n * (which the host needs anyway). It demands equal lengths, so callers hash first or check the\n * length; a mismatch on length answers at once, which reveals the length and nothing else.\n */\nfunction constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n return a.length === b.length && timingSafeEqualBytes(a, b);\n}\n\n/**\n * Whether two secrets are equal, in time that does not depend on where they differ: both are\n * hashed first, so the comparison runs over equal-length digests whatever their lengths — for an\n * authorizer comparing a signature or a token of its own.\n */\nexport async function timingSafeEqual(a: string, b: string): Promise<boolean> {\n const [x, y] = await Promise.all([\n crypto.subtle.digest('SHA-256', utf8.encode(a)),\n crypto.subtle.digest('SHA-256', utf8.encode(b)),\n ]);\n return constantTimeEqual(new Uint8Array(x), new Uint8Array(y));\n}\n\n/**\n * Whether the request carries `token` as a bearer credential (`Authorization: Bearer <token>`).\n * The scheme is matched case-insensitively, as RFC 9110 says schemes are; the token is compared\n * in constant time.\n */\nexport function bearerMatches(request: Request, token: string): boolean {\n const header = request.headers.get('authorization');\n if (header === null) return false;\n const space = header.indexOf(' ');\n if (space === -1) return false;\n if (header.slice(0, space).toLowerCase() !== 'bearer') return false;\n const presented = header.slice(space + 1).trim();\n return constantTimeEqual(utf8.encode(presented), utf8.encode(token));\n}\n\n/**\n * Whether the request may use the API: always when no token is configured (an open development\n * instance — production refuses to run that way, see `createJourneyHost`), otherwise only with\n * the matching bearer.\n */\nexport function authorized(request: Request, env: { API_TOKEN?: string | undefined }): boolean {\n return env.API_TOKEN === undefined || bearerMatches(request, env.API_TOKEN);\n}\n"],"mappings":";;AAgBA,MAAM,OAAO,IAAI,YAAY;;;;;;AAO7B,SAAS,kBAAkB,GAAe,GAAwB;CAChE,OAAO,EAAE,WAAW,EAAE,UAAUA,kBAAqB,GAAG,CAAC;AAC3D;;;;;;AAOA,eAAsB,gBAAgB,GAAW,GAA6B;CAC5E,MAAM,CAAC,GAAG,KAAK,MAAM,QAAQ,IAAI,CAC/B,OAAO,OAAO,OAAO,WAAW,KAAK,OAAO,CAAC,CAAC,GAC9C,OAAO,OAAO,OAAO,WAAW,KAAK,OAAO,CAAC,CAAC,CAChD,CAAC;CACD,OAAO,kBAAkB,IAAI,WAAW,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC;AAC/D;;;;;;AAOA,SAAgB,cAAc,SAAkB,OAAwB;CACtE,MAAM,SAAS,QAAQ,QAAQ,IAAI,eAAe;CAClD,IAAI,WAAW,MAAM,OAAO;CAC5B,MAAM,QAAQ,OAAO,QAAQ,GAAG;CAChC,IAAI,UAAU,IAAI,OAAO;CACzB,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY,MAAM,UAAU,OAAO;CAC9D,MAAM,YAAY,OAAO,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK;CAC/C,OAAO,kBAAkB,KAAK,OAAO,SAAS,GAAG,KAAK,OAAO,KAAK,CAAC;AACrE;;;;;;AAOA,SAAgB,WAAW,SAAkB,KAAkD;CAC7F,OAAO,IAAI,cAAc,KAAA,KAAa,cAAc,SAAS,IAAI,SAAS;AAC5E"}
|
package/dist/host/http.cjs
CHANGED
|
@@ -90,7 +90,7 @@ const routes = [
|
|
|
90
90
|
{
|
|
91
91
|
method: "POST",
|
|
92
92
|
path: "/events",
|
|
93
|
-
auth: "
|
|
93
|
+
auth: "api",
|
|
94
94
|
async handle({ request, env, store, validation }) {
|
|
95
95
|
const { id, userId, event, payload, ts } = await readBody(request);
|
|
96
96
|
if (!isNonEmptyString(userId) || !isNonEmptyString(event)) return json({ error: "userId and event required" }, 400);
|
|
@@ -116,7 +116,7 @@ const routes = [
|
|
|
116
116
|
{
|
|
117
117
|
method: "POST",
|
|
118
118
|
path: "/identify",
|
|
119
|
-
auth: "
|
|
119
|
+
auth: "api",
|
|
120
120
|
async handle({ request, env, store, validation }) {
|
|
121
121
|
const { userId, props } = await readBody(request);
|
|
122
122
|
if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
|
|
@@ -134,7 +134,7 @@ const routes = [
|
|
|
134
134
|
{
|
|
135
135
|
method: "POST",
|
|
136
136
|
path: "/devices",
|
|
137
|
-
auth: "
|
|
137
|
+
auth: "api",
|
|
138
138
|
async handle({ request, env, store }) {
|
|
139
139
|
const { userId, token, platform, ts } = await readBody(request);
|
|
140
140
|
if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
|
|
@@ -153,7 +153,7 @@ const routes = [
|
|
|
153
153
|
{
|
|
154
154
|
method: "POST",
|
|
155
155
|
path: "/devices/unbind",
|
|
156
|
-
auth: "
|
|
156
|
+
auth: "api",
|
|
157
157
|
async handle({ request, env, store }) {
|
|
158
158
|
const { token } = await readBody(request);
|
|
159
159
|
if (!isNonEmptyString(token)) return json({ error: "token required" }, 400);
|
|
@@ -163,7 +163,7 @@ const routes = [
|
|
|
163
163
|
{
|
|
164
164
|
method: "POST",
|
|
165
165
|
path: "/devices/remove",
|
|
166
|
-
auth: "
|
|
166
|
+
auth: "api",
|
|
167
167
|
async handle({ request, env, store }) {
|
|
168
168
|
const { token } = await readBody(request);
|
|
169
169
|
if (!isNonEmptyString(token)) return json({ error: "token required" }, 400);
|
|
@@ -173,7 +173,7 @@ const routes = [
|
|
|
173
173
|
{
|
|
174
174
|
method: "POST",
|
|
175
175
|
path: "/messages/opened",
|
|
176
|
-
auth: "
|
|
176
|
+
auth: "api",
|
|
177
177
|
async handle({ request, store }) {
|
|
178
178
|
const { userId, messageId } = await readBody(request);
|
|
179
179
|
if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
|
|
@@ -188,7 +188,7 @@ const routes = [
|
|
|
188
188
|
{
|
|
189
189
|
method: "POST",
|
|
190
190
|
path: "/users/forget",
|
|
191
|
-
auth: "
|
|
191
|
+
auth: "api",
|
|
192
192
|
async handle({ request, env, store }) {
|
|
193
193
|
const { userId } = await readBody(request);
|
|
194
194
|
if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
|
|
@@ -198,7 +198,7 @@ const routes = [
|
|
|
198
198
|
{
|
|
199
199
|
method: "POST",
|
|
200
200
|
path: "/deploy",
|
|
201
|
-
auth: "
|
|
201
|
+
auth: "api",
|
|
202
202
|
async handle({ request, env, store }) {
|
|
203
203
|
return json(await require_host_ingest.deployBundle(env, await readJson(request), store));
|
|
204
204
|
}
|
|
@@ -206,7 +206,7 @@ const routes = [
|
|
|
206
206
|
{
|
|
207
207
|
method: "GET",
|
|
208
208
|
path: "/bundle",
|
|
209
|
-
auth: "
|
|
209
|
+
auth: "api",
|
|
210
210
|
async handle({ host }) {
|
|
211
211
|
return host.bundle === void 0 ? notFound() : json(host.bundle());
|
|
212
212
|
}
|
|
@@ -214,7 +214,7 @@ const routes = [
|
|
|
214
214
|
{
|
|
215
215
|
method: "POST",
|
|
216
216
|
path: "/bundle/deploy",
|
|
217
|
-
auth: "
|
|
217
|
+
auth: "api",
|
|
218
218
|
async handle({ env, store, host }) {
|
|
219
219
|
if (host.bundle === void 0) return notFound();
|
|
220
220
|
return json(await require_host_ingest.deployBundle(env, host.bundle(), store));
|
|
@@ -223,7 +223,7 @@ const routes = [
|
|
|
223
223
|
{
|
|
224
224
|
method: "POST",
|
|
225
225
|
path: "/segments/sweep",
|
|
226
|
-
auth: "
|
|
226
|
+
auth: "api",
|
|
227
227
|
async handle({ host }) {
|
|
228
228
|
return host.sweep === void 0 ? notFound() : json(await host.sweep());
|
|
229
229
|
}
|
|
@@ -231,7 +231,7 @@ const routes = [
|
|
|
231
231
|
{
|
|
232
232
|
method: "GET",
|
|
233
233
|
path: /^\/preview\//,
|
|
234
|
-
auth: "
|
|
234
|
+
auth: "api",
|
|
235
235
|
async handle({ url, host }) {
|
|
236
236
|
const { preview } = host;
|
|
237
237
|
if (preview === void 0) return notFound();
|
|
@@ -258,11 +258,21 @@ function matches(path, pathname) {
|
|
|
258
258
|
return path(pathname);
|
|
259
259
|
}
|
|
260
260
|
/**
|
|
261
|
-
* Production without a bearer token
|
|
262
|
-
* ingest, identify and deploy. Only what recipients open
|
|
261
|
+
* Production without a bearer token (and no authorizer of the host's own) is not an open
|
|
262
|
+
* instance, it is a mistake: anyone could ingest, identify and deploy. Only what recipients open
|
|
263
|
+
* (`/t/*`) and the health check stay up.
|
|
263
264
|
*/
|
|
264
|
-
function refusesOpen(request,
|
|
265
|
-
return env.API_TOKEN === void 0 && require_host_channels.journeyMode(env) === "production" && !pathname.startsWith("/t/") && !(request.method === "GET" && pathname === "/health");
|
|
265
|
+
function refusesOpen(request, ctx, pathname) {
|
|
266
|
+
return ctx.authorize === void 0 && ctx.env.API_TOKEN === void 0 && require_host_channels.journeyMode(ctx.env) === "production" && !pathname.startsWith("/t/") && !(request.method === "GET" && pathname === "/health");
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Whether the caller may use an API route: the host's authorizer on a copy of the request (so a
|
|
270
|
+
* body it verifies is still there for the route), or the env's `API_TOKEN` as a bearer. An
|
|
271
|
+
* authorizer that throws is a failure, never an admission.
|
|
272
|
+
*/
|
|
273
|
+
async function allowed(request, ctx) {
|
|
274
|
+
if (ctx.authorize === void 0) return require_host_auth.authorized(request, ctx.env);
|
|
275
|
+
return ctx.authorize(request.clone(), ctx.env);
|
|
266
276
|
}
|
|
267
277
|
/**
|
|
268
278
|
* What a failed request answers. Validation problems are the caller's to fix (a 400, saying
|
|
@@ -287,10 +297,19 @@ function failure(error) {
|
|
|
287
297
|
/** One request through the table: the production gate, the bearer, the route, the error mapping. */
|
|
288
298
|
async function serve(request, ctx) {
|
|
289
299
|
const url = new URL(request.url);
|
|
290
|
-
if (ctx.host !== void 0 && refusesOpen(request, ctx
|
|
300
|
+
if (ctx.host !== void 0 && refusesOpen(request, ctx, url.pathname)) return json({ error: "API_TOKEN is not set: production refuses to serve without it (set ENVIRONMENT=development to run open)" }, 503);
|
|
291
301
|
const candidates = routes.filter((route) => matches(route.path, url.pathname));
|
|
292
302
|
if (candidates.length === 0) return notFound();
|
|
293
|
-
if (candidates.some((route) => route.auth === "
|
|
303
|
+
if (candidates.some((route) => route.auth === "api")) {
|
|
304
|
+
let ok;
|
|
305
|
+
try {
|
|
306
|
+
ok = await allowed(request, ctx);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error("[workflow http] authorize failed", error);
|
|
309
|
+
return json({ error: "internal error" }, 500);
|
|
310
|
+
}
|
|
311
|
+
if (!ok) return json({ error: "unauthorized" }, 401);
|
|
312
|
+
}
|
|
294
313
|
const route = candidates.find((r) => r.method === "*" || r.method === request.method);
|
|
295
314
|
if (route === void 0) return json({ error: "method not allowed" }, 405);
|
|
296
315
|
try {
|
package/dist/host/http.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.cjs","names":["handleTracking","checkEvent","ingestEvent","identifyUser","registerDevice","unbindDevice","removeDevice","recordPushOpen","forgetUser","deployBundle","journeyMode","z","BundleIntegrityError","TemplateValidationError","UserErasedError","SchemaMismatchError","authorized","d1Store"],"sources":["../../src/host/http.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type Contract, type ContractResult, checkEvent } from '../contract';\nimport type { EmailRenderer } from '../email-sender';\nimport type { BundleIR, ScalarIR } from '../ir';\nimport { type TrackingOptions, handleTracking, recordPushOpen } from '../reporting/tracking';\nimport {\n type JourneyStore,\n type ProfileProps,\n SchemaMismatchError,\n UserErasedError,\n} from '../store/index';\nimport { TemplateValidationError } from '../templates';\nimport { authorized } from './auth';\nimport { journeyMode } from './channels';\nimport {\n BundleIntegrityError,\n type SweepResult,\n deployBundle,\n forgetUser,\n identifyUser,\n ingestEvent,\n registerDevice,\n removeDevice,\n unbindDevice,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes\n * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A\n * route whose part is absent answers 404, so the two share the table, the bearer check, the\n * body validation and the error mapping.\n */\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\n/** What the full host serves beyond the ingest API. A route whose part is absent answers 404. */\nexport interface HostParts {\n /** The bundle compiled into the code: `GET /bundle` shows it, `POST /bundle/deploy` makes it live. */\n bundle?: (() => BundleIR) | undefined;\n /** `GET /preview/:id?props=<JSON>`: the email templates, rendered. */\n preview?: { has(id: string): boolean; render: EmailRenderer } | undefined;\n /** `/t/*`, what recipients open (no bearer): served on a store that keeps reporting. */\n tracking?: TrackingOptions | undefined;\n /** `POST /segments/sweep`. */\n sweep?: (() => Promise<SweepResult>) | undefined;\n}\n\nexport interface ServeContext {\n env: JourneyEnv;\n store: JourneyStore;\n validation?: IngestValidation | undefined;\n /**\n * The host's parts. Their presence is what makes this a deployed host rather than the bare\n * API: production then refuses to serve without `API_TOKEN`.\n */\n host?: HostParts | undefined;\n}\n\ninterface RouteContext {\n request: Request;\n url: URL;\n env: JourneyEnv;\n store: JourneyStore;\n validation: IngestValidation;\n host: HostParts;\n}\n\ninterface Route {\n /** '*' takes every method (the tracking routes answer GET, HEAD and POST themselves). */\n method: 'GET' | 'POST' | '*';\n path: string | RegExp | ((pathname: string) => boolean);\n auth: 'none' | 'bearer';\n handle(ctx: RouteContext): Promise<Response>;\n}\n\nconst json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\nconst notFound = () => json({ error: 'not found' }, 404);\n\n/* --------------------------------- bodies ---------------------------------- */\n\n/** The request body is not JSON: the caller's to fix (a 400), not a server fault. */\nclass InvalidJsonError extends Error {\n override readonly name = 'InvalidJsonError';\n}\n\nasync function readJson(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch (error) {\n throw new InvalidJsonError('invalid JSON body', { cause: error });\n }\n}\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await readJson(request);\n return isPlainObject(body) ? body : {};\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */\nfunction isScalarOrNull(value: unknown): value is string | number | boolean | null {\n return (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */\nconst TS_FUTURE_TOLERANCE_MS = 5 * 60_000;\nconst TS_ERROR = 'ts must be a non-negative integer no later than 5 minutes in the future';\n\n/**\n * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.\n * A far-future ts would put the event ahead of everything a window condition reads, and a\n * fractional or unsafe one does not survive the store.\n */\nfunction isValidTs(ts: unknown): ts is number {\n return (\n typeof ts === 'number' &&\n Number.isSafeInteger(ts) &&\n ts >= 0 &&\n ts <= Date.now() + TS_FUTURE_TOLERANCE_MS\n );\n}\n\n/** The contract's verdict: the value to use, or the response refusing the request. */\nfunction checked<T>(\n validation: IngestValidation,\n result: ContractResult<T>,\n original: T,\n what: string\n): { value: T } | { refused: Response } {\n if (result.ok) return { value: result.data };\n const issues = result.issues.map((i) => `${i.path || '(root)'}: ${i.message}`);\n if (validation.mode === 'report') {\n console.warn(`[workflow http] contract would refuse ${what}: ${issues.join('; ')}`);\n return { value: original };\n }\n return {\n refused: json({ error: `${what} refused by the contract`, issues: result.issues }, 400),\n };\n}\n\n/* --------------------------------- routes ---------------------------------- */\n\nconst routes: readonly Route[] = [\n { method: 'GET', path: '/health', auth: 'none', handle: async () => json({ ok: true }) },\n {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n method: '*',\n path: /^\\/t\\//,\n auth: 'none',\n async handle({ request, store, host }) {\n if (host.tracking === undefined || store.reporting === undefined) return notFound();\n return (await handleTracking(request, store.reporting, host.tracking)) ?? notFound();\n },\n },\n {\n method: 'POST',\n path: '/events',\n auth: 'bearer',\n async handle({ request, env, store, validation }) {\n const { id, userId, event, payload, ts } = await readBody(request);\n if (!isNonEmptyString(userId) || !isNonEmptyString(event)) {\n return json({ error: 'userId and event required' }, 400);\n }\n if (event.startsWith('$')) {\n return json({ error: 'event names starting with $ are reserved' }, 400);\n }\n // An empty id is rejected rather than treated as absent: a caller that\n // sent one meant to identify the occurrence, and silently minting a\n // different one would turn their retry into a duplicate.\n if (id !== undefined && !isNonEmptyString(id)) {\n return json({ error: 'id must be a non-empty string' }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n if (payload !== undefined && !isPlainObject(payload)) {\n return json({ error: 'payload must be an object' }, 400);\n }\n let accepted = payload;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n checkEvent(validation.contract, event, payload ?? {}),\n accepted ?? {},\n `event '${event}'${typeof id === 'string' ? ` (${id})` : ''}`\n );\n if ('refused' in verdict) return verdict.refused;\n accepted = verdict.value;\n }\n return json(await ingestEvent(env, { id, userId, event, payload: accepted, ts }, store));\n },\n },\n {\n method: 'POST',\n path: '/identify',\n auth: 'bearer',\n async handle({ request, env, store, validation }) {\n const { userId, props } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isPlainObject(props)) return json({ error: 'props must be an object' }, 400);\n // Profiles hold scalars only (both stores merge at the top level; a nested object would\n // be merged differently by each): refused here, contract or not\n if (!Object.values(props).every(isScalarOrNull)) {\n return json(\n { error: 'props values must be scalars (string, number, boolean) or null' },\n 400\n );\n }\n let patch = props as ProfileProps;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n validation.contract.parseProfilePatch(props),\n patch,\n 'identify'\n );\n if ('refused' in verdict) return verdict.refused;\n patch = verdict.value as ProfileProps;\n }\n return json(await identifyUser(env, userId, patch, store));\n },\n },\n {\n method: 'POST',\n path: '/devices',\n auth: 'bearer',\n async handle({ request, env, store }) {\n const { userId, token, platform, ts } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n const device = { userId, token, platform, ...(ts === undefined ? {} : { ts }) };\n return json(await registerDevice(env, device, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/unbind',\n auth: 'bearer',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/remove',\n auth: 'bearer',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/messages/opened',\n auth: 'bearer',\n async handle({ request, store }) {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded =\n store.reporting !== undefined && (await recordPushOpen(store.reporting, userId, messageId));\n return json({ ok: true, recorded });\n },\n },\n {\n method: 'POST',\n path: '/users/forget',\n auth: 'bearer',\n async handle({ request, env, store }) {\n const { userId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n return json(await forgetUser(env, userId, store));\n },\n },\n {\n method: 'POST',\n path: '/deploy',\n auth: 'bearer',\n async handle({ request, env, store }) {\n return json(await deployBundle(env, await readJson(request), store));\n },\n },\n {\n method: 'GET',\n path: '/bundle',\n auth: 'bearer',\n async handle({ host }) {\n return host.bundle === undefined ? notFound() : json(host.bundle());\n },\n },\n {\n method: 'POST',\n path: '/bundle/deploy',\n auth: 'bearer',\n async handle({ env, store, host }) {\n if (host.bundle === undefined) return notFound();\n return json(await deployBundle(env, host.bundle(), store));\n },\n },\n {\n method: 'POST',\n path: '/segments/sweep',\n auth: 'bearer',\n async handle({ host }) {\n return host.sweep === undefined ? notFound() : json(await host.sweep());\n },\n },\n {\n method: 'GET',\n path: /^\\/preview\\//,\n auth: 'bearer',\n async handle({ url, host }) {\n const { preview } = host;\n if (preview === undefined) return notFound();\n const key = url.pathname.slice('/preview/'.length);\n if (!preview.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown;\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (!isPlainObject(props)) return json({ error: 'props must be a JSON object' }, 400);\n const { subject, html } = await preview.render(key, props as Record<string, ScalarIR>);\n // A header holds bytes, not text: a subject with a non-Latin-1 character would throw\n // here, so it travels percent-encoded (the reader `decodeURIComponent`s it)\n return new Response(html, {\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'x-subject': encodeURIComponent(subject),\n },\n });\n },\n },\n];\n\nfunction matches(path: Route['path'], pathname: string): boolean {\n if (typeof path === 'string') return path === pathname;\n if (path instanceof RegExp) return path.test(pathname);\n return path(pathname);\n}\n\n/**\n * Production without a bearer token is not an open instance, it is a mistake: anyone could\n * ingest, identify and deploy. Only what recipients open (`/t/*`) and the health check stay up.\n */\nfunction refusesOpen(request: Request, env: JourneyEnv, pathname: string): boolean {\n return (\n env.API_TOKEN === undefined &&\n journeyMode(env) === 'production' &&\n !pathname.startsWith('/t/') &&\n !(request.method === 'GET' && pathname === '/health')\n );\n}\n\n/**\n * What a failed request answers. Validation problems are the caller's to fix (a 400, saying\n * what); anything else stays out of the response body (internals are logged, not leaked).\n */\nfunction failure(error: unknown): Response {\n if (error instanceof InvalidJsonError) return json({ error: 'invalid JSON body' }, 400);\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) return json({ error: error.message }, 400);\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n // The user has an erasure tombstone: gone, and nothing about them is taken, now or later\n if (error instanceof UserErasedError) return json({ error: 'user is erased' }, 410);\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow http]', error);\n return json({ error: 'internal error' }, 500);\n}\n\n/** One request through the table: the production gate, the bearer, the route, the error mapping. */\nexport async function serve(request: Request, ctx: ServeContext): Promise<Response> {\n const url = new URL(request.url);\n if (ctx.host !== undefined && refusesOpen(request, ctx.env, url.pathname)) {\n return json(\n {\n error:\n 'API_TOKEN is not set: production refuses to serve without it (set ENVIRONMENT=development to run open)',\n },\n 503\n );\n }\n const candidates = routes.filter((route) => matches(route.path, url.pathname));\n if (candidates.length === 0) return notFound();\n // Bearer auth on everything but the health check and the recipients' routes. An unset token\n // means an open dev instance (see JourneyEnv.API_TOKEN); production refuses to run that way\n if (candidates.some((route) => route.auth === 'bearer') && !authorized(request, ctx.env)) {\n return json({ error: 'unauthorized' }, 401);\n }\n const route = candidates.find((r) => r.method === '*' || r.method === request.method);\n if (route === undefined) return json({ error: 'method not allowed' }, 405);\n try {\n return await route.handle({\n request,\n url,\n env: ctx.env,\n store: ctx.store,\n validation: ctx.validation ?? {},\n host: ctx.host ?? {},\n });\n } catch (error) {\n return failure(error);\n }\n}\n\n/** The bare ingest API over one store: `/health`, `/events`, `/identify`, `/devices*`, `/messages/opened`, `/users/forget`, `/deploy`. */\nexport function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore = d1Store(env),\n validation: IngestValidation = {}\n): Promise<Response> {\n return serve(request, { env, store, validation });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.cjs","names":["handleTracking","checkEvent","ingestEvent","identifyUser","registerDevice","unbindDevice","removeDevice","recordPushOpen","forgetUser","deployBundle","journeyMode","authorized","z","BundleIntegrityError","TemplateValidationError","UserErasedError","SchemaMismatchError","d1Store"],"sources":["../../src/host/http.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type Contract, type ContractResult, checkEvent } from '../contract';\nimport type { EmailRenderer } from '../email-sender';\nimport type { BundleIR, ScalarIR } from '../ir';\nimport { type TrackingOptions, handleTracking, recordPushOpen } from '../reporting/tracking';\nimport {\n type JourneyStore,\n type ProfileProps,\n SchemaMismatchError,\n UserErasedError,\n} from '../store/index';\nimport { TemplateValidationError } from '../templates';\nimport { type Authorize, authorized } from './auth';\nimport { journeyMode } from './channels';\nimport {\n BundleIntegrityError,\n type SweepResult,\n deployBundle,\n forgetUser,\n identifyUser,\n ingestEvent,\n registerDevice,\n removeDevice,\n unbindDevice,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes\n * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A\n * route whose part is absent answers 404, so the two share the table, the bearer check, the\n * body validation and the error mapping.\n */\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\n/** What the full host serves beyond the ingest API. A route whose part is absent answers 404. */\nexport interface HostParts {\n /** The bundle compiled into the code: `GET /bundle` shows it, `POST /bundle/deploy` makes it live. */\n bundle?: (() => BundleIR) | undefined;\n /** `GET /preview/:id?props=<JSON>`: the email templates, rendered. */\n preview?: { has(id: string): boolean; render: EmailRenderer } | undefined;\n /** `/t/*`, what recipients open (no bearer): served on a store that keeps reporting. */\n tracking?: TrackingOptions | undefined;\n /** `POST /segments/sweep`. */\n sweep?: (() => Promise<SweepResult>) | undefined;\n}\n\nexport interface ServeContext {\n env: JourneyEnv;\n store: JourneyStore;\n validation?: IngestValidation | undefined;\n /**\n * The host's parts. Their presence is what makes this a deployed host rather than the bare\n * API: production then refuses to serve without `API_TOKEN` — unless the host authorizes\n * callers itself (`authorize`).\n */\n host?: HostParts | undefined;\n /** The host's own authorization for the API routes; the env's `API_TOKEN` bearer without one. */\n authorize?: Authorize<JourneyEnv> | undefined;\n}\n\ninterface RouteContext {\n request: Request;\n url: URL;\n env: JourneyEnv;\n store: JourneyStore;\n validation: IngestValidation;\n host: HostParts;\n}\n\ninterface Route {\n /** '*' takes every method (the tracking routes answer GET, HEAD and POST themselves). */\n method: 'GET' | 'POST' | '*';\n path: string | RegExp | ((pathname: string) => boolean);\n /** `api`: the caller must be authorized (the host's `authorize`, or the bearer); `none`: public. */\n auth: 'none' | 'api';\n handle(ctx: RouteContext): Promise<Response>;\n}\n\nconst json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\nconst notFound = () => json({ error: 'not found' }, 404);\n\n/* --------------------------------- bodies ---------------------------------- */\n\n/** The request body is not JSON: the caller's to fix (a 400), not a server fault. */\nclass InvalidJsonError extends Error {\n override readonly name = 'InvalidJsonError';\n}\n\nasync function readJson(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch (error) {\n throw new InvalidJsonError('invalid JSON body', { cause: error });\n }\n}\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await readJson(request);\n return isPlainObject(body) ? body : {};\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */\nfunction isScalarOrNull(value: unknown): value is string | number | boolean | null {\n return (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */\nconst TS_FUTURE_TOLERANCE_MS = 5 * 60_000;\nconst TS_ERROR = 'ts must be a non-negative integer no later than 5 minutes in the future';\n\n/**\n * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.\n * A far-future ts would put the event ahead of everything a window condition reads, and a\n * fractional or unsafe one does not survive the store.\n */\nfunction isValidTs(ts: unknown): ts is number {\n return (\n typeof ts === 'number' &&\n Number.isSafeInteger(ts) &&\n ts >= 0 &&\n ts <= Date.now() + TS_FUTURE_TOLERANCE_MS\n );\n}\n\n/** The contract's verdict: the value to use, or the response refusing the request. */\nfunction checked<T>(\n validation: IngestValidation,\n result: ContractResult<T>,\n original: T,\n what: string\n): { value: T } | { refused: Response } {\n if (result.ok) return { value: result.data };\n const issues = result.issues.map((i) => `${i.path || '(root)'}: ${i.message}`);\n if (validation.mode === 'report') {\n console.warn(`[workflow http] contract would refuse ${what}: ${issues.join('; ')}`);\n return { value: original };\n }\n return {\n refused: json({ error: `${what} refused by the contract`, issues: result.issues }, 400),\n };\n}\n\n/* --------------------------------- routes ---------------------------------- */\n\nconst routes: readonly Route[] = [\n { method: 'GET', path: '/health', auth: 'none', handle: async () => json({ ok: true }) },\n {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n method: '*',\n path: /^\\/t\\//,\n auth: 'none',\n async handle({ request, store, host }) {\n if (host.tracking === undefined || store.reporting === undefined) return notFound();\n return (await handleTracking(request, store.reporting, host.tracking)) ?? notFound();\n },\n },\n {\n method: 'POST',\n path: '/events',\n auth: 'api',\n async handle({ request, env, store, validation }) {\n const { id, userId, event, payload, ts } = await readBody(request);\n if (!isNonEmptyString(userId) || !isNonEmptyString(event)) {\n return json({ error: 'userId and event required' }, 400);\n }\n if (event.startsWith('$')) {\n return json({ error: 'event names starting with $ are reserved' }, 400);\n }\n // An empty id is rejected rather than treated as absent: a caller that\n // sent one meant to identify the occurrence, and silently minting a\n // different one would turn their retry into a duplicate.\n if (id !== undefined && !isNonEmptyString(id)) {\n return json({ error: 'id must be a non-empty string' }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n if (payload !== undefined && !isPlainObject(payload)) {\n return json({ error: 'payload must be an object' }, 400);\n }\n let accepted = payload;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n checkEvent(validation.contract, event, payload ?? {}),\n accepted ?? {},\n `event '${event}'${typeof id === 'string' ? ` (${id})` : ''}`\n );\n if ('refused' in verdict) return verdict.refused;\n accepted = verdict.value;\n }\n return json(await ingestEvent(env, { id, userId, event, payload: accepted, ts }, store));\n },\n },\n {\n method: 'POST',\n path: '/identify',\n auth: 'api',\n async handle({ request, env, store, validation }) {\n const { userId, props } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isPlainObject(props)) return json({ error: 'props must be an object' }, 400);\n // Profiles hold scalars only (both stores merge at the top level; a nested object would\n // be merged differently by each): refused here, contract or not\n if (!Object.values(props).every(isScalarOrNull)) {\n return json(\n { error: 'props values must be scalars (string, number, boolean) or null' },\n 400\n );\n }\n let patch = props as ProfileProps;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n validation.contract.parseProfilePatch(props),\n patch,\n 'identify'\n );\n if ('refused' in verdict) return verdict.refused;\n patch = verdict.value as ProfileProps;\n }\n return json(await identifyUser(env, userId, patch, store));\n },\n },\n {\n method: 'POST',\n path: '/devices',\n auth: 'api',\n async handle({ request, env, store }) {\n const { userId, token, platform, ts } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n const device = { userId, token, platform, ...(ts === undefined ? {} : { ts }) };\n return json(await registerDevice(env, device, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/unbind',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/remove',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/messages/opened',\n auth: 'api',\n async handle({ request, store }) {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded =\n store.reporting !== undefined && (await recordPushOpen(store.reporting, userId, messageId));\n return json({ ok: true, recorded });\n },\n },\n {\n method: 'POST',\n path: '/users/forget',\n auth: 'api',\n async handle({ request, env, store }) {\n const { userId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n return json(await forgetUser(env, userId, store));\n },\n },\n {\n method: 'POST',\n path: '/deploy',\n auth: 'api',\n async handle({ request, env, store }) {\n return json(await deployBundle(env, await readJson(request), store));\n },\n },\n {\n method: 'GET',\n path: '/bundle',\n auth: 'api',\n async handle({ host }) {\n return host.bundle === undefined ? notFound() : json(host.bundle());\n },\n },\n {\n method: 'POST',\n path: '/bundle/deploy',\n auth: 'api',\n async handle({ env, store, host }) {\n if (host.bundle === undefined) return notFound();\n return json(await deployBundle(env, host.bundle(), store));\n },\n },\n {\n method: 'POST',\n path: '/segments/sweep',\n auth: 'api',\n async handle({ host }) {\n return host.sweep === undefined ? notFound() : json(await host.sweep());\n },\n },\n {\n method: 'GET',\n path: /^\\/preview\\//,\n auth: 'api',\n async handle({ url, host }) {\n const { preview } = host;\n if (preview === undefined) return notFound();\n const key = url.pathname.slice('/preview/'.length);\n if (!preview.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown;\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (!isPlainObject(props)) return json({ error: 'props must be a JSON object' }, 400);\n const { subject, html } = await preview.render(key, props as Record<string, ScalarIR>);\n // A header holds bytes, not text: a subject with a non-Latin-1 character would throw\n // here, so it travels percent-encoded (the reader `decodeURIComponent`s it)\n return new Response(html, {\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'x-subject': encodeURIComponent(subject),\n },\n });\n },\n },\n];\n\nfunction matches(path: Route['path'], pathname: string): boolean {\n if (typeof path === 'string') return path === pathname;\n if (path instanceof RegExp) return path.test(pathname);\n return path(pathname);\n}\n\n/**\n * Production without a bearer token (and no authorizer of the host's own) is not an open\n * instance, it is a mistake: anyone could ingest, identify and deploy. Only what recipients open\n * (`/t/*`) and the health check stay up.\n */\nfunction refusesOpen(request: Request, ctx: ServeContext, pathname: string): boolean {\n return (\n ctx.authorize === undefined &&\n ctx.env.API_TOKEN === undefined &&\n journeyMode(ctx.env) === 'production' &&\n !pathname.startsWith('/t/') &&\n !(request.method === 'GET' && pathname === '/health')\n );\n}\n\n/**\n * Whether the caller may use an API route: the host's authorizer on a copy of the request (so a\n * body it verifies is still there for the route), or the env's `API_TOKEN` as a bearer. An\n * authorizer that throws is a failure, never an admission.\n */\nasync function allowed(request: Request, ctx: ServeContext): Promise<boolean> {\n if (ctx.authorize === undefined) return authorized(request, ctx.env);\n return ctx.authorize(request.clone(), ctx.env);\n}\n\n/**\n * What a failed request answers. Validation problems are the caller's to fix (a 400, saying\n * what); anything else stays out of the response body (internals are logged, not leaked).\n */\nfunction failure(error: unknown): Response {\n if (error instanceof InvalidJsonError) return json({ error: 'invalid JSON body' }, 400);\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) return json({ error: error.message }, 400);\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n // The user has an erasure tombstone: gone, and nothing about them is taken, now or later\n if (error instanceof UserErasedError) return json({ error: 'user is erased' }, 410);\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow http]', error);\n return json({ error: 'internal error' }, 500);\n}\n\n/** One request through the table: the production gate, the bearer, the route, the error mapping. */\nexport async function serve(request: Request, ctx: ServeContext): Promise<Response> {\n const url = new URL(request.url);\n if (ctx.host !== undefined && refusesOpen(request, ctx, url.pathname)) {\n return json(\n {\n error:\n 'API_TOKEN is not set: production refuses to serve without it (set ENVIRONMENT=development to run open)',\n },\n 503\n );\n }\n const candidates = routes.filter((route) => matches(route.path, url.pathname));\n if (candidates.length === 0) return notFound();\n // Every API route asks the authorizer; the health check and the recipients' routes never do.\n // An unset token and no authorizer means an open dev instance (see JourneyEnv.API_TOKEN);\n // production refuses to run that way\n if (candidates.some((route) => route.auth === 'api')) {\n let ok: boolean;\n try {\n ok = await allowed(request, ctx);\n } catch (error) {\n console.error('[workflow http] authorize failed', error);\n return json({ error: 'internal error' }, 500);\n }\n if (!ok) return json({ error: 'unauthorized' }, 401);\n }\n const route = candidates.find((r) => r.method === '*' || r.method === request.method);\n if (route === undefined) return json({ error: 'method not allowed' }, 405);\n try {\n return await route.handle({\n request,\n url,\n env: ctx.env,\n store: ctx.store,\n validation: ctx.validation ?? {},\n host: ctx.host ?? {},\n });\n } catch (error) {\n return failure(error);\n }\n}\n\n/** The bare ingest API over one store: `/health`, `/events`, `/identify`, `/devices*`, `/messages/opened`, `/users/forget`, `/deploy`. */\nexport function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore = d1Store(env),\n validation: IngestValidation = {}\n): Promise<Response> {\n return serve(request, { env, store, validation });\n}\n"],"mappings":"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"}
|
package/dist/host/http.d.cts
CHANGED
|
@@ -3,6 +3,7 @@ import { JourneyEnv } from "./platform.cjs";
|
|
|
3
3
|
import { JourneyStore } from "../store/index.cjs";
|
|
4
4
|
import { Contract } from "../contract.cjs";
|
|
5
5
|
import { EmailRenderer } from "../email-sender.cjs";
|
|
6
|
+
import { Authorize } from "./auth.cjs";
|
|
6
7
|
import { SweepResult } from "./ingest.cjs";
|
|
7
8
|
import { TrackingOptions } from "../reporting/tracking.cjs";
|
|
8
9
|
//#region src/host/http.d.ts
|
|
@@ -42,9 +43,12 @@ export interface ServeContext {
|
|
|
42
43
|
validation?: IngestValidation | undefined;
|
|
43
44
|
/**
|
|
44
45
|
* The host's parts. Their presence is what makes this a deployed host rather than the bare
|
|
45
|
-
* API: production then refuses to serve without `API_TOKEN
|
|
46
|
+
* API: production then refuses to serve without `API_TOKEN` — unless the host authorizes
|
|
47
|
+
* callers itself (`authorize`).
|
|
46
48
|
*/
|
|
47
49
|
host?: HostParts | undefined;
|
|
50
|
+
/** The host's own authorization for the API routes; the env's `API_TOKEN` bearer without one. */
|
|
51
|
+
authorize?: Authorize<JourneyEnv> | undefined;
|
|
48
52
|
}
|
|
49
53
|
/** One request through the table: the production gate, the bearer, the route, the error mapping. */
|
|
50
54
|
export declare function serve(request: Request, ctx: ServeContext): Promise<Response>;
|
package/dist/host/http.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.cts","names":[],"sources":["../../src/host/http.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"http.d.cts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAyCiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA+WF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,QAAO,cACP,aAAY,mBACX,QAAQ"}
|
package/dist/host/http.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ import { JourneyEnv } from "./platform.mjs";
|
|
|
3
3
|
import { JourneyStore } from "../store/index.mjs";
|
|
4
4
|
import { Contract } from "../contract.mjs";
|
|
5
5
|
import { EmailRenderer } from "../email-sender.mjs";
|
|
6
|
+
import { Authorize } from "./auth.mjs";
|
|
6
7
|
import { SweepResult } from "./ingest.mjs";
|
|
7
8
|
import { TrackingOptions } from "../reporting/tracking.mjs";
|
|
8
9
|
//#region src/host/http.d.ts
|
|
@@ -42,9 +43,12 @@ export interface ServeContext {
|
|
|
42
43
|
validation?: IngestValidation | undefined;
|
|
43
44
|
/**
|
|
44
45
|
* The host's parts. Their presence is what makes this a deployed host rather than the bare
|
|
45
|
-
* API: production then refuses to serve without `API_TOKEN
|
|
46
|
+
* API: production then refuses to serve without `API_TOKEN` — unless the host authorizes
|
|
47
|
+
* callers itself (`authorize`).
|
|
46
48
|
*/
|
|
47
49
|
host?: HostParts | undefined;
|
|
50
|
+
/** The host's own authorization for the API routes; the env's `API_TOKEN` bearer without one. */
|
|
51
|
+
authorize?: Authorize<JourneyEnv> | undefined;
|
|
48
52
|
}
|
|
49
53
|
/** One request through the table: the production gate, the bearer, the route, the error mapping. */
|
|
50
54
|
export declare function serve(request: Request, ctx: ServeContext): Promise<Response>;
|
package/dist/host/http.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.mts","names":[],"sources":["../../src/host/http.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"http.d.mts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAyCiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA+WF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,QAAO,cACP,aAAY,mBACX,QAAQ"}
|