@crvouga/mockingbird-service-posthog 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 +169 -0
- package/dist/chunk-A34UTVQR.js +356 -0
- package/dist/chunk-A34UTVQR.js.map +7 -0
- package/dist/chunk-KO6LMI55.js +2945 -0
- package/dist/chunk-KO6LMI55.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1038 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1319 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +91 -0
|
@@ -0,0 +1,2945 @@
|
|
|
1
|
+
// ../core/dist/clock.js
|
|
2
|
+
var createClock = (source = Date.now) => {
|
|
3
|
+
let offsetMs = 0;
|
|
4
|
+
let frozenAt;
|
|
5
|
+
const now = () => frozenAt ?? source() + offsetMs;
|
|
6
|
+
return {
|
|
7
|
+
now,
|
|
8
|
+
set: (epochMs) => {
|
|
9
|
+
if (frozenAt !== void 0)
|
|
10
|
+
frozenAt = epochMs;
|
|
11
|
+
else
|
|
12
|
+
offsetMs = epochMs - source();
|
|
13
|
+
},
|
|
14
|
+
advance: (deltaMs) => {
|
|
15
|
+
if (frozenAt !== void 0)
|
|
16
|
+
frozenAt += deltaMs;
|
|
17
|
+
else
|
|
18
|
+
offsetMs += deltaMs;
|
|
19
|
+
},
|
|
20
|
+
freeze: () => {
|
|
21
|
+
frozenAt = now();
|
|
22
|
+
},
|
|
23
|
+
unfreeze: () => {
|
|
24
|
+
if (frozenAt === void 0)
|
|
25
|
+
return;
|
|
26
|
+
offsetMs = frozenAt - source();
|
|
27
|
+
frozenAt = void 0;
|
|
28
|
+
},
|
|
29
|
+
reset: () => {
|
|
30
|
+
offsetMs = 0;
|
|
31
|
+
frozenAt = void 0;
|
|
32
|
+
},
|
|
33
|
+
state: () => ({ now: now(), frozen: frozenAt !== void 0, offsetMs })
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ../core/dist/collection.js
|
|
38
|
+
var Collection = class {
|
|
39
|
+
sqlite;
|
|
40
|
+
namespace;
|
|
41
|
+
name;
|
|
42
|
+
constructor(sqlite, namespace, name) {
|
|
43
|
+
this.sqlite = sqlite;
|
|
44
|
+
this.namespace = namespace;
|
|
45
|
+
this.name = name;
|
|
46
|
+
}
|
|
47
|
+
bumpCollectionSeq() {
|
|
48
|
+
const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.name);
|
|
49
|
+
const next = (row?.value ?? 0) + 1;
|
|
50
|
+
this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'collection', ?)
|
|
51
|
+
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.name, next);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
nextSequence() {
|
|
55
|
+
return this.sqlite.transaction(() => this.bumpCollectionSeq());
|
|
56
|
+
}
|
|
57
|
+
get(id) {
|
|
58
|
+
const row = this.sqlite.prepare("SELECT value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
|
|
59
|
+
if (!row)
|
|
60
|
+
return void 0;
|
|
61
|
+
return JSON.parse(row.value).value;
|
|
62
|
+
}
|
|
63
|
+
has(id) {
|
|
64
|
+
const row = this.sqlite.prepare("SELECT 1 AS ok FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
|
|
65
|
+
return row !== void 0;
|
|
66
|
+
}
|
|
67
|
+
/** Insert a new record, assigning it the next sequence number. */
|
|
68
|
+
insert(id, value) {
|
|
69
|
+
return this.sqlite.transaction(() => {
|
|
70
|
+
const seq = this.bumpCollectionSeq();
|
|
71
|
+
const stored = { seq, value };
|
|
72
|
+
this.sqlite.prepare(`INSERT INTO mockingbird_records (namespace, collection, id, seq, value)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?)
|
|
74
|
+
ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.name, id, seq, JSON.stringify(stored));
|
|
75
|
+
return stored;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Replace an existing record's value, keeping its position. */
|
|
79
|
+
update(id, value) {
|
|
80
|
+
return this.sqlite.transaction(() => {
|
|
81
|
+
const row = this.sqlite.prepare("SELECT seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.name, id);
|
|
82
|
+
if (!row)
|
|
83
|
+
return void 0;
|
|
84
|
+
const stored = { seq: row.seq, value };
|
|
85
|
+
this.sqlite.prepare("UPDATE mockingbird_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.name, id);
|
|
86
|
+
return stored;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
delete(id) {
|
|
90
|
+
const result = this.sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.name, id);
|
|
91
|
+
return result.changes > 0;
|
|
92
|
+
}
|
|
93
|
+
/** How many records the collection holds, without reading them. */
|
|
94
|
+
count() {
|
|
95
|
+
const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM mockingbird_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.name);
|
|
96
|
+
return Number(row?.n ?? 0);
|
|
97
|
+
}
|
|
98
|
+
list(options = {}) {
|
|
99
|
+
const rows = this.sqlite.prepare("SELECT id, seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.name);
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const stored = JSON.parse(row.value);
|
|
103
|
+
if (options.where && !options.where(stored.value, stored.seq))
|
|
104
|
+
continue;
|
|
105
|
+
out.push({ id: row.id, seq: stored.seq, value: stored.value });
|
|
106
|
+
}
|
|
107
|
+
out.sort((a, b) => options.order === "oldest" ? a.seq - b.seq : b.seq - a.seq);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ../core/dist/control.js
|
|
113
|
+
var HEALTH_PATH = "/health";
|
|
114
|
+
var ADMIN_PREFIX = "/__admin";
|
|
115
|
+
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
|
+
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
|
+
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
|
+
status,
|
|
119
|
+
headers: { "content-type": "application/json" }
|
|
120
|
+
});
|
|
121
|
+
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
|
+
var UNITS = {
|
|
123
|
+
ms: 1,
|
|
124
|
+
s: 1e3,
|
|
125
|
+
m: 6e4,
|
|
126
|
+
h: 36e5,
|
|
127
|
+
d: 864e5
|
|
128
|
+
};
|
|
129
|
+
var parseDuration = (value) => {
|
|
130
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
131
|
+
return value;
|
|
132
|
+
if (typeof value !== "string")
|
|
133
|
+
return void 0;
|
|
134
|
+
const match = /^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/.exec(value.trim());
|
|
135
|
+
if (!match)
|
|
136
|
+
return void 0;
|
|
137
|
+
return Number(match[1]) * UNITS[match[2]];
|
|
138
|
+
};
|
|
139
|
+
var parseInstant = (value) => {
|
|
140
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
141
|
+
return value;
|
|
142
|
+
if (typeof value !== "string")
|
|
143
|
+
return void 0;
|
|
144
|
+
const parsed = Date.parse(value);
|
|
145
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
146
|
+
};
|
|
147
|
+
var matchRoute = (pattern, path) => {
|
|
148
|
+
const want = pattern.split("/").filter(Boolean);
|
|
149
|
+
const have = path.split("/").filter(Boolean);
|
|
150
|
+
if (want.length !== have.length)
|
|
151
|
+
return void 0;
|
|
152
|
+
const params = {};
|
|
153
|
+
for (let i = 0; i < want.length; i++) {
|
|
154
|
+
const segment = want[i];
|
|
155
|
+
const actual = have[i];
|
|
156
|
+
if (segment.startsWith(":"))
|
|
157
|
+
params[segment.slice(1)] = decodeURIComponent(actual);
|
|
158
|
+
else if (segment !== actual)
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
return params;
|
|
162
|
+
};
|
|
163
|
+
var readJson = async (request) => {
|
|
164
|
+
const text = await request.text();
|
|
165
|
+
if (text.trim() === "")
|
|
166
|
+
return void 0;
|
|
167
|
+
return JSON.parse(text);
|
|
168
|
+
};
|
|
169
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
var createControlPlane = (context) => {
|
|
171
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
172
|
+
let snapshotCounter = 0;
|
|
173
|
+
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
|
+
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
175
|
+
const builtin = {
|
|
176
|
+
"GET /": () => json(200, {
|
|
177
|
+
service: context.name,
|
|
178
|
+
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
179
|
+
}),
|
|
180
|
+
"POST /reset": async ({ url, namespace }) => {
|
|
181
|
+
const target = url.searchParams.get("all") === "1" ? "*" : namespace;
|
|
182
|
+
await context.reset(target);
|
|
183
|
+
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
|
+
},
|
|
185
|
+
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
+
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
+
"POST /clock": ({ body }) => {
|
|
188
|
+
if (!isRecord(body))
|
|
189
|
+
return adminError(400, "expected a JSON object");
|
|
190
|
+
if (body.reset === true)
|
|
191
|
+
context.clock.reset();
|
|
192
|
+
if (body.set !== void 0) {
|
|
193
|
+
const instant = parseInstant(body.set);
|
|
194
|
+
if (instant === void 0)
|
|
195
|
+
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
196
|
+
context.clock.set(instant);
|
|
197
|
+
}
|
|
198
|
+
if (body.advance !== void 0) {
|
|
199
|
+
const delta = parseDuration(body.advance);
|
|
200
|
+
if (delta === void 0)
|
|
201
|
+
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
202
|
+
context.clock.advance(delta);
|
|
203
|
+
}
|
|
204
|
+
if (body.freeze === true)
|
|
205
|
+
context.clock.freeze();
|
|
206
|
+
if (body.freeze === false)
|
|
207
|
+
context.clock.unfreeze();
|
|
208
|
+
return json(200, context.clock.state());
|
|
209
|
+
},
|
|
210
|
+
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
|
+
"POST /faults": ({ body, namespace }) => {
|
|
212
|
+
if (isRecord(body) && typeof body.preset === "string") {
|
|
213
|
+
if (!context.applyPreset)
|
|
214
|
+
return adminError(400, `${context.name} has no fault presets`);
|
|
215
|
+
const { preset, ...overrides } = body;
|
|
216
|
+
try {
|
|
217
|
+
return json(201, {
|
|
218
|
+
preset,
|
|
219
|
+
rules: context.applyPreset(preset, namespace, overrides)
|
|
220
|
+
});
|
|
221
|
+
} catch (error2) {
|
|
222
|
+
return adminError(404, error2 instanceof Error ? error2.message : String(error2));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!isRecord(body) || typeof body.status !== "number" && typeof body.delayMs !== "number" && typeof body.latencyMs !== "number" && body.drop !== true && typeof body.effect !== "string") {
|
|
226
|
+
return adminError(400, "a fault needs a numeric status, a delayMs/latencyMs, drop: true, an effect, or a preset");
|
|
227
|
+
}
|
|
228
|
+
const rule = {
|
|
229
|
+
// Scoped to the caller's namespace unless it asks for every one, so one worker's
|
|
230
|
+
// injected failure never lands on another's request.
|
|
231
|
+
namespace,
|
|
232
|
+
...body,
|
|
233
|
+
id: typeof body.id === "string" ? body.id : `fault_${context.faults.list().length + 1}`
|
|
234
|
+
};
|
|
235
|
+
return json(201, context.faults.add(rule));
|
|
236
|
+
},
|
|
237
|
+
"DELETE /faults": ({ url }) => {
|
|
238
|
+
const id = url.searchParams.get("id");
|
|
239
|
+
if (id === null) {
|
|
240
|
+
context.faults.clear();
|
|
241
|
+
return json(200, { status: "ok" });
|
|
242
|
+
}
|
|
243
|
+
return context.faults.remove(id) ? json(200, { status: "ok" }) : adminError(404, `no fault ${id}`);
|
|
244
|
+
},
|
|
245
|
+
"POST /snapshots": ({ namespace }) => {
|
|
246
|
+
const point = context.timeTravel.checkpoint(namespace, "main");
|
|
247
|
+
context.timeTravel.retain(namespace, point.id);
|
|
248
|
+
snapshotCounter++;
|
|
249
|
+
const id = `snap_${snapshotCounter}`;
|
|
250
|
+
snapshots.set(id, { namespace, checkpoint: point.id });
|
|
251
|
+
return json(201, { id, namespace, records: point.records ?? 0 });
|
|
252
|
+
},
|
|
253
|
+
"POST /snapshots/:id/restore": ({ params, namespace }) => {
|
|
254
|
+
const alias = snapshots.get(params.id);
|
|
255
|
+
if (!alias)
|
|
256
|
+
return adminError(404, `no snapshot ${params.id}`);
|
|
257
|
+
if (alias.namespace !== namespace) {
|
|
258
|
+
return adminError(409, `snapshot ${params.id} belongs to namespace ${alias.namespace}`);
|
|
259
|
+
}
|
|
260
|
+
context.timeTravel.checkout(alias.checkpoint, { namespace, branch: "main" });
|
|
261
|
+
return json(200, { status: "ok", id: params.id, namespace });
|
|
262
|
+
},
|
|
263
|
+
"DELETE /snapshots/:id": ({ params }) => {
|
|
264
|
+
const id = params.id;
|
|
265
|
+
const alias = snapshots.get(id);
|
|
266
|
+
if (!alias)
|
|
267
|
+
return adminError(404, `no snapshot ${id}`);
|
|
268
|
+
snapshots.delete(id);
|
|
269
|
+
context.timeTravel.release(alias.namespace, alias.checkpoint);
|
|
270
|
+
return json(200, { status: "ok" });
|
|
271
|
+
},
|
|
272
|
+
"GET /timeline": ({ namespace }) => json(200, context.timeTravel.inspect(namespace)),
|
|
273
|
+
"POST /checkpoints": ({ body, namespace }) => {
|
|
274
|
+
const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
|
|
275
|
+
try {
|
|
276
|
+
return json(201, context.timeTravel.checkpoint(namespace, branch));
|
|
277
|
+
} catch (error2) {
|
|
278
|
+
return adminError(409, error2 instanceof Error ? error2.message : String(error2));
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
"POST /branches/:name": ({ params, body, namespace }) => {
|
|
282
|
+
const at = isRecord(body) && typeof body.at === "string" ? body.at : void 0;
|
|
283
|
+
try {
|
|
284
|
+
return json(201, context.timeTravel.branch(params.name, {
|
|
285
|
+
namespace,
|
|
286
|
+
...at !== void 0 ? { at } : {}
|
|
287
|
+
}));
|
|
288
|
+
} catch (error2) {
|
|
289
|
+
return adminError(409, error2 instanceof Error ? error2.message : String(error2));
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"POST /branches/:name/checkout": ({ params, body, namespace }) => {
|
|
293
|
+
if (!isRecord(body) || typeof body.checkpoint !== "string") {
|
|
294
|
+
return adminError(400, 'expected {"checkpoint":"cp_..."}');
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
context.timeTravel.checkout(body.checkpoint, {
|
|
298
|
+
namespace,
|
|
299
|
+
branch: params.name
|
|
300
|
+
});
|
|
301
|
+
return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
|
|
302
|
+
} catch (error2) {
|
|
303
|
+
return adminError(409, error2 instanceof Error ? error2.message : String(error2));
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"GET /requests": ({ url, namespace }) => {
|
|
307
|
+
const status = url.searchParams.get("status");
|
|
308
|
+
const since = url.searchParams.get("since");
|
|
309
|
+
const limit = url.searchParams.get("limit");
|
|
310
|
+
const sinceMs = since === null ? void 0 : parseInstant(/^\d+$/.test(since) ? Number(since) : since);
|
|
311
|
+
if (since !== null && sinceMs === void 0) {
|
|
312
|
+
return adminError(400, "since: expected epoch ms or ISO-8601");
|
|
313
|
+
}
|
|
314
|
+
if (status !== null && !/^\d{3}$/.test(status))
|
|
315
|
+
return adminError(400, "status: expected an HTTP status");
|
|
316
|
+
if (limit !== null && !/^\d+$/.test(limit))
|
|
317
|
+
return adminError(400, "limit: expected a count");
|
|
318
|
+
const operationId = url.searchParams.get("operationId");
|
|
319
|
+
const everyNamespace = url.searchParams.get("all") === "1";
|
|
320
|
+
return json(200, {
|
|
321
|
+
size: context.journal.size,
|
|
322
|
+
requests: context.journal.list({
|
|
323
|
+
...everyNamespace ? {} : { namespace },
|
|
324
|
+
...operationId !== null ? { operationId } : {},
|
|
325
|
+
...status !== null ? { status: Number(status) } : {},
|
|
326
|
+
...sinceMs !== void 0 ? { since: sinceMs } : {},
|
|
327
|
+
...limit !== null ? { limit: Number(limit) } : {}
|
|
328
|
+
})
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
"DELETE /requests": ({ url, namespace }) => {
|
|
332
|
+
context.journal.clear(url.searchParams.get("all") === "1" ? void 0 : namespace);
|
|
333
|
+
return json(200, { status: "ok" });
|
|
334
|
+
},
|
|
335
|
+
"GET /metrics": () => json(200, context.metrics.report()),
|
|
336
|
+
"DELETE /metrics": () => {
|
|
337
|
+
context.metrics.reset();
|
|
338
|
+
return json(200, { status: "ok" });
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const routes = [...Object.entries(context.routes), ...Object.entries(builtin)].map(([key, handler]) => {
|
|
342
|
+
const space = key.indexOf(" ");
|
|
343
|
+
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
namespaceOf: headerNamespace,
|
|
347
|
+
async handle(request) {
|
|
348
|
+
const url = new URL(request.url);
|
|
349
|
+
if (url.pathname === HEALTH_PATH && request.method === "GET") {
|
|
350
|
+
return json(200, {
|
|
351
|
+
status: "ok",
|
|
352
|
+
service: context.name,
|
|
353
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
+
clock: context.clock.state(),
|
|
355
|
+
namespaces: context.namespaces().length,
|
|
356
|
+
...context.describe()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (url.pathname !== ADMIN_PREFIX && !url.pathname.startsWith(`${ADMIN_PREFIX}/`)) {
|
|
360
|
+
return void 0;
|
|
361
|
+
}
|
|
362
|
+
if (context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
363
|
+
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
364
|
+
}
|
|
365
|
+
const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
|
|
366
|
+
for (const route of routes) {
|
|
367
|
+
if (route.method !== request.method)
|
|
368
|
+
continue;
|
|
369
|
+
const params = matchRoute(route.pattern, path);
|
|
370
|
+
if (!params)
|
|
371
|
+
continue;
|
|
372
|
+
let body;
|
|
373
|
+
try {
|
|
374
|
+
body = await readJson(request);
|
|
375
|
+
} catch {
|
|
376
|
+
return adminError(400, "request body is not valid JSON");
|
|
377
|
+
}
|
|
378
|
+
return route.handler({
|
|
379
|
+
request,
|
|
380
|
+
url,
|
|
381
|
+
params,
|
|
382
|
+
namespace: adminNamespace(request, url),
|
|
383
|
+
body
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${ADMIN_PREFIX} lists them`);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// ../core/dist/credentials.js
|
|
392
|
+
var bearerToken = (request) => {
|
|
393
|
+
const header = request.headers.get("authorization");
|
|
394
|
+
if (!header)
|
|
395
|
+
return void 0;
|
|
396
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
397
|
+
return match?.[1]?.trim() || void 0;
|
|
398
|
+
};
|
|
399
|
+
var createCredentialRegistry = () => {
|
|
400
|
+
const map = /* @__PURE__ */ new Map();
|
|
401
|
+
return {
|
|
402
|
+
set: (credential, namespace) => {
|
|
403
|
+
map.set(credential, namespace);
|
|
404
|
+
},
|
|
405
|
+
get: (credential) => map.get(credential),
|
|
406
|
+
remove: (credential) => map.delete(credential),
|
|
407
|
+
clear: () => map.clear(),
|
|
408
|
+
entries: () => [...map].map(([credential, namespace]) => ({ credential, namespace })).sort((a, b) => a.credential.localeCompare(b.credential))
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
var maskCredential = (credential) => credential.length <= 8 ? `${credential.slice(0, 2)}\u2026` : `${credential.slice(0, 6)}\u2026${credential.slice(-2)}`;
|
|
412
|
+
|
|
413
|
+
// ../core/dist/rng.js
|
|
414
|
+
var seedFrom = (value) => {
|
|
415
|
+
let hash = 2166136261;
|
|
416
|
+
for (let i = 0; i < value.length; i++) {
|
|
417
|
+
hash ^= value.charCodeAt(i);
|
|
418
|
+
hash = Math.imul(hash, 16777619);
|
|
419
|
+
}
|
|
420
|
+
return hash >>> 0;
|
|
421
|
+
};
|
|
422
|
+
var createRng = (seed = 0) => {
|
|
423
|
+
const numeric = typeof seed === "string" ? seedFrom(seed) : seed >>> 0;
|
|
424
|
+
let state = numeric;
|
|
425
|
+
const next = () => {
|
|
426
|
+
state = state + 1831565813 >>> 0;
|
|
427
|
+
let t = state;
|
|
428
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
429
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
430
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
next,
|
|
434
|
+
int: (min, max) => min + Math.floor(next() * (max - min + 1)),
|
|
435
|
+
reset: () => {
|
|
436
|
+
state = numeric;
|
|
437
|
+
},
|
|
438
|
+
state: () => state,
|
|
439
|
+
setState: (next2) => {
|
|
440
|
+
if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
|
|
441
|
+
throw new RangeError("rng state must be an unsigned 32-bit integer");
|
|
442
|
+
}
|
|
443
|
+
state = next2 >>> 0;
|
|
444
|
+
},
|
|
445
|
+
seed: numeric
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// ../core/dist/faults.js
|
|
450
|
+
var matches = (rule, candidate) => {
|
|
451
|
+
if (rule.namespace !== void 0 && rule.namespace !== "*" && rule.namespace !== candidate.namespace) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (rule.operationId !== void 0 && rule.operationId !== candidate.operationId)
|
|
455
|
+
return false;
|
|
456
|
+
if (rule.method !== void 0 && rule.method.toUpperCase() !== candidate.method.toUpperCase()) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (rule.pathPrefix !== void 0 && !candidate.path.startsWith(rule.pathPrefix))
|
|
460
|
+
return false;
|
|
461
|
+
return true;
|
|
462
|
+
};
|
|
463
|
+
var faultResponse = (rule) => {
|
|
464
|
+
const status = rule.status ?? 500;
|
|
465
|
+
const headers = { "content-type": "application/json", ...rule.headers };
|
|
466
|
+
if (typeof rule.body === "string")
|
|
467
|
+
return new Response(rule.body, { status, headers });
|
|
468
|
+
if (rule.body === null)
|
|
469
|
+
return new Response(null, { status, headers: rule.headers ?? {} });
|
|
470
|
+
const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
|
|
471
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
472
|
+
};
|
|
473
|
+
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
474
|
+
const entries = [];
|
|
475
|
+
return {
|
|
476
|
+
add(rule) {
|
|
477
|
+
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
478
|
+
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
479
|
+
if (existing >= 0)
|
|
480
|
+
entries[existing] = entry;
|
|
481
|
+
else
|
|
482
|
+
entries.push(entry);
|
|
483
|
+
return rule;
|
|
484
|
+
},
|
|
485
|
+
list: () => entries.map((e) => ({ ...e.rule, remaining: e.remaining, hits: e.hits })),
|
|
486
|
+
remove(id) {
|
|
487
|
+
const index = entries.findIndex((e) => e.rule.id === id);
|
|
488
|
+
if (index < 0)
|
|
489
|
+
return false;
|
|
490
|
+
entries.splice(index, 1);
|
|
491
|
+
return true;
|
|
492
|
+
},
|
|
493
|
+
clear() {
|
|
494
|
+
entries.length = 0;
|
|
495
|
+
},
|
|
496
|
+
async take(candidate) {
|
|
497
|
+
const hits = [];
|
|
498
|
+
for (const entry of entries) {
|
|
499
|
+
if (entry.remaining === 0)
|
|
500
|
+
continue;
|
|
501
|
+
if (!matches(entry.rule, candidate))
|
|
502
|
+
continue;
|
|
503
|
+
const rate = entry.rule.rate ?? 1;
|
|
504
|
+
if (rng.next() >= rate)
|
|
505
|
+
continue;
|
|
506
|
+
entry.hits++;
|
|
507
|
+
if (entry.remaining !== null)
|
|
508
|
+
entry.remaining--;
|
|
509
|
+
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
510
|
+
if (delay !== void 0 && delay > 0) {
|
|
511
|
+
await sleep(delay);
|
|
512
|
+
}
|
|
513
|
+
const hit = { id: entry.rule.id };
|
|
514
|
+
if (entry.rule.effect !== void 0) {
|
|
515
|
+
hit.effect = { name: entry.rule.effect, params: entry.rule.params ?? {} };
|
|
516
|
+
}
|
|
517
|
+
if (entry.rule.drop === true)
|
|
518
|
+
hit.drop = true;
|
|
519
|
+
else if (entry.rule.status !== void 0)
|
|
520
|
+
hit.response = faultResponse(entry.rule);
|
|
521
|
+
hits.push(hit);
|
|
522
|
+
if (hit.drop || hit.response)
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
return hits;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// ../../openapi/core/dist/refs.js
|
|
531
|
+
var OpenAPIReferenceError = class extends Error {
|
|
532
|
+
ref;
|
|
533
|
+
constructor(ref) {
|
|
534
|
+
super(`unresolvable $ref: ${ref}`);
|
|
535
|
+
this.ref = ref;
|
|
536
|
+
this.name = "OpenAPIReferenceError";
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
540
|
+
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
541
|
+
var resolveRef = (document2, ref) => {
|
|
542
|
+
if (!ref.startsWith("#/"))
|
|
543
|
+
throw new OpenAPIReferenceError(ref);
|
|
544
|
+
let cursor = document2;
|
|
545
|
+
for (const raw of ref.slice(2).split("/")) {
|
|
546
|
+
const segment = unescapePointer(raw);
|
|
547
|
+
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
548
|
+
throw new OpenAPIReferenceError(ref);
|
|
549
|
+
}
|
|
550
|
+
cursor = cursor[segment];
|
|
551
|
+
}
|
|
552
|
+
if (cursor === void 0)
|
|
553
|
+
throw new OpenAPIReferenceError(ref);
|
|
554
|
+
return cursor;
|
|
555
|
+
};
|
|
556
|
+
var deref = (document2, value) => {
|
|
557
|
+
let current = value;
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
while (isReference(current)) {
|
|
560
|
+
if (seen.has(current.$ref))
|
|
561
|
+
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
562
|
+
seen.add(current.$ref);
|
|
563
|
+
current = resolveRef(document2, current.$ref);
|
|
564
|
+
}
|
|
565
|
+
return current;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// ../../openapi/core/dist/types.js
|
|
569
|
+
var HTTP_METHODS = [
|
|
570
|
+
"get",
|
|
571
|
+
"put",
|
|
572
|
+
"post",
|
|
573
|
+
"delete",
|
|
574
|
+
"options",
|
|
575
|
+
"head",
|
|
576
|
+
"patch",
|
|
577
|
+
"trace"
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
// ../../openapi/core/dist/document.js
|
|
581
|
+
var mergeParameters = (document2, item, own) => {
|
|
582
|
+
const merged = /* @__PURE__ */ new Map();
|
|
583
|
+
for (const raw of item.parameters ?? []) {
|
|
584
|
+
const parameter = deref(document2, raw);
|
|
585
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
586
|
+
}
|
|
587
|
+
for (const raw of own ?? []) {
|
|
588
|
+
const parameter = deref(document2, raw);
|
|
589
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
590
|
+
}
|
|
591
|
+
return [...merged.values()];
|
|
592
|
+
};
|
|
593
|
+
var listOperations = (document2) => {
|
|
594
|
+
const operations = [];
|
|
595
|
+
for (const [path, item] of Object.entries(document2.paths)) {
|
|
596
|
+
for (const method of HTTP_METHODS) {
|
|
597
|
+
const operation = item[method];
|
|
598
|
+
if (operation?.operationId === void 0)
|
|
599
|
+
continue;
|
|
600
|
+
const responses = {};
|
|
601
|
+
for (const [status, response] of Object.entries(operation.responses)) {
|
|
602
|
+
responses[status] = deref(document2, response);
|
|
603
|
+
}
|
|
604
|
+
operations.push({
|
|
605
|
+
operationId: operation.operationId,
|
|
606
|
+
method,
|
|
607
|
+
path,
|
|
608
|
+
operation,
|
|
609
|
+
parameters: mergeParameters(document2, item, operation.parameters),
|
|
610
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
|
|
611
|
+
responses
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return operations;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// ../../http/codec/dist/form.js
|
|
619
|
+
var parsePath = (rawKey) => {
|
|
620
|
+
const open = rawKey.indexOf("[");
|
|
621
|
+
if (open === -1)
|
|
622
|
+
return [rawKey];
|
|
623
|
+
const path = [rawKey.slice(0, open)];
|
|
624
|
+
const rest = rawKey.slice(open);
|
|
625
|
+
const pattern = /\[([^\]]*)\]/g;
|
|
626
|
+
let match = pattern.exec(rest);
|
|
627
|
+
let consumed = 0;
|
|
628
|
+
while (match !== null) {
|
|
629
|
+
if (match.index !== consumed)
|
|
630
|
+
return [rawKey];
|
|
631
|
+
path.push(match[1] ?? "");
|
|
632
|
+
consumed = match.index + match[0].length;
|
|
633
|
+
match = pattern.exec(rest);
|
|
634
|
+
}
|
|
635
|
+
if (consumed !== rest.length)
|
|
636
|
+
return [rawKey];
|
|
637
|
+
return path;
|
|
638
|
+
};
|
|
639
|
+
var isIndex = (segment) => /^(0|[1-9][0-9]*)$/.test(segment);
|
|
640
|
+
var put = (target, key, value) => {
|
|
641
|
+
if (key === "__proto__") {
|
|
642
|
+
Object.defineProperty(target, key, {
|
|
643
|
+
value,
|
|
644
|
+
enumerable: true,
|
|
645
|
+
writable: true,
|
|
646
|
+
configurable: true
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
;
|
|
651
|
+
target[key] = value;
|
|
652
|
+
};
|
|
653
|
+
var assign = (target, path, value) => {
|
|
654
|
+
let cursor = target;
|
|
655
|
+
for (let i = 0; i < path.length; i++) {
|
|
656
|
+
const segment = path[i];
|
|
657
|
+
const last = i === path.length - 1;
|
|
658
|
+
if (Array.isArray(cursor)) {
|
|
659
|
+
const index = segment === "" ? cursor.length : isIndex(segment) ? Number(segment) : void 0;
|
|
660
|
+
if (index === void 0)
|
|
661
|
+
return;
|
|
662
|
+
if (last) {
|
|
663
|
+
put(cursor, index, value);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const next = Object.hasOwn(cursor, index) ? cursor[index] : void 0;
|
|
667
|
+
if (next === void 0 || typeof next === "string") {
|
|
668
|
+
const created = path[i + 1] === "" || isIndex(path[i + 1]) ? [] : {};
|
|
669
|
+
put(cursor, index, created);
|
|
670
|
+
cursor = created;
|
|
671
|
+
} else {
|
|
672
|
+
cursor = next;
|
|
673
|
+
}
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (typeof cursor === "string")
|
|
677
|
+
return;
|
|
678
|
+
if (last) {
|
|
679
|
+
put(cursor, segment, value);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const nextSegment = path[i + 1];
|
|
683
|
+
const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : void 0;
|
|
684
|
+
if (existing === void 0 || typeof existing === "string") {
|
|
685
|
+
const created = nextSegment === "" || isIndex(nextSegment) ? [] : {};
|
|
686
|
+
put(cursor, segment, created);
|
|
687
|
+
cursor = created;
|
|
688
|
+
} else {
|
|
689
|
+
cursor = existing;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
var decodeFormPairs = (pairs) => {
|
|
694
|
+
const out = {};
|
|
695
|
+
for (const [rawKey, value] of pairs)
|
|
696
|
+
assign(out, parsePath(rawKey), value);
|
|
697
|
+
return densify(out);
|
|
698
|
+
};
|
|
699
|
+
var densify = (value) => {
|
|
700
|
+
if (typeof value === "string")
|
|
701
|
+
return value;
|
|
702
|
+
if (Array.isArray(value))
|
|
703
|
+
return value.filter((item) => item !== void 0).map(densify);
|
|
704
|
+
const out = {};
|
|
705
|
+
for (const [key, item] of Object.entries(value))
|
|
706
|
+
put(out, key, densify(item));
|
|
707
|
+
return out;
|
|
708
|
+
};
|
|
709
|
+
var decodeForm = (text) => {
|
|
710
|
+
const source = text.startsWith("?") ? text.slice(1) : text;
|
|
711
|
+
return decodeFormPairs(new URLSearchParams(source).entries());
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// ../../http/codec/dist/content.js
|
|
715
|
+
var JSON_MEDIA_TYPE = "application/json";
|
|
716
|
+
var FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
|
|
717
|
+
var mediaTypeOf = (contentType) => {
|
|
718
|
+
if (!contentType)
|
|
719
|
+
return void 0;
|
|
720
|
+
const essence = contentType.split(";")[0]?.trim().toLowerCase();
|
|
721
|
+
return essence ? essence : void 0;
|
|
722
|
+
};
|
|
723
|
+
var isJsonMediaType = (mediaType) => mediaType === JSON_MEDIA_TYPE || mediaType.endsWith("+json") || mediaType === "text/json";
|
|
724
|
+
var utf8 = new TextDecoder("utf-8", { fatal: false });
|
|
725
|
+
var decodeBody = (contentType, bytes) => {
|
|
726
|
+
if (bytes.byteLength === 0)
|
|
727
|
+
return { kind: "empty" };
|
|
728
|
+
const mediaType = mediaTypeOf(contentType);
|
|
729
|
+
if (mediaType === void 0)
|
|
730
|
+
return { kind: "bytes", value: bytes };
|
|
731
|
+
if (isJsonMediaType(mediaType)) {
|
|
732
|
+
const text = utf8.decode(bytes);
|
|
733
|
+
try {
|
|
734
|
+
return { kind: "json", value: JSON.parse(text) };
|
|
735
|
+
} catch (error2) {
|
|
736
|
+
return {
|
|
737
|
+
kind: "invalid",
|
|
738
|
+
mediaType,
|
|
739
|
+
text,
|
|
740
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (mediaType === FORM_MEDIA_TYPE) {
|
|
745
|
+
return { kind: "form", value: decodeForm(utf8.decode(bytes)) };
|
|
746
|
+
}
|
|
747
|
+
if (mediaType.startsWith("text/"))
|
|
748
|
+
return { kind: "text", value: utf8.decode(bytes) };
|
|
749
|
+
return { kind: "bytes", value: bytes };
|
|
750
|
+
};
|
|
751
|
+
var readBody = async (message) => {
|
|
752
|
+
const bytes = new Uint8Array(await message.arrayBuffer());
|
|
753
|
+
return decodeBody(message.headers.get("content-type"), bytes);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// ../core/dist/http.js
|
|
757
|
+
var jsonRes = (status, body, headers = {}) => new Response(JSON.stringify(body), {
|
|
758
|
+
status,
|
|
759
|
+
headers: { "content-type": JSON_MEDIA_TYPE, ...headers }
|
|
760
|
+
});
|
|
761
|
+
var HttpError = class extends Error {
|
|
762
|
+
status;
|
|
763
|
+
body;
|
|
764
|
+
headers;
|
|
765
|
+
constructor(status, body, headers = {}) {
|
|
766
|
+
super(`HTTP ${status}`);
|
|
767
|
+
this.status = status;
|
|
768
|
+
this.body = body;
|
|
769
|
+
this.headers = headers;
|
|
770
|
+
this.name = "HttpError";
|
|
771
|
+
}
|
|
772
|
+
toResponse() {
|
|
773
|
+
const contentType = this.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
|
|
774
|
+
if (contentType === "text/plain") {
|
|
775
|
+
return new Response(String(this.body), {
|
|
776
|
+
status: this.status,
|
|
777
|
+
headers: this.headers
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return jsonRes(this.status, this.body, this.headers);
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// ../core/dist/ids.js
|
|
785
|
+
var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
786
|
+
var mix = (input) => {
|
|
787
|
+
let hash = 2166136261;
|
|
788
|
+
for (let i = 0; i < input.length; i++) {
|
|
789
|
+
hash ^= input.charCodeAt(i);
|
|
790
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
791
|
+
}
|
|
792
|
+
hash ^= hash >>> 16;
|
|
793
|
+
hash = Math.imul(hash, 2246822507) >>> 0;
|
|
794
|
+
hash ^= hash >>> 13;
|
|
795
|
+
return hash >>> 0;
|
|
796
|
+
};
|
|
797
|
+
var opaqueToken = (input, length) => {
|
|
798
|
+
let out = "";
|
|
799
|
+
let round = 0;
|
|
800
|
+
while (out.length < length) {
|
|
801
|
+
let hash = mix(`${input}:${round++}`);
|
|
802
|
+
for (let i = 0; i < 5 && out.length < length; i++) {
|
|
803
|
+
out += ALPHABET.charAt(hash % ALPHABET.length);
|
|
804
|
+
hash = Math.floor(hash / ALPHABET.length);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
};
|
|
809
|
+
var IdSequence = class {
|
|
810
|
+
sqlite;
|
|
811
|
+
namespace;
|
|
812
|
+
salt;
|
|
813
|
+
constructor(sqlite, namespace, salt = "mockingbird") {
|
|
814
|
+
this.sqlite = sqlite;
|
|
815
|
+
this.namespace = namespace;
|
|
816
|
+
this.salt = salt;
|
|
817
|
+
}
|
|
818
|
+
next(prefix, length = 14) {
|
|
819
|
+
return this.sqlite.transaction(() => {
|
|
820
|
+
const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'id'").get(this.namespace, prefix);
|
|
821
|
+
const value = (row?.value ?? 0) + 1;
|
|
822
|
+
this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'id', ?)
|
|
823
|
+
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, prefix, value);
|
|
824
|
+
return `${prefix}${opaqueToken(`${this.salt}:${prefix}:${value}`, length)}`;
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
// ../core/dist/journal.js
|
|
830
|
+
var DEFAULT_JOURNAL_SIZE = 1e3;
|
|
831
|
+
var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
832
|
+
const capacity = Math.max(0, Math.floor(size));
|
|
833
|
+
const rings = /* @__PURE__ */ new Map();
|
|
834
|
+
let sequence = 0;
|
|
835
|
+
const order = /* @__PURE__ */ new WeakMap();
|
|
836
|
+
const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
|
|
837
|
+
return {
|
|
838
|
+
size: capacity,
|
|
839
|
+
record(entry) {
|
|
840
|
+
if (capacity === 0)
|
|
841
|
+
return;
|
|
842
|
+
order.set(entry, sequence++);
|
|
843
|
+
let ring = rings.get(entry.namespace);
|
|
844
|
+
if (!ring) {
|
|
845
|
+
ring = { entries: [], next: 0 };
|
|
846
|
+
rings.set(entry.namespace, ring);
|
|
847
|
+
}
|
|
848
|
+
if (ring.entries.length < capacity)
|
|
849
|
+
ring.entries.push(entry);
|
|
850
|
+
else {
|
|
851
|
+
ring.entries[ring.next] = entry;
|
|
852
|
+
ring.next = (ring.next + 1) % capacity;
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
list(query = {}) {
|
|
856
|
+
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));
|
|
857
|
+
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));
|
|
858
|
+
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
859
|
+
},
|
|
860
|
+
clear(namespace) {
|
|
861
|
+
if (namespace === void 0)
|
|
862
|
+
rings.clear();
|
|
863
|
+
else
|
|
864
|
+
rings.delete(namespace);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
};
|
|
868
|
+
var notes = /* @__PURE__ */ new WeakMap();
|
|
869
|
+
var annotateResponse = (response, extra) => {
|
|
870
|
+
const existing = notes.get(response);
|
|
871
|
+
notes.set(response, {
|
|
872
|
+
...existing,
|
|
873
|
+
...extra,
|
|
874
|
+
...existing?.ids || extra.ids ? { ids: { ...existing?.ids, ...extra.ids } } : {}
|
|
875
|
+
});
|
|
876
|
+
return response;
|
|
877
|
+
};
|
|
878
|
+
var responseNotes = (response) => notes.get(response);
|
|
879
|
+
|
|
880
|
+
// ../core/dist/metrics.js
|
|
881
|
+
var createMetrics = () => {
|
|
882
|
+
let requests = 0;
|
|
883
|
+
let faults = 0;
|
|
884
|
+
let totalDurationMs = 0;
|
|
885
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
886
|
+
const unmatched = /* @__PURE__ */ new Map();
|
|
887
|
+
return {
|
|
888
|
+
record(entry) {
|
|
889
|
+
requests++;
|
|
890
|
+
totalDurationMs += entry.durationMs;
|
|
891
|
+
if (entry.faultId !== void 0)
|
|
892
|
+
faults++;
|
|
893
|
+
const key = `${entry.operationId ?? "(unmatched)"} ${entry.status}`;
|
|
894
|
+
byOperation.set(key, (byOperation.get(key) ?? 0) + 1);
|
|
895
|
+
if (entry.unmatched) {
|
|
896
|
+
const route = `${entry.method} ${entry.path}`;
|
|
897
|
+
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
report: () => ({
|
|
901
|
+
requests,
|
|
902
|
+
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
903
|
+
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
904
|
+
const space = route.indexOf(" ");
|
|
905
|
+
return { method: route.slice(0, space), path: route.slice(space + 1), count };
|
|
906
|
+
}),
|
|
907
|
+
faults,
|
|
908
|
+
totalDurationMs
|
|
909
|
+
}),
|
|
910
|
+
reset() {
|
|
911
|
+
requests = 0;
|
|
912
|
+
faults = 0;
|
|
913
|
+
totalDurationMs = 0;
|
|
914
|
+
byOperation.clear();
|
|
915
|
+
unmatched.clear();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// ../core/dist/outbox.js
|
|
921
|
+
var parseSince = (value) => {
|
|
922
|
+
if (value === null)
|
|
923
|
+
return void 0;
|
|
924
|
+
const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
925
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// ../../core/dist/timeline.js
|
|
929
|
+
var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
930
|
+
var Timeline = class {
|
|
931
|
+
maxCheckpoints;
|
|
932
|
+
now;
|
|
933
|
+
makeId;
|
|
934
|
+
nodes = /* @__PURE__ */ new Map();
|
|
935
|
+
heads = /* @__PURE__ */ new Map();
|
|
936
|
+
/** Unreferenced nodes in the exact order they became collectible. */
|
|
937
|
+
evictable = /* @__PURE__ */ new Set();
|
|
938
|
+
/** Branch heads plus explicit retainers. Absent means zero. */
|
|
939
|
+
references = /* @__PURE__ */ new Map();
|
|
940
|
+
explicitPins = /* @__PURE__ */ new Map();
|
|
941
|
+
sequence = 0;
|
|
942
|
+
constructor(options = {}) {
|
|
943
|
+
const max = options.maxCheckpoints ?? 1e3;
|
|
944
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
945
|
+
throw new RangeError("maxCheckpoints must be a positive integer");
|
|
946
|
+
this.maxCheckpoints = max;
|
|
947
|
+
this.now = options.now ?? (() => this.sequence);
|
|
948
|
+
this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
|
|
949
|
+
}
|
|
950
|
+
/** Capture a new immutable value and move `branch` to it. */
|
|
951
|
+
commit(value, options = {}) {
|
|
952
|
+
const branch = options.branch ?? "main";
|
|
953
|
+
this.assertBranch(branch);
|
|
954
|
+
const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
|
|
955
|
+
if (parent !== null && !this.nodes.has(parent))
|
|
956
|
+
throw new RangeError(`no checkpoint ${parent}`);
|
|
957
|
+
const id = this.makeId(++this.sequence);
|
|
958
|
+
if (this.nodes.has(id))
|
|
959
|
+
throw new RangeError(`duplicate checkpoint id ${id}`);
|
|
960
|
+
const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
|
|
961
|
+
this.nodes.set(id, checkpoint);
|
|
962
|
+
this.moveHead(branch, id);
|
|
963
|
+
this.collect(this.maxCheckpoints);
|
|
964
|
+
return checkpoint;
|
|
965
|
+
}
|
|
966
|
+
/** Create a branch pointer without copying its checkpoint value. */
|
|
967
|
+
fork(branch, options = {}) {
|
|
968
|
+
this.assertBranch(branch);
|
|
969
|
+
if (this.heads.has(branch))
|
|
970
|
+
throw new RangeError(`branch already exists: ${branch}`);
|
|
971
|
+
const from = options.from ?? this.heads.get("main");
|
|
972
|
+
if (from === void 0)
|
|
973
|
+
return void 0;
|
|
974
|
+
const checkpoint = this.get(from);
|
|
975
|
+
this.moveHead(branch, checkpoint.id);
|
|
976
|
+
return checkpoint;
|
|
977
|
+
}
|
|
978
|
+
/** Move a branch pointer to an existing checkpoint. */
|
|
979
|
+
checkout(branch, id) {
|
|
980
|
+
this.assertBranch(branch);
|
|
981
|
+
const checkpoint = this.get(id);
|
|
982
|
+
this.moveHead(branch, checkpoint.id);
|
|
983
|
+
return checkpoint;
|
|
984
|
+
}
|
|
985
|
+
get(id) {
|
|
986
|
+
const checkpoint = this.nodes.get(id);
|
|
987
|
+
if (!checkpoint)
|
|
988
|
+
throw new RangeError(`no checkpoint ${id}`);
|
|
989
|
+
return checkpoint;
|
|
990
|
+
}
|
|
991
|
+
head(branch = "main") {
|
|
992
|
+
const id = this.heads.get(branch);
|
|
993
|
+
return id === void 0 ? void 0 : this.get(id);
|
|
994
|
+
}
|
|
995
|
+
hasBranch(branch) {
|
|
996
|
+
return this.heads.has(branch);
|
|
997
|
+
}
|
|
998
|
+
branches() {
|
|
999
|
+
return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
1000
|
+
}
|
|
1001
|
+
checkpoints() {
|
|
1002
|
+
return [...this.nodes.values()];
|
|
1003
|
+
}
|
|
1004
|
+
/** Number of retained checkpoints without allocating an array. */
|
|
1005
|
+
get size() {
|
|
1006
|
+
return this.nodes.size;
|
|
1007
|
+
}
|
|
1008
|
+
/** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
|
|
1009
|
+
retain(id) {
|
|
1010
|
+
const checkpoint = this.get(id);
|
|
1011
|
+
this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
|
|
1012
|
+
this.addReference(id);
|
|
1013
|
+
return checkpoint;
|
|
1014
|
+
}
|
|
1015
|
+
/** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
|
|
1016
|
+
release(id) {
|
|
1017
|
+
if (!this.nodes.has(id))
|
|
1018
|
+
return false;
|
|
1019
|
+
const pins = this.explicitPins.get(id) ?? 0;
|
|
1020
|
+
if (pins === 0)
|
|
1021
|
+
return false;
|
|
1022
|
+
if (pins === 1)
|
|
1023
|
+
this.explicitPins.delete(id);
|
|
1024
|
+
else
|
|
1025
|
+
this.explicitPins.set(id, pins - 1);
|
|
1026
|
+
this.removeReference(id);
|
|
1027
|
+
this.collect(this.maxCheckpoints);
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
deleteBranch(branch) {
|
|
1031
|
+
if (branch === "main")
|
|
1032
|
+
throw new RangeError("cannot delete main branch");
|
|
1033
|
+
const previous = this.heads.get(branch);
|
|
1034
|
+
const deleted = this.heads.delete(branch);
|
|
1035
|
+
if (previous !== void 0)
|
|
1036
|
+
this.removeReference(previous);
|
|
1037
|
+
this.collect(this.maxCheckpoints);
|
|
1038
|
+
return deleted;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
|
|
1042
|
+
* commits never scan pinned nodes or the retained history. Parents are metadata rather than a
|
|
1043
|
+
* storage dependency, so a retained node remains usable after pruning.
|
|
1044
|
+
*/
|
|
1045
|
+
gc(max = this.maxCheckpoints) {
|
|
1046
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
1047
|
+
throw new RangeError("max must be a positive integer");
|
|
1048
|
+
const removed = [];
|
|
1049
|
+
this.collect(max, removed);
|
|
1050
|
+
return removed;
|
|
1051
|
+
}
|
|
1052
|
+
collect(max, removed) {
|
|
1053
|
+
while (this.nodes.size > max && this.evictable.size > 0) {
|
|
1054
|
+
const id = this.evictable.values().next().value;
|
|
1055
|
+
this.evictable.delete(id);
|
|
1056
|
+
this.nodes.delete(id);
|
|
1057
|
+
removed?.push(id);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
moveHead(branch, id) {
|
|
1061
|
+
const previous = this.heads.get(branch);
|
|
1062
|
+
if (previous === id)
|
|
1063
|
+
return;
|
|
1064
|
+
if (previous !== void 0)
|
|
1065
|
+
this.removeReference(previous);
|
|
1066
|
+
this.heads.set(branch, id);
|
|
1067
|
+
this.addReference(id);
|
|
1068
|
+
}
|
|
1069
|
+
addReference(id) {
|
|
1070
|
+
this.references.set(id, (this.references.get(id) ?? 0) + 1);
|
|
1071
|
+
this.evictable.delete(id);
|
|
1072
|
+
}
|
|
1073
|
+
removeReference(id) {
|
|
1074
|
+
const next = (this.references.get(id) ?? 0) - 1;
|
|
1075
|
+
if (next > 0)
|
|
1076
|
+
this.references.set(id, next);
|
|
1077
|
+
else {
|
|
1078
|
+
this.references.delete(id);
|
|
1079
|
+
if (this.nodes.has(id))
|
|
1080
|
+
this.evictable.add(id);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
assertBranch(branch) {
|
|
1084
|
+
if (!BRANCH_PATTERN.test(branch))
|
|
1085
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
// ../../sqlite/dist/default.js
|
|
1090
|
+
import { Database } from "@crvouga/mockingbird-service-sqlite";
|
|
1091
|
+
var createDefaultSqlite = () => new Database();
|
|
1092
|
+
var resolveSqlite = (sqlite) => sqlite ?? createDefaultSqlite();
|
|
1093
|
+
|
|
1094
|
+
// ../../sqlite/dist/migrate.js
|
|
1095
|
+
var ensureMigrationsTable = (sqlite) => {
|
|
1096
|
+
sqlite.exec(`
|
|
1097
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
1098
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
1099
|
+
applied_at INTEGER NOT NULL
|
|
1100
|
+
)
|
|
1101
|
+
`);
|
|
1102
|
+
};
|
|
1103
|
+
var migrate = (sqlite, migrations) => {
|
|
1104
|
+
ensureMigrationsTable(sqlite);
|
|
1105
|
+
const applied = new Set(sqlite.prepare("SELECT id FROM schema_migrations").all().map((row) => row.id));
|
|
1106
|
+
const pending = migrations.filter((migration) => !applied.has(migration.id));
|
|
1107
|
+
if (pending.length === 0)
|
|
1108
|
+
return;
|
|
1109
|
+
const insert = sqlite.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
|
|
1110
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1111
|
+
sqlite.transaction(() => {
|
|
1112
|
+
for (const migration of pending) {
|
|
1113
|
+
sqlite.exec(migration.sql);
|
|
1114
|
+
insert.run(migration.id, now);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
// ../../sqlite/dist/schema.js
|
|
1120
|
+
var CORE_MIGRATIONS = [
|
|
1121
|
+
{
|
|
1122
|
+
id: "20260322_core_records_sequences",
|
|
1123
|
+
sql: `
|
|
1124
|
+
CREATE TABLE IF NOT EXISTS mockingbird_records (
|
|
1125
|
+
namespace TEXT NOT NULL,
|
|
1126
|
+
collection TEXT NOT NULL,
|
|
1127
|
+
id TEXT NOT NULL,
|
|
1128
|
+
seq INTEGER NOT NULL,
|
|
1129
|
+
value TEXT NOT NULL,
|
|
1130
|
+
PRIMARY KEY (namespace, collection, id)
|
|
1131
|
+
);
|
|
1132
|
+
CREATE INDEX IF NOT EXISTS mockingbird_records_seq
|
|
1133
|
+
ON mockingbird_records (namespace, collection, seq);
|
|
1134
|
+
CREATE TABLE IF NOT EXISTS mockingbird_sequences (
|
|
1135
|
+
namespace TEXT NOT NULL,
|
|
1136
|
+
name TEXT NOT NULL,
|
|
1137
|
+
kind TEXT NOT NULL,
|
|
1138
|
+
value INTEGER NOT NULL,
|
|
1139
|
+
PRIMARY KEY (namespace, name, kind)
|
|
1140
|
+
);
|
|
1141
|
+
`
|
|
1142
|
+
}
|
|
1143
|
+
];
|
|
1144
|
+
var migrateCore = (sqlite) => {
|
|
1145
|
+
migrate(sqlite, CORE_MIGRATIONS);
|
|
1146
|
+
};
|
|
1147
|
+
var clearNamespace = (sqlite, namespace) => {
|
|
1148
|
+
sqlite.transaction(() => {
|
|
1149
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1150
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1151
|
+
});
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// ../../openapi/metadata/dist/types.js
|
|
1155
|
+
var EXTENSION_KEYS = {
|
|
1156
|
+
operation: "x-mockingbird",
|
|
1157
|
+
resource: "x-mockingbird-resource",
|
|
1158
|
+
resourceRef: "x-mockingbird-resource-ref",
|
|
1159
|
+
volatile: "x-mockingbird-volatile",
|
|
1160
|
+
scope: "x-mockingbird-scope",
|
|
1161
|
+
unsupported: "x-mockingbird-unsupported",
|
|
1162
|
+
parityHeader: "x-mockingbird-parity-header"
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// ../../openapi/metadata/dist/read.js
|
|
1166
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1167
|
+
var extensionOf = (holder, key) => holder[key];
|
|
1168
|
+
var operationMetadata = (operation) => {
|
|
1169
|
+
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
1170
|
+
const ext = isRecord2(raw) ? raw : {};
|
|
1171
|
+
const supported = ext.supported ?? true;
|
|
1172
|
+
const parity = ext.parity ?? {};
|
|
1173
|
+
return {
|
|
1174
|
+
supported,
|
|
1175
|
+
reason: typeof ext.reason === "string" ? ext.reason : void 0,
|
|
1176
|
+
parity: {
|
|
1177
|
+
enabled: supported && (parity.enabled ?? true),
|
|
1178
|
+
safe: parity.safe ?? true,
|
|
1179
|
+
reason: typeof parity.reason === "string" ? parity.reason : void 0
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// ../core/dist/service.js
|
|
1185
|
+
import { Hono } from "hono";
|
|
1186
|
+
var defineOperations = (handlers) => handlers;
|
|
1187
|
+
var OperationRegistryError = class extends Error {
|
|
1188
|
+
problems;
|
|
1189
|
+
constructor(problems) {
|
|
1190
|
+
super(`operation registry is inconsistent:
|
|
1191
|
+
${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
1192
|
+
this.problems = problems;
|
|
1193
|
+
this.name = "OperationRegistryError";
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var verifyOperations = (document2, handlers) => {
|
|
1197
|
+
const problems = [];
|
|
1198
|
+
const operations = listOperations(document2);
|
|
1199
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1200
|
+
for (const operation of operations) {
|
|
1201
|
+
if (seen.has(operation.operationId))
|
|
1202
|
+
problems.push(`duplicate operationId ${operation.operationId}`);
|
|
1203
|
+
seen.add(operation.operationId);
|
|
1204
|
+
const supported = operationMetadata(operation.operation).supported;
|
|
1205
|
+
const handler = handlers[operation.operationId];
|
|
1206
|
+
if (supported && !handler)
|
|
1207
|
+
problems.push(`supported operation ${operation.operationId} has no handler`);
|
|
1208
|
+
if (!supported && handler)
|
|
1209
|
+
problems.push(`operation ${operation.operationId} is marked unsupported but has a handler`);
|
|
1210
|
+
}
|
|
1211
|
+
for (const id of Object.keys(handlers)) {
|
|
1212
|
+
if (!seen.has(id))
|
|
1213
|
+
problems.push(`handler ${id} has no OpenAPI operation`);
|
|
1214
|
+
}
|
|
1215
|
+
return problems;
|
|
1216
|
+
};
|
|
1217
|
+
var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
|
|
1218
|
+
var routeOrder = (a, b) => {
|
|
1219
|
+
const sa = a.path.split("/");
|
|
1220
|
+
const sb = b.path.split("/");
|
|
1221
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
1222
|
+
const x = sa[i] ?? "";
|
|
1223
|
+
const y = sb[i] ?? "";
|
|
1224
|
+
const px = x.startsWith("{");
|
|
1225
|
+
const py = y.startsWith("{");
|
|
1226
|
+
if (px !== py)
|
|
1227
|
+
return px ? 1 : -1;
|
|
1228
|
+
if (x !== y)
|
|
1229
|
+
return x < y ? -1 : 1;
|
|
1230
|
+
}
|
|
1231
|
+
return 0;
|
|
1232
|
+
};
|
|
1233
|
+
var queryOf = (url) => decodeFormPairs(url.searchParams.entries());
|
|
1234
|
+
var bootSqlite = (sqlite) => {
|
|
1235
|
+
const client = resolveSqlite(sqlite);
|
|
1236
|
+
migrateCore(client);
|
|
1237
|
+
return client;
|
|
1238
|
+
};
|
|
1239
|
+
var createService = (options) => {
|
|
1240
|
+
const problems = verifyOperations(options.document, options.handlers);
|
|
1241
|
+
if (problems.length > 0)
|
|
1242
|
+
throw new OperationRegistryError(problems);
|
|
1243
|
+
migrateCore(options.sqlite);
|
|
1244
|
+
const now = options.now ?? (() => Date.now());
|
|
1245
|
+
const app = new Hono();
|
|
1246
|
+
app.notFound((c) => options.notFound(c.req.raw));
|
|
1247
|
+
app.onError((error2, c) => options.onError(error2, c.req.raw));
|
|
1248
|
+
const operations = [...listOperations(options.document)].sort(routeOrder);
|
|
1249
|
+
for (const operation of operations) {
|
|
1250
|
+
const metadata = operationMetadata(operation.operation);
|
|
1251
|
+
const handler = options.handlers[operation.operationId];
|
|
1252
|
+
const route = async (c) => {
|
|
1253
|
+
const request = c.req.raw;
|
|
1254
|
+
if (!metadata.supported || !handler) {
|
|
1255
|
+
return options.unsupported ? options.unsupported(request, operation) : options.notFound(request);
|
|
1256
|
+
}
|
|
1257
|
+
const url = new URL(request.url);
|
|
1258
|
+
const context = {
|
|
1259
|
+
request,
|
|
1260
|
+
url,
|
|
1261
|
+
params: c.req.param(),
|
|
1262
|
+
query: queryOf(url),
|
|
1263
|
+
body: await readBody(request),
|
|
1264
|
+
sqlite: options.sqlite,
|
|
1265
|
+
namespace: options.namespace,
|
|
1266
|
+
operation,
|
|
1267
|
+
document: options.document,
|
|
1268
|
+
now
|
|
1269
|
+
};
|
|
1270
|
+
const short = await options.before?.(context);
|
|
1271
|
+
if (short)
|
|
1272
|
+
return short;
|
|
1273
|
+
return handler(context);
|
|
1274
|
+
};
|
|
1275
|
+
app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
app,
|
|
1279
|
+
sqlite: options.sqlite,
|
|
1280
|
+
namespace: options.namespace,
|
|
1281
|
+
fetch: async (request) => app.fetch(request),
|
|
1282
|
+
reset: async () => {
|
|
1283
|
+
clearNamespace(options.sqlite, options.namespace);
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// ../core/dist/snapshot.js
|
|
1289
|
+
var snapshotNamespace = (sqlite, namespace) => ({
|
|
1290
|
+
namespace,
|
|
1291
|
+
records: sqlite.prepare("SELECT collection, id, seq, value FROM mockingbird_records WHERE namespace = ? ORDER BY collection, seq").all(namespace),
|
|
1292
|
+
sequences: sqlite.prepare("SELECT name, kind, value FROM mockingbird_sequences WHERE namespace = ? ORDER BY name, kind").all(namespace)
|
|
1293
|
+
});
|
|
1294
|
+
var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
1295
|
+
sqlite.transaction(() => {
|
|
1296
|
+
sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ?").run(namespace);
|
|
1297
|
+
sqlite.prepare("DELETE FROM mockingbird_sequences WHERE namespace = ?").run(namespace);
|
|
1298
|
+
const record = sqlite.prepare("INSERT INTO mockingbird_records (namespace, collection, id, seq, value) VALUES (?, ?, ?, ?, ?)");
|
|
1299
|
+
for (const row of snapshot.records) {
|
|
1300
|
+
record.run(namespace, row.collection, row.id, row.seq, row.value);
|
|
1301
|
+
}
|
|
1302
|
+
const sequence = sqlite.prepare("INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, ?, ?)");
|
|
1303
|
+
for (const row of snapshot.sequences) {
|
|
1304
|
+
sequence.run(namespace, row.name, row.kind, row.value);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// ../core/dist/version.js
|
|
1310
|
+
var PACKAGE_VERSION = true ? "0.1.0" : UNRELEASED_VERSION;
|
|
1311
|
+
|
|
1312
|
+
// ../core/dist/signing.js
|
|
1313
|
+
var encoder = new TextEncoder();
|
|
1314
|
+
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
1315
|
+
|
|
1316
|
+
// ../core/dist/webhooks.js
|
|
1317
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1318
|
+
var adminError2 = (status, message) => json2(status, { error: { type: "mockingbird_admin", message } });
|
|
1319
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1320
|
+
var parseEndpoint = (value) => {
|
|
1321
|
+
if (!isRecord3(value) || typeof value.url !== "string")
|
|
1322
|
+
return "each endpoint needs a url";
|
|
1323
|
+
try {
|
|
1324
|
+
new URL(value.url);
|
|
1325
|
+
} catch {
|
|
1326
|
+
return `not a URL: ${value.url}`;
|
|
1327
|
+
}
|
|
1328
|
+
const endpoint = { url: value.url };
|
|
1329
|
+
if (typeof value.id === "string")
|
|
1330
|
+
endpoint.id = value.id;
|
|
1331
|
+
if (typeof value.secret === "string")
|
|
1332
|
+
endpoint.secret = value.secret;
|
|
1333
|
+
if (typeof value.signUrl === "string")
|
|
1334
|
+
endpoint.signUrl = value.signUrl;
|
|
1335
|
+
const events = value.events ?? value.enabledEvents;
|
|
1336
|
+
if (Array.isArray(events))
|
|
1337
|
+
endpoint.events = events.map(String);
|
|
1338
|
+
if (isRecord3(value.tags)) {
|
|
1339
|
+
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1340
|
+
}
|
|
1341
|
+
if (typeof value.account === "string")
|
|
1342
|
+
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1343
|
+
if (isRecord3(value.headers)) {
|
|
1344
|
+
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1345
|
+
}
|
|
1346
|
+
return endpoint;
|
|
1347
|
+
};
|
|
1348
|
+
var webhookAdminRoutes = (hub) => ({
|
|
1349
|
+
"GET /webhooks": ({ url, namespace }) => json2(200, {
|
|
1350
|
+
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1351
|
+
const type = url.searchParams.get("type");
|
|
1352
|
+
return type === null || d.type === type;
|
|
1353
|
+
})
|
|
1354
|
+
}),
|
|
1355
|
+
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1356
|
+
const type = url.searchParams.get("type");
|
|
1357
|
+
return json2(200, {
|
|
1358
|
+
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1359
|
+
});
|
|
1360
|
+
},
|
|
1361
|
+
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1362
|
+
const replayed = await hub.replay(params.id);
|
|
1363
|
+
return replayed ? json2(200, replayed) : adminError2(404, `no delivery ${params.id}`);
|
|
1364
|
+
},
|
|
1365
|
+
"POST /webhooks/flush": async () => {
|
|
1366
|
+
await hub.flush();
|
|
1367
|
+
return json2(200, { status: "ok" });
|
|
1368
|
+
},
|
|
1369
|
+
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1370
|
+
if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
1371
|
+
return adminError2(400, "mode must be duplicate, reorder or drop");
|
|
1372
|
+
}
|
|
1373
|
+
const fault = { mode: body.mode };
|
|
1374
|
+
if (typeof body.count === "number")
|
|
1375
|
+
fault.count = body.count;
|
|
1376
|
+
hub.fault(namespace, fault);
|
|
1377
|
+
return json2(201, { namespace, ...fault });
|
|
1378
|
+
},
|
|
1379
|
+
"GET /webhook-endpoints": ({ namespace }) => json2(200, {
|
|
1380
|
+
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1381
|
+
...rest,
|
|
1382
|
+
secret: secret ? "(set)" : null
|
|
1383
|
+
}))
|
|
1384
|
+
}),
|
|
1385
|
+
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
1386
|
+
const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
|
|
1387
|
+
if (!Array.isArray(list))
|
|
1388
|
+
return adminError2(400, "expected [{url, secret?, events?}]");
|
|
1389
|
+
const parsed = [];
|
|
1390
|
+
for (const each of list) {
|
|
1391
|
+
const endpoint = parseEndpoint(each);
|
|
1392
|
+
if (typeof endpoint === "string")
|
|
1393
|
+
return adminError2(400, endpoint);
|
|
1394
|
+
parsed.push(endpoint);
|
|
1395
|
+
}
|
|
1396
|
+
const set = hub.setEndpoints(namespace, parsed);
|
|
1397
|
+
return json2(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
1398
|
+
},
|
|
1399
|
+
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
1400
|
+
hub.setEndpoints(namespace, []);
|
|
1401
|
+
return json2(200, { status: "ok" });
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
var parsePayload = (message) => {
|
|
1405
|
+
if (message.contentType.startsWith("application/json")) {
|
|
1406
|
+
try {
|
|
1407
|
+
return JSON.parse(message.body);
|
|
1408
|
+
} catch {
|
|
1409
|
+
return message.body;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (message.contentType.startsWith("application/x-www-form-urlencoded")) {
|
|
1413
|
+
return Object.fromEntries(new URLSearchParams(message.body));
|
|
1414
|
+
}
|
|
1415
|
+
return message.body;
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// ../core/dist/runtime.js
|
|
1419
|
+
var MOCKINGBIRD_HEADER = "x-mockingbird";
|
|
1420
|
+
var BRANCH_HEADER = "x-mockingbird-branch";
|
|
1421
|
+
var AT_HEADER = "x-mockingbird-at";
|
|
1422
|
+
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
1423
|
+
var DEFAULT_NAMESPACE = "default";
|
|
1424
|
+
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1425
|
+
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
1426
|
+
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1427
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1428
|
+
var effects = /* @__PURE__ */ new WeakMap();
|
|
1429
|
+
var reuseSorted = (fresh, previous, compare, equal) => {
|
|
1430
|
+
if (!previous || previous.length === 0)
|
|
1431
|
+
return fresh.map((row) => Object.freeze(row));
|
|
1432
|
+
const result = new Array(fresh.length);
|
|
1433
|
+
let unchanged = fresh.length === previous.length;
|
|
1434
|
+
let oldIndex = 0;
|
|
1435
|
+
for (let index = 0; index < fresh.length; index++) {
|
|
1436
|
+
const row = fresh[index];
|
|
1437
|
+
while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
|
|
1438
|
+
oldIndex++;
|
|
1439
|
+
}
|
|
1440
|
+
const old = previous[oldIndex];
|
|
1441
|
+
result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
|
|
1442
|
+
if (result[index] !== previous[index])
|
|
1443
|
+
unchanged = false;
|
|
1444
|
+
}
|
|
1445
|
+
return unchanged ? previous : result;
|
|
1446
|
+
};
|
|
1447
|
+
var faultEffects = (request) => (effects.get(request) ?? []).filter((e) => e !== void 0);
|
|
1448
|
+
var DroppedConnectionError = class extends TypeError {
|
|
1449
|
+
code = "MOCKINGBIRD_DROP";
|
|
1450
|
+
constructor() {
|
|
1451
|
+
super("fetch failed: connection dropped by Mockingbird fault");
|
|
1452
|
+
this.name = "TypeError";
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
var operationMatcher = (document2) => {
|
|
1456
|
+
const matchers = listOperations(document2).map((operation) => ({
|
|
1457
|
+
operationId: operation.operationId,
|
|
1458
|
+
method: operation.method.toUpperCase(),
|
|
1459
|
+
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
1460
|
+
params: (operation.path.match(/\{/g) ?? []).length
|
|
1461
|
+
})).sort((a, b) => a.params - b.params);
|
|
1462
|
+
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
1463
|
+
};
|
|
1464
|
+
var createRuntime = (options) => {
|
|
1465
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
1466
|
+
const clock = options.clock ?? createClock();
|
|
1467
|
+
const rng = createRng(options.seed ?? 0);
|
|
1468
|
+
const wallNow = options.io?.wallNow ?? Date.now;
|
|
1469
|
+
const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
|
|
1470
|
+
const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1471
|
+
const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
|
|
1472
|
+
const metrics = createMetrics();
|
|
1473
|
+
const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
|
|
1474
|
+
const version = options.version ?? PACKAGE_VERSION;
|
|
1475
|
+
const instances = /* @__PURE__ */ new Map();
|
|
1476
|
+
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
1477
|
+
const branchRngs = /* @__PURE__ */ new Map();
|
|
1478
|
+
const timelines = /* @__PURE__ */ new Map();
|
|
1479
|
+
const branchStorage = /* @__PURE__ */ new Map();
|
|
1480
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1481
|
+
const credentials = createCredentialRegistry();
|
|
1482
|
+
const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
|
|
1483
|
+
const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
|
|
1484
|
+
const instanceFor = (key, publicNamespace = key, isolatedRng) => {
|
|
1485
|
+
const existing = instances.get(key);
|
|
1486
|
+
if (existing)
|
|
1487
|
+
return existing;
|
|
1488
|
+
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
1489
|
+
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
1490
|
+
}
|
|
1491
|
+
const created = options.create({
|
|
1492
|
+
namespace: storageNamespace(key),
|
|
1493
|
+
publicNamespace,
|
|
1494
|
+
sqlite,
|
|
1495
|
+
clock,
|
|
1496
|
+
rng: isolatedRng ?? rng
|
|
1497
|
+
});
|
|
1498
|
+
instances.set(key, created);
|
|
1499
|
+
publicNamespaces.add(publicNamespace);
|
|
1500
|
+
if (isolatedRng)
|
|
1501
|
+
branchRngs.set(key, isolatedRng);
|
|
1502
|
+
return created;
|
|
1503
|
+
};
|
|
1504
|
+
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
1505
|
+
const capture = (storage) => {
|
|
1506
|
+
const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
|
|
1507
|
+
const previous = captured.get(storage);
|
|
1508
|
+
const snapshot2 = {
|
|
1509
|
+
namespace: fresh.namespace,
|
|
1510
|
+
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),
|
|
1511
|
+
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)
|
|
1512
|
+
};
|
|
1513
|
+
Object.freeze(snapshot2.records);
|
|
1514
|
+
Object.freeze(snapshot2.sequences);
|
|
1515
|
+
Object.freeze(snapshot2);
|
|
1516
|
+
captured.set(storage, snapshot2);
|
|
1517
|
+
return Object.freeze({
|
|
1518
|
+
snapshot: snapshot2,
|
|
1519
|
+
clock: Object.freeze(clock.state()),
|
|
1520
|
+
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
1521
|
+
});
|
|
1522
|
+
};
|
|
1523
|
+
const timeline = (name = DEFAULT_NAMESPACE) => {
|
|
1524
|
+
let found = timelines.get(name);
|
|
1525
|
+
if (found)
|
|
1526
|
+
return found;
|
|
1527
|
+
instance(name);
|
|
1528
|
+
found = new Timeline({
|
|
1529
|
+
now: clock.now,
|
|
1530
|
+
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
1531
|
+
});
|
|
1532
|
+
found.commit(capture(name));
|
|
1533
|
+
timelines.set(name, found);
|
|
1534
|
+
return found;
|
|
1535
|
+
};
|
|
1536
|
+
const physicalBranch = (namespace, branch2) => {
|
|
1537
|
+
if (branch2 === "main")
|
|
1538
|
+
return namespace;
|
|
1539
|
+
const mapKey = `${namespace}\0${branch2}`;
|
|
1540
|
+
const existing = branchStorage.get(mapKey);
|
|
1541
|
+
if (existing)
|
|
1542
|
+
return existing;
|
|
1543
|
+
const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
|
|
1544
|
+
branchStorage.set(mapKey, key);
|
|
1545
|
+
return key;
|
|
1546
|
+
};
|
|
1547
|
+
const ensureBranch = (namespace, branch2, at) => {
|
|
1548
|
+
if (!BRANCH_PATTERN2.test(branch2))
|
|
1549
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
1550
|
+
const history = timeline(namespace);
|
|
1551
|
+
if (branch2 === "main") {
|
|
1552
|
+
if (at !== void 0) {
|
|
1553
|
+
const point = history.checkout("main", at);
|
|
1554
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
1555
|
+
captured.set(namespace, point.value.snapshot);
|
|
1556
|
+
rng.setState(point.value.rngState);
|
|
1557
|
+
clock.set(point.value.clock.now);
|
|
1558
|
+
if (point.value.clock.frozen)
|
|
1559
|
+
clock.freeze();
|
|
1560
|
+
else
|
|
1561
|
+
clock.unfreeze();
|
|
1562
|
+
}
|
|
1563
|
+
return namespace;
|
|
1564
|
+
}
|
|
1565
|
+
const storage = physicalBranch(namespace, branch2);
|
|
1566
|
+
if (!history.hasBranch(branch2)) {
|
|
1567
|
+
if (at === void 0)
|
|
1568
|
+
history.commit(capture(namespace));
|
|
1569
|
+
const point = history.fork(branch2, at === void 0 ? {} : { from: at });
|
|
1570
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1571
|
+
if (point)
|
|
1572
|
+
branchRng.setState(point.value.rngState);
|
|
1573
|
+
instanceFor(storage, namespace, branchRng);
|
|
1574
|
+
if (point)
|
|
1575
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1576
|
+
if (point)
|
|
1577
|
+
captured.set(storage, point.value.snapshot);
|
|
1578
|
+
} else if (at !== void 0 && history.head(branch2)?.id !== at) {
|
|
1579
|
+
const point = history.checkout(branch2, at);
|
|
1580
|
+
if (!instances.has(storage)) {
|
|
1581
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1582
|
+
branchRng.setState(point.value.rngState);
|
|
1583
|
+
instanceFor(storage, namespace, branchRng);
|
|
1584
|
+
}
|
|
1585
|
+
branchRngs.get(storage)?.setState(point.value.rngState);
|
|
1586
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1587
|
+
captured.set(storage, point.value.snapshot);
|
|
1588
|
+
} else {
|
|
1589
|
+
if (!instances.has(storage)) {
|
|
1590
|
+
const point = history.head(branch2);
|
|
1591
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1592
|
+
if (point)
|
|
1593
|
+
branchRng.setState(point.value.rngState);
|
|
1594
|
+
instanceFor(storage, namespace, branchRng);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return storage;
|
|
1598
|
+
};
|
|
1599
|
+
const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
|
|
1600
|
+
const storage = ensureBranch(namespace, branch2);
|
|
1601
|
+
return timeline(namespace).commit(capture(storage), { branch: branch2 });
|
|
1602
|
+
};
|
|
1603
|
+
const branch = (name, branchOptions = {}) => {
|
|
1604
|
+
const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1605
|
+
ensureBranch(namespace, name, branchOptions.at);
|
|
1606
|
+
const head = timeline(namespace).head(name);
|
|
1607
|
+
if (!head)
|
|
1608
|
+
throw new RangeError(`branch ${name} has no checkpoint`);
|
|
1609
|
+
return head;
|
|
1610
|
+
};
|
|
1611
|
+
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
1612
|
+
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1613
|
+
const branchName = checkoutOptions.branch ?? "main";
|
|
1614
|
+
const history = timeline(namespace);
|
|
1615
|
+
const point = history.checkout(branchName, checkpointId);
|
|
1616
|
+
const storage = ensureBranch(namespace, branchName);
|
|
1617
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1618
|
+
captured.set(storage, point.value.snapshot);
|
|
1619
|
+
clock.set(point.value.clock.now);
|
|
1620
|
+
if (point.value.clock.frozen)
|
|
1621
|
+
clock.freeze();
|
|
1622
|
+
else
|
|
1623
|
+
clock.unfreeze();
|
|
1624
|
+
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
1625
|
+
};
|
|
1626
|
+
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
1627
|
+
if (name === "*") {
|
|
1628
|
+
options.webhooks?.clear();
|
|
1629
|
+
for (const each of instances.values())
|
|
1630
|
+
await each.reset();
|
|
1631
|
+
timelines.clear();
|
|
1632
|
+
branchStorage.clear();
|
|
1633
|
+
branchRngs.clear();
|
|
1634
|
+
captured.clear();
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
options.webhooks?.clear(name);
|
|
1638
|
+
const target = instances.get(name);
|
|
1639
|
+
if (target)
|
|
1640
|
+
await target.reset();
|
|
1641
|
+
else
|
|
1642
|
+
clearNamespace(sqlite, storageNamespace(name));
|
|
1643
|
+
for (const [mapping, storage] of branchStorage) {
|
|
1644
|
+
if (!mapping.startsWith(`${name}\0`))
|
|
1645
|
+
continue;
|
|
1646
|
+
const branchInstance = instances.get(storage);
|
|
1647
|
+
if (branchInstance)
|
|
1648
|
+
await branchInstance.reset();
|
|
1649
|
+
else
|
|
1650
|
+
clearNamespace(sqlite, storageNamespace(storage));
|
|
1651
|
+
branchStorage.delete(mapping);
|
|
1652
|
+
branchRngs.delete(storage);
|
|
1653
|
+
captured.delete(storage);
|
|
1654
|
+
}
|
|
1655
|
+
timelines.delete(name);
|
|
1656
|
+
captured.delete(name);
|
|
1657
|
+
};
|
|
1658
|
+
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
1659
|
+
return checkpoint(name, "main").value.snapshot;
|
|
1660
|
+
};
|
|
1661
|
+
const restore = (from, name = DEFAULT_NAMESPACE) => {
|
|
1662
|
+
instance(name);
|
|
1663
|
+
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
1664
|
+
captured.set(name, from);
|
|
1665
|
+
const history = timelines.get(name);
|
|
1666
|
+
if (history)
|
|
1667
|
+
history.commit(capture(name), { branch: "main" });
|
|
1668
|
+
else
|
|
1669
|
+
timeline(name);
|
|
1670
|
+
};
|
|
1671
|
+
const runtime = {
|
|
1672
|
+
name: options.name,
|
|
1673
|
+
sqlite,
|
|
1674
|
+
clock,
|
|
1675
|
+
faults,
|
|
1676
|
+
metrics,
|
|
1677
|
+
journal,
|
|
1678
|
+
rng,
|
|
1679
|
+
credentials,
|
|
1680
|
+
webhooks: options.webhooks,
|
|
1681
|
+
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
1682
|
+
const preset = options.presets?.[name];
|
|
1683
|
+
if (!preset)
|
|
1684
|
+
throw new RangeError(`no fault preset ${JSON.stringify(name)}`);
|
|
1685
|
+
const added = (preset.rules ?? []).map((rule, index) => faults.add({
|
|
1686
|
+
namespace,
|
|
1687
|
+
...rule,
|
|
1688
|
+
...overrides,
|
|
1689
|
+
preset: name,
|
|
1690
|
+
id: `${overrides.id ?? name}${(preset.rules ?? []).length > 1 ? `_${index + 1}` : ""}`
|
|
1691
|
+
}));
|
|
1692
|
+
if (preset.webhook && options.webhooks) {
|
|
1693
|
+
options.webhooks.fault(namespace, {
|
|
1694
|
+
...preset.webhook,
|
|
1695
|
+
...overrides.count !== void 0 ? { count: overrides.count } : {}
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
return added;
|
|
1699
|
+
},
|
|
1700
|
+
instance,
|
|
1701
|
+
namespaces: () => [...publicNamespaces].sort(),
|
|
1702
|
+
reset,
|
|
1703
|
+
snapshot,
|
|
1704
|
+
restore,
|
|
1705
|
+
checkpoint,
|
|
1706
|
+
branch,
|
|
1707
|
+
checkout,
|
|
1708
|
+
timeline,
|
|
1709
|
+
fetch: async (incoming) => {
|
|
1710
|
+
let request = incoming;
|
|
1711
|
+
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
1712
|
+
if (prefixed) {
|
|
1713
|
+
const url2 = new URL(request.url);
|
|
1714
|
+
url2.pathname = prefixed[2] ?? "/";
|
|
1715
|
+
const headers = new Headers(request.headers);
|
|
1716
|
+
if (!headers.has(NAMESPACE_HEADER)) {
|
|
1717
|
+
headers.set(NAMESPACE_HEADER, decodeURIComponent(prefixed[1]));
|
|
1718
|
+
}
|
|
1719
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
1720
|
+
request = new Request(url2, {
|
|
1721
|
+
method: request.method,
|
|
1722
|
+
headers,
|
|
1723
|
+
...hasBody ? { body: await request.arrayBuffer() } : {},
|
|
1724
|
+
signal: request.signal
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
let namespace = control.namespaceOf(request);
|
|
1728
|
+
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
1729
|
+
const credential = options.credential(request);
|
|
1730
|
+
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
1731
|
+
if (mapped !== void 0)
|
|
1732
|
+
namespace = mapped;
|
|
1733
|
+
}
|
|
1734
|
+
const selectedBranch = request.headers.get(BRANCH_HEADER) ?? "main";
|
|
1735
|
+
const at = request.headers.get(AT_HEADER) ?? void 0;
|
|
1736
|
+
const stamp = (response2) => {
|
|
1737
|
+
const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
|
|
1738
|
+
try {
|
|
1739
|
+
response2.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1740
|
+
return response2;
|
|
1741
|
+
} catch {
|
|
1742
|
+
const copy = new Response(response2.body, response2);
|
|
1743
|
+
copy.headers.set(MOCKINGBIRD_HEADER, value);
|
|
1744
|
+
return copy;
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
const handled = await control.handle(request);
|
|
1748
|
+
if (handled)
|
|
1749
|
+
return stamp(handled);
|
|
1750
|
+
const started = monotonicNow();
|
|
1751
|
+
const url = new URL(request.url);
|
|
1752
|
+
const operationId = operationIdFor(request, url.pathname);
|
|
1753
|
+
const log = (status, faultId, response2) => {
|
|
1754
|
+
const noted = response2 ? responseNotes(response2) : void 0;
|
|
1755
|
+
const entry = {
|
|
1756
|
+
service: options.name,
|
|
1757
|
+
namespace,
|
|
1758
|
+
operationId,
|
|
1759
|
+
method: request.method,
|
|
1760
|
+
path: url.pathname,
|
|
1761
|
+
status,
|
|
1762
|
+
durationMs: Math.round((monotonicNow() - started) * 100) / 100,
|
|
1763
|
+
unmatched: options.document !== void 0 && operationId === void 0,
|
|
1764
|
+
...faultId !== void 0 ? { faultId } : {},
|
|
1765
|
+
...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
|
|
1766
|
+
...noted?.adopted ? { adopted: true } : {}
|
|
1767
|
+
};
|
|
1768
|
+
metrics.record(entry);
|
|
1769
|
+
journal.record({ ...entry, at: new Date(clock.now()).toISOString() });
|
|
1770
|
+
options.onLog?.(entry);
|
|
1771
|
+
};
|
|
1772
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
1773
|
+
log(400);
|
|
1774
|
+
return stamp(new Response(JSON.stringify({
|
|
1775
|
+
error: {
|
|
1776
|
+
type: "mockingbird_admin",
|
|
1777
|
+
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
1778
|
+
}
|
|
1779
|
+
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
1780
|
+
}
|
|
1781
|
+
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
1782
|
+
log(400);
|
|
1783
|
+
return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
|
|
1784
|
+
}
|
|
1785
|
+
let storage;
|
|
1786
|
+
try {
|
|
1787
|
+
if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request.method)) {
|
|
1788
|
+
const point = timeline(namespace).get(at);
|
|
1789
|
+
storage = physicalBranch(namespace, `at_${at}`);
|
|
1790
|
+
let viewRng = branchRngs.get(storage);
|
|
1791
|
+
if (!viewRng) {
|
|
1792
|
+
viewRng = createRng(options.seed ?? 0);
|
|
1793
|
+
instanceFor(storage, namespace, viewRng);
|
|
1794
|
+
}
|
|
1795
|
+
viewRng.setState(point.value.rngState);
|
|
1796
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1797
|
+
captured.set(storage, point.value.snapshot);
|
|
1798
|
+
} else {
|
|
1799
|
+
storage = ensureBranch(namespace, selectedBranch, at);
|
|
1800
|
+
}
|
|
1801
|
+
} catch (error2) {
|
|
1802
|
+
log(409);
|
|
1803
|
+
return stamp(adminFail(409, error2 instanceof Error ? error2.message : String(error2)));
|
|
1804
|
+
}
|
|
1805
|
+
const hits = await faults.take({
|
|
1806
|
+
operationId,
|
|
1807
|
+
method: request.method,
|
|
1808
|
+
path: url.pathname,
|
|
1809
|
+
namespace
|
|
1810
|
+
});
|
|
1811
|
+
const final = hits.find((hit) => hit.drop || hit.response);
|
|
1812
|
+
if (final?.drop) {
|
|
1813
|
+
log(0, final.id);
|
|
1814
|
+
throw new DroppedConnectionError();
|
|
1815
|
+
}
|
|
1816
|
+
if (final?.response) {
|
|
1817
|
+
log(final.response.status, final.id);
|
|
1818
|
+
return stamp(final.response);
|
|
1819
|
+
}
|
|
1820
|
+
const fired = hits.filter((hit) => hit.effect !== void 0);
|
|
1821
|
+
if (fired.length > 0)
|
|
1822
|
+
effects.set(request, fired.map((hit) => hit.effect));
|
|
1823
|
+
let response = await instanceFor(storage, namespace).fetch(request);
|
|
1824
|
+
if (MUTATING_METHODS.has(request.method) && response.status >= 200 && response.status < 400) {
|
|
1825
|
+
const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
|
|
1826
|
+
response = mutableResponse(response);
|
|
1827
|
+
response.headers.set(CHECKPOINT_HEADER, point.id);
|
|
1828
|
+
}
|
|
1829
|
+
if (selectedBranch !== "main") {
|
|
1830
|
+
response = mutableResponse(response);
|
|
1831
|
+
response.headers.set(BRANCH_HEADER, selectedBranch);
|
|
1832
|
+
}
|
|
1833
|
+
if (at !== void 0) {
|
|
1834
|
+
response = mutableResponse(response);
|
|
1835
|
+
response.headers.set(AT_HEADER, at);
|
|
1836
|
+
}
|
|
1837
|
+
log(response.status, fired[0]?.id, response);
|
|
1838
|
+
return stamp(response);
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
const control = createControlPlane({
|
|
1842
|
+
name: options.name,
|
|
1843
|
+
startedAt: wallNow(),
|
|
1844
|
+
wallNow,
|
|
1845
|
+
clock,
|
|
1846
|
+
faults,
|
|
1847
|
+
metrics,
|
|
1848
|
+
journal,
|
|
1849
|
+
defaultNamespace: DEFAULT_NAMESPACE,
|
|
1850
|
+
namespaces: runtime.namespaces,
|
|
1851
|
+
reset,
|
|
1852
|
+
timeTravel: {
|
|
1853
|
+
checkpoint: (name, branchName) => {
|
|
1854
|
+
const point = checkpoint(name, branchName);
|
|
1855
|
+
return {
|
|
1856
|
+
id: point.id,
|
|
1857
|
+
branch: point.branch,
|
|
1858
|
+
parent: point.parent,
|
|
1859
|
+
at: point.at,
|
|
1860
|
+
records: point.value.snapshot.records.length
|
|
1861
|
+
};
|
|
1862
|
+
},
|
|
1863
|
+
branch: (branchName, branchOptions) => {
|
|
1864
|
+
const point = branch(branchName, branchOptions);
|
|
1865
|
+
return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
|
|
1866
|
+
},
|
|
1867
|
+
checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
|
|
1868
|
+
retain: (name, checkpointId) => {
|
|
1869
|
+
timeline(name).retain(checkpointId);
|
|
1870
|
+
},
|
|
1871
|
+
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
1872
|
+
inspect: (name) => {
|
|
1873
|
+
const history = timeline(name);
|
|
1874
|
+
return {
|
|
1875
|
+
branches: history.branches(),
|
|
1876
|
+
checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
1877
|
+
id,
|
|
1878
|
+
branch: branchName,
|
|
1879
|
+
parent,
|
|
1880
|
+
at
|
|
1881
|
+
}))
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
describe: options.describe ?? (() => ({})),
|
|
1886
|
+
...options.presets ? {
|
|
1887
|
+
applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
|
|
1888
|
+
} : {},
|
|
1889
|
+
routes: {
|
|
1890
|
+
...credentialRoutes(credentials),
|
|
1891
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
1892
|
+
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
1893
|
+
...options.admin?.(runtime) ?? {}
|
|
1894
|
+
},
|
|
1895
|
+
adminKey: options.adminKey
|
|
1896
|
+
});
|
|
1897
|
+
return runtime;
|
|
1898
|
+
};
|
|
1899
|
+
var mutableResponse = (response) => {
|
|
1900
|
+
try {
|
|
1901
|
+
response.headers.set("x-mockingbird-mutable-probe", "1");
|
|
1902
|
+
response.headers.delete("x-mockingbird-mutable-probe");
|
|
1903
|
+
return response;
|
|
1904
|
+
} catch {
|
|
1905
|
+
return new Response(response.body, response);
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1909
|
+
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
1910
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1911
|
+
var credentialRoutes = (registry) => ({
|
|
1912
|
+
"GET /credentials": () => adminJson(200, {
|
|
1913
|
+
credentials: registry.entries().map(({ credential, namespace }) => ({
|
|
1914
|
+
credential: maskCredential(credential),
|
|
1915
|
+
namespace
|
|
1916
|
+
}))
|
|
1917
|
+
}),
|
|
1918
|
+
"PUT /credentials": ({ body, namespace }) => {
|
|
1919
|
+
const pairs = [];
|
|
1920
|
+
const list = Array.isArray(body) ? body : isObject(body) ? body.credentials : void 0;
|
|
1921
|
+
if (Array.isArray(list)) {
|
|
1922
|
+
for (const each of list) {
|
|
1923
|
+
if (typeof each === "string")
|
|
1924
|
+
pairs.push([each, namespace]);
|
|
1925
|
+
else if (isObject(each) && typeof each.credential === "string") {
|
|
1926
|
+
pairs.push([
|
|
1927
|
+
each.credential,
|
|
1928
|
+
typeof each.namespace === "string" ? each.namespace : namespace
|
|
1929
|
+
]);
|
|
1930
|
+
} else
|
|
1931
|
+
return adminFail(400, "each entry is a credential string or {credential, namespace}");
|
|
1932
|
+
}
|
|
1933
|
+
} else if (isObject(list)) {
|
|
1934
|
+
for (const [credential, target] of Object.entries(list)) {
|
|
1935
|
+
if (typeof target !== "string")
|
|
1936
|
+
return adminFail(400, `namespace for ${credential} must be a string`);
|
|
1937
|
+
pairs.push([credential, target]);
|
|
1938
|
+
}
|
|
1939
|
+
} else if (isObject(body) && typeof body.credential === "string") {
|
|
1940
|
+
pairs.push([body.credential, typeof body.namespace === "string" ? body.namespace : namespace]);
|
|
1941
|
+
} else {
|
|
1942
|
+
return adminFail(400, 'expected {"credentials": {"<credential>": "<namespace>"}}');
|
|
1943
|
+
}
|
|
1944
|
+
for (const [credential, target] of pairs) {
|
|
1945
|
+
if (!NAMESPACE_PATTERN.test(target))
|
|
1946
|
+
return adminFail(400, `bad namespace ${JSON.stringify(target)}`);
|
|
1947
|
+
registry.set(credential, target);
|
|
1948
|
+
}
|
|
1949
|
+
return adminJson(200, { mapped: pairs.length });
|
|
1950
|
+
},
|
|
1951
|
+
"DELETE /credentials": ({ url }) => {
|
|
1952
|
+
const credential = url.searchParams.get("credential");
|
|
1953
|
+
if (credential === null)
|
|
1954
|
+
registry.clear();
|
|
1955
|
+
else
|
|
1956
|
+
registry.remove(credential);
|
|
1957
|
+
return adminJson(200, { status: "ok" });
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
var presetRoutes = (presets, runtime) => ({
|
|
1961
|
+
"GET /faults/presets": () => adminJson(200, {
|
|
1962
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
1963
|
+
}),
|
|
1964
|
+
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
1965
|
+
const name = params.name;
|
|
1966
|
+
if (!presets[name])
|
|
1967
|
+
return adminFail(404, `no fault preset ${name}; GET /__admin/faults/presets`);
|
|
1968
|
+
const overrides = isObject(body) ? body : {};
|
|
1969
|
+
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
// src/body.ts
|
|
1974
|
+
var utf82 = new TextDecoder("utf-8", { fatal: false });
|
|
1975
|
+
var isGzip = (bytes) => bytes.length >= 2 && bytes[0] === 31 && bytes[1] === 139;
|
|
1976
|
+
var gunzip = async (bytes) => {
|
|
1977
|
+
try {
|
|
1978
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
1979
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
1980
|
+
} catch {
|
|
1981
|
+
return void 0;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
var parseJson = (text) => {
|
|
1985
|
+
try {
|
|
1986
|
+
return JSON.parse(text);
|
|
1987
|
+
} catch {
|
|
1988
|
+
return void 0;
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
var fromBase64Text = (text) => {
|
|
1992
|
+
try {
|
|
1993
|
+
return utf82.decode(fromBase64(text.trim().replace(/-/g, "+").replace(/_/g, "/")));
|
|
1994
|
+
} catch {
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
var fromText = (text, compression) => {
|
|
1999
|
+
const trimmed = text.trim();
|
|
2000
|
+
if (trimmed.length === 0) return void 0;
|
|
2001
|
+
if (trimmed.startsWith("data=") || /(^|&)data=/.test(trimmed)) {
|
|
2002
|
+
const data = new URLSearchParams(trimmed).get("data");
|
|
2003
|
+
if (data !== null) return fromText(data, compression);
|
|
2004
|
+
}
|
|
2005
|
+
if (compression !== "base64") {
|
|
2006
|
+
const direct = parseJson(trimmed);
|
|
2007
|
+
if (direct !== void 0) return direct;
|
|
2008
|
+
}
|
|
2009
|
+
const decoded2 = fromBase64Text(trimmed);
|
|
2010
|
+
return decoded2 === void 0 ? void 0 : parseJson(decoded2);
|
|
2011
|
+
};
|
|
2012
|
+
var decodeBytes = async (bytes, headers, url) => {
|
|
2013
|
+
if (bytes.byteLength === 0) return void 0;
|
|
2014
|
+
const compression = url.searchParams.get("compression");
|
|
2015
|
+
let raw = bytes;
|
|
2016
|
+
const encoding = headers.get("content-encoding")?.toLowerCase();
|
|
2017
|
+
if (encoding === "gzip" || isGzip(raw)) {
|
|
2018
|
+
const inflated = await gunzip(raw);
|
|
2019
|
+
if (inflated === void 0) return void 0;
|
|
2020
|
+
raw = inflated;
|
|
2021
|
+
}
|
|
2022
|
+
return fromText(utf82.decode(raw), compression);
|
|
2023
|
+
};
|
|
2024
|
+
var decoded = /* @__PURE__ */ new WeakMap();
|
|
2025
|
+
var decodePostHogBody = (request) => {
|
|
2026
|
+
const cached = decoded.get(request);
|
|
2027
|
+
if (cached) return cached;
|
|
2028
|
+
const pending = request.method === "GET" || request.method === "HEAD" || request.body === null ? Promise.resolve(void 0) : request.clone().arrayBuffer().then(
|
|
2029
|
+
(buffer) => decodeBytes(new Uint8Array(buffer), request.headers, new URL(request.url))
|
|
2030
|
+
).catch(() => void 0);
|
|
2031
|
+
decoded.set(request, pending);
|
|
2032
|
+
return pending;
|
|
2033
|
+
};
|
|
2034
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2035
|
+
var nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
2036
|
+
var tokenFromBody = (body) => {
|
|
2037
|
+
if (Array.isArray(body)) return tokenFromBody(body[0]);
|
|
2038
|
+
if (!isRecord4(body)) return void 0;
|
|
2039
|
+
const direct = nonEmpty(body.token) ?? nonEmpty(body.api_key);
|
|
2040
|
+
if (direct) return direct;
|
|
2041
|
+
if (Array.isArray(body.batch)) return tokenFromBody(body.batch[0]);
|
|
2042
|
+
if (isRecord4(body.properties)) return nonEmpty(body.properties.token);
|
|
2043
|
+
return void 0;
|
|
2044
|
+
};
|
|
2045
|
+
var ARRAY_PATH = /^\/array\/([^/]+)\/config(\.js)?\/?$/;
|
|
2046
|
+
var requestToken = async (request, path) => {
|
|
2047
|
+
const url = new URL(request.url);
|
|
2048
|
+
const fromPath = ARRAY_PATH.exec(path)?.[1];
|
|
2049
|
+
if (fromPath) return decodeURIComponent(fromPath);
|
|
2050
|
+
const fromQuery = nonEmpty(url.searchParams.get("token")) ?? nonEmpty(url.searchParams.get("api_key"));
|
|
2051
|
+
if (fromQuery) return fromQuery;
|
|
2052
|
+
return tokenFromBody(await decodePostHogBody(request));
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
// src/flags.ts
|
|
2056
|
+
var evaluateFlag = (flag, subject) => {
|
|
2057
|
+
if (!flag.active || flag.deleted) return void 0;
|
|
2058
|
+
const email = typeof subject.person_properties?.email === "string" ? subject.person_properties.email.trim().toLowerCase() : void 0;
|
|
2059
|
+
for (const [index, override] of flag.overrides.entries()) {
|
|
2060
|
+
const byId = override.distinct_id !== void 0 && override.distinct_id === subject.distinct_id;
|
|
2061
|
+
const byEmail = override.email !== void 0 && email !== void 0 && override.email.toLowerCase() === email;
|
|
2062
|
+
if (byId || byEmail) {
|
|
2063
|
+
return {
|
|
2064
|
+
key: flag.key,
|
|
2065
|
+
value: override.value,
|
|
2066
|
+
payload: override.payload !== void 0 ? override.payload : flag.payload,
|
|
2067
|
+
conditionIndex: index,
|
|
2068
|
+
flag
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (flag.default === null) return void 0;
|
|
2073
|
+
return {
|
|
2074
|
+
key: flag.key,
|
|
2075
|
+
value: flag.default,
|
|
2076
|
+
payload: flag.payload,
|
|
2077
|
+
conditionIndex: flag.overrides.length,
|
|
2078
|
+
flag
|
|
2079
|
+
};
|
|
2080
|
+
};
|
|
2081
|
+
var enabledOf = (value) => value !== false;
|
|
2082
|
+
var flagDetail = (evaluation) => {
|
|
2083
|
+
const enabled = enabledOf(evaluation.value);
|
|
2084
|
+
return {
|
|
2085
|
+
key: evaluation.key,
|
|
2086
|
+
enabled,
|
|
2087
|
+
variant: typeof evaluation.value === "string" ? evaluation.value : null,
|
|
2088
|
+
reason: enabled ? {
|
|
2089
|
+
code: "condition_match",
|
|
2090
|
+
condition_index: evaluation.conditionIndex,
|
|
2091
|
+
description: `Matched condition set ${evaluation.conditionIndex + 1}`
|
|
2092
|
+
} : {
|
|
2093
|
+
code: "no_condition_match",
|
|
2094
|
+
condition_index: null,
|
|
2095
|
+
description: "No matching condition set"
|
|
2096
|
+
},
|
|
2097
|
+
metadata: {
|
|
2098
|
+
id: evaluation.flag.id,
|
|
2099
|
+
version: evaluation.flag.version,
|
|
2100
|
+
description: evaluation.flag.name || null,
|
|
2101
|
+
...enabled && evaluation.payload !== null ? { payload: evaluation.payload } : {}
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
};
|
|
2105
|
+
var legacyMaps = (evaluations) => {
|
|
2106
|
+
const featureFlags = {};
|
|
2107
|
+
const featureFlagPayloads = {};
|
|
2108
|
+
for (const evaluation of evaluations) {
|
|
2109
|
+
featureFlags[evaluation.key] = evaluation.value;
|
|
2110
|
+
if (enabledOf(evaluation.value) && evaluation.payload !== null) {
|
|
2111
|
+
featureFlagPayloads[evaluation.key] = evaluation.payload;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return { featureFlags, featureFlagPayloads };
|
|
2115
|
+
};
|
|
2116
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2117
|
+
var payloadString = (payload) => payload === void 0 || payload === null ? null : JSON.stringify(payload);
|
|
2118
|
+
var parsePayload2 = (payload) => {
|
|
2119
|
+
if (payload === null) return null;
|
|
2120
|
+
try {
|
|
2121
|
+
return JSON.parse(payload);
|
|
2122
|
+
} catch {
|
|
2123
|
+
return payload;
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
var adminView = (flag) => ({
|
|
2127
|
+
key: flag.key,
|
|
2128
|
+
id: flag.id,
|
|
2129
|
+
name: flag.name,
|
|
2130
|
+
active: flag.active,
|
|
2131
|
+
default: flag.default,
|
|
2132
|
+
payload: parsePayload2(flag.payload),
|
|
2133
|
+
overrides: flag.overrides.map((o) => ({
|
|
2134
|
+
...o.distinct_id !== void 0 ? { distinct_id: o.distinct_id } : {},
|
|
2135
|
+
...o.email !== void 0 ? { email: o.email } : {},
|
|
2136
|
+
value: o.value,
|
|
2137
|
+
...o.payload !== void 0 ? { payload: parsePayload2(o.payload) } : {}
|
|
2138
|
+
})),
|
|
2139
|
+
version: flag.version,
|
|
2140
|
+
updated_at: flag.updated_at
|
|
2141
|
+
});
|
|
2142
|
+
var isValue = (value) => typeof value === "boolean" || typeof value === "string" && value.length > 0;
|
|
2143
|
+
var parseFlagSpec = (body) => {
|
|
2144
|
+
if (body === void 0) return { default: null, payload: null, overrides: [] };
|
|
2145
|
+
if (!isRecord5(body)) return "expected a JSON object";
|
|
2146
|
+
const value = body.default ?? body.value;
|
|
2147
|
+
if (value !== void 0 && value !== null && !isValue(value)) {
|
|
2148
|
+
return "default must be true, false, a variant string, or null (absent)";
|
|
2149
|
+
}
|
|
2150
|
+
const overrides = [];
|
|
2151
|
+
if (body.overrides !== void 0) {
|
|
2152
|
+
if (!Array.isArray(body.overrides)) return "overrides must be a list";
|
|
2153
|
+
for (const each of body.overrides) {
|
|
2154
|
+
if (!isRecord5(each)) return "each override is {distinct_id?|email?, value, payload?}";
|
|
2155
|
+
const id = each.distinct_id ?? each.distinctId;
|
|
2156
|
+
const email = each.email;
|
|
2157
|
+
if (id === void 0 && email === void 0) return "each override needs distinct_id or email";
|
|
2158
|
+
if (id !== void 0 && typeof id !== "string" && typeof id !== "number")
|
|
2159
|
+
return "override distinct_id must be a string";
|
|
2160
|
+
if (email !== void 0 && typeof email !== "string") return "override email must be a string";
|
|
2161
|
+
const overrideValue = each.value ?? true;
|
|
2162
|
+
if (!isValue(overrideValue)) return "override value must be true, false or a variant string";
|
|
2163
|
+
overrides.push({
|
|
2164
|
+
...id !== void 0 ? { distinct_id: String(id) } : {},
|
|
2165
|
+
...typeof email === "string" ? { email } : {},
|
|
2166
|
+
value: overrideValue,
|
|
2167
|
+
..."payload" in each ? { payload: payloadString(each.payload) } : {}
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (body.active !== void 0 && typeof body.active !== "boolean") return "active must be boolean";
|
|
2172
|
+
if (body.name !== void 0 && typeof body.name !== "string") return "name must be a string";
|
|
2173
|
+
return {
|
|
2174
|
+
...typeof body.name === "string" ? { name: body.name } : {},
|
|
2175
|
+
...typeof body.active === "boolean" ? { active: body.active } : {},
|
|
2176
|
+
default: value === void 0 ? null : value,
|
|
2177
|
+
payload: payloadString(body.payload),
|
|
2178
|
+
overrides
|
|
2179
|
+
};
|
|
2180
|
+
};
|
|
2181
|
+
var restView = (flag) => {
|
|
2182
|
+
const groups = flag.overrides.map((o) => ({
|
|
2183
|
+
properties: [
|
|
2184
|
+
{
|
|
2185
|
+
key: o.distinct_id !== void 0 ? "distinct_id" : "email",
|
|
2186
|
+
type: "person",
|
|
2187
|
+
operator: "exact",
|
|
2188
|
+
value: [o.distinct_id ?? o.email ?? ""]
|
|
2189
|
+
}
|
|
2190
|
+
],
|
|
2191
|
+
rollout_percentage: o.value === false ? 0 : 100,
|
|
2192
|
+
variant: typeof o.value === "string" ? o.value : null
|
|
2193
|
+
}));
|
|
2194
|
+
if (flag.default !== null) {
|
|
2195
|
+
groups.push({
|
|
2196
|
+
properties: [],
|
|
2197
|
+
rollout_percentage: flag.default === false ? 0 : 100,
|
|
2198
|
+
variant: typeof flag.default === "string" ? flag.default : null
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
const variants = [
|
|
2202
|
+
...new Set(
|
|
2203
|
+
[flag.default, ...flag.overrides.map((o) => o.value)].filter(
|
|
2204
|
+
(v) => typeof v === "string"
|
|
2205
|
+
)
|
|
2206
|
+
)
|
|
2207
|
+
];
|
|
2208
|
+
const payloads = {};
|
|
2209
|
+
if (flag.payload !== null) {
|
|
2210
|
+
payloads[typeof flag.default === "string" ? flag.default : "true"] = flag.payload;
|
|
2211
|
+
}
|
|
2212
|
+
return {
|
|
2213
|
+
id: flag.id,
|
|
2214
|
+
key: flag.key,
|
|
2215
|
+
name: flag.name,
|
|
2216
|
+
active: flag.active,
|
|
2217
|
+
deleted: flag.deleted,
|
|
2218
|
+
created_at: flag.created_at,
|
|
2219
|
+
updated_at: flag.updated_at,
|
|
2220
|
+
version: flag.version,
|
|
2221
|
+
filters: {
|
|
2222
|
+
groups,
|
|
2223
|
+
multivariate: variants.length > 0 ? {
|
|
2224
|
+
variants: variants.map((key, index) => ({
|
|
2225
|
+
key,
|
|
2226
|
+
rollout_percentage: Math.floor(100 / variants.length) + (index < 100 % variants.length ? 1 : 0)
|
|
2227
|
+
}))
|
|
2228
|
+
} : null,
|
|
2229
|
+
payloads
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
};
|
|
2233
|
+
var propertyValues = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" || typeof v === "number").map(String) : typeof value === "string" || typeof value === "number" ? [String(value)] : [];
|
|
2234
|
+
var fromFilters = (filters) => {
|
|
2235
|
+
const record = isRecord5(filters) ? filters : {};
|
|
2236
|
+
const groups = Array.isArray(record.groups) ? record.groups.filter(isRecord5) : [];
|
|
2237
|
+
const multivariate = isRecord5(record.multivariate) ? record.multivariate : void 0;
|
|
2238
|
+
const variants = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(isRecord5) : [];
|
|
2239
|
+
const topVariant = [...variants].sort((a, b) => Number(b.rollout_percentage ?? 0) - Number(a.rollout_percentage ?? 0)).map((v) => v.key).find((key) => typeof key === "string" && key.length > 0);
|
|
2240
|
+
const on = (group) => {
|
|
2241
|
+
const rollout = group.rollout_percentage ?? 100;
|
|
2242
|
+
if (typeof rollout === "number" && rollout <= 0) return false;
|
|
2243
|
+
if (typeof group.variant === "string" && group.variant) return group.variant;
|
|
2244
|
+
return topVariant ?? true;
|
|
2245
|
+
};
|
|
2246
|
+
const overrides = [];
|
|
2247
|
+
let fallback = groups.length === 0 ? false : null;
|
|
2248
|
+
for (const group of groups) {
|
|
2249
|
+
const properties = Array.isArray(group.properties) ? group.properties.filter(isRecord5) : [];
|
|
2250
|
+
if (properties.length === 0) {
|
|
2251
|
+
const rollout = group.rollout_percentage ?? 100;
|
|
2252
|
+
const value = typeof rollout === "number" && rollout < 100 ? false : on(group);
|
|
2253
|
+
if (fallback === null || fallback === false) fallback = value;
|
|
2254
|
+
continue;
|
|
2255
|
+
}
|
|
2256
|
+
for (const property of properties) {
|
|
2257
|
+
if (property.key !== "distinct_id" && property.key !== "email") continue;
|
|
2258
|
+
for (const each of propertyValues(property.value)) {
|
|
2259
|
+
overrides.push(
|
|
2260
|
+
property.key === "distinct_id" ? { distinct_id: each, value: on(group) } : { email: each, value: on(group) }
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (fallback === null) fallback = false;
|
|
2266
|
+
const payloads = isRecord5(record.payloads) ? record.payloads : {};
|
|
2267
|
+
const rawPayload = typeof fallback === "string" && fallback in payloads ? payloads[fallback] : payloads.true;
|
|
2268
|
+
const payload = rawPayload === void 0 || rawPayload === null ? null : typeof rawPayload === "string" ? rawPayload : JSON.stringify(rawPayload);
|
|
2269
|
+
return { default: fallback, payload, overrides };
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// src/generated/openapi.ts
|
|
2273
|
+
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"PostHog feature flags, capture and management API (Mockingbird subset)","description":"Stateful mock subset of PostHog: remote flag evaluation (\`/flags\` v2 and the legacy\\n\`/decide\` shape), remote config, event capture (\`/batch/\`, \`/e/\`, \`/i/v0/e/\`), session\\nrecording intake, the posthog-js asset and survey endpoints, and the slice of the\\nmanagement API our tooling and crons call (feature-flag list/create/patch, HogQL query).\\nHand-authored from the wire shapes of posthog-node 5.52.2 / @posthog/core 1.54.0,\\nposthog-js 1.433.2 and our own raw fetches.\\n\\nPaths are written without the trailing slash PostHog uses; the mock answers both\\n(\`/flags/?v=2\` from the SDKs and \`/flags?v=2\` from the EMR frontend's raw fetch).\\nCapture and flag bodies may arrive gzip-compressed (\`Content-Encoding: gzip\`, or raw gzip\\nbytes from posthog-js), base64 (\`data=\` form bodies), or as plain JSON; the contract\\ndeclares the decoded JSON.\\n","version":"1","x-mockingbird-upstream":{"note":"Hand-authored from @posthog/core posthog-core-stateless (getFlags, sendBatch, getRemoteConfig, getSurveysStateless), posthog-js request.ts / posthog-featureflags.ts, and the consumer's posthog-server.adapter.ts, posthog-server.ts (EMR frontend), website-posthog-purchase.sink.ts, posthog-hogql.client.ts and feature-flags-cli."}},"servers":[{"url":"https://us.i.posthog.com"}],"paths":{"/flags":{"post":{"operationId":"EvaluateFlags","description":"Remote flag evaluation. \`v=2\` (what every current SDK sends) answers the \`flags\` detail\\nmap; no \`v\` or \`v=1\` answers the legacy \`featureFlags\`/\`featureFlagPayloads\` maps.\\n\`config=true\` adds the remote-config fields. The project key is \`token\` (SDKs) or\\n\`api_key\` (the EMR frontend's raw fetch).\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"},{"$ref":"#/components/parameters/Config"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/decide":{"post":{"operationId":"Decide","description":"The legacy endpoint (makor's Python client sends \`POST /decide/?v=3\` with\\n\`{api_key, distinct_id}\`). \`v\` < 4 answers the legacy maps; \`v=4\` the \`flags\` map.\\n","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/Version"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsRequest"}}}},"responses":{"200":{"description":"Evaluated flags (legacy shape)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagsResponse"}}}},"400":{"description":"Malformed body or missing distinct_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"429":{"description":"Rate limited (fault preset flags_429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault preset flags_5xx)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/array/{token}/config":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfig","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config (posthog-react-native, @posthog/core)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoteConfig"}}}}}}},"/array/{token}/config.js":{"parameters":[{"$ref":"#/components/parameters/TokenPath"}],"get":{"operationId":"GetRemoteConfigScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Remote config as a script that sets \`window._POSTHOG_REMOTE_CONFIG[token]\` (posthog-js)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/batch":{"post":{"operationId":"CaptureBatch","description":"posthog-node and posthog-react-native flushes (Content-Encoding: gzip).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/e":{"post":{"operationId":"CaptureEvent","description":"One event, an array of events, or \`{api_key, batch}\` (posthog-js).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/i/v0/e":{"post":{"operationId":"CaptureEventV0","description":"The capture endpoint remote config advertises (\`analytics.endpoint\`); also our backend's website purchase sink (\`{api_key, event, distinct_id, timestamp, uuid, properties}\`).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}},"400":{"description":"Malformed body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Server error (fault)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/s":{"post":{"operationId":"CaptureRecording","description":"Session-recording snapshots; counted, never stored.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureRequest"}}}},"responses":{"200":{"description":"Accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptureResponse"}}}}}}},"/static/recorder.js":{"get":{"operationId":"GetRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/static/{version}/recorder.js":{"parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$"}}],"get":{"operationId":"GetVersionedRecorderScript","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The versioned session recorder bundle (an inert script here)","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/api/surveys":{"get":{"operationId":"ListSurveys","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No surveys","content":{"application/json":{"schema":{"type":"object","required":["surveys"],"properties":{"surveys":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/web_experiments":{"get":{"operationId":"ListWebExperiments","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"$ref":"#/components/parameters/TokenQuery"}],"responses":{"200":{"description":"No web experiments","content":{"application/json":{"schema":{"type":"object","required":["experiments"],"properties":{"experiments":{"type":"array","maxItems":0}}}}}},"401":{"description":"No project API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"ListFeatureFlags","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"maximum":1000}}],"responses":{"200":{"description":"One page of flags (feature-flags-cli follows \`next\`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlagPage"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}},"post":{"operationId":"CreateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeatureFlagBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid, or the key already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/feature_flags/{flagId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"flagId","in":"path","required":true,"description":"The flag's numeric id. The mock also accepts the flag key (how self-parity walks reference flags they created, since resource identities are strings).","schema":{"type":"string","x-mockingbird-resource-ref":{"type":"feature_flag","missing":"999999"}}}],"patch":{"operationId":"UpdateFeatureFlag","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeatureFlagBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureFlag"}}}},"400":{"description":"Invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"No such flag","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/projects/{projectId}/query":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"RunQuery","description":"HogQL (the marketing-metrics crons). Answers canned results set with \`PUT /__admin/settings {\\"queryResults\\": [...]}\`; \`{results: []}\` otherwise.","security":[{"bearerAuth":[]}],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"Query results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"400":{"description":"Not a HogQL query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"No personal API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"Version":{"name":"v","in":"query","schema":{"type":"integer","minimum":1,"maximum":4}},"Config":{"name":"config","in":"query","schema":{"type":"string","enum":["true","false"]}},"TokenPath":{"name":"token","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"TokenQuery":{"name":"token","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"}},"ProjectId":{"name":"projectId","in":"path","required":true,"schema":{"type":"integer","minimum":1,"maximum":999999999}}},"schemas":{"ErrorBody":{"type":"object","required":["type","code","detail"],"properties":{"type":{"type":"string"},"code":{"type":"string"},"detail":{"type":"string"},"attr":{"type":["string","null"]}}},"FlagsRequest":{"type":"object","required":["distinct_id"],"properties":{"token":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"groups":{"type":"object"},"person_properties":{"type":"object","properties":{"email":{"type":"string","maxLength":120}}},"group_properties":{"type":"object"},"$device_id":{"type":"string","maxLength":64},"$anon_distinct_id":{"type":"string","maxLength":64},"evaluation_contexts":{"type":"array","maxItems":5,"items":{"type":"string","maxLength":40}},"evaluation_runtime":{"type":"string","enum":["server","client","all"]},"flag_keys_to_evaluate":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":64}},"geoip_disable":{"type":"boolean"},"disable_flags":{"type":"boolean"}}},"FlagDetail":{"type":"object","required":["key","enabled","variant","reason","metadata"],"properties":{"key":{"type":"string"},"enabled":{"type":"boolean"},"variant":{"type":["string","null"]},"failed":{"type":"boolean"},"reason":{"type":"object","required":["code","description"],"properties":{"code":{"type":"string"},"condition_index":{"type":["integer","null"]},"description":{"type":"string"}}},"metadata":{"type":"object","required":["id","version"],"properties":{"id":{"type":"integer"},"version":{"type":"integer"},"description":{"type":["string","null"]},"payload":{"type":"string","description":"The payload as a JSON string."}}}}},"FlagsResponse":{"type":"object","required":["errorsWhileComputingFlags"],"properties":{"flags":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/FlagDetail"}},"featureFlags":{"type":"object","additionalProperties":{"type":["boolean","string"]}},"featureFlagPayloads":{"type":"object","additionalProperties":{"type":"string"}},"errorsWhileComputingFlags":{"type":"boolean"},"quotaLimited":{"type":"array","items":{"type":"string"}},"requestId":{"type":"string","x-mockingbird-volatile":{"kind":"id"}},"evaluatedAt":{"type":"integer","x-mockingbird-volatile":{"kind":"timestamp"}},"config":{"type":"object"},"supportedCompression":{"type":"array","items":{"type":"string"}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object"}]}}},"RemoteConfig":{"type":"object","required":["token","supportedCompression","hasFeatureFlags","analytics","sessionRecording"],"properties":{"token":{"type":"string"},"supportedCompression":{"type":"array","items":{"type":"string"}},"hasFeatureFlags":{"type":"boolean"},"analytics":{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}},"sessionRecording":{"oneOf":[{"type":"boolean"},{"type":"object","required":["endpoint"],"properties":{"endpoint":{"type":"string"}}}]},"surveys":{"type":"boolean"},"heatmaps":{"type":"boolean"},"siteApps":{"type":"array"}}},"CapturedEventBody":{"type":"object","required":["event"],"properties":{"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"BatchRequest":{"type":"object","required":["api_key","batch"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"batch":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/CapturedEventBody"}},"sent_at":{"type":"string","format":"date-time"}}},"CaptureRequest":{"type":"object","required":["api_key","event"],"properties":{"api_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"event":{"type":"string","minLength":1,"maxLength":64},"distinct_id":{"type":"string","minLength":1,"maxLength":64},"properties":{"type":"object"},"timestamp":{"type":"string","format":"date-time"},"uuid":{"type":"string","format":"uuid"}}},"CaptureResponse":{"type":"object","required":["status"],"properties":{"status":{"oneOf":[{"type":"string"},{"type":"integer"}]}}},"FeatureFlag":{"type":"object","required":["id","key","name","active","filters"],"properties":{"id":{"type":"integer"},"key":{"type":"string","x-mockingbird-resource":{"type":"feature_flag","identity":true}},"name":{"type":"string"},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"version":{"type":"integer"},"filters":{"type":"object","required":["groups"],"properties":{"groups":{"type":"array","items":{"type":"object"}},"multivariate":{"oneOf":[{"type":"null"},{"type":"object"}]},"payloads":{"type":"object"}}}}},"FeatureFlagPage":{"type":"object","required":["count","next","previous","results"],"properties":{"count":{"type":"integer"},"next":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"previous":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"url"}},"results":{"type":"array","items":{"$ref":"#/components/schemas/FeatureFlag"}}}},"FlagGroup":{"type":"object","properties":{"properties":{"type":"array","maxItems":2,"items":{"type":"object","required":["key","value"],"properties":{"key":{"type":"string","enum":["distinct_id","email"]},"type":{"type":"string","enum":["person"]},"operator":{"type":"string","enum":["exact"]},"value":{"type":"array","maxItems":3,"items":{"type":"string","minLength":1,"maxLength":40}}}}},"rollout_percentage":{"type":"integer","minimum":0,"maximum":100},"variant":{"type":["string","null"],"maxLength":20}}},"FlagFilters":{"type":"object","properties":{"groups":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/FlagGroup"}},"payloads":{"type":"object","properties":{"true":{"type":"string","maxLength":200}}}}},"CreateFeatureFlagBody":{"type":"object","required":["key"],"properties":{"key":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$"},"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"UpdateFeatureFlagBody":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"active":{"type":"boolean"},"deleted":{"type":"boolean"},"filters":{"$ref":"#/components/schemas/FlagFilters"}}},"QueryRequest":{"type":"object","required":["query"],"properties":{"query":{"type":"object","required":["kind","query"],"properties":{"kind":{"type":"string","enum":["HogQLQuery"]},"query":{"type":"string","minLength":1,"maxLength":500}}}}},"QueryResponse":{"type":"object","required":["results"],"properties":{"results":{"type":"array","items":{"type":"array"}},"columns":{"type":"array","items":{"type":"string"}},"is_cached":{"type":"boolean"}}}}}}`);
|
|
2274
|
+
var operationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
|
|
2275
|
+
var supportedOperationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
|
|
2276
|
+
|
|
2277
|
+
// src/state.ts
|
|
2278
|
+
var DEFAULT_SETTINGS = {
|
|
2279
|
+
sessionRecording: false,
|
|
2280
|
+
queryResults: [],
|
|
2281
|
+
generation: 0,
|
|
2282
|
+
recordings: 0
|
|
2283
|
+
};
|
|
2284
|
+
var PostHogState = class {
|
|
2285
|
+
constructor(sqlite, namespace, seed, now) {
|
|
2286
|
+
this.seed = seed;
|
|
2287
|
+
this.now = now;
|
|
2288
|
+
this.flags = new Collection(sqlite, namespace, "flags");
|
|
2289
|
+
this.events = new Collection(sqlite, namespace, "events");
|
|
2290
|
+
this.settings = new Collection(sqlite, namespace, "settings");
|
|
2291
|
+
this.ids = new IdSequence(sqlite, namespace, "posthog");
|
|
2292
|
+
this.ensureSeeded();
|
|
2293
|
+
}
|
|
2294
|
+
seed;
|
|
2295
|
+
now;
|
|
2296
|
+
flags;
|
|
2297
|
+
events;
|
|
2298
|
+
settings;
|
|
2299
|
+
ids;
|
|
2300
|
+
/** Re-apply the seed flags and settings after a reset. */
|
|
2301
|
+
ensureSeeded() {
|
|
2302
|
+
if (!this.settings.has("settings")) {
|
|
2303
|
+
this.settings.insert("settings", { ...DEFAULT_SETTINGS, ...this.seed.settings });
|
|
2304
|
+
for (const [key, spec] of Object.entries(this.seed.flags)) this.putFlag(key, spec);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
current() {
|
|
2308
|
+
return this.settings.get("settings") ?? DEFAULT_SETTINGS;
|
|
2309
|
+
}
|
|
2310
|
+
update(patch) {
|
|
2311
|
+
const next = { ...this.current(), ...patch };
|
|
2312
|
+
this.settings.insert("settings", next);
|
|
2313
|
+
return next;
|
|
2314
|
+
}
|
|
2315
|
+
/** Flags in creation order (PostHog lists by id). */
|
|
2316
|
+
listFlags() {
|
|
2317
|
+
return this.flags.list({ order: "oldest" }).map((row) => row.value).sort((a, b) => a.id - b.id);
|
|
2318
|
+
}
|
|
2319
|
+
findFlag(idOrKey) {
|
|
2320
|
+
return this.flags.get(idOrKey) ?? this.listFlags().find((flag) => String(flag.id) === idOrKey && !flag.deleted);
|
|
2321
|
+
}
|
|
2322
|
+
nextFlagId() {
|
|
2323
|
+
return this.listFlags().reduce((max, flag) => Math.max(max, flag.id), 0) + 1;
|
|
2324
|
+
}
|
|
2325
|
+
/** Create or replace a flag (keeping its id, bumping its version). */
|
|
2326
|
+
putFlag(key, spec) {
|
|
2327
|
+
const existing = this.flags.get(key);
|
|
2328
|
+
const iso = new Date(this.now()).toISOString();
|
|
2329
|
+
const record = {
|
|
2330
|
+
id: existing?.id ?? this.nextFlagId(),
|
|
2331
|
+
key,
|
|
2332
|
+
name: spec.name ?? existing?.name ?? "",
|
|
2333
|
+
active: spec.active ?? true,
|
|
2334
|
+
deleted: false,
|
|
2335
|
+
default: spec.default,
|
|
2336
|
+
payload: spec.payload,
|
|
2337
|
+
overrides: spec.overrides,
|
|
2338
|
+
version: (existing?.version ?? 0) + 1,
|
|
2339
|
+
created_at: existing?.created_at ?? iso,
|
|
2340
|
+
updated_at: iso
|
|
2341
|
+
};
|
|
2342
|
+
this.flags.insert(key, record);
|
|
2343
|
+
return record;
|
|
2344
|
+
}
|
|
2345
|
+
patchFlag(key, patch) {
|
|
2346
|
+
const existing = this.flags.get(key);
|
|
2347
|
+
if (!existing) return void 0;
|
|
2348
|
+
const record = {
|
|
2349
|
+
...existing,
|
|
2350
|
+
...patch,
|
|
2351
|
+
version: existing.version + 1,
|
|
2352
|
+
updated_at: new Date(this.now()).toISOString()
|
|
2353
|
+
};
|
|
2354
|
+
this.flags.insert(key, record);
|
|
2355
|
+
return record;
|
|
2356
|
+
}
|
|
2357
|
+
nextEventId() {
|
|
2358
|
+
return this.ids.next("evt_", 20);
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
// src/flag-state-fixture.ts
|
|
2363
|
+
var GEVITI_FLAG_STATE = JSON.parse(
|
|
2364
|
+
`{"generatedAt":"2026-09-14T22:32:06.078Z","flags":[{"flag":"advanced-hormone-pdfs","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"rollout 0","variants":[]}]}},{"flag":"ai-chat-bloodwork-scheduling","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-books-mp-visits","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"rollout 0","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"rollout 0","variants":[]}]}},{"flag":"ai-chat-care-thread-writes","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-expanded-reads","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-genetics","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"ai-chat-meal-planner","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"ai-chat-model-sonnet-4-6","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"ai-chat-proactive","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-rx-prescreen","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-supplement-writes","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chat-tracker-writes","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"ai-chat-voice","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"ai-chat-wearable-actions","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"ai-chatbot","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"app-intro-walkthrough","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"b2b-member-tagging","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]},{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]},{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"backend-analytics","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"billing-renewal-reminders-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"bloodwork-duplicate-draw-enabled","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"blueprint-modify-v2","posthog":{"dev":[],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]},{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"blueprint-tab","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"body-scan-native-capture","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"body-scan-setup-redesign","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"body-scanning","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"cancellation-adverse-reaction-note","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"care-hub-bug-tracking","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[]}},{"flag":"care-hub-multi-channel-send","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[]}},{"flag":"chat-auto-approve","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"chat-ui-refresh","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"chat-voice-auto-listen","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"chat-web-fab","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"checkout-addon-upsell","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[{"variant":"bump","rolloutPercentage":25},{"variant":"control","rolloutPercentage":25},{"variant":"post","rolloutPercentage":25},{"variant":"step","rolloutPercentage":25}]}],"prod":[]}},{"flag":"checkout-summary-refresh","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"cio-blood-draw-date-sync-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"cio-first-delivery-signals-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"cio-lifecycle-sync-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"targeted","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"cio-p2-events-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"cultureapothecary-checkout","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[{"variant":"checkout","rolloutPercentage":0},{"variant":"control","rolloutPercentage":100}]}],"prod":[]}},{"flag":"customer-io-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"targeted","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"customer-io-notifications","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"customerio-p0-events","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"cycle-tracking","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"daily-check-in-home-screen-card","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"daily-rings","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"daily-tracker-bundle","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"ramping","variants":[]}]}},{"flag":"developer-ui","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"document-preview-proxy","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"dynamic-banner-legacy-member-app","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"dynamic-banner-member-app","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"emr-lifefile-credentials","posthog":{"dev":[],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"emr-lifefile-draft-tasks","posthog":{"dev":[],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"emr-live-notifications","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"targeted","variants":[]}]}},{"flag":"emr-order-itemized-invoices","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"emr-request-intake","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"inactive","variants":[]}]}},{"flag":"emr-vpi-credentials","posthog":{"dev":[],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"emr-wearables","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"ensure-order-failure-visibility","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"erx-portal-agent-fulfillment-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"family-at-checkout","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"family-plans","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"family-plans-adults","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"family-plans-kids","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"flex-hsa-fsa-payments","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"flex-membership-switch","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"force-update","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"formbricks-contract-fail-open","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"forms-answer-reuse","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[]}},{"flag":"forms-profile-ingestion","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[]}},{"flag":"fullscript-labs","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"inactive","variants":[]}]}},{"flag":"fullscript-labs-erx","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"inactive","variants":[]}]}},{"flag":"gamification-legacy-member-app","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"gamification-v2-circle-stats","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"gamification-v2-circles","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"gamification-v2-core","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"gamification-v2-leagues","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[]}},{"flag":"genetic-report-pdf-download-button","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"genetic-report-results","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"genetic-test-view-results","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"genomics-order-attention-modal","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"genomics-preorder","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"geviti-pdf-actions","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"geviti-pdf-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"geviti-pdf-shadow","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"rollout 0","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"rollout 0","variants":[]}]}},{"flag":"health-checkin-dead-end","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"health-learning","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"inactive","variants":[]}]}},{"flag":"healthkit-steps-sync","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"home-marketing-banner","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"hormone-staging-control","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"rollout 0","variants":[]}]}},{"flag":"in-house-chat","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]},{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"targeted","variants":[]},{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"in-page-checkout","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"inhouse-forms-assignments","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"inhouse-forms-shim","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"inhouse-forms-store","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"kids-care-plans","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[]}},{"flag":"kids-waitlist","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"kids-waitlist-joined","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"targeted","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"kill-bill-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"kill-bill-flex-memberships","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"lifecycle-events-enabled","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"living-profile-blueprint-source","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"living-profile-chat-updates","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"living-profile-checkins","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"living-profile-classic-submit","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"living-profile-store","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"targeted","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"maintenance-mode","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"makor-intro-walkthrough","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"meal-planner","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"member-item-hiding","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"member-rx-peptides","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"member-success-polish","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"mobile-smart-links","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"mobile-ui-rework","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"new-member-app-rollout","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"notif-bloodwork-review-nudges-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"notif-progress-onboarding-triggers-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"notif-rx-last-day-reminder-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"notif-shipping-billing-triggers-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"nova-sonic-health-intake","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"nutrition-analysis-ranking","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"nutrition-food-search","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"nutrition-history-nav","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"nutrition-logging","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"nutrition-ui-polish","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"odx-pdf-hidden","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}},{"flag":"paid-tier-intro-walkthrough","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[{"variant":"control","rolloutPercentage":50},{"variant":"test","rolloutPercentage":50}]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[{"variant":"control","rolloutPercentage":50},{"variant":"test","rolloutPercentage":50}]}]}},{"flag":"patient-details-genomics-tab","posthog":{"dev":[{"project":"emr","projectId":495610,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"targeted","variants":[]}]}},{"flag":"payment-history-v2","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"pdf-multi-draw-comparative","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"prepaid-checkout","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"inactive","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"rollout 0","variants":[]}]}},{"flag":"prescription-management","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"protocol-tracking","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"reconcile-by-user-polling","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"referral-link-unavailable-guard","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"rings-v2","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"rx-coupons","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"rx-education-videos","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"rx-failed-payment-retry","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"rx-followup-labs","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"rx-protocol-tab","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"rx-provider-editing","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]},{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"screen-time-shielding","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"session-replay","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"shop-coupons","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"shop-gi-360-free","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"rollout 0","variants":[]}]}},{"flag":"shop-walk-in-serviceability-gate","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"signup-analytics-enqueue-enabled","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"rollout 0","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"smart-link-click-reporting","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"supplement-lock-timeout-alerts","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[]}]}},{"flag":"test-url-2-checkout","posthog":{"dev":[],"prod":[{"project":"member-app","projectId":339666,"state":"live","variants":[{"variant":"bump","rolloutPercentage":0},{"variant":"control","rolloutPercentage":100},{"variant":"no_addons","rolloutPercentage":0},{"variant":"post","rolloutPercentage":0},{"variant":"step","rolloutPercentage":0}]}]}},{"flag":"ui-polish","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"unified-upload-flow","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"visit-reason-tree","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"targeted","variants":[]}]}},{"flag":"walk-in-setup-flow","posthog":{"dev":[{"project":"member-app","projectId":338122,"state":"live","variants":[]}],"prod":[{"project":"member-app","projectId":339666,"state":"inactive","variants":[]}]}},{"flag":"wellness-primary-routing","posthog":{"dev":[],"prod":[{"project":"emr","projectId":495618,"state":"live","variants":[]}]}}]}`
|
|
2365
|
+
);
|
|
2366
|
+
|
|
2367
|
+
// src/import.ts
|
|
2368
|
+
var valueForState = (state, variants = []) => {
|
|
2369
|
+
if (state === "live") {
|
|
2370
|
+
const top = [...variants].sort((a, b) => b.rolloutPercentage - a.rolloutPercentage)[0];
|
|
2371
|
+
return top?.variant ?? true;
|
|
2372
|
+
}
|
|
2373
|
+
if (state === "rollout 0" || state === "targeted" || state === "ramping") return false;
|
|
2374
|
+
return null;
|
|
2375
|
+
};
|
|
2376
|
+
var specsFromState = (file, options) => {
|
|
2377
|
+
const project = options.project ?? "member-app";
|
|
2378
|
+
const specs = {};
|
|
2379
|
+
for (const entry of file.flags) {
|
|
2380
|
+
const row = entry.posthog[options.env]?.find((each) => each.project === project);
|
|
2381
|
+
if (!row) continue;
|
|
2382
|
+
const value = valueForState(row.state, row.variants);
|
|
2383
|
+
if (value === null) continue;
|
|
2384
|
+
specs[entry.flag] = { default: value, payload: null, overrides: [] };
|
|
2385
|
+
}
|
|
2386
|
+
return specs;
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/runtime.ts
|
|
2390
|
+
var FLAG_OPERATIONS = ["EvaluateFlags", "Decide"];
|
|
2391
|
+
var flagRules = (rule) => FLAG_OPERATIONS.map((operationId) => ({ operationId, ...rule }));
|
|
2392
|
+
var POSTHOG_PRESETS = {
|
|
2393
|
+
flags_5xx: {
|
|
2394
|
+
description: "/flags and /decide answer 500 (SDKs return undefined; our adapters fall back)",
|
|
2395
|
+
rules: flagRules({
|
|
2396
|
+
status: 500,
|
|
2397
|
+
body: {
|
|
2398
|
+
type: "server_error",
|
|
2399
|
+
code: "error",
|
|
2400
|
+
detail: "A server error occurred.",
|
|
2401
|
+
attr: null
|
|
2402
|
+
}
|
|
2403
|
+
})
|
|
2404
|
+
},
|
|
2405
|
+
flags_429: {
|
|
2406
|
+
description: "/flags and /decide answer 429 rate_limited",
|
|
2407
|
+
rules: flagRules({
|
|
2408
|
+
status: 429,
|
|
2409
|
+
body: {
|
|
2410
|
+
type: "validation_error",
|
|
2411
|
+
code: "rate_limited",
|
|
2412
|
+
detail: "Rate limit exceeded",
|
|
2413
|
+
attr: null
|
|
2414
|
+
},
|
|
2415
|
+
headers: { "retry-after": "1" }
|
|
2416
|
+
})
|
|
2417
|
+
},
|
|
2418
|
+
flags_hang: {
|
|
2419
|
+
description: "/flags and /decide answer after 1.5 s (trips the backend strict 1 s race and the EMR 1 s race)",
|
|
2420
|
+
rules: flagRules({ latencyMs: 1500 })
|
|
2421
|
+
},
|
|
2422
|
+
errors_while_computing: {
|
|
2423
|
+
description: "/flags answers errorsWhileComputingFlags: true (flags still present)",
|
|
2424
|
+
rules: flagRules({ effect: "errors_while_computing" })
|
|
2425
|
+
},
|
|
2426
|
+
quota_limited: {
|
|
2427
|
+
description: '/flags answers quotaLimited: ["feature_flags"] with no flags',
|
|
2428
|
+
rules: flagRules({ effect: "quota_limited" })
|
|
2429
|
+
},
|
|
2430
|
+
capture_5xx: {
|
|
2431
|
+
description: "Capture endpoints (/batch/, /e/, /i/v0/e/) answer 500",
|
|
2432
|
+
rules: ["CaptureBatch", "CaptureEvent", "CaptureEventV0"].map((operationId) => ({
|
|
2433
|
+
operationId,
|
|
2434
|
+
status: 500,
|
|
2435
|
+
body: { type: "server_error", code: "error", detail: "A server error occurred.", attr: null }
|
|
2436
|
+
}))
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2440
|
+
var adminError3 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2441
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2442
|
+
var parseBulk = (body) => {
|
|
2443
|
+
const source = isRecord6(body) && isRecord6(body.flags) ? body.flags : body;
|
|
2444
|
+
const entries = Array.isArray(source) ? source.map((each) => [isRecord6(each) ? String(each.key ?? "") : "", each]) : isRecord6(source) ? Object.entries(source).filter(([key]) => key !== "replace") : [];
|
|
2445
|
+
if (!Array.isArray(source) && !isRecord6(source)) return "expected {flags: {<key>: spec}}";
|
|
2446
|
+
const specs = {};
|
|
2447
|
+
for (const [key, value] of entries) {
|
|
2448
|
+
if (!key) return "every flag needs a key";
|
|
2449
|
+
const spec = parseFlagSpec(value);
|
|
2450
|
+
if (typeof spec === "string") return `${key}: ${spec}`;
|
|
2451
|
+
specs[key] = spec;
|
|
2452
|
+
}
|
|
2453
|
+
return specs;
|
|
2454
|
+
};
|
|
2455
|
+
var parseQueryResults = (value) => {
|
|
2456
|
+
if (!Array.isArray(value)) return "queryResults: [{match?, columns?, results: [[\u2026]]}]";
|
|
2457
|
+
const out = [];
|
|
2458
|
+
for (const each of value) {
|
|
2459
|
+
if (!isRecord6(each) || !Array.isArray(each.results) || !each.results.every(Array.isArray)) {
|
|
2460
|
+
return "each queryResults entry needs results: unknown[][]";
|
|
2461
|
+
}
|
|
2462
|
+
out.push({
|
|
2463
|
+
...typeof each.match === "string" ? { match: each.match } : {},
|
|
2464
|
+
...Array.isArray(each.columns) ? { columns: each.columns.map(String) } : {},
|
|
2465
|
+
results: each.results
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
return out;
|
|
2469
|
+
};
|
|
2470
|
+
var adminRoutes = (runtime) => {
|
|
2471
|
+
const api = (namespace) => runtime.instance(namespace);
|
|
2472
|
+
return {
|
|
2473
|
+
"GET /flags": ({ namespace }) => json3(200, { flags: api(namespace).flagList() }),
|
|
2474
|
+
"GET /flags/evaluate": ({ url, namespace }) => {
|
|
2475
|
+
const distinctId = url.searchParams.get("distinct_id");
|
|
2476
|
+
if (!distinctId) return adminError3(400, "distinct_id is required");
|
|
2477
|
+
const email = url.searchParams.get("email");
|
|
2478
|
+
const evaluations = api(namespace).evaluate({
|
|
2479
|
+
distinct_id: distinctId,
|
|
2480
|
+
...email ? { person_properties: { email } } : {}
|
|
2481
|
+
});
|
|
2482
|
+
return json3(200, {
|
|
2483
|
+
flags: Object.fromEntries(evaluations.map((e) => [e.key, e.value]))
|
|
2484
|
+
});
|
|
2485
|
+
},
|
|
2486
|
+
"GET /flags/:key": ({ params, namespace }) => {
|
|
2487
|
+
const flag = api(namespace).state.flags.get(params.key);
|
|
2488
|
+
return flag ? json3(200, adminView(flag)) : adminError3(404, `no flag ${params.key}`);
|
|
2489
|
+
},
|
|
2490
|
+
"PUT /flags/:key": ({ params, body, namespace }) => {
|
|
2491
|
+
const spec = parseFlagSpec(body);
|
|
2492
|
+
if (typeof spec === "string") return adminError3(400, spec);
|
|
2493
|
+
return json3(200, adminView(api(namespace).state.putFlag(params.key, spec)));
|
|
2494
|
+
},
|
|
2495
|
+
"DELETE /flags/:key": ({ params, namespace }) => api(namespace).state.flags.delete(params.key) ? json3(200, { deleted: params.key }) : adminError3(404, `no flag ${params.key}`),
|
|
2496
|
+
"PUT /flags": ({ body, namespace }) => {
|
|
2497
|
+
const specs = parseBulk(body);
|
|
2498
|
+
if (typeof specs === "string") return adminError3(400, specs);
|
|
2499
|
+
const state = api(namespace).state;
|
|
2500
|
+
if (isRecord6(body) && body.replace === true) {
|
|
2501
|
+
for (const flag of state.listFlags()) state.flags.delete(flag.key);
|
|
2502
|
+
}
|
|
2503
|
+
for (const [key, spec] of Object.entries(specs)) state.putFlag(key, spec);
|
|
2504
|
+
return json3(200, { flags: api(namespace).flagList() });
|
|
2505
|
+
},
|
|
2506
|
+
"POST /flags/import": ({ body, namespace }) => {
|
|
2507
|
+
const input = isRecord6(body) ? body : {};
|
|
2508
|
+
const env = input.env ?? "dev";
|
|
2509
|
+
if (env !== "dev" && env !== "prod") return adminError3(400, 'env must be "dev" or "prod"');
|
|
2510
|
+
const from = input.from ?? "state.json";
|
|
2511
|
+
let file;
|
|
2512
|
+
if (isRecord6(input.state) && Array.isArray(input.state.flags)) {
|
|
2513
|
+
file = input.state;
|
|
2514
|
+
} else if (from === "state.json") {
|
|
2515
|
+
file = GEVITI_FLAG_STATE;
|
|
2516
|
+
} else {
|
|
2517
|
+
return adminError3(400, 'from must be "state.json" (the bundled copy) or pass "state"');
|
|
2518
|
+
}
|
|
2519
|
+
const project = typeof input.project === "string" ? input.project : void 0;
|
|
2520
|
+
const specs = specsFromState(file, { env, ...project ? { project } : {} });
|
|
2521
|
+
const state = api(namespace).state;
|
|
2522
|
+
if (input.replace === true) {
|
|
2523
|
+
for (const flag of state.listFlags()) state.flags.delete(flag.key);
|
|
2524
|
+
}
|
|
2525
|
+
for (const [key, spec] of Object.entries(specs)) state.putFlag(key, spec);
|
|
2526
|
+
return json3(200, {
|
|
2527
|
+
imported: Object.keys(specs).length,
|
|
2528
|
+
env,
|
|
2529
|
+
project: project ?? "member-app"
|
|
2530
|
+
});
|
|
2531
|
+
},
|
|
2532
|
+
"POST /flags/bump": ({ namespace }) => {
|
|
2533
|
+
const state = api(namespace).state;
|
|
2534
|
+
const next = state.update({ generation: state.current().generation + 1 });
|
|
2535
|
+
return json3(200, {
|
|
2536
|
+
generation: next.generation,
|
|
2537
|
+
note: "Nothing changed server-side. Clear the app's flag caches now (backend 60 s per user, EMR frontend 60 s / 10 s)."
|
|
2538
|
+
});
|
|
2539
|
+
},
|
|
2540
|
+
"GET /events": ({ url, namespace }) => {
|
|
2541
|
+
const since = parseSince(url.searchParams.get("since"));
|
|
2542
|
+
if (since === null) return adminError3(400, "since must be epoch ms or ISO-8601");
|
|
2543
|
+
const distinct = url.searchParams.get("distinct_id");
|
|
2544
|
+
const event = url.searchParams.get("event");
|
|
2545
|
+
return json3(200, {
|
|
2546
|
+
events: api(namespace).events({
|
|
2547
|
+
...distinct !== null ? { distinct_id: distinct } : {},
|
|
2548
|
+
...event !== null ? { event } : {},
|
|
2549
|
+
...since !== void 0 ? { since } : {}
|
|
2550
|
+
})
|
|
2551
|
+
});
|
|
2552
|
+
},
|
|
2553
|
+
"GET /recordings": ({ namespace }) => json3(200, { count: api(namespace).state.current().recordings }),
|
|
2554
|
+
"GET /settings": ({ namespace }) => json3(200, api(namespace).state.current()),
|
|
2555
|
+
"PUT /settings": ({ body, namespace }) => {
|
|
2556
|
+
if (!isRecord6(body)) return adminError3(400, "expected a JSON object");
|
|
2557
|
+
const patch = {};
|
|
2558
|
+
if (body.sessionRecording !== void 0) {
|
|
2559
|
+
if (typeof body.sessionRecording !== "boolean")
|
|
2560
|
+
return adminError3(400, "sessionRecording: boolean");
|
|
2561
|
+
patch.sessionRecording = body.sessionRecording;
|
|
2562
|
+
}
|
|
2563
|
+
if (body.queryResults !== void 0) {
|
|
2564
|
+
const parsed = parseQueryResults(body.queryResults);
|
|
2565
|
+
if (typeof parsed === "string") return adminError3(400, parsed);
|
|
2566
|
+
patch.queryResults = parsed;
|
|
2567
|
+
}
|
|
2568
|
+
return json3(200, api(namespace).state.update(patch));
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
};
|
|
2572
|
+
var createRuntime2 = (options = {}) => {
|
|
2573
|
+
const tokens = /* @__PURE__ */ new WeakMap();
|
|
2574
|
+
const runtime = createRuntime({
|
|
2575
|
+
name: POSTHOG_NAMESPACE,
|
|
2576
|
+
document,
|
|
2577
|
+
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2578
|
+
...options.clock ? { clock: options.clock } : {},
|
|
2579
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
2580
|
+
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
2581
|
+
...options.onLog ? { onLog: options.onLog } : {},
|
|
2582
|
+
credential: (request) => tokens.get(request),
|
|
2583
|
+
presets: POSTHOG_PRESETS,
|
|
2584
|
+
create: ({ sqlite, namespace, clock }) => new PostHogAPI({
|
|
2585
|
+
sqlite,
|
|
2586
|
+
namespace,
|
|
2587
|
+
now: clock.now,
|
|
2588
|
+
...options.flags ? { flags: options.flags } : {},
|
|
2589
|
+
...options.settings ? { settings: options.settings } : {}
|
|
2590
|
+
}),
|
|
2591
|
+
describe: () => ({ flagsImportedFrom: GEVITI_FLAG_STATE.generatedAt ?? null }),
|
|
2592
|
+
admin: adminRoutes
|
|
2593
|
+
});
|
|
2594
|
+
const inner = runtime.fetch;
|
|
2595
|
+
const fetch2 = async (request) => {
|
|
2596
|
+
const path = new URL(request.url).pathname;
|
|
2597
|
+
if (!request.headers.has(NAMESPACE_HEADER) && !path.startsWith("/ns/") && !path.startsWith("/__admin") && path !== "/health") {
|
|
2598
|
+
const token = await requestToken(request, path) ?? bearerToken(request);
|
|
2599
|
+
if (token !== void 0) tokens.set(request, token);
|
|
2600
|
+
}
|
|
2601
|
+
return inner(request);
|
|
2602
|
+
};
|
|
2603
|
+
return Object.assign(runtime, { fetch: fetch2 });
|
|
2604
|
+
};
|
|
2605
|
+
|
|
2606
|
+
// src/index.ts
|
|
2607
|
+
var POSTHOG_NAMESPACE = "posthog";
|
|
2608
|
+
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2609
|
+
var error = (status, type, code, detail, attr) => jsonRes(status, { type, code, detail, attr: attr ?? null });
|
|
2610
|
+
var invalidApiKey = () => error(
|
|
2611
|
+
401,
|
|
2612
|
+
"authentication_error",
|
|
2613
|
+
"invalid_api_key",
|
|
2614
|
+
"Project API key invalid. You can find your project API key in your PostHog project settings."
|
|
2615
|
+
);
|
|
2616
|
+
var malformed = (detail = "Malformed request data") => error(400, "validation_error", "invalid_payload", detail);
|
|
2617
|
+
var remoteConfig = (token, settings) => ({
|
|
2618
|
+
token,
|
|
2619
|
+
supportedCompression: ["gzip", "gzip-js"],
|
|
2620
|
+
// Always true: when false, posthog-react-native skips flag loading altogether.
|
|
2621
|
+
hasFeatureFlags: true,
|
|
2622
|
+
captureDeadClicks: false,
|
|
2623
|
+
capturePerformance: false,
|
|
2624
|
+
autocapture_opt_out: false,
|
|
2625
|
+
autocaptureExceptions: false,
|
|
2626
|
+
analytics: { endpoint: "/i/v0/e/" },
|
|
2627
|
+
elementsChainAsString: true,
|
|
2628
|
+
errorTracking: { autocaptureExceptions: false, suppressionRules: [] },
|
|
2629
|
+
sessionRecording: settings.sessionRecording ? { endpoint: "/s/", consoleLogRecordingEnabled: false, recorderVersion: "v2" } : false,
|
|
2630
|
+
heatmaps: false,
|
|
2631
|
+
surveys: false,
|
|
2632
|
+
defaultIdentifiedOnly: true,
|
|
2633
|
+
siteApps: []
|
|
2634
|
+
});
|
|
2635
|
+
var FREE_TEXT = /message|body|text|content|prompt|stack|trace|html|comment|note|exception_list/i;
|
|
2636
|
+
var EXCEPTION_KEEP = /* @__PURE__ */ new Set(["$lib", "$lib_version", "$exception_level", "$session_id"]);
|
|
2637
|
+
var scrubRecord = (properties) => Object.fromEntries(
|
|
2638
|
+
Object.entries(properties).filter(([key]) => !FREE_TEXT.test(key) && key !== "token")
|
|
2639
|
+
);
|
|
2640
|
+
var scrubProperties = (event, properties) => {
|
|
2641
|
+
if (event === "$exception") {
|
|
2642
|
+
return Object.fromEntries(Object.entries(properties).filter(([key]) => EXCEPTION_KEEP.has(key)));
|
|
2643
|
+
}
|
|
2644
|
+
const kept = scrubRecord(properties);
|
|
2645
|
+
for (const nested of ["$set", "$set_once"]) {
|
|
2646
|
+
if (isRecord7(kept[nested])) kept[nested] = scrubRecord(kept[nested]);
|
|
2647
|
+
}
|
|
2648
|
+
return kept;
|
|
2649
|
+
};
|
|
2650
|
+
var PostHogAPI = class {
|
|
2651
|
+
app;
|
|
2652
|
+
sqlite;
|
|
2653
|
+
state;
|
|
2654
|
+
service;
|
|
2655
|
+
now;
|
|
2656
|
+
/** Fault effects of the original request, keyed by the rewritten one the router sees. */
|
|
2657
|
+
effects = /* @__PURE__ */ new WeakMap();
|
|
2658
|
+
/** Rewritten requests whose original body could not be decoded. */
|
|
2659
|
+
undecodable = /* @__PURE__ */ new WeakSet();
|
|
2660
|
+
constructor(options = {}) {
|
|
2661
|
+
const sqlite = bootSqlite(options.sqlite);
|
|
2662
|
+
const namespace = options.namespace ?? POSTHOG_NAMESPACE;
|
|
2663
|
+
this.now = options.now ?? (() => Date.now());
|
|
2664
|
+
this.state = new PostHogState(
|
|
2665
|
+
sqlite,
|
|
2666
|
+
namespace,
|
|
2667
|
+
{ flags: options.flags ?? {}, settings: options.settings ?? {} },
|
|
2668
|
+
this.now
|
|
2669
|
+
);
|
|
2670
|
+
const script = (body) => new Response(body, {
|
|
2671
|
+
status: 200,
|
|
2672
|
+
headers: { "content-type": "application/javascript; charset=utf-8" }
|
|
2673
|
+
});
|
|
2674
|
+
const handlers = defineOperations({
|
|
2675
|
+
EvaluateFlags: (context) => this.flags(context, "flags"),
|
|
2676
|
+
Decide: (context) => this.flags(context, "decide"),
|
|
2677
|
+
GetRemoteConfig: (context) => jsonRes(200, remoteConfig(context.params.token ?? "", this.state.current())),
|
|
2678
|
+
GetRemoteConfigScript: (context) => {
|
|
2679
|
+
const token = context.params.token ?? "";
|
|
2680
|
+
const config = remoteConfig(token, this.state.current());
|
|
2681
|
+
return script(
|
|
2682
|
+
`(function(){window._POSTHOG_REMOTE_CONFIG=window._POSTHOG_REMOTE_CONFIG||{};window._POSTHOG_REMOTE_CONFIG[${JSON.stringify(token)}]={config:${JSON.stringify(config)},siteApps:[]};})();`
|
|
2683
|
+
);
|
|
2684
|
+
},
|
|
2685
|
+
CaptureBatch: (context) => this.capture(context, "/batch/"),
|
|
2686
|
+
CaptureEvent: (context) => this.capture(context, "/e/"),
|
|
2687
|
+
CaptureEventV0: (context) => this.capture(context, "/i/v0/e/"),
|
|
2688
|
+
CaptureRecording: () => {
|
|
2689
|
+
this.state.update({ recordings: this.state.current().recordings + 1 });
|
|
2690
|
+
return jsonRes(200, { status: 1 });
|
|
2691
|
+
},
|
|
2692
|
+
GetRecorderScript: () => script("/* mockingbird: session recording is not modelled */\n"),
|
|
2693
|
+
GetVersionedRecorderScript: () => script("/* mockingbird: session recording is not modelled */\n"),
|
|
2694
|
+
ListSurveys: (context) => typeof context.query.token === "string" && context.query.token ? jsonRes(200, { surveys: [] }) : invalidApiKey(),
|
|
2695
|
+
ListWebExperiments: (context) => typeof context.query.token === "string" && context.query.token ? jsonRes(200, { experiments: [] }) : invalidApiKey(),
|
|
2696
|
+
ListFeatureFlags: (context) => this.listFlags(context),
|
|
2697
|
+
CreateFeatureFlag: (context) => this.createFlag(context),
|
|
2698
|
+
UpdateFeatureFlag: (context) => this.updateFlag(context),
|
|
2699
|
+
RunQuery: (context) => this.query(context)
|
|
2700
|
+
});
|
|
2701
|
+
this.service = createService({
|
|
2702
|
+
document,
|
|
2703
|
+
handlers,
|
|
2704
|
+
sqlite,
|
|
2705
|
+
namespace,
|
|
2706
|
+
now: this.now,
|
|
2707
|
+
notFound: () => error(404, "invalid_request", "not_found", "Not found."),
|
|
2708
|
+
onError: (thrown) => {
|
|
2709
|
+
if (thrown instanceof HttpError) return thrown.toResponse();
|
|
2710
|
+
throw thrown;
|
|
2711
|
+
},
|
|
2712
|
+
before: (context) => {
|
|
2713
|
+
if (!context.url.pathname.startsWith("/api/projects/")) return void 0;
|
|
2714
|
+
if (bearerToken(context.request)) return void 0;
|
|
2715
|
+
return error(
|
|
2716
|
+
401,
|
|
2717
|
+
"authentication_error",
|
|
2718
|
+
"not_authenticated",
|
|
2719
|
+
"Authentication credentials were not provided."
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
this.app = this.service.app;
|
|
2724
|
+
this.sqlite = this.service.sqlite;
|
|
2725
|
+
}
|
|
2726
|
+
async fetch(request) {
|
|
2727
|
+
const url = new URL(request.url);
|
|
2728
|
+
if (url.pathname.length > 1) url.pathname = url.pathname.replace(/\/+$/, "");
|
|
2729
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD" && request.body !== null;
|
|
2730
|
+
const decoded2 = hasBody ? await decodePostHogBody(request) : void 0;
|
|
2731
|
+
const headers = new Headers(request.headers);
|
|
2732
|
+
headers.delete("content-encoding");
|
|
2733
|
+
headers.delete("content-length");
|
|
2734
|
+
const init = { method: request.method, headers };
|
|
2735
|
+
if (decoded2 !== void 0) {
|
|
2736
|
+
headers.set("content-type", "application/json");
|
|
2737
|
+
init.body = JSON.stringify(decoded2);
|
|
2738
|
+
} else {
|
|
2739
|
+
headers.delete("content-type");
|
|
2740
|
+
}
|
|
2741
|
+
const routed = new Request(url, init);
|
|
2742
|
+
this.effects.set(
|
|
2743
|
+
routed,
|
|
2744
|
+
faultEffects(request).map((e) => e.name)
|
|
2745
|
+
);
|
|
2746
|
+
if (hasBody && decoded2 === void 0) this.undecodable.add(routed);
|
|
2747
|
+
return this.service.fetch(routed);
|
|
2748
|
+
}
|
|
2749
|
+
async reset() {
|
|
2750
|
+
await this.service.reset();
|
|
2751
|
+
this.state.ensureSeeded();
|
|
2752
|
+
}
|
|
2753
|
+
effect(context, name) {
|
|
2754
|
+
return this.effects.get(context.request)?.includes(name) ?? false;
|
|
2755
|
+
}
|
|
2756
|
+
body(context) {
|
|
2757
|
+
if (this.undecodable.has(context.request)) throw new HttpError(400, malformedBody());
|
|
2758
|
+
return context.body.kind === "json" ? context.body.value : void 0;
|
|
2759
|
+
}
|
|
2760
|
+
/** Every flag this subject sees, in id order, restricted to `keys` when given. */
|
|
2761
|
+
evaluate(subject, keys) {
|
|
2762
|
+
const only = keys && keys.length > 0 ? new Set(keys) : void 0;
|
|
2763
|
+
return this.state.listFlags().filter((flag) => !only || only.has(flag.key)).map((flag) => evaluateFlag(flag, subject)).filter((each) => each !== void 0);
|
|
2764
|
+
}
|
|
2765
|
+
flags(context, route) {
|
|
2766
|
+
const body = this.body(context);
|
|
2767
|
+
if (!isRecord7(body)) return malformed();
|
|
2768
|
+
if (!tokenFromBody(body)) return invalidApiKey();
|
|
2769
|
+
const distinctId = body.distinct_id;
|
|
2770
|
+
if (typeof distinctId !== "string" && typeof distinctId !== "number") {
|
|
2771
|
+
return error(400, "validation_error", "missing_distinct_id", "Decide requires a distinct_id.");
|
|
2772
|
+
}
|
|
2773
|
+
const version = Number(context.url.searchParams.get("v") ?? (route === "decide" ? 3 : 1));
|
|
2774
|
+
const detailed = route === "decide" ? version >= 4 : version >= 2;
|
|
2775
|
+
const subject = {
|
|
2776
|
+
distinct_id: String(distinctId),
|
|
2777
|
+
...isRecord7(body.person_properties) ? { person_properties: body.person_properties } : {}
|
|
2778
|
+
};
|
|
2779
|
+
const keys = [body.flag_keys_to_evaluate, body.flag_keys].find(Array.isArray);
|
|
2780
|
+
const quotaLimited = this.effect(context, "quota_limited");
|
|
2781
|
+
const evaluations = quotaLimited || body.disable_flags === true ? [] : this.evaluate(subject, keys?.map(String));
|
|
2782
|
+
const withConfig = route === "decide" || context.url.searchParams.get("config") === "true";
|
|
2783
|
+
const config = withConfig ? (() => {
|
|
2784
|
+
const { token: _token, ...rest } = remoteConfig(
|
|
2785
|
+
String(tokenFromBody(body)),
|
|
2786
|
+
this.state.current()
|
|
2787
|
+
);
|
|
2788
|
+
return { ...rest, config: { enable_collect_everything: true } };
|
|
2789
|
+
})() : {};
|
|
2790
|
+
const common = {
|
|
2791
|
+
errorsWhileComputingFlags: this.effect(context, "errors_while_computing"),
|
|
2792
|
+
...quotaLimited ? { quotaLimited: ["feature_flags"] } : {},
|
|
2793
|
+
requestId: this.state.ids.next("req_", 24),
|
|
2794
|
+
evaluatedAt: this.now()
|
|
2795
|
+
};
|
|
2796
|
+
const answer = detailed ? {
|
|
2797
|
+
...config,
|
|
2798
|
+
flags: Object.fromEntries(evaluations.map((e) => [e.key, flagDetail(e)])),
|
|
2799
|
+
...common
|
|
2800
|
+
} : { ...config, ...legacyMaps(evaluations), ...common };
|
|
2801
|
+
return annotateResponse(jsonRes(200, answer), { ids: { distinctId: subject.distinct_id } });
|
|
2802
|
+
}
|
|
2803
|
+
capture(context, endpoint) {
|
|
2804
|
+
const body = this.body(context);
|
|
2805
|
+
if (body === void 0 || typeof body !== "object" && !Array.isArray(body)) {
|
|
2806
|
+
return malformed();
|
|
2807
|
+
}
|
|
2808
|
+
const token = tokenFromBody(body);
|
|
2809
|
+
if (!token) return invalidApiKey();
|
|
2810
|
+
const items = Array.isArray(body) ? body : isRecord7(body) && Array.isArray(body.batch) ? body.batch : [body];
|
|
2811
|
+
const events = items.filter(
|
|
2812
|
+
(item) => isRecord7(item) && typeof item.event === "string" && item.event.length > 0
|
|
2813
|
+
);
|
|
2814
|
+
if (events.length === 0 && items.length > 0) return malformed("Invalid payload: no event name");
|
|
2815
|
+
const stored = [];
|
|
2816
|
+
for (const raw of events) {
|
|
2817
|
+
const event = raw.event;
|
|
2818
|
+
const properties = isRecord7(raw.properties) ? raw.properties : {};
|
|
2819
|
+
const distinct = raw.distinct_id ?? properties.distinct_id ?? raw.$distinct_id ?? properties.$distinct_id;
|
|
2820
|
+
const uuid = typeof raw.uuid === "string" && raw.uuid ? raw.uuid : this.state.nextEventId();
|
|
2821
|
+
if (this.state.events.has(uuid)) continue;
|
|
2822
|
+
const record = {
|
|
2823
|
+
uuid,
|
|
2824
|
+
event,
|
|
2825
|
+
distinct_id: typeof distinct === "string" || typeof distinct === "number" ? String(distinct) : "",
|
|
2826
|
+
properties: scrubProperties(event, properties),
|
|
2827
|
+
timestamp: typeof raw.timestamp === "string" && raw.timestamp ? raw.timestamp : new Date(this.now()).toISOString(),
|
|
2828
|
+
receivedAtMs: this.now(),
|
|
2829
|
+
endpoint
|
|
2830
|
+
};
|
|
2831
|
+
this.state.events.insert(uuid, record);
|
|
2832
|
+
stored.push(uuid);
|
|
2833
|
+
}
|
|
2834
|
+
return annotateResponse(jsonRes(200, { status: 1 }), {
|
|
2835
|
+
ids: stored.length > 0 ? { eventId: stored[0] } : {}
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
/** Captured events, oldest first. */
|
|
2839
|
+
events(query = {}) {
|
|
2840
|
+
return this.state.events.list({
|
|
2841
|
+
order: "oldest",
|
|
2842
|
+
where: (event) => (query.distinct_id === void 0 || event.distinct_id === query.distinct_id) && (query.event === void 0 || event.event === query.event) && (query.since === void 0 || event.receivedAtMs >= query.since)
|
|
2843
|
+
}).map((row) => row.value);
|
|
2844
|
+
}
|
|
2845
|
+
listFlags(context) {
|
|
2846
|
+
const limit = Math.min(Math.max(Number(context.url.searchParams.get("limit") ?? 100), 1), 100);
|
|
2847
|
+
const offset = Math.max(Number(context.url.searchParams.get("offset") ?? 0), 0);
|
|
2848
|
+
const flags = this.state.listFlags().filter((flag) => !flag.deleted);
|
|
2849
|
+
const base = `${context.url.origin}${context.url.pathname}/`;
|
|
2850
|
+
const page = (at) => `${base}?limit=${limit}${at > 0 ? `&offset=${at}` : ""}`;
|
|
2851
|
+
return jsonRes(200, {
|
|
2852
|
+
count: flags.length,
|
|
2853
|
+
next: offset + limit < flags.length ? page(offset + limit) : null,
|
|
2854
|
+
previous: offset > 0 ? page(Math.max(offset - limit, 0)) : null,
|
|
2855
|
+
results: flags.slice(offset, offset + limit).map(restView)
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
createFlag(context) {
|
|
2859
|
+
const body = this.body(context);
|
|
2860
|
+
if (!isRecord7(body)) return malformed();
|
|
2861
|
+
const key = body.key;
|
|
2862
|
+
if (typeof key !== "string" || !/^[A-Za-z0-9_-]+$/.test(key)) {
|
|
2863
|
+
return error(
|
|
2864
|
+
400,
|
|
2865
|
+
"validation_error",
|
|
2866
|
+
"invalid_input",
|
|
2867
|
+
'Only letters, numbers, hyphens ("-") & underscores ("_") are allowed.',
|
|
2868
|
+
"key"
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
const existing = this.state.flags.get(key);
|
|
2872
|
+
if (existing && !existing.deleted) {
|
|
2873
|
+
return error(
|
|
2874
|
+
400,
|
|
2875
|
+
"validation_error",
|
|
2876
|
+
"unique",
|
|
2877
|
+
"There is already a feature flag with this key.",
|
|
2878
|
+
"key"
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
const flag = this.state.putFlag(key, {
|
|
2882
|
+
...fromFilters(body.filters),
|
|
2883
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
2884
|
+
active: typeof body.active === "boolean" ? body.active : true
|
|
2885
|
+
});
|
|
2886
|
+
return annotateResponse(jsonRes(201, restView(flag)), { ids: { flag: flag.key } });
|
|
2887
|
+
}
|
|
2888
|
+
updateFlag(context) {
|
|
2889
|
+
const flag = this.state.findFlag(context.params.flagId ?? "");
|
|
2890
|
+
if (!flag) return error(404, "invalid_request", "not_found", "Not found.");
|
|
2891
|
+
const body = this.body(context);
|
|
2892
|
+
if (!isRecord7(body)) return malformed();
|
|
2893
|
+
const patch = {};
|
|
2894
|
+
if (typeof body.name === "string") patch.name = body.name;
|
|
2895
|
+
if (typeof body.active === "boolean") patch.active = body.active;
|
|
2896
|
+
if (typeof body.deleted === "boolean") patch.deleted = body.deleted;
|
|
2897
|
+
if (body.filters !== void 0) Object.assign(patch, fromFilters(body.filters));
|
|
2898
|
+
const updated = this.state.patchFlag(flag.key, patch) ?? flag;
|
|
2899
|
+
return annotateResponse(jsonRes(200, restView(updated)), { ids: { flag: flag.key } });
|
|
2900
|
+
}
|
|
2901
|
+
query(context) {
|
|
2902
|
+
const body = this.body(context);
|
|
2903
|
+
const query = isRecord7(body) && isRecord7(body.query) ? body.query : void 0;
|
|
2904
|
+
if (query?.kind !== "HogQLQuery" || typeof query.query !== "string") {
|
|
2905
|
+
return error(400, "validation_error", "invalid_input", "Expected a HogQLQuery.", "query");
|
|
2906
|
+
}
|
|
2907
|
+
const text = query.query;
|
|
2908
|
+
const canned = this.state.current().queryResults.find((each) => each.match === void 0 || text.includes(each.match));
|
|
2909
|
+
return jsonRes(200, {
|
|
2910
|
+
results: canned?.results ?? [],
|
|
2911
|
+
columns: canned?.columns ?? [],
|
|
2912
|
+
is_cached: false
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
/** The admin view of every flag. */
|
|
2916
|
+
flagList() {
|
|
2917
|
+
return this.state.listFlags().map(adminView);
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2920
|
+
var malformedBody = () => ({
|
|
2921
|
+
type: "validation_error",
|
|
2922
|
+
code: "invalid_payload",
|
|
2923
|
+
detail: "Malformed request data",
|
|
2924
|
+
attr: null
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2927
|
+
export {
|
|
2928
|
+
decodePostHogBody,
|
|
2929
|
+
tokenFromBody,
|
|
2930
|
+
evaluateFlag,
|
|
2931
|
+
payloadString,
|
|
2932
|
+
parseFlagSpec,
|
|
2933
|
+
document,
|
|
2934
|
+
operationIds,
|
|
2935
|
+
supportedOperationIds,
|
|
2936
|
+
GEVITI_FLAG_STATE,
|
|
2937
|
+
valueForState,
|
|
2938
|
+
specsFromState,
|
|
2939
|
+
POSTHOG_PRESETS,
|
|
2940
|
+
createRuntime2 as createRuntime,
|
|
2941
|
+
POSTHOG_NAMESPACE,
|
|
2942
|
+
scrubProperties,
|
|
2943
|
+
PostHogAPI
|
|
2944
|
+
};
|
|
2945
|
+
//# sourceMappingURL=chunk-KO6LMI55.js.map
|