@crvouga/mockingbird-service-twilio 0.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.
@@ -0,0 +1,3642 @@
1
+ // ../core/dist/clock.js
2
+ var createClock = (source = Date.now) => {
3
+ let offsetMs = 0;
4
+ let frozenAt;
5
+ const now = () => frozenAt ?? source() + offsetMs;
6
+ return {
7
+ now,
8
+ set: (epochMs) => {
9
+ if (frozenAt !== void 0)
10
+ frozenAt = epochMs;
11
+ else
12
+ offsetMs = epochMs - source();
13
+ },
14
+ advance: (deltaMs) => {
15
+ if (frozenAt !== void 0)
16
+ frozenAt += deltaMs;
17
+ else
18
+ offsetMs += deltaMs;
19
+ },
20
+ freeze: () => {
21
+ frozenAt = now();
22
+ },
23
+ unfreeze: () => {
24
+ if (frozenAt === void 0)
25
+ return;
26
+ offsetMs = frozenAt - source();
27
+ frozenAt = void 0;
28
+ },
29
+ reset: () => {
30
+ offsetMs = 0;
31
+ frozenAt = void 0;
32
+ },
33
+ state: () => ({ now: now(), frozen: frozenAt !== void 0, offsetMs })
34
+ };
35
+ };
36
+
37
+ // ../core/dist/collection.js
38
+ var Collection = class {
39
+ sqlite;
40
+ namespace;
41
+ name;
42
+ constructor(sqlite, namespace, name) {
43
+ this.sqlite = sqlite;
44
+ this.namespace = namespace;
45
+ this.name = name;
46
+ }
47
+ bumpCollectionSeq() {
48
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.name);
49
+ const next = (row?.value ?? 0) + 1;
50
+ this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'collection', ?)
51
+ ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.name, next);
52
+ return next;
53
+ }
54
+ nextSequence() {
55
+ return this.sqlite.transaction(() => this.bumpCollectionSeq());
56
+ }
57
+ get(id) {
58
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
59
+ if (!row)
60
+ return void 0;
61
+ return JSON.parse(row.value).value;
62
+ }
63
+ has(id) {
64
+ const row = this.sqlite.prepare("SELECT 1 AS ok FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
65
+ return row !== void 0;
66
+ }
67
+ /** Insert a new record, assigning it the next sequence number. */
68
+ insert(id, value) {
69
+ return this.sqlite.transaction(() => {
70
+ const seq = this.bumpCollectionSeq();
71
+ const stored = { seq, value };
72
+ this.sqlite.prepare(`INSERT INTO mockingbird_records (namespace, collection, id, seq, value)
73
+ VALUES (?, ?, ?, ?, ?)
74
+ ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.name, id, seq, JSON.stringify(stored));
75
+ return stored;
76
+ });
77
+ }
78
+ /** Replace an existing record's value, keeping its position. */
79
+ update(id, value) {
80
+ return this.sqlite.transaction(() => {
81
+ const row = this.sqlite.prepare("SELECT seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
82
+ if (!row)
83
+ return void 0;
84
+ const stored = { seq: row.seq, value };
85
+ this.sqlite.prepare("UPDATE mockingbird_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.name, id);
86
+ return stored;
87
+ });
88
+ }
89
+ delete(id) {
90
+ const result = this.sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.name, id);
91
+ return result.changes > 0;
92
+ }
93
+ /** How many records the collection holds, without reading them. */
94
+ count() {
95
+ const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM mockingbird_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.name);
96
+ return Number(row?.n ?? 0);
97
+ }
98
+ list(options = {}) {
99
+ const rows = this.sqlite.prepare("SELECT id, seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.name);
100
+ const out = [];
101
+ for (const row of rows) {
102
+ const stored = JSON.parse(row.value);
103
+ if (options.where && !options.where(stored.value, stored.seq))
104
+ continue;
105
+ out.push({ id: row.id, seq: stored.seq, value: stored.value });
106
+ }
107
+ out.sort((a, b) => options.order === "oldest" ? a.seq - b.seq : b.seq - a.seq);
108
+ return out;
109
+ }
110
+ };
111
+
112
+ // ../core/dist/control.js
113
+ var HEALTH_PATH = "/health";
114
+ var ADMIN_PREFIX = "/__admin";
115
+ var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
116
+ var NAMESPACE_HEADER = "x-mockingbird-namespace";
117
+ var json = (status, body) => new Response(JSON.stringify(body), {
118
+ status,
119
+ headers: { "content-type": "application/json" }
120
+ });
121
+ var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
122
+ var UNITS = {
123
+ ms: 1,
124
+ s: 1e3,
125
+ m: 6e4,
126
+ h: 36e5,
127
+ d: 864e5
128
+ };
129
+ var parseDuration = (value) => {
130
+ if (typeof value === "number" && Number.isFinite(value))
131
+ return value;
132
+ if (typeof value !== "string")
133
+ return void 0;
134
+ const match = /^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(value.trim());
135
+ if (!match)
136
+ return void 0;
137
+ return Number(match[1]) * UNITS[match[2]];
138
+ };
139
+ var parseInstant = (value) => {
140
+ if (typeof value === "number" && Number.isFinite(value))
141
+ return value;
142
+ if (typeof value !== "string")
143
+ return void 0;
144
+ const parsed = Date.parse(value);
145
+ return Number.isNaN(parsed) ? void 0 : parsed;
146
+ };
147
+ var matchRoute = (pattern, path) => {
148
+ const want = pattern.split("/").filter(Boolean);
149
+ const have = path.split("/").filter(Boolean);
150
+ if (want.length !== have.length)
151
+ return void 0;
152
+ const params = {};
153
+ for (let i = 0; i < want.length; i++) {
154
+ const segment = want[i];
155
+ const actual = have[i];
156
+ if (segment.startsWith(":"))
157
+ params[segment.slice(1)] = decodeURIComponent(actual);
158
+ else if (segment !== actual)
159
+ return void 0;
160
+ }
161
+ return params;
162
+ };
163
+ var readJson = async (request) => {
164
+ const text = await request.text();
165
+ if (text.trim() === "")
166
+ return void 0;
167
+ return JSON.parse(text);
168
+ };
169
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
170
+ var createControlPlane = (context) => {
171
+ const snapshots = /* @__PURE__ */ new Map();
172
+ let snapshotCounter = 0;
173
+ const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
174
+ const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
175
+ const builtin = {
176
+ "GET /": () => json(200, {
177
+ service: context.name,
178
+ routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
179
+ }),
180
+ "POST /reset": async ({ url, namespace }) => {
181
+ const target = url.searchParams.get("all") === "1" ? "*" : namespace;
182
+ await context.reset(target);
183
+ return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
184
+ },
185
+ "GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
186
+ "GET /clock": () => json(200, context.clock.state()),
187
+ "POST /clock": ({ body }) => {
188
+ if (!isRecord(body))
189
+ return adminError(400, "expected a JSON object");
190
+ if (body.reset === true)
191
+ context.clock.reset();
192
+ if (body.set !== void 0) {
193
+ const instant = parseInstant(body.set);
194
+ if (instant === void 0)
195
+ return adminError(400, "set: expected epoch ms or ISO-8601");
196
+ context.clock.set(instant);
197
+ }
198
+ if (body.advance !== void 0) {
199
+ const delta = parseDuration(body.advance);
200
+ if (delta === void 0)
201
+ return adminError(400, 'advance: expected ms or "15m"-style');
202
+ context.clock.advance(delta);
203
+ }
204
+ if (body.freeze === true)
205
+ context.clock.freeze();
206
+ if (body.freeze === false)
207
+ context.clock.unfreeze();
208
+ return json(200, context.clock.state());
209
+ },
210
+ "GET /faults": () => json(200, { faults: context.faults.list() }),
211
+ "POST /faults": ({ body, namespace }) => {
212
+ if (isRecord(body) && typeof body.preset === "string") {
213
+ if (!context.applyPreset)
214
+ return adminError(400, `${context.name} has no fault presets`);
215
+ const { preset, ...overrides } = body;
216
+ try {
217
+ return json(201, {
218
+ preset,
219
+ rules: context.applyPreset(preset, namespace, overrides)
220
+ });
221
+ } catch (error) {
222
+ return adminError(404, error instanceof Error ? error.message : String(error));
223
+ }
224
+ }
225
+ if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
226
+ return adminError(400, "a fault needs a numeric status, a delayMs/latencyMs, drop: true, an effect, or a preset");
227
+ }
228
+ const rule = {
229
+ // Scoped to the caller's namespace unless it asks for every one, so one worker's
230
+ // injected failure never lands on another's request.
231
+ namespace,
232
+ ...body,
233
+ id: typeof body.id === "string" ? body.id : `fault_${context.faults.list().length + 1}`
234
+ };
235
+ return json(201, context.faults.add(rule));
236
+ },
237
+ "DELETE /faults": ({ url }) => {
238
+ const id = url.searchParams.get("id");
239
+ if (id === null) {
240
+ context.faults.clear();
241
+ return json(200, { status: "ok" });
242
+ }
243
+ return context.faults.remove(id) ? json(200, { status: "ok" }) : adminError(404, `no fault ${id}`);
244
+ },
245
+ "POST /snapshots": ({ namespace }) => {
246
+ const point = context.timeTravel.checkpoint(namespace, "main");
247
+ context.timeTravel.retain(namespace, point.id);
248
+ snapshotCounter++;
249
+ const id = `snap_${snapshotCounter}`;
250
+ snapshots.set(id, { namespace, checkpoint: point.id });
251
+ return json(201, { id, namespace, records: point.records ?? 0 });
252
+ },
253
+ "POST /snapshots/:id/restore": ({ params, namespace }) => {
254
+ const alias = snapshots.get(params.id);
255
+ if (!alias)
256
+ return adminError(404, `no snapshot ${params.id}`);
257
+ if (alias.namespace !== namespace) {
258
+ return adminError(409, `snapshot ${params.id} belongs to namespace ${alias.namespace}`);
259
+ }
260
+ context.timeTravel.checkout(alias.checkpoint, { namespace, branch: "main" });
261
+ return json(200, { status: "ok", id: params.id, namespace });
262
+ },
263
+ "DELETE /snapshots/:id": ({ params }) => {
264
+ const id = params.id;
265
+ const alias = snapshots.get(id);
266
+ if (!alias)
267
+ return adminError(404, `no snapshot ${id}`);
268
+ snapshots.delete(id);
269
+ context.timeTravel.release(alias.namespace, alias.checkpoint);
270
+ return json(200, { status: "ok" });
271
+ },
272
+ "GET /timeline": ({ namespace }) => json(200, context.timeTravel.inspect(namespace)),
273
+ "POST /checkpoints": ({ body, namespace }) => {
274
+ const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
275
+ try {
276
+ return json(201, context.timeTravel.checkpoint(namespace, branch));
277
+ } catch (error) {
278
+ return adminError(409, error instanceof Error ? error.message : String(error));
279
+ }
280
+ },
281
+ "POST /branches/:name": ({ params, body, namespace }) => {
282
+ const at = isRecord(body) && typeof body.at === "string" ? body.at : void 0;
283
+ try {
284
+ return json(201, context.timeTravel.branch(params.name, {
285
+ namespace,
286
+ ...at !== void 0 ? { at } : {}
287
+ }));
288
+ } catch (error) {
289
+ return adminError(409, error instanceof Error ? error.message : String(error));
290
+ }
291
+ },
292
+ "POST /branches/:name/checkout": ({ params, body, namespace }) => {
293
+ if (!isRecord(body) || typeof body.checkpoint !== "string") {
294
+ return adminError(400, 'expected {"checkpoint":"cp_..."}');
295
+ }
296
+ try {
297
+ context.timeTravel.checkout(body.checkpoint, {
298
+ namespace,
299
+ branch: params.name
300
+ });
301
+ return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
302
+ } catch (error) {
303
+ return adminError(409, error instanceof Error ? error.message : String(error));
304
+ }
305
+ },
306
+ "GET /requests": ({ url, namespace }) => {
307
+ const status = url.searchParams.get("status");
308
+ const since = url.searchParams.get("since");
309
+ const limit = url.searchParams.get("limit");
310
+ const sinceMs = since === null ? void 0 : parseInstant(/^\d+$/.test(since) ? Number(since) : since);
311
+ if (since !== null && sinceMs === void 0) {
312
+ return adminError(400, "since: expected epoch ms or ISO-8601");
313
+ }
314
+ if (status !== null && !/^\d{3}$/.test(status))
315
+ return adminError(400, "status: expected an HTTP status");
316
+ if (limit !== null && !/^\d+$/.test(limit))
317
+ return adminError(400, "limit: expected a count");
318
+ const operationId = url.searchParams.get("operationId");
319
+ const everyNamespace = url.searchParams.get("all") === "1";
320
+ return json(200, {
321
+ size: context.journal.size,
322
+ requests: context.journal.list({
323
+ ...everyNamespace ? {} : { namespace },
324
+ ...operationId !== null ? { operationId } : {},
325
+ ...status !== null ? { status: Number(status) } : {},
326
+ ...sinceMs !== void 0 ? { since: sinceMs } : {},
327
+ ...limit !== null ? { limit: Number(limit) } : {}
328
+ })
329
+ });
330
+ },
331
+ "DELETE /requests": ({ url, namespace }) => {
332
+ context.journal.clear(url.searchParams.get("all") === "1" ? void 0 : namespace);
333
+ return json(200, { status: "ok" });
334
+ },
335
+ "GET /metrics": () => json(200, context.metrics.report()),
336
+ "DELETE /metrics": () => {
337
+ context.metrics.reset();
338
+ return json(200, { status: "ok" });
339
+ }
340
+ };
341
+ const routes = [...Object.entries(context.routes), ...Object.entries(builtin)].map(([key, handler]) => {
342
+ const space = key.indexOf(" ");
343
+ return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
344
+ });
345
+ return {
346
+ namespaceOf: headerNamespace,
347
+ async handle(request) {
348
+ const url = new URL(request.url);
349
+ if (url.pathname === HEALTH_PATH && request.method === "GET") {
350
+ return json(200, {
351
+ status: "ok",
352
+ service: context.name,
353
+ uptimeMs: context.wallNow() - context.startedAt,
354
+ clock: context.clock.state(),
355
+ namespaces: context.namespaces().length,
356
+ ...context.describe()
357
+ });
358
+ }
359
+ if (url.pathname !== ADMIN_PREFIX && !url.pathname.startsWith(`${ADMIN_PREFIX}/`)) {
360
+ return void 0;
361
+ }
362
+ if (context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
363
+ return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
364
+ }
365
+ const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
366
+ for (const route of routes) {
367
+ if (route.method !== request.method)
368
+ continue;
369
+ const params = matchRoute(route.pattern, path);
370
+ if (!params)
371
+ continue;
372
+ let body;
373
+ try {
374
+ body = await readJson(request);
375
+ } catch {
376
+ return adminError(400, "request body is not valid JSON");
377
+ }
378
+ return route.handler({
379
+ request,
380
+ url,
381
+ params,
382
+ namespace: adminNamespace(request, url),
383
+ body
384
+ });
385
+ }
386
+ return adminError(404, `no admin route ${request.method} ${path}; GET ${ADMIN_PREFIX} lists them`);
387
+ }
388
+ };
389
+ };
390
+
391
+ // ../core/dist/credentials.js
392
+ var basicAuth = (request) => {
393
+ const header2 = request.headers.get("authorization");
394
+ if (!header2)
395
+ return void 0;
396
+ const match = /^Basic\s+(.+)$/i.exec(header2.trim());
397
+ if (!match?.[1])
398
+ return void 0;
399
+ let decoded;
400
+ try {
401
+ decoded = atob(match[1].trim());
402
+ } catch {
403
+ return void 0;
404
+ }
405
+ const colon = decoded.indexOf(":");
406
+ if (colon < 0)
407
+ return { username: decoded, password: "" };
408
+ return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };
409
+ };
410
+ var createCredentialRegistry = () => {
411
+ const map = /* @__PURE__ */ new Map();
412
+ return {
413
+ set: (credential, namespace) => {
414
+ map.set(credential, namespace);
415
+ },
416
+ get: (credential) => map.get(credential),
417
+ remove: (credential) => map.delete(credential),
418
+ clear: () => map.clear(),
419
+ entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
420
+ };
421
+ };
422
+ var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
423
+
424
+ // ../core/dist/rng.js
425
+ var seedFrom = (value) => {
426
+ let hash = 2166136261;
427
+ for (let i = 0; i < value.length; i++) {
428
+ hash ^= value.charCodeAt(i);
429
+ hash = Math.imul(hash, 16777619);
430
+ }
431
+ return hash >>> 0;
432
+ };
433
+ var createRng = (seed = 0) => {
434
+ const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
435
+ let state = numeric;
436
+ const next = () => {
437
+ state = state + 1831565813 >>> 0;
438
+ let t = state;
439
+ t = Math.imul(t ^ t >>> 15, t | 1);
440
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
441
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
442
+ };
443
+ return {
444
+ next,
445
+ int: (min, max) => min + Math.floor(next() * (max - min + 1)),
446
+ reset: () => {
447
+ state = numeric;
448
+ },
449
+ state: () => state,
450
+ setState: (next2) => {
451
+ if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
452
+ throw new RangeError("rng state must be an unsigned 32-bit integer");
453
+ }
454
+ state = next2 >>> 0;
455
+ },
456
+ seed: numeric
457
+ };
458
+ };
459
+
460
+ // ../core/dist/faults.js
461
+ var matches = (rule, candidate) => {
462
+ if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
463
+ return false;
464
+ }
465
+ if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
466
+ return false;
467
+ if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
468
+ return false;
469
+ }
470
+ if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
471
+ return false;
472
+ return true;
473
+ };
474
+ var faultResponse = (rule) => {
475
+ const status = rule.status ?? 500;
476
+ const headers = { "content-type": "application/json", ...rule.headers };
477
+ if (typeof rule.body === "string")
478
+ return new Response(rule.body, { status, headers });
479
+ if (rule.body === null)
480
+ return new Response(null, { status, headers: rule.headers ?? {} });
481
+ const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
482
+ return new Response(JSON.stringify(body), { status, headers });
483
+ };
484
+ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
485
+ const entries = [];
486
+ return {
487
+ add(rule) {
488
+ const existing = entries.findIndex((e) => e.rule.id === rule.id);
489
+ const entry = { rule, remaining: rule.count ?? null, hits: 0 };
490
+ if (existing >= 0)
491
+ entries[existing] = entry;
492
+ else
493
+ entries.push(entry);
494
+ return rule;
495
+ },
496
+ list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
497
+ remove(id) {
498
+ const index = entries.findIndex((e) => e.rule.id === id);
499
+ if (index < 0)
500
+ return false;
501
+ entries.splice(index, 1);
502
+ return true;
503
+ },
504
+ clear() {
505
+ entries.length = 0;
506
+ },
507
+ async take(candidate) {
508
+ const hits = [];
509
+ for (const entry of entries) {
510
+ if (entry.remaining === 0)
511
+ continue;
512
+ if (!matches(entry.rule, candidate))
513
+ continue;
514
+ const rate = entry.rule.rate ?? 1;
515
+ if (rng.next() >= rate)
516
+ continue;
517
+ entry.hits++;
518
+ if (entry.remaining !== null)
519
+ entry.remaining--;
520
+ const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
521
+ if (delay !== void 0 && delay > 0) {
522
+ await sleep(delay);
523
+ }
524
+ const hit = { id: entry.rule.id };
525
+ if (entry.rule.effect !== void 0) {
526
+ hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
527
+ }
528
+ if (entry.rule.drop === true)
529
+ hit.drop = true;
530
+ else if (entry.rule.status !== void 0)
531
+ hit.response = faultResponse(entry.rule);
532
+ hits.push(hit);
533
+ if (hit.drop || hit.response)
534
+ break;
535
+ }
536
+ return hits;
537
+ }
538
+ };
539
+ };
540
+
541
+ // ../../openapi/core/dist/refs.js
542
+ var OpenAPIReferenceError = class extends Error {
543
+ ref;
544
+ constructor(ref) {
545
+ super(`unresolvable $ref: ${ref}`);
546
+ this.ref = ref;
547
+ this.name = "OpenAPIReferenceError";
548
+ }
549
+ };
550
+ var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
551
+ var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
552
+ var resolveRef = (document2, ref) => {
553
+ if (!ref.startsWith("#/"))
554
+ throw new OpenAPIReferenceError(ref);
555
+ let cursor = document2;
556
+ for (const raw of ref.slice(2).split("/")) {
557
+ const segment = unescapePointer(raw);
558
+ if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
559
+ throw new OpenAPIReferenceError(ref);
560
+ }
561
+ cursor = cursor[segment];
562
+ }
563
+ if (cursor === void 0)
564
+ throw new OpenAPIReferenceError(ref);
565
+ return cursor;
566
+ };
567
+ var deref = (document2, value) => {
568
+ let current = value;
569
+ const seen = /* @__PURE__ */ new Set();
570
+ while (isReference(current)) {
571
+ if (seen.has(current.$ref))
572
+ throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
573
+ seen.add(current.$ref);
574
+ current = resolveRef(document2, current.$ref);
575
+ }
576
+ return current;
577
+ };
578
+
579
+ // ../../openapi/core/dist/types.js
580
+ var HTTP_METHODS = [
581
+ "get",
582
+ "put",
583
+ "post",
584
+ "delete",
585
+ "options",
586
+ "head",
587
+ "patch",
588
+ "trace"
589
+ ];
590
+
591
+ // ../../openapi/core/dist/document.js
592
+ var mergeParameters = (document2, item, own) => {
593
+ const merged = /* @__PURE__ */ new Map();
594
+ for (const raw of item.parameters ?? []) {
595
+ const parameter = deref(document2, raw);
596
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
597
+ }
598
+ for (const raw of own ?? []) {
599
+ const parameter = deref(document2, raw);
600
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
601
+ }
602
+ return [...merged.values()];
603
+ };
604
+ var listOperations = (document2) => {
605
+ const operations = [];
606
+ for (const [path, item] of Object.entries(document2.paths)) {
607
+ for (const method of HTTP_METHODS) {
608
+ const operation = item[method];
609
+ if (operation?.operationId === void 0)
610
+ continue;
611
+ const responses = {};
612
+ for (const [status, response] of Object.entries(operation.responses)) {
613
+ responses[status] = deref(document2, response);
614
+ }
615
+ operations.push({
616
+ operationId: operation.operationId,
617
+ method,
618
+ path,
619
+ operation,
620
+ parameters: mergeParameters(document2, item, operation.parameters),
621
+ requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
622
+ responses
623
+ });
624
+ }
625
+ }
626
+ return operations;
627
+ };
628
+
629
+ // ../../http/codec/dist/form.js
630
+ var parsePath = (rawKey) => {
631
+ const open = rawKey.indexOf("[");
632
+ if (open === -1)
633
+ return [rawKey];
634
+ const path = [rawKey.slice(0, open)];
635
+ const rest = rawKey.slice(open);
636
+ const pattern = /\[([^\]]*)\]/g;
637
+ let match = pattern.exec(rest);
638
+ let consumed = 0;
639
+ while (match !== null) {
640
+ if (match.index !== consumed)
641
+ return [rawKey];
642
+ path.push(match[1] ?? "");
643
+ consumed = match.index + match[0].length;
644
+ match = pattern.exec(rest);
645
+ }
646
+ if (consumed !== rest.length)
647
+ return [rawKey];
648
+ return path;
649
+ };
650
+ var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
651
+ var put = (target, key, value) => {
652
+ if (key === "__proto__") {
653
+ Object.defineProperty(target, key, {
654
+ value,
655
+ enumerable: true,
656
+ writable: true,
657
+ configurable: true
658
+ });
659
+ return;
660
+ }
661
+ ;
662
+ target[key] = value;
663
+ };
664
+ var assign = (target, path, value) => {
665
+ let cursor = target;
666
+ for (let i = 0; i < path.length; i++) {
667
+ const segment = path[i];
668
+ const last = i === path.length - 1;
669
+ if (Array.isArray(cursor)) {
670
+ const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
671
+ if (index === void 0)
672
+ return;
673
+ if (last) {
674
+ put(cursor, index, value);
675
+ return;
676
+ }
677
+ const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
678
+ if (next === void 0 || typeof next === "string") {
679
+ const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
680
+ put(cursor, index, created);
681
+ cursor = created;
682
+ } else {
683
+ cursor = next;
684
+ }
685
+ continue;
686
+ }
687
+ if (typeof cursor === "string")
688
+ return;
689
+ if (last) {
690
+ put(cursor, segment, value);
691
+ return;
692
+ }
693
+ const nextSegment = path[i + 1];
694
+ const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
695
+ if (existing === void 0 || typeof existing === "string") {
696
+ const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
697
+ put(cursor, segment, created);
698
+ cursor = created;
699
+ } else {
700
+ cursor = existing;
701
+ }
702
+ }
703
+ };
704
+ var decodeFormPairs = (pairs) => {
705
+ const out = {};
706
+ for (const [rawKey, value] of pairs)
707
+ assign(out, parsePath(rawKey), value);
708
+ return densify(out);
709
+ };
710
+ var densify = (value) => {
711
+ if (typeof value === "string")
712
+ return value;
713
+ if (Array.isArray(value))
714
+ return value.filter((item) => item !== void 0).map(densify);
715
+ const out = {};
716
+ for (const [key, item] of Object.entries(value))
717
+ put(out, key, densify(item));
718
+ return out;
719
+ };
720
+ var decodeForm = (text) => {
721
+ const source = text.startsWith("?") ? text.slice(1) : text;
722
+ return decodeFormPairs(new URLSearchParams(source).entries());
723
+ };
724
+
725
+ // ../../http/codec/dist/content.js
726
+ var JSON_MEDIA_TYPE = "application/json";
727
+ var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
728
+ var mediaTypeOf = (contentType) => {
729
+ if (!contentType)
730
+ return void 0;
731
+ const essence = contentType.split(";")[0]?.trim().toLowerCase();
732
+ return essence ? essence : void 0;
733
+ };
734
+ var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
735
+ var utf8 = new TextDecoder("utf-8", { fatal: false });
736
+ var decodeBody = (contentType, bytes) => {
737
+ if (bytes.byteLength === 0)
738
+ return { kind: "empty" };
739
+ const mediaType = mediaTypeOf(contentType);
740
+ if (mediaType === void 0)
741
+ return { kind: "bytes", value: bytes };
742
+ if (isJsonMediaType(mediaType)) {
743
+ const text = utf8.decode(bytes);
744
+ try {
745
+ return { kind: "json", value: JSON.parse(text) };
746
+ } catch (error) {
747
+ return {
748
+ kind: "invalid",
749
+ mediaType,
750
+ text,
751
+ error: error instanceof Error ? error.message : String(error)
752
+ };
753
+ }
754
+ }
755
+ if (mediaType === FORM_MEDIA_TYPE) {
756
+ return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
757
+ }
758
+ if (mediaType.startsWith("text/"))
759
+ return { kind: "text", value: utf8.decode(bytes) };
760
+ return { kind: "bytes", value: bytes };
761
+ };
762
+ var readBody = async (message) => {
763
+ const bytes = new Uint8Array(await message.arrayBuffer());
764
+ return decodeBody(message.headers.get("content-type"), bytes);
765
+ };
766
+
767
+ // ../core/dist/http.js
768
+ var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
769
+ status,
770
+ headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
771
+ });
772
+ var HttpError = class extends Error {
773
+ status;
774
+ body;
775
+ headers;
776
+ constructor(status, body, headers = {}) {
777
+ super(`HTTP ${status}`);
778
+ this.status = status;
779
+ this.body = body;
780
+ this.headers = headers;
781
+ this.name = "HttpError";
782
+ }
783
+ toResponse() {
784
+ const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
785
+ if (contentType === "text/plain") {
786
+ return new Response(String(this.body), {
787
+ status: this.status,
788
+ headers: this.headers
789
+ });
790
+ }
791
+ return jsonRes(this.status, this.body, this.headers);
792
+ }
793
+ };
794
+
795
+ // ../core/dist/ids.js
796
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
797
+ var mix = (input) => {
798
+ let hash = 2166136261;
799
+ for (let i = 0; i < input.length; i++) {
800
+ hash ^= input.charCodeAt(i);
801
+ hash = Math.imul(hash, 16777619) >>> 0;
802
+ }
803
+ hash ^= hash >>> 16;
804
+ hash = Math.imul(hash, 2246822507) >>> 0;
805
+ hash ^= hash >>> 13;
806
+ return hash >>> 0;
807
+ };
808
+ var opaqueToken = (input, length) => {
809
+ let out = "";
810
+ let round = 0;
811
+ while (out.length < length) {
812
+ let hash = mix(`${input}:${round++}`);
813
+ for (let i = 0; i < 5 && out.length < length; i++) {
814
+ out += ALPHABET.charAt(hash % ALPHABET.length);
815
+ hash = Math.floor(hash / ALPHABET.length);
816
+ }
817
+ }
818
+ return out;
819
+ };
820
+ var IdSequence = class {
821
+ sqlite;
822
+ namespace;
823
+ salt;
824
+ constructor(sqlite, namespace, salt = "mockingbird") {
825
+ this.sqlite = sqlite;
826
+ this.namespace = namespace;
827
+ this.salt = salt;
828
+ }
829
+ next(prefix, length = 14) {
830
+ return this.sqlite.transaction(() => {
831
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'id'").get(this.namespace, prefix);
832
+ const value = (row?.value ?? 0) + 1;
833
+ this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'id', ?)
834
+ ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, prefix, value);
835
+ return `${prefix}${opaqueToken(`${this.salt}:${prefix}:${value}`, length)}`;
836
+ });
837
+ }
838
+ };
839
+
840
+ // ../core/dist/journal.js
841
+ var DEFAULT_JOURNAL_SIZE = 1e3;
842
+ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
843
+ const capacity = Math.max(0, Math.floor(size));
844
+ const rings = /* @__PURE__ */ new Map();
845
+ let sequence = 0;
846
+ const order = /* @__PURE__ */ new WeakMap();
847
+ const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
848
+ return {
849
+ size: capacity,
850
+ record(entry) {
851
+ if (capacity === 0)
852
+ return;
853
+ order.set(entry, sequence++);
854
+ let ring = rings.get(entry.namespace);
855
+ if (!ring) {
856
+ ring = { entries: [], next: 0 };
857
+ rings.set(entry.namespace, ring);
858
+ }
859
+ if (ring.entries.length < capacity)
860
+ ring.entries.push(entry);
861
+ else {
862
+ ring.entries[ring.next] = entry;
863
+ ring.next = (ring.next + 1) % capacity;
864
+ }
865
+ },
866
+ list(query = {}) {
867
+ const source = query.namespace !== void 0 ? inOrder(rings.get(query.namespace) ?? { entries: [], next: 0 }) : [...rings.values()].flatMap(inOrder).sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
868
+ const matched = source.filter((entry) => (query.operationId === void 0 || entry.operationId === query.operationId) && (query.status === void 0 || entry.status === query.status) && (query.since === void 0 || Date.parse(entry.at) >= query.since));
869
+ return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
870
+ },
871
+ clear(namespace) {
872
+ if (namespace === void 0)
873
+ rings.clear();
874
+ else
875
+ rings.delete(namespace);
876
+ }
877
+ };
878
+ };
879
+ var notes = /* @__PURE__ */ new WeakMap();
880
+ var annotateResponse = (response, extra) => {
881
+ const existing = notes.get(response);
882
+ notes.set(response, {
883
+ ...existing,
884
+ ...extra,
885
+ ...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
886
+ });
887
+ return response;
888
+ };
889
+ var responseNotes = (response) => notes.get(response);
890
+
891
+ // ../core/dist/metrics.js
892
+ var createMetrics = () => {
893
+ let requests = 0;
894
+ let faults = 0;
895
+ let totalDurationMs = 0;
896
+ const byOperation = /* @__PURE__ */ new Map();
897
+ const unmatched = /* @__PURE__ */ new Map();
898
+ return {
899
+ record(entry) {
900
+ requests++;
901
+ totalDurationMs += entry.durationMs;
902
+ if (entry.faultId !== void 0)
903
+ faults++;
904
+ const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
905
+ byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
906
+ if (entry.unmatched) {
907
+ const route = `${entry.method} ${entry.path}`;
908
+ unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
909
+ }
910
+ },
911
+ report: () => ({
912
+ requests,
913
+ byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
914
+ unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
915
+ const space = route.indexOf(" ");
916
+ return { method: route.slice(0, space), path: route.slice(space + 1), count };
917
+ }),
918
+ faults,
919
+ totalDurationMs
920
+ }),
921
+ reset() {
922
+ requests = 0;
923
+ faults = 0;
924
+ totalDurationMs = 0;
925
+ byOperation.clear();
926
+ unmatched.clear();
927
+ }
928
+ };
929
+ };
930
+
931
+ // ../core/dist/outbox.js
932
+ var OutboxStore = class {
933
+ items;
934
+ constructor(sqlite, namespace, name = "outbox") {
935
+ this.items = new Collection(sqlite, namespace, name);
936
+ }
937
+ record(item) {
938
+ this.items.insert(item.id, item);
939
+ return item;
940
+ }
941
+ get(id) {
942
+ return this.items.get(id);
943
+ }
944
+ update(id, item) {
945
+ this.items.update(id, item);
946
+ }
947
+ /** Oldest first, so a suite reads messages in the order they were sent. */
948
+ list(query = {}) {
949
+ const to = query.to?.toLowerCase();
950
+ const matched = this.items.list({ order: "oldest" }).map((row) => row.value).filter((item) => {
951
+ if (to !== void 0) {
952
+ const recipients = Array.isArray(item.to) ? item.to : [item.to];
953
+ if (!recipients.some((r) => r.toLowerCase() === to))
954
+ return false;
955
+ }
956
+ if (query.since !== void 0 && Date.parse(item.createdAt) < query.since)
957
+ return false;
958
+ if (query.where && !query.where(item))
959
+ return false;
960
+ return true;
961
+ });
962
+ return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
963
+ }
964
+ };
965
+ var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
966
+ var parseSince = (value) => {
967
+ if (value === null)
968
+ return void 0;
969
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
970
+ return Number.isNaN(parsed) ? null : parsed;
971
+ };
972
+ var outboxAdminRoutes = (runtime, pick, filter) => ({
973
+ "GET /outbox": ({ url, namespace }) => {
974
+ const since = parseSince(url.searchParams.get("since"));
975
+ if (since === null) {
976
+ return json2(400, {
977
+ error: { type: "mockingbird_admin", message: "since: expected epoch ms or ISO-8601" }
978
+ });
979
+ }
980
+ const limit = url.searchParams.get("limit");
981
+ const where = filter?.(url.searchParams);
982
+ const to = url.searchParams.get("to");
983
+ return json2(200, {
984
+ messages: pick(runtime.instance(namespace)).list({
985
+ ...to !== null ? { to } : {},
986
+ ...since !== void 0 ? { since } : {},
987
+ ...where ? { where } : {},
988
+ ...limit !== null && /^\d+$/.test(limit) ? { limit: Number(limit) } : {}
989
+ })
990
+ });
991
+ },
992
+ "GET /outbox/:id": ({ params, namespace }) => {
993
+ const item = pick(runtime.instance(namespace)).get(params.id);
994
+ return item ? json2(200, item) : json2(404, { error: { type: "mockingbird_admin", message: `no message ${params.id}` } });
995
+ }
996
+ });
997
+
998
+ // ../../core/dist/timeline.js
999
+ var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1000
+ var Timeline = class {
1001
+ maxCheckpoints;
1002
+ now;
1003
+ makeId;
1004
+ nodes = /* @__PURE__ */ new Map();
1005
+ heads = /* @__PURE__ */ new Map();
1006
+ /** Unreferenced nodes in the exact order they became collectible. */
1007
+ evictable = /* @__PURE__ */ new Set();
1008
+ /** Branch heads plus explicit retainers. Absent means zero. */
1009
+ references = /* @__PURE__ */ new Map();
1010
+ explicitPins = /* @__PURE__ */ new Map();
1011
+ sequence = 0;
1012
+ constructor(options = {}) {
1013
+ const max = options.maxCheckpoints ?? 1e3;
1014
+ if (!Number.isSafeInteger(max) || max < 1)
1015
+ throw new RangeError("maxCheckpoints must be a positive integer");
1016
+ this.maxCheckpoints = max;
1017
+ this.now = options.now ?? (() => this.sequence);
1018
+ this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
1019
+ }
1020
+ /** Capture a new immutable value and move `branch` to it. */
1021
+ commit(value, options = {}) {
1022
+ const branch = options.branch ?? "main";
1023
+ this.assertBranch(branch);
1024
+ const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
1025
+ if (parent !== null && !this.nodes.has(parent))
1026
+ throw new RangeError(`no checkpoint ${parent}`);
1027
+ const id = this.makeId(++this.sequence);
1028
+ if (this.nodes.has(id))
1029
+ throw new RangeError(`duplicate checkpoint id ${id}`);
1030
+ const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
1031
+ this.nodes.set(id, checkpoint);
1032
+ this.moveHead(branch, id);
1033
+ this.collect(this.maxCheckpoints);
1034
+ return checkpoint;
1035
+ }
1036
+ /** Create a branch pointer without copying its checkpoint value. */
1037
+ fork(branch, options = {}) {
1038
+ this.assertBranch(branch);
1039
+ if (this.heads.has(branch))
1040
+ throw new RangeError(`branch already exists: ${branch}`);
1041
+ const from = options.from ?? this.heads.get("main");
1042
+ if (from === void 0)
1043
+ return void 0;
1044
+ const checkpoint = this.get(from);
1045
+ this.moveHead(branch, checkpoint.id);
1046
+ return checkpoint;
1047
+ }
1048
+ /** Move a branch pointer to an existing checkpoint. */
1049
+ checkout(branch, id) {
1050
+ this.assertBranch(branch);
1051
+ const checkpoint = this.get(id);
1052
+ this.moveHead(branch, checkpoint.id);
1053
+ return checkpoint;
1054
+ }
1055
+ get(id) {
1056
+ const checkpoint = this.nodes.get(id);
1057
+ if (!checkpoint)
1058
+ throw new RangeError(`no checkpoint ${id}`);
1059
+ return checkpoint;
1060
+ }
1061
+ head(branch = "main") {
1062
+ const id = this.heads.get(branch);
1063
+ return id === void 0 ? void 0 : this.get(id);
1064
+ }
1065
+ hasBranch(branch) {
1066
+ return this.heads.has(branch);
1067
+ }
1068
+ branches() {
1069
+ return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
1070
+ }
1071
+ checkpoints() {
1072
+ return [...this.nodes.values()];
1073
+ }
1074
+ /** Number of retained checkpoints without allocating an array. */
1075
+ get size() {
1076
+ return this.nodes.size;
1077
+ }
1078
+ /** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
1079
+ retain(id) {
1080
+ const checkpoint = this.get(id);
1081
+ this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
1082
+ this.addReference(id);
1083
+ return checkpoint;
1084
+ }
1085
+ /** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
1086
+ release(id) {
1087
+ if (!this.nodes.has(id))
1088
+ return false;
1089
+ const pins = this.explicitPins.get(id) ?? 0;
1090
+ if (pins === 0)
1091
+ return false;
1092
+ if (pins === 1)
1093
+ this.explicitPins.delete(id);
1094
+ else
1095
+ this.explicitPins.set(id, pins - 1);
1096
+ this.removeReference(id);
1097
+ this.collect(this.maxCheckpoints);
1098
+ return true;
1099
+ }
1100
+ deleteBranch(branch) {
1101
+ if (branch === "main")
1102
+ throw new RangeError("cannot delete main branch");
1103
+ const previous = this.heads.get(branch);
1104
+ const deleted = this.heads.delete(branch);
1105
+ if (previous !== void 0)
1106
+ this.removeReference(previous);
1107
+ this.collect(this.maxCheckpoints);
1108
+ return deleted;
1109
+ }
1110
+ /**
1111
+ * Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
1112
+ * commits never scan pinned nodes or the retained history. Parents are metadata rather than a
1113
+ * storage dependency, so a retained node remains usable after pruning.
1114
+ */
1115
+ gc(max = this.maxCheckpoints) {
1116
+ if (!Number.isSafeInteger(max) || max < 1)
1117
+ throw new RangeError("max must be a positive integer");
1118
+ const removed = [];
1119
+ this.collect(max, removed);
1120
+ return removed;
1121
+ }
1122
+ collect(max, removed) {
1123
+ while (this.nodes.size > max && this.evictable.size > 0) {
1124
+ const id = this.evictable.values().next().value;
1125
+ this.evictable.delete(id);
1126
+ this.nodes.delete(id);
1127
+ removed?.push(id);
1128
+ }
1129
+ }
1130
+ moveHead(branch, id) {
1131
+ const previous = this.heads.get(branch);
1132
+ if (previous === id)
1133
+ return;
1134
+ if (previous !== void 0)
1135
+ this.removeReference(previous);
1136
+ this.heads.set(branch, id);
1137
+ this.addReference(id);
1138
+ }
1139
+ addReference(id) {
1140
+ this.references.set(id, (this.references.get(id) ?? 0) + 1);
1141
+ this.evictable.delete(id);
1142
+ }
1143
+ removeReference(id) {
1144
+ const next = (this.references.get(id) ?? 0) - 1;
1145
+ if (next > 0)
1146
+ this.references.set(id, next);
1147
+ else {
1148
+ this.references.delete(id);
1149
+ if (this.nodes.has(id))
1150
+ this.evictable.add(id);
1151
+ }
1152
+ }
1153
+ assertBranch(branch) {
1154
+ if (!BRANCH_PATTERN.test(branch))
1155
+ throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
1156
+ }
1157
+ };
1158
+
1159
+ // ../../sqlite/dist/default.js
1160
+ import { Database } from "@crvouga/mockingbird-service-sqlite";
1161
+ var createDefaultSqlite = () => new Database();
1162
+ var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
1163
+
1164
+ // ../../sqlite/dist/migrate.js
1165
+ var ensureMigrationsTable = (sqlite) => {
1166
+ sqlite.exec(`
1167
+ CREATE TABLE IF NOT EXISTS schema_migrations (
1168
+ id TEXT PRIMARY KEY NOT NULL,
1169
+ applied_at INTEGER NOT NULL
1170
+ )
1171
+ `);
1172
+ };
1173
+ var migrate = (sqlite, migrations) => {
1174
+ ensureMigrationsTable(sqlite);
1175
+ const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
1176
+ const pending = migrations.filter((migration) => !applied.has(migration.id));
1177
+ if (pending.length === 0)
1178
+ return;
1179
+ const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
1180
+ const now = Math.floor(Date.now() / 1e3);
1181
+ sqlite.transaction(() => {
1182
+ for (const migration of pending) {
1183
+ sqlite.exec(migration.sql);
1184
+ insert.run(migration.id, now);
1185
+ }
1186
+ });
1187
+ };
1188
+
1189
+ // ../../sqlite/dist/schema.js
1190
+ var CORE_MIGRATIONS = [
1191
+ {
1192
+ id: "20260322_core_records_sequences",
1193
+ sql: `
1194
+ CREATE TABLE IF NOT EXISTS mockingbird_records (
1195
+ namespace TEXT NOT NULL,
1196
+ collection TEXT NOT NULL,
1197
+ id TEXT NOT NULL,
1198
+ seq INTEGER NOT NULL,
1199
+ value TEXT NOT NULL,
1200
+ PRIMARY KEY (namespace, collection, id)
1201
+ );
1202
+ CREATE INDEX IF NOT EXISTS mockingbird_records_seq
1203
+ ON mockingbird_records (namespace, collection, seq);
1204
+ CREATE TABLE IF NOT EXISTS mockingbird_sequences (
1205
+ namespace TEXT NOT NULL,
1206
+ name TEXT NOT NULL,
1207
+ kind TEXT NOT NULL,
1208
+ value INTEGER NOT NULL,
1209
+ PRIMARY KEY (namespace, name, kind)
1210
+ );
1211
+ `
1212
+ }
1213
+ ];
1214
+ var migrateCore = (sqlite) => {
1215
+ migrate(sqlite, CORE_MIGRATIONS);
1216
+ };
1217
+ var clearNamespace = (sqlite, namespace) => {
1218
+ sqlite.transaction(() => {
1219
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1220
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1221
+ });
1222
+ };
1223
+
1224
+ // ../../openapi/metadata/dist/types.js
1225
+ var EXTENSION_KEYS = {
1226
+ operation: "x-mockingbird",
1227
+ resource: "x-mockingbird-resource",
1228
+ resourceRef: "x-mockingbird-resource-ref",
1229
+ volatile: "x-mockingbird-volatile",
1230
+ scope: "x-mockingbird-scope",
1231
+ unsupported: "x-mockingbird-unsupported",
1232
+ parityHeader: "x-mockingbird-parity-header"
1233
+ };
1234
+
1235
+ // ../../openapi/metadata/dist/read.js
1236
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1237
+ var extensionOf = (holder, key) => holder[key];
1238
+ var operationMetadata = (operation) => {
1239
+ const raw = extensionOf(operation, EXTENSION_KEYS.operation);
1240
+ const ext = isRecord2(raw) ? raw : {};
1241
+ const supported = ext.supported ?? true;
1242
+ const parity = ext.parity ?? {};
1243
+ return {
1244
+ supported,
1245
+ reason: typeof ext.reason === "string" ? ext.reason : void 0,
1246
+ parity: {
1247
+ enabled: supported && (parity.enabled ?? true),
1248
+ safe: parity.safe ?? true,
1249
+ reason: typeof parity.reason === "string" ? parity.reason : void 0
1250
+ }
1251
+ };
1252
+ };
1253
+
1254
+ // ../core/dist/service.js
1255
+ import { Hono } from "hono";
1256
+ var defineOperations = (handlers) => handlers;
1257
+ var OperationRegistryError = class extends Error {
1258
+ problems;
1259
+ constructor(problems) {
1260
+ super(`operation registry is inconsistent:
1261
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
1262
+ this.problems = problems;
1263
+ this.name = "OperationRegistryError";
1264
+ }
1265
+ };
1266
+ var verifyOperations = (document2, handlers) => {
1267
+ const problems = [];
1268
+ const operations = listOperations(document2);
1269
+ const seen = /* @__PURE__ */ new Set();
1270
+ for (const operation of operations) {
1271
+ if (seen.has(operation.operationId))
1272
+ problems.push(`duplicate operationId ${operation.operationId}`);
1273
+ seen.add(operation.operationId);
1274
+ const supported = operationMetadata(operation.operation).supported;
1275
+ const handler = handlers[operation.operationId];
1276
+ if (supported && !handler)
1277
+ problems.push(`supported operation ${operation.operationId} has no handler`);
1278
+ if (!supported && handler)
1279
+ problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
1280
+ }
1281
+ for (const id of Object.keys(handlers)) {
1282
+ if (!seen.has(id))
1283
+ problems.push(`handler ${id} has no OpenAPI operation`);
1284
+ }
1285
+ return problems;
1286
+ };
1287
+ var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
1288
+ var routeOrder = (a, b) => {
1289
+ const sa = a.path.split("/");
1290
+ const sb = b.path.split("/");
1291
+ for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
1292
+ const x = sa[i] ?? "";
1293
+ const y = sb[i] ?? "";
1294
+ const px = x.startsWith("{");
1295
+ const py = y.startsWith("{");
1296
+ if (px !== py)
1297
+ return px ? 1 : -1;
1298
+ if (x !== y)
1299
+ return x < y ? -1 : 1;
1300
+ }
1301
+ return 0;
1302
+ };
1303
+ var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
1304
+ var bootSqlite = (sqlite) => {
1305
+ const client = resolveSqlite(sqlite);
1306
+ migrateCore(client);
1307
+ return client;
1308
+ };
1309
+ var createService = (options) => {
1310
+ const problems = verifyOperations(options.document, options.handlers);
1311
+ if (problems.length > 0)
1312
+ throw new OperationRegistryError(problems);
1313
+ migrateCore(options.sqlite);
1314
+ const now = options.now ?? (() => Date.now());
1315
+ const app = new Hono();
1316
+ app.notFound((c) => options.notFound(c.req.raw));
1317
+ app.onError((error, c) => options.onError(error, c.req.raw));
1318
+ const operations = [...listOperations(options.document)].sort(routeOrder);
1319
+ for (const operation of operations) {
1320
+ const metadata = operationMetadata(operation.operation);
1321
+ const handler = options.handlers[operation.operationId];
1322
+ const route = async (c) => {
1323
+ const request = c.req.raw;
1324
+ if (!metadata.supported || !handler) {
1325
+ return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
1326
+ }
1327
+ const url = new URL(request.url);
1328
+ const context = {
1329
+ request,
1330
+ url,
1331
+ params: c.req.param(),
1332
+ query: queryOf(url),
1333
+ body: await readBody(request),
1334
+ sqlite: options.sqlite,
1335
+ namespace: options.namespace,
1336
+ operation,
1337
+ document: options.document,
1338
+ now
1339
+ };
1340
+ const short = await options.before?.(context);
1341
+ if (short)
1342
+ return short;
1343
+ return handler(context);
1344
+ };
1345
+ app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1346
+ }
1347
+ return {
1348
+ app,
1349
+ sqlite: options.sqlite,
1350
+ namespace: options.namespace,
1351
+ fetch: async (request) => app.fetch(request),
1352
+ reset: async () => {
1353
+ clearNamespace(options.sqlite, options.namespace);
1354
+ }
1355
+ };
1356
+ };
1357
+
1358
+ // ../core/dist/snapshot.js
1359
+ var snapshotNamespace = (sqlite, namespace) => ({
1360
+ namespace,
1361
+ records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
1362
+ sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
1363
+ });
1364
+ var restoreNamespace = (sqlite, namespace, snapshot) => {
1365
+ sqlite.transaction(() => {
1366
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1367
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1368
+ const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
1369
+ for (const row of snapshot.records) {
1370
+ record.run(namespace, row.collection, row.id, row.seq, row.value);
1371
+ }
1372
+ const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
1373
+ for (const row of snapshot.sequences) {
1374
+ sequence.run(namespace, row.name, row.kind, row.value);
1375
+ }
1376
+ });
1377
+ };
1378
+
1379
+ // ../core/dist/version.js
1380
+ var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
1381
+
1382
+ // ../core/dist/signing.js
1383
+ var encoder = new TextEncoder();
1384
+ var toBase64 = (bytes) => {
1385
+ let binary = "";
1386
+ for (const byte of bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)) {
1387
+ binary += String.fromCharCode(byte);
1388
+ }
1389
+ return btoa(binary);
1390
+ };
1391
+ var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
1392
+ var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1393
+ var keyBytes = (key) => typeof key === "string" ? encoder.encode(key) : key;
1394
+ var hmac = async (algorithm, key, message, encoding = "hex") => {
1395
+ const imported = await crypto.subtle.importKey("raw", keyBytes(key), { name: "HMAC", hash: algorithm }, false, ["sign"]);
1396
+ const signed = await crypto.subtle.sign("HMAC", imported, typeof message === "string" ? encoder.encode(message) : message);
1397
+ return encoding === "hex" ? toHex(signed) : toBase64(signed);
1398
+ };
1399
+ var svixSecretBytes = (secret) => {
1400
+ const raw = secret.replace(/^f?whsec_/, "");
1401
+ try {
1402
+ return fromBase64(raw);
1403
+ } catch {
1404
+ throw new TypeError("webhook secret must be whsec_<base64> (as Svix issues it)");
1405
+ }
1406
+ };
1407
+ var signSvix = async (secret, messageId, timestampSeconds, body) => `v1,${await hmac("SHA-256", svixSecretBytes(secret), `${messageId}.${timestampSeconds}.${body}`, "base64")}`;
1408
+ var signTimestamped = async (secret, timestampSeconds, body) => `t=${timestampSeconds},v1=${await hmac("SHA-256", secret, `${timestampSeconds}.${body}`, "hex")}`;
1409
+ var signTwilio = async (authToken, url, params) => {
1410
+ const payload = url + Object.keys(params).sort().map((key) => `${key}${params[key]}`).join("");
1411
+ return hmac("SHA-1", authToken, payload, "base64");
1412
+ };
1413
+
1414
+ // ../core/dist/webhooks.js
1415
+ var signers = {
1416
+ /** No signature. */
1417
+ none: () => () => ({}),
1418
+ /** Svix (`svix-id`, `svix-timestamp`, `svix-signature: v1,<b64>`): Junction, Flex, Resend. */
1419
+ svix: (options = {}) => async ({ messageId, body, timestampSeconds, secret }) => {
1420
+ if (!secret)
1421
+ return {};
1422
+ const prefix = options.prefix ?? "svix";
1423
+ return {
1424
+ [`${prefix}-id`]: messageId,
1425
+ [`${prefix}-timestamp`]: String(timestampSeconds),
1426
+ [`${prefix}-signature`]: await signSvix(secret, messageId, timestampSeconds, body)
1427
+ };
1428
+ },
1429
+ /** `<header>: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>`: Stripe, Persona, Fullscript. */
1430
+ timestamped: (header2 = "Stripe-Signature") => async ({ body, timestampSeconds, secret }) => secret ? { [header2]: await signTimestamped(secret, timestampSeconds, body) } : {},
1431
+ /** `X-Twilio-Signature` over the public URL plus the sorted form parameters. */
1432
+ twilio: () => async ({ signUrl, form, secret }) => secret ? { "X-Twilio-Signature": await signTwilio(secret, signUrl, form ?? {}) } : {},
1433
+ /** The secret itself in a header (optionally templated): RxVortex, Pharmetika, AHA `Token <s>`. */
1434
+ header: (header2, format = (s) => s) => ({ secret }) => secret ? { [header2]: format(secret) } : {},
1435
+ /** Anything else: the service computes the headers itself. */
1436
+ custom: (sign) => sign
1437
+ };
1438
+ var DEFAULT_DELAYS = [0, 5e3, 3e5, 18e5, 72e5];
1439
+ var unref = (timer) => {
1440
+ ;
1441
+ timer.unref?.();
1442
+ };
1443
+ var randomId = (prefix) => `${prefix}${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
1444
+ var matchesEndpoint = (endpoint, message) => {
1445
+ const events = endpoint.events ?? ["*"];
1446
+ if (!events.includes("*") && !events.includes(message.type))
1447
+ return false;
1448
+ for (const [key, value] of Object.entries(endpoint.tags ?? {})) {
1449
+ if (message.tags[key] !== value)
1450
+ return false;
1451
+ }
1452
+ return true;
1453
+ };
1454
+ var createWebhookHub = (options) => {
1455
+ const delays = options.retryDelaysMs ?? DEFAULT_DELAYS;
1456
+ const timeoutMs = options.timeoutMs ?? 15e3;
1457
+ const delivered = options.delivered ?? ((status) => status >= 200 && status < 300);
1458
+ const send = options.fetch ?? ((request) => fetch(request));
1459
+ const keep = options.keep ?? 500;
1460
+ const now = options.now ?? Date.now;
1461
+ const id = options.id ?? randomId;
1462
+ const scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
1463
+ const cancel = options.cancel ?? ((handle) => clearTimeout(handle));
1464
+ const global = (options.endpoints ?? []).map((e, i) => ({ ...e, id: e.id ?? `we_global_${i}` }));
1465
+ const own = /* @__PURE__ */ new Map();
1466
+ const messages = [];
1467
+ const deliveries = /* @__PURE__ */ new Map();
1468
+ const pending = /* @__PURE__ */ new Map();
1469
+ const payloads = /* @__PURE__ */ new Map();
1470
+ const faults = /* @__PURE__ */ new Map();
1471
+ const held = /* @__PURE__ */ new Map();
1472
+ const inFlight = /* @__PURE__ */ new Set();
1473
+ const track = (work) => {
1474
+ inFlight.add(work);
1475
+ void work.finally(() => inFlight.delete(work));
1476
+ };
1477
+ const attempt = async (delivery) => {
1478
+ const entry = payloads.get(delivery.id);
1479
+ if (!entry)
1480
+ return false;
1481
+ const { message, endpoint } = entry;
1482
+ const timestampSeconds = Math.floor(now() / 1e3);
1483
+ const started = now();
1484
+ const record = {
1485
+ attempt: delivery.attempts.length + 1,
1486
+ at: new Date(started).toISOString(),
1487
+ status: null,
1488
+ error: null,
1489
+ durationMs: 0,
1490
+ responseBody: null
1491
+ };
1492
+ const controller = new AbortController();
1493
+ const timer = scheduleTimer(() => controller.abort(), timeoutMs);
1494
+ try {
1495
+ const signed = await options.signer({
1496
+ messageId: message.id,
1497
+ body: message.body,
1498
+ timestampSeconds,
1499
+ url: endpoint.url,
1500
+ secret: endpoint.secret,
1501
+ signUrl: endpoint.signUrl ?? endpoint.url,
1502
+ form: message.contentType.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(message.body)) : void 0,
1503
+ type: message.type,
1504
+ tags: message.tags
1505
+ });
1506
+ const response = await send(new Request(endpoint.url, {
1507
+ method: "POST",
1508
+ headers: {
1509
+ "content-type": message.contentType,
1510
+ ...endpoint.headers,
1511
+ ...message.headers,
1512
+ ...signed
1513
+ },
1514
+ body: message.body,
1515
+ signal: controller.signal
1516
+ }));
1517
+ record.status = response.status;
1518
+ record.responseBody = await response.text();
1519
+ } catch (error) {
1520
+ record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
1521
+ } finally {
1522
+ cancel(timer);
1523
+ record.durationMs = now() - started;
1524
+ delivery.attempts.push(record);
1525
+ }
1526
+ return record.status !== null && delivered(record.status);
1527
+ };
1528
+ const schedule = (delivery) => {
1529
+ const index = delivery.attempts.length;
1530
+ if (index >= delays.length) {
1531
+ delivery.state = "failed";
1532
+ pending.delete(delivery.id);
1533
+ return;
1534
+ }
1535
+ const run = () => {
1536
+ pending.delete(delivery.id);
1537
+ track(attempt(delivery).then((ok) => {
1538
+ if (ok)
1539
+ delivery.state = "delivered";
1540
+ else
1541
+ schedule(delivery);
1542
+ }));
1543
+ };
1544
+ const delay = delays[index] ?? 0;
1545
+ if (delay <= 0) {
1546
+ pending.set(delivery.id, void 0);
1547
+ run();
1548
+ return;
1549
+ }
1550
+ const timer = scheduleTimer(run, delay);
1551
+ unref(timer);
1552
+ pending.set(delivery.id, timer);
1553
+ };
1554
+ const endpointsFor = (namespace) => [...own.get(namespace) ?? [], ...global];
1555
+ const fanOut = (message, state = "pending") => {
1556
+ for (const endpoint of endpointsFor(message.namespace)) {
1557
+ if (!matchesEndpoint(endpoint, message))
1558
+ continue;
1559
+ const delivery = {
1560
+ id: id("dlv_"),
1561
+ messageId: message.id,
1562
+ namespace: message.namespace,
1563
+ type: message.type,
1564
+ endpointId: endpoint.id ?? "we_unknown",
1565
+ url: endpoint.url,
1566
+ state,
1567
+ attempts: []
1568
+ };
1569
+ deliveries.set(delivery.id, delivery);
1570
+ payloads.set(delivery.id, { message, endpoint });
1571
+ if (state === "pending")
1572
+ schedule(delivery);
1573
+ }
1574
+ };
1575
+ const takeFault = (namespace) => {
1576
+ const queue = faults.get(namespace);
1577
+ const head = queue?.[0];
1578
+ if (!queue || !head)
1579
+ return void 0;
1580
+ head.remaining--;
1581
+ if (head.remaining <= 0)
1582
+ queue.shift();
1583
+ return head.mode;
1584
+ };
1585
+ const releaseHeld = (namespace) => {
1586
+ const waiting = held.get(namespace);
1587
+ if (!waiting)
1588
+ return;
1589
+ held.delete(namespace);
1590
+ for (const message of waiting)
1591
+ fanOut(message);
1592
+ };
1593
+ const hub = {
1594
+ publish(input) {
1595
+ const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
1596
+ const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
1597
+ const message = {
1598
+ id: input.id ?? id("msg_"),
1599
+ namespace: input.namespace,
1600
+ type: input.type,
1601
+ body,
1602
+ contentType,
1603
+ tags: input.tags ?? {},
1604
+ headers: input.headers ?? {},
1605
+ publishedAt: new Date(now()).toISOString()
1606
+ };
1607
+ messages.push(message);
1608
+ const ofNamespace = messages.filter((m) => m.namespace === message.namespace);
1609
+ const oldest = ofNamespace[0];
1610
+ if (ofNamespace.length > keep && oldest)
1611
+ messages.splice(messages.indexOf(oldest), 1);
1612
+ options.onMessage?.(message);
1613
+ const fault = takeFault(message.namespace);
1614
+ if (fault === "drop") {
1615
+ fanOut(message, "dropped");
1616
+ return message;
1617
+ }
1618
+ if (fault === "reorder") {
1619
+ held.set(message.namespace, [...held.get(message.namespace) ?? [], message]);
1620
+ return message;
1621
+ }
1622
+ fanOut(message);
1623
+ if (fault === "duplicate")
1624
+ fanOut(message);
1625
+ releaseHeld(message.namespace);
1626
+ return message;
1627
+ },
1628
+ setEndpoints(namespace, endpoints) {
1629
+ const withIds = endpoints.map((e, i) => ({ ...e, id: e.id ?? `we_${namespace}_${i}` }));
1630
+ own.set(namespace, withIds);
1631
+ return withIds;
1632
+ },
1633
+ endpoints: endpointsFor,
1634
+ messages: (namespace) => namespace === void 0 ? [...messages] : messages.filter((m) => m.namespace === namespace),
1635
+ deliveries: (namespace) => [...deliveries.values()].filter((d) => namespace === void 0 || d.namespace === namespace),
1636
+ async replay(id2) {
1637
+ const delivery = deliveries.get(id2);
1638
+ if (!delivery)
1639
+ return void 0;
1640
+ const ok = await attempt(delivery);
1641
+ if (ok)
1642
+ delivery.state = "delivered";
1643
+ return delivery;
1644
+ },
1645
+ async flush() {
1646
+ for (const namespace of [...held.keys()])
1647
+ releaseHeld(namespace);
1648
+ const waiting = [...pending.entries()];
1649
+ for (const [id2, timer] of waiting) {
1650
+ if (timer === void 0)
1651
+ continue;
1652
+ cancel(timer);
1653
+ pending.delete(id2);
1654
+ const delivery = deliveries.get(id2);
1655
+ if (!delivery)
1656
+ continue;
1657
+ track(attempt(delivery).then((ok) => {
1658
+ if (ok)
1659
+ delivery.state = "delivered";
1660
+ else
1661
+ schedule(delivery);
1662
+ }));
1663
+ }
1664
+ await hub.idle();
1665
+ },
1666
+ async idle() {
1667
+ while (inFlight.size > 0)
1668
+ await Promise.allSettled([...inFlight]);
1669
+ },
1670
+ fault(namespace, fault) {
1671
+ const queue = faults.get(namespace) ?? [];
1672
+ queue.push({ mode: fault.mode, remaining: Math.max(1, fault.count ?? 1) });
1673
+ faults.set(namespace, queue);
1674
+ },
1675
+ clear(namespace) {
1676
+ for (const [id2, delivery] of deliveries) {
1677
+ if (namespace !== void 0 && delivery.namespace !== namespace)
1678
+ continue;
1679
+ const timer = pending.get(id2);
1680
+ if (timer !== void 0)
1681
+ cancel(timer);
1682
+ pending.delete(id2);
1683
+ deliveries.delete(id2);
1684
+ payloads.delete(id2);
1685
+ }
1686
+ for (let i = messages.length - 1; i >= 0; i--) {
1687
+ if (namespace === void 0 || messages[i]?.namespace === namespace)
1688
+ messages.splice(i, 1);
1689
+ }
1690
+ if (namespace === void 0) {
1691
+ held.clear();
1692
+ faults.clear();
1693
+ own.clear();
1694
+ } else {
1695
+ held.delete(namespace);
1696
+ faults.delete(namespace);
1697
+ own.delete(namespace);
1698
+ }
1699
+ }
1700
+ };
1701
+ return hub;
1702
+ };
1703
+ var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1704
+ var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
1705
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1706
+ var parseEndpoint = (value) => {
1707
+ if (!isRecord3(value) || typeof value.url !== "string")
1708
+ return "each endpoint needs a url";
1709
+ try {
1710
+ new URL(value.url);
1711
+ } catch {
1712
+ return `not a URL: ${value.url}`;
1713
+ }
1714
+ const endpoint = { url: value.url };
1715
+ if (typeof value.id === "string")
1716
+ endpoint.id = value.id;
1717
+ if (typeof value.secret === "string")
1718
+ endpoint.secret = value.secret;
1719
+ if (typeof value.signUrl === "string")
1720
+ endpoint.signUrl = value.signUrl;
1721
+ const events = value.events ?? value.enabledEvents;
1722
+ if (Array.isArray(events))
1723
+ endpoint.events = events.map(String);
1724
+ if (isRecord3(value.tags)) {
1725
+ endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
1726
+ }
1727
+ if (typeof value.account === "string")
1728
+ endpoint.tags = { ...endpoint.tags, account: value.account };
1729
+ if (isRecord3(value.headers)) {
1730
+ endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
1731
+ }
1732
+ return endpoint;
1733
+ };
1734
+ var webhookAdminRoutes = (hub) => ({
1735
+ "GET /webhooks": ({ url, namespace }) => json3(200, {
1736
+ deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
1737
+ const type = url.searchParams.get("type");
1738
+ return type === null || d.type === type;
1739
+ })
1740
+ }),
1741
+ "GET /webhooks/events": ({ url, namespace }) => {
1742
+ const type = url.searchParams.get("type");
1743
+ return json3(200, {
1744
+ events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
1745
+ });
1746
+ },
1747
+ "POST /webhooks/:id/replay": async ({ params }) => {
1748
+ const replayed = await hub.replay(params.id);
1749
+ return replayed ? json3(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1750
+ },
1751
+ "POST /webhooks/flush": async () => {
1752
+ await hub.flush();
1753
+ return json3(200, { status: "ok" });
1754
+ },
1755
+ "POST /webhooks/faults": ({ body, namespace }) => {
1756
+ if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
1757
+ return adminError2(400, "mode must be duplicate, reorder or drop");
1758
+ }
1759
+ const fault = { mode: body.mode };
1760
+ if (typeof body.count === "number")
1761
+ fault.count = body.count;
1762
+ hub.fault(namespace, fault);
1763
+ return json3(201, { namespace, ...fault });
1764
+ },
1765
+ "GET /webhook-endpoints": ({ namespace }) => json3(200, {
1766
+ endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
1767
+ ...rest,
1768
+ secret: secret ? "(set)" : null
1769
+ }))
1770
+ }),
1771
+ "PUT /webhook-endpoints": ({ body, namespace }) => {
1772
+ const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
1773
+ if (!Array.isArray(list))
1774
+ return adminError2(400, "expected [{url, secret?, events?}]");
1775
+ const parsed = [];
1776
+ for (const each of list) {
1777
+ const endpoint = parseEndpoint(each);
1778
+ if (typeof endpoint === "string")
1779
+ return adminError2(400, endpoint);
1780
+ parsed.push(endpoint);
1781
+ }
1782
+ const set = hub.setEndpoints(namespace, parsed);
1783
+ return json3(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
1784
+ },
1785
+ "DELETE /webhook-endpoints": ({ namespace }) => {
1786
+ hub.setEndpoints(namespace, []);
1787
+ return json3(200, { status: "ok" });
1788
+ }
1789
+ });
1790
+ var parsePayload = (message) => {
1791
+ if (message.contentType.startsWith("application/json")) {
1792
+ try {
1793
+ return JSON.parse(message.body);
1794
+ } catch {
1795
+ return message.body;
1796
+ }
1797
+ }
1798
+ if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
1799
+ return Object.fromEntries(new URLSearchParams(message.body));
1800
+ }
1801
+ return message.body;
1802
+ };
1803
+
1804
+ // ../core/dist/runtime.js
1805
+ var MOCKINGBIRD_HEADER = "x-mockingbird";
1806
+ var BRANCH_HEADER = "x-mockingbird-branch";
1807
+ var AT_HEADER = "x-mockingbird-at";
1808
+ var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
1809
+ var DEFAULT_NAMESPACE = "default";
1810
+ var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1811
+ var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
1812
+ var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
1813
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1814
+ var effects = /* @__PURE__ */ new WeakMap();
1815
+ var reuseSorted = (fresh, previous, compare, equal) => {
1816
+ if (!previous || previous.length === 0)
1817
+ return fresh.map((row) => Object.freeze(row));
1818
+ const result = new Array(fresh.length);
1819
+ let unchanged = fresh.length === previous.length;
1820
+ let oldIndex = 0;
1821
+ for (let index = 0; index < fresh.length; index++) {
1822
+ const row = fresh[index];
1823
+ while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
1824
+ oldIndex++;
1825
+ }
1826
+ const old = previous[oldIndex];
1827
+ result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
1828
+ if (result[index] !== previous[index])
1829
+ unchanged = false;
1830
+ }
1831
+ return unchanged ? previous : result;
1832
+ };
1833
+ var DroppedConnectionError = class extends TypeError {
1834
+ code = "MOCKINGBIRD_DROP";
1835
+ constructor() {
1836
+ super("fetch failed: connection dropped by Mockingbird fault");
1837
+ this.name = "TypeError";
1838
+ }
1839
+ };
1840
+ var operationMatcher = (document2) => {
1841
+ const matchers = listOperations(document2).map((operation) => ({
1842
+ operationId: operation.operationId,
1843
+ method: operation.method.toUpperCase(),
1844
+ pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
1845
+ params: (operation.path.match(/\{/g) ?? []).length
1846
+ })).sort((a, b) => a.params - b.params);
1847
+ return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
1848
+ };
1849
+ var createRuntime = (options) => {
1850
+ const sqlite = bootSqlite(options.sqlite);
1851
+ const clock = options.clock ?? createClock();
1852
+ const rng = createRng(options.seed ?? 0);
1853
+ const wallNow = options.io?.wallNow ?? Date.now;
1854
+ const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
1855
+ const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1856
+ const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
1857
+ const metrics = createMetrics();
1858
+ const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
1859
+ const version = options.version ?? PACKAGE_VERSION;
1860
+ const instances = /* @__PURE__ */ new Map();
1861
+ const publicNamespaces = /* @__PURE__ */ new Set();
1862
+ const branchRngs = /* @__PURE__ */ new Map();
1863
+ const timelines = /* @__PURE__ */ new Map();
1864
+ const branchStorage = /* @__PURE__ */ new Map();
1865
+ const captured = /* @__PURE__ */ new Map();
1866
+ const credentials = createCredentialRegistry();
1867
+ const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
1868
+ const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
1869
+ const instanceFor = (key, publicNamespace = key, isolatedRng) => {
1870
+ const existing = instances.get(key);
1871
+ if (existing)
1872
+ return existing;
1873
+ if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
1874
+ throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
1875
+ }
1876
+ const created = options.create({
1877
+ namespace: storageNamespace(key),
1878
+ publicNamespace,
1879
+ sqlite,
1880
+ clock,
1881
+ rng: isolatedRng ?? rng
1882
+ });
1883
+ instances.set(key, created);
1884
+ publicNamespaces.add(publicNamespace);
1885
+ if (isolatedRng)
1886
+ branchRngs.set(key, isolatedRng);
1887
+ return created;
1888
+ };
1889
+ const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
1890
+ const capture = (storage) => {
1891
+ const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
1892
+ const previous = captured.get(storage);
1893
+ const snapshot2 = {
1894
+ namespace: fresh.namespace,
1895
+ records: reuseSorted(fresh.records, previous?.records, (left, right) => left.collection < right.collection ? -1 : left.collection > right.collection ? 1 : left.seq - right.seq, (left, right) => left.id === right.id && left.value === right.value),
1896
+ sequences: reuseSorted(fresh.sequences, previous?.sequences, (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : left.kind < right.kind ? -1 : left.kind > right.kind ? 1 : 0, (left, right) => left.value === right.value)
1897
+ };
1898
+ Object.freeze(snapshot2.records);
1899
+ Object.freeze(snapshot2.sequences);
1900
+ Object.freeze(snapshot2);
1901
+ captured.set(storage, snapshot2);
1902
+ return Object.freeze({
1903
+ snapshot: snapshot2,
1904
+ clock: Object.freeze(clock.state()),
1905
+ rngState: (branchRngs.get(storage) ?? rng).state()
1906
+ });
1907
+ };
1908
+ const timeline = (name = DEFAULT_NAMESPACE) => {
1909
+ let found = timelines.get(name);
1910
+ if (found)
1911
+ return found;
1912
+ instance(name);
1913
+ found = new Timeline({
1914
+ now: clock.now,
1915
+ ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
1916
+ });
1917
+ found.commit(capture(name));
1918
+ timelines.set(name, found);
1919
+ return found;
1920
+ };
1921
+ const physicalBranch = (namespace, branch2) => {
1922
+ if (branch2 === "main")
1923
+ return namespace;
1924
+ const mapKey = `${namespace}\0${branch2}`;
1925
+ const existing = branchStorage.get(mapKey);
1926
+ if (existing)
1927
+ return existing;
1928
+ const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
1929
+ branchStorage.set(mapKey, key);
1930
+ return key;
1931
+ };
1932
+ const ensureBranch = (namespace, branch2, at) => {
1933
+ if (!BRANCH_PATTERN2.test(branch2))
1934
+ throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
1935
+ const history = timeline(namespace);
1936
+ if (branch2 === "main") {
1937
+ if (at !== void 0) {
1938
+ const point = history.checkout("main", at);
1939
+ restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
1940
+ captured.set(namespace, point.value.snapshot);
1941
+ rng.setState(point.value.rngState);
1942
+ clock.set(point.value.clock.now);
1943
+ if (point.value.clock.frozen)
1944
+ clock.freeze();
1945
+ else
1946
+ clock.unfreeze();
1947
+ }
1948
+ return namespace;
1949
+ }
1950
+ const storage = physicalBranch(namespace, branch2);
1951
+ if (!history.hasBranch(branch2)) {
1952
+ if (at === void 0)
1953
+ history.commit(capture(namespace));
1954
+ const point = history.fork(branch2, at === void 0 ? {} : { from: at });
1955
+ const branchRng = createRng(options.seed ?? 0);
1956
+ if (point)
1957
+ branchRng.setState(point.value.rngState);
1958
+ instanceFor(storage, namespace, branchRng);
1959
+ if (point)
1960
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1961
+ if (point)
1962
+ captured.set(storage, point.value.snapshot);
1963
+ } else if (at !== void 0 && history.head(branch2)?.id !== at) {
1964
+ const point = history.checkout(branch2, at);
1965
+ if (!instances.has(storage)) {
1966
+ const branchRng = createRng(options.seed ?? 0);
1967
+ branchRng.setState(point.value.rngState);
1968
+ instanceFor(storage, namespace, branchRng);
1969
+ }
1970
+ branchRngs.get(storage)?.setState(point.value.rngState);
1971
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1972
+ captured.set(storage, point.value.snapshot);
1973
+ } else {
1974
+ if (!instances.has(storage)) {
1975
+ const point = history.head(branch2);
1976
+ const branchRng = createRng(options.seed ?? 0);
1977
+ if (point)
1978
+ branchRng.setState(point.value.rngState);
1979
+ instanceFor(storage, namespace, branchRng);
1980
+ }
1981
+ }
1982
+ return storage;
1983
+ };
1984
+ const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
1985
+ const storage = ensureBranch(namespace, branch2);
1986
+ return timeline(namespace).commit(capture(storage), { branch: branch2 });
1987
+ };
1988
+ const branch = (name, branchOptions = {}) => {
1989
+ const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
1990
+ ensureBranch(namespace, name, branchOptions.at);
1991
+ const head = timeline(namespace).head(name);
1992
+ if (!head)
1993
+ throw new RangeError(`branch ${name} has no checkpoint`);
1994
+ return head;
1995
+ };
1996
+ const checkout = (checkpointId, checkoutOptions = {}) => {
1997
+ const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
1998
+ const branchName = checkoutOptions.branch ?? "main";
1999
+ const history = timeline(namespace);
2000
+ const point = history.checkout(branchName, checkpointId);
2001
+ const storage = ensureBranch(namespace, branchName);
2002
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
2003
+ captured.set(storage, point.value.snapshot);
2004
+ clock.set(point.value.clock.now);
2005
+ if (point.value.clock.frozen)
2006
+ clock.freeze();
2007
+ else
2008
+ clock.unfreeze();
2009
+ (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
2010
+ };
2011
+ const reset = async (name = DEFAULT_NAMESPACE) => {
2012
+ if (name === "*") {
2013
+ options.webhooks?.clear();
2014
+ for (const each of instances.values())
2015
+ await each.reset();
2016
+ timelines.clear();
2017
+ branchStorage.clear();
2018
+ branchRngs.clear();
2019
+ captured.clear();
2020
+ return;
2021
+ }
2022
+ options.webhooks?.clear(name);
2023
+ const target = instances.get(name);
2024
+ if (target)
2025
+ await target.reset();
2026
+ else
2027
+ clearNamespace(sqlite, storageNamespace(name));
2028
+ for (const [mapping, storage] of branchStorage) {
2029
+ if (!mapping.startsWith(`${name}\0`))
2030
+ continue;
2031
+ const branchInstance = instances.get(storage);
2032
+ if (branchInstance)
2033
+ await branchInstance.reset();
2034
+ else
2035
+ clearNamespace(sqlite, storageNamespace(storage));
2036
+ branchStorage.delete(mapping);
2037
+ branchRngs.delete(storage);
2038
+ captured.delete(storage);
2039
+ }
2040
+ timelines.delete(name);
2041
+ captured.delete(name);
2042
+ };
2043
+ const snapshot = (name = DEFAULT_NAMESPACE) => {
2044
+ return checkpoint(name, "main").value.snapshot;
2045
+ };
2046
+ const restore = (from, name = DEFAULT_NAMESPACE) => {
2047
+ instance(name);
2048
+ restoreNamespace(sqlite, storageNamespace(name), from);
2049
+ captured.set(name, from);
2050
+ const history = timelines.get(name);
2051
+ if (history)
2052
+ history.commit(capture(name), { branch: "main" });
2053
+ else
2054
+ timeline(name);
2055
+ };
2056
+ const runtime = {
2057
+ name: options.name,
2058
+ sqlite,
2059
+ clock,
2060
+ faults,
2061
+ metrics,
2062
+ journal,
2063
+ rng,
2064
+ credentials,
2065
+ webhooks: options.webhooks,
2066
+ applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
2067
+ const preset = options.presets?.[name];
2068
+ if (!preset)
2069
+ throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
2070
+ const added = (preset.rules ?? []).map((rule, index) => faults.add({
2071
+ namespace,
2072
+ ...rule,
2073
+ ...overrides,
2074
+ preset: name,
2075
+ id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
2076
+ }));
2077
+ if (preset.webhook && options.webhooks) {
2078
+ options.webhooks.fault(namespace, {
2079
+ ...preset.webhook,
2080
+ ...overrides.count !== void 0 ? { count: overrides.count } : {}
2081
+ });
2082
+ }
2083
+ return added;
2084
+ },
2085
+ instance,
2086
+ namespaces: () => [...publicNamespaces].sort(),
2087
+ reset,
2088
+ snapshot,
2089
+ restore,
2090
+ checkpoint,
2091
+ branch,
2092
+ checkout,
2093
+ timeline,
2094
+ fetch: async (incoming) => {
2095
+ let request = incoming;
2096
+ const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
2097
+ if (prefixed) {
2098
+ const url2 = new URL(request.url);
2099
+ url2.pathname = prefixed[2] ?? "/";
2100
+ const headers = new Headers(request.headers);
2101
+ if (!headers.has(NAMESPACE_HEADER)) {
2102
+ headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
2103
+ }
2104
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
2105
+ request = new Request(url2, {
2106
+ method: request.method,
2107
+ headers,
2108
+ ...hasBody ? { body: await request.arrayBuffer() } : {},
2109
+ signal: request.signal
2110
+ });
2111
+ }
2112
+ let namespace = control.namespaceOf(request);
2113
+ if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
2114
+ const credential = options.credential(request);
2115
+ const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
2116
+ if (mapped !== void 0)
2117
+ namespace = mapped;
2118
+ }
2119
+ const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
2120
+ const at = request.headers.get(AT_HEADER) ?? void 0;
2121
+ const stamp = (response2) => {
2122
+ const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
2123
+ try {
2124
+ response2.headers.set(MOCKINGBIRD_HEADER, value);
2125
+ return response2;
2126
+ } catch {
2127
+ const copy = new Response(response2.body, response2);
2128
+ copy.headers.set(MOCKINGBIRD_HEADER, value);
2129
+ return copy;
2130
+ }
2131
+ };
2132
+ const handled = await control.handle(request);
2133
+ if (handled)
2134
+ return stamp(handled);
2135
+ const started = monotonicNow();
2136
+ const url = new URL(request.url);
2137
+ const operationId = operationIdFor(request, url.pathname);
2138
+ const log = (status, faultId, response2) => {
2139
+ const noted = response2 ? responseNotes(response2) : void 0;
2140
+ const entry = {
2141
+ service: options.name,
2142
+ namespace,
2143
+ operationId,
2144
+ method: request.method,
2145
+ path: url.pathname,
2146
+ status,
2147
+ durationMs: Math.round((monotonicNow() - started) * 100) / 100,
2148
+ unmatched: options.document !== void 0 && operationId === void 0,
2149
+ ...faultId !== void 0 ? { faultId } : {},
2150
+ ...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
2151
+ ...noted?.adopted ? { adopted: true } : {}
2152
+ };
2153
+ metrics.record(entry);
2154
+ journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
2155
+ options.onLog?.(entry);
2156
+ };
2157
+ if (!NAMESPACE_PATTERN.test(namespace)) {
2158
+ log(400);
2159
+ return stamp(new Response(JSON.stringify({
2160
+ error: {
2161
+ type: "mockingbird_admin",
2162
+ message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
2163
+ }
2164
+ }), { status: 400, headers: { "content-type": "application/json" } }));
2165
+ }
2166
+ if (!BRANCH_PATTERN2.test(selectedBranch)) {
2167
+ log(400);
2168
+ return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
2169
+ }
2170
+ let storage;
2171
+ try {
2172
+ if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
2173
+ const point = timeline(namespace).get(at);
2174
+ storage = physicalBranch(namespace, `at_${at}`);
2175
+ let viewRng = branchRngs.get(storage);
2176
+ if (!viewRng) {
2177
+ viewRng = createRng(options.seed ?? 0);
2178
+ instanceFor(storage, namespace, viewRng);
2179
+ }
2180
+ viewRng.setState(point.value.rngState);
2181
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
2182
+ captured.set(storage, point.value.snapshot);
2183
+ } else {
2184
+ storage = ensureBranch(namespace, selectedBranch, at);
2185
+ }
2186
+ } catch (error) {
2187
+ log(409);
2188
+ return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
2189
+ }
2190
+ const hits = await faults.take({
2191
+ operationId,
2192
+ method: request.method,
2193
+ path: url.pathname,
2194
+ namespace
2195
+ });
2196
+ const final = hits.find((hit) => hit.drop || hit.response);
2197
+ if (final?.drop) {
2198
+ log(0, final.id);
2199
+ throw new DroppedConnectionError();
2200
+ }
2201
+ if (final?.response) {
2202
+ log(final.response.status, final.id);
2203
+ return stamp(final.response);
2204
+ }
2205
+ const fired = hits.filter((hit) => hit.effect !== void 0);
2206
+ if (fired.length > 0)
2207
+ effects.set(request, fired.map((hit) => hit.effect));
2208
+ let response = await instanceFor(storage, namespace).fetch(request);
2209
+ if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
2210
+ const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
2211
+ response = mutableResponse(response);
2212
+ response.headers.set(CHECKPOINT_HEADER, point.id);
2213
+ }
2214
+ if (selectedBranch !== "main") {
2215
+ response = mutableResponse(response);
2216
+ response.headers.set(BRANCH_HEADER, selectedBranch);
2217
+ }
2218
+ if (at !== void 0) {
2219
+ response = mutableResponse(response);
2220
+ response.headers.set(AT_HEADER, at);
2221
+ }
2222
+ log(response.status, fired[0]?.id, response);
2223
+ return stamp(response);
2224
+ }
2225
+ };
2226
+ const control = createControlPlane({
2227
+ name: options.name,
2228
+ startedAt: wallNow(),
2229
+ wallNow,
2230
+ clock,
2231
+ faults,
2232
+ metrics,
2233
+ journal,
2234
+ defaultNamespace: DEFAULT_NAMESPACE,
2235
+ namespaces: runtime.namespaces,
2236
+ reset,
2237
+ timeTravel: {
2238
+ checkpoint: (name, branchName) => {
2239
+ const point = checkpoint(name, branchName);
2240
+ return {
2241
+ id: point.id,
2242
+ branch: point.branch,
2243
+ parent: point.parent,
2244
+ at: point.at,
2245
+ records: point.value.snapshot.records.length
2246
+ };
2247
+ },
2248
+ branch: (branchName, branchOptions) => {
2249
+ const point = branch(branchName, branchOptions);
2250
+ return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
2251
+ },
2252
+ checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
2253
+ retain: (name, checkpointId) => {
2254
+ timeline(name).retain(checkpointId);
2255
+ },
2256
+ release: (name, checkpointId) => timeline(name).release(checkpointId),
2257
+ inspect: (name) => {
2258
+ const history = timeline(name);
2259
+ return {
2260
+ branches: history.branches(),
2261
+ checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
2262
+ id,
2263
+ branch: branchName,
2264
+ parent,
2265
+ at
2266
+ }))
2267
+ };
2268
+ }
2269
+ },
2270
+ describe: options.describe ?? (() => ({})),
2271
+ ...options.presets ? {
2272
+ applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
2273
+ } : {},
2274
+ routes: {
2275
+ ...credentialRoutes(credentials),
2276
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
2277
+ ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
2278
+ ...options.admin?.(runtime) ?? {}
2279
+ },
2280
+ adminKey: options.adminKey
2281
+ });
2282
+ return runtime;
2283
+ };
2284
+ var mutableResponse = (response) => {
2285
+ try {
2286
+ response.headers.set("x-mockingbird-mutable-probe", "1");
2287
+ response.headers.delete("x-mockingbird-mutable-probe");
2288
+ return response;
2289
+ } catch {
2290
+ return new Response(response.body, response);
2291
+ }
2292
+ };
2293
+ var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2294
+ var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
2295
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2296
+ var credentialRoutes = (registry) => ({
2297
+ "GET /credentials": () => adminJson(200, {
2298
+ credentials: registry.entries().map(({ credential, namespace }) => ({
2299
+ credential: maskCredential(credential),
2300
+ namespace
2301
+ }))
2302
+ }),
2303
+ "PUT /credentials": ({ body, namespace }) => {
2304
+ const pairs = [];
2305
+ const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
2306
+ if (Array.isArray(list)) {
2307
+ for (const each of list) {
2308
+ if (typeof each === "string")
2309
+ pairs.push([each, namespace]);
2310
+ else if (isObject(each) && typeof each.credential === "string") {
2311
+ pairs.push([
2312
+ each.credential,
2313
+ typeof each.namespace === "string" ? each.namespace : namespace
2314
+ ]);
2315
+ } else
2316
+ return adminFail(400, "each entry is a credential string or {credential, namespace}");
2317
+ }
2318
+ } else if (isObject(list)) {
2319
+ for (const [credential, target] of Object.entries(list)) {
2320
+ if (typeof target !== "string")
2321
+ return adminFail(400, `namespace for ${credential} must be a string`);
2322
+ pairs.push([credential, target]);
2323
+ }
2324
+ } else if (isObject(body) && typeof body.credential === "string") {
2325
+ pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
2326
+ } else {
2327
+ return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
2328
+ }
2329
+ for (const [credential, target] of pairs) {
2330
+ if (!NAMESPACE_PATTERN.test(target))
2331
+ return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
2332
+ registry.set(credential, target);
2333
+ }
2334
+ return adminJson(200, { mapped: pairs.length });
2335
+ },
2336
+ "DELETE /credentials": ({ url }) => {
2337
+ const credential = url.searchParams.get("credential");
2338
+ if (credential === null)
2339
+ registry.clear();
2340
+ else
2341
+ registry.remove(credential);
2342
+ return adminJson(200, { status: "ok" });
2343
+ }
2344
+ });
2345
+ var presetRoutes = (presets, runtime) => ({
2346
+ "GET /faults/presets": () => adminJson(200, {
2347
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
2348
+ }),
2349
+ "POST /faults/presets/:name": ({ params, body, namespace }) => {
2350
+ const name = params.name;
2351
+ if (!presets[name])
2352
+ return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
2353
+ const overrides = isObject(body) ? body : {};
2354
+ return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
2355
+ }
2356
+ });
2357
+
2358
+ // src/generated/openapi.ts
2359
+ var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Twilio Verify, Lookup, Messaging and Recordings (Mockingbird subset)","description":"Stateful mock subset of Twilio, served on one port. Each product keeps its own host upstream\\n(api.twilio.com, verify.twilio.com, lookups.twilio.com); the mock routes by that host carried\\nas a path prefix (\`/api/\u2026\`, \`/verify/\u2026\`, \`/lookups/\u2026\`), which is what a custom twilio-node\\n\`httpClient\` produces when it rewrites \`https://<product>.twilio.com/<path>\` to\\n\`{mock}/<product>/<path>\`. Hand-authored from Twilio's published API reference, trimmed to\\nwhat our consumer calls, and checked against live Lookup v2 responses.\\n","version":"2010-04-01","x-mockingbird-upstream":{"note":"Trimmed from Twilio's public API reference (twilio-oai) to the operations our consumer calls (twilio.service.ts, twilio-conversation-sms.adapter.ts, notification-dispatcher channels/sms.ts, twilio-recording-http.adapter.ts); Lookup v2 shapes verified against the live API."}},"servers":[{"url":"https://api.twilio.com","description":"Every product on one origin, routed by the /api, /verify or /lookups prefix."}],"security":[{"basicAuth":[]}],"paths":{"/lookups/v2/PhoneNumbers/{PhoneNumber}":{"parameters":[{"name":"PhoneNumber","in":"path","required":true,"description":"E.164 (\`+12025550123\`) or national format; formatting characters are ignored.","schema":{"type":"string","minLength":1,"maxLength":24,"pattern":"^[+]?[0-9 ()-]{1,20}$","examples":["+12025550123","+13105550142","2025550199","+15550100","+442079460123"]}},{"name":"CountryCode","in":"query","required":false,"description":"ISO-3166 alpha-2 region used to parse a national-format number.","schema":{"type":"string","enum":["US","CA","GB"]}},{"name":"Fields","in":"query","required":false,"description":"Paid data packages (line_type_intelligence, caller_name, \u2026); the mock answers null for every package.","schema":{"type":"string","x-mockingbird-unsupported":{"reason":"Data packages cost money upstream and our consumer never requests them."}}}],"get":{"operationId":"FetchPhoneNumber","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Lookup result (also for invalid numbers, with \`valid:false\` and \`validation_errors\`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumber"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/verify/v2/Services/{ServiceSid}/Verifications":{"parameters":[{"$ref":"#/components/parameters/ServiceSid"}],"post":{"operationId":"CreateVerification","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["To","Channel"],"properties":{"To":{"type":"string","description":"E.164 phone number (or an email address for Channel=email).","pattern":"^[+][1-9][0-9]{7,14}$","examples":["+12025550123","+13105550142","+14155550100"]},"Channel":{"type":"string","enum":["sms","call","email","whatsapp","sna","auto"]},"CustomCode":{"type":"string","pattern":"^[0-9]{4,10}$"},"Locale":{"type":"string","maxLength":10}}}}}},"responses":{"201":{"description":"Verification started (or the pending one re-sent)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"400":{"description":"Invalid parameter (code 60200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown Verify service (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Max send attempts reached (code 60203)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/verify/v2/Services/{ServiceSid}/Verifications/{Sid}":{"parameters":[{"$ref":"#/components/parameters/ServiceSid"},{"name":"Sid","in":"path","required":true,"description":"The verification sid (\`VE\u2026\`).","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"verification","missing":"VE00000000000000000000000000000000"}}}],"get":{"operationId":"FetchVerification","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The verification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or expired verification (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"UpdateVerification","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["Status"],"properties":{"Status":{"type":"string","enum":["canceled","approved"]}}}}}},"responses":{"200":{"description":"The updated verification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"400":{"description":"Invalid parameter (code 60200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown, expired or no longer pending verification (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/verify/v2/Services/{ServiceSid}/VerificationCheck":{"parameters":[{"$ref":"#/components/parameters/ServiceSid"}],"post":{"operationId":"CreateVerificationCheck","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["Code"],"properties":{"Code":{"type":"string","pattern":"^[0-9]{4,10}$","examples":["000000","123456"]},"To":{"type":"string","pattern":"^[+][1-9][0-9]{7,14}$","examples":["+12025550123","+13105550142"]},"VerificationSid":{"type":"string","x-mockingbird-resource-ref":{"type":"verification","missing":"VE00000000000000000000000000000000"}}}}}}},"responses":{"200":{"description":"Check result; \`status\` is \`approved\` only for the right code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationCheck"}}}},"400":{"description":"Invalid parameter (code 60200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No pending verification (missing, approved, canceled or expired; code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Max check attempts reached (code 60202)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/2010-04-01/Accounts/{AccountSid}/Messages.json":{"parameters":[{"$ref":"#/components/parameters/AccountSid"}],"post":{"operationId":"CreateMessage","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["To"],"properties":{"To":{"type":"string","pattern":"^[+][1-9][0-9]{7,14}$","examples":["+12025550123","+13105550142"]},"Body":{"type":"string","maxLength":1600},"From":{"type":"string","pattern":"^[+][1-9][0-9]{7,14}$","examples":["+15005550006"]},"MessagingServiceSid":{"type":"string","pattern":"^MG[0-9a-f]{32}$"},"MediaUrl":{"type":"string","format":"uri"},"StatusCallback":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"Message accepted (\`queued\`, or \`accepted\` through a Messaging Service)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Invalid request (21211 bad To, 21602 no Body, 21603 no From, 21604 no To, 21617 too long)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Malformed account sid (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/2010-04-01/Accounts/{AccountSid}/Messages/{Sid}.json":{"parameters":[{"$ref":"#/components/parameters/AccountSid"},{"name":"Sid","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"message","missing":"SM00000000000000000000000000000000"}}}],"get":{"operationId":"FetchMessage","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The message","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown message (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/2010-04-01/Accounts/{AccountSid}/Recordings/{Sid}.wav":{"parameters":[{"$ref":"#/components/parameters/AccountSid"},{"$ref":"#/components/parameters/RecordingSid"},{"name":"RequestedChannels","in":"query","required":false,"description":"2 returns a dual-channel recording as two channels; otherwise it is mixed down to mono.","schema":{"type":"integer","enum":[1,2]}}],"get":{"operationId":"FetchRecordingMedia","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"RIFF WAV (PCM)","content":{"audio/x-wav":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or deleted recording (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/2010-04-01/Accounts/{AccountSid}/Recordings/{Sid}.json":{"parameters":[{"$ref":"#/components/parameters/AccountSid"},{"$ref":"#/components/parameters/RecordingSid"}],"get":{"operationId":"FetchRecording","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Recording metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recording"}}}},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or deleted recording (code 20404)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"DeleteRecording","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"responses":{"204":{"description":"Deleted"},"401":{"description":"Bad credentials (code 20003)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or already deleted recording (code 20404; our consumer tolerates it)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"basicAuth":{"type":"http","scheme":"basic","description":"AccountSid (or an SK API key sid) as the username, AuthToken (or key secret) as the password."}},"parameters":{"ServiceSid":{"name":"ServiceSid","in":"path","required":true,"description":"The Verify service sid (\`VA\u2026\`).","schema":{"type":"string","pattern":"^VA[0-9a-f]{32}$","examples":["VA0123456789abcdef0123456789abcdef"]}},"AccountSid":{"name":"AccountSid","in":"path","required":true,"schema":{"type":"string","pattern":"^AC[0-9a-f]{32}$","examples":["AC33333333333333333333333333333333"]}},"RecordingSid":{"name":"Sid","in":"path","required":true,"description":"The recording sid (\`RE\u2026\`).","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"recording","missing":"RE00000000000000000000000000000000"}}}},"schemas":{"Error":{"type":"object","required":["code","message","more_info","status"],"properties":{"code":{"type":"integer"},"message":{"type":"string"},"more_info":{"type":"string"},"status":{"type":"integer"}}},"PhoneNumber":{"type":"object","required":["calling_country_code","country_code","phone_number","national_format","valid","validation_errors","url"],"properties":{"calling_country_code":{"type":["string","null"]},"country_code":{"type":["string","null"]},"phone_number":{"type":"string"},"national_format":{"type":["string","null"]},"valid":{"type":"boolean"},"validation_errors":{"type":"array","items":{"type":"string","enum":["TOO_SHORT","TOO_LONG","INVALID_BUT_POSSIBLE","INVALID_COUNTRY_CODE","INVALID_LENGTH","NOT_A_NUMBER"]}},"caller_name":{"type":["object","null"]},"sim_swap":{"type":["object","null"]},"call_forwarding":{"type":["object","null"]},"line_status":{"type":["object","null"]},"line_type_intelligence":{"type":["object","null"]},"identity_match":{"type":["object","null"]},"reassigned_number":{"type":["object","null"]},"sms_pumping_risk":{"type":["object","null"]},"phone_number_quality_score":{"type":["object","null"]},"pre_fill":{"type":["object","null"]},"url":{"type":"string"}}},"SendCodeAttempt":{"type":"object","required":["attempt_sid","channel","time"],"properties":{"attempt_sid":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"channel":{"type":"string"},"time":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"Verification":{"type":"object","required":["sid","service_sid","account_sid","to","channel","status","valid","date_created","date_updated","send_code_attempts","url"],"properties":{"sid":{"type":"string","x-mockingbird-resource":{"type":"verification","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"service_sid":{"type":"string"},"account_sid":{"type":"string"},"to":{"type":"string"},"channel":{"type":"string"},"status":{"type":"string","enum":["pending","approved","canceled","expired"]},"valid":{"type":"boolean"},"lookup":{"type":["object","null"]},"amount":{"type":["string","null"]},"payee":{"type":["string","null"]},"send_code_attempts":{"type":"array","items":{"$ref":"#/components/schemas/SendCodeAttempt"}},"sna":{"type":["object","null"]},"date_created":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"date_updated":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}},"VerificationCheck":{"type":"object","required":["sid","service_sid","account_sid","to","channel","status","valid","date_created","date_updated"],"properties":{"sid":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"service_sid":{"type":"string"},"account_sid":{"type":"string"},"to":{"type":"string"},"channel":{"type":"string"},"status":{"type":"string","enum":["pending","approved","canceled","expired"]},"valid":{"type":"boolean"},"amount":{"type":["string","null"]},"payee":{"type":["string","null"]},"sna_attempts_error_codes":{"type":"array","items":{"type":"object"}},"date_created":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"date_updated":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"Message":{"type":"object","required":["sid","account_sid","to","from","body","status","direction","num_segments","num_media","api_version","date_created","date_updated","uri"],"properties":{"sid":{"type":"string","x-mockingbird-resource":{"type":"message","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"account_sid":{"type":"string"},"api_version":{"type":"string"},"body":{"type":"string"},"to":{"type":"string"},"from":{"type":["string","null"]},"messaging_service_sid":{"type":["string","null"]},"status":{"type":"string","enum":["queued","accepted","sending","sent","delivered","undelivered","failed","received"]},"direction":{"type":"string"},"num_segments":{"type":"string"},"num_media":{"type":"string"},"price":{"type":["string","null"]},"price_unit":{"type":["string","null"]},"error_code":{"type":["integer","null"]},"error_message":{"type":["string","null"]},"date_created":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"date_updated":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"date_sent":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"timestamp"}},"uri":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"subresource_uris":{"type":"object","x-mockingbird-volatile":{"kind":"url"}}}},"Recording":{"type":"object","required":["sid","account_sid","call_sid","status","channels","duration","uri","media_url","date_created"],"properties":{"sid":{"type":"string","x-mockingbird-resource":{"type":"recording","identity":true}},"account_sid":{"type":"string"},"api_version":{"type":"string"},"call_sid":{"type":"string"},"status":{"type":"string"},"channels":{"type":"integer"},"duration":{"type":"string"},"source":{"type":"string"},"price":{"type":["string","null"]},"price_unit":{"type":["string","null"]},"error_code":{"type":["integer","null"]},"start_time":{"type":"string"},"date_created":{"type":"string"},"date_updated":{"type":"string"},"uri":{"type":"string"},"media_url":{"type":"string"}}}}}}`);
2360
+ var operationIds = ["FetchPhoneNumber", "CreateVerification", "FetchVerification", "UpdateVerification", "CreateVerificationCheck", "CreateMessage", "FetchMessage", "FetchRecordingMedia", "FetchRecording", "DeleteRecording"];
2361
+ var supportedOperationIds = ["FetchPhoneNumber", "CreateVerification", "FetchVerification", "UpdateVerification", "CreateVerificationCheck", "CreateMessage", "FetchMessage", "FetchRecordingMedia", "FetchRecording", "DeleteRecording"];
2362
+
2363
+ // src/phone.ts
2364
+ import {
2365
+ getCountryCallingCode,
2366
+ parsePhoneNumber,
2367
+ validatePhoneNumberLength
2368
+ } from "libphonenumber-js/max";
2369
+ var LOOKUP_BASE = "https://lookups.twilio.com/v2/PhoneNumbers/";
2370
+ var PACKAGES = {
2371
+ caller_name: null,
2372
+ sim_swap: null,
2373
+ call_forwarding: null,
2374
+ line_status: null,
2375
+ line_type_intelligence: null,
2376
+ identity_match: null,
2377
+ reassigned_number: null,
2378
+ sms_pumping_risk: null,
2379
+ phone_number_quality_score: null,
2380
+ pre_fill: null
2381
+ };
2382
+ var LOCAL_ONLY_LENGTHS = {
2383
+ "1": [7],
2384
+ "44": [4, 5, 6, 8]
2385
+ };
2386
+ var PUNCTUATION = "-x\u2010-\u2015\u2212\u30FC\uFF0D-\uFF0F \xA0\xAD\u200B\u2060\u3000()\uFF08\uFF09\uFF3B\uFF3D.\\[\\]/~\u2053\u223C\uFF5E";
2387
+ var VIABLE = new RegExp(
2388
+ `^(?:[0-9]{2}|\\+*(?:[${PUNCTUATION}*]*[0-9]){3,}[${PUNCTUATION}*]*[A-Za-z0-9]*)$`
2389
+ );
2390
+ var KEYPAD = {
2391
+ a: "2",
2392
+ b: "2",
2393
+ c: "2",
2394
+ d: "3",
2395
+ e: "3",
2396
+ f: "3",
2397
+ g: "4",
2398
+ h: "4",
2399
+ i: "4",
2400
+ j: "5",
2401
+ k: "5",
2402
+ l: "5",
2403
+ m: "6",
2404
+ n: "6",
2405
+ o: "6",
2406
+ p: "7",
2407
+ q: "7",
2408
+ r: "7",
2409
+ s: "7",
2410
+ t: "8",
2411
+ u: "8",
2412
+ v: "8",
2413
+ w: "9",
2414
+ x: "9",
2415
+ y: "9",
2416
+ z: "9"
2417
+ };
2418
+ var digitsOf = (value) => value.toLowerCase().replace(/[a-z]/g, (letter) => KEYPAD[letter] ?? "").replace(/[^0-9]/g, "");
2419
+ var invalid = (phone, error, nationalFormat = null) => ({
2420
+ calling_country_code: null,
2421
+ country_code: null,
2422
+ phone_number: phone,
2423
+ national_format: nationalFormat,
2424
+ valid: false,
2425
+ validation_errors: [error]
2426
+ });
2427
+ var callingCodeOf = (country) => {
2428
+ try {
2429
+ return getCountryCallingCode(country ?? "US");
2430
+ } catch {
2431
+ return "1";
2432
+ }
2433
+ };
2434
+ var lookup = (raw, countryCode) => {
2435
+ const input = raw.trim();
2436
+ const international = input.startsWith("+");
2437
+ const display = displayOf(input, countryCode);
2438
+ if (!VIABLE.test(input)) return invalid(display, "NOT_A_NUMBER");
2439
+ const digits = digitsOf(input);
2440
+ let parsed;
2441
+ try {
2442
+ parsed = international ? parsePhoneNumber(`+${digits}`) : parsePhoneNumber(digits, { defaultCountry: countryCode ?? "US" });
2443
+ } catch (error) {
2444
+ const reason = error instanceof Error ? error.message : "";
2445
+ if (reason === "INVALID_COUNTRY") return invalid(display, "INVALID_COUNTRY_CODE");
2446
+ if (reason === "TOO_LONG") return invalid(display, "TOO_LONG");
2447
+ if (reason === "TOO_SHORT") return invalid(display, "TOO_SHORT");
2448
+ return invalid(display, "NOT_A_NUMBER");
2449
+ }
2450
+ if (!parsed) return invalid(display, "NOT_A_NUMBER");
2451
+ if (parsed.isValid()) {
2452
+ return {
2453
+ calling_country_code: parsed.countryCallingCode,
2454
+ country_code: parsed.country ?? null,
2455
+ phone_number: parsed.number,
2456
+ national_format: parsed.formatNational(),
2457
+ valid: true,
2458
+ validation_errors: []
2459
+ };
2460
+ }
2461
+ if (!international && countryCode === void 0) return invalid(display, "INVALID_COUNTRY_CODE");
2462
+ const national = parsed.nationalNumber;
2463
+ const echoed = international ? `+${digits}` : `+${parsed.countryCallingCode}${digits}`;
2464
+ return invalid(echoed, possibility(parsed.countryCallingCode, national), national);
2465
+ };
2466
+ var possibility = (callingCode, national) => {
2467
+ if ((LOCAL_ONLY_LENGTHS[callingCode] ?? []).includes(national.length)) {
2468
+ return "INVALID_BUT_POSSIBLE";
2469
+ }
2470
+ const reason = validatePhoneNumberLength(`+${callingCode}${national}`);
2471
+ if (reason === "TOO_SHORT" || reason === "TOO_LONG" || reason === "INVALID_LENGTH") return reason;
2472
+ return "INVALID_BUT_POSSIBLE";
2473
+ };
2474
+ var displayOf = (raw, countryCode) => {
2475
+ const input = raw.trim();
2476
+ return input.startsWith("+") ? input : `+${callingCodeOf(countryCode)}${input}`;
2477
+ };
2478
+ var lookupBody = (result, raw, countryCode) => ({
2479
+ ...PACKAGES,
2480
+ ...result,
2481
+ // Live: the url names the normalized number when valid, else the input as sent.
2482
+ url: `${LOOKUP_BASE}${result.valid ? result.phone_number : encodeURI(displayOf(raw, countryCode))}`
2483
+ });
2484
+ var e164Key = (value) => `+${value.replace(/[^0-9]/g, "")}`;
2485
+
2486
+ // src/state.ts
2487
+ var DEFAULT_VERIFY_SETTINGS = {
2488
+ fixedCode: null,
2489
+ ttlSeconds: 600,
2490
+ maxCheckAttempts: 5,
2491
+ maxSendAttempts: 5,
2492
+ sendWindowSeconds: 600
2493
+ };
2494
+ var mix2 = (input) => {
2495
+ let hash = 2166136261;
2496
+ for (let i = 0; i < input.length; i++) {
2497
+ hash ^= input.charCodeAt(i);
2498
+ hash = Math.imul(hash, 16777619) >>> 0;
2499
+ }
2500
+ hash ^= hash >>> 16;
2501
+ hash = Math.imul(hash, 2246822507) >>> 0;
2502
+ hash ^= hash >>> 13;
2503
+ return hash >>> 0;
2504
+ };
2505
+ var hexOf = (input, length = 32) => {
2506
+ let out = "";
2507
+ for (let round = 0; out.length < length; round++) {
2508
+ out += mix2(`${input}:${round}`).toString(16).padStart(8, "0");
2509
+ }
2510
+ return out.slice(0, length);
2511
+ };
2512
+ var digitsOf2 = (input, length = 6) => {
2513
+ let out = "";
2514
+ for (let round = 0; out.length < length; round++) {
2515
+ out += String(mix2(`${input}:${round}`) % 1e9).padStart(9, "0");
2516
+ }
2517
+ return out.slice(0, length);
2518
+ };
2519
+ var TwilioState = class {
2520
+ constructor(sqlite, namespace, seed = {}) {
2521
+ this.namespace = namespace;
2522
+ this.seed = seed;
2523
+ this.verifications = new Collection(sqlite, namespace, "verifications");
2524
+ this.messages = new Collection(sqlite, namespace, "messages");
2525
+ this.recordings = new Collection(sqlite, namespace, "recordings");
2526
+ this.lookups = new Collection(sqlite, namespace, "lookups");
2527
+ this.settings = new Collection(sqlite, namespace, "settings");
2528
+ this.outbox = new OutboxStore(sqlite, namespace);
2529
+ this.ids = new IdSequence(sqlite, namespace, "twilio");
2530
+ }
2531
+ namespace;
2532
+ seed;
2533
+ verifications;
2534
+ messages;
2535
+ recordings;
2536
+ lookups;
2537
+ settings;
2538
+ outbox;
2539
+ ids;
2540
+ /** A Twilio sid: two-letter prefix plus 32 hex characters, deterministic per history. */
2541
+ sid(prefix) {
2542
+ return `${prefix}${hexOf(`${this.namespace}:${this.ids.next(prefix)}`)}`;
2543
+ }
2544
+ /** A random-looking but reproducible 6-digit code for a new verification. */
2545
+ code(sid, length = 6) {
2546
+ return digitsOf2(`${this.namespace}:code:${sid}`, length);
2547
+ }
2548
+ verify() {
2549
+ return this.settings.get("verify") ?? { ...DEFAULT_VERIFY_SETTINGS, ...this.seed };
2550
+ }
2551
+ updateVerify(patch) {
2552
+ const next = { ...this.verify(), ...patch };
2553
+ if (this.settings.has("verify")) this.settings.update("verify", next);
2554
+ else this.settings.insert("verify", next);
2555
+ return next;
2556
+ }
2557
+ /** Newest first. */
2558
+ verificationsTo(to, serviceSid) {
2559
+ return this.verifications.list({
2560
+ where: (v) => v.to === to && (serviceSid === void 0 || v.serviceSid === serviceSid)
2561
+ }).map((row) => row.value);
2562
+ }
2563
+ };
2564
+
2565
+ // src/wav.ts
2566
+ var ascii = (bytes, offset, length) => String.fromCharCode(...bytes.subarray(offset, offset + length));
2567
+ var readWav = (bytes) => {
2568
+ if (bytes.length < 12 || ascii(bytes, 0, 4) !== "RIFF" || ascii(bytes, 8, 4) !== "WAVE") {
2569
+ return void 0;
2570
+ }
2571
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2572
+ let offset = 12;
2573
+ let format;
2574
+ while (offset + 8 <= bytes.length) {
2575
+ const name = ascii(bytes, offset, 4);
2576
+ const size = view.getUint32(offset + 4, true);
2577
+ const start = offset + 8;
2578
+ if (name === "fmt " && size >= 16 && start + 16 <= bytes.length) {
2579
+ format = {
2580
+ audioFormat: view.getUint16(start, true),
2581
+ channels: view.getUint16(start + 2, true),
2582
+ sampleRate: view.getUint32(start + 4, true),
2583
+ bitsPerSample: view.getUint16(start + 14, true)
2584
+ };
2585
+ }
2586
+ if (name === "data" && format) {
2587
+ return { ...format, dataOffset: start, dataLength: Math.min(size, bytes.length - start) };
2588
+ }
2589
+ offset = start + size + size % 2;
2590
+ }
2591
+ return void 0;
2592
+ };
2593
+ var header = (channels, sampleRate, bits, dataLength) => {
2594
+ const out = new Uint8Array(44);
2595
+ const view = new DataView(out.buffer);
2596
+ const write = (offset, text) => {
2597
+ for (let i = 0; i < text.length; i++) out[offset + i] = text.charCodeAt(i);
2598
+ };
2599
+ const blockAlign = channels * bits / 8;
2600
+ write(0, "RIFF");
2601
+ view.setUint32(4, 36 + dataLength, true);
2602
+ write(8, "WAVE");
2603
+ write(12, "fmt ");
2604
+ view.setUint32(16, 16, true);
2605
+ view.setUint16(20, 1, true);
2606
+ view.setUint16(22, channels, true);
2607
+ view.setUint32(24, sampleRate, true);
2608
+ view.setUint32(28, sampleRate * blockAlign, true);
2609
+ view.setUint16(32, blockAlign, true);
2610
+ view.setUint16(34, bits, true);
2611
+ write(36, "data");
2612
+ view.setUint32(40, dataLength, true);
2613
+ return out;
2614
+ };
2615
+ var synthesizeWav = (options = {}) => {
2616
+ const channels = options.channels ?? 2;
2617
+ const sampleRate = 8e3;
2618
+ const frames = Math.max(1, Math.round(sampleRate * (options.seconds ?? 0.25)));
2619
+ const data = new Uint8Array(frames * channels * 2);
2620
+ const view = new DataView(data.buffer);
2621
+ for (let frame = 0; frame < frames; frame++) {
2622
+ for (let channel = 0; channel < channels; channel++) {
2623
+ const pitch = 440 * (channel + 1);
2624
+ const sample = Math.round(2e3 * Math.sin(2 * Math.PI * pitch * frame / sampleRate));
2625
+ view.setInt16((frame * channels + channel) * 2, sample, true);
2626
+ }
2627
+ }
2628
+ const out = new Uint8Array(44 + data.length);
2629
+ out.set(header(channels, sampleRate, 16, data.length), 0);
2630
+ out.set(data, 44);
2631
+ return out;
2632
+ };
2633
+ var mixDownToMono = (bytes) => {
2634
+ const format = readWav(bytes);
2635
+ if (!format || format.channels < 2 || format.audioFormat !== 1 || format.bitsPerSample !== 16) {
2636
+ return bytes;
2637
+ }
2638
+ const source = new DataView(bytes.buffer, bytes.byteOffset + format.dataOffset, format.dataLength);
2639
+ const frames = Math.floor(format.dataLength / (2 * format.channels));
2640
+ const data = new Uint8Array(frames * 2);
2641
+ const target = new DataView(data.buffer);
2642
+ for (let frame = 0; frame < frames; frame++) {
2643
+ let sum = 0;
2644
+ for (let channel = 0; channel < format.channels; channel++) {
2645
+ sum += source.getInt16((frame * format.channels + channel) * 2, true);
2646
+ }
2647
+ target.setInt16(frame * 2, Math.round(sum / format.channels), true);
2648
+ }
2649
+ const out = new Uint8Array(44 + data.length);
2650
+ out.set(header(1, format.sampleRate, 16, data.length), 0);
2651
+ out.set(data, 44);
2652
+ return out;
2653
+ };
2654
+ var durationSeconds = (bytes) => {
2655
+ const format = readWav(bytes);
2656
+ if (!format || format.sampleRate === 0 || format.bitsPerSample === 0) return 0;
2657
+ const bytesPerSecond = format.sampleRate * format.channels * format.bitsPerSample / 8;
2658
+ return Math.round(format.dataLength / bytesPerSecond);
2659
+ };
2660
+
2661
+ // src/rewrite.ts
2662
+ var TWILIO_PRODUCTS = ["api", "verify", "lookups"];
2663
+ var TWILIO_HOST = /^(api|verify|lookups)(?:\.[a-z0-9-]+)*\.twilio\.com$/i;
2664
+ var twilioMockUrl = (uri, mockBaseUrl) => {
2665
+ const upstream = new URL(uri);
2666
+ const product = TWILIO_HOST.exec(upstream.hostname)?.[1]?.toLowerCase();
2667
+ if (!product) return uri;
2668
+ const base = new URL(mockBaseUrl);
2669
+ const prefix = base.pathname.replace(/\/+$/, "");
2670
+ return `${base.origin}${prefix}/${product}${upstream.pathname}${upstream.search}`;
2671
+ };
2672
+ var routeByHost = async (request) => {
2673
+ const url = new URL(request.url);
2674
+ const product = TWILIO_HOST.exec(url.hostname)?.[1]?.toLowerCase();
2675
+ if (!product) return request;
2676
+ const ns = /^(\/ns\/[^/]+)(\/.*)?$/.exec(url.pathname);
2677
+ const prefix = ns?.[1] ?? "";
2678
+ const rest = ns ? ns[2] ?? "/" : url.pathname;
2679
+ if (rest === "/health" || rest === "/__admin" || rest.startsWith("/__admin/") || TWILIO_PRODUCTS.some((p) => rest === `/${p}` || rest.startsWith(`/${p}/`))) {
2680
+ return request;
2681
+ }
2682
+ url.pathname = `${prefix}/${product}${rest}`;
2683
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
2684
+ return new Request(url, {
2685
+ method: request.method,
2686
+ headers: request.headers,
2687
+ ...hasBody ? { body: await request.arrayBuffer() } : {},
2688
+ signal: request.signal
2689
+ });
2690
+ };
2691
+
2692
+ // src/runtime.ts
2693
+ var errorBody = (status, code, message) => ({
2694
+ code,
2695
+ message,
2696
+ more_info: `https://www.twilio.com/docs/errors/${code}`,
2697
+ status
2698
+ });
2699
+ var TWILIO_PRESETS = {
2700
+ verify_5xx: {
2701
+ description: "Verify start and check answer 500 (20500); the backend surfaces a 400",
2702
+ rules: [
2703
+ {
2704
+ operationId: "CreateVerification",
2705
+ status: 500,
2706
+ body: errorBody(500, 20500, "An internal server error has occurred"),
2707
+ headers: { "x-twilio-error-code": "20500" }
2708
+ },
2709
+ {
2710
+ operationId: "CreateVerificationCheck",
2711
+ status: 500,
2712
+ body: errorBody(500, 20500, "An internal server error has occurred"),
2713
+ headers: { "x-twilio-error-code": "20500" }
2714
+ }
2715
+ ]
2716
+ },
2717
+ sms_socket_drop: {
2718
+ description: "Messages.json drops the connection: an unknown outcome the notification dispatcher must not retry",
2719
+ rules: [{ operationId: "CreateMessage", drop: true }]
2720
+ },
2721
+ sms_4xx: {
2722
+ description: "Messages.json answers 400 21211 (invalid To): a definite failure",
2723
+ rules: [
2724
+ {
2725
+ operationId: "CreateMessage",
2726
+ status: 400,
2727
+ body: errorBody(400, 21211, "Invalid 'To' Phone Number"),
2728
+ headers: { "x-twilio-error-code": "21211" }
2729
+ }
2730
+ ]
2731
+ },
2732
+ lookup_5xx: {
2733
+ description: "Lookup answers 503 (20503); the EMR phone validation fails open",
2734
+ rules: [
2735
+ {
2736
+ operationId: "FetchPhoneNumber",
2737
+ status: 503,
2738
+ body: errorBody(503, 20503, "Service is unavailable. Please try again"),
2739
+ headers: { "x-twilio-error-code": "20503" }
2740
+ }
2741
+ ]
2742
+ },
2743
+ webhook_duplicate: {
2744
+ description: "The next inbound webhook is delivered twice (the app dedupes on MessageSid)",
2745
+ webhook: { mode: "duplicate" }
2746
+ },
2747
+ webhook_drop: {
2748
+ description: "The next inbound webhook is never delivered",
2749
+ webhook: { mode: "drop" }
2750
+ }
2751
+ };
2752
+ var TWILIO_WEBHOOK_EVENTS = {
2753
+ "sms.inbound": "/messaging/inbound/sms",
2754
+ "voice.twiml": "/admin/messaging/voice/twiml",
2755
+ "voice.disclosure": "/admin/messaging/voice/disclosure",
2756
+ "voice.status": "/admin/messaging/voice/status",
2757
+ "voice.recording": "/admin/messaging/voice/recording"
2758
+ };
2759
+ var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2760
+ var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
2761
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2762
+ var stringsOf = (value) => isRecord4(value) ? Object.fromEntries(
2763
+ Object.entries(value).filter(([, v]) => typeof v === "string" || typeof v === "number").map(([k, v]) => [k, String(v)])
2764
+ ) : {};
2765
+ var RECORDING_SID = /^RE[0-9a-f]{32}$/i;
2766
+ var VALIDATION_ERRORS = [
2767
+ "TOO_SHORT",
2768
+ "TOO_LONG",
2769
+ "INVALID_BUT_POSSIBLE",
2770
+ "INVALID_COUNTRY_CODE",
2771
+ "INVALID_LENGTH",
2772
+ "NOT_A_NUMBER"
2773
+ ];
2774
+ var clientIdentity = (seed) => {
2775
+ const hex = hexOf(seed, 32);
2776
+ return `client:${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
2777
+ };
2778
+ var createRuntime2 = (options = {}) => {
2779
+ const app = options.app;
2780
+ const publicBase = (app?.publicBaseUrl ?? app?.url ?? "").replace(/\/+$/, "");
2781
+ const appBase = (app?.url ?? "").replace(/\/+$/, "");
2782
+ const send = options.fetch ?? ((request) => fetch(request));
2783
+ const endpoints = app ? Object.entries(TWILIO_WEBHOOK_EVENTS).map(([event, path]) => ({
2784
+ id: `twilio_${event}`,
2785
+ url: `${appBase}${path}`,
2786
+ signUrl: `${publicBase}${path}`,
2787
+ secret: app.authToken,
2788
+ events: [event]
2789
+ })) : [];
2790
+ const hub = createWebhookHub({
2791
+ signer: signers.twilio(),
2792
+ retryDelaysMs: options.retryDelaysMs ?? [0],
2793
+ endpoints,
2794
+ fetch: send
2795
+ });
2796
+ const runtime = createRuntime({
2797
+ name: TWILIO_NAMESPACE,
2798
+ document,
2799
+ ...options.sqlite ? { sqlite: options.sqlite } : {},
2800
+ ...options.clock ? { clock: options.clock } : {},
2801
+ ...options.seed !== void 0 ? { seed: options.seed } : {},
2802
+ ...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
2803
+ ...options.onLog ? { onLog: options.onLog } : {},
2804
+ credential: (request) => basicAuth(request)?.username,
2805
+ presets: TWILIO_PRESETS,
2806
+ webhooks: hub,
2807
+ create: ({ sqlite, namespace, clock }) => new TwilioAPI({
2808
+ sqlite,
2809
+ namespace,
2810
+ now: clock.now,
2811
+ ...options.verify ? { verify: options.verify } : {},
2812
+ ...options.accounts ? { accounts: options.accounts } : {}
2813
+ }),
2814
+ describe: () => ({ webhooks: app ? "on" : "off" }),
2815
+ admin: adminRoutes
2816
+ });
2817
+ const publish = async (namespace, type, params, id) => {
2818
+ const message = hub.publish({
2819
+ namespace,
2820
+ type,
2821
+ body: new URLSearchParams(params).toString(),
2822
+ form: params,
2823
+ id
2824
+ });
2825
+ await hub.idle();
2826
+ const deliveries = hub.deliveries(namespace).filter((d) => d.messageId === message.id).map((d) => {
2827
+ const last = d.attempts.at(-1);
2828
+ return {
2829
+ url: d.url,
2830
+ state: d.state,
2831
+ status: last?.status ?? null,
2832
+ error: last?.error ?? null,
2833
+ response: last?.responseBody ?? null
2834
+ };
2835
+ });
2836
+ return { params, deliveries };
2837
+ };
2838
+ const accountSid = (override) => override ?? app?.accountSid ?? DEFAULT_ACCOUNT_SID;
2839
+ const inboundSms = (input, namespace = "default") => {
2840
+ const instance = runtime.instance(namespace);
2841
+ const media = (input.media ?? []).map((m) => typeof m === "string" ? { url: m } : m);
2842
+ const sid = input.messageSid ?? instance.state.sid(media.length > 0 ? "MM" : "SM");
2843
+ const account = accountSid(input.accountSid);
2844
+ const params = {
2845
+ AccountSid: account,
2846
+ ApiVersion: "2010-04-01",
2847
+ Body: input.body,
2848
+ From: input.from,
2849
+ FromCountry: "US",
2850
+ MessageSid: sid,
2851
+ NumMedia: String(media.length),
2852
+ NumSegments: "1",
2853
+ SmsMessageSid: sid,
2854
+ SmsSid: sid,
2855
+ SmsStatus: "received",
2856
+ To: input.to ?? app?.callerId ?? "",
2857
+ ToCountry: "US",
2858
+ ...app?.messagingServiceSid ? { MessagingServiceSid: app.messagingServiceSid } : {}
2859
+ };
2860
+ media.forEach((item, index) => {
2861
+ params[`MediaUrl${index}`] = item.url;
2862
+ params[`MediaContentType${index}`] = item.contentType ?? "image/jpeg";
2863
+ });
2864
+ Object.assign(params, input.params ?? {});
2865
+ return publish(namespace, "sms.inbound", params, sid);
2866
+ };
2867
+ const voiceWebhook = (kind, overrides = {}, namespace = "default") => {
2868
+ const instance = runtime.instance(namespace);
2869
+ const account = accountSid(overrides.AccountSid);
2870
+ const callSid = overrides.CallSid ?? instance.state.sid("CA");
2871
+ const base = {
2872
+ AccountSid: account,
2873
+ ApiVersion: "2010-04-01",
2874
+ CallSid: callSid
2875
+ };
2876
+ let params;
2877
+ switch (kind) {
2878
+ case "twiml":
2879
+ params = {
2880
+ ...base,
2881
+ CallStatus: "ringing",
2882
+ Direction: "inbound",
2883
+ From: clientIdentity(callSid),
2884
+ To: app?.callerId ?? "",
2885
+ ...overrides
2886
+ };
2887
+ break;
2888
+ case "disclosure":
2889
+ params = { ...base, CallStatus: "in-progress", ...overrides };
2890
+ break;
2891
+ case "status":
2892
+ params = { ...base, CallStatus: "completed", CallDuration: "42", ...overrides };
2893
+ break;
2894
+ case "recording": {
2895
+ const recordingSid = overrides.RecordingSid ?? instance.state.sid("RE");
2896
+ if (!instance.state.recordings.get(recordingSid)) {
2897
+ instance.putRecording(recordingSid, { accountSid: account, callSid });
2898
+ }
2899
+ const recording = instance.state.recordings.get(recordingSid);
2900
+ params = {
2901
+ ...base,
2902
+ RecordingSid: recordingSid,
2903
+ // The canonical form our recording adapter insists on.
2904
+ RecordingUrl: `https://api.twilio.com/2010-04-01/Accounts/${account}/Recordings/${recordingSid}`,
2905
+ RecordingStatus: "completed",
2906
+ RecordingDuration: String(recording?.duration ?? 1),
2907
+ RecordingChannels: String(recording?.channels ?? 2),
2908
+ RecordingSource: "DialVerb",
2909
+ RecordingStartTime: new Date(runtime.clock.now()).toUTCString().replace("GMT", "+0000"),
2910
+ ...overrides
2911
+ };
2912
+ break;
2913
+ }
2914
+ }
2915
+ return publish(namespace, `voice.${kind}`, params, `${callSid}:${kind}:${runtime.clock.now()}`);
2916
+ };
2917
+ const inner = runtime.fetch;
2918
+ return Object.assign(runtime, {
2919
+ webhooks: hub,
2920
+ inboundSms,
2921
+ voiceWebhook,
2922
+ fetch: async (request) => inner(await acceptRawRecording(await routeByHost(request)))
2923
+ });
2924
+ };
2925
+ var acceptRawRecording = async (request) => {
2926
+ if (request.method !== "PUT") return request;
2927
+ const url = new URL(request.url);
2928
+ if (!/(?:^|\/)__admin\/recordings\/[^/]+$/.test(url.pathname)) return request;
2929
+ const type = request.headers.get("content-type") ?? "";
2930
+ if (type.includes("json")) return request;
2931
+ const bytes = new Uint8Array(await request.arrayBuffer());
2932
+ const headers = new Headers(request.headers);
2933
+ headers.set("content-type", "application/json");
2934
+ let binary = "";
2935
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2936
+ return new Request(url, {
2937
+ method: "PUT",
2938
+ headers,
2939
+ body: JSON.stringify(bytes.length > 0 ? { wavBase64: btoa(binary) } : {})
2940
+ });
2941
+ };
2942
+ var adminRoutes = (runtime) => {
2943
+ const twilio = runtime;
2944
+ return {
2945
+ ...outboxAdminRoutes(
2946
+ runtime,
2947
+ (api) => api.state.outbox,
2948
+ (params) => {
2949
+ const kind = params.get("kind");
2950
+ return kind === null ? void 0 : (item) => item.kind === kind;
2951
+ }
2952
+ ),
2953
+ "GET /verify/:e164/latest": ({ params, namespace }) => {
2954
+ const latest = runtime.instance(namespace).latestVerification(params.e164);
2955
+ return latest ? json4(200, latest) : adminError3(404, `no verification to ${params.e164}`);
2956
+ },
2957
+ "GET /verify": ({ namespace }) => json4(200, runtime.instance(namespace).state.verify()),
2958
+ "PUT /verify": ({ body, namespace }) => {
2959
+ if (!isRecord4(body)) return adminError3(400, "expected a JSON object");
2960
+ const patch = {};
2961
+ if (body.fixedCode !== void 0) {
2962
+ if (body.fixedCode !== null && !(typeof body.fixedCode === "string" && /^\d{4,10}$/.test(body.fixedCode))) {
2963
+ return adminError3(400, "fixedCode: 4\u201310 digits, or null for random codes");
2964
+ }
2965
+ patch.fixedCode = body.fixedCode;
2966
+ }
2967
+ for (const key of [
2968
+ "ttlSeconds",
2969
+ "maxCheckAttempts",
2970
+ "maxSendAttempts",
2971
+ "sendWindowSeconds"
2972
+ ]) {
2973
+ if (body[key] === void 0) continue;
2974
+ if (typeof body[key] !== "number" || body[key] < 0) {
2975
+ return adminError3(400, `${key}: a non-negative number`);
2976
+ }
2977
+ patch[key] = body[key];
2978
+ }
2979
+ return json4(200, runtime.instance(namespace).state.updateVerify(patch));
2980
+ },
2981
+ "PUT /lookups/:e164": ({ params, body, namespace }) => {
2982
+ if (!isRecord4(body) || typeof body.valid !== "boolean") {
2983
+ return adminError3(400, 'expected {"valid": boolean, "validationErrors"?: [...]}');
2984
+ }
2985
+ const errors = Array.isArray(body.validationErrors) ? body.validationErrors : [];
2986
+ const bad = errors.find((e) => !VALIDATION_ERRORS.includes(e));
2987
+ if (bad !== void 0) {
2988
+ return adminError3(400, `validationErrors: one of ${VALIDATION_ERRORS.join(", ")}`);
2989
+ }
2990
+ runtime.instance(namespace).setLookup(params.e164, {
2991
+ valid: body.valid,
2992
+ validationErrors: errors
2993
+ });
2994
+ return json4(200, { phoneNumber: params.e164, valid: body.valid, validationErrors: errors });
2995
+ },
2996
+ "DELETE /lookups/:e164": ({ params, namespace }) => {
2997
+ const instance = runtime.instance(namespace);
2998
+ const key = `+${params.e164.replace(/[^0-9]/g, "")}`;
2999
+ return json4(200, { deleted: instance.state.lookups.delete(key) });
3000
+ },
3001
+ "PUT /recordings/:sid": ({ params, body, namespace }) => {
3002
+ const sid = params.sid;
3003
+ if (!RECORDING_SID.test(sid)) return adminError3(400, "sid must look like RE + 32 hex");
3004
+ const input = isRecord4(body) ? body : {};
3005
+ let wav;
3006
+ if (typeof input.wavBase64 === "string") {
3007
+ try {
3008
+ wav = fromBase64(input.wavBase64);
3009
+ } catch {
3010
+ return adminError3(400, "wavBase64 is not base64");
3011
+ }
3012
+ if (!readWav(wav)) return adminError3(400, "the upload is not a RIFF/WAVE file");
3013
+ }
3014
+ const recording = runtime.instance(namespace).putRecording(sid, {
3015
+ ...wav ? { wav } : {},
3016
+ ...typeof input.channels === "number" ? { channels: input.channels } : {},
3017
+ ...typeof input.seconds === "number" ? { seconds: input.seconds } : {},
3018
+ ...typeof input.accountSid === "string" ? { accountSid: input.accountSid } : {},
3019
+ ...typeof input.callSid === "string" ? { callSid: input.callSid } : {}
3020
+ });
3021
+ const { wavBase64: _bytes, ...metadata } = recording;
3022
+ return json4(200, metadata);
3023
+ },
3024
+ "GET /messages": ({ namespace }) => json4(200, { messages: runtime.instance(namespace).messages() }),
3025
+ "POST /inbound/sms": async ({ body, namespace }) => {
3026
+ if (!isRecord4(body) || typeof body.from !== "string" || typeof body.body !== "string") {
3027
+ return adminError3(400, 'expected {"from": "+1\u2026", "body": "\u2026", "to"?, "media"?}');
3028
+ }
3029
+ const media = Array.isArray(body.media) ? body.media.map(
3030
+ (m) => typeof m === "string" ? m : isRecord4(m) && typeof m.url === "string" ? {
3031
+ url: m.url,
3032
+ ...typeof m.contentType === "string" ? { contentType: m.contentType } : {}
3033
+ } : void 0
3034
+ ).filter((m) => m !== void 0) : [];
3035
+ const sent = await twilio.inboundSms(
3036
+ {
3037
+ from: body.from,
3038
+ body: body.body,
3039
+ media,
3040
+ ...typeof body.to === "string" ? { to: body.to } : {},
3041
+ ...typeof body.messageSid === "string" ? { messageSid: body.messageSid } : {},
3042
+ ...typeof body.accountSid === "string" ? { accountSid: body.accountSid } : {},
3043
+ params: stringsOf(body.params)
3044
+ },
3045
+ namespace
3046
+ );
3047
+ return json4(200, sent);
3048
+ },
3049
+ "POST /voice/:kind": async ({ params, body, namespace }) => {
3050
+ const kind = params.kind;
3051
+ if (!["twiml", "disclosure", "status", "recording"].includes(kind)) {
3052
+ return adminError3(404, "kind: twiml, disclosure, status or recording");
3053
+ }
3054
+ return json4(200, await twilio.voiceWebhook(kind, stringsOf(body), namespace));
3055
+ }
3056
+ };
3057
+ };
3058
+
3059
+ // src/index.ts
3060
+ var TWILIO_NAMESPACE = "twilio";
3061
+ var DEFAULT_ACCOUNT_SID = "AC00000000000000000000000000000000";
3062
+ var twilioError = (status, code, message) => jsonRes(
3063
+ status,
3064
+ { code, message, more_info: `https://www.twilio.com/docs/errors/${code}`, status },
3065
+ { "x-twilio-error-code": String(code) }
3066
+ );
3067
+ var fail = (status, code, message) => new HttpError(
3068
+ status,
3069
+ { code, message, more_info: `https://www.twilio.com/docs/errors/${code}`, status },
3070
+ { "x-twilio-error-code": String(code) }
3071
+ );
3072
+ var SID = (prefix) => new RegExp(`^${prefix}[0-9a-f]{32}$`, "i");
3073
+ var ACCOUNT_SID = SID("AC");
3074
+ var SERVICE_SID = SID("VA");
3075
+ var MESSAGING_SERVICE_SID = SID("MG");
3076
+ var SUFFIXED = /\/(Messages|Recordings)\/([^/]+?)\.(json|wav)$/;
3077
+ var routingDocument = (source) => ({
3078
+ ...source,
3079
+ paths: Object.fromEntries(
3080
+ Object.entries(source.paths ?? {}).map(([path, item]) => [
3081
+ path.replace(/\{Sid\}\.(json|wav)$/, "{Sid}/.$1"),
3082
+ item
3083
+ ])
3084
+ )
3085
+ });
3086
+ var ROUTING = routingDocument(document);
3087
+ var upstreamPath = (pathname) => pathname.replace(/^\/(api|verify|lookups)(?=\/)/, "").replace(/\/\.(json|wav)$/, ".$1");
3088
+ var isoSeconds = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
3089
+ var rfc2822 = (ms) => new Date(ms).toUTCString().replace("GMT", "+0000");
3090
+ var GSM = /^[\n\r\x20-\x7E£¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßɤ¡ÄÖÑܧ¿äöñüà€]*$/;
3091
+ var segments = (body) => {
3092
+ const length = [...body].length;
3093
+ if (length === 0) return 1;
3094
+ const [single, part] = GSM.test(body) ? [160, 153] : [70, 67];
3095
+ return length <= single ? 1 : Math.ceil(length / part);
3096
+ };
3097
+ var formOf = (context) => {
3098
+ const value = context.body.kind === "form" || context.body.kind === "json" ? context.body.value : void 0;
3099
+ if (typeof value !== "object" || value === null) return {};
3100
+ const out = {};
3101
+ for (const [key, item] of Object.entries(value)) {
3102
+ if (typeof item === "string") out[key] = item;
3103
+ else if (Array.isArray(item)) out[key] = item.map(String);
3104
+ else if (item !== void 0 && item !== null) out[key] = String(item);
3105
+ }
3106
+ return out;
3107
+ };
3108
+ var one = (value) => Array.isArray(value) ? value.at(-1) : value;
3109
+ var many = (value) => value === void 0 ? [] : Array.isArray(value) ? value : [value];
3110
+ var TwilioAPI = class {
3111
+ app;
3112
+ sqlite;
3113
+ state;
3114
+ service;
3115
+ now;
3116
+ accounts;
3117
+ constructor(options = {}) {
3118
+ const sqlite = bootSqlite(options.sqlite);
3119
+ const namespace = options.namespace ?? TWILIO_NAMESPACE;
3120
+ this.now = options.now ?? (() => Date.now());
3121
+ this.accounts = options.accounts;
3122
+ this.state = new TwilioState(sqlite, namespace, options.verify ?? {});
3123
+ const handlers = defineOperations({
3124
+ FetchPhoneNumber: (context) => this.fetchPhoneNumber(context),
3125
+ CreateVerification: (context) => this.createVerification(context),
3126
+ FetchVerification: (context) => this.fetchVerification(context),
3127
+ UpdateVerification: (context) => this.updateVerification(context),
3128
+ CreateVerificationCheck: (context) => this.createVerificationCheck(context),
3129
+ CreateMessage: (context) => this.createMessage(context),
3130
+ FetchMessage: (context) => this.fetchMessage(context),
3131
+ FetchRecordingMedia: (context) => this.fetchRecordingMedia(context),
3132
+ FetchRecording: (context) => this.fetchRecording(context),
3133
+ DeleteRecording: (context) => this.deleteRecording(context)
3134
+ });
3135
+ this.service = createService({
3136
+ document: ROUTING,
3137
+ handlers,
3138
+ sqlite,
3139
+ namespace,
3140
+ now: this.now,
3141
+ notFound: (request) => twilioError(
3142
+ 404,
3143
+ 20404,
3144
+ `The requested resource ${upstreamPath(new URL(request.url).pathname)} was not found`
3145
+ ),
3146
+ onError: (error) => {
3147
+ if (error instanceof HttpError) return error.toResponse();
3148
+ throw error;
3149
+ },
3150
+ before: (context) => this.authenticate(context)
3151
+ });
3152
+ this.app = this.service.app;
3153
+ this.sqlite = this.service.sqlite;
3154
+ }
3155
+ fetch(request) {
3156
+ const url = new URL(request.url);
3157
+ const suffixed = SUFFIXED.exec(url.pathname);
3158
+ if (!suffixed) return this.service.fetch(request);
3159
+ url.pathname = url.pathname.replace(SUFFIXED, "/$1/$2/.$3");
3160
+ return this.service.fetch(new Request(url, request));
3161
+ }
3162
+ async reset() {
3163
+ await this.service.reset();
3164
+ }
3165
+ authenticate(context) {
3166
+ const credentials = basicAuth(context.request);
3167
+ if (!credentials?.username || !credentials.password || !/^(AC|SK)/.test(credentials.username)) {
3168
+ return twilioError(401, 20003, "Authenticate");
3169
+ }
3170
+ if (this.accounts !== void 0) {
3171
+ const expected = this.accounts[credentials.username];
3172
+ if (expected === void 0) return twilioError(401, 20003, "Authenticate");
3173
+ if (expected !== credentials.password) {
3174
+ return twilioError(
3175
+ 401,
3176
+ 20003,
3177
+ `authentication failed, auth token is not valid for account ${credentials.username}`
3178
+ );
3179
+ }
3180
+ }
3181
+ return void 0;
3182
+ }
3183
+ /** The account a request acts for: the path's, else the Basic username when it is one. */
3184
+ accountOf(context) {
3185
+ const fromPath = context.params.AccountSid;
3186
+ if (fromPath) return fromPath;
3187
+ const username = basicAuth(context.request)?.username ?? "";
3188
+ return username.startsWith("AC") ? username : DEFAULT_ACCOUNT_SID;
3189
+ }
3190
+ requireAccount(context) {
3191
+ const account = context.params.AccountSid ?? "";
3192
+ if (!ACCOUNT_SID.test(account)) {
3193
+ throw fail(
3194
+ 404,
3195
+ 20404,
3196
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3197
+ );
3198
+ }
3199
+ return account;
3200
+ }
3201
+ // ---- Lookup v2 -----------------------------------------------------------------------------
3202
+ /** Lookup validity, with any `PUT /__admin/lookups/:e164` override applied. */
3203
+ resolveLookup(raw, countryCode) {
3204
+ const computed = lookup(raw, countryCode);
3205
+ const override = this.state.lookups.get(computed.phone_number) ?? this.state.lookups.get(e164Key(raw));
3206
+ if (!override) return computed;
3207
+ if (override.valid) return { ...computed, valid: true, validation_errors: [] };
3208
+ return {
3209
+ ...computed,
3210
+ calling_country_code: null,
3211
+ country_code: null,
3212
+ valid: false,
3213
+ validation_errors: override.validationErrors.length > 0 ? override.validationErrors : ["INVALID_BUT_POSSIBLE"]
3214
+ };
3215
+ }
3216
+ fetchPhoneNumber(context) {
3217
+ const raw = context.params.PhoneNumber ?? "";
3218
+ const country = typeof context.query.CountryCode === "string" ? context.query.CountryCode : void 0;
3219
+ const result = this.resolveLookup(raw, country);
3220
+ return jsonRes(200, lookupBody(result, raw, country));
3221
+ }
3222
+ // ---- Verify v2 -----------------------------------------------------------------------------
3223
+ requireService(context) {
3224
+ const sid = context.params.ServiceSid ?? "";
3225
+ if (!SERVICE_SID.test(sid)) {
3226
+ throw fail(
3227
+ 404,
3228
+ 20404,
3229
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3230
+ );
3231
+ }
3232
+ return sid;
3233
+ }
3234
+ /** The verification as of now: a pending one past its expiry becomes `expired`. */
3235
+ current(verification) {
3236
+ const ttl = this.state.verify().ttlSeconds * 1e3;
3237
+ if (verification.status !== "pending" || this.now() < verification.createdAtMs + ttl) {
3238
+ return verification;
3239
+ }
3240
+ const expired = { ...verification, status: "expired" };
3241
+ this.state.verifications.update(verification.sid, expired);
3242
+ return expired;
3243
+ }
3244
+ verificationBody(v) {
3245
+ return {
3246
+ sid: v.sid,
3247
+ service_sid: v.serviceSid,
3248
+ account_sid: v.accountSid,
3249
+ to: v.to,
3250
+ channel: v.channel,
3251
+ status: v.status,
3252
+ valid: v.status === "approved",
3253
+ lookup: { carrier: null },
3254
+ amount: null,
3255
+ payee: null,
3256
+ send_code_attempts: v.sendAttempts.map((a) => ({
3257
+ attempt_sid: a.attemptSid,
3258
+ channel: a.channel,
3259
+ time: a.time
3260
+ })),
3261
+ sna: null,
3262
+ date_created: v.dateCreated,
3263
+ date_updated: v.dateUpdated,
3264
+ url: `https://verify.twilio.com/v2/Services/${v.serviceSid}/Verifications/${v.sid}`
3265
+ };
3266
+ }
3267
+ /** `To` as Verify accepts it: an email for the email channel, else a valid E.164 number. */
3268
+ verifyRecipient(to, channel) {
3269
+ if (to === void 0 || to.trim() === "") throw fail(400, 60200, "Invalid parameter `To`: ");
3270
+ if (channel === "email") {
3271
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(to)) {
3272
+ throw fail(400, 60200, `Invalid parameter \`To\`: ${to}`);
3273
+ }
3274
+ return to.trim().toLowerCase();
3275
+ }
3276
+ const result = to.trim().startsWith("+") ? this.resolveLookup(to) : void 0;
3277
+ if (!result?.valid) throw fail(400, 60200, `Invalid parameter \`To\`: ${to}`);
3278
+ return result.phone_number;
3279
+ }
3280
+ createVerification(context) {
3281
+ const serviceSid = this.requireService(context);
3282
+ const form = formOf(context);
3283
+ const channel = one(form.Channel);
3284
+ if (!channel || !["sms", "call", "email", "whatsapp", "sna", "auto"].includes(channel)) {
3285
+ throw fail(400, 60200, `Invalid parameter \`Channel\`: ${channel ?? ""}`);
3286
+ }
3287
+ const to = this.verifyRecipient(one(form.To), channel);
3288
+ const customCode = one(form.CustomCode);
3289
+ if (customCode !== void 0 && !/^[0-9]{4,10}$/.test(customCode)) {
3290
+ throw fail(400, 60200, `Invalid parameter \`CustomCode\`: ${customCode}`);
3291
+ }
3292
+ const settings = this.state.verify();
3293
+ const now = this.now();
3294
+ const history = this.state.verificationsTo(to, serviceSid).map((v) => this.current(v));
3295
+ const windowStart = now - settings.sendWindowSeconds * 1e3;
3296
+ const recentSends = history.flatMap((v) => v.sendAttempts).filter((attempt2) => attempt2.timeMs > windowStart).length;
3297
+ if (recentSends >= settings.maxSendAttempts) {
3298
+ throw fail(429, 60203, "Max send attempts reached");
3299
+ }
3300
+ const attempt = {
3301
+ attemptSid: this.state.sid("VL"),
3302
+ channel,
3303
+ time: isoSeconds(now),
3304
+ timeMs: now
3305
+ };
3306
+ const pending = history.find((v) => v.status === "pending");
3307
+ const verification = pending ? {
3308
+ ...pending,
3309
+ channel,
3310
+ sendAttempts: [...pending.sendAttempts, attempt],
3311
+ dateUpdated: isoSeconds(now)
3312
+ } : (() => {
3313
+ const sid = this.state.sid("VE");
3314
+ return {
3315
+ sid,
3316
+ serviceSid,
3317
+ accountSid: this.accountOf(context),
3318
+ to,
3319
+ channel,
3320
+ code: customCode ?? settings.fixedCode ?? this.state.code(sid),
3321
+ status: "pending",
3322
+ attempts: 0,
3323
+ sendAttempts: [attempt],
3324
+ createdAtMs: now,
3325
+ dateCreated: isoSeconds(now),
3326
+ dateUpdated: isoSeconds(now)
3327
+ };
3328
+ })();
3329
+ if (pending) this.state.verifications.update(verification.sid, verification);
3330
+ else this.state.verifications.insert(verification.sid, verification);
3331
+ this.state.outbox.record({
3332
+ id: attempt.attemptSid,
3333
+ kind: "verify",
3334
+ sid: verification.sid,
3335
+ to,
3336
+ from: null,
3337
+ messagingServiceSid: null,
3338
+ body: `Your verification code is: ${verification.code}`,
3339
+ channel,
3340
+ code: verification.code,
3341
+ createdAt: new Date(now).toISOString()
3342
+ });
3343
+ return annotateResponse(jsonRes(201, this.verificationBody(verification)), {
3344
+ ids: { verificationSid: verification.sid }
3345
+ });
3346
+ }
3347
+ findVerification(context, sid) {
3348
+ const serviceSid = this.requireService(context);
3349
+ const found = this.state.verifications.get(sid);
3350
+ const verification = found && found.serviceSid === serviceSid ? this.current(found) : void 0;
3351
+ if (!verification || verification.status === "expired") {
3352
+ throw fail(
3353
+ 404,
3354
+ 20404,
3355
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3356
+ );
3357
+ }
3358
+ return verification;
3359
+ }
3360
+ fetchVerification(context) {
3361
+ const verification = this.findVerification(context, context.params.Sid ?? "");
3362
+ return annotateResponse(jsonRes(200, this.verificationBody(verification)), {
3363
+ ids: { verificationSid: verification.sid }
3364
+ });
3365
+ }
3366
+ updateVerification(context) {
3367
+ const verification = this.findVerification(context, context.params.Sid ?? "");
3368
+ const status = one(formOf(context).Status);
3369
+ if (status !== "canceled" && status !== "approved") {
3370
+ throw fail(400, 60200, `Invalid parameter \`Status\`: ${status ?? ""}`);
3371
+ }
3372
+ if (verification.status !== "pending") {
3373
+ throw fail(
3374
+ 404,
3375
+ 20404,
3376
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3377
+ );
3378
+ }
3379
+ const updated = {
3380
+ ...verification,
3381
+ status,
3382
+ dateUpdated: isoSeconds(this.now())
3383
+ };
3384
+ this.state.verifications.update(verification.sid, updated);
3385
+ return annotateResponse(jsonRes(200, this.verificationBody(updated)), {
3386
+ ids: { verificationSid: updated.sid }
3387
+ });
3388
+ }
3389
+ createVerificationCheck(context) {
3390
+ const serviceSid = this.requireService(context);
3391
+ const form = formOf(context);
3392
+ const code = one(form.Code);
3393
+ if (code === void 0 || code === "") throw fail(400, 60200, "Invalid parameter `Code`: ");
3394
+ const verificationSid = one(form.VerificationSid);
3395
+ const to = one(form.To);
3396
+ if (verificationSid === void 0 && to === void 0) {
3397
+ throw fail(400, 60200, "Either a 'To' number or 'VerificationSid' must be specified");
3398
+ }
3399
+ const notFound = () => fail(404, 20404, `The requested resource ${upstreamPath(context.url.pathname)} was not found`);
3400
+ let found;
3401
+ if (verificationSid !== void 0) {
3402
+ const byId = this.state.verifications.get(verificationSid);
3403
+ found = byId && byId.serviceSid === serviceSid ? byId : void 0;
3404
+ } else {
3405
+ const normalized = to?.includes("@") ? to.toLowerCase() : e164Key(to ?? "");
3406
+ found = this.state.verificationsTo(normalized, serviceSid).map((v) => this.current(v)).find((v) => v.status === "pending");
3407
+ }
3408
+ const verification = found ? this.current(found) : void 0;
3409
+ if (verification?.status !== "pending") throw notFound();
3410
+ if (verification.attempts >= this.state.verify().maxCheckAttempts) {
3411
+ throw fail(429, 60202, "Max check attempts reached");
3412
+ }
3413
+ const now = this.now();
3414
+ const approved = code === verification.code;
3415
+ const updated = {
3416
+ ...verification,
3417
+ attempts: verification.attempts + 1,
3418
+ status: approved ? "approved" : "pending",
3419
+ dateUpdated: isoSeconds(now)
3420
+ };
3421
+ this.state.verifications.update(verification.sid, updated);
3422
+ return annotateResponse(
3423
+ jsonRes(200, {
3424
+ sid: updated.sid,
3425
+ service_sid: updated.serviceSid,
3426
+ account_sid: updated.accountSid,
3427
+ to: updated.to,
3428
+ channel: updated.channel,
3429
+ status: updated.status,
3430
+ valid: approved,
3431
+ amount: null,
3432
+ payee: null,
3433
+ sna_attempts_error_codes: [],
3434
+ date_created: updated.dateCreated,
3435
+ date_updated: updated.dateUpdated
3436
+ }),
3437
+ { ids: { verificationSid: updated.sid } }
3438
+ );
3439
+ }
3440
+ // ---- Messaging -----------------------------------------------------------------------------
3441
+ createMessage(context) {
3442
+ const accountSid = this.requireAccount(context);
3443
+ const form = formOf(context);
3444
+ const to = one(form.To);
3445
+ const body = one(form.Body) ?? "";
3446
+ const from = one(form.From);
3447
+ const messagingServiceSid = one(form.MessagingServiceSid);
3448
+ const media = many(form.MediaUrl);
3449
+ if (to === void 0 || to.trim() === "")
3450
+ throw fail(400, 21604, "A 'To' phone number is required.");
3451
+ if (body === "" && media.length === 0) throw fail(400, 21602, "Message body is required.");
3452
+ if (!from && !messagingServiceSid) throw fail(400, 21603, "A 'From' phone number is required.");
3453
+ if (messagingServiceSid !== void 0 && !MESSAGING_SERVICE_SID.test(messagingServiceSid)) {
3454
+ throw fail(400, 21701, `The Messaging Service Sid ${messagingServiceSid} is invalid.`);
3455
+ }
3456
+ if ([...body].length > 1600) {
3457
+ throw fail(400, 21617, "The concatenated message body exceeds the 1600 character limit.");
3458
+ }
3459
+ const recipient = this.resolveLookup(to);
3460
+ if (!recipient.valid) throw fail(400, 21211, `Invalid 'To' Phone Number: ${to}`);
3461
+ const now = this.now();
3462
+ const sid = this.state.sid(media.length > 0 ? "MM" : "SM");
3463
+ const base = `/2010-04-01/Accounts/${accountSid}/Messages/${sid}`;
3464
+ const message = {
3465
+ sid,
3466
+ account_sid: accountSid,
3467
+ api_version: "2010-04-01",
3468
+ body,
3469
+ to: recipient.phone_number,
3470
+ from: from ?? null,
3471
+ messaging_service_sid: messagingServiceSid ?? null,
3472
+ status: messagingServiceSid ? "accepted" : "queued",
3473
+ direction: "outbound-api",
3474
+ num_segments: String(segments(body)),
3475
+ num_media: String(media.length),
3476
+ price: null,
3477
+ price_unit: "USD",
3478
+ error_code: null,
3479
+ error_message: null,
3480
+ date_created: rfc2822(now),
3481
+ date_updated: rfc2822(now),
3482
+ date_sent: null,
3483
+ uri: `${base}.json`,
3484
+ subresource_uris: { media: `${base}/Media.json`, feedback: `${base}/Feedback.json` }
3485
+ };
3486
+ this.state.messages.insert(sid, message);
3487
+ this.state.outbox.record({
3488
+ id: sid,
3489
+ kind: "sms",
3490
+ sid,
3491
+ to: recipient.phone_number,
3492
+ from: from ?? null,
3493
+ messagingServiceSid: messagingServiceSid ?? null,
3494
+ body,
3495
+ channel: "sms",
3496
+ ...media.length > 0 ? { mediaUrls: media } : {},
3497
+ createdAt: new Date(now).toISOString()
3498
+ });
3499
+ return annotateResponse(jsonRes(201, message), { ids: { messageSid: sid } });
3500
+ }
3501
+ fetchMessage(context) {
3502
+ const accountSid = this.requireAccount(context);
3503
+ const message = this.state.messages.get(context.params.Sid ?? "");
3504
+ if (!message || message.account_sid !== accountSid) {
3505
+ throw fail(
3506
+ 404,
3507
+ 20404,
3508
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3509
+ );
3510
+ }
3511
+ return annotateResponse(jsonRes(200, message), { ids: { messageSid: message.sid } });
3512
+ }
3513
+ // ---- Recordings ----------------------------------------------------------------------------
3514
+ findRecording(context) {
3515
+ this.requireAccount(context);
3516
+ const recording = this.state.recordings.get(context.params.Sid ?? "");
3517
+ if (!recording || recording.deleted) {
3518
+ throw fail(
3519
+ 404,
3520
+ 20404,
3521
+ `The requested resource ${upstreamPath(context.url.pathname)} was not found`
3522
+ );
3523
+ }
3524
+ return recording;
3525
+ }
3526
+ fetchRecordingMedia(context) {
3527
+ const recording = this.findRecording(context);
3528
+ const bytes = fromBase64(recording.wavBase64);
3529
+ const served = String(context.query.RequestedChannels ?? "") === "2" ? bytes : mixDownToMono(bytes);
3530
+ return annotateResponse(
3531
+ new Response(served, {
3532
+ status: 200,
3533
+ headers: { "content-type": "audio/x-wav", "content-length": String(served.byteLength) }
3534
+ }),
3535
+ { ids: { recordingSid: recording.sid } }
3536
+ );
3537
+ }
3538
+ recordingBody(recording) {
3539
+ const uri = `/2010-04-01/Accounts/${recording.accountSid}/Recordings/${recording.sid}`;
3540
+ return {
3541
+ sid: recording.sid,
3542
+ account_sid: recording.accountSid,
3543
+ api_version: "2010-04-01",
3544
+ call_sid: recording.callSid,
3545
+ conference_sid: null,
3546
+ status: "completed",
3547
+ channels: recording.channels,
3548
+ duration: String(recording.duration),
3549
+ source: "RecordVerb",
3550
+ price: null,
3551
+ price_unit: "USD",
3552
+ error_code: null,
3553
+ encryption_details: null,
3554
+ start_time: recording.createdAt,
3555
+ date_created: recording.createdAt,
3556
+ date_updated: recording.createdAt,
3557
+ uri: `${uri}.json`,
3558
+ media_url: `https://api.twilio.com${uri}`
3559
+ };
3560
+ }
3561
+ fetchRecording(context) {
3562
+ const recording = this.findRecording(context);
3563
+ return annotateResponse(jsonRes(200, this.recordingBody(recording)), {
3564
+ ids: { recordingSid: recording.sid }
3565
+ });
3566
+ }
3567
+ deleteRecording(context) {
3568
+ const recording = this.findRecording(context);
3569
+ this.state.recordings.update(recording.sid, { ...recording, deleted: true, wavBase64: "" });
3570
+ return annotateResponse(new Response(null, { status: 204 }), {
3571
+ ids: { recordingSid: recording.sid }
3572
+ });
3573
+ }
3574
+ // ---- Admin-plane helpers -------------------------------------------------------------------
3575
+ /** The newest verification to a number (what `GET /__admin/verify/:e164/latest` answers). */
3576
+ latestVerification(to) {
3577
+ const key = to.includes("@") ? to.toLowerCase() : e164Key(to);
3578
+ const newest = this.state.verificationsTo(key).at(0);
3579
+ if (!newest) return void 0;
3580
+ const v = this.current(newest);
3581
+ return {
3582
+ sid: v.sid,
3583
+ code: v.code,
3584
+ status: v.status,
3585
+ to: v.to,
3586
+ channel: v.channel,
3587
+ serviceSid: v.serviceSid,
3588
+ attempts: v.attempts,
3589
+ sendAttempts: v.sendAttempts.length,
3590
+ createdAt: v.dateCreated,
3591
+ expiresAt: isoSeconds(v.createdAtMs + this.state.verify().ttlSeconds * 1e3)
3592
+ };
3593
+ }
3594
+ setLookup(e164, override) {
3595
+ this.state.lookups.insert(e164Key(e164), override);
3596
+ }
3597
+ /** Store (or synthesise) a recording so `GET …/Recordings/{sid}.wav` serves it. */
3598
+ putRecording(sid, input = {}) {
3599
+ const wav = input.wav ?? synthesizeWav({ channels: input.channels ?? 2, seconds: input.seconds ?? 1 });
3600
+ const format = readWav(wav);
3601
+ const recording = {
3602
+ sid,
3603
+ accountSid: input.accountSid ?? DEFAULT_ACCOUNT_SID,
3604
+ callSid: input.callSid ?? this.state.sid("CA"),
3605
+ wavBase64: toBase64(wav),
3606
+ channels: format?.channels ?? 0,
3607
+ duration: durationSeconds(wav),
3608
+ createdAt: rfc2822(this.now()),
3609
+ deleted: false
3610
+ };
3611
+ this.state.recordings.insert(sid, recording);
3612
+ return recording;
3613
+ }
3614
+ outbox() {
3615
+ return this.state.outbox.list();
3616
+ }
3617
+ messages() {
3618
+ return this.state.messages.list({ order: "oldest" }).map((row) => row.value);
3619
+ }
3620
+ };
3621
+
3622
+ export {
3623
+ document,
3624
+ operationIds,
3625
+ supportedOperationIds,
3626
+ lookup,
3627
+ e164Key,
3628
+ DEFAULT_VERIFY_SETTINGS,
3629
+ readWav,
3630
+ synthesizeWav,
3631
+ mixDownToMono,
3632
+ TWILIO_PRODUCTS,
3633
+ twilioMockUrl,
3634
+ TWILIO_PRESETS,
3635
+ TWILIO_WEBHOOK_EVENTS,
3636
+ createRuntime2 as createRuntime,
3637
+ TWILIO_NAMESPACE,
3638
+ DEFAULT_ACCOUNT_SID,
3639
+ twilioError,
3640
+ TwilioAPI
3641
+ };
3642
+ //# sourceMappingURL=chunk-VOBVFMYN.js.map