@crvouga/mockingbird-service-easypost 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-AW2SKHTR.js → chunk-DLVQBVAS.js} +2322 -128
- package/dist/chunk-DLVQBVAS.js.map +7 -0
- package/dist/{chunk-HGQ5K355.js → chunk-LBRDXRJX.js} +3 -2
- package/dist/{chunk-HGQ5K355.js.map → chunk-LBRDXRJX.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-AW2SKHTR.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;
|
|
@@ -549,10 +551,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
549
551
|
};
|
|
550
552
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
551
553
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
552
|
-
var resolveRef = (
|
|
554
|
+
var resolveRef = (document3, ref) => {
|
|
553
555
|
if (!ref.startsWith("#/"))
|
|
554
556
|
throw new OpenAPIReferenceError(ref);
|
|
555
|
-
let cursor =
|
|
557
|
+
let cursor = document3;
|
|
556
558
|
for (const raw of ref.slice(2).split("/")) {
|
|
557
559
|
const segment = unescapePointer(raw);
|
|
558
560
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -564,14 +566,14 @@ var resolveRef = (document2, ref) => {
|
|
|
564
566
|
throw new OpenAPIReferenceError(ref);
|
|
565
567
|
return cursor;
|
|
566
568
|
};
|
|
567
|
-
var deref = (
|
|
569
|
+
var deref = (document3, value) => {
|
|
568
570
|
let current = value;
|
|
569
571
|
const seen = /* @__PURE__ */ new Set();
|
|
570
572
|
while (isReference(current)) {
|
|
571
573
|
if (seen.has(current.$ref))
|
|
572
574
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
573
575
|
seen.add(current.$ref);
|
|
574
|
-
current = resolveRef(
|
|
576
|
+
current = resolveRef(document3, current.$ref);
|
|
575
577
|
}
|
|
576
578
|
return current;
|
|
577
579
|
};
|
|
@@ -589,36 +591,36 @@ var HTTP_METHODS = [
|
|
|
589
591
|
];
|
|
590
592
|
|
|
591
593
|
// ../../openapi/core/dist/document.js
|
|
592
|
-
var mergeParameters = (
|
|
594
|
+
var mergeParameters = (document3, item, own) => {
|
|
593
595
|
const merged = /* @__PURE__ */ new Map();
|
|
594
596
|
for (const raw of item.parameters ?? []) {
|
|
595
|
-
const parameter = deref(
|
|
597
|
+
const parameter = deref(document3, raw);
|
|
596
598
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
597
599
|
}
|
|
598
600
|
for (const raw of own ?? []) {
|
|
599
|
-
const parameter = deref(
|
|
601
|
+
const parameter = deref(document3, raw);
|
|
600
602
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
601
603
|
}
|
|
602
604
|
return [...merged.values()];
|
|
603
605
|
};
|
|
604
|
-
var listOperations = (
|
|
606
|
+
var listOperations = (document3) => {
|
|
605
607
|
const operations = [];
|
|
606
|
-
for (const [path, item] of Object.entries(
|
|
608
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
607
609
|
for (const method of HTTP_METHODS) {
|
|
608
610
|
const operation = item[method];
|
|
609
611
|
if (operation?.operationId === void 0)
|
|
610
612
|
continue;
|
|
611
613
|
const responses = {};
|
|
612
614
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
613
|
-
responses[status] = deref(
|
|
615
|
+
responses[status] = deref(document3, response);
|
|
614
616
|
}
|
|
615
617
|
operations.push({
|
|
616
618
|
operationId: operation.operationId,
|
|
617
619
|
method,
|
|
618
620
|
path,
|
|
619
621
|
operation,
|
|
620
|
-
parameters: mergeParameters(
|
|
621
|
-
requestBody: operation.requestBody === void 0 ? void 0 : deref(
|
|
622
|
+
parameters: mergeParameters(document3, item, operation.parameters),
|
|
623
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
|
|
622
624
|
responses
|
|
623
625
|
});
|
|
624
626
|
}
|
|
@@ -627,7 +629,7 @@ var listOperations = (document2) => {
|
|
|
627
629
|
};
|
|
628
630
|
|
|
629
631
|
// ../../openapi/core/dist/schema.js
|
|
630
|
-
var resolveSchema = (
|
|
632
|
+
var resolveSchema = (document3, schema) => {
|
|
631
633
|
let current = schema;
|
|
632
634
|
const seen = /* @__PURE__ */ new Set();
|
|
633
635
|
while (typeof current.$ref === "string") {
|
|
@@ -636,7 +638,7 @@ var resolveSchema = (document2, schema) => {
|
|
|
636
638
|
break;
|
|
637
639
|
seen.add(ref);
|
|
638
640
|
const { $ref: _ignored, ...siblings } = current;
|
|
639
|
-
const target = resolveRef(
|
|
641
|
+
const target = resolveRef(document3, ref);
|
|
640
642
|
current = { ...target, ...siblings };
|
|
641
643
|
}
|
|
642
644
|
if (current.nullable === true) {
|
|
@@ -707,71 +709,71 @@ var FORMAT_PATTERNS = {
|
|
|
707
709
|
ipv4: /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/
|
|
708
710
|
};
|
|
709
711
|
var graphemeLength = (value) => [...value].length;
|
|
710
|
-
var validateValue = (
|
|
712
|
+
var validateValue = (document3, schema, value, path = []) => {
|
|
711
713
|
const errors = [];
|
|
712
|
-
const s = resolveSchema(
|
|
713
|
-
const
|
|
714
|
+
const s = resolveSchema(document3, schema);
|
|
715
|
+
const fail3 = (message) => errors.push({ path, message });
|
|
714
716
|
const actual = jsonTypeOf(value);
|
|
715
717
|
if (actual === "undefined") {
|
|
716
|
-
|
|
718
|
+
fail3("value is undefined");
|
|
717
719
|
return errors;
|
|
718
720
|
}
|
|
719
721
|
const types = schemaTypes(s);
|
|
720
722
|
if (types.length > 0) {
|
|
721
723
|
const ok2 = types.some((t) => t === actual || t === "number" && actual === "integer");
|
|
722
724
|
if (!ok2) {
|
|
723
|
-
|
|
725
|
+
fail3(`expected type ${types.join("|")}, got ${actual}`);
|
|
724
726
|
return errors;
|
|
725
727
|
}
|
|
726
728
|
}
|
|
727
729
|
if (s.enum && !(value === null && types.includes("null")) && !s.enum.some((candidate) => deepEqual(candidate, value))) {
|
|
728
|
-
|
|
730
|
+
fail3("value not in enum");
|
|
729
731
|
}
|
|
730
732
|
if (s.const !== void 0 && !deepEqual(s.const, value))
|
|
731
|
-
|
|
733
|
+
fail3("value does not equal const");
|
|
732
734
|
if (typeof value === "string") {
|
|
733
735
|
const length = graphemeLength(value);
|
|
734
736
|
if (s.minLength !== void 0 && length < s.minLength)
|
|
735
|
-
|
|
737
|
+
fail3(`length ${length} < minLength ${s.minLength}`);
|
|
736
738
|
if (s.maxLength !== void 0 && length > s.maxLength)
|
|
737
|
-
|
|
739
|
+
fail3(`length ${length} > maxLength ${s.maxLength}`);
|
|
738
740
|
if (s.pattern !== void 0) {
|
|
739
741
|
try {
|
|
740
742
|
if (!new RegExp(s.pattern, "u").test(value))
|
|
741
|
-
|
|
743
|
+
fail3(`does not match pattern ${s.pattern}`);
|
|
742
744
|
} catch {
|
|
743
745
|
}
|
|
744
746
|
}
|
|
745
747
|
if (s.format !== void 0) {
|
|
746
748
|
const pattern = FORMAT_PATTERNS[s.format];
|
|
747
749
|
if (pattern && !pattern.test(value))
|
|
748
|
-
|
|
750
|
+
fail3(`does not match format ${s.format}`);
|
|
749
751
|
}
|
|
750
752
|
}
|
|
751
753
|
if (typeof value === "number") {
|
|
752
754
|
if (s.minimum !== void 0 && value < s.minimum)
|
|
753
|
-
|
|
755
|
+
fail3(`${value} < minimum ${s.minimum}`);
|
|
754
756
|
if (s.maximum !== void 0 && value > s.maximum)
|
|
755
|
-
|
|
757
|
+
fail3(`${value} > maximum ${s.maximum}`);
|
|
756
758
|
if (s.exclusiveMinimum !== void 0 && value <= s.exclusiveMinimum)
|
|
757
|
-
|
|
759
|
+
fail3(`${value} <= exclusiveMinimum ${s.exclusiveMinimum}`);
|
|
758
760
|
if (s.exclusiveMaximum !== void 0 && value >= s.exclusiveMaximum)
|
|
759
|
-
|
|
761
|
+
fail3(`${value} >= exclusiveMaximum ${s.exclusiveMaximum}`);
|
|
760
762
|
if (s.multipleOf !== void 0 && Math.abs(value / s.multipleOf - Math.round(value / s.multipleOf)) > 1e-9) {
|
|
761
|
-
|
|
763
|
+
fail3(`${value} is not a multiple of ${s.multipleOf}`);
|
|
762
764
|
}
|
|
763
765
|
}
|
|
764
766
|
if (Array.isArray(value)) {
|
|
765
767
|
if (s.minItems !== void 0 && value.length < s.minItems)
|
|
766
|
-
|
|
768
|
+
fail3(`${value.length} items < minItems ${s.minItems}`);
|
|
767
769
|
if (s.maxItems !== void 0 && value.length > s.maxItems)
|
|
768
|
-
|
|
770
|
+
fail3(`${value.length} items > maxItems ${s.maxItems}`);
|
|
769
771
|
if (s.uniqueItems && value.some((item, i) => value.slice(0, i).some((prev) => deepEqual(prev, item))))
|
|
770
|
-
|
|
772
|
+
fail3("items are not unique");
|
|
771
773
|
value.forEach((item, i) => {
|
|
772
774
|
const itemSchema = s.prefixItems?.[i] ?? s.items;
|
|
773
775
|
if (itemSchema)
|
|
774
|
-
errors.push(...validateValue(
|
|
776
|
+
errors.push(...validateValue(document3, itemSchema, item, [...path, i]));
|
|
775
777
|
});
|
|
776
778
|
}
|
|
777
779
|
if (actual === "object") {
|
|
@@ -779,41 +781,41 @@ var validateValue = (document2, schema, value, path = []) => {
|
|
|
779
781
|
const keys = Object.keys(record);
|
|
780
782
|
for (const name of s.required ?? [])
|
|
781
783
|
if (!(name in record))
|
|
782
|
-
|
|
784
|
+
fail3(`missing required property ${name}`);
|
|
783
785
|
if (s.minProperties !== void 0 && keys.length < s.minProperties)
|
|
784
|
-
|
|
786
|
+
fail3(`${keys.length} properties < minProperties ${s.minProperties}`);
|
|
785
787
|
if (s.maxProperties !== void 0 && keys.length > s.maxProperties)
|
|
786
|
-
|
|
788
|
+
fail3(`${keys.length} properties > maxProperties ${s.maxProperties}`);
|
|
787
789
|
for (const key of keys) {
|
|
788
790
|
const property = s.properties?.[key];
|
|
789
791
|
if (property) {
|
|
790
|
-
errors.push(...validateValue(
|
|
792
|
+
errors.push(...validateValue(document3, property, record[key], [...path, key]));
|
|
791
793
|
continue;
|
|
792
794
|
}
|
|
793
795
|
if (s.additionalProperties === false)
|
|
794
|
-
|
|
796
|
+
fail3(`unexpected property ${key}`);
|
|
795
797
|
else if (typeof s.additionalProperties === "object") {
|
|
796
|
-
errors.push(...validateValue(
|
|
798
|
+
errors.push(...validateValue(document3, s.additionalProperties, record[key], [...path, key]));
|
|
797
799
|
}
|
|
798
800
|
if (s.propertyNames) {
|
|
799
|
-
const nameErrors = validateValue(
|
|
801
|
+
const nameErrors = validateValue(document3, s.propertyNames, key, [...path, key]);
|
|
800
802
|
if (nameErrors.length > 0)
|
|
801
|
-
|
|
803
|
+
fail3(`property name ${key} is invalid: ${nameErrors[0]?.message}`);
|
|
802
804
|
}
|
|
803
805
|
}
|
|
804
806
|
}
|
|
805
807
|
if (s.allOf)
|
|
806
808
|
for (const branch of s.allOf)
|
|
807
|
-
errors.push(...validateValue(
|
|
808
|
-
if (s.anyOf && !s.anyOf.some((branch) => validateValue(
|
|
809
|
-
|
|
809
|
+
errors.push(...validateValue(document3, branch, value, path));
|
|
810
|
+
if (s.anyOf && !s.anyOf.some((branch) => validateValue(document3, branch, value).length === 0))
|
|
811
|
+
fail3("matches no anyOf branch");
|
|
810
812
|
if (s.oneOf) {
|
|
811
|
-
const matches2 = s.oneOf.filter((branch) => validateValue(
|
|
813
|
+
const matches2 = s.oneOf.filter((branch) => validateValue(document3, branch, value).length === 0).length;
|
|
812
814
|
if (matches2 !== 1)
|
|
813
|
-
|
|
815
|
+
fail3(`matches ${matches2} oneOf branches, expected exactly 1`);
|
|
814
816
|
}
|
|
815
|
-
if (s.not && validateValue(
|
|
816
|
-
|
|
817
|
+
if (s.not && validateValue(document3, s.not, value).length === 0)
|
|
818
|
+
fail3("matches forbidden `not` schema");
|
|
817
819
|
return errors;
|
|
818
820
|
};
|
|
819
821
|
|
|
@@ -1386,6 +1388,1737 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
1386
1388
|
});
|
|
1387
1389
|
};
|
|
1388
1390
|
|
|
1391
|
+
// ../../ui/dist/reset.js
|
|
1392
|
+
var CSS_RESET = `*,
|
|
1393
|
+
*::before,
|
|
1394
|
+
*::after {
|
|
1395
|
+
box-sizing: border-box;
|
|
1396
|
+
margin: 0;
|
|
1397
|
+
padding: 0;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
* {
|
|
1401
|
+
scrollbar-width: thin;
|
|
1402
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
*::-webkit-scrollbar {
|
|
1406
|
+
width: 12px;
|
|
1407
|
+
height: 12px;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
*::-webkit-scrollbar-track,
|
|
1411
|
+
*::-webkit-scrollbar-corner {
|
|
1412
|
+
background: Canvas;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
*::-webkit-scrollbar-thumb {
|
|
1416
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
1417
|
+
border: 3px solid Canvas;
|
|
1418
|
+
border-radius: 999px;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
html {
|
|
1422
|
+
hanging-punctuation: first allow-end last;
|
|
1423
|
+
-webkit-text-size-adjust: none;
|
|
1424
|
+
text-size-adjust: none;
|
|
1425
|
+
tab-size: 4;
|
|
1426
|
+
font-family:
|
|
1427
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1428
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1429
|
+
font-size: 16px;
|
|
1430
|
+
line-height: 1.5;
|
|
1431
|
+
-webkit-font-smoothing: antialiased;
|
|
1432
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1433
|
+
text-rendering: optimizeLegibility;
|
|
1434
|
+
-webkit-tap-highlight-color: transparent;
|
|
1435
|
+
scroll-behavior: smooth;
|
|
1436
|
+
background: Canvas;
|
|
1437
|
+
color: CanvasText;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
body {
|
|
1441
|
+
min-height: 100svh;
|
|
1442
|
+
line-height: 1.5;
|
|
1443
|
+
background: Canvas;
|
|
1444
|
+
color: CanvasText;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
h1 {
|
|
1448
|
+
font-size: 2em;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
h2 {
|
|
1452
|
+
font-size: 1.5em;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
h3 {
|
|
1456
|
+
font-size: 1.17em;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
h4 {
|
|
1460
|
+
font-size: 1em;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
h5 {
|
|
1464
|
+
font-size: 0.83em;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
h6 {
|
|
1468
|
+
font-size: 0.67em;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
h1,
|
|
1472
|
+
h2,
|
|
1473
|
+
h3,
|
|
1474
|
+
h4,
|
|
1475
|
+
h5,
|
|
1476
|
+
h6 {
|
|
1477
|
+
font-weight: 700;
|
|
1478
|
+
line-height: 1.2;
|
|
1479
|
+
text-wrap: balance;
|
|
1480
|
+
overflow-wrap: break-word;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
p,
|
|
1484
|
+
li,
|
|
1485
|
+
figcaption,
|
|
1486
|
+
dd,
|
|
1487
|
+
dt,
|
|
1488
|
+
blockquote {
|
|
1489
|
+
overflow-wrap: break-word;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
p,
|
|
1493
|
+
li,
|
|
1494
|
+
figcaption,
|
|
1495
|
+
blockquote {
|
|
1496
|
+
text-wrap: pretty;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
ul,
|
|
1500
|
+
ol,
|
|
1501
|
+
menu {
|
|
1502
|
+
list-style: none;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
a {
|
|
1506
|
+
color: inherit;
|
|
1507
|
+
text-decoration: underline;
|
|
1508
|
+
text-decoration-thickness: 1px;
|
|
1509
|
+
text-underline-offset: 0.15em;
|
|
1510
|
+
text-decoration-skip-ink: auto;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
b,
|
|
1514
|
+
strong {
|
|
1515
|
+
font-weight: 700;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
small {
|
|
1519
|
+
font-size: 0.875em;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
code,
|
|
1523
|
+
kbd,
|
|
1524
|
+
samp,
|
|
1525
|
+
pre {
|
|
1526
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1527
|
+
font-size: 1em;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
sub,
|
|
1531
|
+
sup {
|
|
1532
|
+
font-size: 75%;
|
|
1533
|
+
line-height: 0;
|
|
1534
|
+
position: relative;
|
|
1535
|
+
vertical-align: baseline;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
sub {
|
|
1539
|
+
bottom: -0.25em;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
sup {
|
|
1543
|
+
top: -0.5em;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
abbr[title] {
|
|
1547
|
+
text-decoration: underline dotted;
|
|
1548
|
+
text-underline-offset: 0.15em;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
mark {
|
|
1552
|
+
background: Mark;
|
|
1553
|
+
color: MarkText;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
hr {
|
|
1557
|
+
height: 0;
|
|
1558
|
+
border: 0;
|
|
1559
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1560
|
+
color: inherit;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
img,
|
|
1564
|
+
picture,
|
|
1565
|
+
video,
|
|
1566
|
+
canvas {
|
|
1567
|
+
display: block;
|
|
1568
|
+
max-width: 100%;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
img,
|
|
1572
|
+
video {
|
|
1573
|
+
height: auto;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
svg {
|
|
1577
|
+
overflow: hidden;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
iframe {
|
|
1581
|
+
border: 0;
|
|
1582
|
+
max-width: 100%;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
table {
|
|
1586
|
+
border-collapse: collapse;
|
|
1587
|
+
border-spacing: 0;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
fieldset {
|
|
1591
|
+
border: 0;
|
|
1592
|
+
min-inline-size: 0;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
legend {
|
|
1596
|
+
padding: 0;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
button,
|
|
1600
|
+
input,
|
|
1601
|
+
select,
|
|
1602
|
+
textarea,
|
|
1603
|
+
optgroup {
|
|
1604
|
+
font: inherit;
|
|
1605
|
+
letter-spacing: inherit;
|
|
1606
|
+
color: inherit;
|
|
1607
|
+
margin: 0;
|
|
1608
|
+
vertical-align: middle;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
button,
|
|
1612
|
+
input[type="button"],
|
|
1613
|
+
input[type="submit"],
|
|
1614
|
+
input[type="reset"] {
|
|
1615
|
+
appearance: none;
|
|
1616
|
+
background: none;
|
|
1617
|
+
border: 0;
|
|
1618
|
+
padding: 0;
|
|
1619
|
+
cursor: pointer;
|
|
1620
|
+
text-align: center;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
input,
|
|
1624
|
+
textarea,
|
|
1625
|
+
select {
|
|
1626
|
+
background-color: Canvas;
|
|
1627
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1628
|
+
border-radius: 0;
|
|
1629
|
+
padding: 0.5em 0.65em;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
input[type="checkbox"],
|
|
1633
|
+
input[type="radio"],
|
|
1634
|
+
input[type="range"],
|
|
1635
|
+
input[type="file"],
|
|
1636
|
+
input[type="color"],
|
|
1637
|
+
input[type="button"],
|
|
1638
|
+
input[type="submit"],
|
|
1639
|
+
input[type="reset"],
|
|
1640
|
+
input[type="image"],
|
|
1641
|
+
input[type="hidden"] {
|
|
1642
|
+
background: none;
|
|
1643
|
+
border: 0;
|
|
1644
|
+
padding: 0;
|
|
1645
|
+
border-radius: 0;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
input[type="checkbox"],
|
|
1649
|
+
input[type="radio"] {
|
|
1650
|
+
accent-color: CanvasText;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
input[type="search"] {
|
|
1654
|
+
appearance: none;
|
|
1655
|
+
background-color: Canvas;
|
|
1656
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1660
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1661
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1662
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1663
|
+
-webkit-appearance: none;
|
|
1664
|
+
display: none;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
input[type="number"] {
|
|
1668
|
+
appearance: textfield;
|
|
1669
|
+
background-color: Canvas;
|
|
1670
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1674
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1675
|
+
-webkit-appearance: none;
|
|
1676
|
+
margin: 0;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
textarea {
|
|
1680
|
+
resize: vertical;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
::placeholder {
|
|
1684
|
+
opacity: 1;
|
|
1685
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
:focus-visible {
|
|
1689
|
+
outline: 2px solid CanvasText;
|
|
1690
|
+
outline-offset: 2px;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
:focus:not(:focus-visible) {
|
|
1694
|
+
outline: none;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
:disabled,
|
|
1698
|
+
[disabled] {
|
|
1699
|
+
cursor: default;
|
|
1700
|
+
opacity: 0.6;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
label,
|
|
1704
|
+
summary,
|
|
1705
|
+
select,
|
|
1706
|
+
[role="button"] {
|
|
1707
|
+
cursor: pointer;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
summary {
|
|
1711
|
+
display: list-item;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
details {
|
|
1715
|
+
display: block;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
dialog {
|
|
1719
|
+
margin: auto;
|
|
1720
|
+
padding: 1rem;
|
|
1721
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1722
|
+
background: Canvas;
|
|
1723
|
+
color: CanvasText;
|
|
1724
|
+
max-width: calc(100vw - 2rem);
|
|
1725
|
+
max-height: calc(100svh - 2rem);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
dialog:not([open]) {
|
|
1729
|
+
display: none;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
::backdrop {
|
|
1733
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
[hidden] {
|
|
1737
|
+
display: none !important;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
::selection {
|
|
1741
|
+
background: Highlight;
|
|
1742
|
+
color: HighlightText;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1746
|
+
*,
|
|
1747
|
+
*::before,
|
|
1748
|
+
*::after {
|
|
1749
|
+
animation-duration: 0.01ms !important;
|
|
1750
|
+
animation-iteration-count: 1 !important;
|
|
1751
|
+
transition-duration: 0.01ms !important;
|
|
1752
|
+
scroll-behavior: auto !important;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
@media (pointer: coarse) {
|
|
1757
|
+
input,
|
|
1758
|
+
select,
|
|
1759
|
+
textarea {
|
|
1760
|
+
font-size: max(16px, 1em);
|
|
1761
|
+
}
|
|
1762
|
+
}`;
|
|
1763
|
+
|
|
1764
|
+
// ../core/dist/admin-client.js
|
|
1765
|
+
function isRecord2(value) {
|
|
1766
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1767
|
+
}
|
|
1768
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1769
|
+
const clip = (value, max) => {
|
|
1770
|
+
if (typeof value !== "string")
|
|
1771
|
+
return null;
|
|
1772
|
+
const trimmed = value.trim();
|
|
1773
|
+
if (trimmed === "")
|
|
1774
|
+
return null;
|
|
1775
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1776
|
+
};
|
|
1777
|
+
const httpUrl = (value) => {
|
|
1778
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1779
|
+
return null;
|
|
1780
|
+
try {
|
|
1781
|
+
const url = new URL(value);
|
|
1782
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1783
|
+
return null;
|
|
1784
|
+
return url.href;
|
|
1785
|
+
} catch {
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
const hexColor = (value) => {
|
|
1790
|
+
if (typeof value !== "string")
|
|
1791
|
+
return null;
|
|
1792
|
+
const raw = value.trim();
|
|
1793
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1794
|
+
};
|
|
1795
|
+
const hostname = (value) => {
|
|
1796
|
+
try {
|
|
1797
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1798
|
+
} catch {
|
|
1799
|
+
return value;
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
host.replaceChildren();
|
|
1803
|
+
host.hidden = true;
|
|
1804
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1805
|
+
return;
|
|
1806
|
+
const row = catalog[service];
|
|
1807
|
+
if (!isRecord2(row))
|
|
1808
|
+
return;
|
|
1809
|
+
const vendor = clip(row.vendor, 80);
|
|
1810
|
+
const website = httpUrl(row.website);
|
|
1811
|
+
const guide = httpUrl(row.guide);
|
|
1812
|
+
const docs = httpUrl(row.docs);
|
|
1813
|
+
const logo = httpUrl(row.logo);
|
|
1814
|
+
const color = hexColor(row.color);
|
|
1815
|
+
const description = clip(row.description, 280);
|
|
1816
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1817
|
+
return;
|
|
1818
|
+
const add = (node) => {
|
|
1819
|
+
host.append(node);
|
|
1820
|
+
return node;
|
|
1821
|
+
};
|
|
1822
|
+
if (logo !== null) {
|
|
1823
|
+
const img = doc.createElement("img");
|
|
1824
|
+
img.alt = "";
|
|
1825
|
+
img.src = logo;
|
|
1826
|
+
img.width = 16;
|
|
1827
|
+
img.height = 16;
|
|
1828
|
+
img.decoding = "async";
|
|
1829
|
+
img.referrerPolicy = "no-referrer";
|
|
1830
|
+
if (color !== null)
|
|
1831
|
+
img.style.borderColor = color;
|
|
1832
|
+
add(img);
|
|
1833
|
+
}
|
|
1834
|
+
const name = doc.createElement("span");
|
|
1835
|
+
name.className = "vendor-name";
|
|
1836
|
+
name.textContent = vendor;
|
|
1837
|
+
if (description !== null)
|
|
1838
|
+
name.title = description;
|
|
1839
|
+
add(name);
|
|
1840
|
+
const sep = () => {
|
|
1841
|
+
const node = doc.createElement("span");
|
|
1842
|
+
node.className = "vendor-sep";
|
|
1843
|
+
node.setAttribute("aria-hidden", "true");
|
|
1844
|
+
node.textContent = "\xB7";
|
|
1845
|
+
return node;
|
|
1846
|
+
};
|
|
1847
|
+
const link = (href, label, title) => {
|
|
1848
|
+
const node = doc.createElement("a");
|
|
1849
|
+
node.className = "vendor-link";
|
|
1850
|
+
node.href = href;
|
|
1851
|
+
node.target = "_blank";
|
|
1852
|
+
node.rel = "noopener noreferrer";
|
|
1853
|
+
node.referrerPolicy = "no-referrer";
|
|
1854
|
+
node.textContent = label;
|
|
1855
|
+
node.title = title;
|
|
1856
|
+
return node;
|
|
1857
|
+
};
|
|
1858
|
+
if (website !== null) {
|
|
1859
|
+
add(sep());
|
|
1860
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1861
|
+
}
|
|
1862
|
+
if (guide !== null) {
|
|
1863
|
+
add(sep());
|
|
1864
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1865
|
+
}
|
|
1866
|
+
if (docs !== null) {
|
|
1867
|
+
add(sep());
|
|
1868
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1869
|
+
}
|
|
1870
|
+
host.removeAttribute("hidden");
|
|
1871
|
+
host.hidden = false;
|
|
1872
|
+
}
|
|
1873
|
+
function startAdminBrand(env) {
|
|
1874
|
+
const requested = env.params.get("brands");
|
|
1875
|
+
let brandsUrl = env.brandsUrl;
|
|
1876
|
+
if (requested) {
|
|
1877
|
+
try {
|
|
1878
|
+
const url = new URL(requested, env.location.href);
|
|
1879
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1880
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1881
|
+
brandsUrl = url.href;
|
|
1882
|
+
} catch {
|
|
1883
|
+
brandsUrl = env.brandsUrl;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const vendorHost = env.$("vendor");
|
|
1887
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1888
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1889
|
+
if (vendorHost === null)
|
|
1890
|
+
return;
|
|
1891
|
+
env.fetch(brandsUrl, {
|
|
1892
|
+
headers: { accept: "application/json" },
|
|
1893
|
+
signal: AbortSignal.timeout(5e3)
|
|
1894
|
+
}).then(async (response) => {
|
|
1895
|
+
if (!response.ok)
|
|
1896
|
+
return null;
|
|
1897
|
+
const catalog = await response.json();
|
|
1898
|
+
return catalog;
|
|
1899
|
+
}).then((catalog) => {
|
|
1900
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1901
|
+
}).catch(() => {
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
function faultPresetList(value) {
|
|
1905
|
+
if (!isRecord2(value))
|
|
1906
|
+
throw new Error("Fault presets response was not an object");
|
|
1907
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1908
|
+
return { presets: [] };
|
|
1909
|
+
}
|
|
1910
|
+
if (!Array.isArray(value.presets))
|
|
1911
|
+
throw new Error("Fault presets response was not a list");
|
|
1912
|
+
return {
|
|
1913
|
+
presets: value.presets.flatMap((item) => {
|
|
1914
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1915
|
+
return [];
|
|
1916
|
+
return [{ name: item.name }];
|
|
1917
|
+
})
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
function bootAdmin(config) {
|
|
1921
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1922
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1923
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1924
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1925
|
+
const readJson2 = (text) => {
|
|
1926
|
+
const value = JSON.parse(text);
|
|
1927
|
+
return value;
|
|
1928
|
+
};
|
|
1929
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1930
|
+
switch (char) {
|
|
1931
|
+
case "&":
|
|
1932
|
+
return "&";
|
|
1933
|
+
case "<":
|
|
1934
|
+
return "<";
|
|
1935
|
+
case ">":
|
|
1936
|
+
return ">";
|
|
1937
|
+
case '"':
|
|
1938
|
+
return """;
|
|
1939
|
+
default:
|
|
1940
|
+
return "'";
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
const el = (id, kind) => {
|
|
1944
|
+
const node = document.getElementById(id);
|
|
1945
|
+
if (!(node instanceof kind))
|
|
1946
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1947
|
+
return node;
|
|
1948
|
+
};
|
|
1949
|
+
const errorMessage = (data, fallback) => {
|
|
1950
|
+
if (!isRecord2(data))
|
|
1951
|
+
return fallback;
|
|
1952
|
+
const error = data.error;
|
|
1953
|
+
if (!isRecord2(error) || typeof error.message !== "string" || error.message === "") {
|
|
1954
|
+
return fallback;
|
|
1955
|
+
}
|
|
1956
|
+
return error.message;
|
|
1957
|
+
};
|
|
1958
|
+
const requireList = (value, label) => {
|
|
1959
|
+
if (value === void 0 || value === null)
|
|
1960
|
+
return [];
|
|
1961
|
+
if (!Array.isArray(value))
|
|
1962
|
+
throw new Error(`${label} was not a list`);
|
|
1963
|
+
return value;
|
|
1964
|
+
};
|
|
1965
|
+
const state = {
|
|
1966
|
+
namespace: "default",
|
|
1967
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1968
|
+
view: "overview",
|
|
1969
|
+
collection: "",
|
|
1970
|
+
shape: null,
|
|
1971
|
+
route: null,
|
|
1972
|
+
editing: null
|
|
1973
|
+
};
|
|
1974
|
+
const standard = new Set(config.standardRoutes);
|
|
1975
|
+
const banner = el("banner", HTMLElement);
|
|
1976
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1977
|
+
keyInput.value = state.key;
|
|
1978
|
+
const params = new URLSearchParams(location.search);
|
|
1979
|
+
const queryKey = params.get("key");
|
|
1980
|
+
if (queryKey) {
|
|
1981
|
+
state.key = queryKey;
|
|
1982
|
+
keyInput.value = state.key;
|
|
1983
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1984
|
+
params.delete("key");
|
|
1985
|
+
const next = params.toString();
|
|
1986
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1987
|
+
}
|
|
1988
|
+
const queryNamespace = params.get("namespace");
|
|
1989
|
+
if (queryNamespace)
|
|
1990
|
+
state.namespace = queryNamespace;
|
|
1991
|
+
const showError = (error) => {
|
|
1992
|
+
banner.textContent = error instanceof Error ? error.message : String(error);
|
|
1993
|
+
banner.classList.add("show");
|
|
1994
|
+
};
|
|
1995
|
+
const clearError = () => {
|
|
1996
|
+
banner.classList.remove("show");
|
|
1997
|
+
};
|
|
1998
|
+
const api = async (method, path, body) => {
|
|
1999
|
+
const headers = { accept: "application/json" };
|
|
2000
|
+
if (state.key !== "")
|
|
2001
|
+
headers[config.adminKeyHeader] = state.key;
|
|
2002
|
+
if (state.namespace !== "")
|
|
2003
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
2004
|
+
if (body !== void 0)
|
|
2005
|
+
headers["content-type"] = "application/json";
|
|
2006
|
+
const init = { method, headers };
|
|
2007
|
+
if (body !== void 0)
|
|
2008
|
+
init.body = JSON.stringify(body);
|
|
2009
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
2010
|
+
const text = await response.text();
|
|
2011
|
+
let data = null;
|
|
2012
|
+
if (text !== "") {
|
|
2013
|
+
try {
|
|
2014
|
+
data = readJson2(text);
|
|
2015
|
+
} catch {
|
|
2016
|
+
data = { raw: text };
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (response.status === 401) {
|
|
2020
|
+
keyInput.focus();
|
|
2021
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
2022
|
+
}
|
|
2023
|
+
if (!response.ok) {
|
|
2024
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
2025
|
+
}
|
|
2026
|
+
return data;
|
|
2027
|
+
};
|
|
2028
|
+
const originGet = async (path) => {
|
|
2029
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
2030
|
+
if (!response.ok)
|
|
2031
|
+
throw new Error(`${response.status} ${path}`);
|
|
2032
|
+
const value = await response.json();
|
|
2033
|
+
return value;
|
|
2034
|
+
};
|
|
2035
|
+
const panelApi = {
|
|
2036
|
+
get: (path) => api("GET", path),
|
|
2037
|
+
send: (method, path, body) => api(method, path, body),
|
|
2038
|
+
namespace: () => state.namespace
|
|
2039
|
+
};
|
|
2040
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
2041
|
+
const setTheme = (theme) => {
|
|
2042
|
+
if (theme)
|
|
2043
|
+
document.documentElement.dataset.theme = theme;
|
|
2044
|
+
else
|
|
2045
|
+
delete document.documentElement.dataset.theme;
|
|
2046
|
+
};
|
|
2047
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2048
|
+
if (storedTheme)
|
|
2049
|
+
setTheme(storedTheme);
|
|
2050
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2051
|
+
const current = document.documentElement.dataset.theme;
|
|
2052
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2053
|
+
const next = dark ? "light" : "dark";
|
|
2054
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
2055
|
+
setTheme(next);
|
|
2056
|
+
});
|
|
2057
|
+
const show = (view) => {
|
|
2058
|
+
state.view = view;
|
|
2059
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
2060
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2061
|
+
continue;
|
|
2062
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
2063
|
+
}
|
|
2064
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
2065
|
+
if (!(section instanceof HTMLElement))
|
|
2066
|
+
continue;
|
|
2067
|
+
section.hidden = section.id !== `view-${view}`;
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2071
|
+
const target = event.target;
|
|
2072
|
+
if (!(target instanceof Element))
|
|
2073
|
+
return;
|
|
2074
|
+
const button = target.closest("button[data-view]");
|
|
2075
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2076
|
+
return;
|
|
2077
|
+
const view = button.dataset.view;
|
|
2078
|
+
if (view === void 0)
|
|
2079
|
+
return;
|
|
2080
|
+
show(view);
|
|
2081
|
+
refresh().catch(showError);
|
|
2082
|
+
});
|
|
2083
|
+
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>`;
|
|
2084
|
+
const table = (columns, rows) => {
|
|
2085
|
+
if (rows.length === 0)
|
|
2086
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
2087
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
2088
|
+
const body = rows.map((row) => {
|
|
2089
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
2090
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2091
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
2092
|
+
}).join("");
|
|
2093
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2094
|
+
};
|
|
2095
|
+
const preview = (value) => {
|
|
2096
|
+
const text = JSON.stringify(value) ?? "null";
|
|
2097
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
2098
|
+
};
|
|
2099
|
+
const when = (epoch) => {
|
|
2100
|
+
const date = new Date(epoch);
|
|
2101
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2102
|
+
};
|
|
2103
|
+
const clockCard = (epoch) => {
|
|
2104
|
+
const date = new Date(epoch);
|
|
2105
|
+
if (Number.isNaN(date.getTime()))
|
|
2106
|
+
return esc(String(epoch));
|
|
2107
|
+
const iso = date.toISOString();
|
|
2108
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
2109
|
+
};
|
|
2110
|
+
const decodeClock = (value) => {
|
|
2111
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
2112
|
+
throw new Error("Clock response was not a clock");
|
|
2113
|
+
}
|
|
2114
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
2115
|
+
};
|
|
2116
|
+
const decodeField = (value) => {
|
|
2117
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
2118
|
+
return void 0;
|
|
2119
|
+
}
|
|
2120
|
+
const field = {
|
|
2121
|
+
name: value.name,
|
|
2122
|
+
kind: value.kind,
|
|
2123
|
+
optional: value.optional === true
|
|
2124
|
+
};
|
|
2125
|
+
if (typeof value.description === "string")
|
|
2126
|
+
field.description = value.description;
|
|
2127
|
+
if (Array.isArray(value.fields)) {
|
|
2128
|
+
field.fields = value.fields.flatMap((item) => {
|
|
2129
|
+
const child = decodeField(item);
|
|
2130
|
+
return child === void 0 ? [] : [child];
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
return field;
|
|
2134
|
+
};
|
|
2135
|
+
const decodeCollection = (value) => {
|
|
2136
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
2137
|
+
return void 0;
|
|
2138
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
2139
|
+
const field = decodeField(item);
|
|
2140
|
+
return field === void 0 ? [] : [field];
|
|
2141
|
+
}) : [];
|
|
2142
|
+
const collection = {
|
|
2143
|
+
name: value.name,
|
|
2144
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
2145
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
2146
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
2147
|
+
fields
|
|
2148
|
+
};
|
|
2149
|
+
if (typeof value.description === "string")
|
|
2150
|
+
collection.description = value.description;
|
|
2151
|
+
return collection;
|
|
2152
|
+
};
|
|
2153
|
+
const decodeState = (value) => {
|
|
2154
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
2155
|
+
throw new Error("State response has no collection list");
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
2159
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
2160
|
+
collections: value.collections.flatMap((item) => {
|
|
2161
|
+
const collection = decodeCollection(item);
|
|
2162
|
+
return collection === void 0 ? [] : [collection];
|
|
2163
|
+
})
|
|
2164
|
+
};
|
|
2165
|
+
};
|
|
2166
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
2167
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
2168
|
+
return [];
|
|
2169
|
+
return [
|
|
2170
|
+
{
|
|
2171
|
+
id: item.id,
|
|
2172
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
2173
|
+
value: item.value,
|
|
2174
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
2175
|
+
}
|
|
2176
|
+
];
|
|
2177
|
+
});
|
|
2178
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
2179
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
2180
|
+
if (!isRecord2(item))
|
|
2181
|
+
return [];
|
|
2182
|
+
return [
|
|
2183
|
+
{
|
|
2184
|
+
at: textOf(item.at),
|
|
2185
|
+
method: textOf(item.method),
|
|
2186
|
+
path: textOf(item.path),
|
|
2187
|
+
status: textOf(item.status),
|
|
2188
|
+
durationMs: textOf(item.durationMs),
|
|
2189
|
+
operationId: textOf(item.operationId)
|
|
2190
|
+
}
|
|
2191
|
+
];
|
|
2192
|
+
});
|
|
2193
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
2194
|
+
if (!isRecord2(item))
|
|
2195
|
+
return [];
|
|
2196
|
+
return [
|
|
2197
|
+
{
|
|
2198
|
+
id: textOf(item.id),
|
|
2199
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
2200
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
2201
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
2202
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
2203
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
2204
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
2205
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
2206
|
+
}
|
|
2207
|
+
];
|
|
2208
|
+
});
|
|
2209
|
+
const loadNamespaces = async () => {
|
|
2210
|
+
const data = await api("GET", "/namespaces");
|
|
2211
|
+
const select = el("namespace", HTMLSelectElement);
|
|
2212
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
2213
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
2214
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
2215
|
+
if (!names.includes(state.namespace))
|
|
2216
|
+
names.unshift(state.namespace);
|
|
2217
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
2218
|
+
select.value = state.namespace;
|
|
2219
|
+
};
|
|
2220
|
+
const renderOverview = async () => {
|
|
2221
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
2222
|
+
originGet("/health"),
|
|
2223
|
+
api("GET", "/state"),
|
|
2224
|
+
api("GET", "/clock"),
|
|
2225
|
+
api("GET", "/requests?limit=8")
|
|
2226
|
+
]);
|
|
2227
|
+
if (!isRecord2(health))
|
|
2228
|
+
throw new Error("Health response was not an object");
|
|
2229
|
+
const decoded = decodeState(shape);
|
|
2230
|
+
state.shape = decoded;
|
|
2231
|
+
const time = decodeClock(clock);
|
|
2232
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
2233
|
+
{
|
|
2234
|
+
k: "Service",
|
|
2235
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
k: "Status",
|
|
2239
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2240
|
+
},
|
|
2241
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
2242
|
+
{ k: "Collections", v: String(decoded.collections.length) },
|
|
2243
|
+
{
|
|
2244
|
+
k: "Records",
|
|
2245
|
+
v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
|
|
2246
|
+
},
|
|
2247
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
2248
|
+
]);
|
|
2249
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
2250
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2251
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2252
|
+
{
|
|
2253
|
+
label: "Status",
|
|
2254
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
2255
|
+
},
|
|
2256
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
2257
|
+
], decodeJournal(journal));
|
|
2258
|
+
};
|
|
2259
|
+
const schemaChips = (collection) => {
|
|
2260
|
+
if (collection.fields.length === 0) {
|
|
2261
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
2262
|
+
}
|
|
2263
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
2264
|
+
};
|
|
2265
|
+
const renderState = async () => {
|
|
2266
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
2267
|
+
state.shape = shape;
|
|
2268
|
+
const host = el("collections", HTMLElement);
|
|
2269
|
+
if (shape.collections.length === 0) {
|
|
2270
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
2271
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
2272
|
+
el("records", HTMLElement).innerHTML = "";
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
2276
|
+
const first = shape.collections[0];
|
|
2277
|
+
if (first === void 0)
|
|
2278
|
+
return;
|
|
2279
|
+
state.collection = first.name;
|
|
2280
|
+
}
|
|
2281
|
+
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("");
|
|
2282
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
2283
|
+
if (selected === void 0)
|
|
2284
|
+
return;
|
|
2285
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
2286
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
2287
|
+
const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
2288
|
+
el("records", HTMLElement).innerHTML = table([
|
|
2289
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
2290
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
2291
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
2292
|
+
], decodeRecords(page));
|
|
2293
|
+
};
|
|
2294
|
+
const renderClock = async () => {
|
|
2295
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
2296
|
+
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>`;
|
|
2297
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
2298
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
2299
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
2300
|
+
};
|
|
2301
|
+
const renderFaults = async () => {
|
|
2302
|
+
const [faults, presets] = await Promise.all([
|
|
2303
|
+
api("GET", "/faults"),
|
|
2304
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
2305
|
+
]);
|
|
2306
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
2307
|
+
const current = select.value;
|
|
2308
|
+
const list = faultPresetList(presets).presets;
|
|
2309
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
2310
|
+
select.value = current;
|
|
2311
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
2312
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
2313
|
+
{
|
|
2314
|
+
label: "Match",
|
|
2315
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
label: "Effect",
|
|
2319
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
2320
|
+
}
|
|
2321
|
+
], decodeFaults(faults));
|
|
2322
|
+
};
|
|
2323
|
+
const renderJournal = async () => {
|
|
2324
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
2325
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
2326
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
2327
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2328
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2329
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
2330
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2331
|
+
], decodeJournal(journal));
|
|
2332
|
+
};
|
|
2333
|
+
const renderRoutes = async () => {
|
|
2334
|
+
const data = await api("GET", "/");
|
|
2335
|
+
if (!isRecord2(data))
|
|
2336
|
+
throw new Error("Route list was not an object");
|
|
2337
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2338
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2339
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2340
|
+
const parts = route.split(" ");
|
|
2341
|
+
const method = parts[0] ?? "";
|
|
2342
|
+
const path = parts.slice(1).join(" ");
|
|
2343
|
+
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>`;
|
|
2344
|
+
}).join("");
|
|
2345
|
+
};
|
|
2346
|
+
const renderers = {
|
|
2347
|
+
overview: renderOverview,
|
|
2348
|
+
state: renderState,
|
|
2349
|
+
clock: renderClock,
|
|
2350
|
+
faults: renderFaults,
|
|
2351
|
+
journal: renderJournal,
|
|
2352
|
+
routes: renderRoutes
|
|
2353
|
+
};
|
|
2354
|
+
async function refresh() {
|
|
2355
|
+
clearError();
|
|
2356
|
+
const render = renderers[state.view];
|
|
2357
|
+
if (render)
|
|
2358
|
+
await render();
|
|
2359
|
+
}
|
|
2360
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
2361
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
2362
|
+
refresh().catch(showError);
|
|
2363
|
+
});
|
|
2364
|
+
keyInput.addEventListener("change", () => {
|
|
2365
|
+
state.key = keyInput.value.trim();
|
|
2366
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2367
|
+
refresh().catch(showError);
|
|
2368
|
+
});
|
|
2369
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
2370
|
+
refresh().catch(showError);
|
|
2371
|
+
});
|
|
2372
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
2373
|
+
const target = event.target;
|
|
2374
|
+
if (!(target instanceof Element))
|
|
2375
|
+
return;
|
|
2376
|
+
const button = target.closest("[data-collection]");
|
|
2377
|
+
if (!(button instanceof HTMLElement))
|
|
2378
|
+
return;
|
|
2379
|
+
const collection = button.dataset.collection;
|
|
2380
|
+
if (collection === void 0)
|
|
2381
|
+
return;
|
|
2382
|
+
state.collection = collection;
|
|
2383
|
+
renderState().catch(showError);
|
|
2384
|
+
});
|
|
2385
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
2386
|
+
const target = event.target;
|
|
2387
|
+
if (!(target instanceof Element))
|
|
2388
|
+
return;
|
|
2389
|
+
const row = target.closest("[data-id]");
|
|
2390
|
+
if (!(row instanceof HTMLElement))
|
|
2391
|
+
return;
|
|
2392
|
+
const id = row.dataset.id;
|
|
2393
|
+
if (id === void 0)
|
|
2394
|
+
return;
|
|
2395
|
+
openEditor(id).catch(showError);
|
|
2396
|
+
});
|
|
2397
|
+
const editor = el("editor", HTMLDialogElement);
|
|
2398
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
2399
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
2400
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
2401
|
+
async function openEditor(id) {
|
|
2402
|
+
state.editing = id;
|
|
2403
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
2404
|
+
editorId.value = id ?? "";
|
|
2405
|
+
editorId.readOnly = id !== null;
|
|
2406
|
+
editorDelete.hidden = id === null;
|
|
2407
|
+
if (id) {
|
|
2408
|
+
const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
2409
|
+
if (!isRecord2(record))
|
|
2410
|
+
throw new Error("Record response was not an object");
|
|
2411
|
+
editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
|
|
2412
|
+
} else {
|
|
2413
|
+
editorValue.value = "{\n \n}";
|
|
2414
|
+
}
|
|
2415
|
+
editor.showModal();
|
|
2416
|
+
}
|
|
2417
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
2418
|
+
openEditor(null).catch(showError);
|
|
2419
|
+
});
|
|
2420
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2421
|
+
const submitter = event.submitter;
|
|
2422
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
2423
|
+
return;
|
|
2424
|
+
event.preventDefault();
|
|
2425
|
+
let value;
|
|
2426
|
+
try {
|
|
2427
|
+
value = readJson2(editorValue.value);
|
|
2428
|
+
} catch {
|
|
2429
|
+
showError(new Error("Value is not JSON"));
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const id = editorId.value.trim();
|
|
2433
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2434
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2435
|
+
request.then(() => {
|
|
2436
|
+
editor.close();
|
|
2437
|
+
return renderState();
|
|
2438
|
+
}).catch(showError);
|
|
2439
|
+
});
|
|
2440
|
+
editorDelete.addEventListener("click", () => {
|
|
2441
|
+
const id = state.editing;
|
|
2442
|
+
if (!id)
|
|
2443
|
+
return;
|
|
2444
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2445
|
+
editor.close();
|
|
2446
|
+
return renderState();
|
|
2447
|
+
}).catch(showError);
|
|
2448
|
+
});
|
|
2449
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2450
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2451
|
+
continue;
|
|
2452
|
+
button.addEventListener("click", () => {
|
|
2453
|
+
const raw = button.dataset.advance;
|
|
2454
|
+
if (raw === void 0)
|
|
2455
|
+
return;
|
|
2456
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2460
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2461
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2462
|
+
});
|
|
2463
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2464
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2465
|
+
});
|
|
2466
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2467
|
+
event.preventDefault();
|
|
2468
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2469
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2470
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2471
|
+
});
|
|
2472
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2473
|
+
event.preventDefault();
|
|
2474
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2475
|
+
if (preset !== "") {
|
|
2476
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2480
|
+
if (!Number.isFinite(status)) {
|
|
2481
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2485
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2486
|
+
api("POST", "/faults", {
|
|
2487
|
+
status,
|
|
2488
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2489
|
+
...method !== "" ? { method } : {}
|
|
2490
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2491
|
+
});
|
|
2492
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2493
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2494
|
+
});
|
|
2495
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2496
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2497
|
+
});
|
|
2498
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2499
|
+
const target = event.target;
|
|
2500
|
+
if (!(target instanceof Element))
|
|
2501
|
+
return;
|
|
2502
|
+
const button = target.closest("[data-route]");
|
|
2503
|
+
if (!(button instanceof HTMLElement))
|
|
2504
|
+
return;
|
|
2505
|
+
const route = button.dataset.route;
|
|
2506
|
+
if (route === void 0)
|
|
2507
|
+
return;
|
|
2508
|
+
state.route = route;
|
|
2509
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2510
|
+
const method = route.split(" ")[0] ?? "";
|
|
2511
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2512
|
+
});
|
|
2513
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2514
|
+
event.preventDefault();
|
|
2515
|
+
const send = async () => {
|
|
2516
|
+
if (state.route === null)
|
|
2517
|
+
throw new Error("Choose a route first");
|
|
2518
|
+
const parts = state.route.split(" ");
|
|
2519
|
+
const method = parts[0];
|
|
2520
|
+
const path = parts.slice(1).join(" ");
|
|
2521
|
+
if (method === void 0)
|
|
2522
|
+
throw new Error("Choose a route first");
|
|
2523
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2524
|
+
let body;
|
|
2525
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2526
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2527
|
+
try {
|
|
2528
|
+
body = readJson2(routeBody.value);
|
|
2529
|
+
} catch {
|
|
2530
|
+
throw new Error("Body is not JSON");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const data = await api(method, concrete, body);
|
|
2534
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2535
|
+
};
|
|
2536
|
+
send().catch(showError);
|
|
2537
|
+
});
|
|
2538
|
+
const decodePanel = (value) => {
|
|
2539
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2540
|
+
return void 0;
|
|
2541
|
+
}
|
|
2542
|
+
const panel = {
|
|
2543
|
+
id: value.id,
|
|
2544
|
+
title: value.title,
|
|
2545
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2546
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2547
|
+
};
|
|
2548
|
+
if (typeof value.script === "string")
|
|
2549
|
+
panel.script = value.script;
|
|
2550
|
+
return panel;
|
|
2551
|
+
};
|
|
2552
|
+
const mountPanels = (panels) => {
|
|
2553
|
+
const nav = el("nav", HTMLElement);
|
|
2554
|
+
const views = el("views", HTMLElement);
|
|
2555
|
+
for (const panel of panels) {
|
|
2556
|
+
const button = document.createElement("button");
|
|
2557
|
+
button.type = "button";
|
|
2558
|
+
button.dataset.view = panel.id;
|
|
2559
|
+
button.textContent = panel.title;
|
|
2560
|
+
nav.append(button);
|
|
2561
|
+
const section = document.createElement("section");
|
|
2562
|
+
section.className = "view";
|
|
2563
|
+
section.id = `view-${panel.id}`;
|
|
2564
|
+
section.hidden = true;
|
|
2565
|
+
const title = document.createElement("h2");
|
|
2566
|
+
title.textContent = panel.title;
|
|
2567
|
+
const lede = document.createElement("p");
|
|
2568
|
+
lede.className = "lede";
|
|
2569
|
+
lede.textContent = panel.description;
|
|
2570
|
+
const body = document.createElement("div");
|
|
2571
|
+
body.className = "panel-body";
|
|
2572
|
+
body.id = `panel-${panel.id}`;
|
|
2573
|
+
body.innerHTML = panel.html;
|
|
2574
|
+
section.append(title, lede, body);
|
|
2575
|
+
views.append(section);
|
|
2576
|
+
if (panel.script === void 0)
|
|
2577
|
+
continue;
|
|
2578
|
+
try {
|
|
2579
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2580
|
+
if (!isPanelScript(compiled))
|
|
2581
|
+
throw new Error("panel script did not compile");
|
|
2582
|
+
compiled(body, panelApi);
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
const note = document.createElement("p");
|
|
2585
|
+
note.className = "banner show";
|
|
2586
|
+
note.textContent = error instanceof Error ? error.message : String(error);
|
|
2587
|
+
body.append(note);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
startAdminBrand({
|
|
2592
|
+
document,
|
|
2593
|
+
$: (id) => document.getElementById(id),
|
|
2594
|
+
params,
|
|
2595
|
+
location: { href: location.href, origin: location.origin },
|
|
2596
|
+
fetch,
|
|
2597
|
+
brandsUrl: config.brandsUrl
|
|
2598
|
+
});
|
|
2599
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2600
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2601
|
+
const decoded = decodePanel(panel);
|
|
2602
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2603
|
+
});
|
|
2604
|
+
mountPanels(panels);
|
|
2605
|
+
return refresh();
|
|
2606
|
+
}).catch(showError);
|
|
2607
|
+
}
|
|
2608
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2609
|
+
var adminClientSource = (config) => {
|
|
2610
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2611
|
+
return `<script>
|
|
2612
|
+
${clientPrelude}
|
|
2613
|
+
bootAdmin(${payload});
|
|
2614
|
+
</script>`;
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// ../core/dist/surface.js
|
|
2618
|
+
var defineMock = (runtime) => runtime;
|
|
2619
|
+
var STANDARD_ADMIN_ROUTES = [
|
|
2620
|
+
"GET /",
|
|
2621
|
+
"POST /reset",
|
|
2622
|
+
"GET /namespaces",
|
|
2623
|
+
"GET /clock",
|
|
2624
|
+
"POST /clock",
|
|
2625
|
+
"GET /faults",
|
|
2626
|
+
"POST /faults",
|
|
2627
|
+
"DELETE /faults",
|
|
2628
|
+
"POST /snapshots",
|
|
2629
|
+
"POST /snapshots/:id/restore",
|
|
2630
|
+
"DELETE /snapshots/:id",
|
|
2631
|
+
"GET /timeline",
|
|
2632
|
+
"POST /checkpoints",
|
|
2633
|
+
"POST /branches/:name",
|
|
2634
|
+
"POST /branches/:name/checkout",
|
|
2635
|
+
"GET /requests",
|
|
2636
|
+
"DELETE /requests",
|
|
2637
|
+
"GET /metrics",
|
|
2638
|
+
"DELETE /metrics",
|
|
2639
|
+
"GET /credentials",
|
|
2640
|
+
"PUT /credentials",
|
|
2641
|
+
"DELETE /credentials",
|
|
2642
|
+
"GET /state",
|
|
2643
|
+
"GET /state/:collection",
|
|
2644
|
+
"GET /state/:collection/:id",
|
|
2645
|
+
"POST /state/:collection",
|
|
2646
|
+
"PUT /state/:collection/:id",
|
|
2647
|
+
"PATCH /state/:collection/:id",
|
|
2648
|
+
"DELETE /state/:collection/:id",
|
|
2649
|
+
"GET /ui",
|
|
2650
|
+
"GET /ui/",
|
|
2651
|
+
"GET /ui/manifest"
|
|
2652
|
+
];
|
|
2653
|
+
var PANEL_ID = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
2654
|
+
var assertAdminUi = (ui) => {
|
|
2655
|
+
if (!ui?.panels)
|
|
2656
|
+
return;
|
|
2657
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2658
|
+
for (const panel of ui.panels) {
|
|
2659
|
+
if (!PANEL_ID.test(panel.id)) {
|
|
2660
|
+
throw new RangeError(`admin panel id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
|
|
2661
|
+
}
|
|
2662
|
+
if (seen.has(panel.id))
|
|
2663
|
+
throw new RangeError(`duplicate admin panel id ${panel.id}`);
|
|
2664
|
+
seen.add(panel.id);
|
|
2665
|
+
if (panel.title.trim() === "")
|
|
2666
|
+
throw new RangeError(`admin panel ${panel.id} needs a title`);
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
// ../core/dist/admin-ui.js
|
|
2671
|
+
var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
2672
|
+
switch (char) {
|
|
2673
|
+
case "&":
|
|
2674
|
+
return "&";
|
|
2675
|
+
case "<":
|
|
2676
|
+
return "<";
|
|
2677
|
+
case ">":
|
|
2678
|
+
return ">";
|
|
2679
|
+
case '"':
|
|
2680
|
+
return """;
|
|
2681
|
+
default:
|
|
2682
|
+
return "'";
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2686
|
+
var renderAdminDocument = (service) => {
|
|
2687
|
+
const name = escapeHtml(service);
|
|
2688
|
+
const script = adminClientSource({
|
|
2689
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2690
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2691
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2692
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2693
|
+
service
|
|
2694
|
+
});
|
|
2695
|
+
return `<!DOCTYPE html>
|
|
2696
|
+
<html lang="en">
|
|
2697
|
+
<head>
|
|
2698
|
+
<meta charset="utf-8">
|
|
2699
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2700
|
+
<meta name="color-scheme" content="light dark">
|
|
2701
|
+
<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">
|
|
2702
|
+
<title>${name} admin</title>
|
|
2703
|
+
<style>
|
|
2704
|
+
${CSS_RESET}
|
|
2705
|
+
:root {
|
|
2706
|
+
color-scheme: light;
|
|
2707
|
+
--bg: #fafafa;
|
|
2708
|
+
--bg-elev: #ffffff;
|
|
2709
|
+
--bg-muted: #f4f4f5;
|
|
2710
|
+
--bg-inset: #f4f4f5;
|
|
2711
|
+
--line: #dedee3;
|
|
2712
|
+
--line-strong: #c4c4cc;
|
|
2713
|
+
--ink: #18181b;
|
|
2714
|
+
--muted: #62626b;
|
|
2715
|
+
--faint: #71717a;
|
|
2716
|
+
--accent: #27272a;
|
|
2717
|
+
--accent-ink: #ffffff;
|
|
2718
|
+
--accent-soft: #f4f4f5;
|
|
2719
|
+
--ok: #166534;
|
|
2720
|
+
--ok-soft: #f0fdf4;
|
|
2721
|
+
--warn: #a16207;
|
|
2722
|
+
--warn-soft: #fefce8;
|
|
2723
|
+
--err: #a82d32;
|
|
2724
|
+
--err-soft: #fef2f2;
|
|
2725
|
+
--shadow: none;
|
|
2726
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2727
|
+
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2728
|
+
--radius: 10px;
|
|
2729
|
+
--header: 56px;
|
|
2730
|
+
--nav: 200px;
|
|
2731
|
+
}
|
|
2732
|
+
:root[data-theme="dark"] {
|
|
2733
|
+
color-scheme: dark;
|
|
2734
|
+
--bg: #111113;
|
|
2735
|
+
--bg-elev: #19191c;
|
|
2736
|
+
--bg-muted: #242428;
|
|
2737
|
+
--bg-inset: #111113;
|
|
2738
|
+
--line: #36363c;
|
|
2739
|
+
--line-strong: #52525b;
|
|
2740
|
+
--ink: #f4f4f5;
|
|
2741
|
+
--muted: #a9a9b2;
|
|
2742
|
+
--faint: #71717a;
|
|
2743
|
+
--accent: #e4e4e7;
|
|
2744
|
+
--accent-ink: #18181b;
|
|
2745
|
+
--accent-soft: #242428;
|
|
2746
|
+
--ok: #86efac;
|
|
2747
|
+
--ok-soft: #14241b;
|
|
2748
|
+
--warn: #fde68a;
|
|
2749
|
+
--warn-soft: #2a2410;
|
|
2750
|
+
--err: #ffaaaa;
|
|
2751
|
+
--err-soft: #2c1516;
|
|
2752
|
+
--shadow: none;
|
|
2753
|
+
}
|
|
2754
|
+
@media (prefers-color-scheme: dark) {
|
|
2755
|
+
:root:not([data-theme="light"]) {
|
|
2756
|
+
color-scheme: dark;
|
|
2757
|
+
--bg: #111113;
|
|
2758
|
+
--bg-elev: #19191c;
|
|
2759
|
+
--bg-muted: #242428;
|
|
2760
|
+
--bg-inset: #111113;
|
|
2761
|
+
--line: #36363c;
|
|
2762
|
+
--line-strong: #52525b;
|
|
2763
|
+
--ink: #f4f4f5;
|
|
2764
|
+
--muted: #a9a9b2;
|
|
2765
|
+
--faint: #71717a;
|
|
2766
|
+
--accent: #e4e4e7;
|
|
2767
|
+
--accent-ink: #18181b;
|
|
2768
|
+
--accent-soft: #242428;
|
|
2769
|
+
--ok: #86efac;
|
|
2770
|
+
--ok-soft: #14241b;
|
|
2771
|
+
--warn: #fde68a;
|
|
2772
|
+
--warn-soft: #2a2410;
|
|
2773
|
+
--err: #ffaaaa;
|
|
2774
|
+
--err-soft: #2c1516;
|
|
2775
|
+
--shadow: none;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2779
|
+
html { height: 100%; }
|
|
2780
|
+
body {
|
|
2781
|
+
margin: 0;
|
|
2782
|
+
min-height: 100%;
|
|
2783
|
+
display: flex;
|
|
2784
|
+
flex-direction: column;
|
|
2785
|
+
font-family: var(--sans);
|
|
2786
|
+
background: var(--bg);
|
|
2787
|
+
color: var(--ink);
|
|
2788
|
+
line-height: 1.45;
|
|
2789
|
+
padding-left: env(safe-area-inset-left);
|
|
2790
|
+
padding-right: env(safe-area-inset-right);
|
|
2791
|
+
-webkit-text-size-adjust: 100%;
|
|
2792
|
+
}
|
|
2793
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2794
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2795
|
+
header.top {
|
|
2796
|
+
position: sticky; top: 0; z-index: 5;
|
|
2797
|
+
display: flex; align-items: center; gap: 12px;
|
|
2798
|
+
min-height: var(--header); padding: 10px 16px;
|
|
2799
|
+
background: var(--bg-elev);
|
|
2800
|
+
border-bottom: 1px solid var(--line);
|
|
2801
|
+
min-width: 0; max-width: 100%;
|
|
2802
|
+
}
|
|
2803
|
+
.brand {
|
|
2804
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2805
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2806
|
+
}
|
|
2807
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2808
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2809
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2810
|
+
.vendor {
|
|
2811
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2812
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2813
|
+
}
|
|
2814
|
+
.vendor[hidden] { display: none; }
|
|
2815
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2816
|
+
.vendor img {
|
|
2817
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2818
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2819
|
+
}
|
|
2820
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2821
|
+
.vendor-sep { color: var(--faint); }
|
|
2822
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2823
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2824
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
2825
|
+
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
2826
|
+
label.field input, label.field select, .controls input[type="password"] {
|
|
2827
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
|
|
2828
|
+
padding: 7px 8px; min-height: 36px;
|
|
2829
|
+
}
|
|
2830
|
+
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2831
|
+
border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
|
|
2832
|
+
background: var(--bg-elev);
|
|
2833
|
+
}
|
|
2834
|
+
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
|
|
2835
|
+
.btn-quiet { background: transparent; }
|
|
2836
|
+
.btn-danger { color: var(--err); }
|
|
2837
|
+
/* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
|
|
2838
|
+
record or the full nav row would widen the page past the viewport. */
|
|
2839
|
+
.layout {
|
|
2840
|
+
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
2841
|
+
flex: 1 0 auto; min-width: 0;
|
|
2842
|
+
}
|
|
2843
|
+
nav.side {
|
|
2844
|
+
border-right: 1px solid var(--line); padding: 12px;
|
|
2845
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
2846
|
+
background: var(--bg-elev);
|
|
2847
|
+
}
|
|
2848
|
+
nav.side button {
|
|
2849
|
+
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2850
|
+
padding: 9px 10px; min-height: 40px; color: var(--muted);
|
|
2851
|
+
}
|
|
2852
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--ink); font-weight: 600; }
|
|
2853
|
+
main { padding: 20px; max-width: 1100px; width: 100%; min-width: 0; }
|
|
2854
|
+
.view h2 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.02em; }
|
|
2855
|
+
.lede { margin: 0 0 16px; color: var(--muted); }
|
|
2856
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
2857
|
+
.card {
|
|
2858
|
+
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2859
|
+
padding: 14px 16px; box-shadow: var(--shadow);
|
|
2860
|
+
}
|
|
2861
|
+
.card .k { color: var(--faint); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
2862
|
+
.card .v { display: flex; flex-direction: column; gap: 2px; font-size: 22px; margin-top: 4px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
2863
|
+
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2864
|
+
.split { display: grid; grid-template-columns: minmax(0, 240px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
2865
|
+
.collection {
|
|
2866
|
+
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2867
|
+
border-radius: 10px; padding: 8px 10px; min-height: 44px;
|
|
2868
|
+
}
|
|
2869
|
+
.collection[aria-current="true"] { background: var(--bg-elev); border-color: var(--line); }
|
|
2870
|
+
.collection small { display: block; color: var(--faint); }
|
|
2871
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2872
|
+
.chip {
|
|
2873
|
+
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2874
|
+
border-radius: 999px; padding: 4px 8px;
|
|
2875
|
+
}
|
|
2876
|
+
.chip i { color: var(--faint); font-style: normal; }
|
|
2877
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
2878
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
2879
|
+
th { color: var(--faint); font-weight: 600; font-size: 12px; }
|
|
2880
|
+
tr.clickable { cursor: pointer; }
|
|
2881
|
+
tr.clickable:hover { background: var(--bg-muted); }
|
|
2882
|
+
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2883
|
+
pre {
|
|
2884
|
+
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2885
|
+
background: var(--bg-inset); border-radius: 10px; padding: 12px; font-size: 12px;
|
|
2886
|
+
}
|
|
2887
|
+
.row-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
|
|
2888
|
+
.banner {
|
|
2889
|
+
display: none; margin-bottom: 12px; padding: 10px 12px; border-radius: 10px;
|
|
2890
|
+
background: var(--err-soft); color: var(--err);
|
|
2891
|
+
}
|
|
2892
|
+
.banner.show { display: block; }
|
|
2893
|
+
.empty { color: var(--muted); padding: 24px 8px; }
|
|
2894
|
+
dialog {
|
|
2895
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2896
|
+
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
|
|
2897
|
+
}
|
|
2898
|
+
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
2899
|
+
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
2900
|
+
textarea {
|
|
2901
|
+
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2902
|
+
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2903
|
+
font-family: var(--mono); font-size: 12px;
|
|
2904
|
+
}
|
|
2905
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2906
|
+
.badge { font-size: 11px; border-radius: 999px; padding: 2px 7px; background: var(--bg-muted); color: var(--muted); }
|
|
2907
|
+
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2908
|
+
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2909
|
+
.status-2 { color: var(--ok); }
|
|
2910
|
+
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2911
|
+
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
2912
|
+
.route {
|
|
2913
|
+
display: grid; grid-template-columns: 88px 1fr auto; gap: 8px; align-items: center;
|
|
2914
|
+
padding: 8px 0; border-bottom: 1px solid var(--line);
|
|
2915
|
+
}
|
|
2916
|
+
.method { font-family: var(--mono); font-size: 12px; font-weight: 700; }
|
|
2917
|
+
@media (max-width: 800px) {
|
|
2918
|
+
header.top {
|
|
2919
|
+
align-items: stretch; flex-direction: column;
|
|
2920
|
+
padding-top: max(10px, env(safe-area-inset-top));
|
|
2921
|
+
}
|
|
2922
|
+
input, select, textarea { font-size: 16px; }
|
|
2923
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
2924
|
+
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2925
|
+
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
2926
|
+
nav.side {
|
|
2927
|
+
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2928
|
+
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2929
|
+
padding: 8px;
|
|
2930
|
+
}
|
|
2931
|
+
nav.side button { white-space: nowrap; flex: none; }
|
|
2932
|
+
.split { grid-template-columns: minmax(0, 1fr); }
|
|
2933
|
+
main { padding: 14px; }
|
|
2934
|
+
table, thead, tbody, tr, th, td { display: block; }
|
|
2935
|
+
thead { display: none; }
|
|
2936
|
+
tr {
|
|
2937
|
+
border: 1px solid var(--line); border-radius: 12px; margin-bottom: 8px; padding: 6px;
|
|
2938
|
+
background: var(--bg-elev); min-width: 0;
|
|
2939
|
+
}
|
|
2940
|
+
td {
|
|
2941
|
+
border: 0; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px;
|
|
2942
|
+
min-width: 0; overflow-wrap: anywhere;
|
|
2943
|
+
}
|
|
2944
|
+
td::before { content: attr(data-label); color: var(--faint); font-size: 12px; flex: none; }
|
|
2945
|
+
.route { grid-template-columns: 72px 1fr; }
|
|
2946
|
+
.route button { grid-column: 1 / -1; }
|
|
2947
|
+
}
|
|
2948
|
+
</style>
|
|
2949
|
+
</head>
|
|
2950
|
+
<body>
|
|
2951
|
+
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2952
|
+
<header class="top">
|
|
2953
|
+
<div class="brand">
|
|
2954
|
+
<div class="brand-id">
|
|
2955
|
+
<strong>${name}</strong>
|
|
2956
|
+
<span>Admin</span>
|
|
2957
|
+
</div>
|
|
2958
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
2959
|
+
</div>
|
|
2960
|
+
<div class="controls">
|
|
2961
|
+
<label class="field">Namespace
|
|
2962
|
+
<select id="namespace" aria-label="Namespace"></select>
|
|
2963
|
+
</label>
|
|
2964
|
+
<label class="field">Admin key
|
|
2965
|
+
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2966
|
+
</label>
|
|
2967
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Toggle color theme">Theme</button>
|
|
2968
|
+
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
2969
|
+
</div>
|
|
2970
|
+
</header>
|
|
2971
|
+
<div class="layout">
|
|
2972
|
+
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2973
|
+
<button type="button" data-view="overview" aria-current="true">Overview</button>
|
|
2974
|
+
<button type="button" data-view="state">State</button>
|
|
2975
|
+
<button type="button" data-view="clock">Clock</button>
|
|
2976
|
+
<button type="button" data-view="faults">Faults</button>
|
|
2977
|
+
<button type="button" data-view="journal">Journal</button>
|
|
2978
|
+
<button type="button" data-view="routes">Routes</button>
|
|
2979
|
+
</nav>
|
|
2980
|
+
<main id="views">
|
|
2981
|
+
<p class="banner" id="banner" role="alert"></p>
|
|
2982
|
+
<section class="view" id="view-overview">
|
|
2983
|
+
<h2>Overview</h2>
|
|
2984
|
+
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
2985
|
+
<div class="grid" id="overview-cards"></div>
|
|
2986
|
+
<h3>Recent requests</h3>
|
|
2987
|
+
<div id="overview-requests"></div>
|
|
2988
|
+
</section>
|
|
2989
|
+
<section class="view" id="view-state" hidden>
|
|
2990
|
+
<h2>State</h2>
|
|
2991
|
+
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
2992
|
+
<div class="split">
|
|
2993
|
+
<div id="collections"></div>
|
|
2994
|
+
<div>
|
|
2995
|
+
<div id="schema"></div>
|
|
2996
|
+
<div class="row-actions">
|
|
2997
|
+
<button class="btn-primary" id="new-record" type="button">New record</button>
|
|
2998
|
+
</div>
|
|
2999
|
+
<div id="records"></div>
|
|
3000
|
+
</div>
|
|
3001
|
+
</div>
|
|
3002
|
+
</section>
|
|
3003
|
+
<section class="view" id="view-clock" hidden>
|
|
3004
|
+
<h2>Clock</h2>
|
|
3005
|
+
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3006
|
+
<div class="card" id="clock-readout"></div>
|
|
3007
|
+
<div class="row-actions">
|
|
3008
|
+
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
3009
|
+
<button class="btn" type="button" data-advance="900000">+15m</button>
|
|
3010
|
+
<button class="btn" type="button" data-advance="3600000">+1h</button>
|
|
3011
|
+
<button class="btn" type="button" data-advance="86400000">+1d</button>
|
|
3012
|
+
<button class="btn" type="button" data-advance="-3600000">\u22121h</button>
|
|
3013
|
+
<button class="btn" id="freeze" type="button">Freeze</button>
|
|
3014
|
+
<button class="btn-quiet" id="clock-reset" type="button">Reset</button>
|
|
3015
|
+
</div>
|
|
3016
|
+
<form id="clock-set" class="row-actions">
|
|
3017
|
+
<label class="field">Set instant
|
|
3018
|
+
<input id="clock-instant" type="text" placeholder="2026-01-01T00:00:00Z or epoch ms" size="32">
|
|
3019
|
+
</label>
|
|
3020
|
+
<button class="btn-primary" type="submit">Set</button>
|
|
3021
|
+
</form>
|
|
3022
|
+
</section>
|
|
3023
|
+
<section class="view" id="view-faults" hidden>
|
|
3024
|
+
<h2>Faults</h2>
|
|
3025
|
+
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3026
|
+
<form id="fault-form" class="card stack">
|
|
3027
|
+
<label class="field">Preset
|
|
3028
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3029
|
+
</label>
|
|
3030
|
+
<label class="field">Status
|
|
3031
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3032
|
+
</label>
|
|
3033
|
+
<label class="field">Path prefix
|
|
3034
|
+
<input id="fault-path" placeholder="/v1">
|
|
3035
|
+
</label>
|
|
3036
|
+
<label class="field">Method
|
|
3037
|
+
<input id="fault-method" placeholder="GET">
|
|
3038
|
+
</label>
|
|
3039
|
+
<div class="dialog-actions">
|
|
3040
|
+
<button class="btn-primary" type="submit">Add fault</button>
|
|
3041
|
+
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
3042
|
+
</div>
|
|
3043
|
+
</form>
|
|
3044
|
+
<div id="fault-list"></div>
|
|
3045
|
+
</section>
|
|
3046
|
+
<section class="view" id="view-journal" hidden>
|
|
3047
|
+
<h2>Journal</h2>
|
|
3048
|
+
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3049
|
+
<div class="row-actions">
|
|
3050
|
+
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3051
|
+
</div>
|
|
3052
|
+
<div id="journal"></div>
|
|
3053
|
+
</section>
|
|
3054
|
+
<section class="view" id="view-routes" hidden>
|
|
3055
|
+
<h2>Routes</h2>
|
|
3056
|
+
<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>
|
|
3057
|
+
<div id="route-list"></div>
|
|
3058
|
+
<form id="route-form" class="card stack">
|
|
3059
|
+
<strong id="route-title">Select a route</strong>
|
|
3060
|
+
<label class="field">JSON body
|
|
3061
|
+
<textarea id="route-body" placeholder="{ }"></textarea>
|
|
3062
|
+
</label>
|
|
3063
|
+
<div class="dialog-actions">
|
|
3064
|
+
<button class="btn-primary" type="submit">Send</button>
|
|
3065
|
+
</div>
|
|
3066
|
+
<pre id="route-result">Response will show here.</pre>
|
|
3067
|
+
</form>
|
|
3068
|
+
</section>
|
|
3069
|
+
</main>
|
|
3070
|
+
</div>
|
|
3071
|
+
</div>
|
|
3072
|
+
<dialog id="editor">
|
|
3073
|
+
<form method="dialog" id="editor-form">
|
|
3074
|
+
<strong id="editor-title">Record</strong>
|
|
3075
|
+
<label class="field">Id
|
|
3076
|
+
<input id="editor-id" required>
|
|
3077
|
+
</label>
|
|
3078
|
+
<label class="field">JSON value
|
|
3079
|
+
<textarea id="editor-value"></textarea>
|
|
3080
|
+
</label>
|
|
3081
|
+
<div class="dialog-actions">
|
|
3082
|
+
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
3083
|
+
<button class="btn-quiet" type="submit" value="cancel">Cancel</button>
|
|
3084
|
+
<button class="btn-primary" type="submit" value="save">Save</button>
|
|
3085
|
+
</div>
|
|
3086
|
+
</form>
|
|
3087
|
+
</dialog>
|
|
3088
|
+
${script}
|
|
3089
|
+
</body>
|
|
3090
|
+
</html>`;
|
|
3091
|
+
};
|
|
3092
|
+
var htmlResponse = (body) => new Response(body, {
|
|
3093
|
+
status: 200,
|
|
3094
|
+
headers: {
|
|
3095
|
+
"content-type": "text/html; charset=utf-8",
|
|
3096
|
+
"cache-control": "no-store"
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
var json2 = (body) => new Response(JSON.stringify(body), {
|
|
3100
|
+
status: 200,
|
|
3101
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
3102
|
+
});
|
|
3103
|
+
var adminUiRoutes = (service, ui) => {
|
|
3104
|
+
const shell = () => renderAdminDocument(service);
|
|
3105
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3106
|
+
return {
|
|
3107
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
3108
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
3109
|
+
"GET /ui/manifest": () => json2({
|
|
3110
|
+
service,
|
|
3111
|
+
panels: (ui?.panels ?? []).map((panel) => ({
|
|
3112
|
+
id: panel.id,
|
|
3113
|
+
title: panel.title,
|
|
3114
|
+
...panel.description !== void 0 ? { description: panel.description } : {},
|
|
3115
|
+
html: panel.html,
|
|
3116
|
+
...panel.script !== void 0 ? { script: panel.script } : {}
|
|
3117
|
+
}))
|
|
3118
|
+
})
|
|
3119
|
+
};
|
|
3120
|
+
};
|
|
3121
|
+
|
|
1389
3122
|
// ../../openapi/metadata/dist/types.js
|
|
1390
3123
|
var EXTENSION_KEYS = {
|
|
1391
3124
|
operation: "x-mockingbird",
|
|
@@ -1398,11 +3131,11 @@ var EXTENSION_KEYS = {
|
|
|
1398
3131
|
};
|
|
1399
3132
|
|
|
1400
3133
|
// ../../openapi/metadata/dist/read.js
|
|
1401
|
-
var
|
|
3134
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1402
3135
|
var extensionOf = (holder, key) => holder[key];
|
|
1403
3136
|
var operationMetadata = (operation) => {
|
|
1404
3137
|
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
1405
|
-
const ext =
|
|
3138
|
+
const ext = isRecord3(raw) ? raw : {};
|
|
1406
3139
|
const supported = ext.supported ?? true;
|
|
1407
3140
|
const parity = ext.parity ?? {};
|
|
1408
3141
|
return {
|
|
@@ -1512,9 +3245,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
1512
3245
|
this.name = "OperationRegistryError";
|
|
1513
3246
|
}
|
|
1514
3247
|
};
|
|
1515
|
-
var verifyOperations = (
|
|
3248
|
+
var verifyOperations = (document3, handlers) => {
|
|
1516
3249
|
const problems = [];
|
|
1517
|
-
const operations = listOperations(
|
|
3250
|
+
const operations = listOperations(document3);
|
|
1518
3251
|
const seen = /* @__PURE__ */ new Set();
|
|
1519
3252
|
for (const operation of operations) {
|
|
1520
3253
|
if (seen.has(operation.operationId))
|
|
@@ -1623,15 +3356,449 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1623
3356
|
});
|
|
1624
3357
|
};
|
|
1625
3358
|
|
|
3359
|
+
// ../core/dist/state-view.js
|
|
3360
|
+
var COLLECTION_NAME = /^[A-Za-z0-9_.@:-]{1,200}$/;
|
|
3361
|
+
var RECORD_ID = /^[^\s/]{1,256}$/;
|
|
3362
|
+
var SAMPLE = 25;
|
|
3363
|
+
var WALK_LIMIT = 1e4;
|
|
3364
|
+
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
3365
|
+
var fail2 = (status, message) => ({ ok: false, status, message });
|
|
3366
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3367
|
+
var kindOf = (value) => {
|
|
3368
|
+
if (value === null)
|
|
3369
|
+
return "null";
|
|
3370
|
+
if (Array.isArray(value))
|
|
3371
|
+
return "array";
|
|
3372
|
+
switch (typeof value) {
|
|
3373
|
+
case "string":
|
|
3374
|
+
return "string";
|
|
3375
|
+
case "number":
|
|
3376
|
+
return Number.isFinite(value) ? "number" : "unknown";
|
|
3377
|
+
case "boolean":
|
|
3378
|
+
return "boolean";
|
|
3379
|
+
case "object":
|
|
3380
|
+
return "object";
|
|
3381
|
+
default:
|
|
3382
|
+
return "unknown";
|
|
3383
|
+
}
|
|
3384
|
+
};
|
|
3385
|
+
var humanize = (name) => {
|
|
3386
|
+
const words = name.replace(/[@:_-]+/g, " ").trim();
|
|
3387
|
+
if (words === "")
|
|
3388
|
+
return name;
|
|
3389
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
3390
|
+
};
|
|
3391
|
+
var jsonValue = (value) => {
|
|
3392
|
+
if (value === void 0)
|
|
3393
|
+
return fail2(400, "value is required");
|
|
3394
|
+
try {
|
|
3395
|
+
return { ok: true, value: JSON.parse(JSON.stringify(value)) };
|
|
3396
|
+
} catch {
|
|
3397
|
+
return fail2(400, "value must be JSON");
|
|
3398
|
+
}
|
|
3399
|
+
};
|
|
3400
|
+
var ctorName = (value) => {
|
|
3401
|
+
const ctor = value.constructor;
|
|
3402
|
+
return typeof ctor?.name === "string" ? ctor.name : "";
|
|
3403
|
+
};
|
|
3404
|
+
var isSqlite = (value) => "prepare" in value && typeof value.prepare === "function" && "transaction" in value;
|
|
3405
|
+
var discoverCollections = (root) => {
|
|
3406
|
+
const found = [];
|
|
3407
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3408
|
+
const visit = (value, depth) => {
|
|
3409
|
+
if (found.length + seen.size > WALK_LIMIT)
|
|
3410
|
+
return;
|
|
3411
|
+
if (value === null || typeof value !== "object")
|
|
3412
|
+
return;
|
|
3413
|
+
if (seen.has(value))
|
|
3414
|
+
return;
|
|
3415
|
+
seen.add(value);
|
|
3416
|
+
if (value instanceof Collection) {
|
|
3417
|
+
found.push(value);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
if (value instanceof Map) {
|
|
3421
|
+
for (const child of value.values())
|
|
3422
|
+
visit(child, depth + 1);
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
if (value instanceof Set) {
|
|
3426
|
+
for (const child of value.values())
|
|
3427
|
+
visit(child, depth + 1);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
if (depth > 6 || isSqlite(value) || SKIP_CTORS.has(ctorName(value)))
|
|
3431
|
+
return;
|
|
3432
|
+
if (Array.isArray(value)) {
|
|
3433
|
+
const cap = Math.min(value.length, 32);
|
|
3434
|
+
for (let i = 0; i < cap; i++)
|
|
3435
|
+
visit(value[i], depth + 1);
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
let children;
|
|
3439
|
+
try {
|
|
3440
|
+
children = Object.values(value);
|
|
3441
|
+
} catch {
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
for (const child of children)
|
|
3445
|
+
visit(child, depth + 1);
|
|
3446
|
+
};
|
|
3447
|
+
visit(root, 0);
|
|
3448
|
+
return found;
|
|
3449
|
+
};
|
|
3450
|
+
var storedNames = (sqlite, namespace) => {
|
|
3451
|
+
const records = sqlite.prepare("SELECT DISTINCT collection AS name FROM mockingbird_records WHERE namespace = ?").all(namespace);
|
|
3452
|
+
const sequences = sqlite.prepare("SELECT DISTINCT name FROM mockingbird_sequences WHERE namespace = ? AND kind = 'collection'").all(namespace);
|
|
3453
|
+
return [...records, ...sequences].map((row) => row.name);
|
|
3454
|
+
};
|
|
3455
|
+
var blankAccum = (name) => ({
|
|
3456
|
+
name,
|
|
3457
|
+
kinds: /* @__PURE__ */ new Set(),
|
|
3458
|
+
present: 0,
|
|
3459
|
+
nested: /* @__PURE__ */ new Map()
|
|
3460
|
+
});
|
|
3461
|
+
var absorb = (accum, value) => {
|
|
3462
|
+
const kind = kindOf(value);
|
|
3463
|
+
accum.kinds.add(kind);
|
|
3464
|
+
accum.present++;
|
|
3465
|
+
if (!isRecord4(value))
|
|
3466
|
+
return;
|
|
3467
|
+
for (const [key, child] of Object.entries(value)) {
|
|
3468
|
+
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
3469
|
+
absorb(nested, child);
|
|
3470
|
+
accum.nested.set(key, nested);
|
|
3471
|
+
}
|
|
3472
|
+
};
|
|
3473
|
+
var finishField = (accum, samples) => {
|
|
3474
|
+
const kinds = [...accum.kinds];
|
|
3475
|
+
const kind = kinds.length === 1 ? kinds[0] : "unknown";
|
|
3476
|
+
const field = {
|
|
3477
|
+
name: accum.name,
|
|
3478
|
+
kind,
|
|
3479
|
+
optional: accum.present < samples || kinds.includes("null")
|
|
3480
|
+
};
|
|
3481
|
+
if (accum.nested.size > 0) {
|
|
3482
|
+
field.fields = [...accum.nested.values()].sort((a, b) => a.name.localeCompare(b.name)).map((child) => finishField(child, accum.present));
|
|
3483
|
+
}
|
|
3484
|
+
return field;
|
|
3485
|
+
};
|
|
3486
|
+
var inferFields = (records) => {
|
|
3487
|
+
if (records.length === 0)
|
|
3488
|
+
return [];
|
|
3489
|
+
const sample = records.slice(0, SAMPLE);
|
|
3490
|
+
const objects = sample.filter((record) => isRecord4(record.value));
|
|
3491
|
+
if (objects.length === 0) {
|
|
3492
|
+
const kinds = new Set(sample.map((record) => kindOf(record.value)));
|
|
3493
|
+
const list = [...kinds];
|
|
3494
|
+
return [
|
|
3495
|
+
{
|
|
3496
|
+
name: "(value)",
|
|
3497
|
+
kind: list.length === 1 ? list[0] : "unknown",
|
|
3498
|
+
optional: false
|
|
3499
|
+
}
|
|
3500
|
+
];
|
|
3501
|
+
}
|
|
3502
|
+
const root = /* @__PURE__ */ new Map();
|
|
3503
|
+
for (const record of objects) {
|
|
3504
|
+
for (const [key, value] of Object.entries(record.value)) {
|
|
3505
|
+
const accum = root.get(key) ?? blankAccum(key);
|
|
3506
|
+
absorb(accum, value);
|
|
3507
|
+
root.set(key, accum);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
return [...root.values()].sort((a, b) => a.name.localeCompare(b.name)).map((accum) => finishField(accum, objects.length));
|
|
3511
|
+
};
|
|
3512
|
+
var mergeFields = (declared, inferred) => {
|
|
3513
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3514
|
+
for (const field of inferred)
|
|
3515
|
+
byName.set(field.name, field);
|
|
3516
|
+
const ordered = [];
|
|
3517
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3518
|
+
for (const field of declared ?? []) {
|
|
3519
|
+
const found = byName.get(field.name);
|
|
3520
|
+
seen.add(field.name);
|
|
3521
|
+
if (!found) {
|
|
3522
|
+
ordered.push({ ...field, optional: field.optional });
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
ordered.push({
|
|
3526
|
+
...found,
|
|
3527
|
+
kind: field.kind === "unknown" ? found.kind : field.kind,
|
|
3528
|
+
optional: field.optional || found.optional,
|
|
3529
|
+
...field.description !== void 0 ? { description: field.description } : {},
|
|
3530
|
+
...field.meta !== void 0 ? { meta: field.meta } : {},
|
|
3531
|
+
...field.fields !== void 0 ? { fields: mergeFields(field.fields, found.fields ?? []) } : {}
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
for (const field of inferred) {
|
|
3535
|
+
if (!seen.has(field.name))
|
|
3536
|
+
ordered.push(field);
|
|
3537
|
+
}
|
|
3538
|
+
return ordered;
|
|
3539
|
+
};
|
|
3540
|
+
var collectionOf = (sqlite, namespace, name) => new Collection(sqlite, namespace, name);
|
|
3541
|
+
var readRecord = (collection, id) => {
|
|
3542
|
+
const rows = collection.list({ where: (_value, _seq) => true, order: "oldest" });
|
|
3543
|
+
const row = rows.find((entry) => entry.id === id);
|
|
3544
|
+
return row ? { id: row.id, seq: row.seq, value: row.value } : void 0;
|
|
3545
|
+
};
|
|
3546
|
+
var inspectState = (scope) => {
|
|
3547
|
+
const declared = /* @__PURE__ */ new Map();
|
|
3548
|
+
for (const declaration of scope.declarations ?? []) {
|
|
3549
|
+
if (COLLECTION_NAME.test(declaration.name))
|
|
3550
|
+
declared.set(declaration.name, declaration);
|
|
3551
|
+
}
|
|
3552
|
+
const names = new Set(declared.keys());
|
|
3553
|
+
for (const name of storedNames(scope.sqlite, scope.storageNamespace))
|
|
3554
|
+
names.add(name);
|
|
3555
|
+
for (const collection of discoverCollections(scope.root)) {
|
|
3556
|
+
if (collection.namespace === scope.storageNamespace)
|
|
3557
|
+
names.add(collection.collectionName);
|
|
3558
|
+
}
|
|
3559
|
+
const collections = [];
|
|
3560
|
+
for (const name of names) {
|
|
3561
|
+
if (!COLLECTION_NAME.test(name))
|
|
3562
|
+
continue;
|
|
3563
|
+
const declaration = declared.get(name);
|
|
3564
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3565
|
+
const records = collection.list({ order: "newest" }).slice(0, SAMPLE).map((row) => ({ id: row.id, seq: row.seq, value: row.value }));
|
|
3566
|
+
const inferred = inferFields(records);
|
|
3567
|
+
const fields = mergeFields(declaration?.fields, inferred);
|
|
3568
|
+
const source = declaration && records.length > 0 ? "mixed" : declaration ? "declared" : "inferred";
|
|
3569
|
+
collections.push({
|
|
3570
|
+
name,
|
|
3571
|
+
label: declaration?.label ?? humanize(name),
|
|
3572
|
+
...declaration?.description !== void 0 ? { description: declaration.description } : {},
|
|
3573
|
+
count: collection.count(),
|
|
3574
|
+
source,
|
|
3575
|
+
fields,
|
|
3576
|
+
...declaration?.meta !== void 0 ? { meta: declaration.meta } : {}
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
const order = /* @__PURE__ */ new Map();
|
|
3580
|
+
for (const [index, item] of (scope.declarations ?? []).entries())
|
|
3581
|
+
order.set(item.name, index);
|
|
3582
|
+
collections.sort((a, b) => {
|
|
3583
|
+
const left = order.get(a.name);
|
|
3584
|
+
const right = order.get(b.name);
|
|
3585
|
+
if (left !== void 0 && right !== void 0)
|
|
3586
|
+
return left - right;
|
|
3587
|
+
if (left !== void 0)
|
|
3588
|
+
return -1;
|
|
3589
|
+
if (right !== void 0)
|
|
3590
|
+
return 1;
|
|
3591
|
+
return a.name.localeCompare(b.name);
|
|
3592
|
+
});
|
|
3593
|
+
return {
|
|
3594
|
+
namespace: scope.namespace,
|
|
3595
|
+
storageNamespace: scope.storageNamespace,
|
|
3596
|
+
collections
|
|
3597
|
+
};
|
|
3598
|
+
};
|
|
3599
|
+
var pageOf = (sqlite, storageNamespace, name, query) => {
|
|
3600
|
+
if (!COLLECTION_NAME.test(name))
|
|
3601
|
+
return fail2(400, "collection name is not allowed");
|
|
3602
|
+
const limit = query.limit ?? 50;
|
|
3603
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
3604
|
+
return fail2(400, "limit must be an integer from 1 to 200");
|
|
3605
|
+
}
|
|
3606
|
+
const order = query.order ?? "newest";
|
|
3607
|
+
if (order !== "newest" && order !== "oldest")
|
|
3608
|
+
return fail2(400, 'order must be "newest" or "oldest"');
|
|
3609
|
+
const collection = collectionOf(sqlite, storageNamespace, name);
|
|
3610
|
+
const rows = collection.list({ order }).filter((row) => {
|
|
3611
|
+
if (query.after === void 0)
|
|
3612
|
+
return true;
|
|
3613
|
+
return order === "newest" ? row.seq < query.after : row.seq > query.after;
|
|
3614
|
+
});
|
|
3615
|
+
const slice = rows.slice(0, limit);
|
|
3616
|
+
const last = slice.at(-1);
|
|
3617
|
+
return {
|
|
3618
|
+
ok: true,
|
|
3619
|
+
value: {
|
|
3620
|
+
collection: name,
|
|
3621
|
+
count: collection.count(),
|
|
3622
|
+
records: slice.map((row) => ({ id: row.id, seq: row.seq, value: row.value })),
|
|
3623
|
+
next: rows.length > limit && last ? last.seq : null
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
};
|
|
3627
|
+
var readStatePage = (scope, name, query = {}) => pageOf(scope.sqlite, scope.storageNamespace, name, query);
|
|
3628
|
+
var readStateRecord = (scope, name, id) => {
|
|
3629
|
+
if (!COLLECTION_NAME.test(name))
|
|
3630
|
+
return fail2(400, "collection name is not allowed");
|
|
3631
|
+
if (!RECORD_ID.test(id))
|
|
3632
|
+
return fail2(400, "record id is not allowed");
|
|
3633
|
+
const record = readRecord(collectionOf(scope.sqlite, scope.storageNamespace, name), id);
|
|
3634
|
+
return record ? { ok: true, value: record } : fail2(404, `no record ${id} in ${name}`);
|
|
3635
|
+
};
|
|
3636
|
+
var requireId = (id) => RECORD_ID.test(id) ? { ok: true, value: id } : fail2(400, "id must be a single path segment");
|
|
3637
|
+
var createStateRecord = (scope, name, input) => {
|
|
3638
|
+
if (!COLLECTION_NAME.test(name))
|
|
3639
|
+
return fail2(400, "collection name is not allowed");
|
|
3640
|
+
const parsed = jsonValue(input.value);
|
|
3641
|
+
if (!parsed.ok)
|
|
3642
|
+
return parsed;
|
|
3643
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3644
|
+
const id = input.id ?? `${name}_${collection.nextSequence()}`;
|
|
3645
|
+
const checked = requireId(id);
|
|
3646
|
+
if (!checked.ok)
|
|
3647
|
+
return checked;
|
|
3648
|
+
if (collection.has(checked.value))
|
|
3649
|
+
return fail2(409, `record ${checked.value} already exists`);
|
|
3650
|
+
const stored = collection.insert(checked.value, parsed.value);
|
|
3651
|
+
return { ok: true, value: { id: checked.value, seq: stored.seq, value: stored.value } };
|
|
3652
|
+
};
|
|
3653
|
+
var replaceStateRecord = (scope, name, id, value) => {
|
|
3654
|
+
if (!COLLECTION_NAME.test(name))
|
|
3655
|
+
return fail2(400, "collection name is not allowed");
|
|
3656
|
+
const checked = requireId(id);
|
|
3657
|
+
if (!checked.ok)
|
|
3658
|
+
return checked;
|
|
3659
|
+
const parsed = jsonValue(value);
|
|
3660
|
+
if (!parsed.ok)
|
|
3661
|
+
return parsed;
|
|
3662
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3663
|
+
if (!collection.has(checked.value))
|
|
3664
|
+
return fail2(404, `no record ${checked.value} in ${name}`);
|
|
3665
|
+
const stored = collection.update(checked.value, parsed.value);
|
|
3666
|
+
if (!stored)
|
|
3667
|
+
return fail2(404, `no record ${checked.value} in ${name}`);
|
|
3668
|
+
return { ok: true, value: { id: checked.value, seq: stored.seq, value: stored.value } };
|
|
3669
|
+
};
|
|
3670
|
+
var patchStateRecord = (scope, name, id, value) => {
|
|
3671
|
+
const current = readStateRecord(scope, name, id);
|
|
3672
|
+
if (!current.ok)
|
|
3673
|
+
return current;
|
|
3674
|
+
const parsed = jsonValue(value);
|
|
3675
|
+
if (!parsed.ok)
|
|
3676
|
+
return parsed;
|
|
3677
|
+
const next = isRecord4(current.value.value) && isRecord4(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
3678
|
+
return replaceStateRecord(scope, name, id, next);
|
|
3679
|
+
};
|
|
3680
|
+
var deleteStateRecord = (scope, name, id) => {
|
|
3681
|
+
if (!COLLECTION_NAME.test(name))
|
|
3682
|
+
return fail2(400, "collection name is not allowed");
|
|
3683
|
+
const checked = requireId(id);
|
|
3684
|
+
if (!checked.ok)
|
|
3685
|
+
return checked;
|
|
3686
|
+
const collection = collectionOf(scope.sqlite, scope.storageNamespace, name);
|
|
3687
|
+
return collection.delete(checked.value) ? { ok: true, value: { id: checked.value } } : fail2(404, `no record ${checked.value} in ${name}`);
|
|
3688
|
+
};
|
|
3689
|
+
var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
3690
|
+
status,
|
|
3691
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
3692
|
+
});
|
|
3693
|
+
var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
3694
|
+
var fromResult = (result, status) => result.ok ? json3(status, result.value) : adminError2(result.status, result.message);
|
|
3695
|
+
var integerParam = (value) => {
|
|
3696
|
+
if (value === null)
|
|
3697
|
+
return void 0;
|
|
3698
|
+
if (!/^-?\d+$/.test(value))
|
|
3699
|
+
return "bad";
|
|
3700
|
+
return Number(value);
|
|
3701
|
+
};
|
|
3702
|
+
var stateAdminRoutes = (routes) => {
|
|
3703
|
+
const open = (namespace) => {
|
|
3704
|
+
try {
|
|
3705
|
+
return { ok: true, value: routes.open(namespace) };
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
return fail2(400, error instanceof Error ? error.message : String(error));
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
const wrote = (namespace) => {
|
|
3711
|
+
routes.afterWrite?.(namespace);
|
|
3712
|
+
};
|
|
3713
|
+
return {
|
|
3714
|
+
"GET /state": ({ namespace }) => {
|
|
3715
|
+
const scope = open(namespace);
|
|
3716
|
+
return fromResult(scope.ok ? { ok: true, value: inspectState(scope.value) } : scope, 200);
|
|
3717
|
+
},
|
|
3718
|
+
"GET /state/:collection": ({ namespace, params, url }) => {
|
|
3719
|
+
const scope = open(namespace);
|
|
3720
|
+
if (!scope.ok)
|
|
3721
|
+
return fromResult(scope, 200);
|
|
3722
|
+
const limit = integerParam(url.searchParams.get("limit"));
|
|
3723
|
+
const after = integerParam(url.searchParams.get("after"));
|
|
3724
|
+
if (limit === "bad")
|
|
3725
|
+
return adminError2(400, "limit: expected an integer");
|
|
3726
|
+
if (after === "bad")
|
|
3727
|
+
return adminError2(400, "after: expected a sequence number");
|
|
3728
|
+
const order = url.searchParams.get("order");
|
|
3729
|
+
return fromResult(readStatePage(scope.value, params.collection ?? "", {
|
|
3730
|
+
...limit !== void 0 ? { limit } : {},
|
|
3731
|
+
...after !== void 0 ? { after } : {},
|
|
3732
|
+
...order === "newest" || order === "oldest" ? { order } : {}
|
|
3733
|
+
}), 200);
|
|
3734
|
+
},
|
|
3735
|
+
"GET /state/:collection/:id": ({ namespace, params }) => {
|
|
3736
|
+
const scope = open(namespace);
|
|
3737
|
+
if (!scope.ok)
|
|
3738
|
+
return fromResult(scope, 200);
|
|
3739
|
+
return fromResult(readStateRecord(scope.value, params.collection ?? "", params.id ?? ""), 200);
|
|
3740
|
+
},
|
|
3741
|
+
"POST /state/:collection": ({ namespace, params, body }) => {
|
|
3742
|
+
const scope = open(namespace);
|
|
3743
|
+
if (!scope.ok)
|
|
3744
|
+
return fromResult(scope, 201);
|
|
3745
|
+
if (!isRecord4(body) || !("value" in body)) {
|
|
3746
|
+
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
3747
|
+
}
|
|
3748
|
+
const id = body.id;
|
|
3749
|
+
if (id !== void 0 && typeof id !== "string")
|
|
3750
|
+
return adminError2(400, "id must be a string");
|
|
3751
|
+
const created = createStateRecord(scope.value, params.collection ?? "", {
|
|
3752
|
+
...typeof id === "string" ? { id } : {},
|
|
3753
|
+
value: body.value
|
|
3754
|
+
});
|
|
3755
|
+
if (created.ok)
|
|
3756
|
+
wrote(namespace);
|
|
3757
|
+
return fromResult(created, 201);
|
|
3758
|
+
},
|
|
3759
|
+
"PUT /state/:collection/:id": ({ namespace, params, body }) => {
|
|
3760
|
+
const scope = open(namespace);
|
|
3761
|
+
if (!scope.ok)
|
|
3762
|
+
return fromResult(scope, 200);
|
|
3763
|
+
if (!isRecord4(body) || !("value" in body))
|
|
3764
|
+
return adminError2(400, 'expected {"value": <json>}');
|
|
3765
|
+
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
3766
|
+
if (replaced.ok)
|
|
3767
|
+
wrote(namespace);
|
|
3768
|
+
return fromResult(replaced, 200);
|
|
3769
|
+
},
|
|
3770
|
+
"PATCH /state/:collection/:id": ({ namespace, params, body }) => {
|
|
3771
|
+
const scope = open(namespace);
|
|
3772
|
+
if (!scope.ok)
|
|
3773
|
+
return fromResult(scope, 200);
|
|
3774
|
+
if (!isRecord4(body) || !("value" in body))
|
|
3775
|
+
return adminError2(400, 'expected {"value": <json>}');
|
|
3776
|
+
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
3777
|
+
if (patched.ok)
|
|
3778
|
+
wrote(namespace);
|
|
3779
|
+
return fromResult(patched, 200);
|
|
3780
|
+
},
|
|
3781
|
+
"DELETE /state/:collection/:id": ({ namespace, params }) => {
|
|
3782
|
+
const scope = open(namespace);
|
|
3783
|
+
if (!scope.ok)
|
|
3784
|
+
return fromResult(scope, 200);
|
|
3785
|
+
const removed = deleteStateRecord(scope.value, params.collection ?? "", params.id ?? "");
|
|
3786
|
+
if (removed.ok)
|
|
3787
|
+
wrote(namespace);
|
|
3788
|
+
return fromResult(removed, 200);
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
};
|
|
3792
|
+
|
|
1626
3793
|
// ../core/dist/version.js
|
|
1627
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3794
|
+
var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
|
|
1628
3795
|
|
|
1629
3796
|
// ../core/dist/webhooks.js
|
|
1630
|
-
var
|
|
1631
|
-
var
|
|
1632
|
-
var
|
|
3797
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
3798
|
+
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3799
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1633
3800
|
var parseEndpoint = (value) => {
|
|
1634
|
-
if (!
|
|
3801
|
+
if (!isRecord5(value) || typeof value.url !== "string")
|
|
1635
3802
|
return "each endpoint needs a url";
|
|
1636
3803
|
try {
|
|
1637
3804
|
new URL(value.url);
|
|
@@ -1648,18 +3815,18 @@ var parseEndpoint = (value) => {
|
|
|
1648
3815
|
const events = value.events ?? value.enabledEvents;
|
|
1649
3816
|
if (Array.isArray(events))
|
|
1650
3817
|
endpoint.events = events.map(String);
|
|
1651
|
-
if (
|
|
3818
|
+
if (isRecord5(value.tags)) {
|
|
1652
3819
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
1653
3820
|
}
|
|
1654
3821
|
if (typeof value.account === "string")
|
|
1655
3822
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
1656
|
-
if (
|
|
3823
|
+
if (isRecord5(value.headers)) {
|
|
1657
3824
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
1658
3825
|
}
|
|
1659
3826
|
return endpoint;
|
|
1660
3827
|
};
|
|
1661
3828
|
var webhookAdminRoutes = (hub) => ({
|
|
1662
|
-
"GET /webhooks": ({ url, namespace }) =>
|
|
3829
|
+
"GET /webhooks": ({ url, namespace }) => json4(200, {
|
|
1663
3830
|
deliveries: hub.deliveries(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((d) => {
|
|
1664
3831
|
const type = url.searchParams.get("type");
|
|
1665
3832
|
return type === null || d.type === type;
|
|
@@ -1667,51 +3834,51 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
1667
3834
|
}),
|
|
1668
3835
|
"GET /webhooks/events": ({ url, namespace }) => {
|
|
1669
3836
|
const type = url.searchParams.get("type");
|
|
1670
|
-
return
|
|
3837
|
+
return json4(200, {
|
|
1671
3838
|
events: hub.messages(url.searchParams.get("all") === "1" ? void 0 : namespace).filter((m) => type === null || m.type === type).map((m) => ({ ...m, payload: parsePayload(m) }))
|
|
1672
3839
|
});
|
|
1673
3840
|
},
|
|
1674
3841
|
"POST /webhooks/:id/replay": async ({ params }) => {
|
|
1675
3842
|
const replayed = await hub.replay(params.id);
|
|
1676
|
-
return replayed ?
|
|
3843
|
+
return replayed ? json4(200, replayed) : adminError3(404, `no delivery ${params.id}`);
|
|
1677
3844
|
},
|
|
1678
3845
|
"POST /webhooks/flush": async () => {
|
|
1679
3846
|
await hub.flush();
|
|
1680
|
-
return
|
|
3847
|
+
return json4(200, { status: "ok" });
|
|
1681
3848
|
},
|
|
1682
3849
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
1683
|
-
if (!
|
|
1684
|
-
return
|
|
3850
|
+
if (!isRecord5(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
3851
|
+
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
1685
3852
|
}
|
|
1686
3853
|
const fault = { mode: body.mode };
|
|
1687
3854
|
if (typeof body.count === "number")
|
|
1688
3855
|
fault.count = body.count;
|
|
1689
3856
|
hub.fault(namespace, fault);
|
|
1690
|
-
return
|
|
3857
|
+
return json4(201, { namespace, ...fault });
|
|
1691
3858
|
},
|
|
1692
|
-
"GET /webhook-endpoints": ({ namespace }) =>
|
|
3859
|
+
"GET /webhook-endpoints": ({ namespace }) => json4(200, {
|
|
1693
3860
|
endpoints: hub.endpoints(namespace).map(({ secret, ...rest }) => ({
|
|
1694
3861
|
...rest,
|
|
1695
3862
|
secret: secret ? "(set)" : null
|
|
1696
3863
|
}))
|
|
1697
3864
|
}),
|
|
1698
3865
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
1699
|
-
const list = Array.isArray(body) ? body :
|
|
3866
|
+
const list = Array.isArray(body) ? body : isRecord5(body) ? body.endpoints : void 0;
|
|
1700
3867
|
if (!Array.isArray(list))
|
|
1701
|
-
return
|
|
3868
|
+
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
1702
3869
|
const parsed = [];
|
|
1703
3870
|
for (const each of list) {
|
|
1704
3871
|
const endpoint = parseEndpoint(each);
|
|
1705
3872
|
if (typeof endpoint === "string")
|
|
1706
|
-
return
|
|
3873
|
+
return adminError3(400, endpoint);
|
|
1707
3874
|
parsed.push(endpoint);
|
|
1708
3875
|
}
|
|
1709
3876
|
const set = hub.setEndpoints(namespace, parsed);
|
|
1710
|
-
return
|
|
3877
|
+
return json4(200, { endpoints: set.map((e) => ({ ...e, secret: e.secret ? "(set)" : null })) });
|
|
1711
3878
|
},
|
|
1712
3879
|
"DELETE /webhook-endpoints": ({ namespace }) => {
|
|
1713
3880
|
hub.setEndpoints(namespace, []);
|
|
1714
|
-
return
|
|
3881
|
+
return json4(200, { status: "ok" });
|
|
1715
3882
|
}
|
|
1716
3883
|
});
|
|
1717
3884
|
var parsePayload = (message) => {
|
|
@@ -1764,8 +3931,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
1764
3931
|
this.name = "TypeError";
|
|
1765
3932
|
}
|
|
1766
3933
|
};
|
|
1767
|
-
var operationMatcher = (
|
|
1768
|
-
const matchers = listOperations(
|
|
3934
|
+
var operationMatcher = (document3) => {
|
|
3935
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
1769
3936
|
operationId: operation.operationId,
|
|
1770
3937
|
method: operation.method.toUpperCase(),
|
|
1771
3938
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -1774,6 +3941,7 @@ var operationMatcher = (document2) => {
|
|
|
1774
3941
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
1775
3942
|
};
|
|
1776
3943
|
var createRuntime = (options) => {
|
|
3944
|
+
assertAdminUi(options.adminUi);
|
|
1777
3945
|
const sqlite = bootSqlite(options.sqlite);
|
|
1778
3946
|
const clock = options.clock ?? createClock();
|
|
1779
3947
|
const rng = createRng(options.seed ?? 0);
|
|
@@ -1859,10 +4027,10 @@ var createRuntime = (options) => {
|
|
|
1859
4027
|
const ensureBranch = (namespace, branch2, at) => {
|
|
1860
4028
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
1861
4029
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
1862
|
-
const
|
|
4030
|
+
const history2 = timeline(namespace);
|
|
1863
4031
|
if (branch2 === "main") {
|
|
1864
4032
|
if (at !== void 0) {
|
|
1865
|
-
const point =
|
|
4033
|
+
const point = history2.checkout("main", at);
|
|
1866
4034
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
1867
4035
|
captured.set(namespace, point.value.snapshot);
|
|
1868
4036
|
rng.setState(point.value.rngState);
|
|
@@ -1875,10 +4043,10 @@ var createRuntime = (options) => {
|
|
|
1875
4043
|
return namespace;
|
|
1876
4044
|
}
|
|
1877
4045
|
const storage = physicalBranch(namespace, branch2);
|
|
1878
|
-
if (!
|
|
4046
|
+
if (!history2.hasBranch(branch2)) {
|
|
1879
4047
|
if (at === void 0)
|
|
1880
|
-
|
|
1881
|
-
const point =
|
|
4048
|
+
history2.commit(capture(namespace));
|
|
4049
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
1882
4050
|
const branchRng = createRng(options.seed ?? 0);
|
|
1883
4051
|
if (point)
|
|
1884
4052
|
branchRng.setState(point.value.rngState);
|
|
@@ -1887,8 +4055,8 @@ var createRuntime = (options) => {
|
|
|
1887
4055
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1888
4056
|
if (point)
|
|
1889
4057
|
captured.set(storage, point.value.snapshot);
|
|
1890
|
-
} else if (at !== void 0 &&
|
|
1891
|
-
const point =
|
|
4058
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
4059
|
+
const point = history2.checkout(branch2, at);
|
|
1892
4060
|
if (!instances.has(storage)) {
|
|
1893
4061
|
const branchRng = createRng(options.seed ?? 0);
|
|
1894
4062
|
branchRng.setState(point.value.rngState);
|
|
@@ -1899,7 +4067,7 @@ var createRuntime = (options) => {
|
|
|
1899
4067
|
captured.set(storage, point.value.snapshot);
|
|
1900
4068
|
} else {
|
|
1901
4069
|
if (!instances.has(storage)) {
|
|
1902
|
-
const point =
|
|
4070
|
+
const point = history2.head(branch2);
|
|
1903
4071
|
const branchRng = createRng(options.seed ?? 0);
|
|
1904
4072
|
if (point)
|
|
1905
4073
|
branchRng.setState(point.value.rngState);
|
|
@@ -1923,8 +4091,8 @@ var createRuntime = (options) => {
|
|
|
1923
4091
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
1924
4092
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1925
4093
|
const branchName = checkoutOptions.branch ?? "main";
|
|
1926
|
-
const
|
|
1927
|
-
const point =
|
|
4094
|
+
const history2 = timeline(namespace);
|
|
4095
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
1928
4096
|
const storage = ensureBranch(namespace, branchName);
|
|
1929
4097
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1930
4098
|
captured.set(storage, point.value.snapshot);
|
|
@@ -1974,12 +4142,25 @@ var createRuntime = (options) => {
|
|
|
1974
4142
|
instance(name);
|
|
1975
4143
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
1976
4144
|
captured.set(name, from);
|
|
1977
|
-
const
|
|
1978
|
-
if (
|
|
1979
|
-
|
|
4145
|
+
const history2 = timelines.get(name);
|
|
4146
|
+
if (history2)
|
|
4147
|
+
history2.commit(capture(name), { branch: "main" });
|
|
1980
4148
|
else
|
|
1981
4149
|
timeline(name);
|
|
1982
4150
|
};
|
|
4151
|
+
const declarationsFor = (namespace) => {
|
|
4152
|
+
const declared = options.state;
|
|
4153
|
+
if (!declared)
|
|
4154
|
+
return [];
|
|
4155
|
+
return typeof declared === "function" ? declared(instance(namespace), namespace) : declared;
|
|
4156
|
+
};
|
|
4157
|
+
const stateScope = (namespace) => ({
|
|
4158
|
+
sqlite,
|
|
4159
|
+
namespace,
|
|
4160
|
+
storageNamespace: storageNamespace(namespace),
|
|
4161
|
+
root: instance(namespace),
|
|
4162
|
+
declarations: declarationsFor(namespace)
|
|
4163
|
+
});
|
|
1983
4164
|
const runtime = {
|
|
1984
4165
|
name: options.name,
|
|
1985
4166
|
sqlite,
|
|
@@ -2018,6 +4199,7 @@ var createRuntime = (options) => {
|
|
|
2018
4199
|
branch,
|
|
2019
4200
|
checkout,
|
|
2020
4201
|
timeline,
|
|
4202
|
+
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
2021
4203
|
fetch: async (incoming) => {
|
|
2022
4204
|
let request = incoming;
|
|
2023
4205
|
const prefixed = PATH_PREFIX.exec(new URL(request.url).pathname);
|
|
@@ -2191,10 +4373,10 @@ var createRuntime = (options) => {
|
|
|
2191
4373
|
},
|
|
2192
4374
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
2193
4375
|
inspect: (name) => {
|
|
2194
|
-
const
|
|
4376
|
+
const history2 = timeline(name);
|
|
2195
4377
|
return {
|
|
2196
|
-
branches:
|
|
2197
|
-
checkpoints:
|
|
4378
|
+
branches: history2.branches(),
|
|
4379
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
2198
4380
|
id,
|
|
2199
4381
|
branch: branchName,
|
|
2200
4382
|
parent,
|
|
@@ -2209,13 +4391,22 @@ var createRuntime = (options) => {
|
|
|
2209
4391
|
} : {},
|
|
2210
4392
|
routes: {
|
|
2211
4393
|
...credentialRoutes(credentials),
|
|
2212
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
2213
4394
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
2214
|
-
...options.admin?.(runtime) ?? {}
|
|
4395
|
+
...options.admin?.(runtime) ?? {},
|
|
4396
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4397
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4398
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4399
|
+
...stateAdminRoutes({
|
|
4400
|
+
open: stateScope,
|
|
4401
|
+
afterWrite: (namespace) => {
|
|
4402
|
+
checkpoint(namespace, "main");
|
|
4403
|
+
}
|
|
4404
|
+
}),
|
|
4405
|
+
...adminUiRoutes(options.name, options.adminUi)
|
|
2215
4406
|
},
|
|
2216
4407
|
adminKey: options.adminKey
|
|
2217
4408
|
});
|
|
2218
|
-
return runtime;
|
|
4409
|
+
return defineMock(runtime);
|
|
2219
4410
|
};
|
|
2220
4411
|
var mutableResponse = (response) => {
|
|
2221
4412
|
try {
|
|
@@ -2279,9 +4470,12 @@ var credentialRoutes = (registry) => ({
|
|
|
2279
4470
|
}
|
|
2280
4471
|
});
|
|
2281
4472
|
var presetRoutes = (presets, runtime) => ({
|
|
2282
|
-
"GET /faults/presets": () =>
|
|
2283
|
-
|
|
2284
|
-
|
|
4473
|
+
"GET /faults/presets": () => {
|
|
4474
|
+
const body = {
|
|
4475
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4476
|
+
};
|
|
4477
|
+
return adminJson(200, body);
|
|
4478
|
+
},
|
|
2285
4479
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
2286
4480
|
const name = params.name;
|
|
2287
4481
|
if (!presets[name])
|
|
@@ -2292,7 +4486,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
2292
4486
|
});
|
|
2293
4487
|
|
|
2294
4488
|
// src/generated/openapi.ts
|
|
2295
|
-
var
|
|
4489
|
+
var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"EasyPost Trackers API (Mockingbird subset)","description":"Stateful mock subset of the EasyPost v2 API: create (or re-use) a tracker for a tracking\\ncode, retrieve it, and list trackers. Trimmed from EasyPost's published reference to what\\nour genomics tracking lookup calls, plus the two reads that make parity walks meaningful.\\n","version":"2","x-mockingbird-upstream":{"note":"Shapes from https://docs.easypost.com/docs/trackers; request/response fields our consumer reads come from packages/lib/src/shipment-tracking-status/easypost-client.ts."}},"servers":[{"url":"https://api.easypost.com"}],"security":[{"basicAuth":[]}],"paths":{"/v2/trackers":{"post":{"operationId":"CreateTracker","description":"Creates a tracker, or answers the existing one when the same tracking code (and carrier) was already tracked. EasyPost's test codes (EZ1000000001 \u2026 EZ7000000007) answer their documented fixed statuses.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/CreateTrackerBody"}},"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrackerBody"}}}},"responses":{"201":{"description":"The tracker (new or re-used)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tracker"}}}},"401":{"description":"Missing or unknown API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Missing tracking code or invalid carrier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"ListTrackers","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"tracking_code","in":"query","required":false,"schema":{"type":"string","maxLength":40}},{"name":"carrier","in":"query","required":false,"schema":{"type":"string","enum":["USPS","UPS","FedEx","DHLExpress"]}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Newest trackers first","content":{"application/json":{"schema":{"type":"object","required":["trackers","has_more"],"properties":{"trackers":{"type":"array","items":{"$ref":"#/components/schemas/Tracker"}},"has_more":{"type":"boolean"}}}}}},"401":{"description":"Missing or unknown API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v2/trackers/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"tracker","missing":"trk_00000000000000000000000000000000"}}}],"get":{"operationId":"RetrieveTracker","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The tracker","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tracker"}}}},"401":{"description":"Missing or unknown API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown tracker","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"basicAuth":{"type":"http","scheme":"basic","description":"The API key as the username, an empty password (\`Basic base64(key:)\`)."}},"schemas":{"CreateTrackerBody":{"type":"object","required":["tracker"],"properties":{"tracker":{"type":"object","required":["tracking_code"],"properties":{"tracking_code":{"type":"string","minLength":1,"maxLength":40,"examples":["EZ1000000001","EZ4000000004","1Z999AA10123456784"]},"carrier":{"type":"string","enum":["USPS","UPS","FedEx","DHL","DHLExpress"]}}}}},"Tracker":{"type":"object","required":["id","object","mode","tracking_code","status","status_detail","carrier","tracking_details","public_url","created_at","updated_at"],"properties":{"id":{"type":"string","x-mockingbird-resource":{"type":"tracker","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"object":{"type":"string","enum":["Tracker"]},"mode":{"type":"string","enum":["test","production"]},"tracking_code":{"type":"string"},"status":{"type":"string","enum":["unknown","pre_transit","in_transit","out_for_delivery","delivered","available_for_pickup","return_to_sender","failure","cancelled","error"]},"status_detail":{"type":"string"},"carrier":{"type":"string"},"signed_by":{"type":["string","null"]},"weight":{"type":["number","null"]},"est_delivery_date":{"type":["string","null"],"x-mockingbird-volatile":{"kind":"timestamp"}},"shipment_id":{"type":["string","null"]},"tracking_details":{"type":"array","items":{"$ref":"#/components/schemas/TrackingDetail"}},"carrier_detail":{"type":["object","null"]},"public_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"fees":{"type":"array","items":{"type":"object"}},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"TrackingDetail":{"type":"object","required":["object","message","status","status_detail","datetime","source"],"properties":{"object":{"type":"string","enum":["TrackingDetail"]},"message":{"type":"string"},"status":{"type":"string"},"status_detail":{"type":"string"},"datetime":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"source":{"type":"string"},"tracking_location":{"type":"object"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object"}}}}}}}}}`);
|
|
2296
4490
|
var operationIds = ["ListTrackers", "CreateTracker", "RetrieveTracker"];
|
|
2297
4491
|
var supportedOperationIds = ["ListTrackers", "CreateTracker", "RetrieveTracker"];
|
|
2298
4492
|
|
|
@@ -2413,14 +4607,14 @@ var EASYPOST_PRESETS = {
|
|
|
2413
4607
|
rules: [{ pathPrefix: "/v2/trackers", latencyMs: 5e3 }]
|
|
2414
4608
|
}
|
|
2415
4609
|
};
|
|
2416
|
-
var
|
|
2417
|
-
var
|
|
2418
|
-
var
|
|
4610
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
4611
|
+
var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4612
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2419
4613
|
var adminRoutes = (runtime) => ({
|
|
2420
|
-
"GET /trackers": ({ namespace }) =>
|
|
4614
|
+
"GET /trackers": ({ namespace }) => json5(200, { trackers: runtime.instance(namespace).trackers() }),
|
|
2421
4615
|
"POST /trackers/:code/transition": ({ params, body, namespace }) => {
|
|
2422
|
-
if (!
|
|
2423
|
-
return
|
|
4616
|
+
if (!isRecord6(body) || !isTrackerStatus(body.status)) {
|
|
4617
|
+
return adminError4(
|
|
2424
4618
|
400,
|
|
2425
4619
|
'expected {"status": "<unknown|pre_transit|in_transit|out_for_delivery|delivered|available_for_pickup|return_to_sender|failure|cancelled|error>", "status_detail"?, "message"?, "carrier"?}'
|
|
2426
4620
|
);
|
|
@@ -2431,22 +4625,22 @@ var adminRoutes = (runtime) => ({
|
|
|
2431
4625
|
{ status: body.status, ...text("status_detail"), ...text("message"), ...text("signed_by") },
|
|
2432
4626
|
typeof body.carrier === "string" ? body.carrier : void 0
|
|
2433
4627
|
);
|
|
2434
|
-
return
|
|
4628
|
+
return json5(200, tracker);
|
|
2435
4629
|
},
|
|
2436
|
-
"GET /settings": ({ namespace }) =>
|
|
4630
|
+
"GET /settings": ({ namespace }) => json5(200, runtime.instance(namespace).state.current()),
|
|
2437
4631
|
"PUT /settings": ({ body, namespace }) => {
|
|
2438
|
-
if (!
|
|
4632
|
+
if (!isRecord6(body)) return adminError4(400, "expected a JSON object");
|
|
2439
4633
|
const patch = {};
|
|
2440
4634
|
if (body.apiKeys !== void 0) {
|
|
2441
|
-
if (!Array.isArray(body.apiKeys)) return
|
|
4635
|
+
if (!Array.isArray(body.apiKeys)) return adminError4(400, "apiKeys: string[]");
|
|
2442
4636
|
patch.apiKeys = body.apiKeys.map(String);
|
|
2443
4637
|
}
|
|
2444
|
-
return
|
|
4638
|
+
return json5(200, runtime.instance(namespace).state.update(patch));
|
|
2445
4639
|
}
|
|
2446
4640
|
});
|
|
2447
4641
|
var createRuntime2 = (options = {}) => createRuntime({
|
|
2448
4642
|
name: EASYPOST_NAMESPACE,
|
|
2449
|
-
document,
|
|
4643
|
+
document: document2,
|
|
2450
4644
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
2451
4645
|
...options.clock ? { clock: options.clock } : {},
|
|
2452
4646
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
@@ -2530,7 +4724,7 @@ var EasyPostAPI = class {
|
|
|
2530
4724
|
}
|
|
2531
4725
|
});
|
|
2532
4726
|
this.service = createService({
|
|
2533
|
-
document,
|
|
4727
|
+
document: document2,
|
|
2534
4728
|
handlers,
|
|
2535
4729
|
sqlite,
|
|
2536
4730
|
namespace,
|
|
@@ -2688,7 +4882,7 @@ var EasyPostAPI = class {
|
|
|
2688
4882
|
};
|
|
2689
4883
|
|
|
2690
4884
|
export {
|
|
2691
|
-
document,
|
|
4885
|
+
document2 as document,
|
|
2692
4886
|
operationIds,
|
|
2693
4887
|
supportedOperationIds,
|
|
2694
4888
|
TRACKER_STATUSES,
|
|
@@ -2702,4 +4896,4 @@ export {
|
|
|
2702
4896
|
isTrackerStatus,
|
|
2703
4897
|
EasyPostAPI
|
|
2704
4898
|
};
|
|
2705
|
-
//# sourceMappingURL=chunk-
|
|
4899
|
+
//# sourceMappingURL=chunk-DLVQBVAS.js.map
|