@crvouga/mockingbird-service-posthog 1.4.0 → 1.5.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 +10 -0
- package/dist/{chunk-UJEAOIXF.js → chunk-A7ITZA26.js} +1433 -629
- package/dist/chunk-A7ITZA26.js.map +7 -0
- package/dist/{chunk-BLA3BJC5.js → chunk-LER2KA4S.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-UJEAOIXF.js.map +0 -7
- /package/dist/{chunk-BLA3BJC5.js.map → chunk-LER2KA4S.js.map} +0 -0
|
@@ -540,10 +540,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
540
540
|
};
|
|
541
541
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
542
542
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
543
|
-
var resolveRef = (
|
|
543
|
+
var resolveRef = (document3, ref) => {
|
|
544
544
|
if (!ref.startsWith("#/"))
|
|
545
545
|
throw new OpenAPIReferenceError(ref);
|
|
546
|
-
let cursor =
|
|
546
|
+
let cursor = document3;
|
|
547
547
|
for (const raw of ref.slice(2).split("/")) {
|
|
548
548
|
const segment = unescapePointer(raw);
|
|
549
549
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -555,14 +555,14 @@ var resolveRef = (document2, ref) => {
|
|
|
555
555
|
throw new OpenAPIReferenceError(ref);
|
|
556
556
|
return cursor;
|
|
557
557
|
};
|
|
558
|
-
var deref = (
|
|
558
|
+
var deref = (document3, value) => {
|
|
559
559
|
let current = value;
|
|
560
560
|
const seen = /* @__PURE__ */ new Set();
|
|
561
561
|
while (isReference(current)) {
|
|
562
562
|
if (seen.has(current.$ref))
|
|
563
563
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
564
564
|
seen.add(current.$ref);
|
|
565
|
-
current = resolveRef(
|
|
565
|
+
current = resolveRef(document3, current.$ref);
|
|
566
566
|
}
|
|
567
567
|
return current;
|
|
568
568
|
};
|
|
@@ -580,36 +580,36 @@ var HTTP_METHODS = [
|
|
|
580
580
|
];
|
|
581
581
|
|
|
582
582
|
// ../../openapi/core/dist/document.js
|
|
583
|
-
var mergeParameters = (
|
|
583
|
+
var mergeParameters = (document3, item, own) => {
|
|
584
584
|
const merged = /* @__PURE__ */ new Map();
|
|
585
585
|
for (const raw of item.parameters ?? []) {
|
|
586
|
-
const parameter = deref(
|
|
586
|
+
const parameter = deref(document3, raw);
|
|
587
587
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
588
588
|
}
|
|
589
589
|
for (const raw of own ?? []) {
|
|
590
|
-
const parameter = deref(
|
|
590
|
+
const parameter = deref(document3, raw);
|
|
591
591
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
592
592
|
}
|
|
593
593
|
return [...merged.values()];
|
|
594
594
|
};
|
|
595
|
-
var listOperations = (
|
|
595
|
+
var listOperations = (document3) => {
|
|
596
596
|
const operations = [];
|
|
597
|
-
for (const [path, item] of Object.entries(
|
|
597
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
598
598
|
for (const method of HTTP_METHODS) {
|
|
599
599
|
const operation = item[method];
|
|
600
600
|
if (operation?.operationId === void 0)
|
|
601
601
|
continue;
|
|
602
602
|
const responses = {};
|
|
603
603
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
604
|
-
responses[status] = deref(
|
|
604
|
+
responses[status] = deref(document3, response);
|
|
605
605
|
}
|
|
606
606
|
operations.push({
|
|
607
607
|
operationId: operation.operationId,
|
|
608
608
|
method,
|
|
609
609
|
path,
|
|
610
610
|
operation,
|
|
611
|
-
parameters: mergeParameters(
|
|
612
|
-
requestBody: operation.requestBody === void 0 ? void 0 : deref(
|
|
611
|
+
parameters: mergeParameters(document3, item, operation.parameters),
|
|
612
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
|
|
613
613
|
responses
|
|
614
614
|
});
|
|
615
615
|
}
|
|
@@ -1153,6 +1153,1232 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
1153
1153
|
});
|
|
1154
1154
|
};
|
|
1155
1155
|
|
|
1156
|
+
// ../../ui/dist/reset.js
|
|
1157
|
+
var CSS_RESET = `*,
|
|
1158
|
+
*::before,
|
|
1159
|
+
*::after {
|
|
1160
|
+
box-sizing: border-box;
|
|
1161
|
+
margin: 0;
|
|
1162
|
+
padding: 0;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
* {
|
|
1166
|
+
scrollbar-width: thin;
|
|
1167
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
*::-webkit-scrollbar {
|
|
1171
|
+
width: 12px;
|
|
1172
|
+
height: 12px;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
*::-webkit-scrollbar-track,
|
|
1176
|
+
*::-webkit-scrollbar-corner {
|
|
1177
|
+
background: Canvas;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
*::-webkit-scrollbar-thumb {
|
|
1181
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
1182
|
+
border: 3px solid Canvas;
|
|
1183
|
+
border-radius: 999px;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
html {
|
|
1187
|
+
hanging-punctuation: first allow-end last;
|
|
1188
|
+
-webkit-text-size-adjust: none;
|
|
1189
|
+
text-size-adjust: none;
|
|
1190
|
+
tab-size: 4;
|
|
1191
|
+
font-family:
|
|
1192
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1193
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1194
|
+
font-size: 16px;
|
|
1195
|
+
line-height: 1.5;
|
|
1196
|
+
-webkit-font-smoothing: antialiased;
|
|
1197
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1198
|
+
text-rendering: optimizeLegibility;
|
|
1199
|
+
-webkit-tap-highlight-color: transparent;
|
|
1200
|
+
scroll-behavior: smooth;
|
|
1201
|
+
background: Canvas;
|
|
1202
|
+
color: CanvasText;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
body {
|
|
1206
|
+
min-height: 100svh;
|
|
1207
|
+
line-height: 1.5;
|
|
1208
|
+
background: Canvas;
|
|
1209
|
+
color: CanvasText;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
h1 {
|
|
1213
|
+
font-size: 2em;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
h2 {
|
|
1217
|
+
font-size: 1.5em;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
h3 {
|
|
1221
|
+
font-size: 1.17em;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
h4 {
|
|
1225
|
+
font-size: 1em;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
h5 {
|
|
1229
|
+
font-size: 0.83em;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
h6 {
|
|
1233
|
+
font-size: 0.67em;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
h1,
|
|
1237
|
+
h2,
|
|
1238
|
+
h3,
|
|
1239
|
+
h4,
|
|
1240
|
+
h5,
|
|
1241
|
+
h6 {
|
|
1242
|
+
font-weight: 700;
|
|
1243
|
+
line-height: 1.2;
|
|
1244
|
+
text-wrap: balance;
|
|
1245
|
+
overflow-wrap: break-word;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
p,
|
|
1249
|
+
li,
|
|
1250
|
+
figcaption,
|
|
1251
|
+
dd,
|
|
1252
|
+
dt,
|
|
1253
|
+
blockquote {
|
|
1254
|
+
overflow-wrap: break-word;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
p,
|
|
1258
|
+
li,
|
|
1259
|
+
figcaption,
|
|
1260
|
+
blockquote {
|
|
1261
|
+
text-wrap: pretty;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
ul,
|
|
1265
|
+
ol,
|
|
1266
|
+
menu {
|
|
1267
|
+
list-style: none;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
a {
|
|
1271
|
+
color: inherit;
|
|
1272
|
+
text-decoration: underline;
|
|
1273
|
+
text-decoration-thickness: 1px;
|
|
1274
|
+
text-underline-offset: 0.15em;
|
|
1275
|
+
text-decoration-skip-ink: auto;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
b,
|
|
1279
|
+
strong {
|
|
1280
|
+
font-weight: 700;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
small {
|
|
1284
|
+
font-size: 0.875em;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
code,
|
|
1288
|
+
kbd,
|
|
1289
|
+
samp,
|
|
1290
|
+
pre {
|
|
1291
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1292
|
+
font-size: 1em;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
sub,
|
|
1296
|
+
sup {
|
|
1297
|
+
font-size: 75%;
|
|
1298
|
+
line-height: 0;
|
|
1299
|
+
position: relative;
|
|
1300
|
+
vertical-align: baseline;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
sub {
|
|
1304
|
+
bottom: -0.25em;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
sup {
|
|
1308
|
+
top: -0.5em;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
abbr[title] {
|
|
1312
|
+
text-decoration: underline dotted;
|
|
1313
|
+
text-underline-offset: 0.15em;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
mark {
|
|
1317
|
+
background: Mark;
|
|
1318
|
+
color: MarkText;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
hr {
|
|
1322
|
+
height: 0;
|
|
1323
|
+
border: 0;
|
|
1324
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1325
|
+
color: inherit;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
img,
|
|
1329
|
+
picture,
|
|
1330
|
+
video,
|
|
1331
|
+
canvas {
|
|
1332
|
+
display: block;
|
|
1333
|
+
max-width: 100%;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
img,
|
|
1337
|
+
video {
|
|
1338
|
+
height: auto;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
svg {
|
|
1342
|
+
overflow: hidden;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
iframe {
|
|
1346
|
+
border: 0;
|
|
1347
|
+
max-width: 100%;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
table {
|
|
1351
|
+
border-collapse: collapse;
|
|
1352
|
+
border-spacing: 0;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
fieldset {
|
|
1356
|
+
border: 0;
|
|
1357
|
+
min-inline-size: 0;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
legend {
|
|
1361
|
+
padding: 0;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
button,
|
|
1365
|
+
input,
|
|
1366
|
+
select,
|
|
1367
|
+
textarea,
|
|
1368
|
+
optgroup {
|
|
1369
|
+
font: inherit;
|
|
1370
|
+
letter-spacing: inherit;
|
|
1371
|
+
color: inherit;
|
|
1372
|
+
margin: 0;
|
|
1373
|
+
vertical-align: middle;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
button,
|
|
1377
|
+
input[type="button"],
|
|
1378
|
+
input[type="submit"],
|
|
1379
|
+
input[type="reset"] {
|
|
1380
|
+
appearance: none;
|
|
1381
|
+
background: none;
|
|
1382
|
+
border: 0;
|
|
1383
|
+
padding: 0;
|
|
1384
|
+
cursor: pointer;
|
|
1385
|
+
text-align: center;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
input,
|
|
1389
|
+
textarea,
|
|
1390
|
+
select {
|
|
1391
|
+
background-color: Canvas;
|
|
1392
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1393
|
+
border-radius: 0;
|
|
1394
|
+
padding: 0.5em 0.65em;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
input[type="checkbox"],
|
|
1398
|
+
input[type="radio"],
|
|
1399
|
+
input[type="range"],
|
|
1400
|
+
input[type="file"],
|
|
1401
|
+
input[type="color"],
|
|
1402
|
+
input[type="button"],
|
|
1403
|
+
input[type="submit"],
|
|
1404
|
+
input[type="reset"],
|
|
1405
|
+
input[type="image"],
|
|
1406
|
+
input[type="hidden"] {
|
|
1407
|
+
background: none;
|
|
1408
|
+
border: 0;
|
|
1409
|
+
padding: 0;
|
|
1410
|
+
border-radius: 0;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
input[type="checkbox"],
|
|
1414
|
+
input[type="radio"] {
|
|
1415
|
+
accent-color: CanvasText;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
input[type="search"] {
|
|
1419
|
+
appearance: none;
|
|
1420
|
+
background-color: Canvas;
|
|
1421
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1425
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1426
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1427
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1428
|
+
-webkit-appearance: none;
|
|
1429
|
+
display: none;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
input[type="number"] {
|
|
1433
|
+
appearance: textfield;
|
|
1434
|
+
background-color: Canvas;
|
|
1435
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1439
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1440
|
+
-webkit-appearance: none;
|
|
1441
|
+
margin: 0;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
textarea {
|
|
1445
|
+
resize: vertical;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
::placeholder {
|
|
1449
|
+
opacity: 1;
|
|
1450
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
:focus-visible {
|
|
1454
|
+
outline: 2px solid CanvasText;
|
|
1455
|
+
outline-offset: 2px;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
:focus:not(:focus-visible) {
|
|
1459
|
+
outline: none;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
:disabled,
|
|
1463
|
+
[disabled] {
|
|
1464
|
+
cursor: default;
|
|
1465
|
+
opacity: 0.6;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
label,
|
|
1469
|
+
summary,
|
|
1470
|
+
select,
|
|
1471
|
+
[role="button"] {
|
|
1472
|
+
cursor: pointer;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
summary {
|
|
1476
|
+
display: list-item;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
details {
|
|
1480
|
+
display: block;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
dialog {
|
|
1484
|
+
margin: auto;
|
|
1485
|
+
padding: 1rem;
|
|
1486
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1487
|
+
background: Canvas;
|
|
1488
|
+
color: CanvasText;
|
|
1489
|
+
max-width: calc(100vw - 2rem);
|
|
1490
|
+
max-height: calc(100svh - 2rem);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
dialog:not([open]) {
|
|
1494
|
+
display: none;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
::backdrop {
|
|
1498
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
[hidden] {
|
|
1502
|
+
display: none !important;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
::selection {
|
|
1506
|
+
background: Highlight;
|
|
1507
|
+
color: HighlightText;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1511
|
+
*,
|
|
1512
|
+
*::before,
|
|
1513
|
+
*::after {
|
|
1514
|
+
animation-duration: 0.01ms !important;
|
|
1515
|
+
animation-iteration-count: 1 !important;
|
|
1516
|
+
transition-duration: 0.01ms !important;
|
|
1517
|
+
scroll-behavior: auto !important;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
@media (pointer: coarse) {
|
|
1522
|
+
input,
|
|
1523
|
+
select,
|
|
1524
|
+
textarea {
|
|
1525
|
+
font-size: max(16px, 1em);
|
|
1526
|
+
}
|
|
1527
|
+
}`;
|
|
1528
|
+
|
|
1529
|
+
// ../core/dist/admin-client.js
|
|
1530
|
+
function isRecord2(value) {
|
|
1531
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1532
|
+
}
|
|
1533
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1534
|
+
const clip = (value, max) => {
|
|
1535
|
+
if (typeof value !== "string")
|
|
1536
|
+
return null;
|
|
1537
|
+
const trimmed = value.trim();
|
|
1538
|
+
if (trimmed === "")
|
|
1539
|
+
return null;
|
|
1540
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1541
|
+
};
|
|
1542
|
+
const httpUrl = (value) => {
|
|
1543
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1544
|
+
return null;
|
|
1545
|
+
try {
|
|
1546
|
+
const url = new URL(value);
|
|
1547
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1548
|
+
return null;
|
|
1549
|
+
return url.href;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
const hexColor = (value) => {
|
|
1555
|
+
if (typeof value !== "string")
|
|
1556
|
+
return null;
|
|
1557
|
+
const raw = value.trim();
|
|
1558
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1559
|
+
};
|
|
1560
|
+
const hostname = (value) => {
|
|
1561
|
+
try {
|
|
1562
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1563
|
+
} catch {
|
|
1564
|
+
return value;
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
host.replaceChildren();
|
|
1568
|
+
host.hidden = true;
|
|
1569
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1570
|
+
return;
|
|
1571
|
+
const row = catalog[service];
|
|
1572
|
+
if (!isRecord2(row))
|
|
1573
|
+
return;
|
|
1574
|
+
const vendor = clip(row.vendor, 80);
|
|
1575
|
+
const website = httpUrl(row.website);
|
|
1576
|
+
const guide = httpUrl(row.guide);
|
|
1577
|
+
const docs = httpUrl(row.docs);
|
|
1578
|
+
const logo = httpUrl(row.logo);
|
|
1579
|
+
const color = hexColor(row.color);
|
|
1580
|
+
const description = clip(row.description, 280);
|
|
1581
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1582
|
+
return;
|
|
1583
|
+
const add = (node) => {
|
|
1584
|
+
host.append(node);
|
|
1585
|
+
return node;
|
|
1586
|
+
};
|
|
1587
|
+
if (logo !== null) {
|
|
1588
|
+
const img = doc.createElement("img");
|
|
1589
|
+
img.alt = "";
|
|
1590
|
+
img.src = logo;
|
|
1591
|
+
img.width = 16;
|
|
1592
|
+
img.height = 16;
|
|
1593
|
+
img.decoding = "async";
|
|
1594
|
+
img.referrerPolicy = "no-referrer";
|
|
1595
|
+
if (color !== null)
|
|
1596
|
+
img.style.borderColor = color;
|
|
1597
|
+
add(img);
|
|
1598
|
+
}
|
|
1599
|
+
const name = doc.createElement("span");
|
|
1600
|
+
name.className = "vendor-name";
|
|
1601
|
+
name.textContent = vendor;
|
|
1602
|
+
if (description !== null)
|
|
1603
|
+
name.title = description;
|
|
1604
|
+
add(name);
|
|
1605
|
+
const sep = () => {
|
|
1606
|
+
const node = doc.createElement("span");
|
|
1607
|
+
node.className = "vendor-sep";
|
|
1608
|
+
node.setAttribute("aria-hidden", "true");
|
|
1609
|
+
node.textContent = "\xB7";
|
|
1610
|
+
return node;
|
|
1611
|
+
};
|
|
1612
|
+
const link = (href, label, title) => {
|
|
1613
|
+
const node = doc.createElement("a");
|
|
1614
|
+
node.className = "vendor-link";
|
|
1615
|
+
node.href = href;
|
|
1616
|
+
node.target = "_blank";
|
|
1617
|
+
node.rel = "noopener noreferrer";
|
|
1618
|
+
node.referrerPolicy = "no-referrer";
|
|
1619
|
+
node.textContent = label;
|
|
1620
|
+
node.title = title;
|
|
1621
|
+
return node;
|
|
1622
|
+
};
|
|
1623
|
+
if (website !== null) {
|
|
1624
|
+
add(sep());
|
|
1625
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1626
|
+
}
|
|
1627
|
+
if (guide !== null) {
|
|
1628
|
+
add(sep());
|
|
1629
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1630
|
+
}
|
|
1631
|
+
if (docs !== null) {
|
|
1632
|
+
add(sep());
|
|
1633
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1634
|
+
}
|
|
1635
|
+
host.removeAttribute("hidden");
|
|
1636
|
+
host.hidden = false;
|
|
1637
|
+
}
|
|
1638
|
+
function startAdminBrand(env) {
|
|
1639
|
+
const requested = env.params.get("brands");
|
|
1640
|
+
let brandsUrl = env.brandsUrl;
|
|
1641
|
+
if (requested) {
|
|
1642
|
+
try {
|
|
1643
|
+
const url = new URL(requested, env.location.href);
|
|
1644
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1645
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1646
|
+
brandsUrl = url.href;
|
|
1647
|
+
} catch {
|
|
1648
|
+
brandsUrl = env.brandsUrl;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const vendorHost = env.$("vendor");
|
|
1652
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1653
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1654
|
+
if (vendorHost === null)
|
|
1655
|
+
return;
|
|
1656
|
+
env.fetch(brandsUrl, {
|
|
1657
|
+
headers: { accept: "application/json" },
|
|
1658
|
+
signal: AbortSignal.timeout(5e3)
|
|
1659
|
+
}).then(async (response) => {
|
|
1660
|
+
if (!response.ok)
|
|
1661
|
+
return null;
|
|
1662
|
+
const catalog = await response.json();
|
|
1663
|
+
return catalog;
|
|
1664
|
+
}).then((catalog) => {
|
|
1665
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1666
|
+
}).catch(() => {
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
function faultPresetList(value) {
|
|
1670
|
+
if (!isRecord2(value))
|
|
1671
|
+
throw new Error("Fault presets response was not an object");
|
|
1672
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1673
|
+
return { presets: [] };
|
|
1674
|
+
}
|
|
1675
|
+
if (!Array.isArray(value.presets))
|
|
1676
|
+
throw new Error("Fault presets response was not a list");
|
|
1677
|
+
return {
|
|
1678
|
+
presets: value.presets.flatMap((item) => {
|
|
1679
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1680
|
+
return [];
|
|
1681
|
+
return [{ name: item.name }];
|
|
1682
|
+
})
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
function bootAdmin(config) {
|
|
1686
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1687
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1688
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1689
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1690
|
+
const readJson2 = (text) => {
|
|
1691
|
+
const value = JSON.parse(text);
|
|
1692
|
+
return value;
|
|
1693
|
+
};
|
|
1694
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1695
|
+
switch (char) {
|
|
1696
|
+
case "&":
|
|
1697
|
+
return "&";
|
|
1698
|
+
case "<":
|
|
1699
|
+
return "<";
|
|
1700
|
+
case ">":
|
|
1701
|
+
return ">";
|
|
1702
|
+
case '"':
|
|
1703
|
+
return """;
|
|
1704
|
+
default:
|
|
1705
|
+
return "'";
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
const el = (id, kind) => {
|
|
1709
|
+
const node = document.getElementById(id);
|
|
1710
|
+
if (!(node instanceof kind))
|
|
1711
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1712
|
+
return node;
|
|
1713
|
+
};
|
|
1714
|
+
const errorMessage = (data, fallback) => {
|
|
1715
|
+
if (!isRecord2(data))
|
|
1716
|
+
return fallback;
|
|
1717
|
+
const error2 = data.error;
|
|
1718
|
+
if (!isRecord2(error2) || typeof error2.message !== "string" || error2.message === "") {
|
|
1719
|
+
return fallback;
|
|
1720
|
+
}
|
|
1721
|
+
return error2.message;
|
|
1722
|
+
};
|
|
1723
|
+
const requireList = (value, label) => {
|
|
1724
|
+
if (value === void 0 || value === null)
|
|
1725
|
+
return [];
|
|
1726
|
+
if (!Array.isArray(value))
|
|
1727
|
+
throw new Error(`${label} was not a list`);
|
|
1728
|
+
return value;
|
|
1729
|
+
};
|
|
1730
|
+
const state = {
|
|
1731
|
+
namespace: "default",
|
|
1732
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1733
|
+
view: "overview",
|
|
1734
|
+
collection: "",
|
|
1735
|
+
shape: null,
|
|
1736
|
+
route: null,
|
|
1737
|
+
editing: null
|
|
1738
|
+
};
|
|
1739
|
+
const standard = new Set(config.standardRoutes);
|
|
1740
|
+
const banner = el("banner", HTMLElement);
|
|
1741
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1742
|
+
keyInput.value = state.key;
|
|
1743
|
+
const params = new URLSearchParams(location.search);
|
|
1744
|
+
const queryKey = params.get("key");
|
|
1745
|
+
if (queryKey) {
|
|
1746
|
+
state.key = queryKey;
|
|
1747
|
+
keyInput.value = state.key;
|
|
1748
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1749
|
+
params.delete("key");
|
|
1750
|
+
const next = params.toString();
|
|
1751
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1752
|
+
}
|
|
1753
|
+
const queryNamespace = params.get("namespace");
|
|
1754
|
+
if (queryNamespace)
|
|
1755
|
+
state.namespace = queryNamespace;
|
|
1756
|
+
const showError = (error2) => {
|
|
1757
|
+
banner.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1758
|
+
banner.classList.add("show");
|
|
1759
|
+
};
|
|
1760
|
+
const clearError = () => {
|
|
1761
|
+
banner.classList.remove("show");
|
|
1762
|
+
};
|
|
1763
|
+
const api = async (method, path, body) => {
|
|
1764
|
+
const headers = { accept: "application/json" };
|
|
1765
|
+
if (state.key !== "")
|
|
1766
|
+
headers[config.adminKeyHeader] = state.key;
|
|
1767
|
+
if (state.namespace !== "")
|
|
1768
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
1769
|
+
if (body !== void 0)
|
|
1770
|
+
headers["content-type"] = "application/json";
|
|
1771
|
+
const init = { method, headers };
|
|
1772
|
+
if (body !== void 0)
|
|
1773
|
+
init.body = JSON.stringify(body);
|
|
1774
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
1775
|
+
const text = await response.text();
|
|
1776
|
+
let data = null;
|
|
1777
|
+
if (text !== "") {
|
|
1778
|
+
try {
|
|
1779
|
+
data = readJson2(text);
|
|
1780
|
+
} catch {
|
|
1781
|
+
data = { raw: text };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (response.status === 401) {
|
|
1785
|
+
keyInput.focus();
|
|
1786
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
1787
|
+
}
|
|
1788
|
+
if (!response.ok) {
|
|
1789
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
1790
|
+
}
|
|
1791
|
+
return data;
|
|
1792
|
+
};
|
|
1793
|
+
const originGet = async (path) => {
|
|
1794
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
1795
|
+
if (!response.ok)
|
|
1796
|
+
throw new Error(`${response.status} ${path}`);
|
|
1797
|
+
const value = await response.json();
|
|
1798
|
+
return value;
|
|
1799
|
+
};
|
|
1800
|
+
const panelApi = {
|
|
1801
|
+
get: (path) => api("GET", path),
|
|
1802
|
+
send: (method, path, body) => api(method, path, body),
|
|
1803
|
+
namespace: () => state.namespace
|
|
1804
|
+
};
|
|
1805
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
1806
|
+
const setTheme = (theme) => {
|
|
1807
|
+
if (theme)
|
|
1808
|
+
document.documentElement.dataset.theme = theme;
|
|
1809
|
+
else
|
|
1810
|
+
delete document.documentElement.dataset.theme;
|
|
1811
|
+
};
|
|
1812
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1813
|
+
if (storedTheme)
|
|
1814
|
+
setTheme(storedTheme);
|
|
1815
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1816
|
+
const current = document.documentElement.dataset.theme;
|
|
1817
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1818
|
+
const next = dark ? "light" : "dark";
|
|
1819
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
1820
|
+
setTheme(next);
|
|
1821
|
+
});
|
|
1822
|
+
const show = (view) => {
|
|
1823
|
+
state.view = view;
|
|
1824
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
1825
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1826
|
+
continue;
|
|
1827
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
1828
|
+
}
|
|
1829
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
1830
|
+
if (!(section instanceof HTMLElement))
|
|
1831
|
+
continue;
|
|
1832
|
+
section.hidden = section.id !== `view-${view}`;
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1836
|
+
const target = event.target;
|
|
1837
|
+
if (!(target instanceof Element))
|
|
1838
|
+
return;
|
|
1839
|
+
const button = target.closest("button[data-view]");
|
|
1840
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1841
|
+
return;
|
|
1842
|
+
const view = button.dataset.view;
|
|
1843
|
+
if (view === void 0)
|
|
1844
|
+
return;
|
|
1845
|
+
show(view);
|
|
1846
|
+
refresh().catch(showError);
|
|
1847
|
+
});
|
|
1848
|
+
const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
|
|
1849
|
+
const table = (columns, rows) => {
|
|
1850
|
+
if (rows.length === 0)
|
|
1851
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
1852
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
1853
|
+
const body = rows.map((row) => {
|
|
1854
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
1855
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1856
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
1857
|
+
}).join("");
|
|
1858
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1859
|
+
};
|
|
1860
|
+
const preview = (value) => {
|
|
1861
|
+
const text = JSON.stringify(value) ?? "null";
|
|
1862
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
1863
|
+
};
|
|
1864
|
+
const when = (epoch) => {
|
|
1865
|
+
const date = new Date(epoch);
|
|
1866
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1867
|
+
};
|
|
1868
|
+
const clockCard = (epoch) => {
|
|
1869
|
+
const date = new Date(epoch);
|
|
1870
|
+
if (Number.isNaN(date.getTime()))
|
|
1871
|
+
return esc(String(epoch));
|
|
1872
|
+
const iso = date.toISOString();
|
|
1873
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
1874
|
+
};
|
|
1875
|
+
const decodeClock = (value) => {
|
|
1876
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
1877
|
+
throw new Error("Clock response was not a clock");
|
|
1878
|
+
}
|
|
1879
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
1880
|
+
};
|
|
1881
|
+
const decodeField = (value) => {
|
|
1882
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
1883
|
+
return void 0;
|
|
1884
|
+
}
|
|
1885
|
+
const field = {
|
|
1886
|
+
name: value.name,
|
|
1887
|
+
kind: value.kind,
|
|
1888
|
+
optional: value.optional === true
|
|
1889
|
+
};
|
|
1890
|
+
if (typeof value.description === "string")
|
|
1891
|
+
field.description = value.description;
|
|
1892
|
+
if (Array.isArray(value.fields)) {
|
|
1893
|
+
field.fields = value.fields.flatMap((item) => {
|
|
1894
|
+
const child = decodeField(item);
|
|
1895
|
+
return child === void 0 ? [] : [child];
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
return field;
|
|
1899
|
+
};
|
|
1900
|
+
const decodeCollection = (value) => {
|
|
1901
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
1902
|
+
return void 0;
|
|
1903
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
1904
|
+
const field = decodeField(item);
|
|
1905
|
+
return field === void 0 ? [] : [field];
|
|
1906
|
+
}) : [];
|
|
1907
|
+
const collection = {
|
|
1908
|
+
name: value.name,
|
|
1909
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
1910
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
1911
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
1912
|
+
fields
|
|
1913
|
+
};
|
|
1914
|
+
if (typeof value.description === "string")
|
|
1915
|
+
collection.description = value.description;
|
|
1916
|
+
return collection;
|
|
1917
|
+
};
|
|
1918
|
+
const decodeState = (value) => {
|
|
1919
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
1920
|
+
throw new Error("State response has no collection list");
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
1924
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
1925
|
+
collections: value.collections.flatMap((item) => {
|
|
1926
|
+
const collection = decodeCollection(item);
|
|
1927
|
+
return collection === void 0 ? [] : [collection];
|
|
1928
|
+
})
|
|
1929
|
+
};
|
|
1930
|
+
};
|
|
1931
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
1932
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
1933
|
+
return [];
|
|
1934
|
+
return [
|
|
1935
|
+
{
|
|
1936
|
+
id: item.id,
|
|
1937
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
1938
|
+
value: item.value,
|
|
1939
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
1940
|
+
}
|
|
1941
|
+
];
|
|
1942
|
+
});
|
|
1943
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
1944
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
1945
|
+
if (!isRecord2(item))
|
|
1946
|
+
return [];
|
|
1947
|
+
return [
|
|
1948
|
+
{
|
|
1949
|
+
at: textOf(item.at),
|
|
1950
|
+
method: textOf(item.method),
|
|
1951
|
+
path: textOf(item.path),
|
|
1952
|
+
status: textOf(item.status),
|
|
1953
|
+
durationMs: textOf(item.durationMs),
|
|
1954
|
+
operationId: textOf(item.operationId)
|
|
1955
|
+
}
|
|
1956
|
+
];
|
|
1957
|
+
});
|
|
1958
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
1959
|
+
if (!isRecord2(item))
|
|
1960
|
+
return [];
|
|
1961
|
+
return [
|
|
1962
|
+
{
|
|
1963
|
+
id: textOf(item.id),
|
|
1964
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
1965
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
1966
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
1967
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
1968
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
1969
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
1970
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
1971
|
+
}
|
|
1972
|
+
];
|
|
1973
|
+
});
|
|
1974
|
+
const loadNamespaces = async () => {
|
|
1975
|
+
const data = await api("GET", "/namespaces");
|
|
1976
|
+
const select = el("namespace", HTMLSelectElement);
|
|
1977
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
1978
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
1979
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
1980
|
+
if (!names.includes(state.namespace))
|
|
1981
|
+
names.unshift(state.namespace);
|
|
1982
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
1983
|
+
select.value = state.namespace;
|
|
1984
|
+
};
|
|
1985
|
+
const renderOverview = async () => {
|
|
1986
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
1987
|
+
originGet("/health"),
|
|
1988
|
+
api("GET", "/state"),
|
|
1989
|
+
api("GET", "/clock"),
|
|
1990
|
+
api("GET", "/requests?limit=8")
|
|
1991
|
+
]);
|
|
1992
|
+
if (!isRecord2(health))
|
|
1993
|
+
throw new Error("Health response was not an object");
|
|
1994
|
+
const decoded2 = decodeState(shape);
|
|
1995
|
+
state.shape = decoded2;
|
|
1996
|
+
const time = decodeClock(clock);
|
|
1997
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
1998
|
+
{
|
|
1999
|
+
k: "Service",
|
|
2000
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
k: "Status",
|
|
2004
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2005
|
+
},
|
|
2006
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
2007
|
+
{ k: "Collections", v: String(decoded2.collections.length) },
|
|
2008
|
+
{
|
|
2009
|
+
k: "Records",
|
|
2010
|
+
v: String(decoded2.collections.reduce((sum, item) => sum + item.count, 0))
|
|
2011
|
+
},
|
|
2012
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
2013
|
+
]);
|
|
2014
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
2015
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2016
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2017
|
+
{
|
|
2018
|
+
label: "Status",
|
|
2019
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
2020
|
+
},
|
|
2021
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
2022
|
+
], decodeJournal(journal));
|
|
2023
|
+
};
|
|
2024
|
+
const schemaChips = (collection) => {
|
|
2025
|
+
if (collection.fields.length === 0) {
|
|
2026
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
2027
|
+
}
|
|
2028
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
2029
|
+
};
|
|
2030
|
+
const renderState = async () => {
|
|
2031
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
2032
|
+
state.shape = shape;
|
|
2033
|
+
const host = el("collections", HTMLElement);
|
|
2034
|
+
if (shape.collections.length === 0) {
|
|
2035
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
2036
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
2037
|
+
el("records", HTMLElement).innerHTML = "";
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
2041
|
+
const first = shape.collections[0];
|
|
2042
|
+
if (first === void 0)
|
|
2043
|
+
return;
|
|
2044
|
+
state.collection = first.name;
|
|
2045
|
+
}
|
|
2046
|
+
host.innerHTML = shape.collections.map((item) => `<button type="button" class="collection" data-collection="${esc(item.name)}" aria-current="${item.name === state.collection ? "true" : "false"}"><strong>${esc(item.label)}</strong><small>${esc(item.count)} \xB7 ${esc(item.source)}</small></button>`).join("");
|
|
2047
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
2048
|
+
if (selected === void 0)
|
|
2049
|
+
return;
|
|
2050
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
2051
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
2052
|
+
const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
2053
|
+
el("records", HTMLElement).innerHTML = table([
|
|
2054
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
2055
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
2056
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
2057
|
+
], decodeRecords(page));
|
|
2058
|
+
};
|
|
2059
|
+
const renderClock = async () => {
|
|
2060
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
2061
|
+
el("clock-readout", HTMLElement).innerHTML = `<div class="k">Now</div><div class="v">${esc(when(clock.now))}</div><p>${esc(clock.now)} ms \xB7 offset ${esc(clock.offsetMs)} ms ${clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>"}</p>`;
|
|
2062
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
2063
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
2064
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
2065
|
+
};
|
|
2066
|
+
const renderFaults = async () => {
|
|
2067
|
+
const [faults, presets] = await Promise.all([
|
|
2068
|
+
api("GET", "/faults"),
|
|
2069
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
2070
|
+
]);
|
|
2071
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
2072
|
+
const current = select.value;
|
|
2073
|
+
const list = faultPresetList(presets).presets;
|
|
2074
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
2075
|
+
select.value = current;
|
|
2076
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
2077
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
2078
|
+
{
|
|
2079
|
+
label: "Match",
|
|
2080
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
label: "Effect",
|
|
2084
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
2085
|
+
}
|
|
2086
|
+
], decodeFaults(faults));
|
|
2087
|
+
};
|
|
2088
|
+
const renderJournal = async () => {
|
|
2089
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
2090
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
2091
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
2092
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2093
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2094
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
2095
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2096
|
+
], decodeJournal(journal));
|
|
2097
|
+
};
|
|
2098
|
+
const renderRoutes = async () => {
|
|
2099
|
+
const data = await api("GET", "/");
|
|
2100
|
+
if (!isRecord2(data))
|
|
2101
|
+
throw new Error("Route list was not an object");
|
|
2102
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2103
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2104
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2105
|
+
const parts = route.split(" ");
|
|
2106
|
+
const method = parts[0] ?? "";
|
|
2107
|
+
const path = parts.slice(1).join(" ");
|
|
2108
|
+
return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}">Use</button></div>`;
|
|
2109
|
+
}).join("");
|
|
2110
|
+
};
|
|
2111
|
+
const renderers = {
|
|
2112
|
+
overview: renderOverview,
|
|
2113
|
+
state: renderState,
|
|
2114
|
+
clock: renderClock,
|
|
2115
|
+
faults: renderFaults,
|
|
2116
|
+
journal: renderJournal,
|
|
2117
|
+
routes: renderRoutes
|
|
2118
|
+
};
|
|
2119
|
+
async function refresh() {
|
|
2120
|
+
clearError();
|
|
2121
|
+
const render = renderers[state.view];
|
|
2122
|
+
if (render)
|
|
2123
|
+
await render();
|
|
2124
|
+
}
|
|
2125
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
2126
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
2127
|
+
refresh().catch(showError);
|
|
2128
|
+
});
|
|
2129
|
+
keyInput.addEventListener("change", () => {
|
|
2130
|
+
state.key = keyInput.value.trim();
|
|
2131
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2132
|
+
refresh().catch(showError);
|
|
2133
|
+
});
|
|
2134
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
2135
|
+
refresh().catch(showError);
|
|
2136
|
+
});
|
|
2137
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
2138
|
+
const target = event.target;
|
|
2139
|
+
if (!(target instanceof Element))
|
|
2140
|
+
return;
|
|
2141
|
+
const button = target.closest("[data-collection]");
|
|
2142
|
+
if (!(button instanceof HTMLElement))
|
|
2143
|
+
return;
|
|
2144
|
+
const collection = button.dataset.collection;
|
|
2145
|
+
if (collection === void 0)
|
|
2146
|
+
return;
|
|
2147
|
+
state.collection = collection;
|
|
2148
|
+
renderState().catch(showError);
|
|
2149
|
+
});
|
|
2150
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
2151
|
+
const target = event.target;
|
|
2152
|
+
if (!(target instanceof Element))
|
|
2153
|
+
return;
|
|
2154
|
+
const row = target.closest("[data-id]");
|
|
2155
|
+
if (!(row instanceof HTMLElement))
|
|
2156
|
+
return;
|
|
2157
|
+
const id = row.dataset.id;
|
|
2158
|
+
if (id === void 0)
|
|
2159
|
+
return;
|
|
2160
|
+
openEditor(id).catch(showError);
|
|
2161
|
+
});
|
|
2162
|
+
const editor = el("editor", HTMLDialogElement);
|
|
2163
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
2164
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
2165
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
2166
|
+
async function openEditor(id) {
|
|
2167
|
+
state.editing = id;
|
|
2168
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
2169
|
+
editorId.value = id ?? "";
|
|
2170
|
+
editorId.readOnly = id !== null;
|
|
2171
|
+
editorDelete.hidden = id === null;
|
|
2172
|
+
if (id) {
|
|
2173
|
+
const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
2174
|
+
if (!isRecord2(record))
|
|
2175
|
+
throw new Error("Record response was not an object");
|
|
2176
|
+
editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
|
|
2177
|
+
} else {
|
|
2178
|
+
editorValue.value = "{\n \n}";
|
|
2179
|
+
}
|
|
2180
|
+
editor.showModal();
|
|
2181
|
+
}
|
|
2182
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
2183
|
+
openEditor(null).catch(showError);
|
|
2184
|
+
});
|
|
2185
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2186
|
+
const submitter = event.submitter;
|
|
2187
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
2188
|
+
return;
|
|
2189
|
+
event.preventDefault();
|
|
2190
|
+
let value;
|
|
2191
|
+
try {
|
|
2192
|
+
value = readJson2(editorValue.value);
|
|
2193
|
+
} catch {
|
|
2194
|
+
showError(new Error("Value is not JSON"));
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
const id = editorId.value.trim();
|
|
2198
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2199
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2200
|
+
request.then(() => {
|
|
2201
|
+
editor.close();
|
|
2202
|
+
return renderState();
|
|
2203
|
+
}).catch(showError);
|
|
2204
|
+
});
|
|
2205
|
+
editorDelete.addEventListener("click", () => {
|
|
2206
|
+
const id = state.editing;
|
|
2207
|
+
if (!id)
|
|
2208
|
+
return;
|
|
2209
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2210
|
+
editor.close();
|
|
2211
|
+
return renderState();
|
|
2212
|
+
}).catch(showError);
|
|
2213
|
+
});
|
|
2214
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2215
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2216
|
+
continue;
|
|
2217
|
+
button.addEventListener("click", () => {
|
|
2218
|
+
const raw = button.dataset.advance;
|
|
2219
|
+
if (raw === void 0)
|
|
2220
|
+
return;
|
|
2221
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2225
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2226
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2227
|
+
});
|
|
2228
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2229
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2230
|
+
});
|
|
2231
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2232
|
+
event.preventDefault();
|
|
2233
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2234
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2235
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2236
|
+
});
|
|
2237
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2238
|
+
event.preventDefault();
|
|
2239
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2240
|
+
if (preset !== "") {
|
|
2241
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2245
|
+
if (!Number.isFinite(status)) {
|
|
2246
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2250
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2251
|
+
api("POST", "/faults", {
|
|
2252
|
+
status,
|
|
2253
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2254
|
+
...method !== "" ? { method } : {}
|
|
2255
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2256
|
+
});
|
|
2257
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2258
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2259
|
+
});
|
|
2260
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2261
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2262
|
+
});
|
|
2263
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2264
|
+
const target = event.target;
|
|
2265
|
+
if (!(target instanceof Element))
|
|
2266
|
+
return;
|
|
2267
|
+
const button = target.closest("[data-route]");
|
|
2268
|
+
if (!(button instanceof HTMLElement))
|
|
2269
|
+
return;
|
|
2270
|
+
const route = button.dataset.route;
|
|
2271
|
+
if (route === void 0)
|
|
2272
|
+
return;
|
|
2273
|
+
state.route = route;
|
|
2274
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2275
|
+
const method = route.split(" ")[0] ?? "";
|
|
2276
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2277
|
+
});
|
|
2278
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2279
|
+
event.preventDefault();
|
|
2280
|
+
const send = async () => {
|
|
2281
|
+
if (state.route === null)
|
|
2282
|
+
throw new Error("Choose a route first");
|
|
2283
|
+
const parts = state.route.split(" ");
|
|
2284
|
+
const method = parts[0];
|
|
2285
|
+
const path = parts.slice(1).join(" ");
|
|
2286
|
+
if (method === void 0)
|
|
2287
|
+
throw new Error("Choose a route first");
|
|
2288
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2289
|
+
let body;
|
|
2290
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2291
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2292
|
+
try {
|
|
2293
|
+
body = readJson2(routeBody.value);
|
|
2294
|
+
} catch {
|
|
2295
|
+
throw new Error("Body is not JSON");
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const data = await api(method, concrete, body);
|
|
2299
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2300
|
+
};
|
|
2301
|
+
send().catch(showError);
|
|
2302
|
+
});
|
|
2303
|
+
const decodePanel = (value) => {
|
|
2304
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2305
|
+
return void 0;
|
|
2306
|
+
}
|
|
2307
|
+
const panel = {
|
|
2308
|
+
id: value.id,
|
|
2309
|
+
title: value.title,
|
|
2310
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2311
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2312
|
+
};
|
|
2313
|
+
if (typeof value.script === "string")
|
|
2314
|
+
panel.script = value.script;
|
|
2315
|
+
return panel;
|
|
2316
|
+
};
|
|
2317
|
+
const mountPanels = (panels) => {
|
|
2318
|
+
const nav = el("nav", HTMLElement);
|
|
2319
|
+
const views = el("views", HTMLElement);
|
|
2320
|
+
for (const panel of panels) {
|
|
2321
|
+
const button = document.createElement("button");
|
|
2322
|
+
button.type = "button";
|
|
2323
|
+
button.dataset.view = panel.id;
|
|
2324
|
+
button.textContent = panel.title;
|
|
2325
|
+
nav.append(button);
|
|
2326
|
+
const section = document.createElement("section");
|
|
2327
|
+
section.className = "view";
|
|
2328
|
+
section.id = `view-${panel.id}`;
|
|
2329
|
+
section.hidden = true;
|
|
2330
|
+
const title = document.createElement("h2");
|
|
2331
|
+
title.textContent = panel.title;
|
|
2332
|
+
const lede = document.createElement("p");
|
|
2333
|
+
lede.className = "lede";
|
|
2334
|
+
lede.textContent = panel.description;
|
|
2335
|
+
const body = document.createElement("div");
|
|
2336
|
+
body.className = "panel-body";
|
|
2337
|
+
body.id = `panel-${panel.id}`;
|
|
2338
|
+
body.innerHTML = panel.html;
|
|
2339
|
+
section.append(title, lede, body);
|
|
2340
|
+
views.append(section);
|
|
2341
|
+
if (panel.script === void 0)
|
|
2342
|
+
continue;
|
|
2343
|
+
try {
|
|
2344
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2345
|
+
if (!isPanelScript(compiled))
|
|
2346
|
+
throw new Error("panel script did not compile");
|
|
2347
|
+
compiled(body, panelApi);
|
|
2348
|
+
} catch (error2) {
|
|
2349
|
+
const note = document.createElement("p");
|
|
2350
|
+
note.className = "banner show";
|
|
2351
|
+
note.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2352
|
+
body.append(note);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
startAdminBrand({
|
|
2357
|
+
document,
|
|
2358
|
+
$: (id) => document.getElementById(id),
|
|
2359
|
+
params,
|
|
2360
|
+
location: { href: location.href, origin: location.origin },
|
|
2361
|
+
fetch,
|
|
2362
|
+
brandsUrl: config.brandsUrl
|
|
2363
|
+
});
|
|
2364
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2365
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2366
|
+
const decoded2 = decodePanel(panel);
|
|
2367
|
+
return decoded2 === void 0 ? [] : [decoded2];
|
|
2368
|
+
});
|
|
2369
|
+
mountPanels(panels);
|
|
2370
|
+
return refresh();
|
|
2371
|
+
}).catch(showError);
|
|
2372
|
+
}
|
|
2373
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2374
|
+
var adminClientSource = (config) => {
|
|
2375
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2376
|
+
return `<script>
|
|
2377
|
+
${clientPrelude}
|
|
2378
|
+
bootAdmin(${payload});
|
|
2379
|
+
</script>`;
|
|
2380
|
+
};
|
|
2381
|
+
|
|
1156
2382
|
// ../core/dist/surface.js
|
|
1157
2383
|
var defineMock = (runtime) => runtime;
|
|
1158
2384
|
var STANDARD_ADMIN_ROUTES = [
|
|
@@ -1221,94 +2447,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1221
2447
|
return "'";
|
|
1222
2448
|
}
|
|
1223
2449
|
});
|
|
2450
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1224
2451
|
var renderAdminDocument = (service) => {
|
|
1225
2452
|
const name = escapeHtml(service);
|
|
1226
|
-
const
|
|
2453
|
+
const script = adminClientSource({
|
|
2454
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2455
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2456
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2457
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2458
|
+
service
|
|
2459
|
+
});
|
|
1227
2460
|
return `<!DOCTYPE html>
|
|
1228
2461
|
<html lang="en">
|
|
1229
2462
|
<head>
|
|
1230
2463
|
<meta charset="utf-8">
|
|
1231
2464
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1232
2465
|
<meta name="color-scheme" content="light dark">
|
|
1233
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='
|
|
2466
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2' fill='%2327272a'/%3E%3C/svg%3E">
|
|
1234
2467
|
<title>${name} admin</title>
|
|
1235
2468
|
<style>
|
|
2469
|
+
${CSS_RESET}
|
|
1236
2470
|
:root {
|
|
1237
2471
|
color-scheme: light;
|
|
1238
|
-
--bg: #
|
|
2472
|
+
--bg: #fafafa;
|
|
1239
2473
|
--bg-elev: #ffffff;
|
|
1240
|
-
--bg-muted: #
|
|
1241
|
-
--bg-inset: #
|
|
1242
|
-
--line: #
|
|
1243
|
-
--line-strong: #
|
|
1244
|
-
--ink: #
|
|
1245
|
-
--muted: #
|
|
1246
|
-
--faint: #
|
|
1247
|
-
--accent: #
|
|
1248
|
-
--accent-ink: #
|
|
1249
|
-
--accent-soft: #
|
|
1250
|
-
--ok: #
|
|
1251
|
-
--ok-soft: #
|
|
1252
|
-
--warn: #
|
|
1253
|
-
--warn-soft: #
|
|
1254
|
-
--err: #
|
|
1255
|
-
--err-soft: #
|
|
1256
|
-
--shadow:
|
|
1257
|
-
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2474
|
+
--bg-muted: #f4f4f5;
|
|
2475
|
+
--bg-inset: #f4f4f5;
|
|
2476
|
+
--line: #dedee3;
|
|
2477
|
+
--line-strong: #c4c4cc;
|
|
2478
|
+
--ink: #18181b;
|
|
2479
|
+
--muted: #62626b;
|
|
2480
|
+
--faint: #71717a;
|
|
2481
|
+
--accent: #27272a;
|
|
2482
|
+
--accent-ink: #ffffff;
|
|
2483
|
+
--accent-soft: #f4f4f5;
|
|
2484
|
+
--ok: #166534;
|
|
2485
|
+
--ok-soft: #f0fdf4;
|
|
2486
|
+
--warn: #a16207;
|
|
2487
|
+
--warn-soft: #fefce8;
|
|
2488
|
+
--err: #a82d32;
|
|
2489
|
+
--err-soft: #fef2f2;
|
|
2490
|
+
--shadow: none;
|
|
2491
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1258
2492
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1259
|
-
--radius:
|
|
1260
|
-
--header:
|
|
1261
|
-
--nav:
|
|
2493
|
+
--radius: 10px;
|
|
2494
|
+
--header: 56px;
|
|
2495
|
+
--nav: 200px;
|
|
1262
2496
|
}
|
|
1263
2497
|
:root[data-theme="dark"] {
|
|
1264
2498
|
color-scheme: dark;
|
|
1265
|
-
--bg: #
|
|
1266
|
-
--bg-elev: #
|
|
1267
|
-
--bg-muted: #
|
|
1268
|
-
--bg-inset: #
|
|
1269
|
-
--line: #
|
|
1270
|
-
--line-strong: #
|
|
1271
|
-
--ink: #
|
|
1272
|
-
--muted: #
|
|
1273
|
-
--faint: #
|
|
1274
|
-
--accent: #
|
|
1275
|
-
--accent-ink: #
|
|
1276
|
-
--accent-soft: #
|
|
1277
|
-
--ok: #
|
|
1278
|
-
--ok-soft: #
|
|
1279
|
-
--warn: #
|
|
1280
|
-
--warn-soft: #
|
|
1281
|
-
--err: #
|
|
1282
|
-
--err-soft: #
|
|
1283
|
-
--shadow:
|
|
2499
|
+
--bg: #111113;
|
|
2500
|
+
--bg-elev: #19191c;
|
|
2501
|
+
--bg-muted: #242428;
|
|
2502
|
+
--bg-inset: #111113;
|
|
2503
|
+
--line: #36363c;
|
|
2504
|
+
--line-strong: #52525b;
|
|
2505
|
+
--ink: #f4f4f5;
|
|
2506
|
+
--muted: #a9a9b2;
|
|
2507
|
+
--faint: #71717a;
|
|
2508
|
+
--accent: #e4e4e7;
|
|
2509
|
+
--accent-ink: #18181b;
|
|
2510
|
+
--accent-soft: #242428;
|
|
2511
|
+
--ok: #86efac;
|
|
2512
|
+
--ok-soft: #14241b;
|
|
2513
|
+
--warn: #fde68a;
|
|
2514
|
+
--warn-soft: #2a2410;
|
|
2515
|
+
--err: #ffaaaa;
|
|
2516
|
+
--err-soft: #2c1516;
|
|
2517
|
+
--shadow: none;
|
|
1284
2518
|
}
|
|
1285
2519
|
@media (prefers-color-scheme: dark) {
|
|
1286
2520
|
:root:not([data-theme="light"]) {
|
|
1287
2521
|
color-scheme: dark;
|
|
1288
|
-
--bg: #
|
|
1289
|
-
--bg-elev: #
|
|
1290
|
-
--bg-muted: #
|
|
1291
|
-
--bg-inset: #
|
|
1292
|
-
--line: #
|
|
1293
|
-
--line-strong: #
|
|
1294
|
-
--ink: #
|
|
1295
|
-
--muted: #
|
|
1296
|
-
--faint: #
|
|
1297
|
-
--accent: #
|
|
1298
|
-
--accent-ink: #
|
|
1299
|
-
--accent-soft: #
|
|
1300
|
-
--ok: #
|
|
1301
|
-
--ok-soft: #
|
|
1302
|
-
--warn: #
|
|
1303
|
-
--warn-soft: #
|
|
1304
|
-
--err: #
|
|
1305
|
-
--err-soft: #
|
|
1306
|
-
--shadow:
|
|
2522
|
+
--bg: #111113;
|
|
2523
|
+
--bg-elev: #19191c;
|
|
2524
|
+
--bg-muted: #242428;
|
|
2525
|
+
--bg-inset: #111113;
|
|
2526
|
+
--line: #36363c;
|
|
2527
|
+
--line-strong: #52525b;
|
|
2528
|
+
--ink: #f4f4f5;
|
|
2529
|
+
--muted: #a9a9b2;
|
|
2530
|
+
--faint: #71717a;
|
|
2531
|
+
--accent: #e4e4e7;
|
|
2532
|
+
--accent-ink: #18181b;
|
|
2533
|
+
--accent-soft: #242428;
|
|
2534
|
+
--ok: #86efac;
|
|
2535
|
+
--ok-soft: #14241b;
|
|
2536
|
+
--warn: #fde68a;
|
|
2537
|
+
--warn-soft: #2a2410;
|
|
2538
|
+
--err: #ffaaaa;
|
|
2539
|
+
--err-soft: #2c1516;
|
|
2540
|
+
--shadow: none;
|
|
1307
2541
|
}
|
|
1308
2542
|
}
|
|
1309
|
-
|
|
1310
|
-
html
|
|
2543
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2544
|
+
html { height: 100%; }
|
|
1311
2545
|
body {
|
|
2546
|
+
margin: 0;
|
|
2547
|
+
min-height: 100%;
|
|
2548
|
+
display: flex;
|
|
2549
|
+
flex-direction: column;
|
|
1312
2550
|
font-family: var(--sans);
|
|
1313
2551
|
background: var(--bg);
|
|
1314
2552
|
color: var(--ink);
|
|
@@ -1317,24 +2555,37 @@ var renderAdminDocument = (service) => {
|
|
|
1317
2555
|
padding-right: env(safe-area-inset-right);
|
|
1318
2556
|
-webkit-text-size-adjust: 100%;
|
|
1319
2557
|
}
|
|
1320
|
-
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1321
|
-
button { cursor: pointer; }
|
|
1322
2558
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1323
|
-
|
|
1324
|
-
* { scroll-behavior: auto !important; transition: none !important; }
|
|
1325
|
-
}
|
|
1326
|
-
.app { min-height: 100%; display: flex; flex-direction: column; }
|
|
2559
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1327
2560
|
header.top {
|
|
1328
2561
|
position: sticky; top: 0; z-index: 5;
|
|
1329
2562
|
display: flex; align-items: center; gap: 12px;
|
|
1330
2563
|
min-height: var(--header); padding: 10px 16px;
|
|
1331
|
-
background:
|
|
2564
|
+
background: var(--bg-elev);
|
|
1332
2565
|
border-bottom: 1px solid var(--line);
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1335
|
-
.brand {
|
|
1336
|
-
|
|
1337
|
-
|
|
2566
|
+
min-width: 0; max-width: 100%;
|
|
2567
|
+
}
|
|
2568
|
+
.brand {
|
|
2569
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2570
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2571
|
+
}
|
|
2572
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2573
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2574
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2575
|
+
.vendor {
|
|
2576
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2577
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2578
|
+
}
|
|
2579
|
+
.vendor[hidden] { display: none; }
|
|
2580
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2581
|
+
.vendor img {
|
|
2582
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2583
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2584
|
+
}
|
|
2585
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2586
|
+
.vendor-sep { color: var(--faint); }
|
|
2587
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2588
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1338
2589
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
1339
2590
|
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
1340
2591
|
label.field input, label.field select, .controls input[type="password"] {
|
|
@@ -1352,7 +2603,7 @@ var renderAdminDocument = (service) => {
|
|
|
1352
2603
|
record or the full nav row would widen the page past the viewport. */
|
|
1353
2604
|
.layout {
|
|
1354
2605
|
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
1355
|
-
flex: 1
|
|
2606
|
+
flex: 1 0 auto; min-width: 0;
|
|
1356
2607
|
}
|
|
1357
2608
|
nav.side {
|
|
1358
2609
|
border-right: 1px solid var(--line); padding: 12px;
|
|
@@ -1406,8 +2657,8 @@ var renderAdminDocument = (service) => {
|
|
|
1406
2657
|
.banner.show { display: block; }
|
|
1407
2658
|
.empty { color: var(--muted); padding: 24px 8px; }
|
|
1408
2659
|
dialog {
|
|
1409
|
-
border: 1px solid var(--line); border-radius:
|
|
1410
|
-
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow:
|
|
2660
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2661
|
+
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
|
|
1411
2662
|
}
|
|
1412
2663
|
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
1413
2664
|
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -1434,7 +2685,7 @@ var renderAdminDocument = (service) => {
|
|
|
1434
2685
|
padding-top: max(10px, env(safe-area-inset-top));
|
|
1435
2686
|
}
|
|
1436
2687
|
input, select, textarea { font-size: 16px; }
|
|
1437
|
-
.controls { width: 100%; }
|
|
2688
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
1438
2689
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
1439
2690
|
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
1440
2691
|
nav.side {
|
|
@@ -1465,8 +2716,11 @@ var renderAdminDocument = (service) => {
|
|
|
1465
2716
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
1466
2717
|
<header class="top">
|
|
1467
2718
|
<div class="brand">
|
|
1468
|
-
<
|
|
1469
|
-
|
|
2719
|
+
<div class="brand-id">
|
|
2720
|
+
<strong>${name}</strong>
|
|
2721
|
+
<span>Admin</span>
|
|
2722
|
+
</div>
|
|
2723
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
1470
2724
|
</div>
|
|
1471
2725
|
<div class="controls">
|
|
1472
2726
|
<label class="field">Namespace
|
|
@@ -1596,460 +2850,7 @@ var renderAdminDocument = (service) => {
|
|
|
1596
2850
|
</div>
|
|
1597
2851
|
</form>
|
|
1598
2852
|
</dialog>
|
|
1599
|
-
|
|
1600
|
-
(() => {
|
|
1601
|
-
const STANDARD = new Set(${standard})
|
|
1602
|
-
const state = {
|
|
1603
|
-
namespace: "default",
|
|
1604
|
-
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1605
|
-
view: "overview",
|
|
1606
|
-
collection: "",
|
|
1607
|
-
shape: null,
|
|
1608
|
-
route: null,
|
|
1609
|
-
editing: null
|
|
1610
|
-
}
|
|
1611
|
-
const $ = (id) => document.getElementById(id)
|
|
1612
|
-
const banner = $("banner")
|
|
1613
|
-
const keyInput = $("admin-key")
|
|
1614
|
-
keyInput.value = state.key
|
|
1615
|
-
const params = new URLSearchParams(location.search)
|
|
1616
|
-
if (params.get("key")) {
|
|
1617
|
-
state.key = params.get("key")
|
|
1618
|
-
keyInput.value = state.key
|
|
1619
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1620
|
-
params.delete("key")
|
|
1621
|
-
const next = params.toString()
|
|
1622
|
-
history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
|
|
1623
|
-
}
|
|
1624
|
-
if (params.get("namespace")) state.namespace = params.get("namespace")
|
|
1625
|
-
|
|
1626
|
-
const showError = (error) => {
|
|
1627
|
-
banner.textContent = error instanceof Error ? error.message : String(error)
|
|
1628
|
-
banner.classList.add("show")
|
|
1629
|
-
}
|
|
1630
|
-
const clearError = () => banner.classList.remove("show")
|
|
1631
|
-
|
|
1632
|
-
async function originGet(path) {
|
|
1633
|
-
const response = await fetch(path, { headers: { accept: "application/json" } })
|
|
1634
|
-
if (!response.ok) throw new Error(response.status + " " + path)
|
|
1635
|
-
return response.json()
|
|
1636
|
-
}
|
|
1637
|
-
async function api(method, path, body) {
|
|
1638
|
-
const headers = { accept: "application/json" }
|
|
1639
|
-
if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
|
|
1640
|
-
if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
|
|
1641
|
-
if (body !== undefined) headers["content-type"] = "application/json"
|
|
1642
|
-
const response = await fetch("${ADMIN_PREFIX}" + path, {
|
|
1643
|
-
method,
|
|
1644
|
-
headers,
|
|
1645
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1646
|
-
})
|
|
1647
|
-
const text = await response.text()
|
|
1648
|
-
let data = null
|
|
1649
|
-
if (text) {
|
|
1650
|
-
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
|
1651
|
-
}
|
|
1652
|
-
if (response.status === 401) {
|
|
1653
|
-
keyInput.focus()
|
|
1654
|
-
throw new Error((data && data.error && data.error.message) || "Admin key required")
|
|
1655
|
-
}
|
|
1656
|
-
if (!response.ok) {
|
|
1657
|
-
throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
|
|
1658
|
-
}
|
|
1659
|
-
return data
|
|
1660
|
-
}
|
|
1661
|
-
const panelApi = {
|
|
1662
|
-
get: (path) => api("GET", path),
|
|
1663
|
-
send: (method, path, body) => api(method, path, body),
|
|
1664
|
-
namespace: () => state.namespace
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function setTheme(theme) {
|
|
1668
|
-
if (theme) document.documentElement.dataset.theme = theme
|
|
1669
|
-
else delete document.documentElement.dataset.theme
|
|
1670
|
-
}
|
|
1671
|
-
const storedTheme = localStorage.getItem("mockingbird-admin-theme")
|
|
1672
|
-
if (storedTheme) setTheme(storedTheme)
|
|
1673
|
-
$("theme").addEventListener("click", () => {
|
|
1674
|
-
const current = document.documentElement.dataset.theme
|
|
1675
|
-
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
|
|
1676
|
-
const next = dark ? "light" : "dark"
|
|
1677
|
-
localStorage.setItem("mockingbird-admin-theme", next)
|
|
1678
|
-
setTheme(next)
|
|
1679
|
-
})
|
|
1680
|
-
|
|
1681
|
-
function show(view) {
|
|
1682
|
-
state.view = view
|
|
1683
|
-
for (const button of document.querySelectorAll("#nav button")) {
|
|
1684
|
-
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
|
|
1685
|
-
}
|
|
1686
|
-
for (const section of document.querySelectorAll("main .view")) {
|
|
1687
|
-
section.hidden = section.id !== "view-" + view
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
document.getElementById("nav").addEventListener("click", (event) => {
|
|
1691
|
-
const button = event.target.closest("button[data-view]")
|
|
1692
|
-
if (!button) return
|
|
1693
|
-
show(button.dataset.view)
|
|
1694
|
-
refresh().catch(showError)
|
|
1695
|
-
})
|
|
1696
|
-
|
|
1697
|
-
function cards(items) {
|
|
1698
|
-
return '<div class="grid">' + items.map((item) =>
|
|
1699
|
-
'<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
|
|
1700
|
-
).join("") + "</div>"
|
|
1701
|
-
}
|
|
1702
|
-
function table(columns, rows) {
|
|
1703
|
-
if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
|
|
1704
|
-
const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
|
|
1705
|
-
const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
|
|
1706
|
-
'<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
|
|
1707
|
-
).join("") + "</tr>").join("")
|
|
1708
|
-
return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
|
|
1709
|
-
}
|
|
1710
|
-
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
1711
|
-
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
1712
|
-
}[char]))
|
|
1713
|
-
const preview = (value) => {
|
|
1714
|
-
const text = JSON.stringify(value)
|
|
1715
|
-
return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
|
|
1716
|
-
}
|
|
1717
|
-
const when = (epoch) => {
|
|
1718
|
-
const date = new Date(epoch)
|
|
1719
|
-
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
|
|
1720
|
-
}
|
|
1721
|
-
const clockCard = (epoch) => {
|
|
1722
|
-
const date = new Date(epoch)
|
|
1723
|
-
if (Number.isNaN(date.getTime())) return esc(String(epoch))
|
|
1724
|
-
const iso = date.toISOString()
|
|
1725
|
-
return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
async function loadNamespaces() {
|
|
1729
|
-
const data = await api("GET", "/namespaces")
|
|
1730
|
-
const select = $("namespace")
|
|
1731
|
-
const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
|
|
1732
|
-
if (!names.includes(state.namespace)) names.unshift(state.namespace)
|
|
1733
|
-
select.innerHTML = names.map((name) =>
|
|
1734
|
-
'<option value="' + esc(name) + '">' + esc(name) + "</option>"
|
|
1735
|
-
).join("")
|
|
1736
|
-
select.value = state.namespace
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
async function renderOverview() {
|
|
1740
|
-
const [health, shape, clock, journal] = await Promise.all([
|
|
1741
|
-
originGet("/health"),
|
|
1742
|
-
api("GET", "/state"),
|
|
1743
|
-
api("GET", "/clock"),
|
|
1744
|
-
api("GET", "/requests?limit=8")
|
|
1745
|
-
])
|
|
1746
|
-
state.shape = shape
|
|
1747
|
-
$("overview-cards").innerHTML = cards([
|
|
1748
|
-
{ k: "Service", v: esc(health.service || "${name}") },
|
|
1749
|
-
{ k: "Status", v: esc(health.status || "ok") },
|
|
1750
|
-
{ k: "Namespace", v: esc(state.namespace) },
|
|
1751
|
-
{ k: "Collections", v: String(shape.collections.length) },
|
|
1752
|
-
{ k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
|
|
1753
|
-
{ k: "Clock", v: clockCard(clock.now) }
|
|
1754
|
-
])
|
|
1755
|
-
const requests = (journal && journal.requests) || []
|
|
1756
|
-
$("overview-requests").innerHTML = table(
|
|
1757
|
-
[
|
|
1758
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1759
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1760
|
-
{ label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
|
|
1761
|
-
{ label: "Operation", cell: (row) => esc(row.operationId || "") }
|
|
1762
|
-
],
|
|
1763
|
-
requests
|
|
1764
|
-
)
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
function schemaChips(collection) {
|
|
1768
|
-
if (!collection || collection.fields.length === 0) {
|
|
1769
|
-
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
|
|
1770
|
-
}
|
|
1771
|
-
return '<div class="chips">' + collection.fields.map((field) =>
|
|
1772
|
-
'<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
|
|
1773
|
-
).join("") + "</div>"
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
async function renderState() {
|
|
1777
|
-
const shape = await api("GET", "/state")
|
|
1778
|
-
state.shape = shape
|
|
1779
|
-
const host = $("collections")
|
|
1780
|
-
if (shape.collections.length === 0) {
|
|
1781
|
-
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
|
|
1782
|
-
$("schema").innerHTML = ""
|
|
1783
|
-
$("records").innerHTML = ""
|
|
1784
|
-
return
|
|
1785
|
-
}
|
|
1786
|
-
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1787
|
-
state.collection = shape.collections[0].name
|
|
1788
|
-
}
|
|
1789
|
-
host.innerHTML = shape.collections.map((item) =>
|
|
1790
|
-
'<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
|
|
1791
|
-
(item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
|
|
1792
|
-
'</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
|
|
1793
|
-
).join("")
|
|
1794
|
-
const selected = shape.collections.find((item) => item.name === state.collection)
|
|
1795
|
-
$("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
|
|
1796
|
-
(selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
|
|
1797
|
-
schemaChips(selected)
|
|
1798
|
-
const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
|
|
1799
|
-
const rows = page.records.map((record) => ({
|
|
1800
|
-
...record,
|
|
1801
|
-
attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
|
|
1802
|
-
}))
|
|
1803
|
-
$("records").innerHTML = table(
|
|
1804
|
-
[
|
|
1805
|
-
{ label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
|
|
1806
|
-
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
1807
|
-
{ label: "Value", cell: (row) => preview(row.value) }
|
|
1808
|
-
],
|
|
1809
|
-
rows
|
|
1810
|
-
)
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
async function renderClock() {
|
|
1814
|
-
const clock = await api("GET", "/clock")
|
|
1815
|
-
$("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
|
|
1816
|
-
"</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
|
|
1817
|
-
(clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
|
|
1818
|
-
$("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
|
|
1819
|
-
$("freeze").dataset.frozen = clock.frozen ? "1" : "0"
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
async function renderFaults() {
|
|
1823
|
-
const [faults, presets] = await Promise.all([
|
|
1824
|
-
api("GET", "/faults"),
|
|
1825
|
-
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
1826
|
-
])
|
|
1827
|
-
const select = $("fault-preset")
|
|
1828
|
-
const current = select.value
|
|
1829
|
-
const list = presets.presets || []
|
|
1830
|
-
select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
|
|
1831
|
-
'<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
|
|
1832
|
-
).join("")
|
|
1833
|
-
select.value = current
|
|
1834
|
-
const rules = faults.faults || []
|
|
1835
|
-
$("fault-list").innerHTML = table(
|
|
1836
|
-
[
|
|
1837
|
-
{ label: "Id", cell: (row) => esc(row.id) },
|
|
1838
|
-
{ label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
|
|
1839
|
-
{ label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
|
|
1840
|
-
],
|
|
1841
|
-
rules
|
|
1842
|
-
)
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
async function renderJournal() {
|
|
1846
|
-
const journal = await api("GET", "/requests?limit=100")
|
|
1847
|
-
$("journal").innerHTML = table(
|
|
1848
|
-
[
|
|
1849
|
-
{ label: "When", cell: (row) => esc(row.at || "") },
|
|
1850
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1851
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1852
|
-
{ label: "Status", cell: (row) => esc(row.status) },
|
|
1853
|
-
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1854
|
-
],
|
|
1855
|
-
journal.requests || []
|
|
1856
|
-
)
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
async function renderRoutes() {
|
|
1860
|
-
const data = await api("GET", "/")
|
|
1861
|
-
const routes = data.routes || []
|
|
1862
|
-
$("route-list").innerHTML = routes.map((route) => {
|
|
1863
|
-
const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
|
|
1864
|
-
return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
|
|
1865
|
-
'</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
|
|
1866
|
-
'</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
|
|
1867
|
-
}).join("")
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
const renderers = {
|
|
1871
|
-
overview: renderOverview,
|
|
1872
|
-
state: renderState,
|
|
1873
|
-
clock: renderClock,
|
|
1874
|
-
faults: renderFaults,
|
|
1875
|
-
journal: renderJournal,
|
|
1876
|
-
routes: renderRoutes
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
async function refresh() {
|
|
1880
|
-
clearError()
|
|
1881
|
-
if (state.view in renderers) await renderers[state.view]()
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
$("namespace").addEventListener("change", () => {
|
|
1885
|
-
state.namespace = $("namespace").value
|
|
1886
|
-
refresh().catch(showError)
|
|
1887
|
-
})
|
|
1888
|
-
keyInput.addEventListener("change", () => {
|
|
1889
|
-
state.key = keyInput.value.trim()
|
|
1890
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1891
|
-
refresh().catch(showError)
|
|
1892
|
-
})
|
|
1893
|
-
$("refresh").addEventListener("click", () => refresh().catch(showError))
|
|
1894
|
-
|
|
1895
|
-
$("collections").addEventListener("click", (event) => {
|
|
1896
|
-
const button = event.target.closest("[data-collection]")
|
|
1897
|
-
if (!button) return
|
|
1898
|
-
state.collection = button.dataset.collection
|
|
1899
|
-
renderState().catch(showError)
|
|
1900
|
-
})
|
|
1901
|
-
$("records").addEventListener("click", (event) => {
|
|
1902
|
-
const row = event.target.closest("[data-id]")
|
|
1903
|
-
if (!row) return
|
|
1904
|
-
openEditor(row.dataset.id).catch(showError)
|
|
1905
|
-
})
|
|
1906
|
-
|
|
1907
|
-
const editor = $("editor")
|
|
1908
|
-
async function openEditor(id) {
|
|
1909
|
-
state.editing = id || null
|
|
1910
|
-
$("editor-title").textContent = id ? "Edit record" : "New record"
|
|
1911
|
-
$("editor-id").value = id || ""
|
|
1912
|
-
$("editor-id").readOnly = Boolean(id)
|
|
1913
|
-
$("editor-delete").hidden = !id
|
|
1914
|
-
if (id) {
|
|
1915
|
-
const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1916
|
-
$("editor-value").value = JSON.stringify(record.value, null, 2)
|
|
1917
|
-
} else {
|
|
1918
|
-
$("editor-value").value = "{\\n \\n}"
|
|
1919
|
-
}
|
|
1920
|
-
editor.showModal()
|
|
1921
|
-
}
|
|
1922
|
-
$("new-record").addEventListener("click", () => openEditor(null).catch(showError))
|
|
1923
|
-
$("editor-form").addEventListener("submit", (event) => {
|
|
1924
|
-
const submitter = event.submitter
|
|
1925
|
-
if (!submitter || submitter.value !== "save") return
|
|
1926
|
-
event.preventDefault()
|
|
1927
|
-
let value
|
|
1928
|
-
try { value = JSON.parse($("editor-value").value) }
|
|
1929
|
-
catch { showError(new Error("Value is not JSON")); return }
|
|
1930
|
-
const id = $("editor-id").value.trim()
|
|
1931
|
-
const path = "/state/" + encodeURIComponent(state.collection)
|
|
1932
|
-
const request = state.editing
|
|
1933
|
-
? api("PUT", path + "/" + encodeURIComponent(id), { value })
|
|
1934
|
-
: api("POST", path, { id: id || undefined, value })
|
|
1935
|
-
request.then(() => { editor.close(); return renderState() }).catch(showError)
|
|
1936
|
-
})
|
|
1937
|
-
$("editor-delete").addEventListener("click", () => {
|
|
1938
|
-
const id = state.editing
|
|
1939
|
-
if (!id) return
|
|
1940
|
-
api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1941
|
-
.then(() => { editor.close(); return renderState() })
|
|
1942
|
-
.catch(showError)
|
|
1943
|
-
})
|
|
1944
|
-
|
|
1945
|
-
document.querySelectorAll("[data-advance]").forEach((button) => {
|
|
1946
|
-
button.addEventListener("click", () => {
|
|
1947
|
-
api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
|
|
1948
|
-
})
|
|
1949
|
-
})
|
|
1950
|
-
$("freeze").addEventListener("click", () => {
|
|
1951
|
-
const frozen = $("freeze").dataset.frozen === "1"
|
|
1952
|
-
api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
|
|
1953
|
-
})
|
|
1954
|
-
$("clock-reset").addEventListener("click", () => {
|
|
1955
|
-
api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
|
|
1956
|
-
})
|
|
1957
|
-
$("clock-set").addEventListener("submit", (event) => {
|
|
1958
|
-
event.preventDefault()
|
|
1959
|
-
const raw = $("clock-instant").value.trim()
|
|
1960
|
-
const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
|
|
1961
|
-
api("POST", "/clock", { set }).then(renderClock).catch(showError)
|
|
1962
|
-
})
|
|
1963
|
-
|
|
1964
|
-
$("fault-form").addEventListener("submit", (event) => {
|
|
1965
|
-
event.preventDefault()
|
|
1966
|
-
const preset = $("fault-preset").value
|
|
1967
|
-
const body = preset
|
|
1968
|
-
? { preset }
|
|
1969
|
-
: {
|
|
1970
|
-
status: Number($("fault-status").value),
|
|
1971
|
-
...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
|
|
1972
|
-
...($("fault-method").value ? { method: $("fault-method").value } : {})
|
|
1973
|
-
}
|
|
1974
|
-
if (!preset && !Number.isFinite(body.status)) {
|
|
1975
|
-
showError(new Error("Status must be a number, or choose a preset"))
|
|
1976
|
-
return
|
|
1977
|
-
}
|
|
1978
|
-
api("POST", "/faults", body).then(renderFaults).catch(showError)
|
|
1979
|
-
})
|
|
1980
|
-
$("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
|
|
1981
|
-
$("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
|
|
1982
|
-
|
|
1983
|
-
$("route-list").addEventListener("click", (event) => {
|
|
1984
|
-
const button = event.target.closest("[data-route]")
|
|
1985
|
-
if (!button) return
|
|
1986
|
-
state.route = button.dataset.route
|
|
1987
|
-
$("route-title").textContent = state.route
|
|
1988
|
-
const method = state.route.split(" ")[0]
|
|
1989
|
-
$("route-body").disabled = method === "GET" || method === "DELETE"
|
|
1990
|
-
})
|
|
1991
|
-
$("route-form").addEventListener("submit", async (event) => {
|
|
1992
|
-
event.preventDefault()
|
|
1993
|
-
if (!state.route) return showError(new Error("Choose a route first"))
|
|
1994
|
-
const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
|
|
1995
|
-
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
|
|
1996
|
-
let body
|
|
1997
|
-
if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
|
|
1998
|
-
try { body = JSON.parse($("route-body").value) }
|
|
1999
|
-
catch { return showError(new Error("Body is not JSON")) }
|
|
2000
|
-
}
|
|
2001
|
-
try {
|
|
2002
|
-
const data = await api(method, concrete, body)
|
|
2003
|
-
$("route-result").textContent = JSON.stringify(data, null, 2)
|
|
2004
|
-
} catch (error) {
|
|
2005
|
-
showError(error)
|
|
2006
|
-
}
|
|
2007
|
-
})
|
|
2008
|
-
|
|
2009
|
-
function mountPanels(panels) {
|
|
2010
|
-
const nav = $("nav")
|
|
2011
|
-
const views = $("views")
|
|
2012
|
-
for (const panel of panels) {
|
|
2013
|
-
const button = document.createElement("button")
|
|
2014
|
-
button.type = "button"
|
|
2015
|
-
button.dataset.view = panel.id
|
|
2016
|
-
button.textContent = panel.title
|
|
2017
|
-
nav.append(button)
|
|
2018
|
-
const section = document.createElement("section")
|
|
2019
|
-
section.className = "view"
|
|
2020
|
-
section.id = "view-" + panel.id
|
|
2021
|
-
section.hidden = true
|
|
2022
|
-
const title = document.createElement("h2")
|
|
2023
|
-
title.textContent = panel.title
|
|
2024
|
-
const lede = document.createElement("p")
|
|
2025
|
-
lede.className = "lede"
|
|
2026
|
-
lede.textContent = panel.description || ""
|
|
2027
|
-
const body = document.createElement("div")
|
|
2028
|
-
body.className = "panel-body"
|
|
2029
|
-
body.id = "panel-" + panel.id
|
|
2030
|
-
body.innerHTML = panel.html || ""
|
|
2031
|
-
section.append(title, lede, body)
|
|
2032
|
-
views.append(section)
|
|
2033
|
-
if (panel.script) {
|
|
2034
|
-
try {
|
|
2035
|
-
const run = new Function("root", "api", panel.script)
|
|
2036
|
-
run(body, panelApi)
|
|
2037
|
-
} catch (error) {
|
|
2038
|
-
const note = document.createElement("p")
|
|
2039
|
-
note.className = "banner show"
|
|
2040
|
-
note.textContent = error instanceof Error ? error.message : String(error)
|
|
2041
|
-
body.append(note)
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
loadNamespaces()
|
|
2048
|
-
.then(() => api("GET", "/ui/manifest"))
|
|
2049
|
-
.then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
|
|
2050
|
-
.catch(showError)
|
|
2051
|
-
})()
|
|
2052
|
-
</script>
|
|
2853
|
+
${script}
|
|
2053
2854
|
</body>
|
|
2054
2855
|
</html>`;
|
|
2055
2856
|
};
|
|
@@ -2066,10 +2867,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
|
|
|
2066
2867
|
});
|
|
2067
2868
|
var adminUiRoutes = (service, ui) => {
|
|
2068
2869
|
const shell = () => renderAdminDocument(service);
|
|
2069
|
-
const
|
|
2870
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
2070
2871
|
return {
|
|
2071
|
-
"GET /ui": () => htmlResponse(
|
|
2072
|
-
"GET /ui/": () => htmlResponse(
|
|
2872
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
2873
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
2073
2874
|
"GET /ui/manifest": () => json2({
|
|
2074
2875
|
service,
|
|
2075
2876
|
panels: (ui?.panels ?? []).map((panel) => ({
|
|
@@ -2095,11 +2896,11 @@ var EXTENSION_KEYS = {
|
|
|
2095
2896
|
};
|
|
2096
2897
|
|
|
2097
2898
|
// ../../openapi/metadata/dist/read.js
|
|
2098
|
-
var
|
|
2899
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2099
2900
|
var extensionOf = (holder, key) => holder[key];
|
|
2100
2901
|
var operationMetadata = (operation) => {
|
|
2101
2902
|
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
2102
|
-
const ext =
|
|
2903
|
+
const ext = isRecord3(raw) ? raw : {};
|
|
2103
2904
|
const supported = ext.supported ?? true;
|
|
2104
2905
|
const parity = ext.parity ?? {};
|
|
2105
2906
|
return {
|
|
@@ -2135,9 +2936,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
2135
2936
|
this.name = "OperationRegistryError";
|
|
2136
2937
|
}
|
|
2137
2938
|
};
|
|
2138
|
-
var verifyOperations = (
|
|
2939
|
+
var verifyOperations = (document3, handlers) => {
|
|
2139
2940
|
const problems = [];
|
|
2140
|
-
const operations = listOperations(
|
|
2941
|
+
const operations = listOperations(document3);
|
|
2141
2942
|
const seen = /* @__PURE__ */ new Set();
|
|
2142
2943
|
for (const operation of operations) {
|
|
2143
2944
|
if (seen.has(operation.operationId))
|
|
@@ -2253,7 +3054,7 @@ var SAMPLE = 25;
|
|
|
2253
3054
|
var WALK_LIMIT = 1e4;
|
|
2254
3055
|
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
2255
3056
|
var fail = (status, message) => ({ ok: false, status, message });
|
|
2256
|
-
var
|
|
3057
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2257
3058
|
var kindOf = (value) => {
|
|
2258
3059
|
if (value === null)
|
|
2259
3060
|
return "null";
|
|
@@ -2352,7 +3153,7 @@ var absorb = (accum, value) => {
|
|
|
2352
3153
|
const kind = kindOf(value);
|
|
2353
3154
|
accum.kinds.add(kind);
|
|
2354
3155
|
accum.present++;
|
|
2355
|
-
if (!
|
|
3156
|
+
if (!isRecord4(value))
|
|
2356
3157
|
return;
|
|
2357
3158
|
for (const [key, child] of Object.entries(value)) {
|
|
2358
3159
|
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
@@ -2377,7 +3178,7 @@ var inferFields = (records) => {
|
|
|
2377
3178
|
if (records.length === 0)
|
|
2378
3179
|
return [];
|
|
2379
3180
|
const sample = records.slice(0, SAMPLE);
|
|
2380
|
-
const objects = sample.filter((record) =>
|
|
3181
|
+
const objects = sample.filter((record) => isRecord4(record.value));
|
|
2381
3182
|
if (objects.length === 0) {
|
|
2382
3183
|
const kinds = new Set(sample.map((record) => kindOf(record.value)));
|
|
2383
3184
|
const list = [...kinds];
|
|
@@ -2564,7 +3365,7 @@ var patchStateRecord = (scope, name, id, value) => {
|
|
|
2564
3365
|
const parsed = jsonValue(value);
|
|
2565
3366
|
if (!parsed.ok)
|
|
2566
3367
|
return parsed;
|
|
2567
|
-
const next =
|
|
3368
|
+
const next = isRecord4(current.value.value) && isRecord4(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
2568
3369
|
return replaceStateRecord(scope, name, id, next);
|
|
2569
3370
|
};
|
|
2570
3371
|
var deleteStateRecord = (scope, name, id) => {
|
|
@@ -2632,7 +3433,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2632
3433
|
const scope = open(namespace);
|
|
2633
3434
|
if (!scope.ok)
|
|
2634
3435
|
return fromResult(scope, 201);
|
|
2635
|
-
if (!
|
|
3436
|
+
if (!isRecord4(body) || !("value" in body)) {
|
|
2636
3437
|
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
2637
3438
|
}
|
|
2638
3439
|
const id = body.id;
|
|
@@ -2650,7 +3451,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2650
3451
|
const scope = open(namespace);
|
|
2651
3452
|
if (!scope.ok)
|
|
2652
3453
|
return fromResult(scope, 200);
|
|
2653
|
-
if (!
|
|
3454
|
+
if (!isRecord4(body) || !("value" in body))
|
|
2654
3455
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2655
3456
|
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2656
3457
|
if (replaced.ok)
|
|
@@ -2661,7 +3462,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2661
3462
|
const scope = open(namespace);
|
|
2662
3463
|
if (!scope.ok)
|
|
2663
3464
|
return fromResult(scope, 200);
|
|
2664
|
-
if (!
|
|
3465
|
+
if (!isRecord4(body) || !("value" in body))
|
|
2665
3466
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2666
3467
|
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2667
3468
|
if (patched.ok)
|
|
@@ -2681,7 +3482,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2681
3482
|
};
|
|
2682
3483
|
|
|
2683
3484
|
// ../core/dist/version.js
|
|
2684
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3485
|
+
var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
|
|
2685
3486
|
|
|
2686
3487
|
// ../core/dist/signing.js
|
|
2687
3488
|
var encoder = new TextEncoder();
|
|
@@ -2690,9 +3491,9 @@ var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCode
|
|
|
2690
3491
|
// ../core/dist/webhooks.js
|
|
2691
3492
|
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
2692
3493
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
2693
|
-
var
|
|
3494
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2694
3495
|
var parseEndpoint = (value) => {
|
|
2695
|
-
if (!
|
|
3496
|
+
if (!isRecord5(value) || typeof value.url !== "string")
|
|
2696
3497
|
return "each endpoint needs a url";
|
|
2697
3498
|
try {
|
|
2698
3499
|
new URL(value.url);
|
|
@@ -2709,12 +3510,12 @@ var parseEndpoint = (value) => {
|
|
|
2709
3510
|
const events = value.events ?? value.enabledEvents;
|
|
2710
3511
|
if (Array.isArray(events))
|
|
2711
3512
|
endpoint.events = events.map(String);
|
|
2712
|
-
if (
|
|
3513
|
+
if (isRecord5(value.tags)) {
|
|
2713
3514
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
2714
3515
|
}
|
|
2715
3516
|
if (typeof value.account === "string")
|
|
2716
3517
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
2717
|
-
if (
|
|
3518
|
+
if (isRecord5(value.headers)) {
|
|
2718
3519
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
2719
3520
|
}
|
|
2720
3521
|
return endpoint;
|
|
@@ -2741,7 +3542,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
2741
3542
|
return json4(200, { status: "ok" });
|
|
2742
3543
|
},
|
|
2743
3544
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
2744
|
-
if (!
|
|
3545
|
+
if (!isRecord5(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
2745
3546
|
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
2746
3547
|
}
|
|
2747
3548
|
const fault = { mode: body.mode };
|
|
@@ -2757,7 +3558,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
2757
3558
|
}))
|
|
2758
3559
|
}),
|
|
2759
3560
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
2760
|
-
const list = Array.isArray(body) ? body :
|
|
3561
|
+
const list = Array.isArray(body) ? body : isRecord5(body) ? body.endpoints : void 0;
|
|
2761
3562
|
if (!Array.isArray(list))
|
|
2762
3563
|
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
2763
3564
|
const parsed = [];
|
|
@@ -2826,8 +3627,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
2826
3627
|
this.name = "TypeError";
|
|
2827
3628
|
}
|
|
2828
3629
|
};
|
|
2829
|
-
var operationMatcher = (
|
|
2830
|
-
const matchers = listOperations(
|
|
3630
|
+
var operationMatcher = (document3) => {
|
|
3631
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
2831
3632
|
operationId: operation.operationId,
|
|
2832
3633
|
method: operation.method.toUpperCase(),
|
|
2833
3634
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -2922,10 +3723,10 @@ var createRuntime = (options) => {
|
|
|
2922
3723
|
const ensureBranch = (namespace, branch2, at) => {
|
|
2923
3724
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
2924
3725
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
2925
|
-
const
|
|
3726
|
+
const history2 = timeline(namespace);
|
|
2926
3727
|
if (branch2 === "main") {
|
|
2927
3728
|
if (at !== void 0) {
|
|
2928
|
-
const point =
|
|
3729
|
+
const point = history2.checkout("main", at);
|
|
2929
3730
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
2930
3731
|
captured.set(namespace, point.value.snapshot);
|
|
2931
3732
|
rng.setState(point.value.rngState);
|
|
@@ -2938,10 +3739,10 @@ var createRuntime = (options) => {
|
|
|
2938
3739
|
return namespace;
|
|
2939
3740
|
}
|
|
2940
3741
|
const storage = physicalBranch(namespace, branch2);
|
|
2941
|
-
if (!
|
|
3742
|
+
if (!history2.hasBranch(branch2)) {
|
|
2942
3743
|
if (at === void 0)
|
|
2943
|
-
|
|
2944
|
-
const point =
|
|
3744
|
+
history2.commit(capture(namespace));
|
|
3745
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
2945
3746
|
const branchRng = createRng(options.seed ?? 0);
|
|
2946
3747
|
if (point)
|
|
2947
3748
|
branchRng.setState(point.value.rngState);
|
|
@@ -2950,8 +3751,8 @@ var createRuntime = (options) => {
|
|
|
2950
3751
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2951
3752
|
if (point)
|
|
2952
3753
|
captured.set(storage, point.value.snapshot);
|
|
2953
|
-
} else if (at !== void 0 &&
|
|
2954
|
-
const point =
|
|
3754
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
3755
|
+
const point = history2.checkout(branch2, at);
|
|
2955
3756
|
if (!instances.has(storage)) {
|
|
2956
3757
|
const branchRng = createRng(options.seed ?? 0);
|
|
2957
3758
|
branchRng.setState(point.value.rngState);
|
|
@@ -2962,7 +3763,7 @@ var createRuntime = (options) => {
|
|
|
2962
3763
|
captured.set(storage, point.value.snapshot);
|
|
2963
3764
|
} else {
|
|
2964
3765
|
if (!instances.has(storage)) {
|
|
2965
|
-
const point =
|
|
3766
|
+
const point = history2.head(branch2);
|
|
2966
3767
|
const branchRng = createRng(options.seed ?? 0);
|
|
2967
3768
|
if (point)
|
|
2968
3769
|
branchRng.setState(point.value.rngState);
|
|
@@ -2986,8 +3787,8 @@ var createRuntime = (options) => {
|
|
|
2986
3787
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
2987
3788
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
2988
3789
|
const branchName = checkoutOptions.branch ?? "main";
|
|
2989
|
-
const
|
|
2990
|
-
const point =
|
|
3790
|
+
const history2 = timeline(namespace);
|
|
3791
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
2991
3792
|
const storage = ensureBranch(namespace, branchName);
|
|
2992
3793
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2993
3794
|
captured.set(storage, point.value.snapshot);
|
|
@@ -3037,9 +3838,9 @@ var createRuntime = (options) => {
|
|
|
3037
3838
|
instance(name);
|
|
3038
3839
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
3039
3840
|
captured.set(name, from);
|
|
3040
|
-
const
|
|
3041
|
-
if (
|
|
3042
|
-
|
|
3841
|
+
const history2 = timelines.get(name);
|
|
3842
|
+
if (history2)
|
|
3843
|
+
history2.commit(capture(name), { branch: "main" });
|
|
3043
3844
|
else
|
|
3044
3845
|
timeline(name);
|
|
3045
3846
|
};
|
|
@@ -3268,10 +4069,10 @@ var createRuntime = (options) => {
|
|
|
3268
4069
|
},
|
|
3269
4070
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
3270
4071
|
inspect: (name) => {
|
|
3271
|
-
const
|
|
4072
|
+
const history2 = timeline(name);
|
|
3272
4073
|
return {
|
|
3273
|
-
branches:
|
|
3274
|
-
checkpoints:
|
|
4074
|
+
branches: history2.branches(),
|
|
4075
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
3275
4076
|
id,
|
|
3276
4077
|
branch: branchName,
|
|
3277
4078
|
parent,
|
|
@@ -3286,11 +4087,11 @@ var createRuntime = (options) => {
|
|
|
3286
4087
|
} : {},
|
|
3287
4088
|
routes: {
|
|
3288
4089
|
...credentialRoutes(credentials),
|
|
3289
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3290
4090
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
3291
4091
|
...options.admin?.(runtime) ?? {},
|
|
3292
|
-
//
|
|
3293
|
-
//
|
|
4092
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4093
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4094
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3294
4095
|
...stateAdminRoutes({
|
|
3295
4096
|
open: stateScope,
|
|
3296
4097
|
afterWrite: (namespace) => {
|
|
@@ -3365,9 +4166,12 @@ var credentialRoutes = (registry) => ({
|
|
|
3365
4166
|
}
|
|
3366
4167
|
});
|
|
3367
4168
|
var presetRoutes = (presets, runtime) => ({
|
|
3368
|
-
"GET /faults/presets": () =>
|
|
3369
|
-
|
|
3370
|
-
|
|
4169
|
+
"GET /faults/presets": () => {
|
|
4170
|
+
const body = {
|
|
4171
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4172
|
+
};
|
|
4173
|
+
return adminJson(200, body);
|
|
4174
|
+
},
|
|
3371
4175
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
3372
4176
|
const name = params.name;
|
|
3373
4177
|
if (!presets[name])
|
|
@@ -3438,15 +4242,15 @@ var decodePostHogBody = (request) => {
|
|
|
3438
4242
|
decoded.set(request, pending);
|
|
3439
4243
|
return pending;
|
|
3440
4244
|
};
|
|
3441
|
-
var
|
|
4245
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3442
4246
|
var nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
3443
4247
|
var tokenFromBody = (body) => {
|
|
3444
4248
|
if (Array.isArray(body)) return tokenFromBody(body[0]);
|
|
3445
|
-
if (!
|
|
4249
|
+
if (!isRecord6(body)) return void 0;
|
|
3446
4250
|
const direct = nonEmpty(body.token) ?? nonEmpty(body.api_key);
|
|
3447
4251
|
if (direct) return direct;
|
|
3448
4252
|
if (Array.isArray(body.batch)) return tokenFromBody(body.batch[0]);
|
|
3449
|
-
if (
|
|
4253
|
+
if (isRecord6(body.properties)) return nonEmpty(body.properties.token);
|
|
3450
4254
|
return void 0;
|
|
3451
4255
|
};
|
|
3452
4256
|
var ARRAY_PATH = /^\/array\/([^/]+)\/config(\.js)?\/?$/;
|
|
@@ -3520,7 +4324,7 @@ var legacyMaps = (evaluations) => {
|
|
|
3520
4324
|
}
|
|
3521
4325
|
return { featureFlags, featureFlagPayloads };
|
|
3522
4326
|
};
|
|
3523
|
-
var
|
|
4327
|
+
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3524
4328
|
var payloadString = (payload) => payload === void 0 || payload === null ? null : JSON.stringify(payload);
|
|
3525
4329
|
var parsePayload2 = (payload) => {
|
|
3526
4330
|
if (payload === null) return null;
|
|
@@ -3549,7 +4353,7 @@ var adminView = (flag) => ({
|
|
|
3549
4353
|
var isValue = (value) => typeof value === "boolean" || typeof value === "string" && value.length > 0;
|
|
3550
4354
|
var parseFlagSpec = (body) => {
|
|
3551
4355
|
if (body === void 0) return { default: null, payload: null, overrides: [] };
|
|
3552
|
-
if (!
|
|
4356
|
+
if (!isRecord7(body)) return "expected a JSON object";
|
|
3553
4357
|
const value = body.default ?? body.value;
|
|
3554
4358
|
if (value !== void 0 && value !== null && !isValue(value)) {
|
|
3555
4359
|
return "default must be true, false, a variant string, or null (absent)";
|
|
@@ -3558,7 +4362,7 @@ var parseFlagSpec = (body) => {
|
|
|
3558
4362
|
if (body.overrides !== void 0) {
|
|
3559
4363
|
if (!Array.isArray(body.overrides)) return "overrides must be a list";
|
|
3560
4364
|
for (const each of body.overrides) {
|
|
3561
|
-
if (!
|
|
4365
|
+
if (!isRecord7(each)) return "each override is {distinct_id?|email?, value, payload?}";
|
|
3562
4366
|
const id = each.distinct_id ?? each.distinctId;
|
|
3563
4367
|
const email = each.email;
|
|
3564
4368
|
if (id === void 0 && email === void 0) return "each override needs distinct_id or email";
|
|
@@ -3639,10 +4443,10 @@ var restView = (flag) => {
|
|
|
3639
4443
|
};
|
|
3640
4444
|
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)] : [];
|
|
3641
4445
|
var fromFilters = (filters) => {
|
|
3642
|
-
const record =
|
|
3643
|
-
const groups = Array.isArray(record.groups) ? record.groups.filter(
|
|
3644
|
-
const multivariate =
|
|
3645
|
-
const variants = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(
|
|
4446
|
+
const record = isRecord7(filters) ? filters : {};
|
|
4447
|
+
const groups = Array.isArray(record.groups) ? record.groups.filter(isRecord7) : [];
|
|
4448
|
+
const multivariate = isRecord7(record.multivariate) ? record.multivariate : void 0;
|
|
4449
|
+
const variants = Array.isArray(multivariate?.variants) ? multivariate.variants.filter(isRecord7) : [];
|
|
3646
4450
|
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);
|
|
3647
4451
|
const on = (group) => {
|
|
3648
4452
|
const rollout = group.rollout_percentage ?? 100;
|
|
@@ -3653,7 +4457,7 @@ var fromFilters = (filters) => {
|
|
|
3653
4457
|
const overrides = [];
|
|
3654
4458
|
let fallback = groups.length === 0 ? false : null;
|
|
3655
4459
|
for (const group of groups) {
|
|
3656
|
-
const properties = Array.isArray(group.properties) ? group.properties.filter(
|
|
4460
|
+
const properties = Array.isArray(group.properties) ? group.properties.filter(isRecord7) : [];
|
|
3657
4461
|
if (properties.length === 0) {
|
|
3658
4462
|
const rollout = group.rollout_percentage ?? 100;
|
|
3659
4463
|
const value = typeof rollout === "number" && rollout < 100 ? false : on(group);
|
|
@@ -3670,14 +4474,14 @@ var fromFilters = (filters) => {
|
|
|
3670
4474
|
}
|
|
3671
4475
|
}
|
|
3672
4476
|
if (fallback === null) fallback = false;
|
|
3673
|
-
const payloads =
|
|
4477
|
+
const payloads = isRecord7(record.payloads) ? record.payloads : {};
|
|
3674
4478
|
const rawPayload = typeof fallback === "string" && fallback in payloads ? payloads[fallback] : payloads.true;
|
|
3675
4479
|
const payload = rawPayload === void 0 || rawPayload === null ? null : typeof rawPayload === "string" ? rawPayload : JSON.stringify(rawPayload);
|
|
3676
4480
|
return { default: fallback, payload, overrides };
|
|
3677
4481
|
};
|
|
3678
4482
|
|
|
3679
4483
|
// src/generated/openapi.ts
|
|
3680
|
-
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 (the 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"}}}}}}`);
|
|
4484
|
+
var document2 = 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 (the 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"}}}}}}`);
|
|
3681
4485
|
var operationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
|
|
3682
4486
|
var supportedOperationIds = ["EvaluateFlags", "Decide", "GetRemoteConfig", "GetRemoteConfigScript", "CaptureBatch", "CaptureEvent", "CaptureEventV0", "CaptureRecording", "GetRecorderScript", "GetVersionedRecorderScript", "ListSurveys", "ListWebExperiments", "ListFeatureFlags", "CreateFeatureFlag", "UpdateFeatureFlag", "RunQuery"];
|
|
3683
4487
|
|
|
@@ -3845,11 +4649,11 @@ var POSTHOG_PRESETS = {
|
|
|
3845
4649
|
};
|
|
3846
4650
|
var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
3847
4651
|
var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
3848
|
-
var
|
|
4652
|
+
var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3849
4653
|
var parseBulk = (body) => {
|
|
3850
|
-
const source =
|
|
3851
|
-
const entries = Array.isArray(source) ? source.map((each) => [
|
|
3852
|
-
if (!Array.isArray(source) && !
|
|
4654
|
+
const source = isRecord8(body) && isRecord8(body.flags) ? body.flags : body;
|
|
4655
|
+
const entries = Array.isArray(source) ? source.map((each) => [isRecord8(each) ? String(each.key ?? "") : "", each]) : isRecord8(source) ? Object.entries(source).filter(([key]) => key !== "replace") : [];
|
|
4656
|
+
if (!Array.isArray(source) && !isRecord8(source)) return "expected {flags: {<key>: spec}}";
|
|
3853
4657
|
const specs = {};
|
|
3854
4658
|
for (const [key, value] of entries) {
|
|
3855
4659
|
if (!key) return "every flag needs a key";
|
|
@@ -3863,7 +4667,7 @@ var parseQueryResults = (value) => {
|
|
|
3863
4667
|
if (!Array.isArray(value)) return "queryResults: [{match?, columns?, results: [[\u2026]]}]";
|
|
3864
4668
|
const out = [];
|
|
3865
4669
|
for (const each of value) {
|
|
3866
|
-
if (!
|
|
4670
|
+
if (!isRecord8(each) || !Array.isArray(each.results) || !each.results.every(Array.isArray)) {
|
|
3867
4671
|
return "each queryResults entry needs results: unknown[][]";
|
|
3868
4672
|
}
|
|
3869
4673
|
out.push({
|
|
@@ -3904,19 +4708,19 @@ var adminRoutes = (runtime) => {
|
|
|
3904
4708
|
const specs = parseBulk(body);
|
|
3905
4709
|
if (typeof specs === "string") return adminError4(400, specs);
|
|
3906
4710
|
const state = api(namespace).state;
|
|
3907
|
-
if (
|
|
4711
|
+
if (isRecord8(body) && body.replace === true) {
|
|
3908
4712
|
for (const flag of state.listFlags()) state.flags.delete(flag.key);
|
|
3909
4713
|
}
|
|
3910
4714
|
for (const [key, spec] of Object.entries(specs)) state.putFlag(key, spec);
|
|
3911
4715
|
return json5(200, { flags: api(namespace).flagList() });
|
|
3912
4716
|
},
|
|
3913
4717
|
"POST /flags/import": ({ body, namespace }) => {
|
|
3914
|
-
const input =
|
|
4718
|
+
const input = isRecord8(body) ? body : {};
|
|
3915
4719
|
const env = input.env ?? "dev";
|
|
3916
4720
|
if (env !== "dev" && env !== "prod") return adminError4(400, 'env must be "dev" or "prod"');
|
|
3917
4721
|
const from = input.from ?? "state.json";
|
|
3918
4722
|
let file;
|
|
3919
|
-
if (
|
|
4723
|
+
if (isRecord8(input.state) && Array.isArray(input.state.flags)) {
|
|
3920
4724
|
file = input.state;
|
|
3921
4725
|
} else if (from === "state.json") {
|
|
3922
4726
|
file = ACME_FLAG_STATE;
|
|
@@ -3960,7 +4764,7 @@ var adminRoutes = (runtime) => {
|
|
|
3960
4764
|
"GET /recordings": ({ namespace }) => json5(200, { count: api(namespace).state.current().recordings }),
|
|
3961
4765
|
"GET /settings": ({ namespace }) => json5(200, api(namespace).state.current()),
|
|
3962
4766
|
"PUT /settings": ({ body, namespace }) => {
|
|
3963
|
-
if (!
|
|
4767
|
+
if (!isRecord8(body)) return adminError4(400, "expected a JSON object");
|
|
3964
4768
|
const patch = {};
|
|
3965
4769
|
if (body.sessionRecording !== void 0) {
|
|
3966
4770
|
if (typeof body.sessionRecording !== "boolean")
|
|
@@ -3980,7 +4784,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
3980
4784
|
const tokens = /* @__PURE__ */ new WeakMap();
|
|
3981
4785
|
const runtime = createRuntime({
|
|
3982
4786
|
name: POSTHOG_NAMESPACE,
|
|
3983
|
-
document,
|
|
4787
|
+
document: document2,
|
|
3984
4788
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
3985
4789
|
...options.clock ? { clock: options.clock } : {},
|
|
3986
4790
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
@@ -4012,7 +4816,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
4012
4816
|
|
|
4013
4817
|
// src/index.ts
|
|
4014
4818
|
var POSTHOG_NAMESPACE = "posthog";
|
|
4015
|
-
var
|
|
4819
|
+
var isRecord9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4016
4820
|
var error = (status, type, code, detail, attr) => jsonRes(status, { type, code, detail, attr: attr ?? null });
|
|
4017
4821
|
var invalidApiKey = () => error(
|
|
4018
4822
|
401,
|
|
@@ -4050,7 +4854,7 @@ var scrubProperties = (event, properties) => {
|
|
|
4050
4854
|
}
|
|
4051
4855
|
const kept = scrubRecord(properties);
|
|
4052
4856
|
for (const nested of ["$set", "$set_once"]) {
|
|
4053
|
-
if (
|
|
4857
|
+
if (isRecord9(kept[nested])) kept[nested] = scrubRecord(kept[nested]);
|
|
4054
4858
|
}
|
|
4055
4859
|
return kept;
|
|
4056
4860
|
};
|
|
@@ -4106,7 +4910,7 @@ var PostHogAPI = class {
|
|
|
4106
4910
|
RunQuery: (context) => this.query(context)
|
|
4107
4911
|
});
|
|
4108
4912
|
this.service = createService({
|
|
4109
|
-
document,
|
|
4913
|
+
document: document2,
|
|
4110
4914
|
handlers,
|
|
4111
4915
|
sqlite,
|
|
4112
4916
|
namespace,
|
|
@@ -4171,7 +4975,7 @@ var PostHogAPI = class {
|
|
|
4171
4975
|
}
|
|
4172
4976
|
flags(context, route) {
|
|
4173
4977
|
const body = this.body(context);
|
|
4174
|
-
if (!
|
|
4978
|
+
if (!isRecord9(body)) return malformed();
|
|
4175
4979
|
if (!tokenFromBody(body)) return invalidApiKey();
|
|
4176
4980
|
const distinctId = body.distinct_id;
|
|
4177
4981
|
if (typeof distinctId !== "string" && typeof distinctId !== "number") {
|
|
@@ -4181,7 +4985,7 @@ var PostHogAPI = class {
|
|
|
4181
4985
|
const detailed = route === "decide" ? version >= 4 : version >= 2;
|
|
4182
4986
|
const subject = {
|
|
4183
4987
|
distinct_id: String(distinctId),
|
|
4184
|
-
...
|
|
4988
|
+
...isRecord9(body.person_properties) ? { person_properties: body.person_properties } : {}
|
|
4185
4989
|
};
|
|
4186
4990
|
const keys = [body.flag_keys_to_evaluate, body.flag_keys].find(Array.isArray);
|
|
4187
4991
|
const quotaLimited = this.effect(context, "quota_limited");
|
|
@@ -4214,15 +5018,15 @@ var PostHogAPI = class {
|
|
|
4214
5018
|
}
|
|
4215
5019
|
const token = tokenFromBody(body);
|
|
4216
5020
|
if (!token) return invalidApiKey();
|
|
4217
|
-
const items = Array.isArray(body) ? body :
|
|
5021
|
+
const items = Array.isArray(body) ? body : isRecord9(body) && Array.isArray(body.batch) ? body.batch : [body];
|
|
4218
5022
|
const events = items.filter(
|
|
4219
|
-
(item) =>
|
|
5023
|
+
(item) => isRecord9(item) && typeof item.event === "string" && item.event.length > 0
|
|
4220
5024
|
);
|
|
4221
5025
|
if (events.length === 0 && items.length > 0) return malformed("Invalid payload: no event name");
|
|
4222
5026
|
const stored = [];
|
|
4223
5027
|
for (const raw of events) {
|
|
4224
5028
|
const event = raw.event;
|
|
4225
|
-
const properties =
|
|
5029
|
+
const properties = isRecord9(raw.properties) ? raw.properties : {};
|
|
4226
5030
|
const distinct = raw.distinct_id ?? properties.distinct_id ?? raw.$distinct_id ?? properties.$distinct_id;
|
|
4227
5031
|
const uuid = typeof raw.uuid === "string" && raw.uuid ? raw.uuid : this.state.nextEventId();
|
|
4228
5032
|
if (this.state.events.has(uuid)) continue;
|
|
@@ -4264,7 +5068,7 @@ var PostHogAPI = class {
|
|
|
4264
5068
|
}
|
|
4265
5069
|
createFlag(context) {
|
|
4266
5070
|
const body = this.body(context);
|
|
4267
|
-
if (!
|
|
5071
|
+
if (!isRecord9(body)) return malformed();
|
|
4268
5072
|
const key = body.key;
|
|
4269
5073
|
if (typeof key !== "string" || !/^[A-Za-z0-9_-]+$/.test(key)) {
|
|
4270
5074
|
return error(
|
|
@@ -4296,7 +5100,7 @@ var PostHogAPI = class {
|
|
|
4296
5100
|
const flag = this.state.findFlag(context.params.flagId ?? "");
|
|
4297
5101
|
if (!flag) return error(404, "invalid_request", "not_found", "Not found.");
|
|
4298
5102
|
const body = this.body(context);
|
|
4299
|
-
if (!
|
|
5103
|
+
if (!isRecord9(body)) return malformed();
|
|
4300
5104
|
const patch = {};
|
|
4301
5105
|
if (typeof body.name === "string") patch.name = body.name;
|
|
4302
5106
|
if (typeof body.active === "boolean") patch.active = body.active;
|
|
@@ -4307,7 +5111,7 @@ var PostHogAPI = class {
|
|
|
4307
5111
|
}
|
|
4308
5112
|
query(context) {
|
|
4309
5113
|
const body = this.body(context);
|
|
4310
|
-
const query =
|
|
5114
|
+
const query = isRecord9(body) && isRecord9(body.query) ? body.query : void 0;
|
|
4311
5115
|
if (query?.kind !== "HogQLQuery" || typeof query.query !== "string") {
|
|
4312
5116
|
return error(400, "validation_error", "invalid_input", "Expected a HogQLQuery.", "query");
|
|
4313
5117
|
}
|
|
@@ -4337,7 +5141,7 @@ export {
|
|
|
4337
5141
|
evaluateFlag,
|
|
4338
5142
|
payloadString,
|
|
4339
5143
|
parseFlagSpec,
|
|
4340
|
-
document,
|
|
5144
|
+
document2 as document,
|
|
4341
5145
|
operationIds,
|
|
4342
5146
|
supportedOperationIds,
|
|
4343
5147
|
ACME_FLAG_STATE,
|
|
@@ -4349,4 +5153,4 @@ export {
|
|
|
4349
5153
|
scrubProperties,
|
|
4350
5154
|
PostHogAPI
|
|
4351
5155
|
};
|
|
4352
|
-
//# sourceMappingURL=chunk-
|
|
5156
|
+
//# sourceMappingURL=chunk-A7ITZA26.js.map
|