@crvouga/mockingbird-service-intercom 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 +218 -0
- package/dist/chunk-5RRE6WVT.js +364 -0
- package/dist/chunk-5RRE6WVT.js.map +7 -0
- package/dist/chunk-OSBG2XJ7.js +3835 -0
- package/dist/chunk-OSBG2XJ7.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1204 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1510 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +87 -0
|
@@ -0,0 +1,3835 @@
|
|
|
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 bearerToken = (request) => {
|
|
393
|
+
const header = request.headers.get("authorization");
|
|
394
|
+
if (!header)
|
|
395
|
+
return void 0;
|
|
396
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
397
|
+
return match?.[1]?.trim() || void 0;
|
|
398
|
+
};
|
|
399
|
+
var createCredentialRegistry = () => {
|
|
400
|
+
const map = /* @__PURE__ */ new Map();
|
|
401
|
+
return {
|
|
402
|
+
set: (credential, namespace) => {
|
|
403
|
+
map.set(credential, namespace);
|
|
404
|
+
},
|
|
405
|
+
get: (credential) => map.get(credential),
|
|
406
|
+
remove: (credential) => map.delete(credential),
|
|
407
|
+
clear: () => map.clear(),
|
|
408
|
+
entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
|
|
412
|
+
|
|
413
|
+
// ../core/dist/rng.js
|
|
414
|
+
var seedFrom = (value) => {
|
|
415
|
+
let hash = 2166136261;
|
|
416
|
+
for (let i = 0; i < value.length; i++) {
|
|
417
|
+
hash ^= value.charCodeAt(i);
|
|
418
|
+
hash = Math.imul(hash, 16777619);
|
|
419
|
+
}
|
|
420
|
+
return hash >>> 0;
|
|
421
|
+
};
|
|
422
|
+
var createRng = (seed = 0) => {
|
|
423
|
+
const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
|
|
424
|
+
let state = numeric;
|
|
425
|
+
const next = () => {
|
|
426
|
+
state = state + 1831565813 >>> 0;
|
|
427
|
+
let t = state;
|
|
428
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
429
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
430
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
next,
|
|
434
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
435
|
+
reset: () => {
|
|
436
|
+
state = numeric;
|
|
437
|
+
},
|
|
438
|
+
state: () => state,
|
|
439
|
+
setState: (next2) => {
|
|
440
|
+
if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
|
|
441
|
+
throw new RangeError("rng state must be an unsigned 32-bit integer");
|
|
442
|
+
}
|
|
443
|
+
state = next2 >>> 0;
|
|
444
|
+
},
|
|
445
|
+
seed: numeric
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// ../core/dist/faults.js
|
|
450
|
+
var matches = (rule, candidate) => {
|
|
451
|
+
if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
|
|
455
|
+
return false;
|
|
456
|
+
if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
|
|
460
|
+
return false;
|
|
461
|
+
return true;
|
|
462
|
+
};
|
|
463
|
+
var faultResponse = (rule) => {
|
|
464
|
+
const status = rule.status ?? 500;
|
|
465
|
+
const headers = { "content-type": "application/json", ...rule.headers };
|
|
466
|
+
if (typeof rule.body === "string")
|
|
467
|
+
return new Response(rule.body, { status, headers });
|
|
468
|
+
if (rule.body === null)
|
|
469
|
+
return new Response(null, { status, headers: rule.headers ?? {} });
|
|
470
|
+
const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
|
|
471
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
472
|
+
};
|
|
473
|
+
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
474
|
+
const entries = [];
|
|
475
|
+
return {
|
|
476
|
+
add(rule) {
|
|
477
|
+
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
478
|
+
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
479
|
+
if (existing >= 0)
|
|
480
|
+
entries[existing] = entry;
|
|
481
|
+
else
|
|
482
|
+
entries.push(entry);
|
|
483
|
+
return rule;
|
|
484
|
+
},
|
|
485
|
+
list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
|
|
486
|
+
remove(id) {
|
|
487
|
+
const index = entries.findIndex((e) => e.rule.id === id);
|
|
488
|
+
if (index < 0)
|
|
489
|
+
return false;
|
|
490
|
+
entries.splice(index, 1);
|
|
491
|
+
return true;
|
|
492
|
+
},
|
|
493
|
+
clear() {
|
|
494
|
+
entries.length = 0;
|
|
495
|
+
},
|
|
496
|
+
async take(candidate) {
|
|
497
|
+
const hits = [];
|
|
498
|
+
for (const entry of entries) {
|
|
499
|
+
if (entry.remaining === 0)
|
|
500
|
+
continue;
|
|
501
|
+
if (!matches(entry.rule, candidate))
|
|
502
|
+
continue;
|
|
503
|
+
const rate = entry.rule.rate ?? 1;
|
|
504
|
+
if (rng.next() >= rate)
|
|
505
|
+
continue;
|
|
506
|
+
entry.hits++;
|
|
507
|
+
if (entry.remaining !== null)
|
|
508
|
+
entry.remaining--;
|
|
509
|
+
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
510
|
+
if (delay !== void 0 && delay > 0) {
|
|
511
|
+
await sleep(delay);
|
|
512
|
+
}
|
|
513
|
+
const hit = { id: entry.rule.id };
|
|
514
|
+
if (entry.rule.effect !== void 0) {
|
|
515
|
+
hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
|
|
516
|
+
}
|
|
517
|
+
if (entry.rule.drop === true)
|
|
518
|
+
hit.drop = true;
|
|
519
|
+
else if (entry.rule.status !== void 0)
|
|
520
|
+
hit.response = faultResponse(entry.rule);
|
|
521
|
+
hits.push(hit);
|
|
522
|
+
if (hit.drop || hit.response)
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
return hits;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// ../../openapi/core/dist/refs.js
|
|
531
|
+
var OpenAPIReferenceError = class extends Error {
|
|
532
|
+
ref;
|
|
533
|
+
constructor(ref) {
|
|
534
|
+
super(`unresolvable $ref: ${ref}`);
|
|
535
|
+
this.ref = ref;
|
|
536
|
+
this.name = "OpenAPIReferenceError";
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
540
|
+
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
541
|
+
var resolveRef = (document2, ref) => {
|
|
542
|
+
if (!ref.startsWith("#/"))
|
|
543
|
+
throw new OpenAPIReferenceError(ref);
|
|
544
|
+
let cursor = document2;
|
|
545
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
546
|
+
const segment = unescapePointer(raw);
|
|
547
|
+
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
548
|
+
throw new OpenAPIReferenceError(ref);
|
|
549
|
+
}
|
|
550
|
+
cursor = cursor[segment];
|
|
551
|
+
}
|
|
552
|
+
if (cursor === void 0)
|
|
553
|
+
throw new OpenAPIReferenceError(ref);
|
|
554
|
+
return cursor;
|
|
555
|
+
};
|
|
556
|
+
var deref = (document2, value) => {
|
|
557
|
+
let current = value;
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
while (isReference(current)) {
|
|
560
|
+
if (seen.has(current.$ref))
|
|
561
|
+
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
562
|
+
seen.add(current.$ref);
|
|
563
|
+
current = resolveRef(document2, current.$ref);
|
|
564
|
+
}
|
|
565
|
+
return current;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// ../../openapi/core/dist/types.js
|
|
569
|
+
var HTTP_METHODS = [
|
|
570
|
+
"get",
|
|
571
|
+
"put",
|
|
572
|
+
"post",
|
|
573
|
+
"delete",
|
|
574
|
+
"options",
|
|
575
|
+
"head",
|
|
576
|
+
"patch",
|
|
577
|
+
"trace"
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
// ../../openapi/core/dist/document.js
|
|
581
|
+
var mergeParameters = (document2, item, own) => {
|
|
582
|
+
const merged = /* @__PURE__ */ new Map();
|
|
583
|
+
for (const raw of item.parameters ?? []) {
|
|
584
|
+
const parameter = deref(document2, raw);
|
|
585
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
586
|
+
}
|
|
587
|
+
for (const raw of own ?? []) {
|
|
588
|
+
const parameter = deref(document2, raw);
|
|
589
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
590
|
+
}
|
|
591
|
+
return [...merged.values()];
|
|
592
|
+
};
|
|
593
|
+
var listOperations = (document2) => {
|
|
594
|
+
const operations = [];
|
|
595
|
+
for (const [path, item] of Object.entries(document2.paths)) {
|
|
596
|
+
for (const method of HTTP_METHODS) {
|
|
597
|
+
const operation = item[method];
|
|
598
|
+
if (operation?.operationId === void 0)
|
|
599
|
+
continue;
|
|
600
|
+
const responses = {};
|
|
601
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
602
|
+
responses[status] = deref(document2, response);
|
|
603
|
+
}
|
|
604
|
+
operations.push({
|
|
605
|
+
operationId: operation.operationId,
|
|
606
|
+
method,
|
|
607
|
+
path,
|
|
608
|
+
operation,
|
|
609
|
+
parameters: mergeParameters(document2, item, operation.parameters),
|
|
610
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
|
|
611
|
+
responses
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return operations;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// ../../openapi/core/dist/schema.js
|
|
619
|
+
var resolveSchema = (document2, schema) => {
|
|
620
|
+
let current = schema;
|
|
621
|
+
const seen = /* @__PURE__ */ new Set();
|
|
622
|
+
while (typeof current.$ref === "string") {
|
|
623
|
+
const ref = current.$ref;
|
|
624
|
+
if (seen.has(ref))
|
|
625
|
+
break;
|
|
626
|
+
seen.add(ref);
|
|
627
|
+
const { $ref: _ignored, ...siblings } = current;
|
|
628
|
+
const target = resolveRef(document2, ref);
|
|
629
|
+
current = { ...target, ...siblings };
|
|
630
|
+
}
|
|
631
|
+
if (current.nullable === true) {
|
|
632
|
+
const { nullable: _nullable, ...rest } = current;
|
|
633
|
+
const types = schemaTypes(rest);
|
|
634
|
+
if (types.length > 0 && !types.includes("null"))
|
|
635
|
+
current = { ...rest, type: [...types, "null"] };
|
|
636
|
+
else
|
|
637
|
+
current = rest;
|
|
638
|
+
}
|
|
639
|
+
return current;
|
|
640
|
+
};
|
|
641
|
+
var schemaTypes = (schema) => {
|
|
642
|
+
if (Array.isArray(schema.type))
|
|
643
|
+
return schema.type;
|
|
644
|
+
if (schema.type !== void 0)
|
|
645
|
+
return [schema.type];
|
|
646
|
+
const inferred = [];
|
|
647
|
+
if (schema.properties || schema.required || schema.additionalProperties !== void 0)
|
|
648
|
+
inferred.push("object");
|
|
649
|
+
if (schema.items || schema.prefixItems || schema.minItems !== void 0 || schema.maxItems !== void 0)
|
|
650
|
+
inferred.push("array");
|
|
651
|
+
if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0)
|
|
652
|
+
inferred.push("string");
|
|
653
|
+
if (schema.minimum !== void 0 || schema.maximum !== void 0 || schema.multipleOf !== void 0)
|
|
654
|
+
inferred.push("number");
|
|
655
|
+
return inferred;
|
|
656
|
+
};
|
|
657
|
+
var jsonTypeOf = (value) => {
|
|
658
|
+
if (value === null)
|
|
659
|
+
return "null";
|
|
660
|
+
if (Array.isArray(value))
|
|
661
|
+
return "array";
|
|
662
|
+
switch (typeof value) {
|
|
663
|
+
case "string":
|
|
664
|
+
return "string";
|
|
665
|
+
case "boolean":
|
|
666
|
+
return "boolean";
|
|
667
|
+
case "number":
|
|
668
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
669
|
+
case "object":
|
|
670
|
+
return "object";
|
|
671
|
+
default:
|
|
672
|
+
return "undefined";
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
var deepEqual = (a, b) => {
|
|
676
|
+
if (a === b)
|
|
677
|
+
return true;
|
|
678
|
+
if (typeof a !== typeof b || a === null || b === null)
|
|
679
|
+
return false;
|
|
680
|
+
if (Array.isArray(a)) {
|
|
681
|
+
return Array.isArray(b) && a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
682
|
+
}
|
|
683
|
+
if (typeof a === "object" && typeof b === "object" && !Array.isArray(b)) {
|
|
684
|
+
const ka = Object.keys(a);
|
|
685
|
+
const kb = Object.keys(b);
|
|
686
|
+
return ka.length === kb.length && ka.every((k) => deepEqual(a[k], b[k]));
|
|
687
|
+
}
|
|
688
|
+
return false;
|
|
689
|
+
};
|
|
690
|
+
var FORMAT_PATTERNS = {
|
|
691
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
692
|
+
date: /^\d{4}-\d{2}-\d{2}$/,
|
|
693
|
+
"date-time": /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/,
|
|
694
|
+
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
695
|
+
uri: /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/,
|
|
696
|
+
ipv4: /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/
|
|
697
|
+
};
|
|
698
|
+
var graphemeLength = (value) => [...value].length;
|
|
699
|
+
var validateValue = (document2, schema, value, path = []) => {
|
|
700
|
+
const errors = [];
|
|
701
|
+
const s = resolveSchema(document2, schema);
|
|
702
|
+
const fail = (message) => errors.push({ path, message });
|
|
703
|
+
const actual = jsonTypeOf(value);
|
|
704
|
+
if (actual === "undefined") {
|
|
705
|
+
fail("value is undefined");
|
|
706
|
+
return errors;
|
|
707
|
+
}
|
|
708
|
+
const types = schemaTypes(s);
|
|
709
|
+
if (types.length > 0) {
|
|
710
|
+
const ok = types.some((t) => t === actual || t === "number" && actual === "integer");
|
|
711
|
+
if (!ok) {
|
|
712
|
+
fail(`expected type ${types.join("|")}, got ${actual}`);
|
|
713
|
+
return errors;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (s.enum && !(value === null && types.includes("null")) && !s.enum.some((candidate) => deepEqual(candidate, value))) {
|
|
717
|
+
fail("value not in enum");
|
|
718
|
+
}
|
|
719
|
+
if (s.const !== void 0 && !deepEqual(s.const, value))
|
|
720
|
+
fail("value does not equal const");
|
|
721
|
+
if (typeof value === "string") {
|
|
722
|
+
const length = graphemeLength(value);
|
|
723
|
+
if (s.minLength !== void 0 && length < s.minLength)
|
|
724
|
+
fail(`length ${length} < minLength ${s.minLength}`);
|
|
725
|
+
if (s.maxLength !== void 0 && length > s.maxLength)
|
|
726
|
+
fail(`length ${length} > maxLength ${s.maxLength}`);
|
|
727
|
+
if (s.pattern !== void 0) {
|
|
728
|
+
try {
|
|
729
|
+
if (!new RegExp(s.pattern, "u").test(value))
|
|
730
|
+
fail(`does not match pattern ${s.pattern}`);
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (s.format !== void 0) {
|
|
735
|
+
const pattern = FORMAT_PATTERNS[s.format];
|
|
736
|
+
if (pattern && !pattern.test(value))
|
|
737
|
+
fail(`does not match format ${s.format}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (typeof value === "number") {
|
|
741
|
+
if (s.minimum !== void 0 && value < s.minimum)
|
|
742
|
+
fail(`${value} < minimum ${s.minimum}`);
|
|
743
|
+
if (s.maximum !== void 0 && value > s.maximum)
|
|
744
|
+
fail(`${value} > maximum ${s.maximum}`);
|
|
745
|
+
if (s.exclusiveMinimum !== void 0 && value <= s.exclusiveMinimum)
|
|
746
|
+
fail(`${value} <= exclusiveMinimum ${s.exclusiveMinimum}`);
|
|
747
|
+
if (s.exclusiveMaximum !== void 0 && value >= s.exclusiveMaximum)
|
|
748
|
+
fail(`${value} >= exclusiveMaximum ${s.exclusiveMaximum}`);
|
|
749
|
+
if (s.multipleOf !== void 0 && Math.abs(value / s.multipleOf - Math.round(value / s.multipleOf)) > 1e-9) {
|
|
750
|
+
fail(`${value} is not a multiple of ${s.multipleOf}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (Array.isArray(value)) {
|
|
754
|
+
if (s.minItems !== void 0 && value.length < s.minItems)
|
|
755
|
+
fail(`${value.length} items < minItems ${s.minItems}`);
|
|
756
|
+
if (s.maxItems !== void 0 && value.length > s.maxItems)
|
|
757
|
+
fail(`${value.length} items > maxItems ${s.maxItems}`);
|
|
758
|
+
if (s.uniqueItems && value.some((item, i) => value.slice(0, i).some((prev) => deepEqual(prev, item))))
|
|
759
|
+
fail("items are not unique");
|
|
760
|
+
value.forEach((item, i) => {
|
|
761
|
+
const itemSchema = s.prefixItems?.[i] ?? s.items;
|
|
762
|
+
if (itemSchema)
|
|
763
|
+
errors.push(...validateValue(document2, itemSchema, item, [...path, i]));
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (actual === "object") {
|
|
767
|
+
const record = value;
|
|
768
|
+
const keys = Object.keys(record);
|
|
769
|
+
for (const name of s.required ?? [])
|
|
770
|
+
if (!(name in record))
|
|
771
|
+
fail(`missing required property ${name}`);
|
|
772
|
+
if (s.minProperties !== void 0 && keys.length < s.minProperties)
|
|
773
|
+
fail(`${keys.length} properties < minProperties ${s.minProperties}`);
|
|
774
|
+
if (s.maxProperties !== void 0 && keys.length > s.maxProperties)
|
|
775
|
+
fail(`${keys.length} properties > maxProperties ${s.maxProperties}`);
|
|
776
|
+
for (const key of keys) {
|
|
777
|
+
const property = s.properties?.[key];
|
|
778
|
+
if (property) {
|
|
779
|
+
errors.push(...validateValue(document2, property, record[key], [...path, key]));
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (s.additionalProperties === false)
|
|
783
|
+
fail(`unexpected property ${key}`);
|
|
784
|
+
else if (typeof s.additionalProperties === "object") {
|
|
785
|
+
errors.push(...validateValue(document2, s.additionalProperties, record[key], [...path, key]));
|
|
786
|
+
}
|
|
787
|
+
if (s.propertyNames) {
|
|
788
|
+
const nameErrors = validateValue(document2, s.propertyNames, key, [...path, key]);
|
|
789
|
+
if (nameErrors.length > 0)
|
|
790
|
+
fail(`property name ${key} is invalid: ${nameErrors[0]?.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (s.allOf)
|
|
795
|
+
for (const branch of s.allOf)
|
|
796
|
+
errors.push(...validateValue(document2, branch, value, path));
|
|
797
|
+
if (s.anyOf && !s.anyOf.some((branch) => validateValue(document2, branch, value).length === 0))
|
|
798
|
+
fail("matches no anyOf branch");
|
|
799
|
+
if (s.oneOf) {
|
|
800
|
+
const matches3 = s.oneOf.filter((branch) => validateValue(document2, branch, value).length === 0).length;
|
|
801
|
+
if (matches3 !== 1)
|
|
802
|
+
fail(`matches ${matches3} oneOf branches, expected exactly 1`);
|
|
803
|
+
}
|
|
804
|
+
if (s.not && validateValue(document2, s.not, value).length === 0)
|
|
805
|
+
fail("matches forbidden `not` schema");
|
|
806
|
+
return errors;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// ../../http/codec/dist/form.js
|
|
810
|
+
var parsePath = (rawKey) => {
|
|
811
|
+
const open = rawKey.indexOf("[");
|
|
812
|
+
if (open === -1)
|
|
813
|
+
return [rawKey];
|
|
814
|
+
const path = [rawKey.slice(0, open)];
|
|
815
|
+
const rest = rawKey.slice(open);
|
|
816
|
+
const pattern = /\[([^\]]*)\]/g;
|
|
817
|
+
let match = pattern.exec(rest);
|
|
818
|
+
let consumed = 0;
|
|
819
|
+
while (match !== null) {
|
|
820
|
+
if (match.index !== consumed)
|
|
821
|
+
return [rawKey];
|
|
822
|
+
path.push(match[1] ?? "");
|
|
823
|
+
consumed = match.index + match[0].length;
|
|
824
|
+
match = pattern.exec(rest);
|
|
825
|
+
}
|
|
826
|
+
if (consumed !== rest.length)
|
|
827
|
+
return [rawKey];
|
|
828
|
+
return path;
|
|
829
|
+
};
|
|
830
|
+
var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
|
|
831
|
+
var put = (target, key, value) => {
|
|
832
|
+
if (key === "__proto__") {
|
|
833
|
+
Object.defineProperty(target, key, {
|
|
834
|
+
value,
|
|
835
|
+
enumerable: true,
|
|
836
|
+
writable: true,
|
|
837
|
+
configurable: true
|
|
838
|
+
});
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
;
|
|
842
|
+
target[key] = value;
|
|
843
|
+
};
|
|
844
|
+
var assign = (target, path, value) => {
|
|
845
|
+
let cursor = target;
|
|
846
|
+
for (let i = 0; i < path.length; i++) {
|
|
847
|
+
const segment = path[i];
|
|
848
|
+
const last = i === path.length - 1;
|
|
849
|
+
if (Array.isArray(cursor)) {
|
|
850
|
+
const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
|
|
851
|
+
if (index === void 0)
|
|
852
|
+
return;
|
|
853
|
+
if (last) {
|
|
854
|
+
put(cursor, index, value);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
|
|
858
|
+
if (next === void 0 || typeof next === "string") {
|
|
859
|
+
const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
|
|
860
|
+
put(cursor, index, created);
|
|
861
|
+
cursor = created;
|
|
862
|
+
} else {
|
|
863
|
+
cursor = next;
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (typeof cursor === "string")
|
|
868
|
+
return;
|
|
869
|
+
if (last) {
|
|
870
|
+
put(cursor, segment, value);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const nextSegment = path[i + 1];
|
|
874
|
+
const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
|
|
875
|
+
if (existing === void 0 || typeof existing === "string") {
|
|
876
|
+
const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
|
|
877
|
+
put(cursor, segment, created);
|
|
878
|
+
cursor = created;
|
|
879
|
+
} else {
|
|
880
|
+
cursor = existing;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
var decodeFormPairs = (pairs) => {
|
|
885
|
+
const out = {};
|
|
886
|
+
for (const [rawKey, value] of pairs)
|
|
887
|
+
assign(out, parsePath(rawKey), value);
|
|
888
|
+
return densify(out);
|
|
889
|
+
};
|
|
890
|
+
var densify = (value) => {
|
|
891
|
+
if (typeof value === "string")
|
|
892
|
+
return value;
|
|
893
|
+
if (Array.isArray(value))
|
|
894
|
+
return value.filter((item) => item !== void 0).map(densify);
|
|
895
|
+
const out = {};
|
|
896
|
+
for (const [key, item] of Object.entries(value))
|
|
897
|
+
put(out, key, densify(item));
|
|
898
|
+
return out;
|
|
899
|
+
};
|
|
900
|
+
var decodeForm = (text2) => {
|
|
901
|
+
const source = text2.startsWith("?") ? text2.slice(1) : text2;
|
|
902
|
+
return decodeFormPairs(new URLSearchParams(source).entries());
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// ../../http/codec/dist/content.js
|
|
906
|
+
var JSON_MEDIA_TYPE = "application/json";
|
|
907
|
+
var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
|
|
908
|
+
var mediaTypeOf = (contentType) => {
|
|
909
|
+
if (!contentType)
|
|
910
|
+
return void 0;
|
|
911
|
+
const essence = contentType.split(";")[0]?.trim().toLowerCase();
|
|
912
|
+
return essence ? essence : void 0;
|
|
913
|
+
};
|
|
914
|
+
var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
|
|
915
|
+
var utf8 = new TextDecoder("utf-8", { fatal: false });
|
|
916
|
+
var decodeBody = (contentType, bytes) => {
|
|
917
|
+
if (bytes.byteLength === 0)
|
|
918
|
+
return { kind: "empty" };
|
|
919
|
+
const mediaType = mediaTypeOf(contentType);
|
|
920
|
+
if (mediaType === void 0)
|
|
921
|
+
return { kind: "bytes", value: bytes };
|
|
922
|
+
if (isJsonMediaType(mediaType)) {
|
|
923
|
+
const text2 = utf8.decode(bytes);
|
|
924
|
+
try {
|
|
925
|
+
return { kind: "json", value: JSON.parse(text2) };
|
|
926
|
+
} catch (error) {
|
|
927
|
+
return {
|
|
928
|
+
kind: "invalid",
|
|
929
|
+
mediaType,
|
|
930
|
+
text: text2,
|
|
931
|
+
error: error instanceof Error ? error.message : String(error)
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (mediaType === FORM_MEDIA_TYPE) {
|
|
936
|
+
return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
|
|
937
|
+
}
|
|
938
|
+
if (mediaType.startsWith("text/"))
|
|
939
|
+
return { kind: "text", value: utf8.decode(bytes) };
|
|
940
|
+
return { kind: "bytes", value: bytes };
|
|
941
|
+
};
|
|
942
|
+
var readBody = async (message) => {
|
|
943
|
+
const bytes = new Uint8Array(await message.arrayBuffer());
|
|
944
|
+
return decodeBody(message.headers.get("content-type"), bytes);
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// ../core/dist/http.js
|
|
948
|
+
var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
|
|
949
|
+
status,
|
|
950
|
+
headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
|
|
951
|
+
});
|
|
952
|
+
var HttpError = class extends Error {
|
|
953
|
+
status;
|
|
954
|
+
body;
|
|
955
|
+
headers;
|
|
956
|
+
constructor(status, body, headers = {}) {
|
|
957
|
+
super(`HTTP ${status}`);
|
|
958
|
+
this.status = status;
|
|
959
|
+
this.body = body;
|
|
960
|
+
this.headers = headers;
|
|
961
|
+
this.name = "HttpError";
|
|
962
|
+
}
|
|
963
|
+
toResponse() {
|
|
964
|
+
const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
965
|
+
if (contentType === "text/plain") {
|
|
966
|
+
return new Response(String(this.body), {
|
|
967
|
+
status: this.status,
|
|
968
|
+
headers: this.headers
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
return jsonRes(this.status, this.body, this.headers);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// ../core/dist/idempotency.js
|
|
976
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
977
|
+
var IdempotencyStore = class {
|
|
978
|
+
namespace;
|
|
979
|
+
responses;
|
|
980
|
+
constructor(sqlite, namespace, name = "idempotency") {
|
|
981
|
+
this.namespace = namespace;
|
|
982
|
+
this.responses = new Collection(sqlite, namespace, name);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Run `handler` once per `key`. `fingerprint` identifies the request's parameters (e.g.
|
|
986
|
+
* the method, path and canonical body). Only `replayable` responses are stored (default:
|
|
987
|
+
* every status below 500, as Stripe does), so a transient failure can be retried.
|
|
988
|
+
*/
|
|
989
|
+
async run(key, fingerprint, errors, handler, replayable = (status) => status < 500) {
|
|
990
|
+
const slot = `${this.namespace}\0${key}`;
|
|
991
|
+
const stored = this.responses.get(key);
|
|
992
|
+
if (stored) {
|
|
993
|
+
if (stored.fingerprint !== fingerprint)
|
|
994
|
+
return errors.mismatch();
|
|
995
|
+
return new Response(stored.body, {
|
|
996
|
+
status: stored.status,
|
|
997
|
+
headers: [...stored.headers, ["idempotent-replayed", "true"]]
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (inFlight.has(slot))
|
|
1001
|
+
return errors.conflict();
|
|
1002
|
+
let release = () => {
|
|
1003
|
+
};
|
|
1004
|
+
inFlight.set(slot, new Promise((resolve) => {
|
|
1005
|
+
release = resolve;
|
|
1006
|
+
}));
|
|
1007
|
+
try {
|
|
1008
|
+
const response = await handler();
|
|
1009
|
+
if (!replayable(response.status))
|
|
1010
|
+
return response;
|
|
1011
|
+
const body = await response.clone().text();
|
|
1012
|
+
this.responses.insert(key, {
|
|
1013
|
+
fingerprint,
|
|
1014
|
+
status: response.status,
|
|
1015
|
+
headers: [...response.headers],
|
|
1016
|
+
body
|
|
1017
|
+
});
|
|
1018
|
+
return response;
|
|
1019
|
+
} finally {
|
|
1020
|
+
inFlight.delete(slot);
|
|
1021
|
+
release();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
var requestFingerprint = (method, path, body) => `${method.toUpperCase()} ${path} ${stableStringify(body)}`;
|
|
1026
|
+
var stableStringify = (value) => {
|
|
1027
|
+
if (Array.isArray(value))
|
|
1028
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1029
|
+
if (value && typeof value === "object") {
|
|
1030
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
|
|
1031
|
+
}
|
|
1032
|
+
return JSON.stringify(value) ?? "undefined";
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// ../core/dist/ids.js
|
|
1036
|
+
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
1037
|
+
var mix = (input) => {
|
|
1038
|
+
let hash = 2166136261;
|
|
1039
|
+
for (let i = 0; i < input.length; i++) {
|
|
1040
|
+
hash ^= input.charCodeAt(i);
|
|
1041
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
1042
|
+
}
|
|
1043
|
+
hash ^= hash >>> 16;
|
|
1044
|
+
hash = Math.imul(hash, 2246822507) >>> 0;
|
|
1045
|
+
hash ^= hash >>> 13;
|
|
1046
|
+
return hash >>> 0;
|
|
1047
|
+
};
|
|
1048
|
+
var opaqueToken = (input, length) => {
|
|
1049
|
+
let out = "";
|
|
1050
|
+
let round = 0;
|
|
1051
|
+
while (out.length < length) {
|
|
1052
|
+
let hash = mix(`${input}:${round++}`);
|
|
1053
|
+
for (let i = 0; i < 5 && out.length < length; i++) {
|
|
1054
|
+
out += ALPHABET.charAt(hash % ALPHABET.length);
|
|
1055
|
+
hash = Math.floor(hash / ALPHABET.length);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return out;
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// ../core/dist/journal.js
|
|
1062
|
+
var DEFAULT_JOURNAL_SIZE = 1e3;
|
|
1063
|
+
var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
1064
|
+
const capacity = Math.max(0, Math.floor(size));
|
|
1065
|
+
const rings = /* @__PURE__ */ new Map();
|
|
1066
|
+
let sequence = 0;
|
|
1067
|
+
const order = /* @__PURE__ */ new WeakMap();
|
|
1068
|
+
const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
|
|
1069
|
+
return {
|
|
1070
|
+
size: capacity,
|
|
1071
|
+
record(entry) {
|
|
1072
|
+
if (capacity === 0)
|
|
1073
|
+
return;
|
|
1074
|
+
order.set(entry, sequence++);
|
|
1075
|
+
let ring = rings.get(entry.namespace);
|
|
1076
|
+
if (!ring) {
|
|
1077
|
+
ring = { entries: [], next: 0 };
|
|
1078
|
+
rings.set(entry.namespace, ring);
|
|
1079
|
+
}
|
|
1080
|
+
if (ring.entries.length < capacity)
|
|
1081
|
+
ring.entries.push(entry);
|
|
1082
|
+
else {
|
|
1083
|
+
ring.entries[ring.next] = entry;
|
|
1084
|
+
ring.next = (ring.next + 1) % capacity;
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
list(query = {}) {
|
|
1088
|
+
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));
|
|
1089
|
+
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));
|
|
1090
|
+
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
1091
|
+
},
|
|
1092
|
+
clear(namespace) {
|
|
1093
|
+
if (namespace === void 0)
|
|
1094
|
+
rings.clear();
|
|
1095
|
+
else
|
|
1096
|
+
rings.delete(namespace);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
};
|
|
1100
|
+
var notes = /* @__PURE__ */ new WeakMap();
|
|
1101
|
+
var annotateResponse = (response, extra) => {
|
|
1102
|
+
const existing = notes.get(response);
|
|
1103
|
+
notes.set(response, {
|
|
1104
|
+
...existing,
|
|
1105
|
+
...extra,
|
|
1106
|
+
...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
|
|
1107
|
+
});
|
|
1108
|
+
return response;
|
|
1109
|
+
};
|
|
1110
|
+
var responseNotes = (response) => notes.get(response);
|
|
1111
|
+
|
|
1112
|
+
// ../core/dist/metrics.js
|
|
1113
|
+
var createMetrics = () => {
|
|
1114
|
+
let requests = 0;
|
|
1115
|
+
let faults = 0;
|
|
1116
|
+
let totalDurationMs = 0;
|
|
1117
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
1118
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
1119
|
+
return {
|
|
1120
|
+
record(entry) {
|
|
1121
|
+
requests++;
|
|
1122
|
+
totalDurationMs += entry.durationMs;
|
|
1123
|
+
if (entry.faultId !== void 0)
|
|
1124
|
+
faults++;
|
|
1125
|
+
const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
|
|
1126
|
+
byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
|
|
1127
|
+
if (entry.unmatched) {
|
|
1128
|
+
const route = `${entry.method} ${entry.path}`;
|
|
1129
|
+
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
report: () => ({
|
|
1133
|
+
requests,
|
|
1134
|
+
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1135
|
+
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
1136
|
+
const space = route.indexOf(" ");
|
|
1137
|
+
return { method: route.slice(0, space), path: route.slice(space + 1), count };
|
|
1138
|
+
}),
|
|
1139
|
+
faults,
|
|
1140
|
+
totalDurationMs
|
|
1141
|
+
}),
|
|
1142
|
+
reset() {
|
|
1143
|
+
requests = 0;
|
|
1144
|
+
faults = 0;
|
|
1145
|
+
totalDurationMs = 0;
|
|
1146
|
+
byOperation.clear();
|
|
1147
|
+
unmatched.clear();
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// ../../core/dist/timeline.js
|
|
1153
|
+
var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1154
|
+
var Timeline = class {
|
|
1155
|
+
maxCheckpoints;
|
|
1156
|
+
now;
|
|
1157
|
+
makeId;
|
|
1158
|
+
nodes = /* @__PURE__ */ new Map();
|
|
1159
|
+
heads = /* @__PURE__ */ new Map();
|
|
1160
|
+
/** Unreferenced nodes in the exact order they became collectible. */
|
|
1161
|
+
evictable = /* @__PURE__ */ new Set();
|
|
1162
|
+
/** Branch heads plus explicit retainers. Absent means zero. */
|
|
1163
|
+
references = /* @__PURE__ */ new Map();
|
|
1164
|
+
explicitPins = /* @__PURE__ */ new Map();
|
|
1165
|
+
sequence = 0;
|
|
1166
|
+
constructor(options = {}) {
|
|
1167
|
+
const max = options.maxCheckpoints ?? 1e3;
|
|
1168
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
1169
|
+
throw new RangeError("maxCheckpoints must be a positive integer");
|
|
1170
|
+
this.maxCheckpoints = max;
|
|
1171
|
+
this.now = options.now ?? (() => this.sequence);
|
|
1172
|
+
this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
|
|
1173
|
+
}
|
|
1174
|
+
/** Capture a new immutable value and move `branch` to it. */
|
|
1175
|
+
commit(value, options = {}) {
|
|
1176
|
+
const branch = options.branch ?? "main";
|
|
1177
|
+
this.assertBranch(branch);
|
|
1178
|
+
const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
|
|
1179
|
+
if (parent !== null && !this.nodes.has(parent))
|
|
1180
|
+
throw new RangeError(`no checkpoint ${parent}`);
|
|
1181
|
+
const id = this.makeId(++this.sequence);
|
|
1182
|
+
if (this.nodes.has(id))
|
|
1183
|
+
throw new RangeError(`duplicate checkpoint id ${id}`);
|
|
1184
|
+
const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
|
|
1185
|
+
this.nodes.set(id, checkpoint);
|
|
1186
|
+
this.moveHead(branch, id);
|
|
1187
|
+
this.collect(this.maxCheckpoints);
|
|
1188
|
+
return checkpoint;
|
|
1189
|
+
}
|
|
1190
|
+
/** Create a branch pointer without copying its checkpoint value. */
|
|
1191
|
+
fork(branch, options = {}) {
|
|
1192
|
+
this.assertBranch(branch);
|
|
1193
|
+
if (this.heads.has(branch))
|
|
1194
|
+
throw new RangeError(`branch already exists: ${branch}`);
|
|
1195
|
+
const from = options.from ?? this.heads.get("main");
|
|
1196
|
+
if (from === void 0)
|
|
1197
|
+
return void 0;
|
|
1198
|
+
const checkpoint = this.get(from);
|
|
1199
|
+
this.moveHead(branch, checkpoint.id);
|
|
1200
|
+
return checkpoint;
|
|
1201
|
+
}
|
|
1202
|
+
/** Move a branch pointer to an existing checkpoint. */
|
|
1203
|
+
checkout(branch, id) {
|
|
1204
|
+
this.assertBranch(branch);
|
|
1205
|
+
const checkpoint = this.get(id);
|
|
1206
|
+
this.moveHead(branch, checkpoint.id);
|
|
1207
|
+
return checkpoint;
|
|
1208
|
+
}
|
|
1209
|
+
get(id) {
|
|
1210
|
+
const checkpoint = this.nodes.get(id);
|
|
1211
|
+
if (!checkpoint)
|
|
1212
|
+
throw new RangeError(`no checkpoint ${id}`);
|
|
1213
|
+
return checkpoint;
|
|
1214
|
+
}
|
|
1215
|
+
head(branch = "main") {
|
|
1216
|
+
const id = this.heads.get(branch);
|
|
1217
|
+
return id === void 0 ? void 0 : this.get(id);
|
|
1218
|
+
}
|
|
1219
|
+
hasBranch(branch) {
|
|
1220
|
+
return this.heads.has(branch);
|
|
1221
|
+
}
|
|
1222
|
+
branches() {
|
|
1223
|
+
return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
1224
|
+
}
|
|
1225
|
+
checkpoints() {
|
|
1226
|
+
return [...this.nodes.values()];
|
|
1227
|
+
}
|
|
1228
|
+
/** Number of retained checkpoints without allocating an array. */
|
|
1229
|
+
get size() {
|
|
1230
|
+
return this.nodes.size;
|
|
1231
|
+
}
|
|
1232
|
+
/** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
|
|
1233
|
+
retain(id) {
|
|
1234
|
+
const checkpoint = this.get(id);
|
|
1235
|
+
this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
|
|
1236
|
+
this.addReference(id);
|
|
1237
|
+
return checkpoint;
|
|
1238
|
+
}
|
|
1239
|
+
/** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
|
|
1240
|
+
release(id) {
|
|
1241
|
+
if (!this.nodes.has(id))
|
|
1242
|
+
return false;
|
|
1243
|
+
const pins = this.explicitPins.get(id) ?? 0;
|
|
1244
|
+
if (pins === 0)
|
|
1245
|
+
return false;
|
|
1246
|
+
if (pins === 1)
|
|
1247
|
+
this.explicitPins.delete(id);
|
|
1248
|
+
else
|
|
1249
|
+
this.explicitPins.set(id, pins - 1);
|
|
1250
|
+
this.removeReference(id);
|
|
1251
|
+
this.collect(this.maxCheckpoints);
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1254
|
+
deleteBranch(branch) {
|
|
1255
|
+
if (branch === "main")
|
|
1256
|
+
throw new RangeError("cannot delete main branch");
|
|
1257
|
+
const previous = this.heads.get(branch);
|
|
1258
|
+
const deleted = this.heads.delete(branch);
|
|
1259
|
+
if (previous !== void 0)
|
|
1260
|
+
this.removeReference(previous);
|
|
1261
|
+
this.collect(this.maxCheckpoints);
|
|
1262
|
+
return deleted;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
|
|
1266
|
+
* commits never scan pinned nodes or the retained history. Parents are metadata rather than a
|
|
1267
|
+
* storage dependency, so a retained node remains usable after pruning.
|
|
1268
|
+
*/
|
|
1269
|
+
gc(max = this.maxCheckpoints) {
|
|
1270
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
1271
|
+
throw new RangeError("max must be a positive integer");
|
|
1272
|
+
const removed = [];
|
|
1273
|
+
this.collect(max, removed);
|
|
1274
|
+
return removed;
|
|
1275
|
+
}
|
|
1276
|
+
collect(max, removed) {
|
|
1277
|
+
while (this.nodes.size > max && this.evictable.size > 0) {
|
|
1278
|
+
const id = this.evictable.values().next().value;
|
|
1279
|
+
this.evictable.delete(id);
|
|
1280
|
+
this.nodes.delete(id);
|
|
1281
|
+
removed?.push(id);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
moveHead(branch, id) {
|
|
1285
|
+
const previous = this.heads.get(branch);
|
|
1286
|
+
if (previous === id)
|
|
1287
|
+
return;
|
|
1288
|
+
if (previous !== void 0)
|
|
1289
|
+
this.removeReference(previous);
|
|
1290
|
+
this.heads.set(branch, id);
|
|
1291
|
+
this.addReference(id);
|
|
1292
|
+
}
|
|
1293
|
+
addReference(id) {
|
|
1294
|
+
this.references.set(id, (this.references.get(id) ?? 0) + 1);
|
|
1295
|
+
this.evictable.delete(id);
|
|
1296
|
+
}
|
|
1297
|
+
removeReference(id) {
|
|
1298
|
+
const next = (this.references.get(id) ?? 0) - 1;
|
|
1299
|
+
if (next > 0)
|
|
1300
|
+
this.references.set(id, next);
|
|
1301
|
+
else {
|
|
1302
|
+
this.references.delete(id);
|
|
1303
|
+
if (this.nodes.has(id))
|
|
1304
|
+
this.evictable.add(id);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
assertBranch(branch) {
|
|
1308
|
+
if (!BRANCH_PATTERN.test(branch))
|
|
1309
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// ../../sqlite/dist/default.js
|
|
1314
|
+
import { Database } from "@crvouga/mockingbird-service-sqlite";
|
|
1315
|
+
var createDefaultSqlite = () => new Database();
|
|
1316
|
+
var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
|
|
1317
|
+
|
|
1318
|
+
// ../../sqlite/dist/migrate.js
|
|
1319
|
+
var ensureMigrationsTable = (sqlite) => {
|
|
1320
|
+
sqlite.exec(`
|
|
1321
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
1322
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
1323
|
+
applied_at INTEGER NOT NULL
|
|
1324
|
+
)
|
|
1325
|
+
`);
|
|
1326
|
+
};
|
|
1327
|
+
var migrate = (sqlite, migrations) => {
|
|
1328
|
+
ensureMigrationsTable(sqlite);
|
|
1329
|
+
const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
1330
|
+
const pending = migrations.filter((migration) => !applied.has(migration.id));
|
|
1331
|
+
if (pending.length === 0)
|
|
1332
|
+
return;
|
|
1333
|
+
const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
1334
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1335
|
+
sqlite.transaction(() => {
|
|
1336
|
+
for (const migration of pending) {
|
|
1337
|
+
sqlite.exec(migration.sql);
|
|
1338
|
+
insert.run(migration.id, now);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// ../../sqlite/dist/schema.js
|
|
1344
|
+
var CORE_MIGRATIONS = [
|
|
1345
|
+
{
|
|
1346
|
+
id: "20260322_core_records_sequences",
|
|
1347
|
+
sql: `
|
|
1348
|
+
CREATE TABLE IF NOT EXISTS mockingbird_records (
|
|
1349
|
+
namespace TEXT NOT NULL,
|
|
1350
|
+
collection TEXT NOT NULL,
|
|
1351
|
+
id TEXT NOT NULL,
|
|
1352
|
+
seq INTEGER NOT NULL,
|
|
1353
|
+
value TEXT NOT NULL,
|
|
1354
|
+
PRIMARY KEY (namespace, collection, id)
|
|
1355
|
+
);
|
|
1356
|
+
CREATE INDEX IF NOT EXISTS mockingbird_records_seq
|
|
1357
|
+
ON mockingbird_records (namespace, collection, seq);
|
|
1358
|
+
CREATE TABLE IF NOT EXISTS mockingbird_sequences (
|
|
1359
|
+
namespace TEXT NOT NULL,
|
|
1360
|
+
name TEXT NOT NULL,
|
|
1361
|
+
kind TEXT NOT NULL,
|
|
1362
|
+
value INTEGER NOT NULL,
|
|
1363
|
+
PRIMARY KEY (namespace, name, kind)
|
|
1364
|
+
);
|
|
1365
|
+
`
|
|
1366
|
+
}
|
|
1367
|
+
];
|
|
1368
|
+
var migrateCore = (sqlite) => {
|
|
1369
|
+
migrate(sqlite, CORE_MIGRATIONS);
|
|
1370
|
+
};
|
|
1371
|
+
var clearNamespace = (sqlite, namespace) => {
|
|
1372
|
+
sqlite.transaction(() => {
|
|
1373
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1374
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1375
|
+
});
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
// ../../openapi/metadata/dist/types.js
|
|
1379
|
+
var EXTENSION_KEYS = {
|
|
1380
|
+
operation: "x-mockingbird",
|
|
1381
|
+
resource: "x-mockingbird-resource",
|
|
1382
|
+
resourceRef: "x-mockingbird-resource-ref",
|
|
1383
|
+
volatile: "x-mockingbird-volatile",
|
|
1384
|
+
scope: "x-mockingbird-scope",
|
|
1385
|
+
unsupported: "x-mockingbird-unsupported",
|
|
1386
|
+
parityHeader: "x-mockingbird-parity-header"
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
// ../../openapi/metadata/dist/read.js
|
|
1390
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1391
|
+
var extensionOf = (holder, key) => holder[key];
|
|
1392
|
+
var operationMetadata = (operation) => {
|
|
1393
|
+
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
1394
|
+
const ext = isRecord2(raw) ? raw : {};
|
|
1395
|
+
const supported = ext.supported ?? true;
|
|
1396
|
+
const parity = ext.parity ?? {};
|
|
1397
|
+
return {
|
|
1398
|
+
supported,
|
|
1399
|
+
reason: typeof ext.reason === "string" ? ext.reason : void 0,
|
|
1400
|
+
parity: {
|
|
1401
|
+
enabled: supported && (parity.enabled ?? true),
|
|
1402
|
+
safe: parity.safe ?? true,
|
|
1403
|
+
reason: typeof parity.reason === "string" ? parity.reason : void 0
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// ../core/dist/service.js
|
|
1409
|
+
import { Hono } from "hono";
|
|
1410
|
+
var defineOperations = (handlers) => handlers;
|
|
1411
|
+
var OperationRegistryError = class extends Error {
|
|
1412
|
+
problems;
|
|
1413
|
+
constructor(problems) {
|
|
1414
|
+
super(`operation registry is inconsistent:
|
|
1415
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
1416
|
+
this.problems = problems;
|
|
1417
|
+
this.name = "OperationRegistryError";
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
var verifyOperations = (document2, handlers) => {
|
|
1421
|
+
const problems = [];
|
|
1422
|
+
const operations = listOperations(document2);
|
|
1423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1424
|
+
for (const operation of operations) {
|
|
1425
|
+
if (seen.has(operation.operationId))
|
|
1426
|
+
problems.push(`duplicate operationId ${operation.operationId}`);
|
|
1427
|
+
seen.add(operation.operationId);
|
|
1428
|
+
const supported = operationMetadata(operation.operation).supported;
|
|
1429
|
+
const handler = handlers[operation.operationId];
|
|
1430
|
+
if (supported && !handler)
|
|
1431
|
+
problems.push(`supported operation ${operation.operationId} has no handler`);
|
|
1432
|
+
if (!supported && handler)
|
|
1433
|
+
problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
|
|
1434
|
+
}
|
|
1435
|
+
for (const id of Object.keys(handlers)) {
|
|
1436
|
+
if (!seen.has(id))
|
|
1437
|
+
problems.push(`handler ${id} has no OpenAPI operation`);
|
|
1438
|
+
}
|
|
1439
|
+
return problems;
|
|
1440
|
+
};
|
|
1441
|
+
var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
|
|
1442
|
+
var routeOrder = (a, b) => {
|
|
1443
|
+
const sa = a.path.split("/");
|
|
1444
|
+
const sb = b.path.split("/");
|
|
1445
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
1446
|
+
const x = sa[i] ?? "";
|
|
1447
|
+
const y = sb[i] ?? "";
|
|
1448
|
+
const px = x.startsWith("{");
|
|
1449
|
+
const py = y.startsWith("{");
|
|
1450
|
+
if (px !== py)
|
|
1451
|
+
return px ? 1 : -1;
|
|
1452
|
+
if (x !== y)
|
|
1453
|
+
return x < y ? -1 : 1;
|
|
1454
|
+
}
|
|
1455
|
+
return 0;
|
|
1456
|
+
};
|
|
1457
|
+
var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
|
|
1458
|
+
var bootSqlite = (sqlite) => {
|
|
1459
|
+
const client = resolveSqlite(sqlite);
|
|
1460
|
+
migrateCore(client);
|
|
1461
|
+
return client;
|
|
1462
|
+
};
|
|
1463
|
+
var createService = (options) => {
|
|
1464
|
+
const problems = verifyOperations(options.document, options.handlers);
|
|
1465
|
+
if (problems.length > 0)
|
|
1466
|
+
throw new OperationRegistryError(problems);
|
|
1467
|
+
migrateCore(options.sqlite);
|
|
1468
|
+
const now = options.now ?? (() => Date.now());
|
|
1469
|
+
const app = new Hono();
|
|
1470
|
+
app.notFound((c) => options.notFound(c.req.raw));
|
|
1471
|
+
app.onError((error, c) => options.onError(error, c.req.raw));
|
|
1472
|
+
const operations = [...listOperations(options.document)].sort(routeOrder);
|
|
1473
|
+
for (const operation of operations) {
|
|
1474
|
+
const metadata = operationMetadata(operation.operation);
|
|
1475
|
+
const handler = options.handlers[operation.operationId];
|
|
1476
|
+
const route = async (c) => {
|
|
1477
|
+
const request = c.req.raw;
|
|
1478
|
+
if (!metadata.supported || !handler) {
|
|
1479
|
+
return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
|
|
1480
|
+
}
|
|
1481
|
+
const url = new URL(request.url);
|
|
1482
|
+
const context = {
|
|
1483
|
+
request,
|
|
1484
|
+
url,
|
|
1485
|
+
params: c.req.param(),
|
|
1486
|
+
query: queryOf(url),
|
|
1487
|
+
body: await readBody(request),
|
|
1488
|
+
sqlite: options.sqlite,
|
|
1489
|
+
namespace: options.namespace,
|
|
1490
|
+
operation,
|
|
1491
|
+
document: options.document,
|
|
1492
|
+
now
|
|
1493
|
+
};
|
|
1494
|
+
const short = await options.before?.(context);
|
|
1495
|
+
if (short)
|
|
1496
|
+
return short;
|
|
1497
|
+
return handler(context);
|
|
1498
|
+
};
|
|
1499
|
+
app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
app,
|
|
1503
|
+
sqlite: options.sqlite,
|
|
1504
|
+
namespace: options.namespace,
|
|
1505
|
+
fetch: async (request) => app.fetch(request),
|
|
1506
|
+
reset: async () => {
|
|
1507
|
+
clearNamespace(options.sqlite, options.namespace);
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
// ../core/dist/snapshot.js
|
|
1513
|
+
var snapshotNamespace = (sqlite, namespace) => ({
|
|
1514
|
+
namespace,
|
|
1515
|
+
records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
|
|
1516
|
+
sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
|
|
1517
|
+
});
|
|
1518
|
+
var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
1519
|
+
sqlite.transaction(() => {
|
|
1520
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1521
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1522
|
+
const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
|
|
1523
|
+
for (const row of snapshot.records) {
|
|
1524
|
+
record.run(namespace, row.collection, row.id, row.seq, row.value);
|
|
1525
|
+
}
|
|
1526
|
+
const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
|
|
1527
|
+
for (const row of snapshot.sequences) {
|
|
1528
|
+
sequence.run(namespace, row.name, row.kind, row.value);
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// ../core/dist/version.js
|
|
1534
|
+
var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
|
|
1535
|
+
|
|
1536
|
+
// ../core/dist/signing.js
|
|
1537
|
+
var encoder = new TextEncoder();
|
|
1538
|
+
var toBase64 = (bytes) => {
|
|
1539
|
+
let binary = "";
|
|
1540
|
+
for (const byte of bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)) {
|
|
1541
|
+
binary += String.fromCharCode(byte);
|
|
1542
|
+
}
|
|
1543
|
+
return btoa(binary);
|
|
1544
|
+
};
|
|
1545
|
+
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
1546
|
+
var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1547
|
+
var keyBytes = (key) => typeof key === "string" ? encoder.encode(key) : key;
|
|
1548
|
+
var hmac = async (algorithm, key, message, encoding = "hex") => {
|
|
1549
|
+
const imported = await crypto.subtle.importKey("raw", keyBytes(key), { name: "HMAC", hash: algorithm }, false, ["sign"]);
|
|
1550
|
+
const signed = await crypto.subtle.sign("HMAC", imported, typeof message === "string" ? encoder.encode(message) : message);
|
|
1551
|
+
return encoding === "hex" ? toHex(signed) : toBase64(signed);
|
|
1552
|
+
};
|
|
1553
|
+
var svixSecretBytes = (secret) => {
|
|
1554
|
+
const raw = secret.replace(/^f?whsec_/, "");
|
|
1555
|
+
try {
|
|
1556
|
+
return fromBase64(raw);
|
|
1557
|
+
} catch {
|
|
1558
|
+
throw new TypeError("webhook secret must be whsec_<base64> (as Svix issues it)");
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
var signSvix = async (secret, messageId, timestampSeconds, body) => `v1,${await hmac("SHA-256", svixSecretBytes(secret), `${messageId}.${timestampSeconds}.${body}`, "base64")}`;
|
|
1562
|
+
var signTimestamped = async (secret, timestampSeconds, body) => `t=${timestampSeconds},v1=${await hmac("SHA-256", secret, `${timestampSeconds}.${body}`, "hex")}`;
|
|
1563
|
+
var signTwilio = async (authToken, url, params) => {
|
|
1564
|
+
const payload = url + Object.keys(params).sort().map((key) => `${key}${params[key]}`).join("");
|
|
1565
|
+
return hmac("SHA-1", authToken, payload, "base64");
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// ../core/dist/webhooks.js
|
|
1569
|
+
var signers = {
|
|
1570
|
+
/** No signature. */
|
|
1571
|
+
none: () => () => ({}),
|
|
1572
|
+
/** Svix (`svix-id`, `svix-timestamp`, `svix-signature: v1,<b64>`): Junction, Flex, Resend. */
|
|
1573
|
+
svix: (options = {}) => async ({ messageId, body, timestampSeconds, secret }) => {
|
|
1574
|
+
if (!secret)
|
|
1575
|
+
return {};
|
|
1576
|
+
const prefix = options.prefix ?? "svix";
|
|
1577
|
+
return {
|
|
1578
|
+
[`${prefix}-id`]: messageId,
|
|
1579
|
+
[`${prefix}-timestamp`]: String(timestampSeconds),
|
|
1580
|
+
[`${prefix}-signature`]: await signSvix(secret, messageId, timestampSeconds, body)
|
|
1581
|
+
};
|
|
1582
|
+
},
|
|
1583
|
+
/** `<header>: t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>`: Stripe, Persona, Fullscript. */
|
|
1584
|
+
timestamped: (header = "Stripe-Signature") => async ({ body, timestampSeconds, secret }) => secret ? { [header]: await signTimestamped(secret, timestampSeconds, body) } : {},
|
|
1585
|
+
/** `X-Twilio-Signature` over the public URL plus the sorted form parameters. */
|
|
1586
|
+
twilio: () => async ({ signUrl, form, secret }) => secret ? { "X-Twilio-Signature": await signTwilio(secret, signUrl, form ?? {}) } : {},
|
|
1587
|
+
/** The secret itself in a header (optionally templated): RxVortex, Pharmetika, AHA `Token <s>`. */
|
|
1588
|
+
header: (header, format = (s) => s) => ({ secret }) => secret ? { [header]: format(secret) } : {},
|
|
1589
|
+
/** Anything else: the service computes the headers itself. */
|
|
1590
|
+
custom: (sign) => sign
|
|
1591
|
+
};
|
|
1592
|
+
var DEFAULT_DELAYS = [0, 5e3, 3e5, 18e5, 72e5];
|
|
1593
|
+
var unref = (timer) => {
|
|
1594
|
+
;
|
|
1595
|
+
timer.unref?.();
|
|
1596
|
+
};
|
|
1597
|
+
var randomId = (prefix) => `${prefix}${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
1598
|
+
var matchesEndpoint = (endpoint, message) => {
|
|
1599
|
+
const events = endpoint.events ?? ["*"];
|
|
1600
|
+
if (!events.includes("*") && !events.includes(message.type))
|
|
1601
|
+
return false;
|
|
1602
|
+
for (const [key, value] of Object.entries(endpoint.tags ?? {})) {
|
|
1603
|
+
if (message.tags[key] !== value)
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
return true;
|
|
1607
|
+
};
|
|
1608
|
+
var createWebhookHub = (options) => {
|
|
1609
|
+
const delays = options.retryDelaysMs ?? DEFAULT_DELAYS;
|
|
1610
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
1611
|
+
const delivered = options.delivered ?? ((status) => status >= 200 && status < 300);
|
|
1612
|
+
const send = options.fetch ?? ((request) => fetch(request));
|
|
1613
|
+
const keep = options.keep ?? 500;
|
|
1614
|
+
const now = options.now ?? Date.now;
|
|
1615
|
+
const id = options.id ?? randomId;
|
|
1616
|
+
const scheduleTimer = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
1617
|
+
const cancel = options.cancel ?? ((handle) => clearTimeout(handle));
|
|
1618
|
+
const global = (options.endpoints ?? []).map((e, i) => ({ ...e, id: e.id ?? `we_global_${i}` }));
|
|
1619
|
+
const own = /* @__PURE__ */ new Map();
|
|
1620
|
+
const messages = [];
|
|
1621
|
+
const deliveries = /* @__PURE__ */ new Map();
|
|
1622
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1623
|
+
const payloads = /* @__PURE__ */ new Map();
|
|
1624
|
+
const faults = /* @__PURE__ */ new Map();
|
|
1625
|
+
const held = /* @__PURE__ */ new Map();
|
|
1626
|
+
const inFlight2 = /* @__PURE__ */ new Set();
|
|
1627
|
+
const track = (work) => {
|
|
1628
|
+
inFlight2.add(work);
|
|
1629
|
+
void work.finally(() => inFlight2.delete(work));
|
|
1630
|
+
};
|
|
1631
|
+
const attempt = async (delivery) => {
|
|
1632
|
+
const entry = payloads.get(delivery.id);
|
|
1633
|
+
if (!entry)
|
|
1634
|
+
return false;
|
|
1635
|
+
const { message, endpoint } = entry;
|
|
1636
|
+
const timestampSeconds = Math.floor(now() / 1e3);
|
|
1637
|
+
const started = now();
|
|
1638
|
+
const record = {
|
|
1639
|
+
attempt: delivery.attempts.length + 1,
|
|
1640
|
+
at: new Date(started).toISOString(),
|
|
1641
|
+
status: null,
|
|
1642
|
+
error: null,
|
|
1643
|
+
durationMs: 0,
|
|
1644
|
+
responseBody: null
|
|
1645
|
+
};
|
|
1646
|
+
const controller = new AbortController();
|
|
1647
|
+
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
1648
|
+
try {
|
|
1649
|
+
const signed = await options.signer({
|
|
1650
|
+
messageId: message.id,
|
|
1651
|
+
body: message.body,
|
|
1652
|
+
timestampSeconds,
|
|
1653
|
+
url: endpoint.url,
|
|
1654
|
+
secret: endpoint.secret,
|
|
1655
|
+
signUrl: endpoint.signUrl ?? endpoint.url,
|
|
1656
|
+
form: message.contentType.startsWith("application/x-www-form-urlencoded") ? Object.fromEntries(new URLSearchParams(message.body)) : void 0,
|
|
1657
|
+
type: message.type,
|
|
1658
|
+
tags: message.tags
|
|
1659
|
+
});
|
|
1660
|
+
const response = await send(new Request(endpoint.url, {
|
|
1661
|
+
method: "POST",
|
|
1662
|
+
headers: {
|
|
1663
|
+
"content-type": message.contentType,
|
|
1664
|
+
...endpoint.headers,
|
|
1665
|
+
...message.headers,
|
|
1666
|
+
...signed
|
|
1667
|
+
},
|
|
1668
|
+
body: message.body,
|
|
1669
|
+
signal: controller.signal
|
|
1670
|
+
}));
|
|
1671
|
+
record.status = response.status;
|
|
1672
|
+
record.responseBody = await response.text();
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
1675
|
+
} finally {
|
|
1676
|
+
cancel(timer);
|
|
1677
|
+
record.durationMs = now() - started;
|
|
1678
|
+
delivery.attempts.push(record);
|
|
1679
|
+
}
|
|
1680
|
+
return record.status !== null && delivered(record.status);
|
|
1681
|
+
};
|
|
1682
|
+
const schedule = (delivery) => {
|
|
1683
|
+
const index = delivery.attempts.length;
|
|
1684
|
+
if (index >= delays.length) {
|
|
1685
|
+
delivery.state = "failed";
|
|
1686
|
+
pending.delete(delivery.id);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const run = () => {
|
|
1690
|
+
pending.delete(delivery.id);
|
|
1691
|
+
track(attempt(delivery).then((ok) => {
|
|
1692
|
+
if (ok)
|
|
1693
|
+
delivery.state = "delivered";
|
|
1694
|
+
else
|
|
1695
|
+
schedule(delivery);
|
|
1696
|
+
}));
|
|
1697
|
+
};
|
|
1698
|
+
const delay = delays[index] ?? 0;
|
|
1699
|
+
if (delay <= 0) {
|
|
1700
|
+
pending.set(delivery.id, void 0);
|
|
1701
|
+
run();
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const timer = scheduleTimer(run, delay);
|
|
1705
|
+
unref(timer);
|
|
1706
|
+
pending.set(delivery.id, timer);
|
|
1707
|
+
};
|
|
1708
|
+
const endpointsFor = (namespace) => [...own.get(namespace) ?? [], ...global];
|
|
1709
|
+
const fanOut = (message, state = "pending") => {
|
|
1710
|
+
for (const endpoint of endpointsFor(message.namespace)) {
|
|
1711
|
+
if (!matchesEndpoint(endpoint, message))
|
|
1712
|
+
continue;
|
|
1713
|
+
const delivery = {
|
|
1714
|
+
id: id("dlv_"),
|
|
1715
|
+
messageId: message.id,
|
|
1716
|
+
namespace: message.namespace,
|
|
1717
|
+
type: message.type,
|
|
1718
|
+
endpointId: endpoint.id ?? "we_unknown",
|
|
1719
|
+
url: endpoint.url,
|
|
1720
|
+
state,
|
|
1721
|
+
attempts: []
|
|
1722
|
+
};
|
|
1723
|
+
deliveries.set(delivery.id, delivery);
|
|
1724
|
+
payloads.set(delivery.id, { message, endpoint });
|
|
1725
|
+
if (state === "pending")
|
|
1726
|
+
schedule(delivery);
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
const takeFault = (namespace) => {
|
|
1730
|
+
const queue = faults.get(namespace);
|
|
1731
|
+
const head = queue?.[0];
|
|
1732
|
+
if (!queue || !head)
|
|
1733
|
+
return void 0;
|
|
1734
|
+
head.remaining--;
|
|
1735
|
+
if (head.remaining <= 0)
|
|
1736
|
+
queue.shift();
|
|
1737
|
+
return head.mode;
|
|
1738
|
+
};
|
|
1739
|
+
const releaseHeld = (namespace) => {
|
|
1740
|
+
const waiting = held.get(namespace);
|
|
1741
|
+
if (!waiting)
|
|
1742
|
+
return;
|
|
1743
|
+
held.delete(namespace);
|
|
1744
|
+
for (const message of waiting)
|
|
1745
|
+
fanOut(message);
|
|
1746
|
+
};
|
|
1747
|
+
const hub = {
|
|
1748
|
+
publish(input) {
|
|
1749
|
+
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
1750
|
+
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
1751
|
+
const message = {
|
|
1752
|
+
id: input.id ?? id("msg_"),
|
|
1753
|
+
namespace: input.namespace,
|
|
1754
|
+
type: input.type,
|
|
1755
|
+
body,
|
|
1756
|
+
contentType,
|
|
1757
|
+
tags: input.tags ?? {},
|
|
1758
|
+
headers: input.headers ?? {},
|
|
1759
|
+
publishedAt: new Date(now()).toISOString()
|
|
1760
|
+
};
|
|
1761
|
+
messages.push(message);
|
|
1762
|
+
const ofNamespace = messages.filter((m) => m.namespace === message.namespace);
|
|
1763
|
+
const oldest = ofNamespace[0];
|
|
1764
|
+
if (ofNamespace.length > keep && oldest)
|
|
1765
|
+
messages.splice(messages.indexOf(oldest), 1);
|
|
1766
|
+
options.onMessage?.(message);
|
|
1767
|
+
const fault = takeFault(message.namespace);
|
|
1768
|
+
if (fault === "drop") {
|
|
1769
|
+
fanOut(message, "dropped");
|
|
1770
|
+
return message;
|
|
1771
|
+
}
|
|
1772
|
+
if (fault === "reorder") {
|
|
1773
|
+
held.set(message.namespace, [...held.get(message.namespace) ?? [], message]);
|
|
1774
|
+
return message;
|
|
1775
|
+
}
|
|
1776
|
+
fanOut(message);
|
|
1777
|
+
if (fault === "duplicate")
|
|
1778
|
+
fanOut(message);
|
|
1779
|
+
releaseHeld(message.namespace);
|
|
1780
|
+
return message;
|
|
1781
|
+
},
|
|
1782
|
+
setEndpoints(namespace, endpoints) {
|
|
1783
|
+
const withIds = endpoints.map((e, i) => ({ ...e, id: e.id ?? `we_${namespace}_${i}` }));
|
|
1784
|
+
own.set(namespace, withIds);
|
|
1785
|
+
return withIds;
|
|
1786
|
+
},
|
|
1787
|
+
endpoints: endpointsFor,
|
|
1788
|
+
messages: (namespace) => namespace === void 0 ? [...messages] : messages.filter((m) => m.namespace === namespace),
|
|
1789
|
+
deliveries: (namespace) => [...deliveries.values()].filter((d) => namespace === void 0 || d.namespace === namespace),
|
|
1790
|
+
async replay(id2) {
|
|
1791
|
+
const delivery = deliveries.get(id2);
|
|
1792
|
+
if (!delivery)
|
|
1793
|
+
return void 0;
|
|
1794
|
+
const ok = await attempt(delivery);
|
|
1795
|
+
if (ok)
|
|
1796
|
+
delivery.state = "delivered";
|
|
1797
|
+
return delivery;
|
|
1798
|
+
},
|
|
1799
|
+
async flush() {
|
|
1800
|
+
for (const namespace of [...held.keys()])
|
|
1801
|
+
releaseHeld(namespace);
|
|
1802
|
+
const waiting = [...pending.entries()];
|
|
1803
|
+
for (const [id2, timer] of waiting) {
|
|
1804
|
+
if (timer === void 0)
|
|
1805
|
+
continue;
|
|
1806
|
+
cancel(timer);
|
|
1807
|
+
pending.delete(id2);
|
|
1808
|
+
const delivery = deliveries.get(id2);
|
|
1809
|
+
if (!delivery)
|
|
1810
|
+
continue;
|
|
1811
|
+
track(attempt(delivery).then((ok) => {
|
|
1812
|
+
if (ok)
|
|
1813
|
+
delivery.state = "delivered";
|
|
1814
|
+
else
|
|
1815
|
+
schedule(delivery);
|
|
1816
|
+
}));
|
|
1817
|
+
}
|
|
1818
|
+
await hub.idle();
|
|
1819
|
+
},
|
|
1820
|
+
async idle() {
|
|
1821
|
+
while (inFlight2.size > 0)
|
|
1822
|
+
await Promise.allSettled([...inFlight2]);
|
|
1823
|
+
},
|
|
1824
|
+
fault(namespace, fault) {
|
|
1825
|
+
const queue = faults.get(namespace) ?? [];
|
|
1826
|
+
queue.push({ mode: fault.mode, remaining: Math.max(1, fault.count ?? 1) });
|
|
1827
|
+
faults.set(namespace, queue);
|
|
1828
|
+
},
|
|
1829
|
+
clear(namespace) {
|
|
1830
|
+
for (const [id2, delivery] of deliveries) {
|
|
1831
|
+
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
1832
|
+
continue;
|
|
1833
|
+
const timer = pending.get(id2);
|
|
1834
|
+
if (timer !== void 0)
|
|
1835
|
+
cancel(timer);
|
|
1836
|
+
pending.delete(id2);
|
|
1837
|
+
deliveries.delete(id2);
|
|
1838
|
+
payloads.delete(id2);
|
|
1839
|
+
}
|
|
1840
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1841
|
+
if (namespace === void 0 || messages[i]?.namespace === namespace)
|
|
1842
|
+
messages.splice(i, 1);
|
|
1843
|
+
}
|
|
1844
|
+
if (namespace === void 0) {
|
|
1845
|
+
held.clear();
|
|
1846
|
+
faults.clear();
|
|
1847
|
+
own.clear();
|
|
1848
|
+
} else {
|
|
1849
|
+
held.delete(namespace);
|
|
1850
|
+
faults.delete(namespace);
|
|
1851
|
+
own.delete(namespace);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
return hub;
|
|
1856
|
+
};
|
|
1857
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1858
|
+
var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
|
|
1859
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1860
|
+
var parseEndpoint = (value) => {
|
|
1861
|
+
if (!isRecord3(value) || typeof value.url !== "string")
|
|
1862
|
+
return "each endpoint needs a url";
|
|
1863
|
+
try {
|
|
1864
|
+
new URL(value.url);
|
|
1865
|
+
} catch {
|
|
1866
|
+
return `not a URL: ${value.url}`;
|
|
1867
|
+
}
|
|
1868
|
+
const endpoint = { url: value.url };
|
|
1869
|
+
if (typeof value.id === "string")
|
|
1870
|
+
endpoint.id = value.id;
|
|
1871
|
+
if (typeof value.secret === "string")
|
|
1872
|
+
endpoint.secret = value.secret;
|
|
1873
|
+
if (typeof value.signUrl === "string")
|
|
1874
|
+
endpoint.signUrl = value.signUrl;
|
|
1875
|
+
const events = value.events ?? value.enabledEvents;
|
|
1876
|
+
if (Array.isArray(events))
|
|
1877
|
+
endpoint.events = events.map(String);
|
|
1878
|
+
if (isRecord3(value.tags)) {
|
|
1879
|
+
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1880
|
+
}
|
|
1881
|
+
if (typeof value.account === "string")
|
|
1882
|
+
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1883
|
+
if (isRecord3(value.headers)) {
|
|
1884
|
+
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1885
|
+
}
|
|
1886
|
+
return endpoint;
|
|
1887
|
+
};
|
|
1888
|
+
var webhookAdminRoutes = (hub) => ({
|
|
1889
|
+
"GET /webhooks": ({ url, namespace }) => json2(200, {
|
|
1890
|
+
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1891
|
+
const type = url.searchParams.get("type");
|
|
1892
|
+
return type === null || d.type === type;
|
|
1893
|
+
})
|
|
1894
|
+
}),
|
|
1895
|
+
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1896
|
+
const type = url.searchParams.get("type");
|
|
1897
|
+
return json2(200, {
|
|
1898
|
+
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1899
|
+
});
|
|
1900
|
+
},
|
|
1901
|
+
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1902
|
+
const replayed = await hub.replay(params.id);
|
|
1903
|
+
return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
|
|
1904
|
+
},
|
|
1905
|
+
"POST /webhooks/flush": async () => {
|
|
1906
|
+
await hub.flush();
|
|
1907
|
+
return json2(200, { status: "ok" });
|
|
1908
|
+
},
|
|
1909
|
+
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1910
|
+
if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
1911
|
+
return adminError2(400, "mode must be duplicate, reorder or drop");
|
|
1912
|
+
}
|
|
1913
|
+
const fault = { mode: body.mode };
|
|
1914
|
+
if (typeof body.count === "number")
|
|
1915
|
+
fault.count = body.count;
|
|
1916
|
+
hub.fault(namespace, fault);
|
|
1917
|
+
return json2(201, { namespace, ...fault });
|
|
1918
|
+
},
|
|
1919
|
+
"GET /webhook-endpoints": ({ namespace }) => json2(200, {
|
|
1920
|
+
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1921
|
+
...rest,
|
|
1922
|
+
secret: secret ? "(set)" : null
|
|
1923
|
+
}))
|
|
1924
|
+
}),
|
|
1925
|
+
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
1926
|
+
const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
|
|
1927
|
+
if (!Array.isArray(list))
|
|
1928
|
+
return adminError2(400, "expected [{url, secret?, events?}]");
|
|
1929
|
+
const parsed = [];
|
|
1930
|
+
for (const each of list) {
|
|
1931
|
+
const endpoint = parseEndpoint(each);
|
|
1932
|
+
if (typeof endpoint === "string")
|
|
1933
|
+
return adminError2(400, endpoint);
|
|
1934
|
+
parsed.push(endpoint);
|
|
1935
|
+
}
|
|
1936
|
+
const set = hub.setEndpoints(namespace, parsed);
|
|
1937
|
+
return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
1938
|
+
},
|
|
1939
|
+
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
1940
|
+
hub.setEndpoints(namespace, []);
|
|
1941
|
+
return json2(200, { status: "ok" });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
var parsePayload = (message) => {
|
|
1945
|
+
if (message.contentType.startsWith("application/json")) {
|
|
1946
|
+
try {
|
|
1947
|
+
return JSON.parse(message.body);
|
|
1948
|
+
} catch {
|
|
1949
|
+
return message.body;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
|
|
1953
|
+
return Object.fromEntries(new URLSearchParams(message.body));
|
|
1954
|
+
}
|
|
1955
|
+
return message.body;
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
// ../core/dist/runtime.js
|
|
1959
|
+
var MOCKINGBIRD_HEADER = "x-mockingbird";
|
|
1960
|
+
var BRANCH_HEADER = "x-mockingbird-branch";
|
|
1961
|
+
var AT_HEADER = "x-mockingbird-at";
|
|
1962
|
+
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
1963
|
+
var DEFAULT_NAMESPACE = "default";
|
|
1964
|
+
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1965
|
+
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
1966
|
+
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1967
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1968
|
+
var effects = /* @__PURE__ */ new WeakMap();
|
|
1969
|
+
var reuseSorted = (fresh, previous, compare, equal) => {
|
|
1970
|
+
if (!previous || previous.length === 0)
|
|
1971
|
+
return fresh.map((row) => Object.freeze(row));
|
|
1972
|
+
const result = new Array(fresh.length);
|
|
1973
|
+
let unchanged = fresh.length === previous.length;
|
|
1974
|
+
let oldIndex = 0;
|
|
1975
|
+
for (let index = 0; index < fresh.length; index++) {
|
|
1976
|
+
const row = fresh[index];
|
|
1977
|
+
while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
|
|
1978
|
+
oldIndex++;
|
|
1979
|
+
}
|
|
1980
|
+
const old = previous[oldIndex];
|
|
1981
|
+
result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
|
|
1982
|
+
if (result[index] !== previous[index])
|
|
1983
|
+
unchanged = false;
|
|
1984
|
+
}
|
|
1985
|
+
return unchanged ? previous : result;
|
|
1986
|
+
};
|
|
1987
|
+
var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
|
|
1988
|
+
var faultEffect = (request, name) => faultEffects(request).find((e) => e.name === name)?.params;
|
|
1989
|
+
var DroppedConnectionError = class extends TypeError {
|
|
1990
|
+
code = "MOCKINGBIRD_DROP";
|
|
1991
|
+
constructor() {
|
|
1992
|
+
super("fetch failed: connection dropped by Mockingbird fault");
|
|
1993
|
+
this.name = "TypeError";
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
var operationMatcher = (document2) => {
|
|
1997
|
+
const matchers = listOperations(document2).map((operation) => ({
|
|
1998
|
+
operationId: operation.operationId,
|
|
1999
|
+
method: operation.method.toUpperCase(),
|
|
2000
|
+
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
2001
|
+
params: (operation.path.match(/\{/g) ?? []).length
|
|
2002
|
+
})).sort((a, b) => a.params - b.params);
|
|
2003
|
+
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
2004
|
+
};
|
|
2005
|
+
var createRuntime = (options) => {
|
|
2006
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
2007
|
+
const clock = options.clock ?? createClock();
|
|
2008
|
+
const rng = createRng(options.seed ?? 0);
|
|
2009
|
+
const wallNow = options.io?.wallNow ?? Date.now;
|
|
2010
|
+
const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
|
|
2011
|
+
const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2012
|
+
const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
|
|
2013
|
+
const metrics = createMetrics();
|
|
2014
|
+
const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
|
|
2015
|
+
const version = options.version ?? PACKAGE_VERSION;
|
|
2016
|
+
const instances = /* @__PURE__ */ new Map();
|
|
2017
|
+
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
2018
|
+
const branchRngs = /* @__PURE__ */ new Map();
|
|
2019
|
+
const timelines = /* @__PURE__ */ new Map();
|
|
2020
|
+
const branchStorage = /* @__PURE__ */ new Map();
|
|
2021
|
+
const captured = /* @__PURE__ */ new Map();
|
|
2022
|
+
const credentials = createCredentialRegistry();
|
|
2023
|
+
const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
|
|
2024
|
+
const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
|
|
2025
|
+
const instanceFor = (key, publicNamespace = key, isolatedRng) => {
|
|
2026
|
+
const existing = instances.get(key);
|
|
2027
|
+
if (existing)
|
|
2028
|
+
return existing;
|
|
2029
|
+
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
2030
|
+
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
2031
|
+
}
|
|
2032
|
+
const created = options.create({
|
|
2033
|
+
namespace: storageNamespace(key),
|
|
2034
|
+
publicNamespace,
|
|
2035
|
+
sqlite,
|
|
2036
|
+
clock,
|
|
2037
|
+
rng: isolatedRng ?? rng
|
|
2038
|
+
});
|
|
2039
|
+
instances.set(key, created);
|
|
2040
|
+
publicNamespaces.add(publicNamespace);
|
|
2041
|
+
if (isolatedRng)
|
|
2042
|
+
branchRngs.set(key, isolatedRng);
|
|
2043
|
+
return created;
|
|
2044
|
+
};
|
|
2045
|
+
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
2046
|
+
const capture = (storage) => {
|
|
2047
|
+
const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
|
|
2048
|
+
const previous = captured.get(storage);
|
|
2049
|
+
const snapshot2 = {
|
|
2050
|
+
namespace: fresh.namespace,
|
|
2051
|
+
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),
|
|
2052
|
+
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)
|
|
2053
|
+
};
|
|
2054
|
+
Object.freeze(snapshot2.records);
|
|
2055
|
+
Object.freeze(snapshot2.sequences);
|
|
2056
|
+
Object.freeze(snapshot2);
|
|
2057
|
+
captured.set(storage, snapshot2);
|
|
2058
|
+
return Object.freeze({
|
|
2059
|
+
snapshot: snapshot2,
|
|
2060
|
+
clock: Object.freeze(clock.state()),
|
|
2061
|
+
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
2062
|
+
});
|
|
2063
|
+
};
|
|
2064
|
+
const timeline = (name = DEFAULT_NAMESPACE) => {
|
|
2065
|
+
let found = timelines.get(name);
|
|
2066
|
+
if (found)
|
|
2067
|
+
return found;
|
|
2068
|
+
instance(name);
|
|
2069
|
+
found = new Timeline({
|
|
2070
|
+
now: clock.now,
|
|
2071
|
+
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
2072
|
+
});
|
|
2073
|
+
found.commit(capture(name));
|
|
2074
|
+
timelines.set(name, found);
|
|
2075
|
+
return found;
|
|
2076
|
+
};
|
|
2077
|
+
const physicalBranch = (namespace, branch2) => {
|
|
2078
|
+
if (branch2 === "main")
|
|
2079
|
+
return namespace;
|
|
2080
|
+
const mapKey = `${namespace}\0${branch2}`;
|
|
2081
|
+
const existing = branchStorage.get(mapKey);
|
|
2082
|
+
if (existing)
|
|
2083
|
+
return existing;
|
|
2084
|
+
const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
|
|
2085
|
+
branchStorage.set(mapKey, key);
|
|
2086
|
+
return key;
|
|
2087
|
+
};
|
|
2088
|
+
const ensureBranch = (namespace, branch2, at) => {
|
|
2089
|
+
if (!BRANCH_PATTERN2.test(branch2))
|
|
2090
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
2091
|
+
const history = timeline(namespace);
|
|
2092
|
+
if (branch2 === "main") {
|
|
2093
|
+
if (at !== void 0) {
|
|
2094
|
+
const point = history.checkout("main", at);
|
|
2095
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
2096
|
+
captured.set(namespace, point.value.snapshot);
|
|
2097
|
+
rng.setState(point.value.rngState);
|
|
2098
|
+
clock.set(point.value.clock.now);
|
|
2099
|
+
if (point.value.clock.frozen)
|
|
2100
|
+
clock.freeze();
|
|
2101
|
+
else
|
|
2102
|
+
clock.unfreeze();
|
|
2103
|
+
}
|
|
2104
|
+
return namespace;
|
|
2105
|
+
}
|
|
2106
|
+
const storage = physicalBranch(namespace, branch2);
|
|
2107
|
+
if (!history.hasBranch(branch2)) {
|
|
2108
|
+
if (at === void 0)
|
|
2109
|
+
history.commit(capture(namespace));
|
|
2110
|
+
const point = history.fork(branch2, at === void 0 ? {} : { from: at });
|
|
2111
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
2112
|
+
if (point)
|
|
2113
|
+
branchRng.setState(point.value.rngState);
|
|
2114
|
+
instanceFor(storage, namespace, branchRng);
|
|
2115
|
+
if (point)
|
|
2116
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2117
|
+
if (point)
|
|
2118
|
+
captured.set(storage, point.value.snapshot);
|
|
2119
|
+
} else if (at !== void 0 && history.head(branch2)?.id !== at) {
|
|
2120
|
+
const point = history.checkout(branch2, at);
|
|
2121
|
+
if (!instances.has(storage)) {
|
|
2122
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
2123
|
+
branchRng.setState(point.value.rngState);
|
|
2124
|
+
instanceFor(storage, namespace, branchRng);
|
|
2125
|
+
}
|
|
2126
|
+
branchRngs.get(storage)?.setState(point.value.rngState);
|
|
2127
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2128
|
+
captured.set(storage, point.value.snapshot);
|
|
2129
|
+
} else {
|
|
2130
|
+
if (!instances.has(storage)) {
|
|
2131
|
+
const point = history.head(branch2);
|
|
2132
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
2133
|
+
if (point)
|
|
2134
|
+
branchRng.setState(point.value.rngState);
|
|
2135
|
+
instanceFor(storage, namespace, branchRng);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return storage;
|
|
2139
|
+
};
|
|
2140
|
+
const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
|
|
2141
|
+
const storage = ensureBranch(namespace, branch2);
|
|
2142
|
+
return timeline(namespace).commit(capture(storage), { branch: branch2 });
|
|
2143
|
+
};
|
|
2144
|
+
const branch = (name, branchOptions = {}) => {
|
|
2145
|
+
const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
2146
|
+
ensureBranch(namespace, name, branchOptions.at);
|
|
2147
|
+
const head = timeline(namespace).head(name);
|
|
2148
|
+
if (!head)
|
|
2149
|
+
throw new RangeError(`branch ${name} has no checkpoint`);
|
|
2150
|
+
return head;
|
|
2151
|
+
};
|
|
2152
|
+
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
2153
|
+
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
2154
|
+
const branchName = checkoutOptions.branch ?? "main";
|
|
2155
|
+
const history = timeline(namespace);
|
|
2156
|
+
const point = history.checkout(branchName, checkpointId);
|
|
2157
|
+
const storage = ensureBranch(namespace, branchName);
|
|
2158
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2159
|
+
captured.set(storage, point.value.snapshot);
|
|
2160
|
+
clock.set(point.value.clock.now);
|
|
2161
|
+
if (point.value.clock.frozen)
|
|
2162
|
+
clock.freeze();
|
|
2163
|
+
else
|
|
2164
|
+
clock.unfreeze();
|
|
2165
|
+
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
2166
|
+
};
|
|
2167
|
+
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
2168
|
+
if (name === "*") {
|
|
2169
|
+
options.webhooks?.clear();
|
|
2170
|
+
for (const each of instances.values())
|
|
2171
|
+
await each.reset();
|
|
2172
|
+
timelines.clear();
|
|
2173
|
+
branchStorage.clear();
|
|
2174
|
+
branchRngs.clear();
|
|
2175
|
+
captured.clear();
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
options.webhooks?.clear(name);
|
|
2179
|
+
const target = instances.get(name);
|
|
2180
|
+
if (target)
|
|
2181
|
+
await target.reset();
|
|
2182
|
+
else
|
|
2183
|
+
clearNamespace(sqlite, storageNamespace(name));
|
|
2184
|
+
for (const [mapping, storage] of branchStorage) {
|
|
2185
|
+
if (!mapping.startsWith(`${name}\0`))
|
|
2186
|
+
continue;
|
|
2187
|
+
const branchInstance = instances.get(storage);
|
|
2188
|
+
if (branchInstance)
|
|
2189
|
+
await branchInstance.reset();
|
|
2190
|
+
else
|
|
2191
|
+
clearNamespace(sqlite, storageNamespace(storage));
|
|
2192
|
+
branchStorage.delete(mapping);
|
|
2193
|
+
branchRngs.delete(storage);
|
|
2194
|
+
captured.delete(storage);
|
|
2195
|
+
}
|
|
2196
|
+
timelines.delete(name);
|
|
2197
|
+
captured.delete(name);
|
|
2198
|
+
};
|
|
2199
|
+
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
2200
|
+
return checkpoint(name, "main").value.snapshot;
|
|
2201
|
+
};
|
|
2202
|
+
const restore = (from, name = DEFAULT_NAMESPACE) => {
|
|
2203
|
+
instance(name);
|
|
2204
|
+
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
2205
|
+
captured.set(name, from);
|
|
2206
|
+
const history = timelines.get(name);
|
|
2207
|
+
if (history)
|
|
2208
|
+
history.commit(capture(name), { branch: "main" });
|
|
2209
|
+
else
|
|
2210
|
+
timeline(name);
|
|
2211
|
+
};
|
|
2212
|
+
const runtime = {
|
|
2213
|
+
name: options.name,
|
|
2214
|
+
sqlite,
|
|
2215
|
+
clock,
|
|
2216
|
+
faults,
|
|
2217
|
+
metrics,
|
|
2218
|
+
journal,
|
|
2219
|
+
rng,
|
|
2220
|
+
credentials,
|
|
2221
|
+
webhooks: options.webhooks,
|
|
2222
|
+
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
2223
|
+
const preset = options.presets?.[name];
|
|
2224
|
+
if (!preset)
|
|
2225
|
+
throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
|
|
2226
|
+
const added = (preset.rules ?? []).map((rule, index) => faults.add({
|
|
2227
|
+
namespace,
|
|
2228
|
+
...rule,
|
|
2229
|
+
...overrides,
|
|
2230
|
+
preset: name,
|
|
2231
|
+
id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
|
|
2232
|
+
}));
|
|
2233
|
+
if (preset.webhook && options.webhooks) {
|
|
2234
|
+
options.webhooks.fault(namespace, {
|
|
2235
|
+
...preset.webhook,
|
|
2236
|
+
...overrides.count !== void 0 ? { count: overrides.count } : {}
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
return added;
|
|
2240
|
+
},
|
|
2241
|
+
instance,
|
|
2242
|
+
namespaces: () => [...publicNamespaces].sort(),
|
|
2243
|
+
reset,
|
|
2244
|
+
snapshot,
|
|
2245
|
+
restore,
|
|
2246
|
+
checkpoint,
|
|
2247
|
+
branch,
|
|
2248
|
+
checkout,
|
|
2249
|
+
timeline,
|
|
2250
|
+
fetch: async (incoming) => {
|
|
2251
|
+
let request = incoming;
|
|
2252
|
+
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
2253
|
+
if (prefixed) {
|
|
2254
|
+
const url2 = new URL(request.url);
|
|
2255
|
+
url2.pathname = prefixed[2] ?? "/";
|
|
2256
|
+
const headers = new Headers(request.headers);
|
|
2257
|
+
if (!headers.has(NAMESPACE_HEADER)) {
|
|
2258
|
+
headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
|
|
2259
|
+
}
|
|
2260
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
2261
|
+
request = new Request(url2, {
|
|
2262
|
+
method: request.method,
|
|
2263
|
+
headers,
|
|
2264
|
+
...hasBody ? { body: await request.arrayBuffer() } : {},
|
|
2265
|
+
signal: request.signal
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
let namespace = control.namespaceOf(request);
|
|
2269
|
+
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
2270
|
+
const credential = options.credential(request);
|
|
2271
|
+
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
2272
|
+
if (mapped !== void 0)
|
|
2273
|
+
namespace = mapped;
|
|
2274
|
+
}
|
|
2275
|
+
const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
|
|
2276
|
+
const at = request.headers.get(AT_HEADER) ?? void 0;
|
|
2277
|
+
const stamp = (response2) => {
|
|
2278
|
+
const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
|
|
2279
|
+
try {
|
|
2280
|
+
response2.headers.set(MOCKINGBIRD_HEADER, value);
|
|
2281
|
+
return response2;
|
|
2282
|
+
} catch {
|
|
2283
|
+
const copy = new Response(response2.body, response2);
|
|
2284
|
+
copy.headers.set(MOCKINGBIRD_HEADER, value);
|
|
2285
|
+
return copy;
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
const handled = await control.handle(request);
|
|
2289
|
+
if (handled)
|
|
2290
|
+
return stamp(handled);
|
|
2291
|
+
const started = monotonicNow();
|
|
2292
|
+
const url = new URL(request.url);
|
|
2293
|
+
const operationId = operationIdFor(request, url.pathname);
|
|
2294
|
+
const log = (status, faultId, response2) => {
|
|
2295
|
+
const noted = response2 ? responseNotes(response2) : void 0;
|
|
2296
|
+
const entry = {
|
|
2297
|
+
service: options.name,
|
|
2298
|
+
namespace,
|
|
2299
|
+
operationId,
|
|
2300
|
+
method: request.method,
|
|
2301
|
+
path: url.pathname,
|
|
2302
|
+
status,
|
|
2303
|
+
durationMs: Math.round((monotonicNow() - started) * 100) / 100,
|
|
2304
|
+
unmatched: options.document !== void 0 && operationId === void 0,
|
|
2305
|
+
...faultId !== void 0 ? { faultId } : {},
|
|
2306
|
+
...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
|
|
2307
|
+
...noted?.adopted ? { adopted: true } : {}
|
|
2308
|
+
};
|
|
2309
|
+
metrics.record(entry);
|
|
2310
|
+
journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
|
|
2311
|
+
options.onLog?.(entry);
|
|
2312
|
+
};
|
|
2313
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
2314
|
+
log(400);
|
|
2315
|
+
return stamp(new Response(JSON.stringify({
|
|
2316
|
+
error: {
|
|
2317
|
+
type: "mockingbird_admin",
|
|
2318
|
+
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
2319
|
+
}
|
|
2320
|
+
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
2321
|
+
}
|
|
2322
|
+
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
2323
|
+
log(400);
|
|
2324
|
+
return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
|
|
2325
|
+
}
|
|
2326
|
+
let storage;
|
|
2327
|
+
try {
|
|
2328
|
+
if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
|
|
2329
|
+
const point = timeline(namespace).get(at);
|
|
2330
|
+
storage = physicalBranch(namespace, `at_${at}`);
|
|
2331
|
+
let viewRng = branchRngs.get(storage);
|
|
2332
|
+
if (!viewRng) {
|
|
2333
|
+
viewRng = createRng(options.seed ?? 0);
|
|
2334
|
+
instanceFor(storage, namespace, viewRng);
|
|
2335
|
+
}
|
|
2336
|
+
viewRng.setState(point.value.rngState);
|
|
2337
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2338
|
+
captured.set(storage, point.value.snapshot);
|
|
2339
|
+
} else {
|
|
2340
|
+
storage = ensureBranch(namespace, selectedBranch, at);
|
|
2341
|
+
}
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
log(409);
|
|
2344
|
+
return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
|
|
2345
|
+
}
|
|
2346
|
+
const hits = await faults.take({
|
|
2347
|
+
operationId,
|
|
2348
|
+
method: request.method,
|
|
2349
|
+
path: url.pathname,
|
|
2350
|
+
namespace
|
|
2351
|
+
});
|
|
2352
|
+
const final = hits.find((hit) => hit.drop || hit.response);
|
|
2353
|
+
if (final?.drop) {
|
|
2354
|
+
log(0, final.id);
|
|
2355
|
+
throw new DroppedConnectionError();
|
|
2356
|
+
}
|
|
2357
|
+
if (final?.response) {
|
|
2358
|
+
log(final.response.status, final.id);
|
|
2359
|
+
return stamp(final.response);
|
|
2360
|
+
}
|
|
2361
|
+
const fired = hits.filter((hit) => hit.effect !== void 0);
|
|
2362
|
+
if (fired.length > 0)
|
|
2363
|
+
effects.set(request, fired.map((hit) => hit.effect));
|
|
2364
|
+
let response = await instanceFor(storage, namespace).fetch(request);
|
|
2365
|
+
if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
|
|
2366
|
+
const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
|
|
2367
|
+
response = mutableResponse(response);
|
|
2368
|
+
response.headers.set(CHECKPOINT_HEADER, point.id);
|
|
2369
|
+
}
|
|
2370
|
+
if (selectedBranch !== "main") {
|
|
2371
|
+
response = mutableResponse(response);
|
|
2372
|
+
response.headers.set(BRANCH_HEADER, selectedBranch);
|
|
2373
|
+
}
|
|
2374
|
+
if (at !== void 0) {
|
|
2375
|
+
response = mutableResponse(response);
|
|
2376
|
+
response.headers.set(AT_HEADER, at);
|
|
2377
|
+
}
|
|
2378
|
+
log(response.status, fired[0]?.id, response);
|
|
2379
|
+
return stamp(response);
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
const control = createControlPlane({
|
|
2383
|
+
name: options.name,
|
|
2384
|
+
startedAt: wallNow(),
|
|
2385
|
+
wallNow,
|
|
2386
|
+
clock,
|
|
2387
|
+
faults,
|
|
2388
|
+
metrics,
|
|
2389
|
+
journal,
|
|
2390
|
+
defaultNamespace: DEFAULT_NAMESPACE,
|
|
2391
|
+
namespaces: runtime.namespaces,
|
|
2392
|
+
reset,
|
|
2393
|
+
timeTravel: {
|
|
2394
|
+
checkpoint: (name, branchName) => {
|
|
2395
|
+
const point = checkpoint(name, branchName);
|
|
2396
|
+
return {
|
|
2397
|
+
id: point.id,
|
|
2398
|
+
branch: point.branch,
|
|
2399
|
+
parent: point.parent,
|
|
2400
|
+
at: point.at,
|
|
2401
|
+
records: point.value.snapshot.records.length
|
|
2402
|
+
};
|
|
2403
|
+
},
|
|
2404
|
+
branch: (branchName, branchOptions) => {
|
|
2405
|
+
const point = branch(branchName, branchOptions);
|
|
2406
|
+
return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
|
|
2407
|
+
},
|
|
2408
|
+
checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
|
|
2409
|
+
retain: (name, checkpointId) => {
|
|
2410
|
+
timeline(name).retain(checkpointId);
|
|
2411
|
+
},
|
|
2412
|
+
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
2413
|
+
inspect: (name) => {
|
|
2414
|
+
const history = timeline(name);
|
|
2415
|
+
return {
|
|
2416
|
+
branches: history.branches(),
|
|
2417
|
+
checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
2418
|
+
id,
|
|
2419
|
+
branch: branchName,
|
|
2420
|
+
parent,
|
|
2421
|
+
at
|
|
2422
|
+
}))
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
},
|
|
2426
|
+
describe: options.describe ?? (() => ({})),
|
|
2427
|
+
...options.presets ? {
|
|
2428
|
+
applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
|
|
2429
|
+
} : {},
|
|
2430
|
+
routes: {
|
|
2431
|
+
...credentialRoutes(credentials),
|
|
2432
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
2433
|
+
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
2434
|
+
...options.admin?.(runtime) ?? {}
|
|
2435
|
+
},
|
|
2436
|
+
adminKey: options.adminKey
|
|
2437
|
+
});
|
|
2438
|
+
return runtime;
|
|
2439
|
+
};
|
|
2440
|
+
var mutableResponse = (response) => {
|
|
2441
|
+
try {
|
|
2442
|
+
response.headers.set("x-mockingbird-mutable-probe", "1");
|
|
2443
|
+
response.headers.delete("x-mockingbird-mutable-probe");
|
|
2444
|
+
return response;
|
|
2445
|
+
} catch {
|
|
2446
|
+
return new Response(response.body, response);
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2450
|
+
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
2451
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2452
|
+
var credentialRoutes = (registry) => ({
|
|
2453
|
+
"GET /credentials": () => adminJson(200, {
|
|
2454
|
+
credentials: registry.entries().map(({ credential, namespace }) => ({
|
|
2455
|
+
credential: maskCredential(credential),
|
|
2456
|
+
namespace
|
|
2457
|
+
}))
|
|
2458
|
+
}),
|
|
2459
|
+
"PUT /credentials": ({ body, namespace }) => {
|
|
2460
|
+
const pairs = [];
|
|
2461
|
+
const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
|
|
2462
|
+
if (Array.isArray(list)) {
|
|
2463
|
+
for (const each of list) {
|
|
2464
|
+
if (typeof each === "string")
|
|
2465
|
+
pairs.push([each, namespace]);
|
|
2466
|
+
else if (isObject(each) && typeof each.credential === "string") {
|
|
2467
|
+
pairs.push([
|
|
2468
|
+
each.credential,
|
|
2469
|
+
typeof each.namespace === "string" ? each.namespace : namespace
|
|
2470
|
+
]);
|
|
2471
|
+
} else
|
|
2472
|
+
return adminFail(400, "each entry is a credential string or {credential, namespace}");
|
|
2473
|
+
}
|
|
2474
|
+
} else if (isObject(list)) {
|
|
2475
|
+
for (const [credential, target] of Object.entries(list)) {
|
|
2476
|
+
if (typeof target !== "string")
|
|
2477
|
+
return adminFail(400, `namespace for ${credential} must be a string`);
|
|
2478
|
+
pairs.push([credential, target]);
|
|
2479
|
+
}
|
|
2480
|
+
} else if (isObject(body) && typeof body.credential === "string") {
|
|
2481
|
+
pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
|
|
2482
|
+
} else {
|
|
2483
|
+
return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
|
|
2484
|
+
}
|
|
2485
|
+
for (const [credential, target] of pairs) {
|
|
2486
|
+
if (!NAMESPACE_PATTERN.test(target))
|
|
2487
|
+
return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
|
|
2488
|
+
registry.set(credential, target);
|
|
2489
|
+
}
|
|
2490
|
+
return adminJson(200, { mapped: pairs.length });
|
|
2491
|
+
},
|
|
2492
|
+
"DELETE /credentials": ({ url }) => {
|
|
2493
|
+
const credential = url.searchParams.get("credential");
|
|
2494
|
+
if (credential === null)
|
|
2495
|
+
registry.clear();
|
|
2496
|
+
else
|
|
2497
|
+
registry.remove(credential);
|
|
2498
|
+
return adminJson(200, { status: "ok" });
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
var presetRoutes = (presets, runtime) => ({
|
|
2502
|
+
"GET /faults/presets": () => adminJson(200, {
|
|
2503
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
2504
|
+
}),
|
|
2505
|
+
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
2506
|
+
const name = params.name;
|
|
2507
|
+
if (!presets[name])
|
|
2508
|
+
return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
|
|
2509
|
+
const overrides = isObject(body) ? body : {};
|
|
2510
|
+
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
// ../core/dist/validation.js
|
|
2515
|
+
var bodyIssues = (context, contentType = "application/json") => {
|
|
2516
|
+
const requestBody = context.operation.operation.requestBody;
|
|
2517
|
+
if (!requestBody)
|
|
2518
|
+
return [];
|
|
2519
|
+
const resolved = deref(context.document, requestBody);
|
|
2520
|
+
const schema = resolved.content?.[contentType]?.schema;
|
|
2521
|
+
if (!schema)
|
|
2522
|
+
return [];
|
|
2523
|
+
const value = context.body.kind === "json" || context.body.kind === "form" ? context.body.value : void 0;
|
|
2524
|
+
if (context.body.kind === "invalid") {
|
|
2525
|
+
return [{ path: "", message: `request body is not valid ${context.body.mediaType}` }];
|
|
2526
|
+
}
|
|
2527
|
+
if (value === void 0) {
|
|
2528
|
+
return resolved.required ? [{ path: "", message: "request body is required" }] : [];
|
|
2529
|
+
}
|
|
2530
|
+
return validateValue(context.document, resolveSchema(context.document, schema), value).map((issue) => ({ path: issue.path.join("."), message: issue.message }));
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// src/generated/openapi.ts
|
|
2534
|
+
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Intercom REST API 2.11 (Mockingbird subset)","description":"Stateful mock subset of the Intercom REST API (version 2.11): contacts (search, create,\\nupdate, get), conversations (create, update, reply as user or admin with attachments,\\nclose/open/snooze/assign, get, search) and the admin identity endpoints. Covers the backend's\\nmember messaging adapter and the Intercom sync adapter.\\n","version":"2.11","x-mockingbird-upstream":{"note":"Trimmed from Intercom's published 2.11 API reference to the operations and fields our consumers use (intercom-messaging.adapter.ts, intercom-api.adapter.ts) and the webhook receivers (messaging.controller.ts, the EMR IntercomWebhookService)."}},"servers":[{"url":"https://api.intercom.io"}],"security":[{"bearerAuth":[]}],"paths":{"/contacts/search":{"post":{"operationId":"SearchContacts","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"$ref":"#/components/schemas/ContactQuery"},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"responses":{"200":{"description":"Matching contacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/contacts":{"post":{"operationId":"CreateContact","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"}}}},"responses":{"200":{"description":"The created contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/contacts/{contact_id}":{"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"contact","missing":0}}}],"get":{"operationId":"GetContact","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"UpdateContact","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"responses":{"200":{"description":"The updated contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/conversations":{"post":{"operationId":"CreateConversation","description":"A contact-initiated conversation. An optional \`Idempotency-Key\` header replays.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,40}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["from","body"],"properties":{"from":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string","enum":["user","lead","contact"]},"id":{"type":"string","x-mockingbird-resource-ref":{"type":"contact","missing":0}}}},"body":{"type":"string","minLength":1,"maxLength":400},"created_at":{"type":"integer","minimum":1600000000,"maximum":1900000000}}}}}},"responses":{"200":{"description":"The first message of the new conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/conversations/search":{"post":{"operationId":"SearchConversations","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/DisplayAs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"$ref":"#/components/schemas/ConversationQuery"},"pagination":{"$ref":"#/components/schemas/Pagination"},"sort_field":{"type":"string","enum":["updated_at","created_at","id","waiting_since"]},"sort_order":{"type":"string","enum":["ascending","descending"]}}}}}},"responses":{"200":{"description":"A page of matching conversations (without conversation parts)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/conversations/{conversation_id}":{"parameters":[{"$ref":"#/components/parameters/ConversationId"}],"get":{"operationId":"GetConversation","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/DisplayAs"}],"responses":{"200":{"description":"The conversation with its parts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"UpdateConversation","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"$ref":"#/components/parameters/DisplayAs"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"read":{"type":"boolean"},"title":{"type":"string","maxLength":120},"custom_attributes":{"type":"object","maxProperties":4,"additionalProperties":{"type":["string","number","boolean","null"]}}}}}}},"responses":{"200":{"description":"The updated conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/conversations/{conversation_id}/reply":{"parameters":[{"$ref":"#/components/parameters/ConversationId"}],"post":{"operationId":"ReplyConversation","description":"Reply as the contact (\`type: user\` + \`intercom_user_id\`) or as an admin (\`type: admin\` + \`admin_id\`), as JSON (attachments as base64 \`attachment_files\`) or multipart (\`attachment_files[]\` file parts). Admin comments fire \`conversation.admin.replied\`.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplyBody"}},"multipart/form-data":{"schema":{"type":"object","required":["message_type","type"],"properties":{"message_type":{"type":"string"},"type":{"type":"string"},"intercom_user_id":{"type":"string"},"admin_id":{"type":"string"},"body":{"type":"string"},"attachment_files[]":{"type":"array","items":{"type":"string","format":"binary"}}}}}}},"responses":{"200":{"description":"The conversation, with the new part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/conversations/{conversation_id}/parts":{"parameters":[{"$ref":"#/components/parameters/ConversationId"}],"post":{"operationId":"ManageConversation","description":"Close, open, snooze or assign; close and open fire \`conversation.admin.closed\` / \`opened\`.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message_type","type","admin_id"],"properties":{"message_type":{"type":"string","enum":["close","open","snoozed","assignment"]},"type":{"type":"string","enum":["admin"]},"admin_id":{"type":"string","x-mockingbird-resource-ref":{"type":"admin","missing":"9999999"}},"body":{"type":"string","maxLength":400},"snoozed_until":{"type":"integer","minimum":1600000000,"maximum":1900000000},"assignee_id":{"type":"string","x-mockingbird-resource-ref":{"type":"admin","missing":"9999999"}}}}}}},"responses":{"200":{"description":"The conversation, with the new part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/admins":{"get":{"operationId":"ListAdmins","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The workspace's admins","content":{"application/json":{"schema":{"type":"object","required":["type","admins"],"properties":{"type":{"type":"string","enum":["admin.list"]},"admins":{"type":"array","items":{"$ref":"#/components/schemas/Admin"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/me":{"get":{"operationId":"GetMe","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The admin that owns the access token","content":{"application/json":{"schema":{"type":"object","required":["type","id","email","app"],"properties":{"type":{"type":"string","enum":["admin"]},"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"email_verified":{"type":"boolean"},"has_inbox_seat":{"type":"boolean"},"avatar":{"type":"object"},"app":{"type":"object","required":["type","id_code"],"properties":{"type":{"type":"string"},"id_code":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"integer"},"secure":{"type":"boolean"},"identity_verification":{"type":"boolean"},"timezone":{"type":"string"},"region":{"type":"string"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"ConversationId":{"name":"conversation_id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"conversation","missing":"999999999999999"}}},"DisplayAs":{"name":"display_as","in":"query","schema":{"type":"string","enum":["plaintext"]}}},"responses":{"BadRequest":{"description":"Invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorList"}}}},"Unauthorized":{"description":"Missing or invalid access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorList"}}}},"NotFound":{"description":"Unknown resource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorList"}}}},"Conflict":{"description":"A contact with that external_id or email already exists (or an idempotency conflict)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorList"}}}}},"schemas":{"ErrorList":{"type":"object","required":["type","errors"],"properties":{"type":{"type":"string","enum":["error.list"]},"request_id":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"errors":{"type":"array","minItems":1,"items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"Pagination":{"type":"object","properties":{"per_page":{"type":"integer","minimum":1,"maximum":150},"starting_after":{"type":"string","maxLength":80}}},"ContactFilter":{"type":"object","required":["field","operator","value"],"properties":{"field":{"type":"string","enum":["external_id","email","id","name","role","phone"]},"operator":{"type":"string","enum":["=","!=","~","!~","^","$","IN","NIN"]},"value":{"oneOf":[{"type":"string","maxLength":80},{"type":"array","maxItems":3,"items":{"type":"string","maxLength":80}}]}}},"ContactQuery":{"oneOf":[{"$ref":"#/components/schemas/ContactFilter"},{"type":"object","required":["operator","value"],"properties":{"operator":{"type":"string","enum":["AND","OR"]},"value":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/ContactFilter"}}}}]},"ConversationFilter":{"type":"object","required":["field","operator","value"],"properties":{"field":{"type":"string","enum":["id","contact_ids","admin_assignee_id","team_assignee_id","state","open","read","created_at","updated_at","source.author.email","source.delivered_as"]},"operator":{"type":"string","enum":["=","!=","<",">","~","IN","NIN"]},"value":{"oneOf":[{"type":"string","maxLength":80},{"type":"integer"},{"type":"boolean"},{"type":"array","maxItems":3,"items":{"type":"string","maxLength":80}}]}}},"ConversationQuery":{"oneOf":[{"$ref":"#/components/schemas/ConversationFilter"},{"type":"object","required":["operator","value"],"properties":{"operator":{"type":"string","enum":["AND","OR"]},"value":{"type":"array","minItems":1,"maxItems":3,"items":{"$ref":"#/components/schemas/ConversationFilter"}}}}]},"CustomAttributes":{"type":"object","maxProperties":30,"additionalProperties":{"type":["string","number","boolean","null"]}},"ContactCreate":{"type":"object","properties":{"role":{"type":"string","enum":["user","lead"]},"external_id":{"description":"Any string. The first branch steers the walk generator toward a few values, so random walks reach the duplicate (409) path.","anyOf":[{"type":"string","pattern":"^[1-4]$"},{"type":"string","minLength":1,"maxLength":64}]},"email":{"type":"string","format":"email","maxLength":120},"name":{"type":["string","null"],"maxLength":80},"phone":{"type":["string","null"],"maxLength":40},"signed_up_at":{"type":["integer","null"],"minimum":1000000000,"maximum":1900000000},"last_seen_at":{"type":["integer","null"],"minimum":1000000000,"maximum":1900000000},"custom_attributes":{"$ref":"#/components/schemas/CustomAttributes"}}},"ContactUpdate":{"allOf":[{"$ref":"#/components/schemas/ContactCreate"}]},"ReplyBody":{"type":"object","required":["message_type","type"],"properties":{"message_type":{"type":"string","enum":["comment","note","quick_reply"]},"type":{"type":"string","enum":["user","admin"]},"body":{"type":"string","maxLength":400},"intercom_user_id":{"type":"string","x-mockingbird-resource-ref":{"type":"contact","missing":0}},"user_id":{"type":"string","maxLength":64},"email":{"type":"string","maxLength":120},"admin_id":{"type":"string","x-mockingbird-resource-ref":{"type":"admin","missing":"9999999"}},"created_at":{"type":"integer","minimum":1600000000,"maximum":1900000000},"attachment_urls":{"type":"array","maxItems":3,"items":{"type":"string","maxLength":200}},"attachment_files":{"type":"array","maxItems":3,"items":{"type":"object","required":["content_type","data","name"],"properties":{"content_type":{"type":"string","maxLength":80},"data":{"type":"string","maxLength":400},"name":{"type":"string","minLength":1,"maxLength":80}}}}}},"Contact":{"type":"object","required":["type","id","role","created_at","updated_at","custom_attributes"],"properties":{"type":{"type":"string","enum":["contact"]},"id":{"type":"string","x-mockingbird-resource":{"type":"contact","identity":true}},"workspace_id":{"type":"string"},"external_id":{"type":["string","null"]},"role":{"type":"string"},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"name":{"type":["string","null"]},"avatar":{"type":["string","null"]},"owner_id":{"type":["integer","null"]},"has_hard_bounced":{"type":"boolean"},"marked_email_as_spam":{"type":"boolean"},"unsubscribed_from_emails":{"type":"boolean"},"created_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"signed_up_at":{"type":["integer","null"]},"last_seen_at":{"type":["integer","null"]},"custom_attributes":{"type":"object"},"tags":{"type":"object"},"notes":{"type":"object"},"companies":{"type":"object"},"location":{"type":"object"},"social_profiles":{"type":"object"}}},"Pages":{"type":"object","required":["type","page","per_page","total_pages"],"properties":{"type":{"type":"string","enum":["pages"]},"page":{"type":"integer"},"per_page":{"type":"integer"},"total_pages":{"type":"integer"},"next":{"type":"object","required":["page","starting_after"],"properties":{"page":{"type":"integer"},"starting_after":{"type":"string"}}}}},"ContactList":{"type":"object","required":["type","data","total_count","pages"],"properties":{"type":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"total_count":{"type":"integer"},"pages":{"$ref":"#/components/schemas/Pages"}}},"Author":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string"},"id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":["string","null"]}}},"Attachment":{"type":"object","required":["type","name","url","content_type"],"properties":{"type":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"content_type":{"type":"string"},"filesize":{"type":"integer"},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]}}},"ConversationPart":{"type":"object","required":["type","id","part_type","created_at","author"],"properties":{"type":{"type":"string","enum":["conversation_part"]},"id":{"type":"string"},"part_type":{"type":"string"},"body":{"type":["string","null"]},"created_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"notified_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"assigned_to":{"type":["object","null"]},"author":{"$ref":"#/components/schemas/Author"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/Attachment"}},"external_id":{"type":["string","null"]},"redacted":{"type":"boolean"}}},"Conversation":{"type":"object","required":["type","id","created_at","updated_at","open","state","read","source","contacts"],"properties":{"type":{"type":"string","enum":["conversation"]},"id":{"type":"string","x-mockingbird-resource":{"type":"conversation","identity":true}},"title":{"type":["string","null"]},"created_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"waiting_since":{"type":["integer","null"],"x-mockingbird-volatile":{"kind":"timestamp"}},"snoozed_until":{"type":["integer","null"]},"open":{"type":"boolean"},"state":{"type":"string","enum":["open","closed","snoozed"]},"read":{"type":"boolean"},"priority":{"type":"string"},"admin_assignee_id":{"type":["integer","null"]},"team_assignee_id":{"type":["string","null"]},"tags":{"type":"object"},"conversation_rating":{"type":["object","null"]},"source":{"type":"object","required":["type","id","delivered_as","body","author"],"properties":{"type":{"type":"string"},"id":{"type":"string"},"delivered_as":{"type":"string"},"subject":{"type":"string"},"body":{"type":["string","null"]},"author":{"$ref":"#/components/schemas/Author"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/Attachment"}},"url":{"type":["string","null"]},"redacted":{"type":"boolean"}}},"contacts":{"type":"object","required":["type","contacts"],"properties":{"type":{"type":"string"},"contacts":{"type":"array","items":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string"},"id":{"type":"string"},"external_id":{"type":["string","null"]}}}}}},"teammates":{"type":"object"},"custom_attributes":{"type":"object"},"first_contact_reply":{"type":["object","null"]},"sla_applied":{"type":["object","null"]},"statistics":{"type":["object","null"]},"ai_agent_participated":{"type":"boolean"},"conversation_parts":{"type":"object","required":["type","conversation_parts","total_count"],"properties":{"type":{"type":"string"},"conversation_parts":{"type":"array","items":{"$ref":"#/components/schemas/ConversationPart"}},"total_count":{"type":"integer"}}}}},"ConversationList":{"type":"object","required":["type","pages","total_count","conversations"],"properties":{"type":{"type":"string","enum":["conversation.list"]},"pages":{"$ref":"#/components/schemas/Pages"},"total_count":{"type":"integer"},"conversations":{"type":"array","items":{"$ref":"#/components/schemas/Conversation"}}}},"Message":{"type":"object","required":["type","id","created_at","body","message_type","conversation_id"],"properties":{"type":{"type":"string","enum":["user_message"]},"id":{"type":"string"},"created_at":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"body":{"type":"string"},"message_type":{"type":"string","enum":["inapp"]},"conversation_id":{"type":"string","x-mockingbird-resource":{"type":"conversation","identity":true}}}},"Admin":{"type":"object","required":["type","id","name","email"],"properties":{"type":{"type":"string","enum":["admin"]},"id":{"type":"string","x-mockingbird-resource":{"type":"admin","identity":true}},"name":{"type":"string"},"email":{"type":"string"},"job_title":{"type":["string","null"]},"away_mode_enabled":{"type":"boolean"},"away_mode_reassign":{"type":"boolean"},"has_inbox_seat":{"type":"boolean"},"team_ids":{"type":"array","items":{"type":"integer"}}}}}}}`);
|
|
2535
|
+
var operationIds = ["SearchContacts", "CreateContact", "GetContact", "UpdateContact", "CreateConversation", "SearchConversations", "GetConversation", "UpdateConversation", "ReplyConversation", "ManageConversation", "ListAdmins", "GetMe"];
|
|
2536
|
+
var supportedOperationIds = ["SearchContacts", "CreateContact", "GetContact", "UpdateContact", "CreateConversation", "SearchConversations", "GetConversation", "UpdateConversation", "ReplyConversation", "ManageConversation", "ListAdmins", "GetMe"];
|
|
2537
|
+
|
|
2538
|
+
// src/query.ts
|
|
2539
|
+
var QueryError = class extends Error {
|
|
2540
|
+
};
|
|
2541
|
+
var FILTER_OPERATORS = ["=", "!=", "IN", "NIN", "<", ">", "~", "!~", "^", "$"];
|
|
2542
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2543
|
+
var parseQuery = (value, fields, depth = 0) => {
|
|
2544
|
+
if (!isRecord4(value)) throw new QueryError("query must be an object");
|
|
2545
|
+
const operator = value.operator;
|
|
2546
|
+
if (operator === "AND" || operator === "OR") {
|
|
2547
|
+
if (depth >= 2) throw new QueryError("queries can be nested at most two levels deep");
|
|
2548
|
+
if (!Array.isArray(value.value) || value.value.length === 0) {
|
|
2549
|
+
throw new QueryError(`${operator} requires a non-empty value array`);
|
|
2550
|
+
}
|
|
2551
|
+
return { operator, value: value.value.map((each) => parseQuery(each, fields, depth + 1)) };
|
|
2552
|
+
}
|
|
2553
|
+
if (typeof value.field !== "string") throw new QueryError("query field is required");
|
|
2554
|
+
const custom = value.field.startsWith("custom_attributes.");
|
|
2555
|
+
if (!fields.includes(value.field) && !(custom && fields.includes("custom_attributes.*"))) {
|
|
2556
|
+
throw new QueryError(`${value.field} is not a searchable field`);
|
|
2557
|
+
}
|
|
2558
|
+
if (typeof operator !== "string" || !FILTER_OPERATORS.includes(operator)) {
|
|
2559
|
+
throw new QueryError(`operator ${String(operator)} is not supported`);
|
|
2560
|
+
}
|
|
2561
|
+
if ((operator === "IN" || operator === "NIN") !== Array.isArray(value.value)) {
|
|
2562
|
+
throw new QueryError(
|
|
2563
|
+
`operator ${operator} ${Array.isArray(value.value) ? "does not take" : "requires"} an array value`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
if (value.value === void 0) throw new QueryError("query value is required");
|
|
2567
|
+
return { field: value.field, operator, value: value.value };
|
|
2568
|
+
};
|
|
2569
|
+
var same = (actual, expected) => {
|
|
2570
|
+
if (actual === null || actual === void 0) return expected === null || expected === "null";
|
|
2571
|
+
if (Array.isArray(actual)) return actual.some((each) => same(each, expected));
|
|
2572
|
+
return String(actual).toLowerCase() === String(expected).toLowerCase();
|
|
2573
|
+
};
|
|
2574
|
+
var text = (value) => value === null || value === void 0 ? "" : String(value).toLowerCase();
|
|
2575
|
+
var matchFilter = (filter, resolve) => {
|
|
2576
|
+
const actual = resolve(filter.field);
|
|
2577
|
+
const expected = filter.value;
|
|
2578
|
+
switch (filter.operator) {
|
|
2579
|
+
case "=":
|
|
2580
|
+
return same(actual, expected);
|
|
2581
|
+
case "!=":
|
|
2582
|
+
return !same(actual, expected);
|
|
2583
|
+
case "IN":
|
|
2584
|
+
return expected.some((each) => same(actual, each));
|
|
2585
|
+
case "NIN":
|
|
2586
|
+
return !expected.some((each) => same(actual, each));
|
|
2587
|
+
case "<":
|
|
2588
|
+
return actual !== null && actual !== void 0 && Number(actual) < Number(expected);
|
|
2589
|
+
case ">":
|
|
2590
|
+
return actual !== null && actual !== void 0 && Number(actual) > Number(expected);
|
|
2591
|
+
case "~":
|
|
2592
|
+
return text(actual).includes(text(expected));
|
|
2593
|
+
case "!~":
|
|
2594
|
+
return !text(actual).includes(text(expected));
|
|
2595
|
+
case "^":
|
|
2596
|
+
return text(actual).startsWith(text(expected));
|
|
2597
|
+
case "$":
|
|
2598
|
+
return text(actual).endsWith(text(expected));
|
|
2599
|
+
default:
|
|
2600
|
+
return false;
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
var matches2 = (query, resolve) => {
|
|
2604
|
+
if ("field" in query) return matchFilter(query, resolve);
|
|
2605
|
+
return query.operator === "AND" ? query.value.every((each) => matches2(each, resolve)) : query.value.some((each) => matches2(each, resolve));
|
|
2606
|
+
};
|
|
2607
|
+
var encodeCursor = (offset) => toBase64(new TextEncoder().encode(`[${offset}]`));
|
|
2608
|
+
var decodeCursor = (cursor) => {
|
|
2609
|
+
try {
|
|
2610
|
+
const parsed = JSON.parse(new TextDecoder().decode(fromBase64(cursor)));
|
|
2611
|
+
if (Array.isArray(parsed) && Number.isInteger(parsed[0]) && parsed[0] >= 0) {
|
|
2612
|
+
return parsed[0];
|
|
2613
|
+
}
|
|
2614
|
+
} catch {
|
|
2615
|
+
}
|
|
2616
|
+
throw new QueryError("starting_after is not a valid cursor");
|
|
2617
|
+
};
|
|
2618
|
+
var paginate = (items, pagination, defaultPerPage) => {
|
|
2619
|
+
const options = isRecord4(pagination) ? pagination : {};
|
|
2620
|
+
const perPage = options.per_page === void 0 ? defaultPerPage : Number(options.per_page);
|
|
2621
|
+
if (!Number.isInteger(perPage) || perPage < 1 || perPage > 150) {
|
|
2622
|
+
throw new QueryError("per_page must be between 1 and 150");
|
|
2623
|
+
}
|
|
2624
|
+
const offset = typeof options.starting_after === "string" && options.starting_after.length > 0 ? decodeCursor(options.starting_after) : 0;
|
|
2625
|
+
const slice = items.slice(offset, offset + perPage);
|
|
2626
|
+
const page = Math.floor(offset / perPage) + 1;
|
|
2627
|
+
const total = Math.max(1, Math.ceil(items.length / perPage));
|
|
2628
|
+
const hasMore = offset + perPage < items.length;
|
|
2629
|
+
return {
|
|
2630
|
+
items: slice,
|
|
2631
|
+
pages: {
|
|
2632
|
+
type: "pages",
|
|
2633
|
+
page,
|
|
2634
|
+
per_page: perPage,
|
|
2635
|
+
total_pages: total,
|
|
2636
|
+
...hasMore ? { next: { page: page + 1, starting_after: encodeCursor(offset + perPage) } } : {}
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
};
|
|
2640
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2641
|
+
var toHtml = (body) => {
|
|
2642
|
+
if (/<[a-z][\s\S]*>/i.test(body)) return body;
|
|
2643
|
+
return body.split(/\r?\n/).map((line) => `<p>${escapeHtml(line)}</p>`).join("");
|
|
2644
|
+
};
|
|
2645
|
+
var toPlaintext = (html) => {
|
|
2646
|
+
if (html === null) return null;
|
|
2647
|
+
return html.replace(/<\/p>\s*<p[^>]*>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]*>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/&/g, "&").trim();
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2650
|
+
// src/state.ts
|
|
2651
|
+
var DEFAULT_SETTINGS = { tokens: [], customAttributes: null };
|
|
2652
|
+
var DEFAULT_ADMINS = [
|
|
2653
|
+
{
|
|
2654
|
+
type: "admin",
|
|
2655
|
+
id: "1000001",
|
|
2656
|
+
name: "Mock Support",
|
|
2657
|
+
email: "support@mock.intercom.local",
|
|
2658
|
+
job_title: "Member Support",
|
|
2659
|
+
away_mode_enabled: false,
|
|
2660
|
+
away_mode_reassign: false,
|
|
2661
|
+
has_inbox_seat: true,
|
|
2662
|
+
team_ids: []
|
|
2663
|
+
},
|
|
2664
|
+
{
|
|
2665
|
+
type: "admin",
|
|
2666
|
+
id: "1000002",
|
|
2667
|
+
name: "Mock Clinician",
|
|
2668
|
+
email: "clinician@mock.intercom.local",
|
|
2669
|
+
job_title: "Longevity Specialist",
|
|
2670
|
+
away_mode_enabled: false,
|
|
2671
|
+
away_mode_reassign: false,
|
|
2672
|
+
has_inbox_seat: true,
|
|
2673
|
+
team_ids: []
|
|
2674
|
+
}
|
|
2675
|
+
];
|
|
2676
|
+
var hex = (input, length) => [...opaqueToken(input, length)].map((c) => (c.charCodeAt(0) % 16).toString(16)).join("");
|
|
2677
|
+
var IntercomState = class {
|
|
2678
|
+
constructor(sqlite, namespace, seed) {
|
|
2679
|
+
this.namespace = namespace;
|
|
2680
|
+
this.seed = seed;
|
|
2681
|
+
this.contacts = new Collection(sqlite, namespace, "contacts");
|
|
2682
|
+
this.conversations = new Collection(sqlite, namespace, "conversations");
|
|
2683
|
+
this.admins = new Collection(sqlite, namespace, "admins");
|
|
2684
|
+
this.settings = new Collection(sqlite, namespace, "settings");
|
|
2685
|
+
this.counters = new Collection(sqlite, namespace, "counters");
|
|
2686
|
+
this.workspaceId = "mockapp";
|
|
2687
|
+
this.ensureSeeded();
|
|
2688
|
+
}
|
|
2689
|
+
namespace;
|
|
2690
|
+
seed;
|
|
2691
|
+
contacts;
|
|
2692
|
+
conversations;
|
|
2693
|
+
admins;
|
|
2694
|
+
settings;
|
|
2695
|
+
counters;
|
|
2696
|
+
workspaceId;
|
|
2697
|
+
ensureSeeded() {
|
|
2698
|
+
if (this.admins.count() === 0) {
|
|
2699
|
+
for (const admin of this.seed.admins) this.admins.insert(admin.id, admin);
|
|
2700
|
+
}
|
|
2701
|
+
if (!this.settings.has("settings")) {
|
|
2702
|
+
this.settings.insert("settings", { ...DEFAULT_SETTINGS, ...this.seed.settings });
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
current() {
|
|
2706
|
+
return this.settings.get("settings") ?? DEFAULT_SETTINGS;
|
|
2707
|
+
}
|
|
2708
|
+
update(patch) {
|
|
2709
|
+
const next = { ...this.current(), ...patch };
|
|
2710
|
+
this.settings.insert("settings", next);
|
|
2711
|
+
return next;
|
|
2712
|
+
}
|
|
2713
|
+
/** The next value of a named counter (1, 2, …), reset with the namespace. */
|
|
2714
|
+
next(name) {
|
|
2715
|
+
const value = (this.counters.get(name) ?? 0) + 1;
|
|
2716
|
+
this.counters.insert(name, value);
|
|
2717
|
+
return value;
|
|
2718
|
+
}
|
|
2719
|
+
/** A 24-hex contact id, like Intercom's. */
|
|
2720
|
+
nextContactId() {
|
|
2721
|
+
return hex(`intercom:contact:${this.namespace}:${this.next("contact")}`, 24);
|
|
2722
|
+
}
|
|
2723
|
+
/** A numeric-string conversation id, like Intercom's. */
|
|
2724
|
+
nextConversationId() {
|
|
2725
|
+
return String(21547e10 + this.next("conversation"));
|
|
2726
|
+
}
|
|
2727
|
+
nextPartId() {
|
|
2728
|
+
return String(3e10 + this.next("part"));
|
|
2729
|
+
}
|
|
2730
|
+
nextMessageId() {
|
|
2731
|
+
return String(4e10 + this.next("message"));
|
|
2732
|
+
}
|
|
2733
|
+
nextRequestId() {
|
|
2734
|
+
return `req_${hex(`intercom:request:${this.namespace}:${this.next("request")}`, 20)}`;
|
|
2735
|
+
}
|
|
2736
|
+
nextNotificationId() {
|
|
2737
|
+
return `notif_${hex(`intercom:notification:${this.namespace}:${this.next("notification")}`, 32)}`;
|
|
2738
|
+
}
|
|
2739
|
+
findContact(where) {
|
|
2740
|
+
return this.contacts.list({ order: "oldest", where }).at(0)?.value;
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2744
|
+
// src/runtime.ts
|
|
2745
|
+
var HUB_SIGNATURE_HEADER = "X-Hub-Signature";
|
|
2746
|
+
var signHub = async (secret, body) => `sha1=${await hmac("SHA-1", secret, body, "hex")}`;
|
|
2747
|
+
var DEFAULT_TOPICS = [
|
|
2748
|
+
"conversation.admin.replied",
|
|
2749
|
+
"conversation.admin.closed",
|
|
2750
|
+
"conversation.admin.opened",
|
|
2751
|
+
"conversation.admin.single.created"
|
|
2752
|
+
];
|
|
2753
|
+
var errorBody = (code, message) => ({
|
|
2754
|
+
type: "error.list",
|
|
2755
|
+
request_id: "req_mockingbird_fault",
|
|
2756
|
+
errors: [{ code, message }]
|
|
2757
|
+
});
|
|
2758
|
+
var INTERCOM_PRESETS = {
|
|
2759
|
+
rate_limited: {
|
|
2760
|
+
description: "Every call answers 429 rate_limit_exceeded (the messaging adapter maps it to 429)",
|
|
2761
|
+
rules: [
|
|
2762
|
+
{
|
|
2763
|
+
status: 429,
|
|
2764
|
+
body: errorBody("rate_limit_exceeded", "Rate Limit Exceeded"),
|
|
2765
|
+
headers: { "X-RateLimit-Limit": "10000", "X-RateLimit-Remaining": "0" }
|
|
2766
|
+
}
|
|
2767
|
+
]
|
|
2768
|
+
},
|
|
2769
|
+
server_error: {
|
|
2770
|
+
description: "Every call answers 500 (the messaging adapter throws)",
|
|
2771
|
+
rules: [{ status: 500, body: errorBody("server_error", "Server Error") }]
|
|
2772
|
+
},
|
|
2773
|
+
service_unavailable: {
|
|
2774
|
+
description: "Every call answers 503",
|
|
2775
|
+
rules: [{ status: 503, body: errorBody("service_unavailable", "Service Unavailable") }]
|
|
2776
|
+
},
|
|
2777
|
+
unauthorized: {
|
|
2778
|
+
description: "Every call answers 401 unauthorized",
|
|
2779
|
+
rules: [{ status: 401, body: errorBody("unauthorized", "Access Token Invalid") }]
|
|
2780
|
+
},
|
|
2781
|
+
contact_stale_404: {
|
|
2782
|
+
description: "The next conversation search answers 404, as when a cached contact id went stale: the adapter re-resolves the contact and retries once",
|
|
2783
|
+
rules: [
|
|
2784
|
+
{
|
|
2785
|
+
operationId: "SearchConversations",
|
|
2786
|
+
status: 404,
|
|
2787
|
+
body: errorBody("not_found", "User Not Found"),
|
|
2788
|
+
count: 1
|
|
2789
|
+
}
|
|
2790
|
+
]
|
|
2791
|
+
},
|
|
2792
|
+
search_unavailable: {
|
|
2793
|
+
description: "Conversation search answers `conversations: null` (the admin inbox answers 503)",
|
|
2794
|
+
rules: [{ operationId: "SearchConversations", effect: "search_unavailable" }]
|
|
2795
|
+
},
|
|
2796
|
+
repeated_cursor: {
|
|
2797
|
+
description: "Conversation search always answers the same next cursor (the admin inbox detects the loop and answers 503)",
|
|
2798
|
+
rules: [{ operationId: "SearchConversations", effect: "repeated_cursor" }]
|
|
2799
|
+
},
|
|
2800
|
+
webhook_duplicate: {
|
|
2801
|
+
description: "The next webhook is delivered twice (receivers dedupe on the notification id)",
|
|
2802
|
+
webhook: { mode: "duplicate" }
|
|
2803
|
+
},
|
|
2804
|
+
webhook_reorder: {
|
|
2805
|
+
description: "The next two webhooks arrive swapped",
|
|
2806
|
+
webhook: { mode: "reorder" }
|
|
2807
|
+
},
|
|
2808
|
+
webhook_drop: {
|
|
2809
|
+
description: "The next webhook is never delivered",
|
|
2810
|
+
webhook: { mode: "drop" }
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2813
|
+
var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2814
|
+
var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2815
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2816
|
+
var guard = (run) => {
|
|
2817
|
+
try {
|
|
2818
|
+
return run();
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
if (error instanceof IntercomError) return adminError3(error.status, error.message);
|
|
2821
|
+
throw error;
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
var adminRoutes = (runtime) => {
|
|
2825
|
+
const api = (namespace) => runtime.instance(namespace);
|
|
2826
|
+
const conversation = (namespace, id) => api(namespace).state.conversations.get(id);
|
|
2827
|
+
const defaultAdmin = (namespace) => api(namespace).state.admins.list({ order: "oldest" }).at(0)?.value.id ?? "";
|
|
2828
|
+
return {
|
|
2829
|
+
"GET /contacts": ({ namespace }) => json3(200, { contacts: api(namespace).contacts() }),
|
|
2830
|
+
"GET /conversations": ({ namespace }) => json3(200, { conversations: api(namespace).conversations() }),
|
|
2831
|
+
"POST /conversations": ({ body, namespace }) => guard(() => {
|
|
2832
|
+
if (!isRecord5(body) || typeof body.body !== "string") {
|
|
2833
|
+
return adminError3(400, 'expected {"contactId" | "externalId", "adminId"?, "body"}');
|
|
2834
|
+
}
|
|
2835
|
+
const state = api(namespace).state;
|
|
2836
|
+
const contactId = typeof body.contactId === "string" ? body.contactId : typeof body.externalId === "string" ? state.findContact((c) => c.external_id === body.externalId)?.id : void 0;
|
|
2837
|
+
if (!contactId) return adminError3(404, "no such contact");
|
|
2838
|
+
const created = api(namespace).startAdminConversation({
|
|
2839
|
+
contactId,
|
|
2840
|
+
adminId: typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace),
|
|
2841
|
+
body: body.body
|
|
2842
|
+
});
|
|
2843
|
+
return json3(201, created);
|
|
2844
|
+
}),
|
|
2845
|
+
"POST /conversations/:id/admin-reply": ({ params, body, namespace }) => guard(() => {
|
|
2846
|
+
const found = conversation(namespace, params.id);
|
|
2847
|
+
if (!found) return adminError3(404, `no conversation ${params.id}`);
|
|
2848
|
+
if (!isRecord5(body) || typeof body.body !== "string") {
|
|
2849
|
+
return adminError3(
|
|
2850
|
+
400,
|
|
2851
|
+
'expected {"adminId"?, "body", "messageType"?: "comment" | "note"}'
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
const instance = api(namespace);
|
|
2855
|
+
const author = instance.state.admins.get(
|
|
2856
|
+
typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace)
|
|
2857
|
+
);
|
|
2858
|
+
if (!author) return adminError3(404, `no admin ${String(body.adminId)}`);
|
|
2859
|
+
const next = instance.appendPart(found, {
|
|
2860
|
+
partType: body.messageType === "note" ? "note" : "comment",
|
|
2861
|
+
author: { type: "admin", id: author.id, name: author.name, email: author.email },
|
|
2862
|
+
body: toHtml(body.body)
|
|
2863
|
+
});
|
|
2864
|
+
return json3(200, next);
|
|
2865
|
+
}),
|
|
2866
|
+
"POST /conversations/:id/close": ({ params, body, namespace }) => guard(() => {
|
|
2867
|
+
const found = conversation(namespace, params.id);
|
|
2868
|
+
if (!found) return adminError3(404, `no conversation ${params.id}`);
|
|
2869
|
+
const adminId = isRecord5(body) && typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace);
|
|
2870
|
+
return json3(200, api(namespace).manage(found, { action: "close", adminId }));
|
|
2871
|
+
}),
|
|
2872
|
+
"POST /conversations/:id/open": ({ params, body, namespace }) => guard(() => {
|
|
2873
|
+
const found = conversation(namespace, params.id);
|
|
2874
|
+
if (!found) return adminError3(404, `no conversation ${params.id}`);
|
|
2875
|
+
const adminId = isRecord5(body) && typeof body.adminId === "string" ? body.adminId : defaultAdmin(namespace);
|
|
2876
|
+
return json3(200, api(namespace).manage(found, { action: "open", adminId }));
|
|
2877
|
+
}),
|
|
2878
|
+
"PUT /admins": ({ body, namespace }) => {
|
|
2879
|
+
const list = Array.isArray(body) ? body : isRecord5(body) ? body.admins : void 0;
|
|
2880
|
+
if (!Array.isArray(list) || !list.every((a) => isRecord5(a) && typeof a.id === "string")) {
|
|
2881
|
+
return adminError3(400, "expected [{id, name, email}, \u2026]");
|
|
2882
|
+
}
|
|
2883
|
+
const state = api(namespace).state;
|
|
2884
|
+
for (const row of state.admins.list()) state.admins.delete(row.id);
|
|
2885
|
+
for (const each of list) {
|
|
2886
|
+
state.admins.insert(String(each.id), {
|
|
2887
|
+
type: "admin",
|
|
2888
|
+
id: String(each.id),
|
|
2889
|
+
name: String(each.name ?? `Admin ${each.id}`),
|
|
2890
|
+
email: String(each.email ?? `admin${each.id}@mock.intercom.local`),
|
|
2891
|
+
job_title: typeof each.job_title === "string" ? each.job_title : null,
|
|
2892
|
+
away_mode_enabled: each.away_mode_enabled === true,
|
|
2893
|
+
away_mode_reassign: false,
|
|
2894
|
+
has_inbox_seat: true,
|
|
2895
|
+
team_ids: []
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
return json3(200, { admins: state.admins.list({ order: "oldest" }).map((row) => row.value) });
|
|
2899
|
+
},
|
|
2900
|
+
"GET /settings": ({ namespace }) => json3(200, api(namespace).state.current()),
|
|
2901
|
+
"PUT /settings": ({ body, namespace }) => {
|
|
2902
|
+
if (!isRecord5(body)) return adminError3(400, "expected a JSON object");
|
|
2903
|
+
const patch = {};
|
|
2904
|
+
if (body.tokens !== void 0) {
|
|
2905
|
+
if (!Array.isArray(body.tokens)) return adminError3(400, "tokens: string[]");
|
|
2906
|
+
patch.tokens = body.tokens.map(String);
|
|
2907
|
+
}
|
|
2908
|
+
if (body.customAttributes !== void 0) {
|
|
2909
|
+
if (body.customAttributes !== null && !Array.isArray(body.customAttributes)) {
|
|
2910
|
+
return adminError3(400, "customAttributes: string[] | null");
|
|
2911
|
+
}
|
|
2912
|
+
patch.customAttributes = body.customAttributes === null ? null : body.customAttributes.map(String);
|
|
2913
|
+
}
|
|
2914
|
+
return json3(200, api(namespace).state.update(patch));
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
};
|
|
2918
|
+
var createRuntime2 = (options = {}) => {
|
|
2919
|
+
const hooks = options.webhooks;
|
|
2920
|
+
const hub = createWebhookHub({
|
|
2921
|
+
signer: signers.custom(
|
|
2922
|
+
async ({ body, secret }) => secret ? { [HUB_SIGNATURE_HEADER]: await signHub(secret, body) } : {}
|
|
2923
|
+
),
|
|
2924
|
+
...hooks?.retryDelaysMs ? { retryDelaysMs: hooks.retryDelaysMs } : {},
|
|
2925
|
+
...hooks?.fetch ? { fetch: hooks.fetch } : {},
|
|
2926
|
+
...options.wallClock ? { now: options.wallClock } : {},
|
|
2927
|
+
endpoints: (hooks?.urls ?? []).map(
|
|
2928
|
+
(url, index) => ({
|
|
2929
|
+
id: `we_intercom_${index}`,
|
|
2930
|
+
url,
|
|
2931
|
+
...hooks?.secret ? { secret: hooks.secret } : {},
|
|
2932
|
+
events: [...hooks?.events ?? DEFAULT_TOPICS]
|
|
2933
|
+
})
|
|
2934
|
+
)
|
|
2935
|
+
});
|
|
2936
|
+
const runtime = createRuntime({
|
|
2937
|
+
name: INTERCOM_NAMESPACE,
|
|
2938
|
+
document,
|
|
2939
|
+
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2940
|
+
...options.clock ? { clock: options.clock } : {},
|
|
2941
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
2942
|
+
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
2943
|
+
...options.onLog ? { onLog: options.onLog } : {},
|
|
2944
|
+
credential: bearerToken,
|
|
2945
|
+
presets: INTERCOM_PRESETS,
|
|
2946
|
+
webhooks: hub,
|
|
2947
|
+
create: ({ sqlite, namespace, publicNamespace, clock }) => new IntercomAPI({
|
|
2948
|
+
sqlite,
|
|
2949
|
+
namespace,
|
|
2950
|
+
now: clock.now,
|
|
2951
|
+
...options.wallClock ? { wallClock: options.wallClock } : {},
|
|
2952
|
+
...options.admins ? { admins: options.admins } : {},
|
|
2953
|
+
...options.settings ? { settings: options.settings } : {},
|
|
2954
|
+
onWebhook: (notification) => hub.publish({
|
|
2955
|
+
namespace: publicNamespace,
|
|
2956
|
+
type: notification.topic,
|
|
2957
|
+
body: notification,
|
|
2958
|
+
id: notification.id
|
|
2959
|
+
})
|
|
2960
|
+
}),
|
|
2961
|
+
describe: () => ({ webhooks: hub.endpoints("default").length > 0 ? "on" : "off" }),
|
|
2962
|
+
admin: adminRoutes
|
|
2963
|
+
});
|
|
2964
|
+
return Object.assign(runtime, { webhooks: hub });
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
// src/index.ts
|
|
2968
|
+
var INTERCOM_NAMESPACE = "intercom";
|
|
2969
|
+
var INTERCOM_TOPICS = [
|
|
2970
|
+
"conversation.admin.replied",
|
|
2971
|
+
"conversation.admin.closed",
|
|
2972
|
+
"conversation.admin.opened",
|
|
2973
|
+
"conversation.admin.snoozed",
|
|
2974
|
+
"conversation.admin.assigned",
|
|
2975
|
+
"conversation.admin.single.created"
|
|
2976
|
+
];
|
|
2977
|
+
var CONTACT_FIELDS = [
|
|
2978
|
+
"id",
|
|
2979
|
+
"external_id",
|
|
2980
|
+
"email",
|
|
2981
|
+
"name",
|
|
2982
|
+
"phone",
|
|
2983
|
+
"role",
|
|
2984
|
+
"created_at",
|
|
2985
|
+
"updated_at",
|
|
2986
|
+
"signed_up_at",
|
|
2987
|
+
"last_seen_at",
|
|
2988
|
+
"custom_attributes.*"
|
|
2989
|
+
];
|
|
2990
|
+
var CONVERSATION_FIELDS = [
|
|
2991
|
+
"id",
|
|
2992
|
+
"contact_ids",
|
|
2993
|
+
"teammate_ids",
|
|
2994
|
+
"admin_assignee_id",
|
|
2995
|
+
"team_assignee_id",
|
|
2996
|
+
"state",
|
|
2997
|
+
"open",
|
|
2998
|
+
"read",
|
|
2999
|
+
"priority",
|
|
3000
|
+
"title",
|
|
3001
|
+
"created_at",
|
|
3002
|
+
"updated_at",
|
|
3003
|
+
"waiting_since",
|
|
3004
|
+
"source.id",
|
|
3005
|
+
"source.type",
|
|
3006
|
+
"source.delivered_as",
|
|
3007
|
+
"source.subject",
|
|
3008
|
+
"source.body",
|
|
3009
|
+
"source.author.id",
|
|
3010
|
+
"source.author.type",
|
|
3011
|
+
"source.author.name",
|
|
3012
|
+
"source.author.email"
|
|
3013
|
+
];
|
|
3014
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3015
|
+
var str = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3016
|
+
var IntercomError = class extends Error {
|
|
3017
|
+
constructor(status, code, message) {
|
|
3018
|
+
super(message);
|
|
3019
|
+
this.status = status;
|
|
3020
|
+
this.code = code;
|
|
3021
|
+
}
|
|
3022
|
+
status;
|
|
3023
|
+
code;
|
|
3024
|
+
};
|
|
3025
|
+
var IntercomAPI = class {
|
|
3026
|
+
app;
|
|
3027
|
+
sqlite;
|
|
3028
|
+
state;
|
|
3029
|
+
service;
|
|
3030
|
+
idempotency;
|
|
3031
|
+
now;
|
|
3032
|
+
wallClock;
|
|
3033
|
+
onWebhook;
|
|
3034
|
+
constructor(options = {}) {
|
|
3035
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
3036
|
+
const namespace = options.namespace ?? INTERCOM_NAMESPACE;
|
|
3037
|
+
this.now = options.now ?? (() => Date.now());
|
|
3038
|
+
this.wallClock = options.wallClock ?? Date.now;
|
|
3039
|
+
this.onWebhook = options.onWebhook;
|
|
3040
|
+
this.state = new IntercomState(sqlite, namespace, {
|
|
3041
|
+
admins: options.admins ?? DEFAULT_ADMINS,
|
|
3042
|
+
settings: options.settings ?? {}
|
|
3043
|
+
});
|
|
3044
|
+
this.idempotency = new IdempotencyStore(sqlite, namespace);
|
|
3045
|
+
const handlers = defineOperations({
|
|
3046
|
+
SearchContacts: (context) => this.searchContacts(context),
|
|
3047
|
+
CreateContact: (context) => this.createContact(context),
|
|
3048
|
+
GetContact: (context) => {
|
|
3049
|
+
const contact = this.state.contacts.get(context.params.contact_id ?? "");
|
|
3050
|
+
return contact ? annotateResponse(jsonRes(200, this.contactBody(contact)), {
|
|
3051
|
+
ids: { contactId: contact.id }
|
|
3052
|
+
}) : this.error(404, "not_found", "User Not Found");
|
|
3053
|
+
},
|
|
3054
|
+
UpdateContact: (context) => this.updateContact(context),
|
|
3055
|
+
CreateConversation: (context) => this.createConversation(context),
|
|
3056
|
+
UpdateConversation: (context) => this.updateConversation(context),
|
|
3057
|
+
ReplyConversation: (context) => this.reply(context),
|
|
3058
|
+
ManageConversation: (context) => this.manageConversation(context),
|
|
3059
|
+
GetConversation: (context) => this.withConversation(
|
|
3060
|
+
context,
|
|
3061
|
+
(conversation) => jsonRes(
|
|
3062
|
+
200,
|
|
3063
|
+
this.conversationBody(conversation, {
|
|
3064
|
+
plaintext: context.query.display_as === "plaintext",
|
|
3065
|
+
parts: conversation.parts
|
|
3066
|
+
})
|
|
3067
|
+
)
|
|
3068
|
+
),
|
|
3069
|
+
SearchConversations: (context) => this.searchConversations(context),
|
|
3070
|
+
ListAdmins: () => jsonRes(200, {
|
|
3071
|
+
type: "admin.list",
|
|
3072
|
+
admins: this.state.admins.list({ order: "oldest" }).map((row) => row.value)
|
|
3073
|
+
}),
|
|
3074
|
+
GetMe: () => jsonRes(200, {
|
|
3075
|
+
type: "admin",
|
|
3076
|
+
id: "1000000",
|
|
3077
|
+
name: "Mockingbird API",
|
|
3078
|
+
email: "api@mock.intercom.local",
|
|
3079
|
+
email_verified: true,
|
|
3080
|
+
has_inbox_seat: false,
|
|
3081
|
+
avatar: { type: "avatar", image_url: null },
|
|
3082
|
+
app: {
|
|
3083
|
+
type: "app",
|
|
3084
|
+
id_code: this.state.workspaceId,
|
|
3085
|
+
name: "Mockingbird",
|
|
3086
|
+
created_at: 16e8,
|
|
3087
|
+
secure: false,
|
|
3088
|
+
identity_verification: false,
|
|
3089
|
+
timezone: "America/Los_Angeles",
|
|
3090
|
+
region: "US"
|
|
3091
|
+
}
|
|
3092
|
+
})
|
|
3093
|
+
});
|
|
3094
|
+
this.service = createService({
|
|
3095
|
+
document,
|
|
3096
|
+
handlers,
|
|
3097
|
+
sqlite,
|
|
3098
|
+
namespace,
|
|
3099
|
+
now: this.now,
|
|
3100
|
+
notFound: () => this.error(404, "not_found", "Resource Not Found"),
|
|
3101
|
+
onError: (thrown) => {
|
|
3102
|
+
if (thrown instanceof HttpError) return thrown.toResponse();
|
|
3103
|
+
if (thrown instanceof QueryError)
|
|
3104
|
+
return this.error(400, "parameter_invalid", thrown.message);
|
|
3105
|
+
if (thrown instanceof IntercomError)
|
|
3106
|
+
return this.error(thrown.status, thrown.code, thrown.message);
|
|
3107
|
+
throw thrown;
|
|
3108
|
+
},
|
|
3109
|
+
before: (context) => {
|
|
3110
|
+
const token = bearerToken(context.request);
|
|
3111
|
+
if (!token) return this.error(401, "unauthorized", "Access Token Required");
|
|
3112
|
+
const tokens = this.state.current().tokens;
|
|
3113
|
+
if (tokens.length > 0 && !tokens.includes(token)) {
|
|
3114
|
+
return this.error(401, "unauthorized", "Access Token Invalid");
|
|
3115
|
+
}
|
|
3116
|
+
const version = context.request.headers.get("intercom-version");
|
|
3117
|
+
if (version !== null && !/^(\d+\.\d+|Unstable)$/.test(version.trim())) {
|
|
3118
|
+
return this.error(
|
|
3119
|
+
400,
|
|
3120
|
+
"intercom_version_invalid",
|
|
3121
|
+
"The requested version could not be found"
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
return void 0;
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
3127
|
+
this.app = this.service.app;
|
|
3128
|
+
this.sqlite = this.service.sqlite;
|
|
3129
|
+
}
|
|
3130
|
+
fetch(request) {
|
|
3131
|
+
return this.service.fetch(request);
|
|
3132
|
+
}
|
|
3133
|
+
async reset() {
|
|
3134
|
+
await this.service.reset();
|
|
3135
|
+
this.state.ensureSeeded();
|
|
3136
|
+
}
|
|
3137
|
+
seconds() {
|
|
3138
|
+
return Math.floor(this.now() / 1e3);
|
|
3139
|
+
}
|
|
3140
|
+
/** Intercom's error envelope. */
|
|
3141
|
+
error(status, code, message) {
|
|
3142
|
+
return jsonRes(status, {
|
|
3143
|
+
type: "error.list",
|
|
3144
|
+
request_id: this.state.nextRequestId(),
|
|
3145
|
+
errors: [{ code, message }]
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
json(context) {
|
|
3149
|
+
const issues = bodyIssues(context);
|
|
3150
|
+
if (issues.length > 0) {
|
|
3151
|
+
const first = issues[0];
|
|
3152
|
+
const missing = /^missing required property (.+)$/.exec(first.message);
|
|
3153
|
+
throw missing ? new IntercomError(
|
|
3154
|
+
400,
|
|
3155
|
+
"parameter_not_found",
|
|
3156
|
+
`${[first.path, missing[1]].filter(Boolean).join(".")} is required`
|
|
3157
|
+
) : new IntercomError(400, "parameter_invalid", `${first.path || "body"} ${first.message}`);
|
|
3158
|
+
}
|
|
3159
|
+
return context.body.kind === "json" && isRecord6(context.body.value) ? context.body.value : {};
|
|
3160
|
+
}
|
|
3161
|
+
// ─── Contacts ───────────────────────────────────────────────────────────────────────────
|
|
3162
|
+
contactBody(contact) {
|
|
3163
|
+
const list = (url) => ({ type: "list", data: [], url, total_count: 0, has_more: false });
|
|
3164
|
+
return {
|
|
3165
|
+
type: "contact",
|
|
3166
|
+
id: contact.id,
|
|
3167
|
+
workspace_id: this.state.workspaceId,
|
|
3168
|
+
external_id: contact.external_id,
|
|
3169
|
+
role: contact.role,
|
|
3170
|
+
email: contact.email,
|
|
3171
|
+
phone: contact.phone,
|
|
3172
|
+
name: contact.name,
|
|
3173
|
+
avatar: null,
|
|
3174
|
+
owner_id: null,
|
|
3175
|
+
social_profiles: { type: "list", data: [] },
|
|
3176
|
+
has_hard_bounced: false,
|
|
3177
|
+
marked_email_as_spam: false,
|
|
3178
|
+
unsubscribed_from_emails: false,
|
|
3179
|
+
created_at: contact.created_at,
|
|
3180
|
+
updated_at: contact.updated_at,
|
|
3181
|
+
signed_up_at: contact.signed_up_at,
|
|
3182
|
+
last_seen_at: contact.last_seen_at,
|
|
3183
|
+
last_replied_at: null,
|
|
3184
|
+
last_contacted_at: null,
|
|
3185
|
+
last_email_opened_at: null,
|
|
3186
|
+
last_email_clicked_at: null,
|
|
3187
|
+
language_override: null,
|
|
3188
|
+
browser: null,
|
|
3189
|
+
browser_version: null,
|
|
3190
|
+
browser_language: null,
|
|
3191
|
+
os: null,
|
|
3192
|
+
location: { type: "location", country: null, region: null, city: null },
|
|
3193
|
+
custom_attributes: contact.custom_attributes,
|
|
3194
|
+
tags: list(`/contacts/${contact.id}/tags`),
|
|
3195
|
+
notes: list(`/contacts/${contact.id}/notes`),
|
|
3196
|
+
companies: list(`/contacts/${contact.id}/companies`)
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
contactField(contact, field) {
|
|
3200
|
+
if (field.startsWith("custom_attributes.")) {
|
|
3201
|
+
return contact.custom_attributes[field.slice("custom_attributes.".length)];
|
|
3202
|
+
}
|
|
3203
|
+
return contact[field];
|
|
3204
|
+
}
|
|
3205
|
+
searchContacts(context) {
|
|
3206
|
+
const body = this.json(context);
|
|
3207
|
+
const query = parseQuery(body.query, CONTACT_FIELDS);
|
|
3208
|
+
const found = this.state.contacts.list({ order: "oldest" }).map((row) => row.value).filter((contact) => matches2(query, (field) => this.contactField(contact, field)));
|
|
3209
|
+
const page = paginate(found, body.pagination, 50);
|
|
3210
|
+
return jsonRes(200, {
|
|
3211
|
+
type: "list",
|
|
3212
|
+
data: page.items.map((contact) => this.contactBody(contact)),
|
|
3213
|
+
total_count: found.length,
|
|
3214
|
+
pages: page.pages
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
checkCustomAttributes(attributes) {
|
|
3218
|
+
if (attributes === void 0) return {};
|
|
3219
|
+
const defined = this.state.current().customAttributes;
|
|
3220
|
+
const given = isRecord6(attributes) ? attributes : {};
|
|
3221
|
+
if (defined !== null) {
|
|
3222
|
+
const unknown = Object.keys(given).find((key) => !defined.includes(key));
|
|
3223
|
+
if (unknown !== void 0) {
|
|
3224
|
+
throw new IntercomError(
|
|
3225
|
+
400,
|
|
3226
|
+
"parameter_invalid",
|
|
3227
|
+
`Custom attribute '${unknown}' does not exist`
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return given;
|
|
3232
|
+
}
|
|
3233
|
+
/** The contact another contact's identifiers would collide with (users only). */
|
|
3234
|
+
duplicateOf(candidate, except) {
|
|
3235
|
+
if (candidate.role !== "user") return void 0;
|
|
3236
|
+
return this.state.findContact(
|
|
3237
|
+
(other) => other.id !== except && other.role === "user" && (candidate.external_id !== null && other.external_id === candidate.external_id || candidate.email !== null && other.email !== null && other.email.toLowerCase() === candidate.email.toLowerCase())
|
|
3238
|
+
);
|
|
3239
|
+
}
|
|
3240
|
+
createContact(context) {
|
|
3241
|
+
const body = this.json(context);
|
|
3242
|
+
const role = body.role === "lead" ? "lead" : "user";
|
|
3243
|
+
const externalId = str(body.external_id) ?? null;
|
|
3244
|
+
const email = str(body.email) ?? null;
|
|
3245
|
+
if (role === "user" && externalId === null && email === null) {
|
|
3246
|
+
return this.error(400, "parameter_invalid", "A user contact requires an email or external_id");
|
|
3247
|
+
}
|
|
3248
|
+
const customAttributes = this.checkCustomAttributes(body.custom_attributes);
|
|
3249
|
+
const duplicate = this.duplicateOf({ role, external_id: externalId, email });
|
|
3250
|
+
if (duplicate) {
|
|
3251
|
+
return annotateResponse(
|
|
3252
|
+
this.error(
|
|
3253
|
+
409,
|
|
3254
|
+
"conflict",
|
|
3255
|
+
`A contact matching those details already exists with id=${duplicate.id}`
|
|
3256
|
+
),
|
|
3257
|
+
{ ids: { contactId: duplicate.id } }
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
const now = this.seconds();
|
|
3261
|
+
const contact = {
|
|
3262
|
+
id: this.state.nextContactId(),
|
|
3263
|
+
external_id: externalId,
|
|
3264
|
+
role,
|
|
3265
|
+
email,
|
|
3266
|
+
phone: str(body.phone) ?? null,
|
|
3267
|
+
name: str(body.name) ?? null,
|
|
3268
|
+
created_at: now,
|
|
3269
|
+
updated_at: now,
|
|
3270
|
+
signed_up_at: typeof body.signed_up_at === "number" ? body.signed_up_at : null,
|
|
3271
|
+
last_seen_at: typeof body.last_seen_at === "number" ? body.last_seen_at : null,
|
|
3272
|
+
custom_attributes: customAttributes
|
|
3273
|
+
};
|
|
3274
|
+
this.state.contacts.insert(contact.id, contact);
|
|
3275
|
+
return annotateResponse(jsonRes(200, this.contactBody(contact)), {
|
|
3276
|
+
ids: { contactId: contact.id }
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
updateContact(context) {
|
|
3280
|
+
const existing = this.state.contacts.get(context.params.contact_id ?? "");
|
|
3281
|
+
if (!existing) return this.error(404, "not_found", "User Not Found");
|
|
3282
|
+
const body = this.json(context);
|
|
3283
|
+
const customAttributes = this.checkCustomAttributes(body.custom_attributes);
|
|
3284
|
+
const next = {
|
|
3285
|
+
...existing,
|
|
3286
|
+
...body.role === "user" || body.role === "lead" ? { role: body.role } : {},
|
|
3287
|
+
..."external_id" in body ? { external_id: str(body.external_id) ?? null } : {},
|
|
3288
|
+
..."email" in body ? { email: str(body.email) ?? null } : {},
|
|
3289
|
+
..."name" in body ? { name: str(body.name) ?? null } : {},
|
|
3290
|
+
..."phone" in body ? { phone: str(body.phone) ?? null } : {},
|
|
3291
|
+
...typeof body.signed_up_at === "number" || body.signed_up_at === null ? { signed_up_at: body.signed_up_at } : {},
|
|
3292
|
+
...typeof body.last_seen_at === "number" || body.last_seen_at === null ? { last_seen_at: body.last_seen_at } : {},
|
|
3293
|
+
custom_attributes: { ...existing.custom_attributes, ...customAttributes },
|
|
3294
|
+
updated_at: this.seconds()
|
|
3295
|
+
};
|
|
3296
|
+
const duplicate = this.duplicateOf(next, existing.id);
|
|
3297
|
+
if (duplicate) {
|
|
3298
|
+
return this.error(
|
|
3299
|
+
409,
|
|
3300
|
+
"conflict",
|
|
3301
|
+
`A contact matching those details already exists with id=${duplicate.id}`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
this.state.contacts.update(existing.id, next);
|
|
3305
|
+
return annotateResponse(jsonRes(200, this.contactBody(next)), { ids: { contactId: next.id } });
|
|
3306
|
+
}
|
|
3307
|
+
// ─── Conversations ──────────────────────────────────────────────────────────────────────
|
|
3308
|
+
withConversation(context, handle) {
|
|
3309
|
+
const conversation = this.state.conversations.get(context.params.conversation_id ?? "");
|
|
3310
|
+
if (!conversation) return this.error(404, "not_found", "Resource Not Found");
|
|
3311
|
+
return handle(conversation);
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* A conversation as Intercom serializes it. `parts` is omitted in search results, as the
|
|
3315
|
+
* real API does; `plaintext` renders bodies for `?display_as=plaintext`.
|
|
3316
|
+
*/
|
|
3317
|
+
conversationBody(conversation, options = {}) {
|
|
3318
|
+
const render = (body) => options.plaintext ? toPlaintext(body) : body;
|
|
3319
|
+
const contact = this.state.contacts.get(conversation.contactId);
|
|
3320
|
+
return {
|
|
3321
|
+
type: "conversation",
|
|
3322
|
+
id: conversation.id,
|
|
3323
|
+
title: conversation.title,
|
|
3324
|
+
created_at: conversation.created_at,
|
|
3325
|
+
updated_at: conversation.updated_at,
|
|
3326
|
+
waiting_since: conversation.waiting_since,
|
|
3327
|
+
snoozed_until: conversation.snoozed_until,
|
|
3328
|
+
open: conversation.state !== "closed",
|
|
3329
|
+
state: conversation.state,
|
|
3330
|
+
read: conversation.read,
|
|
3331
|
+
priority: "not_priority",
|
|
3332
|
+
admin_assignee_id: conversation.admin_assignee_id,
|
|
3333
|
+
team_assignee_id: null,
|
|
3334
|
+
tags: { type: "tag.list", tags: [] },
|
|
3335
|
+
conversation_rating: null,
|
|
3336
|
+
source: { ...conversation.source, body: render(conversation.source.body) },
|
|
3337
|
+
contacts: {
|
|
3338
|
+
type: "contact.list",
|
|
3339
|
+
contacts: [
|
|
3340
|
+
{
|
|
3341
|
+
type: "contact",
|
|
3342
|
+
id: conversation.contactId,
|
|
3343
|
+
external_id: contact?.external_id ?? null
|
|
3344
|
+
}
|
|
3345
|
+
]
|
|
3346
|
+
},
|
|
3347
|
+
teammates: {
|
|
3348
|
+
type: "admin.list",
|
|
3349
|
+
admins: conversation.teammates.map((id) => ({ type: "admin", id }))
|
|
3350
|
+
},
|
|
3351
|
+
custom_attributes: conversation.custom_attributes,
|
|
3352
|
+
first_contact_reply: null,
|
|
3353
|
+
sla_applied: null,
|
|
3354
|
+
statistics: null,
|
|
3355
|
+
ai_agent_participated: false,
|
|
3356
|
+
...options.parts ? {
|
|
3357
|
+
conversation_parts: {
|
|
3358
|
+
type: "conversation_part.list",
|
|
3359
|
+
conversation_parts: options.parts.map((part) => ({
|
|
3360
|
+
...part,
|
|
3361
|
+
body: render(part.body)
|
|
3362
|
+
})),
|
|
3363
|
+
total_count: options.parts.length
|
|
3364
|
+
}
|
|
3365
|
+
} : {}
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
authorOf(contact) {
|
|
3369
|
+
return { type: "user", id: contact.id, name: contact.name, email: contact.email };
|
|
3370
|
+
}
|
|
3371
|
+
adminAuthor(adminId) {
|
|
3372
|
+
const admin = typeof adminId === "string" ? this.state.admins.get(adminId) : void 0;
|
|
3373
|
+
if (!admin) throw new IntercomError(404, "not_found", "Admin Not Found");
|
|
3374
|
+
return { type: "admin", id: admin.id, name: admin.name, email: admin.email };
|
|
3375
|
+
}
|
|
3376
|
+
createConversation(context) {
|
|
3377
|
+
const body = this.json(context);
|
|
3378
|
+
const create = () => {
|
|
3379
|
+
const from = body.from;
|
|
3380
|
+
const contact = this.state.contacts.get(from.id);
|
|
3381
|
+
if (!contact) return this.error(404, "not_found", "User Not Found");
|
|
3382
|
+
const now = this.seconds();
|
|
3383
|
+
const conversation = {
|
|
3384
|
+
id: this.state.nextConversationId(),
|
|
3385
|
+
contactId: contact.id,
|
|
3386
|
+
created_at: now,
|
|
3387
|
+
updated_at: now,
|
|
3388
|
+
waiting_since: now,
|
|
3389
|
+
snoozed_until: null,
|
|
3390
|
+
state: "open",
|
|
3391
|
+
read: true,
|
|
3392
|
+
title: null,
|
|
3393
|
+
admin_assignee_id: null,
|
|
3394
|
+
teammates: [],
|
|
3395
|
+
custom_attributes: {},
|
|
3396
|
+
source: {
|
|
3397
|
+
type: "conversation",
|
|
3398
|
+
id: this.state.nextMessageId(),
|
|
3399
|
+
delivered_as: "customer_initiated",
|
|
3400
|
+
subject: "",
|
|
3401
|
+
body: toHtml(String(body.body)),
|
|
3402
|
+
author: this.authorOf(contact),
|
|
3403
|
+
attachments: [],
|
|
3404
|
+
url: null,
|
|
3405
|
+
redacted: false
|
|
3406
|
+
},
|
|
3407
|
+
parts: []
|
|
3408
|
+
};
|
|
3409
|
+
this.state.conversations.insert(conversation.id, conversation);
|
|
3410
|
+
return annotateResponse(
|
|
3411
|
+
jsonRes(200, {
|
|
3412
|
+
type: "user_message",
|
|
3413
|
+
id: conversation.source.id,
|
|
3414
|
+
created_at: now,
|
|
3415
|
+
body: conversation.source.body,
|
|
3416
|
+
message_type: "inapp",
|
|
3417
|
+
conversation_id: conversation.id
|
|
3418
|
+
}),
|
|
3419
|
+
{ ids: { contactId: contact.id, conversationId: conversation.id } }
|
|
3420
|
+
);
|
|
3421
|
+
};
|
|
3422
|
+
const key = context.request.headers.get("idempotency-key");
|
|
3423
|
+
if (!key) return create();
|
|
3424
|
+
return this.idempotency.run(
|
|
3425
|
+
key,
|
|
3426
|
+
requestFingerprint("POST", "/conversations", body),
|
|
3427
|
+
{
|
|
3428
|
+
mismatch: () => this.error(409, "conflict", "Idempotency-Key was already used with different parameters"),
|
|
3429
|
+
conflict: () => this.error(409, "conflict", "A request with this Idempotency-Key is still in progress")
|
|
3430
|
+
},
|
|
3431
|
+
create
|
|
3432
|
+
);
|
|
3433
|
+
}
|
|
3434
|
+
updateConversation(context) {
|
|
3435
|
+
return this.withConversation(context, (conversation) => {
|
|
3436
|
+
const body = this.json(context);
|
|
3437
|
+
const next = {
|
|
3438
|
+
...conversation,
|
|
3439
|
+
...typeof body.read === "boolean" ? { read: body.read } : {},
|
|
3440
|
+
...typeof body.title === "string" ? { title: body.title } : {},
|
|
3441
|
+
custom_attributes: {
|
|
3442
|
+
...conversation.custom_attributes,
|
|
3443
|
+
...isRecord6(body.custom_attributes) ? body.custom_attributes : {}
|
|
3444
|
+
},
|
|
3445
|
+
updated_at: this.seconds()
|
|
3446
|
+
};
|
|
3447
|
+
this.state.conversations.update(conversation.id, next);
|
|
3448
|
+
return annotateResponse(
|
|
3449
|
+
jsonRes(
|
|
3450
|
+
200,
|
|
3451
|
+
this.conversationBody(next, {
|
|
3452
|
+
plaintext: context.query.display_as === "plaintext",
|
|
3453
|
+
parts: next.parts
|
|
3454
|
+
})
|
|
3455
|
+
),
|
|
3456
|
+
{ ids: { conversationId: next.id } }
|
|
3457
|
+
);
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
async readReply(context) {
|
|
3461
|
+
if (context.body.kind === "bytes" || context.body.kind === "text") {
|
|
3462
|
+
const contentType = context.request.headers.get("content-type") ?? "";
|
|
3463
|
+
if (!contentType.toLowerCase().startsWith("multipart/form-data")) {
|
|
3464
|
+
throw new IntercomError(400, "parameter_invalid", "Unsupported content type");
|
|
3465
|
+
}
|
|
3466
|
+
let form;
|
|
3467
|
+
try {
|
|
3468
|
+
const raw = context.body.kind === "bytes" ? context.body.value : new TextEncoder().encode(context.body.value);
|
|
3469
|
+
form = await new Response(raw, {
|
|
3470
|
+
headers: { "content-type": contentType }
|
|
3471
|
+
}).formData();
|
|
3472
|
+
} catch {
|
|
3473
|
+
throw new IntercomError(400, "parameter_invalid", "Malformed multipart body");
|
|
3474
|
+
}
|
|
3475
|
+
const fields2 = {};
|
|
3476
|
+
const attachments2 = [];
|
|
3477
|
+
for (const [name, value] of form.entries()) {
|
|
3478
|
+
const entry = value;
|
|
3479
|
+
if (typeof entry === "string") {
|
|
3480
|
+
fields2[name] = entry;
|
|
3481
|
+
} else if (name === "attachment_files[]" || name === "attachment_files") {
|
|
3482
|
+
attachments2.push({
|
|
3483
|
+
type: "upload",
|
|
3484
|
+
name: entry.name || "attachment",
|
|
3485
|
+
content_type: entry.type || "application/octet-stream",
|
|
3486
|
+
filesize: entry.size,
|
|
3487
|
+
width: null,
|
|
3488
|
+
height: null
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
return { fields: fields2, attachments: attachments2, urls: [] };
|
|
3493
|
+
}
|
|
3494
|
+
const fields = this.json(context);
|
|
3495
|
+
const attachments = (Array.isArray(fields.attachment_files) ? fields.attachment_files : []).map(
|
|
3496
|
+
(file) => {
|
|
3497
|
+
const entry = file;
|
|
3498
|
+
let size;
|
|
3499
|
+
try {
|
|
3500
|
+
size = fromBase64(entry.data).byteLength;
|
|
3501
|
+
} catch {
|
|
3502
|
+
throw new IntercomError(
|
|
3503
|
+
400,
|
|
3504
|
+
"parameter_invalid",
|
|
3505
|
+
`attachment ${entry.name} is not valid base64`
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
return {
|
|
3509
|
+
type: "upload",
|
|
3510
|
+
name: entry.name,
|
|
3511
|
+
content_type: entry.content_type,
|
|
3512
|
+
filesize: size,
|
|
3513
|
+
width: null,
|
|
3514
|
+
height: null
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
);
|
|
3518
|
+
const urls = (Array.isArray(fields.attachment_urls) ? fields.attachment_urls : []).map(String);
|
|
3519
|
+
return { fields, attachments, urls };
|
|
3520
|
+
}
|
|
3521
|
+
async reply(context) {
|
|
3522
|
+
const conversation = this.state.conversations.get(context.params.conversation_id ?? "");
|
|
3523
|
+
if (!conversation) return this.error(404, "not_found", "Resource Not Found");
|
|
3524
|
+
const { fields, attachments, urls } = await this.readReply(context);
|
|
3525
|
+
const messageType = fields.message_type;
|
|
3526
|
+
if (messageType !== "comment" && messageType !== "note" && messageType !== "quick_reply") {
|
|
3527
|
+
return this.error(
|
|
3528
|
+
400,
|
|
3529
|
+
"parameter_invalid",
|
|
3530
|
+
"message_type must be comment, note or quick_reply"
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
let author;
|
|
3534
|
+
if (fields.type === "user") {
|
|
3535
|
+
if (messageType !== "comment") {
|
|
3536
|
+
return this.error(
|
|
3537
|
+
400,
|
|
3538
|
+
"parameter_invalid",
|
|
3539
|
+
"A user can only reply with message_type comment"
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
const contact = str(fields.intercom_user_id) && this.state.contacts.get(fields.intercom_user_id) || str(fields.user_id) && this.state.findContact((c) => c.external_id === fields.user_id) || str(fields.email) && this.state.findContact(
|
|
3543
|
+
(c) => c.email?.toLowerCase() === String(fields.email).toLowerCase()
|
|
3544
|
+
) || void 0;
|
|
3545
|
+
if (!contact) return this.error(404, "not_found", "User Not Found");
|
|
3546
|
+
author = this.authorOf(contact);
|
|
3547
|
+
} else if (fields.type === "admin") {
|
|
3548
|
+
author = this.adminAuthor(fields.admin_id);
|
|
3549
|
+
} else {
|
|
3550
|
+
return this.error(400, "parameter_invalid", "type must be user or admin");
|
|
3551
|
+
}
|
|
3552
|
+
const total = attachments.length + urls.length;
|
|
3553
|
+
if (total > 10) return this.error(400, "parameter_invalid", "At most 10 attachments per reply");
|
|
3554
|
+
const body = str(fields.body);
|
|
3555
|
+
if (body === void 0 && total === 0)
|
|
3556
|
+
return this.error(400, "parameter_not_found", "Body is required");
|
|
3557
|
+
const partId = this.state.nextPartId();
|
|
3558
|
+
const stored = [
|
|
3559
|
+
...attachments.map((a, index) => ({
|
|
3560
|
+
...a,
|
|
3561
|
+
url: `https://downloads.intercomcdn.com/i/o/${partId}/${index}/${encodeURIComponent(a.name)}`
|
|
3562
|
+
})),
|
|
3563
|
+
...urls.map((url) => ({
|
|
3564
|
+
type: "upload",
|
|
3565
|
+
name: url.split("/").pop() || "attachment",
|
|
3566
|
+
url,
|
|
3567
|
+
content_type: "application/octet-stream",
|
|
3568
|
+
filesize: 0,
|
|
3569
|
+
width: null,
|
|
3570
|
+
height: null
|
|
3571
|
+
}))
|
|
3572
|
+
];
|
|
3573
|
+
const next = this.appendPart(conversation, {
|
|
3574
|
+
id: partId,
|
|
3575
|
+
partType: messageType,
|
|
3576
|
+
author,
|
|
3577
|
+
body: body === void 0 ? null : toHtml(body),
|
|
3578
|
+
attachments: stored
|
|
3579
|
+
});
|
|
3580
|
+
return annotateResponse(jsonRes(200, this.conversationBody(next, { parts: next.parts })), {
|
|
3581
|
+
ids: { conversationId: next.id, partId }
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
/** Add a comment/note part, update read/state, and fire `conversation.admin.replied`. */
|
|
3585
|
+
appendPart(conversation, input) {
|
|
3586
|
+
const now = this.seconds();
|
|
3587
|
+
const part = {
|
|
3588
|
+
type: "conversation_part",
|
|
3589
|
+
id: input.id ?? this.state.nextPartId(),
|
|
3590
|
+
part_type: input.partType,
|
|
3591
|
+
body: input.body,
|
|
3592
|
+
created_at: now,
|
|
3593
|
+
updated_at: now,
|
|
3594
|
+
notified_at: now,
|
|
3595
|
+
assigned_to: null,
|
|
3596
|
+
author: input.author,
|
|
3597
|
+
attachments: input.attachments ?? [],
|
|
3598
|
+
external_id: null,
|
|
3599
|
+
redacted: false
|
|
3600
|
+
};
|
|
3601
|
+
const byAdmin = input.author.type === "admin";
|
|
3602
|
+
const visible = input.partType !== "note";
|
|
3603
|
+
const next = {
|
|
3604
|
+
...conversation,
|
|
3605
|
+
parts: [...conversation.parts, part],
|
|
3606
|
+
updated_at: now,
|
|
3607
|
+
...byAdmin ? {
|
|
3608
|
+
teammates: conversation.teammates.includes(input.author.id) ? conversation.teammates : [...conversation.teammates, input.author.id],
|
|
3609
|
+
...visible ? { read: false, waiting_since: null } : {}
|
|
3610
|
+
} : { read: true, waiting_since: now, state: "open", snoozed_until: null }
|
|
3611
|
+
};
|
|
3612
|
+
this.state.conversations.update(conversation.id, next);
|
|
3613
|
+
if (byAdmin && visible) this.notify("conversation.admin.replied", next, [part]);
|
|
3614
|
+
return next;
|
|
3615
|
+
}
|
|
3616
|
+
manageConversation(context) {
|
|
3617
|
+
return this.withConversation(context, (conversation) => {
|
|
3618
|
+
const body = this.json(context);
|
|
3619
|
+
const next = this.manage(conversation, {
|
|
3620
|
+
action: body.message_type,
|
|
3621
|
+
adminId: String(body.admin_id),
|
|
3622
|
+
...str(body.body) ? { body: body.body } : {},
|
|
3623
|
+
...typeof body.snoozed_until === "number" ? { snoozedUntil: body.snoozed_until } : {},
|
|
3624
|
+
...body.assignee_id !== void 0 ? { assigneeId: String(body.assignee_id) } : {}
|
|
3625
|
+
});
|
|
3626
|
+
return annotateResponse(jsonRes(200, this.conversationBody(next, { parts: next.parts })), {
|
|
3627
|
+
ids: { conversationId: next.id, partId: next.parts.at(-1)?.id ?? "" }
|
|
3628
|
+
});
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
/** Close, open, snooze or assign as an admin; close and open fire their webhooks. */
|
|
3632
|
+
manage(conversation, input) {
|
|
3633
|
+
const author = this.adminAuthor(input.adminId);
|
|
3634
|
+
const now = this.seconds();
|
|
3635
|
+
if (input.action === "snoozed" && input.snoozedUntil === void 0) {
|
|
3636
|
+
throw new IntercomError(400, "parameter_not_found", "snoozed_until is required");
|
|
3637
|
+
}
|
|
3638
|
+
let assignee;
|
|
3639
|
+
if (input.action === "assignment") {
|
|
3640
|
+
if (input.assigneeId === void 0) {
|
|
3641
|
+
throw new IntercomError(400, "parameter_not_found", "assignee_id is required");
|
|
3642
|
+
}
|
|
3643
|
+
assignee = input.assigneeId === "0" ? void 0 : this.state.admins.get(input.assigneeId);
|
|
3644
|
+
if (input.assigneeId !== "0" && !assignee) {
|
|
3645
|
+
throw new IntercomError(404, "not_found", "Admin Not Found");
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
const part = {
|
|
3649
|
+
type: "conversation_part",
|
|
3650
|
+
id: this.state.nextPartId(),
|
|
3651
|
+
part_type: input.action,
|
|
3652
|
+
body: input.body === void 0 ? null : toHtml(input.body),
|
|
3653
|
+
created_at: now,
|
|
3654
|
+
updated_at: now,
|
|
3655
|
+
notified_at: now,
|
|
3656
|
+
assigned_to: assignee ? { type: "admin", id: assignee.id } : null,
|
|
3657
|
+
author,
|
|
3658
|
+
attachments: [],
|
|
3659
|
+
external_id: null,
|
|
3660
|
+
redacted: false
|
|
3661
|
+
};
|
|
3662
|
+
const next = {
|
|
3663
|
+
...conversation,
|
|
3664
|
+
parts: [...conversation.parts, part],
|
|
3665
|
+
updated_at: now,
|
|
3666
|
+
...input.action === "close" ? { state: "closed", snoozed_until: null } : {},
|
|
3667
|
+
...input.action === "open" ? { state: "open", snoozed_until: null } : {},
|
|
3668
|
+
...input.action === "snoozed" ? { state: "snoozed", snoozed_until: input.snoozedUntil ?? null } : {},
|
|
3669
|
+
...input.action === "assignment" ? { admin_assignee_id: assignee ? Number(assignee.id) : null } : {}
|
|
3670
|
+
};
|
|
3671
|
+
this.state.conversations.update(conversation.id, next);
|
|
3672
|
+
const topic = {
|
|
3673
|
+
close: "conversation.admin.closed",
|
|
3674
|
+
open: "conversation.admin.opened",
|
|
3675
|
+
snoozed: "conversation.admin.snoozed",
|
|
3676
|
+
assignment: "conversation.admin.assigned"
|
|
3677
|
+
}[input.action];
|
|
3678
|
+
this.notify(topic, next, [part]);
|
|
3679
|
+
return next;
|
|
3680
|
+
}
|
|
3681
|
+
/** An admin-initiated conversation (Intercom's outbound message): fires `admin.single.created`. */
|
|
3682
|
+
startAdminConversation(input) {
|
|
3683
|
+
const contact = this.state.contacts.get(input.contactId);
|
|
3684
|
+
if (!contact) throw new IntercomError(404, "not_found", "User Not Found");
|
|
3685
|
+
const author = this.adminAuthor(input.adminId);
|
|
3686
|
+
const now = this.seconds();
|
|
3687
|
+
const conversation = {
|
|
3688
|
+
id: this.state.nextConversationId(),
|
|
3689
|
+
contactId: contact.id,
|
|
3690
|
+
created_at: now,
|
|
3691
|
+
updated_at: now,
|
|
3692
|
+
waiting_since: null,
|
|
3693
|
+
snoozed_until: null,
|
|
3694
|
+
state: "open",
|
|
3695
|
+
read: false,
|
|
3696
|
+
title: null,
|
|
3697
|
+
admin_assignee_id: null,
|
|
3698
|
+
teammates: [author.id],
|
|
3699
|
+
custom_attributes: {},
|
|
3700
|
+
source: {
|
|
3701
|
+
type: "conversation",
|
|
3702
|
+
id: this.state.nextMessageId(),
|
|
3703
|
+
delivered_as: "admin_initiated",
|
|
3704
|
+
subject: "",
|
|
3705
|
+
body: toHtml(input.body),
|
|
3706
|
+
author,
|
|
3707
|
+
attachments: [],
|
|
3708
|
+
url: null,
|
|
3709
|
+
redacted: false
|
|
3710
|
+
},
|
|
3711
|
+
parts: []
|
|
3712
|
+
};
|
|
3713
|
+
this.state.conversations.insert(conversation.id, conversation);
|
|
3714
|
+
this.notify("conversation.admin.single.created", conversation, []);
|
|
3715
|
+
return conversation;
|
|
3716
|
+
}
|
|
3717
|
+
conversationField(conversation, field) {
|
|
3718
|
+
switch (field) {
|
|
3719
|
+
case "contact_ids":
|
|
3720
|
+
return [conversation.contactId];
|
|
3721
|
+
case "teammate_ids":
|
|
3722
|
+
return conversation.teammates;
|
|
3723
|
+
case "open":
|
|
3724
|
+
return conversation.state !== "closed";
|
|
3725
|
+
case "team_assignee_id":
|
|
3726
|
+
return null;
|
|
3727
|
+
case "priority":
|
|
3728
|
+
return "not_priority";
|
|
3729
|
+
default: {
|
|
3730
|
+
if (field.startsWith("source.")) {
|
|
3731
|
+
let value = conversation.source;
|
|
3732
|
+
for (const key of field.slice("source.".length).split(".")) {
|
|
3733
|
+
value = isRecord6(value) ? value[key] : void 0;
|
|
3734
|
+
}
|
|
3735
|
+
return value;
|
|
3736
|
+
}
|
|
3737
|
+
return conversation[field];
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
searchConversations(context) {
|
|
3742
|
+
const body = this.json(context);
|
|
3743
|
+
const query = parseQuery(body.query, CONVERSATION_FIELDS);
|
|
3744
|
+
const sortField = str(body.sort_field) ?? "updated_at";
|
|
3745
|
+
const descending = body.sort_order !== "ascending";
|
|
3746
|
+
const found = this.state.conversations.list({ order: "oldest" }).map((row) => row.value).filter(
|
|
3747
|
+
(conversation) => matches2(query, (field) => this.conversationField(conversation, field))
|
|
3748
|
+
).sort((a, b) => {
|
|
3749
|
+
const x = Number(this.conversationField(a, sortField) ?? 0);
|
|
3750
|
+
const y = Number(this.conversationField(b, sortField) ?? 0);
|
|
3751
|
+
return (descending ? y - x : x - y) || (descending ? Number(b.id) - Number(a.id) : Number(a.id) - Number(b.id));
|
|
3752
|
+
});
|
|
3753
|
+
const page = paginate(found, body.pagination, 20);
|
|
3754
|
+
const plaintext = context.query.display_as === "plaintext";
|
|
3755
|
+
if (faultEffect(context.request, "search_unavailable") !== void 0) {
|
|
3756
|
+
return jsonRes(200, {
|
|
3757
|
+
type: "conversation.list",
|
|
3758
|
+
pages: page.pages,
|
|
3759
|
+
total_count: found.length,
|
|
3760
|
+
conversations: null
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
const pages = faultEffect(context.request, "repeated_cursor") !== void 0 ? {
|
|
3764
|
+
...page.pages,
|
|
3765
|
+
next: { page: page.pages.page + 1, starting_after: encodeCursor(1e6) }
|
|
3766
|
+
} : page.pages;
|
|
3767
|
+
return jsonRes(200, {
|
|
3768
|
+
type: "conversation.list",
|
|
3769
|
+
pages,
|
|
3770
|
+
total_count: found.length,
|
|
3771
|
+
conversations: page.items.map(
|
|
3772
|
+
(conversation) => this.conversationBody(conversation, { plaintext })
|
|
3773
|
+
)
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Build and emit a `notification_event`. The item is the conversation with only the new
|
|
3778
|
+
* part(s), whose timestamps — like the envelope's — are wall-clock time, never the mock
|
|
3779
|
+
* clock: the EMR receiver rejects parts older than 5 minutes against its own clock.
|
|
3780
|
+
*/
|
|
3781
|
+
notify(topic, conversation, parts) {
|
|
3782
|
+
if (!this.onWebhook) return;
|
|
3783
|
+
const wall = Math.floor(this.wallClock() / 1e3);
|
|
3784
|
+
const item = this.conversationBody(conversation, {
|
|
3785
|
+
parts: parts.map((part) => ({
|
|
3786
|
+
...part,
|
|
3787
|
+
created_at: wall,
|
|
3788
|
+
updated_at: wall,
|
|
3789
|
+
notified_at: wall
|
|
3790
|
+
}))
|
|
3791
|
+
});
|
|
3792
|
+
this.onWebhook({
|
|
3793
|
+
type: "notification_event",
|
|
3794
|
+
app_id: this.state.workspaceId,
|
|
3795
|
+
data: { type: "notification_event_data", item },
|
|
3796
|
+
links: {},
|
|
3797
|
+
id: this.state.nextNotificationId(),
|
|
3798
|
+
topic,
|
|
3799
|
+
delivery_status: "pending",
|
|
3800
|
+
delivery_attempts: 1,
|
|
3801
|
+
delivered_at: 0,
|
|
3802
|
+
first_sent_at: wall,
|
|
3803
|
+
created_at: wall,
|
|
3804
|
+
self: null
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
/** Every conversation, oldest first (admin inspection). */
|
|
3808
|
+
conversations() {
|
|
3809
|
+
return this.state.conversations.list({ order: "oldest" }).map((row) => row.value);
|
|
3810
|
+
}
|
|
3811
|
+
contacts() {
|
|
3812
|
+
return this.state.contacts.list({ order: "oldest" }).map((row) => row.value);
|
|
3813
|
+
}
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
export {
|
|
3817
|
+
document,
|
|
3818
|
+
operationIds,
|
|
3819
|
+
supportedOperationIds,
|
|
3820
|
+
encodeCursor,
|
|
3821
|
+
decodeCursor,
|
|
3822
|
+
toHtml,
|
|
3823
|
+
toPlaintext,
|
|
3824
|
+
DEFAULT_SETTINGS,
|
|
3825
|
+
DEFAULT_ADMINS,
|
|
3826
|
+
HUB_SIGNATURE_HEADER,
|
|
3827
|
+
signHub,
|
|
3828
|
+
INTERCOM_PRESETS,
|
|
3829
|
+
createRuntime2 as createRuntime,
|
|
3830
|
+
INTERCOM_NAMESPACE,
|
|
3831
|
+
INTERCOM_TOPICS,
|
|
3832
|
+
IntercomError,
|
|
3833
|
+
IntercomAPI
|
|
3834
|
+
};
|
|
3835
|
+
//# sourceMappingURL=chunk-OSBG2XJ7.js.map
|