@crvouga/mockingbird-service-oauth 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 +289 -0
- package/dist/chunk-WYBO4XJW.js +378 -0
- package/dist/chunk-WYBO4XJW.js.map +7 -0
- package/dist/chunk-X6FYBOEA.js +3510 -0
- package/dist/chunk-X6FYBOEA.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1058 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1413 -0
- package/dist/server.js +14 -0
- package/dist/server.js.map +7 -0
- package/package.json +102 -0
|
@@ -0,0 +1,3510 @@
|
|
|
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 match2 = /^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(value.trim());
|
|
135
|
+
if (!match2)
|
|
136
|
+
return void 0;
|
|
137
|
+
return Number(match2[1]) * UNITS[match2[2]];
|
|
138
|
+
};
|
|
139
|
+
var parseInstant = (value) => {
|
|
140
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
141
|
+
return value;
|
|
142
|
+
if (typeof value !== "string")
|
|
143
|
+
return void 0;
|
|
144
|
+
const parsed = Date.parse(value);
|
|
145
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
146
|
+
};
|
|
147
|
+
var matchRoute = (pattern, path) => {
|
|
148
|
+
const want = pattern.split("/").filter(Boolean);
|
|
149
|
+
const have = path.split("/").filter(Boolean);
|
|
150
|
+
if (want.length !== have.length)
|
|
151
|
+
return void 0;
|
|
152
|
+
const params = {};
|
|
153
|
+
for (let i = 0; i < want.length; i++) {
|
|
154
|
+
const segment = want[i];
|
|
155
|
+
const actual = have[i];
|
|
156
|
+
if (segment.startsWith(":"))
|
|
157
|
+
params[segment.slice(1)] = decodeURIComponent(actual);
|
|
158
|
+
else if (segment !== actual)
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
return params;
|
|
162
|
+
};
|
|
163
|
+
var readJson = async (request) => {
|
|
164
|
+
const text = await request.text();
|
|
165
|
+
if (text.trim() === "")
|
|
166
|
+
return void 0;
|
|
167
|
+
return JSON.parse(text);
|
|
168
|
+
};
|
|
169
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
var createControlPlane = (context) => {
|
|
171
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
172
|
+
let snapshotCounter = 0;
|
|
173
|
+
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
|
+
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
175
|
+
const builtin = {
|
|
176
|
+
"GET /": () => json(200, {
|
|
177
|
+
service: context.name,
|
|
178
|
+
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
179
|
+
}),
|
|
180
|
+
"POST /reset": async ({ url, namespace }) => {
|
|
181
|
+
const target = url.searchParams.get("all") === "1" ? "*" : namespace;
|
|
182
|
+
await context.reset(target);
|
|
183
|
+
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
|
+
},
|
|
185
|
+
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
+
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
+
"POST /clock": ({ body }) => {
|
|
188
|
+
if (!isRecord(body))
|
|
189
|
+
return adminError(400, "expected a JSON object");
|
|
190
|
+
if (body.reset === true)
|
|
191
|
+
context.clock.reset();
|
|
192
|
+
if (body.set !== void 0) {
|
|
193
|
+
const instant = parseInstant(body.set);
|
|
194
|
+
if (instant === void 0)
|
|
195
|
+
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
196
|
+
context.clock.set(instant);
|
|
197
|
+
}
|
|
198
|
+
if (body.advance !== void 0) {
|
|
199
|
+
const delta = parseDuration(body.advance);
|
|
200
|
+
if (delta === void 0)
|
|
201
|
+
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
202
|
+
context.clock.advance(delta);
|
|
203
|
+
}
|
|
204
|
+
if (body.freeze === true)
|
|
205
|
+
context.clock.freeze();
|
|
206
|
+
if (body.freeze === false)
|
|
207
|
+
context.clock.unfreeze();
|
|
208
|
+
return json(200, context.clock.state());
|
|
209
|
+
},
|
|
210
|
+
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
|
+
"POST /faults": ({ body, namespace }) => {
|
|
212
|
+
if (isRecord(body) && typeof body.preset === "string") {
|
|
213
|
+
if (!context.applyPreset)
|
|
214
|
+
return adminError(400, `${context.name} has no fault presets`);
|
|
215
|
+
const { preset, ...overrides } = body;
|
|
216
|
+
try {
|
|
217
|
+
return json(201, {
|
|
218
|
+
preset,
|
|
219
|
+
rules: context.applyPreset(preset, namespace, overrides)
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return adminError(404, error instanceof Error ? error.message : String(error));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
|
|
226
|
+
return adminError(400, "a fault needs a numeric status, a delayMs/latencyMs, drop: true, an effect, or a preset");
|
|
227
|
+
}
|
|
228
|
+
const rule = {
|
|
229
|
+
// Scoped to the caller's namespace unless it asks for every one, so one worker's
|
|
230
|
+
// injected failure never lands on another's request.
|
|
231
|
+
namespace,
|
|
232
|
+
...body,
|
|
233
|
+
id: typeof body.id === "string" ? body.id : `fault_${context.faults.list().length + 1}`
|
|
234
|
+
};
|
|
235
|
+
return json(201, context.faults.add(rule));
|
|
236
|
+
},
|
|
237
|
+
"DELETE /faults": ({ url }) => {
|
|
238
|
+
const id = url.searchParams.get("id");
|
|
239
|
+
if (id === null) {
|
|
240
|
+
context.faults.clear();
|
|
241
|
+
return json(200, { status: "ok" });
|
|
242
|
+
}
|
|
243
|
+
return context.faults.remove(id) ? json(200, { status: "ok" }) : adminError(404, `no fault ${id}`);
|
|
244
|
+
},
|
|
245
|
+
"POST /snapshots": ({ namespace }) => {
|
|
246
|
+
const point = context.timeTravel.checkpoint(namespace, "main");
|
|
247
|
+
context.timeTravel.retain(namespace, point.id);
|
|
248
|
+
snapshotCounter++;
|
|
249
|
+
const id = `snap_${snapshotCounter}`;
|
|
250
|
+
snapshots.set(id, { namespace, checkpoint: point.id });
|
|
251
|
+
return json(201, { id, namespace, records: point.records ?? 0 });
|
|
252
|
+
},
|
|
253
|
+
"POST /snapshots/:id/restore": ({ params, namespace }) => {
|
|
254
|
+
const alias = snapshots.get(params.id);
|
|
255
|
+
if (!alias)
|
|
256
|
+
return adminError(404, `no snapshot ${params.id}`);
|
|
257
|
+
if (alias.namespace !== namespace) {
|
|
258
|
+
return adminError(409, `snapshot ${params.id} belongs to namespace ${alias.namespace}`);
|
|
259
|
+
}
|
|
260
|
+
context.timeTravel.checkout(alias.checkpoint, { namespace, branch: "main" });
|
|
261
|
+
return json(200, { status: "ok", id: params.id, namespace });
|
|
262
|
+
},
|
|
263
|
+
"DELETE /snapshots/:id": ({ params }) => {
|
|
264
|
+
const id = params.id;
|
|
265
|
+
const alias = snapshots.get(id);
|
|
266
|
+
if (!alias)
|
|
267
|
+
return adminError(404, `no snapshot ${id}`);
|
|
268
|
+
snapshots.delete(id);
|
|
269
|
+
context.timeTravel.release(alias.namespace, alias.checkpoint);
|
|
270
|
+
return json(200, { status: "ok" });
|
|
271
|
+
},
|
|
272
|
+
"GET /timeline": ({ namespace }) => json(200, context.timeTravel.inspect(namespace)),
|
|
273
|
+
"POST /checkpoints": ({ body, namespace }) => {
|
|
274
|
+
const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
|
|
275
|
+
try {
|
|
276
|
+
return json(201, context.timeTravel.checkpoint(namespace, branch));
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"POST /branches/:name": ({ params, body, namespace }) => {
|
|
282
|
+
const at = isRecord(body) && typeof body.at === "string" ? body.at : void 0;
|
|
283
|
+
try {
|
|
284
|
+
return json(201, context.timeTravel.branch(params.name, {
|
|
285
|
+
namespace,
|
|
286
|
+
...at !== void 0 ? { at } : {}
|
|
287
|
+
}));
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"POST /branches/:name/checkout": ({ params, body, namespace }) => {
|
|
293
|
+
if (!isRecord(body) || typeof body.checkpoint !== "string") {
|
|
294
|
+
return adminError(400, 'expected {"checkpoint":"cp_..."}');
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
context.timeTravel.checkout(body.checkpoint, {
|
|
298
|
+
namespace,
|
|
299
|
+
branch: params.name
|
|
300
|
+
});
|
|
301
|
+
return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"GET /requests": ({ url, namespace }) => {
|
|
307
|
+
const status = url.searchParams.get("status");
|
|
308
|
+
const since = url.searchParams.get("since");
|
|
309
|
+
const limit = url.searchParams.get("limit");
|
|
310
|
+
const sinceMs = since === null ? void 0 : parseInstant(/^\d+$/.test(since) ? Number(since) : since);
|
|
311
|
+
if (since !== null && sinceMs === void 0) {
|
|
312
|
+
return adminError(400, "since: expected epoch ms or ISO-8601");
|
|
313
|
+
}
|
|
314
|
+
if (status !== null && !/^\d{3}$/.test(status))
|
|
315
|
+
return adminError(400, "status: expected an HTTP status");
|
|
316
|
+
if (limit !== null && !/^\d+$/.test(limit))
|
|
317
|
+
return adminError(400, "limit: expected a count");
|
|
318
|
+
const operationId = url.searchParams.get("operationId");
|
|
319
|
+
const everyNamespace = url.searchParams.get("all") === "1";
|
|
320
|
+
return json(200, {
|
|
321
|
+
size: context.journal.size,
|
|
322
|
+
requests: context.journal.list({
|
|
323
|
+
...everyNamespace ? {} : { namespace },
|
|
324
|
+
...operationId !== null ? { operationId } : {},
|
|
325
|
+
...status !== null ? { status: Number(status) } : {},
|
|
326
|
+
...sinceMs !== void 0 ? { since: sinceMs } : {},
|
|
327
|
+
...limit !== null ? { limit: Number(limit) } : {}
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
"DELETE /requests": ({ url, namespace }) => {
|
|
332
|
+
context.journal.clear(url.searchParams.get("all") === "1" ? void 0 : namespace);
|
|
333
|
+
return json(200, { status: "ok" });
|
|
334
|
+
},
|
|
335
|
+
"GET /metrics": () => json(200, context.metrics.report()),
|
|
336
|
+
"DELETE /metrics": () => {
|
|
337
|
+
context.metrics.reset();
|
|
338
|
+
return json(200, { status: "ok" });
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const routes = [...Object.entries(context.routes), ...Object.entries(builtin)].map(([key, handler]) => {
|
|
342
|
+
const space = key.indexOf(" ");
|
|
343
|
+
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
namespaceOf: headerNamespace,
|
|
347
|
+
async handle(request) {
|
|
348
|
+
const url = new URL(request.url);
|
|
349
|
+
if (url.pathname === HEALTH_PATH && request.method === "GET") {
|
|
350
|
+
return json(200, {
|
|
351
|
+
status: "ok",
|
|
352
|
+
service: context.name,
|
|
353
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
+
clock: context.clock.state(),
|
|
355
|
+
namespaces: context.namespaces().length,
|
|
356
|
+
...context.describe()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (url.pathname !== ADMIN_PREFIX && !url.pathname.startsWith(`${ADMIN_PREFIX}/`)) {
|
|
360
|
+
return void 0;
|
|
361
|
+
}
|
|
362
|
+
if (context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
363
|
+
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
364
|
+
}
|
|
365
|
+
const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
|
|
366
|
+
for (const route of routes) {
|
|
367
|
+
if (route.method !== request.method)
|
|
368
|
+
continue;
|
|
369
|
+
const params = matchRoute(route.pattern, path);
|
|
370
|
+
if (!params)
|
|
371
|
+
continue;
|
|
372
|
+
let body;
|
|
373
|
+
try {
|
|
374
|
+
body = await readJson(request);
|
|
375
|
+
} catch {
|
|
376
|
+
return adminError(400, "request body is not valid JSON");
|
|
377
|
+
}
|
|
378
|
+
return route.handler({
|
|
379
|
+
request,
|
|
380
|
+
url,
|
|
381
|
+
params,
|
|
382
|
+
namespace: adminNamespace(request, url),
|
|
383
|
+
body
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${ADMIN_PREFIX} lists them`);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// ../core/dist/credentials.js
|
|
392
|
+
var createCredentialRegistry = () => {
|
|
393
|
+
const map = /* @__PURE__ */ new Map();
|
|
394
|
+
return {
|
|
395
|
+
set: (credential, namespace) => {
|
|
396
|
+
map.set(credential, namespace);
|
|
397
|
+
},
|
|
398
|
+
get: (credential) => map.get(credential),
|
|
399
|
+
remove: (credential) => map.delete(credential),
|
|
400
|
+
clear: () => map.clear(),
|
|
401
|
+
entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
|
|
405
|
+
|
|
406
|
+
// ../core/dist/rng.js
|
|
407
|
+
var seedFrom = (value) => {
|
|
408
|
+
let hash2 = 2166136261;
|
|
409
|
+
for (let i = 0; i < value.length; i++) {
|
|
410
|
+
hash2 ^= value.charCodeAt(i);
|
|
411
|
+
hash2 = Math.imul(hash2, 16777619);
|
|
412
|
+
}
|
|
413
|
+
return hash2 >>> 0;
|
|
414
|
+
};
|
|
415
|
+
var createRng = (seed = 0) => {
|
|
416
|
+
const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
|
|
417
|
+
let state = numeric;
|
|
418
|
+
const next = () => {
|
|
419
|
+
state = state + 1831565813 >>> 0;
|
|
420
|
+
let t = state;
|
|
421
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
422
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
423
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
next,
|
|
427
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
428
|
+
reset: () => {
|
|
429
|
+
state = numeric;
|
|
430
|
+
},
|
|
431
|
+
state: () => state,
|
|
432
|
+
setState: (next2) => {
|
|
433
|
+
if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
|
|
434
|
+
throw new RangeError("rng state must be an unsigned 32-bit integer");
|
|
435
|
+
}
|
|
436
|
+
state = next2 >>> 0;
|
|
437
|
+
},
|
|
438
|
+
seed: numeric
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// ../core/dist/faults.js
|
|
443
|
+
var matches = (rule, candidate) => {
|
|
444
|
+
if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
|
|
448
|
+
return false;
|
|
449
|
+
if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
|
|
453
|
+
return false;
|
|
454
|
+
return true;
|
|
455
|
+
};
|
|
456
|
+
var faultResponse = (rule) => {
|
|
457
|
+
const status = rule.status ?? 500;
|
|
458
|
+
const headers = { "content-type": "application/json", ...rule.headers };
|
|
459
|
+
if (typeof rule.body === "string")
|
|
460
|
+
return new Response(rule.body, { status, headers });
|
|
461
|
+
if (rule.body === null)
|
|
462
|
+
return new Response(null, { status, headers: rule.headers ?? {} });
|
|
463
|
+
const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
|
|
464
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
465
|
+
};
|
|
466
|
+
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
467
|
+
const entries = [];
|
|
468
|
+
return {
|
|
469
|
+
add(rule) {
|
|
470
|
+
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
471
|
+
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
472
|
+
if (existing >= 0)
|
|
473
|
+
entries[existing] = entry;
|
|
474
|
+
else
|
|
475
|
+
entries.push(entry);
|
|
476
|
+
return rule;
|
|
477
|
+
},
|
|
478
|
+
list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
|
|
479
|
+
remove(id) {
|
|
480
|
+
const index = entries.findIndex((e) => e.rule.id === id);
|
|
481
|
+
if (index < 0)
|
|
482
|
+
return false;
|
|
483
|
+
entries.splice(index, 1);
|
|
484
|
+
return true;
|
|
485
|
+
},
|
|
486
|
+
clear() {
|
|
487
|
+
entries.length = 0;
|
|
488
|
+
},
|
|
489
|
+
async take(candidate) {
|
|
490
|
+
const hits = [];
|
|
491
|
+
for (const entry of entries) {
|
|
492
|
+
if (entry.remaining === 0)
|
|
493
|
+
continue;
|
|
494
|
+
if (!matches(entry.rule, candidate))
|
|
495
|
+
continue;
|
|
496
|
+
const rate = entry.rule.rate ?? 1;
|
|
497
|
+
if (rng.next() >= rate)
|
|
498
|
+
continue;
|
|
499
|
+
entry.hits++;
|
|
500
|
+
if (entry.remaining !== null)
|
|
501
|
+
entry.remaining--;
|
|
502
|
+
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
503
|
+
if (delay !== void 0 && delay > 0) {
|
|
504
|
+
await sleep(delay);
|
|
505
|
+
}
|
|
506
|
+
const hit = { id: entry.rule.id };
|
|
507
|
+
if (entry.rule.effect !== void 0) {
|
|
508
|
+
hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
|
|
509
|
+
}
|
|
510
|
+
if (entry.rule.drop === true)
|
|
511
|
+
hit.drop = true;
|
|
512
|
+
else if (entry.rule.status !== void 0)
|
|
513
|
+
hit.response = faultResponse(entry.rule);
|
|
514
|
+
hits.push(hit);
|
|
515
|
+
if (hit.drop || hit.response)
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
return hits;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// ../../openapi/core/dist/refs.js
|
|
524
|
+
var OpenAPIReferenceError = class extends Error {
|
|
525
|
+
ref;
|
|
526
|
+
constructor(ref) {
|
|
527
|
+
super(`unresolvable $ref: ${ref}`);
|
|
528
|
+
this.ref = ref;
|
|
529
|
+
this.name = "OpenAPIReferenceError";
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
533
|
+
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
534
|
+
var resolveRef = (document2, ref) => {
|
|
535
|
+
if (!ref.startsWith("#/"))
|
|
536
|
+
throw new OpenAPIReferenceError(ref);
|
|
537
|
+
let cursor = document2;
|
|
538
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
539
|
+
const segment = unescapePointer(raw);
|
|
540
|
+
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
541
|
+
throw new OpenAPIReferenceError(ref);
|
|
542
|
+
}
|
|
543
|
+
cursor = cursor[segment];
|
|
544
|
+
}
|
|
545
|
+
if (cursor === void 0)
|
|
546
|
+
throw new OpenAPIReferenceError(ref);
|
|
547
|
+
return cursor;
|
|
548
|
+
};
|
|
549
|
+
var deref = (document2, value) => {
|
|
550
|
+
let current = value;
|
|
551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
552
|
+
while (isReference(current)) {
|
|
553
|
+
if (seen.has(current.$ref))
|
|
554
|
+
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
555
|
+
seen.add(current.$ref);
|
|
556
|
+
current = resolveRef(document2, current.$ref);
|
|
557
|
+
}
|
|
558
|
+
return current;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// ../../openapi/core/dist/types.js
|
|
562
|
+
var HTTP_METHODS = [
|
|
563
|
+
"get",
|
|
564
|
+
"put",
|
|
565
|
+
"post",
|
|
566
|
+
"delete",
|
|
567
|
+
"options",
|
|
568
|
+
"head",
|
|
569
|
+
"patch",
|
|
570
|
+
"trace"
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
// ../../openapi/core/dist/document.js
|
|
574
|
+
var mergeParameters = (document2, item, own) => {
|
|
575
|
+
const merged = /* @__PURE__ */ new Map();
|
|
576
|
+
for (const raw of item.parameters ?? []) {
|
|
577
|
+
const parameter = deref(document2, raw);
|
|
578
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
579
|
+
}
|
|
580
|
+
for (const raw of own ?? []) {
|
|
581
|
+
const parameter = deref(document2, raw);
|
|
582
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
583
|
+
}
|
|
584
|
+
return [...merged.values()];
|
|
585
|
+
};
|
|
586
|
+
var listOperations = (document2) => {
|
|
587
|
+
const operations = [];
|
|
588
|
+
for (const [path, item] of Object.entries(document2.paths)) {
|
|
589
|
+
for (const method of HTTP_METHODS) {
|
|
590
|
+
const operation = item[method];
|
|
591
|
+
if (operation?.operationId === void 0)
|
|
592
|
+
continue;
|
|
593
|
+
const responses = {};
|
|
594
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
595
|
+
responses[status] = deref(document2, response);
|
|
596
|
+
}
|
|
597
|
+
operations.push({
|
|
598
|
+
operationId: operation.operationId,
|
|
599
|
+
method,
|
|
600
|
+
path,
|
|
601
|
+
operation,
|
|
602
|
+
parameters: mergeParameters(document2, item, operation.parameters),
|
|
603
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
|
|
604
|
+
responses
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return operations;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// ../core/dist/journal.js
|
|
612
|
+
var DEFAULT_JOURNAL_SIZE = 1e3;
|
|
613
|
+
var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
614
|
+
const capacity = Math.max(0, Math.floor(size));
|
|
615
|
+
const rings = /* @__PURE__ */ new Map();
|
|
616
|
+
let sequence = 0;
|
|
617
|
+
const order = /* @__PURE__ */ new WeakMap();
|
|
618
|
+
const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
|
|
619
|
+
return {
|
|
620
|
+
size: capacity,
|
|
621
|
+
record(entry) {
|
|
622
|
+
if (capacity === 0)
|
|
623
|
+
return;
|
|
624
|
+
order.set(entry, sequence++);
|
|
625
|
+
let ring = rings.get(entry.namespace);
|
|
626
|
+
if (!ring) {
|
|
627
|
+
ring = { entries: [], next: 0 };
|
|
628
|
+
rings.set(entry.namespace, ring);
|
|
629
|
+
}
|
|
630
|
+
if (ring.entries.length < capacity)
|
|
631
|
+
ring.entries.push(entry);
|
|
632
|
+
else {
|
|
633
|
+
ring.entries[ring.next] = entry;
|
|
634
|
+
ring.next = (ring.next + 1) % capacity;
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
list(query = {}) {
|
|
638
|
+
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));
|
|
639
|
+
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));
|
|
640
|
+
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
641
|
+
},
|
|
642
|
+
clear(namespace) {
|
|
643
|
+
if (namespace === void 0)
|
|
644
|
+
rings.clear();
|
|
645
|
+
else
|
|
646
|
+
rings.delete(namespace);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
var notes = /* @__PURE__ */ new WeakMap();
|
|
651
|
+
var responseNotes = (response) => notes.get(response);
|
|
652
|
+
|
|
653
|
+
// ../core/dist/metrics.js
|
|
654
|
+
var createMetrics = () => {
|
|
655
|
+
let requests = 0;
|
|
656
|
+
let faults = 0;
|
|
657
|
+
let totalDurationMs = 0;
|
|
658
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
659
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
660
|
+
return {
|
|
661
|
+
record(entry) {
|
|
662
|
+
requests++;
|
|
663
|
+
totalDurationMs += entry.durationMs;
|
|
664
|
+
if (entry.faultId !== void 0)
|
|
665
|
+
faults++;
|
|
666
|
+
const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
|
|
667
|
+
byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
|
|
668
|
+
if (entry.unmatched) {
|
|
669
|
+
const route = `${entry.method} ${entry.path}`;
|
|
670
|
+
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
report: () => ({
|
|
674
|
+
requests,
|
|
675
|
+
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
676
|
+
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
677
|
+
const space = route.indexOf(" ");
|
|
678
|
+
return { method: route.slice(0, space), path: route.slice(space + 1), count };
|
|
679
|
+
}),
|
|
680
|
+
faults,
|
|
681
|
+
totalDurationMs
|
|
682
|
+
}),
|
|
683
|
+
reset() {
|
|
684
|
+
requests = 0;
|
|
685
|
+
faults = 0;
|
|
686
|
+
totalDurationMs = 0;
|
|
687
|
+
byOperation.clear();
|
|
688
|
+
unmatched.clear();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// ../../core/dist/timeline.js
|
|
694
|
+
var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
695
|
+
var Timeline = class {
|
|
696
|
+
maxCheckpoints;
|
|
697
|
+
now;
|
|
698
|
+
makeId;
|
|
699
|
+
nodes = /* @__PURE__ */ new Map();
|
|
700
|
+
heads = /* @__PURE__ */ new Map();
|
|
701
|
+
/** Unreferenced nodes in the exact order they became collectible. */
|
|
702
|
+
evictable = /* @__PURE__ */ new Set();
|
|
703
|
+
/** Branch heads plus explicit retainers. Absent means zero. */
|
|
704
|
+
references = /* @__PURE__ */ new Map();
|
|
705
|
+
explicitPins = /* @__PURE__ */ new Map();
|
|
706
|
+
sequence = 0;
|
|
707
|
+
constructor(options = {}) {
|
|
708
|
+
const max = options.maxCheckpoints ?? 1e3;
|
|
709
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
710
|
+
throw new RangeError("maxCheckpoints must be a positive integer");
|
|
711
|
+
this.maxCheckpoints = max;
|
|
712
|
+
this.now = options.now ?? (() => this.sequence);
|
|
713
|
+
this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
|
|
714
|
+
}
|
|
715
|
+
/** Capture a new immutable value and move `branch` to it. */
|
|
716
|
+
commit(value, options = {}) {
|
|
717
|
+
const branch = options.branch ?? "main";
|
|
718
|
+
this.assertBranch(branch);
|
|
719
|
+
const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
|
|
720
|
+
if (parent !== null && !this.nodes.has(parent))
|
|
721
|
+
throw new RangeError(`no checkpoint ${parent}`);
|
|
722
|
+
const id = this.makeId(++this.sequence);
|
|
723
|
+
if (this.nodes.has(id))
|
|
724
|
+
throw new RangeError(`duplicate checkpoint id ${id}`);
|
|
725
|
+
const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
|
|
726
|
+
this.nodes.set(id, checkpoint);
|
|
727
|
+
this.moveHead(branch, id);
|
|
728
|
+
this.collect(this.maxCheckpoints);
|
|
729
|
+
return checkpoint;
|
|
730
|
+
}
|
|
731
|
+
/** Create a branch pointer without copying its checkpoint value. */
|
|
732
|
+
fork(branch, options = {}) {
|
|
733
|
+
this.assertBranch(branch);
|
|
734
|
+
if (this.heads.has(branch))
|
|
735
|
+
throw new RangeError(`branch already exists: ${branch}`);
|
|
736
|
+
const from = options.from ?? this.heads.get("main");
|
|
737
|
+
if (from === void 0)
|
|
738
|
+
return void 0;
|
|
739
|
+
const checkpoint = this.get(from);
|
|
740
|
+
this.moveHead(branch, checkpoint.id);
|
|
741
|
+
return checkpoint;
|
|
742
|
+
}
|
|
743
|
+
/** Move a branch pointer to an existing checkpoint. */
|
|
744
|
+
checkout(branch, id) {
|
|
745
|
+
this.assertBranch(branch);
|
|
746
|
+
const checkpoint = this.get(id);
|
|
747
|
+
this.moveHead(branch, checkpoint.id);
|
|
748
|
+
return checkpoint;
|
|
749
|
+
}
|
|
750
|
+
get(id) {
|
|
751
|
+
const checkpoint = this.nodes.get(id);
|
|
752
|
+
if (!checkpoint)
|
|
753
|
+
throw new RangeError(`no checkpoint ${id}`);
|
|
754
|
+
return checkpoint;
|
|
755
|
+
}
|
|
756
|
+
head(branch = "main") {
|
|
757
|
+
const id = this.heads.get(branch);
|
|
758
|
+
return id === void 0 ? void 0 : this.get(id);
|
|
759
|
+
}
|
|
760
|
+
hasBranch(branch) {
|
|
761
|
+
return this.heads.has(branch);
|
|
762
|
+
}
|
|
763
|
+
branches() {
|
|
764
|
+
return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
765
|
+
}
|
|
766
|
+
checkpoints() {
|
|
767
|
+
return [...this.nodes.values()];
|
|
768
|
+
}
|
|
769
|
+
/** Number of retained checkpoints without allocating an array. */
|
|
770
|
+
get size() {
|
|
771
|
+
return this.nodes.size;
|
|
772
|
+
}
|
|
773
|
+
/** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
|
|
774
|
+
retain(id) {
|
|
775
|
+
const checkpoint = this.get(id);
|
|
776
|
+
this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
|
|
777
|
+
this.addReference(id);
|
|
778
|
+
return checkpoint;
|
|
779
|
+
}
|
|
780
|
+
/** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
|
|
781
|
+
release(id) {
|
|
782
|
+
if (!this.nodes.has(id))
|
|
783
|
+
return false;
|
|
784
|
+
const pins = this.explicitPins.get(id) ?? 0;
|
|
785
|
+
if (pins === 0)
|
|
786
|
+
return false;
|
|
787
|
+
if (pins === 1)
|
|
788
|
+
this.explicitPins.delete(id);
|
|
789
|
+
else
|
|
790
|
+
this.explicitPins.set(id, pins - 1);
|
|
791
|
+
this.removeReference(id);
|
|
792
|
+
this.collect(this.maxCheckpoints);
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
deleteBranch(branch) {
|
|
796
|
+
if (branch === "main")
|
|
797
|
+
throw new RangeError("cannot delete main branch");
|
|
798
|
+
const previous = this.heads.get(branch);
|
|
799
|
+
const deleted = this.heads.delete(branch);
|
|
800
|
+
if (previous !== void 0)
|
|
801
|
+
this.removeReference(previous);
|
|
802
|
+
this.collect(this.maxCheckpoints);
|
|
803
|
+
return deleted;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
|
|
807
|
+
* commits never scan pinned nodes or the retained history. Parents are metadata rather than a
|
|
808
|
+
* storage dependency, so a retained node remains usable after pruning.
|
|
809
|
+
*/
|
|
810
|
+
gc(max = this.maxCheckpoints) {
|
|
811
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
812
|
+
throw new RangeError("max must be a positive integer");
|
|
813
|
+
const removed = [];
|
|
814
|
+
this.collect(max, removed);
|
|
815
|
+
return removed;
|
|
816
|
+
}
|
|
817
|
+
collect(max, removed) {
|
|
818
|
+
while (this.nodes.size > max && this.evictable.size > 0) {
|
|
819
|
+
const id = this.evictable.values().next().value;
|
|
820
|
+
this.evictable.delete(id);
|
|
821
|
+
this.nodes.delete(id);
|
|
822
|
+
removed?.push(id);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
moveHead(branch, id) {
|
|
826
|
+
const previous = this.heads.get(branch);
|
|
827
|
+
if (previous === id)
|
|
828
|
+
return;
|
|
829
|
+
if (previous !== void 0)
|
|
830
|
+
this.removeReference(previous);
|
|
831
|
+
this.heads.set(branch, id);
|
|
832
|
+
this.addReference(id);
|
|
833
|
+
}
|
|
834
|
+
addReference(id) {
|
|
835
|
+
this.references.set(id, (this.references.get(id) ?? 0) + 1);
|
|
836
|
+
this.evictable.delete(id);
|
|
837
|
+
}
|
|
838
|
+
removeReference(id) {
|
|
839
|
+
const next = (this.references.get(id) ?? 0) - 1;
|
|
840
|
+
if (next > 0)
|
|
841
|
+
this.references.set(id, next);
|
|
842
|
+
else {
|
|
843
|
+
this.references.delete(id);
|
|
844
|
+
if (this.nodes.has(id))
|
|
845
|
+
this.evictable.add(id);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
assertBranch(branch) {
|
|
849
|
+
if (!BRANCH_PATTERN.test(branch))
|
|
850
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// ../../sqlite/dist/default.js
|
|
855
|
+
import { Database } from "@crvouga/mockingbird-service-sqlite";
|
|
856
|
+
var createDefaultSqlite = () => new Database();
|
|
857
|
+
var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
|
|
858
|
+
|
|
859
|
+
// ../../sqlite/dist/migrate.js
|
|
860
|
+
var ensureMigrationsTable = (sqlite) => {
|
|
861
|
+
sqlite.exec(`
|
|
862
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
863
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
864
|
+
applied_at INTEGER NOT NULL
|
|
865
|
+
)
|
|
866
|
+
`);
|
|
867
|
+
};
|
|
868
|
+
var migrate = (sqlite, migrations) => {
|
|
869
|
+
ensureMigrationsTable(sqlite);
|
|
870
|
+
const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
871
|
+
const pending = migrations.filter((migration) => !applied.has(migration.id));
|
|
872
|
+
if (pending.length === 0)
|
|
873
|
+
return;
|
|
874
|
+
const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
875
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
876
|
+
sqlite.transaction(() => {
|
|
877
|
+
for (const migration of pending) {
|
|
878
|
+
sqlite.exec(migration.sql);
|
|
879
|
+
insert.run(migration.id, now);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// ../../sqlite/dist/schema.js
|
|
885
|
+
var CORE_MIGRATIONS = [
|
|
886
|
+
{
|
|
887
|
+
id: "20260322_core_records_sequences",
|
|
888
|
+
sql: `
|
|
889
|
+
CREATE TABLE IF NOT EXISTS mockingbird_records (
|
|
890
|
+
namespace TEXT NOT NULL,
|
|
891
|
+
collection TEXT NOT NULL,
|
|
892
|
+
id TEXT NOT NULL,
|
|
893
|
+
seq INTEGER NOT NULL,
|
|
894
|
+
value TEXT NOT NULL,
|
|
895
|
+
PRIMARY KEY (namespace, collection, id)
|
|
896
|
+
);
|
|
897
|
+
CREATE INDEX IF NOT EXISTS mockingbird_records_seq
|
|
898
|
+
ON mockingbird_records (namespace, collection, seq);
|
|
899
|
+
CREATE TABLE IF NOT EXISTS mockingbird_sequences (
|
|
900
|
+
namespace TEXT NOT NULL,
|
|
901
|
+
name TEXT NOT NULL,
|
|
902
|
+
kind TEXT NOT NULL,
|
|
903
|
+
value INTEGER NOT NULL,
|
|
904
|
+
PRIMARY KEY (namespace, name, kind)
|
|
905
|
+
);
|
|
906
|
+
`
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
var migrateCore = (sqlite) => {
|
|
910
|
+
migrate(sqlite, CORE_MIGRATIONS);
|
|
911
|
+
};
|
|
912
|
+
var clearNamespace = (sqlite, namespace) => {
|
|
913
|
+
sqlite.transaction(() => {
|
|
914
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
915
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
916
|
+
});
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// ../../../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/request/constants.js
|
|
920
|
+
var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
|
|
921
|
+
|
|
922
|
+
// ../../../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/utils/body.js
|
|
923
|
+
var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
|
|
924
|
+
const { all = false, dot = false } = options;
|
|
925
|
+
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers;
|
|
926
|
+
const contentType = headers.get("Content-Type");
|
|
927
|
+
if (contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded")) {
|
|
928
|
+
return parseFormData(request, { all, dot });
|
|
929
|
+
}
|
|
930
|
+
return {};
|
|
931
|
+
};
|
|
932
|
+
async function parseFormData(request, options) {
|
|
933
|
+
const formData = await request.formData();
|
|
934
|
+
if (formData) {
|
|
935
|
+
return convertFormDataToBodyData(formData, options);
|
|
936
|
+
}
|
|
937
|
+
return {};
|
|
938
|
+
}
|
|
939
|
+
function convertFormDataToBodyData(formData, options) {
|
|
940
|
+
const form = /* @__PURE__ */ Object.create(null);
|
|
941
|
+
formData.forEach((value, key) => {
|
|
942
|
+
const shouldParseAllValues = options.all || key.endsWith("[]");
|
|
943
|
+
if (!shouldParseAllValues) {
|
|
944
|
+
form[key] = value;
|
|
945
|
+
} else {
|
|
946
|
+
handleParsingAllValues(form, key, value);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
if (options.dot) {
|
|
950
|
+
Object.entries(form).forEach(([key, value]) => {
|
|
951
|
+
const shouldParseDotValues = key.includes(".");
|
|
952
|
+
if (shouldParseDotValues) {
|
|
953
|
+
handleParsingNestedValues(form, key, value);
|
|
954
|
+
delete form[key];
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return form;
|
|
959
|
+
}
|
|
960
|
+
var handleParsingAllValues = (form, key, value) => {
|
|
961
|
+
if (form[key] !== void 0) {
|
|
962
|
+
if (Array.isArray(form[key])) {
|
|
963
|
+
;
|
|
964
|
+
form[key].push(value);
|
|
965
|
+
} else {
|
|
966
|
+
form[key] = [form[key], value];
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
if (!key.endsWith("[]")) {
|
|
970
|
+
form[key] = value;
|
|
971
|
+
} else {
|
|
972
|
+
form[key] = [value];
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
var handleParsingNestedValues = (form, key, value) => {
|
|
977
|
+
let nestedForm = form;
|
|
978
|
+
const keys2 = key.split(".");
|
|
979
|
+
keys2.forEach((key2, index) => {
|
|
980
|
+
if (index === keys2.length - 1) {
|
|
981
|
+
nestedForm[key2] = value;
|
|
982
|
+
} else {
|
|
983
|
+
if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
|
|
984
|
+
nestedForm[key2] = /* @__PURE__ */ Object.create(null);
|
|
985
|
+
}
|
|
986
|
+
nestedForm = nestedForm[key2];
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// ../../../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/utils/url.js
|
|
992
|
+
var tryDecode = (str, decoder) => {
|
|
993
|
+
try {
|
|
994
|
+
return decoder(str);
|
|
995
|
+
} catch {
|
|
996
|
+
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
|
|
997
|
+
try {
|
|
998
|
+
return decoder(match2);
|
|
999
|
+
} catch {
|
|
1000
|
+
return match2;
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
var _decodeURI = (value) => {
|
|
1006
|
+
if (!/[%+]/.test(value)) {
|
|
1007
|
+
return value;
|
|
1008
|
+
}
|
|
1009
|
+
if (value.indexOf("+") !== -1) {
|
|
1010
|
+
value = value.replace(/\+/g, " ");
|
|
1011
|
+
}
|
|
1012
|
+
return value.indexOf("%") !== -1 ? tryDecode(value, decodeURIComponent_) : value;
|
|
1013
|
+
};
|
|
1014
|
+
var _getQueryParam = (url, key, multiple) => {
|
|
1015
|
+
let encoded;
|
|
1016
|
+
if (!multiple && key && !/[%+]/.test(key)) {
|
|
1017
|
+
let keyIndex2 = url.indexOf("?", 8);
|
|
1018
|
+
if (keyIndex2 === -1) {
|
|
1019
|
+
return void 0;
|
|
1020
|
+
}
|
|
1021
|
+
if (!url.startsWith(key, keyIndex2 + 1)) {
|
|
1022
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
1023
|
+
}
|
|
1024
|
+
while (keyIndex2 !== -1) {
|
|
1025
|
+
const trailingKeyCode = url.charCodeAt(keyIndex2 + key.length + 1);
|
|
1026
|
+
if (trailingKeyCode === 61) {
|
|
1027
|
+
const valueIndex = keyIndex2 + key.length + 2;
|
|
1028
|
+
const endIndex = url.indexOf("&", valueIndex);
|
|
1029
|
+
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
|
|
1030
|
+
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) {
|
|
1031
|
+
return "";
|
|
1032
|
+
}
|
|
1033
|
+
keyIndex2 = url.indexOf(`&${key}`, keyIndex2 + 1);
|
|
1034
|
+
}
|
|
1035
|
+
encoded = /[%+]/.test(url);
|
|
1036
|
+
if (!encoded) {
|
|
1037
|
+
return void 0;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const results = {};
|
|
1041
|
+
encoded ??= /[%+]/.test(url);
|
|
1042
|
+
let keyIndex = url.indexOf("?", 8);
|
|
1043
|
+
while (keyIndex !== -1) {
|
|
1044
|
+
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
|
|
1045
|
+
let valueIndex = url.indexOf("=", keyIndex);
|
|
1046
|
+
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) {
|
|
1047
|
+
valueIndex = -1;
|
|
1048
|
+
}
|
|
1049
|
+
let name = url.slice(
|
|
1050
|
+
keyIndex + 1,
|
|
1051
|
+
valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex
|
|
1052
|
+
);
|
|
1053
|
+
if (encoded) {
|
|
1054
|
+
name = _decodeURI(name);
|
|
1055
|
+
}
|
|
1056
|
+
keyIndex = nextKeyIndex;
|
|
1057
|
+
if (name === "") {
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
let value;
|
|
1061
|
+
if (valueIndex === -1) {
|
|
1062
|
+
value = "";
|
|
1063
|
+
} else {
|
|
1064
|
+
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
|
|
1065
|
+
if (encoded) {
|
|
1066
|
+
value = _decodeURI(value);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (multiple) {
|
|
1070
|
+
if (!(results[name] && Array.isArray(results[name]))) {
|
|
1071
|
+
results[name] = [];
|
|
1072
|
+
}
|
|
1073
|
+
;
|
|
1074
|
+
results[name].push(value);
|
|
1075
|
+
} else {
|
|
1076
|
+
results[name] ??= value;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return key ? results[key] : results;
|
|
1080
|
+
};
|
|
1081
|
+
var getQueryParam = _getQueryParam;
|
|
1082
|
+
var getQueryParams = (url, key) => {
|
|
1083
|
+
return _getQueryParam(url, key, true);
|
|
1084
|
+
};
|
|
1085
|
+
var decodeURIComponent_ = decodeURIComponent;
|
|
1086
|
+
|
|
1087
|
+
// ../../../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/request.js
|
|
1088
|
+
var tryDecodeURIComponent = (str) => tryDecode(str, decodeURIComponent_);
|
|
1089
|
+
var HonoRequest = class {
|
|
1090
|
+
/**
|
|
1091
|
+
* `.raw` can get the raw Request object.
|
|
1092
|
+
*
|
|
1093
|
+
* @see {@link https://hono.dev/docs/api/request#raw}
|
|
1094
|
+
*
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```ts
|
|
1097
|
+
* // For Cloudflare Workers
|
|
1098
|
+
* app.post('/', async (c) => {
|
|
1099
|
+
* const metadata = c.req.raw.cf?.hostMetadata?
|
|
1100
|
+
* ...
|
|
1101
|
+
* })
|
|
1102
|
+
* ```
|
|
1103
|
+
*/
|
|
1104
|
+
raw;
|
|
1105
|
+
#validatedData;
|
|
1106
|
+
// Short name of validatedData
|
|
1107
|
+
#matchResult;
|
|
1108
|
+
routeIndex = 0;
|
|
1109
|
+
/**
|
|
1110
|
+
* `.path` can get the pathname of the request.
|
|
1111
|
+
*
|
|
1112
|
+
* @see {@link https://hono.dev/docs/api/request#path}
|
|
1113
|
+
*
|
|
1114
|
+
* @example
|
|
1115
|
+
* ```ts
|
|
1116
|
+
* app.get('/about/me', (c) => {
|
|
1117
|
+
* const pathname = c.req.path // `/about/me`
|
|
1118
|
+
* })
|
|
1119
|
+
* ```
|
|
1120
|
+
*/
|
|
1121
|
+
path;
|
|
1122
|
+
bodyCache = {};
|
|
1123
|
+
constructor(request, path = "/", matchResult = [[]]) {
|
|
1124
|
+
this.raw = request;
|
|
1125
|
+
this.path = path;
|
|
1126
|
+
this.#matchResult = matchResult;
|
|
1127
|
+
this.#validatedData = {};
|
|
1128
|
+
}
|
|
1129
|
+
param(key) {
|
|
1130
|
+
return key ? this.#getDecodedParam(key) : this.#getAllDecodedParams();
|
|
1131
|
+
}
|
|
1132
|
+
#getDecodedParam(key) {
|
|
1133
|
+
const paramKey = this.#matchResult[0][this.routeIndex][1][key];
|
|
1134
|
+
const param = this.#getParamValue(paramKey);
|
|
1135
|
+
return param && /\%/.test(param) ? tryDecodeURIComponent(param) : param;
|
|
1136
|
+
}
|
|
1137
|
+
#getAllDecodedParams() {
|
|
1138
|
+
const decoded = {};
|
|
1139
|
+
const keys2 = Object.keys(this.#matchResult[0][this.routeIndex][1]);
|
|
1140
|
+
for (const key of keys2) {
|
|
1141
|
+
const value = this.#getParamValue(this.#matchResult[0][this.routeIndex][1][key]);
|
|
1142
|
+
if (value !== void 0) {
|
|
1143
|
+
decoded[key] = /\%/.test(value) ? tryDecodeURIComponent(value) : value;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return decoded;
|
|
1147
|
+
}
|
|
1148
|
+
#getParamValue(paramKey) {
|
|
1149
|
+
return this.#matchResult[1] ? this.#matchResult[1][paramKey] : paramKey;
|
|
1150
|
+
}
|
|
1151
|
+
query(key) {
|
|
1152
|
+
return getQueryParam(this.url, key);
|
|
1153
|
+
}
|
|
1154
|
+
queries(key) {
|
|
1155
|
+
return getQueryParams(this.url, key);
|
|
1156
|
+
}
|
|
1157
|
+
header(name) {
|
|
1158
|
+
if (name) {
|
|
1159
|
+
return this.raw.headers.get(name) ?? void 0;
|
|
1160
|
+
}
|
|
1161
|
+
const headerData = {};
|
|
1162
|
+
this.raw.headers.forEach((value, key) => {
|
|
1163
|
+
headerData[key] = value;
|
|
1164
|
+
});
|
|
1165
|
+
return headerData;
|
|
1166
|
+
}
|
|
1167
|
+
async parseBody(options) {
|
|
1168
|
+
return this.bodyCache.parsedBody ??= await parseBody(this, options);
|
|
1169
|
+
}
|
|
1170
|
+
#cachedBody = (key) => {
|
|
1171
|
+
const { bodyCache, raw } = this;
|
|
1172
|
+
const cachedBody = bodyCache[key];
|
|
1173
|
+
if (cachedBody) {
|
|
1174
|
+
return cachedBody;
|
|
1175
|
+
}
|
|
1176
|
+
const anyCachedKey = Object.keys(bodyCache)[0];
|
|
1177
|
+
if (anyCachedKey) {
|
|
1178
|
+
return bodyCache[anyCachedKey].then((body) => {
|
|
1179
|
+
if (anyCachedKey === "json") {
|
|
1180
|
+
body = JSON.stringify(body);
|
|
1181
|
+
}
|
|
1182
|
+
return new Response(body)[key]();
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return bodyCache[key] = raw[key]();
|
|
1186
|
+
};
|
|
1187
|
+
/**
|
|
1188
|
+
* `.json()` can parse Request body of type `application/json`
|
|
1189
|
+
*
|
|
1190
|
+
* @see {@link https://hono.dev/docs/api/request#json}
|
|
1191
|
+
*
|
|
1192
|
+
* @example
|
|
1193
|
+
* ```ts
|
|
1194
|
+
* app.post('/entry', async (c) => {
|
|
1195
|
+
* const body = await c.req.json()
|
|
1196
|
+
* })
|
|
1197
|
+
* ```
|
|
1198
|
+
*/
|
|
1199
|
+
json() {
|
|
1200
|
+
return this.#cachedBody("text").then((text) => JSON.parse(text));
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* `.text()` can parse Request body of type `text/plain`
|
|
1204
|
+
*
|
|
1205
|
+
* @see {@link https://hono.dev/docs/api/request#text}
|
|
1206
|
+
*
|
|
1207
|
+
* @example
|
|
1208
|
+
* ```ts
|
|
1209
|
+
* app.post('/entry', async (c) => {
|
|
1210
|
+
* const body = await c.req.text()
|
|
1211
|
+
* })
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
text() {
|
|
1215
|
+
return this.#cachedBody("text");
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* `.arrayBuffer()` parse Request body as an `ArrayBuffer`
|
|
1219
|
+
*
|
|
1220
|
+
* @see {@link https://hono.dev/docs/api/request#arraybuffer}
|
|
1221
|
+
*
|
|
1222
|
+
* @example
|
|
1223
|
+
* ```ts
|
|
1224
|
+
* app.post('/entry', async (c) => {
|
|
1225
|
+
* const body = await c.req.arrayBuffer()
|
|
1226
|
+
* })
|
|
1227
|
+
* ```
|
|
1228
|
+
*/
|
|
1229
|
+
arrayBuffer() {
|
|
1230
|
+
return this.#cachedBody("arrayBuffer");
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Parses the request body as a `Blob`.
|
|
1234
|
+
* @example
|
|
1235
|
+
* ```ts
|
|
1236
|
+
* app.post('/entry', async (c) => {
|
|
1237
|
+
* const body = await c.req.blob();
|
|
1238
|
+
* });
|
|
1239
|
+
* ```
|
|
1240
|
+
* @see https://hono.dev/docs/api/request#blob
|
|
1241
|
+
*/
|
|
1242
|
+
blob() {
|
|
1243
|
+
return this.#cachedBody("blob");
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Parses the request body as `FormData`.
|
|
1247
|
+
* @example
|
|
1248
|
+
* ```ts
|
|
1249
|
+
* app.post('/entry', async (c) => {
|
|
1250
|
+
* const body = await c.req.formData();
|
|
1251
|
+
* });
|
|
1252
|
+
* ```
|
|
1253
|
+
* @see https://hono.dev/docs/api/request#formdata
|
|
1254
|
+
*/
|
|
1255
|
+
formData() {
|
|
1256
|
+
return this.#cachedBody("formData");
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Adds validated data to the request.
|
|
1260
|
+
*
|
|
1261
|
+
* @param target - The target of the validation.
|
|
1262
|
+
* @param data - The validated data to add.
|
|
1263
|
+
*/
|
|
1264
|
+
addValidatedData(target, data) {
|
|
1265
|
+
this.#validatedData[target] = data;
|
|
1266
|
+
}
|
|
1267
|
+
valid(target) {
|
|
1268
|
+
return this.#validatedData[target];
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* `.url()` can get the request url strings.
|
|
1272
|
+
*
|
|
1273
|
+
* @see {@link https://hono.dev/docs/api/request#url}
|
|
1274
|
+
*
|
|
1275
|
+
* @example
|
|
1276
|
+
* ```ts
|
|
1277
|
+
* app.get('/about/me', (c) => {
|
|
1278
|
+
* const url = c.req.url // `http://localhost:8787/about/me`
|
|
1279
|
+
* ...
|
|
1280
|
+
* })
|
|
1281
|
+
* ```
|
|
1282
|
+
*/
|
|
1283
|
+
get url() {
|
|
1284
|
+
return this.raw.url;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* `.method()` can get the method name of the request.
|
|
1288
|
+
*
|
|
1289
|
+
* @see {@link https://hono.dev/docs/api/request#method}
|
|
1290
|
+
*
|
|
1291
|
+
* @example
|
|
1292
|
+
* ```ts
|
|
1293
|
+
* app.get('/about/me', (c) => {
|
|
1294
|
+
* const method = c.req.method // `GET`
|
|
1295
|
+
* })
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
get method() {
|
|
1299
|
+
return this.raw.method;
|
|
1300
|
+
}
|
|
1301
|
+
get [GET_MATCH_RESULT]() {
|
|
1302
|
+
return this.#matchResult;
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* `.matchedRoutes()` can return a matched route in the handler
|
|
1306
|
+
*
|
|
1307
|
+
* @deprecated
|
|
1308
|
+
*
|
|
1309
|
+
* Use matchedRoutes helper defined in "hono/route" instead.
|
|
1310
|
+
*
|
|
1311
|
+
* @see {@link https://hono.dev/docs/api/request#matchedroutes}
|
|
1312
|
+
*
|
|
1313
|
+
* @example
|
|
1314
|
+
* ```ts
|
|
1315
|
+
* app.use('*', async function logger(c, next) {
|
|
1316
|
+
* await next()
|
|
1317
|
+
* c.req.matchedRoutes.forEach(({ handler, method, path }, i) => {
|
|
1318
|
+
* const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]')
|
|
1319
|
+
* console.log(
|
|
1320
|
+
* method,
|
|
1321
|
+
* ' ',
|
|
1322
|
+
* path,
|
|
1323
|
+
* ' '.repeat(Math.max(10 - path.length, 0)),
|
|
1324
|
+
* name,
|
|
1325
|
+
* i === c.req.routeIndex ? '<- respond from here' : ''
|
|
1326
|
+
* )
|
|
1327
|
+
* })
|
|
1328
|
+
* })
|
|
1329
|
+
* ```
|
|
1330
|
+
*/
|
|
1331
|
+
get matchedRoutes() {
|
|
1332
|
+
return this.#matchResult[0].map(([[, route]]) => route);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* `routePath()` can retrieve the path registered within the handler
|
|
1336
|
+
*
|
|
1337
|
+
* @deprecated
|
|
1338
|
+
*
|
|
1339
|
+
* Use routePath helper defined in "hono/route" instead.
|
|
1340
|
+
*
|
|
1341
|
+
* @see {@link https://hono.dev/docs/api/request#routepath}
|
|
1342
|
+
*
|
|
1343
|
+
* @example
|
|
1344
|
+
* ```ts
|
|
1345
|
+
* app.get('/posts/:id', (c) => {
|
|
1346
|
+
* return c.json({ path: c.req.routePath })
|
|
1347
|
+
* })
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
get routePath() {
|
|
1351
|
+
return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
// ../../../node_modules/.bun/hono@4.11.9/node_modules/hono/dist/router/reg-exp-router/node.js
|
|
1356
|
+
var regExpMetaChars = new Set(".\\+*[^]$()");
|
|
1357
|
+
|
|
1358
|
+
// ../core/dist/service.js
|
|
1359
|
+
var bootSqlite = (sqlite) => {
|
|
1360
|
+
const client = resolveSqlite(sqlite);
|
|
1361
|
+
migrateCore(client);
|
|
1362
|
+
return client;
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
// ../core/dist/snapshot.js
|
|
1366
|
+
var snapshotNamespace = (sqlite, namespace) => ({
|
|
1367
|
+
namespace,
|
|
1368
|
+
records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
|
|
1369
|
+
sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
|
|
1370
|
+
});
|
|
1371
|
+
var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
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
|
+
const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
|
|
1376
|
+
for (const row of snapshot.records) {
|
|
1377
|
+
record.run(namespace, row.collection, row.id, row.seq, row.value);
|
|
1378
|
+
}
|
|
1379
|
+
const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
|
|
1380
|
+
for (const row of snapshot.sequences) {
|
|
1381
|
+
sequence.run(namespace, row.name, row.kind, row.value);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// ../core/dist/version.js
|
|
1387
|
+
var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
|
|
1388
|
+
|
|
1389
|
+
// ../core/dist/webhooks.js
|
|
1390
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1391
|
+
var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
|
|
1392
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1393
|
+
var parseEndpoint = (value) => {
|
|
1394
|
+
if (!isRecord2(value) || typeof value.url !== "string")
|
|
1395
|
+
return "each endpoint needs a url";
|
|
1396
|
+
try {
|
|
1397
|
+
new URL(value.url);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return `not a URL: ${value.url}`;
|
|
1400
|
+
}
|
|
1401
|
+
const endpoint = { url: value.url };
|
|
1402
|
+
if (typeof value.id === "string")
|
|
1403
|
+
endpoint.id = value.id;
|
|
1404
|
+
if (typeof value.secret === "string")
|
|
1405
|
+
endpoint.secret = value.secret;
|
|
1406
|
+
if (typeof value.signUrl === "string")
|
|
1407
|
+
endpoint.signUrl = value.signUrl;
|
|
1408
|
+
const events = value.events ?? value.enabledEvents;
|
|
1409
|
+
if (Array.isArray(events))
|
|
1410
|
+
endpoint.events = events.map(String);
|
|
1411
|
+
if (isRecord2(value.tags)) {
|
|
1412
|
+
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof value.account === "string")
|
|
1415
|
+
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1416
|
+
if (isRecord2(value.headers)) {
|
|
1417
|
+
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1418
|
+
}
|
|
1419
|
+
return endpoint;
|
|
1420
|
+
};
|
|
1421
|
+
var webhookAdminRoutes = (hub) => ({
|
|
1422
|
+
"GET /webhooks": ({ url, namespace }) => json2(200, {
|
|
1423
|
+
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1424
|
+
const type = url.searchParams.get("type");
|
|
1425
|
+
return type === null || d.type === type;
|
|
1426
|
+
})
|
|
1427
|
+
}),
|
|
1428
|
+
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1429
|
+
const type = url.searchParams.get("type");
|
|
1430
|
+
return json2(200, {
|
|
1431
|
+
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1432
|
+
});
|
|
1433
|
+
},
|
|
1434
|
+
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1435
|
+
const replayed = await hub.replay(params.id);
|
|
1436
|
+
return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
|
|
1437
|
+
},
|
|
1438
|
+
"POST /webhooks/flush": async () => {
|
|
1439
|
+
await hub.flush();
|
|
1440
|
+
return json2(200, { status: "ok" });
|
|
1441
|
+
},
|
|
1442
|
+
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1443
|
+
if (!isRecord2(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
1444
|
+
return adminError2(400, "mode must be duplicate, reorder or drop");
|
|
1445
|
+
}
|
|
1446
|
+
const fault = { mode: body.mode };
|
|
1447
|
+
if (typeof body.count === "number")
|
|
1448
|
+
fault.count = body.count;
|
|
1449
|
+
hub.fault(namespace, fault);
|
|
1450
|
+
return json2(201, { namespace, ...fault });
|
|
1451
|
+
},
|
|
1452
|
+
"GET /webhook-endpoints": ({ namespace }) => json2(200, {
|
|
1453
|
+
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1454
|
+
...rest,
|
|
1455
|
+
secret: secret ? "(set)" : null
|
|
1456
|
+
}))
|
|
1457
|
+
}),
|
|
1458
|
+
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
1459
|
+
const list = Array.isArray(body) ? body : isRecord2(body) ? body.endpoints : void 0;
|
|
1460
|
+
if (!Array.isArray(list))
|
|
1461
|
+
return adminError2(400, "expected [{url, secret?, events?}]");
|
|
1462
|
+
const parsed = [];
|
|
1463
|
+
for (const each of list) {
|
|
1464
|
+
const endpoint = parseEndpoint(each);
|
|
1465
|
+
if (typeof endpoint === "string")
|
|
1466
|
+
return adminError2(400, endpoint);
|
|
1467
|
+
parsed.push(endpoint);
|
|
1468
|
+
}
|
|
1469
|
+
const set = hub.setEndpoints(namespace, parsed);
|
|
1470
|
+
return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
1471
|
+
},
|
|
1472
|
+
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
1473
|
+
hub.setEndpoints(namespace, []);
|
|
1474
|
+
return json2(200, { status: "ok" });
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
var parsePayload = (message) => {
|
|
1478
|
+
if (message.contentType.startsWith("application/json")) {
|
|
1479
|
+
try {
|
|
1480
|
+
return JSON.parse(message.body);
|
|
1481
|
+
} catch {
|
|
1482
|
+
return message.body;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
|
|
1486
|
+
return Object.fromEntries(new URLSearchParams(message.body));
|
|
1487
|
+
}
|
|
1488
|
+
return message.body;
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// ../core/dist/runtime.js
|
|
1492
|
+
var MOCKINGBIRD_HEADER = "x-mockingbird";
|
|
1493
|
+
var BRANCH_HEADER = "x-mockingbird-branch";
|
|
1494
|
+
var AT_HEADER = "x-mockingbird-at";
|
|
1495
|
+
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
1496
|
+
var DEFAULT_NAMESPACE = "default";
|
|
1497
|
+
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1498
|
+
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
1499
|
+
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1500
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1501
|
+
var effects = /* @__PURE__ */ new WeakMap();
|
|
1502
|
+
var reuseSorted = (fresh, previous, compare, equal) => {
|
|
1503
|
+
if (!previous || previous.length === 0)
|
|
1504
|
+
return fresh.map((row) => Object.freeze(row));
|
|
1505
|
+
const result = new Array(fresh.length);
|
|
1506
|
+
let unchanged = fresh.length === previous.length;
|
|
1507
|
+
let oldIndex = 0;
|
|
1508
|
+
for (let index = 0; index < fresh.length; index++) {
|
|
1509
|
+
const row = fresh[index];
|
|
1510
|
+
while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
|
|
1511
|
+
oldIndex++;
|
|
1512
|
+
}
|
|
1513
|
+
const old = previous[oldIndex];
|
|
1514
|
+
result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
|
|
1515
|
+
if (result[index] !== previous[index])
|
|
1516
|
+
unchanged = false;
|
|
1517
|
+
}
|
|
1518
|
+
return unchanged ? previous : result;
|
|
1519
|
+
};
|
|
1520
|
+
var DroppedConnectionError = class extends TypeError {
|
|
1521
|
+
code = "MOCKINGBIRD_DROP";
|
|
1522
|
+
constructor() {
|
|
1523
|
+
super("fetch failed: connection dropped by Mockingbird fault");
|
|
1524
|
+
this.name = "TypeError";
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
var operationMatcher = (document2) => {
|
|
1528
|
+
const matchers = listOperations(document2).map((operation) => ({
|
|
1529
|
+
operationId: operation.operationId,
|
|
1530
|
+
method: operation.method.toUpperCase(),
|
|
1531
|
+
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
1532
|
+
params: (operation.path.match(/\{/g) ?? []).length
|
|
1533
|
+
})).sort((a, b) => a.params - b.params);
|
|
1534
|
+
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
1535
|
+
};
|
|
1536
|
+
var createRuntime = (options) => {
|
|
1537
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
1538
|
+
const clock = options.clock ?? createClock();
|
|
1539
|
+
const rng = createRng(options.seed ?? 0);
|
|
1540
|
+
const wallNow = options.io?.wallNow ?? Date.now;
|
|
1541
|
+
const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
|
|
1542
|
+
const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1543
|
+
const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
|
|
1544
|
+
const metrics = createMetrics();
|
|
1545
|
+
const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
|
|
1546
|
+
const version = options.version ?? PACKAGE_VERSION;
|
|
1547
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1548
|
+
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
1549
|
+
const branchRngs = /* @__PURE__ */ new Map();
|
|
1550
|
+
const timelines = /* @__PURE__ */ new Map();
|
|
1551
|
+
const branchStorage = /* @__PURE__ */ new Map();
|
|
1552
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1553
|
+
const credentials = createCredentialRegistry();
|
|
1554
|
+
const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
|
|
1555
|
+
const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
|
|
1556
|
+
const instanceFor = (key, publicNamespace = key, isolatedRng) => {
|
|
1557
|
+
const existing = instances.get(key);
|
|
1558
|
+
if (existing)
|
|
1559
|
+
return existing;
|
|
1560
|
+
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
1561
|
+
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
1562
|
+
}
|
|
1563
|
+
const created = options.create({
|
|
1564
|
+
namespace: storageNamespace(key),
|
|
1565
|
+
publicNamespace,
|
|
1566
|
+
sqlite,
|
|
1567
|
+
clock,
|
|
1568
|
+
rng: isolatedRng ?? rng
|
|
1569
|
+
});
|
|
1570
|
+
instances.set(key, created);
|
|
1571
|
+
publicNamespaces.add(publicNamespace);
|
|
1572
|
+
if (isolatedRng)
|
|
1573
|
+
branchRngs.set(key, isolatedRng);
|
|
1574
|
+
return created;
|
|
1575
|
+
};
|
|
1576
|
+
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
1577
|
+
const capture = (storage) => {
|
|
1578
|
+
const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
|
|
1579
|
+
const previous = captured.get(storage);
|
|
1580
|
+
const snapshot2 = {
|
|
1581
|
+
namespace: fresh.namespace,
|
|
1582
|
+
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),
|
|
1583
|
+
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)
|
|
1584
|
+
};
|
|
1585
|
+
Object.freeze(snapshot2.records);
|
|
1586
|
+
Object.freeze(snapshot2.sequences);
|
|
1587
|
+
Object.freeze(snapshot2);
|
|
1588
|
+
captured.set(storage, snapshot2);
|
|
1589
|
+
return Object.freeze({
|
|
1590
|
+
snapshot: snapshot2,
|
|
1591
|
+
clock: Object.freeze(clock.state()),
|
|
1592
|
+
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
1593
|
+
});
|
|
1594
|
+
};
|
|
1595
|
+
const timeline = (name = DEFAULT_NAMESPACE) => {
|
|
1596
|
+
let found = timelines.get(name);
|
|
1597
|
+
if (found)
|
|
1598
|
+
return found;
|
|
1599
|
+
instance(name);
|
|
1600
|
+
found = new Timeline({
|
|
1601
|
+
now: clock.now,
|
|
1602
|
+
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
1603
|
+
});
|
|
1604
|
+
found.commit(capture(name));
|
|
1605
|
+
timelines.set(name, found);
|
|
1606
|
+
return found;
|
|
1607
|
+
};
|
|
1608
|
+
const physicalBranch = (namespace, branch2) => {
|
|
1609
|
+
if (branch2 === "main")
|
|
1610
|
+
return namespace;
|
|
1611
|
+
const mapKey = `${namespace}\0${branch2}`;
|
|
1612
|
+
const existing = branchStorage.get(mapKey);
|
|
1613
|
+
if (existing)
|
|
1614
|
+
return existing;
|
|
1615
|
+
const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
|
|
1616
|
+
branchStorage.set(mapKey, key);
|
|
1617
|
+
return key;
|
|
1618
|
+
};
|
|
1619
|
+
const ensureBranch = (namespace, branch2, at) => {
|
|
1620
|
+
if (!BRANCH_PATTERN2.test(branch2))
|
|
1621
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
1622
|
+
const history = timeline(namespace);
|
|
1623
|
+
if (branch2 === "main") {
|
|
1624
|
+
if (at !== void 0) {
|
|
1625
|
+
const point = history.checkout("main", at);
|
|
1626
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
1627
|
+
captured.set(namespace, point.value.snapshot);
|
|
1628
|
+
rng.setState(point.value.rngState);
|
|
1629
|
+
clock.set(point.value.clock.now);
|
|
1630
|
+
if (point.value.clock.frozen)
|
|
1631
|
+
clock.freeze();
|
|
1632
|
+
else
|
|
1633
|
+
clock.unfreeze();
|
|
1634
|
+
}
|
|
1635
|
+
return namespace;
|
|
1636
|
+
}
|
|
1637
|
+
const storage = physicalBranch(namespace, branch2);
|
|
1638
|
+
if (!history.hasBranch(branch2)) {
|
|
1639
|
+
if (at === void 0)
|
|
1640
|
+
history.commit(capture(namespace));
|
|
1641
|
+
const point = history.fork(branch2, at === void 0 ? {} : { from: at });
|
|
1642
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1643
|
+
if (point)
|
|
1644
|
+
branchRng.setState(point.value.rngState);
|
|
1645
|
+
instanceFor(storage, namespace, branchRng);
|
|
1646
|
+
if (point)
|
|
1647
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1648
|
+
if (point)
|
|
1649
|
+
captured.set(storage, point.value.snapshot);
|
|
1650
|
+
} else if (at !== void 0 && history.head(branch2)?.id !== at) {
|
|
1651
|
+
const point = history.checkout(branch2, at);
|
|
1652
|
+
if (!instances.has(storage)) {
|
|
1653
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1654
|
+
branchRng.setState(point.value.rngState);
|
|
1655
|
+
instanceFor(storage, namespace, branchRng);
|
|
1656
|
+
}
|
|
1657
|
+
branchRngs.get(storage)?.setState(point.value.rngState);
|
|
1658
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1659
|
+
captured.set(storage, point.value.snapshot);
|
|
1660
|
+
} else {
|
|
1661
|
+
if (!instances.has(storage)) {
|
|
1662
|
+
const point = history.head(branch2);
|
|
1663
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1664
|
+
if (point)
|
|
1665
|
+
branchRng.setState(point.value.rngState);
|
|
1666
|
+
instanceFor(storage, namespace, branchRng);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return storage;
|
|
1670
|
+
};
|
|
1671
|
+
const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
|
|
1672
|
+
const storage = ensureBranch(namespace, branch2);
|
|
1673
|
+
return timeline(namespace).commit(capture(storage), { branch: branch2 });
|
|
1674
|
+
};
|
|
1675
|
+
const branch = (name, branchOptions = {}) => {
|
|
1676
|
+
const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1677
|
+
ensureBranch(namespace, name, branchOptions.at);
|
|
1678
|
+
const head = timeline(namespace).head(name);
|
|
1679
|
+
if (!head)
|
|
1680
|
+
throw new RangeError(`branch ${name} has no checkpoint`);
|
|
1681
|
+
return head;
|
|
1682
|
+
};
|
|
1683
|
+
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
1684
|
+
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1685
|
+
const branchName = checkoutOptions.branch ?? "main";
|
|
1686
|
+
const history = timeline(namespace);
|
|
1687
|
+
const point = history.checkout(branchName, checkpointId);
|
|
1688
|
+
const storage = ensureBranch(namespace, branchName);
|
|
1689
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1690
|
+
captured.set(storage, point.value.snapshot);
|
|
1691
|
+
clock.set(point.value.clock.now);
|
|
1692
|
+
if (point.value.clock.frozen)
|
|
1693
|
+
clock.freeze();
|
|
1694
|
+
else
|
|
1695
|
+
clock.unfreeze();
|
|
1696
|
+
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
1697
|
+
};
|
|
1698
|
+
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
1699
|
+
if (name === "*") {
|
|
1700
|
+
options.webhooks?.clear();
|
|
1701
|
+
for (const each of instances.values())
|
|
1702
|
+
await each.reset();
|
|
1703
|
+
timelines.clear();
|
|
1704
|
+
branchStorage.clear();
|
|
1705
|
+
branchRngs.clear();
|
|
1706
|
+
captured.clear();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
options.webhooks?.clear(name);
|
|
1710
|
+
const target = instances.get(name);
|
|
1711
|
+
if (target)
|
|
1712
|
+
await target.reset();
|
|
1713
|
+
else
|
|
1714
|
+
clearNamespace(sqlite, storageNamespace(name));
|
|
1715
|
+
for (const [mapping, storage] of branchStorage) {
|
|
1716
|
+
if (!mapping.startsWith(`${name}\0`))
|
|
1717
|
+
continue;
|
|
1718
|
+
const branchInstance = instances.get(storage);
|
|
1719
|
+
if (branchInstance)
|
|
1720
|
+
await branchInstance.reset();
|
|
1721
|
+
else
|
|
1722
|
+
clearNamespace(sqlite, storageNamespace(storage));
|
|
1723
|
+
branchStorage.delete(mapping);
|
|
1724
|
+
branchRngs.delete(storage);
|
|
1725
|
+
captured.delete(storage);
|
|
1726
|
+
}
|
|
1727
|
+
timelines.delete(name);
|
|
1728
|
+
captured.delete(name);
|
|
1729
|
+
};
|
|
1730
|
+
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
1731
|
+
return checkpoint(name, "main").value.snapshot;
|
|
1732
|
+
};
|
|
1733
|
+
const restore = (from, name = DEFAULT_NAMESPACE) => {
|
|
1734
|
+
instance(name);
|
|
1735
|
+
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
1736
|
+
captured.set(name, from);
|
|
1737
|
+
const history = timelines.get(name);
|
|
1738
|
+
if (history)
|
|
1739
|
+
history.commit(capture(name), { branch: "main" });
|
|
1740
|
+
else
|
|
1741
|
+
timeline(name);
|
|
1742
|
+
};
|
|
1743
|
+
const runtime = {
|
|
1744
|
+
name: options.name,
|
|
1745
|
+
sqlite,
|
|
1746
|
+
clock,
|
|
1747
|
+
faults,
|
|
1748
|
+
metrics,
|
|
1749
|
+
journal,
|
|
1750
|
+
rng,
|
|
1751
|
+
credentials,
|
|
1752
|
+
webhooks: options.webhooks,
|
|
1753
|
+
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
1754
|
+
const preset = options.presets?.[name];
|
|
1755
|
+
if (!preset)
|
|
1756
|
+
throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
|
|
1757
|
+
const added = (preset.rules ?? []).map((rule, index) => faults.add({
|
|
1758
|
+
namespace,
|
|
1759
|
+
...rule,
|
|
1760
|
+
...overrides,
|
|
1761
|
+
preset: name,
|
|
1762
|
+
id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
|
|
1763
|
+
}));
|
|
1764
|
+
if (preset.webhook && options.webhooks) {
|
|
1765
|
+
options.webhooks.fault(namespace, {
|
|
1766
|
+
...preset.webhook,
|
|
1767
|
+
...overrides.count !== void 0 ? { count: overrides.count } : {}
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
return added;
|
|
1771
|
+
},
|
|
1772
|
+
instance,
|
|
1773
|
+
namespaces: () => [...publicNamespaces].sort(),
|
|
1774
|
+
reset,
|
|
1775
|
+
snapshot,
|
|
1776
|
+
restore,
|
|
1777
|
+
checkpoint,
|
|
1778
|
+
branch,
|
|
1779
|
+
checkout,
|
|
1780
|
+
timeline,
|
|
1781
|
+
fetch: async (incoming) => {
|
|
1782
|
+
let request = incoming;
|
|
1783
|
+
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
1784
|
+
if (prefixed) {
|
|
1785
|
+
const url2 = new URL(request.url);
|
|
1786
|
+
url2.pathname = prefixed[2] ?? "/";
|
|
1787
|
+
const headers = new Headers(request.headers);
|
|
1788
|
+
if (!headers.has(NAMESPACE_HEADER)) {
|
|
1789
|
+
headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
|
|
1790
|
+
}
|
|
1791
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
1792
|
+
request = new Request(url2, {
|
|
1793
|
+
method: request.method,
|
|
1794
|
+
headers,
|
|
1795
|
+
...hasBody ? { body: await request.arrayBuffer() } : {},
|
|
1796
|
+
signal: request.signal
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
let namespace = control.namespaceOf(request);
|
|
1800
|
+
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
1801
|
+
const credential = options.credential(request);
|
|
1802
|
+
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
1803
|
+
if (mapped !== void 0)
|
|
1804
|
+
namespace = mapped;
|
|
1805
|
+
}
|
|
1806
|
+
const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
|
|
1807
|
+
const at = request.headers.get(AT_HEADER) ?? void 0;
|
|
1808
|
+
const stamp = (response2) => {
|
|
1809
|
+
const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
|
|
1810
|
+
try {
|
|
1811
|
+
response2.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1812
|
+
return response2;
|
|
1813
|
+
} catch {
|
|
1814
|
+
const copy = new Response(response2.body, response2);
|
|
1815
|
+
copy.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1816
|
+
return copy;
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
const handled = await control.handle(request);
|
|
1820
|
+
if (handled)
|
|
1821
|
+
return stamp(handled);
|
|
1822
|
+
const started = monotonicNow();
|
|
1823
|
+
const url = new URL(request.url);
|
|
1824
|
+
const operationId = operationIdFor(request, url.pathname);
|
|
1825
|
+
const log = (status, faultId, response2) => {
|
|
1826
|
+
const noted = response2 ? responseNotes(response2) : void 0;
|
|
1827
|
+
const entry = {
|
|
1828
|
+
service: options.name,
|
|
1829
|
+
namespace,
|
|
1830
|
+
operationId,
|
|
1831
|
+
method: request.method,
|
|
1832
|
+
path: url.pathname,
|
|
1833
|
+
status,
|
|
1834
|
+
durationMs: Math.round((monotonicNow() - started) * 100) / 100,
|
|
1835
|
+
unmatched: options.document !== void 0 && operationId === void 0,
|
|
1836
|
+
...faultId !== void 0 ? { faultId } : {},
|
|
1837
|
+
...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
|
|
1838
|
+
...noted?.adopted ? { adopted: true } : {}
|
|
1839
|
+
};
|
|
1840
|
+
metrics.record(entry);
|
|
1841
|
+
journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
|
|
1842
|
+
options.onLog?.(entry);
|
|
1843
|
+
};
|
|
1844
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
1845
|
+
log(400);
|
|
1846
|
+
return stamp(new Response(JSON.stringify({
|
|
1847
|
+
error: {
|
|
1848
|
+
type: "mockingbird_admin",
|
|
1849
|
+
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
1850
|
+
}
|
|
1851
|
+
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
1852
|
+
}
|
|
1853
|
+
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
1854
|
+
log(400);
|
|
1855
|
+
return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
|
|
1856
|
+
}
|
|
1857
|
+
let storage;
|
|
1858
|
+
try {
|
|
1859
|
+
if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
|
|
1860
|
+
const point = timeline(namespace).get(at);
|
|
1861
|
+
storage = physicalBranch(namespace, `at_${at}`);
|
|
1862
|
+
let viewRng = branchRngs.get(storage);
|
|
1863
|
+
if (!viewRng) {
|
|
1864
|
+
viewRng = createRng(options.seed ?? 0);
|
|
1865
|
+
instanceFor(storage, namespace, viewRng);
|
|
1866
|
+
}
|
|
1867
|
+
viewRng.setState(point.value.rngState);
|
|
1868
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1869
|
+
captured.set(storage, point.value.snapshot);
|
|
1870
|
+
} else {
|
|
1871
|
+
storage = ensureBranch(namespace, selectedBranch, at);
|
|
1872
|
+
}
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
log(409);
|
|
1875
|
+
return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
|
|
1876
|
+
}
|
|
1877
|
+
const hits = await faults.take({
|
|
1878
|
+
operationId,
|
|
1879
|
+
method: request.method,
|
|
1880
|
+
path: url.pathname,
|
|
1881
|
+
namespace
|
|
1882
|
+
});
|
|
1883
|
+
const final = hits.find((hit) => hit.drop || hit.response);
|
|
1884
|
+
if (final?.drop) {
|
|
1885
|
+
log(0, final.id);
|
|
1886
|
+
throw new DroppedConnectionError();
|
|
1887
|
+
}
|
|
1888
|
+
if (final?.response) {
|
|
1889
|
+
log(final.response.status, final.id);
|
|
1890
|
+
return stamp(final.response);
|
|
1891
|
+
}
|
|
1892
|
+
const fired = hits.filter((hit) => hit.effect !== void 0);
|
|
1893
|
+
if (fired.length > 0)
|
|
1894
|
+
effects.set(request, fired.map((hit) => hit.effect));
|
|
1895
|
+
let response = await instanceFor(storage, namespace).fetch(request);
|
|
1896
|
+
if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
|
|
1897
|
+
const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
|
|
1898
|
+
response = mutableResponse(response);
|
|
1899
|
+
response.headers.set(CHECKPOINT_HEADER, point.id);
|
|
1900
|
+
}
|
|
1901
|
+
if (selectedBranch !== "main") {
|
|
1902
|
+
response = mutableResponse(response);
|
|
1903
|
+
response.headers.set(BRANCH_HEADER, selectedBranch);
|
|
1904
|
+
}
|
|
1905
|
+
if (at !== void 0) {
|
|
1906
|
+
response = mutableResponse(response);
|
|
1907
|
+
response.headers.set(AT_HEADER, at);
|
|
1908
|
+
}
|
|
1909
|
+
log(response.status, fired[0]?.id, response);
|
|
1910
|
+
return stamp(response);
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
const control = createControlPlane({
|
|
1914
|
+
name: options.name,
|
|
1915
|
+
startedAt: wallNow(),
|
|
1916
|
+
wallNow,
|
|
1917
|
+
clock,
|
|
1918
|
+
faults,
|
|
1919
|
+
metrics,
|
|
1920
|
+
journal,
|
|
1921
|
+
defaultNamespace: DEFAULT_NAMESPACE,
|
|
1922
|
+
namespaces: runtime.namespaces,
|
|
1923
|
+
reset,
|
|
1924
|
+
timeTravel: {
|
|
1925
|
+
checkpoint: (name, branchName) => {
|
|
1926
|
+
const point = checkpoint(name, branchName);
|
|
1927
|
+
return {
|
|
1928
|
+
id: point.id,
|
|
1929
|
+
branch: point.branch,
|
|
1930
|
+
parent: point.parent,
|
|
1931
|
+
at: point.at,
|
|
1932
|
+
records: point.value.snapshot.records.length
|
|
1933
|
+
};
|
|
1934
|
+
},
|
|
1935
|
+
branch: (branchName, branchOptions) => {
|
|
1936
|
+
const point = branch(branchName, branchOptions);
|
|
1937
|
+
return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
|
|
1938
|
+
},
|
|
1939
|
+
checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
|
|
1940
|
+
retain: (name, checkpointId) => {
|
|
1941
|
+
timeline(name).retain(checkpointId);
|
|
1942
|
+
},
|
|
1943
|
+
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
1944
|
+
inspect: (name) => {
|
|
1945
|
+
const history = timeline(name);
|
|
1946
|
+
return {
|
|
1947
|
+
branches: history.branches(),
|
|
1948
|
+
checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
1949
|
+
id,
|
|
1950
|
+
branch: branchName,
|
|
1951
|
+
parent,
|
|
1952
|
+
at
|
|
1953
|
+
}))
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
describe: options.describe ?? (() => ({})),
|
|
1958
|
+
...options.presets ? {
|
|
1959
|
+
applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
|
|
1960
|
+
} : {},
|
|
1961
|
+
routes: {
|
|
1962
|
+
...credentialRoutes(credentials),
|
|
1963
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
1964
|
+
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
1965
|
+
...options.admin?.(runtime) ?? {}
|
|
1966
|
+
},
|
|
1967
|
+
adminKey: options.adminKey
|
|
1968
|
+
});
|
|
1969
|
+
return runtime;
|
|
1970
|
+
};
|
|
1971
|
+
var mutableResponse = (response) => {
|
|
1972
|
+
try {
|
|
1973
|
+
response.headers.set("x-mockingbird-mutable-probe", "1");
|
|
1974
|
+
response.headers.delete("x-mockingbird-mutable-probe");
|
|
1975
|
+
return response;
|
|
1976
|
+
} catch {
|
|
1977
|
+
return new Response(response.body, response);
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1981
|
+
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
1982
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1983
|
+
var credentialRoutes = (registry) => ({
|
|
1984
|
+
"GET /credentials": () => adminJson(200, {
|
|
1985
|
+
credentials: registry.entries().map(({ credential, namespace }) => ({
|
|
1986
|
+
credential: maskCredential(credential),
|
|
1987
|
+
namespace
|
|
1988
|
+
}))
|
|
1989
|
+
}),
|
|
1990
|
+
"PUT /credentials": ({ body, namespace }) => {
|
|
1991
|
+
const pairs = [];
|
|
1992
|
+
const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
|
|
1993
|
+
if (Array.isArray(list)) {
|
|
1994
|
+
for (const each of list) {
|
|
1995
|
+
if (typeof each === "string")
|
|
1996
|
+
pairs.push([each, namespace]);
|
|
1997
|
+
else if (isObject(each) && typeof each.credential === "string") {
|
|
1998
|
+
pairs.push([
|
|
1999
|
+
each.credential,
|
|
2000
|
+
typeof each.namespace === "string" ? each.namespace : namespace
|
|
2001
|
+
]);
|
|
2002
|
+
} else
|
|
2003
|
+
return adminFail(400, "each entry is a credential string or {credential, namespace}");
|
|
2004
|
+
}
|
|
2005
|
+
} else if (isObject(list)) {
|
|
2006
|
+
for (const [credential, target] of Object.entries(list)) {
|
|
2007
|
+
if (typeof target !== "string")
|
|
2008
|
+
return adminFail(400, `namespace for ${credential} must be a string`);
|
|
2009
|
+
pairs.push([credential, target]);
|
|
2010
|
+
}
|
|
2011
|
+
} else if (isObject(body) && typeof body.credential === "string") {
|
|
2012
|
+
pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
|
|
2013
|
+
} else {
|
|
2014
|
+
return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
|
|
2015
|
+
}
|
|
2016
|
+
for (const [credential, target] of pairs) {
|
|
2017
|
+
if (!NAMESPACE_PATTERN.test(target))
|
|
2018
|
+
return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
|
|
2019
|
+
registry.set(credential, target);
|
|
2020
|
+
}
|
|
2021
|
+
return adminJson(200, { mapped: pairs.length });
|
|
2022
|
+
},
|
|
2023
|
+
"DELETE /credentials": ({ url }) => {
|
|
2024
|
+
const credential = url.searchParams.get("credential");
|
|
2025
|
+
if (credential === null)
|
|
2026
|
+
registry.clear();
|
|
2027
|
+
else
|
|
2028
|
+
registry.remove(credential);
|
|
2029
|
+
return adminJson(200, { status: "ok" });
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
var presetRoutes = (presets, runtime) => ({
|
|
2033
|
+
"GET /faults/presets": () => adminJson(200, {
|
|
2034
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
2035
|
+
}),
|
|
2036
|
+
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
2037
|
+
const name = params.name;
|
|
2038
|
+
if (!presets[name])
|
|
2039
|
+
return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
|
|
2040
|
+
const overrides = isObject(body) ? body : {};
|
|
2041
|
+
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// src/behavior.ts
|
|
2046
|
+
var OAUTH_SCENARIOS = {
|
|
2047
|
+
apple_private_relay: { apple: { emailMode: "hide" } },
|
|
2048
|
+
apple_share_email: { apple: { emailMode: "share" } },
|
|
2049
|
+
apple_returning_user: { apple: { omitUser: true } },
|
|
2050
|
+
apple_boolean_claims: { apple: { booleanClaims: "boolean" } },
|
|
2051
|
+
microsoft_missing_email: { claims: { omitEmail: true } },
|
|
2052
|
+
microsoft_spa_expiry: { tokens: { refreshTtlSeconds: 86400 } },
|
|
2053
|
+
github_unverified_email: { claims: { unverifiedEmail: true } },
|
|
2054
|
+
missing_email: { claims: { omitEmail: true } },
|
|
2055
|
+
missing_name: { claims: { omitName: true } },
|
|
2056
|
+
unverified_email: { claims: { unverifiedEmail: true } },
|
|
2057
|
+
google_no_refresh_token: { google: { refreshToken: "never" } },
|
|
2058
|
+
google_reauthentication: { tokens: { refreshError: "invalid_rapt" } },
|
|
2059
|
+
revoked_refresh_token: { tokens: { refreshError: "invalid_grant" } },
|
|
2060
|
+
rotating_refresh_tokens: { tokens: { refreshRotation: "rotate" } },
|
|
2061
|
+
short_lived_tokens: { tokens: { accessTtlSeconds: 5, codeTtlSeconds: 5, refreshTtlSeconds: 30 } },
|
|
2062
|
+
consent_denied: { consent: { error: "access_denied" } },
|
|
2063
|
+
intermittent_token_failure: { probabilities: { tokenUnavailable: 0.25 } }
|
|
2064
|
+
};
|
|
2065
|
+
var object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
2066
|
+
var keys = (value, allowed) => {
|
|
2067
|
+
for (const k of Object.keys(value))
|
|
2068
|
+
if (!allowed.includes(k)) throw new Error(`Unknown behavior option: ${k}`);
|
|
2069
|
+
};
|
|
2070
|
+
var enumeration = (v, values) => {
|
|
2071
|
+
if (v !== void 0 && !values.includes(v))
|
|
2072
|
+
throw new Error(`Expected one of ${values.join(", ")}`);
|
|
2073
|
+
};
|
|
2074
|
+
var strings = (v) => {
|
|
2075
|
+
if (v !== void 0 && (!Array.isArray(v) || v.some((s) => typeof s !== "string" || !s || /\s/.test(s))))
|
|
2076
|
+
throw new Error("Scopes must be nonempty strings without whitespace");
|
|
2077
|
+
};
|
|
2078
|
+
function validateBehavior(input) {
|
|
2079
|
+
if (!object(input)) throw new Error("Behavior must be an object");
|
|
2080
|
+
keys(input, [
|
|
2081
|
+
"preset",
|
|
2082
|
+
"session",
|
|
2083
|
+
"probabilities",
|
|
2084
|
+
"apple",
|
|
2085
|
+
"google",
|
|
2086
|
+
"claims",
|
|
2087
|
+
"consent",
|
|
2088
|
+
"tokens",
|
|
2089
|
+
"additionalScopes"
|
|
2090
|
+
]);
|
|
2091
|
+
if (input.preset !== void 0 && (typeof input.preset !== "string" || !Object.hasOwn(OAUTH_SCENARIOS, input.preset)))
|
|
2092
|
+
throw new Error("Unknown OAuth scenario");
|
|
2093
|
+
for (const key of ["probabilities", "apple", "google", "claims", "consent", "tokens", "session"])
|
|
2094
|
+
if (input[key] !== void 0 && !object(input[key])) throw new Error(`${key} must be an object`);
|
|
2095
|
+
const session = input.session;
|
|
2096
|
+
if (session) {
|
|
2097
|
+
keys(session, ["reuseLastAccount"]);
|
|
2098
|
+
enumeration(session.reuseLastAccount, [true, false]);
|
|
2099
|
+
}
|
|
2100
|
+
const p = input.probabilities;
|
|
2101
|
+
if (p) {
|
|
2102
|
+
keys(p, [
|
|
2103
|
+
"hideEmail",
|
|
2104
|
+
"omitEmail",
|
|
2105
|
+
"omitName",
|
|
2106
|
+
"unverifiedEmail",
|
|
2107
|
+
"denyConsent",
|
|
2108
|
+
"tokenUnavailable",
|
|
2109
|
+
"invalidGrant"
|
|
2110
|
+
]);
|
|
2111
|
+
for (const v of Object.values(p))
|
|
2112
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)
|
|
2113
|
+
throw new Error("Probabilities must be finite numbers in [0,1]");
|
|
2114
|
+
}
|
|
2115
|
+
const apple = input.apple;
|
|
2116
|
+
if (apple) {
|
|
2117
|
+
keys(apple, ["emailMode", "booleanClaims", "omitUser"]);
|
|
2118
|
+
enumeration(apple.emailMode, ["choose", "hide", "share"]);
|
|
2119
|
+
enumeration(apple.booleanClaims, ["string", "boolean"]);
|
|
2120
|
+
enumeration(apple.omitUser, [true, false]);
|
|
2121
|
+
}
|
|
2122
|
+
const google = input.google;
|
|
2123
|
+
if (google) {
|
|
2124
|
+
keys(google, ["refreshToken", "testing", "maxRefreshTokens"]);
|
|
2125
|
+
enumeration(google.refreshToken, ["first-consent", "always", "never"]);
|
|
2126
|
+
enumeration(google.testing, [true, false]);
|
|
2127
|
+
if (google.maxRefreshTokens !== void 0 && (typeof google.maxRefreshTokens !== "number" || !Number.isSafeInteger(google.maxRefreshTokens) || google.maxRefreshTokens < 1 || google.maxRefreshTokens > 1e3))
|
|
2128
|
+
throw new Error("maxRefreshTokens must be an integer in [1,1000]");
|
|
2129
|
+
}
|
|
2130
|
+
const claims = input.claims;
|
|
2131
|
+
if (claims) {
|
|
2132
|
+
keys(claims, ["omitEmail", "omitName", "unverifiedEmail"]);
|
|
2133
|
+
for (const v of Object.values(claims)) enumeration(v, [true, false]);
|
|
2134
|
+
}
|
|
2135
|
+
const consent = input.consent;
|
|
2136
|
+
if (consent) {
|
|
2137
|
+
keys(consent, ["deniedScopes", "error"]);
|
|
2138
|
+
strings(consent.deniedScopes);
|
|
2139
|
+
enumeration(consent.error, ["access_denied", "interaction_required", "temporarily_unavailable"]);
|
|
2140
|
+
}
|
|
2141
|
+
const tokens = input.tokens;
|
|
2142
|
+
if (tokens) {
|
|
2143
|
+
keys(tokens, [
|
|
2144
|
+
"accessTtlSeconds",
|
|
2145
|
+
"codeTtlSeconds",
|
|
2146
|
+
"refreshTtlSeconds",
|
|
2147
|
+
"refreshRotation",
|
|
2148
|
+
"refreshError"
|
|
2149
|
+
]);
|
|
2150
|
+
enumeration(tokens.refreshRotation, ["reuse", "rotate"]);
|
|
2151
|
+
enumeration(tokens.refreshError, ["invalid_grant", "invalid_rapt"]);
|
|
2152
|
+
for (const key of ["accessTtlSeconds", "codeTtlSeconds", "refreshTtlSeconds"]) {
|
|
2153
|
+
const v = tokens[key];
|
|
2154
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isSafeInteger(v) || v < 1 || v > 31536e4))
|
|
2155
|
+
throw new Error(`${key} must be an integer in [1,315360000]`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
strings(input.additionalScopes);
|
|
2159
|
+
const { preset, ...config } = input;
|
|
2160
|
+
const base = preset ? structuredClone(OAUTH_SCENARIOS[preset]) : {};
|
|
2161
|
+
return {
|
|
2162
|
+
...base,
|
|
2163
|
+
...structuredClone(config),
|
|
2164
|
+
...Object.fromEntries(
|
|
2165
|
+
["probabilities", "apple", "google", "claims", "consent", "tokens", "session"].filter((k) => k in base || k in config).map((k) => [
|
|
2166
|
+
k,
|
|
2167
|
+
{
|
|
2168
|
+
...base[k],
|
|
2169
|
+
...config[k]
|
|
2170
|
+
}
|
|
2171
|
+
])
|
|
2172
|
+
)
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
var BehaviorState = class {
|
|
2176
|
+
constructor(sqlite, namespace, seed, initial) {
|
|
2177
|
+
this.seed = seed;
|
|
2178
|
+
this.state = new Collection(sqlite, namespace, "oauth_behavior");
|
|
2179
|
+
if (!this.state.has("state")) this.configure(initial);
|
|
2180
|
+
}
|
|
2181
|
+
seed;
|
|
2182
|
+
state;
|
|
2183
|
+
configure(input) {
|
|
2184
|
+
const config = validateBehavior(input);
|
|
2185
|
+
this.state.insert("state", { config, cursor: 0, events: [] });
|
|
2186
|
+
return config;
|
|
2187
|
+
}
|
|
2188
|
+
get config() {
|
|
2189
|
+
return this.state.get("state")?.config ?? {};
|
|
2190
|
+
}
|
|
2191
|
+
get events() {
|
|
2192
|
+
return this.state.get("state")?.events ?? [];
|
|
2193
|
+
}
|
|
2194
|
+
sample(stage, names) {
|
|
2195
|
+
const state = this.state.get("state") ?? { config: {}, cursor: 0, events: [] };
|
|
2196
|
+
const sequence = state.cursor++;
|
|
2197
|
+
const outcomes = Object.fromEntries(
|
|
2198
|
+
names.map((name) => [
|
|
2199
|
+
name,
|
|
2200
|
+
seedFrom(`${this.seed}:${sequence}:${name}`) / 4294967296 < (state.config.probabilities?.[name] ?? 0)
|
|
2201
|
+
])
|
|
2202
|
+
);
|
|
2203
|
+
state.events.push({ sequence, stage, outcomes });
|
|
2204
|
+
state.events = state.events.slice(-100);
|
|
2205
|
+
this.state.insert("state", state);
|
|
2206
|
+
return outcomes;
|
|
2207
|
+
}
|
|
2208
|
+
decisions() {
|
|
2209
|
+
const sampled = this.sample("authorization", [
|
|
2210
|
+
"hideEmail",
|
|
2211
|
+
"omitEmail",
|
|
2212
|
+
"omitName",
|
|
2213
|
+
"unverifiedEmail",
|
|
2214
|
+
"denyConsent"
|
|
2215
|
+
]);
|
|
2216
|
+
return {
|
|
2217
|
+
hideEmail: sampled.hideEmail ?? false,
|
|
2218
|
+
omitEmail: !!(sampled.omitEmail || this.config.claims?.omitEmail),
|
|
2219
|
+
omitName: !!(sampled.omitName || this.config.claims?.omitName),
|
|
2220
|
+
unverifiedEmail: !!(sampled.unverifiedEmail || this.config.claims?.unverifiedEmail),
|
|
2221
|
+
denyConsent: sampled.denyConsent ?? false
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
|
|
2226
|
+
// src/crypto.ts
|
|
2227
|
+
var encoder = new TextEncoder();
|
|
2228
|
+
var base64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2229
|
+
var random = () => base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
2230
|
+
var hash = async (value) => base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))));
|
|
2231
|
+
var Signer = class {
|
|
2232
|
+
pair = crypto.subtle.generateKey(
|
|
2233
|
+
{
|
|
2234
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
2235
|
+
modulusLength: 2048,
|
|
2236
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
2237
|
+
hash: "SHA-256"
|
|
2238
|
+
},
|
|
2239
|
+
true,
|
|
2240
|
+
["sign", "verify"]
|
|
2241
|
+
);
|
|
2242
|
+
kid = random();
|
|
2243
|
+
async jwks() {
|
|
2244
|
+
const pair = await this.pair;
|
|
2245
|
+
return {
|
|
2246
|
+
keys: [
|
|
2247
|
+
{
|
|
2248
|
+
...await crypto.subtle.exportKey("jwk", pair.publicKey),
|
|
2249
|
+
kid: this.kid,
|
|
2250
|
+
use: "sig",
|
|
2251
|
+
alg: "RS256"
|
|
2252
|
+
}
|
|
2253
|
+
]
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
async sign(claims) {
|
|
2257
|
+
const header = base64url(
|
|
2258
|
+
encoder.encode(JSON.stringify({ alg: "RS256", kid: this.kid, typ: "JWT" }))
|
|
2259
|
+
);
|
|
2260
|
+
const payload = base64url(encoder.encode(JSON.stringify(claims)));
|
|
2261
|
+
const input = `${header}.${payload}`;
|
|
2262
|
+
const signature = await crypto.subtle.sign(
|
|
2263
|
+
"RSASSA-PKCS1-v1_5",
|
|
2264
|
+
(await this.pair).privateKey,
|
|
2265
|
+
encoder.encode(input)
|
|
2266
|
+
);
|
|
2267
|
+
return `${input}.${base64url(new Uint8Array(signature))}`;
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
var halfHash = async (value) => base64url(
|
|
2271
|
+
new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))).slice(0, 16)
|
|
2272
|
+
);
|
|
2273
|
+
async function verifyAppleSecret(token, clientId, apple, now) {
|
|
2274
|
+
try {
|
|
2275
|
+
const parts = token.split(".");
|
|
2276
|
+
const [header, payload, signature] = parts;
|
|
2277
|
+
if (parts.length !== 3 || !header || !payload || !signature) return false;
|
|
2278
|
+
const decode = (value) => Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
2279
|
+
const h = JSON.parse(new TextDecoder().decode(decode(header)));
|
|
2280
|
+
const p = JSON.parse(new TextDecoder().decode(decode(payload)));
|
|
2281
|
+
const seconds = Math.floor(now / 1e3);
|
|
2282
|
+
if (h.alg !== "ES256" || h.kid !== apple.keyId || p.iss !== apple.teamId || p.sub !== clientId || p.aud !== "https://appleid.apple.com" || typeof p.iat !== "number" || typeof p.exp !== "number" || p.iat > seconds + 60 || p.exp <= seconds || p.exp <= p.iat || p.exp - p.iat > 15777e3)
|
|
2283
|
+
return false;
|
|
2284
|
+
const key = await crypto.subtle.importKey(
|
|
2285
|
+
"jwk",
|
|
2286
|
+
apple.publicKey,
|
|
2287
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
2288
|
+
false,
|
|
2289
|
+
["verify"]
|
|
2290
|
+
);
|
|
2291
|
+
return await crypto.subtle.verify(
|
|
2292
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
2293
|
+
key,
|
|
2294
|
+
decode(signature),
|
|
2295
|
+
encoder.encode(`${header}.${payload}`)
|
|
2296
|
+
);
|
|
2297
|
+
} catch {
|
|
2298
|
+
return false;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/ui.ts
|
|
2303
|
+
var escapeHtml = (value) => value.replace(
|
|
2304
|
+
/[&<>"']/g,
|
|
2305
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c
|
|
2306
|
+
);
|
|
2307
|
+
var style = `
|
|
2308
|
+
:root{color-scheme:light dark;--bg:#fafafa;--card:#fff;--ink:#18181b;--muted:#62626b;--line:#dedee3;--soft:#f4f4f5;--accent:#27272a;--on-accent:#fff;--error:#a82d32;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
2309
|
+
@media(prefers-color-scheme:dark){:root{--bg:#111113;--card:#19191c;--ink:#f4f4f5;--muted:#a9a9b2;--line:#36363c;--soft:#242428;--accent:#e4e4e7;--on-accent:#18181b;--error:#ffaaaa}}
|
|
2310
|
+
.privacy{border:1px solid var(--line);border-radius:12px;padding:14px;margin:0 0 20px}.privacy legend{font-size:13px;font-weight:600;padding:0 5px}.privacy label{display:flex;align-items:flex-start;gap:10px;margin:10px 0;font-size:14px;cursor:pointer}.privacy input{width:18px;height:18px;min-height:18px;accent-color:var(--accent);flex-shrink:0}.privacy small{display:block;font-size:12px;font-weight:400;color:var(--muted);line-height:1.6;margin-top:3px}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);min-height:100svh;display:flex;flex-direction:column}a{color:var(--accent);text-underline-offset:4px}header{padding:28px 5vw;display:flex;align-items:center;gap:12px;font-size:15px;font-weight:650;letter-spacing:-.3px}.mark{display:grid;place-items:center;width:34px;height:34px;border-radius:11px;background:var(--accent);color:var(--on-accent);font-size:21px}.badge{margin-left:auto;border:1px solid var(--line);border-radius:30px;padding:7px 12px;font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}main{width:min(100% - 32px,460px);margin:auto; padding:35px 0 55px}.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:28px}.eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.16em;color:var(--accent);font-weight:700}h1{font-size:30px;line-height:1.18;letter-spacing:-.5px;font-weight:600;margin:16px 0 12px}p{line-height:1.65;color:var(--muted);font-size:14px;margin:0 0 24px}.app{color:var(--ink);font-weight:600}.accounts{display:grid;gap:10px;margin:26px 0}button,input{font:inherit}button{cursor:pointer}button:disabled{opacity:.55;cursor:wait}.account{width:100%;display:flex;gap:13px;align-items:center;text-align:left;background:var(--card);border:1px solid var(--line);border-radius:13px;padding:14px;color:var(--ink)}.account:hover{background:var(--soft);border-color:var(--accent)}.avatar{flex-shrink:0;display:grid;place-items:center;width:40px;height:40px;background:var(--soft);color:var(--accent);border-radius:50%;font-size:15px;font-weight:600}.identity{min-width:0;flex:1}.identity strong,.identity small{display:block;overflow-wrap:anywhere}.identity strong{font-size:14px;font-weight:600}.identity small{font-size:12px;color:var(--muted);margin-top:4px}.arrow{color:var(--muted)}.primary,.secondary{width:100%;min-height:46px;border-radius:11px;padding:12px 16px;font-weight:600;font-size:14px}.primary{background:var(--accent);color:var(--on-accent);border:1px solid var(--accent)}.primary:hover{filter:brightness(1.08)}.secondary{background:transparent;color:var(--ink);border:1px solid var(--line);margin-top:10px}.secondary:hover{background:var(--soft)}label{display:block;font-size:13px;font-weight:600;margin:18px 0 7px}input{width:100%;min-height:46px;border:1px solid var(--line);border-radius:10px;background:var(--card);color:var(--ink);padding:11px 13px;font-size:15px}input:focus{border-color:var(--accent)}:is(a,button,input,select,textarea,summary,[tabindex="0"]):focus-visible{outline:3px solid var(--accent);outline-offset:4px}h1[tabindex="-1"]:focus{outline:none}form{margin:0}.fields{margin:24px 0}.note{font-size:12px;text-align:center;margin:22px 8px 0;line-height:1.7}.divider{height:1px;background:var(--line);margin:25px 0}footer{display:flex;justify-content:center;gap:20px;padding:24px;font-size:11px;color:var(--muted)}.error{color:var(--error);background:var(--soft);border-radius:10px;padding:12px;font-size:13px;margin:18px 0}.permissions{padding:0;list-style:none;margin:22px 0}.permissions li{padding:13px 0;border-bottom:1px solid var(--line);font-size:14px;display:flex;gap:12px}.check{color:var(--accent)}.back{display:block;text-align:center;font-size:13px;margin-top:20px}.empty{padding:20px 0}.skip{position:absolute;top:-100px;left:16px;background:var(--card);padding:12px;z-index:2}.skip:focus{top:10px}@media(max-width:480px){header{padding:20px}.card{padding:26px 23px}main{padding-top:18px}h1{font-size:28px}}@media(prefers-reduced-motion:no-preference){button{transition:background .15s,border-color .15s}.card{animation:arrive .2s ease-out}@keyframes arrive{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}}
|
|
2311
|
+
:root:has(input[name="oauth-theme"][value="light"]:checked){color-scheme:light;--bg:#fafafa;--card:#fff;--ink:#18181b;--muted:#62626b;--line:#dedee3;--soft:#f4f4f5;--accent:#27272a;--on-accent:#fff;--error:#a82d32}
|
|
2312
|
+
:root:has(input[name="oauth-theme"][value="dark"]:checked){color-scheme:dark;--bg:#111113;--card:#19191c;--ink:#f4f4f5;--muted:#a9a9b2;--line:#36363c;--soft:#242428;--accent:#e4e4e7;--on-accent:#18181b;--error:#ffaaaa}
|
|
2313
|
+
.theme{border:0;padding:0;display:flex;gap:16px;align-items:center}.theme legend{float:left;margin-right:16px;padding:0}.theme label{display:flex;align-items:center;gap:5px;margin:0;font-weight:400;font-size:12px}.theme input{width:14px;height:14px;min-height:0;margin:0;padding:0;accent-color:var(--accent)}
|
|
2314
|
+
|
|
2315
|
+
`;
|
|
2316
|
+
function page(title, body, status = 200, action = "'self'", nonce = crypto.randomUUID()) {
|
|
2317
|
+
return new Response(
|
|
2318
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>${escapeHtml(title)} \xB7 OAuth Mock</title><style nonce="${nonce}">${style}</style></head><body><a class="skip" href="#main">Skip to content</a><header><span>OAuth Mock</span></header><main id="main"><section class="card" aria-labelledby="title">${body}</section><p class="note">Test accounts only.</p></main><footer><fieldset class="theme"><legend>Appearance</legend><label><input type="radio" name="oauth-theme" value="system" checked>System</label><label><input type="radio" name="oauth-theme" value="light">Light</label><label><input type="radio" name="oauth-theme" value="dark">Dark</label></fieldset></footer><script nonce="${nonce}">(()=>{const root=document.documentElement;const inputs=document.querySelectorAll('input[name="oauth-theme"]');const apply=value=>{root.dataset.theme=value;for(const input of inputs)input.checked=input.value===value;try{sessionStorage.setItem('oauth-mock-theme',value)}catch{}};try{const saved=sessionStorage.getItem('oauth-mock-theme');if(['system','light','dark'].includes(saved))apply(saved)}catch{}for(const input of inputs)input.addEventListener('change',()=>apply(input.value))})()</script></body></html>`,
|
|
2319
|
+
{
|
|
2320
|
+
status,
|
|
2321
|
+
headers: {
|
|
2322
|
+
"content-type": "text/html; charset=utf-8",
|
|
2323
|
+
"cache-control": "no-store",
|
|
2324
|
+
"referrer-policy": "same-origin",
|
|
2325
|
+
"x-content-type-options": "nosniff",
|
|
2326
|
+
"content-security-policy": `default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; form-action ${action}; base-uri 'none'; frame-ancestors 'none'`
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
function loginPage(transaction, client, accounts, base, signup = false, error = "") {
|
|
2332
|
+
const hidden = `<input type="hidden" name="transaction" value="${escapeHtml(transaction)}">`;
|
|
2333
|
+
const endpoint = escapeHtml(`${base}/interaction`);
|
|
2334
|
+
const intro = `<span class="eyebrow">Sign in</span><h1 id="title">${signup ? "Create your account" : "Choose an account"}</h1><p>${signup ? "Create an account to continue to" : "Choose an account to continue to"}<br><span class="app">${escapeHtml(client)}</span></p>${error ? `<div class="error" role="alert">${escapeHtml(error)}</div>` : ""}`;
|
|
2335
|
+
const body = signup ? `<form method="post" action="${endpoint}">${hidden}<input type="hidden" name="action" value="signup"><div class="fields"><label for="name">Full name</label><input id="name" name="name" autocomplete="name" required maxlength="120"><label for="email">Email address</label><input id="email" name="email" type="email" autocomplete="email" required maxlength="254" aria-describedby="email-help"><p id="email-help" class="note">Use a fictional address. No email will be sent.</p></div><button class="primary">Create account & continue</button></form><a class="back" href="${endpoint}?transaction=${escapeHtml(transaction)}">Back to accounts</a>` : `<div class="accounts">${accounts.map((a) => `<form method="post" action="${endpoint}">${hidden}<input type="hidden" name="action" value="select"><button class="account" name="account" value="${escapeHtml(a.id)}"><span class="avatar" aria-hidden="true">${escapeHtml(a.name.slice(0, 1).toUpperCase())}</span><span class="identity"><strong>${escapeHtml(a.name)}</strong><small>${escapeHtml(a.email)}</small></span><span class="arrow" aria-hidden="true">\u2192</span></button></form>`).join("") || '<p class="empty">No accounts yet. Create your first test identity below.</p>'}</div><form method="get" action="${endpoint}">${hidden}<input type="hidden" name="screen" value="signup"><button class="secondary">\uFF0B Create a new account</button></form>`;
|
|
2336
|
+
return page(
|
|
2337
|
+
signup ? "Create account" : "Choose an account",
|
|
2338
|
+
`${intro}${body}<div class="divider"></div><form method="post" action="${endpoint}">${hidden}<button class="secondary" name="action" value="deny">Cancel sign-in</button></form>`,
|
|
2339
|
+
error ? 400 : 200
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
function consentPage(transaction, client, account, scopes2, base, privacy) {
|
|
2343
|
+
const labels = {
|
|
2344
|
+
openid: "Confirm your identity",
|
|
2345
|
+
email: "View your email address",
|
|
2346
|
+
profile: "View your name and profile",
|
|
2347
|
+
name: "View your name",
|
|
2348
|
+
offline_access: "Stay connected when you\u2019re away"
|
|
2349
|
+
};
|
|
2350
|
+
const privacyFields = privacy ? privacy.choice ? `<fieldset class="privacy"><legend>Choose what to share</legend><label><input type="radio" name="email_choice" value="share" ${!privacy.hideEmail ? "checked" : ""}><span>Share my email<small>Your app will receive ${escapeHtml(account.email)}.</small></span></label><label><input type="radio" name="email_choice" value="hide" ${privacy.hideEmail ? "checked" : ""}><span>Hide my email<small>Use a private relay address to keep your email private.</small></span></label></fieldset>` : `<p>${privacy.hideEmail ? "A private relay address will be shared with this app." : "Your email address will be shared with this app."}</p>` : "";
|
|
2351
|
+
return page(
|
|
2352
|
+
"Review access",
|
|
2353
|
+
`<span class="eyebrow">Permissions</span><h1 id="title">Review access</h1><p><span class="app">${escapeHtml(client)}</span> would like access to your account.</p><div class="account"><span class="avatar" aria-hidden="true">${escapeHtml(account.name.slice(0, 1))}</span><span class="identity"><strong>${escapeHtml(account.name)}</strong><small>${escapeHtml(account.email)}</small></span></div><ul class="permissions">${scopes2.split(" ").filter(Boolean).map(
|
|
2354
|
+
(s) => `<li><span class="check" aria-hidden="true">\u2713</span>${escapeHtml(labels[s] ?? s)}</li>`
|
|
2355
|
+
).join(
|
|
2356
|
+
""
|
|
2357
|
+
)}</ul><p>You can cancel now without sharing anything.</p><form method="post" action="${escapeHtml(base)}/interaction"><input type="hidden" name="transaction" value="${escapeHtml(transaction)}">${privacyFields}<button class="primary" name="action" value="allow">Allow & continue</button><button class="secondary" name="action" value="deny">Cancel</button></form>`
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// src/generated/openapi.ts
|
|
2362
|
+
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Mockingbird OAuth & Social Login","version":"1.0.0"},"paths":{"/.well-known/openid-configuration":{"get":{"operationId":"Discovery","summary":"Discovery","x-mockingbird":{"parity":{"enabled":true,"reason":"Google, Apple and Microsoft discovery metadata is checked against their public live endpoints by scripts/parity.ts."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/jwks":{"get":{"operationId":"Jwks","summary":"Jwks","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/authorize":{"get":{"operationId":"Authorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/interaction":{"get":{"operationId":"InteractionPage","summary":"InteractionPage","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"operationId":"Interact","summary":"Interact","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/token":{"post":{"operationId":"Token","summary":"Token","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/userinfo":{"get":{"operationId":"UserInfo","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"UserInfoPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/revoke":{"post":{"operationId":"Revoke","summary":"Revoke","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/o/oauth2/v2/auth":{"get":{"operationId":"GoogleAuthorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/o/oauth2/auth":{"get":{"operationId":"GoogleLegacyAuthorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/auth/authorize":{"get":{"operationId":"AppleAuthorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/auth/token":{"post":{"operationId":"AppleToken","summary":"Token","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/auth/keys":{"get":{"operationId":"AppleKeys","summary":"Jwks","x-mockingbird":{"parity":{"enabled":true,"reason":"Apple's public signing-key contract is checked by scripts/parity.ts."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/oauth2/v3/certs":{"get":{"operationId":"GoogleKeys","summary":"Jwks","x-mockingbird":{"parity":{"enabled":true,"reason":"Google's public signing-key contract is checked by scripts/parity.ts."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/v1/userinfo":{"get":{"operationId":"GoogleUserInfo","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"GoogleUserInfoPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/oauth2/v3/userinfo":{"get":{"operationId":"GoogleV3UserInfo","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"GoogleV3UserInfoPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/auth/revoke":{"post":{"operationId":"AppleRevoke","summary":"Revoke","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/oauth2/v2.0/authorize":{"get":{"operationId":"MicrosoftAuthorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/oauth2/v2.0/token":{"post":{"operationId":"MicrosoftToken","summary":"Token","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/discovery/v2.0/keys":{"get":{"operationId":"MicrosoftKeys","summary":"Jwks","x-mockingbird":{"parity":{"enabled":true,"reason":"Microsoft's public signing-key contract is checked by scripts/parity.ts."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/oidc/userinfo":{"get":{"operationId":"MicrosoftUserInfo","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"MicrosoftUserInfoPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/login/oauth/authorize":{"get":{"operationId":"GitHubAuthorize","summary":"Authorize","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/login/oauth/access_token":{"post":{"operationId":"GitHubToken","summary":"Token","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"application/x-www-form-urlencoded":{"schema":{"type":"string"}}}}}}},"/user":{"get":{"operationId":"GitHubUser","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"GitHubUserPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/user/emails":{"get":{"operationId":"GitHubEmails","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"post":{"operationId":"GitHubEmailsPost","summary":"UserInfo","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"302":{"description":"302","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"400","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"401","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"403","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"404","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"503":{"description":"503","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/":{"get":{"operationId":"Welcome","summary":"InteractionPage","x-mockingbird":{"parity":{"enabled":false,"reason":"Interactive OAuth transactions are exercised by protocol and independent JOSE tests."}},"responses":{"200":{"description":"200","content":{"text/html":{"schema":{"type":"string"}}}},"302":{"description":"302","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"400","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"description":"401","content":{"text/html":{"schema":{"type":"string"}}}},"403":{"description":"403","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"404","content":{"text/html":{"schema":{"type":"string"}}}},"503":{"description":"503","content":{"text/html":{"schema":{"type":"string"}}}}}}}}}`);
|
|
2363
|
+
var operationIds = ["Discovery", "Jwks", "Authorize", "InteractionPage", "Interact", "Token", "UserInfo", "UserInfoPost", "Revoke", "GoogleAuthorize", "GoogleLegacyAuthorize", "AppleAuthorize", "AppleToken", "AppleKeys", "GoogleKeys", "GoogleUserInfo", "GoogleUserInfoPost", "GoogleV3UserInfo", "GoogleV3UserInfoPost", "AppleRevoke", "MicrosoftAuthorize", "MicrosoftToken", "MicrosoftKeys", "MicrosoftUserInfo", "MicrosoftUserInfoPost", "GitHubAuthorize", "GitHubToken", "GitHubUser", "GitHubUserPost", "GitHubEmails", "GitHubEmailsPost", "Welcome"];
|
|
2364
|
+
var supportedOperationIds = ["Discovery", "Jwks", "Authorize", "InteractionPage", "Interact", "Token", "UserInfo", "UserInfoPost", "Revoke", "GoogleAuthorize", "GoogleLegacyAuthorize", "AppleAuthorize", "AppleToken", "AppleKeys", "GoogleKeys", "GoogleUserInfo", "GoogleUserInfoPost", "GoogleV3UserInfo", "GoogleV3UserInfoPost", "AppleRevoke", "MicrosoftAuthorize", "MicrosoftToken", "MicrosoftKeys", "MicrosoftUserInfo", "MicrosoftUserInfoPost", "GitHubAuthorize", "GitHubToken", "GitHubUser", "GitHubUserPost", "GitHubEmails", "GitHubEmailsPost", "Welcome"];
|
|
2365
|
+
|
|
2366
|
+
// src/runtime.ts
|
|
2367
|
+
var OAUTH_PRESETS = {
|
|
2368
|
+
token_unavailable: {
|
|
2369
|
+
description: "Token endpoint returns temporarily_unavailable",
|
|
2370
|
+
rules: ["Token", "AppleToken", "MicrosoftToken", "GitHubToken"].map((operationId) => ({
|
|
2371
|
+
operationId,
|
|
2372
|
+
status: 503,
|
|
2373
|
+
body: { error: "temporarily_unavailable" }
|
|
2374
|
+
}))
|
|
2375
|
+
},
|
|
2376
|
+
access_denied: {
|
|
2377
|
+
description: "Authorization is denied before interaction",
|
|
2378
|
+
rules: [
|
|
2379
|
+
"Authorize",
|
|
2380
|
+
"GoogleAuthorize",
|
|
2381
|
+
"GoogleLegacyAuthorize",
|
|
2382
|
+
"AppleAuthorize",
|
|
2383
|
+
"MicrosoftAuthorize",
|
|
2384
|
+
"GitHubAuthorize"
|
|
2385
|
+
].map((operationId) => ({ operationId, status: 403, body: { error: "access_denied" } }))
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
function createRuntime2(options = {}) {
|
|
2389
|
+
return createRuntime({
|
|
2390
|
+
name: options.runtimeName ?? "oauth",
|
|
2391
|
+
document,
|
|
2392
|
+
presets: OAUTH_PRESETS,
|
|
2393
|
+
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2394
|
+
...options.clock ? { clock: options.clock } : {},
|
|
2395
|
+
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
2396
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
2397
|
+
create: ({ sqlite, namespace, publicNamespace, clock }) => new OAuthAPI({ ...options, sqlite, namespace, publicNamespace, now: clock.now }),
|
|
2398
|
+
admin: (runtime) => ({
|
|
2399
|
+
"GET /scenarios": () => Response.json(OAUTH_SCENARIOS),
|
|
2400
|
+
"GET /behavior": ({ namespace }) => Response.json({
|
|
2401
|
+
behavior: runtime.instance(namespace).behavior.config,
|
|
2402
|
+
events: runtime.instance(namespace).behavior.events
|
|
2403
|
+
}),
|
|
2404
|
+
"PUT /behavior": ({ namespace, body }) => {
|
|
2405
|
+
try {
|
|
2406
|
+
return Response.json(runtime.instance(namespace).behavior.configure(body));
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
return Response.json({ error: String(error) }, { status: 400 });
|
|
2409
|
+
}
|
|
2410
|
+
},
|
|
2411
|
+
"POST /consents/revoke": ({ namespace, body }) => {
|
|
2412
|
+
const input = body;
|
|
2413
|
+
if (!input || typeof input.clientId !== "string" || typeof input.accountId !== "string")
|
|
2414
|
+
return Response.json({ error: "clientId and accountId are required" }, { status: 400 });
|
|
2415
|
+
runtime.instance(namespace).revokeConsent(input.clientId, input.accountId);
|
|
2416
|
+
return Response.json({ revoked: true });
|
|
2417
|
+
},
|
|
2418
|
+
"POST /keys/rotate": ({ namespace, body }) => {
|
|
2419
|
+
const input = body;
|
|
2420
|
+
if (input?.retainPrevious !== void 0 && typeof input.retainPrevious !== "boolean")
|
|
2421
|
+
return Response.json({ error: "retainPrevious must be boolean" }, { status: 400 });
|
|
2422
|
+
return Response.json(
|
|
2423
|
+
runtime.instance(namespace).rotateSigningKey(input?.retainPrevious !== false)
|
|
2424
|
+
);
|
|
2425
|
+
},
|
|
2426
|
+
"GET /accounts": ({ namespace }) => Response.json({
|
|
2427
|
+
accounts: runtime.instance(namespace).accounts.list({ order: "oldest" }).map((a) => a.value)
|
|
2428
|
+
}),
|
|
2429
|
+
"POST /accounts": ({ namespace, body }) => {
|
|
2430
|
+
try {
|
|
2431
|
+
return Response.json(runtime.instance(namespace).seedAccount(body), {
|
|
2432
|
+
status: 201
|
|
2433
|
+
});
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
return Response.json({ error: String(error) }, { status: 400 });
|
|
2436
|
+
}
|
|
2437
|
+
},
|
|
2438
|
+
"GET /clients": ({ namespace }) => Response.json({
|
|
2439
|
+
clients: runtime.instance(namespace).clients.list({ order: "oldest" }).map(({ value }) => ({
|
|
2440
|
+
id: value.id,
|
|
2441
|
+
name: value.name,
|
|
2442
|
+
redirectUris: value.redirectUris,
|
|
2443
|
+
requirePkce: value.requirePkce ?? value.secret === void 0
|
|
2444
|
+
}))
|
|
2445
|
+
}),
|
|
2446
|
+
"POST /clients": ({ namespace, body }) => {
|
|
2447
|
+
try {
|
|
2448
|
+
const client = runtime.instance(namespace).registerClient(body);
|
|
2449
|
+
return Response.json(
|
|
2450
|
+
{ id: client.id, name: client.name, redirectUris: client.redirectUris },
|
|
2451
|
+
{ status: 201 }
|
|
2452
|
+
);
|
|
2453
|
+
} catch (error) {
|
|
2454
|
+
return Response.json({ error: String(error) }, { status: 400 });
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
})
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/multi.ts
|
|
2462
|
+
var MOUNT_PATTERN = /^\/[A-Za-z0-9](?:[A-Za-z0-9._~-]*)(?:\/[A-Za-z0-9](?:[A-Za-z0-9._~-]*))*$/;
|
|
2463
|
+
var NS_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
2464
|
+
var json3 = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
|
|
2465
|
+
var validateMounts = (mounts) => {
|
|
2466
|
+
if (mounts.length === 0) throw new Error("at least one OAuth mount is required");
|
|
2467
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2468
|
+
const issuers = /* @__PURE__ */ new Set();
|
|
2469
|
+
for (const mount of mounts) {
|
|
2470
|
+
if (!MOUNT_PATTERN.test(mount.path) || mount.path.includes("//"))
|
|
2471
|
+
throw new Error(`invalid OAuth mount path ${JSON.stringify(mount.path)}`);
|
|
2472
|
+
if (paths.has(mount.path)) throw new Error(`duplicate OAuth mount path ${mount.path}`);
|
|
2473
|
+
paths.add(mount.path);
|
|
2474
|
+
if (mount.issuer) {
|
|
2475
|
+
const normalized = mount.issuer.replace(/\/$/, "");
|
|
2476
|
+
if (issuers.has(normalized)) throw new Error(`duplicate OAuth issuer ${normalized}`);
|
|
2477
|
+
issuers.add(normalized);
|
|
2478
|
+
}
|
|
2479
|
+
const clientIds = /* @__PURE__ */ new Set();
|
|
2480
|
+
const keyIds = /* @__PURE__ */ new Set();
|
|
2481
|
+
for (const client of mount.clients ?? []) {
|
|
2482
|
+
if (clientIds.has(client.id))
|
|
2483
|
+
throw new Error(`duplicate client ID ${client.id} in OAuth mount ${mount.path}`);
|
|
2484
|
+
clientIds.add(client.id);
|
|
2485
|
+
if (client.apple?.keyId) {
|
|
2486
|
+
if (keyIds.has(client.apple.keyId))
|
|
2487
|
+
throw new Error(
|
|
2488
|
+
`duplicate Apple key ID ${client.apple.keyId} in OAuth mount ${mount.path}`
|
|
2489
|
+
);
|
|
2490
|
+
keyIds.add(client.apple.keyId);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
var withoutMount = (request, namespacePrefix, rest) => {
|
|
2496
|
+
const url = new URL(request.url);
|
|
2497
|
+
url.pathname = `${namespacePrefix}${rest || "/"}`;
|
|
2498
|
+
return new Request(url, request);
|
|
2499
|
+
};
|
|
2500
|
+
function createMultiRuntime(options) {
|
|
2501
|
+
validateMounts(options.mounts);
|
|
2502
|
+
const clock = options.clock ?? createClock();
|
|
2503
|
+
const runtimes = /* @__PURE__ */ new Map();
|
|
2504
|
+
for (const mount of options.mounts) {
|
|
2505
|
+
const { path, ...runtimeOptions } = mount;
|
|
2506
|
+
const runtime = createRuntime2({
|
|
2507
|
+
...runtimeOptions,
|
|
2508
|
+
clock,
|
|
2509
|
+
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2510
|
+
...runtimeOptions.adminKey ?? options.adminKey ? { adminKey: runtimeOptions.adminKey ?? options.adminKey } : {},
|
|
2511
|
+
...(runtimeOptions.seed ?? options.seed) !== void 0 ? { seed: runtimeOptions.seed ?? options.seed } : {},
|
|
2512
|
+
mountPath: path,
|
|
2513
|
+
runtimeName: `oauth@${path}`
|
|
2514
|
+
});
|
|
2515
|
+
runtime.instance();
|
|
2516
|
+
runtimes.set(path, runtime);
|
|
2517
|
+
}
|
|
2518
|
+
const reset = async (namespace = "*") => {
|
|
2519
|
+
await Promise.all([...runtimes.values()].map((runtime) => runtime.reset(namespace)));
|
|
2520
|
+
};
|
|
2521
|
+
return {
|
|
2522
|
+
name: "oauth",
|
|
2523
|
+
clock,
|
|
2524
|
+
mounts: runtimes,
|
|
2525
|
+
reset,
|
|
2526
|
+
async fetch(request) {
|
|
2527
|
+
const url = new URL(request.url);
|
|
2528
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
2529
|
+
const health = await Promise.all(
|
|
2530
|
+
[...runtimes.entries()].map(async ([path2, runtime]) => [
|
|
2531
|
+
path2,
|
|
2532
|
+
await (await runtime.fetch(new Request(new URL("/health", url), request))).json()
|
|
2533
|
+
])
|
|
2534
|
+
);
|
|
2535
|
+
return json3({ status: "ok", service: "oauth", mounts: Object.fromEntries(health) });
|
|
2536
|
+
}
|
|
2537
|
+
if (url.pathname.startsWith("/__admin") && options.adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== options.adminKey)
|
|
2538
|
+
return json3({ error: { type: "mockingbird_admin", message: "invalid admin key" } }, 401);
|
|
2539
|
+
if (url.pathname === "/__admin/mounts" && request.method === "GET")
|
|
2540
|
+
return json3({
|
|
2541
|
+
mounts: options.mounts.map(({ path: path2, provider, issuer }) => ({ path: path2, provider, issuer }))
|
|
2542
|
+
});
|
|
2543
|
+
if (url.pathname === "/__admin/reset" && request.method === "POST") {
|
|
2544
|
+
const namespace2 = url.searchParams.get("all") === "1" ? "*" : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? "default";
|
|
2545
|
+
await reset(namespace2);
|
|
2546
|
+
return json3({ status: "ok", mounts: [...runtimes.keys()], namespace: namespace2 });
|
|
2547
|
+
}
|
|
2548
|
+
if (url.pathname.startsWith("/__admin/")) {
|
|
2549
|
+
const selected = url.searchParams.get("mount");
|
|
2550
|
+
const runtime = selected && runtimes.get(selected);
|
|
2551
|
+
if (!runtime)
|
|
2552
|
+
return json3({ error: { type: "mockingbird_admin", message: "mount is required" } }, 400);
|
|
2553
|
+
url.searchParams.delete("mount");
|
|
2554
|
+
return runtime.fetch(new Request(url, request));
|
|
2555
|
+
}
|
|
2556
|
+
const namespace = NS_PREFIX.exec(url.pathname);
|
|
2557
|
+
const namespacePrefix = namespace ? `/ns/${namespace[1]}` : "";
|
|
2558
|
+
const path = namespace ? namespace[2] ?? "/" : url.pathname;
|
|
2559
|
+
for (const [mount, runtime] of runtimes) {
|
|
2560
|
+
if (path !== mount && !path.startsWith(`${mount}/`)) continue;
|
|
2561
|
+
return runtime.fetch(withoutMount(request, namespacePrefix, path.slice(mount.length)));
|
|
2562
|
+
}
|
|
2563
|
+
return json3({ error: { type: "mockingbird_not_found", message: "unknown OAuth mount" } }, 404);
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/index.ts
|
|
2569
|
+
var json4 = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store", pragma: "no-cache" } });
|
|
2570
|
+
var fail = (error, description, status = 400) => json4({ error, error_description: description }, status);
|
|
2571
|
+
var scopes = (value) => new Set(value.split(/\s+/).filter(Boolean));
|
|
2572
|
+
var validEmail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) && s.length <= 254;
|
|
2573
|
+
var WEB_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2574
|
+
var FORBIDDEN_REDIRECT_SCHEMES = /* @__PURE__ */ new Set(["blob:", "data:", "file:", "javascript:", "vbscript:"]);
|
|
2575
|
+
var safeRedirect = (s) => {
|
|
2576
|
+
try {
|
|
2577
|
+
const u = new URL(s);
|
|
2578
|
+
return /^[a-z][a-z0-9+.-]*:$/i.test(u.protocol) && !FORBIDDEN_REDIRECT_SCHEMES.has(u.protocol.toLowerCase()) && !u.hash && !u.username && !u.password;
|
|
2579
|
+
} catch {
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
var OAuthAPI = class {
|
|
2584
|
+
constructor(options = {}) {
|
|
2585
|
+
this.options = options;
|
|
2586
|
+
this.sqlite = bootSqlite(options.sqlite);
|
|
2587
|
+
this.namespace = options.namespace ?? "oauth";
|
|
2588
|
+
this.now = options.now ?? Date.now;
|
|
2589
|
+
this.provider = options.provider ?? "oidc";
|
|
2590
|
+
if (options.issuer) {
|
|
2591
|
+
const url = new URL(options.issuer);
|
|
2592
|
+
if (!safeRedirect(options.issuer) || url.search)
|
|
2593
|
+
throw new Error("issuer must be an HTTP(S) URL without query or fragment");
|
|
2594
|
+
}
|
|
2595
|
+
this.accounts = new Collection(this.sqlite, this.namespace, "accounts");
|
|
2596
|
+
this.clients = new Collection(this.sqlite, this.namespace, "clients");
|
|
2597
|
+
this.transactions = new Collection(this.sqlite, this.namespace, "transactions");
|
|
2598
|
+
this.codes = new Collection(this.sqlite, this.namespace, "codes");
|
|
2599
|
+
this.tokens = new Collection(this.sqlite, this.namespace, "tokens");
|
|
2600
|
+
this.sessions = new Collection(this.sqlite, this.namespace, "sessions");
|
|
2601
|
+
this.consents = new Collection(this.sqlite, this.namespace, "consents");
|
|
2602
|
+
this.identities = new Collection(this.sqlite, this.namespace, "identities");
|
|
2603
|
+
this.appleDisclosures = new Collection(this.sqlite, this.namespace, "apple_disclosures");
|
|
2604
|
+
this.refreshIssued = new Collection(this.sqlite, this.namespace, "refresh_issued");
|
|
2605
|
+
this.behavior = new BehaviorState(
|
|
2606
|
+
this.sqlite,
|
|
2607
|
+
this.namespace,
|
|
2608
|
+
options.seed ?? 0,
|
|
2609
|
+
options.behavior ?? {}
|
|
2610
|
+
);
|
|
2611
|
+
this.seed();
|
|
2612
|
+
}
|
|
2613
|
+
options;
|
|
2614
|
+
accounts;
|
|
2615
|
+
clients;
|
|
2616
|
+
transactions;
|
|
2617
|
+
codes;
|
|
2618
|
+
tokens;
|
|
2619
|
+
sessions;
|
|
2620
|
+
consents;
|
|
2621
|
+
signer = new Signer();
|
|
2622
|
+
previousSigners = [];
|
|
2623
|
+
behavior;
|
|
2624
|
+
identities;
|
|
2625
|
+
appleDisclosures;
|
|
2626
|
+
refreshIssued;
|
|
2627
|
+
sqlite;
|
|
2628
|
+
namespace;
|
|
2629
|
+
now;
|
|
2630
|
+
provider;
|
|
2631
|
+
seed() {
|
|
2632
|
+
for (const client of this.options.clients ?? []) this.registerClient(client);
|
|
2633
|
+
for (const account of this.options.accounts ?? []) this.seedAccount(account);
|
|
2634
|
+
}
|
|
2635
|
+
seedAccount(account) {
|
|
2636
|
+
if (!account || typeof account.id !== "string" || !account.id || typeof account.name !== "string" || !account.name.trim() || typeof account.email !== "string" || !validEmail(account.email))
|
|
2637
|
+
throw new Error("Account requires id, name and a valid email");
|
|
2638
|
+
const email = account.email.toLowerCase().trim();
|
|
2639
|
+
if (this.accounts.list().some((a) => a.value.email === email && a.id !== account.id))
|
|
2640
|
+
throw new Error("An account with this email already exists");
|
|
2641
|
+
if (account.relayEmail !== void 0 && (typeof account.relayEmail !== "string" || !/^[a-zA-Z0-9._-]+@privaterelay\.appleid\.com$/.test(account.relayEmail)))
|
|
2642
|
+
throw new Error("relayEmail must be an Apple private relay address");
|
|
2643
|
+
if (account.realUserStatus !== void 0 && ![0, 1, 2].includes(account.realUserStatus))
|
|
2644
|
+
throw new Error("realUserStatus must be 0, 1 or 2");
|
|
2645
|
+
if (account.transferSub !== void 0 && (typeof account.transferSub !== "string" || !account.transferSub))
|
|
2646
|
+
throw new Error("transferSub must be a nonempty string");
|
|
2647
|
+
if (account.github && (!Number.isSafeInteger(account.github.id) || account.github.id < 1 || typeof account.github.login !== "string" || !account.github.login))
|
|
2648
|
+
throw new Error("GitHub account requires a positive integer id and login");
|
|
2649
|
+
if (account.github?.emails && (!Array.isArray(account.github.emails) || account.github.emails.some(
|
|
2650
|
+
(e) => !e || typeof e.email !== "string" || !validEmail(e.email) || typeof e.primary !== "boolean" || typeof e.verified !== "boolean" || !["public", "private", null].includes(e.visibility)
|
|
2651
|
+
)))
|
|
2652
|
+
throw new Error("Invalid GitHub email records");
|
|
2653
|
+
const value = { ...account, email };
|
|
2654
|
+
this.accounts.insert(account.id, value);
|
|
2655
|
+
return value;
|
|
2656
|
+
}
|
|
2657
|
+
registerClient(client) {
|
|
2658
|
+
if (!client || typeof client.id !== "string" || !client.id || typeof client.name !== "string" || !client.name || !Array.isArray(client.redirectUris) || !client.redirectUris.length || !client.redirectUris.every((s) => typeof s === "string" && safeRedirect(s)))
|
|
2659
|
+
throw new Error(
|
|
2660
|
+
"Client requires id, name and exact safe redirectUris without fragments or credentials"
|
|
2661
|
+
);
|
|
2662
|
+
if (client.apple && (client.secret !== void 0 || !client.apple.teamId || !client.apple.keyId || client.apple.publicKey?.kty !== "EC" || client.apple.publicKey.crv !== "P-256" || client.apple.publicKey.d))
|
|
2663
|
+
throw new Error(
|
|
2664
|
+
"Apple client requires teamId, keyId and a public P-256 JWK, without a static secret"
|
|
2665
|
+
);
|
|
2666
|
+
this.clients.insert(client.id, structuredClone(client));
|
|
2667
|
+
return client;
|
|
2668
|
+
}
|
|
2669
|
+
async reset() {
|
|
2670
|
+
clearNamespace(this.sqlite, this.namespace);
|
|
2671
|
+
this.behavior.configure(this.options.behavior ?? {});
|
|
2672
|
+
this.seed();
|
|
2673
|
+
}
|
|
2674
|
+
issuer(request) {
|
|
2675
|
+
const prefix = this.options.publicNamespace && this.options.publicNamespace !== "default" ? `/ns/${encodeURIComponent(this.options.publicNamespace)}` : "";
|
|
2676
|
+
return (this.options.issuer ?? `${new URL(request.url).origin}${prefix}${this.options.mountPath ?? ""}`).replace(/\/$/, "");
|
|
2677
|
+
}
|
|
2678
|
+
configureBehavior(input) {
|
|
2679
|
+
return this.behavior.configure(input);
|
|
2680
|
+
}
|
|
2681
|
+
rotateSigningKey(retainPrevious = true) {
|
|
2682
|
+
this.previousSigners = retainPrevious ? [this.signer, ...this.previousSigners].slice(0, 4) : [];
|
|
2683
|
+
this.signer = new Signer();
|
|
2684
|
+
return { kid: this.signer.kid };
|
|
2685
|
+
}
|
|
2686
|
+
revokeConsent(clientId, accountId) {
|
|
2687
|
+
const key = JSON.stringify([clientId, accountId]);
|
|
2688
|
+
this.consents.delete(key);
|
|
2689
|
+
this.refreshIssued.delete(key);
|
|
2690
|
+
const client = this.clients.get(clientId);
|
|
2691
|
+
if (client) {
|
|
2692
|
+
this.identities.delete(this.identityKey(client, accountId));
|
|
2693
|
+
this.appleDisclosures.delete(this.identityKey(client, accountId));
|
|
2694
|
+
}
|
|
2695
|
+
for (const row of this.codes.list({
|
|
2696
|
+
where: (c) => c.clientId === clientId && c.accountId === accountId
|
|
2697
|
+
}))
|
|
2698
|
+
this.codes.delete(row.id);
|
|
2699
|
+
for (const row of this.tokens.list({
|
|
2700
|
+
where: (t) => t.clientId === clientId && t.accountId === accountId
|
|
2701
|
+
}))
|
|
2702
|
+
this.tokens.delete(row.id);
|
|
2703
|
+
}
|
|
2704
|
+
paths() {
|
|
2705
|
+
const paths = {
|
|
2706
|
+
google: {
|
|
2707
|
+
authorize: "/o/oauth2/v2/auth",
|
|
2708
|
+
token: "/token",
|
|
2709
|
+
jwks: "/oauth2/v3/certs",
|
|
2710
|
+
userinfo: "/v1/userinfo",
|
|
2711
|
+
revoke: "/revoke"
|
|
2712
|
+
},
|
|
2713
|
+
apple: {
|
|
2714
|
+
authorize: "/auth/authorize",
|
|
2715
|
+
token: "/auth/token",
|
|
2716
|
+
jwks: "/auth/keys",
|
|
2717
|
+
userinfo: "/userinfo",
|
|
2718
|
+
revoke: "/auth/revoke"
|
|
2719
|
+
},
|
|
2720
|
+
microsoft: {
|
|
2721
|
+
authorize: "/oauth2/v2.0/authorize",
|
|
2722
|
+
token: "/oauth2/v2.0/token",
|
|
2723
|
+
jwks: "/discovery/v2.0/keys",
|
|
2724
|
+
userinfo: "/oidc/userinfo",
|
|
2725
|
+
revoke: "/revoke"
|
|
2726
|
+
},
|
|
2727
|
+
github: {
|
|
2728
|
+
authorize: "/login/oauth/authorize",
|
|
2729
|
+
token: "/login/oauth/access_token",
|
|
2730
|
+
jwks: "/jwks",
|
|
2731
|
+
userinfo: "/user",
|
|
2732
|
+
revoke: "/revoke"
|
|
2733
|
+
},
|
|
2734
|
+
oidc: {
|
|
2735
|
+
authorize: "/authorize",
|
|
2736
|
+
token: "/token",
|
|
2737
|
+
jwks: "/jwks",
|
|
2738
|
+
userinfo: "/userinfo",
|
|
2739
|
+
revoke: "/revoke"
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
return paths[this.provider];
|
|
2743
|
+
}
|
|
2744
|
+
supportedScopes() {
|
|
2745
|
+
const base = this.provider === "apple" ? ["openid", "email", "name"] : this.provider === "github" ? ["read:user", "user:email", "user"] : [
|
|
2746
|
+
"openid",
|
|
2747
|
+
"email",
|
|
2748
|
+
"profile",
|
|
2749
|
+
...this.provider !== "google" ? ["offline_access"] : [],
|
|
2750
|
+
...this.provider === "microsoft" ? ["User.Read"] : []
|
|
2751
|
+
];
|
|
2752
|
+
return [
|
|
2753
|
+
.../* @__PURE__ */ new Set([
|
|
2754
|
+
...base,
|
|
2755
|
+
...this.provider === "google" ? [
|
|
2756
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
2757
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
2758
|
+
] : [],
|
|
2759
|
+
...this.behavior.config.additionalScopes ?? []
|
|
2760
|
+
])
|
|
2761
|
+
];
|
|
2762
|
+
}
|
|
2763
|
+
identityKey(client, accountId) {
|
|
2764
|
+
return JSON.stringify([
|
|
2765
|
+
this.provider,
|
|
2766
|
+
client.subjectGroup ?? client.apple?.teamId ?? client.id,
|
|
2767
|
+
accountId
|
|
2768
|
+
]);
|
|
2769
|
+
}
|
|
2770
|
+
async identity(account, auth) {
|
|
2771
|
+
const client = this.clients.get(auth.clientId);
|
|
2772
|
+
if (!client) throw new Error("Client no longer exists");
|
|
2773
|
+
const key = this.identityKey(client, account.id);
|
|
2774
|
+
const previous = this.identities.get(key);
|
|
2775
|
+
const digest = await hash(key);
|
|
2776
|
+
const existing = this.identities.get(key) ?? previous;
|
|
2777
|
+
const hide = existing?.privateEmail ?? (auth.emailChoice ? auth.emailChoice === "hide" : account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail));
|
|
2778
|
+
const identity = {
|
|
2779
|
+
sub: this.provider === "apple" || this.provider === "microsoft" ? digest : account.id,
|
|
2780
|
+
email: this.provider === "apple" && hide ? existing?.email ?? account.relayEmail ?? `${digest.slice(0, 20).toLowerCase()}@privaterelay.appleid.com` : account.email,
|
|
2781
|
+
privateEmail: this.provider === "apple" && hide
|
|
2782
|
+
};
|
|
2783
|
+
this.identities.insert(key, identity);
|
|
2784
|
+
return identity;
|
|
2785
|
+
}
|
|
2786
|
+
profile(account, grant) {
|
|
2787
|
+
const requested = scopes(grant.scope);
|
|
2788
|
+
if (requested.has("https://www.googleapis.com/auth/userinfo.email")) requested.add("email");
|
|
2789
|
+
if (requested.has("https://www.googleapis.com/auth/userinfo.profile")) requested.add("profile");
|
|
2790
|
+
const omitEmail = grant.decisions.omitEmail || account.omitEmail;
|
|
2791
|
+
const omitName = grant.decisions.omitName || account.omitName;
|
|
2792
|
+
const claims = { sub: grant.identity.sub };
|
|
2793
|
+
if (requested.has("email") && !omitEmail)
|
|
2794
|
+
Object.assign(claims, {
|
|
2795
|
+
email: grant.identity.privateEmail ? grant.identity.email : account.email,
|
|
2796
|
+
...this.provider !== "microsoft" ? {
|
|
2797
|
+
email_verified: this.provider === "apple" && this.behavior.config.apple?.booleanClaims !== "boolean" ? String(!grant.decisions.unverifiedEmail && (account.emailVerified ?? true)) : !grant.decisions.unverifiedEmail && (account.emailVerified ?? true)
|
|
2798
|
+
} : {}
|
|
2799
|
+
});
|
|
2800
|
+
if (this.provider !== "apple" && requested.has("profile"))
|
|
2801
|
+
Object.assign(claims, {
|
|
2802
|
+
...!omitName ? {
|
|
2803
|
+
name: account.name,
|
|
2804
|
+
given_name: account.givenName ?? account.name.split(" ")[0],
|
|
2805
|
+
family_name: account.familyName ?? account.name.split(" ").slice(1).join(" ")
|
|
2806
|
+
} : {},
|
|
2807
|
+
...account.picture ? { picture: account.picture } : {},
|
|
2808
|
+
locale: account.locale ?? "en"
|
|
2809
|
+
});
|
|
2810
|
+
if (this.provider === "google" && account.hostedDomain) claims.hd = account.hostedDomain;
|
|
2811
|
+
if (this.provider === "apple") {
|
|
2812
|
+
if (requested.has("email") && !omitEmail)
|
|
2813
|
+
claims.is_private_email = this.behavior.config.apple?.booleanClaims === "boolean" ? grant.identity.privateEmail : String(grant.identity.privateEmail);
|
|
2814
|
+
if (account.realUserStatus !== void 0) claims.real_user_status = account.realUserStatus;
|
|
2815
|
+
if (account.transferSub) claims.transfer_sub = account.transferSub;
|
|
2816
|
+
}
|
|
2817
|
+
if (this.provider === "microsoft" && requested.has("profile"))
|
|
2818
|
+
Object.assign(claims, {
|
|
2819
|
+
ver: "2.0",
|
|
2820
|
+
oid: account.objectId ?? account.id,
|
|
2821
|
+
tid: account.tenantId ?? "9188040d-6c67-4c5b-b112-36a304b66dad",
|
|
2822
|
+
preferred_username: account.preferredUsername ?? account.email
|
|
2823
|
+
});
|
|
2824
|
+
return claims;
|
|
2825
|
+
}
|
|
2826
|
+
async idToken(grant, issuer, extra = {}) {
|
|
2827
|
+
const account = this.accounts.get(grant.accountId);
|
|
2828
|
+
if (!account) throw new Error("Account no longer exists");
|
|
2829
|
+
return this.signer.sign({
|
|
2830
|
+
...this.profile(account, grant),
|
|
2831
|
+
iss: issuer,
|
|
2832
|
+
aud: grant.clientId,
|
|
2833
|
+
iat: Math.floor(this.now() / 1e3),
|
|
2834
|
+
exp: Math.floor(this.now() / 1e3) + (this.behavior.config.tokens?.accessTtlSeconds ?? 3600),
|
|
2835
|
+
auth_time: Math.floor(grant.authTime / 1e3),
|
|
2836
|
+
...grant.nonce ? { nonce: grant.nonce } : {},
|
|
2837
|
+
...extra
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
async fetch(request) {
|
|
2841
|
+
const url = new URL(request.url);
|
|
2842
|
+
const issuer = this.issuer(request);
|
|
2843
|
+
const mount = new URL(issuer).pathname.replace(/\/$/, "");
|
|
2844
|
+
const path = mount && url.pathname.startsWith(`${mount}/`) ? url.pathname.slice(mount.length) : url.pathname;
|
|
2845
|
+
const paths = this.paths();
|
|
2846
|
+
if (request.method === "GET" && path === "/.well-known/openid-configuration" && this.provider !== "github")
|
|
2847
|
+
return json4({
|
|
2848
|
+
issuer,
|
|
2849
|
+
authorization_endpoint: issuer + paths.authorize,
|
|
2850
|
+
token_endpoint: issuer + paths.token,
|
|
2851
|
+
jwks_uri: issuer + paths.jwks,
|
|
2852
|
+
...this.provider !== "apple" ? { userinfo_endpoint: issuer + paths.userinfo } : {},
|
|
2853
|
+
...this.provider !== "microsoft" ? { revocation_endpoint: issuer + paths.revoke } : {},
|
|
2854
|
+
response_types_supported: this.provider === "apple" ? ["code", "code id_token"] : ["code"],
|
|
2855
|
+
response_modes_supported: ["query", "form_post", "fragment"],
|
|
2856
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2857
|
+
subject_types_supported: [
|
|
2858
|
+
this.provider === "apple" || this.provider === "microsoft" ? "pairwise" : "public"
|
|
2859
|
+
],
|
|
2860
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
2861
|
+
token_endpoint_auth_methods_supported: this.provider === "apple" ? ["client_secret_post"] : this.provider === "oidc" ? ["client_secret_post", "client_secret_basic", "none"] : ["client_secret_post", "client_secret_basic"],
|
|
2862
|
+
...this.provider !== "apple" ? { code_challenge_methods_supported: ["S256"] } : {},
|
|
2863
|
+
scopes_supported: this.supportedScopes(),
|
|
2864
|
+
claims_supported: this.provider === "apple" ? [
|
|
2865
|
+
"aud",
|
|
2866
|
+
"email",
|
|
2867
|
+
"email_verified",
|
|
2868
|
+
"exp",
|
|
2869
|
+
"iat",
|
|
2870
|
+
"is_private_email",
|
|
2871
|
+
"iss",
|
|
2872
|
+
"nonce",
|
|
2873
|
+
"real_user_status",
|
|
2874
|
+
"sub",
|
|
2875
|
+
"transfer_sub"
|
|
2876
|
+
] : this.provider === "microsoft" ? [
|
|
2877
|
+
"aud",
|
|
2878
|
+
"auth_time",
|
|
2879
|
+
"email",
|
|
2880
|
+
"exp",
|
|
2881
|
+
"family_name",
|
|
2882
|
+
"given_name",
|
|
2883
|
+
"iat",
|
|
2884
|
+
"iss",
|
|
2885
|
+
"name",
|
|
2886
|
+
"nonce",
|
|
2887
|
+
"oid",
|
|
2888
|
+
"preferred_username",
|
|
2889
|
+
"sub",
|
|
2890
|
+
"tid",
|
|
2891
|
+
"ver"
|
|
2892
|
+
] : [
|
|
2893
|
+
"aud",
|
|
2894
|
+
"auth_time",
|
|
2895
|
+
"email",
|
|
2896
|
+
"email_verified",
|
|
2897
|
+
"exp",
|
|
2898
|
+
"family_name",
|
|
2899
|
+
"given_name",
|
|
2900
|
+
"iat",
|
|
2901
|
+
"iss",
|
|
2902
|
+
"locale",
|
|
2903
|
+
"name",
|
|
2904
|
+
"nonce",
|
|
2905
|
+
"picture",
|
|
2906
|
+
"sub",
|
|
2907
|
+
...this.provider === "google" ? ["hd"] : []
|
|
2908
|
+
]
|
|
2909
|
+
});
|
|
2910
|
+
if (request.method === "GET" && [paths.jwks, "/jwks"].includes(path))
|
|
2911
|
+
return json4({
|
|
2912
|
+
keys: (await Promise.all([this.signer, ...this.previousSigners].map((s) => s.jwks()))).flatMap((j) => j.keys)
|
|
2913
|
+
});
|
|
2914
|
+
if (request.method === "GET" && [
|
|
2915
|
+
paths.authorize,
|
|
2916
|
+
"/authorize",
|
|
2917
|
+
...this.provider === "google" ? ["/o/oauth2/auth"] : []
|
|
2918
|
+
].includes(path))
|
|
2919
|
+
return this.authorize(request, url.searchParams, issuer);
|
|
2920
|
+
if (path === "/interaction" && ["GET", "POST"].includes(request.method))
|
|
2921
|
+
return this.interact(request, issuer);
|
|
2922
|
+
if (request.method === "POST" && [paths.token, "/token"].includes(path)) {
|
|
2923
|
+
const response = await this.token(request, issuer);
|
|
2924
|
+
return this.provider === "github" ? this.githubTokenResponse(request, response) : response;
|
|
2925
|
+
}
|
|
2926
|
+
if (["GET", "POST"].includes(request.method) && [
|
|
2927
|
+
paths.userinfo,
|
|
2928
|
+
"/userinfo",
|
|
2929
|
+
...this.provider === "google" ? ["/oauth2/v3/userinfo"] : [],
|
|
2930
|
+
...this.provider === "github" ? ["/user/emails"] : []
|
|
2931
|
+
].includes(path) && this.provider !== "apple") {
|
|
2932
|
+
const token = this.tokens.get(
|
|
2933
|
+
request.headers.get("authorization")?.replace(/^Bearer\s+/i, "") ?? ""
|
|
2934
|
+
);
|
|
2935
|
+
const account = token && this.accounts.get(token.accountId);
|
|
2936
|
+
if (token?.kind !== "access" || token.expires <= this.now() || !account || account.disabled) {
|
|
2937
|
+
const res = this.provider === "github" ? json4(
|
|
2938
|
+
{ message: "Bad credentials", documentation_url: "https://docs.github.com/rest" },
|
|
2939
|
+
401
|
|
2940
|
+
) : fail("invalid_token", "Invalid or expired access token", 401);
|
|
2941
|
+
res.headers.set("www-authenticate", 'Bearer error="invalid_token"');
|
|
2942
|
+
return res;
|
|
2943
|
+
}
|
|
2944
|
+
if (this.provider === "github") {
|
|
2945
|
+
const granted = scopes(token.scope);
|
|
2946
|
+
const scopeHeaders = {
|
|
2947
|
+
"x-oauth-scopes": [...granted].sort().join(", "),
|
|
2948
|
+
"x-accepted-oauth-scopes": path === "/user/emails" ? "user:email" : ""
|
|
2949
|
+
};
|
|
2950
|
+
if (path === "/user/emails") {
|
|
2951
|
+
if (!granted.has("user:email") && !granted.has("user"))
|
|
2952
|
+
return new Response(
|
|
2953
|
+
JSON.stringify({ message: "Resource not accessible by integration" }),
|
|
2954
|
+
{
|
|
2955
|
+
status: 403,
|
|
2956
|
+
headers: { "content-type": "application/json", ...scopeHeaders }
|
|
2957
|
+
}
|
|
2958
|
+
);
|
|
2959
|
+
const emails = token.decisions.omitEmail || account.omitEmail ? [] : account.github?.emails ?? [
|
|
2960
|
+
{
|
|
2961
|
+
email: account.email,
|
|
2962
|
+
primary: true,
|
|
2963
|
+
verified: !token.decisions.unverifiedEmail && (account.emailVerified ?? true),
|
|
2964
|
+
visibility: "private"
|
|
2965
|
+
}
|
|
2966
|
+
];
|
|
2967
|
+
const response2 = json4(emails);
|
|
2968
|
+
for (const [key, value] of Object.entries(scopeHeaders)) response2.headers.set(key, value);
|
|
2969
|
+
return response2;
|
|
2970
|
+
}
|
|
2971
|
+
const response = json4({
|
|
2972
|
+
login: account.github?.login ?? account.id,
|
|
2973
|
+
id: account.github?.id ?? seedFrom(account.id) + 1,
|
|
2974
|
+
node_id: btoa(`User:${account.github?.id ?? seedFrom(account.id) + 1}`),
|
|
2975
|
+
name: token.decisions.omitName || account.omitName ? null : account.name,
|
|
2976
|
+
email: token.decisions.omitEmail || account.omitEmail ? null : account.github?.publicEmail ?? null,
|
|
2977
|
+
avatar_url: account.picture ?? "",
|
|
2978
|
+
type: "User"
|
|
2979
|
+
});
|
|
2980
|
+
for (const [key, value] of Object.entries(scopeHeaders)) response.headers.set(key, value);
|
|
2981
|
+
return response;
|
|
2982
|
+
}
|
|
2983
|
+
return json4(this.profile(account, token));
|
|
2984
|
+
}
|
|
2985
|
+
if (request.method === "POST" && [paths.revoke, "/revoke"].includes(path)) {
|
|
2986
|
+
const data = await this.form(request);
|
|
2987
|
+
if (!data) return fail("invalid_request", "Expected form-encoded body");
|
|
2988
|
+
const client = await this.authenticate(request, data);
|
|
2989
|
+
if (client instanceof Response) return client;
|
|
2990
|
+
const token = this.tokens.get(data.get("token") ?? "");
|
|
2991
|
+
if (token && token.clientId === client.id)
|
|
2992
|
+
for (const row of this.tokens.list({ where: (t) => t.family === token.family }))
|
|
2993
|
+
this.tokens.delete(row.id);
|
|
2994
|
+
return json4({});
|
|
2995
|
+
}
|
|
2996
|
+
if (request.method === "GET" && path === "/")
|
|
2997
|
+
return page(
|
|
2998
|
+
"Identity sandbox",
|
|
2999
|
+
'<span class="eyebrow">Mockingbird Identity</span><h1 id="title">Make sign-in<br>feel real.</h1><p>Your identity sandbox is ready. Start sign-in from your application to choose an account, create a new identity, and review access.</p><div class="account"><span class="avatar" aria-hidden="true">\u2713</span><span class="identity"><strong>Ready when you are</strong><small>OAuth 2.0 \xB7 OpenID Connect</small></span></div>'
|
|
3000
|
+
);
|
|
3001
|
+
return fail("not_found", "Unknown endpoint", 404);
|
|
3002
|
+
}
|
|
3003
|
+
async form(request) {
|
|
3004
|
+
if (!request.headers.get("content-type")?.startsWith("application/x-www-form-urlencoded"))
|
|
3005
|
+
return void 0;
|
|
3006
|
+
const body = await request.text();
|
|
3007
|
+
if (body.length > 16384) return void 0;
|
|
3008
|
+
const params = new URLSearchParams(body);
|
|
3009
|
+
if ([...params.keys()].some((k) => params.getAll(k).length !== 1)) return void 0;
|
|
3010
|
+
return params;
|
|
3011
|
+
}
|
|
3012
|
+
async authenticate(request, data) {
|
|
3013
|
+
let id = data.get("client_id") ?? "", secret = data.get("client_secret") ?? "";
|
|
3014
|
+
const auth = request.headers.get("authorization");
|
|
3015
|
+
if (auth) {
|
|
3016
|
+
if (this.provider === "apple")
|
|
3017
|
+
return fail("invalid_client", "Apple requires client_secret_post", 401);
|
|
3018
|
+
if (!auth.startsWith("Basic ") || data.has("client_secret"))
|
|
3019
|
+
return fail("invalid_client", "Invalid client authentication", 401);
|
|
3020
|
+
try {
|
|
3021
|
+
const decoded = atob(auth.slice(6));
|
|
3022
|
+
const colon = decoded.indexOf(":");
|
|
3023
|
+
if (colon < 0) throw new Error();
|
|
3024
|
+
id = decodeURIComponent(decoded.slice(0, colon).replace(/\+/g, " "));
|
|
3025
|
+
secret = decodeURIComponent(decoded.slice(colon + 1).replace(/\+/g, " "));
|
|
3026
|
+
} catch {
|
|
3027
|
+
return fail("invalid_client", "Malformed client authentication", 401);
|
|
3028
|
+
}
|
|
3029
|
+
if (data.has("client_id") && data.get("client_id") !== id)
|
|
3030
|
+
return fail("invalid_client", "Conflicting client identities", 401);
|
|
3031
|
+
}
|
|
3032
|
+
const client = this.clients.get(id);
|
|
3033
|
+
if (!client || client.secret !== void 0 && client.secret !== secret || client.secret === void 0 && !client.apple && secret)
|
|
3034
|
+
return fail("invalid_client", "Client authentication failed", 401);
|
|
3035
|
+
if (client.apple && !await verifyAppleSecret(secret, client.id, client.apple, this.now()))
|
|
3036
|
+
return fail("invalid_client", "Invalid Apple client-secret JWT", 401);
|
|
3037
|
+
return client;
|
|
3038
|
+
}
|
|
3039
|
+
async authorize(request, p, issuer) {
|
|
3040
|
+
if ([...p.keys()].some((k) => p.getAll(k).length !== 1))
|
|
3041
|
+
return fail("invalid_request", "Duplicate parameters");
|
|
3042
|
+
const client = this.clients.get(p.get("client_id") ?? "");
|
|
3043
|
+
const redirectUri = p.get("redirect_uri") ?? (this.provider === "github" ? client?.redirectUris[0] ?? "" : "");
|
|
3044
|
+
if (!client?.redirectUris.includes(redirectUri))
|
|
3045
|
+
return fail("invalid_request", "Unknown client or redirect_uri mismatch");
|
|
3046
|
+
const mode = p.get("response_mode") ?? "query";
|
|
3047
|
+
const auth = {
|
|
3048
|
+
clientId: client.id,
|
|
3049
|
+
redirectUri,
|
|
3050
|
+
scope: p.get("scope") ?? (this.provider === "github" ? "read:user" : this.provider === "apple" ? "openid" : ""),
|
|
3051
|
+
state: p.get("state") ?? "",
|
|
3052
|
+
nonce: p.get("nonce") ?? "",
|
|
3053
|
+
responseType: p.get("response_type") ?? (this.provider === "github" ? "code" : ""),
|
|
3054
|
+
responseMode: ["query", "form_post", "fragment"].includes(mode) ? mode : "query",
|
|
3055
|
+
challenge: p.get("code_challenge") ?? "",
|
|
3056
|
+
expires: this.now() + 6e5,
|
|
3057
|
+
offline: p.get("access_type") === "offline",
|
|
3058
|
+
forceConsent: scopes(p.get("prompt") ?? "").has("consent"),
|
|
3059
|
+
includeGrantedScopes: p.get("include_granted_scopes") === "true",
|
|
3060
|
+
decisions: this.behavior.decisions(),
|
|
3061
|
+
authTime: this.now()
|
|
3062
|
+
};
|
|
3063
|
+
const error = (code, message) => this.callback(auth, { error: code, error_description: message });
|
|
3064
|
+
if (!["query", "form_post", "fragment"].includes(mode))
|
|
3065
|
+
return error("invalid_request", "Unsupported response_mode");
|
|
3066
|
+
if (auth.responseType !== "code" && !(this.provider === "apple" && auth.responseType === "code id_token"))
|
|
3067
|
+
return error("unsupported_response_type", "Unsupported response_type");
|
|
3068
|
+
if (auth.responseType.includes("id_token") && (!auth.nonce || mode === "query"))
|
|
3069
|
+
return error("invalid_request", "Hybrid flow requires nonce and form_post or fragment");
|
|
3070
|
+
if (this.provider === "apple" && /\b(email|name)\b/.test(auth.scope) && mode !== "form_post")
|
|
3071
|
+
return error("invalid_request", "Apple name/email scopes require form_post");
|
|
3072
|
+
const supported = this.supportedScopes();
|
|
3073
|
+
if (!auth.scope || [...scopes(auth.scope)].some((s) => !supported.includes(s)))
|
|
3074
|
+
return error("invalid_scope", "Unsupported scope");
|
|
3075
|
+
if ((client.requirePkce || client.secret === void 0 && !client.apple) && !auth.challenge)
|
|
3076
|
+
return error("invalid_request", "PKCE is required for this client");
|
|
3077
|
+
if (auth.challenge && (p.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(auth.challenge)))
|
|
3078
|
+
return error("invalid_request", "PKCE requires a valid S256 challenge");
|
|
3079
|
+
const prompt = scopes(p.get("prompt") ?? "");
|
|
3080
|
+
if ([...prompt].some((s) => !["none", "login", "consent", "select_account"].includes(s)) || prompt.has("none") && prompt.size > 1)
|
|
3081
|
+
return error("invalid_request", "Invalid prompt");
|
|
3082
|
+
const maxAge = p.get("max_age");
|
|
3083
|
+
if (maxAge !== null && !/^\d+$/.test(maxAge)) return error("invalid_request", "Invalid max_age");
|
|
3084
|
+
const cookie = request.headers.get(this.options.cookieHeaders?.request ?? "cookie")?.split(";").map((v) => v.trim()).find((v) => v.startsWith("mb_session="))?.slice(11);
|
|
3085
|
+
const session = cookie ? this.sessions.get(cookie) : void 0;
|
|
3086
|
+
const account = session && session.expires > this.now() ? this.accounts.get(session.accountId) : void 0;
|
|
3087
|
+
const current = this.behavior.config.session?.reuseLastAccount !== false && account && !account.disabled && session && maxAge !== "0" && (maxAge === null || this.now() - session.authTime <= Number(maxAge) * 1e3) ? account : void 0;
|
|
3088
|
+
const consent = current ? this.consents.get(JSON.stringify([client.id, current.id])) : void 0;
|
|
3089
|
+
const granted = consent && [...scopes(auth.scope)].every((s) => scopes(consent.scope).has(s));
|
|
3090
|
+
if (prompt.has("none")) {
|
|
3091
|
+
if (!current || !session) return error("login_required", "An active session is required");
|
|
3092
|
+
if (!granted) return error("consent_required", "Consent is required");
|
|
3093
|
+
return this.finish({ ...auth, accountId: current.id, authTime: session.authTime }, issuer);
|
|
3094
|
+
}
|
|
3095
|
+
const id = random();
|
|
3096
|
+
if (current && session && !prompt.has("login") && !prompt.has("select_account")) {
|
|
3097
|
+
auth.accountId = current.id;
|
|
3098
|
+
auth.authTime = session.authTime;
|
|
3099
|
+
if (granted && !prompt.has("consent"))
|
|
3100
|
+
return this.finish(auth, issuer);
|
|
3101
|
+
}
|
|
3102
|
+
this.transactions.insert(id, auth);
|
|
3103
|
+
if (auth.accountId && current) return this.consent(id, client, current, auth, issuer);
|
|
3104
|
+
const accounts = this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
|
|
3105
|
+
const hint = p.get("login_hint");
|
|
3106
|
+
if (hint)
|
|
3107
|
+
accounts.sort(
|
|
3108
|
+
(a, b) => Number(b.email === hint || b.id === hint) - Number(a.email === hint || a.id === hint)
|
|
3109
|
+
);
|
|
3110
|
+
return loginPage(id, client.name, accounts, issuer);
|
|
3111
|
+
}
|
|
3112
|
+
consent(id, client, account, auth, issuer) {
|
|
3113
|
+
const existing = this.identities.get(this.identityKey(client, account.id));
|
|
3114
|
+
const denied = new Set(this.behavior.config.consent?.deniedScopes ?? []);
|
|
3115
|
+
const allowed = [...scopes(auth.scope)].filter((s) => !denied.has(s)).join(" ");
|
|
3116
|
+
return consentPage(
|
|
3117
|
+
id,
|
|
3118
|
+
client.name,
|
|
3119
|
+
account,
|
|
3120
|
+
allowed,
|
|
3121
|
+
issuer,
|
|
3122
|
+
this.provider === "apple" && scopes(auth.scope).has("email") && !existing ? {
|
|
3123
|
+
hideEmail: account.privateEmail ?? (this.behavior.config.apple?.emailMode === "hide" || (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && auth.decisions.hideEmail),
|
|
3124
|
+
choice: (this.behavior.config.apple?.emailMode ?? "choose") === "choose"
|
|
3125
|
+
} : void 0
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
async interact(request, issuer) {
|
|
3129
|
+
const p = request.method === "GET" ? new URL(request.url).searchParams : await this.form(request);
|
|
3130
|
+
if (!p) return fail("invalid_request", "Expected form-encoded body");
|
|
3131
|
+
if (request.method === "POST" && request.headers.get("origin") && request.headers.get("origin") !== new URL(issuer).origin)
|
|
3132
|
+
return fail("invalid_request", "Cross-origin interaction rejected", 403);
|
|
3133
|
+
const id = p.get("transaction") ?? "";
|
|
3134
|
+
const auth = this.transactions.get(id);
|
|
3135
|
+
const client = auth && this.clients.get(auth.clientId);
|
|
3136
|
+
if (!auth || auth.expires <= this.now() || !client)
|
|
3137
|
+
return page(
|
|
3138
|
+
"Session expired",
|
|
3139
|
+
'<span class="eyebrow">Let\u2019s try again</span><h1 id="title">This sign-in expired</h1><p>Return to your application and start sign-in again.</p>',
|
|
3140
|
+
400
|
|
3141
|
+
);
|
|
3142
|
+
const accounts = () => this.accounts.list({ order: "oldest", where: (a) => !a.disabled }).map((a) => a.value);
|
|
3143
|
+
if (request.method === "GET")
|
|
3144
|
+
return loginPage(id, client.name, accounts(), issuer, p.get("screen") === "signup");
|
|
3145
|
+
const action = p.get("action");
|
|
3146
|
+
if (action === "deny") {
|
|
3147
|
+
this.transactions.delete(id);
|
|
3148
|
+
return this.callback(auth, {
|
|
3149
|
+
error: "access_denied",
|
|
3150
|
+
error_description: "The user denied access"
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
if (action === "signup") {
|
|
3154
|
+
const email = (p.get("email") ?? "").trim().toLowerCase(), name = (p.get("name") ?? "").trim();
|
|
3155
|
+
if (!validEmail(email) || !name || name.length > 120)
|
|
3156
|
+
return loginPage(
|
|
3157
|
+
id,
|
|
3158
|
+
client.name,
|
|
3159
|
+
[],
|
|
3160
|
+
issuer,
|
|
3161
|
+
true,
|
|
3162
|
+
"Enter a full name and a valid email address."
|
|
3163
|
+
);
|
|
3164
|
+
if (accounts().some((a) => a.email === email) || this.accounts.list().some((a) => a.value.email === email))
|
|
3165
|
+
return loginPage(
|
|
3166
|
+
id,
|
|
3167
|
+
client.name,
|
|
3168
|
+
[],
|
|
3169
|
+
issuer,
|
|
3170
|
+
true,
|
|
3171
|
+
"This email already has an account. Go back to choose it."
|
|
3172
|
+
);
|
|
3173
|
+
const account = this.seedAccount({ id: random(), email, name, emailVerified: true });
|
|
3174
|
+
auth.accountId = account.id;
|
|
3175
|
+
auth.authTime = this.now();
|
|
3176
|
+
this.transactions.update(id, auth);
|
|
3177
|
+
return this.consent(id, client, account, auth, issuer);
|
|
3178
|
+
}
|
|
3179
|
+
if (action === "select") {
|
|
3180
|
+
const account = this.accounts.get(p.get("account") ?? "");
|
|
3181
|
+
if (!account || account.disabled)
|
|
3182
|
+
return loginPage(id, client.name, accounts(), issuer, false, "Choose an available account.");
|
|
3183
|
+
auth.accountId = account.id;
|
|
3184
|
+
auth.authTime = this.now();
|
|
3185
|
+
this.transactions.update(id, auth);
|
|
3186
|
+
return this.consent(id, client, account, auth, issuer);
|
|
3187
|
+
}
|
|
3188
|
+
if (action === "allow" && auth.accountId) {
|
|
3189
|
+
const account = this.accounts.get(auth.accountId);
|
|
3190
|
+
if (!account || account.disabled) return fail("access_denied", "Account is unavailable");
|
|
3191
|
+
const choice = p.get("email_choice");
|
|
3192
|
+
if (choice !== null && choice !== "hide" && choice !== "share")
|
|
3193
|
+
return fail("invalid_request", "Invalid email choice");
|
|
3194
|
+
if (choice && this.provider === "apple" && (this.behavior.config.apple?.emailMode ?? "choose") === "choose" && !this.identities.has(this.identityKey(client, account.id)))
|
|
3195
|
+
auth.emailChoice = choice;
|
|
3196
|
+
this.transactions.delete(id);
|
|
3197
|
+
const result = await this.finish(auth, issuer);
|
|
3198
|
+
const session = random();
|
|
3199
|
+
this.sessions.insert(session, {
|
|
3200
|
+
accountId: account.id,
|
|
3201
|
+
authTime: auth.authTime,
|
|
3202
|
+
expires: this.now() + 864e5
|
|
3203
|
+
});
|
|
3204
|
+
result.headers.append(
|
|
3205
|
+
this.options.cookieHeaders?.response ?? "set-cookie",
|
|
3206
|
+
`mb_session=${session}; Path=${new URL(issuer).pathname.replace(/\/$/, "") || "/"}; HttpOnly; SameSite=Lax; Max-Age=86400${issuer.startsWith("https:") ? "; Secure" : ""}`
|
|
3207
|
+
);
|
|
3208
|
+
return result;
|
|
3209
|
+
}
|
|
3210
|
+
return fail("invalid_request", "Invalid interaction action");
|
|
3211
|
+
}
|
|
3212
|
+
async finish(auth, issuer) {
|
|
3213
|
+
const behavior = this.behavior.config;
|
|
3214
|
+
if (auth.decisions.denyConsent || behavior.consent?.error)
|
|
3215
|
+
return this.callback(auth, {
|
|
3216
|
+
error: behavior.consent?.error ?? "access_denied",
|
|
3217
|
+
error_description: "The authorization request was declined"
|
|
3218
|
+
});
|
|
3219
|
+
const account = this.accounts.get(auth.accountId);
|
|
3220
|
+
if (!account || account.disabled)
|
|
3221
|
+
return this.callback(auth, {
|
|
3222
|
+
error: "access_denied",
|
|
3223
|
+
error_description: "Account is unavailable"
|
|
3224
|
+
});
|
|
3225
|
+
const identity = await this.identity(account, auth);
|
|
3226
|
+
const key = JSON.stringify([auth.clientId, auth.accountId]);
|
|
3227
|
+
const previous = this.consents.get(key);
|
|
3228
|
+
const denied = new Set(behavior.consent?.deniedScopes ?? []);
|
|
3229
|
+
const scope = [
|
|
3230
|
+
...scopes(
|
|
3231
|
+
`${this.provider === "google" && auth.includeGrantedScopes ? previous?.scope ?? "" : ""} ${auth.scope}`
|
|
3232
|
+
)
|
|
3233
|
+
].filter((s) => !denied.has(s)).join(" ");
|
|
3234
|
+
if (!scope || scopes(auth.scope).has("openid") && !scopes(scope).has("openid"))
|
|
3235
|
+
return this.callback(auth, {
|
|
3236
|
+
error: "access_denied",
|
|
3237
|
+
error_description: "Required identity scope was declined"
|
|
3238
|
+
});
|
|
3239
|
+
const googleRefresh = behavior.google?.refreshToken ?? "first-consent";
|
|
3240
|
+
const issueRefresh = this.provider === "google" ? auth.offline && googleRefresh !== "never" && (googleRefresh === "always" || auth.forceConsent || !previous) : this.provider === "apple" || scopes(scope).has("offline_access");
|
|
3241
|
+
const code = random();
|
|
3242
|
+
const grant = {
|
|
3243
|
+
...auth,
|
|
3244
|
+
scope,
|
|
3245
|
+
identity,
|
|
3246
|
+
issueRefresh,
|
|
3247
|
+
expires: this.now() + (behavior.tokens?.codeTtlSeconds ?? 300) * 1e3,
|
|
3248
|
+
family: random()
|
|
3249
|
+
};
|
|
3250
|
+
this.codes.insert(code, grant);
|
|
3251
|
+
this.consents.insert(key, { scope: [...scopes(`${previous?.scope ?? ""} ${scope}`)].join(" ") });
|
|
3252
|
+
const values = { code };
|
|
3253
|
+
if (this.provider === "google" || this.provider === "github") values.scope = scope;
|
|
3254
|
+
if (auth.responseType.includes("id_token"))
|
|
3255
|
+
values.id_token = await this.idToken(grant, issuer, { c_hash: await halfHash(code) });
|
|
3256
|
+
const appleClient = this.clients.get(auth.clientId);
|
|
3257
|
+
const appleKey = appleClient ? this.identityKey(appleClient, account.id) : "";
|
|
3258
|
+
const disclosed = this.appleDisclosures.has(appleKey);
|
|
3259
|
+
if (this.provider === "apple") this.appleDisclosures.insert(appleKey, { disclosed: true });
|
|
3260
|
+
if (this.provider === "apple" && !disclosed && !behavior.apple?.omitUser && (scopes(scope).has("name") || scopes(scope).has("email"))) {
|
|
3261
|
+
values.user = JSON.stringify({
|
|
3262
|
+
...scopes(scope).has("name") && !auth.decisions.omitName && !account.omitName ? {
|
|
3263
|
+
name: {
|
|
3264
|
+
firstName: account.givenName ?? account.name.split(" ")[0],
|
|
3265
|
+
lastName: account.familyName ?? account.name.split(" ").slice(1).join(" ")
|
|
3266
|
+
}
|
|
3267
|
+
} : {},
|
|
3268
|
+
...scopes(scope).has("email") && !auth.decisions.omitEmail && !account.omitEmail ? { email: identity.email } : {}
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
return this.callback(auth, values);
|
|
3272
|
+
}
|
|
3273
|
+
callback(auth, values) {
|
|
3274
|
+
if (auth.state) values.state = auth.state;
|
|
3275
|
+
const redirect = new URL(auth.redirectUri);
|
|
3276
|
+
if (auth.responseMode === "form_post") {
|
|
3277
|
+
const nonce = this.options.nonce?.() ?? crypto.randomUUID();
|
|
3278
|
+
const result = page(
|
|
3279
|
+
"Continue to your app",
|
|
3280
|
+
`<span class="eyebrow">All set</span><h1 id="title">Back to your app</h1><p>Your sign-in response is ready.</p><form id="callback" method="post" action="${escapeHtml(auth.redirectUri)}">${Object.entries(
|
|
3281
|
+
values
|
|
3282
|
+
).map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join(
|
|
3283
|
+
""
|
|
3284
|
+
)}<button class="primary">Continue</button></form><script nonce="${nonce}">document.getElementById('callback').submit()</script>`,
|
|
3285
|
+
200,
|
|
3286
|
+
WEB_SCHEMES.has(redirect.protocol) ? redirect.origin : redirect.protocol,
|
|
3287
|
+
nonce
|
|
3288
|
+
);
|
|
3289
|
+
return result;
|
|
3290
|
+
}
|
|
3291
|
+
let location;
|
|
3292
|
+
if (!WEB_SCHEMES.has(redirect.protocol)) {
|
|
3293
|
+
const encoded = new URLSearchParams(values).toString();
|
|
3294
|
+
location = `${auth.redirectUri}${auth.responseMode === "fragment" ? "#" : auth.redirectUri.includes("?") ? "&" : "?"}${encoded}`;
|
|
3295
|
+
} else {
|
|
3296
|
+
if (auth.responseMode === "fragment") redirect.hash = new URLSearchParams(values).toString();
|
|
3297
|
+
else for (const [k, v] of Object.entries(values)) redirect.searchParams.set(k, v);
|
|
3298
|
+
location = redirect.href;
|
|
3299
|
+
}
|
|
3300
|
+
return new Response(null, {
|
|
3301
|
+
status: 302,
|
|
3302
|
+
headers: {
|
|
3303
|
+
location,
|
|
3304
|
+
"cache-control": "no-store",
|
|
3305
|
+
"referrer-policy": "no-referrer"
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
async githubTokenResponse(request, response) {
|
|
3310
|
+
if (!response.headers.get("content-type")?.includes("application/json")) return response;
|
|
3311
|
+
const body = await response.json();
|
|
3312
|
+
if (body.error === "invalid_client") body.error = "incorrect_client_credentials";
|
|
3313
|
+
else if (body.error === "invalid_grant")
|
|
3314
|
+
body.error = String(body.error_description).includes("redirect_uri") ? "redirect_uri_mismatch" : "bad_verification_code";
|
|
3315
|
+
if (body.error && response.status < 500)
|
|
3316
|
+
body.error_uri = `https://docs.github.com/en/apps/oauth-apps/maintaining-oauth-apps/troubleshooting-oauth-app-access-token-request-errors#${String(body.error).replace(/_/g, "-")}`;
|
|
3317
|
+
const status = body.error && response.status < 500 ? 200 : response.status;
|
|
3318
|
+
if (request.headers.get("accept")?.includes("application/json")) return json4(body, status);
|
|
3319
|
+
return new Response(
|
|
3320
|
+
new URLSearchParams(Object.fromEntries(Object.entries(body).map(([k, v]) => [k, String(v)]))),
|
|
3321
|
+
{
|
|
3322
|
+
status,
|
|
3323
|
+
headers: {
|
|
3324
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3325
|
+
"cache-control": "no-store",
|
|
3326
|
+
...response.headers.has("retry-after") ? { "retry-after": response.headers.get("retry-after") ?? "1" } : {}
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
async token(request, issuer) {
|
|
3332
|
+
let p;
|
|
3333
|
+
if (this.provider === "github" && request.headers.get("content-type")?.startsWith("application/json")) {
|
|
3334
|
+
try {
|
|
3335
|
+
const text = await request.text();
|
|
3336
|
+
if (text.length <= 16384) {
|
|
3337
|
+
const body = JSON.parse(text);
|
|
3338
|
+
if (body && typeof body === "object" && !Array.isArray(body) && Object.values(body).every((v) => typeof v === "string"))
|
|
3339
|
+
p = new URLSearchParams(body);
|
|
3340
|
+
}
|
|
3341
|
+
} catch {
|
|
3342
|
+
}
|
|
3343
|
+
} else p = await this.form(request);
|
|
3344
|
+
if (!p)
|
|
3345
|
+
return fail("invalid_request", "Expected form-encoded body without duplicate parameters");
|
|
3346
|
+
const client = await this.authenticate(request, p);
|
|
3347
|
+
if (client instanceof Response) return client;
|
|
3348
|
+
const type = p.get("grant_type") ?? (this.provider === "github" ? "authorization_code" : null);
|
|
3349
|
+
if (type !== "authorization_code" && type !== "refresh_token")
|
|
3350
|
+
return fail("unsupported_grant_type", "Unsupported grant_type");
|
|
3351
|
+
const key = p.get(type === "authorization_code" ? "code" : "refresh_token") ?? "";
|
|
3352
|
+
const stored = type === "authorization_code" ? this.codes.get(key) : this.tokens.get(key);
|
|
3353
|
+
const account = stored && this.accounts.get(stored.accountId);
|
|
3354
|
+
if (!stored || stored.clientId !== client.id || stored.expires <= this.now() || !account || account.disabled || type === "refresh_token" && (!("kind" in stored) || stored.kind !== "refresh"))
|
|
3355
|
+
return fail("invalid_grant", "Invalid, expired or consumed grant");
|
|
3356
|
+
if (type === "refresh_token" && this.provider === "google" && "lastUsed" in stored && typeof stored.lastUsed === "number") {
|
|
3357
|
+
const inactiveUntil = new Date(stored.lastUsed);
|
|
3358
|
+
inactiveUntil.setUTCMonth(inactiveUntil.getUTCMonth() + 6);
|
|
3359
|
+
if (this.now() >= inactiveUntil.getTime()) {
|
|
3360
|
+
this.tokens.delete(key);
|
|
3361
|
+
return fail("invalid_grant", "Refresh token expired after six months of inactivity");
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (this.provider === "github" && (account.emailVerified === false || account.github?.emails?.find((e) => e.primary)?.verified === false || stored.decisions.unverifiedEmail))
|
|
3365
|
+
return fail("unverified_user_email", "The user must have a verified primary email.");
|
|
3366
|
+
const behavior = this.behavior.config;
|
|
3367
|
+
const effects2 = this.behavior.sample("token", ["tokenUnavailable", "invalidGrant"]);
|
|
3368
|
+
if (effects2.tokenUnavailable) {
|
|
3369
|
+
const response = fail(
|
|
3370
|
+
"temporarily_unavailable",
|
|
3371
|
+
"Authorization server temporarily unavailable",
|
|
3372
|
+
503
|
|
3373
|
+
);
|
|
3374
|
+
response.headers.set("retry-after", "1");
|
|
3375
|
+
return response;
|
|
3376
|
+
}
|
|
3377
|
+
if (type === "refresh_token" && (behavior.tokens?.refreshError || effects2.invalidGrant)) {
|
|
3378
|
+
if (behavior.tokens?.refreshError === "invalid_grant" || effects2.invalidGrant)
|
|
3379
|
+
for (const row of this.tokens.list({ where: (t) => t.family === stored.family }))
|
|
3380
|
+
this.tokens.delete(row.id);
|
|
3381
|
+
return json4(
|
|
3382
|
+
{
|
|
3383
|
+
error: "invalid_grant",
|
|
3384
|
+
error_description: "Token has been expired or revoked.",
|
|
3385
|
+
...behavior.tokens?.refreshError === "invalid_rapt" ? { error_subtype: "invalid_rapt" } : {}
|
|
3386
|
+
},
|
|
3387
|
+
400
|
|
3388
|
+
);
|
|
3389
|
+
}
|
|
3390
|
+
if (effects2.invalidGrant) {
|
|
3391
|
+
this.codes.delete(key);
|
|
3392
|
+
return fail("invalid_grant", "Token has been expired or revoked.");
|
|
3393
|
+
}
|
|
3394
|
+
if (type === "refresh_token" && "consumed" in stored && stored.consumed) {
|
|
3395
|
+
for (const row of this.tokens.list({ where: (t) => t.family === stored.family }))
|
|
3396
|
+
this.tokens.delete(row.id);
|
|
3397
|
+
return fail("invalid_grant", "Refresh token reuse detected; token family revoked");
|
|
3398
|
+
}
|
|
3399
|
+
if (type === "authorization_code") {
|
|
3400
|
+
if ((this.provider !== "github" || p.has("redirect_uri")) && p.get("redirect_uri") !== stored.redirectUri)
|
|
3401
|
+
return fail("invalid_grant", "redirect_uri mismatch");
|
|
3402
|
+
if (stored.challenge) {
|
|
3403
|
+
const verifier = p.get("code_verifier") ?? "";
|
|
3404
|
+
if (!/^[A-Za-z0-9._~-]{43,128}$/.test(verifier) || await hash(verifier) !== stored.challenge)
|
|
3405
|
+
return fail("invalid_grant", "PKCE verification failed");
|
|
3406
|
+
}
|
|
3407
|
+
if (!this.codes.delete(key))
|
|
3408
|
+
return fail("invalid_grant", "Authorization code already consumed");
|
|
3409
|
+
}
|
|
3410
|
+
const grant = { ...stored };
|
|
3411
|
+
if (type === "refresh_token" && p.has("scope")) {
|
|
3412
|
+
const requested = p.get("scope") ?? "";
|
|
3413
|
+
if ([...scopes(requested)].some((s) => !scopes(stored.scope).has(s)))
|
|
3414
|
+
return fail("invalid_scope", "Cannot expand granted scope");
|
|
3415
|
+
grant.scope = requested;
|
|
3416
|
+
}
|
|
3417
|
+
if (type === "refresh_token")
|
|
3418
|
+
this.tokens.update(key, { ...stored, kind: "refresh", lastUsed: this.now() });
|
|
3419
|
+
const accessTtl = behavior.tokens?.accessTtlSeconds ?? 3600;
|
|
3420
|
+
const access = random();
|
|
3421
|
+
const result = {
|
|
3422
|
+
access_token: access,
|
|
3423
|
+
token_type: "Bearer",
|
|
3424
|
+
expires_in: accessTtl,
|
|
3425
|
+
scope: grant.scope
|
|
3426
|
+
};
|
|
3427
|
+
if (this.provider === "apple") {
|
|
3428
|
+
result.token_type = "bearer";
|
|
3429
|
+
delete result.scope;
|
|
3430
|
+
}
|
|
3431
|
+
if (this.provider === "microsoft") result.ext_expires_in = accessTtl;
|
|
3432
|
+
if (this.provider === "github") {
|
|
3433
|
+
delete result.expires_in;
|
|
3434
|
+
result.token_type = "bearer";
|
|
3435
|
+
result.scope = [...scopes(grant.scope)].sort().join(",");
|
|
3436
|
+
}
|
|
3437
|
+
const rotating = type === "refresh_token" && behavior.tokens?.refreshRotation === "rotate";
|
|
3438
|
+
const microsoftRefresh = type === "refresh_token" && this.provider === "microsoft";
|
|
3439
|
+
if (rotating) this.tokens.update(key, { ...stored, kind: "refresh", consumed: true });
|
|
3440
|
+
this.tokens.insert(access, {
|
|
3441
|
+
...grant,
|
|
3442
|
+
expires: this.provider === "github" && !behavior.tokens?.accessTtlSeconds ? Number.MAX_SAFE_INTEGER : this.now() + accessTtl * 1e3,
|
|
3443
|
+
kind: "access",
|
|
3444
|
+
consumed: false
|
|
3445
|
+
});
|
|
3446
|
+
const refreshKey = JSON.stringify([grant.clientId, grant.accountId]);
|
|
3447
|
+
const googleEligible = this.provider !== "google" || behavior.google?.refreshToken === "always" || grant.forceConsent || !this.refreshIssued.has(refreshKey);
|
|
3448
|
+
if (type === "authorization_code" && grant.issueRefresh && googleEligible || rotating || microsoftRefresh) {
|
|
3449
|
+
const refresh = random();
|
|
3450
|
+
const basicScopes = [
|
|
3451
|
+
"openid",
|
|
3452
|
+
"email",
|
|
3453
|
+
"profile",
|
|
3454
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
3455
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
3456
|
+
];
|
|
3457
|
+
const testingExpiry = this.provider === "google" && behavior.google?.testing && [...scopes(grant.scope)].some((s) => !basicScopes.includes(s));
|
|
3458
|
+
const refreshTtl = behavior.tokens?.refreshTtlSeconds ?? (testingExpiry ? 604800 : this.provider === "microsoft" ? 7776e3 : 2592e3);
|
|
3459
|
+
const unbounded = behavior.tokens?.refreshTtlSeconds === void 0 && !testingExpiry && (this.provider === "google" || this.provider === "apple");
|
|
3460
|
+
this.tokens.insert(refresh, {
|
|
3461
|
+
...grant,
|
|
3462
|
+
expires: rotating || microsoftRefresh ? stored.expires : unbounded ? Number.MAX_SAFE_INTEGER : this.now() + refreshTtl * 1e3,
|
|
3463
|
+
lastUsed: this.now(),
|
|
3464
|
+
kind: "refresh",
|
|
3465
|
+
consumed: false
|
|
3466
|
+
});
|
|
3467
|
+
if (this.provider === "google") {
|
|
3468
|
+
const outstanding = this.tokens.list({
|
|
3469
|
+
order: "oldest",
|
|
3470
|
+
where: (t) => t.kind === "refresh" && t.accountId === grant.accountId && t.clientId === grant.clientId
|
|
3471
|
+
});
|
|
3472
|
+
for (const row of outstanding.slice(
|
|
3473
|
+
0,
|
|
3474
|
+
Math.max(0, outstanding.length - (behavior.google?.maxRefreshTokens ?? 100))
|
|
3475
|
+
))
|
|
3476
|
+
this.tokens.delete(row.id);
|
|
3477
|
+
}
|
|
3478
|
+
this.refreshIssued.insert(refreshKey, { issued: true });
|
|
3479
|
+
result.refresh_token = refresh;
|
|
3480
|
+
if (testingExpiry) result.refresh_token_expires_in = refreshTtl;
|
|
3481
|
+
}
|
|
3482
|
+
if (this.provider !== "github" && (scopes(grant.scope).has("openid") || this.provider === "apple"))
|
|
3483
|
+
result.id_token = await this.idToken(grant, issuer, { at_hash: await halfHash(access) });
|
|
3484
|
+
if (this.provider === "github" && !request.headers.get("accept")?.includes("application/json"))
|
|
3485
|
+
return new Response(
|
|
3486
|
+
new URLSearchParams(
|
|
3487
|
+
Object.fromEntries(Object.entries(result).map(([k, v]) => [k, String(v)]))
|
|
3488
|
+
),
|
|
3489
|
+
{
|
|
3490
|
+
headers: {
|
|
3491
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3492
|
+
"cache-control": "no-store"
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
);
|
|
3496
|
+
return json4(result);
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
|
|
3500
|
+
export {
|
|
3501
|
+
OAUTH_SCENARIOS,
|
|
3502
|
+
document,
|
|
3503
|
+
operationIds,
|
|
3504
|
+
supportedOperationIds,
|
|
3505
|
+
OAUTH_PRESETS,
|
|
3506
|
+
createRuntime2 as createRuntime,
|
|
3507
|
+
createMultiRuntime,
|
|
3508
|
+
OAuthAPI
|
|
3509
|
+
};
|
|
3510
|
+
//# sourceMappingURL=chunk-X6FYBOEA.js.map
|