@crvouga/mockingbird-service-aws-speech 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.
- package/CHANGELOG.md +5 -0
- package/README.md +132 -0
- package/dist/chunk-3DE3INNY.js +3410 -0
- package/dist/chunk-3DE3INNY.js.map +7 -0
- package/dist/chunk-A46XUZ6Z.js +306 -0
- package/dist/chunk-A46XUZ6Z.js.map +7 -0
- package/dist/cli.js +371 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1066 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1314 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +7 -0
- package/package.json +96 -0
|
@@ -0,0 +1,3410 @@
|
|
|
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 text2 = await request.text();
|
|
165
|
+
if (text2.trim() === "")
|
|
166
|
+
return void 0;
|
|
167
|
+
return JSON.parse(text2);
|
|
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 sigV4AccessKeyId = (request) => {
|
|
393
|
+
const header = request.headers.get("authorization");
|
|
394
|
+
const fromHeader = header ? /Credential=([^/,\s]+)\//.exec(header)?.[1] : void 0;
|
|
395
|
+
if (fromHeader)
|
|
396
|
+
return fromHeader;
|
|
397
|
+
const query = new URL(request.url).searchParams.get("X-Amz-Credential");
|
|
398
|
+
return query ? query.split("/")[0] ?? void 0 : void 0;
|
|
399
|
+
};
|
|
400
|
+
var createCredentialRegistry = () => {
|
|
401
|
+
const map = /* @__PURE__ */ new Map();
|
|
402
|
+
return {
|
|
403
|
+
set: (credential, namespace) => {
|
|
404
|
+
map.set(credential, namespace);
|
|
405
|
+
},
|
|
406
|
+
get: (credential) => map.get(credential),
|
|
407
|
+
remove: (credential) => map.delete(credential),
|
|
408
|
+
clear: () => map.clear(),
|
|
409
|
+
entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
|
|
413
|
+
|
|
414
|
+
// ../core/dist/rng.js
|
|
415
|
+
var seedFrom = (value) => {
|
|
416
|
+
let hash = 2166136261;
|
|
417
|
+
for (let i = 0; i < value.length; i++) {
|
|
418
|
+
hash ^= value.charCodeAt(i);
|
|
419
|
+
hash = Math.imul(hash, 16777619);
|
|
420
|
+
}
|
|
421
|
+
return hash >>> 0;
|
|
422
|
+
};
|
|
423
|
+
var createRng = (seed = 0) => {
|
|
424
|
+
const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
|
|
425
|
+
let state = numeric;
|
|
426
|
+
const next = () => {
|
|
427
|
+
state = state + 1831565813 >>> 0;
|
|
428
|
+
let t = state;
|
|
429
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
430
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
431
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
432
|
+
};
|
|
433
|
+
return {
|
|
434
|
+
next,
|
|
435
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
436
|
+
reset: () => {
|
|
437
|
+
state = numeric;
|
|
438
|
+
},
|
|
439
|
+
state: () => state,
|
|
440
|
+
setState: (next2) => {
|
|
441
|
+
if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
|
|
442
|
+
throw new RangeError("rng state must be an unsigned 32-bit integer");
|
|
443
|
+
}
|
|
444
|
+
state = next2 >>> 0;
|
|
445
|
+
},
|
|
446
|
+
seed: numeric
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// ../core/dist/faults.js
|
|
451
|
+
var matches = (rule, candidate) => {
|
|
452
|
+
if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
|
|
456
|
+
return false;
|
|
457
|
+
if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
|
|
461
|
+
return false;
|
|
462
|
+
return true;
|
|
463
|
+
};
|
|
464
|
+
var faultResponse = (rule) => {
|
|
465
|
+
const status = rule.status ?? 500;
|
|
466
|
+
const headers = { "content-type": "application/json", ...rule.headers };
|
|
467
|
+
if (typeof rule.body === "string")
|
|
468
|
+
return new Response(rule.body, { status, headers });
|
|
469
|
+
if (rule.body === null)
|
|
470
|
+
return new Response(null, { status, headers: rule.headers ?? {} });
|
|
471
|
+
const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
|
|
472
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
473
|
+
};
|
|
474
|
+
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
475
|
+
const entries = [];
|
|
476
|
+
return {
|
|
477
|
+
add(rule) {
|
|
478
|
+
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
479
|
+
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
480
|
+
if (existing >= 0)
|
|
481
|
+
entries[existing] = entry;
|
|
482
|
+
else
|
|
483
|
+
entries.push(entry);
|
|
484
|
+
return rule;
|
|
485
|
+
},
|
|
486
|
+
list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
|
|
487
|
+
remove(id) {
|
|
488
|
+
const index = entries.findIndex((e) => e.rule.id === id);
|
|
489
|
+
if (index < 0)
|
|
490
|
+
return false;
|
|
491
|
+
entries.splice(index, 1);
|
|
492
|
+
return true;
|
|
493
|
+
},
|
|
494
|
+
clear() {
|
|
495
|
+
entries.length = 0;
|
|
496
|
+
},
|
|
497
|
+
async take(candidate) {
|
|
498
|
+
const hits = [];
|
|
499
|
+
for (const entry of entries) {
|
|
500
|
+
if (entry.remaining === 0)
|
|
501
|
+
continue;
|
|
502
|
+
if (!matches(entry.rule, candidate))
|
|
503
|
+
continue;
|
|
504
|
+
const rate = entry.rule.rate ?? 1;
|
|
505
|
+
if (rng.next() >= rate)
|
|
506
|
+
continue;
|
|
507
|
+
entry.hits++;
|
|
508
|
+
if (entry.remaining !== null)
|
|
509
|
+
entry.remaining--;
|
|
510
|
+
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
511
|
+
if (delay !== void 0 && delay > 0) {
|
|
512
|
+
await sleep(delay);
|
|
513
|
+
}
|
|
514
|
+
const hit = { id: entry.rule.id };
|
|
515
|
+
if (entry.rule.effect !== void 0) {
|
|
516
|
+
hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
|
|
517
|
+
}
|
|
518
|
+
if (entry.rule.drop === true)
|
|
519
|
+
hit.drop = true;
|
|
520
|
+
else if (entry.rule.status !== void 0)
|
|
521
|
+
hit.response = faultResponse(entry.rule);
|
|
522
|
+
hits.push(hit);
|
|
523
|
+
if (hit.drop || hit.response)
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
return hits;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// ../../openapi/core/dist/refs.js
|
|
532
|
+
var OpenAPIReferenceError = class extends Error {
|
|
533
|
+
ref;
|
|
534
|
+
constructor(ref) {
|
|
535
|
+
super(`unresolvable $ref: ${ref}`);
|
|
536
|
+
this.ref = ref;
|
|
537
|
+
this.name = "OpenAPIReferenceError";
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
541
|
+
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
542
|
+
var resolveRef = (document2, ref) => {
|
|
543
|
+
if (!ref.startsWith("#/"))
|
|
544
|
+
throw new OpenAPIReferenceError(ref);
|
|
545
|
+
let cursor = document2;
|
|
546
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
547
|
+
const segment = unescapePointer(raw);
|
|
548
|
+
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
549
|
+
throw new OpenAPIReferenceError(ref);
|
|
550
|
+
}
|
|
551
|
+
cursor = cursor[segment];
|
|
552
|
+
}
|
|
553
|
+
if (cursor === void 0)
|
|
554
|
+
throw new OpenAPIReferenceError(ref);
|
|
555
|
+
return cursor;
|
|
556
|
+
};
|
|
557
|
+
var deref = (document2, value) => {
|
|
558
|
+
let current = value;
|
|
559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
560
|
+
while (isReference(current)) {
|
|
561
|
+
if (seen.has(current.$ref))
|
|
562
|
+
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
563
|
+
seen.add(current.$ref);
|
|
564
|
+
current = resolveRef(document2, current.$ref);
|
|
565
|
+
}
|
|
566
|
+
return current;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// ../../openapi/core/dist/types.js
|
|
570
|
+
var HTTP_METHODS = [
|
|
571
|
+
"get",
|
|
572
|
+
"put",
|
|
573
|
+
"post",
|
|
574
|
+
"delete",
|
|
575
|
+
"options",
|
|
576
|
+
"head",
|
|
577
|
+
"patch",
|
|
578
|
+
"trace"
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
// ../../openapi/core/dist/document.js
|
|
582
|
+
var mergeParameters = (document2, item, own) => {
|
|
583
|
+
const merged = /* @__PURE__ */ new Map();
|
|
584
|
+
for (const raw of item.parameters ?? []) {
|
|
585
|
+
const parameter = deref(document2, raw);
|
|
586
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
587
|
+
}
|
|
588
|
+
for (const raw of own ?? []) {
|
|
589
|
+
const parameter = deref(document2, raw);
|
|
590
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
591
|
+
}
|
|
592
|
+
return [...merged.values()];
|
|
593
|
+
};
|
|
594
|
+
var listOperations = (document2) => {
|
|
595
|
+
const operations = [];
|
|
596
|
+
for (const [path, item] of Object.entries(document2.paths)) {
|
|
597
|
+
for (const method of HTTP_METHODS) {
|
|
598
|
+
const operation = item[method];
|
|
599
|
+
if (operation?.operationId === void 0)
|
|
600
|
+
continue;
|
|
601
|
+
const responses = {};
|
|
602
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
603
|
+
responses[status] = deref(document2, response);
|
|
604
|
+
}
|
|
605
|
+
operations.push({
|
|
606
|
+
operationId: operation.operationId,
|
|
607
|
+
method,
|
|
608
|
+
path,
|
|
609
|
+
operation,
|
|
610
|
+
parameters: mergeParameters(document2, item, operation.parameters),
|
|
611
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
|
|
612
|
+
responses
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return operations;
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// ../../http/codec/dist/form.js
|
|
620
|
+
var parsePath = (rawKey) => {
|
|
621
|
+
const open = rawKey.indexOf("[");
|
|
622
|
+
if (open === -1)
|
|
623
|
+
return [rawKey];
|
|
624
|
+
const path = [rawKey.slice(0, open)];
|
|
625
|
+
const rest = rawKey.slice(open);
|
|
626
|
+
const pattern = /\[([^\]]*)\]/g;
|
|
627
|
+
let match = pattern.exec(rest);
|
|
628
|
+
let consumed = 0;
|
|
629
|
+
while (match !== null) {
|
|
630
|
+
if (match.index !== consumed)
|
|
631
|
+
return [rawKey];
|
|
632
|
+
path.push(match[1] ?? "");
|
|
633
|
+
consumed = match.index + match[0].length;
|
|
634
|
+
match = pattern.exec(rest);
|
|
635
|
+
}
|
|
636
|
+
if (consumed !== rest.length)
|
|
637
|
+
return [rawKey];
|
|
638
|
+
return path;
|
|
639
|
+
};
|
|
640
|
+
var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
|
|
641
|
+
var put = (target, key, value) => {
|
|
642
|
+
if (key === "__proto__") {
|
|
643
|
+
Object.defineProperty(target, key, {
|
|
644
|
+
value,
|
|
645
|
+
enumerable: true,
|
|
646
|
+
writable: true,
|
|
647
|
+
configurable: true
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
;
|
|
652
|
+
target[key] = value;
|
|
653
|
+
};
|
|
654
|
+
var assign = (target, path, value) => {
|
|
655
|
+
let cursor = target;
|
|
656
|
+
for (let i = 0; i < path.length; i++) {
|
|
657
|
+
const segment = path[i];
|
|
658
|
+
const last = i === path.length - 1;
|
|
659
|
+
if (Array.isArray(cursor)) {
|
|
660
|
+
const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
|
|
661
|
+
if (index === void 0)
|
|
662
|
+
return;
|
|
663
|
+
if (last) {
|
|
664
|
+
put(cursor, index, value);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
|
|
668
|
+
if (next === void 0 || typeof next === "string") {
|
|
669
|
+
const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
|
|
670
|
+
put(cursor, index, created);
|
|
671
|
+
cursor = created;
|
|
672
|
+
} else {
|
|
673
|
+
cursor = next;
|
|
674
|
+
}
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (typeof cursor === "string")
|
|
678
|
+
return;
|
|
679
|
+
if (last) {
|
|
680
|
+
put(cursor, segment, value);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const nextSegment = path[i + 1];
|
|
684
|
+
const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
|
|
685
|
+
if (existing === void 0 || typeof existing === "string") {
|
|
686
|
+
const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
|
|
687
|
+
put(cursor, segment, created);
|
|
688
|
+
cursor = created;
|
|
689
|
+
} else {
|
|
690
|
+
cursor = existing;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
var decodeFormPairs = (pairs) => {
|
|
695
|
+
const out = {};
|
|
696
|
+
for (const [rawKey, value] of pairs)
|
|
697
|
+
assign(out, parsePath(rawKey), value);
|
|
698
|
+
return densify(out);
|
|
699
|
+
};
|
|
700
|
+
var densify = (value) => {
|
|
701
|
+
if (typeof value === "string")
|
|
702
|
+
return value;
|
|
703
|
+
if (Array.isArray(value))
|
|
704
|
+
return value.filter((item) => item !== void 0).map(densify);
|
|
705
|
+
const out = {};
|
|
706
|
+
for (const [key, item] of Object.entries(value))
|
|
707
|
+
put(out, key, densify(item));
|
|
708
|
+
return out;
|
|
709
|
+
};
|
|
710
|
+
var decodeForm = (text2) => {
|
|
711
|
+
const source = text2.startsWith("?") ? text2.slice(1) : text2;
|
|
712
|
+
return decodeFormPairs(new URLSearchParams(source).entries());
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// ../../http/codec/dist/content.js
|
|
716
|
+
var JSON_MEDIA_TYPE = "application/json";
|
|
717
|
+
var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
|
|
718
|
+
var mediaTypeOf = (contentType) => {
|
|
719
|
+
if (!contentType)
|
|
720
|
+
return void 0;
|
|
721
|
+
const essence = contentType.split(";")[0]?.trim().toLowerCase();
|
|
722
|
+
return essence ? essence : void 0;
|
|
723
|
+
};
|
|
724
|
+
var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
|
|
725
|
+
var utf8 = new TextDecoder("utf-8", { fatal: false });
|
|
726
|
+
var decodeBody = (contentType, bytes) => {
|
|
727
|
+
if (bytes.byteLength === 0)
|
|
728
|
+
return { kind: "empty" };
|
|
729
|
+
const mediaType = mediaTypeOf(contentType);
|
|
730
|
+
if (mediaType === void 0)
|
|
731
|
+
return { kind: "bytes", value: bytes };
|
|
732
|
+
if (isJsonMediaType(mediaType)) {
|
|
733
|
+
const text2 = utf8.decode(bytes);
|
|
734
|
+
try {
|
|
735
|
+
return { kind: "json", value: JSON.parse(text2) };
|
|
736
|
+
} catch (error) {
|
|
737
|
+
return {
|
|
738
|
+
kind: "invalid",
|
|
739
|
+
mediaType,
|
|
740
|
+
text: text2,
|
|
741
|
+
error: error instanceof Error ? error.message : String(error)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (mediaType === FORM_MEDIA_TYPE) {
|
|
746
|
+
return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
|
|
747
|
+
}
|
|
748
|
+
if (mediaType.startsWith("text/"))
|
|
749
|
+
return { kind: "text", value: utf8.decode(bytes) };
|
|
750
|
+
return { kind: "bytes", value: bytes };
|
|
751
|
+
};
|
|
752
|
+
var readBody = async (message) => {
|
|
753
|
+
const bytes = new Uint8Array(await message.arrayBuffer());
|
|
754
|
+
return decodeBody(message.headers.get("content-type"), bytes);
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// ../core/dist/http.js
|
|
758
|
+
var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
|
|
759
|
+
status,
|
|
760
|
+
headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
|
|
761
|
+
});
|
|
762
|
+
var HttpError = class extends Error {
|
|
763
|
+
status;
|
|
764
|
+
body;
|
|
765
|
+
headers;
|
|
766
|
+
constructor(status, body, headers = {}) {
|
|
767
|
+
super(`HTTP ${status}`);
|
|
768
|
+
this.status = status;
|
|
769
|
+
this.body = body;
|
|
770
|
+
this.headers = headers;
|
|
771
|
+
this.name = "HttpError";
|
|
772
|
+
}
|
|
773
|
+
toResponse() {
|
|
774
|
+
const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
775
|
+
if (contentType === "text/plain") {
|
|
776
|
+
return new Response(String(this.body), {
|
|
777
|
+
status: this.status,
|
|
778
|
+
headers: this.headers
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return jsonRes(this.status, this.body, this.headers);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// ../core/dist/ids.js
|
|
786
|
+
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
787
|
+
var mix = (input) => {
|
|
788
|
+
let hash = 2166136261;
|
|
789
|
+
for (let i = 0; i < input.length; i++) {
|
|
790
|
+
hash ^= input.charCodeAt(i);
|
|
791
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
792
|
+
}
|
|
793
|
+
hash ^= hash >>> 16;
|
|
794
|
+
hash = Math.imul(hash, 2246822507) >>> 0;
|
|
795
|
+
hash ^= hash >>> 13;
|
|
796
|
+
return hash >>> 0;
|
|
797
|
+
};
|
|
798
|
+
var opaqueToken = (input, length) => {
|
|
799
|
+
let out = "";
|
|
800
|
+
let round = 0;
|
|
801
|
+
while (out.length < length) {
|
|
802
|
+
let hash = mix(`${input}:${round++}`);
|
|
803
|
+
for (let i = 0; i < 5 && out.length < length; i++) {
|
|
804
|
+
out += ALPHABET.charAt(hash % ALPHABET.length);
|
|
805
|
+
hash = Math.floor(hash / ALPHABET.length);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return out;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// ../core/dist/journal.js
|
|
812
|
+
var DEFAULT_JOURNAL_SIZE = 1e3;
|
|
813
|
+
var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
814
|
+
const capacity = Math.max(0, Math.floor(size));
|
|
815
|
+
const rings = /* @__PURE__ */ new Map();
|
|
816
|
+
let sequence = 0;
|
|
817
|
+
const order = /* @__PURE__ */ new WeakMap();
|
|
818
|
+
const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
|
|
819
|
+
return {
|
|
820
|
+
size: capacity,
|
|
821
|
+
record(entry) {
|
|
822
|
+
if (capacity === 0)
|
|
823
|
+
return;
|
|
824
|
+
order.set(entry, sequence++);
|
|
825
|
+
let ring = rings.get(entry.namespace);
|
|
826
|
+
if (!ring) {
|
|
827
|
+
ring = { entries: [], next: 0 };
|
|
828
|
+
rings.set(entry.namespace, ring);
|
|
829
|
+
}
|
|
830
|
+
if (ring.entries.length < capacity)
|
|
831
|
+
ring.entries.push(entry);
|
|
832
|
+
else {
|
|
833
|
+
ring.entries[ring.next] = entry;
|
|
834
|
+
ring.next = (ring.next + 1) % capacity;
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
list(query = {}) {
|
|
838
|
+
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));
|
|
839
|
+
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));
|
|
840
|
+
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
841
|
+
},
|
|
842
|
+
clear(namespace) {
|
|
843
|
+
if (namespace === void 0)
|
|
844
|
+
rings.clear();
|
|
845
|
+
else
|
|
846
|
+
rings.delete(namespace);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
};
|
|
850
|
+
var notes = /* @__PURE__ */ new WeakMap();
|
|
851
|
+
var annotateResponse = (response, extra) => {
|
|
852
|
+
const existing = notes.get(response);
|
|
853
|
+
notes.set(response, {
|
|
854
|
+
...existing,
|
|
855
|
+
...extra,
|
|
856
|
+
...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
|
|
857
|
+
});
|
|
858
|
+
return response;
|
|
859
|
+
};
|
|
860
|
+
var responseNotes = (response) => notes.get(response);
|
|
861
|
+
|
|
862
|
+
// ../core/dist/metrics.js
|
|
863
|
+
var createMetrics = () => {
|
|
864
|
+
let requests = 0;
|
|
865
|
+
let faults = 0;
|
|
866
|
+
let totalDurationMs = 0;
|
|
867
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
868
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
869
|
+
return {
|
|
870
|
+
record(entry) {
|
|
871
|
+
requests++;
|
|
872
|
+
totalDurationMs += entry.durationMs;
|
|
873
|
+
if (entry.faultId !== void 0)
|
|
874
|
+
faults++;
|
|
875
|
+
const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
|
|
876
|
+
byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
|
|
877
|
+
if (entry.unmatched) {
|
|
878
|
+
const route = `${entry.method} ${entry.path}`;
|
|
879
|
+
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
report: () => ({
|
|
883
|
+
requests,
|
|
884
|
+
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
885
|
+
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
886
|
+
const space = route.indexOf(" ");
|
|
887
|
+
return { method: route.slice(0, space), path: route.slice(space + 1), count };
|
|
888
|
+
}),
|
|
889
|
+
faults,
|
|
890
|
+
totalDurationMs
|
|
891
|
+
}),
|
|
892
|
+
reset() {
|
|
893
|
+
requests = 0;
|
|
894
|
+
faults = 0;
|
|
895
|
+
totalDurationMs = 0;
|
|
896
|
+
byOperation.clear();
|
|
897
|
+
unmatched.clear();
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// ../../core/dist/timeline.js
|
|
903
|
+
var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
904
|
+
var Timeline = class {
|
|
905
|
+
maxCheckpoints;
|
|
906
|
+
now;
|
|
907
|
+
makeId;
|
|
908
|
+
nodes = /* @__PURE__ */ new Map();
|
|
909
|
+
heads = /* @__PURE__ */ new Map();
|
|
910
|
+
/** Unreferenced nodes in the exact order they became collectible. */
|
|
911
|
+
evictable = /* @__PURE__ */ new Set();
|
|
912
|
+
/** Branch heads plus explicit retainers. Absent means zero. */
|
|
913
|
+
references = /* @__PURE__ */ new Map();
|
|
914
|
+
explicitPins = /* @__PURE__ */ new Map();
|
|
915
|
+
sequence = 0;
|
|
916
|
+
constructor(options = {}) {
|
|
917
|
+
const max = options.maxCheckpoints ?? 1e3;
|
|
918
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
919
|
+
throw new RangeError("maxCheckpoints must be a positive integer");
|
|
920
|
+
this.maxCheckpoints = max;
|
|
921
|
+
this.now = options.now ?? (() => this.sequence);
|
|
922
|
+
this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
|
|
923
|
+
}
|
|
924
|
+
/** Capture a new immutable value and move `branch` to it. */
|
|
925
|
+
commit(value, options = {}) {
|
|
926
|
+
const branch = options.branch ?? "main";
|
|
927
|
+
this.assertBranch(branch);
|
|
928
|
+
const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
|
|
929
|
+
if (parent !== null && !this.nodes.has(parent))
|
|
930
|
+
throw new RangeError(`no checkpoint ${parent}`);
|
|
931
|
+
const id = this.makeId(++this.sequence);
|
|
932
|
+
if (this.nodes.has(id))
|
|
933
|
+
throw new RangeError(`duplicate checkpoint id ${id}`);
|
|
934
|
+
const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
|
|
935
|
+
this.nodes.set(id, checkpoint);
|
|
936
|
+
this.moveHead(branch, id);
|
|
937
|
+
this.collect(this.maxCheckpoints);
|
|
938
|
+
return checkpoint;
|
|
939
|
+
}
|
|
940
|
+
/** Create a branch pointer without copying its checkpoint value. */
|
|
941
|
+
fork(branch, options = {}) {
|
|
942
|
+
this.assertBranch(branch);
|
|
943
|
+
if (this.heads.has(branch))
|
|
944
|
+
throw new RangeError(`branch already exists: ${branch}`);
|
|
945
|
+
const from = options.from ?? this.heads.get("main");
|
|
946
|
+
if (from === void 0)
|
|
947
|
+
return void 0;
|
|
948
|
+
const checkpoint = this.get(from);
|
|
949
|
+
this.moveHead(branch, checkpoint.id);
|
|
950
|
+
return checkpoint;
|
|
951
|
+
}
|
|
952
|
+
/** Move a branch pointer to an existing checkpoint. */
|
|
953
|
+
checkout(branch, id) {
|
|
954
|
+
this.assertBranch(branch);
|
|
955
|
+
const checkpoint = this.get(id);
|
|
956
|
+
this.moveHead(branch, checkpoint.id);
|
|
957
|
+
return checkpoint;
|
|
958
|
+
}
|
|
959
|
+
get(id) {
|
|
960
|
+
const checkpoint = this.nodes.get(id);
|
|
961
|
+
if (!checkpoint)
|
|
962
|
+
throw new RangeError(`no checkpoint ${id}`);
|
|
963
|
+
return checkpoint;
|
|
964
|
+
}
|
|
965
|
+
head(branch = "main") {
|
|
966
|
+
const id = this.heads.get(branch);
|
|
967
|
+
return id === void 0 ? void 0 : this.get(id);
|
|
968
|
+
}
|
|
969
|
+
hasBranch(branch) {
|
|
970
|
+
return this.heads.has(branch);
|
|
971
|
+
}
|
|
972
|
+
branches() {
|
|
973
|
+
return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
974
|
+
}
|
|
975
|
+
checkpoints() {
|
|
976
|
+
return [...this.nodes.values()];
|
|
977
|
+
}
|
|
978
|
+
/** Number of retained checkpoints without allocating an array. */
|
|
979
|
+
get size() {
|
|
980
|
+
return this.nodes.size;
|
|
981
|
+
}
|
|
982
|
+
/** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
|
|
983
|
+
retain(id) {
|
|
984
|
+
const checkpoint = this.get(id);
|
|
985
|
+
this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
|
|
986
|
+
this.addReference(id);
|
|
987
|
+
return checkpoint;
|
|
988
|
+
}
|
|
989
|
+
/** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
|
|
990
|
+
release(id) {
|
|
991
|
+
if (!this.nodes.has(id))
|
|
992
|
+
return false;
|
|
993
|
+
const pins = this.explicitPins.get(id) ?? 0;
|
|
994
|
+
if (pins === 0)
|
|
995
|
+
return false;
|
|
996
|
+
if (pins === 1)
|
|
997
|
+
this.explicitPins.delete(id);
|
|
998
|
+
else
|
|
999
|
+
this.explicitPins.set(id, pins - 1);
|
|
1000
|
+
this.removeReference(id);
|
|
1001
|
+
this.collect(this.maxCheckpoints);
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
deleteBranch(branch) {
|
|
1005
|
+
if (branch === "main")
|
|
1006
|
+
throw new RangeError("cannot delete main branch");
|
|
1007
|
+
const previous = this.heads.get(branch);
|
|
1008
|
+
const deleted = this.heads.delete(branch);
|
|
1009
|
+
if (previous !== void 0)
|
|
1010
|
+
this.removeReference(previous);
|
|
1011
|
+
this.collect(this.maxCheckpoints);
|
|
1012
|
+
return deleted;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
|
|
1016
|
+
* commits never scan pinned nodes or the retained history. Parents are metadata rather than a
|
|
1017
|
+
* storage dependency, so a retained node remains usable after pruning.
|
|
1018
|
+
*/
|
|
1019
|
+
gc(max = this.maxCheckpoints) {
|
|
1020
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
1021
|
+
throw new RangeError("max must be a positive integer");
|
|
1022
|
+
const removed = [];
|
|
1023
|
+
this.collect(max, removed);
|
|
1024
|
+
return removed;
|
|
1025
|
+
}
|
|
1026
|
+
collect(max, removed) {
|
|
1027
|
+
while (this.nodes.size > max && this.evictable.size > 0) {
|
|
1028
|
+
const id = this.evictable.values().next().value;
|
|
1029
|
+
this.evictable.delete(id);
|
|
1030
|
+
this.nodes.delete(id);
|
|
1031
|
+
removed?.push(id);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
moveHead(branch, id) {
|
|
1035
|
+
const previous = this.heads.get(branch);
|
|
1036
|
+
if (previous === id)
|
|
1037
|
+
return;
|
|
1038
|
+
if (previous !== void 0)
|
|
1039
|
+
this.removeReference(previous);
|
|
1040
|
+
this.heads.set(branch, id);
|
|
1041
|
+
this.addReference(id);
|
|
1042
|
+
}
|
|
1043
|
+
addReference(id) {
|
|
1044
|
+
this.references.set(id, (this.references.get(id) ?? 0) + 1);
|
|
1045
|
+
this.evictable.delete(id);
|
|
1046
|
+
}
|
|
1047
|
+
removeReference(id) {
|
|
1048
|
+
const next = (this.references.get(id) ?? 0) - 1;
|
|
1049
|
+
if (next > 0)
|
|
1050
|
+
this.references.set(id, next);
|
|
1051
|
+
else {
|
|
1052
|
+
this.references.delete(id);
|
|
1053
|
+
if (this.nodes.has(id))
|
|
1054
|
+
this.evictable.add(id);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
assertBranch(branch) {
|
|
1058
|
+
if (!BRANCH_PATTERN.test(branch))
|
|
1059
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// ../../sqlite/dist/default.js
|
|
1064
|
+
import { Database } from "@crvouga/mockingbird-service-sqlite";
|
|
1065
|
+
var createDefaultSqlite = () => new Database();
|
|
1066
|
+
var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
|
|
1067
|
+
|
|
1068
|
+
// ../../sqlite/dist/migrate.js
|
|
1069
|
+
var ensureMigrationsTable = (sqlite) => {
|
|
1070
|
+
sqlite.exec(`
|
|
1071
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
1072
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
1073
|
+
applied_at INTEGER NOT NULL
|
|
1074
|
+
)
|
|
1075
|
+
`);
|
|
1076
|
+
};
|
|
1077
|
+
var migrate = (sqlite, migrations) => {
|
|
1078
|
+
ensureMigrationsTable(sqlite);
|
|
1079
|
+
const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
1080
|
+
const pending = migrations.filter((migration) => !applied.has(migration.id));
|
|
1081
|
+
if (pending.length === 0)
|
|
1082
|
+
return;
|
|
1083
|
+
const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
1084
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1085
|
+
sqlite.transaction(() => {
|
|
1086
|
+
for (const migration of pending) {
|
|
1087
|
+
sqlite.exec(migration.sql);
|
|
1088
|
+
insert.run(migration.id, now);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// ../../sqlite/dist/schema.js
|
|
1094
|
+
var CORE_MIGRATIONS = [
|
|
1095
|
+
{
|
|
1096
|
+
id: "20260322_core_records_sequences",
|
|
1097
|
+
sql: `
|
|
1098
|
+
CREATE TABLE IF NOT EXISTS mockingbird_records (
|
|
1099
|
+
namespace TEXT NOT NULL,
|
|
1100
|
+
collection TEXT NOT NULL,
|
|
1101
|
+
id TEXT NOT NULL,
|
|
1102
|
+
seq INTEGER NOT NULL,
|
|
1103
|
+
value TEXT NOT NULL,
|
|
1104
|
+
PRIMARY KEY (namespace, collection, id)
|
|
1105
|
+
);
|
|
1106
|
+
CREATE INDEX IF NOT EXISTS mockingbird_records_seq
|
|
1107
|
+
ON mockingbird_records (namespace, collection, seq);
|
|
1108
|
+
CREATE TABLE IF NOT EXISTS mockingbird_sequences (
|
|
1109
|
+
namespace TEXT NOT NULL,
|
|
1110
|
+
name TEXT NOT NULL,
|
|
1111
|
+
kind TEXT NOT NULL,
|
|
1112
|
+
value INTEGER NOT NULL,
|
|
1113
|
+
PRIMARY KEY (namespace, name, kind)
|
|
1114
|
+
);
|
|
1115
|
+
`
|
|
1116
|
+
}
|
|
1117
|
+
];
|
|
1118
|
+
var migrateCore = (sqlite) => {
|
|
1119
|
+
migrate(sqlite, CORE_MIGRATIONS);
|
|
1120
|
+
};
|
|
1121
|
+
var clearNamespace = (sqlite, namespace) => {
|
|
1122
|
+
sqlite.transaction(() => {
|
|
1123
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1124
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// ../../openapi/metadata/dist/types.js
|
|
1129
|
+
var EXTENSION_KEYS = {
|
|
1130
|
+
operation: "x-mockingbird",
|
|
1131
|
+
resource: "x-mockingbird-resource",
|
|
1132
|
+
resourceRef: "x-mockingbird-resource-ref",
|
|
1133
|
+
volatile: "x-mockingbird-volatile",
|
|
1134
|
+
scope: "x-mockingbird-scope",
|
|
1135
|
+
unsupported: "x-mockingbird-unsupported",
|
|
1136
|
+
parityHeader: "x-mockingbird-parity-header"
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// ../../openapi/metadata/dist/read.js
|
|
1140
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1141
|
+
var extensionOf = (holder, key) => holder[key];
|
|
1142
|
+
var operationMetadata = (operation) => {
|
|
1143
|
+
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
1144
|
+
const ext = isRecord2(raw) ? raw : {};
|
|
1145
|
+
const supported = ext.supported ?? true;
|
|
1146
|
+
const parity = ext.parity ?? {};
|
|
1147
|
+
return {
|
|
1148
|
+
supported,
|
|
1149
|
+
reason: typeof ext.reason === "string" ? ext.reason : void 0,
|
|
1150
|
+
parity: {
|
|
1151
|
+
enabled: supported && (parity.enabled ?? true),
|
|
1152
|
+
safe: parity.safe ?? true,
|
|
1153
|
+
reason: typeof parity.reason === "string" ? parity.reason : void 0
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// ../core/dist/service.js
|
|
1159
|
+
import { Hono } from "hono";
|
|
1160
|
+
var defineOperations = (handlers) => handlers;
|
|
1161
|
+
var OperationRegistryError = class extends Error {
|
|
1162
|
+
problems;
|
|
1163
|
+
constructor(problems) {
|
|
1164
|
+
super(`operation registry is inconsistent:
|
|
1165
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
1166
|
+
this.problems = problems;
|
|
1167
|
+
this.name = "OperationRegistryError";
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
var verifyOperations = (document2, handlers) => {
|
|
1171
|
+
const problems = [];
|
|
1172
|
+
const operations = listOperations(document2);
|
|
1173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1174
|
+
for (const operation of operations) {
|
|
1175
|
+
if (seen.has(operation.operationId))
|
|
1176
|
+
problems.push(`duplicate operationId ${operation.operationId}`);
|
|
1177
|
+
seen.add(operation.operationId);
|
|
1178
|
+
const supported = operationMetadata(operation.operation).supported;
|
|
1179
|
+
const handler = handlers[operation.operationId];
|
|
1180
|
+
if (supported && !handler)
|
|
1181
|
+
problems.push(`supported operation ${operation.operationId} has no handler`);
|
|
1182
|
+
if (!supported && handler)
|
|
1183
|
+
problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
|
|
1184
|
+
}
|
|
1185
|
+
for (const id of Object.keys(handlers)) {
|
|
1186
|
+
if (!seen.has(id))
|
|
1187
|
+
problems.push(`handler ${id} has no OpenAPI operation`);
|
|
1188
|
+
}
|
|
1189
|
+
return problems;
|
|
1190
|
+
};
|
|
1191
|
+
var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
|
|
1192
|
+
var routeOrder = (a, b) => {
|
|
1193
|
+
const sa = a.path.split("/");
|
|
1194
|
+
const sb = b.path.split("/");
|
|
1195
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
1196
|
+
const x = sa[i] ?? "";
|
|
1197
|
+
const y = sb[i] ?? "";
|
|
1198
|
+
const px = x.startsWith("{");
|
|
1199
|
+
const py = y.startsWith("{");
|
|
1200
|
+
if (px !== py)
|
|
1201
|
+
return px ? 1 : -1;
|
|
1202
|
+
if (x !== y)
|
|
1203
|
+
return x < y ? -1 : 1;
|
|
1204
|
+
}
|
|
1205
|
+
return 0;
|
|
1206
|
+
};
|
|
1207
|
+
var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
|
|
1208
|
+
var bootSqlite = (sqlite) => {
|
|
1209
|
+
const client = resolveSqlite(sqlite);
|
|
1210
|
+
migrateCore(client);
|
|
1211
|
+
return client;
|
|
1212
|
+
};
|
|
1213
|
+
var createService = (options) => {
|
|
1214
|
+
const problems = verifyOperations(options.document, options.handlers);
|
|
1215
|
+
if (problems.length > 0)
|
|
1216
|
+
throw new OperationRegistryError(problems);
|
|
1217
|
+
migrateCore(options.sqlite);
|
|
1218
|
+
const now = options.now ?? (() => Date.now());
|
|
1219
|
+
const app = new Hono();
|
|
1220
|
+
app.notFound((c) => options.notFound(c.req.raw));
|
|
1221
|
+
app.onError((error, c) => options.onError(error, c.req.raw));
|
|
1222
|
+
const operations = [...listOperations(options.document)].sort(routeOrder);
|
|
1223
|
+
for (const operation of operations) {
|
|
1224
|
+
const metadata = operationMetadata(operation.operation);
|
|
1225
|
+
const handler = options.handlers[operation.operationId];
|
|
1226
|
+
const route = async (c) => {
|
|
1227
|
+
const request = c.req.raw;
|
|
1228
|
+
if (!metadata.supported || !handler) {
|
|
1229
|
+
return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
|
|
1230
|
+
}
|
|
1231
|
+
const url = new URL(request.url);
|
|
1232
|
+
const context = {
|
|
1233
|
+
request,
|
|
1234
|
+
url,
|
|
1235
|
+
params: c.req.param(),
|
|
1236
|
+
query: queryOf(url),
|
|
1237
|
+
body: await readBody(request),
|
|
1238
|
+
sqlite: options.sqlite,
|
|
1239
|
+
namespace: options.namespace,
|
|
1240
|
+
operation,
|
|
1241
|
+
document: options.document,
|
|
1242
|
+
now
|
|
1243
|
+
};
|
|
1244
|
+
const short = await options.before?.(context);
|
|
1245
|
+
if (short)
|
|
1246
|
+
return short;
|
|
1247
|
+
return handler(context);
|
|
1248
|
+
};
|
|
1249
|
+
app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
app,
|
|
1253
|
+
sqlite: options.sqlite,
|
|
1254
|
+
namespace: options.namespace,
|
|
1255
|
+
fetch: async (request) => app.fetch(request),
|
|
1256
|
+
reset: async () => {
|
|
1257
|
+
clearNamespace(options.sqlite, options.namespace);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// ../core/dist/snapshot.js
|
|
1263
|
+
var snapshotNamespace = (sqlite, namespace) => ({
|
|
1264
|
+
namespace,
|
|
1265
|
+
records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
|
|
1266
|
+
sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
|
|
1267
|
+
});
|
|
1268
|
+
var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
1269
|
+
sqlite.transaction(() => {
|
|
1270
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1271
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1272
|
+
const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
|
|
1273
|
+
for (const row of snapshot.records) {
|
|
1274
|
+
record.run(namespace, row.collection, row.id, row.seq, row.value);
|
|
1275
|
+
}
|
|
1276
|
+
const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
|
|
1277
|
+
for (const row of snapshot.sequences) {
|
|
1278
|
+
sequence.run(namespace, row.name, row.kind, row.value);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// ../core/dist/version.js
|
|
1284
|
+
var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
|
|
1285
|
+
|
|
1286
|
+
// ../core/dist/signing.js
|
|
1287
|
+
var encoder = new TextEncoder();
|
|
1288
|
+
var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1289
|
+
|
|
1290
|
+
// ../core/dist/webhooks.js
|
|
1291
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1292
|
+
var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
|
|
1293
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1294
|
+
var parseEndpoint = (value) => {
|
|
1295
|
+
if (!isRecord3(value) || typeof value.url !== "string")
|
|
1296
|
+
return "each endpoint needs a url";
|
|
1297
|
+
try {
|
|
1298
|
+
new URL(value.url);
|
|
1299
|
+
} catch {
|
|
1300
|
+
return `not a URL: ${value.url}`;
|
|
1301
|
+
}
|
|
1302
|
+
const endpoint = { url: value.url };
|
|
1303
|
+
if (typeof value.id === "string")
|
|
1304
|
+
endpoint.id = value.id;
|
|
1305
|
+
if (typeof value.secret === "string")
|
|
1306
|
+
endpoint.secret = value.secret;
|
|
1307
|
+
if (typeof value.signUrl === "string")
|
|
1308
|
+
endpoint.signUrl = value.signUrl;
|
|
1309
|
+
const events = value.events ?? value.enabledEvents;
|
|
1310
|
+
if (Array.isArray(events))
|
|
1311
|
+
endpoint.events = events.map(String);
|
|
1312
|
+
if (isRecord3(value.tags)) {
|
|
1313
|
+
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1314
|
+
}
|
|
1315
|
+
if (typeof value.account === "string")
|
|
1316
|
+
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1317
|
+
if (isRecord3(value.headers)) {
|
|
1318
|
+
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1319
|
+
}
|
|
1320
|
+
return endpoint;
|
|
1321
|
+
};
|
|
1322
|
+
var webhookAdminRoutes = (hub) => ({
|
|
1323
|
+
"GET /webhooks": ({ url, namespace }) => json2(200, {
|
|
1324
|
+
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1325
|
+
const type = url.searchParams.get("type");
|
|
1326
|
+
return type === null || d.type === type;
|
|
1327
|
+
})
|
|
1328
|
+
}),
|
|
1329
|
+
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1330
|
+
const type = url.searchParams.get("type");
|
|
1331
|
+
return json2(200, {
|
|
1332
|
+
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1333
|
+
});
|
|
1334
|
+
},
|
|
1335
|
+
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1336
|
+
const replayed = await hub.replay(params.id);
|
|
1337
|
+
return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
|
|
1338
|
+
},
|
|
1339
|
+
"POST /webhooks/flush": async () => {
|
|
1340
|
+
await hub.flush();
|
|
1341
|
+
return json2(200, { status: "ok" });
|
|
1342
|
+
},
|
|
1343
|
+
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1344
|
+
if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
1345
|
+
return adminError2(400, "mode must be duplicate, reorder or drop");
|
|
1346
|
+
}
|
|
1347
|
+
const fault2 = { mode: body.mode };
|
|
1348
|
+
if (typeof body.count === "number")
|
|
1349
|
+
fault2.count = body.count;
|
|
1350
|
+
hub.fault(namespace, fault2);
|
|
1351
|
+
return json2(201, { namespace, ...fault2 });
|
|
1352
|
+
},
|
|
1353
|
+
"GET /webhook-endpoints": ({ namespace }) => json2(200, {
|
|
1354
|
+
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1355
|
+
...rest,
|
|
1356
|
+
secret: secret ? "(set)" : null
|
|
1357
|
+
}))
|
|
1358
|
+
}),
|
|
1359
|
+
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
1360
|
+
const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
|
|
1361
|
+
if (!Array.isArray(list))
|
|
1362
|
+
return adminError2(400, "expected [{url, secret?, events?}]");
|
|
1363
|
+
const parsed = [];
|
|
1364
|
+
for (const each of list) {
|
|
1365
|
+
const endpoint = parseEndpoint(each);
|
|
1366
|
+
if (typeof endpoint === "string")
|
|
1367
|
+
return adminError2(400, endpoint);
|
|
1368
|
+
parsed.push(endpoint);
|
|
1369
|
+
}
|
|
1370
|
+
const set = hub.setEndpoints(namespace, parsed);
|
|
1371
|
+
return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
1372
|
+
},
|
|
1373
|
+
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
1374
|
+
hub.setEndpoints(namespace, []);
|
|
1375
|
+
return json2(200, { status: "ok" });
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
var parsePayload = (message) => {
|
|
1379
|
+
if (message.contentType.startsWith("application/json")) {
|
|
1380
|
+
try {
|
|
1381
|
+
return JSON.parse(message.body);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return message.body;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
|
|
1387
|
+
return Object.fromEntries(new URLSearchParams(message.body));
|
|
1388
|
+
}
|
|
1389
|
+
return message.body;
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
// ../core/dist/runtime.js
|
|
1393
|
+
var MOCKINGBIRD_HEADER = "x-mockingbird";
|
|
1394
|
+
var BRANCH_HEADER = "x-mockingbird-branch";
|
|
1395
|
+
var AT_HEADER = "x-mockingbird-at";
|
|
1396
|
+
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
1397
|
+
var DEFAULT_NAMESPACE = "default";
|
|
1398
|
+
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1399
|
+
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
1400
|
+
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1401
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1402
|
+
var effects = /* @__PURE__ */ new WeakMap();
|
|
1403
|
+
var reuseSorted = (fresh, previous, compare, equal) => {
|
|
1404
|
+
if (!previous || previous.length === 0)
|
|
1405
|
+
return fresh.map((row) => Object.freeze(row));
|
|
1406
|
+
const result = new Array(fresh.length);
|
|
1407
|
+
let unchanged = fresh.length === previous.length;
|
|
1408
|
+
let oldIndex = 0;
|
|
1409
|
+
for (let index = 0; index < fresh.length; index++) {
|
|
1410
|
+
const row = fresh[index];
|
|
1411
|
+
while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
|
|
1412
|
+
oldIndex++;
|
|
1413
|
+
}
|
|
1414
|
+
const old = previous[oldIndex];
|
|
1415
|
+
result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
|
|
1416
|
+
if (result[index] !== previous[index])
|
|
1417
|
+
unchanged = false;
|
|
1418
|
+
}
|
|
1419
|
+
return unchanged ? previous : result;
|
|
1420
|
+
};
|
|
1421
|
+
var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
|
|
1422
|
+
var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
|
|
1423
|
+
var DroppedConnectionError = class extends TypeError {
|
|
1424
|
+
code = "MOCKINGBIRD_DROP";
|
|
1425
|
+
constructor() {
|
|
1426
|
+
super("fetch failed: connection dropped by Mockingbird fault");
|
|
1427
|
+
this.name = "TypeError";
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
var operationMatcher = (document2) => {
|
|
1431
|
+
const matchers = listOperations(document2).map((operation) => ({
|
|
1432
|
+
operationId: operation.operationId,
|
|
1433
|
+
method: operation.method.toUpperCase(),
|
|
1434
|
+
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
1435
|
+
params: (operation.path.match(/\{/g) ?? []).length
|
|
1436
|
+
})).sort((a, b) => a.params - b.params);
|
|
1437
|
+
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
1438
|
+
};
|
|
1439
|
+
var createRuntime = (options) => {
|
|
1440
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
1441
|
+
const clock = options.clock ?? createClock();
|
|
1442
|
+
const rng = createRng(options.seed ?? 0);
|
|
1443
|
+
const wallNow = options.io?.wallNow ?? Date.now;
|
|
1444
|
+
const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
|
|
1445
|
+
const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1446
|
+
const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
|
|
1447
|
+
const metrics = createMetrics();
|
|
1448
|
+
const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
|
|
1449
|
+
const version = options.version ?? PACKAGE_VERSION;
|
|
1450
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1451
|
+
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
1452
|
+
const branchRngs = /* @__PURE__ */ new Map();
|
|
1453
|
+
const timelines = /* @__PURE__ */ new Map();
|
|
1454
|
+
const branchStorage = /* @__PURE__ */ new Map();
|
|
1455
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1456
|
+
const credentials = createCredentialRegistry();
|
|
1457
|
+
const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
|
|
1458
|
+
const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
|
|
1459
|
+
const instanceFor = (key, publicNamespace = key, isolatedRng) => {
|
|
1460
|
+
const existing = instances.get(key);
|
|
1461
|
+
if (existing)
|
|
1462
|
+
return existing;
|
|
1463
|
+
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
1464
|
+
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
1465
|
+
}
|
|
1466
|
+
const created = options.create({
|
|
1467
|
+
namespace: storageNamespace(key),
|
|
1468
|
+
publicNamespace,
|
|
1469
|
+
sqlite,
|
|
1470
|
+
clock,
|
|
1471
|
+
rng: isolatedRng ?? rng
|
|
1472
|
+
});
|
|
1473
|
+
instances.set(key, created);
|
|
1474
|
+
publicNamespaces.add(publicNamespace);
|
|
1475
|
+
if (isolatedRng)
|
|
1476
|
+
branchRngs.set(key, isolatedRng);
|
|
1477
|
+
return created;
|
|
1478
|
+
};
|
|
1479
|
+
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
1480
|
+
const capture = (storage) => {
|
|
1481
|
+
const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
|
|
1482
|
+
const previous = captured.get(storage);
|
|
1483
|
+
const snapshot2 = {
|
|
1484
|
+
namespace: fresh.namespace,
|
|
1485
|
+
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),
|
|
1486
|
+
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)
|
|
1487
|
+
};
|
|
1488
|
+
Object.freeze(snapshot2.records);
|
|
1489
|
+
Object.freeze(snapshot2.sequences);
|
|
1490
|
+
Object.freeze(snapshot2);
|
|
1491
|
+
captured.set(storage, snapshot2);
|
|
1492
|
+
return Object.freeze({
|
|
1493
|
+
snapshot: snapshot2,
|
|
1494
|
+
clock: Object.freeze(clock.state()),
|
|
1495
|
+
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
1496
|
+
});
|
|
1497
|
+
};
|
|
1498
|
+
const timeline = (name = DEFAULT_NAMESPACE) => {
|
|
1499
|
+
let found = timelines.get(name);
|
|
1500
|
+
if (found)
|
|
1501
|
+
return found;
|
|
1502
|
+
instance(name);
|
|
1503
|
+
found = new Timeline({
|
|
1504
|
+
now: clock.now,
|
|
1505
|
+
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
1506
|
+
});
|
|
1507
|
+
found.commit(capture(name));
|
|
1508
|
+
timelines.set(name, found);
|
|
1509
|
+
return found;
|
|
1510
|
+
};
|
|
1511
|
+
const physicalBranch = (namespace, branch2) => {
|
|
1512
|
+
if (branch2 === "main")
|
|
1513
|
+
return namespace;
|
|
1514
|
+
const mapKey = `${namespace}\0${branch2}`;
|
|
1515
|
+
const existing = branchStorage.get(mapKey);
|
|
1516
|
+
if (existing)
|
|
1517
|
+
return existing;
|
|
1518
|
+
const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
|
|
1519
|
+
branchStorage.set(mapKey, key);
|
|
1520
|
+
return key;
|
|
1521
|
+
};
|
|
1522
|
+
const ensureBranch = (namespace, branch2, at) => {
|
|
1523
|
+
if (!BRANCH_PATTERN2.test(branch2))
|
|
1524
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
1525
|
+
const history = timeline(namespace);
|
|
1526
|
+
if (branch2 === "main") {
|
|
1527
|
+
if (at !== void 0) {
|
|
1528
|
+
const point = history.checkout("main", at);
|
|
1529
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
1530
|
+
captured.set(namespace, point.value.snapshot);
|
|
1531
|
+
rng.setState(point.value.rngState);
|
|
1532
|
+
clock.set(point.value.clock.now);
|
|
1533
|
+
if (point.value.clock.frozen)
|
|
1534
|
+
clock.freeze();
|
|
1535
|
+
else
|
|
1536
|
+
clock.unfreeze();
|
|
1537
|
+
}
|
|
1538
|
+
return namespace;
|
|
1539
|
+
}
|
|
1540
|
+
const storage = physicalBranch(namespace, branch2);
|
|
1541
|
+
if (!history.hasBranch(branch2)) {
|
|
1542
|
+
if (at === void 0)
|
|
1543
|
+
history.commit(capture(namespace));
|
|
1544
|
+
const point = history.fork(branch2, at === void 0 ? {} : { from: at });
|
|
1545
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1546
|
+
if (point)
|
|
1547
|
+
branchRng.setState(point.value.rngState);
|
|
1548
|
+
instanceFor(storage, namespace, branchRng);
|
|
1549
|
+
if (point)
|
|
1550
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1551
|
+
if (point)
|
|
1552
|
+
captured.set(storage, point.value.snapshot);
|
|
1553
|
+
} else if (at !== void 0 && history.head(branch2)?.id !== at) {
|
|
1554
|
+
const point = history.checkout(branch2, at);
|
|
1555
|
+
if (!instances.has(storage)) {
|
|
1556
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1557
|
+
branchRng.setState(point.value.rngState);
|
|
1558
|
+
instanceFor(storage, namespace, branchRng);
|
|
1559
|
+
}
|
|
1560
|
+
branchRngs.get(storage)?.setState(point.value.rngState);
|
|
1561
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1562
|
+
captured.set(storage, point.value.snapshot);
|
|
1563
|
+
} else {
|
|
1564
|
+
if (!instances.has(storage)) {
|
|
1565
|
+
const point = history.head(branch2);
|
|
1566
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1567
|
+
if (point)
|
|
1568
|
+
branchRng.setState(point.value.rngState);
|
|
1569
|
+
instanceFor(storage, namespace, branchRng);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return storage;
|
|
1573
|
+
};
|
|
1574
|
+
const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
|
|
1575
|
+
const storage = ensureBranch(namespace, branch2);
|
|
1576
|
+
return timeline(namespace).commit(capture(storage), { branch: branch2 });
|
|
1577
|
+
};
|
|
1578
|
+
const branch = (name, branchOptions = {}) => {
|
|
1579
|
+
const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1580
|
+
ensureBranch(namespace, name, branchOptions.at);
|
|
1581
|
+
const head = timeline(namespace).head(name);
|
|
1582
|
+
if (!head)
|
|
1583
|
+
throw new RangeError(`branch ${name} has no checkpoint`);
|
|
1584
|
+
return head;
|
|
1585
|
+
};
|
|
1586
|
+
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
1587
|
+
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1588
|
+
const branchName = checkoutOptions.branch ?? "main";
|
|
1589
|
+
const history = timeline(namespace);
|
|
1590
|
+
const point = history.checkout(branchName, checkpointId);
|
|
1591
|
+
const storage = ensureBranch(namespace, branchName);
|
|
1592
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1593
|
+
captured.set(storage, point.value.snapshot);
|
|
1594
|
+
clock.set(point.value.clock.now);
|
|
1595
|
+
if (point.value.clock.frozen)
|
|
1596
|
+
clock.freeze();
|
|
1597
|
+
else
|
|
1598
|
+
clock.unfreeze();
|
|
1599
|
+
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
1600
|
+
};
|
|
1601
|
+
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
1602
|
+
if (name === "*") {
|
|
1603
|
+
options.webhooks?.clear();
|
|
1604
|
+
for (const each of instances.values())
|
|
1605
|
+
await each.reset();
|
|
1606
|
+
timelines.clear();
|
|
1607
|
+
branchStorage.clear();
|
|
1608
|
+
branchRngs.clear();
|
|
1609
|
+
captured.clear();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
options.webhooks?.clear(name);
|
|
1613
|
+
const target = instances.get(name);
|
|
1614
|
+
if (target)
|
|
1615
|
+
await target.reset();
|
|
1616
|
+
else
|
|
1617
|
+
clearNamespace(sqlite, storageNamespace(name));
|
|
1618
|
+
for (const [mapping, storage] of branchStorage) {
|
|
1619
|
+
if (!mapping.startsWith(`${name}\0`))
|
|
1620
|
+
continue;
|
|
1621
|
+
const branchInstance = instances.get(storage);
|
|
1622
|
+
if (branchInstance)
|
|
1623
|
+
await branchInstance.reset();
|
|
1624
|
+
else
|
|
1625
|
+
clearNamespace(sqlite, storageNamespace(storage));
|
|
1626
|
+
branchStorage.delete(mapping);
|
|
1627
|
+
branchRngs.delete(storage);
|
|
1628
|
+
captured.delete(storage);
|
|
1629
|
+
}
|
|
1630
|
+
timelines.delete(name);
|
|
1631
|
+
captured.delete(name);
|
|
1632
|
+
};
|
|
1633
|
+
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
1634
|
+
return checkpoint(name, "main").value.snapshot;
|
|
1635
|
+
};
|
|
1636
|
+
const restore = (from, name = DEFAULT_NAMESPACE) => {
|
|
1637
|
+
instance(name);
|
|
1638
|
+
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
1639
|
+
captured.set(name, from);
|
|
1640
|
+
const history = timelines.get(name);
|
|
1641
|
+
if (history)
|
|
1642
|
+
history.commit(capture(name), { branch: "main" });
|
|
1643
|
+
else
|
|
1644
|
+
timeline(name);
|
|
1645
|
+
};
|
|
1646
|
+
const runtime = {
|
|
1647
|
+
name: options.name,
|
|
1648
|
+
sqlite,
|
|
1649
|
+
clock,
|
|
1650
|
+
faults,
|
|
1651
|
+
metrics,
|
|
1652
|
+
journal,
|
|
1653
|
+
rng,
|
|
1654
|
+
credentials,
|
|
1655
|
+
webhooks: options.webhooks,
|
|
1656
|
+
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
1657
|
+
const preset = options.presets?.[name];
|
|
1658
|
+
if (!preset)
|
|
1659
|
+
throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
|
|
1660
|
+
const added = (preset.rules ?? []).map((rule, index) => faults.add({
|
|
1661
|
+
namespace,
|
|
1662
|
+
...rule,
|
|
1663
|
+
...overrides,
|
|
1664
|
+
preset: name,
|
|
1665
|
+
id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
|
|
1666
|
+
}));
|
|
1667
|
+
if (preset.webhook && options.webhooks) {
|
|
1668
|
+
options.webhooks.fault(namespace, {
|
|
1669
|
+
...preset.webhook,
|
|
1670
|
+
...overrides.count !== void 0 ? { count: overrides.count } : {}
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
return added;
|
|
1674
|
+
},
|
|
1675
|
+
instance,
|
|
1676
|
+
namespaces: () => [...publicNamespaces].sort(),
|
|
1677
|
+
reset,
|
|
1678
|
+
snapshot,
|
|
1679
|
+
restore,
|
|
1680
|
+
checkpoint,
|
|
1681
|
+
branch,
|
|
1682
|
+
checkout,
|
|
1683
|
+
timeline,
|
|
1684
|
+
fetch: async (incoming) => {
|
|
1685
|
+
let request = incoming;
|
|
1686
|
+
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
1687
|
+
if (prefixed) {
|
|
1688
|
+
const url2 = new URL(request.url);
|
|
1689
|
+
url2.pathname = prefixed[2] ?? "/";
|
|
1690
|
+
const headers = new Headers(request.headers);
|
|
1691
|
+
if (!headers.has(NAMESPACE_HEADER)) {
|
|
1692
|
+
headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
|
|
1693
|
+
}
|
|
1694
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
1695
|
+
request = new Request(url2, {
|
|
1696
|
+
method: request.method,
|
|
1697
|
+
headers,
|
|
1698
|
+
...hasBody ? { body: await request.arrayBuffer() } : {},
|
|
1699
|
+
signal: request.signal
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
let namespace = control.namespaceOf(request);
|
|
1703
|
+
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
1704
|
+
const credential = options.credential(request);
|
|
1705
|
+
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
1706
|
+
if (mapped !== void 0)
|
|
1707
|
+
namespace = mapped;
|
|
1708
|
+
}
|
|
1709
|
+
const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
|
|
1710
|
+
const at = request.headers.get(AT_HEADER) ?? void 0;
|
|
1711
|
+
const stamp = (response2) => {
|
|
1712
|
+
const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
|
|
1713
|
+
try {
|
|
1714
|
+
response2.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1715
|
+
return response2;
|
|
1716
|
+
} catch {
|
|
1717
|
+
const copy = new Response(response2.body, response2);
|
|
1718
|
+
copy.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1719
|
+
return copy;
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
const handled = await control.handle(request);
|
|
1723
|
+
if (handled)
|
|
1724
|
+
return stamp(handled);
|
|
1725
|
+
const started = monotonicNow();
|
|
1726
|
+
const url = new URL(request.url);
|
|
1727
|
+
const operationId = operationIdFor(request, url.pathname);
|
|
1728
|
+
const log = (status, faultId, response2) => {
|
|
1729
|
+
const noted = response2 ? responseNotes(response2) : void 0;
|
|
1730
|
+
const entry = {
|
|
1731
|
+
service: options.name,
|
|
1732
|
+
namespace,
|
|
1733
|
+
operationId,
|
|
1734
|
+
method: request.method,
|
|
1735
|
+
path: url.pathname,
|
|
1736
|
+
status,
|
|
1737
|
+
durationMs: Math.round((monotonicNow() - started) * 100) / 100,
|
|
1738
|
+
unmatched: options.document !== void 0 && operationId === void 0,
|
|
1739
|
+
...faultId !== void 0 ? { faultId } : {},
|
|
1740
|
+
...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
|
|
1741
|
+
...noted?.adopted ? { adopted: true } : {}
|
|
1742
|
+
};
|
|
1743
|
+
metrics.record(entry);
|
|
1744
|
+
journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
|
|
1745
|
+
options.onLog?.(entry);
|
|
1746
|
+
};
|
|
1747
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
1748
|
+
log(400);
|
|
1749
|
+
return stamp(new Response(JSON.stringify({
|
|
1750
|
+
error: {
|
|
1751
|
+
type: "mockingbird_admin",
|
|
1752
|
+
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
1753
|
+
}
|
|
1754
|
+
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
1755
|
+
}
|
|
1756
|
+
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
1757
|
+
log(400);
|
|
1758
|
+
return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
|
|
1759
|
+
}
|
|
1760
|
+
let storage;
|
|
1761
|
+
try {
|
|
1762
|
+
if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
|
|
1763
|
+
const point = timeline(namespace).get(at);
|
|
1764
|
+
storage = physicalBranch(namespace, `at_${at}`);
|
|
1765
|
+
let viewRng = branchRngs.get(storage);
|
|
1766
|
+
if (!viewRng) {
|
|
1767
|
+
viewRng = createRng(options.seed ?? 0);
|
|
1768
|
+
instanceFor(storage, namespace, viewRng);
|
|
1769
|
+
}
|
|
1770
|
+
viewRng.setState(point.value.rngState);
|
|
1771
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1772
|
+
captured.set(storage, point.value.snapshot);
|
|
1773
|
+
} else {
|
|
1774
|
+
storage = ensureBranch(namespace, selectedBranch, at);
|
|
1775
|
+
}
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
log(409);
|
|
1778
|
+
return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
|
|
1779
|
+
}
|
|
1780
|
+
const hits = await faults.take({
|
|
1781
|
+
operationId,
|
|
1782
|
+
method: request.method,
|
|
1783
|
+
path: url.pathname,
|
|
1784
|
+
namespace
|
|
1785
|
+
});
|
|
1786
|
+
const final = hits.find((hit) => hit.drop || hit.response);
|
|
1787
|
+
if (final?.drop) {
|
|
1788
|
+
log(0, final.id);
|
|
1789
|
+
throw new DroppedConnectionError();
|
|
1790
|
+
}
|
|
1791
|
+
if (final?.response) {
|
|
1792
|
+
log(final.response.status, final.id);
|
|
1793
|
+
return stamp(final.response);
|
|
1794
|
+
}
|
|
1795
|
+
const fired = hits.filter((hit) => hit.effect !== void 0);
|
|
1796
|
+
if (fired.length > 0)
|
|
1797
|
+
effects.set(request, fired.map((hit) => hit.effect));
|
|
1798
|
+
let response = await instanceFor(storage, namespace).fetch(request);
|
|
1799
|
+
if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
|
|
1800
|
+
const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
|
|
1801
|
+
response = mutableResponse(response);
|
|
1802
|
+
response.headers.set(CHECKPOINT_HEADER, point.id);
|
|
1803
|
+
}
|
|
1804
|
+
if (selectedBranch !== "main") {
|
|
1805
|
+
response = mutableResponse(response);
|
|
1806
|
+
response.headers.set(BRANCH_HEADER, selectedBranch);
|
|
1807
|
+
}
|
|
1808
|
+
if (at !== void 0) {
|
|
1809
|
+
response = mutableResponse(response);
|
|
1810
|
+
response.headers.set(AT_HEADER, at);
|
|
1811
|
+
}
|
|
1812
|
+
log(response.status, fired[0]?.id, response);
|
|
1813
|
+
return stamp(response);
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
const control = createControlPlane({
|
|
1817
|
+
name: options.name,
|
|
1818
|
+
startedAt: wallNow(),
|
|
1819
|
+
wallNow,
|
|
1820
|
+
clock,
|
|
1821
|
+
faults,
|
|
1822
|
+
metrics,
|
|
1823
|
+
journal,
|
|
1824
|
+
defaultNamespace: DEFAULT_NAMESPACE,
|
|
1825
|
+
namespaces: runtime.namespaces,
|
|
1826
|
+
reset,
|
|
1827
|
+
timeTravel: {
|
|
1828
|
+
checkpoint: (name, branchName) => {
|
|
1829
|
+
const point = checkpoint(name, branchName);
|
|
1830
|
+
return {
|
|
1831
|
+
id: point.id,
|
|
1832
|
+
branch: point.branch,
|
|
1833
|
+
parent: point.parent,
|
|
1834
|
+
at: point.at,
|
|
1835
|
+
records: point.value.snapshot.records.length
|
|
1836
|
+
};
|
|
1837
|
+
},
|
|
1838
|
+
branch: (branchName, branchOptions) => {
|
|
1839
|
+
const point = branch(branchName, branchOptions);
|
|
1840
|
+
return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
|
|
1841
|
+
},
|
|
1842
|
+
checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
|
|
1843
|
+
retain: (name, checkpointId) => {
|
|
1844
|
+
timeline(name).retain(checkpointId);
|
|
1845
|
+
},
|
|
1846
|
+
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
1847
|
+
inspect: (name) => {
|
|
1848
|
+
const history = timeline(name);
|
|
1849
|
+
return {
|
|
1850
|
+
branches: history.branches(),
|
|
1851
|
+
checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
1852
|
+
id,
|
|
1853
|
+
branch: branchName,
|
|
1854
|
+
parent,
|
|
1855
|
+
at
|
|
1856
|
+
}))
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
},
|
|
1860
|
+
describe: options.describe ?? (() => ({})),
|
|
1861
|
+
...options.presets ? {
|
|
1862
|
+
applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
|
|
1863
|
+
} : {},
|
|
1864
|
+
routes: {
|
|
1865
|
+
...credentialRoutes(credentials),
|
|
1866
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
1867
|
+
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
1868
|
+
...options.admin?.(runtime) ?? {}
|
|
1869
|
+
},
|
|
1870
|
+
adminKey: options.adminKey
|
|
1871
|
+
});
|
|
1872
|
+
return runtime;
|
|
1873
|
+
};
|
|
1874
|
+
var mutableResponse = (response) => {
|
|
1875
|
+
try {
|
|
1876
|
+
response.headers.set("x-mockingbird-mutable-probe", "1");
|
|
1877
|
+
response.headers.delete("x-mockingbird-mutable-probe");
|
|
1878
|
+
return response;
|
|
1879
|
+
} catch {
|
|
1880
|
+
return new Response(response.body, response);
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1884
|
+
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
1885
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1886
|
+
var credentialRoutes = (registry) => ({
|
|
1887
|
+
"GET /credentials": () => adminJson(200, {
|
|
1888
|
+
credentials: registry.entries().map(({ credential, namespace }) => ({
|
|
1889
|
+
credential: maskCredential(credential),
|
|
1890
|
+
namespace
|
|
1891
|
+
}))
|
|
1892
|
+
}),
|
|
1893
|
+
"PUT /credentials": ({ body, namespace }) => {
|
|
1894
|
+
const pairs = [];
|
|
1895
|
+
const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
|
|
1896
|
+
if (Array.isArray(list)) {
|
|
1897
|
+
for (const each of list) {
|
|
1898
|
+
if (typeof each === "string")
|
|
1899
|
+
pairs.push([each, namespace]);
|
|
1900
|
+
else if (isObject(each) && typeof each.credential === "string") {
|
|
1901
|
+
pairs.push([
|
|
1902
|
+
each.credential,
|
|
1903
|
+
typeof each.namespace === "string" ? each.namespace : namespace
|
|
1904
|
+
]);
|
|
1905
|
+
} else
|
|
1906
|
+
return adminFail(400, "each entry is a credential string or {credential, namespace}");
|
|
1907
|
+
}
|
|
1908
|
+
} else if (isObject(list)) {
|
|
1909
|
+
for (const [credential, target] of Object.entries(list)) {
|
|
1910
|
+
if (typeof target !== "string")
|
|
1911
|
+
return adminFail(400, `namespace for ${credential} must be a string`);
|
|
1912
|
+
pairs.push([credential, target]);
|
|
1913
|
+
}
|
|
1914
|
+
} else if (isObject(body) && typeof body.credential === "string") {
|
|
1915
|
+
pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
|
|
1916
|
+
} else {
|
|
1917
|
+
return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
|
|
1918
|
+
}
|
|
1919
|
+
for (const [credential, target] of pairs) {
|
|
1920
|
+
if (!NAMESPACE_PATTERN.test(target))
|
|
1921
|
+
return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
|
|
1922
|
+
registry.set(credential, target);
|
|
1923
|
+
}
|
|
1924
|
+
return adminJson(200, { mapped: pairs.length });
|
|
1925
|
+
},
|
|
1926
|
+
"DELETE /credentials": ({ url }) => {
|
|
1927
|
+
const credential = url.searchParams.get("credential");
|
|
1928
|
+
if (credential === null)
|
|
1929
|
+
registry.clear();
|
|
1930
|
+
else
|
|
1931
|
+
registry.remove(credential);
|
|
1932
|
+
return adminJson(200, { status: "ok" });
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
var presetRoutes = (presets, runtime) => ({
|
|
1936
|
+
"GET /faults/presets": () => adminJson(200, {
|
|
1937
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
1938
|
+
}),
|
|
1939
|
+
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
1940
|
+
const name = params.name;
|
|
1941
|
+
if (!presets[name])
|
|
1942
|
+
return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
|
|
1943
|
+
const overrides = isObject(body) ? body : {};
|
|
1944
|
+
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
// ../core/dist/s3.js
|
|
1949
|
+
var encoder2 = new TextEncoder();
|
|
1950
|
+
var hmacBytes = async (key, message) => {
|
|
1951
|
+
const imported = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1952
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", imported, encoder2.encode(message)));
|
|
1953
|
+
};
|
|
1954
|
+
var sha256Hex = async (data) => toHex(await crypto.subtle.digest("SHA-256", typeof data === "string" ? encoder2.encode(data) : data));
|
|
1955
|
+
var encodeSegment = (segment) => encodeURIComponent(segment).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1956
|
+
var signV4 = async (input) => {
|
|
1957
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1958
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1959
|
+
const date = amzDate.slice(0, 8);
|
|
1960
|
+
const payloadHash = await sha256Hex(input.body);
|
|
1961
|
+
const headers = {
|
|
1962
|
+
...Object.fromEntries(Object.entries(input.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])),
|
|
1963
|
+
host: input.url.host,
|
|
1964
|
+
"x-amz-content-sha256": payloadHash,
|
|
1965
|
+
"x-amz-date": amzDate
|
|
1966
|
+
};
|
|
1967
|
+
const signedHeaders = Object.keys(headers).sort();
|
|
1968
|
+
const canonicalRequest = [
|
|
1969
|
+
input.method,
|
|
1970
|
+
input.url.pathname.split("/").map((s) => encodeSegment(decodeURIComponent(s))).join("/"),
|
|
1971
|
+
[...input.url.searchParams].map(([k, v]) => `${encodeSegment(k)}=${encodeSegment(v)}`).sort().join("&"),
|
|
1972
|
+
signedHeaders.map((h) => `${h}:${String(headers[h]).trim()}
|
|
1973
|
+
`).join(""),
|
|
1974
|
+
signedHeaders.join(";"),
|
|
1975
|
+
payloadHash
|
|
1976
|
+
].join("\n");
|
|
1977
|
+
const scope = `${date}/${input.region}/${input.service}/aws4_request`;
|
|
1978
|
+
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, await sha256Hex(canonicalRequest)].join("\n");
|
|
1979
|
+
let key = await hmacBytes(encoder2.encode(`AWS4${input.secretAccessKey}`), date);
|
|
1980
|
+
key = await hmacBytes(key, input.region);
|
|
1981
|
+
key = await hmacBytes(key, input.service);
|
|
1982
|
+
key = await hmacBytes(key, "aws4_request");
|
|
1983
|
+
const signature = toHex(await hmacBytes(key, stringToSign));
|
|
1984
|
+
const { host: _host, ...rest } = headers;
|
|
1985
|
+
return {
|
|
1986
|
+
...rest,
|
|
1987
|
+
authorization: `AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, SignedHeaders=${signedHeaders.join(";")}, Signature=${signature}`
|
|
1988
|
+
};
|
|
1989
|
+
};
|
|
1990
|
+
var putObject = async (target, key, body, contentType = "application/octet-stream") => {
|
|
1991
|
+
const bytes = typeof body === "string" ? encoder2.encode(body) : body;
|
|
1992
|
+
const url = new URL(`${target.endpoint.replace(/\/+$/, "")}/${encodeSegment(target.bucket)}/${key.split("/").map(encodeSegment).join("/")}`);
|
|
1993
|
+
const headers = await signV4({
|
|
1994
|
+
method: "PUT",
|
|
1995
|
+
url,
|
|
1996
|
+
body: bytes,
|
|
1997
|
+
region: target.region ?? "us-east-1",
|
|
1998
|
+
service: "s3",
|
|
1999
|
+
accessKeyId: target.accessKeyId ?? "S3RVER",
|
|
2000
|
+
secretAccessKey: target.secretAccessKey ?? "S3RVER",
|
|
2001
|
+
headers: { "content-type": contentType }
|
|
2002
|
+
});
|
|
2003
|
+
const send = target.fetch ?? ((request) => fetch(request));
|
|
2004
|
+
const response = await send(new Request(url, { method: "PUT", headers, body: bytes }));
|
|
2005
|
+
if (!response.ok) {
|
|
2006
|
+
const text2 = await response.text().catch(() => "");
|
|
2007
|
+
throw new Error(`S3 PutObject ${url.pathname} failed: ${response.status} ${text2.slice(0, 200)}`);
|
|
2008
|
+
}
|
|
2009
|
+
await response.body?.cancel();
|
|
2010
|
+
return `s3://${target.bucket}/${key}`;
|
|
2011
|
+
};
|
|
2012
|
+
|
|
2013
|
+
// src/audio.ts
|
|
2014
|
+
var MS_PER_CHARACTER = 60;
|
|
2015
|
+
var durationFor = (text2) => Math.max(1, text2.length) * MS_PER_CHARACTER;
|
|
2016
|
+
var pcmTone = (durationMs, sampleRate) => {
|
|
2017
|
+
const samples = Math.max(1, Math.round(sampleRate * durationMs / 1e3));
|
|
2018
|
+
const out = new Uint8Array(samples * 2);
|
|
2019
|
+
const view = new DataView(out.buffer);
|
|
2020
|
+
for (let i = 0; i < samples; i++) {
|
|
2021
|
+
const value = Math.round(Math.sin(2 * Math.PI * 440 * i / sampleRate) * 0.25 * 32767);
|
|
2022
|
+
view.setInt16(i * 2, value, true);
|
|
2023
|
+
}
|
|
2024
|
+
return out;
|
|
2025
|
+
};
|
|
2026
|
+
var MP3_RATES = {
|
|
2027
|
+
22050: { version: 2, index: 0 },
|
|
2028
|
+
24e3: { version: 2, index: 1 },
|
|
2029
|
+
16e3: { version: 2, index: 2 },
|
|
2030
|
+
11025: { version: 0, index: 0 },
|
|
2031
|
+
12e3: { version: 0, index: 1 },
|
|
2032
|
+
8e3: { version: 0, index: 2 }
|
|
2033
|
+
};
|
|
2034
|
+
var MP3_SAMPLE_RATES = Object.keys(MP3_RATES).map(Number);
|
|
2035
|
+
var MP3_SAMPLES_PER_FRAME = 576;
|
|
2036
|
+
var mp3Frame = (sampleRate) => {
|
|
2037
|
+
const rate = MP3_RATES[sampleRate];
|
|
2038
|
+
if (!rate) throw new RangeError(`no MPEG-2 Layer III sample rate ${sampleRate}`);
|
|
2039
|
+
const bitrateIndex = 4;
|
|
2040
|
+
const length = Math.floor(72 * 32e3 / sampleRate);
|
|
2041
|
+
const frame = new Uint8Array(length);
|
|
2042
|
+
frame[0] = 255;
|
|
2043
|
+
frame[1] = 224 | rate.version << 3 | 1 << 1 | 1;
|
|
2044
|
+
frame[2] = bitrateIndex << 4 | rate.index << 2;
|
|
2045
|
+
frame[3] = 3 << 6;
|
|
2046
|
+
return frame;
|
|
2047
|
+
};
|
|
2048
|
+
var mp3Audio = (durationMs, sampleRate) => {
|
|
2049
|
+
const frames = Math.max(1, Math.ceil(sampleRate * durationMs / 1e3 / MP3_SAMPLES_PER_FRAME));
|
|
2050
|
+
const one = mp3Frame(sampleRate);
|
|
2051
|
+
const out = new Uint8Array(one.length * frames);
|
|
2052
|
+
for (let i = 0; i < frames; i++) out.set(one, i * one.length);
|
|
2053
|
+
return out;
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
// src/eventstream.ts
|
|
2057
|
+
var EventStreamError = class extends Error {
|
|
2058
|
+
constructor(message) {
|
|
2059
|
+
super(message);
|
|
2060
|
+
this.name = "EventStreamError";
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
var PRELUDE = 12;
|
|
2064
|
+
var TRAILER = 4;
|
|
2065
|
+
var utf82 = new TextEncoder();
|
|
2066
|
+
var text = new TextDecoder();
|
|
2067
|
+
var CRC_TABLE = (() => {
|
|
2068
|
+
const table = new Uint32Array(256);
|
|
2069
|
+
for (let n = 0; n < 256; n++) {
|
|
2070
|
+
let c = n;
|
|
2071
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2072
|
+
table[n] = c >>> 0;
|
|
2073
|
+
}
|
|
2074
|
+
return table;
|
|
2075
|
+
})();
|
|
2076
|
+
var crc32 = (bytes) => {
|
|
2077
|
+
let crc = 4294967295;
|
|
2078
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2079
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
2080
|
+
}
|
|
2081
|
+
return (crc ^ 4294967295) >>> 0;
|
|
2082
|
+
};
|
|
2083
|
+
var toBytes = (body) => body === void 0 ? new Uint8Array(0) : typeof body === "string" ? utf82.encode(body) : body;
|
|
2084
|
+
var encodeHeaders = (headers) => {
|
|
2085
|
+
const parts = [];
|
|
2086
|
+
for (const [name, raw] of Object.entries(headers)) {
|
|
2087
|
+
const header = typeof raw === "string" ? { type: "string", value: raw } : raw;
|
|
2088
|
+
const nameBytes = utf82.encode(name);
|
|
2089
|
+
if (nameBytes.length > 255) throw new EventStreamError(`header name too long: ${name}`);
|
|
2090
|
+
let value;
|
|
2091
|
+
switch (header.type) {
|
|
2092
|
+
case "boolean":
|
|
2093
|
+
value = Uint8Array.of(header.value ? 0 : 1);
|
|
2094
|
+
break;
|
|
2095
|
+
case "byte":
|
|
2096
|
+
value = Uint8Array.of(2, header.value & 255);
|
|
2097
|
+
break;
|
|
2098
|
+
case "short": {
|
|
2099
|
+
value = new Uint8Array(3);
|
|
2100
|
+
value[0] = 3;
|
|
2101
|
+
new DataView(value.buffer).setInt16(1, header.value, false);
|
|
2102
|
+
break;
|
|
2103
|
+
}
|
|
2104
|
+
case "integer": {
|
|
2105
|
+
value = new Uint8Array(5);
|
|
2106
|
+
value[0] = 4;
|
|
2107
|
+
new DataView(value.buffer).setInt32(1, header.value, false);
|
|
2108
|
+
break;
|
|
2109
|
+
}
|
|
2110
|
+
case "long": {
|
|
2111
|
+
value = new Uint8Array(9);
|
|
2112
|
+
value[0] = 5;
|
|
2113
|
+
new DataView(value.buffer).setBigInt64(1, header.value, false);
|
|
2114
|
+
break;
|
|
2115
|
+
}
|
|
2116
|
+
case "binary":
|
|
2117
|
+
case "string": {
|
|
2118
|
+
const bytes = header.type === "binary" ? header.value : utf82.encode(header.value);
|
|
2119
|
+
if (bytes.length > 65535) throw new EventStreamError(`header ${name} value too long`);
|
|
2120
|
+
value = new Uint8Array(3 + bytes.length);
|
|
2121
|
+
value[0] = header.type === "binary" ? 6 : 7;
|
|
2122
|
+
new DataView(value.buffer).setUint16(1, bytes.length, false);
|
|
2123
|
+
value.set(bytes, 3);
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
case "timestamp": {
|
|
2127
|
+
value = new Uint8Array(9);
|
|
2128
|
+
value[0] = 8;
|
|
2129
|
+
new DataView(value.buffer).setBigInt64(1, BigInt(header.value.getTime()), false);
|
|
2130
|
+
break;
|
|
2131
|
+
}
|
|
2132
|
+
case "uuid": {
|
|
2133
|
+
const hex = header.value.replace(/-/g, "");
|
|
2134
|
+
if (!/^[0-9a-f]{32}$/i.test(hex)) throw new EventStreamError(`bad uuid header ${name}`);
|
|
2135
|
+
value = new Uint8Array(17);
|
|
2136
|
+
value[0] = 9;
|
|
2137
|
+
for (let i = 0; i < 16; i++) value[i + 1] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2138
|
+
break;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const entry = new Uint8Array(1 + nameBytes.length + value.length);
|
|
2142
|
+
entry[0] = nameBytes.length;
|
|
2143
|
+
entry.set(nameBytes, 1);
|
|
2144
|
+
entry.set(value, 1 + nameBytes.length);
|
|
2145
|
+
parts.push(entry);
|
|
2146
|
+
}
|
|
2147
|
+
return concat(parts);
|
|
2148
|
+
};
|
|
2149
|
+
var concat = (parts) => {
|
|
2150
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
2151
|
+
let offset = 0;
|
|
2152
|
+
for (const part of parts) {
|
|
2153
|
+
out.set(part, offset);
|
|
2154
|
+
offset += part.length;
|
|
2155
|
+
}
|
|
2156
|
+
return out;
|
|
2157
|
+
};
|
|
2158
|
+
var encodeMessage = (message) => {
|
|
2159
|
+
const headers = encodeHeaders(message.headers);
|
|
2160
|
+
const body = toBytes(message.body);
|
|
2161
|
+
const total = PRELUDE + headers.length + body.length + TRAILER;
|
|
2162
|
+
const frame = new Uint8Array(total);
|
|
2163
|
+
const view = new DataView(frame.buffer);
|
|
2164
|
+
view.setUint32(0, total, false);
|
|
2165
|
+
view.setUint32(4, headers.length, false);
|
|
2166
|
+
view.setUint32(8, crc32(frame.subarray(0, 8)), false);
|
|
2167
|
+
frame.set(headers, PRELUDE);
|
|
2168
|
+
frame.set(body, PRELUDE + headers.length);
|
|
2169
|
+
view.setUint32(total - TRAILER, crc32(frame.subarray(0, total - TRAILER)), false);
|
|
2170
|
+
return frame;
|
|
2171
|
+
};
|
|
2172
|
+
var decodeHeaders = (bytes) => {
|
|
2173
|
+
const headers = {};
|
|
2174
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2175
|
+
let at = 0;
|
|
2176
|
+
while (at < bytes.length) {
|
|
2177
|
+
const nameLength = bytes[at];
|
|
2178
|
+
const name = text.decode(bytes.subarray(at + 1, at + 1 + nameLength));
|
|
2179
|
+
at += 1 + nameLength;
|
|
2180
|
+
const type = bytes[at];
|
|
2181
|
+
at += 1;
|
|
2182
|
+
switch (type) {
|
|
2183
|
+
case 0:
|
|
2184
|
+
case 1:
|
|
2185
|
+
headers[name] = { type: "boolean", value: type === 0 };
|
|
2186
|
+
break;
|
|
2187
|
+
case 2:
|
|
2188
|
+
headers[name] = { type: "byte", value: view.getInt8(at) };
|
|
2189
|
+
at += 1;
|
|
2190
|
+
break;
|
|
2191
|
+
case 3:
|
|
2192
|
+
headers[name] = { type: "short", value: view.getInt16(at, false) };
|
|
2193
|
+
at += 2;
|
|
2194
|
+
break;
|
|
2195
|
+
case 4:
|
|
2196
|
+
headers[name] = { type: "integer", value: view.getInt32(at, false) };
|
|
2197
|
+
at += 4;
|
|
2198
|
+
break;
|
|
2199
|
+
case 5:
|
|
2200
|
+
headers[name] = { type: "long", value: view.getBigInt64(at, false) };
|
|
2201
|
+
at += 8;
|
|
2202
|
+
break;
|
|
2203
|
+
case 6:
|
|
2204
|
+
case 7: {
|
|
2205
|
+
const length = view.getUint16(at, false);
|
|
2206
|
+
const value = bytes.slice(at + 2, at + 2 + length);
|
|
2207
|
+
headers[name] = type === 6 ? { type: "binary", value } : { type: "string", value: text.decode(value) };
|
|
2208
|
+
at += 2 + length;
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
case 8:
|
|
2212
|
+
headers[name] = { type: "timestamp", value: new Date(Number(view.getBigInt64(at, false))) };
|
|
2213
|
+
at += 8;
|
|
2214
|
+
break;
|
|
2215
|
+
case 9: {
|
|
2216
|
+
const hex = [...bytes.subarray(at, at + 16)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2217
|
+
headers[name] = {
|
|
2218
|
+
type: "uuid",
|
|
2219
|
+
value: `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
2220
|
+
};
|
|
2221
|
+
at += 16;
|
|
2222
|
+
break;
|
|
2223
|
+
}
|
|
2224
|
+
default:
|
|
2225
|
+
throw new EventStreamError(`unknown header type ${type} for ${name}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return headers;
|
|
2229
|
+
};
|
|
2230
|
+
var decodeMessage = (frame) => {
|
|
2231
|
+
if (frame.length < PRELUDE + TRAILER) throw new EventStreamError("frame shorter than a prelude");
|
|
2232
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
2233
|
+
const total = view.getUint32(0, false);
|
|
2234
|
+
const headersLength = view.getUint32(4, false);
|
|
2235
|
+
if (total !== frame.length) throw new EventStreamError(`frame length ${frame.length} \u2260 ${total}`);
|
|
2236
|
+
if (view.getUint32(8, false) !== crc32(frame.subarray(0, 8))) {
|
|
2237
|
+
throw new EventStreamError("prelude checksum mismatch");
|
|
2238
|
+
}
|
|
2239
|
+
if (view.getUint32(total - TRAILER, false) !== crc32(frame.subarray(0, total - TRAILER))) {
|
|
2240
|
+
throw new EventStreamError("message checksum mismatch");
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
headers: decodeHeaders(frame.subarray(PRELUDE, PRELUDE + headersLength)),
|
|
2244
|
+
body: frame.slice(PRELUDE + headersLength, total - TRAILER)
|
|
2245
|
+
};
|
|
2246
|
+
};
|
|
2247
|
+
var FrameReader = class {
|
|
2248
|
+
buffer = new Uint8Array(0);
|
|
2249
|
+
/** Add bytes; returns every frame they complete. */
|
|
2250
|
+
push(chunk) {
|
|
2251
|
+
this.buffer = this.buffer.length === 0 ? chunk.slice() : concat([this.buffer, chunk]);
|
|
2252
|
+
const out = [];
|
|
2253
|
+
while (this.buffer.length >= 4) {
|
|
2254
|
+
const total = new DataView(
|
|
2255
|
+
this.buffer.buffer,
|
|
2256
|
+
this.buffer.byteOffset,
|
|
2257
|
+
this.buffer.byteLength
|
|
2258
|
+
).getUint32(0, false);
|
|
2259
|
+
if (total < PRELUDE + TRAILER) throw new EventStreamError(`impossible frame length ${total}`);
|
|
2260
|
+
if (this.buffer.length < total) break;
|
|
2261
|
+
out.push(decodeMessage(this.buffer.subarray(0, total)));
|
|
2262
|
+
this.buffer = this.buffer.slice(total);
|
|
2263
|
+
}
|
|
2264
|
+
return out;
|
|
2265
|
+
}
|
|
2266
|
+
/** Bytes of an incomplete frame still waiting for the rest. */
|
|
2267
|
+
get pending() {
|
|
2268
|
+
return this.buffer.length;
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
var headerString = (message, name) => {
|
|
2272
|
+
const header = message.headers[name];
|
|
2273
|
+
return header?.type === "string" ? header.value : void 0;
|
|
2274
|
+
};
|
|
2275
|
+
var unwrapSigned = (message) => {
|
|
2276
|
+
if (message.headers[":chunk-signature"] === void 0) return message;
|
|
2277
|
+
if (message.body.length === 0) return null;
|
|
2278
|
+
return decodeMessage(message.body);
|
|
2279
|
+
};
|
|
2280
|
+
var eventFrame = (eventType, payload, contentType = payload instanceof Uint8Array ? "application/octet-stream" : "application/json") => encodeMessage({
|
|
2281
|
+
headers: {
|
|
2282
|
+
":event-type": eventType,
|
|
2283
|
+
":content-type": contentType,
|
|
2284
|
+
":message-type": "event"
|
|
2285
|
+
},
|
|
2286
|
+
body: payload instanceof Uint8Array ? payload : JSON.stringify(payload)
|
|
2287
|
+
});
|
|
2288
|
+
var exceptionFrame = (exceptionType, body) => encodeMessage({
|
|
2289
|
+
headers: {
|
|
2290
|
+
":exception-type": exceptionType,
|
|
2291
|
+
":content-type": "application/json",
|
|
2292
|
+
":message-type": "exception"
|
|
2293
|
+
},
|
|
2294
|
+
body: JSON.stringify(body)
|
|
2295
|
+
});
|
|
2296
|
+
var payloadJson = (message) => {
|
|
2297
|
+
try {
|
|
2298
|
+
return JSON.parse(text.decode(message.body));
|
|
2299
|
+
} catch {
|
|
2300
|
+
return void 0;
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
async function* readFrames(body) {
|
|
2304
|
+
if (!body) return;
|
|
2305
|
+
const reader = new FrameReader();
|
|
2306
|
+
const stream = body.getReader();
|
|
2307
|
+
try {
|
|
2308
|
+
for (; ; ) {
|
|
2309
|
+
const { done, value } = await stream.read();
|
|
2310
|
+
if (done) break;
|
|
2311
|
+
for (const frame of reader.push(value)) yield frame;
|
|
2312
|
+
}
|
|
2313
|
+
} finally {
|
|
2314
|
+
stream.releaseLock();
|
|
2315
|
+
}
|
|
2316
|
+
if (reader.pending > 0) throw new EventStreamError("stream ended inside a frame");
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/generated/openapi.ts
|
|
2320
|
+
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Amazon Polly + Amazon Transcribe (Mockingbird subset)","description":"The speech surface our consumer calls: Polly \`SynthesizeSpeech\` and\\n\`StartSpeechSynthesisStream\` (HTTP/2 duplex event stream), Transcribe Streaming\\n\`StartStreamTranscription\` (HTTP/2 duplex, parameters in \`x-amzn-transcribe-*\` headers),\\nand Transcribe batch \`StartTranscriptionJob\` / \`GetTranscriptionJob\` (AWS JSON 1.1 on\\n\`POST /\`, selected by \`X-Amz-Target\`). Hand-trimmed from the Smithy models behind\\n\`@aws-sdk/client-polly\`, \`@aws-sdk/client-transcribe-streaming\` and\\n\`@aws-sdk/client-transcribe\` 3.1132.0.\\n","version":"2016-06-10","x-mockingbird-upstream":{"note":"Polly and Transcribe Streaming are restJson1 (errors in \`x-amzn-ErrorType\` + \`{\\"message\\"}\`); Transcribe batch is awsJson1_1 (errors as \`{\\"__type\\", \\"Message\\"}\`). One mock serves all three hosts (\`AWS_ENDPOINT_URL_POLLY\`, \`AWS_ENDPOINT_URL_TRANSCRIBE_STREAMING\`, \`AWS_ENDPOINT_URL_TRANSCRIBE\`) because their paths do not collide."}},"servers":[{"url":"https://polly.us-east-1.amazonaws.com"}],"security":[{"sigv4":[]}],"paths":{"/v1/speech":{"post":{"operationId":"SynthesizeSpeech","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SynthesizeSpeechInput"}}}},"responses":{"200":{"description":"The audio (\`audio/pcm\` raw s16le mono, or \`audio/mpeg\`), with \`x-amzn-RequestCharacters\`.","headers":{"x-amzn-RequestCharacters":{"schema":{"type":"integer"}}},"content":{"audio/pcm":{"schema":{"type":"string","format":"binary"}},"audio/mpeg":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/synthesisStream":{"post":{"operationId":"StartSpeechSynthesisStream","description":"HTTP/2 duplex. Parameters in \`x-amzn-Engine\`, \`x-amzn-VoiceId\`, \`x-amzn-OutputFormat\`, \`x-amzn-SampleRate\`; the request body streams \`TextEvent\` / \`CloseStreamEvent\`, the response streams \`AudioEvent\` then \`StreamClosedEvent\` (or an exception event).","x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"A duplex HTTP/2 session driven by the SDK's event stream; random bodies cannot drive it."}},"requestBody":{"required":true,"content":{"application/vnd.amazon.eventstream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Event stream of AudioEvent, StreamClosedEvent and exception events.","content":{"application/vnd.amazon.eventstream":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/stream-transcription":{"post":{"operationId":"StartStreamTranscription","description":"HTTP/2 duplex (Transcribe Streaming). Parameters in \`x-amzn-transcribe-*\` headers; the request body streams \`AudioEvent\`s, the response streams \`TranscriptEvent\`s: scripted partials as audio arrives, then the final result when the audio ends.","x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"A duplex HTTP/2 session driven by the SDK's event stream; random bodies cannot drive it."}},"requestBody":{"required":true,"content":{"application/vnd.amazon.eventstream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Event stream of TranscriptEvent and exception events.","content":{"application/vnd.amazon.eventstream":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/":{"post":{"operationId":"TranscribeJsonRpc","description":"Transcribe batch (AWS JSON 1.1): \`X-Amz-Target: Transcribe.StartTranscriptionJob\` or \`Transcribe.GetTranscriptionJob\`. Other targets answer \`UnknownOperationException\`. Unsafe for live runs: StartTranscriptionJob starts a billed job on real media.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"X-Amz-Target","in":"header","required":true,"schema":{"type":"string","enum":["Transcribe.StartTranscriptionJob","Transcribe.GetTranscriptionJob"]}}],"requestBody":{"required":true,"description":"The SDK sends \`application/x-amz-json-1.1\`; plain JSON is accepted too.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionJobRequest"}}}},"responses":{"200":{"description":"\`{TranscriptionJob}\`","content":{"application/x-amz-json-1.1":{"schema":{"$ref":"#/components/schemas/TranscriptionJobResponse"}}}},"400":{"description":"BadRequestException, ConflictException, LimitExceededException or a validation error.","content":{"application/x-amz-json-1.1":{"schema":{"$ref":"#/components/schemas/JsonError"}}}},"500":{"description":"InternalFailureException.","content":{"application/x-amz-json-1.1":{"schema":{"$ref":"#/components/schemas/JsonError"}}}}}}}},"components":{"securitySchemes":{"sigv4":{"type":"apiKey","in":"header","name":"Authorization","description":"AWS SigV4 (services \`polly\`, \`transcribe\`). Accepted without verification; the access key id selects a namespace."}},"responses":{"Error":{"description":"A restJson1 error (type in \`x-amzn-ErrorType\`).","headers":{"x-amzn-ErrorType":{"schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string","x-mockingbird-volatile":{"kind":"opaque"}}}}}}}},"schemas":{"SynthesizeSpeechInput":{"type":"object","required":["OutputFormat","Text","VoiceId"],"properties":{"Engine":{"type":"string","enum":["standard","neural","long-form","generative"]},"OutputFormat":{"type":"string","enum":["pcm","mp3","ogg_vorbis","ogg_opus","mulaw","alaw","json"]},"SampleRate":{"type":"string","enum":["8000","16000","22050","24000"]},"Text":{"type":"string","minLength":1,"maxLength":6000},"TextType":{"type":"string","enum":["text","ssml"]},"VoiceId":{"description":"Any Polly voice is accepted; the enum only steers generated requests.","type":"string","enum":["Danielle","Joanna","Matthew","Ruth","Salli","Stephen","Tiffany","Joey","Justin","Kendra","Kimberly","Ivy","Kevin","Gregory","Amy","Brian","Emma","Olivia"]},"LanguageCode":{"type":"string"},"LexiconNames":{"type":"array","items":{"type":"string"}},"SpeechMarkTypes":{"type":"array","items":{"type":"string","enum":["sentence","ssml","viseme","word"]}}}},"TranscriptionJobRequest":{"type":"object","required":["TranscriptionJobName"],"properties":{"TranscriptionJobName":{"type":"string","pattern":"^[0-9a-zA-Z._-]{1,200}$"},"LanguageCode":{"type":"string","enum":["en-US","en-GB","es-US"]},"MediaFormat":{"type":"string","enum":["mp3","mp4","wav","flac","ogg","amr","webm","m4a"]},"MediaSampleRateHertz":{"type":"integer","minimum":8000,"maximum":48000},"Media":{"type":"object","properties":{"MediaFileUri":{"type":"string","pattern":"^s3://[a-z0-9.-]{3,63}/[a-zA-Z0-9/_.-]{1,200}$"}}},"OutputBucketName":{"type":"string","pattern":"^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$"},"OutputKey":{"type":"string","pattern":"^[a-zA-Z0-9/_.!*'()-]{1,200}$"},"Settings":{"type":"object","properties":{"ChannelIdentification":{"type":"boolean"},"ShowSpeakerLabels":{"type":"boolean"},"MaxSpeakerLabels":{"type":"integer","minimum":2,"maximum":30},"VocabularyName":{"type":"string"}}}}},"TranscriptionJob":{"type":"object","required":["TranscriptionJobName","TranscriptionJobStatus"],"properties":{"TranscriptionJobName":{"type":"string"},"TranscriptionJobStatus":{"type":"string","enum":["QUEUED","IN_PROGRESS","FAILED","COMPLETED"]},"LanguageCode":{"type":"string"},"MediaFormat":{"type":"string"},"Media":{"type":"object"},"Settings":{"type":"object"},"StartTime":{"type":"number","x-mockingbird-volatile":{"kind":"timestamp"}},"CreationTime":{"type":"number","x-mockingbird-volatile":{"kind":"timestamp"}},"CompletionTime":{"type":"number","x-mockingbird-volatile":{"kind":"timestamp"}},"FailureReason":{"type":"string"},"Transcript":{"type":"object","properties":{"TranscriptFileUri":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}}}},"TranscriptionJobResponse":{"type":"object","required":["TranscriptionJob"],"properties":{"TranscriptionJob":{"$ref":"#/components/schemas/TranscriptionJob"}}},"JsonError":{"type":"object","required":["__type"],"properties":{"__type":{"type":"string"},"Message":{"type":"string","x-mockingbird-volatile":{"kind":"opaque"}},"message":{"type":"string","x-mockingbird-volatile":{"kind":"opaque"}}}}}}}`);
|
|
2321
|
+
var operationIds = ["SynthesizeSpeech", "StartSpeechSynthesisStream", "StartStreamTranscription", "TranscribeJsonRpc"];
|
|
2322
|
+
var supportedOperationIds = ["SynthesizeSpeech", "StartSpeechSynthesisStream", "StartStreamTranscription", "TranscribeJsonRpc"];
|
|
2323
|
+
|
|
2324
|
+
// src/state.ts
|
|
2325
|
+
var DEFAULT_SETTINGS = {
|
|
2326
|
+
defaultTranscript: "Hello.",
|
|
2327
|
+
jobDurationMs: 2e3
|
|
2328
|
+
};
|
|
2329
|
+
var SpeechState = class {
|
|
2330
|
+
constructor(sqlite, namespace, seed) {
|
|
2331
|
+
this.seed = seed;
|
|
2332
|
+
this.transcripts = new Collection(sqlite, namespace, "transcripts");
|
|
2333
|
+
this.uses = new Collection(sqlite, namespace, "transcript_uses");
|
|
2334
|
+
this.jobs = new Collection(sqlite, namespace, "jobs");
|
|
2335
|
+
this.settings = new Collection(sqlite, namespace, "settings");
|
|
2336
|
+
this.stats = new Collection(sqlite, namespace, "stats");
|
|
2337
|
+
this.log = new Collection(sqlite, namespace, "speech_log");
|
|
2338
|
+
this.ensureSeeded();
|
|
2339
|
+
}
|
|
2340
|
+
seed;
|
|
2341
|
+
transcripts;
|
|
2342
|
+
uses;
|
|
2343
|
+
jobs;
|
|
2344
|
+
settings;
|
|
2345
|
+
stats;
|
|
2346
|
+
log;
|
|
2347
|
+
ensureSeeded() {
|
|
2348
|
+
if (!this.settings.has("settings")) {
|
|
2349
|
+
this.settings.insert("settings", { ...DEFAULT_SETTINGS, ...this.seed.settings });
|
|
2350
|
+
for (const script of this.seed.transcripts) this.transcripts.insert(script.id, script);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
current() {
|
|
2354
|
+
return this.settings.get("settings") ?? DEFAULT_SETTINGS;
|
|
2355
|
+
}
|
|
2356
|
+
update(patch) {
|
|
2357
|
+
const next = { ...this.current(), ...patch };
|
|
2358
|
+
this.settings.insert("settings", next);
|
|
2359
|
+
return next;
|
|
2360
|
+
}
|
|
2361
|
+
scripts() {
|
|
2362
|
+
return this.transcripts.list({ order: "oldest" }).map((row) => row.value);
|
|
2363
|
+
}
|
|
2364
|
+
put(scripts, replace) {
|
|
2365
|
+
if (replace) {
|
|
2366
|
+
for (const row of this.transcripts.list()) this.transcripts.delete(row.id);
|
|
2367
|
+
for (const row of this.uses.list()) this.uses.delete(row.id);
|
|
2368
|
+
}
|
|
2369
|
+
for (const script of scripts) this.transcripts.insert(script.id, script);
|
|
2370
|
+
return this.scripts();
|
|
2371
|
+
}
|
|
2372
|
+
remove(id) {
|
|
2373
|
+
const targets = id === void 0 ? this.transcripts.list().map((row) => row.id) : [id];
|
|
2374
|
+
let removed = 0;
|
|
2375
|
+
for (const each of targets) {
|
|
2376
|
+
if (this.transcripts.delete(each)) removed++;
|
|
2377
|
+
this.uses.delete(each);
|
|
2378
|
+
}
|
|
2379
|
+
return removed;
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* The transcript for a session or job: the first script naming it exactly, else the first
|
|
2383
|
+
* `any` (or unmatched) script with uses left. Counts the use and the stats.
|
|
2384
|
+
*/
|
|
2385
|
+
pick(target) {
|
|
2386
|
+
const usable = this.scripts().filter(
|
|
2387
|
+
(s) => s.times === void 0 || (this.uses.get(s.id) ?? 0) < s.times
|
|
2388
|
+
);
|
|
2389
|
+
const exact = usable.find(
|
|
2390
|
+
(s) => target.sessionIndex !== void 0 && s.match?.sessionIndex === target.sessionIndex || target.jobName !== void 0 && s.match?.jobName === target.jobName
|
|
2391
|
+
);
|
|
2392
|
+
const chosen = exact ?? usable.find(
|
|
2393
|
+
(s) => !s.match || s.match.any === true || s.match.sessionIndex === void 0 && s.match.jobName === void 0
|
|
2394
|
+
);
|
|
2395
|
+
const stats = this.currentStats();
|
|
2396
|
+
if (chosen) this.uses.insert(chosen.id, (this.uses.get(chosen.id) ?? 0) + 1);
|
|
2397
|
+
this.stats.insert("stats", {
|
|
2398
|
+
...stats,
|
|
2399
|
+
scripted: stats.scripted + (chosen ? 1 : 0),
|
|
2400
|
+
unscripted: stats.unscripted + (chosen ? 0 : 1)
|
|
2401
|
+
});
|
|
2402
|
+
return chosen;
|
|
2403
|
+
}
|
|
2404
|
+
/** The 0-based index of the next streaming session in this namespace. */
|
|
2405
|
+
nextSessionIndex() {
|
|
2406
|
+
const stats = this.currentStats();
|
|
2407
|
+
this.stats.insert("stats", { ...stats, sessions: stats.sessions + 1 });
|
|
2408
|
+
return stats.sessions;
|
|
2409
|
+
}
|
|
2410
|
+
currentStats() {
|
|
2411
|
+
return this.stats.get("stats") ?? { sessions: 0, scripted: 0, unscripted: 0 };
|
|
2412
|
+
}
|
|
2413
|
+
record(entry) {
|
|
2414
|
+
this.log.insert(String(this.log.nextSequence()), entry);
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
// src/runtime.ts
|
|
2419
|
+
var effect = (type, operationId, description, params = {}) => ({
|
|
2420
|
+
description,
|
|
2421
|
+
rules: [{ operationId, effect: "speech_fault", params: { type, ...params } }]
|
|
2422
|
+
});
|
|
2423
|
+
var SPEECH_PRESETS = {
|
|
2424
|
+
polly_throttling: effect(
|
|
2425
|
+
"polly_throttling",
|
|
2426
|
+
"SynthesizeSpeech",
|
|
2427
|
+
"SynthesizeSpeech answers 429 ThrottlingException"
|
|
2428
|
+
),
|
|
2429
|
+
polly_service_failure: effect(
|
|
2430
|
+
"polly_service_failure",
|
|
2431
|
+
"SynthesizeSpeech",
|
|
2432
|
+
"SynthesizeSpeech answers 500 ServiceFailureException"
|
|
2433
|
+
),
|
|
2434
|
+
polly_stream_throttling: effect(
|
|
2435
|
+
"polly_stream_throttling",
|
|
2436
|
+
"StartSpeechSynthesisStream",
|
|
2437
|
+
"The speech stream opens, then a ThrottlingException event before any audio (our adapter falls back to SynthesizeSpeech)"
|
|
2438
|
+
),
|
|
2439
|
+
polly_stream_validation: effect(
|
|
2440
|
+
"polly_stream_validation",
|
|
2441
|
+
"StartSpeechSynthesisStream",
|
|
2442
|
+
"A ValidationException event before any audio"
|
|
2443
|
+
),
|
|
2444
|
+
polly_stream_quota: effect(
|
|
2445
|
+
"polly_stream_quota",
|
|
2446
|
+
"StartSpeechSynthesisStream",
|
|
2447
|
+
"A ServiceQuotaExceededException event before any audio"
|
|
2448
|
+
),
|
|
2449
|
+
polly_stream_failure: effect(
|
|
2450
|
+
"polly_stream_failure",
|
|
2451
|
+
"StartSpeechSynthesisStream",
|
|
2452
|
+
"A ServiceFailureException event after the first text's audio",
|
|
2453
|
+
{ afterEvents: 1 }
|
|
2454
|
+
),
|
|
2455
|
+
polly_stream_no_close: effect(
|
|
2456
|
+
"polly_stream_no_close",
|
|
2457
|
+
"StartSpeechSynthesisStream",
|
|
2458
|
+
"Audio, but the stream ends without the required StreamClosedEvent"
|
|
2459
|
+
),
|
|
2460
|
+
transcribe_bad_request: effect(
|
|
2461
|
+
"transcribe_bad_request",
|
|
2462
|
+
"StartStreamTranscription",
|
|
2463
|
+
"400 BadRequestException before the stream opens"
|
|
2464
|
+
),
|
|
2465
|
+
transcribe_limit_exceeded: effect(
|
|
2466
|
+
"transcribe_limit_exceeded",
|
|
2467
|
+
"StartStreamTranscription",
|
|
2468
|
+
"429 LimitExceededException (concurrent stream limit)"
|
|
2469
|
+
),
|
|
2470
|
+
transcribe_service_unavailable: effect(
|
|
2471
|
+
"transcribe_service_unavailable",
|
|
2472
|
+
"StartStreamTranscription",
|
|
2473
|
+
"503 ServiceUnavailableException"
|
|
2474
|
+
),
|
|
2475
|
+
transcribe_mid_stream_failure: effect(
|
|
2476
|
+
"transcribe_mid_stream_failure",
|
|
2477
|
+
"StartStreamTranscription",
|
|
2478
|
+
"An InternalFailureException event after the first partial result",
|
|
2479
|
+
{ afterEvents: 1 }
|
|
2480
|
+
),
|
|
2481
|
+
transcribe_job_limit: effect(
|
|
2482
|
+
"transcribe_job_limit",
|
|
2483
|
+
"TranscribeJsonRpc",
|
|
2484
|
+
"StartTranscriptionJob answers LimitExceededException"
|
|
2485
|
+
),
|
|
2486
|
+
transcribe_job_failed: effect(
|
|
2487
|
+
"transcribe_job_failed",
|
|
2488
|
+
"TranscribeJsonRpc",
|
|
2489
|
+
"The next started job is FAILED with a FailureReason"
|
|
2490
|
+
)
|
|
2491
|
+
};
|
|
2492
|
+
var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2493
|
+
var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2494
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2495
|
+
var parseTranscripts = (body, existing) => {
|
|
2496
|
+
const list = Array.isArray(body) ? body : isRecord4(body) && Array.isArray(body.transcripts) ? body.transcripts : [body];
|
|
2497
|
+
const out = [];
|
|
2498
|
+
for (const [index, each] of list.entries()) {
|
|
2499
|
+
const path = `transcripts[${index}]`;
|
|
2500
|
+
if (!isRecord4(each)) return `${path}: an object`;
|
|
2501
|
+
if (typeof each.final !== "string") return `${path}.final: a string`;
|
|
2502
|
+
if (each.partials !== void 0 && (!Array.isArray(each.partials) || each.partials.some((p) => typeof p !== "string"))) {
|
|
2503
|
+
return `${path}.partials: string[]`;
|
|
2504
|
+
}
|
|
2505
|
+
const match = each.match;
|
|
2506
|
+
if (match !== void 0) {
|
|
2507
|
+
if (!isRecord4(match)) return `${path}.match: {sessionIndex} | {jobName} | {any: true}`;
|
|
2508
|
+
if (match.sessionIndex !== void 0 && (typeof match.sessionIndex !== "number" || match.sessionIndex < 0)) {
|
|
2509
|
+
return `${path}.match.sessionIndex: a 0-based index`;
|
|
2510
|
+
}
|
|
2511
|
+
if (match.jobName !== void 0 && typeof match.jobName !== "string")
|
|
2512
|
+
return `${path}.match.jobName: a string`;
|
|
2513
|
+
}
|
|
2514
|
+
if (each.times !== void 0 && (typeof each.times !== "number" || each.times < 1))
|
|
2515
|
+
return `${path}.times: a positive count`;
|
|
2516
|
+
out.push({
|
|
2517
|
+
...each,
|
|
2518
|
+
id: typeof each.id === "string" ? each.id : `transcript_${existing + index + 1}`
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
return out;
|
|
2522
|
+
};
|
|
2523
|
+
var adminRoutes = (runtime) => {
|
|
2524
|
+
const store = (replace) => ({ body, namespace }) => {
|
|
2525
|
+
const api = runtime.instance(namespace);
|
|
2526
|
+
const parsed = parseTranscripts(body, replace ? 0 : api.state.scripts().length);
|
|
2527
|
+
if (typeof parsed === "string") return adminError3(400, parsed);
|
|
2528
|
+
return json3(200, { transcripts: api.state.put(parsed, replace) });
|
|
2529
|
+
};
|
|
2530
|
+
return {
|
|
2531
|
+
"GET /transcripts": ({ namespace }) => {
|
|
2532
|
+
const api = runtime.instance(namespace);
|
|
2533
|
+
return json3(200, { transcripts: api.state.scripts(), stats: api.stats() });
|
|
2534
|
+
},
|
|
2535
|
+
"PUT /transcripts": store(true),
|
|
2536
|
+
"POST /transcripts": store(false),
|
|
2537
|
+
"DELETE /transcripts": ({ url, namespace }) => json3(200, {
|
|
2538
|
+
removed: runtime.instance(namespace).state.remove(url.searchParams.get("id") ?? void 0)
|
|
2539
|
+
}),
|
|
2540
|
+
"GET /jobs": ({ namespace }) => json3(200, { jobs: runtime.instance(namespace).jobs() }),
|
|
2541
|
+
"POST /jobs/:name/complete": ({ params, body, namespace }) => {
|
|
2542
|
+
const transcript = isRecord4(body) && typeof body.transcript === "string" ? body.transcript : void 0;
|
|
2543
|
+
const job = runtime.instance(namespace).complete(params.name, transcript);
|
|
2544
|
+
return job ? json3(200, job) : adminError3(404, `no job ${params.name}`);
|
|
2545
|
+
},
|
|
2546
|
+
"POST /jobs/:name/fail": ({ params, body, namespace }) => {
|
|
2547
|
+
const reason = isRecord4(body) && typeof body.reason === "string" ? body.reason : "The job failed.";
|
|
2548
|
+
const job = runtime.instance(namespace).fail(params.name, reason);
|
|
2549
|
+
return job ? json3(200, job) : adminError3(404, `no job ${params.name}`);
|
|
2550
|
+
},
|
|
2551
|
+
"GET /jobs/:name/transcript": ({ params, namespace }) => {
|
|
2552
|
+
const api = runtime.instance(namespace);
|
|
2553
|
+
const job = api.jobs().find((j) => j.TranscriptionJobName === params.name);
|
|
2554
|
+
if (!job) return adminError3(404, `no job ${params.name}`);
|
|
2555
|
+
if (job.TranscriptionJobStatus !== "COMPLETED")
|
|
2556
|
+
return adminError3(409, `job ${params.name} is ${job.TranscriptionJobStatus}`);
|
|
2557
|
+
return json3(200, api.transcriptDocument(job));
|
|
2558
|
+
},
|
|
2559
|
+
"GET /speech": ({ namespace }) => json3(200, { entries: runtime.instance(namespace).speechLog() }),
|
|
2560
|
+
"GET /settings": ({ namespace }) => json3(200, runtime.instance(namespace).state.current()),
|
|
2561
|
+
"PUT /settings": ({ body, namespace }) => {
|
|
2562
|
+
if (!isRecord4(body)) return adminError3(400, "expected a JSON object");
|
|
2563
|
+
const patch = {};
|
|
2564
|
+
if (body.defaultTranscript !== void 0) {
|
|
2565
|
+
if (typeof body.defaultTranscript !== "string")
|
|
2566
|
+
return adminError3(400, "defaultTranscript: string");
|
|
2567
|
+
patch.defaultTranscript = body.defaultTranscript;
|
|
2568
|
+
}
|
|
2569
|
+
if (body.jobDurationMs !== void 0) {
|
|
2570
|
+
if (typeof body.jobDurationMs !== "number" || body.jobDurationMs < 0)
|
|
2571
|
+
return adminError3(400, "jobDurationMs: ms \u2265 0");
|
|
2572
|
+
patch.jobDurationMs = body.jobDurationMs;
|
|
2573
|
+
}
|
|
2574
|
+
const unknown = Object.keys(body).find(
|
|
2575
|
+
(key) => key !== "defaultTranscript" && key !== "jobDurationMs"
|
|
2576
|
+
);
|
|
2577
|
+
if (unknown) return adminError3(400, `unknown setting ${unknown}`);
|
|
2578
|
+
return json3(200, runtime.instance(namespace).state.update(patch));
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2581
|
+
};
|
|
2582
|
+
var createRuntime2 = (options = {}) => createRuntime({
|
|
2583
|
+
name: SPEECH_NAMESPACE,
|
|
2584
|
+
document,
|
|
2585
|
+
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2586
|
+
...options.clock ? { clock: options.clock } : {},
|
|
2587
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
2588
|
+
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
2589
|
+
...options.onLog ? { onLog: options.onLog } : {},
|
|
2590
|
+
credential: accessKeyCredential,
|
|
2591
|
+
presets: SPEECH_PRESETS,
|
|
2592
|
+
create: ({ sqlite, namespace, clock }) => new SpeechAPI({
|
|
2593
|
+
sqlite,
|
|
2594
|
+
namespace,
|
|
2595
|
+
now: clock.now,
|
|
2596
|
+
...options.settings ? { settings: options.settings } : {},
|
|
2597
|
+
...options.transcripts ? { transcripts: options.transcripts } : {},
|
|
2598
|
+
...options.transcriptStore ? { transcriptStore: options.transcriptStore } : {}
|
|
2599
|
+
}),
|
|
2600
|
+
describe: () => ({
|
|
2601
|
+
transcripts: options.transcripts?.length ?? 0,
|
|
2602
|
+
transcriptStore: options.transcriptStore ? "on" : "off"
|
|
2603
|
+
}),
|
|
2604
|
+
admin: adminRoutes
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
// src/index.ts
|
|
2608
|
+
var SPEECH_NAMESPACE = "aws-speech";
|
|
2609
|
+
var POLLY_VOICES = [
|
|
2610
|
+
"Aditi",
|
|
2611
|
+
"Adriano",
|
|
2612
|
+
"Ambre",
|
|
2613
|
+
"Amy",
|
|
2614
|
+
"Andres",
|
|
2615
|
+
"Aria",
|
|
2616
|
+
"Arlet",
|
|
2617
|
+
"Arthur",
|
|
2618
|
+
"Astrid",
|
|
2619
|
+
"Ayanda",
|
|
2620
|
+
"Beatrice",
|
|
2621
|
+
"Bianca",
|
|
2622
|
+
"Brian",
|
|
2623
|
+
"Burcu",
|
|
2624
|
+
"Camila",
|
|
2625
|
+
"Carla",
|
|
2626
|
+
"Carmen",
|
|
2627
|
+
"Celine",
|
|
2628
|
+
"Chantal",
|
|
2629
|
+
"Conchita",
|
|
2630
|
+
"Cristiano",
|
|
2631
|
+
"Daniel",
|
|
2632
|
+
"Danielle",
|
|
2633
|
+
"Dora",
|
|
2634
|
+
"Elin",
|
|
2635
|
+
"Emma",
|
|
2636
|
+
"Enrique",
|
|
2637
|
+
"Ewa",
|
|
2638
|
+
"Filiz",
|
|
2639
|
+
"Florian",
|
|
2640
|
+
"Gabrielle",
|
|
2641
|
+
"Geraint",
|
|
2642
|
+
"Giorgio",
|
|
2643
|
+
"Gregory",
|
|
2644
|
+
"Gwyneth",
|
|
2645
|
+
"Hala",
|
|
2646
|
+
"Hannah",
|
|
2647
|
+
"Hans",
|
|
2648
|
+
"Hiujin",
|
|
2649
|
+
"Ida",
|
|
2650
|
+
"Ines",
|
|
2651
|
+
"Isabelle",
|
|
2652
|
+
"Ivy",
|
|
2653
|
+
"Jacek",
|
|
2654
|
+
"Jan",
|
|
2655
|
+
"Jasmine",
|
|
2656
|
+
"Jihye",
|
|
2657
|
+
"Jitka",
|
|
2658
|
+
"Joanna",
|
|
2659
|
+
"Joey",
|
|
2660
|
+
"Justin",
|
|
2661
|
+
"Kajal",
|
|
2662
|
+
"Karl",
|
|
2663
|
+
"Kazuha",
|
|
2664
|
+
"Kendra",
|
|
2665
|
+
"Kevin",
|
|
2666
|
+
"Kimberly",
|
|
2667
|
+
"Laura",
|
|
2668
|
+
"Lea",
|
|
2669
|
+
"Lennart",
|
|
2670
|
+
"Liam",
|
|
2671
|
+
"Lisa",
|
|
2672
|
+
"Liv",
|
|
2673
|
+
"Lorenzo",
|
|
2674
|
+
"Lotte",
|
|
2675
|
+
"Lucia",
|
|
2676
|
+
"Lupe",
|
|
2677
|
+
"Mads",
|
|
2678
|
+
"Maja",
|
|
2679
|
+
"Marlene",
|
|
2680
|
+
"Mathieu",
|
|
2681
|
+
"Matthew",
|
|
2682
|
+
"Maxim",
|
|
2683
|
+
"Mia",
|
|
2684
|
+
"Miguel",
|
|
2685
|
+
"Mizuki",
|
|
2686
|
+
"Naja",
|
|
2687
|
+
"Niamh",
|
|
2688
|
+
"Nicole",
|
|
2689
|
+
"Ola",
|
|
2690
|
+
"Olivia",
|
|
2691
|
+
"Pedro",
|
|
2692
|
+
"Penelope",
|
|
2693
|
+
"Raveena",
|
|
2694
|
+
"Remi",
|
|
2695
|
+
"Ricardo",
|
|
2696
|
+
"Ruben",
|
|
2697
|
+
"Russell",
|
|
2698
|
+
"Ruth",
|
|
2699
|
+
"Sabrina",
|
|
2700
|
+
"Salli",
|
|
2701
|
+
"Seoyeon",
|
|
2702
|
+
"Sergio",
|
|
2703
|
+
"Sofie",
|
|
2704
|
+
"Stephen",
|
|
2705
|
+
"Suvi",
|
|
2706
|
+
"Takumi",
|
|
2707
|
+
"Tatyana",
|
|
2708
|
+
"Thiago",
|
|
2709
|
+
"Tiffany",
|
|
2710
|
+
"Tomoko",
|
|
2711
|
+
"Vicki",
|
|
2712
|
+
"Vitoria",
|
|
2713
|
+
"Zayd",
|
|
2714
|
+
"Zeina",
|
|
2715
|
+
"Zhiyu"
|
|
2716
|
+
];
|
|
2717
|
+
var ENGINES = ["standard", "neural", "long-form", "generative"];
|
|
2718
|
+
var MAX_BILLED_CHARACTERS = 3e3;
|
|
2719
|
+
var FAULT_ERRORS = {
|
|
2720
|
+
polly_throttling: [429, "ThrottlingException", "Rate exceeded."],
|
|
2721
|
+
polly_service_failure: [
|
|
2722
|
+
500,
|
|
2723
|
+
"ServiceFailureException",
|
|
2724
|
+
"An unknown condition has caused a service failure."
|
|
2725
|
+
],
|
|
2726
|
+
transcribe_bad_request: [400, "BadRequestException", "Your request has an invalid parameter."],
|
|
2727
|
+
transcribe_limit_exceeded: [
|
|
2728
|
+
429,
|
|
2729
|
+
"LimitExceededException",
|
|
2730
|
+
"You have reached your concurrent stream limit."
|
|
2731
|
+
],
|
|
2732
|
+
transcribe_service_unavailable: [
|
|
2733
|
+
503,
|
|
2734
|
+
"ServiceUnavailableException",
|
|
2735
|
+
"The service is currently unavailable."
|
|
2736
|
+
]
|
|
2737
|
+
};
|
|
2738
|
+
var speechError = (status, type, message, requestId) => new Response(JSON.stringify({ message }), {
|
|
2739
|
+
status,
|
|
2740
|
+
headers: {
|
|
2741
|
+
"content-type": "application/json",
|
|
2742
|
+
"x-amzn-errortype": type,
|
|
2743
|
+
...requestId ? { "x-amzn-requestid": requestId } : {}
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
var jsonRpcError = (status, type, message) => new Response(JSON.stringify({ __type: type, Message: message }), {
|
|
2747
|
+
status,
|
|
2748
|
+
headers: { "content-type": "application/x-amz-json-1.1", "x-amzn-errortype": type }
|
|
2749
|
+
});
|
|
2750
|
+
var fault = (request) => {
|
|
2751
|
+
const params = faultEffect(request, "speech_fault");
|
|
2752
|
+
return params && typeof params.type === "string" ? params : void 0;
|
|
2753
|
+
};
|
|
2754
|
+
var regionOf = (request) => /Credential=[^/]+\/\d{8}\/([^/]+)\//.exec(request.headers.get("authorization") ?? "")?.[1] ?? "us-east-1";
|
|
2755
|
+
var accessKeyCredential = sigV4AccessKeyId;
|
|
2756
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2757
|
+
var utf83 = new TextDecoder();
|
|
2758
|
+
var AUDIO_EVENT_BYTES = 16384;
|
|
2759
|
+
var SpeechAPI = class {
|
|
2760
|
+
app;
|
|
2761
|
+
sqlite;
|
|
2762
|
+
state;
|
|
2763
|
+
service;
|
|
2764
|
+
now;
|
|
2765
|
+
transcriptStore;
|
|
2766
|
+
requests = 0;
|
|
2767
|
+
constructor(options = {}) {
|
|
2768
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
2769
|
+
const namespace = options.namespace ?? SPEECH_NAMESPACE;
|
|
2770
|
+
this.now = options.now ?? (() => Date.now());
|
|
2771
|
+
this.transcriptStore = options.transcriptStore;
|
|
2772
|
+
this.state = new SpeechState(sqlite, namespace, {
|
|
2773
|
+
settings: options.settings ?? {},
|
|
2774
|
+
transcripts: options.transcripts ?? []
|
|
2775
|
+
});
|
|
2776
|
+
const handlers = defineOperations({
|
|
2777
|
+
SynthesizeSpeech: (context) => this.synthesize(context),
|
|
2778
|
+
// The duplex operations are served by `fetch` before routing (bodies never buffered).
|
|
2779
|
+
StartSpeechSynthesisStream: (context) => this.synthesisStream(context.request),
|
|
2780
|
+
StartStreamTranscription: (context) => this.streamTranscription(context.request),
|
|
2781
|
+
TranscribeJsonRpc: (context) => this.jsonRpc(context)
|
|
2782
|
+
});
|
|
2783
|
+
this.service = createService({
|
|
2784
|
+
document,
|
|
2785
|
+
handlers,
|
|
2786
|
+
sqlite,
|
|
2787
|
+
namespace,
|
|
2788
|
+
now: this.now,
|
|
2789
|
+
notFound: (request) => speechError(
|
|
2790
|
+
404,
|
|
2791
|
+
"UnknownOperationException",
|
|
2792
|
+
`No operation matches ${request.method} ${new URL(request.url).pathname}`
|
|
2793
|
+
),
|
|
2794
|
+
onError: (error) => {
|
|
2795
|
+
if (error instanceof HttpError) return error.toResponse();
|
|
2796
|
+
throw error;
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
this.app = this.service.app;
|
|
2800
|
+
this.sqlite = this.service.sqlite;
|
|
2801
|
+
}
|
|
2802
|
+
fetch(request) {
|
|
2803
|
+
const path = new URL(request.url).pathname;
|
|
2804
|
+
if (request.method === "POST" && path === "/v1/synthesisStream")
|
|
2805
|
+
return this.synthesisStream(request);
|
|
2806
|
+
if (request.method === "POST" && path === "/stream-transcription")
|
|
2807
|
+
return this.streamTranscription(request);
|
|
2808
|
+
return this.service.fetch(request);
|
|
2809
|
+
}
|
|
2810
|
+
async reset() {
|
|
2811
|
+
await this.service.reset();
|
|
2812
|
+
this.state.ensureSeeded();
|
|
2813
|
+
}
|
|
2814
|
+
/** Metadata of every synthesis and transcription so far (never text). */
|
|
2815
|
+
speechLog() {
|
|
2816
|
+
return this.state.log.list({ order: "oldest" }).map((row) => row.value);
|
|
2817
|
+
}
|
|
2818
|
+
jobs() {
|
|
2819
|
+
return this.state.jobs.list({ order: "oldest" }).map((row) => this.advance(row.value));
|
|
2820
|
+
}
|
|
2821
|
+
stats() {
|
|
2822
|
+
return this.state.currentStats();
|
|
2823
|
+
}
|
|
2824
|
+
requestId() {
|
|
2825
|
+
const hex = opaqueToken(`speech:${this.requests++}:${this.now()}`, 32).split("").map((c) => (c.charCodeAt(0) % 16).toString(16)).join("");
|
|
2826
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
2827
|
+
}
|
|
2828
|
+
// ── Polly ───────────────────────────────────────────────────────
|
|
2829
|
+
/** Shared checks for both Polly operations; a string is the error. */
|
|
2830
|
+
checkVoice(input) {
|
|
2831
|
+
const { voiceId, engine = "standard", outputFormat } = input;
|
|
2832
|
+
if (typeof voiceId !== "string" || !POLLY_VOICES.includes(voiceId)) {
|
|
2833
|
+
return speechError(
|
|
2834
|
+
400,
|
|
2835
|
+
"ValidationException",
|
|
2836
|
+
`1 validation error detected: Value '${String(voiceId)}' at 'voiceId' failed to satisfy constraint: Member must satisfy enum value set`
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
if (typeof engine !== "string" || !ENGINES.includes(engine)) {
|
|
2840
|
+
return speechError(
|
|
2841
|
+
400,
|
|
2842
|
+
"ValidationException",
|
|
2843
|
+
`1 validation error detected: Value '${String(engine)}' at 'engine' failed to satisfy constraint: Member must satisfy enum value set: [standard, neural, long-form, generative]`
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
if (outputFormat !== "pcm" && outputFormat !== "mp3") {
|
|
2847
|
+
return speechError(
|
|
2848
|
+
400,
|
|
2849
|
+
"ValidationException",
|
|
2850
|
+
`OutputFormat ${String(outputFormat)} is not modelled by the Mockingbird mock (pcm and mp3 are).`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const fallbackRate = outputFormat === "pcm" ? 16e3 : engine === "standard" ? 22050 : 24e3;
|
|
2854
|
+
const sampleRate = input.sampleRate === void 0 ? fallbackRate : Number(input.sampleRate);
|
|
2855
|
+
const allowed = outputFormat === "pcm" ? [8e3, 16e3] : MP3_SAMPLE_RATES.filter((r) => r >= 8e3 && r !== 11025 && r !== 12e3);
|
|
2856
|
+
if (!allowed.includes(sampleRate)) {
|
|
2857
|
+
return speechError(
|
|
2858
|
+
400,
|
|
2859
|
+
"InvalidSampleRateException",
|
|
2860
|
+
`The specified sample rate ${String(input.sampleRate)} is not valid for ${outputFormat}.`
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
return { voiceId, engine, outputFormat, sampleRate };
|
|
2864
|
+
}
|
|
2865
|
+
audio(text2, outputFormat, sampleRate) {
|
|
2866
|
+
const ms = durationFor(text2);
|
|
2867
|
+
return outputFormat === "pcm" ? pcmTone(ms, sampleRate) : mp3Audio(ms, sampleRate);
|
|
2868
|
+
}
|
|
2869
|
+
async synthesize(context) {
|
|
2870
|
+
const requestId = this.requestId();
|
|
2871
|
+
const body = context.body.kind === "json" ? context.body.value : void 0;
|
|
2872
|
+
if (!isRecord5(body) || typeof body.Text !== "string" || body.Text.length === 0) {
|
|
2873
|
+
return speechError(
|
|
2874
|
+
400,
|
|
2875
|
+
"ValidationException",
|
|
2876
|
+
"1 validation error detected: Value null at 'text' failed to satisfy constraint: Member must not be null",
|
|
2877
|
+
requestId
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
const injected = fault(context.request);
|
|
2881
|
+
const known = injected ? FAULT_ERRORS[injected.type] : void 0;
|
|
2882
|
+
if (known && injected?.type.startsWith("polly_")) {
|
|
2883
|
+
return speechError(known[0], known[1], injected?.message ?? known[2], requestId);
|
|
2884
|
+
}
|
|
2885
|
+
const voice = this.checkVoice({
|
|
2886
|
+
voiceId: body.VoiceId,
|
|
2887
|
+
engine: body.Engine,
|
|
2888
|
+
outputFormat: body.OutputFormat,
|
|
2889
|
+
sampleRate: body.SampleRate
|
|
2890
|
+
});
|
|
2891
|
+
if (voice instanceof Response) return voice;
|
|
2892
|
+
const text2 = body.TextType === "ssml" ? body.Text.replace(/<[^>]*>/g, "") : body.Text;
|
|
2893
|
+
if (text2.length > MAX_BILLED_CHARACTERS) {
|
|
2894
|
+
return speechError(
|
|
2895
|
+
400,
|
|
2896
|
+
"TextLengthExceededException",
|
|
2897
|
+
`Maximum text length has been exceeded (${MAX_BILLED_CHARACTERS} billed characters).`,
|
|
2898
|
+
requestId
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
const audio = this.audio(text2, voice.outputFormat, voice.sampleRate);
|
|
2902
|
+
this.state.record({
|
|
2903
|
+
operation: "SynthesizeSpeech",
|
|
2904
|
+
voiceId: voice.voiceId,
|
|
2905
|
+
engine: voice.engine,
|
|
2906
|
+
outputFormat: voice.outputFormat,
|
|
2907
|
+
sampleRate: String(voice.sampleRate),
|
|
2908
|
+
characters: text2.length,
|
|
2909
|
+
audioBytes: audio.length
|
|
2910
|
+
});
|
|
2911
|
+
return annotateResponse(
|
|
2912
|
+
new Response(audio, {
|
|
2913
|
+
status: 200,
|
|
2914
|
+
headers: {
|
|
2915
|
+
"content-type": voice.outputFormat === "pcm" ? "audio/pcm" : "audio/mpeg",
|
|
2916
|
+
"x-amzn-requestcharacters": String(text2.length),
|
|
2917
|
+
"x-amzn-requestid": requestId
|
|
2918
|
+
}
|
|
2919
|
+
}),
|
|
2920
|
+
{
|
|
2921
|
+
ids: {
|
|
2922
|
+
voiceId: voice.voiceId,
|
|
2923
|
+
engine: voice.engine,
|
|
2924
|
+
outputFormat: voice.outputFormat,
|
|
2925
|
+
characters: String(text2.length)
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
async synthesisStream(request) {
|
|
2931
|
+
const requestId = this.requestId();
|
|
2932
|
+
const headers = request.headers;
|
|
2933
|
+
const injected = fault(request);
|
|
2934
|
+
const known = injected ? FAULT_ERRORS[injected.type] : void 0;
|
|
2935
|
+
if (known && injected?.type.startsWith("polly_")) {
|
|
2936
|
+
return speechError(known[0], known[1], injected?.message ?? known[2], requestId);
|
|
2937
|
+
}
|
|
2938
|
+
const voice = this.checkVoice({
|
|
2939
|
+
voiceId: headers.get("x-amzn-voiceid") ?? void 0,
|
|
2940
|
+
engine: headers.get("x-amzn-engine") ?? void 0,
|
|
2941
|
+
outputFormat: headers.get("x-amzn-outputformat") ?? void 0,
|
|
2942
|
+
sampleRate: headers.get("x-amzn-samplerate") ?? void 0
|
|
2943
|
+
});
|
|
2944
|
+
if (voice instanceof Response) return voice;
|
|
2945
|
+
if (voice.engine !== "generative") {
|
|
2946
|
+
return speechError(
|
|
2947
|
+
400,
|
|
2948
|
+
"ValidationException",
|
|
2949
|
+
"StartSpeechSynthesisStream supports only the generative engine.",
|
|
2950
|
+
requestId
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
const streamFault = injected?.type.startsWith("polly_stream_") ? injected : void 0;
|
|
2954
|
+
let characters = 0;
|
|
2955
|
+
let audioBytes = 0;
|
|
2956
|
+
const body = new ReadableStream({
|
|
2957
|
+
start: (controller) => {
|
|
2958
|
+
void (async () => {
|
|
2959
|
+
const exception = (type, message) => controller.enqueue(exceptionFrame(type, { message }));
|
|
2960
|
+
const failure = {
|
|
2961
|
+
polly_stream_throttling: ["ThrottlingException", "Rate exceeded."],
|
|
2962
|
+
polly_stream_validation: [
|
|
2963
|
+
"ValidationException",
|
|
2964
|
+
"The input fails to satisfy the constraints."
|
|
2965
|
+
],
|
|
2966
|
+
polly_stream_quota: ["ServiceQuotaExceededException", "Service quota exceeded."],
|
|
2967
|
+
polly_stream_failure: [
|
|
2968
|
+
"ServiceFailureException",
|
|
2969
|
+
"An unknown condition has caused a service failure."
|
|
2970
|
+
]
|
|
2971
|
+
};
|
|
2972
|
+
const planned = streamFault ? failure[streamFault.type] : void 0;
|
|
2973
|
+
let events = 0;
|
|
2974
|
+
try {
|
|
2975
|
+
for await (const raw of readFrames(request.body)) {
|
|
2976
|
+
const frame = unwrapSigned(raw);
|
|
2977
|
+
if (frame === null) break;
|
|
2978
|
+
const type = headerString(frame, ":event-type");
|
|
2979
|
+
if (type === "CloseStreamEvent") break;
|
|
2980
|
+
if (type !== "TextEvent") continue;
|
|
2981
|
+
const payload = payloadJson(frame);
|
|
2982
|
+
const text2 = isRecord5(payload) && typeof payload.Text === "string" ? payload.Text : "";
|
|
2983
|
+
if (planned && events >= (streamFault?.afterEvents ?? 0)) {
|
|
2984
|
+
exception(planned[0], streamFault?.message ?? planned[1]);
|
|
2985
|
+
controller.close();
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
characters += text2.length;
|
|
2989
|
+
const audio = this.audio(text2, voice.outputFormat, voice.sampleRate);
|
|
2990
|
+
audioBytes += audio.length;
|
|
2991
|
+
for (let at = 0; at < audio.length; at += AUDIO_EVENT_BYTES) {
|
|
2992
|
+
controller.enqueue(
|
|
2993
|
+
eventFrame("AudioEvent", audio.subarray(at, at + AUDIO_EVENT_BYTES))
|
|
2994
|
+
);
|
|
2995
|
+
events++;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
if (planned) exception(planned[0], streamFault?.message ?? planned[1]);
|
|
2999
|
+
else if (streamFault?.type !== "polly_stream_no_close") {
|
|
3000
|
+
controller.enqueue(eventFrame("StreamClosedEvent", { RequestCharacters: characters }));
|
|
3001
|
+
}
|
|
3002
|
+
} catch {
|
|
3003
|
+
}
|
|
3004
|
+
this.state.record({
|
|
3005
|
+
operation: "StartSpeechSynthesisStream",
|
|
3006
|
+
voiceId: voice.voiceId,
|
|
3007
|
+
engine: voice.engine,
|
|
3008
|
+
outputFormat: voice.outputFormat,
|
|
3009
|
+
sampleRate: String(voice.sampleRate),
|
|
3010
|
+
characters,
|
|
3011
|
+
audioBytes
|
|
3012
|
+
});
|
|
3013
|
+
try {
|
|
3014
|
+
controller.close();
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
3017
|
+
})();
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
return annotateResponse(
|
|
3021
|
+
new Response(body, {
|
|
3022
|
+
status: 200,
|
|
3023
|
+
headers: {
|
|
3024
|
+
"content-type": "application/vnd.amazon.eventstream",
|
|
3025
|
+
"x-amzn-requestid": requestId
|
|
3026
|
+
}
|
|
3027
|
+
}),
|
|
3028
|
+
{ ids: { voiceId: voice.voiceId, engine: voice.engine, outputFormat: voice.outputFormat } }
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
// ── Transcribe streaming ────────────────────────────────────────
|
|
3032
|
+
async streamTranscription(request) {
|
|
3033
|
+
const requestId = this.requestId();
|
|
3034
|
+
const headers = request.headers;
|
|
3035
|
+
const injected = fault(request);
|
|
3036
|
+
const known = injected ? FAULT_ERRORS[injected.type] : void 0;
|
|
3037
|
+
if (known && injected?.type.startsWith("transcribe_")) {
|
|
3038
|
+
return speechError(known[0], known[1], injected?.message ?? known[2], requestId);
|
|
3039
|
+
}
|
|
3040
|
+
const language = headers.get("x-amzn-transcribe-language-code");
|
|
3041
|
+
const identify = headers.get("x-amzn-transcribe-identify-language") === "true";
|
|
3042
|
+
const encoding = headers.get("x-amzn-transcribe-media-encoding");
|
|
3043
|
+
const rate = Number(headers.get("x-amzn-transcribe-sample-rate"));
|
|
3044
|
+
if (!identify && (!language || !/^[a-z]{2}-[A-Z]{2}$/.test(language))) {
|
|
3045
|
+
return speechError(
|
|
3046
|
+
400,
|
|
3047
|
+
"BadRequestException",
|
|
3048
|
+
"A language code is required unless IdentifyLanguage is set.",
|
|
3049
|
+
requestId
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
if (!encoding || !["pcm", "ogg-opus", "flac"].includes(encoding)) {
|
|
3053
|
+
return speechError(
|
|
3054
|
+
400,
|
|
3055
|
+
"BadRequestException",
|
|
3056
|
+
`1 validation error detected: Value '${String(encoding)}' at 'mediaEncoding' failed to satisfy constraint: Member must satisfy enum value set: [ogg-opus, flac, pcm]`,
|
|
3057
|
+
requestId
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
if (!Number.isInteger(rate) || rate < 8e3 || rate > 48e3) {
|
|
3061
|
+
return speechError(
|
|
3062
|
+
400,
|
|
3063
|
+
"BadRequestException",
|
|
3064
|
+
"1 validation error detected: Value at 'mediaSampleRateHertz' failed to satisfy constraint: Member must have value between 8000 and 48000",
|
|
3065
|
+
requestId
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
const sessionIndex = this.state.nextSessionIndex();
|
|
3069
|
+
const script = this.state.pick({ sessionIndex });
|
|
3070
|
+
const partials = script?.partials ?? [];
|
|
3071
|
+
const final = script?.final ?? this.state.current().defaultTranscript;
|
|
3072
|
+
const sessionId = headers.get("x-amzn-transcribe-session-id") ?? this.requestId();
|
|
3073
|
+
const midStream = injected?.type === "transcribe_mid_stream_failure" ? injected : void 0;
|
|
3074
|
+
let chunks = 0;
|
|
3075
|
+
let audioBytes = 0;
|
|
3076
|
+
let sent = 0;
|
|
3077
|
+
let resultIndex = 0;
|
|
3078
|
+
const result = (transcript, isPartial) => {
|
|
3079
|
+
const end = Math.max(0.1, Math.round(audioBytes / 2 / rate * 1e3) / 1e3);
|
|
3080
|
+
return eventFrame("TranscriptEvent", {
|
|
3081
|
+
Transcript: {
|
|
3082
|
+
Results: [
|
|
3083
|
+
{
|
|
3084
|
+
ResultId: `${sessionId}-${resultIndex++}`,
|
|
3085
|
+
StartTime: 0,
|
|
3086
|
+
EndTime: end,
|
|
3087
|
+
IsPartial: isPartial,
|
|
3088
|
+
Alternatives: [{ Transcript: transcript, Items: [] }],
|
|
3089
|
+
...headers.get("x-amzn-transcribe-enable-channel-identification") === "true" ? { ChannelId: "ch_0" } : {}
|
|
3090
|
+
}
|
|
3091
|
+
]
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
};
|
|
3095
|
+
const body = new ReadableStream({
|
|
3096
|
+
start: (controller) => {
|
|
3097
|
+
void (async () => {
|
|
3098
|
+
let emitted = 0;
|
|
3099
|
+
const emit = (frame) => {
|
|
3100
|
+
if (midStream && emitted >= (midStream.afterEvents ?? 1)) {
|
|
3101
|
+
controller.enqueue(
|
|
3102
|
+
exceptionFrame("InternalFailureException", {
|
|
3103
|
+
Message: midStream.message ?? "A problem occurred while processing the audio."
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
controller.enqueue(frame);
|
|
3109
|
+
emitted++;
|
|
3110
|
+
return true;
|
|
3111
|
+
};
|
|
3112
|
+
try {
|
|
3113
|
+
for await (const raw of readFrames(request.body)) {
|
|
3114
|
+
const frame = unwrapSigned(raw);
|
|
3115
|
+
if (frame === null) break;
|
|
3116
|
+
if (headerString(frame, ":event-type") !== "AudioEvent") continue;
|
|
3117
|
+
chunks++;
|
|
3118
|
+
audioBytes += frame.body.length;
|
|
3119
|
+
if (frame.body.length === 0) break;
|
|
3120
|
+
if (sent < partials.length && !emit(result(partials[sent++], true))) {
|
|
3121
|
+
controller.close();
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
while (sent < partials.length) {
|
|
3126
|
+
if (!emit(result(partials[sent++], true))) {
|
|
3127
|
+
controller.close();
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (final.length > 0 && !emit(result(final, false))) {
|
|
3132
|
+
controller.close();
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
} catch {
|
|
3136
|
+
}
|
|
3137
|
+
this.state.record({
|
|
3138
|
+
operation: "StartStreamTranscription",
|
|
3139
|
+
audioBytes,
|
|
3140
|
+
chunks,
|
|
3141
|
+
...script ? { script: script.id } : {}
|
|
3142
|
+
});
|
|
3143
|
+
try {
|
|
3144
|
+
controller.close();
|
|
3145
|
+
} catch {
|
|
3146
|
+
}
|
|
3147
|
+
})();
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
const echo = (name) => {
|
|
3151
|
+
const value = headers.get(name);
|
|
3152
|
+
return value === null ? {} : { [name]: value };
|
|
3153
|
+
};
|
|
3154
|
+
return annotateResponse(
|
|
3155
|
+
new Response(body, {
|
|
3156
|
+
status: 200,
|
|
3157
|
+
headers: {
|
|
3158
|
+
"content-type": "application/vnd.amazon.eventstream",
|
|
3159
|
+
"x-amzn-request-id": requestId,
|
|
3160
|
+
"x-amzn-transcribe-session-id": sessionId,
|
|
3161
|
+
...echo("x-amzn-transcribe-language-code"),
|
|
3162
|
+
...echo("x-amzn-transcribe-sample-rate"),
|
|
3163
|
+
...echo("x-amzn-transcribe-media-encoding"),
|
|
3164
|
+
...echo("x-amzn-transcribe-vocabulary-name"),
|
|
3165
|
+
...echo("x-amzn-transcribe-enable-partial-results-stabilization"),
|
|
3166
|
+
...echo("x-amzn-transcribe-partial-results-stability")
|
|
3167
|
+
}
|
|
3168
|
+
}),
|
|
3169
|
+
{ ids: { sessionIndex: String(sessionIndex), transcript: script?.id ?? "unscripted" } }
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
// ── Transcribe batch (AWS JSON 1.1) ─────────────────────────────
|
|
3173
|
+
/** Move a job along the mock clock: IN_PROGRESS → COMPLETED after `jobDurationMs`. */
|
|
3174
|
+
advance(job) {
|
|
3175
|
+
if (job.TranscriptionJobStatus !== "IN_PROGRESS") return job;
|
|
3176
|
+
if (this.now() < job.createdAtMs + this.state.current().jobDurationMs) return job;
|
|
3177
|
+
return this.complete(job.TranscriptionJobName, void 0) ?? job;
|
|
3178
|
+
}
|
|
3179
|
+
/** Complete a job (admin or clock), fixing its transcript; writes it to S3 when configured. */
|
|
3180
|
+
complete(name, transcript) {
|
|
3181
|
+
const job = this.state.jobs.get(name);
|
|
3182
|
+
if (!job) return void 0;
|
|
3183
|
+
const script = transcript === void 0 ? this.state.pick({ jobName: name }) : void 0;
|
|
3184
|
+
const next = {
|
|
3185
|
+
...job,
|
|
3186
|
+
TranscriptionJobStatus: "COMPLETED",
|
|
3187
|
+
completedAtMs: this.now(),
|
|
3188
|
+
transcript: transcript ?? script?.final ?? this.state.current().defaultTranscript
|
|
3189
|
+
};
|
|
3190
|
+
this.state.jobs.update(name, next);
|
|
3191
|
+
if (this.transcriptStore && next.OutputBucketName) {
|
|
3192
|
+
void putObject(
|
|
3193
|
+
{ ...this.transcriptStore, bucket: next.OutputBucketName },
|
|
3194
|
+
this.outputKey(next),
|
|
3195
|
+
JSON.stringify(this.transcriptDocument(next)),
|
|
3196
|
+
"application/json"
|
|
3197
|
+
).catch(() => void 0);
|
|
3198
|
+
}
|
|
3199
|
+
return next;
|
|
3200
|
+
}
|
|
3201
|
+
/** Fail a job with a reason (admin, or the `transcribe_job_failed` preset). */
|
|
3202
|
+
fail(name, reason) {
|
|
3203
|
+
const job = this.state.jobs.get(name);
|
|
3204
|
+
if (!job) return void 0;
|
|
3205
|
+
const next = {
|
|
3206
|
+
...job,
|
|
3207
|
+
TranscriptionJobStatus: "FAILED",
|
|
3208
|
+
completedAtMs: this.now(),
|
|
3209
|
+
failureReason: reason
|
|
3210
|
+
};
|
|
3211
|
+
this.state.jobs.update(name, next);
|
|
3212
|
+
return next;
|
|
3213
|
+
}
|
|
3214
|
+
outputKey(job) {
|
|
3215
|
+
return job.OutputKey ?? `${job.TranscriptionJobName}.json`;
|
|
3216
|
+
}
|
|
3217
|
+
/** The transcript JSON Transcribe writes to S3 (the shape our formatter reads). */
|
|
3218
|
+
transcriptDocument(job) {
|
|
3219
|
+
const words = (job.transcript ?? "").split(/\s+/).filter(Boolean);
|
|
3220
|
+
return {
|
|
3221
|
+
jobName: job.TranscriptionJobName,
|
|
3222
|
+
accountId: "123456789012",
|
|
3223
|
+
status: "COMPLETED",
|
|
3224
|
+
results: {
|
|
3225
|
+
transcripts: [{ transcript: job.transcript ?? "" }],
|
|
3226
|
+
...job.Settings?.ChannelIdentification === true ? {
|
|
3227
|
+
channel_labels: {
|
|
3228
|
+
channels: [{ channel_label: "ch_0", items: [] }],
|
|
3229
|
+
number_of_channels: 1
|
|
3230
|
+
}
|
|
3231
|
+
} : {},
|
|
3232
|
+
items: words.map((word, i) => ({
|
|
3233
|
+
start_time: (i * 0.5).toFixed(3),
|
|
3234
|
+
end_time: (i * 0.5 + 0.4).toFixed(3),
|
|
3235
|
+
alternatives: [{ confidence: "0.99", content: word.replace(/[.,!?]$/, "") }],
|
|
3236
|
+
type: "pronunciation"
|
|
3237
|
+
}))
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
jobBody(job) {
|
|
3242
|
+
const seconds = (ms) => ms / 1e3;
|
|
3243
|
+
const uri = job.OutputBucketName !== void 0 ? `https://s3.${job.region}.amazonaws.com/${job.OutputBucketName}/${this.outputKey(job)}` : `https://s3.${job.region}.amazonaws.com/aws-transcribe-${job.region}-prod/123456789012/${job.TranscriptionJobName}/asrOutput.json`;
|
|
3244
|
+
return {
|
|
3245
|
+
TranscriptionJobName: job.TranscriptionJobName,
|
|
3246
|
+
TranscriptionJobStatus: job.TranscriptionJobStatus,
|
|
3247
|
+
LanguageCode: job.LanguageCode,
|
|
3248
|
+
...job.MediaFormat ? { MediaFormat: job.MediaFormat } : {},
|
|
3249
|
+
...job.MediaSampleRateHertz ? { MediaSampleRateHertz: job.MediaSampleRateHertz } : {},
|
|
3250
|
+
Media: job.Media,
|
|
3251
|
+
...job.Settings ? { Settings: job.Settings } : {},
|
|
3252
|
+
CreationTime: seconds(job.createdAtMs),
|
|
3253
|
+
StartTime: seconds(job.createdAtMs),
|
|
3254
|
+
...job.completedAtMs !== void 0 ? { CompletionTime: seconds(job.completedAtMs) } : {},
|
|
3255
|
+
...job.TranscriptionJobStatus === "COMPLETED" ? { Transcript: { TranscriptFileUri: uri } } : {},
|
|
3256
|
+
...job.failureReason ? { FailureReason: job.failureReason } : {}
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
async jsonRpc(context) {
|
|
3260
|
+
const target = context.request.headers.get("x-amz-target") ?? "";
|
|
3261
|
+
let body;
|
|
3262
|
+
try {
|
|
3263
|
+
const bytes = context.body.kind === "bytes" ? context.body.value : context.body.kind === "json" ? void 0 : new Uint8Array(0);
|
|
3264
|
+
body = bytes === void 0 ? context.body.value : JSON.parse(utf83.decode(bytes) || "{}");
|
|
3265
|
+
} catch {
|
|
3266
|
+
return jsonRpcError(400, "SerializationException", "Request body is not valid JSON.");
|
|
3267
|
+
}
|
|
3268
|
+
if (!isRecord5(body))
|
|
3269
|
+
return jsonRpcError(400, "SerializationException", "Request body is not a JSON object.");
|
|
3270
|
+
const name = body.TranscriptionJobName;
|
|
3271
|
+
if (typeof name !== "string" || !/^[0-9a-zA-Z._-]{1,200}$/.test(name)) {
|
|
3272
|
+
return jsonRpcError(
|
|
3273
|
+
400,
|
|
3274
|
+
"BadRequestException",
|
|
3275
|
+
"1 validation error detected: Value at 'transcriptionJobName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[0-9a-zA-Z._-]+"
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
const injected = fault(context.request);
|
|
3279
|
+
const notes2 = (response) => annotateResponse(response, {
|
|
3280
|
+
ids: { target: target.replace(/^Transcribe\./, ""), jobName: name }
|
|
3281
|
+
});
|
|
3282
|
+
switch (target) {
|
|
3283
|
+
case "Transcribe.StartTranscriptionJob": {
|
|
3284
|
+
if (injected?.type === "transcribe_job_limit") {
|
|
3285
|
+
return notes2(
|
|
3286
|
+
jsonRpcError(
|
|
3287
|
+
400,
|
|
3288
|
+
"LimitExceededException",
|
|
3289
|
+
injected.message ?? "You have exceeded the maximum number of concurrent transcription jobs."
|
|
3290
|
+
)
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
if (this.state.jobs.has(name)) {
|
|
3294
|
+
return notes2(
|
|
3295
|
+
jsonRpcError(
|
|
3296
|
+
400,
|
|
3297
|
+
"ConflictException",
|
|
3298
|
+
"The requested job name already exists. Use a different job name."
|
|
3299
|
+
)
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
const media = isRecord5(body.Media) ? body.Media : {};
|
|
3303
|
+
if (typeof media.MediaFileUri !== "string" || !media.MediaFileUri.startsWith("s3://")) {
|
|
3304
|
+
return notes2(
|
|
3305
|
+
jsonRpcError(
|
|
3306
|
+
400,
|
|
3307
|
+
"BadRequestException",
|
|
3308
|
+
"The S3 URI that you specified for the media file isn't valid."
|
|
3309
|
+
)
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
const language = typeof body.LanguageCode === "string" ? body.LanguageCode : void 0;
|
|
3313
|
+
if (!language && body.IdentifyLanguage !== true) {
|
|
3314
|
+
return notes2(
|
|
3315
|
+
jsonRpcError(
|
|
3316
|
+
400,
|
|
3317
|
+
"BadRequestException",
|
|
3318
|
+
"Either LanguageCode or IdentifyLanguage must be specified."
|
|
3319
|
+
)
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
const job = {
|
|
3323
|
+
TranscriptionJobName: name,
|
|
3324
|
+
TranscriptionJobStatus: "IN_PROGRESS",
|
|
3325
|
+
LanguageCode: language ?? "en-US",
|
|
3326
|
+
...typeof body.MediaFormat === "string" ? { MediaFormat: body.MediaFormat } : {},
|
|
3327
|
+
...typeof body.MediaSampleRateHertz === "number" ? { MediaSampleRateHertz: body.MediaSampleRateHertz } : {},
|
|
3328
|
+
Media: { MediaFileUri: media.MediaFileUri },
|
|
3329
|
+
...isRecord5(body.Settings) ? { Settings: body.Settings } : {},
|
|
3330
|
+
...typeof body.OutputBucketName === "string" ? { OutputBucketName: body.OutputBucketName } : {},
|
|
3331
|
+
...typeof body.OutputKey === "string" ? { OutputKey: body.OutputKey } : {},
|
|
3332
|
+
region: regionOf(context.request),
|
|
3333
|
+
createdAtMs: this.now()
|
|
3334
|
+
};
|
|
3335
|
+
this.state.jobs.insert(name, job);
|
|
3336
|
+
if (injected?.type === "transcribe_job_failed") {
|
|
3337
|
+
this.fail(
|
|
3338
|
+
name,
|
|
3339
|
+
injected.message ?? "The media format provided does not match the detected media format."
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
return notes2(
|
|
3343
|
+
jsonRes(
|
|
3344
|
+
200,
|
|
3345
|
+
{ TranscriptionJob: this.jobBody(this.state.jobs.get(name) ?? job) },
|
|
3346
|
+
{ "content-type": "application/x-amz-json-1.1" }
|
|
3347
|
+
)
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
case "Transcribe.GetTranscriptionJob": {
|
|
3351
|
+
const job = this.state.jobs.get(name);
|
|
3352
|
+
if (!job) {
|
|
3353
|
+
return notes2(
|
|
3354
|
+
jsonRpcError(
|
|
3355
|
+
400,
|
|
3356
|
+
"BadRequestException",
|
|
3357
|
+
"The requested job couldn't be found. Check the job name and try your request again."
|
|
3358
|
+
)
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
return notes2(
|
|
3362
|
+
jsonRes(
|
|
3363
|
+
200,
|
|
3364
|
+
{ TranscriptionJob: this.jobBody(this.advance(job)) },
|
|
3365
|
+
{ "content-type": "application/x-amz-json-1.1" }
|
|
3366
|
+
)
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
default:
|
|
3370
|
+
return notes2(
|
|
3371
|
+
jsonRpcError(
|
|
3372
|
+
400,
|
|
3373
|
+
"UnknownOperationException",
|
|
3374
|
+
`The operation ${target || "(none)"} is not modelled by the Mockingbird mock.`
|
|
3375
|
+
)
|
|
3376
|
+
);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
|
|
3381
|
+
export {
|
|
3382
|
+
MS_PER_CHARACTER,
|
|
3383
|
+
durationFor,
|
|
3384
|
+
pcmTone,
|
|
3385
|
+
MP3_SAMPLE_RATES,
|
|
3386
|
+
MP3_SAMPLES_PER_FRAME,
|
|
3387
|
+
mp3Frame,
|
|
3388
|
+
mp3Audio,
|
|
3389
|
+
EventStreamError,
|
|
3390
|
+
crc32,
|
|
3391
|
+
encodeMessage,
|
|
3392
|
+
decodeMessage,
|
|
3393
|
+
FrameReader,
|
|
3394
|
+
unwrapSigned,
|
|
3395
|
+
eventFrame,
|
|
3396
|
+
exceptionFrame,
|
|
3397
|
+
readFrames,
|
|
3398
|
+
document,
|
|
3399
|
+
operationIds,
|
|
3400
|
+
supportedOperationIds,
|
|
3401
|
+
DEFAULT_SETTINGS,
|
|
3402
|
+
SPEECH_PRESETS,
|
|
3403
|
+
createRuntime2 as createRuntime,
|
|
3404
|
+
SPEECH_NAMESPACE,
|
|
3405
|
+
POLLY_VOICES,
|
|
3406
|
+
speechError,
|
|
3407
|
+
accessKeyCredential,
|
|
3408
|
+
SpeechAPI
|
|
3409
|
+
};
|
|
3410
|
+
//# sourceMappingURL=chunk-3DE3INNY.js.map
|