@crvouga/mockingbird-service-fullscript 1.3.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 +14 -2
- package/dist/{chunk-7353U527.js → chunk-HEWMSDJK.js} +2340 -146
- package/dist/chunk-HEWMSDJK.js.map +7 -0
- package/dist/{chunk-4M25CDSF.js → chunk-ZSH3VNPO.js} +3 -2
- package/dist/{chunk-4M25CDSF.js.map → chunk-ZSH3VNPO.js.map} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +41 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +73 -3
- package/dist/server.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-7353U527.js.map +0 -7
|
@@ -38,30 +38,30 @@ var createClock = (source = Date.now) => {
|
|
|
38
38
|
var Collection = class {
|
|
39
39
|
sqlite;
|
|
40
40
|
namespace;
|
|
41
|
-
|
|
42
|
-
constructor(sqlite, namespace,
|
|
41
|
+
collectionName;
|
|
42
|
+
constructor(sqlite, namespace, collectionName) {
|
|
43
43
|
this.sqlite = sqlite;
|
|
44
44
|
this.namespace = namespace;
|
|
45
|
-
this.
|
|
45
|
+
this.collectionName = collectionName;
|
|
46
46
|
}
|
|
47
47
|
bumpCollectionSeq() {
|
|
48
|
-
const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.
|
|
48
|
+
const row = this.sqlite.prepare("SELECT value FROM mockingbird_sequences WHERE namespace = ? AND name = ? AND kind = 'collection'").get(this.namespace, this.collectionName);
|
|
49
49
|
const next = (row?.value ?? 0) + 1;
|
|
50
50
|
this.sqlite.prepare(`INSERT INTO mockingbird_sequences (namespace, name, kind, value) VALUES (?, ?, 'collection', ?)
|
|
51
|
-
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.
|
|
51
|
+
ON CONFLICT(namespace, name, kind) DO UPDATE SET value = excluded.value`).run(this.namespace, this.collectionName, next);
|
|
52
52
|
return next;
|
|
53
53
|
}
|
|
54
54
|
nextSequence() {
|
|
55
55
|
return this.sqlite.transaction(() => this.bumpCollectionSeq());
|
|
56
56
|
}
|
|
57
57
|
get(id) {
|
|
58
|
-
const row = this.sqlite.prepare("SELECT value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.
|
|
58
|
+
const row = this.sqlite.prepare("SELECT value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
59
59
|
if (!row)
|
|
60
60
|
return void 0;
|
|
61
61
|
return JSON.parse(row.value).value;
|
|
62
62
|
}
|
|
63
63
|
has(id) {
|
|
64
|
-
const row = this.sqlite.prepare("SELECT 1 AS ok FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.
|
|
64
|
+
const row = this.sqlite.prepare("SELECT 1 AS ok FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
65
65
|
return row !== void 0;
|
|
66
66
|
}
|
|
67
67
|
/** Insert a new record, assigning it the next sequence number. */
|
|
@@ -71,32 +71,32 @@ var Collection = class {
|
|
|
71
71
|
const stored = { seq, value };
|
|
72
72
|
this.sqlite.prepare(`INSERT INTO mockingbird_records (namespace, collection, id, seq, value)
|
|
73
73
|
VALUES (?, ?, ?, ?, ?)
|
|
74
|
-
ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.
|
|
74
|
+
ON CONFLICT(namespace, collection, id) DO UPDATE SET seq = excluded.seq, value = excluded.value`).run(this.namespace, this.collectionName, id, seq, JSON.stringify(stored));
|
|
75
75
|
return stored;
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
/** Replace an existing record's value, keeping its position. */
|
|
79
79
|
update(id, value) {
|
|
80
80
|
return this.sqlite.transaction(() => {
|
|
81
|
-
const row = this.sqlite.prepare("SELECT seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.
|
|
81
|
+
const row = this.sqlite.prepare("SELECT seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").get(this.namespace, this.collectionName, id);
|
|
82
82
|
if (!row)
|
|
83
83
|
return void 0;
|
|
84
84
|
const stored = { seq: row.seq, value };
|
|
85
|
-
this.sqlite.prepare("UPDATE mockingbird_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.
|
|
85
|
+
this.sqlite.prepare("UPDATE mockingbird_records SET value = ? WHERE namespace = ? AND collection = ? AND id = ?").run(JSON.stringify(stored), this.namespace, this.collectionName, id);
|
|
86
86
|
return stored;
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
delete(id) {
|
|
90
|
-
const result = this.sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.
|
|
90
|
+
const result = this.sqlite.prepare("DELETE FROM mockingbird_records WHERE namespace = ? AND collection = ? AND id = ?").run(this.namespace, this.collectionName, id);
|
|
91
91
|
return result.changes > 0;
|
|
92
92
|
}
|
|
93
93
|
/** How many records the collection holds, without reading them. */
|
|
94
94
|
count() {
|
|
95
|
-
const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM mockingbird_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.
|
|
95
|
+
const row = this.sqlite.prepare("SELECT COUNT(*) AS n FROM mockingbird_records WHERE namespace = ? AND collection = ?").get(this.namespace, this.collectionName);
|
|
96
96
|
return Number(row?.n ?? 0);
|
|
97
97
|
}
|
|
98
98
|
list(options = {}) {
|
|
99
|
-
const rows = this.sqlite.prepare("SELECT id, seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.
|
|
99
|
+
const rows = this.sqlite.prepare("SELECT id, seq, value FROM mockingbird_records WHERE namespace = ? AND collection = ?").all(this.namespace, this.collectionName);
|
|
100
100
|
const out = [];
|
|
101
101
|
for (const row of rows) {
|
|
102
102
|
const stored = JSON.parse(row.value);
|
|
@@ -353,16 +353,18 @@ var createControlPlane = (context) => {
|
|
|
353
353
|
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
354
|
clock: context.clock.state(),
|
|
355
355
|
namespaces: context.namespaces().length,
|
|
356
|
+
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
356
357
|
...context.describe()
|
|
357
358
|
});
|
|
358
359
|
}
|
|
359
360
|
if (url.pathname !== ADMIN_PREFIX && !url.pathname.startsWith(`${ADMIN_PREFIX}/`)) {
|
|
360
361
|
return void 0;
|
|
361
362
|
}
|
|
362
|
-
|
|
363
|
+
const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
|
|
364
|
+
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
|
+
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
363
366
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
364
367
|
}
|
|
365
|
-
const path = url.pathname.slice(ADMIN_PREFIX.length) || "/";
|
|
366
368
|
for (const route of routes) {
|
|
367
369
|
if (route.method !== request.method)
|
|
368
370
|
continue;
|
|
@@ -538,10 +540,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
538
540
|
};
|
|
539
541
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
540
542
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
541
|
-
var resolveRef = (
|
|
543
|
+
var resolveRef = (document3, ref) => {
|
|
542
544
|
if (!ref.startsWith("#/"))
|
|
543
545
|
throw new OpenAPIReferenceError(ref);
|
|
544
|
-
let cursor =
|
|
546
|
+
let cursor = document3;
|
|
545
547
|
for (const raw of ref.slice(2).split("/")) {
|
|
546
548
|
const segment = unescapePointer(raw);
|
|
547
549
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -553,14 +555,14 @@ var resolveRef = (document2, ref) => {
|
|
|
553
555
|
throw new OpenAPIReferenceError(ref);
|
|
554
556
|
return cursor;
|
|
555
557
|
};
|
|
556
|
-
var deref = (
|
|
558
|
+
var deref = (document3, value) => {
|
|
557
559
|
let current = value;
|
|
558
560
|
const seen = /* @__PURE__ */ new Set();
|
|
559
561
|
while (isReference(current)) {
|
|
560
562
|
if (seen.has(current.$ref))
|
|
561
563
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
562
564
|
seen.add(current.$ref);
|
|
563
|
-
current = resolveRef(
|
|
565
|
+
current = resolveRef(document3, current.$ref);
|
|
564
566
|
}
|
|
565
567
|
return current;
|
|
566
568
|
};
|
|
@@ -578,36 +580,36 @@ var HTTP_METHODS = [
|
|
|
578
580
|
];
|
|
579
581
|
|
|
580
582
|
// ../../openapi/core/dist/document.js
|
|
581
|
-
var mergeParameters = (
|
|
583
|
+
var mergeParameters = (document3, item, own) => {
|
|
582
584
|
const merged = /* @__PURE__ */ new Map();
|
|
583
585
|
for (const raw of item.parameters ?? []) {
|
|
584
|
-
const parameter = deref(
|
|
586
|
+
const parameter = deref(document3, raw);
|
|
585
587
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
586
588
|
}
|
|
587
589
|
for (const raw of own ?? []) {
|
|
588
|
-
const parameter = deref(
|
|
590
|
+
const parameter = deref(document3, raw);
|
|
589
591
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
590
592
|
}
|
|
591
593
|
return [...merged.values()];
|
|
592
594
|
};
|
|
593
|
-
var listOperations = (
|
|
595
|
+
var listOperations = (document3) => {
|
|
594
596
|
const operations = [];
|
|
595
|
-
for (const [path, item] of Object.entries(
|
|
597
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
596
598
|
for (const method of HTTP_METHODS) {
|
|
597
599
|
const operation = item[method];
|
|
598
600
|
if (operation?.operationId === void 0)
|
|
599
601
|
continue;
|
|
600
602
|
const responses = {};
|
|
601
603
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
602
|
-
responses[status] = deref(
|
|
604
|
+
responses[status] = deref(document3, response);
|
|
603
605
|
}
|
|
604
606
|
operations.push({
|
|
605
607
|
operationId: operation.operationId,
|
|
606
608
|
method,
|
|
607
609
|
path,
|
|
608
610
|
operation,
|
|
609
|
-
parameters: mergeParameters(
|
|
610
|
-
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),
|
|
611
613
|
responses
|
|
612
614
|
});
|
|
613
615
|
}
|
|
@@ -616,7 +618,7 @@ var listOperations = (document2) => {
|
|
|
616
618
|
};
|
|
617
619
|
|
|
618
620
|
// ../../openapi/core/dist/schema.js
|
|
619
|
-
var resolveSchema = (
|
|
621
|
+
var resolveSchema = (document3, schema) => {
|
|
620
622
|
let current = schema;
|
|
621
623
|
const seen = /* @__PURE__ */ new Set();
|
|
622
624
|
while (typeof current.$ref === "string") {
|
|
@@ -625,7 +627,7 @@ var resolveSchema = (document2, schema) => {
|
|
|
625
627
|
break;
|
|
626
628
|
seen.add(ref);
|
|
627
629
|
const { $ref: _ignored, ...siblings } = current;
|
|
628
|
-
const target = resolveRef(
|
|
630
|
+
const target = resolveRef(document3, ref);
|
|
629
631
|
current = { ...target, ...siblings };
|
|
630
632
|
}
|
|
631
633
|
if (current.nullable === true) {
|
|
@@ -696,71 +698,71 @@ var FORMAT_PATTERNS = {
|
|
|
696
698
|
ipv4: /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/
|
|
697
699
|
};
|
|
698
700
|
var graphemeLength = (value) => [...value].length;
|
|
699
|
-
var validateValue = (
|
|
701
|
+
var validateValue = (document3, schema, value, path = []) => {
|
|
700
702
|
const errors = [];
|
|
701
|
-
const s = resolveSchema(
|
|
702
|
-
const
|
|
703
|
+
const s = resolveSchema(document3, schema);
|
|
704
|
+
const fail3 = (message) => errors.push({ path, message });
|
|
703
705
|
const actual = jsonTypeOf(value);
|
|
704
706
|
if (actual === "undefined") {
|
|
705
|
-
|
|
707
|
+
fail3("value is undefined");
|
|
706
708
|
return errors;
|
|
707
709
|
}
|
|
708
710
|
const types = schemaTypes(s);
|
|
709
711
|
if (types.length > 0) {
|
|
710
712
|
const ok2 = types.some((t) => t === actual || t === "number" && actual === "integer");
|
|
711
713
|
if (!ok2) {
|
|
712
|
-
|
|
714
|
+
fail3(`expected type ${types.join("|")}, got ${actual}`);
|
|
713
715
|
return errors;
|
|
714
716
|
}
|
|
715
717
|
}
|
|
716
718
|
if (s.enum && !(value === null && types.includes("null")) && !s.enum.some((candidate) => deepEqual(candidate, value))) {
|
|
717
|
-
|
|
719
|
+
fail3("value not in enum");
|
|
718
720
|
}
|
|
719
721
|
if (s.const !== void 0 && !deepEqual(s.const, value))
|
|
720
|
-
|
|
722
|
+
fail3("value does not equal const");
|
|
721
723
|
if (typeof value === "string") {
|
|
722
724
|
const length = graphemeLength(value);
|
|
723
725
|
if (s.minLength !== void 0 && length < s.minLength)
|
|
724
|
-
|
|
726
|
+
fail3(`length ${length} < minLength ${s.minLength}`);
|
|
725
727
|
if (s.maxLength !== void 0 && length > s.maxLength)
|
|
726
|
-
|
|
728
|
+
fail3(`length ${length} > maxLength ${s.maxLength}`);
|
|
727
729
|
if (s.pattern !== void 0) {
|
|
728
730
|
try {
|
|
729
731
|
if (!new RegExp(s.pattern, "u").test(value))
|
|
730
|
-
|
|
732
|
+
fail3(`does not match pattern ${s.pattern}`);
|
|
731
733
|
} catch {
|
|
732
734
|
}
|
|
733
735
|
}
|
|
734
736
|
if (s.format !== void 0) {
|
|
735
737
|
const pattern = FORMAT_PATTERNS[s.format];
|
|
736
738
|
if (pattern && !pattern.test(value))
|
|
737
|
-
|
|
739
|
+
fail3(`does not match format ${s.format}`);
|
|
738
740
|
}
|
|
739
741
|
}
|
|
740
742
|
if (typeof value === "number") {
|
|
741
743
|
if (s.minimum !== void 0 && value < s.minimum)
|
|
742
|
-
|
|
744
|
+
fail3(`${value} < minimum ${s.minimum}`);
|
|
743
745
|
if (s.maximum !== void 0 && value > s.maximum)
|
|
744
|
-
|
|
746
|
+
fail3(`${value} > maximum ${s.maximum}`);
|
|
745
747
|
if (s.exclusiveMinimum !== void 0 && value <= s.exclusiveMinimum)
|
|
746
|
-
|
|
748
|
+
fail3(`${value} <= exclusiveMinimum ${s.exclusiveMinimum}`);
|
|
747
749
|
if (s.exclusiveMaximum !== void 0 && value >= s.exclusiveMaximum)
|
|
748
|
-
|
|
750
|
+
fail3(`${value} >= exclusiveMaximum ${s.exclusiveMaximum}`);
|
|
749
751
|
if (s.multipleOf !== void 0 && Math.abs(value / s.multipleOf - Math.round(value / s.multipleOf)) > 1e-9) {
|
|
750
|
-
|
|
752
|
+
fail3(`${value} is not a multiple of ${s.multipleOf}`);
|
|
751
753
|
}
|
|
752
754
|
}
|
|
753
755
|
if (Array.isArray(value)) {
|
|
754
756
|
if (s.minItems !== void 0 && value.length < s.minItems)
|
|
755
|
-
|
|
757
|
+
fail3(`${value.length} items < minItems ${s.minItems}`);
|
|
756
758
|
if (s.maxItems !== void 0 && value.length > s.maxItems)
|
|
757
|
-
|
|
759
|
+
fail3(`${value.length} items > maxItems ${s.maxItems}`);
|
|
758
760
|
if (s.uniqueItems && value.some((item, i) => value.slice(0, i).some((prev) => deepEqual(prev, item))))
|
|
759
|
-
|
|
761
|
+
fail3("items are not unique");
|
|
760
762
|
value.forEach((item, i) => {
|
|
761
763
|
const itemSchema = s.prefixItems?.[i] ?? s.items;
|
|
762
764
|
if (itemSchema)
|
|
763
|
-
errors.push(...validateValue(
|
|
765
|
+
errors.push(...validateValue(document3, itemSchema, item, [...path, i]));
|
|
764
766
|
});
|
|
765
767
|
}
|
|
766
768
|
if (actual === "object") {
|
|
@@ -768,41 +770,41 @@ var validateValue = (document2, schema, value, path = []) => {
|
|
|
768
770
|
const keys = Object.keys(record);
|
|
769
771
|
for (const name of s.required ?? [])
|
|
770
772
|
if (!(name in record))
|
|
771
|
-
|
|
773
|
+
fail3(`missing required property ${name}`);
|
|
772
774
|
if (s.minProperties !== void 0 && keys.length < s.minProperties)
|
|
773
|
-
|
|
775
|
+
fail3(`${keys.length} properties < minProperties ${s.minProperties}`);
|
|
774
776
|
if (s.maxProperties !== void 0 && keys.length > s.maxProperties)
|
|
775
|
-
|
|
777
|
+
fail3(`${keys.length} properties > maxProperties ${s.maxProperties}`);
|
|
776
778
|
for (const key of keys) {
|
|
777
779
|
const property = s.properties?.[key];
|
|
778
780
|
if (property) {
|
|
779
|
-
errors.push(...validateValue(
|
|
781
|
+
errors.push(...validateValue(document3, property, record[key], [...path, key]));
|
|
780
782
|
continue;
|
|
781
783
|
}
|
|
782
784
|
if (s.additionalProperties === false)
|
|
783
|
-
|
|
785
|
+
fail3(`unexpected property ${key}`);
|
|
784
786
|
else if (typeof s.additionalProperties === "object") {
|
|
785
|
-
errors.push(...validateValue(
|
|
787
|
+
errors.push(...validateValue(document3, s.additionalProperties, record[key], [...path, key]));
|
|
786
788
|
}
|
|
787
789
|
if (s.propertyNames) {
|
|
788
|
-
const nameErrors = validateValue(
|
|
790
|
+
const nameErrors = validateValue(document3, s.propertyNames, key, [...path, key]);
|
|
789
791
|
if (nameErrors.length > 0)
|
|
790
|
-
|
|
792
|
+
fail3(`property name ${key} is invalid: ${nameErrors[0]?.message}`);
|
|
791
793
|
}
|
|
792
794
|
}
|
|
793
795
|
}
|
|
794
796
|
if (s.allOf)
|
|
795
797
|
for (const branch of s.allOf)
|
|
796
|
-
errors.push(...validateValue(
|
|
797
|
-
if (s.anyOf && !s.anyOf.some((branch) => validateValue(
|
|
798
|
-
|
|
798
|
+
errors.push(...validateValue(document3, branch, value, path));
|
|
799
|
+
if (s.anyOf && !s.anyOf.some((branch) => validateValue(document3, branch, value).length === 0))
|
|
800
|
+
fail3("matches no anyOf branch");
|
|
799
801
|
if (s.oneOf) {
|
|
800
|
-
const matches2 = s.oneOf.filter((branch) => validateValue(
|
|
802
|
+
const matches2 = s.oneOf.filter((branch) => validateValue(document3, branch, value).length === 0).length;
|
|
801
803
|
if (matches2 !== 1)
|
|
802
|
-
|
|
804
|
+
fail3(`matches ${matches2} oneOf branches, expected exactly 1`);
|
|
803
805
|
}
|
|
804
|
-
if (s.not && validateValue(
|
|
805
|
-
|
|
806
|
+
if (s.not && validateValue(document3, s.not, value).length === 0)
|
|
807
|
+
fail3("matches forbidden `not` schema");
|
|
806
808
|
return errors;
|
|
807
809
|
};
|
|
808
810
|
|
|
@@ -1356,6 +1358,1737 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
1356
1358
|
});
|
|
1357
1359
|
};
|
|
1358
1360
|
|
|
1361
|
+
// ../../ui/dist/reset.js
|
|
1362
|
+
var CSS_RESET = `*,
|
|
1363
|
+
*::before,
|
|
1364
|
+
*::after {
|
|
1365
|
+
box-sizing: border-box;
|
|
1366
|
+
margin: 0;
|
|
1367
|
+
padding: 0;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
* {
|
|
1371
|
+
scrollbar-width: thin;
|
|
1372
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
*::-webkit-scrollbar {
|
|
1376
|
+
width: 12px;
|
|
1377
|
+
height: 12px;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
*::-webkit-scrollbar-track,
|
|
1381
|
+
*::-webkit-scrollbar-corner {
|
|
1382
|
+
background: Canvas;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
*::-webkit-scrollbar-thumb {
|
|
1386
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
1387
|
+
border: 3px solid Canvas;
|
|
1388
|
+
border-radius: 999px;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
html {
|
|
1392
|
+
hanging-punctuation: first allow-end last;
|
|
1393
|
+
-webkit-text-size-adjust: none;
|
|
1394
|
+
text-size-adjust: none;
|
|
1395
|
+
tab-size: 4;
|
|
1396
|
+
font-family:
|
|
1397
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1398
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1399
|
+
font-size: 16px;
|
|
1400
|
+
line-height: 1.5;
|
|
1401
|
+
-webkit-font-smoothing: antialiased;
|
|
1402
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1403
|
+
text-rendering: optimizeLegibility;
|
|
1404
|
+
-webkit-tap-highlight-color: transparent;
|
|
1405
|
+
scroll-behavior: smooth;
|
|
1406
|
+
background: Canvas;
|
|
1407
|
+
color: CanvasText;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
body {
|
|
1411
|
+
min-height: 100svh;
|
|
1412
|
+
line-height: 1.5;
|
|
1413
|
+
background: Canvas;
|
|
1414
|
+
color: CanvasText;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
h1 {
|
|
1418
|
+
font-size: 2em;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
h2 {
|
|
1422
|
+
font-size: 1.5em;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
h3 {
|
|
1426
|
+
font-size: 1.17em;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
h4 {
|
|
1430
|
+
font-size: 1em;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
h5 {
|
|
1434
|
+
font-size: 0.83em;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
h6 {
|
|
1438
|
+
font-size: 0.67em;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
h1,
|
|
1442
|
+
h2,
|
|
1443
|
+
h3,
|
|
1444
|
+
h4,
|
|
1445
|
+
h5,
|
|
1446
|
+
h6 {
|
|
1447
|
+
font-weight: 700;
|
|
1448
|
+
line-height: 1.2;
|
|
1449
|
+
text-wrap: balance;
|
|
1450
|
+
overflow-wrap: break-word;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
p,
|
|
1454
|
+
li,
|
|
1455
|
+
figcaption,
|
|
1456
|
+
dd,
|
|
1457
|
+
dt,
|
|
1458
|
+
blockquote {
|
|
1459
|
+
overflow-wrap: break-word;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
p,
|
|
1463
|
+
li,
|
|
1464
|
+
figcaption,
|
|
1465
|
+
blockquote {
|
|
1466
|
+
text-wrap: pretty;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
ul,
|
|
1470
|
+
ol,
|
|
1471
|
+
menu {
|
|
1472
|
+
list-style: none;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
a {
|
|
1476
|
+
color: inherit;
|
|
1477
|
+
text-decoration: underline;
|
|
1478
|
+
text-decoration-thickness: 1px;
|
|
1479
|
+
text-underline-offset: 0.15em;
|
|
1480
|
+
text-decoration-skip-ink: auto;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
b,
|
|
1484
|
+
strong {
|
|
1485
|
+
font-weight: 700;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
small {
|
|
1489
|
+
font-size: 0.875em;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
code,
|
|
1493
|
+
kbd,
|
|
1494
|
+
samp,
|
|
1495
|
+
pre {
|
|
1496
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1497
|
+
font-size: 1em;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
sub,
|
|
1501
|
+
sup {
|
|
1502
|
+
font-size: 75%;
|
|
1503
|
+
line-height: 0;
|
|
1504
|
+
position: relative;
|
|
1505
|
+
vertical-align: baseline;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
sub {
|
|
1509
|
+
bottom: -0.25em;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
sup {
|
|
1513
|
+
top: -0.5em;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
abbr[title] {
|
|
1517
|
+
text-decoration: underline dotted;
|
|
1518
|
+
text-underline-offset: 0.15em;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
mark {
|
|
1522
|
+
background: Mark;
|
|
1523
|
+
color: MarkText;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
hr {
|
|
1527
|
+
height: 0;
|
|
1528
|
+
border: 0;
|
|
1529
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1530
|
+
color: inherit;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
img,
|
|
1534
|
+
picture,
|
|
1535
|
+
video,
|
|
1536
|
+
canvas {
|
|
1537
|
+
display: block;
|
|
1538
|
+
max-width: 100%;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
img,
|
|
1542
|
+
video {
|
|
1543
|
+
height: auto;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
svg {
|
|
1547
|
+
overflow: hidden;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
iframe {
|
|
1551
|
+
border: 0;
|
|
1552
|
+
max-width: 100%;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
table {
|
|
1556
|
+
border-collapse: collapse;
|
|
1557
|
+
border-spacing: 0;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
fieldset {
|
|
1561
|
+
border: 0;
|
|
1562
|
+
min-inline-size: 0;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
legend {
|
|
1566
|
+
padding: 0;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
button,
|
|
1570
|
+
input,
|
|
1571
|
+
select,
|
|
1572
|
+
textarea,
|
|
1573
|
+
optgroup {
|
|
1574
|
+
font: inherit;
|
|
1575
|
+
letter-spacing: inherit;
|
|
1576
|
+
color: inherit;
|
|
1577
|
+
margin: 0;
|
|
1578
|
+
vertical-align: middle;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
button,
|
|
1582
|
+
input[type="button"],
|
|
1583
|
+
input[type="submit"],
|
|
1584
|
+
input[type="reset"] {
|
|
1585
|
+
appearance: none;
|
|
1586
|
+
background: none;
|
|
1587
|
+
border: 0;
|
|
1588
|
+
padding: 0;
|
|
1589
|
+
cursor: pointer;
|
|
1590
|
+
text-align: center;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
input,
|
|
1594
|
+
textarea,
|
|
1595
|
+
select {
|
|
1596
|
+
background-color: Canvas;
|
|
1597
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1598
|
+
border-radius: 0;
|
|
1599
|
+
padding: 0.5em 0.65em;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
input[type="checkbox"],
|
|
1603
|
+
input[type="radio"],
|
|
1604
|
+
input[type="range"],
|
|
1605
|
+
input[type="file"],
|
|
1606
|
+
input[type="color"],
|
|
1607
|
+
input[type="button"],
|
|
1608
|
+
input[type="submit"],
|
|
1609
|
+
input[type="reset"],
|
|
1610
|
+
input[type="image"],
|
|
1611
|
+
input[type="hidden"] {
|
|
1612
|
+
background: none;
|
|
1613
|
+
border: 0;
|
|
1614
|
+
padding: 0;
|
|
1615
|
+
border-radius: 0;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
input[type="checkbox"],
|
|
1619
|
+
input[type="radio"] {
|
|
1620
|
+
accent-color: CanvasText;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
input[type="search"] {
|
|
1624
|
+
appearance: none;
|
|
1625
|
+
background-color: Canvas;
|
|
1626
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1630
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1631
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1632
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1633
|
+
-webkit-appearance: none;
|
|
1634
|
+
display: none;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
input[type="number"] {
|
|
1638
|
+
appearance: textfield;
|
|
1639
|
+
background-color: Canvas;
|
|
1640
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1644
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1645
|
+
-webkit-appearance: none;
|
|
1646
|
+
margin: 0;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
textarea {
|
|
1650
|
+
resize: vertical;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
::placeholder {
|
|
1654
|
+
opacity: 1;
|
|
1655
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
:focus-visible {
|
|
1659
|
+
outline: 2px solid CanvasText;
|
|
1660
|
+
outline-offset: 2px;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
:focus:not(:focus-visible) {
|
|
1664
|
+
outline: none;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
:disabled,
|
|
1668
|
+
[disabled] {
|
|
1669
|
+
cursor: default;
|
|
1670
|
+
opacity: 0.6;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
label,
|
|
1674
|
+
summary,
|
|
1675
|
+
select,
|
|
1676
|
+
[role="button"] {
|
|
1677
|
+
cursor: pointer;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
summary {
|
|
1681
|
+
display: list-item;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
details {
|
|
1685
|
+
display: block;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
dialog {
|
|
1689
|
+
margin: auto;
|
|
1690
|
+
padding: 1rem;
|
|
1691
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1692
|
+
background: Canvas;
|
|
1693
|
+
color: CanvasText;
|
|
1694
|
+
max-width: calc(100vw - 2rem);
|
|
1695
|
+
max-height: calc(100svh - 2rem);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
dialog:not([open]) {
|
|
1699
|
+
display: none;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
::backdrop {
|
|
1703
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
[hidden] {
|
|
1707
|
+
display: none !important;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
::selection {
|
|
1711
|
+
background: Highlight;
|
|
1712
|
+
color: HighlightText;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1716
|
+
*,
|
|
1717
|
+
*::before,
|
|
1718
|
+
*::after {
|
|
1719
|
+
animation-duration: 0.01ms !important;
|
|
1720
|
+
animation-iteration-count: 1 !important;
|
|
1721
|
+
transition-duration: 0.01ms !important;
|
|
1722
|
+
scroll-behavior: auto !important;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
@media (pointer: coarse) {
|
|
1727
|
+
input,
|
|
1728
|
+
select,
|
|
1729
|
+
textarea {
|
|
1730
|
+
font-size: max(16px, 1em);
|
|
1731
|
+
}
|
|
1732
|
+
}`;
|
|
1733
|
+
|
|
1734
|
+
// ../core/dist/admin-client.js
|
|
1735
|
+
function isRecord2(value) {
|
|
1736
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1737
|
+
}
|
|
1738
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1739
|
+
const clip = (value, max) => {
|
|
1740
|
+
if (typeof value !== "string")
|
|
1741
|
+
return null;
|
|
1742
|
+
const trimmed = value.trim();
|
|
1743
|
+
if (trimmed === "")
|
|
1744
|
+
return null;
|
|
1745
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1746
|
+
};
|
|
1747
|
+
const httpUrl = (value) => {
|
|
1748
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1749
|
+
return null;
|
|
1750
|
+
try {
|
|
1751
|
+
const url = new URL(value);
|
|
1752
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1753
|
+
return null;
|
|
1754
|
+
return url.href;
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
const hexColor = (value) => {
|
|
1760
|
+
if (typeof value !== "string")
|
|
1761
|
+
return null;
|
|
1762
|
+
const raw = value.trim();
|
|
1763
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1764
|
+
};
|
|
1765
|
+
const hostname = (value) => {
|
|
1766
|
+
try {
|
|
1767
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1768
|
+
} catch {
|
|
1769
|
+
return value;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
host.replaceChildren();
|
|
1773
|
+
host.hidden = true;
|
|
1774
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1775
|
+
return;
|
|
1776
|
+
const row = catalog[service];
|
|
1777
|
+
if (!isRecord2(row))
|
|
1778
|
+
return;
|
|
1779
|
+
const vendor = clip(row.vendor, 80);
|
|
1780
|
+
const website = httpUrl(row.website);
|
|
1781
|
+
const guide = httpUrl(row.guide);
|
|
1782
|
+
const docs = httpUrl(row.docs);
|
|
1783
|
+
const logo = httpUrl(row.logo);
|
|
1784
|
+
const color = hexColor(row.color);
|
|
1785
|
+
const description = clip(row.description, 280);
|
|
1786
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1787
|
+
return;
|
|
1788
|
+
const add = (node) => {
|
|
1789
|
+
host.append(node);
|
|
1790
|
+
return node;
|
|
1791
|
+
};
|
|
1792
|
+
if (logo !== null) {
|
|
1793
|
+
const img = doc.createElement("img");
|
|
1794
|
+
img.alt = "";
|
|
1795
|
+
img.src = logo;
|
|
1796
|
+
img.width = 16;
|
|
1797
|
+
img.height = 16;
|
|
1798
|
+
img.decoding = "async";
|
|
1799
|
+
img.referrerPolicy = "no-referrer";
|
|
1800
|
+
if (color !== null)
|
|
1801
|
+
img.style.borderColor = color;
|
|
1802
|
+
add(img);
|
|
1803
|
+
}
|
|
1804
|
+
const name = doc.createElement("span");
|
|
1805
|
+
name.className = "vendor-name";
|
|
1806
|
+
name.textContent = vendor;
|
|
1807
|
+
if (description !== null)
|
|
1808
|
+
name.title = description;
|
|
1809
|
+
add(name);
|
|
1810
|
+
const sep = () => {
|
|
1811
|
+
const node = doc.createElement("span");
|
|
1812
|
+
node.className = "vendor-sep";
|
|
1813
|
+
node.setAttribute("aria-hidden", "true");
|
|
1814
|
+
node.textContent = "\xB7";
|
|
1815
|
+
return node;
|
|
1816
|
+
};
|
|
1817
|
+
const link = (href, label, title) => {
|
|
1818
|
+
const node = doc.createElement("a");
|
|
1819
|
+
node.className = "vendor-link";
|
|
1820
|
+
node.href = href;
|
|
1821
|
+
node.target = "_blank";
|
|
1822
|
+
node.rel = "noopener noreferrer";
|
|
1823
|
+
node.referrerPolicy = "no-referrer";
|
|
1824
|
+
node.textContent = label;
|
|
1825
|
+
node.title = title;
|
|
1826
|
+
return node;
|
|
1827
|
+
};
|
|
1828
|
+
if (website !== null) {
|
|
1829
|
+
add(sep());
|
|
1830
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1831
|
+
}
|
|
1832
|
+
if (guide !== null) {
|
|
1833
|
+
add(sep());
|
|
1834
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1835
|
+
}
|
|
1836
|
+
if (docs !== null) {
|
|
1837
|
+
add(sep());
|
|
1838
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1839
|
+
}
|
|
1840
|
+
host.removeAttribute("hidden");
|
|
1841
|
+
host.hidden = false;
|
|
1842
|
+
}
|
|
1843
|
+
function startAdminBrand(env) {
|
|
1844
|
+
const requested = env.params.get("brands");
|
|
1845
|
+
let brandsUrl = env.brandsUrl;
|
|
1846
|
+
if (requested) {
|
|
1847
|
+
try {
|
|
1848
|
+
const url = new URL(requested, env.location.href);
|
|
1849
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1850
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1851
|
+
brandsUrl = url.href;
|
|
1852
|
+
} catch {
|
|
1853
|
+
brandsUrl = env.brandsUrl;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const vendorHost = env.$("vendor");
|
|
1857
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1858
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1859
|
+
if (vendorHost === null)
|
|
1860
|
+
return;
|
|
1861
|
+
env.fetch(brandsUrl, {
|
|
1862
|
+
headers: { accept: "application/json" },
|
|
1863
|
+
signal: AbortSignal.timeout(5e3)
|
|
1864
|
+
}).then(async (response) => {
|
|
1865
|
+
if (!response.ok)
|
|
1866
|
+
return null;
|
|
1867
|
+
const catalog = await response.json();
|
|
1868
|
+
return catalog;
|
|
1869
|
+
}).then((catalog) => {
|
|
1870
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1871
|
+
}).catch(() => {
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
function faultPresetList(value) {
|
|
1875
|
+
if (!isRecord2(value))
|
|
1876
|
+
throw new Error("Fault presets response was not an object");
|
|
1877
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1878
|
+
return { presets: [] };
|
|
1879
|
+
}
|
|
1880
|
+
if (!Array.isArray(value.presets))
|
|
1881
|
+
throw new Error("Fault presets response was not a list");
|
|
1882
|
+
return {
|
|
1883
|
+
presets: value.presets.flatMap((item) => {
|
|
1884
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1885
|
+
return [];
|
|
1886
|
+
return [{ name: item.name }];
|
|
1887
|
+
})
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function bootAdmin(config) {
|
|
1891
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1892
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1893
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1894
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1895
|
+
const readJson2 = (text) => {
|
|
1896
|
+
const value = JSON.parse(text);
|
|
1897
|
+
return value;
|
|
1898
|
+
};
|
|
1899
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1900
|
+
switch (char) {
|
|
1901
|
+
case "&":
|
|
1902
|
+
return "&";
|
|
1903
|
+
case "<":
|
|
1904
|
+
return "<";
|
|
1905
|
+
case ">":
|
|
1906
|
+
return ">";
|
|
1907
|
+
case '"':
|
|
1908
|
+
return """;
|
|
1909
|
+
default:
|
|
1910
|
+
return "'";
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
const el = (id, kind) => {
|
|
1914
|
+
const node = document.getElementById(id);
|
|
1915
|
+
if (!(node instanceof kind))
|
|
1916
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1917
|
+
return node;
|
|
1918
|
+
};
|
|
1919
|
+
const errorMessage = (data, fallback) => {
|
|
1920
|
+
if (!isRecord2(data))
|
|
1921
|
+
return fallback;
|
|
1922
|
+
const error = data.error;
|
|
1923
|
+
if (!isRecord2(error) || typeof error.message !== "string" || error.message === "") {
|
|
1924
|
+
return fallback;
|
|
1925
|
+
}
|
|
1926
|
+
return error.message;
|
|
1927
|
+
};
|
|
1928
|
+
const requireList = (value, label) => {
|
|
1929
|
+
if (value === void 0 || value === null)
|
|
1930
|
+
return [];
|
|
1931
|
+
if (!Array.isArray(value))
|
|
1932
|
+
throw new Error(`${label} was not a list`);
|
|
1933
|
+
return value;
|
|
1934
|
+
};
|
|
1935
|
+
const state = {
|
|
1936
|
+
namespace: "default",
|
|
1937
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1938
|
+
view: "overview",
|
|
1939
|
+
collection: "",
|
|
1940
|
+
shape: null,
|
|
1941
|
+
route: null,
|
|
1942
|
+
editing: null
|
|
1943
|
+
};
|
|
1944
|
+
const standard = new Set(config.standardRoutes);
|
|
1945
|
+
const banner = el("banner", HTMLElement);
|
|
1946
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1947
|
+
keyInput.value = state.key;
|
|
1948
|
+
const params = new URLSearchParams(location.search);
|
|
1949
|
+
const queryKey = params.get("key");
|
|
1950
|
+
if (queryKey) {
|
|
1951
|
+
state.key = queryKey;
|
|
1952
|
+
keyInput.value = state.key;
|
|
1953
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1954
|
+
params.delete("key");
|
|
1955
|
+
const next = params.toString();
|
|
1956
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1957
|
+
}
|
|
1958
|
+
const queryNamespace = params.get("namespace");
|
|
1959
|
+
if (queryNamespace)
|
|
1960
|
+
state.namespace = queryNamespace;
|
|
1961
|
+
const showError = (error) => {
|
|
1962
|
+
banner.textContent = error instanceof Error ? error.message : String(error);
|
|
1963
|
+
banner.classList.add("show");
|
|
1964
|
+
};
|
|
1965
|
+
const clearError = () => {
|
|
1966
|
+
banner.classList.remove("show");
|
|
1967
|
+
};
|
|
1968
|
+
const api = async (method, path, body) => {
|
|
1969
|
+
const headers = { accept: "application/json" };
|
|
1970
|
+
if (state.key !== "")
|
|
1971
|
+
headers[config.adminKeyHeader] = state.key;
|
|
1972
|
+
if (state.namespace !== "")
|
|
1973
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
1974
|
+
if (body !== void 0)
|
|
1975
|
+
headers["content-type"] = "application/json";
|
|
1976
|
+
const init = { method, headers };
|
|
1977
|
+
if (body !== void 0)
|
|
1978
|
+
init.body = JSON.stringify(body);
|
|
1979
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
1980
|
+
const text = await response.text();
|
|
1981
|
+
let data = null;
|
|
1982
|
+
if (text !== "") {
|
|
1983
|
+
try {
|
|
1984
|
+
data = readJson2(text);
|
|
1985
|
+
} catch {
|
|
1986
|
+
data = { raw: text };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (response.status === 401) {
|
|
1990
|
+
keyInput.focus();
|
|
1991
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
1992
|
+
}
|
|
1993
|
+
if (!response.ok) {
|
|
1994
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
1995
|
+
}
|
|
1996
|
+
return data;
|
|
1997
|
+
};
|
|
1998
|
+
const originGet = async (path) => {
|
|
1999
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
2000
|
+
if (!response.ok)
|
|
2001
|
+
throw new Error(`${response.status} ${path}`);
|
|
2002
|
+
const value = await response.json();
|
|
2003
|
+
return value;
|
|
2004
|
+
};
|
|
2005
|
+
const panelApi = {
|
|
2006
|
+
get: (path) => api("GET", path),
|
|
2007
|
+
send: (method, path, body) => api(method, path, body),
|
|
2008
|
+
namespace: () => state.namespace
|
|
2009
|
+
};
|
|
2010
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
2011
|
+
const setTheme = (theme) => {
|
|
2012
|
+
if (theme)
|
|
2013
|
+
document.documentElement.dataset.theme = theme;
|
|
2014
|
+
else
|
|
2015
|
+
delete document.documentElement.dataset.theme;
|
|
2016
|
+
};
|
|
2017
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2018
|
+
if (storedTheme)
|
|
2019
|
+
setTheme(storedTheme);
|
|
2020
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2021
|
+
const current = document.documentElement.dataset.theme;
|
|
2022
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2023
|
+
const next = dark ? "light" : "dark";
|
|
2024
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
2025
|
+
setTheme(next);
|
|
2026
|
+
});
|
|
2027
|
+
const show = (view) => {
|
|
2028
|
+
state.view = view;
|
|
2029
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
2030
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2031
|
+
continue;
|
|
2032
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
2033
|
+
}
|
|
2034
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
2035
|
+
if (!(section instanceof HTMLElement))
|
|
2036
|
+
continue;
|
|
2037
|
+
section.hidden = section.id !== `view-${view}`;
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2041
|
+
const target = event.target;
|
|
2042
|
+
if (!(target instanceof Element))
|
|
2043
|
+
return;
|
|
2044
|
+
const button = target.closest("button[data-view]");
|
|
2045
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2046
|
+
return;
|
|
2047
|
+
const view = button.dataset.view;
|
|
2048
|
+
if (view === void 0)
|
|
2049
|
+
return;
|
|
2050
|
+
show(view);
|
|
2051
|
+
refresh().catch(showError);
|
|
2052
|
+
});
|
|
2053
|
+
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>`;
|
|
2054
|
+
const table = (columns, rows) => {
|
|
2055
|
+
if (rows.length === 0)
|
|
2056
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
2057
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
2058
|
+
const body = rows.map((row) => {
|
|
2059
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
2060
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2061
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
2062
|
+
}).join("");
|
|
2063
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2064
|
+
};
|
|
2065
|
+
const preview = (value) => {
|
|
2066
|
+
const text = JSON.stringify(value) ?? "null";
|
|
2067
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
2068
|
+
};
|
|
2069
|
+
const when = (epoch) => {
|
|
2070
|
+
const date = new Date(epoch);
|
|
2071
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2072
|
+
};
|
|
2073
|
+
const clockCard = (epoch) => {
|
|
2074
|
+
const date = new Date(epoch);
|
|
2075
|
+
if (Number.isNaN(date.getTime()))
|
|
2076
|
+
return esc(String(epoch));
|
|
2077
|
+
const iso = date.toISOString();
|
|
2078
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
2079
|
+
};
|
|
2080
|
+
const decodeClock = (value) => {
|
|
2081
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
2082
|
+
throw new Error("Clock response was not a clock");
|
|
2083
|
+
}
|
|
2084
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
2085
|
+
};
|
|
2086
|
+
const decodeField = (value) => {
|
|
2087
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
2088
|
+
return void 0;
|
|
2089
|
+
}
|
|
2090
|
+
const field = {
|
|
2091
|
+
name: value.name,
|
|
2092
|
+
kind: value.kind,
|
|
2093
|
+
optional: value.optional === true
|
|
2094
|
+
};
|
|
2095
|
+
if (typeof value.description === "string")
|
|
2096
|
+
field.description = value.description;
|
|
2097
|
+
if (Array.isArray(value.fields)) {
|
|
2098
|
+
field.fields = value.fields.flatMap((item) => {
|
|
2099
|
+
const child = decodeField(item);
|
|
2100
|
+
return child === void 0 ? [] : [child];
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return field;
|
|
2104
|
+
};
|
|
2105
|
+
const decodeCollection = (value) => {
|
|
2106
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
2107
|
+
return void 0;
|
|
2108
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
2109
|
+
const field = decodeField(item);
|
|
2110
|
+
return field === void 0 ? [] : [field];
|
|
2111
|
+
}) : [];
|
|
2112
|
+
const collection = {
|
|
2113
|
+
name: value.name,
|
|
2114
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
2115
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
2116
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
2117
|
+
fields
|
|
2118
|
+
};
|
|
2119
|
+
if (typeof value.description === "string")
|
|
2120
|
+
collection.description = value.description;
|
|
2121
|
+
return collection;
|
|
2122
|
+
};
|
|
2123
|
+
const decodeState = (value) => {
|
|
2124
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
2125
|
+
throw new Error("State response has no collection list");
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
2129
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
2130
|
+
collections: value.collections.flatMap((item) => {
|
|
2131
|
+
const collection = decodeCollection(item);
|
|
2132
|
+
return collection === void 0 ? [] : [collection];
|
|
2133
|
+
})
|
|
2134
|
+
};
|
|
2135
|
+
};
|
|
2136
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
2137
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
2138
|
+
return [];
|
|
2139
|
+
return [
|
|
2140
|
+
{
|
|
2141
|
+
id: item.id,
|
|
2142
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
2143
|
+
value: item.value,
|
|
2144
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
2145
|
+
}
|
|
2146
|
+
];
|
|
2147
|
+
});
|
|
2148
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
2149
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
2150
|
+
if (!isRecord2(item))
|
|
2151
|
+
return [];
|
|
2152
|
+
return [
|
|
2153
|
+
{
|
|
2154
|
+
at: textOf(item.at),
|
|
2155
|
+
method: textOf(item.method),
|
|
2156
|
+
path: textOf(item.path),
|
|
2157
|
+
status: textOf(item.status),
|
|
2158
|
+
durationMs: textOf(item.durationMs),
|
|
2159
|
+
operationId: textOf(item.operationId)
|
|
2160
|
+
}
|
|
2161
|
+
];
|
|
2162
|
+
});
|
|
2163
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
2164
|
+
if (!isRecord2(item))
|
|
2165
|
+
return [];
|
|
2166
|
+
return [
|
|
2167
|
+
{
|
|
2168
|
+
id: textOf(item.id),
|
|
2169
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
2170
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
2171
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
2172
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
2173
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
2174
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
2175
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
2176
|
+
}
|
|
2177
|
+
];
|
|
2178
|
+
});
|
|
2179
|
+
const loadNamespaces = async () => {
|
|
2180
|
+
const data = await api("GET", "/namespaces");
|
|
2181
|
+
const select = el("namespace", HTMLSelectElement);
|
|
2182
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
2183
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
2184
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
2185
|
+
if (!names.includes(state.namespace))
|
|
2186
|
+
names.unshift(state.namespace);
|
|
2187
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
2188
|
+
select.value = state.namespace;
|
|
2189
|
+
};
|
|
2190
|
+
const renderOverview = async () => {
|
|
2191
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
2192
|
+
originGet("/health"),
|
|
2193
|
+
api("GET", "/state"),
|
|
2194
|
+
api("GET", "/clock"),
|
|
2195
|
+
api("GET", "/requests?limit=8")
|
|
2196
|
+
]);
|
|
2197
|
+
if (!isRecord2(health))
|
|
2198
|
+
throw new Error("Health response was not an object");
|
|
2199
|
+
const decoded = decodeState(shape);
|
|
2200
|
+
state.shape = decoded;
|
|
2201
|
+
const time = decodeClock(clock);
|
|
2202
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
2203
|
+
{
|
|
2204
|
+
k: "Service",
|
|
2205
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
k: "Status",
|
|
2209
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2210
|
+
},
|
|
2211
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
2212
|
+
{ k: "Collections", v: String(decoded.collections.length) },
|
|
2213
|
+
{
|
|
2214
|
+
k: "Records",
|
|
2215
|
+
v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
|
|
2216
|
+
},
|
|
2217
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
2218
|
+
]);
|
|
2219
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
2220
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2221
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2222
|
+
{
|
|
2223
|
+
label: "Status",
|
|
2224
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
2225
|
+
},
|
|
2226
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
2227
|
+
], decodeJournal(journal));
|
|
2228
|
+
};
|
|
2229
|
+
const schemaChips = (collection) => {
|
|
2230
|
+
if (collection.fields.length === 0) {
|
|
2231
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
2232
|
+
}
|
|
2233
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
2234
|
+
};
|
|
2235
|
+
const renderState = async () => {
|
|
2236
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
2237
|
+
state.shape = shape;
|
|
2238
|
+
const host = el("collections", HTMLElement);
|
|
2239
|
+
if (shape.collections.length === 0) {
|
|
2240
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
2241
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
2242
|
+
el("records", HTMLElement).innerHTML = "";
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
2246
|
+
const first = shape.collections[0];
|
|
2247
|
+
if (first === void 0)
|
|
2248
|
+
return;
|
|
2249
|
+
state.collection = first.name;
|
|
2250
|
+
}
|
|
2251
|
+
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("");
|
|
2252
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
2253
|
+
if (selected === void 0)
|
|
2254
|
+
return;
|
|
2255
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
2256
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
2257
|
+
const page2 = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
2258
|
+
el("records", HTMLElement).innerHTML = table([
|
|
2259
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
2260
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
2261
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
2262
|
+
], decodeRecords(page2));
|
|
2263
|
+
};
|
|
2264
|
+
const renderClock = async () => {
|
|
2265
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
2266
|
+
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>`;
|
|
2267
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
2268
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
2269
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
2270
|
+
};
|
|
2271
|
+
const renderFaults = async () => {
|
|
2272
|
+
const [faults, presets] = await Promise.all([
|
|
2273
|
+
api("GET", "/faults"),
|
|
2274
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
2275
|
+
]);
|
|
2276
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
2277
|
+
const current = select.value;
|
|
2278
|
+
const list = faultPresetList(presets).presets;
|
|
2279
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
2280
|
+
select.value = current;
|
|
2281
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
2282
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
2283
|
+
{
|
|
2284
|
+
label: "Match",
|
|
2285
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
label: "Effect",
|
|
2289
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
2290
|
+
}
|
|
2291
|
+
], decodeFaults(faults));
|
|
2292
|
+
};
|
|
2293
|
+
const renderJournal = async () => {
|
|
2294
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
2295
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
2296
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
2297
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2298
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2299
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
2300
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2301
|
+
], decodeJournal(journal));
|
|
2302
|
+
};
|
|
2303
|
+
const renderRoutes = async () => {
|
|
2304
|
+
const data = await api("GET", "/");
|
|
2305
|
+
if (!isRecord2(data))
|
|
2306
|
+
throw new Error("Route list was not an object");
|
|
2307
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2308
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2309
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2310
|
+
const parts = route.split(" ");
|
|
2311
|
+
const method = parts[0] ?? "";
|
|
2312
|
+
const path = parts.slice(1).join(" ");
|
|
2313
|
+
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>`;
|
|
2314
|
+
}).join("");
|
|
2315
|
+
};
|
|
2316
|
+
const renderers = {
|
|
2317
|
+
overview: renderOverview,
|
|
2318
|
+
state: renderState,
|
|
2319
|
+
clock: renderClock,
|
|
2320
|
+
faults: renderFaults,
|
|
2321
|
+
journal: renderJournal,
|
|
2322
|
+
routes: renderRoutes
|
|
2323
|
+
};
|
|
2324
|
+
async function refresh() {
|
|
2325
|
+
clearError();
|
|
2326
|
+
const render = renderers[state.view];
|
|
2327
|
+
if (render)
|
|
2328
|
+
await render();
|
|
2329
|
+
}
|
|
2330
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
2331
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
2332
|
+
refresh().catch(showError);
|
|
2333
|
+
});
|
|
2334
|
+
keyInput.addEventListener("change", () => {
|
|
2335
|
+
state.key = keyInput.value.trim();
|
|
2336
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2337
|
+
refresh().catch(showError);
|
|
2338
|
+
});
|
|
2339
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
2340
|
+
refresh().catch(showError);
|
|
2341
|
+
});
|
|
2342
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
2343
|
+
const target = event.target;
|
|
2344
|
+
if (!(target instanceof Element))
|
|
2345
|
+
return;
|
|
2346
|
+
const button = target.closest("[data-collection]");
|
|
2347
|
+
if (!(button instanceof HTMLElement))
|
|
2348
|
+
return;
|
|
2349
|
+
const collection = button.dataset.collection;
|
|
2350
|
+
if (collection === void 0)
|
|
2351
|
+
return;
|
|
2352
|
+
state.collection = collection;
|
|
2353
|
+
renderState().catch(showError);
|
|
2354
|
+
});
|
|
2355
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
2356
|
+
const target = event.target;
|
|
2357
|
+
if (!(target instanceof Element))
|
|
2358
|
+
return;
|
|
2359
|
+
const row = target.closest("[data-id]");
|
|
2360
|
+
if (!(row instanceof HTMLElement))
|
|
2361
|
+
return;
|
|
2362
|
+
const id = row.dataset.id;
|
|
2363
|
+
if (id === void 0)
|
|
2364
|
+
return;
|
|
2365
|
+
openEditor(id).catch(showError);
|
|
2366
|
+
});
|
|
2367
|
+
const editor = el("editor", HTMLDialogElement);
|
|
2368
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
2369
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
2370
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
2371
|
+
async function openEditor(id) {
|
|
2372
|
+
state.editing = id;
|
|
2373
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
2374
|
+
editorId.value = id ?? "";
|
|
2375
|
+
editorId.readOnly = id !== null;
|
|
2376
|
+
editorDelete.hidden = id === null;
|
|
2377
|
+
if (id) {
|
|
2378
|
+
const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
2379
|
+
if (!isRecord2(record))
|
|
2380
|
+
throw new Error("Record response was not an object");
|
|
2381
|
+
editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
|
|
2382
|
+
} else {
|
|
2383
|
+
editorValue.value = "{\n \n}";
|
|
2384
|
+
}
|
|
2385
|
+
editor.showModal();
|
|
2386
|
+
}
|
|
2387
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
2388
|
+
openEditor(null).catch(showError);
|
|
2389
|
+
});
|
|
2390
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2391
|
+
const submitter = event.submitter;
|
|
2392
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
2393
|
+
return;
|
|
2394
|
+
event.preventDefault();
|
|
2395
|
+
let value;
|
|
2396
|
+
try {
|
|
2397
|
+
value = readJson2(editorValue.value);
|
|
2398
|
+
} catch {
|
|
2399
|
+
showError(new Error("Value is not JSON"));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const id = editorId.value.trim();
|
|
2403
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2404
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2405
|
+
request.then(() => {
|
|
2406
|
+
editor.close();
|
|
2407
|
+
return renderState();
|
|
2408
|
+
}).catch(showError);
|
|
2409
|
+
});
|
|
2410
|
+
editorDelete.addEventListener("click", () => {
|
|
2411
|
+
const id = state.editing;
|
|
2412
|
+
if (!id)
|
|
2413
|
+
return;
|
|
2414
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2415
|
+
editor.close();
|
|
2416
|
+
return renderState();
|
|
2417
|
+
}).catch(showError);
|
|
2418
|
+
});
|
|
2419
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2420
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2421
|
+
continue;
|
|
2422
|
+
button.addEventListener("click", () => {
|
|
2423
|
+
const raw = button.dataset.advance;
|
|
2424
|
+
if (raw === void 0)
|
|
2425
|
+
return;
|
|
2426
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2430
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2431
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2432
|
+
});
|
|
2433
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2434
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2435
|
+
});
|
|
2436
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2437
|
+
event.preventDefault();
|
|
2438
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2439
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2440
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2441
|
+
});
|
|
2442
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2443
|
+
event.preventDefault();
|
|
2444
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2445
|
+
if (preset !== "") {
|
|
2446
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2450
|
+
if (!Number.isFinite(status)) {
|
|
2451
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2455
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2456
|
+
api("POST", "/faults", {
|
|
2457
|
+
status,
|
|
2458
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2459
|
+
...method !== "" ? { method } : {}
|
|
2460
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2461
|
+
});
|
|
2462
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2463
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2464
|
+
});
|
|
2465
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2466
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2467
|
+
});
|
|
2468
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2469
|
+
const target = event.target;
|
|
2470
|
+
if (!(target instanceof Element))
|
|
2471
|
+
return;
|
|
2472
|
+
const button = target.closest("[data-route]");
|
|
2473
|
+
if (!(button instanceof HTMLElement))
|
|
2474
|
+
return;
|
|
2475
|
+
const route = button.dataset.route;
|
|
2476
|
+
if (route === void 0)
|
|
2477
|
+
return;
|
|
2478
|
+
state.route = route;
|
|
2479
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2480
|
+
const method = route.split(" ")[0] ?? "";
|
|
2481
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2482
|
+
});
|
|
2483
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2484
|
+
event.preventDefault();
|
|
2485
|
+
const send = async () => {
|
|
2486
|
+
if (state.route === null)
|
|
2487
|
+
throw new Error("Choose a route first");
|
|
2488
|
+
const parts = state.route.split(" ");
|
|
2489
|
+
const method = parts[0];
|
|
2490
|
+
const path = parts.slice(1).join(" ");
|
|
2491
|
+
if (method === void 0)
|
|
2492
|
+
throw new Error("Choose a route first");
|
|
2493
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2494
|
+
let body;
|
|
2495
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2496
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2497
|
+
try {
|
|
2498
|
+
body = readJson2(routeBody.value);
|
|
2499
|
+
} catch {
|
|
2500
|
+
throw new Error("Body is not JSON");
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const data = await api(method, concrete, body);
|
|
2504
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2505
|
+
};
|
|
2506
|
+
send().catch(showError);
|
|
2507
|
+
});
|
|
2508
|
+
const decodePanel = (value) => {
|
|
2509
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2510
|
+
return void 0;
|
|
2511
|
+
}
|
|
2512
|
+
const panel = {
|
|
2513
|
+
id: value.id,
|
|
2514
|
+
title: value.title,
|
|
2515
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2516
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2517
|
+
};
|
|
2518
|
+
if (typeof value.script === "string")
|
|
2519
|
+
panel.script = value.script;
|
|
2520
|
+
return panel;
|
|
2521
|
+
};
|
|
2522
|
+
const mountPanels = (panels) => {
|
|
2523
|
+
const nav = el("nav", HTMLElement);
|
|
2524
|
+
const views = el("views", HTMLElement);
|
|
2525
|
+
for (const panel of panels) {
|
|
2526
|
+
const button = document.createElement("button");
|
|
2527
|
+
button.type = "button";
|
|
2528
|
+
button.dataset.view = panel.id;
|
|
2529
|
+
button.textContent = panel.title;
|
|
2530
|
+
nav.append(button);
|
|
2531
|
+
const section = document.createElement("section");
|
|
2532
|
+
section.className = "view";
|
|
2533
|
+
section.id = `view-${panel.id}`;
|
|
2534
|
+
section.hidden = true;
|
|
2535
|
+
const title = document.createElement("h2");
|
|
2536
|
+
title.textContent = panel.title;
|
|
2537
|
+
const lede = document.createElement("p");
|
|
2538
|
+
lede.className = "lede";
|
|
2539
|
+
lede.textContent = panel.description;
|
|
2540
|
+
const body = document.createElement("div");
|
|
2541
|
+
body.className = "panel-body";
|
|
2542
|
+
body.id = `panel-${panel.id}`;
|
|
2543
|
+
body.innerHTML = panel.html;
|
|
2544
|
+
section.append(title, lede, body);
|
|
2545
|
+
views.append(section);
|
|
2546
|
+
if (panel.script === void 0)
|
|
2547
|
+
continue;
|
|
2548
|
+
try {
|
|
2549
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2550
|
+
if (!isPanelScript(compiled))
|
|
2551
|
+
throw new Error("panel script did not compile");
|
|
2552
|
+
compiled(body, panelApi);
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
const note = document.createElement("p");
|
|
2555
|
+
note.className = "banner show";
|
|
2556
|
+
note.textContent = error instanceof Error ? error.message : String(error);
|
|
2557
|
+
body.append(note);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
startAdminBrand({
|
|
2562
|
+
document,
|
|
2563
|
+
$: (id) => document.getElementById(id),
|
|
2564
|
+
params,
|
|
2565
|
+
location: { href: location.href, origin: location.origin },
|
|
2566
|
+
fetch,
|
|
2567
|
+
brandsUrl: config.brandsUrl
|
|
2568
|
+
});
|
|
2569
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2570
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2571
|
+
const decoded = decodePanel(panel);
|
|
2572
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2573
|
+
});
|
|
2574
|
+
mountPanels(panels);
|
|
2575
|
+
return refresh();
|
|
2576
|
+
}).catch(showError);
|
|
2577
|
+
}
|
|
2578
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2579
|
+
var adminClientSource = (config) => {
|
|
2580
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2581
|
+
return `<script>
|
|
2582
|
+
${clientPrelude}
|
|
2583
|
+
bootAdmin(${payload});
|
|
2584
|
+
</script>`;
|
|
2585
|
+
};
|
|
2586
|
+
|
|
2587
|
+
// ../core/dist/surface.js
|
|
2588
|
+
var defineMock = (runtime) => runtime;
|
|
2589
|
+
var STANDARD_ADMIN_ROUTES = [
|
|
2590
|
+
"GET /",
|
|
2591
|
+
"POST /reset",
|
|
2592
|
+
"GET /namespaces",
|
|
2593
|
+
"GET /clock",
|
|
2594
|
+
"POST /clock",
|
|
2595
|
+
"GET /faults",
|
|
2596
|
+
"POST /faults",
|
|
2597
|
+
"DELETE /faults",
|
|
2598
|
+
"POST /snapshots",
|
|
2599
|
+
"POST /snapshots/:id/restore",
|
|
2600
|
+
"DELETE /snapshots/:id",
|
|
2601
|
+
"GET /timeline",
|
|
2602
|
+
"POST /checkpoints",
|
|
2603
|
+
"POST /branches/:name",
|
|
2604
|
+
"POST /branches/:name/checkout",
|
|
2605
|
+
"GET /requests",
|
|
2606
|
+
"DELETE /requests",
|
|
2607
|
+
"GET /metrics",
|
|
2608
|
+
"DELETE /metrics",
|
|
2609
|
+
"GET /credentials",
|
|
2610
|
+
"PUT /credentials",
|
|
2611
|
+
"DELETE /credentials",
|
|
2612
|
+
"GET /state",
|
|
2613
|
+
"GET /state/:collection",
|
|
2614
|
+
"GET /state/:collection/:id",
|
|
2615
|
+
"POST /state/:collection",
|
|
2616
|
+
"PUT /state/:collection/:id",
|
|
2617
|
+
"PATCH /state/:collection/:id",
|
|
2618
|
+
"DELETE /state/:collection/:id",
|
|
2619
|
+
"GET /ui",
|
|
2620
|
+
"GET /ui/",
|
|
2621
|
+
"GET /ui/manifest"
|
|
2622
|
+
];
|
|
2623
|
+
var PANEL_ID = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
2624
|
+
var assertAdminUi = (ui) => {
|
|
2625
|
+
if (!ui?.panels)
|
|
2626
|
+
return;
|
|
2627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2628
|
+
for (const panel of ui.panels) {
|
|
2629
|
+
if (!PANEL_ID.test(panel.id)) {
|
|
2630
|
+
throw new RangeError(`admin panel id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
|
|
2631
|
+
}
|
|
2632
|
+
if (seen.has(panel.id))
|
|
2633
|
+
throw new RangeError(`duplicate admin panel id ${panel.id}`);
|
|
2634
|
+
seen.add(panel.id);
|
|
2635
|
+
if (panel.title.trim() === "")
|
|
2636
|
+
throw new RangeError(`admin panel ${panel.id} needs a title`);
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
// ../core/dist/admin-ui.js
|
|
2641
|
+
var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
2642
|
+
switch (char) {
|
|
2643
|
+
case "&":
|
|
2644
|
+
return "&";
|
|
2645
|
+
case "<":
|
|
2646
|
+
return "<";
|
|
2647
|
+
case ">":
|
|
2648
|
+
return ">";
|
|
2649
|
+
case '"':
|
|
2650
|
+
return """;
|
|
2651
|
+
default:
|
|
2652
|
+
return "'";
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2656
|
+
var renderAdminDocument = (service) => {
|
|
2657
|
+
const name = escapeHtml(service);
|
|
2658
|
+
const script = adminClientSource({
|
|
2659
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2660
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2661
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2662
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2663
|
+
service
|
|
2664
|
+
});
|
|
2665
|
+
return `<!DOCTYPE html>
|
|
2666
|
+
<html lang="en">
|
|
2667
|
+
<head>
|
|
2668
|
+
<meta charset="utf-8">
|
|
2669
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2670
|
+
<meta name="color-scheme" content="light dark">
|
|
2671
|
+
<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">
|
|
2672
|
+
<title>${name} admin</title>
|
|
2673
|
+
<style>
|
|
2674
|
+
${CSS_RESET}
|
|
2675
|
+
:root {
|
|
2676
|
+
color-scheme: light;
|
|
2677
|
+
--bg: #fafafa;
|
|
2678
|
+
--bg-elev: #ffffff;
|
|
2679
|
+
--bg-muted: #f4f4f5;
|
|
2680
|
+
--bg-inset: #f4f4f5;
|
|
2681
|
+
--line: #dedee3;
|
|
2682
|
+
--line-strong: #c4c4cc;
|
|
2683
|
+
--ink: #18181b;
|
|
2684
|
+
--muted: #62626b;
|
|
2685
|
+
--faint: #71717a;
|
|
2686
|
+
--accent: #27272a;
|
|
2687
|
+
--accent-ink: #ffffff;
|
|
2688
|
+
--accent-soft: #f4f4f5;
|
|
2689
|
+
--ok: #166534;
|
|
2690
|
+
--ok-soft: #f0fdf4;
|
|
2691
|
+
--warn: #a16207;
|
|
2692
|
+
--warn-soft: #fefce8;
|
|
2693
|
+
--err: #a82d32;
|
|
2694
|
+
--err-soft: #fef2f2;
|
|
2695
|
+
--shadow: none;
|
|
2696
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2697
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2698
|
+
--radius: 10px;
|
|
2699
|
+
--header: 56px;
|
|
2700
|
+
--nav: 200px;
|
|
2701
|
+
}
|
|
2702
|
+
:root[data-theme="dark"] {
|
|
2703
|
+
color-scheme: dark;
|
|
2704
|
+
--bg: #111113;
|
|
2705
|
+
--bg-elev: #19191c;
|
|
2706
|
+
--bg-muted: #242428;
|
|
2707
|
+
--bg-inset: #111113;
|
|
2708
|
+
--line: #36363c;
|
|
2709
|
+
--line-strong: #52525b;
|
|
2710
|
+
--ink: #f4f4f5;
|
|
2711
|
+
--muted: #a9a9b2;
|
|
2712
|
+
--faint: #71717a;
|
|
2713
|
+
--accent: #e4e4e7;
|
|
2714
|
+
--accent-ink: #18181b;
|
|
2715
|
+
--accent-soft: #242428;
|
|
2716
|
+
--ok: #86efac;
|
|
2717
|
+
--ok-soft: #14241b;
|
|
2718
|
+
--warn: #fde68a;
|
|
2719
|
+
--warn-soft: #2a2410;
|
|
2720
|
+
--err: #ffaaaa;
|
|
2721
|
+
--err-soft: #2c1516;
|
|
2722
|
+
--shadow: none;
|
|
2723
|
+
}
|
|
2724
|
+
@media (prefers-color-scheme: dark) {
|
|
2725
|
+
:root:not([data-theme="light"]) {
|
|
2726
|
+
color-scheme: dark;
|
|
2727
|
+
--bg: #111113;
|
|
2728
|
+
--bg-elev: #19191c;
|
|
2729
|
+
--bg-muted: #242428;
|
|
2730
|
+
--bg-inset: #111113;
|
|
2731
|
+
--line: #36363c;
|
|
2732
|
+
--line-strong: #52525b;
|
|
2733
|
+
--ink: #f4f4f5;
|
|
2734
|
+
--muted: #a9a9b2;
|
|
2735
|
+
--faint: #71717a;
|
|
2736
|
+
--accent: #e4e4e7;
|
|
2737
|
+
--accent-ink: #18181b;
|
|
2738
|
+
--accent-soft: #242428;
|
|
2739
|
+
--ok: #86efac;
|
|
2740
|
+
--ok-soft: #14241b;
|
|
2741
|
+
--warn: #fde68a;
|
|
2742
|
+
--warn-soft: #2a2410;
|
|
2743
|
+
--err: #ffaaaa;
|
|
2744
|
+
--err-soft: #2c1516;
|
|
2745
|
+
--shadow: none;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2749
|
+
html { height: 100%; }
|
|
2750
|
+
body {
|
|
2751
|
+
margin: 0;
|
|
2752
|
+
min-height: 100%;
|
|
2753
|
+
display: flex;
|
|
2754
|
+
flex-direction: column;
|
|
2755
|
+
font-family: var(--sans);
|
|
2756
|
+
background: var(--bg);
|
|
2757
|
+
color: var(--ink);
|
|
2758
|
+
line-height: 1.45;
|
|
2759
|
+
padding-left: env(safe-area-inset-left);
|
|
2760
|
+
padding-right: env(safe-area-inset-right);
|
|
2761
|
+
-webkit-text-size-adjust: 100%;
|
|
2762
|
+
}
|
|
2763
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2764
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2765
|
+
header.top {
|
|
2766
|
+
position: sticky; top: 0; z-index: 5;
|
|
2767
|
+
display: flex; align-items: center; gap: 12px;
|
|
2768
|
+
min-height: var(--header); padding: 10px 16px;
|
|
2769
|
+
background: var(--bg-elev);
|
|
2770
|
+
border-bottom: 1px solid var(--line);
|
|
2771
|
+
min-width: 0; max-width: 100%;
|
|
2772
|
+
}
|
|
2773
|
+
.brand {
|
|
2774
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2775
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2776
|
+
}
|
|
2777
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2778
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2779
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2780
|
+
.vendor {
|
|
2781
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2782
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2783
|
+
}
|
|
2784
|
+
.vendor[hidden] { display: none; }
|
|
2785
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2786
|
+
.vendor img {
|
|
2787
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2788
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2789
|
+
}
|
|
2790
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2791
|
+
.vendor-sep { color: var(--faint); }
|
|
2792
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2793
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2794
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
2795
|
+
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
2796
|
+
label.field input, label.field select, .controls input[type="password"] {
|
|
2797
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
|
|
2798
|
+
padding: 7px 8px; min-height: 36px;
|
|
2799
|
+
}
|
|
2800
|
+
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2801
|
+
border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
|
|
2802
|
+
background: var(--bg-elev);
|
|
2803
|
+
}
|
|
2804
|
+
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
|
|
2805
|
+
.btn-quiet { background: transparent; }
|
|
2806
|
+
.btn-danger { color: var(--err); }
|
|
2807
|
+
/* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
|
|
2808
|
+
record or the full nav row would widen the page past the viewport. */
|
|
2809
|
+
.layout {
|
|
2810
|
+
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
2811
|
+
flex: 1 0 auto; min-width: 0;
|
|
2812
|
+
}
|
|
2813
|
+
nav.side {
|
|
2814
|
+
border-right: 1px solid var(--line); padding: 12px;
|
|
2815
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
2816
|
+
background: var(--bg-elev);
|
|
2817
|
+
}
|
|
2818
|
+
nav.side button {
|
|
2819
|
+
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2820
|
+
padding: 9px 10px; min-height: 40px; color: var(--muted);
|
|
2821
|
+
}
|
|
2822
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--ink); font-weight: 600; }
|
|
2823
|
+
main { padding: 20px; max-width: 1100px; width: 100%; min-width: 0; }
|
|
2824
|
+
.view h2 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.02em; }
|
|
2825
|
+
.lede { margin: 0 0 16px; color: var(--muted); }
|
|
2826
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
2827
|
+
.card {
|
|
2828
|
+
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2829
|
+
padding: 14px 16px; box-shadow: var(--shadow);
|
|
2830
|
+
}
|
|
2831
|
+
.card .k { color: var(--faint); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
2832
|
+
.card .v { display: flex; flex-direction: column; gap: 2px; font-size: 22px; margin-top: 4px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
2833
|
+
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2834
|
+
.split { display: grid; grid-template-columns: minmax(0, 240px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
2835
|
+
.collection {
|
|
2836
|
+
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2837
|
+
border-radius: 10px; padding: 8px 10px; min-height: 44px;
|
|
2838
|
+
}
|
|
2839
|
+
.collection[aria-current="true"] { background: var(--bg-elev); border-color: var(--line); }
|
|
2840
|
+
.collection small { display: block; color: var(--faint); }
|
|
2841
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2842
|
+
.chip {
|
|
2843
|
+
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2844
|
+
border-radius: 999px; padding: 4px 8px;
|
|
2845
|
+
}
|
|
2846
|
+
.chip i { color: var(--faint); font-style: normal; }
|
|
2847
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
2848
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
2849
|
+
th { color: var(--faint); font-weight: 600; font-size: 12px; }
|
|
2850
|
+
tr.clickable { cursor: pointer; }
|
|
2851
|
+
tr.clickable:hover { background: var(--bg-muted); }
|
|
2852
|
+
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2853
|
+
pre {
|
|
2854
|
+
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2855
|
+
background: var(--bg-inset); border-radius: 10px; padding: 12px; font-size: 12px;
|
|
2856
|
+
}
|
|
2857
|
+
.row-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
|
|
2858
|
+
.banner {
|
|
2859
|
+
display: none; margin-bottom: 12px; padding: 10px 12px; border-radius: 10px;
|
|
2860
|
+
background: var(--err-soft); color: var(--err);
|
|
2861
|
+
}
|
|
2862
|
+
.banner.show { display: block; }
|
|
2863
|
+
.empty { color: var(--muted); padding: 24px 8px; }
|
|
2864
|
+
dialog {
|
|
2865
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2866
|
+
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
|
|
2867
|
+
}
|
|
2868
|
+
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
2869
|
+
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
2870
|
+
textarea {
|
|
2871
|
+
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2872
|
+
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2873
|
+
font-family: var(--mono); font-size: 12px;
|
|
2874
|
+
}
|
|
2875
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2876
|
+
.badge { font-size: 11px; border-radius: 999px; padding: 2px 7px; background: var(--bg-muted); color: var(--muted); }
|
|
2877
|
+
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2878
|
+
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2879
|
+
.status-2 { color: var(--ok); }
|
|
2880
|
+
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2881
|
+
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
2882
|
+
.route {
|
|
2883
|
+
display: grid; grid-template-columns: 88px 1fr auto; gap: 8px; align-items: center;
|
|
2884
|
+
padding: 8px 0; border-bottom: 1px solid var(--line);
|
|
2885
|
+
}
|
|
2886
|
+
.method { font-family: var(--mono); font-size: 12px; font-weight: 700; }
|
|
2887
|
+
@media (max-width: 800px) {
|
|
2888
|
+
header.top {
|
|
2889
|
+
align-items: stretch; flex-direction: column;
|
|
2890
|
+
padding-top: max(10px, env(safe-area-inset-top));
|
|
2891
|
+
}
|
|
2892
|
+
input, select, textarea { font-size: 16px; }
|
|
2893
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
2894
|
+
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2895
|
+
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
2896
|
+
nav.side {
|
|
2897
|
+
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2898
|
+
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2899
|
+
padding: 8px;
|
|
2900
|
+
}
|
|
2901
|
+
nav.side button { white-space: nowrap; flex: none; }
|
|
2902
|
+
.split { grid-template-columns: minmax(0, 1fr); }
|
|
2903
|
+
main { padding: 14px; }
|
|
2904
|
+
table, thead, tbody, tr, th, td { display: block; }
|
|
2905
|
+
thead { display: none; }
|
|
2906
|
+
tr {
|
|
2907
|
+
border: 1px solid var(--line); border-radius: 12px; margin-bottom: 8px; padding: 6px;
|
|
2908
|
+
background: var(--bg-elev); min-width: 0;
|
|
2909
|
+
}
|
|
2910
|
+
td {
|
|
2911
|
+
border: 0; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px;
|
|
2912
|
+
min-width: 0; overflow-wrap: anywhere;
|
|
2913
|
+
}
|
|
2914
|
+
td::before { content: attr(data-label); color: var(--faint); font-size: 12px; flex: none; }
|
|
2915
|
+
.route { grid-template-columns: 72px 1fr; }
|
|
2916
|
+
.route button { grid-column: 1 / -1; }
|
|
2917
|
+
}
|
|
2918
|
+
</style>
|
|
2919
|
+
</head>
|
|
2920
|
+
<body>
|
|
2921
|
+
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2922
|
+
<header class="top">
|
|
2923
|
+
<div class="brand">
|
|
2924
|
+
<div class="brand-id">
|
|
2925
|
+
<strong>${name}</strong>
|
|
2926
|
+
<span>Admin</span>
|
|
2927
|
+
</div>
|
|
2928
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
2929
|
+
</div>
|
|
2930
|
+
<div class="controls">
|
|
2931
|
+
<label class="field">Namespace
|
|
2932
|
+
<select id="namespace" aria-label="Namespace"></select>
|
|
2933
|
+
</label>
|
|
2934
|
+
<label class="field">Admin key
|
|
2935
|
+
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2936
|
+
</label>
|
|
2937
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Toggle color theme">Theme</button>
|
|
2938
|
+
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
2939
|
+
</div>
|
|
2940
|
+
</header>
|
|
2941
|
+
<div class="layout">
|
|
2942
|
+
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2943
|
+
<button type="button" data-view="overview" aria-current="true">Overview</button>
|
|
2944
|
+
<button type="button" data-view="state">State</button>
|
|
2945
|
+
<button type="button" data-view="clock">Clock</button>
|
|
2946
|
+
<button type="button" data-view="faults">Faults</button>
|
|
2947
|
+
<button type="button" data-view="journal">Journal</button>
|
|
2948
|
+
<button type="button" data-view="routes">Routes</button>
|
|
2949
|
+
</nav>
|
|
2950
|
+
<main id="views">
|
|
2951
|
+
<p class="banner" id="banner" role="alert"></p>
|
|
2952
|
+
<section class="view" id="view-overview">
|
|
2953
|
+
<h2>Overview</h2>
|
|
2954
|
+
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
2955
|
+
<div class="grid" id="overview-cards"></div>
|
|
2956
|
+
<h3>Recent requests</h3>
|
|
2957
|
+
<div id="overview-requests"></div>
|
|
2958
|
+
</section>
|
|
2959
|
+
<section class="view" id="view-state" hidden>
|
|
2960
|
+
<h2>State</h2>
|
|
2961
|
+
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
2962
|
+
<div class="split">
|
|
2963
|
+
<div id="collections"></div>
|
|
2964
|
+
<div>
|
|
2965
|
+
<div id="schema"></div>
|
|
2966
|
+
<div class="row-actions">
|
|
2967
|
+
<button class="btn-primary" id="new-record" type="button">New record</button>
|
|
2968
|
+
</div>
|
|
2969
|
+
<div id="records"></div>
|
|
2970
|
+
</div>
|
|
2971
|
+
</div>
|
|
2972
|
+
</section>
|
|
2973
|
+
<section class="view" id="view-clock" hidden>
|
|
2974
|
+
<h2>Clock</h2>
|
|
2975
|
+
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
2976
|
+
<div class="card" id="clock-readout"></div>
|
|
2977
|
+
<div class="row-actions">
|
|
2978
|
+
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
2979
|
+
<button class="btn" type="button" data-advance="900000">+15m</button>
|
|
2980
|
+
<button class="btn" type="button" data-advance="3600000">+1h</button>
|
|
2981
|
+
<button class="btn" type="button" data-advance="86400000">+1d</button>
|
|
2982
|
+
<button class="btn" type="button" data-advance="-3600000">\u22121h</button>
|
|
2983
|
+
<button class="btn" id="freeze" type="button">Freeze</button>
|
|
2984
|
+
<button class="btn-quiet" id="clock-reset" type="button">Reset</button>
|
|
2985
|
+
</div>
|
|
2986
|
+
<form id="clock-set" class="row-actions">
|
|
2987
|
+
<label class="field">Set instant
|
|
2988
|
+
<input id="clock-instant" type="text" placeholder="2026-01-01T00:00:00Z or epoch ms" size="32">
|
|
2989
|
+
</label>
|
|
2990
|
+
<button class="btn-primary" type="submit">Set</button>
|
|
2991
|
+
</form>
|
|
2992
|
+
</section>
|
|
2993
|
+
<section class="view" id="view-faults" hidden>
|
|
2994
|
+
<h2>Faults</h2>
|
|
2995
|
+
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
2996
|
+
<form id="fault-form" class="card stack">
|
|
2997
|
+
<label class="field">Preset
|
|
2998
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
2999
|
+
</label>
|
|
3000
|
+
<label class="field">Status
|
|
3001
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3002
|
+
</label>
|
|
3003
|
+
<label class="field">Path prefix
|
|
3004
|
+
<input id="fault-path" placeholder="/v1">
|
|
3005
|
+
</label>
|
|
3006
|
+
<label class="field">Method
|
|
3007
|
+
<input id="fault-method" placeholder="GET">
|
|
3008
|
+
</label>
|
|
3009
|
+
<div class="dialog-actions">
|
|
3010
|
+
<button class="btn-primary" type="submit">Add fault</button>
|
|
3011
|
+
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
3012
|
+
</div>
|
|
3013
|
+
</form>
|
|
3014
|
+
<div id="fault-list"></div>
|
|
3015
|
+
</section>
|
|
3016
|
+
<section class="view" id="view-journal" hidden>
|
|
3017
|
+
<h2>Journal</h2>
|
|
3018
|
+
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3019
|
+
<div class="row-actions">
|
|
3020
|
+
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3021
|
+
</div>
|
|
3022
|
+
<div id="journal"></div>
|
|
3023
|
+
</section>
|
|
3024
|
+
<section class="view" id="view-routes" hidden>
|
|
3025
|
+
<h2>Routes</h2>
|
|
3026
|
+
<p class="lede">Shared controls, plus any routes this mock adds. Extra routes are how a bespoke workflow shows up beside the default UI.</p>
|
|
3027
|
+
<div id="route-list"></div>
|
|
3028
|
+
<form id="route-form" class="card stack">
|
|
3029
|
+
<strong id="route-title">Select a route</strong>
|
|
3030
|
+
<label class="field">JSON body
|
|
3031
|
+
<textarea id="route-body" placeholder="{ }"></textarea>
|
|
3032
|
+
</label>
|
|
3033
|
+
<div class="dialog-actions">
|
|
3034
|
+
<button class="btn-primary" type="submit">Send</button>
|
|
3035
|
+
</div>
|
|
3036
|
+
<pre id="route-result">Response will show here.</pre>
|
|
3037
|
+
</form>
|
|
3038
|
+
</section>
|
|
3039
|
+
</main>
|
|
3040
|
+
</div>
|
|
3041
|
+
</div>
|
|
3042
|
+
<dialog id="editor">
|
|
3043
|
+
<form method="dialog" id="editor-form">
|
|
3044
|
+
<strong id="editor-title">Record</strong>
|
|
3045
|
+
<label class="field">Id
|
|
3046
|
+
<input id="editor-id" required>
|
|
3047
|
+
</label>
|
|
3048
|
+
<label class="field">JSON value
|
|
3049
|
+
<textarea id="editor-value"></textarea>
|
|
3050
|
+
</label>
|
|
3051
|
+
<div class="dialog-actions">
|
|
3052
|
+
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
3053
|
+
<button class="btn-quiet" type="submit" value="cancel">Cancel</button>
|
|
3054
|
+
<button class="btn-primary" type="submit" value="save">Save</button>
|
|
3055
|
+
</div>
|
|
3056
|
+
</form>
|
|
3057
|
+
</dialog>
|
|
3058
|
+
${script}
|
|
3059
|
+
</body>
|
|
3060
|
+
</html>`;
|
|
3061
|
+
};
|
|
3062
|
+
var htmlResponse = (body) => new Response(body, {
|
|
3063
|
+
status: 200,
|
|
3064
|
+
headers: {
|
|
3065
|
+
"content-type": "text/html; charset=utf-8",
|
|
3066
|
+
"cache-control": "no-store"
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
var json2 = (body) => new Response(JSON.stringify(body), {
|
|
3070
|
+
status: 200,
|
|
3071
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
3072
|
+
});
|
|
3073
|
+
var adminUiRoutes = (service, ui) => {
|
|
3074
|
+
const shell = () => renderAdminDocument(service);
|
|
3075
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3076
|
+
return {
|
|
3077
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
3078
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
3079
|
+
"GET /ui/manifest": () => json2({
|
|
3080
|
+
service,
|
|
3081
|
+
panels: (ui?.panels ?? []).map((panel) => ({
|
|
3082
|
+
id: panel.id,
|
|
3083
|
+
title: panel.title,
|
|
3084
|
+
...panel.description !== void 0 ? { description: panel.description } : {},
|
|
3085
|
+
html: panel.html,
|
|
3086
|
+
...panel.script !== void 0 ? { script: panel.script } : {}
|
|
3087
|
+
}))
|
|
3088
|
+
})
|
|
3089
|
+
};
|
|
3090
|
+
};
|
|
3091
|
+
|
|
1359
3092
|
// ../../openapi/metadata/dist/types.js
|
|
1360
3093
|
var EXTENSION_KEYS = {
|
|
1361
3094
|
operation: "x-mockingbird",
|
|
@@ -1368,11 +3101,11 @@ var EXTENSION_KEYS = {
|
|
|
1368
3101
|
};
|
|
1369
3102
|
|
|
1370
3103
|
// ../../openapi/metadata/dist/read.js
|
|
1371
|
-
var
|
|
3104
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1372
3105
|
var extensionOf = (holder, key) => holder[key];
|
|
1373
3106
|
var operationMetadata = (operation) => {
|
|
1374
3107
|
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
1375
|
-
const ext =
|
|
3108
|
+
const ext = isRecord3(raw) ? raw : {};
|
|
1376
3109
|
const supported = ext.supported ?? true;
|
|
1377
3110
|
const parity = ext.parity ?? {};
|
|
1378
3111
|
return {
|
|
@@ -1482,9 +3215,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
1482
3215
|
this.name = "OperationRegistryError";
|
|
1483
3216
|
}
|
|
1484
3217
|
};
|
|
1485
|
-
var verifyOperations = (
|
|
3218
|
+
var verifyOperations = (document3, handlers) => {
|
|
1486
3219
|
const problems = [];
|
|
1487
|
-
const operations = listOperations(
|
|
3220
|
+
const operations = listOperations(document3);
|
|
1488
3221
|
const seen = /* @__PURE__ */ new Set();
|
|
1489
3222
|
for (const operation of operations) {
|
|
1490
3223
|
if (seen.has(operation.operationId))
|
|
@@ -1593,8 +3326,442 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1593
3326
|
});
|
|
1594
3327
|
};
|
|
1595
3328
|
|
|
3329
|
+
// ../core/dist/state-view.js
|
|
3330
|
+
var COLLECTION_NAME = /^[A-Za-z0-9_.@:-]{1,200}$/;
|
|
3331
|
+
var RECORD_ID = /^[^\s/]{1,256}$/;
|
|
3332
|
+
var SAMPLE = 25;
|
|
3333
|
+
var WALK_LIMIT = 1e4;
|
|
3334
|
+
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
3335
|
+
var fail2 = (status, message) => ({ ok: false, status, message });
|
|
3336
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3337
|
+
var kindOf = (value) => {
|
|
3338
|
+
if (value === null)
|
|
3339
|
+
return "null";
|
|
3340
|
+
if (Array.isArray(value))
|
|
3341
|
+
return "array";
|
|
3342
|
+
switch (typeof value) {
|
|
3343
|
+
case "string":
|
|
3344
|
+
return "string";
|
|
3345
|
+
case "number":
|
|
3346
|
+
return Number.isFinite(value) ? "number" : "unknown";
|
|
3347
|
+
case "boolean":
|
|
3348
|
+
return "boolean";
|
|
3349
|
+
case "object":
|
|
3350
|
+
return "object";
|
|
3351
|
+
default:
|
|
3352
|
+
return "unknown";
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
var humanize = (name) => {
|
|
3356
|
+
const words = name.replace(/[@:_-]+/g, " ").trim();
|
|
3357
|
+
if (words === "")
|
|
3358
|
+
return name;
|
|
3359
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
3360
|
+
};
|
|
3361
|
+
var jsonValue = (value) => {
|
|
3362
|
+
if (value === void 0)
|
|
3363
|
+
return fail2(400, "value is required");
|
|
3364
|
+
try {
|
|
3365
|
+
return { ok: true, value: JSON.parse(JSON.stringify(value)) };
|
|
3366
|
+
} catch {
|
|
3367
|
+
return fail2(400, "value must be JSON");
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
var ctorName = (value) => {
|
|
3371
|
+
const ctor = value.constructor;
|
|
3372
|
+
return typeof ctor?.name === "string" ? ctor.name : "";
|
|
3373
|
+
};
|
|
3374
|
+
var isSqlite = (value) => "prepare" in value && typeof value.prepare === "function" && "transaction" in value;
|
|
3375
|
+
var discoverCollections = (root) => {
|
|
3376
|
+
const found = [];
|
|
3377
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3378
|
+
const visit = (value, depth) => {
|
|
3379
|
+
if (found.length + seen.size > WALK_LIMIT)
|
|
3380
|
+
return;
|
|
3381
|
+
if (value === null || typeof value !== "object")
|
|
3382
|
+
return;
|
|
3383
|
+
if (seen.has(value))
|
|
3384
|
+
return;
|
|
3385
|
+
seen.add(value);
|
|
3386
|
+
if (value instanceof Collection) {
|
|
3387
|
+
found.push(value);
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
if (value instanceof Map) {
|
|
3391
|
+
for (const child of value.values())
|
|
3392
|
+
visit(child, depth + 1);
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
if (value instanceof Set) {
|
|
3396
|
+
for (const child of value.values())
|
|
3397
|
+
visit(child, depth + 1);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (depth > 6 || isSqlite(value) || SKIP_CTORS.has(ctorName(value)))
|
|
3401
|
+
return;
|
|
3402
|
+
if (Array.isArray(value)) {
|
|
3403
|
+
const cap = Math.min(value.length, 32);
|
|
3404
|
+
for (let i = 0; i < cap; i++)
|
|
3405
|
+
visit(value[i], depth + 1);
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
let children;
|
|
3409
|
+
try {
|
|
3410
|
+
children = Object.values(value);
|
|
3411
|
+
} catch {
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
for (const child of children)
|
|
3415
|
+
visit(child, depth + 1);
|
|
3416
|
+
};
|
|
3417
|
+
visit(root, 0);
|
|
3418
|
+
return found;
|
|
3419
|
+
};
|
|
3420
|
+
var storedNames = (sqlite, namespace) => {
|
|
3421
|
+
const records = sqlite.prepare("SELECT DISTINCT collection AS name FROM mockingbird_records WHERE namespace = ?").all(namespace);
|
|
3422
|
+
const sequences = sqlite.prepare("SELECT DISTINCT name FROM mockingbird_sequences WHERE namespace = ? AND kind = 'collection'").all(namespace);
|
|
3423
|
+
return [...records, ...sequences].map((row) => row.name);
|
|
3424
|
+
};
|
|
3425
|
+
var blankAccum = (name) => ({
|
|
3426
|
+
name,
|
|
3427
|
+
kinds: /* @__PURE__ */ new Set(),
|
|
3428
|
+
present: 0,
|
|
3429
|
+
nested: /* @__PURE__ */ new Map()
|
|
3430
|
+
});
|
|
3431
|
+
var absorb = (accum, value) => {
|
|
3432
|
+
const kind = kindOf(value);
|
|
3433
|
+
accum.kinds.add(kind);
|
|
3434
|
+
accum.present++;
|
|
3435
|
+
if (!isRecord4(value))
|
|
3436
|
+
return;
|
|
3437
|
+
for (const [key, child] of Object.entries(value)) {
|
|
3438
|
+
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
3439
|
+
absorb(nested, child);
|
|
3440
|
+
accum.nested.set(key, nested);
|
|
3441
|
+
}
|
|
3442
|
+
};
|
|
3443
|
+
var finishField = (accum, samples) => {
|
|
3444
|
+
const kinds = [...accum.kinds];
|
|
3445
|
+
const kind = kinds.length === 1 ? kinds[0] : "unknown";
|
|
3446
|
+
const field = {
|
|
3447
|
+
name: accum.name,
|
|
3448
|
+
kind,
|
|
3449
|
+
optional: accum.present < samples || kinds.includes("null")
|
|
3450
|
+
};
|
|
3451
|
+
if (accum.nested.size > 0) {
|
|
3452
|
+
field.fields = [...accum.nested.values()].sort((a, b) => a.name.localeCompare(b.name)).map((child) => finishField(child, accum.present));
|
|
3453
|
+
}
|
|
3454
|
+
return field;
|
|
3455
|
+
};
|
|
3456
|
+
var inferFields = (records) => {
|
|
3457
|
+
if (records.length === 0)
|
|
3458
|
+
return [];
|
|
3459
|
+
const sample = records.slice(0, SAMPLE);
|
|
3460
|
+
const objects = sample.filter((record) => isRecord4(record.value));
|
|
3461
|
+
if (objects.length === 0) {
|
|
3462
|
+
const kinds = new Set(sample.map((record) => kindOf(record.value)));
|
|
3463
|
+
const list = [...kinds];
|
|
3464
|
+
return [
|
|
3465
|
+
{
|
|
3466
|
+
name: "(value)",
|
|
3467
|
+
kind: list.length === 1 ? list[0] : "unknown",
|
|
3468
|
+
optional: false
|
|
3469
|
+
}
|
|
3470
|
+
];
|
|
3471
|
+
}
|
|
3472
|
+
const root = /* @__PURE__ */ new Map();
|
|
3473
|
+
for (const record of objects) {
|
|
3474
|
+
for (const [key, value] of Object.entries(record.value)) {
|
|
3475
|
+
const accum = root.get(key) ?? blankAccum(key);
|
|
3476
|
+
absorb(accum, value);
|
|
3477
|
+
root.set(key, accum);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
return [...root.values()].sort((a, b) => a.name.localeCompare(b.name)).map((accum) => finishField(accum, objects.length));
|
|
3481
|
+
};
|
|
3482
|
+
var mergeFields = (declared, inferred) => {
|
|
3483
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3484
|
+
for (const field of inferred)
|
|
3485
|
+
byName.set(field.name, field);
|
|
3486
|
+
const ordered = [];
|
|
3487
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3488
|
+
for (const field of declared ?? []) {
|
|
3489
|
+
const found = byName.get(field.name);
|
|
3490
|
+
seen.add(field.name);
|
|
3491
|
+
if (!found) {
|
|
3492
|
+
ordered.push({ ...field, optional: field.optional });
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3495
|
+
ordered.push({
|
|
3496
|
+
...found,
|
|
3497
|
+
kind: field.kind === "unknown" ? found.kind : field.kind,
|
|
3498
|
+
optional: field.optional || found.optional,
|
|
3499
|
+
...field.description !== void 0 ? { description: field.description } : {},
|
|
3500
|
+
...field.meta !== void 0 ? { meta: field.meta } : {},
|
|
3501
|
+
...field.fields !== void 0 ? { fields: mergeFields(field.fields, found.fields ?? []) } : {}
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
for (const field of inferred) {
|
|
3505
|
+
if (!seen.has(field.name))
|
|
3506
|
+
ordered.push(field);
|
|
3507
|
+
}
|
|
3508
|
+
return ordered;
|
|
3509
|
+
};
|
|
3510
|
+
var collectionOf = (sqlite, namespace, name) => new Collection(sqlite, namespace, name);
|
|
3511
|
+
var readRecord = (collection, id) => {
|
|
3512
|
+
const rows = collection.list({ where: (_value, _seq) => true, order: "oldest" });
|
|
3513
|
+
const row = rows.find((entry) => entry.id === id);
|
|
3514
|
+
return row ? { id: row.id, seq: row.seq, value: row.value } : void 0;
|
|
3515
|
+
};
|
|
3516
|
+
var inspectState = (scope) => {
|
|
3517
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3518
|
+
for (const declaration of scope.declarations ?? []) {
|
|
3519
|
+
if (COLLECTION_NAME.test(declaration.name))
|
|
3520
|
+
declared.set(declaration.name, declaration);
|
|
3521
|
+
}
|
|
3522
|
+
const names = new Set(declared.keys());
|
|
3523
|
+
for (const name of storedNames(scope.sqlite, scope.storageNamespace))
|
|
3524
|
+
names.add(name);
|
|
3525
|
+
for (const collection of discoverCollections(scope.root)) {
|
|
3526
|
+
if (collection.namespace === scope.storageNamespace)
|
|
3527
|
+
names.add(collection.collectionName);
|
|
3528
|
+
}
|
|
3529
|
+
const collections = [];
|
|
3530
|
+
for (const name of names) {
|
|
3531
|
+
if (!COLLECTION_NAME.test(name))
|
|
3532
|
+
continue;
|
|
3533
|
+
const declaration = declared.get(name);
|
|
3534
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3535
|
+
const records = collection.list({ order: "newest" }).slice(0, SAMPLE).map((row) => ({ id: row.id, seq: row.seq, value: row.value }));
|
|
3536
|
+
const inferred = inferFields(records);
|
|
3537
|
+
const fields = mergeFields(declaration?.fields, inferred);
|
|
3538
|
+
const source = declaration && records.length > 0 ? "mixed" : declaration ? "declared" : "inferred";
|
|
3539
|
+
collections.push({
|
|
3540
|
+
name,
|
|
3541
|
+
label: declaration?.label ?? humanize(name),
|
|
3542
|
+
...declaration?.description !== void 0 ? { description: declaration.description } : {},
|
|
3543
|
+
count: collection.count(),
|
|
3544
|
+
source,
|
|
3545
|
+
fields,
|
|
3546
|
+
...declaration?.meta !== void 0 ? { meta: declaration.meta } : {}
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
const order = /* @__PURE__ */ new Map();
|
|
3550
|
+
for (const [index, item] of (scope.declarations ?? []).entries())
|
|
3551
|
+
order.set(item.name, index);
|
|
3552
|
+
collections.sort((a, b) => {
|
|
3553
|
+
const left = order.get(a.name);
|
|
3554
|
+
const right = order.get(b.name);
|
|
3555
|
+
if (left !== void 0 && right !== void 0)
|
|
3556
|
+
return left - right;
|
|
3557
|
+
if (left !== void 0)
|
|
3558
|
+
return -1;
|
|
3559
|
+
if (right !== void 0)
|
|
3560
|
+
return 1;
|
|
3561
|
+
return a.name.localeCompare(b.name);
|
|
3562
|
+
});
|
|
3563
|
+
return {
|
|
3564
|
+
namespace: scope.namespace,
|
|
3565
|
+
storageNamespace: scope.storageNamespace,
|
|
3566
|
+
collections
|
|
3567
|
+
};
|
|
3568
|
+
};
|
|
3569
|
+
var pageOf = (sqlite, storageNamespace, name, query) => {
|
|
3570
|
+
if (!COLLECTION_NAME.test(name))
|
|
3571
|
+
return fail2(400, "collection name is not allowed");
|
|
3572
|
+
const limit = query.limit ?? 50;
|
|
3573
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
3574
|
+
return fail2(400, "limit must be an integer from 1 to 200");
|
|
3575
|
+
}
|
|
3576
|
+
const order = query.order ?? "newest";
|
|
3577
|
+
if (order !== "newest" && order !== "oldest")
|
|
3578
|
+
return fail2(400, 'order must be "newest" or "oldest"');
|
|
3579
|
+
const collection = collectionOf(sqlite, storageNamespace, name);
|
|
3580
|
+
const rows = collection.list({ order }).filter((row) => {
|
|
3581
|
+
if (query.after === void 0)
|
|
3582
|
+
return true;
|
|
3583
|
+
return order === "newest" ? row.seq < query.after : row.seq > query.after;
|
|
3584
|
+
});
|
|
3585
|
+
const slice = rows.slice(0, limit);
|
|
3586
|
+
const last = slice.at(-1);
|
|
3587
|
+
return {
|
|
3588
|
+
ok: true,
|
|
3589
|
+
value: {
|
|
3590
|
+
collection: name,
|
|
3591
|
+
count: collection.count(),
|
|
3592
|
+
records: slice.map((row) => ({ id: row.id, seq: row.seq, value: row.value })),
|
|
3593
|
+
next: rows.length > limit && last ? last.seq : null
|
|
3594
|
+
}
|
|
3595
|
+
};
|
|
3596
|
+
};
|
|
3597
|
+
var readStatePage = (scope, name, query = {}) => pageOf(scope.sqlite, scope.storageNamespace, name, query);
|
|
3598
|
+
var readStateRecord = (scope, name, id) => {
|
|
3599
|
+
if (!COLLECTION_NAME.test(name))
|
|
3600
|
+
return fail2(400, "collection name is not allowed");
|
|
3601
|
+
if (!RECORD_ID.test(id))
|
|
3602
|
+
return fail2(400, "record id is not allowed");
|
|
3603
|
+
const record = readRecord(collectionOf(scope.sqlite, scope.storageNamespace, name), id);
|
|
3604
|
+
return record ? { ok: true, value: record } : fail2(404, `no record ${id} in ${name}`);
|
|
3605
|
+
};
|
|
3606
|
+
var requireId = (id) => RECORD_ID.test(id) ? { ok: true, value: id } : fail2(400, "id must be a single path segment");
|
|
3607
|
+
var createStateRecord = (scope, name, input) => {
|
|
3608
|
+
if (!COLLECTION_NAME.test(name))
|
|
3609
|
+
return fail2(400, "collection name is not allowed");
|
|
3610
|
+
const parsed = jsonValue(input.value);
|
|
3611
|
+
if (!parsed.ok)
|
|
3612
|
+
return parsed;
|
|
3613
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3614
|
+
const id = input.id ?? `${name}_${collection.nextSequence()}`;
|
|
3615
|
+
const checked = requireId(id);
|
|
3616
|
+
if (!checked.ok)
|
|
3617
|
+
return checked;
|
|
3618
|
+
if (collection.has(checked.value))
|
|
3619
|
+
return fail2(409, `record ${checked.value} already exists`);
|
|
3620
|
+
const stored = collection.insert(checked.value, parsed.value);
|
|
3621
|
+
return { ok: true, value: { id: checked.value, seq: stored.seq, value: stored.value } };
|
|
3622
|
+
};
|
|
3623
|
+
var replaceStateRecord = (scope, name, id, value) => {
|
|
3624
|
+
if (!COLLECTION_NAME.test(name))
|
|
3625
|
+
return fail2(400, "collection name is not allowed");
|
|
3626
|
+
const checked = requireId(id);
|
|
3627
|
+
if (!checked.ok)
|
|
3628
|
+
return checked;
|
|
3629
|
+
const parsed = jsonValue(value);
|
|
3630
|
+
if (!parsed.ok)
|
|
3631
|
+
return parsed;
|
|
3632
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3633
|
+
if (!collection.has(checked.value))
|
|
3634
|
+
return fail2(404, `no record ${checked.value} in ${name}`);
|
|
3635
|
+
const stored = collection.update(checked.value, parsed.value);
|
|
3636
|
+
if (!stored)
|
|
3637
|
+
return fail2(404, `no record ${checked.value} in ${name}`);
|
|
3638
|
+
return { ok: true, value: { id: checked.value, seq: stored.seq, value: stored.value } };
|
|
3639
|
+
};
|
|
3640
|
+
var patchStateRecord = (scope, name, id, value) => {
|
|
3641
|
+
const current = readStateRecord(scope, name, id);
|
|
3642
|
+
if (!current.ok)
|
|
3643
|
+
return current;
|
|
3644
|
+
const parsed = jsonValue(value);
|
|
3645
|
+
if (!parsed.ok)
|
|
3646
|
+
return parsed;
|
|
3647
|
+
const next = isRecord4(current.value.value) && isRecord4(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
3648
|
+
return replaceStateRecord(scope, name, id, next);
|
|
3649
|
+
};
|
|
3650
|
+
var deleteStateRecord = (scope, name, id) => {
|
|
3651
|
+
if (!COLLECTION_NAME.test(name))
|
|
3652
|
+
return fail2(400, "collection name is not allowed");
|
|
3653
|
+
const checked = requireId(id);
|
|
3654
|
+
if (!checked.ok)
|
|
3655
|
+
return checked;
|
|
3656
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3657
|
+
return collection.delete(checked.value) ? { ok: true, value: { id: checked.value } } : fail2(404, `no record ${checked.value} in ${name}`);
|
|
3658
|
+
};
|
|
3659
|
+
var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
3660
|
+
status,
|
|
3661
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
3662
|
+
});
|
|
3663
|
+
var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
3664
|
+
var fromResult = (result, status) => result.ok ? json3(status, result.value) : adminError2(result.status, result.message);
|
|
3665
|
+
var integerParam = (value) => {
|
|
3666
|
+
if (value === null)
|
|
3667
|
+
return void 0;
|
|
3668
|
+
if (!/^-?\d+$/.test(value))
|
|
3669
|
+
return "bad";
|
|
3670
|
+
return Number(value);
|
|
3671
|
+
};
|
|
3672
|
+
var stateAdminRoutes = (routes) => {
|
|
3673
|
+
const open = (namespace) => {
|
|
3674
|
+
try {
|
|
3675
|
+
return { ok: true, value: routes.open(namespace) };
|
|
3676
|
+
} catch (error) {
|
|
3677
|
+
return fail2(400, error instanceof Error ? error.message : String(error));
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
const wrote = (namespace) => {
|
|
3681
|
+
routes.afterWrite?.(namespace);
|
|
3682
|
+
};
|
|
3683
|
+
return {
|
|
3684
|
+
"GET /state": ({ namespace }) => {
|
|
3685
|
+
const scope = open(namespace);
|
|
3686
|
+
return fromResult(scope.ok ? { ok: true, value: inspectState(scope.value) } : scope, 200);
|
|
3687
|
+
},
|
|
3688
|
+
"GET /state/:collection": ({ namespace, params, url }) => {
|
|
3689
|
+
const scope = open(namespace);
|
|
3690
|
+
if (!scope.ok)
|
|
3691
|
+
return fromResult(scope, 200);
|
|
3692
|
+
const limit = integerParam(url.searchParams.get("limit"));
|
|
3693
|
+
const after = integerParam(url.searchParams.get("after"));
|
|
3694
|
+
if (limit === "bad")
|
|
3695
|
+
return adminError2(400, "limit: expected an integer");
|
|
3696
|
+
if (after === "bad")
|
|
3697
|
+
return adminError2(400, "after: expected a sequence number");
|
|
3698
|
+
const order = url.searchParams.get("order");
|
|
3699
|
+
return fromResult(readStatePage(scope.value, params.collection ?? "", {
|
|
3700
|
+
...limit !== void 0 ? { limit } : {},
|
|
3701
|
+
...after !== void 0 ? { after } : {},
|
|
3702
|
+
...order === "newest" || order === "oldest" ? { order } : {}
|
|
3703
|
+
}), 200);
|
|
3704
|
+
},
|
|
3705
|
+
"GET /state/:collection/:id": ({ namespace, params }) => {
|
|
3706
|
+
const scope = open(namespace);
|
|
3707
|
+
if (!scope.ok)
|
|
3708
|
+
return fromResult(scope, 200);
|
|
3709
|
+
return fromResult(readStateRecord(scope.value, params.collection ?? "", params.id ?? ""), 200);
|
|
3710
|
+
},
|
|
3711
|
+
"POST /state/:collection": ({ namespace, params, body }) => {
|
|
3712
|
+
const scope = open(namespace);
|
|
3713
|
+
if (!scope.ok)
|
|
3714
|
+
return fromResult(scope, 201);
|
|
3715
|
+
if (!isRecord4(body) || !("value" in body)) {
|
|
3716
|
+
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
3717
|
+
}
|
|
3718
|
+
const id = body.id;
|
|
3719
|
+
if (id !== void 0 && typeof id !== "string")
|
|
3720
|
+
return adminError2(400, "id must be a string");
|
|
3721
|
+
const created = createStateRecord(scope.value, params.collection ?? "", {
|
|
3722
|
+
...typeof id === "string" ? { id } : {},
|
|
3723
|
+
value: body.value
|
|
3724
|
+
});
|
|
3725
|
+
if (created.ok)
|
|
3726
|
+
wrote(namespace);
|
|
3727
|
+
return fromResult(created, 201);
|
|
3728
|
+
},
|
|
3729
|
+
"PUT /state/:collection/:id": ({ namespace, params, body }) => {
|
|
3730
|
+
const scope = open(namespace);
|
|
3731
|
+
if (!scope.ok)
|
|
3732
|
+
return fromResult(scope, 200);
|
|
3733
|
+
if (!isRecord4(body) || !("value" in body))
|
|
3734
|
+
return adminError2(400, 'expected {"value": <json>}');
|
|
3735
|
+
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
3736
|
+
if (replaced.ok)
|
|
3737
|
+
wrote(namespace);
|
|
3738
|
+
return fromResult(replaced, 200);
|
|
3739
|
+
},
|
|
3740
|
+
"PATCH /state/:collection/:id": ({ namespace, params, body }) => {
|
|
3741
|
+
const scope = open(namespace);
|
|
3742
|
+
if (!scope.ok)
|
|
3743
|
+
return fromResult(scope, 200);
|
|
3744
|
+
if (!isRecord4(body) || !("value" in body))
|
|
3745
|
+
return adminError2(400, 'expected {"value": <json>}');
|
|
3746
|
+
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
3747
|
+
if (patched.ok)
|
|
3748
|
+
wrote(namespace);
|
|
3749
|
+
return fromResult(patched, 200);
|
|
3750
|
+
},
|
|
3751
|
+
"DELETE /state/:collection/:id": ({ namespace, params }) => {
|
|
3752
|
+
const scope = open(namespace);
|
|
3753
|
+
if (!scope.ok)
|
|
3754
|
+
return fromResult(scope, 200);
|
|
3755
|
+
const removed = deleteStateRecord(scope.value, params.collection ?? "", params.id ?? "");
|
|
3756
|
+
if (removed.ok)
|
|
3757
|
+
wrote(namespace);
|
|
3758
|
+
return fromResult(removed, 200);
|
|
3759
|
+
}
|
|
3760
|
+
};
|
|
3761
|
+
};
|
|
3762
|
+
|
|
1596
3763
|
// ../core/dist/version.js
|
|
1597
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3764
|
+
var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
|
|
1598
3765
|
|
|
1599
3766
|
// ../core/dist/signing.js
|
|
1600
3767
|
var encoder = new TextEncoder();
|
|
@@ -1929,11 +4096,11 @@ var createWebhookHub = (options) => {
|
|
|
1929
4096
|
};
|
|
1930
4097
|
return hub;
|
|
1931
4098
|
};
|
|
1932
|
-
var
|
|
1933
|
-
var
|
|
1934
|
-
var
|
|
4099
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
4100
|
+
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4101
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1935
4102
|
var parseEndpoint = (value) => {
|
|
1936
|
-
if (!
|
|
4103
|
+
if (!isRecord5(value) || typeof value.url !== "string")
|
|
1937
4104
|
return "each endpoint needs a url";
|
|
1938
4105
|
try {
|
|
1939
4106
|
new URL(value.url);
|
|
@@ -1950,18 +4117,18 @@ var parseEndpoint = (value) => {
|
|
|
1950
4117
|
const events = value.events ?? value.enabledEvents;
|
|
1951
4118
|
if (Array.isArray(events))
|
|
1952
4119
|
endpoint.events = events.map(String);
|
|
1953
|
-
if (
|
|
4120
|
+
if (isRecord5(value.tags)) {
|
|
1954
4121
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1955
4122
|
}
|
|
1956
4123
|
if (typeof value.account === "string")
|
|
1957
4124
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1958
|
-
if (
|
|
4125
|
+
if (isRecord5(value.headers)) {
|
|
1959
4126
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1960
4127
|
}
|
|
1961
4128
|
return endpoint;
|
|
1962
4129
|
};
|
|
1963
4130
|
var webhookAdminRoutes = (hub) => ({
|
|
1964
|
-
"GET /webhooks": ({ url, namespace }) =>
|
|
4131
|
+
"GET /webhooks": ({ url, namespace }) => json4(200, {
|
|
1965
4132
|
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1966
4133
|
const type = url.searchParams.get("type");
|
|
1967
4134
|
return type === null || d.type === type;
|
|
@@ -1969,51 +4136,51 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
1969
4136
|
}),
|
|
1970
4137
|
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1971
4138
|
const type = url.searchParams.get("type");
|
|
1972
|
-
return
|
|
4139
|
+
return json4(200, {
|
|
1973
4140
|
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1974
4141
|
});
|
|
1975
4142
|
},
|
|
1976
4143
|
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1977
4144
|
const replayed = await hub.replay(params.id);
|
|
1978
|
-
return replayed ?
|
|
4145
|
+
return replayed ? json4(200, replayed) : adminError3(404, `no delivery ${params.id}`);
|
|
1979
4146
|
},
|
|
1980
4147
|
"POST /webhooks/flush": async () => {
|
|
1981
4148
|
await hub.flush();
|
|
1982
|
-
return
|
|
4149
|
+
return json4(200, { status: "ok" });
|
|
1983
4150
|
},
|
|
1984
4151
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1985
|
-
if (!
|
|
1986
|
-
return
|
|
4152
|
+
if (!isRecord5(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
4153
|
+
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
1987
4154
|
}
|
|
1988
4155
|
const fault = { mode: body.mode };
|
|
1989
4156
|
if (typeof body.count === "number")
|
|
1990
4157
|
fault.count = body.count;
|
|
1991
4158
|
hub.fault(namespace, fault);
|
|
1992
|
-
return
|
|
4159
|
+
return json4(201, { namespace, ...fault });
|
|
1993
4160
|
},
|
|
1994
|
-
"GET /webhook-endpoints": ({ namespace }) =>
|
|
4161
|
+
"GET /webhook-endpoints": ({ namespace }) => json4(200, {
|
|
1995
4162
|
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1996
4163
|
...rest,
|
|
1997
4164
|
secret: secret ? "(set)" : null
|
|
1998
4165
|
}))
|
|
1999
4166
|
}),
|
|
2000
4167
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
2001
|
-
const list = Array.isArray(body) ? body :
|
|
4168
|
+
const list = Array.isArray(body) ? body : isRecord5(body) ? body.endpoints : void 0;
|
|
2002
4169
|
if (!Array.isArray(list))
|
|
2003
|
-
return
|
|
4170
|
+
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
2004
4171
|
const parsed = [];
|
|
2005
4172
|
for (const each of list) {
|
|
2006
4173
|
const endpoint = parseEndpoint(each);
|
|
2007
4174
|
if (typeof endpoint === "string")
|
|
2008
|
-
return
|
|
4175
|
+
return adminError3(400, endpoint);
|
|
2009
4176
|
parsed.push(endpoint);
|
|
2010
4177
|
}
|
|
2011
4178
|
const set = hub.setEndpoints(namespace, parsed);
|
|
2012
|
-
return
|
|
4179
|
+
return json4(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
2013
4180
|
},
|
|
2014
4181
|
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
2015
4182
|
hub.setEndpoints(namespace, []);
|
|
2016
|
-
return
|
|
4183
|
+
return json4(200, { status: "ok" });
|
|
2017
4184
|
}
|
|
2018
4185
|
});
|
|
2019
4186
|
var parsePayload = (message) => {
|
|
@@ -2068,8 +4235,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
2068
4235
|
this.name = "TypeError";
|
|
2069
4236
|
}
|
|
2070
4237
|
};
|
|
2071
|
-
var operationMatcher = (
|
|
2072
|
-
const matchers = listOperations(
|
|
4238
|
+
var operationMatcher = (document3) => {
|
|
4239
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
2073
4240
|
operationId: operation.operationId,
|
|
2074
4241
|
method: operation.method.toUpperCase(),
|
|
2075
4242
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -2078,6 +4245,7 @@ var operationMatcher = (document2) => {
|
|
|
2078
4245
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
2079
4246
|
};
|
|
2080
4247
|
var createRuntime = (options) => {
|
|
4248
|
+
assertAdminUi(options.adminUi);
|
|
2081
4249
|
const sqlite = bootSqlite(options.sqlite);
|
|
2082
4250
|
const clock = options.clock ?? createClock();
|
|
2083
4251
|
const rng = createRng(options.seed ?? 0);
|
|
@@ -2163,10 +4331,10 @@ var createRuntime = (options) => {
|
|
|
2163
4331
|
const ensureBranch = (namespace, branch2, at) => {
|
|
2164
4332
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
2165
4333
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
2166
|
-
const
|
|
4334
|
+
const history2 = timeline(namespace);
|
|
2167
4335
|
if (branch2 === "main") {
|
|
2168
4336
|
if (at !== void 0) {
|
|
2169
|
-
const point =
|
|
4337
|
+
const point = history2.checkout("main", at);
|
|
2170
4338
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
2171
4339
|
captured.set(namespace, point.value.snapshot);
|
|
2172
4340
|
rng.setState(point.value.rngState);
|
|
@@ -2179,10 +4347,10 @@ var createRuntime = (options) => {
|
|
|
2179
4347
|
return namespace;
|
|
2180
4348
|
}
|
|
2181
4349
|
const storage = physicalBranch(namespace, branch2);
|
|
2182
|
-
if (!
|
|
4350
|
+
if (!history2.hasBranch(branch2)) {
|
|
2183
4351
|
if (at === void 0)
|
|
2184
|
-
|
|
2185
|
-
const point =
|
|
4352
|
+
history2.commit(capture(namespace));
|
|
4353
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
2186
4354
|
const branchRng = createRng(options.seed ?? 0);
|
|
2187
4355
|
if (point)
|
|
2188
4356
|
branchRng.setState(point.value.rngState);
|
|
@@ -2191,8 +4359,8 @@ var createRuntime = (options) => {
|
|
|
2191
4359
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2192
4360
|
if (point)
|
|
2193
4361
|
captured.set(storage, point.value.snapshot);
|
|
2194
|
-
} else if (at !== void 0 &&
|
|
2195
|
-
const point =
|
|
4362
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
4363
|
+
const point = history2.checkout(branch2, at);
|
|
2196
4364
|
if (!instances.has(storage)) {
|
|
2197
4365
|
const branchRng = createRng(options.seed ?? 0);
|
|
2198
4366
|
branchRng.setState(point.value.rngState);
|
|
@@ -2203,7 +4371,7 @@ var createRuntime = (options) => {
|
|
|
2203
4371
|
captured.set(storage, point.value.snapshot);
|
|
2204
4372
|
} else {
|
|
2205
4373
|
if (!instances.has(storage)) {
|
|
2206
|
-
const point =
|
|
4374
|
+
const point = history2.head(branch2);
|
|
2207
4375
|
const branchRng = createRng(options.seed ?? 0);
|
|
2208
4376
|
if (point)
|
|
2209
4377
|
branchRng.setState(point.value.rngState);
|
|
@@ -2227,8 +4395,8 @@ var createRuntime = (options) => {
|
|
|
2227
4395
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
2228
4396
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
2229
4397
|
const branchName = checkoutOptions.branch ?? "main";
|
|
2230
|
-
const
|
|
2231
|
-
const point =
|
|
4398
|
+
const history2 = timeline(namespace);
|
|
4399
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
2232
4400
|
const storage = ensureBranch(namespace, branchName);
|
|
2233
4401
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
2234
4402
|
captured.set(storage, point.value.snapshot);
|
|
@@ -2278,12 +4446,25 @@ var createRuntime = (options) => {
|
|
|
2278
4446
|
instance(name);
|
|
2279
4447
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
2280
4448
|
captured.set(name, from);
|
|
2281
|
-
const
|
|
2282
|
-
if (
|
|
2283
|
-
|
|
4449
|
+
const history2 = timelines.get(name);
|
|
4450
|
+
if (history2)
|
|
4451
|
+
history2.commit(capture(name), { branch: "main" });
|
|
2284
4452
|
else
|
|
2285
4453
|
timeline(name);
|
|
2286
4454
|
};
|
|
4455
|
+
const declarationsFor = (namespace) => {
|
|
4456
|
+
const declared = options.state;
|
|
4457
|
+
if (!declared)
|
|
4458
|
+
return [];
|
|
4459
|
+
return typeof declared === "function" ? declared(instance(namespace), namespace) : declared;
|
|
4460
|
+
};
|
|
4461
|
+
const stateScope = (namespace) => ({
|
|
4462
|
+
sqlite,
|
|
4463
|
+
namespace,
|
|
4464
|
+
storageNamespace: storageNamespace(namespace),
|
|
4465
|
+
root: instance(namespace),
|
|
4466
|
+
declarations: declarationsFor(namespace)
|
|
4467
|
+
});
|
|
2287
4468
|
const runtime = {
|
|
2288
4469
|
name: options.name,
|
|
2289
4470
|
sqlite,
|
|
@@ -2322,6 +4503,7 @@ var createRuntime = (options) => {
|
|
|
2322
4503
|
branch,
|
|
2323
4504
|
checkout,
|
|
2324
4505
|
timeline,
|
|
4506
|
+
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
2325
4507
|
fetch: async (incoming) => {
|
|
2326
4508
|
let request = incoming;
|
|
2327
4509
|
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
@@ -2495,10 +4677,10 @@ var createRuntime = (options) => {
|
|
|
2495
4677
|
},
|
|
2496
4678
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
2497
4679
|
inspect: (name) => {
|
|
2498
|
-
const
|
|
4680
|
+
const history2 = timeline(name);
|
|
2499
4681
|
return {
|
|
2500
|
-
branches:
|
|
2501
|
-
checkpoints:
|
|
4682
|
+
branches: history2.branches(),
|
|
4683
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
2502
4684
|
id,
|
|
2503
4685
|
branch: branchName,
|
|
2504
4686
|
parent,
|
|
@@ -2513,13 +4695,22 @@ var createRuntime = (options) => {
|
|
|
2513
4695
|
} : {},
|
|
2514
4696
|
routes: {
|
|
2515
4697
|
...credentialRoutes(credentials),
|
|
2516
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
2517
4698
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
2518
|
-
...options.admin?.(runtime) ?? {}
|
|
4699
|
+
...options.admin?.(runtime) ?? {},
|
|
4700
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4701
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4702
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4703
|
+
...stateAdminRoutes({
|
|
4704
|
+
open: stateScope,
|
|
4705
|
+
afterWrite: (namespace) => {
|
|
4706
|
+
checkpoint(namespace, "main");
|
|
4707
|
+
}
|
|
4708
|
+
}),
|
|
4709
|
+
...adminUiRoutes(options.name, options.adminUi)
|
|
2519
4710
|
},
|
|
2520
4711
|
adminKey: options.adminKey
|
|
2521
4712
|
});
|
|
2522
|
-
return runtime;
|
|
4713
|
+
return defineMock(runtime);
|
|
2523
4714
|
};
|
|
2524
4715
|
var mutableResponse = (response) => {
|
|
2525
4716
|
try {
|
|
@@ -2583,9 +4774,12 @@ var credentialRoutes = (registry) => ({
|
|
|
2583
4774
|
}
|
|
2584
4775
|
});
|
|
2585
4776
|
var presetRoutes = (presets, runtime) => ({
|
|
2586
|
-
"GET /faults/presets": () =>
|
|
2587
|
-
|
|
2588
|
-
|
|
4777
|
+
"GET /faults/presets": () => {
|
|
4778
|
+
const body = {
|
|
4779
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4780
|
+
};
|
|
4781
|
+
return adminJson(200, body);
|
|
4782
|
+
},
|
|
2589
4783
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
2590
4784
|
const name = params.name;
|
|
2591
4785
|
if (!presets[name])
|
|
@@ -2596,7 +4790,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
2596
4790
|
});
|
|
2597
4791
|
|
|
2598
4792
|
// src/generated/openapi.ts
|
|
2599
|
-
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Fullscript API (Mockingbird subset)","description":"Stateful mock subset of the Fullscript API for lab ordering: per-practitioner OAuth (authorize, token with authorization_code and refresh_token grants, revoke), clinic, embeddable session grants, lab orders, lab-order events, expiring result PDFs, and signed webhooks. Hand-authored from the EMR consumer (fullscript-api-client.ts, fullscript-event-model.ts, fullscript-webhook-signature.ts).\\n","version":"1","x-mockingbird-upstream":{"note":"Wire shapes from the consumer app's EMR backend (its Fullscript services and routers); the vendor's API reference is behind a partner login."}},"servers":[{"url":"https://api-us-snd.fullscript.io"}],"security":[{"bearerAuth":[]}],"paths":{"/api/oauth/token":{"post":{"operationId":"OAuthToken","description":"Exchange an authorization code, or rotate a refresh token (the old refresh token stops working).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}}},"400":{"description":"invalid_grant / unsupported_grant_type / invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}}}},"/api/oauth/revoke":{"post":{"operationId":"OAuthRevoke","description":"Revoke an access or refresh token (RFC 7009: always 200).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}}}},"/api/clinic":{"get":{"operationId":"GetClinic","description":"The token's clinic.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Clinic","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClinicResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/embeddable/session_grants":{"post":{"operationId":"CreateSessionGrant","description":"A short-lived secret for the embedded Fullscript widget.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"201":{"description":"Grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionGrant"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/labs/orders":{"get":{"operationId":"ListLabOrders","description":"The clinic's lab orders (optionally one patient's), paged.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrdersPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"patient_id","in":"query","required":false,"schema":{"type":"string","enum":["pat_seed_1","pat_seed_2","pat_missing"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/clinic/labs/orders/{orderId}":{"get":{"operationId":"GetLabOrder","description":"One lab order with tests, per-test results and the aggregated result.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrderResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string","enum":["lo_seed_1","lo_seed_2","lo_seed_3","lo_missing"]}}]}},"/api/events/lab_orders":{"get":{"operationId":"ListLabOrderEvents","description":"The clinic's lab_order.updated events, newest first by default.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"order_by","in":"query","required":false,"schema":{"type":"string","enum":["DESC","ASC"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/events/{eventId}":{"get":{"operationId":"GetEvent","description":"One event with its data.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string","enum":["evt_1","evt_2","evt_3","evt_missing"]}}]}},"/oauth/authorize":{"get":{"operationId":"Authorize","description":"The consent page: auto-approves as the requested (or default) practitioner and redirects to redirect_uri with code and state.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Browser redirect flow; covered by acceptance tests."}},"parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string"}},{"name":"practitioner_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back with ?code=&state="},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/results/{artifactId}":{"get":{"operationId":"GetResultPdf","description":"An expiring result PDF URL (what pdf_url points at).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Binary PDF behind an expiring signed URL; covered by acceptance tests."}},"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"}},{"name":"expires","in":"query","required":true,"schema":{"type":"string"}},{"name":"signature","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Expired or bad signature"},"404":{"description":"Unknown artifact"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"TokenRequest":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-client"]},"client_secret":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-secret"]},"code":{"type":"string","maxLength":64},"refresh_token":{"type":"string","maxLength":200},"redirect_uri":{"type":"string","maxLength":200,"examples":["https://emr.example.com/v1/fullscript/oauth/callback"]}}},"RevokeRequest":{"type":"object","required":["client_id","client_secret","token"],"properties":{"client_id":{"type":"string","minLength":1,"maxLength":64},"client_secret":{"type":"string","minLength":1,"maxLength":64},"token":{"type":"string","minLength":1,"maxLength":200}}},"OAuthTokenResponse":{"type":"object","required":["oauth"],"properties":{"oauth":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope","created_at","resource_owner"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"refresh_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"scope":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"resource_owner":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["Practitioner","Staff"]},"clinic_id":{"type":"string"}}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"ApiErrors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"ClinicResponse":{"type":"object","required":["clinic"],"properties":{"clinic":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}}}},"SessionGrant":{"type":"object","required":["secret_token","expires_at"],"properties":{"secret_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrderSummary":{"type":"object","required":["id","state","treatment_plan_id","patient_id","created_at","updated_at"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrdersPage":{"type":"object","required":["orders","meta"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/LabOrderSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"LabOrderResponse":{"type":"object","required":["order"],"properties":{"order":{"type":"object","required":["id","state","treatment_plan_id","tests","results","latest_aggregated_result"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"collection_method":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"tests":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"lab_type":{"type":"string"}}}},"results":{"type":"array","items":{"type":"object","required":["id","name","state","pdf_url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}}},"latest_aggregated_result":{"type":["object","null"],"properties":{"id":{"type":"string"},"artifact_id":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"status":{"type":"string"}}}}}}},"EventSummary":{"type":"object","required":["id","type","clinic_id","created_at"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["lab_order.updated"]},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"EventsPage":{"type":"object","required":["events","meta"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/EventSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"EventResponse":{"type":"object","required":["event"],"properties":{"event":{"type":"object","required":["id","type","clinic_id","created_at","data"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"data":{"type":"object"}}}}}}}}`);
|
|
4793
|
+
var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Fullscript API (Mockingbird subset)","description":"Stateful mock subset of the Fullscript API for lab ordering: per-practitioner OAuth (authorize, token with authorization_code and refresh_token grants, revoke), clinic, embeddable session grants, lab orders, lab-order events, expiring result PDFs, and signed webhooks. Hand-authored from the EMR consumer (fullscript-api-client.ts, fullscript-event-model.ts, fullscript-webhook-signature.ts).\\n","version":"1","x-mockingbird-upstream":{"note":"Wire shapes from the consumer app's EMR backend (its Fullscript services and routers); the vendor's API reference is behind a partner login."}},"servers":[{"url":"https://api-us-snd.fullscript.io"}],"security":[{"bearerAuth":[]}],"paths":{"/api/oauth/token":{"post":{"operationId":"OAuthToken","description":"Exchange an authorization code, or rotate a refresh token (the old refresh token stops working).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}}},"400":{"description":"invalid_grant / unsupported_grant_type / invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}}}},"/api/oauth/revoke":{"post":{"operationId":"OAuthRevoke","description":"Revoke an access or refresh token (RFC 7009: always 200).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}}}},"/api/clinic":{"get":{"operationId":"GetClinic","description":"The token's clinic.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Clinic","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClinicResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/embeddable/session_grants":{"post":{"operationId":"CreateSessionGrant","description":"A short-lived secret for the embedded Fullscript widget.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"201":{"description":"Grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionGrant"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/labs/orders":{"get":{"operationId":"ListLabOrders","description":"The clinic's lab orders (optionally one patient's), paged.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrdersPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"patient_id","in":"query","required":false,"schema":{"type":"string","enum":["pat_seed_1","pat_seed_2","pat_missing"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/clinic/labs/orders/{orderId}":{"get":{"operationId":"GetLabOrder","description":"One lab order with tests, per-test results and the aggregated result.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrderResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string","enum":["lo_seed_1","lo_seed_2","lo_seed_3","lo_missing"]}}]}},"/api/events/lab_orders":{"get":{"operationId":"ListLabOrderEvents","description":"The clinic's lab_order.updated events, newest first by default.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"order_by","in":"query","required":false,"schema":{"type":"string","enum":["DESC","ASC"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/events/{eventId}":{"get":{"operationId":"GetEvent","description":"One event with its data.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string","enum":["evt_1","evt_2","evt_3","evt_missing"]}}]}},"/oauth/authorize":{"get":{"operationId":"Authorize","description":"The consent page: auto-approves as the requested (or default) practitioner and redirects to redirect_uri with code and state.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Browser redirect flow; covered by acceptance tests."}},"parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string"}},{"name":"practitioner_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back with ?code=&state="},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/results/{artifactId}":{"get":{"operationId":"GetResultPdf","description":"An expiring result PDF URL (what pdf_url points at).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Binary PDF behind an expiring signed URL; covered by acceptance tests."}},"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"}},{"name":"expires","in":"query","required":true,"schema":{"type":"string"}},{"name":"signature","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Expired or bad signature"},"404":{"description":"Unknown artifact"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"TokenRequest":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-client"]},"client_secret":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-secret"]},"code":{"type":"string","maxLength":64},"refresh_token":{"type":"string","maxLength":200},"redirect_uri":{"type":"string","maxLength":200,"examples":["https://emr.example.com/v1/fullscript/oauth/callback"]}}},"RevokeRequest":{"type":"object","required":["client_id","client_secret","token"],"properties":{"client_id":{"type":"string","minLength":1,"maxLength":64},"client_secret":{"type":"string","minLength":1,"maxLength":64},"token":{"type":"string","minLength":1,"maxLength":200}}},"OAuthTokenResponse":{"type":"object","required":["oauth"],"properties":{"oauth":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope","created_at","resource_owner"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"refresh_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"scope":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"resource_owner":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["Practitioner","Staff"]},"clinic_id":{"type":"string"}}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"ApiErrors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"ClinicResponse":{"type":"object","required":["clinic"],"properties":{"clinic":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}}}},"SessionGrant":{"type":"object","required":["secret_token","expires_at"],"properties":{"secret_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrderSummary":{"type":"object","required":["id","state","treatment_plan_id","patient_id","created_at","updated_at"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrdersPage":{"type":"object","required":["orders","meta"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/LabOrderSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"LabOrderResponse":{"type":"object","required":["order"],"properties":{"order":{"type":"object","required":["id","state","treatment_plan_id","tests","results","latest_aggregated_result"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"collection_method":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"tests":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"lab_type":{"type":"string"}}}},"results":{"type":"array","items":{"type":"object","required":["id","name","state","pdf_url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}}},"latest_aggregated_result":{"type":["object","null"],"properties":{"id":{"type":"string"},"artifact_id":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"status":{"type":"string"}}}}}}},"EventSummary":{"type":"object","required":["id","type","clinic_id","created_at"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["lab_order.updated"]},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"EventsPage":{"type":"object","required":["events","meta"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/EventSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"EventResponse":{"type":"object","required":["event"],"properties":{"event":{"type":"object","required":["id","type","clinic_id","created_at","data"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"data":{"type":"object"}}}}}}}}`);
|
|
2600
4794
|
var operationIds = ["OAuthToken", "OAuthRevoke", "GetClinic", "CreateSessionGrant", "ListLabOrders", "GetLabOrder", "ListLabOrderEvents", "GetEvent", "Authorize", "GetResultPdf"];
|
|
2601
4795
|
var supportedOperationIds = ["OAuthToken", "OAuthRevoke", "GetClinic", "CreateSessionGrant", "ListLabOrders", "GetLabOrder", "ListLabOrderEvents", "GetEvent", "Authorize", "GetResultPdf"];
|
|
2602
4796
|
|
|
@@ -2781,9 +4975,9 @@ var FULLSCRIPT_PRESETS = {
|
|
|
2781
4975
|
},
|
|
2782
4976
|
webhook_drop: { description: "The next webhook is never delivered", webhook: { mode: "drop" } }
|
|
2783
4977
|
};
|
|
2784
|
-
var
|
|
2785
|
-
var
|
|
2786
|
-
var
|
|
4978
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
4979
|
+
var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4980
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2787
4981
|
var echoes = async (response, challenge) => {
|
|
2788
4982
|
try {
|
|
2789
4983
|
const body = await response.clone().json();
|
|
@@ -2794,21 +4988,21 @@ var echoes = async (response, challenge) => {
|
|
|
2794
4988
|
};
|
|
2795
4989
|
var adminRoutes = (send, challenge) => (runtime) => ({
|
|
2796
4990
|
"POST /oauth/codes": ({ body, namespace }) => {
|
|
2797
|
-
const input =
|
|
4991
|
+
const input = isRecord6(body) ? body : {};
|
|
2798
4992
|
const api = runtime.instance(namespace);
|
|
2799
4993
|
const practitionerId = typeof input.practitionerId === "string" ? input.practitionerId : "prac_mock_1";
|
|
2800
4994
|
if (!api.state.practitioners.get(practitionerId))
|
|
2801
|
-
return
|
|
4995
|
+
return adminError4(404, `no practitioner ${practitionerId}`);
|
|
2802
4996
|
const code = api.issueCode(
|
|
2803
4997
|
practitionerId,
|
|
2804
4998
|
typeof input.clientId === "string" ? input.clientId : "any",
|
|
2805
4999
|
typeof input.redirectUri === "string" ? input.redirectUri : null
|
|
2806
5000
|
);
|
|
2807
|
-
return
|
|
5001
|
+
return json5(201, { code });
|
|
2808
5002
|
},
|
|
2809
5003
|
"POST /practitioners": ({ body, namespace }) => {
|
|
2810
|
-
if (!
|
|
2811
|
-
return
|
|
5004
|
+
if (!isRecord6(body) || typeof body.id !== "string" || typeof body.clinicId !== "string") {
|
|
5005
|
+
return adminError4(
|
|
2812
5006
|
400,
|
|
2813
5007
|
'expected {"id", "clinicId", "type"?: "Practitioner"|"Staff", "clinicName"?}'
|
|
2814
5008
|
);
|
|
@@ -2827,12 +5021,12 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
2827
5021
|
type: body.type === "Staff" ? "Staff" : "Practitioner"
|
|
2828
5022
|
};
|
|
2829
5023
|
api.state.practitioners.insert(practitioner.id, practitioner);
|
|
2830
|
-
return
|
|
5024
|
+
return json5(201, practitioner);
|
|
2831
5025
|
},
|
|
2832
|
-
"GET /lab-orders": ({ namespace }) =>
|
|
5026
|
+
"GET /lab-orders": ({ namespace }) => json5(200, { orders: runtime.instance(namespace).labOrders() }),
|
|
2833
5027
|
"POST /lab-orders": ({ body, namespace }) => {
|
|
2834
|
-
if (!
|
|
2835
|
-
return
|
|
5028
|
+
if (!isRecord6(body) || typeof body.patientId !== "string") {
|
|
5029
|
+
return adminError4(
|
|
2836
5030
|
400,
|
|
2837
5031
|
'expected {"patientId", "id"?, "clinicId"?, "treatmentPlanId"?, "name"?, "collectionMethod"?, "tests"?: [names]}'
|
|
2838
5032
|
);
|
|
@@ -2847,23 +5041,23 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
2847
5041
|
...text("collectionMethod"),
|
|
2848
5042
|
...Array.isArray(body.tests) ? { tests: body.tests.map(String) } : {}
|
|
2849
5043
|
});
|
|
2850
|
-
return
|
|
5044
|
+
return json5(201, order);
|
|
2851
5045
|
},
|
|
2852
5046
|
"POST /lab-orders/:id/transition": ({ params, body, namespace }) => {
|
|
2853
|
-
if (!
|
|
2854
|
-
return
|
|
5047
|
+
if (!isRecord6(body) || !isLabOrderState(body.to)) {
|
|
5048
|
+
return adminError4(
|
|
2855
5049
|
400,
|
|
2856
5050
|
'expected {"to": "<purchased|schedule_appointment|upcoming_appointment|processing|partial_results|results_ready|interpretation_shared|results_amended>"}'
|
|
2857
5051
|
);
|
|
2858
5052
|
}
|
|
2859
5053
|
try {
|
|
2860
|
-
return
|
|
5054
|
+
return json5(200, runtime.instance(namespace).transition(params.id, body.to));
|
|
2861
5055
|
} catch (error) {
|
|
2862
5056
|
if (error instanceof HttpError) return error.toResponse();
|
|
2863
5057
|
throw error;
|
|
2864
5058
|
}
|
|
2865
5059
|
},
|
|
2866
|
-
"GET /events": ({ namespace }) =>
|
|
5060
|
+
"GET /events": ({ namespace }) => json5(200, { events: runtime.instance(namespace).eventsList() }),
|
|
2867
5061
|
"POST /webhooks/verify": async ({ namespace }) => {
|
|
2868
5062
|
const endpoints = runtime.webhooks?.endpoints(namespace) ?? [];
|
|
2869
5063
|
const results = await Promise.all(
|
|
@@ -2889,29 +5083,29 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
2889
5083
|
}
|
|
2890
5084
|
})
|
|
2891
5085
|
);
|
|
2892
|
-
return
|
|
5086
|
+
return json5(200, { endpoints: results });
|
|
2893
5087
|
},
|
|
2894
|
-
"GET /settings": ({ namespace }) =>
|
|
5088
|
+
"GET /settings": ({ namespace }) => json5(200, runtime.instance(namespace).state.current()),
|
|
2895
5089
|
"PUT /settings": ({ body, namespace }) => {
|
|
2896
|
-
if (!
|
|
5090
|
+
if (!isRecord6(body)) return adminError4(400, "expected a JSON object");
|
|
2897
5091
|
const patch = {};
|
|
2898
5092
|
for (const key of ["accessTokenTtlSeconds", "codeTtlSeconds", "pdfUrlTtlSeconds"]) {
|
|
2899
5093
|
if (body[key] !== void 0) {
|
|
2900
|
-
if (typeof body[key] !== "number") return
|
|
5094
|
+
if (typeof body[key] !== "number") return adminError4(400, `${key}: number`);
|
|
2901
5095
|
patch[key] = body[key];
|
|
2902
5096
|
}
|
|
2903
5097
|
}
|
|
2904
5098
|
if (body.resultsBaseUrl !== void 0) {
|
|
2905
5099
|
if (body.resultsBaseUrl !== null && typeof body.resultsBaseUrl !== "string")
|
|
2906
|
-
return
|
|
5100
|
+
return adminError4(400, "resultsBaseUrl: string | null");
|
|
2907
5101
|
patch.resultsBaseUrl = body.resultsBaseUrl;
|
|
2908
5102
|
}
|
|
2909
5103
|
if (body.clients !== void 0) {
|
|
2910
5104
|
if (!Array.isArray(body.clients))
|
|
2911
|
-
return
|
|
2912
|
-
patch.clients = body.clients.filter(
|
|
5105
|
+
return adminError4(400, "clients: [{clientId, clientSecret}]");
|
|
5106
|
+
patch.clients = body.clients.filter(isRecord6).map((c) => ({ clientId: String(c.clientId), clientSecret: String(c.clientSecret) }));
|
|
2913
5107
|
}
|
|
2914
|
-
return
|
|
5108
|
+
return json5(200, runtime.instance(namespace).state.update(patch));
|
|
2915
5109
|
}
|
|
2916
5110
|
});
|
|
2917
5111
|
var createRuntime2 = (options = {}) => {
|
|
@@ -2931,7 +5125,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
2931
5125
|
});
|
|
2932
5126
|
const runtime = createRuntime({
|
|
2933
5127
|
name: FULLSCRIPT_NAMESPACE,
|
|
2934
|
-
document,
|
|
5128
|
+
document: document2,
|
|
2935
5129
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2936
5130
|
...options.clock ? { clock: options.clock } : {},
|
|
2937
5131
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
@@ -3122,7 +5316,7 @@ var FullscriptAPI = class {
|
|
|
3122
5316
|
GetResultPdf: (context) => this.pdf(context)
|
|
3123
5317
|
});
|
|
3124
5318
|
this.service = createService({
|
|
3125
|
-
document,
|
|
5319
|
+
document: document2,
|
|
3126
5320
|
handlers,
|
|
3127
5321
|
sqlite,
|
|
3128
5322
|
namespace,
|
|
@@ -3289,11 +5483,11 @@ var FullscriptAPI = class {
|
|
|
3289
5483
|
return oauthError(400, "invalid_request", `Unknown practitioner ${practitionerId}.`);
|
|
3290
5484
|
}
|
|
3291
5485
|
const code = this.issueCode(practitionerId, clientId, redirect);
|
|
3292
|
-
const
|
|
3293
|
-
|
|
5486
|
+
const location2 = new URL(redirect);
|
|
5487
|
+
location2.searchParams.set("code", code);
|
|
3294
5488
|
const state = url.searchParams.get("state");
|
|
3295
|
-
if (state !== null)
|
|
3296
|
-
return new Response(null, { status: 302, headers: { location:
|
|
5489
|
+
if (state !== null) location2.searchParams.set("state", state);
|
|
5490
|
+
return new Response(null, { status: 302, headers: { location: location2.toString() } });
|
|
3297
5491
|
}
|
|
3298
5492
|
summary(order) {
|
|
3299
5493
|
return {
|
|
@@ -3473,7 +5667,7 @@ var FullscriptAPI = class {
|
|
|
3473
5667
|
};
|
|
3474
5668
|
|
|
3475
5669
|
export {
|
|
3476
|
-
document,
|
|
5670
|
+
document2 as document,
|
|
3477
5671
|
operationIds,
|
|
3478
5672
|
supportedOperationIds,
|
|
3479
5673
|
RESULT_PDF,
|
|
@@ -3493,4 +5687,4 @@ export {
|
|
|
3493
5687
|
envelope,
|
|
3494
5688
|
FullscriptAPI
|
|
3495
5689
|
};
|
|
3496
|
-
//# sourceMappingURL=chunk-
|
|
5690
|
+
//# sourceMappingURL=chunk-HEWMSDJK.js.map
|