wirl 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,42 +1,40 @@
1
1
  {
2
2
  "name": "wirl",
3
- "version": "0.1.1",
4
- "description": "Wirl SDK for email sequence automation",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
13
- }
3
+ "version": "0.2.0",
4
+ "description": "Deploy an internal app somewhere real, privately. One command; sharing by email; API keys declared, never pasted.",
5
+ "keywords": [
6
+ "deploy",
7
+ "internal-tools",
8
+ "cli",
9
+ "mcp",
10
+ "claude-code",
11
+ "agents",
12
+ "cloudflare-workers"
13
+ ],
14
+ "homepage": "https://wirl.dev",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Brycce/wirl-platform.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/Brycce/wirl-platform/issues"
22
+ },
23
+ "type": "module",
24
+ "bin": {
25
+ "wirl": "./bin.js"
14
26
  },
15
27
  "files": [
16
- "dist",
28
+ "bin.js",
29
+ "serve-module.mjs",
17
30
  "README.md"
18
31
  ],
19
- "scripts": {
20
- "build": "tsup src/index.ts --format cjs,esm --dts",
21
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
- "lint": "eslint src",
23
- "prepublishOnly": "npm run build"
32
+ "engines": {
33
+ "node": ">=22.13"
24
34
  },
25
- "keywords": [
26
- "email",
27
- "automation",
28
- "sequences",
29
- "marketing",
30
- "wirl"
31
- ],
32
- "author": "",
33
- "license": "MIT",
34
- "devDependencies": {
35
- "@types/node": "^20.0.0",
36
- "tsup": "^8.0.0",
37
- "typescript": "^5.0.0"
35
+ "dependencies": {
36
+ "tar": "^7.5.22",
37
+ "esbuild": "^0.28.2"
38
38
  },
39
- "engines": {
40
- "node": ">=18.0.0"
41
- }
39
+ "license": "UNLICENSED"
42
40
  }
@@ -0,0 +1,537 @@
1
+ // packages/runtime/src/serve-module.mjs
2
+ import fs from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { createServer } from "node:http";
5
+ import { syncBuiltinESMExports } from "node:module";
6
+ import { join, resolve } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ // packages/runtime/src/local-db.ts
10
+ import { DatabaseSync } from "node:sqlite";
11
+ var D1_MAX_BOUND_PARAMETERS = 100;
12
+ var D1_MAX_VALUE_BYTES = 2e6;
13
+ var asBytes = (view) => {
14
+ if (view.byteLength === 0) return new Uint8Array(0);
15
+ return view instanceof Uint8Array ? view : new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
16
+ };
17
+ var d1TypeError = (value) => new Error(`D1_TYPE_ERROR: Type '${typeof value}' not supported for value '${String(value)}'`, {
18
+ cause: new Error(`Type '${typeof value}' not supported for value '${String(value)}'`)
19
+ });
20
+ function toSql(value, index) {
21
+ if (value === void 0 || value === null) return null;
22
+ if (typeof value === "boolean") return value ? 1 : 0;
23
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "string") return value;
24
+ if (value instanceof Uint8Array || value instanceof Uint8ClampedArray) return asBytes(value);
25
+ if (value instanceof ArrayBuffer) return asBytes(new Uint8Array(value));
26
+ const place = `bound value ${String(index + 1)}`;
27
+ if (ArrayBuffer.isView(value)) {
28
+ const kind = value.constructor?.name ?? "view";
29
+ throw new Error(
30
+ `wirl dev refuses ${place}, ${/^[AEIO]/.test(kind) ? "an" : "a"} ${kind}: D1 binds a view as the list of its elements, not its bytes, and what it stores for this one has not been measured. To store the view's bytes, bind new Uint8Array(value.buffer, value.byteOffset, value.byteLength).`
31
+ );
32
+ }
33
+ if (Array.isArray(value)) {
34
+ if (!value.every((item) => typeof item === "number" && item >= 0 && item < 256)) throw d1TypeError(value);
35
+ if (!value.every((item) => Number.isInteger(item))) {
36
+ throw new Error(
37
+ `wirl dev refuses ${place}, an array holding a number that is not a whole byte: D1 passes it on as it is, and what it stores for a fraction has not been measured. Bind whole numbers from 0 to 255.`
38
+ );
39
+ }
40
+ return Uint8Array.from(value);
41
+ }
42
+ throw d1TypeError(value);
43
+ }
44
+ function fromSql(value) {
45
+ return value instanceof Uint8Array ? Array.from(value) : value;
46
+ }
47
+ function d1Row(row) {
48
+ return Object.fromEntries(Object.entries(row).map(([name, value]) => [name, fromSql(value)]));
49
+ }
50
+ var now = () => performance.now();
51
+ function offsetOfNthPlaceholder(sql, n) {
52
+ let seen = 0;
53
+ for (let index = 0; index < sql.length; index += 1) {
54
+ if (sql[index] === "?") {
55
+ seen += 1;
56
+ if (seen === n) return index;
57
+ }
58
+ }
59
+ return 0;
60
+ }
61
+ function assertBoundParamLimit(sql, paramCount) {
62
+ if (paramCount <= D1_MAX_BOUND_PARAMETERS) return;
63
+ const offset = offsetOfNthPlaceholder(sql, D1_MAX_BOUND_PARAMETERS + 1);
64
+ throw new Error(`D1_ERROR: too many SQL variables at offset ${offset}: SQLITE_ERROR`);
65
+ }
66
+ function valueBytes(value) {
67
+ if (typeof value === "string") return Buffer.byteLength(value, "utf8");
68
+ if (value instanceof Uint8Array) return value.byteLength;
69
+ return 0;
70
+ }
71
+ function assertValueSizes(params) {
72
+ for (const [index, value] of params.entries()) {
73
+ const bytes = valueBytes(value);
74
+ if (bytes <= D1_MAX_VALUE_BYTES) continue;
75
+ throw new Error(
76
+ `D1_ERROR: string or blob too big: SQLITE_TOOBIG. wirl dev refuses this statement because bound value ${String(index + 1)} is ${bytes.toLocaleString("en-US")} bytes, over ${D1_MAX_VALUE_BYTES.toLocaleString("en-US")} bytes, the limit Cloudflare publishes for a D1 string, BLOB or row. Store a larger file in chunks, one row each.`
77
+ );
78
+ }
79
+ }
80
+ var meta = (over) => ({
81
+ duration: 0,
82
+ changes: 0,
83
+ last_row_id: 0,
84
+ rows_read: 0,
85
+ rows_written: 0,
86
+ served_by: "local",
87
+ changed_db: false,
88
+ size_after: 0,
89
+ ...over
90
+ });
91
+ var Statement = class _Statement {
92
+ #db;
93
+ #sql;
94
+ #params;
95
+ constructor(db, sql, params = []) {
96
+ this.#db = db;
97
+ this.#sql = sql;
98
+ this.#params = params;
99
+ }
100
+ /** A new statement, as D1 does. The original stays unbound and reusable. */
101
+ bind(...values) {
102
+ return new _Statement(this.#db, this.#sql, values.map((value, index) => toSql(value, index)));
103
+ }
104
+ /** Synchronous inside, because node:sqlite is; wrapped so the shape is D1's. */
105
+ execute() {
106
+ assertBoundParamLimit(this.#sql, this.#params.length);
107
+ assertValueSizes(this.#params);
108
+ const began = now();
109
+ const prepared = this.#db.prepare(this.#sql);
110
+ const returnsRows = prepared.columns().length > 0;
111
+ if (returnsRows) {
112
+ const results = prepared.all(...this.#params).map(
113
+ (row) => d1Row(row)
114
+ );
115
+ return {
116
+ success: true,
117
+ results,
118
+ meta: meta({ duration: now() - began, rows_read: results.length })
119
+ };
120
+ }
121
+ const outcome = prepared.run(...this.#params);
122
+ const changes = Number(outcome.changes);
123
+ return {
124
+ success: true,
125
+ results: [],
126
+ meta: meta({
127
+ duration: now() - began,
128
+ changes,
129
+ last_row_id: Number(outcome.lastInsertRowid),
130
+ rows_written: changes,
131
+ changed_db: changes > 0
132
+ })
133
+ };
134
+ }
135
+ async all() {
136
+ return this.execute();
137
+ }
138
+ async run() {
139
+ return this.execute();
140
+ }
141
+ async first(column) {
142
+ const row = this.execute().results[0];
143
+ if (row === void 0) return null;
144
+ if (column === void 0) return row;
145
+ return row[column] ?? null;
146
+ }
147
+ async raw(options = {}) {
148
+ assertBoundParamLimit(this.#sql, this.#params.length);
149
+ assertValueSizes(this.#params);
150
+ const prepared = this.#db.prepare(this.#sql);
151
+ const names = prepared.columns().map((column) => column.name);
152
+ const rows = prepared.all(...this.#params);
153
+ const values = rows.map((row) => names.map((name) => fromSql(row[name])));
154
+ return options.columnNames ? [names, ...values] : values;
155
+ }
156
+ };
157
+ function createLocalDatabase(path) {
158
+ const db = new DatabaseSync(path);
159
+ return {
160
+ prepare: (sql) => new Statement(db, sql),
161
+ async batch(statements) {
162
+ db.exec("BEGIN");
163
+ try {
164
+ const results = [];
165
+ for (const statement of statements) results.push(statement.execute());
166
+ db.exec("COMMIT");
167
+ return results;
168
+ } catch (error) {
169
+ db.exec("ROLLBACK");
170
+ throw error;
171
+ }
172
+ },
173
+ async exec(sql) {
174
+ const began = now();
175
+ const lines = sql.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
176
+ for (const [index, line] of lines.entries()) {
177
+ try {
178
+ db.exec(line);
179
+ } catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ throw new Error(`D1_EXEC_ERROR: Error in line ${index + 1}: ${line}: ${message}: SQLITE_ERROR`);
182
+ }
183
+ }
184
+ return { count: lines.length, duration: now() - began };
185
+ }
186
+ };
187
+ }
188
+
189
+ // packages/runtime/src/serve-module.mjs
190
+ var modulePath = process.env.WIRL_MODULE;
191
+ var port = Number(process.env.PORT);
192
+ var die = (message) => {
193
+ process.stderr.write(`${message}
194
+ `);
195
+ process.exit(1);
196
+ };
197
+ if (!modulePath) die("WIRL_MODULE is not set, so there is no module to serve.");
198
+ if (!Number.isInteger(port) || port <= 0) die(`PORT is not a usable port: ${process.env.PORT}`);
199
+ var STATUS_TEXT = { 502: "Bad Gateway", 504: "Gateway Timeout" };
200
+ var brokerUrl = process.env.WIRL_BROKER_URL;
201
+ var brokerToken = process.env.WIRL_BROKER_TOKEN;
202
+ if (brokerUrl && brokerToken) {
203
+ const brokerOrigin = new URL(brokerUrl).origin;
204
+ const direct = globalThis.fetch;
205
+ const NOT_FORWARDED = /* @__PURE__ */ new Set(["host", "content-length", "authorization", "x-wirl-target"]);
206
+ globalThis.fetch = async (input, init) => {
207
+ const request = new Request(input, init);
208
+ let target;
209
+ try {
210
+ target = new URL(request.url);
211
+ } catch {
212
+ return direct(input, init);
213
+ }
214
+ const local = target.hostname === "127.0.0.1" || target.hostname === "localhost" || target.hostname === "[::1]";
215
+ if (target.origin === brokerOrigin || local || target.protocol !== "https:" && target.protocol !== "http:") {
216
+ return direct(input, init);
217
+ }
218
+ const headers = new Headers();
219
+ request.headers.forEach((value, name) => {
220
+ if (!NOT_FORWARDED.has(name.toLowerCase())) headers.set(name, value);
221
+ });
222
+ headers.set("authorization", `Bearer ${brokerToken}`);
223
+ headers.set("x-wirl-target", request.url);
224
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
225
+ const brokered = await direct(`${brokerUrl}/_/`, {
226
+ method: request.method,
227
+ headers,
228
+ ...hasBody ? { body: await request.arrayBuffer() } : {},
229
+ redirect: "manual",
230
+ // The app's own abort, carried to the broker: a stop button or a
231
+ // timeout closes this connection, which is what tells the broker to
232
+ // stop the vendor's call and give back its stream slot, as the
233
+ // platform's fetch does deployed. The app's read then rejects with its
234
+ // AbortError here too, rather than going on as if nothing happened.
235
+ signal: request.signal
236
+ });
237
+ const carried = brokered.headers.get("x-wirl-status");
238
+ if (carried === null) return brokered;
239
+ const restoredHeaders = new Headers(brokered.headers);
240
+ restoredHeaders.delete("x-wirl-status");
241
+ const named = Number(carried);
242
+ const status = brokered.status === 500 && (named === 502 || named === 504) ? named : brokered.status;
243
+ return new Response(brokered.body, { status, statusText: STATUS_TEXT[status] ?? brokered.statusText, headers: restoredHeaders });
244
+ };
245
+ }
246
+ if (process.env.WIRL_DEV_FS_WARN === "1") {
247
+ const WARNING_PREFIX = "wirl dev: the app is writing ";
248
+ const WROTE_PREFIX = "wirl dev wrote: ";
249
+ const NAMED_EVERY_MS = 250;
250
+ const named = /* @__PURE__ */ new Map();
251
+ const { O_WRONLY, O_RDWR, O_CREAT, O_APPEND, O_TRUNC } = fs.constants;
252
+ const forWriting = (flags) => typeof flags === "string" ? /[wa+]/.test(flags) : typeof flags === "number" && (flags & (O_WRONLY | O_RDWR | O_CREAT | O_APPEND | O_TRUNC)) !== 0;
253
+ const pathOf = (target) => {
254
+ if (typeof target === "string") return target;
255
+ if (Buffer.isBuffer(target)) return target.toString("utf8");
256
+ if (target instanceof URL) {
257
+ try {
258
+ return fileURLToPath(target);
259
+ } catch {
260
+ return target.href;
261
+ }
262
+ }
263
+ return null;
264
+ };
265
+ let told = false;
266
+ const tell = (target) => {
267
+ const path = pathOf(target);
268
+ if (path === null) return;
269
+ const absolute = resolve(path);
270
+ const now2 = Date.now();
271
+ const last = named.get(absolute);
272
+ if (last === void 0 || now2 - last >= NAMED_EVERY_MS) {
273
+ if (last === void 0 && named.size >= 1e3) named.clear();
274
+ named.set(absolute, now2);
275
+ process.stderr.write(`${WROTE_PREFIX}${JSON.stringify(absolute)}
276
+ `);
277
+ }
278
+ if (told) return;
279
+ told = true;
280
+ const shown = path.length > 200 ? `${path.slice(0, 200)}...` : path;
281
+ process.stderr.write(
282
+ `${WARNING_PREFIX}${JSON.stringify(shown)} through node:fs. Here it stays on disk; deployed, it will not be there by the next request. Keep what must last in env.DB.
283
+ `
284
+ );
285
+ };
286
+ const WRITES = [
287
+ ["writeFile", 0],
288
+ ["appendFile", 0],
289
+ ["mkdir", 0],
290
+ ["copyFile", 1],
291
+ ["cp", 1],
292
+ ["rename", 1],
293
+ ["open", 0, 1]
294
+ ];
295
+ const watch = (target, name, at, flagsAt) => {
296
+ const original = target[name];
297
+ if (typeof original !== "function") return;
298
+ target[name] = function(...args) {
299
+ if (flagsAt === void 0 || forWriting(args[flagsAt])) tell(args[at]);
300
+ return original.apply(this, args);
301
+ };
302
+ };
303
+ for (const [name, at, flagsAt] of WRITES) {
304
+ watch(fs, name, at, flagsAt);
305
+ watch(fs, `${name}Sync`, at, flagsAt);
306
+ watch(fs.promises, name, at, flagsAt);
307
+ }
308
+ watch(fs, "createWriteStream", 0);
309
+ syncBuiltinESMExports();
310
+ }
311
+ var loaded = await import(pathToFileURL(modulePath).href).catch(
312
+ (error) => die(`Could not import ${modulePath}: ${error instanceof Error ? error.message : String(error)}`)
313
+ );
314
+ var handler = loaded?.default?.fetch;
315
+ if (typeof handler !== "function") die(`${modulePath} has no default export with a fetch.`);
316
+ var localDb = process.env.WIRL_LOCAL_DB;
317
+ var env = localDb ? { DB: createLocalDatabase(localDb) } : {};
318
+ var devIdentity = null;
319
+ if (process.env.WIRL_DEV_IDENTITY) {
320
+ try {
321
+ devIdentity = JSON.parse(process.env.WIRL_DEV_IDENTITY);
322
+ } catch {
323
+ die("WIRL_DEV_IDENTITY is not valid JSON.");
324
+ }
325
+ }
326
+ var SCHEDULE_PREFIX = "/__wirl/schedule/";
327
+ var FLOW_PREFIX = "/__wirl/flow/";
328
+ var FLOW_ONLY_HEADERS = ["x-wirl-flow", "x-wirl-step", "x-wirl-n", "x-wirl-idempotency-key"];
329
+ var STEP_WORD = /^[A-Za-z0-9_-]{1,100}$/;
330
+ function flowStepOf(body) {
331
+ let posted = null;
332
+ try {
333
+ posted = JSON.parse(body.toString("utf8"));
334
+ } catch {
335
+ posted = null;
336
+ }
337
+ const fields = posted !== null && typeof posted === "object" && !Array.isArray(posted) ? posted : {};
338
+ const word = (value, fallback) => typeof value === "string" && STEP_WORD.test(value) ? value : fallback;
339
+ const count = (value, least, fallback) => Number.isSafeInteger(value) && value >= least ? value : fallback;
340
+ return { run: word(fields.run, "dev"), step: word(fields.step, "start"), n: count(fields.n, 0, 0), attempt: count(fields.attempt, 1, 1) };
341
+ }
342
+ function idempotencyKeyOf(appId, { run, n, step }) {
343
+ return step === "failed" || step === "cancelled" ? `${appId}:${run}:${n}:${step}` : `${appId}:${run}:${n}`;
344
+ }
345
+ var VISITOR_HEADERS = ["x-wirl-user-id", "x-wirl-user-email", "x-wirl-user-name", "x-wirl-role"];
346
+ var publicPaths = [];
347
+ if (process.env.WIRL_DEV_PUBLIC_PATHS) {
348
+ try {
349
+ publicPaths = JSON.parse(process.env.WIRL_DEV_PUBLIC_PATHS);
350
+ } catch {
351
+ die("WIRL_DEV_PUBLIC_PATHS is not valid JSON.");
352
+ }
353
+ }
354
+ var isPublicPath = (pathname) => publicPaths.some(
355
+ (path) => path.endsWith("/*") ? pathname.startsWith(path.slice(0, -1)) : pathname === path
356
+ );
357
+ var staticFiles = null;
358
+ if (process.env.WIRL_DEV_STATIC) {
359
+ try {
360
+ staticFiles = JSON.parse(process.env.WIRL_DEV_STATIC);
361
+ } catch {
362
+ die("WIRL_DEV_STATIC is not valid JSON.");
363
+ }
364
+ }
365
+ var CONTENT_TYPES = {
366
+ html: "text/html; charset=utf-8",
367
+ css: "text/css; charset=utf-8",
368
+ js: "text/javascript; charset=utf-8",
369
+ mjs: "text/javascript; charset=utf-8",
370
+ json: "application/json",
371
+ map: "application/json",
372
+ svg: "image/svg+xml",
373
+ png: "image/png",
374
+ jpg: "image/jpeg",
375
+ jpeg: "image/jpeg",
376
+ gif: "image/gif",
377
+ webp: "image/webp",
378
+ avif: "image/avif",
379
+ ico: "image/x-icon",
380
+ woff: "font/woff",
381
+ woff2: "font/woff2",
382
+ ttf: "font/ttf",
383
+ txt: "text/plain; charset=utf-8",
384
+ xml: "application/xml",
385
+ pdf: "application/pdf",
386
+ wasm: "application/wasm",
387
+ webmanifest: "application/manifest+json"
388
+ };
389
+ function staticKey(pathname) {
390
+ let path;
391
+ try {
392
+ path = decodeURIComponent(pathname);
393
+ } catch {
394
+ return null;
395
+ }
396
+ if (/[\u0000-\u001f\u007f]/.test(path) || path.includes("\\")) return null;
397
+ if (path === "" || path.endsWith("/")) path += "index.html";
398
+ const parts = [];
399
+ for (const segment of path.split("/")) {
400
+ if (segment === "" || segment === ".") continue;
401
+ if (segment === "..") return null;
402
+ parts.push(segment);
403
+ }
404
+ return parts.length === 0 ? "index.html" : parts.join("/");
405
+ }
406
+ async function staticResponse(method, pathname, accept) {
407
+ if (staticFiles === null || method !== "GET" && method !== "HEAD") return null;
408
+ const key = staticKey(pathname);
409
+ if (key === null) return null;
410
+ const read = (name2) => readFile(join(staticFiles.root, name2)).catch(() => null);
411
+ let name = key;
412
+ let body = await read(name);
413
+ if (body === null && staticFiles.spa && !/\.[a-z0-9]{1,8}$/i.test(key) && accept.includes("text/html")) {
414
+ name = "index.html";
415
+ body = await read(name);
416
+ }
417
+ if (body === null) return null;
418
+ const extension = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).toLowerCase() : "";
419
+ return { body, contentType: CONTENT_TYPES[extension] ?? "application/octet-stream" };
420
+ }
421
+ var verbose = process.env.WIRL_DEV_VERBOSE === "1";
422
+ createServer(async (incoming, outgoing) => {
423
+ if (verbose) {
424
+ const began = Date.now();
425
+ const path = (incoming.url ?? "/").split("?")[0] ?? "/";
426
+ outgoing.once("close", () => {
427
+ process.stdout.write(`${incoming.method} ${path} ${outgoing.statusCode} \xB7 ${Date.now() - began} ms
428
+ `);
429
+ });
430
+ }
431
+ try {
432
+ {
433
+ const path = (incoming.url ?? "/").split("?")[0] ?? "/";
434
+ if (!path.startsWith(SCHEDULE_PREFIX) && !path.startsWith(FLOW_PREFIX) && !isPublicPath(path)) {
435
+ const file = await staticResponse(incoming.method ?? "GET", path, String(incoming.headers.accept ?? ""));
436
+ if (file !== null) {
437
+ outgoing.writeHead(200, { "content-type": file.contentType, "content-length": String(file.body.byteLength) });
438
+ outgoing.end(incoming.method === "HEAD" ? void 0 : file.body);
439
+ return;
440
+ }
441
+ }
442
+ }
443
+ const headers = new Headers();
444
+ for (const [name, value] of Object.entries(incoming.headers)) {
445
+ if (Array.isArray(value)) for (const each of value) headers.append(name, each);
446
+ else if (typeof value === "string") headers.set(name, value);
447
+ }
448
+ const chunks = [];
449
+ for await (const chunk of incoming) chunks.push(chunk);
450
+ const hasBody = incoming.method !== "GET" && incoming.method !== "HEAD" && chunks.length > 0;
451
+ if (devIdentity) {
452
+ const fillIn = (name, value) => {
453
+ if (!headers.has(name)) headers.set(name, String(value));
454
+ };
455
+ const pathname = (incoming.url ?? "/").split("?")[0] ?? "/";
456
+ let flowStep = null;
457
+ for (const name of FLOW_ONLY_HEADERS) headers.delete(name);
458
+ if (pathname.startsWith(SCHEDULE_PREFIX)) {
459
+ for (const name of VISITOR_HEADERS) headers.set(name, "");
460
+ headers.set("x-wirl-schedule", pathname.slice(SCHEDULE_PREFIX.length));
461
+ headers.set("x-wirl-run", "dev");
462
+ headers.set("x-wirl-attempt", "1");
463
+ } else if (pathname.startsWith(FLOW_PREFIX)) {
464
+ for (const name of VISITOR_HEADERS) headers.set(name, "");
465
+ flowStep = flowStepOf(Buffer.concat(chunks));
466
+ headers.set("x-wirl-flow", pathname.slice(FLOW_PREFIX.length));
467
+ headers.set("x-wirl-run", flowStep.run);
468
+ headers.set("x-wirl-step", flowStep.step);
469
+ headers.set("x-wirl-n", String(flowStep.n));
470
+ headers.set("x-wirl-attempt", String(flowStep.attempt));
471
+ } else if (isPublicPath(pathname)) {
472
+ for (const name of VISITOR_HEADERS) headers.set(name, "");
473
+ headers.set("x-wirl-public", "1");
474
+ } else {
475
+ fillIn("x-wirl-user-id", devIdentity.userId ?? "");
476
+ fillIn("x-wirl-user-email", devIdentity.email ?? "");
477
+ fillIn("x-wirl-user-name", devIdentity.name ?? "");
478
+ fillIn("x-wirl-role", devIdentity.role ?? "");
479
+ }
480
+ fillIn("x-wirl-org", devIdentity.org ?? "");
481
+ fillIn("x-wirl-app-id", devIdentity.appId ?? "local");
482
+ if (flowStep !== null) headers.set("x-wirl-idempotency-key", idempotencyKeyOf(headers.get("x-wirl-app-id") ?? "", flowStep));
483
+ }
484
+ const request = new Request(`http://${incoming.headers.host ?? "localhost"}${incoming.url ?? "/"}`, {
485
+ method: incoming.method,
486
+ headers,
487
+ ...hasBody ? { body: Buffer.concat(chunks) } : {}
488
+ });
489
+ const response = await handler(request, env, {});
490
+ response.headers.forEach((value, name) => {
491
+ if (name.toLowerCase() !== "set-cookie") outgoing.setHeader(name, value);
492
+ });
493
+ const cookies = response.headers.getSetCookie();
494
+ if (cookies.length > 0) outgoing.setHeader("set-cookie", cookies);
495
+ outgoing.writeHead(response.status);
496
+ if (response.body === null) {
497
+ outgoing.end();
498
+ } else {
499
+ outgoing.flushHeaders();
500
+ const reader = response.body.getReader();
501
+ const gone = new Promise((resolve2) => {
502
+ outgoing.once("close", () => {
503
+ resolve2();
504
+ void reader.cancel().catch(() => {
505
+ });
506
+ });
507
+ });
508
+ for (; ; ) {
509
+ const { done, value } = await reader.read();
510
+ if (done) break;
511
+ if (!outgoing.write(value)) {
512
+ let resume;
513
+ const drained = new Promise((resolve2) => {
514
+ resume = resolve2;
515
+ outgoing.once("drain", resolve2);
516
+ });
517
+ await Promise.race([drained, gone]);
518
+ outgoing.off("drain", resume);
519
+ }
520
+ }
521
+ outgoing.end();
522
+ }
523
+ } catch (error) {
524
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
525
+ `);
526
+ if (outgoing.headersSent) {
527
+ outgoing.destroy();
528
+ return;
529
+ }
530
+ outgoing.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
531
+ outgoing.end(`Internal error: ${error instanceof Error ? error.message : String(error)}
532
+ `);
533
+ }
534
+ }).listen(port, "0.0.0.0", () => {
535
+ process.stdout.write(`serving ${modulePath} on ${port}
536
+ `);
537
+ });
package/dist/index.d.mts DELETED
@@ -1,73 +0,0 @@
1
- interface WirlOptions {
2
- apiKey: string;
3
- baseUrl?: string;
4
- }
5
- interface TrackParams {
6
- email: string;
7
- event: string;
8
- properties?: Record<string, unknown>;
9
- }
10
- interface IdentifyParams {
11
- email: string;
12
- properties: Record<string, unknown>;
13
- }
14
- interface TriggerParams {
15
- email: string;
16
- sequence: string;
17
- }
18
- interface ExitParams {
19
- email: string;
20
- sequence: string;
21
- }
22
- interface TrackResponse {
23
- success: boolean;
24
- contact_id: string;
25
- event_id: string;
26
- }
27
- interface IdentifyResponse {
28
- success: boolean;
29
- contact_id: string;
30
- created: boolean;
31
- }
32
- interface TriggerResponse {
33
- success: boolean;
34
- enrolled: boolean;
35
- }
36
- interface ExitResponse {
37
- success: boolean;
38
- exited: boolean;
39
- }
40
- interface WirlError {
41
- error: string;
42
- details?: unknown;
43
- }
44
- declare class WirlAPIError extends Error {
45
- readonly statusCode: number;
46
- readonly details?: unknown;
47
- constructor(message: string, statusCode: number, details?: unknown);
48
- }
49
- declare class Wirl {
50
- private readonly apiKey;
51
- private readonly baseUrl;
52
- constructor(options: WirlOptions);
53
- private request;
54
- /**
55
- * Track an event for a contact.
56
- * Creates the contact if they don't exist.
57
- */
58
- track(params: TrackParams): Promise<TrackResponse>;
59
- /**
60
- * Create or update a contact with properties.
61
- */
62
- identify(params: IdentifyParams): Promise<IdentifyResponse>;
63
- /**
64
- * Manually enroll a contact in a sequence.
65
- */
66
- trigger(params: TriggerParams): Promise<TriggerResponse>;
67
- /**
68
- * Remove a contact from an active sequence.
69
- */
70
- exit(params: ExitParams): Promise<ExitResponse>;
71
- }
72
-
73
- export { type ExitParams, type ExitResponse, type IdentifyParams, type IdentifyResponse, type TrackParams, type TrackResponse, type TriggerParams, type TriggerResponse, Wirl, WirlAPIError, type WirlError, type WirlOptions, Wirl as default };