@crvouga/mockingbird-service-formbricks 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3980 @@
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 (error2) {
222
+ return adminError(404, error2 instanceof Error ? error2.message : String(error2));
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 (error2) {
278
+ return adminError(409, error2 instanceof Error ? error2.message : String(error2));
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 (error2) {
289
+ return adminError(409, error2 instanceof Error ? error2.message : String(error2));
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 (error2) {
303
+ return adminError(409, error2 instanceof Error ? error2.message : String(error2));
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 createCredentialRegistry = () => {
393
+ const map = /* @__PURE__ */ new Map();
394
+ return {
395
+ set: (credential, namespace) => {
396
+ map.set(credential, namespace);
397
+ },
398
+ get: (credential) => map.get(credential),
399
+ remove: (credential) => map.delete(credential),
400
+ clear: () => map.clear(),
401
+ entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
402
+ };
403
+ };
404
+ var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
405
+
406
+ // ../core/dist/rng.js
407
+ var seedFrom = (value) => {
408
+ let hash = 2166136261;
409
+ for (let i = 0; i < value.length; i++) {
410
+ hash ^= value.charCodeAt(i);
411
+ hash = Math.imul(hash, 16777619);
412
+ }
413
+ return hash >>> 0;
414
+ };
415
+ var createRng = (seed = 0) => {
416
+ const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
417
+ let state = numeric;
418
+ const next = () => {
419
+ state = state + 1831565813 >>> 0;
420
+ let t = state;
421
+ t = Math.imul(t ^ t >>> 15, t | 1);
422
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
423
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
424
+ };
425
+ return {
426
+ next,
427
+ int: (min, max) => min + Math.floor(next() * (max - min + 1)),
428
+ reset: () => {
429
+ state = numeric;
430
+ },
431
+ state: () => state,
432
+ setState: (next2) => {
433
+ if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
434
+ throw new RangeError("rng state must be an unsigned 32-bit integer");
435
+ }
436
+ state = next2 >>> 0;
437
+ },
438
+ seed: numeric
439
+ };
440
+ };
441
+
442
+ // ../core/dist/faults.js
443
+ var matches = (rule, candidate) => {
444
+ if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
445
+ return false;
446
+ }
447
+ if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
448
+ return false;
449
+ if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
450
+ return false;
451
+ }
452
+ if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
453
+ return false;
454
+ return true;
455
+ };
456
+ var faultResponse = (rule) => {
457
+ const status = rule.status ?? 500;
458
+ const headers = { "content-type": "application/json", ...rule.headers };
459
+ if (typeof rule.body === "string")
460
+ return new Response(rule.body, { status, headers });
461
+ if (rule.body === null)
462
+ return new Response(null, { status, headers: rule.headers ?? {} });
463
+ const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
464
+ return new Response(JSON.stringify(body), { status, headers });
465
+ };
466
+ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
467
+ const entries = [];
468
+ return {
469
+ add(rule) {
470
+ const existing = entries.findIndex((e) => e.rule.id === rule.id);
471
+ const entry = { rule, remaining: rule.count ?? null, hits: 0 };
472
+ if (existing >= 0)
473
+ entries[existing] = entry;
474
+ else
475
+ entries.push(entry);
476
+ return rule;
477
+ },
478
+ list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
479
+ remove(id) {
480
+ const index = entries.findIndex((e) => e.rule.id === id);
481
+ if (index < 0)
482
+ return false;
483
+ entries.splice(index, 1);
484
+ return true;
485
+ },
486
+ clear() {
487
+ entries.length = 0;
488
+ },
489
+ async take(candidate) {
490
+ const hits = [];
491
+ for (const entry of entries) {
492
+ if (entry.remaining === 0)
493
+ continue;
494
+ if (!matches(entry.rule, candidate))
495
+ continue;
496
+ const rate = entry.rule.rate ?? 1;
497
+ if (rng.next() >= rate)
498
+ continue;
499
+ entry.hits++;
500
+ if (entry.remaining !== null)
501
+ entry.remaining--;
502
+ const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
503
+ if (delay !== void 0 && delay > 0) {
504
+ await sleep(delay);
505
+ }
506
+ const hit = { id: entry.rule.id };
507
+ if (entry.rule.effect !== void 0) {
508
+ hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
509
+ }
510
+ if (entry.rule.drop === true)
511
+ hit.drop = true;
512
+ else if (entry.rule.status !== void 0)
513
+ hit.response = faultResponse(entry.rule);
514
+ hits.push(hit);
515
+ if (hit.drop || hit.response)
516
+ break;
517
+ }
518
+ return hits;
519
+ }
520
+ };
521
+ };
522
+
523
+ // ../../openapi/core/dist/refs.js
524
+ var OpenAPIReferenceError = class extends Error {
525
+ ref;
526
+ constructor(ref) {
527
+ super(`unresolvable $ref: ${ref}`);
528
+ this.ref = ref;
529
+ this.name = "OpenAPIReferenceError";
530
+ }
531
+ };
532
+ var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
533
+ var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
534
+ var resolveRef = (document2, ref) => {
535
+ if (!ref.startsWith("#/"))
536
+ throw new OpenAPIReferenceError(ref);
537
+ let cursor = document2;
538
+ for (const raw of ref.slice(2).split("/")) {
539
+ const segment = unescapePointer(raw);
540
+ if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
541
+ throw new OpenAPIReferenceError(ref);
542
+ }
543
+ cursor = cursor[segment];
544
+ }
545
+ if (cursor === void 0)
546
+ throw new OpenAPIReferenceError(ref);
547
+ return cursor;
548
+ };
549
+ var deref = (document2, value) => {
550
+ let current = value;
551
+ const seen = /* @__PURE__ */ new Set();
552
+ while (isReference(current)) {
553
+ if (seen.has(current.$ref))
554
+ throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
555
+ seen.add(current.$ref);
556
+ current = resolveRef(document2, current.$ref);
557
+ }
558
+ return current;
559
+ };
560
+
561
+ // ../../openapi/core/dist/types.js
562
+ var HTTP_METHODS = [
563
+ "get",
564
+ "put",
565
+ "post",
566
+ "delete",
567
+ "options",
568
+ "head",
569
+ "patch",
570
+ "trace"
571
+ ];
572
+
573
+ // ../../openapi/core/dist/document.js
574
+ var mergeParameters = (document2, item, own) => {
575
+ const merged = /* @__PURE__ */ new Map();
576
+ for (const raw of item.parameters ?? []) {
577
+ const parameter = deref(document2, raw);
578
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
579
+ }
580
+ for (const raw of own ?? []) {
581
+ const parameter = deref(document2, raw);
582
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
583
+ }
584
+ return [...merged.values()];
585
+ };
586
+ var listOperations = (document2) => {
587
+ const operations = [];
588
+ for (const [path, item] of Object.entries(document2.paths)) {
589
+ for (const method of HTTP_METHODS) {
590
+ const operation = item[method];
591
+ if (operation?.operationId === void 0)
592
+ continue;
593
+ const responses = {};
594
+ for (const [status, response] of Object.entries(operation.responses)) {
595
+ responses[status] = deref(document2, response);
596
+ }
597
+ operations.push({
598
+ operationId: operation.operationId,
599
+ method,
600
+ path,
601
+ operation,
602
+ parameters: mergeParameters(document2, item, operation.parameters),
603
+ requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
604
+ responses
605
+ });
606
+ }
607
+ }
608
+ return operations;
609
+ };
610
+
611
+ // ../../http/codec/dist/form.js
612
+ var parsePath = (rawKey) => {
613
+ const open = rawKey.indexOf("[");
614
+ if (open === -1)
615
+ return [rawKey];
616
+ const path = [rawKey.slice(0, open)];
617
+ const rest = rawKey.slice(open);
618
+ const pattern = /\[([^\]]*)\]/g;
619
+ let match = pattern.exec(rest);
620
+ let consumed = 0;
621
+ while (match !== null) {
622
+ if (match.index !== consumed)
623
+ return [rawKey];
624
+ path.push(match[1] ?? "");
625
+ consumed = match.index + match[0].length;
626
+ match = pattern.exec(rest);
627
+ }
628
+ if (consumed !== rest.length)
629
+ return [rawKey];
630
+ return path;
631
+ };
632
+ var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
633
+ var put = (target, key, value) => {
634
+ if (key === "__proto__") {
635
+ Object.defineProperty(target, key, {
636
+ value,
637
+ enumerable: true,
638
+ writable: true,
639
+ configurable: true
640
+ });
641
+ return;
642
+ }
643
+ ;
644
+ target[key] = value;
645
+ };
646
+ var assign = (target, path, value) => {
647
+ let cursor = target;
648
+ for (let i = 0; i < path.length; i++) {
649
+ const segment = path[i];
650
+ const last = i === path.length - 1;
651
+ if (Array.isArray(cursor)) {
652
+ const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
653
+ if (index === void 0)
654
+ return;
655
+ if (last) {
656
+ put(cursor, index, value);
657
+ return;
658
+ }
659
+ const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
660
+ if (next === void 0 || typeof next === "string") {
661
+ const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
662
+ put(cursor, index, created);
663
+ cursor = created;
664
+ } else {
665
+ cursor = next;
666
+ }
667
+ continue;
668
+ }
669
+ if (typeof cursor === "string")
670
+ return;
671
+ if (last) {
672
+ put(cursor, segment, value);
673
+ return;
674
+ }
675
+ const nextSegment = path[i + 1];
676
+ const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
677
+ if (existing === void 0 || typeof existing === "string") {
678
+ const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
679
+ put(cursor, segment, created);
680
+ cursor = created;
681
+ } else {
682
+ cursor = existing;
683
+ }
684
+ }
685
+ };
686
+ var decodeFormPairs = (pairs) => {
687
+ const out = {};
688
+ for (const [rawKey, value] of pairs)
689
+ assign(out, parsePath(rawKey), value);
690
+ return densify(out);
691
+ };
692
+ var densify = (value) => {
693
+ if (typeof value === "string")
694
+ return value;
695
+ if (Array.isArray(value))
696
+ return value.filter((item) => item !== void 0).map(densify);
697
+ const out = {};
698
+ for (const [key, item] of Object.entries(value))
699
+ put(out, key, densify(item));
700
+ return out;
701
+ };
702
+ var decodeForm = (text) => {
703
+ const source = text.startsWith("?") ? text.slice(1) : text;
704
+ return decodeFormPairs(new URLSearchParams(source).entries());
705
+ };
706
+
707
+ // ../../http/codec/dist/content.js
708
+ var JSON_MEDIA_TYPE = "application/json";
709
+ var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
710
+ var mediaTypeOf = (contentType) => {
711
+ if (!contentType)
712
+ return void 0;
713
+ const essence = contentType.split(";")[0]?.trim().toLowerCase();
714
+ return essence ? essence : void 0;
715
+ };
716
+ var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
717
+ var utf8 = new TextDecoder("utf-8", { fatal: false });
718
+ var decodeBody = (contentType, bytes) => {
719
+ if (bytes.byteLength === 0)
720
+ return { kind: "empty" };
721
+ const mediaType = mediaTypeOf(contentType);
722
+ if (mediaType === void 0)
723
+ return { kind: "bytes", value: bytes };
724
+ if (isJsonMediaType(mediaType)) {
725
+ const text = utf8.decode(bytes);
726
+ try {
727
+ return { kind: "json", value: JSON.parse(text) };
728
+ } catch (error2) {
729
+ return {
730
+ kind: "invalid",
731
+ mediaType,
732
+ text,
733
+ error: error2 instanceof Error ? error2.message : String(error2)
734
+ };
735
+ }
736
+ }
737
+ if (mediaType === FORM_MEDIA_TYPE) {
738
+ return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
739
+ }
740
+ if (mediaType.startsWith("text/"))
741
+ return { kind: "text", value: utf8.decode(bytes) };
742
+ return { kind: "bytes", value: bytes };
743
+ };
744
+ var readBody = async (message) => {
745
+ const bytes = new Uint8Array(await message.arrayBuffer());
746
+ return decodeBody(message.headers.get("content-type"), bytes);
747
+ };
748
+
749
+ // ../core/dist/http.js
750
+ var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
751
+ status,
752
+ headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
753
+ });
754
+ var HttpError = class extends Error {
755
+ status;
756
+ body;
757
+ headers;
758
+ constructor(status, body, headers = {}) {
759
+ super(`HTTP ${status}`);
760
+ this.status = status;
761
+ this.body = body;
762
+ this.headers = headers;
763
+ this.name = "HttpError";
764
+ }
765
+ toResponse() {
766
+ const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
767
+ if (contentType === "text/plain") {
768
+ return new Response(String(this.body), {
769
+ status: this.status,
770
+ headers: this.headers
771
+ });
772
+ }
773
+ return jsonRes(this.status, this.body, this.headers);
774
+ }
775
+ };
776
+
777
+ // ../core/dist/ids.js
778
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
779
+ var mix = (input) => {
780
+ let hash = 2166136261;
781
+ for (let i = 0; i < input.length; i++) {
782
+ hash ^= input.charCodeAt(i);
783
+ hash = Math.imul(hash, 16777619) >>> 0;
784
+ }
785
+ hash ^= hash >>> 16;
786
+ hash = Math.imul(hash, 2246822507) >>> 0;
787
+ hash ^= hash >>> 13;
788
+ return hash >>> 0;
789
+ };
790
+ var opaqueToken = (input, length) => {
791
+ let out = "";
792
+ let round = 0;
793
+ while (out.length < length) {
794
+ let hash = mix(`${input}:${round++}`);
795
+ for (let i = 0; i < 5 && out.length < length; i++) {
796
+ out += ALPHABET.charAt(hash % ALPHABET.length);
797
+ hash = Math.floor(hash / ALPHABET.length);
798
+ }
799
+ }
800
+ return out;
801
+ };
802
+ var IdSequence = class {
803
+ sqlite;
804
+ namespace;
805
+ salt;
806
+ constructor(sqlite, namespace, salt = "mockingbird") {
807
+ this.sqlite = sqlite;
808
+ this.namespace = namespace;
809
+ this.salt = salt;
810
+ }
811
+ next(prefix, length = 14) {
812
+ return this.sqlite.transaction(() => {
813
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'id'").get(this.namespace, prefix);
814
+ const value = (row?.value ?? 0) + 1;
815
+ this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'id', ?)
816
+ ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, prefix, value);
817
+ return `${prefix}${opaqueToken(`${this.salt}:${prefix}:${value}`, length)}`;
818
+ });
819
+ }
820
+ };
821
+
822
+ // ../core/dist/journal.js
823
+ var DEFAULT_JOURNAL_SIZE = 1e3;
824
+ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
825
+ const capacity = Math.max(0, Math.floor(size));
826
+ const rings = /* @__PURE__ */ new Map();
827
+ let sequence = 0;
828
+ const order = /* @__PURE__ */ new WeakMap();
829
+ const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
830
+ return {
831
+ size: capacity,
832
+ record(entry) {
833
+ if (capacity === 0)
834
+ return;
835
+ order.set(entry, sequence++);
836
+ let ring = rings.get(entry.namespace);
837
+ if (!ring) {
838
+ ring = { entries: [], next: 0 };
839
+ rings.set(entry.namespace, ring);
840
+ }
841
+ if (ring.entries.length < capacity)
842
+ ring.entries.push(entry);
843
+ else {
844
+ ring.entries[ring.next] = entry;
845
+ ring.next = (ring.next + 1) % capacity;
846
+ }
847
+ },
848
+ list(query = {}) {
849
+ 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));
850
+ 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));
851
+ return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
852
+ },
853
+ clear(namespace) {
854
+ if (namespace === void 0)
855
+ rings.clear();
856
+ else
857
+ rings.delete(namespace);
858
+ }
859
+ };
860
+ };
861
+ var notes = /* @__PURE__ */ new WeakMap();
862
+ var annotateResponse = (response, extra) => {
863
+ const existing = notes.get(response);
864
+ notes.set(response, {
865
+ ...existing,
866
+ ...extra,
867
+ ...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
868
+ });
869
+ return response;
870
+ };
871
+ var responseNotes = (response) => notes.get(response);
872
+
873
+ // ../core/dist/metrics.js
874
+ var createMetrics = () => {
875
+ let requests = 0;
876
+ let faults = 0;
877
+ let totalDurationMs = 0;
878
+ const byOperation = /* @__PURE__ */ new Map();
879
+ const unmatched = /* @__PURE__ */ new Map();
880
+ return {
881
+ record(entry) {
882
+ requests++;
883
+ totalDurationMs += entry.durationMs;
884
+ if (entry.faultId !== void 0)
885
+ faults++;
886
+ const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
887
+ byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
888
+ if (entry.unmatched) {
889
+ const route = `${entry.method} ${entry.path}`;
890
+ unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
891
+ }
892
+ },
893
+ report: () => ({
894
+ requests,
895
+ byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
896
+ unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
897
+ const space = route.indexOf(" ");
898
+ return { method: route.slice(0, space), path: route.slice(space + 1), count };
899
+ }),
900
+ faults,
901
+ totalDurationMs
902
+ }),
903
+ reset() {
904
+ requests = 0;
905
+ faults = 0;
906
+ totalDurationMs = 0;
907
+ byOperation.clear();
908
+ unmatched.clear();
909
+ }
910
+ };
911
+ };
912
+
913
+ // ../../core/dist/timeline.js
914
+ var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
915
+ var Timeline = class {
916
+ maxCheckpoints;
917
+ now;
918
+ makeId;
919
+ nodes = /* @__PURE__ */ new Map();
920
+ heads = /* @__PURE__ */ new Map();
921
+ /** Unreferenced nodes in the exact order they became collectible. */
922
+ evictable = /* @__PURE__ */ new Set();
923
+ /** Branch heads plus explicit retainers. Absent means zero. */
924
+ references = /* @__PURE__ */ new Map();
925
+ explicitPins = /* @__PURE__ */ new Map();
926
+ sequence = 0;
927
+ constructor(options = {}) {
928
+ const max = options.maxCheckpoints ?? 1e3;
929
+ if (!Number.isSafeInteger(max) || max < 1)
930
+ throw new RangeError("maxCheckpoints must be a positive integer");
931
+ this.maxCheckpoints = max;
932
+ this.now = options.now ?? (() => this.sequence);
933
+ this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
934
+ }
935
+ /** Capture a new immutable value and move `branch` to it. */
936
+ commit(value, options = {}) {
937
+ const branch = options.branch ?? "main";
938
+ this.assertBranch(branch);
939
+ const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
940
+ if (parent !== null && !this.nodes.has(parent))
941
+ throw new RangeError(`no checkpoint ${parent}`);
942
+ const id = this.makeId(++this.sequence);
943
+ if (this.nodes.has(id))
944
+ throw new RangeError(`duplicate checkpoint id ${id}`);
945
+ const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
946
+ this.nodes.set(id, checkpoint);
947
+ this.moveHead(branch, id);
948
+ this.collect(this.maxCheckpoints);
949
+ return checkpoint;
950
+ }
951
+ /** Create a branch pointer without copying its checkpoint value. */
952
+ fork(branch, options = {}) {
953
+ this.assertBranch(branch);
954
+ if (this.heads.has(branch))
955
+ throw new RangeError(`branch already exists: ${branch}`);
956
+ const from = options.from ?? this.heads.get("main");
957
+ if (from === void 0)
958
+ return void 0;
959
+ const checkpoint = this.get(from);
960
+ this.moveHead(branch, checkpoint.id);
961
+ return checkpoint;
962
+ }
963
+ /** Move a branch pointer to an existing checkpoint. */
964
+ checkout(branch, id) {
965
+ this.assertBranch(branch);
966
+ const checkpoint = this.get(id);
967
+ this.moveHead(branch, checkpoint.id);
968
+ return checkpoint;
969
+ }
970
+ get(id) {
971
+ const checkpoint = this.nodes.get(id);
972
+ if (!checkpoint)
973
+ throw new RangeError(`no checkpoint ${id}`);
974
+ return checkpoint;
975
+ }
976
+ head(branch = "main") {
977
+ const id = this.heads.get(branch);
978
+ return id === void 0 ? void 0 : this.get(id);
979
+ }
980
+ hasBranch(branch) {
981
+ return this.heads.has(branch);
982
+ }
983
+ branches() {
984
+ return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
985
+ }
986
+ checkpoints() {
987
+ return [...this.nodes.values()];
988
+ }
989
+ /** Number of retained checkpoints without allocating an array. */
990
+ get size() {
991
+ return this.nodes.size;
992
+ }
993
+ /** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
994
+ retain(id) {
995
+ const checkpoint = this.get(id);
996
+ this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
997
+ this.addReference(id);
998
+ return checkpoint;
999
+ }
1000
+ /** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
1001
+ release(id) {
1002
+ if (!this.nodes.has(id))
1003
+ return false;
1004
+ const pins = this.explicitPins.get(id) ?? 0;
1005
+ if (pins === 0)
1006
+ return false;
1007
+ if (pins === 1)
1008
+ this.explicitPins.delete(id);
1009
+ else
1010
+ this.explicitPins.set(id, pins - 1);
1011
+ this.removeReference(id);
1012
+ this.collect(this.maxCheckpoints);
1013
+ return true;
1014
+ }
1015
+ deleteBranch(branch) {
1016
+ if (branch === "main")
1017
+ throw new RangeError("cannot delete main branch");
1018
+ const previous = this.heads.get(branch);
1019
+ const deleted = this.heads.delete(branch);
1020
+ if (previous !== void 0)
1021
+ this.removeReference(previous);
1022
+ this.collect(this.maxCheckpoints);
1023
+ return deleted;
1024
+ }
1025
+ /**
1026
+ * Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
1027
+ * commits never scan pinned nodes or the retained history. Parents are metadata rather than a
1028
+ * storage dependency, so a retained node remains usable after pruning.
1029
+ */
1030
+ gc(max = this.maxCheckpoints) {
1031
+ if (!Number.isSafeInteger(max) || max < 1)
1032
+ throw new RangeError("max must be a positive integer");
1033
+ const removed = [];
1034
+ this.collect(max, removed);
1035
+ return removed;
1036
+ }
1037
+ collect(max, removed) {
1038
+ while (this.nodes.size > max && this.evictable.size > 0) {
1039
+ const id = this.evictable.values().next().value;
1040
+ this.evictable.delete(id);
1041
+ this.nodes.delete(id);
1042
+ removed?.push(id);
1043
+ }
1044
+ }
1045
+ moveHead(branch, id) {
1046
+ const previous = this.heads.get(branch);
1047
+ if (previous === id)
1048
+ return;
1049
+ if (previous !== void 0)
1050
+ this.removeReference(previous);
1051
+ this.heads.set(branch, id);
1052
+ this.addReference(id);
1053
+ }
1054
+ addReference(id) {
1055
+ this.references.set(id, (this.references.get(id) ?? 0) + 1);
1056
+ this.evictable.delete(id);
1057
+ }
1058
+ removeReference(id) {
1059
+ const next = (this.references.get(id) ?? 0) - 1;
1060
+ if (next > 0)
1061
+ this.references.set(id, next);
1062
+ else {
1063
+ this.references.delete(id);
1064
+ if (this.nodes.has(id))
1065
+ this.evictable.add(id);
1066
+ }
1067
+ }
1068
+ assertBranch(branch) {
1069
+ if (!BRANCH_PATTERN.test(branch))
1070
+ throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
1071
+ }
1072
+ };
1073
+
1074
+ // ../../sqlite/dist/default.js
1075
+ import { Database } from "@crvouga/mockingbird-service-sqlite";
1076
+ var createDefaultSqlite = () => new Database();
1077
+ var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
1078
+
1079
+ // ../../sqlite/dist/migrate.js
1080
+ var ensureMigrationsTable = (sqlite) => {
1081
+ sqlite.exec(`
1082
+ CREATE TABLE IF NOT EXISTS schema_migrations (
1083
+ id TEXT PRIMARY KEY NOT NULL,
1084
+ applied_at INTEGER NOT NULL
1085
+ )
1086
+ `);
1087
+ };
1088
+ var migrate = (sqlite, migrations) => {
1089
+ ensureMigrationsTable(sqlite);
1090
+ const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
1091
+ const pending = migrations.filter((migration) => !applied.has(migration.id));
1092
+ if (pending.length === 0)
1093
+ return;
1094
+ const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
1095
+ const now = Math.floor(Date.now() / 1e3);
1096
+ sqlite.transaction(() => {
1097
+ for (const migration of pending) {
1098
+ sqlite.exec(migration.sql);
1099
+ insert.run(migration.id, now);
1100
+ }
1101
+ });
1102
+ };
1103
+
1104
+ // ../../sqlite/dist/schema.js
1105
+ var CORE_MIGRATIONS = [
1106
+ {
1107
+ id: "20260322_core_records_sequences",
1108
+ sql: `
1109
+ CREATE TABLE IF NOT EXISTS mockingbird_records (
1110
+ namespace TEXT NOT NULL,
1111
+ collection TEXT NOT NULL,
1112
+ id TEXT NOT NULL,
1113
+ seq INTEGER NOT NULL,
1114
+ value TEXT NOT NULL,
1115
+ PRIMARY KEY (namespace, collection, id)
1116
+ );
1117
+ CREATE INDEX IF NOT EXISTS mockingbird_records_seq
1118
+ ON mockingbird_records (namespace, collection, seq);
1119
+ CREATE TABLE IF NOT EXISTS mockingbird_sequences (
1120
+ namespace TEXT NOT NULL,
1121
+ name TEXT NOT NULL,
1122
+ kind TEXT NOT NULL,
1123
+ value INTEGER NOT NULL,
1124
+ PRIMARY KEY (namespace, name, kind)
1125
+ );
1126
+ `
1127
+ }
1128
+ ];
1129
+ var migrateCore = (sqlite) => {
1130
+ migrate(sqlite, CORE_MIGRATIONS);
1131
+ };
1132
+ var clearNamespace = (sqlite, namespace) => {
1133
+ sqlite.transaction(() => {
1134
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1135
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1136
+ });
1137
+ };
1138
+
1139
+ // ../../openapi/metadata/dist/types.js
1140
+ var EXTENSION_KEYS = {
1141
+ operation: "x-mockingbird",
1142
+ resource: "x-mockingbird-resource",
1143
+ resourceRef: "x-mockingbird-resource-ref",
1144
+ volatile: "x-mockingbird-volatile",
1145
+ scope: "x-mockingbird-scope",
1146
+ unsupported: "x-mockingbird-unsupported",
1147
+ parityHeader: "x-mockingbird-parity-header"
1148
+ };
1149
+
1150
+ // ../../openapi/metadata/dist/read.js
1151
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1152
+ var extensionOf = (holder, key) => holder[key];
1153
+ var operationMetadata = (operation) => {
1154
+ const raw = extensionOf(operation, EXTENSION_KEYS.operation);
1155
+ const ext = isRecord2(raw) ? raw : {};
1156
+ const supported = ext.supported ?? true;
1157
+ const parity = ext.parity ?? {};
1158
+ return {
1159
+ supported,
1160
+ reason: typeof ext.reason === "string" ? ext.reason : void 0,
1161
+ parity: {
1162
+ enabled: supported && (parity.enabled ?? true),
1163
+ safe: parity.safe ?? true,
1164
+ reason: typeof parity.reason === "string" ? parity.reason : void 0
1165
+ }
1166
+ };
1167
+ };
1168
+
1169
+ // ../core/dist/service.js
1170
+ import { Hono } from "hono";
1171
+ var defineOperations = (handlers) => handlers;
1172
+ var OperationRegistryError = class extends Error {
1173
+ problems;
1174
+ constructor(problems) {
1175
+ super(`operation registry is inconsistent:
1176
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
1177
+ this.problems = problems;
1178
+ this.name = "OperationRegistryError";
1179
+ }
1180
+ };
1181
+ var verifyOperations = (document2, handlers) => {
1182
+ const problems = [];
1183
+ const operations = listOperations(document2);
1184
+ const seen = /* @__PURE__ */ new Set();
1185
+ for (const operation of operations) {
1186
+ if (seen.has(operation.operationId))
1187
+ problems.push(`duplicate operationId ${operation.operationId}`);
1188
+ seen.add(operation.operationId);
1189
+ const supported = operationMetadata(operation.operation).supported;
1190
+ const handler = handlers[operation.operationId];
1191
+ if (supported && !handler)
1192
+ problems.push(`supported operation ${operation.operationId} has no handler`);
1193
+ if (!supported && handler)
1194
+ problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
1195
+ }
1196
+ for (const id of Object.keys(handlers)) {
1197
+ if (!seen.has(id))
1198
+ problems.push(`handler ${id} has no OpenAPI operation`);
1199
+ }
1200
+ return problems;
1201
+ };
1202
+ var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
1203
+ var routeOrder = (a, b) => {
1204
+ const sa = a.path.split("/");
1205
+ const sb = b.path.split("/");
1206
+ for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
1207
+ const x = sa[i] ?? "";
1208
+ const y = sb[i] ?? "";
1209
+ const px = x.startsWith("{");
1210
+ const py = y.startsWith("{");
1211
+ if (px !== py)
1212
+ return px ? 1 : -1;
1213
+ if (x !== y)
1214
+ return x < y ? -1 : 1;
1215
+ }
1216
+ return 0;
1217
+ };
1218
+ var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
1219
+ var bootSqlite = (sqlite) => {
1220
+ const client = resolveSqlite(sqlite);
1221
+ migrateCore(client);
1222
+ return client;
1223
+ };
1224
+ var createService = (options) => {
1225
+ const problems = verifyOperations(options.document, options.handlers);
1226
+ if (problems.length > 0)
1227
+ throw new OperationRegistryError(problems);
1228
+ migrateCore(options.sqlite);
1229
+ const now = options.now ?? (() => Date.now());
1230
+ const app = new Hono();
1231
+ app.notFound((c) => options.notFound(c.req.raw));
1232
+ app.onError((error2, c) => options.onError(error2, c.req.raw));
1233
+ const operations = [...listOperations(options.document)].sort(routeOrder);
1234
+ for (const operation of operations) {
1235
+ const metadata = operationMetadata(operation.operation);
1236
+ const handler = options.handlers[operation.operationId];
1237
+ const route = async (c) => {
1238
+ const request = c.req.raw;
1239
+ if (!metadata.supported || !handler) {
1240
+ return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
1241
+ }
1242
+ const url = new URL(request.url);
1243
+ const context = {
1244
+ request,
1245
+ url,
1246
+ params: c.req.param(),
1247
+ query: queryOf(url),
1248
+ body: await readBody(request),
1249
+ sqlite: options.sqlite,
1250
+ namespace: options.namespace,
1251
+ operation,
1252
+ document: options.document,
1253
+ now
1254
+ };
1255
+ const short = await options.before?.(context);
1256
+ if (short)
1257
+ return short;
1258
+ return handler(context);
1259
+ };
1260
+ app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1261
+ }
1262
+ return {
1263
+ app,
1264
+ sqlite: options.sqlite,
1265
+ namespace: options.namespace,
1266
+ fetch: async (request) => app.fetch(request),
1267
+ reset: async () => {
1268
+ clearNamespace(options.sqlite, options.namespace);
1269
+ }
1270
+ };
1271
+ };
1272
+
1273
+ // ../core/dist/snapshot.js
1274
+ var snapshotNamespace = (sqlite, namespace) => ({
1275
+ namespace,
1276
+ records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
1277
+ sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
1278
+ });
1279
+ var restoreNamespace = (sqlite, namespace, snapshot) => {
1280
+ sqlite.transaction(() => {
1281
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1282
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1283
+ const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
1284
+ for (const row of snapshot.records) {
1285
+ record.run(namespace, row.collection, row.id, row.seq, row.value);
1286
+ }
1287
+ const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
1288
+ for (const row of snapshot.sequences) {
1289
+ sequence.run(namespace, row.name, row.kind, row.value);
1290
+ }
1291
+ });
1292
+ };
1293
+
1294
+ // ../core/dist/version.js
1295
+ var PACKAGE_VERSION = true ? "1.0.0" : UNRELEASED_VERSION;
1296
+
1297
+ // ../core/dist/signing.js
1298
+ var encoder = new TextEncoder();
1299
+ var toBase64 = (bytes) => {
1300
+ let binary = "";
1301
+ for (const byte of bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)) {
1302
+ binary += String.fromCharCode(byte);
1303
+ }
1304
+ return btoa(binary);
1305
+ };
1306
+ var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
1307
+ var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1308
+ var keyBytes = (key) => typeof key === "string" ? encoder.encode(key) : key;
1309
+ var hmac = async (algorithm, key, message, encoding = "hex") => {
1310
+ const imported = await crypto.subtle.importKey("raw", keyBytes(key), { name: "HMAC", hash: algorithm }, false, ["sign"]);
1311
+ const signed = await crypto.subtle.sign("HMAC", imported, typeof message === "string" ? encoder.encode(message) : message);
1312
+ return encoding === "hex" ? toHex(signed) : toBase64(signed);
1313
+ };
1314
+ var svixSecretBytes = (secret) => {
1315
+ const raw = secret.replace(/^f?whsec_/, "");
1316
+ try {
1317
+ return fromBase64(raw);
1318
+ } catch {
1319
+ throw new TypeError("webhook secret must be whsec_<base64> (as Svix issues it)");
1320
+ }
1321
+ };
1322
+ var signSvix = async (secret, messageId, timestampSeconds, body) => `v1,${await hmac("SHA-256", svixSecretBytes(secret), `${messageId}.${timestampSeconds}.${body}`, "base64")}`;
1323
+ var signTimestamped = async (secret, timestampSeconds, body) => `t=${timestampSeconds},v1=${await hmac("SHA-256", secret, `${timestampSeconds}.${body}`, "hex")}`;
1324
+ var signTwilio = async (authToken, url, params) => {
1325
+ const payload = url + Object.keys(params).sort().map((key) => `${key}${params[key]}`).join("");
1326
+ return hmac("SHA-1", authToken, payload, "base64");
1327
+ };
1328
+
1329
+ // ../core/dist/webhooks.js
1330
+ var signers = {
1331
+ /** No signature. */
1332
+ none: () => () => ({}),
1333
+ /** Svix (`svix-id`, `svix-timestamp`, `svix-signature: v1,<b64>`): Junction, Flex, Resend. */
1334
+ svix: (options = {}) => async ({ messageId, body, timestampSeconds, secret }) => {
1335
+ if (!secret)
1336
+ return {};
1337
+ const prefix = options.prefix ?? "svix";
1338
+ return {
1339
+ [`${prefix}-id`]: messageId,
1340
+ [`${prefix}-timestamp`]: String(timestampSeconds),
1341
+ [`${prefix}-signature`]: await signSvix(secret, messageId, timestampSeconds, body)
1342
+ };
1343
+ },
1344
+ /** `<header>: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>`: Stripe, Persona, Fullscript. */
1345
+ timestamped: (header = "Stripe-Signature") => async ({ body, timestampSeconds, secret }) => secret ? { [header]: await signTimestamped(secret, timestampSeconds, body) } : {},
1346
+ /** `X-Twilio-Signature` over the public URL plus the sorted form parameters. */
1347
+ twilio: () => async ({ signUrl, form, secret }) => secret ? { "X-Twilio-Signature": await signTwilio(secret, signUrl, form ?? {}) } : {},
1348
+ /** The secret itself in a header (optionally templated): RxVortex, Pharmetika, AHA `Token <s>`. */
1349
+ header: (header, format = (s) => s) => ({ secret }) => secret ? { [header]: format(secret) } : {},
1350
+ /** Anything else: the service computes the headers itself. */
1351
+ custom: (sign) => sign
1352
+ };
1353
+ var DEFAULT_DELAYS = [0, 5e3, 3e5, 18e5, 72e5];
1354
+ var unref = (timer) => {
1355
+ ;
1356
+ timer.unref?.();
1357
+ };
1358
+ var randomId = (prefix) => `${prefix}${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
1359
+ var matchesEndpoint = (endpoint, message) => {
1360
+ const events = endpoint.events ?? ["*"];
1361
+ if (!events.includes("*") && !events.includes(message.type))
1362
+ return false;
1363
+ for (const [key, value] of Object.entries(endpoint.tags ?? {})) {
1364
+ if (message.tags[key] !== value)
1365
+ return false;
1366
+ }
1367
+ return true;
1368
+ };
1369
+ var createWebhookHub = (options) => {
1370
+ const delays = options.retryDelaysMs ?? DEFAULT_DELAYS;
1371
+ const timeoutMs = options.timeoutMs ?? 15e3;
1372
+ const delivered = options.delivered ?? ((status) => status >= 200 && status < 300);
1373
+ const send = options.fetch ?? ((request) => fetch(request));
1374
+ const keep = options.keep ?? 500;
1375
+ const now = options.now ?? Date.now;
1376
+ const id = options.id ?? randomId;
1377
+ const scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
1378
+ const cancel = options.cancel ?? ((handle) => clearTimeout(handle));
1379
+ const global = (options.endpoints ?? []).map((e, i) => ({ ...e, id: e.id ?? `we_global_${i}` }));
1380
+ const own = /* @__PURE__ */ new Map();
1381
+ const messages = [];
1382
+ const deliveries = /* @__PURE__ */ new Map();
1383
+ const pending = /* @__PURE__ */ new Map();
1384
+ const payloads = /* @__PURE__ */ new Map();
1385
+ const faults = /* @__PURE__ */ new Map();
1386
+ const held = /* @__PURE__ */ new Map();
1387
+ const inFlight = /* @__PURE__ */ new Set();
1388
+ const track = (work) => {
1389
+ inFlight.add(work);
1390
+ void work.finally(() => inFlight.delete(work));
1391
+ };
1392
+ const attempt = async (delivery) => {
1393
+ const entry = payloads.get(delivery.id);
1394
+ if (!entry)
1395
+ return false;
1396
+ const { message, endpoint } = entry;
1397
+ const timestampSeconds = Math.floor(now() / 1e3);
1398
+ const started = now();
1399
+ const record = {
1400
+ attempt: delivery.attempts.length + 1,
1401
+ at: new Date(started).toISOString(),
1402
+ status: null,
1403
+ error: null,
1404
+ durationMs: 0,
1405
+ responseBody: null
1406
+ };
1407
+ const controller = new AbortController();
1408
+ const timer = scheduleTimer(() => controller.abort(), timeoutMs);
1409
+ try {
1410
+ const signed = await options.signer({
1411
+ messageId: message.id,
1412
+ body: message.body,
1413
+ timestampSeconds,
1414
+ url: endpoint.url,
1415
+ secret: endpoint.secret,
1416
+ signUrl: endpoint.signUrl ?? endpoint.url,
1417
+ form: message.contentType.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(message.body)) : void 0,
1418
+ type: message.type,
1419
+ tags: message.tags
1420
+ });
1421
+ const response = await send(new Request(endpoint.url, {
1422
+ method: "POST",
1423
+ headers: {
1424
+ "content-type": message.contentType,
1425
+ ...endpoint.headers,
1426
+ ...message.headers,
1427
+ ...signed
1428
+ },
1429
+ body: message.body,
1430
+ signal: controller.signal
1431
+ }));
1432
+ record.status = response.status;
1433
+ record.responseBody = await response.text();
1434
+ } catch (error2) {
1435
+ record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error2 instanceof Error ? error2.message : String(error2);
1436
+ } finally {
1437
+ cancel(timer);
1438
+ record.durationMs = now() - started;
1439
+ delivery.attempts.push(record);
1440
+ }
1441
+ return record.status !== null && delivered(record.status);
1442
+ };
1443
+ const schedule = (delivery) => {
1444
+ const index = delivery.attempts.length;
1445
+ if (index >= delays.length) {
1446
+ delivery.state = "failed";
1447
+ pending.delete(delivery.id);
1448
+ return;
1449
+ }
1450
+ const run = () => {
1451
+ pending.delete(delivery.id);
1452
+ track(attempt(delivery).then((ok) => {
1453
+ if (ok)
1454
+ delivery.state = "delivered";
1455
+ else
1456
+ schedule(delivery);
1457
+ }));
1458
+ };
1459
+ const delay = delays[index] ?? 0;
1460
+ if (delay <= 0) {
1461
+ pending.set(delivery.id, void 0);
1462
+ run();
1463
+ return;
1464
+ }
1465
+ const timer = scheduleTimer(run, delay);
1466
+ unref(timer);
1467
+ pending.set(delivery.id, timer);
1468
+ };
1469
+ const endpointsFor = (namespace) => [...own.get(namespace) ?? [], ...global];
1470
+ const fanOut = (message, state = "pending") => {
1471
+ for (const endpoint of endpointsFor(message.namespace)) {
1472
+ if (!matchesEndpoint(endpoint, message))
1473
+ continue;
1474
+ const delivery = {
1475
+ id: id("dlv_"),
1476
+ messageId: message.id,
1477
+ namespace: message.namespace,
1478
+ type: message.type,
1479
+ endpointId: endpoint.id ?? "we_unknown",
1480
+ url: endpoint.url,
1481
+ state,
1482
+ attempts: []
1483
+ };
1484
+ deliveries.set(delivery.id, delivery);
1485
+ payloads.set(delivery.id, { message, endpoint });
1486
+ if (state === "pending")
1487
+ schedule(delivery);
1488
+ }
1489
+ };
1490
+ const takeFault = (namespace) => {
1491
+ const queue = faults.get(namespace);
1492
+ const head = queue?.[0];
1493
+ if (!queue || !head)
1494
+ return void 0;
1495
+ head.remaining--;
1496
+ if (head.remaining <= 0)
1497
+ queue.shift();
1498
+ return head.mode;
1499
+ };
1500
+ const releaseHeld = (namespace) => {
1501
+ const waiting = held.get(namespace);
1502
+ if (!waiting)
1503
+ return;
1504
+ held.delete(namespace);
1505
+ for (const message of waiting)
1506
+ fanOut(message);
1507
+ };
1508
+ const hub = {
1509
+ publish(input) {
1510
+ const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
1511
+ const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
1512
+ const message = {
1513
+ id: input.id ?? id("msg_"),
1514
+ namespace: input.namespace,
1515
+ type: input.type,
1516
+ body,
1517
+ contentType,
1518
+ tags: input.tags ?? {},
1519
+ headers: input.headers ?? {},
1520
+ publishedAt: new Date(now()).toISOString()
1521
+ };
1522
+ messages.push(message);
1523
+ const ofNamespace = messages.filter((m) => m.namespace === message.namespace);
1524
+ const oldest = ofNamespace[0];
1525
+ if (ofNamespace.length > keep && oldest)
1526
+ messages.splice(messages.indexOf(oldest), 1);
1527
+ options.onMessage?.(message);
1528
+ const fault = takeFault(message.namespace);
1529
+ if (fault === "drop") {
1530
+ fanOut(message, "dropped");
1531
+ return message;
1532
+ }
1533
+ if (fault === "reorder") {
1534
+ held.set(message.namespace, [...held.get(message.namespace) ?? [], message]);
1535
+ return message;
1536
+ }
1537
+ fanOut(message);
1538
+ if (fault === "duplicate")
1539
+ fanOut(message);
1540
+ releaseHeld(message.namespace);
1541
+ return message;
1542
+ },
1543
+ setEndpoints(namespace, endpoints) {
1544
+ const withIds = endpoints.map((e, i) => ({ ...e, id: e.id ?? `we_${namespace}_${i}` }));
1545
+ own.set(namespace, withIds);
1546
+ return withIds;
1547
+ },
1548
+ endpoints: endpointsFor,
1549
+ messages: (namespace) => namespace === void 0 ? [...messages] : messages.filter((m) => m.namespace === namespace),
1550
+ deliveries: (namespace) => [...deliveries.values()].filter((d) => namespace === void 0 || d.namespace === namespace),
1551
+ async replay(id2) {
1552
+ const delivery = deliveries.get(id2);
1553
+ if (!delivery)
1554
+ return void 0;
1555
+ const ok = await attempt(delivery);
1556
+ if (ok)
1557
+ delivery.state = "delivered";
1558
+ return delivery;
1559
+ },
1560
+ async flush() {
1561
+ for (const namespace of [...held.keys()])
1562
+ releaseHeld(namespace);
1563
+ const waiting = [...pending.entries()];
1564
+ for (const [id2, timer] of waiting) {
1565
+ if (timer === void 0)
1566
+ continue;
1567
+ cancel(timer);
1568
+ pending.delete(id2);
1569
+ const delivery = deliveries.get(id2);
1570
+ if (!delivery)
1571
+ continue;
1572
+ track(attempt(delivery).then((ok) => {
1573
+ if (ok)
1574
+ delivery.state = "delivered";
1575
+ else
1576
+ schedule(delivery);
1577
+ }));
1578
+ }
1579
+ await hub.idle();
1580
+ },
1581
+ async idle() {
1582
+ while (inFlight.size > 0)
1583
+ await Promise.allSettled([...inFlight]);
1584
+ },
1585
+ fault(namespace, fault) {
1586
+ const queue = faults.get(namespace) ?? [];
1587
+ queue.push({ mode: fault.mode, remaining: Math.max(1, fault.count ?? 1) });
1588
+ faults.set(namespace, queue);
1589
+ },
1590
+ clear(namespace) {
1591
+ for (const [id2, delivery] of deliveries) {
1592
+ if (namespace !== void 0 && delivery.namespace !== namespace)
1593
+ continue;
1594
+ const timer = pending.get(id2);
1595
+ if (timer !== void 0)
1596
+ cancel(timer);
1597
+ pending.delete(id2);
1598
+ deliveries.delete(id2);
1599
+ payloads.delete(id2);
1600
+ }
1601
+ for (let i = messages.length - 1; i >= 0; i--) {
1602
+ if (namespace === void 0 || messages[i]?.namespace === namespace)
1603
+ messages.splice(i, 1);
1604
+ }
1605
+ if (namespace === void 0) {
1606
+ held.clear();
1607
+ faults.clear();
1608
+ own.clear();
1609
+ } else {
1610
+ held.delete(namespace);
1611
+ faults.delete(namespace);
1612
+ own.delete(namespace);
1613
+ }
1614
+ }
1615
+ };
1616
+ return hub;
1617
+ };
1618
+ var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1619
+ var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
1620
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1621
+ var parseEndpoint = (value) => {
1622
+ if (!isRecord3(value) || typeof value.url !== "string")
1623
+ return "each endpoint needs a url";
1624
+ try {
1625
+ new URL(value.url);
1626
+ } catch {
1627
+ return `not a URL: ${value.url}`;
1628
+ }
1629
+ const endpoint = { url: value.url };
1630
+ if (typeof value.id === "string")
1631
+ endpoint.id = value.id;
1632
+ if (typeof value.secret === "string")
1633
+ endpoint.secret = value.secret;
1634
+ if (typeof value.signUrl === "string")
1635
+ endpoint.signUrl = value.signUrl;
1636
+ const events = value.events ?? value.enabledEvents;
1637
+ if (Array.isArray(events))
1638
+ endpoint.events = events.map(String);
1639
+ if (isRecord3(value.tags)) {
1640
+ endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
1641
+ }
1642
+ if (typeof value.account === "string")
1643
+ endpoint.tags = { ...endpoint.tags, account: value.account };
1644
+ if (isRecord3(value.headers)) {
1645
+ endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
1646
+ }
1647
+ return endpoint;
1648
+ };
1649
+ var webhookAdminRoutes = (hub) => ({
1650
+ "GET /webhooks": ({ url, namespace }) => json2(200, {
1651
+ deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
1652
+ const type = url.searchParams.get("type");
1653
+ return type === null || d.type === type;
1654
+ })
1655
+ }),
1656
+ "GET /webhooks/events": ({ url, namespace }) => {
1657
+ const type = url.searchParams.get("type");
1658
+ return json2(200, {
1659
+ events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
1660
+ });
1661
+ },
1662
+ "POST /webhooks/:id/replay": async ({ params }) => {
1663
+ const replayed = await hub.replay(params.id);
1664
+ return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1665
+ },
1666
+ "POST /webhooks/flush": async () => {
1667
+ await hub.flush();
1668
+ return json2(200, { status: "ok" });
1669
+ },
1670
+ "POST /webhooks/faults": ({ body, namespace }) => {
1671
+ if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
1672
+ return adminError2(400, "mode must be duplicate, reorder or drop");
1673
+ }
1674
+ const fault = { mode: body.mode };
1675
+ if (typeof body.count === "number")
1676
+ fault.count = body.count;
1677
+ hub.fault(namespace, fault);
1678
+ return json2(201, { namespace, ...fault });
1679
+ },
1680
+ "GET /webhook-endpoints": ({ namespace }) => json2(200, {
1681
+ endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
1682
+ ...rest,
1683
+ secret: secret ? "(set)" : null
1684
+ }))
1685
+ }),
1686
+ "PUT /webhook-endpoints": ({ body, namespace }) => {
1687
+ const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
1688
+ if (!Array.isArray(list))
1689
+ return adminError2(400, "expected [{url, secret?, events?}]");
1690
+ const parsed = [];
1691
+ for (const each of list) {
1692
+ const endpoint = parseEndpoint(each);
1693
+ if (typeof endpoint === "string")
1694
+ return adminError2(400, endpoint);
1695
+ parsed.push(endpoint);
1696
+ }
1697
+ const set = hub.setEndpoints(namespace, parsed);
1698
+ return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
1699
+ },
1700
+ "DELETE /webhook-endpoints": ({ namespace }) => {
1701
+ hub.setEndpoints(namespace, []);
1702
+ return json2(200, { status: "ok" });
1703
+ }
1704
+ });
1705
+ var parsePayload = (message) => {
1706
+ if (message.contentType.startsWith("application/json")) {
1707
+ try {
1708
+ return JSON.parse(message.body);
1709
+ } catch {
1710
+ return message.body;
1711
+ }
1712
+ }
1713
+ if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
1714
+ return Object.fromEntries(new URLSearchParams(message.body));
1715
+ }
1716
+ return message.body;
1717
+ };
1718
+
1719
+ // ../core/dist/runtime.js
1720
+ var MOCKINGBIRD_HEADER = "x-mockingbird";
1721
+ var BRANCH_HEADER = "x-mockingbird-branch";
1722
+ var AT_HEADER = "x-mockingbird-at";
1723
+ var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
1724
+ var DEFAULT_NAMESPACE = "default";
1725
+ var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1726
+ var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
1727
+ var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
1728
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1729
+ var effects = /* @__PURE__ */ new WeakMap();
1730
+ var reuseSorted = (fresh, previous, compare, equal) => {
1731
+ if (!previous || previous.length === 0)
1732
+ return fresh.map((row) => Object.freeze(row));
1733
+ const result = new Array(fresh.length);
1734
+ let unchanged = fresh.length === previous.length;
1735
+ let oldIndex = 0;
1736
+ for (let index = 0; index < fresh.length; index++) {
1737
+ const row = fresh[index];
1738
+ while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
1739
+ oldIndex++;
1740
+ }
1741
+ const old = previous[oldIndex];
1742
+ result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
1743
+ if (result[index] !== previous[index])
1744
+ unchanged = false;
1745
+ }
1746
+ return unchanged ? previous : result;
1747
+ };
1748
+ var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
1749
+ var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
1750
+ var DroppedConnectionError = class extends TypeError {
1751
+ code = "MOCKINGBIRD_DROP";
1752
+ constructor() {
1753
+ super("fetch failed: connection dropped by Mockingbird fault");
1754
+ this.name = "TypeError";
1755
+ }
1756
+ };
1757
+ var operationMatcher = (document2) => {
1758
+ const matchers = listOperations(document2).map((operation) => ({
1759
+ operationId: operation.operationId,
1760
+ method: operation.method.toUpperCase(),
1761
+ pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
1762
+ params: (operation.path.match(/\{/g) ?? []).length
1763
+ })).sort((a, b) => a.params - b.params);
1764
+ return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
1765
+ };
1766
+ var createRuntime = (options) => {
1767
+ const sqlite = bootSqlite(options.sqlite);
1768
+ const clock = options.clock ?? createClock();
1769
+ const rng = createRng(options.seed ?? 0);
1770
+ const wallNow = options.io?.wallNow ?? Date.now;
1771
+ const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
1772
+ const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1773
+ const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
1774
+ const metrics = createMetrics();
1775
+ const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
1776
+ const version = options.version ?? PACKAGE_VERSION;
1777
+ const instances = /* @__PURE__ */ new Map();
1778
+ const publicNamespaces = /* @__PURE__ */ new Set();
1779
+ const branchRngs = /* @__PURE__ */ new Map();
1780
+ const timelines = /* @__PURE__ */ new Map();
1781
+ const branchStorage = /* @__PURE__ */ new Map();
1782
+ const captured = /* @__PURE__ */ new Map();
1783
+ const credentials = createCredentialRegistry();
1784
+ const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
1785
+ const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
1786
+ const instanceFor = (key, publicNamespace = key, isolatedRng) => {
1787
+ const existing = instances.get(key);
1788
+ if (existing)
1789
+ return existing;
1790
+ if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
1791
+ throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
1792
+ }
1793
+ const created = options.create({
1794
+ namespace: storageNamespace(key),
1795
+ publicNamespace,
1796
+ sqlite,
1797
+ clock,
1798
+ rng: isolatedRng ?? rng
1799
+ });
1800
+ instances.set(key, created);
1801
+ publicNamespaces.add(publicNamespace);
1802
+ if (isolatedRng)
1803
+ branchRngs.set(key, isolatedRng);
1804
+ return created;
1805
+ };
1806
+ const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
1807
+ const capture = (storage) => {
1808
+ const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
1809
+ const previous = captured.get(storage);
1810
+ const snapshot2 = {
1811
+ namespace: fresh.namespace,
1812
+ 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),
1813
+ 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)
1814
+ };
1815
+ Object.freeze(snapshot2.records);
1816
+ Object.freeze(snapshot2.sequences);
1817
+ Object.freeze(snapshot2);
1818
+ captured.set(storage, snapshot2);
1819
+ return Object.freeze({
1820
+ snapshot: snapshot2,
1821
+ clock: Object.freeze(clock.state()),
1822
+ rngState: (branchRngs.get(storage) ?? rng).state()
1823
+ });
1824
+ };
1825
+ const timeline = (name = DEFAULT_NAMESPACE) => {
1826
+ let found = timelines.get(name);
1827
+ if (found)
1828
+ return found;
1829
+ instance(name);
1830
+ found = new Timeline({
1831
+ now: clock.now,
1832
+ ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
1833
+ });
1834
+ found.commit(capture(name));
1835
+ timelines.set(name, found);
1836
+ return found;
1837
+ };
1838
+ const physicalBranch = (namespace, branch2) => {
1839
+ if (branch2 === "main")
1840
+ return namespace;
1841
+ const mapKey = `${namespace}\0${branch2}`;
1842
+ const existing = branchStorage.get(mapKey);
1843
+ if (existing)
1844
+ return existing;
1845
+ const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
1846
+ branchStorage.set(mapKey, key);
1847
+ return key;
1848
+ };
1849
+ const ensureBranch = (namespace, branch2, at) => {
1850
+ if (!BRANCH_PATTERN2.test(branch2))
1851
+ throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
1852
+ const history = timeline(namespace);
1853
+ if (branch2 === "main") {
1854
+ if (at !== void 0) {
1855
+ const point = history.checkout("main", at);
1856
+ restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
1857
+ captured.set(namespace, point.value.snapshot);
1858
+ rng.setState(point.value.rngState);
1859
+ clock.set(point.value.clock.now);
1860
+ if (point.value.clock.frozen)
1861
+ clock.freeze();
1862
+ else
1863
+ clock.unfreeze();
1864
+ }
1865
+ return namespace;
1866
+ }
1867
+ const storage = physicalBranch(namespace, branch2);
1868
+ if (!history.hasBranch(branch2)) {
1869
+ if (at === void 0)
1870
+ history.commit(capture(namespace));
1871
+ const point = history.fork(branch2, at === void 0 ? {} : { from: at });
1872
+ const branchRng = createRng(options.seed ?? 0);
1873
+ if (point)
1874
+ branchRng.setState(point.value.rngState);
1875
+ instanceFor(storage, namespace, branchRng);
1876
+ if (point)
1877
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1878
+ if (point)
1879
+ captured.set(storage, point.value.snapshot);
1880
+ } else if (at !== void 0 && history.head(branch2)?.id !== at) {
1881
+ const point = history.checkout(branch2, at);
1882
+ if (!instances.has(storage)) {
1883
+ const branchRng = createRng(options.seed ?? 0);
1884
+ branchRng.setState(point.value.rngState);
1885
+ instanceFor(storage, namespace, branchRng);
1886
+ }
1887
+ branchRngs.get(storage)?.setState(point.value.rngState);
1888
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1889
+ captured.set(storage, point.value.snapshot);
1890
+ } else {
1891
+ if (!instances.has(storage)) {
1892
+ const point = history.head(branch2);
1893
+ const branchRng = createRng(options.seed ?? 0);
1894
+ if (point)
1895
+ branchRng.setState(point.value.rngState);
1896
+ instanceFor(storage, namespace, branchRng);
1897
+ }
1898
+ }
1899
+ return storage;
1900
+ };
1901
+ const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
1902
+ const storage = ensureBranch(namespace, branch2);
1903
+ return timeline(namespace).commit(capture(storage), { branch: branch2 });
1904
+ };
1905
+ const branch = (name, branchOptions = {}) => {
1906
+ const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
1907
+ ensureBranch(namespace, name, branchOptions.at);
1908
+ const head = timeline(namespace).head(name);
1909
+ if (!head)
1910
+ throw new RangeError(`branch ${name} has no checkpoint`);
1911
+ return head;
1912
+ };
1913
+ const checkout = (checkpointId, checkoutOptions = {}) => {
1914
+ const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
1915
+ const branchName = checkoutOptions.branch ?? "main";
1916
+ const history = timeline(namespace);
1917
+ const point = history.checkout(branchName, checkpointId);
1918
+ const storage = ensureBranch(namespace, branchName);
1919
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1920
+ captured.set(storage, point.value.snapshot);
1921
+ clock.set(point.value.clock.now);
1922
+ if (point.value.clock.frozen)
1923
+ clock.freeze();
1924
+ else
1925
+ clock.unfreeze();
1926
+ (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
1927
+ };
1928
+ const reset = async (name = DEFAULT_NAMESPACE) => {
1929
+ if (name === "*") {
1930
+ options.webhooks?.clear();
1931
+ for (const each of instances.values())
1932
+ await each.reset();
1933
+ timelines.clear();
1934
+ branchStorage.clear();
1935
+ branchRngs.clear();
1936
+ captured.clear();
1937
+ return;
1938
+ }
1939
+ options.webhooks?.clear(name);
1940
+ const target = instances.get(name);
1941
+ if (target)
1942
+ await target.reset();
1943
+ else
1944
+ clearNamespace(sqlite, storageNamespace(name));
1945
+ for (const [mapping, storage] of branchStorage) {
1946
+ if (!mapping.startsWith(`${name}\0`))
1947
+ continue;
1948
+ const branchInstance = instances.get(storage);
1949
+ if (branchInstance)
1950
+ await branchInstance.reset();
1951
+ else
1952
+ clearNamespace(sqlite, storageNamespace(storage));
1953
+ branchStorage.delete(mapping);
1954
+ branchRngs.delete(storage);
1955
+ captured.delete(storage);
1956
+ }
1957
+ timelines.delete(name);
1958
+ captured.delete(name);
1959
+ };
1960
+ const snapshot = (name = DEFAULT_NAMESPACE) => {
1961
+ return checkpoint(name, "main").value.snapshot;
1962
+ };
1963
+ const restore = (from, name = DEFAULT_NAMESPACE) => {
1964
+ instance(name);
1965
+ restoreNamespace(sqlite, storageNamespace(name), from);
1966
+ captured.set(name, from);
1967
+ const history = timelines.get(name);
1968
+ if (history)
1969
+ history.commit(capture(name), { branch: "main" });
1970
+ else
1971
+ timeline(name);
1972
+ };
1973
+ const runtime = {
1974
+ name: options.name,
1975
+ sqlite,
1976
+ clock,
1977
+ faults,
1978
+ metrics,
1979
+ journal,
1980
+ rng,
1981
+ credentials,
1982
+ webhooks: options.webhooks,
1983
+ applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
1984
+ const preset = options.presets?.[name];
1985
+ if (!preset)
1986
+ throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
1987
+ const added = (preset.rules ?? []).map((rule, index) => faults.add({
1988
+ namespace,
1989
+ ...rule,
1990
+ ...overrides,
1991
+ preset: name,
1992
+ id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
1993
+ }));
1994
+ if (preset.webhook && options.webhooks) {
1995
+ options.webhooks.fault(namespace, {
1996
+ ...preset.webhook,
1997
+ ...overrides.count !== void 0 ? { count: overrides.count } : {}
1998
+ });
1999
+ }
2000
+ return added;
2001
+ },
2002
+ instance,
2003
+ namespaces: () => [...publicNamespaces].sort(),
2004
+ reset,
2005
+ snapshot,
2006
+ restore,
2007
+ checkpoint,
2008
+ branch,
2009
+ checkout,
2010
+ timeline,
2011
+ fetch: async (incoming) => {
2012
+ let request = incoming;
2013
+ const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
2014
+ if (prefixed) {
2015
+ const url2 = new URL(request.url);
2016
+ url2.pathname = prefixed[2] ?? "/";
2017
+ const headers = new Headers(request.headers);
2018
+ if (!headers.has(NAMESPACE_HEADER)) {
2019
+ headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
2020
+ }
2021
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
2022
+ request = new Request(url2, {
2023
+ method: request.method,
2024
+ headers,
2025
+ ...hasBody ? { body: await request.arrayBuffer() } : {},
2026
+ signal: request.signal
2027
+ });
2028
+ }
2029
+ let namespace = control.namespaceOf(request);
2030
+ if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
2031
+ const credential = options.credential(request);
2032
+ const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
2033
+ if (mapped !== void 0)
2034
+ namespace = mapped;
2035
+ }
2036
+ const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
2037
+ const at = request.headers.get(AT_HEADER) ?? void 0;
2038
+ const stamp = (response2) => {
2039
+ const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
2040
+ try {
2041
+ response2.headers.set(MOCKINGBIRD_HEADER, value);
2042
+ return response2;
2043
+ } catch {
2044
+ const copy = new Response(response2.body, response2);
2045
+ copy.headers.set(MOCKINGBIRD_HEADER, value);
2046
+ return copy;
2047
+ }
2048
+ };
2049
+ const handled = await control.handle(request);
2050
+ if (handled)
2051
+ return stamp(handled);
2052
+ const started = monotonicNow();
2053
+ const url = new URL(request.url);
2054
+ const operationId = operationIdFor(request, url.pathname);
2055
+ const log = (status, faultId, response2) => {
2056
+ const noted = response2 ? responseNotes(response2) : void 0;
2057
+ const entry = {
2058
+ service: options.name,
2059
+ namespace,
2060
+ operationId,
2061
+ method: request.method,
2062
+ path: url.pathname,
2063
+ status,
2064
+ durationMs: Math.round((monotonicNow() - started) * 100) / 100,
2065
+ unmatched: options.document !== void 0 && operationId === void 0,
2066
+ ...faultId !== void 0 ? { faultId } : {},
2067
+ ...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
2068
+ ...noted?.adopted ? { adopted: true } : {}
2069
+ };
2070
+ metrics.record(entry);
2071
+ journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
2072
+ options.onLog?.(entry);
2073
+ };
2074
+ if (!NAMESPACE_PATTERN.test(namespace)) {
2075
+ log(400);
2076
+ return stamp(new Response(JSON.stringify({
2077
+ error: {
2078
+ type: "mockingbird_admin",
2079
+ message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
2080
+ }
2081
+ }), { status: 400, headers: { "content-type": "application/json" } }));
2082
+ }
2083
+ if (!BRANCH_PATTERN2.test(selectedBranch)) {
2084
+ log(400);
2085
+ return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
2086
+ }
2087
+ let storage;
2088
+ try {
2089
+ if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
2090
+ const point = timeline(namespace).get(at);
2091
+ storage = physicalBranch(namespace, `at_${at}`);
2092
+ let viewRng = branchRngs.get(storage);
2093
+ if (!viewRng) {
2094
+ viewRng = createRng(options.seed ?? 0);
2095
+ instanceFor(storage, namespace, viewRng);
2096
+ }
2097
+ viewRng.setState(point.value.rngState);
2098
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
2099
+ captured.set(storage, point.value.snapshot);
2100
+ } else {
2101
+ storage = ensureBranch(namespace, selectedBranch, at);
2102
+ }
2103
+ } catch (error2) {
2104
+ log(409);
2105
+ return stamp(adminFail(409, error2 instanceof Error ? error2.message : String(error2)));
2106
+ }
2107
+ const hits = await faults.take({
2108
+ operationId,
2109
+ method: request.method,
2110
+ path: url.pathname,
2111
+ namespace
2112
+ });
2113
+ const final = hits.find((hit) => hit.drop || hit.response);
2114
+ if (final?.drop) {
2115
+ log(0, final.id);
2116
+ throw new DroppedConnectionError();
2117
+ }
2118
+ if (final?.response) {
2119
+ log(final.response.status, final.id);
2120
+ return stamp(final.response);
2121
+ }
2122
+ const fired = hits.filter((hit) => hit.effect !== void 0);
2123
+ if (fired.length > 0)
2124
+ effects.set(request, fired.map((hit) => hit.effect));
2125
+ let response = await instanceFor(storage, namespace).fetch(request);
2126
+ if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
2127
+ const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
2128
+ response = mutableResponse(response);
2129
+ response.headers.set(CHECKPOINT_HEADER, point.id);
2130
+ }
2131
+ if (selectedBranch !== "main") {
2132
+ response = mutableResponse(response);
2133
+ response.headers.set(BRANCH_HEADER, selectedBranch);
2134
+ }
2135
+ if (at !== void 0) {
2136
+ response = mutableResponse(response);
2137
+ response.headers.set(AT_HEADER, at);
2138
+ }
2139
+ log(response.status, fired[0]?.id, response);
2140
+ return stamp(response);
2141
+ }
2142
+ };
2143
+ const control = createControlPlane({
2144
+ name: options.name,
2145
+ startedAt: wallNow(),
2146
+ wallNow,
2147
+ clock,
2148
+ faults,
2149
+ metrics,
2150
+ journal,
2151
+ defaultNamespace: DEFAULT_NAMESPACE,
2152
+ namespaces: runtime.namespaces,
2153
+ reset,
2154
+ timeTravel: {
2155
+ checkpoint: (name, branchName) => {
2156
+ const point = checkpoint(name, branchName);
2157
+ return {
2158
+ id: point.id,
2159
+ branch: point.branch,
2160
+ parent: point.parent,
2161
+ at: point.at,
2162
+ records: point.value.snapshot.records.length
2163
+ };
2164
+ },
2165
+ branch: (branchName, branchOptions) => {
2166
+ const point = branch(branchName, branchOptions);
2167
+ return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
2168
+ },
2169
+ checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
2170
+ retain: (name, checkpointId) => {
2171
+ timeline(name).retain(checkpointId);
2172
+ },
2173
+ release: (name, checkpointId) => timeline(name).release(checkpointId),
2174
+ inspect: (name) => {
2175
+ const history = timeline(name);
2176
+ return {
2177
+ branches: history.branches(),
2178
+ checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
2179
+ id,
2180
+ branch: branchName,
2181
+ parent,
2182
+ at
2183
+ }))
2184
+ };
2185
+ }
2186
+ },
2187
+ describe: options.describe ?? (() => ({})),
2188
+ ...options.presets ? {
2189
+ applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
2190
+ } : {},
2191
+ routes: {
2192
+ ...credentialRoutes(credentials),
2193
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
2194
+ ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
2195
+ ...options.admin?.(runtime) ?? {}
2196
+ },
2197
+ adminKey: options.adminKey
2198
+ });
2199
+ return runtime;
2200
+ };
2201
+ var mutableResponse = (response) => {
2202
+ try {
2203
+ response.headers.set("x-mockingbird-mutable-probe", "1");
2204
+ response.headers.delete("x-mockingbird-mutable-probe");
2205
+ return response;
2206
+ } catch {
2207
+ return new Response(response.body, response);
2208
+ }
2209
+ };
2210
+ var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2211
+ var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
2212
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2213
+ var credentialRoutes = (registry) => ({
2214
+ "GET /credentials": () => adminJson(200, {
2215
+ credentials: registry.entries().map(({ credential, namespace }) => ({
2216
+ credential: maskCredential(credential),
2217
+ namespace
2218
+ }))
2219
+ }),
2220
+ "PUT /credentials": ({ body, namespace }) => {
2221
+ const pairs = [];
2222
+ const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
2223
+ if (Array.isArray(list)) {
2224
+ for (const each of list) {
2225
+ if (typeof each === "string")
2226
+ pairs.push([each, namespace]);
2227
+ else if (isObject(each) && typeof each.credential === "string") {
2228
+ pairs.push([
2229
+ each.credential,
2230
+ typeof each.namespace === "string" ? each.namespace : namespace
2231
+ ]);
2232
+ } else
2233
+ return adminFail(400, "each entry is a credential string or {credential, namespace}");
2234
+ }
2235
+ } else if (isObject(list)) {
2236
+ for (const [credential, target] of Object.entries(list)) {
2237
+ if (typeof target !== "string")
2238
+ return adminFail(400, `namespace for ${credential} must be a string`);
2239
+ pairs.push([credential, target]);
2240
+ }
2241
+ } else if (isObject(body) && typeof body.credential === "string") {
2242
+ pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
2243
+ } else {
2244
+ return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
2245
+ }
2246
+ for (const [credential, target] of pairs) {
2247
+ if (!NAMESPACE_PATTERN.test(target))
2248
+ return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
2249
+ registry.set(credential, target);
2250
+ }
2251
+ return adminJson(200, { mapped: pairs.length });
2252
+ },
2253
+ "DELETE /credentials": ({ url }) => {
2254
+ const credential = url.searchParams.get("credential");
2255
+ if (credential === null)
2256
+ registry.clear();
2257
+ else
2258
+ registry.remove(credential);
2259
+ return adminJson(200, { status: "ok" });
2260
+ }
2261
+ });
2262
+ var presetRoutes = (presets, runtime) => ({
2263
+ "GET /faults/presets": () => adminJson(200, {
2264
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
2265
+ }),
2266
+ "POST /faults/presets/:name": ({ params, body, namespace }) => {
2267
+ const name = params.name;
2268
+ if (!presets[name])
2269
+ return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
2270
+ const overrides = isObject(body) ? body : {};
2271
+ return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
2272
+ }
2273
+ });
2274
+
2275
+ // src/generated/openapi.ts
2276
+ var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Formbricks client + management API (Mockingbird subset)","description":"Stateful mock subset of open-source Formbricks (6.x): the client environment state (surveys)\\nand response creation the JS SDK uses, the v1 management API for responses and surveys\\n(\`x-api-key\`), and the widget script the web SDK loads. Errors use Formbricks' v1\\n\`{code, message, details}\` shape (\`apps/web/app/lib/api/response.ts\`). Client routes take a\\nworkspace id or a legacy (pre-5) environment id in the path.\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from upstream Formbricks 6.0.0 (https://github.com/formbricks/formbricks): apps/web/app/api/v1/client/[workspaceId]/environment, apps/web/app/api/v2/client/[workspaceId]/responses, apps/web/app/api/v1/management/{responses,surveys}, and the response pipeline's webhook body (apps/web/modules/response-pipeline). Docs: https://formbricks.com/docs"}},"servers":[{"url":"https://app.formbricks.com"}],"paths":{"/api/v1/client/{workspaceId}/environment":{"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"get":{"operationId":"GetEnvironmentState","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The environment state the SDK caches (surveys, action classes, project)","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["data","expiresAt"],"properties":{"data":{"type":"object","required":["surveys","actionClasses","project"],"properties":{"surveys":{"type":"array","items":{"$ref":"#/components/schemas/Survey"}},"actionClasses":{"type":"array","items":{"type":"object"}},"project":{"type":"object"}}},"expiresAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}}}}}}},"400":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/v2/client/{workspaceId}/responses":{"parameters":[{"$ref":"#/components/parameters/WorkspaceId"}],"post":{"operationId":"CreateClientResponse","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["surveyId","finished","data"],"properties":{"surveyId":{"type":"string","x-mockingbird-resource-ref":{"type":"survey","missing":"czzzzzzzzzzzzzzzzzzzzzzzz"}},"finished":{"type":"boolean"},"data":{"$ref":"#/components/schemas/ResponseData"},"contactId":{"type":["string","null"]},"displayId":{"type":["string","null"]},"singleUseId":{"type":["string","null"]},"endingId":{"type":["string","null"]},"language":{"type":"string","maxLength":10},"variables":{"type":"object"},"ttc":{"type":"object","additionalProperties":{"type":"number"}},"meta":{"type":"object","properties":{"source":{"type":"string"},"url":{"type":"string"},"action":{"type":"string"},"pagePath":{"type":"string"},"utmSource":{"type":"string"}}}}}}}},"responses":{"200":{"description":"The created response's id","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"response","identity":true}},"quotaFull":{"type":"boolean"}}}}}}}},"400":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/v1/management/responses":{"get":{"operationId":"ListResponses","security":[{"apiKey":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"surveyId","in":"query","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"survey","missing":"czzzzzzzzzzzzzzzzzzzzzzzz"}}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"skip","in":"query","schema":{"type":"integer","minimum":0,"maximum":100}}],"responses":{"200":{"description":"Responses, newest first","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Response"}}}}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/v1/management/responses/{responseId}":{"parameters":[{"name":"responseId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"response","missing":"czzzzzzzzzzzzzzzzzzzzzzzy"}}}],"get":{"operationId":"GetResponse","security":[{"apiKey":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"One response","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Response"}}}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/v1/management/surveys":{"get":{"operationId":"ListSurveys","security":[{"apiKey":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":100}}],"responses":{"200":{"description":"Surveys, most recently updated first","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Survey"}}}}}}},"401":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"CreateSurvey","security":[{"apiKey":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["workspaceId","name","blocks"],"properties":{"workspaceId":{"description":"A workspace id, or (as \`environmentId\`) a legacy environment id.","type":"string","enum":["cworkspace000000000000001","cenvironment0000000000001"]},"name":{"type":"string","minLength":1,"maxLength":80},"type":{"type":"string","enum":["app","link"]},"status":{"type":"string","enum":["draft","inProgress","paused","completed"]},"blocks":{"description":"The survey's blocks of elements. The legacy \`questions\` array (one element per block) is also accepted, but not together with \`blocks\`.","type":"array","minItems":1,"maxItems":3,"items":{"type":"object","required":["id","elements"],"properties":{"id":{"type":"string","minLength":1,"maxLength":30},"name":{"type":"string","maxLength":30},"elements":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/Element"}}}}}}}}}},"responses":{"200":{"description":"The created survey","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Survey"}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/v1/management/surveys/{surveyId}":{"parameters":[{"name":"surveyId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"survey","missing":"czzzzzzzzzzzzzzzzzzzzzzzz"}}}],"get":{"operationId":"GetSurvey","security":[{"apiKey":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"One survey","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Survey"}}}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/js/formbricks.umd.cjs":{"get":{"operationId":"GetWidgetScript","description":"The widget bundle the web SDK loads; served as a no-op script.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"A no-op script defining \`window.formbricks\`","content":{"application/javascript":{"schema":{"type":"string"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"parameters":{"WorkspaceId":{"name":"workspaceId","in":"path","required":true,"description":"A workspace id, or a legacy (pre-Formbricks-5) environment id.","schema":{"type":"string","enum":["cworkspace000000000000001","cenvironment0000000000001"]}}},"responses":{"Error":{"description":"Formbricks' v1 \`{code, message, details}\` error","content":{"application/json":{"schema":{"type":"object","required":["code","message","details"],"properties":{"code":{"type":"string","enum":["not_found","gone","bad_request","internal_server_error","unauthorized","method_not_allowed","not_authenticated","forbidden","too_many_requests","conflict","payload_too_large"]},"message":{"type":"string"},"details":{"type":"object"}}}}}}},"schemas":{"ResponseData":{"type":"object","maxProperties":5,"additionalProperties":{"anyOf":[{"type":"string","maxLength":60},{"type":"number"},{"type":"array","maxItems":3,"items":{"type":"string","maxLength":30}},{"type":"object","maxProperties":3,"additionalProperties":{"type":"string","maxLength":30}}]}},"Element":{"type":"object","required":["id","type","headline"],"properties":{"id":{"type":"string","minLength":1,"maxLength":30},"type":{"type":"string","enum":["openText","multipleChoiceSingle","multipleChoiceMulti","nps","rating","date","cta","consent"]},"headline":{"type":"object","required":["default"],"properties":{"default":{"type":"string","maxLength":80}}},"required":{"type":"boolean"},"inputType":{"type":"string","enum":["text","email","url","number","phone"]}}},"Survey":{"type":"object","required":["id","name","type","status"],"properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"survey","identity":true}},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"workspaceId":{"type":"string"},"environmentId":{"description":"The legacy environment id of the survey's workspace (v1 compatibility).","type":"string"},"questions":{"type":["array","object"]},"blocks":{"type":"array"},"createdAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updatedAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"Response":{"type":"object","required":["id","createdAt","updatedAt","surveyId","finished","data"],"properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"response","identity":true}},"createdAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updatedAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"surveyId":{"type":"string"},"displayId":{"type":["string","null"]},"contact":{"type":["object","null"],"properties":{"id":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"userId":{"type":"string"}}},"contactAttributes":{"type":["object","null"]},"finished":{"type":"boolean"},"endingId":{"type":["string","null"]},"data":{"description":"An object, or (for some deployments) the same object as a JSON string.","type":["object","string"]},"variables":{"type":"object"},"ttc":{"type":"object"},"tags":{"type":"array"},"meta":{"type":"object"},"singleUseId":{"type":["string","null"]},"language":{"type":["string","null"]}}}}}}`);
2277
+ var operationIds = ["GetEnvironmentState", "CreateClientResponse", "ListResponses", "GetResponse", "ListSurveys", "CreateSurvey", "GetSurvey", "GetWidgetScript"];
2278
+ var supportedOperationIds = ["GetEnvironmentState", "CreateClientResponse", "ListResponses", "GetResponse", "ListSurveys", "CreateSurvey", "GetSurvey", "GetWidgetScript"];
2279
+
2280
+ // src/corpus/surveys.json
2281
+ var surveys_default = {
2282
+ note: "Synthetic survey corpus for the Formbricks mock: invented surveys in the Formbricks 6 survey shape (blocks of elements). No real data.",
2283
+ surveys: [
2284
+ {
2285
+ id: "csurveynps000000000000001",
2286
+ createdAt: "2026-01-05T09:00:00.000Z",
2287
+ updatedAt: "2026-01-05T09:00:00.000Z",
2288
+ name: "Net Promoter Score (NPS)",
2289
+ type: "app",
2290
+ status: "inProgress",
2291
+ welcomeCard: {
2292
+ enabled: false,
2293
+ headline: {
2294
+ default: "Welcome!"
2295
+ },
2296
+ buttonLabel: {
2297
+ default: "Next"
2298
+ },
2299
+ timeToFinish: false,
2300
+ showResponseCount: false
2301
+ },
2302
+ questions: [],
2303
+ blocks: [
2304
+ {
2305
+ id: "npsblockscore0000000000001",
2306
+ name: "Block 1",
2307
+ elements: [
2308
+ {
2309
+ id: "nps_score",
2310
+ type: "nps",
2311
+ headline: {
2312
+ default: "How likely are you to recommend Example App to a friend or colleague?"
2313
+ },
2314
+ required: true,
2315
+ lowerLabel: {
2316
+ default: "Not likely"
2317
+ },
2318
+ upperLabel: {
2319
+ default: "Very likely"
2320
+ },
2321
+ isColorCodingEnabled: true
2322
+ }
2323
+ ],
2324
+ buttonLabel: {
2325
+ default: ""
2326
+ },
2327
+ backButtonLabel: {
2328
+ default: ""
2329
+ }
2330
+ },
2331
+ {
2332
+ id: "npsblockreason000000000001",
2333
+ name: "Block 2",
2334
+ elements: [
2335
+ {
2336
+ id: "nps_reason",
2337
+ type: "openText",
2338
+ headline: {
2339
+ default: "What is the main reason for your score?"
2340
+ },
2341
+ required: false,
2342
+ inputType: "text",
2343
+ charLimit: {
2344
+ enabled: false
2345
+ }
2346
+ }
2347
+ ],
2348
+ buttonLabel: {
2349
+ default: ""
2350
+ },
2351
+ backButtonLabel: {
2352
+ default: ""
2353
+ }
2354
+ },
2355
+ {
2356
+ id: "npsblockvalue0000000000001",
2357
+ name: "Block 3",
2358
+ elements: [
2359
+ {
2360
+ id: "nps_value",
2361
+ type: "multipleChoiceMulti",
2362
+ headline: {
2363
+ default: "Which parts of Example App do you use most?"
2364
+ },
2365
+ required: false,
2366
+ shuffleOption: "none",
2367
+ choices: [
2368
+ {
2369
+ id: "dashboards",
2370
+ label: {
2371
+ default: "Dashboards"
2372
+ }
2373
+ },
2374
+ {
2375
+ id: "reports",
2376
+ label: {
2377
+ default: "Reports"
2378
+ }
2379
+ },
2380
+ {
2381
+ id: "integrations",
2382
+ label: {
2383
+ default: "Integrations"
2384
+ }
2385
+ },
2386
+ {
2387
+ id: "mobile",
2388
+ label: {
2389
+ default: "Mobile app"
2390
+ }
2391
+ }
2392
+ ]
2393
+ }
2394
+ ],
2395
+ buttonLabel: {
2396
+ default: "Finish"
2397
+ },
2398
+ backButtonLabel: {
2399
+ default: ""
2400
+ }
2401
+ }
2402
+ ],
2403
+ endings: [
2404
+ {
2405
+ id: "csurveynps000000000000end",
2406
+ type: "endScreen",
2407
+ headline: {
2408
+ default: "Thank you!"
2409
+ },
2410
+ subheader: {
2411
+ default: "We appreciate your feedback."
2412
+ }
2413
+ }
2414
+ ],
2415
+ hiddenFields: {
2416
+ enabled: true,
2417
+ fieldIds: [
2418
+ "accountId"
2419
+ ]
2420
+ },
2421
+ variables: [],
2422
+ languages: [],
2423
+ showLanguageSwitch: null,
2424
+ autoClose: null,
2425
+ delay: 0,
2426
+ displayOption: "displayOnce",
2427
+ displayLimit: null,
2428
+ displayPercentage: null,
2429
+ recontactDays: null,
2430
+ segment: null,
2431
+ triggers: [
2432
+ {
2433
+ actionClass: {
2434
+ name: "New Session"
2435
+ }
2436
+ }
2437
+ ],
2438
+ styling: null,
2439
+ workspaceOverwrites: null,
2440
+ isBackButtonHidden: false,
2441
+ recaptcha: {
2442
+ enabled: false,
2443
+ threshold: 0.1
2444
+ }
2445
+ },
2446
+ {
2447
+ id: "csurveyonboarding00000001",
2448
+ createdAt: "2026-01-12T09:00:00.000Z",
2449
+ updatedAt: "2026-01-12T09:00:00.000Z",
2450
+ name: "Onboarding",
2451
+ type: "app",
2452
+ status: "inProgress",
2453
+ welcomeCard: {
2454
+ enabled: false,
2455
+ headline: {
2456
+ default: "Welcome!"
2457
+ },
2458
+ buttonLabel: {
2459
+ default: "Next"
2460
+ },
2461
+ timeToFinish: false,
2462
+ showResponseCount: false
2463
+ },
2464
+ questions: [],
2465
+ blocks: [
2466
+ {
2467
+ id: "onbblockname0000000000001",
2468
+ name: "Block 1",
2469
+ elements: [
2470
+ {
2471
+ id: "first_name",
2472
+ type: "openText",
2473
+ headline: {
2474
+ default: "First name"
2475
+ },
2476
+ required: true,
2477
+ inputType: "text",
2478
+ charLimit: {
2479
+ enabled: false
2480
+ }
2481
+ },
2482
+ {
2483
+ id: "last_name",
2484
+ type: "openText",
2485
+ headline: {
2486
+ default: "Last name"
2487
+ },
2488
+ required: true,
2489
+ inputType: "text",
2490
+ charLimit: {
2491
+ enabled: false
2492
+ }
2493
+ },
2494
+ {
2495
+ id: "work_email",
2496
+ type: "openText",
2497
+ headline: {
2498
+ default: "Work email"
2499
+ },
2500
+ required: true,
2501
+ inputType: "email",
2502
+ charLimit: {
2503
+ enabled: false
2504
+ }
2505
+ },
2506
+ {
2507
+ id: "phone",
2508
+ type: "openText",
2509
+ headline: {
2510
+ default: "Phone number"
2511
+ },
2512
+ required: false,
2513
+ inputType: "phone",
2514
+ charLimit: {
2515
+ enabled: false
2516
+ }
2517
+ }
2518
+ ],
2519
+ buttonLabel: {
2520
+ default: ""
2521
+ },
2522
+ backButtonLabel: {
2523
+ default: ""
2524
+ }
2525
+ },
2526
+ {
2527
+ id: "onbblockrole0000000000001",
2528
+ name: "Block 2",
2529
+ elements: [
2530
+ {
2531
+ id: "role",
2532
+ type: "multipleChoiceSingle",
2533
+ headline: {
2534
+ default: "What best describes your role?"
2535
+ },
2536
+ required: true,
2537
+ shuffleOption: "none",
2538
+ choices: [
2539
+ {
2540
+ id: "engineering",
2541
+ label: {
2542
+ default: "Engineering"
2543
+ }
2544
+ },
2545
+ {
2546
+ id: "product",
2547
+ label: {
2548
+ default: "Product"
2549
+ }
2550
+ },
2551
+ {
2552
+ id: "design",
2553
+ label: {
2554
+ default: "Design"
2555
+ }
2556
+ },
2557
+ {
2558
+ id: "other",
2559
+ label: {
2560
+ default: "Other"
2561
+ }
2562
+ }
2563
+ ]
2564
+ },
2565
+ {
2566
+ id: "team_size",
2567
+ type: "multipleChoiceSingle",
2568
+ headline: {
2569
+ default: "How big is your team?"
2570
+ },
2571
+ required: false,
2572
+ shuffleOption: "none",
2573
+ choices: [
2574
+ {
2575
+ id: "solo",
2576
+ label: {
2577
+ default: "Just me"
2578
+ }
2579
+ },
2580
+ {
2581
+ id: "small",
2582
+ label: {
2583
+ default: "2-10"
2584
+ }
2585
+ },
2586
+ {
2587
+ id: "medium",
2588
+ label: {
2589
+ default: "11-50"
2590
+ }
2591
+ },
2592
+ {
2593
+ id: "large",
2594
+ label: {
2595
+ default: "51+"
2596
+ }
2597
+ }
2598
+ ]
2599
+ },
2600
+ {
2601
+ id: "website",
2602
+ type: "openText",
2603
+ headline: {
2604
+ default: "Company website"
2605
+ },
2606
+ required: false,
2607
+ inputType: "url",
2608
+ charLimit: {
2609
+ enabled: false
2610
+ }
2611
+ }
2612
+ ],
2613
+ buttonLabel: {
2614
+ default: "Finish"
2615
+ },
2616
+ backButtonLabel: {
2617
+ default: ""
2618
+ }
2619
+ }
2620
+ ],
2621
+ endings: [
2622
+ {
2623
+ id: "csurveyonboarding00000end",
2624
+ type: "endScreen",
2625
+ headline: {
2626
+ default: "Thank you!"
2627
+ },
2628
+ subheader: {
2629
+ default: "We appreciate your feedback."
2630
+ }
2631
+ }
2632
+ ],
2633
+ hiddenFields: {
2634
+ enabled: true,
2635
+ fieldIds: [
2636
+ "accountId"
2637
+ ]
2638
+ },
2639
+ variables: [],
2640
+ languages: [],
2641
+ showLanguageSwitch: null,
2642
+ autoClose: null,
2643
+ delay: 0,
2644
+ displayOption: "displayOnce",
2645
+ displayLimit: null,
2646
+ displayPercentage: null,
2647
+ recontactDays: null,
2648
+ segment: null,
2649
+ triggers: [
2650
+ {
2651
+ actionClass: {
2652
+ name: "New Session"
2653
+ }
2654
+ }
2655
+ ],
2656
+ styling: null,
2657
+ workspaceOverwrites: null,
2658
+ isBackButtonHidden: false,
2659
+ recaptcha: {
2660
+ enabled: false,
2661
+ threshold: 0.1
2662
+ }
2663
+ },
2664
+ {
2665
+ id: "csurveyfeedback0000000001",
2666
+ createdAt: "2026-01-19T09:00:00.000Z",
2667
+ updatedAt: "2026-01-19T09:00:00.000Z",
2668
+ name: "Product Feedback",
2669
+ type: "app",
2670
+ status: "inProgress",
2671
+ welcomeCard: {
2672
+ enabled: false,
2673
+ headline: {
2674
+ default: "Welcome!"
2675
+ },
2676
+ buttonLabel: {
2677
+ default: "Next"
2678
+ },
2679
+ timeToFinish: false,
2680
+ showResponseCount: false
2681
+ },
2682
+ questions: [],
2683
+ blocks: [
2684
+ {
2685
+ id: "fbkblockrating00000000001",
2686
+ name: "Block 1",
2687
+ elements: [
2688
+ {
2689
+ id: "satisfaction",
2690
+ type: "rating",
2691
+ headline: {
2692
+ default: "How satisfied are you with Example App?"
2693
+ },
2694
+ required: true,
2695
+ scale: "star",
2696
+ range: 5,
2697
+ lowerLabel: {
2698
+ default: "Not satisfied"
2699
+ },
2700
+ upperLabel: {
2701
+ default: "Very satisfied"
2702
+ },
2703
+ isColorCodingEnabled: false
2704
+ }
2705
+ ],
2706
+ buttonLabel: {
2707
+ default: ""
2708
+ },
2709
+ backButtonLabel: {
2710
+ default: ""
2711
+ }
2712
+ },
2713
+ {
2714
+ id: "fbkblockideas000000000001",
2715
+ name: "Block 2",
2716
+ elements: [
2717
+ {
2718
+ id: "improvement",
2719
+ type: "openText",
2720
+ headline: {
2721
+ default: "What should we improve next?"
2722
+ },
2723
+ required: false,
2724
+ inputType: "text",
2725
+ charLimit: {
2726
+ enabled: false
2727
+ }
2728
+ }
2729
+ ],
2730
+ buttonLabel: {
2731
+ default: ""
2732
+ },
2733
+ backButtonLabel: {
2734
+ default: ""
2735
+ }
2736
+ },
2737
+ {
2738
+ id: "fbkblockconsent0000000001",
2739
+ name: "Block 3",
2740
+ elements: [
2741
+ {
2742
+ id: "follow_up",
2743
+ type: "consent",
2744
+ headline: {
2745
+ default: "May we contact you about your feedback?"
2746
+ },
2747
+ label: {
2748
+ default: "Yes, you may contact me"
2749
+ },
2750
+ required: false
2751
+ }
2752
+ ],
2753
+ buttonLabel: {
2754
+ default: "Finish"
2755
+ },
2756
+ backButtonLabel: {
2757
+ default: ""
2758
+ }
2759
+ }
2760
+ ],
2761
+ endings: [
2762
+ {
2763
+ id: "csurveyfeedback0000000end",
2764
+ type: "endScreen",
2765
+ headline: {
2766
+ default: "Thank you!"
2767
+ },
2768
+ subheader: {
2769
+ default: "We appreciate your feedback."
2770
+ }
2771
+ }
2772
+ ],
2773
+ hiddenFields: {
2774
+ enabled: true,
2775
+ fieldIds: [
2776
+ "accountId"
2777
+ ]
2778
+ },
2779
+ variables: [],
2780
+ languages: [],
2781
+ showLanguageSwitch: null,
2782
+ autoClose: null,
2783
+ delay: 0,
2784
+ displayOption: "displayOnce",
2785
+ displayLimit: null,
2786
+ displayPercentage: null,
2787
+ recontactDays: null,
2788
+ segment: null,
2789
+ triggers: [
2790
+ {
2791
+ actionClass: {
2792
+ name: "New Session"
2793
+ }
2794
+ }
2795
+ ],
2796
+ styling: null,
2797
+ workspaceOverwrites: null,
2798
+ isBackButtonHidden: false,
2799
+ recaptcha: {
2800
+ enabled: false,
2801
+ threshold: 0.1
2802
+ }
2803
+ },
2804
+ {
2805
+ id: "csurveyevent0000000000001",
2806
+ createdAt: "2026-01-26T09:00:00.000Z",
2807
+ updatedAt: "2026-01-26T09:00:00.000Z",
2808
+ name: "Event Registration",
2809
+ type: "link",
2810
+ status: "inProgress",
2811
+ welcomeCard: {
2812
+ enabled: false,
2813
+ headline: {
2814
+ default: "Welcome!"
2815
+ },
2816
+ buttonLabel: {
2817
+ default: "Next"
2818
+ },
2819
+ timeToFinish: false,
2820
+ showResponseCount: false
2821
+ },
2822
+ questions: [],
2823
+ blocks: [
2824
+ {
2825
+ id: "evtblockattend00000000001",
2826
+ name: "Block 1",
2827
+ elements: [
2828
+ {
2829
+ id: "attendee_email",
2830
+ type: "openText",
2831
+ headline: {
2832
+ default: "Email"
2833
+ },
2834
+ required: true,
2835
+ inputType: "email",
2836
+ charLimit: {
2837
+ enabled: false
2838
+ }
2839
+ },
2840
+ {
2841
+ id: "session",
2842
+ type: "multipleChoiceMulti",
2843
+ headline: {
2844
+ default: "Which sessions will you attend?"
2845
+ },
2846
+ required: true,
2847
+ shuffleOption: "none",
2848
+ choices: [
2849
+ {
2850
+ id: "keynote",
2851
+ label: {
2852
+ default: "Keynote"
2853
+ }
2854
+ },
2855
+ {
2856
+ id: "workshop",
2857
+ label: {
2858
+ default: "Workshop"
2859
+ }
2860
+ },
2861
+ {
2862
+ id: "social",
2863
+ label: {
2864
+ default: "Evening social"
2865
+ }
2866
+ }
2867
+ ]
2868
+ }
2869
+ ],
2870
+ buttonLabel: {
2871
+ default: "Register"
2872
+ },
2873
+ backButtonLabel: {
2874
+ default: ""
2875
+ }
2876
+ }
2877
+ ],
2878
+ endings: [
2879
+ {
2880
+ id: "csurveyevent0000000000end",
2881
+ type: "endScreen",
2882
+ headline: {
2883
+ default: "Thank you!"
2884
+ },
2885
+ subheader: {
2886
+ default: "We appreciate your feedback."
2887
+ }
2888
+ }
2889
+ ],
2890
+ hiddenFields: {
2891
+ enabled: true,
2892
+ fieldIds: []
2893
+ },
2894
+ variables: [],
2895
+ languages: [],
2896
+ showLanguageSwitch: null,
2897
+ autoClose: null,
2898
+ delay: 0,
2899
+ displayOption: "displayOnce",
2900
+ displayLimit: null,
2901
+ displayPercentage: null,
2902
+ recontactDays: null,
2903
+ segment: null,
2904
+ triggers: [],
2905
+ styling: null,
2906
+ workspaceOverwrites: null,
2907
+ isBackButtonHidden: false,
2908
+ recaptcha: {
2909
+ enabled: false,
2910
+ threshold: 0.1
2911
+ }
2912
+ },
2913
+ {
2914
+ id: "csurveyexit00000000000001",
2915
+ createdAt: "2026-02-02T09:00:00.000Z",
2916
+ updatedAt: "2026-02-02T09:00:00.000Z",
2917
+ name: "Exit Survey",
2918
+ type: "app",
2919
+ status: "paused",
2920
+ welcomeCard: {
2921
+ enabled: false,
2922
+ headline: {
2923
+ default: "Welcome!"
2924
+ },
2925
+ buttonLabel: {
2926
+ default: "Next"
2927
+ },
2928
+ timeToFinish: false,
2929
+ showResponseCount: false
2930
+ },
2931
+ questions: [],
2932
+ blocks: [
2933
+ {
2934
+ id: "extblockreason00000000001",
2935
+ name: "Block 1",
2936
+ elements: [
2937
+ {
2938
+ id: "exit_reason",
2939
+ type: "openText",
2940
+ headline: {
2941
+ default: "Why are you leaving?"
2942
+ },
2943
+ required: true,
2944
+ inputType: "text",
2945
+ charLimit: {
2946
+ enabled: false
2947
+ }
2948
+ }
2949
+ ],
2950
+ buttonLabel: {
2951
+ default: "Finish"
2952
+ },
2953
+ backButtonLabel: {
2954
+ default: ""
2955
+ }
2956
+ }
2957
+ ],
2958
+ endings: [
2959
+ {
2960
+ id: "csurveyexit00000000000end",
2961
+ type: "endScreen",
2962
+ headline: {
2963
+ default: "Thank you!"
2964
+ },
2965
+ subheader: {
2966
+ default: "We appreciate your feedback."
2967
+ }
2968
+ }
2969
+ ],
2970
+ hiddenFields: {
2971
+ enabled: true,
2972
+ fieldIds: [
2973
+ "accountId"
2974
+ ]
2975
+ },
2976
+ variables: [],
2977
+ languages: [],
2978
+ showLanguageSwitch: null,
2979
+ autoClose: null,
2980
+ delay: 0,
2981
+ displayOption: "displayOnce",
2982
+ displayLimit: null,
2983
+ displayPercentage: null,
2984
+ recontactDays: null,
2985
+ segment: null,
2986
+ triggers: [
2987
+ {
2988
+ actionClass: {
2989
+ name: "New Session"
2990
+ }
2991
+ }
2992
+ ],
2993
+ styling: null,
2994
+ workspaceOverwrites: null,
2995
+ isBackButtonHidden: false,
2996
+ recaptcha: {
2997
+ enabled: false,
2998
+ threshold: 0.1
2999
+ }
3000
+ }
3001
+ ]
3002
+ };
3003
+
3004
+ // src/state.ts
3005
+ var WORKSPACE_ID = "cworkspace000000000000001";
3006
+ var ENVIRONMENT_ID = "cenvironment0000000000001";
3007
+ var DEFAULT_SETTINGS = {
3008
+ workspaces: [WORKSPACE_ID],
3009
+ legacyEnvironmentIds: { [ENVIRONMENT_ID]: WORKSPACE_ID },
3010
+ apiKeys: [],
3011
+ webhookId: "cwebhook00000000000000001",
3012
+ contactsEnabled: false
3013
+ };
3014
+ var CORPUS_SURVEYS = surveys_default.surveys;
3015
+ var workspaceSettings = (workspaceId) => ({
3016
+ id: workspaceId,
3017
+ recontactDays: 7,
3018
+ clickOutsideClose: true,
3019
+ overlay: "none",
3020
+ placement: "bottomRight",
3021
+ inAppSurveyBranding: true,
3022
+ styling: { allowStyleOverwrite: true }
3023
+ });
3024
+ var FormbricksState = class {
3025
+ constructor(sqlite, namespace, seed) {
3026
+ this.seed = seed;
3027
+ this.surveys = new Collection(sqlite, namespace, "surveys");
3028
+ this.responses = new Collection(sqlite, namespace, "responses");
3029
+ this.contacts = new Collection(sqlite, namespace, "contacts");
3030
+ this.settings = new Collection(sqlite, namespace, "settings");
3031
+ this.ids = new IdSequence(sqlite, namespace, "formbricks");
3032
+ this.ensureSeeded();
3033
+ }
3034
+ seed;
3035
+ surveys;
3036
+ responses;
3037
+ contacts;
3038
+ settings;
3039
+ ids;
3040
+ ensureSeeded() {
3041
+ if (this.surveys.count() === 0) {
3042
+ for (const survey of this.seed.surveys) {
3043
+ this.surveys.insert(survey.id, { ...survey, workspaceId: survey.workspaceId ?? null });
3044
+ }
3045
+ }
3046
+ if (!this.settings.has("settings")) {
3047
+ this.settings.insert("settings", { ...DEFAULT_SETTINGS, ...this.seed.settings });
3048
+ }
3049
+ }
3050
+ current() {
3051
+ return this.settings.get("settings") ?? DEFAULT_SETTINGS;
3052
+ }
3053
+ update(patch) {
3054
+ const next = { ...this.current(), ...patch };
3055
+ this.settings.insert("settings", next);
3056
+ return next;
3057
+ }
3058
+ /** Formbricks ids are cuid2: lower-case alphanumerics starting with a letter, 25 long. */
3059
+ nextId() {
3060
+ return `c${this.ids.next("", 24).toLowerCase()}`;
3061
+ }
3062
+ /**
3063
+ * `resolveClientApiIds`: a workspace id, or a legacy environment id, to its workspace id
3064
+ * (`undefined` when neither is known).
3065
+ */
3066
+ resolveWorkspace(id) {
3067
+ const settings = this.current();
3068
+ if (settings.workspaces.includes(id)) return id;
3069
+ const aliased = settings.legacyEnvironmentIds[id];
3070
+ if (aliased !== void 0) return aliased;
3071
+ return this.allSurveys().some((s) => s.workspaceId === id) ? id : void 0;
3072
+ }
3073
+ /** The v1 `environmentId` of a workspace: its legacy environment id, else its own id. */
3074
+ legacyEnvironmentId(workspaceId) {
3075
+ const entry = Object.entries(this.current().legacyEnvironmentIds).find(
3076
+ ([, target]) => target === workspaceId
3077
+ );
3078
+ return entry?.[0] ?? workspaceId;
3079
+ }
3080
+ allSurveys() {
3081
+ return this.surveys.list({ order: "oldest" }).map((row) => row.value);
3082
+ }
3083
+ /** Surveys a workspace serves: its own, plus the shared corpus when it is configured. */
3084
+ surveysOf(workspaceId) {
3085
+ return this.allSurveys().filter((s) => this.belongsTo(s, workspaceId));
3086
+ }
3087
+ /** Whether a survey belongs to a workspace (`survey.workspaceId !== workspaceId` check). */
3088
+ belongsTo(survey, workspaceId) {
3089
+ return (survey.workspaceId ?? null) === null ? this.current().workspaces.includes(workspaceId) : survey.workspaceId === workspaceId;
3090
+ }
3091
+ /** `getContact(contactId, workspaceId)`: a contact of that workspace (or a shared one). */
3092
+ contactOf(contactId, workspaceId) {
3093
+ const contact = this.contacts.get(contactId);
3094
+ if (!contact) return void 0;
3095
+ return contact.workspaceId === null || contact.workspaceId === workspaceId ? contact : void 0;
3096
+ }
3097
+ };
3098
+
3099
+ // src/validation.ts
3100
+ var isEmpty = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0 || typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
3101
+ var EMAIL = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
3102
+ var PHONE = /^[\d+][\d+\- ]*\d$/;
3103
+ var validUrl = (value) => {
3104
+ try {
3105
+ new URL(value);
3106
+ return true;
3107
+ } catch {
3108
+ return false;
3109
+ }
3110
+ };
3111
+ var REQUIRED = "Please fill out this field";
3112
+ var INVALID_FORMAT = "Please enter a valid format";
3113
+ var surveyElements = (survey) => {
3114
+ const blocks = Array.isArray(survey.blocks) ? survey.blocks : [];
3115
+ const fromBlocks = blocks.flatMap(
3116
+ (b) => b && Array.isArray(b.elements) ? b.elements : []
3117
+ );
3118
+ const elements = fromBlocks.length > 0 ? fromBlocks : Array.isArray(survey.questions) ? survey.questions : [];
3119
+ return elements.filter((e) => typeof e === "object" && e !== null);
3120
+ };
3121
+ var requiredError = (element, value) => {
3122
+ if (!element.required || element.type === "cta") return false;
3123
+ if (element.type === "ranking") return !Array.isArray(value) || value.length < 1;
3124
+ if (element.type === "matrix") {
3125
+ if (isEmpty(value)) return true;
3126
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && element.rows) {
3127
+ return !Object.values(value).some((v) => v !== "" && v !== null && v !== void 0);
3128
+ }
3129
+ return false;
3130
+ }
3131
+ return isEmpty(value);
3132
+ };
3133
+ var invalidOption = (element, value, language) => {
3134
+ if (element.type !== "multipleChoiceSingle" && element.type !== "multipleChoiceMulti")
3135
+ return false;
3136
+ if (!Array.isArray(element.choices)) return false;
3137
+ if (element.choices.some((c) => c.id === "other") || isEmpty(value)) return false;
3138
+ const known = /* @__PURE__ */ new Set();
3139
+ for (const choice of element.choices) {
3140
+ known.add(choice.id);
3141
+ const label = choice.label?.[language] ?? choice.label?.default;
3142
+ if (label) known.add(label);
3143
+ }
3144
+ const submitted = Array.isArray(value) ? value : [value];
3145
+ return submitted.some((v) => v !== "" && (typeof v !== "string" || !known.has(v)));
3146
+ };
3147
+ var validateResponseData = (survey, data, language = "en") => {
3148
+ const present = surveyElements(survey).filter((e) => Object.keys(data).includes(e.id));
3149
+ const errors = {};
3150
+ for (const element of present) {
3151
+ const value = data[element.id];
3152
+ const messages = [];
3153
+ if (requiredError(element, value)) messages.push(REQUIRED);
3154
+ if (invalidOption(element, value, language)) messages.push(INVALID_FORMAT);
3155
+ if (element.type === "openText" && typeof value === "string" && value !== "") {
3156
+ if (element.inputType === "email" && !EMAIL.test(value)) {
3157
+ messages.push("Please enter a valid email address");
3158
+ } else if (element.inputType === "url" && !validUrl(value)) {
3159
+ messages.push("Please enter a valid URL");
3160
+ } else if (element.inputType === "phone" && !PHONE.test(value)) {
3161
+ messages.push("Please enter a valid phone number");
3162
+ }
3163
+ }
3164
+ if (messages.length > 0) errors[element.id] = messages;
3165
+ }
3166
+ return Object.keys(errors).length === 0 ? null : errors;
3167
+ };
3168
+
3169
+ // src/runtime.ts
3170
+ var WEBHOOK_PATH = "/webhooks/formbricks";
3171
+ var error = (code, message) => ({ code, message, details: {} });
3172
+ var FORMBRICKS_PRESETS = {
3173
+ rate_limited: {
3174
+ description: "Response creation answers 429 too_many_requests (the client rate limit)",
3175
+ rules: [
3176
+ {
3177
+ operationId: "CreateClientResponse",
3178
+ status: 429,
3179
+ body: error("too_many_requests", "Too many requests, please try again later")
3180
+ }
3181
+ ]
3182
+ },
3183
+ server_error: {
3184
+ description: "Response creation answers 500 internal_server_error",
3185
+ rules: [
3186
+ {
3187
+ operationId: "CreateClientResponse",
3188
+ status: 500,
3189
+ body: error("internal_server_error", "Internal server error")
3190
+ }
3191
+ ]
3192
+ },
3193
+ missing_response_id: {
3194
+ description: "Response creation stores the response but answers without an id",
3195
+ rules: [{ operationId: "CreateClientResponse", effect: "missing_response_id" }]
3196
+ },
3197
+ environment_unavailable: {
3198
+ description: "The environment state answers 500 (the SDK cannot load surveys)",
3199
+ rules: [
3200
+ {
3201
+ operationId: "GetEnvironmentState",
3202
+ status: 500,
3203
+ body: error("internal_server_error", "Internal server error")
3204
+ }
3205
+ ]
3206
+ },
3207
+ data_as_string: {
3208
+ description: "Management reads return each response's data as a JSON string",
3209
+ rules: [
3210
+ { operationId: "ListResponses", effect: "data_as_string" },
3211
+ { operationId: "GetResponse", effect: "data_as_string" }
3212
+ ]
3213
+ },
3214
+ management_unauthorized: {
3215
+ description: "Management calls answer 401 not_authenticated (a revoked API key)",
3216
+ rules: [
3217
+ {
3218
+ pathPrefix: "/api/v1/management",
3219
+ status: 401,
3220
+ body: {
3221
+ code: "not_authenticated",
3222
+ message: "Not authenticated",
3223
+ details: { "x-Api-Key": "Header not provided or API Key invalid" }
3224
+ }
3225
+ }
3226
+ ]
3227
+ },
3228
+ connection_drop: {
3229
+ description: "Response creation drops the connection (a client timeout path)",
3230
+ rules: [{ operationId: "CreateClientResponse", drop: true }]
3231
+ },
3232
+ duplicate: {
3233
+ description: "The next webhook is delivered twice (receivers should dedupe on webhook-id)",
3234
+ webhook: { mode: "duplicate" }
3235
+ },
3236
+ webhook_drop: {
3237
+ description: "The next webhook is never delivered",
3238
+ webhook: { mode: "drop" }
3239
+ },
3240
+ webhook_reorder: {
3241
+ description: "The next two webhooks arrive swapped",
3242
+ webhook: { mode: "reorder" }
3243
+ }
3244
+ };
3245
+ var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
3246
+ var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
3247
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3248
+ var adminRoutes = (runtime) => ({
3249
+ "GET /responses": ({ url, namespace }) => {
3250
+ const surveyId = url.searchParams.get("surveyId");
3251
+ return json3(200, {
3252
+ responses: runtime.instance(namespace).responses().filter((r) => surveyId === null || r.surveyId === surveyId)
3253
+ });
3254
+ },
3255
+ "GET /surveys": ({ namespace }) => json3(200, { surveys: runtime.instance(namespace).state.allSurveys() }),
3256
+ "PUT /surveys": ({ body, namespace }) => {
3257
+ const list = Array.isArray(body) ? body : isRecord4(body) ? body.surveys : void 0;
3258
+ if (!Array.isArray(list)) return adminError3(400, "expected [survey] or {surveys: [survey]}");
3259
+ const state = runtime.instance(namespace).state;
3260
+ for (const [index, survey] of list.entries()) {
3261
+ if (!isRecord4(survey) || typeof survey.id !== "string" || typeof survey.name !== "string") {
3262
+ return adminError3(400, `surveys[${index}]: id and name are required`);
3263
+ }
3264
+ }
3265
+ for (const survey of list) {
3266
+ const { environmentId, ...rest } = survey;
3267
+ const owner = survey.workspaceId ?? environmentId;
3268
+ state.surveys.insert(survey.id, {
3269
+ ...rest,
3270
+ type: survey.type ?? "app",
3271
+ status: survey.status ?? "inProgress",
3272
+ workspaceId: owner ? state.resolveWorkspace(owner) ?? owner : null
3273
+ });
3274
+ }
3275
+ return json3(200, { surveys: state.allSurveys().map((s) => s.id) });
3276
+ },
3277
+ "GET /contacts": ({ namespace }) => json3(200, {
3278
+ contacts: runtime.instance(namespace).state.contacts.list({ order: "oldest" }).map((row) => row.value)
3279
+ }),
3280
+ "PUT /contacts": ({ body, namespace }) => {
3281
+ const list = Array.isArray(body) ? body : isRecord4(body) ? body.contacts : void 0;
3282
+ if (!Array.isArray(list)) return adminError3(400, "expected [contact] or {contacts: [contact]}");
3283
+ const state = runtime.instance(namespace).state;
3284
+ for (const [index, contact] of list.entries()) {
3285
+ if (!isRecord4(contact) || typeof contact.id !== "string") {
3286
+ return adminError3(400, `contacts[${index}]: id is required`);
3287
+ }
3288
+ }
3289
+ for (const contact of list) {
3290
+ const attributes = isRecord4(contact.attributes) ? Object.fromEntries(Object.entries(contact.attributes).map(([k, v]) => [k, String(v)])) : {};
3291
+ if (typeof contact.userId === "string") attributes.userId = contact.userId;
3292
+ state.contacts.insert(contact.id, {
3293
+ id: contact.id,
3294
+ workspaceId: typeof contact.workspaceId === "string" ? contact.workspaceId : null,
3295
+ attributes
3296
+ });
3297
+ }
3298
+ return json3(200, { contacts: state.contacts.list({ order: "oldest" }).map((r) => r.value.id) });
3299
+ },
3300
+ "GET /settings": ({ namespace }) => json3(200, runtime.instance(namespace).state.current()),
3301
+ "PUT /settings": ({ body, namespace }) => {
3302
+ if (!isRecord4(body)) return adminError3(400, "expected a JSON object");
3303
+ const patch = {};
3304
+ if (Array.isArray(body.workspaces)) patch.workspaces = body.workspaces.map(String);
3305
+ if (isRecord4(body.legacyEnvironmentIds)) {
3306
+ patch.legacyEnvironmentIds = Object.fromEntries(
3307
+ Object.entries(body.legacyEnvironmentIds).map(([k, v]) => [k, String(v)])
3308
+ );
3309
+ }
3310
+ if (Array.isArray(body.apiKeys)) patch.apiKeys = body.apiKeys.map(String);
3311
+ if (typeof body.webhookId === "string") patch.webhookId = body.webhookId;
3312
+ if (typeof body.contactsEnabled === "boolean") patch.contactsEnabled = body.contactsEnabled;
3313
+ return json3(200, runtime.instance(namespace).state.update(patch));
3314
+ }
3315
+ });
3316
+ var formbricksCredential = (request) => {
3317
+ const key = request.headers.get("x-api-key")?.trim();
3318
+ if (key) return key;
3319
+ return /\/api\/v[12]\/client\/([^/]+)\//.exec(new URL(request.url).pathname)?.[1];
3320
+ };
3321
+ var createRuntime2 = (options = {}) => {
3322
+ const { retryDelaysMs, fetch: send, ...endpoint } = options.webhooks ?? { url: "" };
3323
+ const hub = createWebhookHub({
3324
+ // Formbricks always sends webhook-id and webhook-timestamp; webhook-signature only when
3325
+ // the webhook has a secret (Standard Webhooks, `v1,<base64 HMAC>` over "id.ts.body").
3326
+ signer: signers.custom(async ({ messageId, timestampSeconds, body, secret }) => ({
3327
+ "webhook-id": messageId,
3328
+ "webhook-timestamp": String(timestampSeconds),
3329
+ ...secret?.startsWith("whsec_") ? { "webhook-signature": await signSvix(secret, messageId, timestampSeconds, body) } : {}
3330
+ })),
3331
+ ...retryDelaysMs ? { retryDelaysMs } : {},
3332
+ ...send ? { fetch: send } : {},
3333
+ endpoints: options.webhooks ? [{ events: ["responseFinished"], ...endpoint }] : []
3334
+ });
3335
+ const runtime = createRuntime({
3336
+ name: FORMBRICKS_NAMESPACE,
3337
+ document,
3338
+ ...options.sqlite ? { sqlite: options.sqlite } : {},
3339
+ ...options.clock ? { clock: options.clock } : {},
3340
+ ...options.seed !== void 0 ? { seed: options.seed } : {},
3341
+ ...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
3342
+ ...options.onLog ? { onLog: options.onLog } : {},
3343
+ credential: formbricksCredential,
3344
+ presets: FORMBRICKS_PRESETS,
3345
+ webhooks: hub,
3346
+ create: ({ sqlite, namespace, publicNamespace, clock }) => new FormbricksAPI({
3347
+ sqlite,
3348
+ namespace,
3349
+ now: clock.now,
3350
+ ...options.surveys ? { surveys: options.surveys } : {},
3351
+ ...options.settings ? { settings: options.settings } : {},
3352
+ // Formbricks calls only the webhooks whose triggers include the event: publish nothing
3353
+ // (so no webhook fault is spent) when no endpoint subscribes to it.
3354
+ onWebhook: (event) => {
3355
+ const subscribed = hub.endpoints(publicNamespace).some((e) => !e.events || e.events.includes("*") || e.events.includes(event.event));
3356
+ if (subscribed)
3357
+ hub.publish({ namespace: publicNamespace, type: event.event, body: event });
3358
+ }
3359
+ }),
3360
+ describe: () => ({ webhooks: hub.endpoints("default").length > 0 ? "on" : "off" }),
3361
+ admin: adminRoutes
3362
+ });
3363
+ return Object.assign(runtime, { webhooks: hub });
3364
+ };
3365
+
3366
+ // src/index.ts
3367
+ var FORMBRICKS_NAMESPACE = "formbricks";
3368
+ var formbricksError = (status, code, message, details = {}, cors = true) => jsonRes(
3369
+ status,
3370
+ { code, message, details },
3371
+ {
3372
+ "cache-control": "private, no-store",
3373
+ ...cors ? { "access-control-allow-origin": "*" } : {}
3374
+ }
3375
+ );
3376
+ var notFound = (resourceType, resourceId, cors = true) => formbricksError(
3377
+ 404,
3378
+ "not_found",
3379
+ `${resourceType} not found`,
3380
+ { resource_id: resourceId, resource_type: resourceType },
3381
+ cors
3382
+ );
3383
+ var CUID2 = /^[0-9a-z]+$/;
3384
+ var zodType = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "number" && Number.isNaN(value) ? "NaN" : typeof value;
3385
+ var AUTO_CAPTURED_META = {
3386
+ pagePath: "string",
3387
+ pageReferrer: "string",
3388
+ utmSource: "string",
3389
+ utmMedium: "string",
3390
+ utmCampaign: "string",
3391
+ utmTerm: "string",
3392
+ utmContent: "string",
3393
+ screenWidth: "number",
3394
+ screenHeight: "number",
3395
+ viewportWidth: "number",
3396
+ viewportHeight: "number",
3397
+ timezone: "string",
3398
+ locale: "string"
3399
+ };
3400
+ var responseInputIssues = (body) => {
3401
+ const details = {};
3402
+ const expect = (path, value, type, optional = false) => {
3403
+ if (value === void 0) {
3404
+ if (!optional) details[path] = `Invalid input: expected ${type}, received undefined`;
3405
+ return false;
3406
+ }
3407
+ const actual = zodType(value);
3408
+ if (actual !== type) {
3409
+ details[path] = `Invalid input: expected ${type}, received ${actual}`;
3410
+ return false;
3411
+ }
3412
+ return true;
3413
+ };
3414
+ const nullish = (key, cuid = false) => {
3415
+ const value = body[key];
3416
+ if (value === void 0 || value === null) return;
3417
+ if (expect(key, value, "string") && cuid && !CUID2.test(value)) {
3418
+ details[key] = "Invalid cuid2";
3419
+ }
3420
+ };
3421
+ if (expect("surveyId", body.surveyId, "string") && !CUID2.test(body.surveyId)) {
3422
+ details.surveyId = "Invalid cuid2";
3423
+ }
3424
+ nullish("displayId");
3425
+ if (body.singleUseId !== null) expect("singleUseId", body.singleUseId, "string", true);
3426
+ nullish("pinAuthToken");
3427
+ nullish("recaptchaToken");
3428
+ expect("finished", body.finished, "boolean");
3429
+ nullish("endingId");
3430
+ expect("language", body.language, "string", true);
3431
+ if (zodType(body.data) !== "object") {
3432
+ details.data = `Invalid input: expected record, received ${zodType(body.data)}`;
3433
+ } else {
3434
+ for (const [key, value] of Object.entries(body.data)) {
3435
+ const ok = value === void 0 || typeof value === "string" || typeof value === "number" && !Number.isNaN(value) || Array.isArray(value) && value.every((v) => typeof v === "string") || zodType(value) === "object" && Object.values(value).every((v) => typeof v === "string");
3436
+ if (!ok) details[`data.${key}`] = "Invalid input";
3437
+ }
3438
+ }
3439
+ if (body.variables !== void 0) {
3440
+ if (zodType(body.variables) !== "object") {
3441
+ details.variables = `Invalid input: expected record, received ${zodType(body.variables)}`;
3442
+ } else {
3443
+ for (const [key, value] of Object.entries(body.variables)) {
3444
+ if (typeof value !== "string" && typeof value !== "number") {
3445
+ details[`variables.${key}`] = "Invalid input";
3446
+ }
3447
+ }
3448
+ }
3449
+ }
3450
+ if (body.ttc !== void 0) {
3451
+ if (zodType(body.ttc) !== "object") {
3452
+ details.ttc = `Invalid input: expected record, received ${zodType(body.ttc)}`;
3453
+ } else {
3454
+ for (const [key, value] of Object.entries(body.ttc)) {
3455
+ expect(`ttc.${key}`, value, "number");
3456
+ }
3457
+ }
3458
+ }
3459
+ if (body.meta !== void 0 && expect("meta", body.meta, "object")) {
3460
+ const meta = body.meta;
3461
+ for (const [key, type] of Object.entries(AUTO_CAPTURED_META)) {
3462
+ expect(`meta.${key}`, meta[key], type, true);
3463
+ }
3464
+ for (const key of ["source", "url", "country", "action", "ipAddress"]) {
3465
+ expect(`meta.${key}`, meta[key], "string", true);
3466
+ }
3467
+ expect("meta.userAgent", meta.userAgent, "object", true);
3468
+ }
3469
+ nullish("contactId", true);
3470
+ return details;
3471
+ };
3472
+ var CANONICAL_LANGUAGES = {
3473
+ da: "da-DK",
3474
+ de: "de-DE",
3475
+ en: "en-US",
3476
+ es: "es-ES",
3477
+ fi: "fi-FI",
3478
+ fr: "fr-FR",
3479
+ it: "it-IT",
3480
+ ja: "ja-JP",
3481
+ ko: "ko-KR",
3482
+ nl: "nl-NL",
3483
+ pl: "pl-PL",
3484
+ pt: "pt-BR",
3485
+ ru: "ru-RU",
3486
+ sv: "sv-SE",
3487
+ tr: "tr-TR",
3488
+ zh: "zh-Hans-CN"
3489
+ };
3490
+ var normalizeLanguage = (language) => {
3491
+ if (typeof language !== "string") return null;
3492
+ const trimmed = language.trim();
3493
+ if (!trimmed) return null;
3494
+ return CANONICAL_LANGUAGES[trimmed.toLowerCase()] ?? trimmed;
3495
+ };
3496
+ var ingestData = (survey, data) => {
3497
+ const elementIds = new Set(surveyElements(survey).map((e) => e.id));
3498
+ const hidden = survey.hiddenFields;
3499
+ const hiddenIds = new Set(Array.isArray(hidden?.fieldIds) ? hidden.fieldIds : []);
3500
+ return Object.fromEntries(
3501
+ Object.entries(data).filter(
3502
+ ([key, value]) => value !== void 0 && (elementIds.has(key) || hiddenIds.has(key))
3503
+ )
3504
+ );
3505
+ };
3506
+ var ENVIRONMENT_SURVEY_FIELDS = [
3507
+ "id",
3508
+ "name",
3509
+ "welcomeCard",
3510
+ "questions",
3511
+ "blocks",
3512
+ "variables",
3513
+ "type",
3514
+ "showLanguageSwitch",
3515
+ "languages",
3516
+ "endings",
3517
+ "autoClose",
3518
+ "styling",
3519
+ "status",
3520
+ "segment",
3521
+ "recontactDays",
3522
+ "displayLimit",
3523
+ "displayOption",
3524
+ "hiddenFields",
3525
+ "embeddedFields",
3526
+ "triggers",
3527
+ "displayPercentage",
3528
+ "delay",
3529
+ "workspaceOverwrites",
3530
+ "isBackButtonHidden",
3531
+ "isAutoProgressingEnabled",
3532
+ "recaptcha"
3533
+ ];
3534
+ var withLegacyProjectOverwrites = (survey) => "workspaceOverwrites" in survey ? { ...survey, projectOverwrites: survey.workspaceOverwrites } : survey;
3535
+ var hasLabel = (label) => typeof label === "object" && label !== null && Object.values(label).some((v) => typeof v === "string" && v !== "");
3536
+ var blocksToQuestions = (blocks) => blocks.flatMap((block) => {
3537
+ const elements = Array.isArray(block?.elements) ? block.elements.filter((e) => typeof e === "object" && e !== null) : [];
3538
+ return elements.map((raw, index) => {
3539
+ const element = { ...raw };
3540
+ if (element.type === "cta" && element.ctaButtonLabel) {
3541
+ element.buttonLabel = element.ctaButtonLabel;
3542
+ }
3543
+ if (index === elements.length - 1) {
3544
+ if (hasLabel(block.buttonLabel)) element.buttonLabel = block.buttonLabel;
3545
+ if (hasLabel(block.backButtonLabel)) element.backButtonLabel = block.backButtonLabel;
3546
+ }
3547
+ return element;
3548
+ });
3549
+ });
3550
+ var questionsToBlocks = (questions, nextId) => questions.map((question, index) => {
3551
+ const { logic, logicFallback, buttonLabel, backButtonLabel, ...element } = question;
3552
+ return {
3553
+ id: nextId(),
3554
+ name: `Block ${index + 1}`,
3555
+ elements: [element],
3556
+ ...buttonLabel !== void 0 ? { buttonLabel } : {},
3557
+ ...backButtonLabel !== void 0 ? { backButtonLabel } : {},
3558
+ ...logic !== void 0 ? { logic } : {},
3559
+ ...logicFallback !== void 0 ? { logicFallback } : {}
3560
+ };
3561
+ });
3562
+ var WIDGET_SCRIPT = `/* Mockingbird Formbricks widget stub: a no-op window.formbricks. */
3563
+ (function () {
3564
+ var noop = function () { return Promise.resolve(); };
3565
+ var api = {
3566
+ setup: noop, init: noop, setUserId: noop, setEmail: noop, setAttribute: noop,
3567
+ setAttributes: noop, setLanguage: noop, track: noop, logout: noop, reset: noop,
3568
+ registerRouteChange: noop
3569
+ };
3570
+ if (typeof window !== "undefined") { window.formbricks = window.formbricks || api; }
3571
+ })();
3572
+ `;
3573
+ var FormbricksAPI = class {
3574
+ app;
3575
+ sqlite;
3576
+ state;
3577
+ service;
3578
+ now;
3579
+ onWebhook;
3580
+ constructor(options = {}) {
3581
+ const sqlite = bootSqlite(options.sqlite);
3582
+ const namespace = options.namespace ?? FORMBRICKS_NAMESPACE;
3583
+ this.now = options.now ?? (() => Date.now());
3584
+ this.onWebhook = options.onWebhook;
3585
+ this.state = new FormbricksState(sqlite, namespace, {
3586
+ surveys: options.surveys ?? CORPUS_SURVEYS,
3587
+ settings: options.settings ?? {}
3588
+ });
3589
+ const handlers = defineOperations({
3590
+ GetEnvironmentState: (context) => this.environmentState(context),
3591
+ CreateClientResponse: (context) => this.createResponse(context),
3592
+ ListResponses: (context) => this.listResponses(context),
3593
+ GetResponse: (context) => {
3594
+ const id = context.params.responseId ?? "";
3595
+ const record = this.state.responses.get(id);
3596
+ if (!record) return notFound("Response", id, false);
3597
+ return jsonRes(200, { data: this.render(record, context) });
3598
+ },
3599
+ ListSurveys: (context) => this.listSurveys(context),
3600
+ GetSurvey: (context) => {
3601
+ const id = context.params.surveyId ?? "";
3602
+ const survey = this.state.surveys.get(id);
3603
+ if (!survey) return notFound("Survey", id, false);
3604
+ return jsonRes(200, { data: this.wireSurvey(survey) });
3605
+ },
3606
+ CreateSurvey: (context) => this.createSurvey(context),
3607
+ GetWidgetScript: () => new Response(WIDGET_SCRIPT, {
3608
+ status: 200,
3609
+ headers: {
3610
+ "content-type": "application/javascript; charset=utf-8",
3611
+ "cache-control": "public, max-age=3600",
3612
+ "access-control-allow-origin": "*"
3613
+ }
3614
+ })
3615
+ });
3616
+ this.service = createService({
3617
+ document,
3618
+ handlers,
3619
+ sqlite,
3620
+ namespace,
3621
+ now: this.now,
3622
+ notFound: () => formbricksError(404, "not_found", "Not found", {}, false),
3623
+ onError: (error2) => {
3624
+ if (error2 instanceof HttpError) return error2.toResponse();
3625
+ throw error2;
3626
+ },
3627
+ before: (context) => {
3628
+ if (!context.operation.path.startsWith("/api/v1/management/")) return void 0;
3629
+ const key = context.request.headers.get("x-api-key")?.trim();
3630
+ const keys = this.state.current().apiKeys;
3631
+ if (!key || keys.length > 0 && !keys.includes(key)) {
3632
+ return formbricksError(
3633
+ 401,
3634
+ "not_authenticated",
3635
+ "Not authenticated",
3636
+ { "x-Api-Key": "Header not provided or API Key invalid" },
3637
+ false
3638
+ );
3639
+ }
3640
+ return void 0;
3641
+ }
3642
+ });
3643
+ this.app = this.service.app;
3644
+ this.sqlite = this.service.sqlite;
3645
+ }
3646
+ fetch(request) {
3647
+ return this.service.fetch(request);
3648
+ }
3649
+ async reset() {
3650
+ await this.service.reset();
3651
+ this.state.ensureSeeded();
3652
+ }
3653
+ iso() {
3654
+ return new Date(this.now()).toISOString();
3655
+ }
3656
+ /**
3657
+ * A survey as the v1 management API serves it: `questions` derived from `blocks`, the owning
3658
+ * `workspaceId` and its legacy `environmentId`, and `projectOverwrites`. Shared corpus surveys
3659
+ * are reported under the first configured workspace.
3660
+ */
3661
+ wireSurvey(survey) {
3662
+ const { workspaceId: stored, ...rest } = survey;
3663
+ const workspaceId = stored ?? this.state.current().workspaces[0];
3664
+ const blocks = Array.isArray(rest.blocks) ? rest.blocks : [];
3665
+ const questions = blocks.length > 0 ? blocksToQuestions(blocks) : rest.questions;
3666
+ return withLegacyProjectOverwrites({
3667
+ ...rest,
3668
+ ...questions !== void 0 ? { questions } : {},
3669
+ ...workspaceId ? { workspaceId, environmentId: this.state.legacyEnvironmentId(workspaceId) } : {}
3670
+ });
3671
+ }
3672
+ render(record, context) {
3673
+ const { workspaceId: _workspaceId, ...rest } = record;
3674
+ const asString = context ? faultEffect(context.request, "data_as_string") !== void 0 : false;
3675
+ return asString ? { ...rest, data: JSON.stringify(rest.data) } : rest;
3676
+ }
3677
+ environmentState(context) {
3678
+ const id = (context.params.workspaceId ?? "").trim();
3679
+ if (!CUID2.test(id)) {
3680
+ return formbricksError(400, "bad_request", "Invalid ID format");
3681
+ }
3682
+ const workspaceId = this.state.resolveWorkspace(id);
3683
+ if (!workspaceId) return notFound("Workspace", id, false);
3684
+ const surveys = this.state.surveysOf(workspaceId).filter((s) => s.type === "app" && s.status === "inProgress").map(
3685
+ (s) => withLegacyProjectOverwrites(
3686
+ Object.fromEntries(ENVIRONMENT_SURVEY_FIELDS.filter((k) => k in s).map((k) => [k, s[k]]))
3687
+ )
3688
+ );
3689
+ const workspace = workspaceSettings(workspaceId);
3690
+ return jsonRes(
3691
+ 200,
3692
+ {
3693
+ data: {
3694
+ data: { surveys, actionClasses: [], workspace, project: workspace },
3695
+ expiresAt: new Date(this.now() + 60 * 60 * 1e3).toISOString()
3696
+ }
3697
+ },
3698
+ {
3699
+ "cache-control": "public, s-maxage=60, max-age=60, stale-while-revalidate=60, stale-if-error=60",
3700
+ "access-control-allow-origin": "*"
3701
+ }
3702
+ );
3703
+ }
3704
+ createResponse(context) {
3705
+ const rawId = context.params.workspaceId ?? "";
3706
+ const workspaceId = this.state.resolveWorkspace(rawId);
3707
+ if (!workspaceId) return notFound("Workspace", rawId, false);
3708
+ if (context.body.kind !== "json") {
3709
+ return formbricksError(400, "bad_request", "Invalid JSON in request body", {
3710
+ error: "request body is not valid JSON"
3711
+ });
3712
+ }
3713
+ const body = zodType(context.body.value) === "object" ? context.body.value : {};
3714
+ const issues = responseInputIssues(body);
3715
+ if (Object.keys(issues).length > 0) {
3716
+ return formbricksError(
3717
+ 400,
3718
+ "bad_request",
3719
+ "Fields are missing or incorrectly formatted",
3720
+ issues
3721
+ );
3722
+ }
3723
+ const contactId = typeof body.contactId === "string" ? body.contactId : null;
3724
+ if (contactId && !this.state.current().contactsEnabled) {
3725
+ return formbricksError(
3726
+ 403,
3727
+ "forbidden",
3728
+ "User identification is only available for enterprise users."
3729
+ );
3730
+ }
3731
+ const survey = this.state.surveys.get(body.surveyId);
3732
+ if (!survey) return notFound("Survey", body.surveyId);
3733
+ const data = ingestData(survey, body.data);
3734
+ if (!this.state.belongsTo(survey, workspaceId)) {
3735
+ return formbricksError(400, "bad_request", "Survey is part of another workspace", {
3736
+ workspaceId
3737
+ });
3738
+ }
3739
+ if (survey.status !== "inProgress") {
3740
+ return formbricksError(403, "forbidden", "Survey is not accepting submissions", {
3741
+ surveyId: survey.id
3742
+ });
3743
+ }
3744
+ const errors = validateResponseData(
3745
+ survey,
3746
+ data,
3747
+ typeof body.language === "string" ? body.language : "en"
3748
+ );
3749
+ if (errors) {
3750
+ const details = {};
3751
+ for (const [elementId, messages] of Object.entries(errors)) {
3752
+ details[`response.data.${elementId}`] = messages.join("; ");
3753
+ }
3754
+ return formbricksError(400, "bad_request", "Validation failed", details);
3755
+ }
3756
+ const now = this.iso();
3757
+ const finished = body.finished;
3758
+ const input = body.meta ?? {};
3759
+ const contact = contactId ? this.state.contactOf(contactId, workspaceId) : void 0;
3760
+ const ttcIn = body.ttc ?? {};
3761
+ const ttc = Object.fromEntries(
3762
+ Object.entries(ttcIn).map(([k, v]) => [k, Math.max(v, 0)])
3763
+ );
3764
+ if (finished && body.ttc !== void 0) {
3765
+ ttc._total = Object.values(ttc).reduce((a, b) => a + b, 0);
3766
+ }
3767
+ const country = context.request.headers.get("cf-ipcountry") ?? context.request.headers.get("cloudfront-viewer-country") ?? void 0;
3768
+ const meta = {
3769
+ ...Object.fromEntries(Object.keys(AUTO_CAPTURED_META).map((k) => [k, input[k]])),
3770
+ source: input.source,
3771
+ url: input.url,
3772
+ userAgent: { device: "desktop" },
3773
+ country,
3774
+ action: input.action
3775
+ };
3776
+ const record = {
3777
+ id: this.state.nextId(),
3778
+ createdAt: now,
3779
+ updatedAt: now,
3780
+ surveyId: survey.id,
3781
+ workspaceId,
3782
+ displayId: typeof body.displayId === "string" ? body.displayId : null,
3783
+ contact: contact ? {
3784
+ id: contact.id,
3785
+ ...contact.attributes.userId !== void 0 ? { userId: contact.attributes.userId } : {}
3786
+ } : null,
3787
+ contactAttributes: contact ? contact.attributes : null,
3788
+ finished,
3789
+ endingId: typeof body.endingId === "string" ? body.endingId : null,
3790
+ data,
3791
+ variables: body.variables ?? {},
3792
+ ttc,
3793
+ tags: [],
3794
+ meta: Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== void 0)),
3795
+ singleUseId: typeof body.singleUseId === "string" ? body.singleUseId : null,
3796
+ language: normalizeLanguage(body.language)
3797
+ };
3798
+ this.state.responses.insert(record.id, record);
3799
+ this.pipeline("responseCreated", record, survey);
3800
+ if (record.finished) this.pipeline("responseFinished", record, survey);
3801
+ const ids = { responseId: record.id, surveyId: survey.id };
3802
+ if (faultEffect(context.request, "missing_response_id") !== void 0) {
3803
+ return annotateResponse(jsonRes(200, { data: { quotaFull: false } }), { ids });
3804
+ }
3805
+ return annotateResponse(
3806
+ jsonRes(
3807
+ 200,
3808
+ { data: { id: record.id, quotaFull: false } },
3809
+ { "cache-control": "private, no-store", "access-control-allow-origin": "*" }
3810
+ ),
3811
+ { ids }
3812
+ );
3813
+ }
3814
+ /** Emit a pipeline event (`responseCreated` / `responseFinished`) for a stored response. */
3815
+ pipeline(event, record, survey) {
3816
+ this.onWebhook?.({
3817
+ webhookId: this.state.current().webhookId,
3818
+ event,
3819
+ data: {
3820
+ ...this.render(record),
3821
+ survey: {
3822
+ title: survey.name,
3823
+ type: survey.type,
3824
+ status: survey.status,
3825
+ createdAt: typeof survey.createdAt === "string" ? survey.createdAt : null,
3826
+ updatedAt: typeof survey.updatedAt === "string" ? survey.updatedAt : null
3827
+ }
3828
+ }
3829
+ });
3830
+ }
3831
+ listResponses(context) {
3832
+ const params = context.url.searchParams;
3833
+ const surveyId = params.get("surveyId");
3834
+ if (surveyId && !this.state.surveys.get(surveyId)) return notFound("Survey", surveyId);
3835
+ const limit = params.get("limit") ? Number(params.get("limit")) : void 0;
3836
+ const skip = params.get("skip") ? Number(params.get("skip")) : 0;
3837
+ const all = this.state.responses.list({ order: "newest", where: (r) => !surveyId || r.surveyId === surveyId }).map((row) => row.value);
3838
+ const page = all.slice(skip, limit === void 0 ? void 0 : skip + limit);
3839
+ return jsonRes(200, { data: page.map((r) => this.render(r, context)) });
3840
+ }
3841
+ /** `getSurveys(workspaceIds, limit, offset)`: newest `updatedAt` first. */
3842
+ listSurveys(context) {
3843
+ const params = context.url.searchParams;
3844
+ const limit = params.has("limit") ? Number(params.get("limit")) : void 0;
3845
+ const offset = params.has("offset") ? Number(params.get("offset")) : 0;
3846
+ const surveys = this.state.allSurveys().map((survey, index) => ({ survey, index })).sort((a, b) => {
3847
+ const byDate = String(b.survey.updatedAt ?? "").localeCompare(
3848
+ String(a.survey.updatedAt ?? "")
3849
+ );
3850
+ return byDate !== 0 ? byDate : b.index - a.index;
3851
+ }).map(({ survey }) => survey);
3852
+ const page = surveys.slice(offset, limit === void 0 ? void 0 : offset + limit);
3853
+ return jsonRes(200, { data: page.map((s) => this.wireSurvey(s)) });
3854
+ }
3855
+ createSurvey(context) {
3856
+ const body = context.body.kind === "json" && zodType(context.body.value) === "object" ? context.body.value : void 0;
3857
+ if (!body) {
3858
+ return formbricksError(
3859
+ 400,
3860
+ "bad_request",
3861
+ "Malformed JSON input, please check your request body",
3862
+ {},
3863
+ false
3864
+ );
3865
+ }
3866
+ const rawId = body.workspaceId ?? body.environmentId;
3867
+ if (!rawId) {
3868
+ return formbricksError(400, "bad_request", "workspaceId must be provided", {}, false);
3869
+ }
3870
+ if (typeof rawId !== "string") {
3871
+ return formbricksError(400, "bad_request", "workspaceId must be a string", {}, false);
3872
+ }
3873
+ const workspaceId = this.state.resolveWorkspace(rawId);
3874
+ if (!workspaceId) return notFound("Workspace", rawId, false);
3875
+ const details = {};
3876
+ if (typeof body.name !== "string") {
3877
+ details.name = `Invalid input: expected string, received ${zodType(body.name)}`;
3878
+ }
3879
+ for (const key of ["questions", "blocks"]) {
3880
+ const list = body[key];
3881
+ if (list === void 0) continue;
3882
+ if (!Array.isArray(list)) {
3883
+ details[key] = `Invalid input: expected array, received ${zodType(list)}`;
3884
+ continue;
3885
+ }
3886
+ for (const [index, item] of list.entries()) {
3887
+ if (zodType(item) !== "object") {
3888
+ details[`${key}.${index}`] = `Invalid input: expected object, received ${zodType(item)}`;
3889
+ } else if (key === "blocks" && !Array.isArray(item.elements)) {
3890
+ details[`blocks.${index}.elements`] = `Invalid input: expected array, received ${zodType(item.elements)}`;
3891
+ }
3892
+ }
3893
+ }
3894
+ if (body.type !== void 0 && body.type !== "app" && body.type !== "link") {
3895
+ details.type = 'Invalid option: expected one of "link"|"app"';
3896
+ }
3897
+ if (Object.keys(details).length > 0) {
3898
+ return formbricksError(
3899
+ 400,
3900
+ "bad_request",
3901
+ "Fields are missing or incorrectly formatted",
3902
+ details
3903
+ );
3904
+ }
3905
+ const questions = body.questions ?? [];
3906
+ let blocks = body.blocks ?? [];
3907
+ if (questions.length > 0 && blocks.length > 0) {
3908
+ return formbricksError(
3909
+ 400,
3910
+ "bad_request",
3911
+ "Cannot provide both questions and blocks. Please provide only one of these fields.",
3912
+ {},
3913
+ false
3914
+ );
3915
+ }
3916
+ if (questions.length === 0 && blocks.length === 0) {
3917
+ return formbricksError(
3918
+ 400,
3919
+ "bad_request",
3920
+ "Must provide either questions or blocks. Both cannot be empty.",
3921
+ {},
3922
+ false
3923
+ );
3924
+ }
3925
+ if (questions.length > 0) blocks = questionsToBlocks(questions, () => this.state.nextId());
3926
+ const now = this.iso();
3927
+ const survey = {
3928
+ id: this.state.nextId(),
3929
+ createdAt: now,
3930
+ updatedAt: now,
3931
+ name: body.name,
3932
+ type: typeof body.type === "string" ? body.type : "link",
3933
+ status: typeof body.status === "string" ? body.status : "draft",
3934
+ workspaceId,
3935
+ welcomeCard: { enabled: false },
3936
+ questions: [],
3937
+ blocks,
3938
+ endings: [],
3939
+ hiddenFields: { enabled: true, fieldIds: [] },
3940
+ variables: [],
3941
+ languages: [],
3942
+ displayOption: "displayOnce",
3943
+ recontactDays: null,
3944
+ displayLimit: null,
3945
+ autoClose: null,
3946
+ delay: 0,
3947
+ displayPercentage: null,
3948
+ segment: null,
3949
+ triggers: [],
3950
+ workspaceOverwrites: null,
3951
+ isAutoProgressingEnabled: false
3952
+ };
3953
+ this.state.surveys.insert(survey.id, survey);
3954
+ return annotateResponse(jsonRes(200, { data: this.wireSurvey(survey) }), {
3955
+ ids: { surveyId: survey.id }
3956
+ });
3957
+ }
3958
+ responses() {
3959
+ return this.state.responses.list({ order: "oldest" }).map((row) => this.render(row.value));
3960
+ }
3961
+ };
3962
+
3963
+ export {
3964
+ document,
3965
+ operationIds,
3966
+ supportedOperationIds,
3967
+ WORKSPACE_ID,
3968
+ ENVIRONMENT_ID,
3969
+ DEFAULT_SETTINGS,
3970
+ CORPUS_SURVEYS,
3971
+ validateResponseData,
3972
+ WEBHOOK_PATH,
3973
+ FORMBRICKS_PRESETS,
3974
+ formbricksCredential,
3975
+ createRuntime2 as createRuntime,
3976
+ FORMBRICKS_NAMESPACE,
3977
+ formbricksError,
3978
+ FormbricksAPI
3979
+ };
3980
+ //# sourceMappingURL=chunk-ZFDC3GHV.js.map