@crvouga/mockingbird-service-edamam 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3485 @@
1
+ // ../core/dist/clock.js
2
+ var createClock = (source = Date.now) => {
3
+ let offsetMs = 0;
4
+ let frozenAt;
5
+ const now = () => frozenAt ?? source() + offsetMs;
6
+ return {
7
+ now,
8
+ set: (epochMs) => {
9
+ if (frozenAt !== void 0)
10
+ frozenAt = epochMs;
11
+ else
12
+ offsetMs = epochMs - source();
13
+ },
14
+ advance: (deltaMs) => {
15
+ if (frozenAt !== void 0)
16
+ frozenAt += deltaMs;
17
+ else
18
+ offsetMs += deltaMs;
19
+ },
20
+ freeze: () => {
21
+ frozenAt = now();
22
+ },
23
+ unfreeze: () => {
24
+ if (frozenAt === void 0)
25
+ return;
26
+ offsetMs = frozenAt - source();
27
+ frozenAt = void 0;
28
+ },
29
+ reset: () => {
30
+ offsetMs = 0;
31
+ frozenAt = void 0;
32
+ },
33
+ state: () => ({ now: now(), frozen: frozenAt !== void 0, offsetMs })
34
+ };
35
+ };
36
+
37
+ // ../core/dist/collection.js
38
+ var Collection = class {
39
+ sqlite;
40
+ namespace;
41
+ name;
42
+ constructor(sqlite, namespace, name) {
43
+ this.sqlite = sqlite;
44
+ this.namespace = namespace;
45
+ this.name = name;
46
+ }
47
+ bumpCollectionSeq() {
48
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.name);
49
+ const next = (row?.value ?? 0) + 1;
50
+ this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'collection', ?)
51
+ ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.name, next);
52
+ return next;
53
+ }
54
+ nextSequence() {
55
+ return this.sqlite.transaction(() => this.bumpCollectionSeq());
56
+ }
57
+ get(id) {
58
+ const row = this.sqlite.prepare("SELECT value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
59
+ if (!row)
60
+ return void 0;
61
+ return JSON.parse(row.value).value;
62
+ }
63
+ has(id) {
64
+ const row = this.sqlite.prepare("SELECT 1 AS ok FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
65
+ return row !== void 0;
66
+ }
67
+ /** Insert a new record, assigning it the next sequence number. */
68
+ insert(id, value) {
69
+ return this.sqlite.transaction(() => {
70
+ const seq = this.bumpCollectionSeq();
71
+ const stored = { seq, value };
72
+ this.sqlite.prepare(`INSERT INTO mockingbird_records (namespace, collection, id, seq, value)
73
+ VALUES (?, ?, ?, ?, ?)
74
+ ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.name, id, seq, JSON.stringify(stored));
75
+ return stored;
76
+ });
77
+ }
78
+ /** Replace an existing record's value, keeping its position. */
79
+ update(id, value) {
80
+ return this.sqlite.transaction(() => {
81
+ const row = this.sqlite.prepare("SELECT seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
82
+ if (!row)
83
+ return void 0;
84
+ const stored = { seq: row.seq, value };
85
+ this.sqlite.prepare("UPDATE mockingbird_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.name, id);
86
+ return stored;
87
+ });
88
+ }
89
+ delete(id) {
90
+ const result = this.sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.name, id);
91
+ return result.changes > 0;
92
+ }
93
+ /** How many records the collection holds, without reading them. */
94
+ count() {
95
+ const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM mockingbird_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.name);
96
+ return Number(row?.n ?? 0);
97
+ }
98
+ list(options = {}) {
99
+ const rows = this.sqlite.prepare("SELECT id, seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.name);
100
+ const out = [];
101
+ for (const row of rows) {
102
+ const stored = JSON.parse(row.value);
103
+ if (options.where && !options.where(stored.value, stored.seq))
104
+ continue;
105
+ out.push({ id: row.id, seq: stored.seq, value: stored.value });
106
+ }
107
+ out.sort((a, b) => options.order === "oldest" ? a.seq - b.seq : b.seq - a.seq);
108
+ return out;
109
+ }
110
+ };
111
+
112
+ // ../core/dist/control.js
113
+ var HEALTH_PATH = "/health";
114
+ var ADMIN_PREFIX = "/__admin";
115
+ var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
116
+ var NAMESPACE_HEADER = "x-mockingbird-namespace";
117
+ var json = (status, body) => new Response(JSON.stringify(body), {
118
+ status,
119
+ headers: { "content-type": "application/json" }
120
+ });
121
+ var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
122
+ var UNITS = {
123
+ ms: 1,
124
+ s: 1e3,
125
+ m: 6e4,
126
+ h: 36e5,
127
+ d: 864e5
128
+ };
129
+ var parseDuration = (value) => {
130
+ if (typeof value === "number" && Number.isFinite(value))
131
+ return value;
132
+ if (typeof value !== "string")
133
+ return void 0;
134
+ const match = /^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(value.trim());
135
+ if (!match)
136
+ return void 0;
137
+ return Number(match[1]) * UNITS[match[2]];
138
+ };
139
+ var parseInstant = (value) => {
140
+ if (typeof value === "number" && Number.isFinite(value))
141
+ return value;
142
+ if (typeof value !== "string")
143
+ return void 0;
144
+ const parsed = Date.parse(value);
145
+ return Number.isNaN(parsed) ? void 0 : parsed;
146
+ };
147
+ var matchRoute = (pattern, path) => {
148
+ const want = pattern.split("/").filter(Boolean);
149
+ const have = path.split("/").filter(Boolean);
150
+ if (want.length !== have.length)
151
+ return void 0;
152
+ const params = {};
153
+ for (let i = 0; i < want.length; i++) {
154
+ const segment = want[i];
155
+ const actual = have[i];
156
+ if (segment.startsWith(":"))
157
+ params[segment.slice(1)] = decodeURIComponent(actual);
158
+ else if (segment !== actual)
159
+ return void 0;
160
+ }
161
+ return params;
162
+ };
163
+ var readJson = async (request) => {
164
+ const text = await request.text();
165
+ if (text.trim() === "")
166
+ return void 0;
167
+ return JSON.parse(text);
168
+ };
169
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
170
+ var createControlPlane = (context) => {
171
+ const snapshots = /* @__PURE__ */ new Map();
172
+ let snapshotCounter = 0;
173
+ const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
174
+ const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
175
+ const builtin = {
176
+ "GET /": () => json(200, {
177
+ service: context.name,
178
+ routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
179
+ }),
180
+ "POST /reset": async ({ url, namespace }) => {
181
+ const target = url.searchParams.get("all") === "1" ? "*" : namespace;
182
+ await context.reset(target);
183
+ return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
184
+ },
185
+ "GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
186
+ "GET /clock": () => json(200, context.clock.state()),
187
+ "POST /clock": ({ body }) => {
188
+ if (!isRecord(body))
189
+ return adminError(400, "expected a JSON object");
190
+ if (body.reset === true)
191
+ context.clock.reset();
192
+ if (body.set !== void 0) {
193
+ const instant = parseInstant(body.set);
194
+ if (instant === void 0)
195
+ return adminError(400, "set: expected epoch ms or ISO-8601");
196
+ context.clock.set(instant);
197
+ }
198
+ if (body.advance !== void 0) {
199
+ const delta = parseDuration(body.advance);
200
+ if (delta === void 0)
201
+ return adminError(400, 'advance: expected ms or "15m"-style');
202
+ context.clock.advance(delta);
203
+ }
204
+ if (body.freeze === true)
205
+ context.clock.freeze();
206
+ if (body.freeze === false)
207
+ context.clock.unfreeze();
208
+ return json(200, context.clock.state());
209
+ },
210
+ "GET /faults": () => json(200, { faults: context.faults.list() }),
211
+ "POST /faults": ({ body, namespace }) => {
212
+ if (isRecord(body) && typeof body.preset === "string") {
213
+ if (!context.applyPreset)
214
+ return adminError(400, `${context.name} has no fault presets`);
215
+ const { preset, ...overrides } = body;
216
+ try {
217
+ return json(201, {
218
+ preset,
219
+ rules: context.applyPreset(preset, namespace, overrides)
220
+ });
221
+ } catch (error) {
222
+ return adminError(404, error instanceof Error ? error.message : String(error));
223
+ }
224
+ }
225
+ if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
226
+ return adminError(400, "a fault needs a numeric status, a delayMs/latencyMs, drop: true, an effect, or a preset");
227
+ }
228
+ const rule = {
229
+ // Scoped to the caller's namespace unless it asks for every one, so one worker's
230
+ // injected failure never lands on another's request.
231
+ namespace,
232
+ ...body,
233
+ id: typeof body.id === "string" ? body.id : `fault_${context.faults.list().length + 1}`
234
+ };
235
+ return json(201, context.faults.add(rule));
236
+ },
237
+ "DELETE /faults": ({ url }) => {
238
+ const id = url.searchParams.get("id");
239
+ if (id === null) {
240
+ context.faults.clear();
241
+ return json(200, { status: "ok" });
242
+ }
243
+ return context.faults.remove(id) ? json(200, { status: "ok" }) : adminError(404, `no fault ${id}`);
244
+ },
245
+ "POST /snapshots": ({ namespace }) => {
246
+ const point = context.timeTravel.checkpoint(namespace, "main");
247
+ context.timeTravel.retain(namespace, point.id);
248
+ snapshotCounter++;
249
+ const id = `snap_${snapshotCounter}`;
250
+ snapshots.set(id, { namespace, checkpoint: point.id });
251
+ return json(201, { id, namespace, records: point.records ?? 0 });
252
+ },
253
+ "POST /snapshots/:id/restore": ({ params, namespace }) => {
254
+ const alias = snapshots.get(params.id);
255
+ if (!alias)
256
+ return adminError(404, `no snapshot ${params.id}`);
257
+ if (alias.namespace !== namespace) {
258
+ return adminError(409, `snapshot ${params.id} belongs to namespace ${alias.namespace}`);
259
+ }
260
+ context.timeTravel.checkout(alias.checkpoint, { namespace, branch: "main" });
261
+ return json(200, { status: "ok", id: params.id, namespace });
262
+ },
263
+ "DELETE /snapshots/:id": ({ params }) => {
264
+ const id = params.id;
265
+ const alias = snapshots.get(id);
266
+ if (!alias)
267
+ return adminError(404, `no snapshot ${id}`);
268
+ snapshots.delete(id);
269
+ context.timeTravel.release(alias.namespace, alias.checkpoint);
270
+ return json(200, { status: "ok" });
271
+ },
272
+ "GET /timeline": ({ namespace }) => json(200, context.timeTravel.inspect(namespace)),
273
+ "POST /checkpoints": ({ body, namespace }) => {
274
+ const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
275
+ try {
276
+ return json(201, context.timeTravel.checkpoint(namespace, branch));
277
+ } catch (error) {
278
+ return adminError(409, error instanceof Error ? error.message : String(error));
279
+ }
280
+ },
281
+ "POST /branches/:name": ({ params, body, namespace }) => {
282
+ const at = isRecord(body) && typeof body.at === "string" ? body.at : void 0;
283
+ try {
284
+ return json(201, context.timeTravel.branch(params.name, {
285
+ namespace,
286
+ ...at !== void 0 ? { at } : {}
287
+ }));
288
+ } catch (error) {
289
+ return adminError(409, error instanceof Error ? error.message : String(error));
290
+ }
291
+ },
292
+ "POST /branches/:name/checkout": ({ params, body, namespace }) => {
293
+ if (!isRecord(body) || typeof body.checkpoint !== "string") {
294
+ return adminError(400, 'expected {"checkpoint":"cp_..."}');
295
+ }
296
+ try {
297
+ context.timeTravel.checkout(body.checkpoint, {
298
+ namespace,
299
+ branch: params.name
300
+ });
301
+ return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
302
+ } catch (error) {
303
+ return adminError(409, error instanceof Error ? error.message : String(error));
304
+ }
305
+ },
306
+ "GET /requests": ({ url, namespace }) => {
307
+ const status = url.searchParams.get("status");
308
+ const since = url.searchParams.get("since");
309
+ const limit = url.searchParams.get("limit");
310
+ const sinceMs = since === null ? void 0 : parseInstant(/^\d+$/.test(since) ? Number(since) : since);
311
+ if (since !== null && sinceMs === void 0) {
312
+ return adminError(400, "since: expected epoch ms or ISO-8601");
313
+ }
314
+ if (status !== null && !/^\d{3}$/.test(status))
315
+ return adminError(400, "status: expected an HTTP status");
316
+ if (limit !== null && !/^\d+$/.test(limit))
317
+ return adminError(400, "limit: expected a count");
318
+ const operationId = url.searchParams.get("operationId");
319
+ const everyNamespace = url.searchParams.get("all") === "1";
320
+ return json(200, {
321
+ size: context.journal.size,
322
+ requests: context.journal.list({
323
+ ...everyNamespace ? {} : { namespace },
324
+ ...operationId !== null ? { operationId } : {},
325
+ ...status !== null ? { status: Number(status) } : {},
326
+ ...sinceMs !== void 0 ? { since: sinceMs } : {},
327
+ ...limit !== null ? { limit: Number(limit) } : {}
328
+ })
329
+ });
330
+ },
331
+ "DELETE /requests": ({ url, namespace }) => {
332
+ context.journal.clear(url.searchParams.get("all") === "1" ? void 0 : namespace);
333
+ return json(200, { status: "ok" });
334
+ },
335
+ "GET /metrics": () => json(200, context.metrics.report()),
336
+ "DELETE /metrics": () => {
337
+ context.metrics.reset();
338
+ return json(200, { status: "ok" });
339
+ }
340
+ };
341
+ const routes = [...Object.entries(context.routes), ...Object.entries(builtin)].map(([key, handler]) => {
342
+ const space = key.indexOf(" ");
343
+ return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
344
+ });
345
+ return {
346
+ namespaceOf: headerNamespace,
347
+ async handle(request) {
348
+ const url = new URL(request.url);
349
+ if (url.pathname === HEALTH_PATH && request.method === "GET") {
350
+ return json(200, {
351
+ status: "ok",
352
+ service: context.name,
353
+ uptimeMs: context.wallNow() - context.startedAt,
354
+ clock: context.clock.state(),
355
+ namespaces: context.namespaces().length,
356
+ ...context.describe()
357
+ });
358
+ }
359
+ if (url.pathname !== ADMIN_PREFIX && !url.pathname.startsWith(`${ADMIN_PREFIX}/`)) {
360
+ return void 0;
361
+ }
362
+ if (context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
363
+ return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
364
+ }
365
+ const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
366
+ for (const route of routes) {
367
+ if (route.method !== request.method)
368
+ continue;
369
+ const params = matchRoute(route.pattern, path);
370
+ if (!params)
371
+ continue;
372
+ let body;
373
+ try {
374
+ body = await readJson(request);
375
+ } catch {
376
+ return adminError(400, "request body is not valid JSON");
377
+ }
378
+ return route.handler({
379
+ request,
380
+ url,
381
+ params,
382
+ namespace: adminNamespace(request, url),
383
+ body
384
+ });
385
+ }
386
+ return adminError(404, `no admin route ${request.method} ${path}; GET ${ADMIN_PREFIX} lists them`);
387
+ }
388
+ };
389
+ };
390
+
391
+ // ../core/dist/credentials.js
392
+ var basicAuth = (request) => {
393
+ const header = request.headers.get("authorization");
394
+ if (!header)
395
+ return void 0;
396
+ const match = /^Basic\s+(.+)$/i.exec(header.trim());
397
+ if (!match?.[1])
398
+ return void 0;
399
+ let decoded;
400
+ try {
401
+ decoded = atob(match[1].trim());
402
+ } catch {
403
+ return void 0;
404
+ }
405
+ const colon = decoded.indexOf(":");
406
+ if (colon < 0)
407
+ return { username: decoded, password: "" };
408
+ return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };
409
+ };
410
+ var createCredentialRegistry = () => {
411
+ const map = /* @__PURE__ */ new Map();
412
+ return {
413
+ set: (credential, namespace) => {
414
+ map.set(credential, namespace);
415
+ },
416
+ get: (credential) => map.get(credential),
417
+ remove: (credential) => map.delete(credential),
418
+ clear: () => map.clear(),
419
+ entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
420
+ };
421
+ };
422
+ var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
423
+
424
+ // ../core/dist/rng.js
425
+ var seedFrom = (value) => {
426
+ let hash = 2166136261;
427
+ for (let i = 0; i < value.length; i++) {
428
+ hash ^= value.charCodeAt(i);
429
+ hash = Math.imul(hash, 16777619);
430
+ }
431
+ return hash >>> 0;
432
+ };
433
+ var createRng = (seed = 0) => {
434
+ const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
435
+ let state = numeric;
436
+ const next = () => {
437
+ state = state + 1831565813 >>> 0;
438
+ let t = state;
439
+ t = Math.imul(t ^ t >>> 15, t | 1);
440
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
441
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
442
+ };
443
+ return {
444
+ next,
445
+ int: (min, max) => min + Math.floor(next() * (max - min + 1)),
446
+ reset: () => {
447
+ state = numeric;
448
+ },
449
+ state: () => state,
450
+ setState: (next2) => {
451
+ if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
452
+ throw new RangeError("rng state must be an unsigned 32-bit integer");
453
+ }
454
+ state = next2 >>> 0;
455
+ },
456
+ seed: numeric
457
+ };
458
+ };
459
+
460
+ // ../core/dist/faults.js
461
+ var matches = (rule, candidate) => {
462
+ if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
463
+ return false;
464
+ }
465
+ if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
466
+ return false;
467
+ if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
468
+ return false;
469
+ }
470
+ if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
471
+ return false;
472
+ return true;
473
+ };
474
+ var faultResponse = (rule) => {
475
+ const status = rule.status ?? 500;
476
+ const headers = { "content-type": "application/json", ...rule.headers };
477
+ if (typeof rule.body === "string")
478
+ return new Response(rule.body, { status, headers });
479
+ if (rule.body === null)
480
+ return new Response(null, { status, headers: rule.headers ?? {} });
481
+ const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
482
+ return new Response(JSON.stringify(body), { status, headers });
483
+ };
484
+ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
485
+ const entries = [];
486
+ return {
487
+ add(rule) {
488
+ const existing = entries.findIndex((e) => e.rule.id === rule.id);
489
+ const entry = { rule, remaining: rule.count ?? null, hits: 0 };
490
+ if (existing >= 0)
491
+ entries[existing] = entry;
492
+ else
493
+ entries.push(entry);
494
+ return rule;
495
+ },
496
+ list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
497
+ remove(id) {
498
+ const index = entries.findIndex((e) => e.rule.id === id);
499
+ if (index < 0)
500
+ return false;
501
+ entries.splice(index, 1);
502
+ return true;
503
+ },
504
+ clear() {
505
+ entries.length = 0;
506
+ },
507
+ async take(candidate) {
508
+ const hits = [];
509
+ for (const entry of entries) {
510
+ if (entry.remaining === 0)
511
+ continue;
512
+ if (!matches(entry.rule, candidate))
513
+ continue;
514
+ const rate = entry.rule.rate ?? 1;
515
+ if (rng.next() >= rate)
516
+ continue;
517
+ entry.hits++;
518
+ if (entry.remaining !== null)
519
+ entry.remaining--;
520
+ const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
521
+ if (delay !== void 0 && delay > 0) {
522
+ await sleep(delay);
523
+ }
524
+ const hit = { id: entry.rule.id };
525
+ if (entry.rule.effect !== void 0) {
526
+ hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
527
+ }
528
+ if (entry.rule.drop === true)
529
+ hit.drop = true;
530
+ else if (entry.rule.status !== void 0)
531
+ hit.response = faultResponse(entry.rule);
532
+ hits.push(hit);
533
+ if (hit.drop || hit.response)
534
+ break;
535
+ }
536
+ return hits;
537
+ }
538
+ };
539
+ };
540
+
541
+ // ../../openapi/core/dist/refs.js
542
+ var OpenAPIReferenceError = class extends Error {
543
+ ref;
544
+ constructor(ref) {
545
+ super(`unresolvable $ref: ${ref}`);
546
+ this.ref = ref;
547
+ this.name = "OpenAPIReferenceError";
548
+ }
549
+ };
550
+ var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
551
+ var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
552
+ var resolveRef = (document2, ref) => {
553
+ if (!ref.startsWith("#/"))
554
+ throw new OpenAPIReferenceError(ref);
555
+ let cursor = document2;
556
+ for (const raw of ref.slice(2).split("/")) {
557
+ const segment = unescapePointer(raw);
558
+ if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
559
+ throw new OpenAPIReferenceError(ref);
560
+ }
561
+ cursor = cursor[segment];
562
+ }
563
+ if (cursor === void 0)
564
+ throw new OpenAPIReferenceError(ref);
565
+ return cursor;
566
+ };
567
+ var deref = (document2, value) => {
568
+ let current = value;
569
+ const seen = /* @__PURE__ */ new Set();
570
+ while (isReference(current)) {
571
+ if (seen.has(current.$ref))
572
+ throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
573
+ seen.add(current.$ref);
574
+ current = resolveRef(document2, current.$ref);
575
+ }
576
+ return current;
577
+ };
578
+
579
+ // ../../openapi/core/dist/types.js
580
+ var HTTP_METHODS = [
581
+ "get",
582
+ "put",
583
+ "post",
584
+ "delete",
585
+ "options",
586
+ "head",
587
+ "patch",
588
+ "trace"
589
+ ];
590
+
591
+ // ../../openapi/core/dist/document.js
592
+ var mergeParameters = (document2, item, own) => {
593
+ const merged = /* @__PURE__ */ new Map();
594
+ for (const raw of item.parameters ?? []) {
595
+ const parameter = deref(document2, raw);
596
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
597
+ }
598
+ for (const raw of own ?? []) {
599
+ const parameter = deref(document2, raw);
600
+ merged.set(`${parameter.in}:${parameter.name}`, parameter);
601
+ }
602
+ return [...merged.values()];
603
+ };
604
+ var listOperations = (document2) => {
605
+ const operations = [];
606
+ for (const [path, item] of Object.entries(document2.paths)) {
607
+ for (const method of HTTP_METHODS) {
608
+ const operation = item[method];
609
+ if (operation?.operationId === void 0)
610
+ continue;
611
+ const responses = {};
612
+ for (const [status, response] of Object.entries(operation.responses)) {
613
+ responses[status] = deref(document2, response);
614
+ }
615
+ operations.push({
616
+ operationId: operation.operationId,
617
+ method,
618
+ path,
619
+ operation,
620
+ parameters: mergeParameters(document2, item, operation.parameters),
621
+ requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
622
+ responses
623
+ });
624
+ }
625
+ }
626
+ return operations;
627
+ };
628
+
629
+ // ../../openapi/core/dist/schema.js
630
+ var resolveSchema = (document2, schema) => {
631
+ let current = schema;
632
+ const seen = /* @__PURE__ */ new Set();
633
+ while (typeof current.$ref === "string") {
634
+ const ref = current.$ref;
635
+ if (seen.has(ref))
636
+ break;
637
+ seen.add(ref);
638
+ const { $ref: _ignored, ...siblings } = current;
639
+ const target = resolveRef(document2, ref);
640
+ current = { ...target, ...siblings };
641
+ }
642
+ if (current.nullable === true) {
643
+ const { nullable: _nullable, ...rest } = current;
644
+ const types = schemaTypes(rest);
645
+ if (types.length > 0 && !types.includes("null"))
646
+ current = { ...rest, type: [...types, "null"] };
647
+ else
648
+ current = rest;
649
+ }
650
+ return current;
651
+ };
652
+ var schemaTypes = (schema) => {
653
+ if (Array.isArray(schema.type))
654
+ return schema.type;
655
+ if (schema.type !== void 0)
656
+ return [schema.type];
657
+ const inferred = [];
658
+ if (schema.properties || schema.required || schema.additionalProperties !== void 0)
659
+ inferred.push("object");
660
+ if (schema.items || schema.prefixItems || schema.minItems !== void 0 || schema.maxItems !== void 0)
661
+ inferred.push("array");
662
+ if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0)
663
+ inferred.push("string");
664
+ if (schema.minimum !== void 0 || schema.maximum !== void 0 || schema.multipleOf !== void 0)
665
+ inferred.push("number");
666
+ return inferred;
667
+ };
668
+ var jsonTypeOf = (value) => {
669
+ if (value === null)
670
+ return "null";
671
+ if (Array.isArray(value))
672
+ return "array";
673
+ switch (typeof value) {
674
+ case "string":
675
+ return "string";
676
+ case "boolean":
677
+ return "boolean";
678
+ case "number":
679
+ return Number.isInteger(value) ? "integer" : "number";
680
+ case "object":
681
+ return "object";
682
+ default:
683
+ return "undefined";
684
+ }
685
+ };
686
+ var deepEqual = (a, b) => {
687
+ if (a === b)
688
+ return true;
689
+ if (typeof a !== typeof b || a === null || b === null)
690
+ return false;
691
+ if (Array.isArray(a)) {
692
+ return Array.isArray(b) && a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
693
+ }
694
+ if (typeof a === "object" && typeof b === "object" && !Array.isArray(b)) {
695
+ const ka = Object.keys(a);
696
+ const kb = Object.keys(b);
697
+ return ka.length === kb.length && ka.every((k) => deepEqual(a[k], b[k]));
698
+ }
699
+ return false;
700
+ };
701
+ var FORMAT_PATTERNS = {
702
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
703
+ date: /^\d{4}-\d{2}-\d{2}$/,
704
+ "date-time": /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/,
705
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
706
+ uri: /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/,
707
+ ipv4: /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/
708
+ };
709
+ var graphemeLength = (value) => [...value].length;
710
+ var validateValue = (document2, schema, value, path = []) => {
711
+ const errors = [];
712
+ const s = resolveSchema(document2, schema);
713
+ const fail = (message) => errors.push({ path, message });
714
+ const actual = jsonTypeOf(value);
715
+ if (actual === "undefined") {
716
+ fail("value is undefined");
717
+ return errors;
718
+ }
719
+ const types = schemaTypes(s);
720
+ if (types.length > 0) {
721
+ const ok = types.some((t) => t === actual || t === "number" && actual === "integer");
722
+ if (!ok) {
723
+ fail(`expected type ${types.join("|")}, got ${actual}`);
724
+ return errors;
725
+ }
726
+ }
727
+ if (s.enum && !(value === null && types.includes("null")) && !s.enum.some((candidate) => deepEqual(candidate, value))) {
728
+ fail("value not in enum");
729
+ }
730
+ if (s.const !== void 0 && !deepEqual(s.const, value))
731
+ fail("value does not equal const");
732
+ if (typeof value === "string") {
733
+ const length = graphemeLength(value);
734
+ if (s.minLength !== void 0 && length < s.minLength)
735
+ fail(`length ${length} < minLength ${s.minLength}`);
736
+ if (s.maxLength !== void 0 && length > s.maxLength)
737
+ fail(`length ${length} > maxLength ${s.maxLength}`);
738
+ if (s.pattern !== void 0) {
739
+ try {
740
+ if (!new RegExp(s.pattern, "u").test(value))
741
+ fail(`does not match pattern ${s.pattern}`);
742
+ } catch {
743
+ }
744
+ }
745
+ if (s.format !== void 0) {
746
+ const pattern = FORMAT_PATTERNS[s.format];
747
+ if (pattern && !pattern.test(value))
748
+ fail(`does not match format ${s.format}`);
749
+ }
750
+ }
751
+ if (typeof value === "number") {
752
+ if (s.minimum !== void 0 && value < s.minimum)
753
+ fail(`${value} < minimum ${s.minimum}`);
754
+ if (s.maximum !== void 0 && value > s.maximum)
755
+ fail(`${value} > maximum ${s.maximum}`);
756
+ if (s.exclusiveMinimum !== void 0 && value <= s.exclusiveMinimum)
757
+ fail(`${value} <= exclusiveMinimum ${s.exclusiveMinimum}`);
758
+ if (s.exclusiveMaximum !== void 0 && value >= s.exclusiveMaximum)
759
+ fail(`${value} >= exclusiveMaximum ${s.exclusiveMaximum}`);
760
+ if (s.multipleOf !== void 0 && Math.abs(value / s.multipleOf - Math.round(value / s.multipleOf)) > 1e-9) {
761
+ fail(`${value} is not a multiple of ${s.multipleOf}`);
762
+ }
763
+ }
764
+ if (Array.isArray(value)) {
765
+ if (s.minItems !== void 0 && value.length < s.minItems)
766
+ fail(`${value.length} items < minItems ${s.minItems}`);
767
+ if (s.maxItems !== void 0 && value.length > s.maxItems)
768
+ fail(`${value.length} items > maxItems ${s.maxItems}`);
769
+ if (s.uniqueItems && value.some((item, i) => value.slice(0, i).some((prev) => deepEqual(prev, item))))
770
+ fail("items are not unique");
771
+ value.forEach((item, i) => {
772
+ const itemSchema = s.prefixItems?.[i] ?? s.items;
773
+ if (itemSchema)
774
+ errors.push(...validateValue(document2, itemSchema, item, [...path, i]));
775
+ });
776
+ }
777
+ if (actual === "object") {
778
+ const record = value;
779
+ const keys = Object.keys(record);
780
+ for (const name of s.required ?? [])
781
+ if (!(name in record))
782
+ fail(`missing required property ${name}`);
783
+ if (s.minProperties !== void 0 && keys.length < s.minProperties)
784
+ fail(`${keys.length} properties < minProperties ${s.minProperties}`);
785
+ if (s.maxProperties !== void 0 && keys.length > s.maxProperties)
786
+ fail(`${keys.length} properties > maxProperties ${s.maxProperties}`);
787
+ for (const key of keys) {
788
+ const property = s.properties?.[key];
789
+ if (property) {
790
+ errors.push(...validateValue(document2, property, record[key], [...path, key]));
791
+ continue;
792
+ }
793
+ if (s.additionalProperties === false)
794
+ fail(`unexpected property ${key}`);
795
+ else if (typeof s.additionalProperties === "object") {
796
+ errors.push(...validateValue(document2, s.additionalProperties, record[key], [...path, key]));
797
+ }
798
+ if (s.propertyNames) {
799
+ const nameErrors = validateValue(document2, s.propertyNames, key, [...path, key]);
800
+ if (nameErrors.length > 0)
801
+ fail(`property name ${key} is invalid: ${nameErrors[0]?.message}`);
802
+ }
803
+ }
804
+ }
805
+ if (s.allOf)
806
+ for (const branch of s.allOf)
807
+ errors.push(...validateValue(document2, branch, value, path));
808
+ if (s.anyOf && !s.anyOf.some((branch) => validateValue(document2, branch, value).length === 0))
809
+ fail("matches no anyOf branch");
810
+ if (s.oneOf) {
811
+ const matches2 = s.oneOf.filter((branch) => validateValue(document2, branch, value).length === 0).length;
812
+ if (matches2 !== 1)
813
+ fail(`matches ${matches2} oneOf branches, expected exactly 1`);
814
+ }
815
+ if (s.not && validateValue(document2, s.not, value).length === 0)
816
+ fail("matches forbidden `not` schema");
817
+ return errors;
818
+ };
819
+
820
+ // ../../http/codec/dist/form.js
821
+ var parsePath = (rawKey) => {
822
+ const open = rawKey.indexOf("[");
823
+ if (open === -1)
824
+ return [rawKey];
825
+ const path = [rawKey.slice(0, open)];
826
+ const rest = rawKey.slice(open);
827
+ const pattern = /\[([^\]]*)\]/g;
828
+ let match = pattern.exec(rest);
829
+ let consumed = 0;
830
+ while (match !== null) {
831
+ if (match.index !== consumed)
832
+ return [rawKey];
833
+ path.push(match[1] ?? "");
834
+ consumed = match.index + match[0].length;
835
+ match = pattern.exec(rest);
836
+ }
837
+ if (consumed !== rest.length)
838
+ return [rawKey];
839
+ return path;
840
+ };
841
+ var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
842
+ var put = (target, key, value) => {
843
+ if (key === "__proto__") {
844
+ Object.defineProperty(target, key, {
845
+ value,
846
+ enumerable: true,
847
+ writable: true,
848
+ configurable: true
849
+ });
850
+ return;
851
+ }
852
+ ;
853
+ target[key] = value;
854
+ };
855
+ var assign = (target, path, value) => {
856
+ let cursor = target;
857
+ for (let i = 0; i < path.length; i++) {
858
+ const segment = path[i];
859
+ const last = i === path.length - 1;
860
+ if (Array.isArray(cursor)) {
861
+ const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
862
+ if (index === void 0)
863
+ return;
864
+ if (last) {
865
+ put(cursor, index, value);
866
+ return;
867
+ }
868
+ const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
869
+ if (next === void 0 || typeof next === "string") {
870
+ const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
871
+ put(cursor, index, created);
872
+ cursor = created;
873
+ } else {
874
+ cursor = next;
875
+ }
876
+ continue;
877
+ }
878
+ if (typeof cursor === "string")
879
+ return;
880
+ if (last) {
881
+ put(cursor, segment, value);
882
+ return;
883
+ }
884
+ const nextSegment = path[i + 1];
885
+ const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
886
+ if (existing === void 0 || typeof existing === "string") {
887
+ const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
888
+ put(cursor, segment, created);
889
+ cursor = created;
890
+ } else {
891
+ cursor = existing;
892
+ }
893
+ }
894
+ };
895
+ var decodeFormPairs = (pairs) => {
896
+ const out = {};
897
+ for (const [rawKey, value] of pairs)
898
+ assign(out, parsePath(rawKey), value);
899
+ return densify(out);
900
+ };
901
+ var densify = (value) => {
902
+ if (typeof value === "string")
903
+ return value;
904
+ if (Array.isArray(value))
905
+ return value.filter((item) => item !== void 0).map(densify);
906
+ const out = {};
907
+ for (const [key, item] of Object.entries(value))
908
+ put(out, key, densify(item));
909
+ return out;
910
+ };
911
+ var decodeForm = (text) => {
912
+ const source = text.startsWith("?") ? text.slice(1) : text;
913
+ return decodeFormPairs(new URLSearchParams(source).entries());
914
+ };
915
+
916
+ // ../../http/codec/dist/content.js
917
+ var JSON_MEDIA_TYPE = "application/json";
918
+ var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
919
+ var mediaTypeOf = (contentType) => {
920
+ if (!contentType)
921
+ return void 0;
922
+ const essence = contentType.split(";")[0]?.trim().toLowerCase();
923
+ return essence ? essence : void 0;
924
+ };
925
+ var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
926
+ var utf8 = new TextDecoder("utf-8", { fatal: false });
927
+ var decodeBody = (contentType, bytes) => {
928
+ if (bytes.byteLength === 0)
929
+ return { kind: "empty" };
930
+ const mediaType = mediaTypeOf(contentType);
931
+ if (mediaType === void 0)
932
+ return { kind: "bytes", value: bytes };
933
+ if (isJsonMediaType(mediaType)) {
934
+ const text = utf8.decode(bytes);
935
+ try {
936
+ return { kind: "json", value: JSON.parse(text) };
937
+ } catch (error) {
938
+ return {
939
+ kind: "invalid",
940
+ mediaType,
941
+ text,
942
+ error: error instanceof Error ? error.message : String(error)
943
+ };
944
+ }
945
+ }
946
+ if (mediaType === FORM_MEDIA_TYPE) {
947
+ return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
948
+ }
949
+ if (mediaType.startsWith("text/"))
950
+ return { kind: "text", value: utf8.decode(bytes) };
951
+ return { kind: "bytes", value: bytes };
952
+ };
953
+ var readBody = async (message) => {
954
+ const bytes = new Uint8Array(await message.arrayBuffer());
955
+ return decodeBody(message.headers.get("content-type"), bytes);
956
+ };
957
+
958
+ // ../core/dist/http.js
959
+ var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
960
+ status,
961
+ headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
962
+ });
963
+ var HttpError = class extends Error {
964
+ status;
965
+ body;
966
+ headers;
967
+ constructor(status, body, headers = {}) {
968
+ super(`HTTP ${status}`);
969
+ this.status = status;
970
+ this.body = body;
971
+ this.headers = headers;
972
+ this.name = "HttpError";
973
+ }
974
+ toResponse() {
975
+ const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
976
+ if (contentType === "text/plain") {
977
+ return new Response(String(this.body), {
978
+ status: this.status,
979
+ headers: this.headers
980
+ });
981
+ }
982
+ return jsonRes(this.status, this.body, this.headers);
983
+ }
984
+ };
985
+
986
+ // ../core/dist/ids.js
987
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
988
+ var mix = (input) => {
989
+ let hash = 2166136261;
990
+ for (let i = 0; i < input.length; i++) {
991
+ hash ^= input.charCodeAt(i);
992
+ hash = Math.imul(hash, 16777619) >>> 0;
993
+ }
994
+ hash ^= hash >>> 16;
995
+ hash = Math.imul(hash, 2246822507) >>> 0;
996
+ hash ^= hash >>> 13;
997
+ return hash >>> 0;
998
+ };
999
+ var opaqueToken = (input, length) => {
1000
+ let out = "";
1001
+ let round2 = 0;
1002
+ while (out.length < length) {
1003
+ let hash = mix(`${input}:${round2++}`);
1004
+ for (let i = 0; i < 5 && out.length < length; i++) {
1005
+ out += ALPHABET.charAt(hash % ALPHABET.length);
1006
+ hash = Math.floor(hash / ALPHABET.length);
1007
+ }
1008
+ }
1009
+ return out;
1010
+ };
1011
+
1012
+ // ../core/dist/journal.js
1013
+ var DEFAULT_JOURNAL_SIZE = 1e3;
1014
+ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
1015
+ const capacity = Math.max(0, Math.floor(size));
1016
+ const rings = /* @__PURE__ */ new Map();
1017
+ let sequence = 0;
1018
+ const order = /* @__PURE__ */ new WeakMap();
1019
+ const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
1020
+ return {
1021
+ size: capacity,
1022
+ record(entry) {
1023
+ if (capacity === 0)
1024
+ return;
1025
+ order.set(entry, sequence++);
1026
+ let ring = rings.get(entry.namespace);
1027
+ if (!ring) {
1028
+ ring = { entries: [], next: 0 };
1029
+ rings.set(entry.namespace, ring);
1030
+ }
1031
+ if (ring.entries.length < capacity)
1032
+ ring.entries.push(entry);
1033
+ else {
1034
+ ring.entries[ring.next] = entry;
1035
+ ring.next = (ring.next + 1) % capacity;
1036
+ }
1037
+ },
1038
+ list(query = {}) {
1039
+ 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));
1040
+ 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));
1041
+ return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
1042
+ },
1043
+ clear(namespace) {
1044
+ if (namespace === void 0)
1045
+ rings.clear();
1046
+ else
1047
+ rings.delete(namespace);
1048
+ }
1049
+ };
1050
+ };
1051
+ var notes = /* @__PURE__ */ new WeakMap();
1052
+ var annotateResponse = (response, extra) => {
1053
+ const existing = notes.get(response);
1054
+ notes.set(response, {
1055
+ ...existing,
1056
+ ...extra,
1057
+ ...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
1058
+ });
1059
+ return response;
1060
+ };
1061
+ var responseNotes = (response) => notes.get(response);
1062
+
1063
+ // ../core/dist/metrics.js
1064
+ var createMetrics = () => {
1065
+ let requests = 0;
1066
+ let faults = 0;
1067
+ let totalDurationMs = 0;
1068
+ const byOperation = /* @__PURE__ */ new Map();
1069
+ const unmatched = /* @__PURE__ */ new Map();
1070
+ return {
1071
+ record(entry) {
1072
+ requests++;
1073
+ totalDurationMs += entry.durationMs;
1074
+ if (entry.faultId !== void 0)
1075
+ faults++;
1076
+ const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
1077
+ byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
1078
+ if (entry.unmatched) {
1079
+ const route = `${entry.method} ${entry.path}`;
1080
+ unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
1081
+ }
1082
+ },
1083
+ report: () => ({
1084
+ requests,
1085
+ byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
1086
+ unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
1087
+ const space = route.indexOf(" ");
1088
+ return { method: route.slice(0, space), path: route.slice(space + 1), count };
1089
+ }),
1090
+ faults,
1091
+ totalDurationMs
1092
+ }),
1093
+ reset() {
1094
+ requests = 0;
1095
+ faults = 0;
1096
+ totalDurationMs = 0;
1097
+ byOperation.clear();
1098
+ unmatched.clear();
1099
+ }
1100
+ };
1101
+ };
1102
+
1103
+ // ../../core/dist/timeline.js
1104
+ var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1105
+ var Timeline = class {
1106
+ maxCheckpoints;
1107
+ now;
1108
+ makeId;
1109
+ nodes = /* @__PURE__ */ new Map();
1110
+ heads = /* @__PURE__ */ new Map();
1111
+ /** Unreferenced nodes in the exact order they became collectible. */
1112
+ evictable = /* @__PURE__ */ new Set();
1113
+ /** Branch heads plus explicit retainers. Absent means zero. */
1114
+ references = /* @__PURE__ */ new Map();
1115
+ explicitPins = /* @__PURE__ */ new Map();
1116
+ sequence = 0;
1117
+ constructor(options = {}) {
1118
+ const max = options.maxCheckpoints ?? 1e3;
1119
+ if (!Number.isSafeInteger(max) || max < 1)
1120
+ throw new RangeError("maxCheckpoints must be a positive integer");
1121
+ this.maxCheckpoints = max;
1122
+ this.now = options.now ?? (() => this.sequence);
1123
+ this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
1124
+ }
1125
+ /** Capture a new immutable value and move `branch` to it. */
1126
+ commit(value, options = {}) {
1127
+ const branch = options.branch ?? "main";
1128
+ this.assertBranch(branch);
1129
+ const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
1130
+ if (parent !== null && !this.nodes.has(parent))
1131
+ throw new RangeError(`no checkpoint ${parent}`);
1132
+ const id = this.makeId(++this.sequence);
1133
+ if (this.nodes.has(id))
1134
+ throw new RangeError(`duplicate checkpoint id ${id}`);
1135
+ const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
1136
+ this.nodes.set(id, checkpoint);
1137
+ this.moveHead(branch, id);
1138
+ this.collect(this.maxCheckpoints);
1139
+ return checkpoint;
1140
+ }
1141
+ /** Create a branch pointer without copying its checkpoint value. */
1142
+ fork(branch, options = {}) {
1143
+ this.assertBranch(branch);
1144
+ if (this.heads.has(branch))
1145
+ throw new RangeError(`branch already exists: ${branch}`);
1146
+ const from = options.from ?? this.heads.get("main");
1147
+ if (from === void 0)
1148
+ return void 0;
1149
+ const checkpoint = this.get(from);
1150
+ this.moveHead(branch, checkpoint.id);
1151
+ return checkpoint;
1152
+ }
1153
+ /** Move a branch pointer to an existing checkpoint. */
1154
+ checkout(branch, id) {
1155
+ this.assertBranch(branch);
1156
+ const checkpoint = this.get(id);
1157
+ this.moveHead(branch, checkpoint.id);
1158
+ return checkpoint;
1159
+ }
1160
+ get(id) {
1161
+ const checkpoint = this.nodes.get(id);
1162
+ if (!checkpoint)
1163
+ throw new RangeError(`no checkpoint ${id}`);
1164
+ return checkpoint;
1165
+ }
1166
+ head(branch = "main") {
1167
+ const id = this.heads.get(branch);
1168
+ return id === void 0 ? void 0 : this.get(id);
1169
+ }
1170
+ hasBranch(branch) {
1171
+ return this.heads.has(branch);
1172
+ }
1173
+ branches() {
1174
+ return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
1175
+ }
1176
+ checkpoints() {
1177
+ return [...this.nodes.values()];
1178
+ }
1179
+ /** Number of retained checkpoints without allocating an array. */
1180
+ get size() {
1181
+ return this.nodes.size;
1182
+ }
1183
+ /** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
1184
+ retain(id) {
1185
+ const checkpoint = this.get(id);
1186
+ this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
1187
+ this.addReference(id);
1188
+ return checkpoint;
1189
+ }
1190
+ /** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
1191
+ release(id) {
1192
+ if (!this.nodes.has(id))
1193
+ return false;
1194
+ const pins = this.explicitPins.get(id) ?? 0;
1195
+ if (pins === 0)
1196
+ return false;
1197
+ if (pins === 1)
1198
+ this.explicitPins.delete(id);
1199
+ else
1200
+ this.explicitPins.set(id, pins - 1);
1201
+ this.removeReference(id);
1202
+ this.collect(this.maxCheckpoints);
1203
+ return true;
1204
+ }
1205
+ deleteBranch(branch) {
1206
+ if (branch === "main")
1207
+ throw new RangeError("cannot delete main branch");
1208
+ const previous = this.heads.get(branch);
1209
+ const deleted = this.heads.delete(branch);
1210
+ if (previous !== void 0)
1211
+ this.removeReference(previous);
1212
+ this.collect(this.maxCheckpoints);
1213
+ return deleted;
1214
+ }
1215
+ /**
1216
+ * Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
1217
+ * commits never scan pinned nodes or the retained history. Parents are metadata rather than a
1218
+ * storage dependency, so a retained node remains usable after pruning.
1219
+ */
1220
+ gc(max = this.maxCheckpoints) {
1221
+ if (!Number.isSafeInteger(max) || max < 1)
1222
+ throw new RangeError("max must be a positive integer");
1223
+ const removed = [];
1224
+ this.collect(max, removed);
1225
+ return removed;
1226
+ }
1227
+ collect(max, removed) {
1228
+ while (this.nodes.size > max && this.evictable.size > 0) {
1229
+ const id = this.evictable.values().next().value;
1230
+ this.evictable.delete(id);
1231
+ this.nodes.delete(id);
1232
+ removed?.push(id);
1233
+ }
1234
+ }
1235
+ moveHead(branch, id) {
1236
+ const previous = this.heads.get(branch);
1237
+ if (previous === id)
1238
+ return;
1239
+ if (previous !== void 0)
1240
+ this.removeReference(previous);
1241
+ this.heads.set(branch, id);
1242
+ this.addReference(id);
1243
+ }
1244
+ addReference(id) {
1245
+ this.references.set(id, (this.references.get(id) ?? 0) + 1);
1246
+ this.evictable.delete(id);
1247
+ }
1248
+ removeReference(id) {
1249
+ const next = (this.references.get(id) ?? 0) - 1;
1250
+ if (next > 0)
1251
+ this.references.set(id, next);
1252
+ else {
1253
+ this.references.delete(id);
1254
+ if (this.nodes.has(id))
1255
+ this.evictable.add(id);
1256
+ }
1257
+ }
1258
+ assertBranch(branch) {
1259
+ if (!BRANCH_PATTERN.test(branch))
1260
+ throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
1261
+ }
1262
+ };
1263
+
1264
+ // ../../sqlite/dist/default.js
1265
+ import { Database } from "@crvouga/mockingbird-service-sqlite";
1266
+ var createDefaultSqlite = () => new Database();
1267
+ var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
1268
+
1269
+ // ../../sqlite/dist/migrate.js
1270
+ var ensureMigrationsTable = (sqlite) => {
1271
+ sqlite.exec(`
1272
+ CREATE TABLE IF NOT EXISTS schema_migrations (
1273
+ id TEXT PRIMARY KEY NOT NULL,
1274
+ applied_at INTEGER NOT NULL
1275
+ )
1276
+ `);
1277
+ };
1278
+ var migrate = (sqlite, migrations) => {
1279
+ ensureMigrationsTable(sqlite);
1280
+ const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
1281
+ const pending = migrations.filter((migration) => !applied.has(migration.id));
1282
+ if (pending.length === 0)
1283
+ return;
1284
+ const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
1285
+ const now = Math.floor(Date.now() / 1e3);
1286
+ sqlite.transaction(() => {
1287
+ for (const migration of pending) {
1288
+ sqlite.exec(migration.sql);
1289
+ insert.run(migration.id, now);
1290
+ }
1291
+ });
1292
+ };
1293
+
1294
+ // ../../sqlite/dist/schema.js
1295
+ var CORE_MIGRATIONS = [
1296
+ {
1297
+ id: "20260322_core_records_sequences",
1298
+ sql: `
1299
+ CREATE TABLE IF NOT EXISTS mockingbird_records (
1300
+ namespace TEXT NOT NULL,
1301
+ collection TEXT NOT NULL,
1302
+ id TEXT NOT NULL,
1303
+ seq INTEGER NOT NULL,
1304
+ value TEXT NOT NULL,
1305
+ PRIMARY KEY (namespace, collection, id)
1306
+ );
1307
+ CREATE INDEX IF NOT EXISTS mockingbird_records_seq
1308
+ ON mockingbird_records (namespace, collection, seq);
1309
+ CREATE TABLE IF NOT EXISTS mockingbird_sequences (
1310
+ namespace TEXT NOT NULL,
1311
+ name TEXT NOT NULL,
1312
+ kind TEXT NOT NULL,
1313
+ value INTEGER NOT NULL,
1314
+ PRIMARY KEY (namespace, name, kind)
1315
+ );
1316
+ `
1317
+ }
1318
+ ];
1319
+ var migrateCore = (sqlite) => {
1320
+ migrate(sqlite, CORE_MIGRATIONS);
1321
+ };
1322
+ var clearNamespace = (sqlite, namespace) => {
1323
+ sqlite.transaction(() => {
1324
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1325
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1326
+ });
1327
+ };
1328
+
1329
+ // ../../openapi/metadata/dist/types.js
1330
+ var EXTENSION_KEYS = {
1331
+ operation: "x-mockingbird",
1332
+ resource: "x-mockingbird-resource",
1333
+ resourceRef: "x-mockingbird-resource-ref",
1334
+ volatile: "x-mockingbird-volatile",
1335
+ scope: "x-mockingbird-scope",
1336
+ unsupported: "x-mockingbird-unsupported",
1337
+ parityHeader: "x-mockingbird-parity-header"
1338
+ };
1339
+
1340
+ // ../../openapi/metadata/dist/read.js
1341
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1342
+ var extensionOf = (holder, key) => holder[key];
1343
+ var operationMetadata = (operation) => {
1344
+ const raw = extensionOf(operation, EXTENSION_KEYS.operation);
1345
+ const ext = isRecord2(raw) ? raw : {};
1346
+ const supported = ext.supported ?? true;
1347
+ const parity = ext.parity ?? {};
1348
+ return {
1349
+ supported,
1350
+ reason: typeof ext.reason === "string" ? ext.reason : void 0,
1351
+ parity: {
1352
+ enabled: supported && (parity.enabled ?? true),
1353
+ safe: parity.safe ?? true,
1354
+ reason: typeof parity.reason === "string" ? parity.reason : void 0
1355
+ }
1356
+ };
1357
+ };
1358
+
1359
+ // ../core/dist/service.js
1360
+ import { Hono } from "hono";
1361
+ var defineOperations = (handlers) => handlers;
1362
+ var OperationRegistryError = class extends Error {
1363
+ problems;
1364
+ constructor(problems) {
1365
+ super(`operation registry is inconsistent:
1366
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
1367
+ this.problems = problems;
1368
+ this.name = "OperationRegistryError";
1369
+ }
1370
+ };
1371
+ var verifyOperations = (document2, handlers) => {
1372
+ const problems = [];
1373
+ const operations = listOperations(document2);
1374
+ const seen = /* @__PURE__ */ new Set();
1375
+ for (const operation of operations) {
1376
+ if (seen.has(operation.operationId))
1377
+ problems.push(`duplicate operationId ${operation.operationId}`);
1378
+ seen.add(operation.operationId);
1379
+ const supported = operationMetadata(operation.operation).supported;
1380
+ const handler = handlers[operation.operationId];
1381
+ if (supported && !handler)
1382
+ problems.push(`supported operation ${operation.operationId} has no handler`);
1383
+ if (!supported && handler)
1384
+ problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
1385
+ }
1386
+ for (const id of Object.keys(handlers)) {
1387
+ if (!seen.has(id))
1388
+ problems.push(`handler ${id} has no OpenAPI operation`);
1389
+ }
1390
+ return problems;
1391
+ };
1392
+ var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
1393
+ var routeOrder = (a, b) => {
1394
+ const sa = a.path.split("/");
1395
+ const sb = b.path.split("/");
1396
+ for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
1397
+ const x = sa[i] ?? "";
1398
+ const y = sb[i] ?? "";
1399
+ const px = x.startsWith("{");
1400
+ const py = y.startsWith("{");
1401
+ if (px !== py)
1402
+ return px ? 1 : -1;
1403
+ if (x !== y)
1404
+ return x < y ? -1 : 1;
1405
+ }
1406
+ return 0;
1407
+ };
1408
+ var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
1409
+ var bootSqlite = (sqlite) => {
1410
+ const client = resolveSqlite(sqlite);
1411
+ migrateCore(client);
1412
+ return client;
1413
+ };
1414
+ var createService = (options) => {
1415
+ const problems = verifyOperations(options.document, options.handlers);
1416
+ if (problems.length > 0)
1417
+ throw new OperationRegistryError(problems);
1418
+ migrateCore(options.sqlite);
1419
+ const now = options.now ?? (() => Date.now());
1420
+ const app = new Hono();
1421
+ app.notFound((c) => options.notFound(c.req.raw));
1422
+ app.onError((error, c) => options.onError(error, c.req.raw));
1423
+ const operations = [...listOperations(options.document)].sort(routeOrder);
1424
+ for (const operation of operations) {
1425
+ const metadata = operationMetadata(operation.operation);
1426
+ const handler = options.handlers[operation.operationId];
1427
+ const route = async (c) => {
1428
+ const request = c.req.raw;
1429
+ if (!metadata.supported || !handler) {
1430
+ return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
1431
+ }
1432
+ const url = new URL(request.url);
1433
+ const context = {
1434
+ request,
1435
+ url,
1436
+ params: c.req.param(),
1437
+ query: queryOf(url),
1438
+ body: await readBody(request),
1439
+ sqlite: options.sqlite,
1440
+ namespace: options.namespace,
1441
+ operation,
1442
+ document: options.document,
1443
+ now
1444
+ };
1445
+ const short = await options.before?.(context);
1446
+ if (short)
1447
+ return short;
1448
+ return handler(context);
1449
+ };
1450
+ app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1451
+ }
1452
+ return {
1453
+ app,
1454
+ sqlite: options.sqlite,
1455
+ namespace: options.namespace,
1456
+ fetch: async (request) => app.fetch(request),
1457
+ reset: async () => {
1458
+ clearNamespace(options.sqlite, options.namespace);
1459
+ }
1460
+ };
1461
+ };
1462
+
1463
+ // ../core/dist/snapshot.js
1464
+ var snapshotNamespace = (sqlite, namespace) => ({
1465
+ namespace,
1466
+ records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
1467
+ sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
1468
+ });
1469
+ var restoreNamespace = (sqlite, namespace, snapshot) => {
1470
+ sqlite.transaction(() => {
1471
+ sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
1472
+ sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
1473
+ const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
1474
+ for (const row of snapshot.records) {
1475
+ record.run(namespace, row.collection, row.id, row.seq, row.value);
1476
+ }
1477
+ const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
1478
+ for (const row of snapshot.sequences) {
1479
+ sequence.run(namespace, row.name, row.kind, row.value);
1480
+ }
1481
+ });
1482
+ };
1483
+
1484
+ // ../core/dist/version.js
1485
+ var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
1486
+
1487
+ // ../core/dist/signing.js
1488
+ var encoder = new TextEncoder();
1489
+ var toBase64 = (bytes) => {
1490
+ let binary = "";
1491
+ for (const byte of bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)) {
1492
+ binary += String.fromCharCode(byte);
1493
+ }
1494
+ return btoa(binary);
1495
+ };
1496
+ var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
1497
+
1498
+ // ../core/dist/webhooks.js
1499
+ var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
1500
+ var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
1501
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1502
+ var parseEndpoint = (value) => {
1503
+ if (!isRecord3(value) || typeof value.url !== "string")
1504
+ return "each endpoint needs a url";
1505
+ try {
1506
+ new URL(value.url);
1507
+ } catch {
1508
+ return `not a URL: ${value.url}`;
1509
+ }
1510
+ const endpoint = { url: value.url };
1511
+ if (typeof value.id === "string")
1512
+ endpoint.id = value.id;
1513
+ if (typeof value.secret === "string")
1514
+ endpoint.secret = value.secret;
1515
+ if (typeof value.signUrl === "string")
1516
+ endpoint.signUrl = value.signUrl;
1517
+ const events = value.events ?? value.enabledEvents;
1518
+ if (Array.isArray(events))
1519
+ endpoint.events = events.map(String);
1520
+ if (isRecord3(value.tags)) {
1521
+ endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
1522
+ }
1523
+ if (typeof value.account === "string")
1524
+ endpoint.tags = { ...endpoint.tags, account: value.account };
1525
+ if (isRecord3(value.headers)) {
1526
+ endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
1527
+ }
1528
+ return endpoint;
1529
+ };
1530
+ var webhookAdminRoutes = (hub) => ({
1531
+ "GET /webhooks": ({ url, namespace }) => json2(200, {
1532
+ deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
1533
+ const type = url.searchParams.get("type");
1534
+ return type === null || d.type === type;
1535
+ })
1536
+ }),
1537
+ "GET /webhooks/events": ({ url, namespace }) => {
1538
+ const type = url.searchParams.get("type");
1539
+ return json2(200, {
1540
+ events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m2) => type === null || m2.type === type).map((m2) => ({ ...m2, payload: parsePayload(m2) }))
1541
+ });
1542
+ },
1543
+ "POST /webhooks/:id/replay": async ({ params }) => {
1544
+ const replayed = await hub.replay(params.id);
1545
+ return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
1546
+ },
1547
+ "POST /webhooks/flush": async () => {
1548
+ await hub.flush();
1549
+ return json2(200, { status: "ok" });
1550
+ },
1551
+ "POST /webhooks/faults": ({ body, namespace }) => {
1552
+ if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
1553
+ return adminError2(400, "mode must be duplicate, reorder or drop");
1554
+ }
1555
+ const fault = { mode: body.mode };
1556
+ if (typeof body.count === "number")
1557
+ fault.count = body.count;
1558
+ hub.fault(namespace, fault);
1559
+ return json2(201, { namespace, ...fault });
1560
+ },
1561
+ "GET /webhook-endpoints": ({ namespace }) => json2(200, {
1562
+ endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
1563
+ ...rest,
1564
+ secret: secret ? "(set)" : null
1565
+ }))
1566
+ }),
1567
+ "PUT /webhook-endpoints": ({ body, namespace }) => {
1568
+ const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
1569
+ if (!Array.isArray(list))
1570
+ return adminError2(400, "expected [{url, secret?, events?}]");
1571
+ const parsed = [];
1572
+ for (const each of list) {
1573
+ const endpoint = parseEndpoint(each);
1574
+ if (typeof endpoint === "string")
1575
+ return adminError2(400, endpoint);
1576
+ parsed.push(endpoint);
1577
+ }
1578
+ const set = hub.setEndpoints(namespace, parsed);
1579
+ return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
1580
+ },
1581
+ "DELETE /webhook-endpoints": ({ namespace }) => {
1582
+ hub.setEndpoints(namespace, []);
1583
+ return json2(200, { status: "ok" });
1584
+ }
1585
+ });
1586
+ var parsePayload = (message) => {
1587
+ if (message.contentType.startsWith("application/json")) {
1588
+ try {
1589
+ return JSON.parse(message.body);
1590
+ } catch {
1591
+ return message.body;
1592
+ }
1593
+ }
1594
+ if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
1595
+ return Object.fromEntries(new URLSearchParams(message.body));
1596
+ }
1597
+ return message.body;
1598
+ };
1599
+
1600
+ // ../core/dist/runtime.js
1601
+ var MOCKINGBIRD_HEADER = "x-mockingbird";
1602
+ var BRANCH_HEADER = "x-mockingbird-branch";
1603
+ var AT_HEADER = "x-mockingbird-at";
1604
+ var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
1605
+ var DEFAULT_NAMESPACE = "default";
1606
+ var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
1607
+ var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
1608
+ var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
1609
+ var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
1610
+ var effects = /* @__PURE__ */ new WeakMap();
1611
+ var reuseSorted = (fresh, previous, compare, equal) => {
1612
+ if (!previous || previous.length === 0)
1613
+ return fresh.map((row) => Object.freeze(row));
1614
+ const result = new Array(fresh.length);
1615
+ let unchanged = fresh.length === previous.length;
1616
+ let oldIndex = 0;
1617
+ for (let index = 0; index < fresh.length; index++) {
1618
+ const row = fresh[index];
1619
+ while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
1620
+ oldIndex++;
1621
+ }
1622
+ const old = previous[oldIndex];
1623
+ result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
1624
+ if (result[index] !== previous[index])
1625
+ unchanged = false;
1626
+ }
1627
+ return unchanged ? previous : result;
1628
+ };
1629
+ var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
1630
+ var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
1631
+ var DroppedConnectionError = class extends TypeError {
1632
+ code = "MOCKINGBIRD_DROP";
1633
+ constructor() {
1634
+ super("fetch failed: connection dropped by Mockingbird fault");
1635
+ this.name = "TypeError";
1636
+ }
1637
+ };
1638
+ var operationMatcher = (document2) => {
1639
+ const matchers = listOperations(document2).map((operation) => ({
1640
+ operationId: operation.operationId,
1641
+ method: operation.method.toUpperCase(),
1642
+ pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
1643
+ params: (operation.path.match(/\{/g) ?? []).length
1644
+ })).sort((a, b) => a.params - b.params);
1645
+ return (request, path) => matchers.find((m2) => m2.method === request.method && m2.pattern.test(path))?.operationId;
1646
+ };
1647
+ var createRuntime = (options) => {
1648
+ const sqlite = bootSqlite(options.sqlite);
1649
+ const clock = options.clock ?? createClock();
1650
+ const rng = createRng(options.seed ?? 0);
1651
+ const wallNow = options.io?.wallNow ?? Date.now;
1652
+ const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
1653
+ const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1654
+ const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
1655
+ const metrics = createMetrics();
1656
+ const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
1657
+ const version = options.version ?? PACKAGE_VERSION;
1658
+ const instances = /* @__PURE__ */ new Map();
1659
+ const publicNamespaces = /* @__PURE__ */ new Set();
1660
+ const branchRngs = /* @__PURE__ */ new Map();
1661
+ const timelines = /* @__PURE__ */ new Map();
1662
+ const branchStorage = /* @__PURE__ */ new Map();
1663
+ const captured = /* @__PURE__ */ new Map();
1664
+ const credentials = createCredentialRegistry();
1665
+ const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
1666
+ const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
1667
+ const instanceFor = (key, publicNamespace = key, isolatedRng) => {
1668
+ const existing = instances.get(key);
1669
+ if (existing)
1670
+ return existing;
1671
+ if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
1672
+ throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
1673
+ }
1674
+ const created = options.create({
1675
+ namespace: storageNamespace(key),
1676
+ publicNamespace,
1677
+ sqlite,
1678
+ clock,
1679
+ rng: isolatedRng ?? rng
1680
+ });
1681
+ instances.set(key, created);
1682
+ publicNamespaces.add(publicNamespace);
1683
+ if (isolatedRng)
1684
+ branchRngs.set(key, isolatedRng);
1685
+ return created;
1686
+ };
1687
+ const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
1688
+ const capture = (storage) => {
1689
+ const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
1690
+ const previous = captured.get(storage);
1691
+ const snapshot2 = {
1692
+ namespace: fresh.namespace,
1693
+ 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),
1694
+ 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)
1695
+ };
1696
+ Object.freeze(snapshot2.records);
1697
+ Object.freeze(snapshot2.sequences);
1698
+ Object.freeze(snapshot2);
1699
+ captured.set(storage, snapshot2);
1700
+ return Object.freeze({
1701
+ snapshot: snapshot2,
1702
+ clock: Object.freeze(clock.state()),
1703
+ rngState: (branchRngs.get(storage) ?? rng).state()
1704
+ });
1705
+ };
1706
+ const timeline = (name = DEFAULT_NAMESPACE) => {
1707
+ let found = timelines.get(name);
1708
+ if (found)
1709
+ return found;
1710
+ instance(name);
1711
+ found = new Timeline({
1712
+ now: clock.now,
1713
+ ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
1714
+ });
1715
+ found.commit(capture(name));
1716
+ timelines.set(name, found);
1717
+ return found;
1718
+ };
1719
+ const physicalBranch = (namespace, branch2) => {
1720
+ if (branch2 === "main")
1721
+ return namespace;
1722
+ const mapKey = `${namespace}\0${branch2}`;
1723
+ const existing = branchStorage.get(mapKey);
1724
+ if (existing)
1725
+ return existing;
1726
+ const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
1727
+ branchStorage.set(mapKey, key);
1728
+ return key;
1729
+ };
1730
+ const ensureBranch = (namespace, branch2, at) => {
1731
+ if (!BRANCH_PATTERN2.test(branch2))
1732
+ throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
1733
+ const history = timeline(namespace);
1734
+ if (branch2 === "main") {
1735
+ if (at !== void 0) {
1736
+ const point = history.checkout("main", at);
1737
+ restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
1738
+ captured.set(namespace, point.value.snapshot);
1739
+ rng.setState(point.value.rngState);
1740
+ clock.set(point.value.clock.now);
1741
+ if (point.value.clock.frozen)
1742
+ clock.freeze();
1743
+ else
1744
+ clock.unfreeze();
1745
+ }
1746
+ return namespace;
1747
+ }
1748
+ const storage = physicalBranch(namespace, branch2);
1749
+ if (!history.hasBranch(branch2)) {
1750
+ if (at === void 0)
1751
+ history.commit(capture(namespace));
1752
+ const point = history.fork(branch2, at === void 0 ? {} : { from: at });
1753
+ const branchRng = createRng(options.seed ?? 0);
1754
+ if (point)
1755
+ branchRng.setState(point.value.rngState);
1756
+ instanceFor(storage, namespace, branchRng);
1757
+ if (point)
1758
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1759
+ if (point)
1760
+ captured.set(storage, point.value.snapshot);
1761
+ } else if (at !== void 0 && history.head(branch2)?.id !== at) {
1762
+ const point = history.checkout(branch2, at);
1763
+ if (!instances.has(storage)) {
1764
+ const branchRng = createRng(options.seed ?? 0);
1765
+ branchRng.setState(point.value.rngState);
1766
+ instanceFor(storage, namespace, branchRng);
1767
+ }
1768
+ branchRngs.get(storage)?.setState(point.value.rngState);
1769
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1770
+ captured.set(storage, point.value.snapshot);
1771
+ } else {
1772
+ if (!instances.has(storage)) {
1773
+ const point = history.head(branch2);
1774
+ const branchRng = createRng(options.seed ?? 0);
1775
+ if (point)
1776
+ branchRng.setState(point.value.rngState);
1777
+ instanceFor(storage, namespace, branchRng);
1778
+ }
1779
+ }
1780
+ return storage;
1781
+ };
1782
+ const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
1783
+ const storage = ensureBranch(namespace, branch2);
1784
+ return timeline(namespace).commit(capture(storage), { branch: branch2 });
1785
+ };
1786
+ const branch = (name, branchOptions = {}) => {
1787
+ const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
1788
+ ensureBranch(namespace, name, branchOptions.at);
1789
+ const head = timeline(namespace).head(name);
1790
+ if (!head)
1791
+ throw new RangeError(`branch ${name} has no checkpoint`);
1792
+ return head;
1793
+ };
1794
+ const checkout = (checkpointId, checkoutOptions = {}) => {
1795
+ const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
1796
+ const branchName = checkoutOptions.branch ?? "main";
1797
+ const history = timeline(namespace);
1798
+ const point = history.checkout(branchName, checkpointId);
1799
+ const storage = ensureBranch(namespace, branchName);
1800
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1801
+ captured.set(storage, point.value.snapshot);
1802
+ clock.set(point.value.clock.now);
1803
+ if (point.value.clock.frozen)
1804
+ clock.freeze();
1805
+ else
1806
+ clock.unfreeze();
1807
+ (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
1808
+ };
1809
+ const reset = async (name = DEFAULT_NAMESPACE) => {
1810
+ if (name === "*") {
1811
+ options.webhooks?.clear();
1812
+ for (const each of instances.values())
1813
+ await each.reset();
1814
+ timelines.clear();
1815
+ branchStorage.clear();
1816
+ branchRngs.clear();
1817
+ captured.clear();
1818
+ return;
1819
+ }
1820
+ options.webhooks?.clear(name);
1821
+ const target = instances.get(name);
1822
+ if (target)
1823
+ await target.reset();
1824
+ else
1825
+ clearNamespace(sqlite, storageNamespace(name));
1826
+ for (const [mapping, storage] of branchStorage) {
1827
+ if (!mapping.startsWith(`${name}\0`))
1828
+ continue;
1829
+ const branchInstance = instances.get(storage);
1830
+ if (branchInstance)
1831
+ await branchInstance.reset();
1832
+ else
1833
+ clearNamespace(sqlite, storageNamespace(storage));
1834
+ branchStorage.delete(mapping);
1835
+ branchRngs.delete(storage);
1836
+ captured.delete(storage);
1837
+ }
1838
+ timelines.delete(name);
1839
+ captured.delete(name);
1840
+ };
1841
+ const snapshot = (name = DEFAULT_NAMESPACE) => {
1842
+ return checkpoint(name, "main").value.snapshot;
1843
+ };
1844
+ const restore = (from, name = DEFAULT_NAMESPACE) => {
1845
+ instance(name);
1846
+ restoreNamespace(sqlite, storageNamespace(name), from);
1847
+ captured.set(name, from);
1848
+ const history = timelines.get(name);
1849
+ if (history)
1850
+ history.commit(capture(name), { branch: "main" });
1851
+ else
1852
+ timeline(name);
1853
+ };
1854
+ const runtime = {
1855
+ name: options.name,
1856
+ sqlite,
1857
+ clock,
1858
+ faults,
1859
+ metrics,
1860
+ journal,
1861
+ rng,
1862
+ credentials,
1863
+ webhooks: options.webhooks,
1864
+ applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
1865
+ const preset = options.presets?.[name];
1866
+ if (!preset)
1867
+ throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
1868
+ const added = (preset.rules ?? []).map((rule, index) => faults.add({
1869
+ namespace,
1870
+ ...rule,
1871
+ ...overrides,
1872
+ preset: name,
1873
+ id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
1874
+ }));
1875
+ if (preset.webhook && options.webhooks) {
1876
+ options.webhooks.fault(namespace, {
1877
+ ...preset.webhook,
1878
+ ...overrides.count !== void 0 ? { count: overrides.count } : {}
1879
+ });
1880
+ }
1881
+ return added;
1882
+ },
1883
+ instance,
1884
+ namespaces: () => [...publicNamespaces].sort(),
1885
+ reset,
1886
+ snapshot,
1887
+ restore,
1888
+ checkpoint,
1889
+ branch,
1890
+ checkout,
1891
+ timeline,
1892
+ fetch: async (incoming) => {
1893
+ let request = incoming;
1894
+ const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
1895
+ if (prefixed) {
1896
+ const url2 = new URL(request.url);
1897
+ url2.pathname = prefixed[2] ?? "/";
1898
+ const headers = new Headers(request.headers);
1899
+ if (!headers.has(NAMESPACE_HEADER)) {
1900
+ headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
1901
+ }
1902
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
1903
+ request = new Request(url2, {
1904
+ method: request.method,
1905
+ headers,
1906
+ ...hasBody ? { body: await request.arrayBuffer() } : {},
1907
+ signal: request.signal
1908
+ });
1909
+ }
1910
+ let namespace = control.namespaceOf(request);
1911
+ if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
1912
+ const credential = options.credential(request);
1913
+ const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
1914
+ if (mapped !== void 0)
1915
+ namespace = mapped;
1916
+ }
1917
+ const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
1918
+ const at = request.headers.get(AT_HEADER) ?? void 0;
1919
+ const stamp = (response2) => {
1920
+ const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
1921
+ try {
1922
+ response2.headers.set(MOCKINGBIRD_HEADER, value);
1923
+ return response2;
1924
+ } catch {
1925
+ const copy = new Response(response2.body, response2);
1926
+ copy.headers.set(MOCKINGBIRD_HEADER, value);
1927
+ return copy;
1928
+ }
1929
+ };
1930
+ const handled = await control.handle(request);
1931
+ if (handled)
1932
+ return stamp(handled);
1933
+ const started = monotonicNow();
1934
+ const url = new URL(request.url);
1935
+ const operationId = operationIdFor(request, url.pathname);
1936
+ const log = (status, faultId, response2) => {
1937
+ const noted = response2 ? responseNotes(response2) : void 0;
1938
+ const entry = {
1939
+ service: options.name,
1940
+ namespace,
1941
+ operationId,
1942
+ method: request.method,
1943
+ path: url.pathname,
1944
+ status,
1945
+ durationMs: Math.round((monotonicNow() - started) * 100) / 100,
1946
+ unmatched: options.document !== void 0 && operationId === void 0,
1947
+ ...faultId !== void 0 ? { faultId } : {},
1948
+ ...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
1949
+ ...noted?.adopted ? { adopted: true } : {}
1950
+ };
1951
+ metrics.record(entry);
1952
+ journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
1953
+ options.onLog?.(entry);
1954
+ };
1955
+ if (!NAMESPACE_PATTERN.test(namespace)) {
1956
+ log(400);
1957
+ return stamp(new Response(JSON.stringify({
1958
+ error: {
1959
+ type: "mockingbird_admin",
1960
+ message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
1961
+ }
1962
+ }), { status: 400, headers: { "content-type": "application/json" } }));
1963
+ }
1964
+ if (!BRANCH_PATTERN2.test(selectedBranch)) {
1965
+ log(400);
1966
+ return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
1967
+ }
1968
+ let storage;
1969
+ try {
1970
+ if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
1971
+ const point = timeline(namespace).get(at);
1972
+ storage = physicalBranch(namespace, `at_${at}`);
1973
+ let viewRng = branchRngs.get(storage);
1974
+ if (!viewRng) {
1975
+ viewRng = createRng(options.seed ?? 0);
1976
+ instanceFor(storage, namespace, viewRng);
1977
+ }
1978
+ viewRng.setState(point.value.rngState);
1979
+ restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
1980
+ captured.set(storage, point.value.snapshot);
1981
+ } else {
1982
+ storage = ensureBranch(namespace, selectedBranch, at);
1983
+ }
1984
+ } catch (error) {
1985
+ log(409);
1986
+ return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
1987
+ }
1988
+ const hits = await faults.take({
1989
+ operationId,
1990
+ method: request.method,
1991
+ path: url.pathname,
1992
+ namespace
1993
+ });
1994
+ const final = hits.find((hit) => hit.drop || hit.response);
1995
+ if (final?.drop) {
1996
+ log(0, final.id);
1997
+ throw new DroppedConnectionError();
1998
+ }
1999
+ if (final?.response) {
2000
+ log(final.response.status, final.id);
2001
+ return stamp(final.response);
2002
+ }
2003
+ const fired = hits.filter((hit) => hit.effect !== void 0);
2004
+ if (fired.length > 0)
2005
+ effects.set(request, fired.map((hit) => hit.effect));
2006
+ let response = await instanceFor(storage, namespace).fetch(request);
2007
+ if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
2008
+ const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
2009
+ response = mutableResponse(response);
2010
+ response.headers.set(CHECKPOINT_HEADER, point.id);
2011
+ }
2012
+ if (selectedBranch !== "main") {
2013
+ response = mutableResponse(response);
2014
+ response.headers.set(BRANCH_HEADER, selectedBranch);
2015
+ }
2016
+ if (at !== void 0) {
2017
+ response = mutableResponse(response);
2018
+ response.headers.set(AT_HEADER, at);
2019
+ }
2020
+ log(response.status, fired[0]?.id, response);
2021
+ return stamp(response);
2022
+ }
2023
+ };
2024
+ const control = createControlPlane({
2025
+ name: options.name,
2026
+ startedAt: wallNow(),
2027
+ wallNow,
2028
+ clock,
2029
+ faults,
2030
+ metrics,
2031
+ journal,
2032
+ defaultNamespace: DEFAULT_NAMESPACE,
2033
+ namespaces: runtime.namespaces,
2034
+ reset,
2035
+ timeTravel: {
2036
+ checkpoint: (name, branchName) => {
2037
+ const point = checkpoint(name, branchName);
2038
+ return {
2039
+ id: point.id,
2040
+ branch: point.branch,
2041
+ parent: point.parent,
2042
+ at: point.at,
2043
+ records: point.value.snapshot.records.length
2044
+ };
2045
+ },
2046
+ branch: (branchName, branchOptions) => {
2047
+ const point = branch(branchName, branchOptions);
2048
+ return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
2049
+ },
2050
+ checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
2051
+ retain: (name, checkpointId) => {
2052
+ timeline(name).retain(checkpointId);
2053
+ },
2054
+ release: (name, checkpointId) => timeline(name).release(checkpointId),
2055
+ inspect: (name) => {
2056
+ const history = timeline(name);
2057
+ return {
2058
+ branches: history.branches(),
2059
+ checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
2060
+ id,
2061
+ branch: branchName,
2062
+ parent,
2063
+ at
2064
+ }))
2065
+ };
2066
+ }
2067
+ },
2068
+ describe: options.describe ?? (() => ({})),
2069
+ ...options.presets ? {
2070
+ applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
2071
+ } : {},
2072
+ routes: {
2073
+ ...credentialRoutes(credentials),
2074
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
2075
+ ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
2076
+ ...options.admin?.(runtime) ?? {}
2077
+ },
2078
+ adminKey: options.adminKey
2079
+ });
2080
+ return runtime;
2081
+ };
2082
+ var mutableResponse = (response) => {
2083
+ try {
2084
+ response.headers.set("x-mockingbird-mutable-probe", "1");
2085
+ response.headers.delete("x-mockingbird-mutable-probe");
2086
+ return response;
2087
+ } catch {
2088
+ return new Response(response.body, response);
2089
+ }
2090
+ };
2091
+ var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2092
+ var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
2093
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2094
+ var credentialRoutes = (registry) => ({
2095
+ "GET /credentials": () => adminJson(200, {
2096
+ credentials: registry.entries().map(({ credential, namespace }) => ({
2097
+ credential: maskCredential(credential),
2098
+ namespace
2099
+ }))
2100
+ }),
2101
+ "PUT /credentials": ({ body, namespace }) => {
2102
+ const pairs = [];
2103
+ const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
2104
+ if (Array.isArray(list)) {
2105
+ for (const each of list) {
2106
+ if (typeof each === "string")
2107
+ pairs.push([each, namespace]);
2108
+ else if (isObject(each) && typeof each.credential === "string") {
2109
+ pairs.push([
2110
+ each.credential,
2111
+ typeof each.namespace === "string" ? each.namespace : namespace
2112
+ ]);
2113
+ } else
2114
+ return adminFail(400, "each entry is a credential string or {credential, namespace}");
2115
+ }
2116
+ } else if (isObject(list)) {
2117
+ for (const [credential, target] of Object.entries(list)) {
2118
+ if (typeof target !== "string")
2119
+ return adminFail(400, `namespace for ${credential} must be a string`);
2120
+ pairs.push([credential, target]);
2121
+ }
2122
+ } else if (isObject(body) && typeof body.credential === "string") {
2123
+ pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
2124
+ } else {
2125
+ return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
2126
+ }
2127
+ for (const [credential, target] of pairs) {
2128
+ if (!NAMESPACE_PATTERN.test(target))
2129
+ return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
2130
+ registry.set(credential, target);
2131
+ }
2132
+ return adminJson(200, { mapped: pairs.length });
2133
+ },
2134
+ "DELETE /credentials": ({ url }) => {
2135
+ const credential = url.searchParams.get("credential");
2136
+ if (credential === null)
2137
+ registry.clear();
2138
+ else
2139
+ registry.remove(credential);
2140
+ return adminJson(200, { status: "ok" });
2141
+ }
2142
+ });
2143
+ var presetRoutes = (presets, runtime) => ({
2144
+ "GET /faults/presets": () => adminJson(200, {
2145
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
2146
+ }),
2147
+ "POST /faults/presets/:name": ({ params, body, namespace }) => {
2148
+ const name = params.name;
2149
+ if (!presets[name])
2150
+ return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
2151
+ const overrides = isObject(body) ? body : {};
2152
+ return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
2153
+ }
2154
+ });
2155
+
2156
+ // ../core/dist/validation.js
2157
+ var bodyIssues = (context, contentType = "application/json") => {
2158
+ const requestBody = context.operation.operation.requestBody;
2159
+ if (!requestBody)
2160
+ return [];
2161
+ const resolved = deref(context.document, requestBody);
2162
+ const schema = resolved.content?.[contentType]?.schema;
2163
+ if (!schema)
2164
+ return [];
2165
+ const value = context.body.kind === "json" || context.body.kind === "form" ? context.body.value : void 0;
2166
+ if (context.body.kind === "invalid") {
2167
+ return [{ path: "", message: `request body is not valid ${context.body.mediaType}` }];
2168
+ }
2169
+ if (value === void 0) {
2170
+ return resolved.required ? [{ path: "", message: "request body is required" }] : [];
2171
+ }
2172
+ return validateValue(context.document, resolveSchema(context.document, schema), value).map((issue) => ({ path: issue.path.join("."), message: issue.message }));
2173
+ };
2174
+
2175
+ // src/corpus.ts
2176
+ var MEASURE_URI = "http://www.edamam.com/ontologies/edamam.owl#Measure_";
2177
+ var RECIPE_URI = "http://www.edamam.com/ontologies/edamam.owl#recipe_";
2178
+ var NUTRIENTS = {
2179
+ ENERC_KCAL: { label: "Energy", unit: "kcal", daily: 2e3 },
2180
+ PROCNT: { label: "Protein", unit: "g", daily: 50 },
2181
+ FAT: { label: "Fat", unit: "g", daily: 78 },
2182
+ CHOCDF: { label: "Carbs", unit: "g", daily: 275 },
2183
+ FIBTG: { label: "Fiber", unit: "g", daily: 28 },
2184
+ SUGAR: { label: "Sugars", unit: "g", daily: 50 }
2185
+ };
2186
+ var m = (label, weight, name = label) => ({
2187
+ uri: `${MEASURE_URI}${name.toLowerCase().replace(/\s+/g, "_")}`,
2188
+ label,
2189
+ weight
2190
+ });
2191
+ var gram = m("Gram", 1);
2192
+ var ounce = m("Ounce", 28.349523125);
2193
+ var pound = m("Pound", 453.59237);
2194
+ var kilogram = m("Kilogram", 1e3);
2195
+ var weights = [gram, ounce, pound, kilogram];
2196
+ var food = (id, label, nutrients, measures, extra = {}) => ({
2197
+ foodId: `food_${id}`,
2198
+ label,
2199
+ knownAs: label.toLowerCase(),
2200
+ nutrients,
2201
+ category: "Generic foods",
2202
+ categoryLabel: "food",
2203
+ image: `https://www.edamam.com/food-img/${id}.jpg`,
2204
+ measures: [...measures, ...weights],
2205
+ healthLabels: [],
2206
+ ...extra
2207
+ });
2208
+ var VEG = ["VEGETARIAN", "PESCATARIAN", "GLUTEN_FREE", "DAIRY_FREE"];
2209
+ var VEGAN = [...VEG, "VEGAN"];
2210
+ var DEFAULT_FOODS = [
2211
+ food(
2212
+ "egg",
2213
+ "Egg",
2214
+ { ENERC_KCAL: 143, PROCNT: 12.6, FAT: 9.5, CHOCDF: 0.7, FIBTG: 0, SUGAR: 0.4 },
2215
+ [
2216
+ m("Whole", 50, "unit"),
2217
+ m("Serving", 50),
2218
+ m("Large", 50),
2219
+ m("Medium", 44),
2220
+ m("Small", 38),
2221
+ m("Cup", 243)
2222
+ ],
2223
+ { healthLabels: ["VEGETARIAN", "GLUTEN_FREE", "DAIRY_FREE"] }
2224
+ ),
2225
+ food(
2226
+ "chicken_breast",
2227
+ "Chicken Breast",
2228
+ { ENERC_KCAL: 165, PROCNT: 31, FAT: 3.6, CHOCDF: 0, FIBTG: 0, SUGAR: 0 },
2229
+ [m("Whole", 174, "unit"), m("Serving", 120), m("Breast", 174), m("Cup", 140)],
2230
+ { healthLabels: ["GLUTEN_FREE", "DAIRY_FREE"] }
2231
+ ),
2232
+ food(
2233
+ "rice_cooked",
2234
+ "Cooked White Rice",
2235
+ { ENERC_KCAL: 130, PROCNT: 2.7, FAT: 0.3, CHOCDF: 28.2, FIBTG: 0.4, SUGAR: 0.1 },
2236
+ [m("Serving", 158), m("Cup", 158), m("Tablespoon", 10)],
2237
+ { knownAs: "rice", healthLabels: VEGAN }
2238
+ ),
2239
+ food(
2240
+ "banana",
2241
+ "Banana",
2242
+ { ENERC_KCAL: 89, PROCNT: 1.1, FAT: 0.3, CHOCDF: 22.8, FIBTG: 2.6, SUGAR: 12.2 },
2243
+ [m("Whole", 118, "unit"), m("Serving", 118), m("Medium", 118), m("Cup", 150)],
2244
+ { healthLabels: VEGAN }
2245
+ ),
2246
+ food(
2247
+ "apple",
2248
+ "Apple",
2249
+ { ENERC_KCAL: 52, PROCNT: 0.3, FAT: 0.2, CHOCDF: 13.8, FIBTG: 2.4, SUGAR: 10.4 },
2250
+ [m("Whole", 182, "unit"), m("Serving", 182), m("Cup", 125)],
2251
+ { healthLabels: VEGAN }
2252
+ ),
2253
+ food(
2254
+ "oats",
2255
+ "Oats",
2256
+ { ENERC_KCAL: 389, PROCNT: 16.9, FAT: 6.9, CHOCDF: 66.3, FIBTG: 10.6, SUGAR: 1 },
2257
+ [m("Serving", 40), m("Cup", 81), m("Tablespoon", 5)],
2258
+ { knownAs: "oatmeal", healthLabels: VEGAN }
2259
+ ),
2260
+ food(
2261
+ "milk",
2262
+ "Whole Milk",
2263
+ { ENERC_KCAL: 61, PROCNT: 3.2, FAT: 3.3, CHOCDF: 4.8, FIBTG: 0, SUGAR: 5.1 },
2264
+ [m("Serving", 244), m("Cup", 244), m("Fluid ounce", 30.5)],
2265
+ { knownAs: "milk", healthLabels: ["VEGETARIAN", "GLUTEN_FREE"] }
2266
+ ),
2267
+ food(
2268
+ "greek_yogurt",
2269
+ "Greek Yogurt",
2270
+ { ENERC_KCAL: 59, PROCNT: 10.2, FAT: 0.4, CHOCDF: 3.6, FIBTG: 0, SUGAR: 3.2 },
2271
+ [m("Container", 170), m("Serving", 170), m("Cup", 245)],
2272
+ { knownAs: "yogurt", healthLabels: ["VEGETARIAN", "GLUTEN_FREE"] }
2273
+ ),
2274
+ food(
2275
+ "salmon",
2276
+ "Salmon",
2277
+ { ENERC_KCAL: 208, PROCNT: 20.4, FAT: 13.4, CHOCDF: 0, FIBTG: 0, SUGAR: 0 },
2278
+ [m("Fillet", 198), m("Serving", 113)],
2279
+ { healthLabels: ["PESCATARIAN", "GLUTEN_FREE", "DAIRY_FREE"] }
2280
+ ),
2281
+ food(
2282
+ "avocado",
2283
+ "Avocado",
2284
+ { ENERC_KCAL: 160, PROCNT: 2, FAT: 14.7, CHOCDF: 8.5, FIBTG: 6.7, SUGAR: 0.7 },
2285
+ [m("Whole", 201, "unit"), m("Serving", 50), m("Cup", 150)],
2286
+ { healthLabels: VEGAN }
2287
+ ),
2288
+ food(
2289
+ "almonds",
2290
+ "Almonds",
2291
+ { ENERC_KCAL: 579, PROCNT: 21.2, FAT: 49.9, CHOCDF: 21.6, FIBTG: 12.5, SUGAR: 4.4 },
2292
+ [m("Serving", 28), m("Cup", 143), m("Almond", 1.2)],
2293
+ { healthLabels: VEGAN }
2294
+ ),
2295
+ food(
2296
+ "bread",
2297
+ "Whole Wheat Bread",
2298
+ { ENERC_KCAL: 247, PROCNT: 13, FAT: 3.4, CHOCDF: 41, FIBTG: 7, SUGAR: 6 },
2299
+ [m("Slice", 32), m("Serving", 32)],
2300
+ { knownAs: "bread", healthLabels: ["VEGETARIAN", "VEGAN", "DAIRY_FREE"] }
2301
+ ),
2302
+ food(
2303
+ "spinach",
2304
+ "Spinach",
2305
+ { ENERC_KCAL: 23, PROCNT: 2.9, FAT: 0.4, CHOCDF: 3.6, FIBTG: 2.2, SUGAR: 0.4 },
2306
+ [m("Cup", 30), m("Serving", 30), m("Bunch", 340)],
2307
+ { healthLabels: VEGAN }
2308
+ ),
2309
+ food(
2310
+ "olive_oil",
2311
+ "Olive Oil",
2312
+ { ENERC_KCAL: 884, PROCNT: 0, FAT: 100, CHOCDF: 0, FIBTG: 0, SUGAR: 0 },
2313
+ [m("Tablespoon", 13.5), m("Teaspoon", 4.5), m("Serving", 13.5)],
2314
+ { knownAs: "oil", healthLabels: VEGAN }
2315
+ ),
2316
+ food(
2317
+ "chicken_salad",
2318
+ "Chicken Salad",
2319
+ { ENERC_KCAL: 229, PROCNT: 13.2, FAT: 18, CHOCDF: 3.3, FIBTG: 0.4, SUGAR: 1.9 },
2320
+ [m("Serving", 226), m("Cup", 226)],
2321
+ {
2322
+ category: "Generic meals",
2323
+ categoryLabel: "meal",
2324
+ foodContentsLabel: "chicken; mayonnaise; celery; onion",
2325
+ healthLabels: ["GLUTEN_FREE"]
2326
+ }
2327
+ ),
2328
+ food(
2329
+ "protein_bar",
2330
+ "Protein Bar",
2331
+ { ENERC_KCAL: 350, PROCNT: 30, FAT: 10, CHOCDF: 40, FIBTG: 12, SUGAR: 5 },
2332
+ [m("Serving", 60), m("Package", 60)],
2333
+ {
2334
+ category: "Packaged foods",
2335
+ brand: "Mockingbird Foods",
2336
+ foodContentsLabel: "protein blend; almonds; chicory root fiber; cocoa",
2337
+ servingSizes: [{ uri: `${MEASURE_URI}gram`, label: "Gram", quantity: 60 }],
2338
+ upc: "850000000012",
2339
+ healthLabels: ["VEGETARIAN", "GLUTEN_FREE"]
2340
+ }
2341
+ ),
2342
+ food("mystery_snack", "Mystery Snack", {}, [m("Package", 40)], {
2343
+ category: "Packaged foods",
2344
+ brand: "Mockingbird Foods",
2345
+ upc: "850000000029"
2346
+ }),
2347
+ food(
2348
+ "plain_greek_yogurt",
2349
+ "Plain Greek Yogurt",
2350
+ { ENERC_KCAL: 59, PROCNT: 10, FAT: 0.4, CHOCDF: 3.5, FIBTG: 0, SUGAR: 3.3 },
2351
+ [m("Container", 170), m("Serving", 170)],
2352
+ {
2353
+ category: "Packaged foods",
2354
+ brand: "Mockingbird Dairy",
2355
+ foodContentsLabel: "cultured pasteurized nonfat milk",
2356
+ servingSizes: [{ uri: `${MEASURE_URI}container`, label: "Container", quantity: 1 }],
2357
+ upc: "850000000036",
2358
+ healthLabels: ["VEGETARIAN", "GLUTEN_FREE"]
2359
+ }
2360
+ )
2361
+ ];
2362
+ var ing = (foodId, quantity, measure, text) => ({
2363
+ foodId: `food_${foodId}`,
2364
+ quantity,
2365
+ measure,
2366
+ text
2367
+ });
2368
+ var DEFAULT_RECIPES = [
2369
+ {
2370
+ id: "overnight_oats",
2371
+ label: "Banana Overnight Oats",
2372
+ yield: 2,
2373
+ totalTime: 5,
2374
+ mealType: ["breakfast"],
2375
+ dishType: ["cereals"],
2376
+ cuisineType: ["american"],
2377
+ dietLabels: ["High-Fiber"],
2378
+ healthLabels: ["VEGETARIAN", "PESCATARIAN", "GLUTEN_FREE"],
2379
+ cautions: [],
2380
+ ingredients: [
2381
+ ing("oats", 1, "Cup", "1 cup rolled oats"),
2382
+ ing("milk", 1, "Cup", "1 cup milk"),
2383
+ ing("banana", 1, "Whole", "1 banana, sliced"),
2384
+ ing("greek_yogurt", 0.5, "Cup", "1/2 cup greek yogurt")
2385
+ ]
2386
+ },
2387
+ {
2388
+ id: "veggie_omelette",
2389
+ label: "Spinach Omelette",
2390
+ yield: 1,
2391
+ totalTime: 10,
2392
+ mealType: ["breakfast"],
2393
+ dishType: ["egg"],
2394
+ cuisineType: ["french"],
2395
+ dietLabels: ["Low-Carb", "High-Protein"],
2396
+ healthLabels: ["VEGETARIAN", "PESCATARIAN", "GLUTEN_FREE", "DAIRY_FREE"],
2397
+ cautions: [],
2398
+ ingredients: [
2399
+ ing("egg", 3, "Large", "3 large eggs"),
2400
+ ing("spinach", 1, "Cup", "1 cup spinach"),
2401
+ ing("olive_oil", 1, "Teaspoon", "1 tsp olive oil")
2402
+ ]
2403
+ },
2404
+ {
2405
+ id: "avocado_toast",
2406
+ label: "Avocado Toast",
2407
+ yield: 1,
2408
+ totalTime: 5,
2409
+ mealType: ["breakfast", "lunch/dinner"],
2410
+ dishType: ["sandwiches"],
2411
+ cuisineType: ["american"],
2412
+ dietLabels: ["High-Fiber"],
2413
+ healthLabels: ["VEGAN", "VEGETARIAN", "PESCATARIAN", "DAIRY_FREE"],
2414
+ cautions: [],
2415
+ ingredients: [
2416
+ ing("bread", 2, "Slice", "2 slices whole wheat bread"),
2417
+ ing("avocado", 1, "Whole", "1 avocado"),
2418
+ ing("olive_oil", 1, "Teaspoon", "1 tsp olive oil")
2419
+ ]
2420
+ },
2421
+ {
2422
+ id: "chicken_rice_bowl",
2423
+ label: "Chicken and Rice Bowl",
2424
+ yield: 2,
2425
+ totalTime: 30,
2426
+ mealType: ["lunch/dinner"],
2427
+ dishType: ["main course"],
2428
+ cuisineType: ["asian"],
2429
+ dietLabels: ["High-Protein"],
2430
+ healthLabels: ["GLUTEN_FREE", "DAIRY_FREE"],
2431
+ cautions: [],
2432
+ ingredients: [
2433
+ ing("chicken_breast", 2, "Breast", "2 chicken breasts"),
2434
+ ing("rice_cooked", 2, "Cup", "2 cups cooked rice"),
2435
+ ing("spinach", 2, "Cup", "2 cups spinach"),
2436
+ ing("olive_oil", 1, "Tablespoon", "1 tbsp olive oil")
2437
+ ]
2438
+ },
2439
+ {
2440
+ id: "baked_salmon",
2441
+ label: "Lemon Baked Salmon",
2442
+ yield: 2,
2443
+ totalTime: 25,
2444
+ mealType: ["lunch/dinner"],
2445
+ dishType: ["main course"],
2446
+ cuisineType: ["mediterranean"],
2447
+ dietLabels: ["Low-Carb", "High-Protein"],
2448
+ healthLabels: ["PESCATARIAN", "GLUTEN_FREE", "DAIRY_FREE"],
2449
+ cautions: ["FODMAP"],
2450
+ ingredients: [
2451
+ ing("salmon", 2, "Fillet", "2 salmon fillets"),
2452
+ ing("olive_oil", 1, "Tablespoon", "1 tbsp olive oil"),
2453
+ ing("spinach", 2, "Cup", "2 cups spinach")
2454
+ ]
2455
+ },
2456
+ {
2457
+ id: "chicken_salad_plate",
2458
+ label: "Chicken Salad Plate",
2459
+ yield: 1,
2460
+ totalTime: 10,
2461
+ mealType: ["lunch/dinner"],
2462
+ dishType: ["salad"],
2463
+ cuisineType: ["american"],
2464
+ dietLabels: ["Low-Carb"],
2465
+ healthLabels: ["GLUTEN_FREE"],
2466
+ cautions: ["Eggs"],
2467
+ ingredients: [
2468
+ ing("chicken_salad", 1, "Cup", "1 cup chicken salad"),
2469
+ ing("spinach", 1, "Cup", "1 cup spinach")
2470
+ ]
2471
+ },
2472
+ {
2473
+ id: "yogurt_parfait",
2474
+ label: "Greek Yogurt Parfait",
2475
+ yield: 1,
2476
+ totalTime: 5,
2477
+ mealType: ["snack", "breakfast"],
2478
+ dishType: ["desserts"],
2479
+ cuisineType: ["american"],
2480
+ dietLabels: ["High-Protein"],
2481
+ healthLabels: ["VEGETARIAN", "PESCATARIAN", "GLUTEN_FREE"],
2482
+ cautions: [],
2483
+ ingredients: [
2484
+ ing("greek_yogurt", 1, "Container", "1 container greek yogurt"),
2485
+ ing("almonds", 1, "Serving", "1 oz almonds"),
2486
+ ing("banana", 0.5, "Whole", "1/2 banana")
2487
+ ]
2488
+ },
2489
+ {
2490
+ id: "apple_almonds",
2491
+ label: "Apple with Almonds",
2492
+ yield: 1,
2493
+ totalTime: 2,
2494
+ mealType: ["snack"],
2495
+ dishType: ["starter"],
2496
+ cuisineType: ["american"],
2497
+ dietLabels: ["Balanced"],
2498
+ healthLabels: ["VEGAN", "VEGETARIAN", "PESCATARIAN", "GLUTEN_FREE", "DAIRY_FREE"],
2499
+ cautions: [],
2500
+ ingredients: [
2501
+ ing("apple", 1, "Whole", "1 apple"),
2502
+ ing("almonds", 1, "Serving", "1 oz almonds")
2503
+ ]
2504
+ }
2505
+ ];
2506
+
2507
+ // src/generated/openapi.ts
2508
+ var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Edamam APIs (Mockingbird subset)","description":"Stateful mock subset of Edamam's Food Database v2 (parser, nutrients, image), Nutrition Analysis (nutrition-data, nutrition-details), Recipe Search v2 (search, by-uri, by id), Meal Planner v1 (select) and Shopping List v2, over a built-in corpus. Hand-authored from Edamam's docs and the consumers (edamam-nutrition.adapter.ts, edamam-meal-planning.adapter.ts, makor chat tools/nutrition/client.py).\\n","version":"2","x-mockingbird-upstream":{"note":"Edamam publishes per-API docs, no single OpenAPI; shapes trimmed to the fields our consumers read (zod schemas in edamam-nutrition.adapter.ts and edamam-meal-planning.schemas.ts)."}},"servers":[{"url":"https://api.edamam.com"}],"paths":{"/api/food-database/v2/parser":{"get":{"operationId":"FoodParser","description":"Parse a food description (\`ingr\`) or look up a barcode (\`upc\`): \`parsed\` (the best match with quantity and measure) and \`hints\` (every related food with its measures).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"ingr","in":"query","required":false,"schema":{"type":"string","enum":["2 large eggs","1 cup cooked rice","banana","chicken breast","greek yogurt","3 slices bread","salmon fillet","chicken salad","xyzzy"]},"description":"Parity vocabulary; the mock parses any text."},{"name":"upc","in":"query","required":false,"schema":{"type":"string","enum":["850000000012","850000000029","850000000036","000000000000"]}},{"name":"nutrition-type","in":"query","required":false,"schema":{"type":"string","enum":["cooking","logging"]}},{"name":"categoryLabel","in":"query","required":false,"schema":{"type":"string","enum":["food","meal"]}},{"name":"category","in":"query","required":false,"schema":{"type":"string","enum":["generic-foods","packaged-foods","generic-meals"]}},{"name":"health","in":"query","required":false,"schema":{"type":"string","enum":["vegan","vegetarian","gluten-free","dairy-free"]}},{"name":"calories","in":"query","required":false,"schema":{"type":"string","enum":["50-200","300+","100"]}}],"responses":{"200":{"description":"Parser result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParserResponse"}}}},"400":{"description":"Neither ingr nor upc","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"404":{"description":"Unknown UPC","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}}}}},"/api/food-database/v2/nutrients":{"post":{"operationId":"FoodNutrients","description":"Full nutrition for foods at given measures.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"}],"responses":{"200":{"description":"Nutrition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NutritionAnalysis"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"422":{"description":"Unknown food or measure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NutrientsRequest"}}}}}},"/api/food-database/nutrients-from-image":{"post":{"operationId":"FoodFromImage","description":"Recognise a plated meal from an image (beta).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"beta","in":"query","required":false,"schema":{"type":"string","enum":["true"]}}],"responses":{"200":{"description":"Recognition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisionResponse"}}}},"400":{"description":"Not an image","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageRequest"}}}}}},"/api/nutrition-data":{"get":{"operationId":"NutritionData","description":"Nutrition analysis of one ingredient line.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"ingr","in":"query","required":true,"schema":{"type":"string","enum":["1 cup cooked rice","2 large eggs","1 banana","xyzzy"]},"description":"Parity vocabulary; the mock parses any text."},{"name":"nutrition-type","in":"query","required":false,"schema":{"type":"string","enum":["cooking","logging"]}}],"responses":{"200":{"description":"Nutrition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NutritionAnalysis"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"422":{"description":"Could not parse the ingredient","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}}}}},"/api/nutrition-details":{"post":{"operationId":"NutritionDetails","description":"Nutrition analysis of a recipe (ingredient lines).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"}],"responses":{"200":{"description":"Nutrition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NutritionAnalysis"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"422":{"description":"No ingredients","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}},"555":{"description":"Insufficient quality","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FoodError"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeAnalysisRequest"}}}}}},"/api/recipes/v2":{"get":{"operationId":"RecipeSearch","description":"Recipe search v2 (20 per page, \`_links.next\` carries \`_cont\`).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"Edamam-Account-User","in":"header","required":false,"schema":{"type":"string","pattern":"^[a-f0-9]{1,30}$","description":"The first 30 hex chars of sha256(user id), as our meal adapter sends it."}},{"name":"type","in":"query","required":true,"schema":{"type":"string","enum":["public","user","any"]}},{"name":"q","in":"query","required":false,"schema":{"type":"string","enum":["chicken","salmon","oats","yogurt","toast","xyzzy"]},"description":"Parity vocabulary; the mock parses any text."},{"name":"health","in":"query","required":false,"schema":{"type":"string","enum":["vegan","vegetarian","gluten-free","dairy-free","pescatarian"]}},{"name":"diet","in":"query","required":false,"schema":{"type":"string","enum":["high-protein","low-carb","high-fiber","balanced"]}},{"name":"mealType","in":"query","required":false,"schema":{"type":"string","enum":["Breakfast","Lunch","Dinner","Snack"]}},{"name":"dishType","in":"query","required":false,"schema":{"type":"string","enum":["Main course","Salad","Cereals","Egg"]}},{"name":"cuisineType","in":"query","required":false,"schema":{"type":"string","enum":["American","Asian","Mediterranean","French"]}},{"name":"calories","in":"query","required":false,"schema":{"type":"string","enum":["100-400","500+","300"]}},{"name":"time","in":"query","required":false,"schema":{"type":"string","enum":["1-10","30","20+"]}},{"name":"excluded","in":"query","required":false,"schema":{"type":"string","enum":["spinach","almonds"]}},{"name":"imageSize","in":"query","required":false,"schema":{"type":"string","enum":["REGULAR","SMALL","THUMBNAIL","LARGE"]}},{"name":"random","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}}],"responses":{"200":{"description":"Hits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeSearchResponse"}}}},"400":{"description":"Illegal parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}}}}},"/api/recipes/v2/by-uri":{"get":{"operationId":"RecipesByUri","description":"Recipes by URI (up to 20 \`uri\` parameters).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"Edamam-Account-User","in":"header","required":false,"schema":{"type":"string","pattern":"^[a-f0-9]{1,30}$","description":"The first 30 hex chars of sha256(user id), as our meal adapter sends it."}},{"name":"type","in":"query","required":true,"schema":{"type":"string","enum":["public"]}},{"name":"uri","in":"query","required":true,"schema":{"type":"string","enum":["http://www.edamam.com/ontologies/edamam.owl#recipe_overnight_oats","http://www.edamam.com/ontologies/edamam.owl#recipe_baked_salmon","http://www.edamam.com/ontologies/edamam.owl#recipe_missing"]}}],"responses":{"200":{"description":"Hits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeSearchResponse"}}}},"400":{"description":"Illegal parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}}}}},"/api/recipes/v2/{id}":{"get":{"operationId":"RecipeById","description":"One recipe (the \`_links.self\` target).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"Edamam-Account-User","in":"header","required":false,"schema":{"type":"string","pattern":"^[a-f0-9]{1,30}$","description":"The first 30 hex chars of sha256(user id), as our meal adapter sends it."}},{"name":"id","in":"path","required":true,"schema":{"type":"string","enum":["overnight_oats","veggie_omelette","chicken_rice_bowl","missing"]}},{"name":"type","in":"query","required":true,"schema":{"type":"string","enum":["public"]}}],"responses":{"200":{"description":"The recipe","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeHit"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"404":{"description":"Unknown recipe","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}}}}},"/api/meal-planner/v1/{app_id}/select":{"post":{"operationId":"MealPlanSelect","description":"Select recipes for \`size\` days of a plan (Basic auth plus app_id/app_key).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"Edamam-Account-User","in":"header","required":false,"schema":{"type":"string","pattern":"^[a-f0-9]{1,30}$","description":"The first 30 hex chars of sha256(user id), as our meal adapter sends it."}},{"name":"app_id","in":"path","required":true,"schema":{"type":"string","enum":["parity"]}},{"name":"type","in":"query","required":false,"schema":{"type":"string","enum":["public"]}}],"responses":{"200":{"description":"Selection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlanResponse"}}}},"400":{"description":"Illegal plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlanRequest"}}}}}},"/api/shopping-list/v2":{"post":{"operationId":"ShoppingList","description":"Aggregate the ingredients of recipes into a shopping list (optionally a cart link).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/AppId"},{"$ref":"#/components/parameters/AppKey"},{"name":"Edamam-Account-User","in":"header","required":false,"schema":{"type":"string","pattern":"^[a-f0-9]{1,30}$","description":"The first 30 hex chars of sha256(user id), as our meal adapter sends it."}},{"name":"shopping-cart","in":"query","required":false,"schema":{"type":"string","enum":["true"]}},{"name":"beta","in":"query","required":false,"schema":{"type":"string","enum":["true"]}}],"responses":{"200":{"description":"Shopping list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShoppingListResponse"}}}},"400":{"description":"Unknown recipe","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"401":{"description":"Unknown app_id / app_key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}},"429":{"description":"Usage limits are exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeErrors"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShoppingListRequest"}}}}}}},"components":{"parameters":{"AppId":{"name":"app_id","in":"query","required":true,"description":"Edamam application id (any value unless \`apps\` is set). The parity vocabulary is fixed.","schema":{"type":"string","enum":["parity"]}},"AppKey":{"name":"app_key","in":"query","required":true,"schema":{"type":"string","enum":["parity-key"]}}},"schemas":{"Measure":{"type":"object","required":["uri","label","weight"],"properties":{"uri":{"type":"string"},"label":{"type":"string"},"weight":{"type":"number"}}},"Food":{"type":"object","required":["foodId","label","category","categoryLabel"],"properties":{"foodId":{"type":"string"},"label":{"type":"string"},"knownAs":{"type":"string"},"nutrients":{"type":"object","additionalProperties":{"type":"number"}},"brand":{"type":"string"},"category":{"type":"string"},"categoryLabel":{"type":"string"},"foodContentsLabel":{"type":"string"},"image":{"type":"string"},"servingSizes":{"type":"array","items":{"type":"object"}},"servingsPerContainer":{"type":"number"}}},"Recipe":{"type":"object","required":["uri","label","url","yield","ingredientLines","ingredients","totalNutrients","cautions","dietLabels","healthLabels"],"properties":{"uri":{"type":"string"},"label":{"type":"string"},"image":{"type":"string"},"source":{"type":"string"},"url":{"type":"string"},"shareAs":{"type":"string"},"yield":{"type":"number"},"dietLabels":{"type":"array","items":{"type":"string"}},"healthLabels":{"type":"array","items":{"type":"string"}},"cautions":{"type":"array","items":{"type":"string"}},"ingredientLines":{"type":"array","items":{"type":"string"}},"ingredients":{"type":"array","items":{"type":"object","required":["food","quantity"],"properties":{"text":{"type":"string"},"quantity":{"type":"number"},"measure":{"type":["string","null"]},"food":{"type":"string"},"weight":{"type":"number"},"foodCategory":{"type":["string","null"]},"foodId":{"type":"string"},"image":{"type":["string","null"]}}}},"calories":{"type":"number"},"totalWeight":{"type":"number"},"totalTime":{"type":"number"},"cuisineType":{"type":"array","items":{"type":"string"}},"mealType":{"type":"array","items":{"type":"string"}},"dishType":{"type":"array","items":{"type":"string"}},"totalNutrients":{"type":"object","additionalProperties":{"type":"object","required":["label","quantity","unit"],"properties":{"label":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}},"totalDaily":{"type":"object","additionalProperties":{"type":"object","required":["label","quantity","unit"],"properties":{"label":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}}}},"Link":{"type":"object","required":["href"],"properties":{"href":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"title":{"type":"string"}}},"ParserResponse":{"type":"object","required":["text","parsed","hints"],"properties":{"text":{"type":"string"},"parsed":{"type":"array","items":{"type":"object","required":["food"],"properties":{"food":{"$ref":"#/components/schemas/Food"},"quantity":{"type":"number"},"measure":{"$ref":"#/components/schemas/Measure"}}}},"hints":{"type":"array","items":{"type":"object","required":["food","measures"],"properties":{"food":{"$ref":"#/components/schemas/Food"},"measures":{"type":"array","items":{"$ref":"#/components/schemas/Measure"}}}}},"_links":{"type":"object"}}},"NutrientsRequest":{"type":"object","required":["ingredients"],"properties":{"ingredients":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","required":["quantity","measureURI","foodId"],"properties":{"quantity":{"type":"number","exclusiveMinimum":0,"maximum":20},"measureURI":{"type":"string","examples":["http://www.edamam.com/ontologies/edamam.owl#Measure_unit","http://www.edamam.com/ontologies/edamam.owl#Measure_cup","http://www.edamam.com/ontologies/edamam.owl#Measure_gram"]},"foodId":{"type":"string","examples":["food_egg","food_banana","food_rice_cooked"]},"qualifiers":{"type":"array","items":{"type":"string"}}}}}}},"NutritionAnalysis":{"type":"object","required":["calories","totalWeight","dietLabels","healthLabels","cautions","totalNutrients","totalDaily"],"properties":{"uri":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"calories":{"type":"number"},"totalWeight":{"type":"number"},"dietLabels":{"type":"array","items":{"type":"string"}},"healthLabels":{"type":"array","items":{"type":"string"}},"cautions":{"type":"array","items":{"type":"string"}},"totalNutrients":{"type":"object","additionalProperties":{"type":"object","required":["label","quantity","unit"],"properties":{"label":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}},"totalDaily":{"type":"object","additionalProperties":{"type":"object","required":["label","quantity","unit"],"properties":{"label":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}},"ingredients":{"type":"array","items":{"type":"object"}},"yield":{"type":"number"}}},"ImageRequest":{"type":"object","required":["image"],"properties":{"image":{"type":"string","minLength":1,"maxLength":200,"examples":["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ==","data:image/png;base64,iVBORw0KGgo="]}}},"VisionResponse":{"type":"object","properties":{"parsed":{"type":"object","properties":{"food":{"$ref":"#/components/schemas/Food"},"quantity":{"type":"number"},"measure":{"$ref":"#/components/schemas/Measure"}}},"recipe":{"type":"object","properties":{"label":{"type":"string"},"calories":{"type":"number"},"totalNutrients":{"type":"object","additionalProperties":{"type":"object","required":["label","quantity","unit"],"properties":{"label":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"}}}}}}}},"RecipeAnalysisRequest":{"type":"object","required":["ingr"],"properties":{"title":{"type":"string","maxLength":80},"yield":{"type":"integer","minimum":1,"maximum":8},"ingr":{"type":"array","maxItems":4,"items":{"type":"string","minLength":1,"maxLength":60,"examples":["2 large eggs","1 cup cooked rice","1 banana","1 slice bread","xyzzy"]}}}},"RecipeHit":{"type":"object","required":["recipe"],"properties":{"recipe":{"$ref":"#/components/schemas/Recipe"},"_links":{"type":"object","properties":{"self":{"$ref":"#/components/schemas/Link"}}}}},"RecipeSearchResponse":{"type":"object","required":["from","to","count","hits"],"properties":{"from":{"type":"integer"},"to":{"type":"integer"},"count":{"type":"integer"},"_links":{"type":"object","properties":{"next":{"$ref":"#/components/schemas/Link"}}},"hits":{"type":"array","items":{"$ref":"#/components/schemas/RecipeHit"}}}},"MealPlanRequest":{"type":"object","required":["size","plan"],"properties":{"size":{"type":"integer","minimum":1,"maximum":7},"plan":{"$ref":"#/components/schemas/PlanSection"}}},"PlanSection":{"type":"object","properties":{"accept":{"type":"object","required":["all"],"properties":{"all":{"type":"array","maxItems":2,"items":{"type":"object","properties":{"health":{"type":"array","maxItems":2,"items":{"type":"string","enum":["VEGAN","VEGETARIAN","GLUTEN_FREE","DAIRY_FREE","PESCATARIAN"]}},"meal":{"type":"array","maxItems":2,"items":{"type":"string","enum":["breakfast","lunch/dinner","snack"]}},"dish":{"type":"array","maxItems":2,"items":{"type":"string","enum":["main course","salad","cereals","egg"]}}}}}}},"fit":{"type":"object","properties":{"ENERC_KCAL":{"type":"object","properties":{"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0},"mark":{"type":"number","minimum":0}}},"PROCNT":{"type":"object","properties":{"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0},"mark":{"type":"number","minimum":0}}},"CHOCDF":{"type":"object","properties":{"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0},"mark":{"type":"number","minimum":0}}},"FAT":{"type":"object","properties":{"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0},"mark":{"type":"number","minimum":0}}},"FIBTG":{"type":"object","properties":{"min":{"type":"number","minimum":0},"max":{"type":"number","minimum":0},"mark":{"type":"number","minimum":0}}}}},"exclude":{"type":"array","maxItems":2,"items":{"type":"string","enum":["http://www.edamam.com/ontologies/edamam.owl#recipe_overnight_oats","http://www.edamam.com/ontologies/edamam.owl#recipe_baked_salmon"]}},"sections":{"type":"object","maxProperties":4,"additionalProperties":{"$ref":"#/components/schemas/PlanSection"},"examples":[{"Breakfast":{},"Lunch":{},"Dinner":{}}]}}},"MealPlanResponse":{"type":"object","properties":{"status":{"type":"string","enum":["OK","INCOMPLETE","TIME_OUT"]},"selection":{"type":"array","items":{"type":"object","required":["sections"],"properties":{"sections":{"type":"object","additionalProperties":{"type":"object","properties":{"assigned":{"type":"string"},"_links":{"type":"object","properties":{"self":{"$ref":"#/components/schemas/Link"}}},"sections":{"type":"object"}}}}}}}}},"ShoppingListRequest":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","required":["quantity","item"],"properties":{"quantity":{"type":"number","exclusiveMinimum":0,"maximum":10},"measure":{"type":"string","enum":["http://www.edamam.com/ontologies/edamam.owl#Measure_serving"]},"item":{"type":"string","examples":["http://www.edamam.com/ontologies/edamam.owl#recipe_overnight_oats","http://www.edamam.com/ontologies/edamam.owl#recipe_chicken_rice_bowl","http://www.edamam.com/ontologies/edamam.owl#recipe_missing"]}}}}}},"ShoppingListResponse":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["foodId","food","quantities"],"properties":{"foodId":{"type":"string"},"food":{"type":"string"},"quantities":{"type":"array","items":{"type":"object","required":["quantity","measure"],"properties":{"quantity":{"type":"number"},"measure":{"type":"string"},"qualifiers":{"type":"array","items":{"type":"string"}}}}}}}},"_links":{"type":"object","properties":{"shopping-cart":{"$ref":"#/components/schemas/Link"}}}}},"FoodError":{"type":"object","required":["status","message"],"properties":{"status":{"type":"string","enum":["error"]},"message":{"type":"string"},"error":{"type":"string"}}},"RecipeErrors":{"type":"array","items":{"type":"object","required":["errorCode","message"],"properties":{"errorCode":{"type":"string"},"message":{"type":"string"},"params":{"type":"array","items":{"type":"string"}}}}}}}}`);
2509
+ var operationIds = ["FoodParser", "FoodNutrients", "FoodFromImage", "NutritionData", "NutritionDetails", "RecipeSearch", "RecipesByUri", "RecipeById", "MealPlanSelect", "ShoppingList"];
2510
+ var supportedOperationIds = ["FoodParser", "FoodNutrients", "FoodFromImage", "NutritionData", "NutritionDetails", "RecipeSearch", "RecipesByUri", "RecipeById", "MealPlanSelect", "ShoppingList"];
2511
+
2512
+ // src/logic.ts
2513
+ var round = (value, digits = 3) => Math.round(value * 10 ** digits) / 10 ** digits;
2514
+ var CODES = Object.keys(NUTRIENTS);
2515
+ var WORD_NUMBERS = {
2516
+ a: 1,
2517
+ an: 1,
2518
+ one: 1,
2519
+ two: 2,
2520
+ three: 3,
2521
+ four: 4,
2522
+ five: 5,
2523
+ six: 6,
2524
+ half: 0.5
2525
+ };
2526
+ var MEASURE_ALIASES = {
2527
+ g: "gram",
2528
+ gr: "gram",
2529
+ grams: "gram",
2530
+ oz: "ounce",
2531
+ lb: "pound",
2532
+ lbs: "pound",
2533
+ kg: "kilogram",
2534
+ tbsp: "tablespoon",
2535
+ tbs: "tablespoon",
2536
+ tsp: "teaspoon",
2537
+ whole: "whole",
2538
+ fillets: "fillet"
2539
+ };
2540
+ var STOP = /* @__PURE__ */ new Set(["of", "and", "with", "the", "fresh", "cooked", "raw", "sliced"]);
2541
+ var tokens = (text) => text.toLowerCase().replace(/[^a-z0-9./ ]+/g, " ").split(/\s+/).filter(Boolean).map((t) => t.length > 3 && t.endsWith("ies") ? `${t.slice(0, -3)}y` : t).map((t) => t.length > 3 && /(ch|sh|s|x)es$/.test(t) ? t.slice(0, -2) : t).map((t) => t.length > 2 && t.endsWith("s") && !t.endsWith("ss") ? t.slice(0, -1) : t);
2542
+ var quantityOf = (token) => {
2543
+ if (!token) return void 0;
2544
+ if (token in WORD_NUMBERS) return WORD_NUMBERS[token];
2545
+ if (/^\d+(\.\d+)?$/.test(token)) return Number(token);
2546
+ const fraction = /^(\d+)\/(\d+)$/.exec(token);
2547
+ if (fraction) return Number(fraction[1]) / Number(fraction[2]);
2548
+ return void 0;
2549
+ };
2550
+ var foodTokens = (food2) => [tokens(food2.label), tokens(food2.knownAs)];
2551
+ var parseLine = (text, foods) => {
2552
+ const words = tokens(text);
2553
+ let index = 0;
2554
+ let quantity;
2555
+ const first = quantityOf(words[0]);
2556
+ if (first !== void 0) {
2557
+ quantity = first;
2558
+ index = 1;
2559
+ const second = quantityOf(words[1]);
2560
+ if (second !== void 0 && second < 1 && words[1]?.includes("/")) {
2561
+ quantity += second;
2562
+ index = 2;
2563
+ }
2564
+ }
2565
+ const rest = words.slice(index).filter((w) => !STOP.has(w));
2566
+ let best;
2567
+ for (const food3 of foods) {
2568
+ for (const candidate of foodTokens(food3)) {
2569
+ if (candidate.length > 0 && candidate.every((t) => rest.includes(t))) {
2570
+ if (!best || candidate.length > best.score) best = { food: food3, score: candidate.length };
2571
+ }
2572
+ }
2573
+ }
2574
+ if (!best) return void 0;
2575
+ const food2 = best.food;
2576
+ const labelTokens = new Set(foodTokens(food2).flat());
2577
+ const measureWord = rest.find((w) => !labelTokens.has(w));
2578
+ const wanted = measureWord ? MEASURE_ALIASES[measureWord] ?? measureWord : void 0;
2579
+ const explicit = wanted ? food2.measures.find(
2580
+ (m2) => tokens(m2.label).join(" ") === wanted || m2.label.toLowerCase() === wanted
2581
+ ) : void 0;
2582
+ if (quantity === void 0 && !explicit) return { food: food2 };
2583
+ const fallback = food2.measures.find((m2) => m2.label === "Whole") ?? food2.measures.find((m2) => m2.label === "Serving");
2584
+ const measure = explicit ?? fallback;
2585
+ return { food: food2, quantity: quantity ?? 1, ...measure ? { measure } : {} };
2586
+ };
2587
+ var relatedFoods = (text, foods) => {
2588
+ const words = new Set(tokens(text).filter((w) => !STOP.has(w) && quantityOf(w) === void 0));
2589
+ return foods.filter(
2590
+ (food2) => foodTokens(food2).flat().some((t) => words.has(t))
2591
+ );
2592
+ };
2593
+ var parseRange = (value) => {
2594
+ const both = /^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/.exec(value);
2595
+ if (both) return { min: Number(both[1]), max: Number(both[2]) };
2596
+ const plus = /^(\d+(?:\.\d+)?)\+$/.exec(value);
2597
+ if (plus) return { min: Number(plus[1]), max: Number.POSITIVE_INFINITY };
2598
+ if (/^\d+(\.\d+)?$/.test(value)) return { min: 0, max: Number(value) };
2599
+ return void 0;
2600
+ };
2601
+ var publicFood = (food2) => ({
2602
+ foodId: food2.foodId,
2603
+ label: food2.label,
2604
+ knownAs: food2.knownAs,
2605
+ nutrients: food2.nutrients,
2606
+ ...food2.brand ? { brand: food2.brand } : {},
2607
+ category: food2.category,
2608
+ categoryLabel: food2.categoryLabel,
2609
+ ...food2.foodContentsLabel ? { foodContentsLabel: food2.foodContentsLabel } : {},
2610
+ image: food2.image,
2611
+ ...food2.servingSizes ? { servingSizes: food2.servingSizes } : {}
2612
+ });
2613
+ var nutrientMap = (totals) => Object.fromEntries(
2614
+ CODES.filter((code) => totals[code] !== void 0).map((code) => [
2615
+ code,
2616
+ {
2617
+ label: NUTRIENTS[code].label,
2618
+ quantity: round(totals[code] ?? 0),
2619
+ unit: NUTRIENTS[code].unit
2620
+ }
2621
+ ])
2622
+ );
2623
+ var dailyMap = (totals) => Object.fromEntries(
2624
+ CODES.filter((code) => totals[code] !== void 0 && code !== "SUGAR").map((code) => [
2625
+ code,
2626
+ {
2627
+ label: NUTRIENTS[code].label,
2628
+ quantity: round((totals[code] ?? 0) / NUTRIENTS[code].daily * 100),
2629
+ unit: "%"
2630
+ }
2631
+ ])
2632
+ );
2633
+ var scaled = (food2, grams) => Object.fromEntries(
2634
+ Object.entries(food2.nutrients).map(([code, per100]) => [code, (per100 ?? 0) * grams / 100])
2635
+ );
2636
+ var add = (a, b) => {
2637
+ const out = { ...a };
2638
+ for (const code of CODES) {
2639
+ if (b[code] !== void 0) out[code] = (out[code] ?? 0) + (b[code] ?? 0);
2640
+ }
2641
+ return out;
2642
+ };
2643
+ var dietLabels = (totals) => {
2644
+ const kcal = totals.ENERC_KCAL ?? 0;
2645
+ if (kcal <= 0) return [];
2646
+ const labels = [];
2647
+ if ((totals.PROCNT ?? 0) * 4 / kcal >= 0.3) labels.push("HIGH_PROTEIN");
2648
+ if ((totals.CHOCDF ?? 0) * 4 / kcal <= 0.2) labels.push("LOW_CARB");
2649
+ if ((totals.FIBTG ?? 0) >= 5) labels.push("HIGH_FIBER");
2650
+ return labels.length > 0 ? labels : ["BALANCED"];
2651
+ };
2652
+ var analysis = (portions, opts) => {
2653
+ let totals = {};
2654
+ let weight = 0;
2655
+ const ingredients = portions.map((p) => {
2656
+ const grams = p.quantity * p.measure.weight;
2657
+ weight += grams;
2658
+ const nutrients = scaled(p.food, grams);
2659
+ totals = add(totals, nutrients);
2660
+ return {
2661
+ ...p.text !== void 0 ? { text: p.text } : {},
2662
+ parsed: [
2663
+ {
2664
+ quantity: p.quantity,
2665
+ measure: p.measure.label.toLowerCase(),
2666
+ foodMatch: p.food.knownAs,
2667
+ food: p.food.knownAs,
2668
+ foodId: p.food.foodId,
2669
+ weight: round(grams),
2670
+ retainedWeight: round(grams),
2671
+ nutrients: nutrientMap(nutrients),
2672
+ measureURI: p.measure.uri,
2673
+ status: "OK"
2674
+ }
2675
+ ]
2676
+ };
2677
+ });
2678
+ const health = portions.length === 0 ? [] : portions.map((p) => p.food.healthLabels).reduce((acc, labels) => acc.filter((l) => labels.includes(l)));
2679
+ return {
2680
+ uri: `http://www.edamam.com/ontologies/edamam.owl#${opaqueToken(opts.seed, 24)}`,
2681
+ ...opts.yield !== void 0 ? { yield: opts.yield } : {},
2682
+ calories: Math.round(totals.ENERC_KCAL ?? 0),
2683
+ totalWeight: round(weight),
2684
+ dietLabels: dietLabels(totals),
2685
+ healthLabels: health,
2686
+ cautions: [],
2687
+ totalNutrients: nutrientMap(totals),
2688
+ totalDaily: dailyMap(totals),
2689
+ ingredients
2690
+ };
2691
+ };
2692
+ var titleCase = (s) => s.replace(/\b\w/g, (c) => c.toUpperCase());
2693
+ var buildRecipe = (seed, foods) => {
2694
+ let totals = {};
2695
+ let weight = 0;
2696
+ const ingredients = seed.ingredients.map((i) => {
2697
+ const food2 = foods.find((f) => f.foodId === i.foodId);
2698
+ const measure = food2?.measures.find((mm) => mm.label === i.measure);
2699
+ const grams = i.quantity * (measure?.weight ?? 100);
2700
+ weight += grams;
2701
+ if (food2) totals = add(totals, scaled(food2, grams));
2702
+ return {
2703
+ text: i.text,
2704
+ quantity: i.quantity,
2705
+ measure: i.measure.toLowerCase(),
2706
+ food: food2?.knownAs ?? i.foodId,
2707
+ weight: round(grams),
2708
+ foodCategory: food2?.category.toLowerCase() ?? null,
2709
+ foodId: i.foodId,
2710
+ image: food2?.image ?? null
2711
+ };
2712
+ });
2713
+ const slug = seed.label.toLowerCase().replace(/[^a-z0-9]+/g, "-");
2714
+ return {
2715
+ uri: `${RECIPE_URI}${seed.id}`,
2716
+ label: seed.label,
2717
+ image: `https://edamam-product-images.s3.amazonaws.com/web-img/${seed.id}.jpg`,
2718
+ source: "Mockingbird Kitchen",
2719
+ url: `https://kitchen.mockingbird.dev/recipes/${slug}`,
2720
+ shareAs: `http://www.edamam.com/recipe/${slug}/${seed.id}`,
2721
+ yield: seed.yield,
2722
+ dietLabels: seed.dietLabels,
2723
+ healthLabels: seed.healthLabels.map((l) => titleCase(l.toLowerCase().replace(/_/g, "-"))),
2724
+ cautions: seed.cautions,
2725
+ ingredientLines: seed.ingredients.map((i) => i.text),
2726
+ ingredients,
2727
+ calories: round(totals.ENERC_KCAL ?? 0),
2728
+ totalWeight: round(weight),
2729
+ totalTime: seed.totalTime,
2730
+ cuisineType: seed.cuisineType,
2731
+ mealType: seed.mealType,
2732
+ dishType: seed.dishType,
2733
+ totalNutrients: nutrientMap(totals),
2734
+ totalDaily: dailyMap(totals)
2735
+ };
2736
+ };
2737
+ var perServing = (recipe, code) => (recipe.totalNutrients[code]?.quantity ?? 0) / Math.max(1, recipe.yield);
2738
+ var norm = (s) => s.toLowerCase().replace(/[\s_]+/g, "-");
2739
+ var mealMatches = (recipeMeals, wanted) => {
2740
+ const w = wanted.toLowerCase();
2741
+ return recipeMeals.some((m2) => m2 === w || m2.split("/").includes(w) || w.split("/").includes(m2));
2742
+ };
2743
+ var matchesFilters = (recipe, seed, f) => {
2744
+ if (f.q) {
2745
+ const haystack = /* @__PURE__ */ new Set([
2746
+ ...tokens(recipe.label),
2747
+ ...recipe.ingredients.flatMap((i) => tokens(i.food))
2748
+ ]);
2749
+ if (!tokens(f.q).every((t) => haystack.has(t))) return false;
2750
+ }
2751
+ const health = seed.healthLabels.map(norm);
2752
+ if (!f.health.every((h) => health.includes(norm(h)))) return false;
2753
+ const diets = recipe.dietLabels.map(norm);
2754
+ if (!f.diet.every((d) => diets.includes(norm(d)))) return false;
2755
+ if (f.mealType.length > 0 && !f.mealType.some((m2) => mealMatches(recipe.mealType, m2)))
2756
+ return false;
2757
+ if (f.dishType.length > 0 && !f.dishType.some((d) => recipe.dishType.includes(d.toLowerCase())))
2758
+ return false;
2759
+ if (f.cuisineType.length > 0 && !f.cuisineType.some((c) => recipe.cuisineType.includes(c.toLowerCase())))
2760
+ return false;
2761
+ if (f.excluded.some((x) => recipe.ingredients.some((i) => i.food.includes(x.toLowerCase()))))
2762
+ return false;
2763
+ const kcal = perServing(recipe, "ENERC_KCAL");
2764
+ if (f.calories && (kcal < f.calories.min || kcal > f.calories.max)) return false;
2765
+ if (f.time && (recipe.totalTime < f.time.min || recipe.totalTime > f.time.max)) return false;
2766
+ for (const [code, range] of Object.entries(f.nutrients)) {
2767
+ const value = perServing(recipe, code);
2768
+ if (range && (value < range.min || value > range.max)) return false;
2769
+ }
2770
+ return true;
2771
+ };
2772
+ var shuffle = (items, seed) => items.map((item, i) => ({ item, key: opaqueToken(`${seed}:${i}`, 8) })).sort((a, b) => a.key < b.key ? -1 : 1).map((x) => x.item);
2773
+ var MEASURE_SERVING = `${MEASURE_URI}serving`;
2774
+ var MEASURE_GRAM = `${MEASURE_URI}gram`;
2775
+
2776
+ // src/state.ts
2777
+ var DEFAULT_SETTINGS = { apps: [], requireAccountUser: false, vision: null };
2778
+ var EdamamState = class {
2779
+ constructor(sqlite, namespace, seed) {
2780
+ this.seed = seed;
2781
+ this.foods = new Collection(sqlite, namespace, "foods");
2782
+ this.recipes = new Collection(sqlite, namespace, "recipes");
2783
+ this.settings = new Collection(sqlite, namespace, "settings");
2784
+ this.ensureSeeded();
2785
+ }
2786
+ seed;
2787
+ foods;
2788
+ recipes;
2789
+ settings;
2790
+ ensureSeeded() {
2791
+ if (this.foods.count() === 0) {
2792
+ for (const food2 of this.seed.foods.length > 0 ? this.seed.foods : DEFAULT_FOODS) {
2793
+ this.foods.insert(food2.foodId, food2);
2794
+ }
2795
+ }
2796
+ if (this.recipes.count() === 0) {
2797
+ for (const recipe of this.seed.recipes.length > 0 ? this.seed.recipes : DEFAULT_RECIPES) {
2798
+ this.recipes.insert(recipe.id, recipe);
2799
+ }
2800
+ }
2801
+ if (!this.settings.has("settings")) {
2802
+ this.settings.insert("settings", { ...DEFAULT_SETTINGS, ...this.seed.settings });
2803
+ }
2804
+ }
2805
+ current() {
2806
+ return this.settings.get("settings") ?? DEFAULT_SETTINGS;
2807
+ }
2808
+ update(patch) {
2809
+ const next = { ...this.current(), ...patch };
2810
+ this.settings.insert("settings", next);
2811
+ return next;
2812
+ }
2813
+ allFoods() {
2814
+ return this.foods.list({ order: "oldest" }).map((r) => r.value);
2815
+ }
2816
+ allRecipes() {
2817
+ return this.recipes.list({ order: "oldest" }).map((r) => r.value);
2818
+ }
2819
+ };
2820
+
2821
+ // src/runtime.ts
2822
+ var FOOD_PREFIXES = ["/api/food-database", "/api/nutrition-data", "/api/nutrition-details"];
2823
+ var RECIPE_PREFIXES = ["/api/recipes", "/api/meal-planner", "/api/shopping-list"];
2824
+ var everywhere = (status, error, message) => [
2825
+ ...FOOD_PREFIXES.map((pathPrefix) => ({
2826
+ pathPrefix,
2827
+ status,
2828
+ body: { status: "error", error, message }
2829
+ })),
2830
+ ...RECIPE_PREFIXES.map((pathPrefix) => ({
2831
+ pathPrefix,
2832
+ status,
2833
+ body: [{ errorCode: error, message, params: [] }]
2834
+ }))
2835
+ ];
2836
+ var EDAMAM_PRESETS = {
2837
+ rate_limited: {
2838
+ description: "Every API answers 429 Usage limits are exceeded (the meal adapter flags rateLimited)",
2839
+ rules: everywhere(429, "usage_limits", "Usage limits are exceeded")
2840
+ },
2841
+ payment_required: {
2842
+ description: "Every API answers 402 (plan quota exhausted; makor chat treats it as a rate limit)",
2843
+ rules: everywhere(402, "payment_required", "Payment required: plan limits reached")
2844
+ },
2845
+ unauthorized: {
2846
+ description: "Every API answers 401 (keys revoked)",
2847
+ rules: everywhere(401, "unauthorized", "Unauthorized app_id")
2848
+ },
2849
+ server_error: {
2850
+ description: "Every API answers 500",
2851
+ rules: everywhere(500, "internal_error", "Internal server error")
2852
+ },
2853
+ parser_schema_drift: {
2854
+ description: "The food parser answers `parsed` as a string (our zod schema rejects it)",
2855
+ rules: [{ operationId: "FoodParser", effect: "parser_schema_drift" }]
2856
+ },
2857
+ recipe_quality: {
2858
+ description: "nutrition-details answers 555 Recipe with insufficient quality",
2859
+ rules: [{ operationId: "NutritionDetails", effect: "recipe_quality" }]
2860
+ },
2861
+ vision_not_found: {
2862
+ description: "nutrients-from-image recognises nothing (an empty object)",
2863
+ rules: [{ operationId: "FoodFromImage", effect: "vision_not_found" }]
2864
+ },
2865
+ meal_plan_incomplete: {
2866
+ description: "The meal planner leaves each day's last section unassigned (status INCOMPLETE)",
2867
+ rules: [{ operationId: "MealPlanSelect", effect: "meal_plan_incomplete" }]
2868
+ },
2869
+ meal_plan_timeout: {
2870
+ description: "The meal planner answers status TIME_OUT with no selection",
2871
+ rules: [{ operationId: "MealPlanSelect", effect: "meal_plan_timeout" }]
2872
+ },
2873
+ slow: {
2874
+ description: "Every call answers after 12 s (past our 10 s parser and recipe timeouts)",
2875
+ rules: [...FOOD_PREFIXES, ...RECIPE_PREFIXES].map((pathPrefix) => ({
2876
+ pathPrefix,
2877
+ latencyMs: 12e3
2878
+ }))
2879
+ },
2880
+ connection_drop: {
2881
+ description: "The connection drops before any answer (fetch rejects)",
2882
+ rules: [...FOOD_PREFIXES, ...RECIPE_PREFIXES].map((pathPrefix) => ({ pathPrefix, drop: true }))
2883
+ }
2884
+ };
2885
+ var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2886
+ var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
2887
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2888
+ var adminRoutes = (runtime) => ({
2889
+ "GET /foods": ({ namespace }) => json3(200, { foods: runtime.instance(namespace).state.allFoods() }),
2890
+ "POST /foods": ({ body, namespace }) => {
2891
+ if (!isRecord4(body) || typeof body.foodId !== "string" || typeof body.label !== "string" || !Array.isArray(body.measures)) {
2892
+ return adminError3(
2893
+ 400,
2894
+ "expected a food {foodId, label, nutrients, measures: [{uri, label, weight}], \u2026}"
2895
+ );
2896
+ }
2897
+ const defaults = {
2898
+ knownAs: body.label.toLowerCase(),
2899
+ nutrients: {},
2900
+ category: "Generic foods",
2901
+ categoryLabel: "food",
2902
+ image: "",
2903
+ healthLabels: []
2904
+ };
2905
+ const food2 = { ...defaults, ...body };
2906
+ return json3(201, runtime.instance(namespace).addFood(food2));
2907
+ },
2908
+ "GET /recipes": ({ namespace }) => json3(200, { recipes: runtime.instance(namespace).recipes() }),
2909
+ "POST /recipes": ({ body, namespace }) => {
2910
+ if (!isRecord4(body) || typeof body.id !== "string" || typeof body.label !== "string" || !Array.isArray(body.ingredients)) {
2911
+ return adminError3(
2912
+ 400,
2913
+ "expected a recipe seed {id, label, yield, ingredients: [{foodId, quantity, measure, text}], \u2026}"
2914
+ );
2915
+ }
2916
+ const defaults = {
2917
+ yield: 1,
2918
+ totalTime: 0,
2919
+ mealType: [],
2920
+ dishType: [],
2921
+ cuisineType: [],
2922
+ dietLabels: [],
2923
+ healthLabels: [],
2924
+ cautions: []
2925
+ };
2926
+ const seed = { ...defaults, ...body };
2927
+ return json3(201, runtime.instance(namespace).addRecipe(seed));
2928
+ },
2929
+ "PUT /vision": ({ body, namespace }) => {
2930
+ if (body === null) return json3(200, runtime.instance(namespace).state.update({ vision: null }));
2931
+ if (!isRecord4(body) || typeof body.foodId !== "string" && body.notFound !== true) {
2932
+ return adminError3(
2933
+ 400,
2934
+ 'expected {"foodId", "quantity"?, "measure"?}, {"notFound": true} or null'
2935
+ );
2936
+ }
2937
+ const vision = body.notFound === true ? { notFound: true } : {
2938
+ foodId: String(body.foodId),
2939
+ ...typeof body.quantity === "number" ? { quantity: body.quantity } : {},
2940
+ ...typeof body.measure === "string" ? { measure: body.measure } : {}
2941
+ };
2942
+ return json3(200, runtime.instance(namespace).state.update({ vision }));
2943
+ },
2944
+ "GET /settings": ({ namespace }) => json3(200, runtime.instance(namespace).state.current()),
2945
+ "PUT /settings": ({ body, namespace }) => {
2946
+ if (!isRecord4(body)) return adminError3(400, "expected a JSON object");
2947
+ const patch = {};
2948
+ if (body.apps !== void 0) {
2949
+ if (!Array.isArray(body.apps)) return adminError3(400, "apps: [{appId, appKey}]");
2950
+ patch.apps = body.apps.filter(isRecord4).map((a) => ({ appId: String(a.appId), appKey: String(a.appKey) }));
2951
+ }
2952
+ if (body.requireAccountUser !== void 0) {
2953
+ if (typeof body.requireAccountUser !== "boolean")
2954
+ return adminError3(400, "requireAccountUser: boolean");
2955
+ patch.requireAccountUser = body.requireAccountUser;
2956
+ }
2957
+ return json3(200, runtime.instance(namespace).state.update(patch));
2958
+ }
2959
+ });
2960
+ var createRuntime2 = (options = {}) => createRuntime({
2961
+ name: EDAMAM_NAMESPACE,
2962
+ document,
2963
+ ...options.sqlite ? { sqlite: options.sqlite } : {},
2964
+ ...options.clock ? { clock: options.clock } : {},
2965
+ ...options.seed !== void 0 ? { seed: options.seed } : {},
2966
+ ...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
2967
+ ...options.onLog ? { onLog: options.onLog } : {},
2968
+ credential: appIdCredential,
2969
+ presets: EDAMAM_PRESETS,
2970
+ create: ({ sqlite, namespace, clock }) => new EdamamAPI({
2971
+ sqlite,
2972
+ namespace,
2973
+ now: clock.now,
2974
+ ...options.foods ? { foods: options.foods } : {},
2975
+ ...options.recipes ? { recipes: options.recipes } : {},
2976
+ ...options.settings ? { settings: options.settings } : {}
2977
+ }),
2978
+ admin: adminRoutes
2979
+ });
2980
+
2981
+ // src/index.ts
2982
+ var EDAMAM_NAMESPACE = "edamam";
2983
+ var ACCOUNT_USER_HEADER = "edamam-account-user";
2984
+ var PAGE_SIZE = 20;
2985
+ var foodError = (status, error, message) => jsonRes(status, { status: "error", error, message });
2986
+ var recipeErrors = (status, errorCode, message, params = []) => jsonRes(status, [{ errorCode, message, params }]);
2987
+ var appIdCredential = (request) => new URL(request.url).searchParams.get("app_id") ?? basicAuth(request)?.username ?? void 0;
2988
+ var RECIPE_FAMILY = /* @__PURE__ */ new Set([
2989
+ "RecipeSearch",
2990
+ "RecipesByUri",
2991
+ "RecipeById",
2992
+ "MealPlanSelect",
2993
+ "ShoppingList"
2994
+ ]);
2995
+ var many = (url, key) => url.searchParams.getAll(key).filter((v) => v.length > 0);
2996
+ var base64url = (value) => toBase64(new TextEncoder().encode(value)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2997
+ var EdamamAPI = class {
2998
+ app;
2999
+ sqlite;
3000
+ state;
3001
+ service;
3002
+ now;
3003
+ constructor(options = {}) {
3004
+ const sqlite = bootSqlite(options.sqlite);
3005
+ const namespace = options.namespace ?? EDAMAM_NAMESPACE;
3006
+ this.now = options.now ?? (() => Date.now());
3007
+ this.state = new EdamamState(sqlite, namespace, {
3008
+ foods: options.foods ?? [],
3009
+ recipes: options.recipes ?? [],
3010
+ settings: options.settings ?? {}
3011
+ });
3012
+ const handlers = defineOperations({
3013
+ FoodParser: (context) => this.parser(context),
3014
+ FoodNutrients: (context) => this.nutrients(context),
3015
+ FoodFromImage: (context) => this.vision(context),
3016
+ NutritionData: (context) => this.nutritionData(context),
3017
+ NutritionDetails: (context) => this.nutritionDetails(context),
3018
+ RecipeSearch: (context) => this.recipeSearch(context),
3019
+ RecipesByUri: (context) => this.recipesByUri(context),
3020
+ RecipeById: (context) => {
3021
+ const seed = this.state.recipes.get(context.params.id ?? "");
3022
+ if (!seed) return recipeErrors(404, "not_found", `Recipe ${context.params.id} not found`);
3023
+ return jsonRes(200, this.hit(this.recipe(seed), context.url.origin));
3024
+ },
3025
+ MealPlanSelect: (context) => this.mealPlan(context),
3026
+ ShoppingList: (context) => this.shoppingList(context)
3027
+ });
3028
+ this.service = createService({
3029
+ document,
3030
+ handlers,
3031
+ sqlite,
3032
+ namespace,
3033
+ now: this.now,
3034
+ notFound: () => foodError(404, "not_found", "Not Found"),
3035
+ onError: (error) => {
3036
+ if (error instanceof HttpError) return error.toResponse();
3037
+ throw error;
3038
+ },
3039
+ before: (context) => this.authenticate(context)
3040
+ });
3041
+ this.app = this.service.app;
3042
+ this.sqlite = this.service.sqlite;
3043
+ }
3044
+ fetch(request) {
3045
+ return this.service.fetch(request);
3046
+ }
3047
+ async reset() {
3048
+ await this.service.reset();
3049
+ this.state.ensureSeeded();
3050
+ }
3051
+ authenticate(context) {
3052
+ const recipeFamily = RECIPE_FAMILY.has(context.operation.operationId);
3053
+ const fail = (message) => recipeFamily ? recipeErrors(401, "unauthorized", message) : foodError(401, "unauthorized", message);
3054
+ const basic = basicAuth(context.request);
3055
+ const appId = context.url.searchParams.get("app_id") ?? basic?.username;
3056
+ const appKey = context.url.searchParams.get("app_key") ?? basic?.password;
3057
+ if (!appId || !appKey) return fail("Missing app_id or app_key");
3058
+ const apps = this.state.current().apps;
3059
+ if (apps.length > 0 && !apps.some((a) => a.appId === appId && a.appKey === appKey)) {
3060
+ return fail(`Unauthorized app_id = ${appId}. This app_id is not configured for this API.`);
3061
+ }
3062
+ if (recipeFamily && this.state.current().requireAccountUser && !context.request.headers.get(ACCOUNT_USER_HEADER)) {
3063
+ return fail("Edamam-Account-User header is required for this application");
3064
+ }
3065
+ return void 0;
3066
+ }
3067
+ foods() {
3068
+ return this.state.allFoods();
3069
+ }
3070
+ recipe(seed) {
3071
+ return buildRecipe(seed, this.foods());
3072
+ }
3073
+ hit(recipe, origin) {
3074
+ const id = recipe.uri.slice(RECIPE_URI.length);
3075
+ return {
3076
+ recipe,
3077
+ _links: { self: { title: "Self", href: `${origin}/api/recipes/v2/${id}?type=public` } }
3078
+ };
3079
+ }
3080
+ jsonBody(context, recipeFamily = false, invalidStatus = recipeFamily ? 400 : 422) {
3081
+ const issues = bodyIssues(context);
3082
+ if (issues.length > 0) {
3083
+ const message = `Invalid request body: ${issues.map((i) => `${i.path || "body"} ${i.message}`).join("; ")}`;
3084
+ throw new HttpError(
3085
+ invalidStatus,
3086
+ recipeFamily ? [{ errorCode: "illegal_param", message, params: issues.map((i) => i.path) }] : { status: "error", error: "bad_request", message }
3087
+ );
3088
+ }
3089
+ return context.body.kind === "json" ? context.body.value : {};
3090
+ }
3091
+ foodFilter(url) {
3092
+ const categoryLabel = url.searchParams.get("categoryLabel");
3093
+ const category = url.searchParams.get("category");
3094
+ const health = many(url, "health").map((h) => h.toUpperCase().replace(/-/g, "_"));
3095
+ const caloriesParam = url.searchParams.get("calories");
3096
+ const calories = caloriesParam ? parseRange(caloriesParam) : void 0;
3097
+ if (caloriesParam && !calories) {
3098
+ return foodError(400, "bad_request", `Illegal value for calories: ${caloriesParam}`);
3099
+ }
3100
+ const categories = {
3101
+ "generic-foods": "Generic foods",
3102
+ "packaged-foods": "Packaged foods",
3103
+ "generic-meals": "Generic meals"
3104
+ };
3105
+ return (food2) => (!categoryLabel || food2.categoryLabel === categoryLabel) && (!category || food2.category === categories[category]) && health.every((h) => food2.healthLabels.includes(h)) && (!calories || food2.nutrients.ENERC_KCAL !== void 0 && food2.nutrients.ENERC_KCAL >= calories.min && food2.nutrients.ENERC_KCAL <= calories.max);
3106
+ }
3107
+ parser(context) {
3108
+ const url = context.url;
3109
+ const ingr = url.searchParams.get("ingr");
3110
+ const upc = url.searchParams.get("upc");
3111
+ const filter = this.foodFilter(url);
3112
+ if (filter instanceof Response) return filter;
3113
+ const foods = this.foods().filter(filter);
3114
+ if (upc) {
3115
+ const food2 = this.foods().find((f) => f.upc === upc);
3116
+ if (!food2) return foodError(404, "not_found", `No food found for UPC ${upc}`);
3117
+ return annotateResponse(
3118
+ jsonRes(200, {
3119
+ text: upc,
3120
+ parsed: [],
3121
+ hints: [{ food: publicFood(food2), measures: food2.measures }]
3122
+ }),
3123
+ { ids: { foodId: food2.foodId } }
3124
+ );
3125
+ }
3126
+ if (!ingr) return foodError(400, "bad_request", "Missing required parameter: ingr or upc");
3127
+ if (faultEffect(context.request, "parser_schema_drift") !== void 0) {
3128
+ return jsonRes(200, { text: ingr, parsed: "unavailable", hints: [] });
3129
+ }
3130
+ const parsedLine = parseLine(ingr, foods);
3131
+ const hints = relatedFoods(ingr, foods);
3132
+ const ordered = parsedLine ? [parsedLine.food, ...hints.filter((f) => f !== parsedLine.food)] : hints;
3133
+ return jsonRes(200, {
3134
+ text: ingr,
3135
+ parsed: parsedLine ? [
3136
+ {
3137
+ food: publicFood(parsedLine.food),
3138
+ ...parsedLine.quantity !== void 0 ? { quantity: parsedLine.quantity } : {},
3139
+ ...parsedLine.measure ? { measure: parsedLine.measure } : {}
3140
+ }
3141
+ ] : [],
3142
+ hints: ordered.map((food2) => ({ food: publicFood(food2), measures: food2.measures })),
3143
+ _links: {}
3144
+ });
3145
+ }
3146
+ nutrients(context) {
3147
+ const body = this.jsonBody(context);
3148
+ const lines = body.ingredients;
3149
+ const portions = [];
3150
+ for (const line of lines) {
3151
+ const food2 = this.state.foods.get(line.foodId);
3152
+ const measure = food2?.measures.find((m2) => m2.uri === line.measureURI);
3153
+ if (!food2 || !measure) {
3154
+ return foodError(
3155
+ 422,
3156
+ "low_quality",
3157
+ `Unknown food ${line.foodId} or measure ${line.measureURI}`
3158
+ );
3159
+ }
3160
+ portions.push({ food: food2, quantity: line.quantity, measure });
3161
+ }
3162
+ return jsonRes(200, analysis(portions, { seed: JSON.stringify(lines) }));
3163
+ }
3164
+ vision(context) {
3165
+ const body = this.jsonBody(context, false, 400);
3166
+ const image = String(body.image);
3167
+ if (!/^(data:image\/[a-z+]+;base64,|https?:\/\/)/.test(image)) {
3168
+ return foodError(400, "bad_request", "The image must be a data URL or an http(s) URL");
3169
+ }
3170
+ const override = this.state.current().vision;
3171
+ if (faultEffect(context.request, "vision_not_found") !== void 0 || override && "notFound" in override) {
3172
+ return jsonRes(200, {});
3173
+ }
3174
+ const candidates = this.foods().filter(
3175
+ (f) => f.categoryLabel === "meal" || f.foodId === "food_salmon"
3176
+ );
3177
+ const pick = override && "foodId" in override ? this.state.foods.get(override.foodId) : candidates[Number.parseInt(opaqueToken(image, 4), 36) % Math.max(1, candidates.length)];
3178
+ if (!pick) return jsonRes(200, {});
3179
+ const measure = pick.measures.find(
3180
+ (m2) => m2.label === (override && "measure" in override ? override.measure : "Serving")
3181
+ ) ?? pick.measures[0];
3182
+ const quantity = override && "quantity" in override && override.quantity ? override.quantity : 1;
3183
+ const result = analysis(measure ? [{ food: pick, quantity, measure }] : [], { seed: image });
3184
+ return jsonRes(200, {
3185
+ parsed: { food: publicFood(pick), quantity, ...measure ? { measure } : {} },
3186
+ recipe: {
3187
+ label: pick.label,
3188
+ calories: result.calories,
3189
+ totalNutrients: result.totalNutrients
3190
+ }
3191
+ });
3192
+ }
3193
+ portionOf(text) {
3194
+ const line = parseLine(text, this.foods());
3195
+ if (!line?.measure) {
3196
+ if (!line) return void 0;
3197
+ const measure = line.food.measures.find((m2) => m2.label === "Serving") ?? line.food.measures[0];
3198
+ return measure ? { food: line.food, quantity: 1, measure, text } : void 0;
3199
+ }
3200
+ return { food: line.food, quantity: line.quantity ?? 1, measure: line.measure, text };
3201
+ }
3202
+ nutritionData(context) {
3203
+ const ingr = context.url.searchParams.get("ingr") ?? "";
3204
+ const portion = this.portionOf(ingr);
3205
+ if (!portion) return foodError(422, "low_quality", `Could not parse ingredient: ${ingr}`);
3206
+ return jsonRes(200, analysis([portion], { seed: ingr }));
3207
+ }
3208
+ nutritionDetails(context) {
3209
+ const body = this.jsonBody(context);
3210
+ const lines = body.ingr ?? [];
3211
+ if (lines.length === 0) return foodError(422, "low_quality", "No ingredients to analyse");
3212
+ if (faultEffect(context.request, "recipe_quality") !== void 0) {
3213
+ return foodError(555, "low_quality", "Recipe with insufficient quality to process correctly");
3214
+ }
3215
+ const portions = lines.map((line) => this.portionOf(line));
3216
+ if (portions.some((p) => p === void 0)) {
3217
+ return foodError(555, "low_quality", "Recipe with insufficient quality to process correctly");
3218
+ }
3219
+ return jsonRes(
3220
+ 200,
3221
+ analysis(portions, {
3222
+ seed: JSON.stringify(body),
3223
+ yield: typeof body.yield === "number" ? body.yield : 1
3224
+ })
3225
+ );
3226
+ }
3227
+ recipeFilters(url) {
3228
+ const range = (key) => {
3229
+ const value = url.searchParams.get(key);
3230
+ if (value === null) return void 0;
3231
+ const parsed = parseRange(value);
3232
+ if (!parsed)
3233
+ throw new HttpError(400, [
3234
+ {
3235
+ errorCode: "illegal_param",
3236
+ message: `Illegal value for ${key}: ${value}`,
3237
+ params: [key]
3238
+ }
3239
+ ]);
3240
+ return parsed;
3241
+ };
3242
+ try {
3243
+ const nutrients = {};
3244
+ for (const code of [
3245
+ "ENERC_KCAL",
3246
+ "PROCNT",
3247
+ "FAT",
3248
+ "CHOCDF",
3249
+ "FIBTG",
3250
+ "SUGAR"
3251
+ ]) {
3252
+ const r = range(`nutrients[${code}]`);
3253
+ if (r) nutrients[code] = r;
3254
+ }
3255
+ const calories = range("calories");
3256
+ const time = range("time");
3257
+ const q = url.searchParams.get("q");
3258
+ return {
3259
+ ...q ? { q } : {},
3260
+ health: many(url, "health"),
3261
+ diet: many(url, "diet"),
3262
+ mealType: many(url, "mealType"),
3263
+ dishType: many(url, "dishType"),
3264
+ cuisineType: many(url, "cuisineType"),
3265
+ excluded: many(url, "excluded"),
3266
+ ...calories ? { calories } : {},
3267
+ ...time ? { time } : {},
3268
+ nutrients
3269
+ };
3270
+ } catch (error) {
3271
+ if (error instanceof HttpError) return error.toResponse();
3272
+ throw error;
3273
+ }
3274
+ }
3275
+ recipeSearch(context) {
3276
+ const url = context.url;
3277
+ if (!url.searchParams.get("type")) {
3278
+ return recipeErrors(400, "illegal_param", "Parameter 'type' is required", ["type"]);
3279
+ }
3280
+ const filters = this.recipeFilters(url);
3281
+ if (filters instanceof Response) return filters;
3282
+ let matches2 = this.state.allRecipes().map((seed) => ({ seed, recipe: this.recipe(seed) })).filter(({ seed, recipe }) => matchesFilters(recipe, seed, filters)).map(({ recipe }) => recipe);
3283
+ if (url.searchParams.get("random") === "true") matches2 = shuffle(matches2, url.search);
3284
+ const cont = url.searchParams.get("_cont");
3285
+ let offset = 0;
3286
+ if (cont) {
3287
+ try {
3288
+ offset = Number(
3289
+ new TextDecoder().decode(fromBase64(cont.replace(/-/g, "+").replace(/_/g, "/")))
3290
+ );
3291
+ } catch {
3292
+ offset = Number.NaN;
3293
+ }
3294
+ if (!Number.isInteger(offset) || offset < 0) {
3295
+ return recipeErrors(400, "illegal_param", "Invalid _cont token", ["_cont"]);
3296
+ }
3297
+ }
3298
+ const page = matches2.slice(offset, offset + PAGE_SIZE);
3299
+ const next = new URL(`${url.origin}${url.pathname}`);
3300
+ for (const [key, value] of url.searchParams)
3301
+ if (key !== "_cont") next.searchParams.append(key, value);
3302
+ next.searchParams.set("_cont", base64url(String(offset + PAGE_SIZE)));
3303
+ return jsonRes(200, {
3304
+ from: page.length > 0 ? offset + 1 : 0,
3305
+ to: offset + page.length,
3306
+ count: matches2.length,
3307
+ _links: offset + PAGE_SIZE < matches2.length ? { next: { href: next.toString(), title: "Next page" } } : {},
3308
+ hits: page.map((recipe) => this.hit(recipe, url.origin))
3309
+ });
3310
+ }
3311
+ recipesByUri(context) {
3312
+ const url = context.url;
3313
+ const uris = many(url, "uri");
3314
+ if (uris.length === 0 || uris.length > 20) {
3315
+ return recipeErrors(400, "illegal_param", "Between 1 and 20 uri parameters are required", [
3316
+ "uri"
3317
+ ]);
3318
+ }
3319
+ const hits = uris.map(
3320
+ (uri) => this.state.recipes.get(uri.startsWith(RECIPE_URI) ? uri.slice(RECIPE_URI.length) : uri)
3321
+ ).filter((seed) => seed !== void 0).map((seed) => this.hit(this.recipe(seed), url.origin));
3322
+ return jsonRes(200, {
3323
+ from: hits.length > 0 ? 1 : 0,
3324
+ to: hits.length,
3325
+ count: hits.length,
3326
+ _links: {},
3327
+ hits
3328
+ });
3329
+ }
3330
+ /** Whether a recipe satisfies a plan section's `accept` predicates and per-serving `fit`. */
3331
+ fits(recipe, seed, section, inherited) {
3332
+ const predicates = [...inherited.accept?.all ?? [], ...section.accept?.all ?? []];
3333
+ for (const p of predicates) {
3334
+ if (p.health && !p.health.every((h) => seed.healthLabels.includes(h.toUpperCase().replace(/-/g, "_"))))
3335
+ return false;
3336
+ if (p.meal && !p.meal.some((meal) => recipe.mealType.includes(meal.toLowerCase())))
3337
+ return false;
3338
+ if (p.dish && !p.dish.some((dish) => recipe.dishType.includes(dish.toLowerCase())))
3339
+ return false;
3340
+ }
3341
+ if ([...inherited.exclude ?? [], ...section.exclude ?? []].includes(recipe.uri))
3342
+ return false;
3343
+ for (const [code, band] of Object.entries(section.fit ?? {})) {
3344
+ const value = perServing(recipe, code);
3345
+ if (band?.min !== void 0 && value < band.min) return false;
3346
+ if (band?.max !== void 0 && value > band.max) return false;
3347
+ }
3348
+ return true;
3349
+ }
3350
+ mealPlan(context) {
3351
+ const url = context.url;
3352
+ const appId = url.searchParams.get("app_id") ?? basicAuth(context.request)?.username;
3353
+ if (context.params.app_id !== appId) {
3354
+ return recipeErrors(
3355
+ 401,
3356
+ "unauthorized",
3357
+ "The app_id in the path does not match the credentials"
3358
+ );
3359
+ }
3360
+ const body = this.jsonBody(context, true);
3361
+ const size = Number(body.size);
3362
+ const plan = body.plan ?? {};
3363
+ const sections = plan.sections ?? {};
3364
+ if (Object.keys(sections).length === 0) {
3365
+ return recipeErrors(400, "illegal_param", "The plan must define at least one section", [
3366
+ "plan.sections"
3367
+ ]);
3368
+ }
3369
+ if (faultEffect(context.request, "meal_plan_timeout") !== void 0) {
3370
+ return jsonRes(200, { status: "TIME_OUT", selection: [] });
3371
+ }
3372
+ const all = this.state.allRecipes().map((seed) => ({ seed, recipe: this.recipe(seed) }));
3373
+ let complete = true;
3374
+ const selection = Array.from({ length: size }, (_, day) => {
3375
+ const used = /* @__PURE__ */ new Set();
3376
+ const out = {};
3377
+ let dayTotals = 0;
3378
+ for (const [name, section] of Object.entries(sections)) {
3379
+ const candidates = all.filter(
3380
+ ({ seed, recipe }) => !used.has(recipe.uri) && this.fits(recipe, seed, section, plan)
3381
+ );
3382
+ const chosen = candidates.length > 0 ? candidates[day % candidates.length] : void 0;
3383
+ if (!chosen) {
3384
+ complete = false;
3385
+ out[name] = {};
3386
+ continue;
3387
+ }
3388
+ used.add(chosen.recipe.uri);
3389
+ dayTotals += perServing(chosen.recipe, "ENERC_KCAL");
3390
+ const id = chosen.recipe.uri.slice(RECIPE_URI.length);
3391
+ out[name] = {
3392
+ assigned: chosen.recipe.uri,
3393
+ _links: {
3394
+ self: {
3395
+ title: "Recipe details",
3396
+ href: `${url.origin}/api/recipes/v2/${id}?type=public`
3397
+ }
3398
+ }
3399
+ };
3400
+ }
3401
+ const kcal = plan.fit?.ENERC_KCAL;
3402
+ if (kcal && (kcal.min !== void 0 && dayTotals < kcal.min || kcal.max !== void 0 && dayTotals > kcal.max)) {
3403
+ complete = false;
3404
+ }
3405
+ return { sections: out };
3406
+ });
3407
+ if (faultEffect(context.request, "meal_plan_incomplete") !== void 0) {
3408
+ complete = false;
3409
+ for (const day of selection) {
3410
+ const last = Object.keys(day.sections).at(-1);
3411
+ if (last) day.sections[last] = {};
3412
+ }
3413
+ }
3414
+ return jsonRes(200, { status: complete ? "OK" : "INCOMPLETE", selection });
3415
+ }
3416
+ shoppingList(context) {
3417
+ const url = context.url;
3418
+ const body = this.jsonBody(context, true);
3419
+ const entries = body.entries;
3420
+ const totals = /* @__PURE__ */ new Map();
3421
+ for (const entry of entries) {
3422
+ const id = entry.item.startsWith(RECIPE_URI) ? entry.item.slice(RECIPE_URI.length) : entry.item;
3423
+ const seed = this.state.recipes.get(id);
3424
+ if (!seed)
3425
+ return recipeErrors(400, "not_found", `Unknown recipe ${entry.item}`, ["entries.item"]);
3426
+ const recipe = this.recipe(seed);
3427
+ const factor = entry.measure === MEASURE_SERVING ? entry.quantity / Math.max(1, recipe.yield) : entry.quantity;
3428
+ for (const ingredient of recipe.ingredients) {
3429
+ const current = totals.get(ingredient.foodId) ?? { food: ingredient.food, grams: 0 };
3430
+ current.grams += ingredient.weight * factor;
3431
+ totals.set(ingredient.foodId, current);
3432
+ }
3433
+ }
3434
+ const cart = url.searchParams.get("shopping-cart") === "true" && url.searchParams.get("beta") === "true";
3435
+ return jsonRes(200, {
3436
+ entries: [...totals.entries()].map(([foodId, { food: food2, grams }]) => ({
3437
+ foodId,
3438
+ food: food2,
3439
+ quantities: [
3440
+ { quantity: Math.round(grams * 10) / 10, measure: MEASURE_GRAM, qualifiers: [] }
3441
+ ]
3442
+ })),
3443
+ _links: cart ? {
3444
+ "shopping-cart": {
3445
+ title: "Shopping cart",
3446
+ href: `${url.origin}/shopping-cart/${opaqueToken(JSON.stringify(entries), 16)}`
3447
+ }
3448
+ } : {}
3449
+ });
3450
+ }
3451
+ addFood(food2) {
3452
+ this.state.foods.insert(food2.foodId, food2);
3453
+ return food2;
3454
+ }
3455
+ addRecipe(seed) {
3456
+ this.state.recipes.insert(seed.id, seed);
3457
+ return this.recipe(seed);
3458
+ }
3459
+ recipes() {
3460
+ return this.state.allRecipes().map((seed) => this.recipe(seed));
3461
+ }
3462
+ };
3463
+
3464
+ export {
3465
+ MEASURE_URI,
3466
+ RECIPE_URI,
3467
+ NUTRIENTS,
3468
+ DEFAULT_FOODS,
3469
+ DEFAULT_RECIPES,
3470
+ document,
3471
+ operationIds,
3472
+ supportedOperationIds,
3473
+ parseLine,
3474
+ buildRecipe,
3475
+ perServing,
3476
+ EDAMAM_PRESETS,
3477
+ createRuntime2 as createRuntime,
3478
+ EDAMAM_NAMESPACE,
3479
+ ACCOUNT_USER_HEADER,
3480
+ foodError,
3481
+ recipeErrors,
3482
+ appIdCredential,
3483
+ EdamamAPI
3484
+ };
3485
+ //# sourceMappingURL=chunk-DWHV3RH2.js.map