@crvouga/mockingbird-service-oauth 1.11.0 → 2.0.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 +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +3 -3
- package/SUPPORT.md +42 -0
- package/dist/{chunk-K4RVFKFZ.js → chunk-2BINFVVC.js} +44 -23
- package/dist/chunk-2BINFVVC.js.map +7 -0
- package/dist/{chunk-XQVBZTJA.js → chunk-I76SEED6.js} +641 -246
- package/dist/chunk-I76SEED6.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +2885 -0
- package/package.json +29 -4
- package/dist/chunk-K4RVFKFZ.js.map +0 -7
- package/dist/chunk-XQVBZTJA.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -339,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
339
371
|
return json(200, { status: "ok" });
|
|
340
372
|
}
|
|
341
373
|
};
|
|
342
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
343
382
|
const space = key.indexOf(" ");
|
|
344
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
345
384
|
});
|
|
@@ -347,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
347
386
|
namespaceOf: headerNamespace,
|
|
348
387
|
async handle(request) {
|
|
349
388
|
const url = new URL(request.url);
|
|
350
|
-
if (url.pathname ===
|
|
351
|
-
return
|
|
352
|
-
status: "ok",
|
|
353
|
-
service: context.name,
|
|
354
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
355
|
-
clock: context.clock.state(),
|
|
356
|
-
namespaces: context.namespaces().length,
|
|
357
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
358
|
-
...context.describe()
|
|
359
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
360
391
|
}
|
|
361
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
362
393
|
return void 0;
|
|
363
394
|
}
|
|
364
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
365
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
366
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
367
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -369,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
369
400
|
for (const route of routes) {
|
|
370
401
|
if (route.method !== request.method)
|
|
371
402
|
continue;
|
|
372
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
373
409
|
if (!params)
|
|
374
410
|
continue;
|
|
375
411
|
let body;
|
|
@@ -378,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
378
414
|
} catch {
|
|
379
415
|
return adminError(400, "request body is not valid JSON");
|
|
380
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
381
420
|
return route.handler({
|
|
382
421
|
request,
|
|
383
422
|
url,
|
|
384
423
|
params,
|
|
385
|
-
namespace
|
|
424
|
+
namespace,
|
|
386
425
|
body
|
|
387
426
|
});
|
|
388
427
|
}
|
|
389
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
390
429
|
}
|
|
391
430
|
};
|
|
392
431
|
};
|
|
@@ -1346,6 +1385,34 @@ dialog:not([open]) {
|
|
|
1346
1385
|
function isRecord2(value) {
|
|
1347
1386
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1348
1387
|
}
|
|
1388
|
+
function highlightJsonText(text) {
|
|
1389
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1390
|
+
switch (char) {
|
|
1391
|
+
case "&":
|
|
1392
|
+
return "&";
|
|
1393
|
+
case "<":
|
|
1394
|
+
return "<";
|
|
1395
|
+
case ">":
|
|
1396
|
+
return ">";
|
|
1397
|
+
case '"':
|
|
1398
|
+
return """;
|
|
1399
|
+
default:
|
|
1400
|
+
return "'";
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
const token = /("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"\s*:|"(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"|\b(?:true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
|
|
1404
|
+
let cursor = 0;
|
|
1405
|
+
let html = "";
|
|
1406
|
+
for (const match2 of text.matchAll(token)) {
|
|
1407
|
+
const index = match2.index ?? 0;
|
|
1408
|
+
const value = match2[0];
|
|
1409
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1410
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1411
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1412
|
+
cursor = index + value.length;
|
|
1413
|
+
}
|
|
1414
|
+
return html + escapeToken(text.slice(cursor));
|
|
1415
|
+
}
|
|
1349
1416
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1350
1417
|
const clip = (value, max) => {
|
|
1351
1418
|
if (typeof value !== "string")
|
|
@@ -1707,14 +1774,16 @@ function bootAdmin(config) {
|
|
|
1707
1774
|
const state = {
|
|
1708
1775
|
namespace: "default",
|
|
1709
1776
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1710
|
-
view: "overview",
|
|
1777
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1711
1778
|
collection: "",
|
|
1712
1779
|
shape: null,
|
|
1713
1780
|
route: null,
|
|
1781
|
+
routes: [],
|
|
1714
1782
|
editing: null
|
|
1715
1783
|
};
|
|
1716
1784
|
const standard = new Set(config.standardRoutes);
|
|
1717
1785
|
const banner = el("banner", HTMLElement);
|
|
1786
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1718
1787
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1719
1788
|
keyInput.value = state.key;
|
|
1720
1789
|
const params = new URLSearchParams(location.search);
|
|
@@ -1725,18 +1794,21 @@ function bootAdmin(config) {
|
|
|
1725
1794
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1726
1795
|
params.delete("key");
|
|
1727
1796
|
const next = params.toString();
|
|
1728
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1797
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1729
1798
|
}
|
|
1730
1799
|
const queryNamespace = params.get("namespace");
|
|
1731
1800
|
if (queryNamespace)
|
|
1732
1801
|
state.namespace = queryNamespace;
|
|
1733
1802
|
const showError = (error) => {
|
|
1734
|
-
|
|
1803
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1735
1804
|
banner.classList.add("show");
|
|
1805
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1736
1806
|
};
|
|
1737
1807
|
const clearError = () => {
|
|
1738
1808
|
banner.classList.remove("show");
|
|
1809
|
+
bannerMessage.textContent = "";
|
|
1739
1810
|
};
|
|
1811
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1740
1812
|
const api = async (method, path, body) => {
|
|
1741
1813
|
const headers = { accept: "application/json" };
|
|
1742
1814
|
if (state.key !== "")
|
|
@@ -1780,16 +1852,21 @@ function bootAdmin(config) {
|
|
|
1780
1852
|
namespace: () => state.namespace
|
|
1781
1853
|
};
|
|
1782
1854
|
const isPanelScript = (value) => typeof value === "function";
|
|
1855
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1783
1856
|
const setTheme = (theme) => {
|
|
1784
1857
|
if (theme)
|
|
1785
1858
|
document.documentElement.dataset.theme = theme;
|
|
1786
1859
|
else
|
|
1787
1860
|
delete document.documentElement.dataset.theme;
|
|
1861
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1862
|
+
themeButton.innerHTML = dark ? '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.5"/><path d="M10 2v1.5M10 16.5V18M18 10h-1.5M3.5 10H2M15.7 4.3l-1 1M5.3 14.7l-1 1M15.7 15.7l-1-1M5.3 5.3l-1-1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>' : '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M16.8 12.2A7 7 0 0 1 7.8 3.2a7 7 0 1 0 9 9Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>';
|
|
1863
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
1864
|
+
themeButton.setAttribute("aria-label", label);
|
|
1865
|
+
themeButton.title = label;
|
|
1788
1866
|
};
|
|
1789
1867
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1868
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
1869
|
+
themeButton.addEventListener("click", () => {
|
|
1793
1870
|
const current = document.documentElement.dataset.theme;
|
|
1794
1871
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1795
1872
|
const next = dark ? "light" : "dark";
|
|
@@ -1808,6 +1885,7 @@ function bootAdmin(config) {
|
|
|
1808
1885
|
continue;
|
|
1809
1886
|
section.hidden = section.id !== `view-${view}`;
|
|
1810
1887
|
}
|
|
1888
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1811
1889
|
};
|
|
1812
1890
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1813
1891
|
const target = event.target;
|
|
@@ -1822,7 +1900,7 @@ function bootAdmin(config) {
|
|
|
1822
1900
|
show(view);
|
|
1823
1901
|
refresh().catch(showError);
|
|
1824
1902
|
});
|
|
1825
|
-
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>`;
|
|
1903
|
+
const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card stat-card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
|
|
1826
1904
|
const table = (columns, rows) => {
|
|
1827
1905
|
if (rows.length === 0)
|
|
1828
1906
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1832,16 +1910,22 @@ function bootAdmin(config) {
|
|
|
1832
1910
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1833
1911
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1834
1912
|
}).join("");
|
|
1835
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1913
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1836
1914
|
};
|
|
1837
1915
|
const preview = (value) => {
|
|
1838
1916
|
const text = JSON.stringify(value) ?? "null";
|
|
1839
|
-
|
|
1917
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
1918
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1840
1919
|
};
|
|
1841
1920
|
const when = (epoch) => {
|
|
1842
1921
|
const date = new Date(epoch);
|
|
1843
1922
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1844
1923
|
};
|
|
1924
|
+
const journalWhen = (value) => {
|
|
1925
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
1926
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
1927
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
1928
|
+
};
|
|
1845
1929
|
const clockCard = (epoch) => {
|
|
1846
1930
|
const date = new Date(epoch);
|
|
1847
1931
|
if (Number.isNaN(date.getTime()))
|
|
@@ -1961,7 +2045,7 @@ function bootAdmin(config) {
|
|
|
1961
2045
|
};
|
|
1962
2046
|
const renderOverview = async () => {
|
|
1963
2047
|
const [health, shape, clock, journal] = await Promise.all([
|
|
1964
|
-
originGet(
|
|
2048
|
+
originGet(`${config.adminPrefix}/health`),
|
|
1965
2049
|
api("GET", "/state"),
|
|
1966
2050
|
api("GET", "/clock"),
|
|
1967
2051
|
api("GET", "/requests?limit=8")
|
|
@@ -1978,7 +2062,7 @@ function bootAdmin(config) {
|
|
|
1978
2062
|
},
|
|
1979
2063
|
{
|
|
1980
2064
|
k: "Status",
|
|
1981
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2065
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
1982
2066
|
},
|
|
1983
2067
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
1984
2068
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2065,26 +2149,46 @@ function bootAdmin(config) {
|
|
|
2065
2149
|
const renderJournal = async () => {
|
|
2066
2150
|
const journal = await api("GET", "/requests?limit=100");
|
|
2067
2151
|
el("journal", HTMLElement).innerHTML = table([
|
|
2068
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2069
|
-
{
|
|
2152
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2153
|
+
{
|
|
2154
|
+
label: "Method",
|
|
2155
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2156
|
+
},
|
|
2070
2157
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2071
|
-
{
|
|
2158
|
+
{
|
|
2159
|
+
label: "Status",
|
|
2160
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2161
|
+
},
|
|
2072
2162
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2073
2163
|
], decodeJournal(journal));
|
|
2074
2164
|
};
|
|
2075
|
-
const
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2165
|
+
const paintRoutes = () => {
|
|
2166
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2167
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2168
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2169
|
+
if (state.routes.length === 0) {
|
|
2170
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
if (routes.length === 0) {
|
|
2174
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2080
2177
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2081
2178
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2082
2179
|
const parts = route.split(" ");
|
|
2083
2180
|
const method = parts[0] ?? "";
|
|
2084
2181
|
const path = parts.slice(1).join(" ");
|
|
2085
|
-
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>`;
|
|
2182
|
+
return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}" aria-label="Use ${esc(route)}">Use</button></div>`;
|
|
2086
2183
|
}).join("");
|
|
2087
2184
|
};
|
|
2185
|
+
const renderRoutes = async () => {
|
|
2186
|
+
const data = await api("GET", "/");
|
|
2187
|
+
if (!isRecord2(data))
|
|
2188
|
+
throw new Error("Route list was not an object");
|
|
2189
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2190
|
+
paintRoutes();
|
|
2191
|
+
};
|
|
2088
2192
|
const renderers = {
|
|
2089
2193
|
overview: renderOverview,
|
|
2090
2194
|
state: renderState,
|
|
@@ -2093,19 +2197,50 @@ function bootAdmin(config) {
|
|
|
2093
2197
|
journal: renderJournal,
|
|
2094
2198
|
routes: renderRoutes
|
|
2095
2199
|
};
|
|
2200
|
+
let refreshToken = 0;
|
|
2096
2201
|
async function refresh() {
|
|
2202
|
+
const token = ++refreshToken;
|
|
2097
2203
|
clearError();
|
|
2204
|
+
const main = el("views", HTMLElement);
|
|
2205
|
+
const sync = el("sync-status", HTMLElement);
|
|
2206
|
+
main.setAttribute("aria-busy", "true");
|
|
2207
|
+
sync.classList.add("busy");
|
|
2208
|
+
sync.textContent = "Updating";
|
|
2098
2209
|
const render = renderers[state.view];
|
|
2099
|
-
|
|
2100
|
-
|
|
2210
|
+
try {
|
|
2211
|
+
if (render)
|
|
2212
|
+
await render();
|
|
2213
|
+
if (token === refreshToken)
|
|
2214
|
+
sync.textContent = "Up to date";
|
|
2215
|
+
} finally {
|
|
2216
|
+
if (token === refreshToken) {
|
|
2217
|
+
main.removeAttribute("aria-busy");
|
|
2218
|
+
sync.classList.remove("busy");
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2101
2221
|
}
|
|
2102
2222
|
const delay = (ms) => new Promise((resolve) => {
|
|
2103
2223
|
window.setTimeout(resolve, ms);
|
|
2104
2224
|
});
|
|
2105
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2225
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2106
2226
|
if (button.dataset.pending === "1")
|
|
2107
2227
|
return;
|
|
2108
2228
|
const label = button.textContent ?? "";
|
|
2229
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2230
|
+
button.dataset.confirming = "1";
|
|
2231
|
+
button.classList.add("confirming");
|
|
2232
|
+
button.textContent = confirmLabel;
|
|
2233
|
+
window.setTimeout(() => {
|
|
2234
|
+
if (button.dataset.confirming !== "1")
|
|
2235
|
+
return;
|
|
2236
|
+
button.textContent = label;
|
|
2237
|
+
button.classList.remove("confirming");
|
|
2238
|
+
delete button.dataset.confirming;
|
|
2239
|
+
}, 3e3);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
button.classList.remove("confirming");
|
|
2243
|
+
delete button.dataset.confirming;
|
|
2109
2244
|
const width = button.offsetWidth;
|
|
2110
2245
|
button.dataset.pending = "1";
|
|
2111
2246
|
button.classList.add("pressed");
|
|
@@ -2126,6 +2261,65 @@ function bootAdmin(config) {
|
|
|
2126
2261
|
delete button.dataset.pending;
|
|
2127
2262
|
}
|
|
2128
2263
|
};
|
|
2264
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2265
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2266
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2267
|
+
const status = el(statusId, HTMLElement);
|
|
2268
|
+
const shell = el(shellId, HTMLElement);
|
|
2269
|
+
const sync = () => {
|
|
2270
|
+
const text = textarea.value;
|
|
2271
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2272
|
+
`;
|
|
2273
|
+
let valid = true;
|
|
2274
|
+
let message = "Valid JSON";
|
|
2275
|
+
if (allowEmpty && text.trim() === "")
|
|
2276
|
+
message = "No body";
|
|
2277
|
+
else {
|
|
2278
|
+
try {
|
|
2279
|
+
readJson2(text);
|
|
2280
|
+
} catch {
|
|
2281
|
+
valid = false;
|
|
2282
|
+
message = "Invalid JSON";
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
shell.classList.toggle("invalid", !valid);
|
|
2286
|
+
status.classList.toggle("invalid", !valid);
|
|
2287
|
+
status.textContent = message;
|
|
2288
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2289
|
+
};
|
|
2290
|
+
const syncScroll = () => {
|
|
2291
|
+
const preview2 = highlight.parentElement;
|
|
2292
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2293
|
+
return;
|
|
2294
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2295
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2296
|
+
};
|
|
2297
|
+
textarea.addEventListener("input", sync);
|
|
2298
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2299
|
+
sync();
|
|
2300
|
+
return sync;
|
|
2301
|
+
};
|
|
2302
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2303
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2304
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2305
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2306
|
+
continue;
|
|
2307
|
+
button.addEventListener("click", () => {
|
|
2308
|
+
const target = button.dataset.format;
|
|
2309
|
+
if (target === void 0)
|
|
2310
|
+
return;
|
|
2311
|
+
const textarea = document.getElementById(target);
|
|
2312
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2313
|
+
return;
|
|
2314
|
+
try {
|
|
2315
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2316
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2317
|
+
textarea.focus();
|
|
2318
|
+
} catch {
|
|
2319
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2129
2323
|
document.addEventListener("click", (event) => {
|
|
2130
2324
|
const target = event.target;
|
|
2131
2325
|
if (!(target instanceof Element))
|
|
@@ -2198,6 +2392,7 @@ function bootAdmin(config) {
|
|
|
2198
2392
|
} else {
|
|
2199
2393
|
editorValue.value = "{\n \n}";
|
|
2200
2394
|
}
|
|
2395
|
+
syncRecordEditor();
|
|
2201
2396
|
editor.showModal();
|
|
2202
2397
|
}
|
|
2203
2398
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2227,10 +2422,10 @@ function bootAdmin(config) {
|
|
|
2227
2422
|
const id = state.editing;
|
|
2228
2423
|
if (!id)
|
|
2229
2424
|
return;
|
|
2230
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2425
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2231
2426
|
editor.close();
|
|
2232
2427
|
return renderState();
|
|
2233
|
-
}).catch(showError);
|
|
2428
|
+
}), "Confirm delete").catch(showError);
|
|
2234
2429
|
});
|
|
2235
2430
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2236
2431
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2276,10 +2471,10 @@ function bootAdmin(config) {
|
|
|
2276
2471
|
}).then(() => renderFaults()).catch(showError);
|
|
2277
2472
|
});
|
|
2278
2473
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2279
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2474
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2280
2475
|
});
|
|
2281
2476
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2282
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2477
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2283
2478
|
});
|
|
2284
2479
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2285
2480
|
const target = event.target;
|
|
@@ -2293,9 +2488,15 @@ function bootAdmin(config) {
|
|
|
2293
2488
|
return;
|
|
2294
2489
|
state.route = route;
|
|
2295
2490
|
el("route-title", HTMLElement).textContent = route;
|
|
2491
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2296
2492
|
const method = route.split(" ")[0] ?? "";
|
|
2297
|
-
el("route-body", HTMLTextAreaElement)
|
|
2493
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2494
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2495
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2496
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2497
|
+
syncRouteEditor();
|
|
2298
2498
|
});
|
|
2499
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2299
2500
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2300
2501
|
event.preventDefault();
|
|
2301
2502
|
const send = async () => {
|
|
@@ -2317,7 +2518,8 @@ function bootAdmin(config) {
|
|
|
2317
2518
|
}
|
|
2318
2519
|
}
|
|
2319
2520
|
const data = await api(method, concrete, body);
|
|
2320
|
-
|
|
2521
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2522
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2321
2523
|
};
|
|
2322
2524
|
send().catch(showError);
|
|
2323
2525
|
});
|
|
@@ -2435,11 +2637,13 @@ function bootAdmin(config) {
|
|
|
2435
2637
|
views.append(section);
|
|
2436
2638
|
mountSqlExplorer(body, panelApi);
|
|
2437
2639
|
}
|
|
2640
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2438
2641
|
return refresh();
|
|
2439
2642
|
}).catch(showError);
|
|
2440
2643
|
}
|
|
2441
2644
|
var clientPrelude = [
|
|
2442
2645
|
isRecord2,
|
|
2646
|
+
highlightJsonText,
|
|
2443
2647
|
mountAdminBrand,
|
|
2444
2648
|
startAdminBrand,
|
|
2445
2649
|
faultPresetList,
|
|
@@ -2462,6 +2666,7 @@ ${entry}(${payload});
|
|
|
2462
2666
|
var defineMock = (runtime) => runtime;
|
|
2463
2667
|
var STANDARD_ADMIN_ROUTES = [
|
|
2464
2668
|
"GET /",
|
|
2669
|
+
"GET /health",
|
|
2465
2670
|
"POST /reset",
|
|
2466
2671
|
"GET /namespaces",
|
|
2467
2672
|
"GET /clock",
|
|
@@ -2541,11 +2746,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2541
2746
|
}
|
|
2542
2747
|
});
|
|
2543
2748
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2544
|
-
var renderAdminDocument = (service) => {
|
|
2749
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2545
2750
|
const name = escapeHtml(service);
|
|
2546
2751
|
const script = adminClientSource({
|
|
2547
2752
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2548
|
-
adminPrefix
|
|
2753
|
+
adminPrefix,
|
|
2549
2754
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2550
2755
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2551
2756
|
service
|
|
@@ -2562,75 +2767,99 @@ var renderAdminDocument = (service) => {
|
|
|
2562
2767
|
${CSS_RESET}
|
|
2563
2768
|
:root {
|
|
2564
2769
|
color-scheme: light;
|
|
2565
|
-
--bg: #
|
|
2770
|
+
--bg: #f7f8fb;
|
|
2566
2771
|
--bg-elev: #ffffff;
|
|
2567
|
-
--bg-muted: #
|
|
2568
|
-
--bg-inset: #
|
|
2569
|
-
--line: #
|
|
2570
|
-
--line-strong: #
|
|
2571
|
-
--ink: #
|
|
2572
|
-
--muted: #
|
|
2573
|
-
--faint: #
|
|
2574
|
-
--accent: #
|
|
2772
|
+
--bg-muted: #f1f3f7;
|
|
2773
|
+
--bg-inset: #f8f9fc;
|
|
2774
|
+
--line: #e4e7ec;
|
|
2775
|
+
--line-strong: #cfd4dc;
|
|
2776
|
+
--ink: #101828;
|
|
2777
|
+
--muted: #475467;
|
|
2778
|
+
--faint: #667085;
|
|
2779
|
+
--accent: #6941c6;
|
|
2780
|
+
--accent-hover: #5933b4;
|
|
2575
2781
|
--accent-ink: #ffffff;
|
|
2576
|
-
--accent-soft: #
|
|
2577
|
-
--ok: #
|
|
2578
|
-
--ok-soft: #
|
|
2579
|
-
--warn: #
|
|
2580
|
-
--warn-soft: #
|
|
2581
|
-
--err: #
|
|
2582
|
-
--err-soft: #
|
|
2583
|
-
--
|
|
2782
|
+
--accent-soft: #f4f0ff;
|
|
2783
|
+
--ok: #067647;
|
|
2784
|
+
--ok-soft: #ecfdf3;
|
|
2785
|
+
--warn: #b54708;
|
|
2786
|
+
--warn-soft: #fffaeb;
|
|
2787
|
+
--err: #b42318;
|
|
2788
|
+
--err-soft: #fef3f2;
|
|
2789
|
+
--code-key: #6941c6;
|
|
2790
|
+
--code-string: #067647;
|
|
2791
|
+
--code-number: #175cd3;
|
|
2792
|
+
--code-boolean: #c11574;
|
|
2793
|
+
--code-null: #667085;
|
|
2794
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2795
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2796
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2584
2797
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2585
2798
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2586
|
-
--radius:
|
|
2587
|
-
--header:
|
|
2588
|
-
--nav:
|
|
2799
|
+
--radius: 12px;
|
|
2800
|
+
--header: 72px;
|
|
2801
|
+
--nav: 224px;
|
|
2589
2802
|
}
|
|
2590
2803
|
:root[data-theme="dark"] {
|
|
2591
2804
|
color-scheme: dark;
|
|
2592
|
-
--bg: #
|
|
2593
|
-
--bg-elev: #
|
|
2594
|
-
--bg-muted: #
|
|
2595
|
-
--bg-inset: #
|
|
2596
|
-
--line: #
|
|
2597
|
-
--line-strong: #
|
|
2598
|
-
--ink: #
|
|
2599
|
-
--muted: #
|
|
2600
|
-
--faint: #
|
|
2601
|
-
--accent: #
|
|
2602
|
-
--accent-
|
|
2603
|
-
--accent-
|
|
2604
|
-
--
|
|
2605
|
-
--ok
|
|
2606
|
-
--
|
|
2607
|
-
--warn
|
|
2608
|
-
--
|
|
2609
|
-
--err
|
|
2610
|
-
--
|
|
2805
|
+
--bg: #0c0e14;
|
|
2806
|
+
--bg-elev: #151821;
|
|
2807
|
+
--bg-muted: #20232e;
|
|
2808
|
+
--bg-inset: #11141c;
|
|
2809
|
+
--line: #2b303d;
|
|
2810
|
+
--line-strong: #414858;
|
|
2811
|
+
--ink: #f5f7fa;
|
|
2812
|
+
--muted: #b6bdc9;
|
|
2813
|
+
--faint: #8d96a7;
|
|
2814
|
+
--accent: #9e77ed;
|
|
2815
|
+
--accent-hover: #b692f6;
|
|
2816
|
+
--accent-ink: #ffffff;
|
|
2817
|
+
--accent-soft: #2b2142;
|
|
2818
|
+
--ok: #75e0a7;
|
|
2819
|
+
--ok-soft: #102a20;
|
|
2820
|
+
--warn: #fec84b;
|
|
2821
|
+
--warn-soft: #302611;
|
|
2822
|
+
--err: #fda29b;
|
|
2823
|
+
--err-soft: #321817;
|
|
2824
|
+
--code-key: #c3a6ff;
|
|
2825
|
+
--code-string: #75e0a7;
|
|
2826
|
+
--code-number: #84adff;
|
|
2827
|
+
--code-boolean: #f9a8d4;
|
|
2828
|
+
--code-null: #98a2b3;
|
|
2829
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2830
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2831
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2611
2832
|
}
|
|
2612
2833
|
@media (prefers-color-scheme: dark) {
|
|
2613
2834
|
:root:not([data-theme="light"]) {
|
|
2614
2835
|
color-scheme: dark;
|
|
2615
|
-
--bg: #
|
|
2616
|
-
--bg-elev: #
|
|
2617
|
-
--bg-muted: #
|
|
2618
|
-
--bg-inset: #
|
|
2619
|
-
--line: #
|
|
2620
|
-
--line-strong: #
|
|
2621
|
-
--ink: #
|
|
2622
|
-
--muted: #
|
|
2623
|
-
--faint: #
|
|
2624
|
-
--accent: #
|
|
2625
|
-
--accent-
|
|
2626
|
-
--accent-
|
|
2627
|
-
--
|
|
2628
|
-
--ok
|
|
2629
|
-
--
|
|
2630
|
-
--warn
|
|
2631
|
-
--
|
|
2632
|
-
--err
|
|
2633
|
-
--
|
|
2836
|
+
--bg: #0c0e14;
|
|
2837
|
+
--bg-elev: #151821;
|
|
2838
|
+
--bg-muted: #20232e;
|
|
2839
|
+
--bg-inset: #11141c;
|
|
2840
|
+
--line: #2b303d;
|
|
2841
|
+
--line-strong: #414858;
|
|
2842
|
+
--ink: #f5f7fa;
|
|
2843
|
+
--muted: #b6bdc9;
|
|
2844
|
+
--faint: #8d96a7;
|
|
2845
|
+
--accent: #9e77ed;
|
|
2846
|
+
--accent-hover: #b692f6;
|
|
2847
|
+
--accent-ink: #ffffff;
|
|
2848
|
+
--accent-soft: #2b2142;
|
|
2849
|
+
--ok: #75e0a7;
|
|
2850
|
+
--ok-soft: #102a20;
|
|
2851
|
+
--warn: #fec84b;
|
|
2852
|
+
--warn-soft: #302611;
|
|
2853
|
+
--err: #fda29b;
|
|
2854
|
+
--err-soft: #321817;
|
|
2855
|
+
--code-key: #c3a6ff;
|
|
2856
|
+
--code-string: #75e0a7;
|
|
2857
|
+
--code-number: #84adff;
|
|
2858
|
+
--code-boolean: #f9a8d4;
|
|
2859
|
+
--code-null: #98a2b3;
|
|
2860
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2861
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2862
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2634
2863
|
}
|
|
2635
2864
|
}
|
|
2636
2865
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2648,13 +2877,17 @@ var renderAdminDocument = (service) => {
|
|
|
2648
2877
|
padding-right: env(safe-area-inset-right);
|
|
2649
2878
|
-webkit-text-size-adjust: 100%;
|
|
2650
2879
|
}
|
|
2651
|
-
|
|
2880
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2881
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2882
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2883
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2652
2884
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2653
2885
|
header.top {
|
|
2654
|
-
position: sticky; top: 0; z-index:
|
|
2886
|
+
position: sticky; top: 0; z-index: 20;
|
|
2655
2887
|
display: flex; align-items: center; gap: 12px;
|
|
2656
|
-
min-height: var(--header); padding:
|
|
2657
|
-
background: var(--bg-elev);
|
|
2888
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2889
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2890
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2658
2891
|
border-bottom: 1px solid var(--line);
|
|
2659
2892
|
min-width: 0; max-width: 100%;
|
|
2660
2893
|
}
|
|
@@ -2662,12 +2895,21 @@ var renderAdminDocument = (service) => {
|
|
|
2662
2895
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2663
2896
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2664
2897
|
}
|
|
2665
|
-
.brand-
|
|
2666
|
-
.brand-
|
|
2898
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2899
|
+
.brand-mark {
|
|
2900
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2901
|
+
border-radius: 11px; color: #fff;
|
|
2902
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2903
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2904
|
+
}
|
|
2905
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2906
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2907
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2667
2908
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2668
2909
|
.vendor {
|
|
2669
2910
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2670
2911
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2912
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2671
2913
|
}
|
|
2672
2914
|
.vendor[hidden] { display: none; }
|
|
2673
2915
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2679,12 +2921,17 @@ var renderAdminDocument = (service) => {
|
|
|
2679
2921
|
.vendor-sep { color: var(--faint); }
|
|
2680
2922
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2681
2923
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2682
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2683
|
-
|
|
2924
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2925
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2926
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2927
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2928
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2684
2929
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2685
|
-
background-color: var(--bg-
|
|
2686
|
-
padding:
|
|
2930
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2931
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2687
2932
|
}
|
|
2933
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2934
|
+
label.field input:focus, label.field select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); outline: none; }
|
|
2688
2935
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2689
2936
|
the text fields in the same form. Paint it like those fields. */
|
|
2690
2937
|
label.field select {
|
|
@@ -2701,12 +2948,20 @@ var renderAdminDocument = (service) => {
|
|
|
2701
2948
|
dialog .field > textarea { width: 100%; }
|
|
2702
2949
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2703
2950
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
.btn
|
|
2951
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
2952
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
2953
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
2954
|
+
box-shadow: var(--shadow-xs);
|
|
2955
|
+
}
|
|
2956
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
2957
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
2958
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
2959
|
+
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); box-shadow: 0 1px 2px rgb(16 24 40 / 0.08), 0 0 0 1px rgb(255 255 255 / 0.08) inset; }
|
|
2960
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
2961
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
2962
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
2963
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
2964
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2710
2965
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2711
2966
|
so the press itself has to stay visible. */
|
|
2712
2967
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2722,93 +2977,159 @@ var renderAdminDocument = (service) => {
|
|
|
2722
2977
|
flex: 1 0 auto; min-width: 0;
|
|
2723
2978
|
}
|
|
2724
2979
|
nav.side {
|
|
2725
|
-
|
|
2980
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
2981
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2726
2982
|
display: flex; flex-direction: column; gap: 4px;
|
|
2727
|
-
background: var(--bg-elev);
|
|
2983
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2728
2984
|
}
|
|
2985
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2729
2986
|
nav.side button {
|
|
2987
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2730
2988
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2731
|
-
padding: 9px 10px; min-height:
|
|
2732
|
-
}
|
|
2733
|
-
nav.side button
|
|
2734
|
-
|
|
2735
|
-
.
|
|
2736
|
-
.
|
|
2737
|
-
.
|
|
2989
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
2990
|
+
}
|
|
2991
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
2992
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
2993
|
+
nav.side button[aria-current="true"]::before { content: ""; position: absolute; left: -12px; width: 3px; height: 22px; border-radius: 0 4px 4px 0; background: var(--accent); }
|
|
2994
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
2995
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
2996
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
2997
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
2998
|
+
.view { animation: view-in 180ms ease-out; }
|
|
2999
|
+
.view[hidden] { display: none; }
|
|
3000
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3001
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3002
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3003
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3004
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2738
3005
|
.card {
|
|
2739
3006
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2740
|
-
padding:
|
|
3007
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2741
3008
|
}
|
|
2742
|
-
.card
|
|
2743
|
-
.card
|
|
3009
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3010
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3011
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3012
|
+
.card .v { display: flex; flex-direction: column; gap: 2px; font-size: 24px; font-weight: 650; margin-top: 7px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; letter-spacing: -0.025em; }
|
|
2744
3013
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2745
|
-
.
|
|
3014
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3015
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3016
|
+
.section-title h3 { margin: 0; }
|
|
3017
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3018
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2746
3019
|
.collection {
|
|
2747
3020
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2748
|
-
border-radius:
|
|
3021
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2749
3022
|
}
|
|
2750
|
-
.collection
|
|
3023
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3024
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2751
3025
|
.collection small { display: block; color: var(--faint); }
|
|
2752
3026
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2753
|
-
.chip {
|
|
2754
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2755
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2756
|
-
}
|
|
3027
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2757
3028
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2758
3029
|
.sql-layout {
|
|
2759
3030
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2760
3031
|
}
|
|
2761
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2762
|
-
.sql-tables button { text-align: left; }
|
|
3032
|
+
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); }
|
|
3033
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3034
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2763
3035
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2764
|
-
table { width: 100%; border-
|
|
2765
|
-
|
|
2766
|
-
th {
|
|
3036
|
+
.table-shell { width: 100%; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3037
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3038
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3039
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3040
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2767
3041
|
tr.clickable { cursor: pointer; }
|
|
2768
|
-
tr.clickable:hover { background: var(--
|
|
3042
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2769
3043
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2770
3044
|
pre {
|
|
2771
3045
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2772
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2773
|
-
}
|
|
2774
|
-
.
|
|
3046
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3047
|
+
}
|
|
3048
|
+
.json-key { color: var(--code-key); }
|
|
3049
|
+
.json-string { color: var(--code-string); }
|
|
3050
|
+
.json-number { color: var(--code-number); }
|
|
3051
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3052
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3053
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3054
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3055
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3056
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3057
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3058
|
+
.json-editor pre, .json-editor textarea { margin: 0; padding: 12px; border: 0; border-radius: 0; min-height: 190px; width: 100%; font: 12px/1.65 var(--mono); tab-size: 2; white-space: pre; overflow: auto; }
|
|
3059
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3060
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3061
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3062
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3063
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3064
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3065
|
+
.json-status.invalid { color: var(--err); }
|
|
3066
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3067
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2775
3068
|
.banner {
|
|
2776
|
-
display: none;
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
3069
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3070
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3071
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3072
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3073
|
+
}
|
|
3074
|
+
.banner.show { display: flex; }
|
|
3075
|
+
.banner span { flex: 1; }
|
|
3076
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3077
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3078
|
+
.empty { color: var(--muted); padding: 34px 18px; text-align: center; border: 1px dashed var(--line-strong); border-radius: var(--radius); background: color-mix(in srgb, var(--bg-elev) 55%, transparent); }
|
|
2781
3079
|
dialog {
|
|
2782
|
-
border: 1px solid var(--line); border-radius:
|
|
2783
|
-
color: var(--ink); width: min(
|
|
2784
|
-
}
|
|
2785
|
-
dialog
|
|
2786
|
-
dialog
|
|
3080
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3081
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3082
|
+
}
|
|
3083
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3084
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3085
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3086
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3087
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3088
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2787
3089
|
textarea {
|
|
2788
3090
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2789
3091
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2790
3092
|
font-family: var(--mono); font-size: 12px;
|
|
2791
3093
|
}
|
|
2792
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2793
|
-
.
|
|
3094
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3095
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3096
|
+
.badge { display: inline-flex; align-items: center; font-size: 10px; font-weight: 700; letter-spacing: 0.03em; border: 1px solid var(--line); border-radius: 999px; padding: 3px 7px; background: var(--bg-muted); color: var(--muted); }
|
|
2794
3097
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2795
3098
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2796
3099
|
.status-2 { color: var(--ok); }
|
|
2797
3100
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2798
3101
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3102
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3103
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2799
3104
|
.route {
|
|
2800
|
-
display: grid; grid-template-columns:
|
|
2801
|
-
padding:
|
|
2802
|
-
}
|
|
2803
|
-
.
|
|
3105
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3106
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3107
|
+
}
|
|
3108
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3109
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3110
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3111
|
+
.route-tools .field { flex: 1; }
|
|
3112
|
+
.route-tools input { width: 100%; }
|
|
3113
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3114
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3115
|
+
.method { width: fit-content; font-family: var(--mono); font-size: 11px; font-weight: 750; color: var(--accent); background: var(--accent-soft); border-radius: 6px; padding: 4px 6px; }
|
|
3116
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3117
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3118
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3119
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3120
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2804
3121
|
@media (max-width: 800px) {
|
|
3122
|
+
:root { --header: 0px; }
|
|
2805
3123
|
header.top {
|
|
2806
|
-
align-items: stretch; flex-direction: column;
|
|
2807
|
-
padding
|
|
3124
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3125
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2808
3126
|
}
|
|
2809
3127
|
input, select, textarea { font-size: 16px; }
|
|
2810
3128
|
.brand, .vendor, .controls { width: 100%; }
|
|
3129
|
+
.brand { gap: 10px; }
|
|
3130
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2811
3131
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3132
|
+
.sync-status { display: none; }
|
|
2812
3133
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2813
3134
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2814
3135
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2818,14 +3139,23 @@ var renderAdminDocument = (service) => {
|
|
|
2818
3139
|
align-content: start;
|
|
2819
3140
|
}
|
|
2820
3141
|
nav.side {
|
|
2821
|
-
align-self: start; height: auto;
|
|
3142
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2822
3143
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2823
3144
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2824
|
-
padding: 8px;
|
|
3145
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2825
3146
|
}
|
|
2826
|
-
nav
|
|
3147
|
+
.nav-label { display: none; }
|
|
3148
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3149
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2827
3150
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2828
|
-
|
|
3151
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3152
|
+
.form-grid .wide { grid-column: auto; }
|
|
3153
|
+
main { padding: 22px 14px 48px; }
|
|
3154
|
+
.view-head { margin-bottom: 18px; }
|
|
3155
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3156
|
+
.card { padding: 14px; }
|
|
3157
|
+
.card .v { font-size: 20px; }
|
|
3158
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2829
3159
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2830
3160
|
thead { display: none; }
|
|
2831
3161
|
tr {
|
|
@@ -2840,50 +3170,63 @@ var renderAdminDocument = (service) => {
|
|
|
2840
3170
|
.route { grid-template-columns: 72px 1fr; }
|
|
2841
3171
|
.route button { grid-column: 1 / -1; }
|
|
2842
3172
|
}
|
|
3173
|
+
@media (max-width: 460px) {
|
|
3174
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3175
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3176
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3177
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3178
|
+
.dialog-actions button { flex: 1; }
|
|
3179
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3180
|
+
}
|
|
2843
3181
|
</style>
|
|
2844
3182
|
</head>
|
|
2845
3183
|
<body>
|
|
2846
3184
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2847
3185
|
<header class="top">
|
|
2848
3186
|
<div class="brand">
|
|
2849
|
-
<div class="brand-
|
|
2850
|
-
<
|
|
2851
|
-
|
|
3187
|
+
<div class="brand-lockup">
|
|
3188
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3189
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M5 17.5V7l7 5 7-5v10.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 15.5 12 18l4-2.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
3190
|
+
</div>
|
|
3191
|
+
<div class="brand-id">
|
|
3192
|
+
<strong>${name}</strong>
|
|
3193
|
+
<span>Mock admin</span>
|
|
3194
|
+
</div>
|
|
2852
3195
|
</div>
|
|
2853
3196
|
<div class="vendor" id="vendor" hidden></div>
|
|
2854
3197
|
</div>
|
|
2855
3198
|
<div class="controls">
|
|
3199
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2856
3200
|
<label class="field">Namespace
|
|
2857
3201
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2858
3202
|
</label>
|
|
2859
3203
|
<label class="field">Admin key
|
|
2860
3204
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2861
3205
|
</label>
|
|
2862
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2863
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3206
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3207
|
+
<button class="btn" id="refresh" type="button"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M15.8 7.2A6.2 6.2 0 1 0 16 12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M12.8 7.2h3v-3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>Refresh</button>
|
|
2864
3208
|
</div>
|
|
2865
3209
|
</header>
|
|
2866
3210
|
<div class="layout">
|
|
2867
3211
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2868
|
-
<
|
|
2869
|
-
<button type="button" data-view="
|
|
2870
|
-
<button type="button" data-view="
|
|
2871
|
-
<button type="button" data-view="
|
|
2872
|
-
<button type="button" data-view="
|
|
2873
|
-
<button type="button" data-view="
|
|
3212
|
+
<span class="nav-label">Workspace</span>
|
|
3213
|
+
<button type="button" data-view="overview" aria-current="true"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><rect x="2.5" y="2.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="11.5" y="2.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="2.5" y="11.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="11.5" y="11.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/></svg></span>Overview</button>
|
|
3214
|
+
<button type="button" data-view="state"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><ellipse cx="10" cy="5" rx="6.5" ry="2.5" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 5v5c0 1.4 2.9 2.5 6.5 2.5s6.5-1.1 6.5-2.5V5M3.5 10v5c0 1.4 2.9 2.5 6.5 2.5s6.5-1.1 6.5-2.5v-5" stroke="currentColor" stroke-width="1.5"/></svg></span>State</button>
|
|
3215
|
+
<button type="button" data-view="clock"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.5"/><path d="M10 5.8v4.6l3 1.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>Clock</button>
|
|
3216
|
+
<button type="button" data-view="faults"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m11.2 2.8-6.5 9h5.1l-1 5.4 6.5-9h-5.1l1-5.4Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></span>Faults</button>
|
|
3217
|
+
<button type="button" data-view="journal"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="M5 3.5h10a1.5 1.5 0 0 1 1.5 1.5v12l-2.2-1.4-2.2 1.4-2.1-1.4L7.8 17l-2.2-1.4L3.5 17V5A1.5 1.5 0 0 1 5 3.5Z" stroke="currentColor" stroke-width="1.5"/><path d="M6.7 7h6.6M6.7 10h6.6M6.7 13h3.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>Journal</button>
|
|
3218
|
+
<button type="button" data-view="routes"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="M6.5 4.5h7a2 2 0 0 1 2 2v7M13 11l2.5 2.5L18 11M13.5 15.5h-7a2 2 0 0 1-2-2v-7M7 9 4.5 6.5 2 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Routes</button>
|
|
2874
3219
|
</nav>
|
|
2875
3220
|
<main id="views">
|
|
2876
|
-
<
|
|
3221
|
+
<div class="banner" id="banner" role="alert"><span id="banner-message"></span><button id="banner-dismiss" type="button" aria-label="Dismiss error">\xD7</button></div>
|
|
2877
3222
|
<section class="view" id="view-overview">
|
|
2878
|
-
<h2>Overview</h2>
|
|
2879
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3223
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2880
3224
|
<div class="grid" id="overview-cards"></div>
|
|
2881
3225
|
<h3>Recent requests</h3>
|
|
2882
3226
|
<div id="overview-requests"></div>
|
|
2883
3227
|
</section>
|
|
2884
3228
|
<section class="view" id="view-state" hidden>
|
|
2885
|
-
<h2>State</h2>
|
|
2886
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3229
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2887
3230
|
<div class="split">
|
|
2888
3231
|
<div id="collections"></div>
|
|
2889
3232
|
<div>
|
|
@@ -2896,8 +3239,7 @@ var renderAdminDocument = (service) => {
|
|
|
2896
3239
|
</div>
|
|
2897
3240
|
</section>
|
|
2898
3241
|
<section class="view" id="view-clock" hidden>
|
|
2899
|
-
<h2>Clock</h2>
|
|
2900
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3242
|
+
<div class="view-head"><div><h2>Clock</h2><p class="lede">Control the source of time used by every timestamp in this mock.</p></div></div>
|
|
2901
3243
|
<div class="card" id="clock-readout"></div>
|
|
2902
3244
|
<div class="row-actions">
|
|
2903
3245
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2916,21 +3258,22 @@ var renderAdminDocument = (service) => {
|
|
|
2916
3258
|
</form>
|
|
2917
3259
|
</section>
|
|
2918
3260
|
<section class="view" id="view-faults" hidden>
|
|
2919
|
-
<h2>Faults</h2>
|
|
2920
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3261
|
+
<div class="view-head"><div><h2>Faults</h2><p class="lede">Test unhappy paths with a response, delay, or named preset scoped to this namespace.</p></div></div>
|
|
2921
3262
|
<form id="fault-form" class="card stack">
|
|
2922
|
-
<
|
|
2923
|
-
<
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
<
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
<
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
<
|
|
2933
|
-
|
|
3263
|
+
<div class="form-grid">
|
|
3264
|
+
<label class="field wide">Preset
|
|
3265
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3266
|
+
</label>
|
|
3267
|
+
<label class="field">Status
|
|
3268
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3269
|
+
</label>
|
|
3270
|
+
<label class="field wide">Path prefix
|
|
3271
|
+
<input id="fault-path" placeholder="/v1">
|
|
3272
|
+
</label>
|
|
3273
|
+
<label class="field">Method
|
|
3274
|
+
<input id="fault-method" placeholder="GET">
|
|
3275
|
+
</label>
|
|
3276
|
+
</div>
|
|
2934
3277
|
<div class="dialog-actions">
|
|
2935
3278
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2936
3279
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2939,26 +3282,31 @@ var renderAdminDocument = (service) => {
|
|
|
2939
3282
|
<div id="fault-list"></div>
|
|
2940
3283
|
</section>
|
|
2941
3284
|
<section class="view" id="view-journal" hidden>
|
|
2942
|
-
<h2>Journal</h2>
|
|
2943
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2944
|
-
<div class="row-actions">
|
|
2945
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2946
|
-
</div>
|
|
3285
|
+
<div class="view-head"><div><h2>Journal</h2><p class="lede">Requests this namespace has seen. Bodies are never stored.</p></div><button class="btn-danger" id="journal-clear" type="button">Clear journal</button></div>
|
|
2947
3286
|
<div id="journal"></div>
|
|
2948
3287
|
</section>
|
|
2949
3288
|
<section class="view" id="view-routes" hidden>
|
|
2950
|
-
<h2>Routes</h2>
|
|
2951
|
-
<
|
|
3289
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3290
|
+
<div class="route-tools">
|
|
3291
|
+
<label class="field">Filter routes
|
|
3292
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3293
|
+
</label>
|
|
3294
|
+
<span class="route-count" id="route-count"></span>
|
|
3295
|
+
</div>
|
|
2952
3296
|
<div id="route-list"></div>
|
|
2953
|
-
<form id="route-form" class="card stack">
|
|
3297
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2954
3298
|
<strong id="route-title">Select a route</strong>
|
|
2955
3299
|
<label class="field">JSON body
|
|
2956
|
-
<
|
|
3300
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3301
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3302
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3303
|
+
</div>
|
|
3304
|
+
<span class="editor-meta" hidden><span class="json-status" id="route-json-status">Valid JSON</span><button class="format-json" type="button" data-format="route-body">Format JSON</button></span>
|
|
2957
3305
|
</label>
|
|
2958
3306
|
<div class="dialog-actions">
|
|
2959
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3307
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2960
3308
|
</div>
|
|
2961
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3309
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2962
3310
|
</form>
|
|
2963
3311
|
</section>
|
|
2964
3312
|
</main>
|
|
@@ -2966,12 +3314,16 @@ var renderAdminDocument = (service) => {
|
|
|
2966
3314
|
</div>
|
|
2967
3315
|
<dialog id="editor">
|
|
2968
3316
|
<form method="dialog" id="editor-form">
|
|
2969
|
-
<strong id="editor-title">Record</strong>
|
|
3317
|
+
<div class="dialog-heading"><div><strong id="editor-title">Record</strong><br><span>Edit the ID and JSON value stored in this collection.</span></div></div>
|
|
2970
3318
|
<label class="field">Id
|
|
2971
3319
|
<input id="editor-id" required>
|
|
2972
3320
|
</label>
|
|
2973
3321
|
<label class="field">JSON value
|
|
2974
|
-
<
|
|
3322
|
+
<div class="json-editor" id="record-editor">
|
|
3323
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3324
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3325
|
+
</div>
|
|
3326
|
+
<span class="editor-meta"><span class="json-status" id="editor-json-status">Valid JSON</span><button class="format-json" type="button" data-format="editor-value">Format JSON</button></span>
|
|
2975
3327
|
</label>
|
|
2976
3328
|
<div class="dialog-actions">
|
|
2977
3329
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3013,8 +3365,8 @@ var describeExtension = (extension) => {
|
|
|
3013
3365
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3014
3366
|
};
|
|
3015
3367
|
};
|
|
3016
|
-
var adminUiRoutes = (service, ui) => {
|
|
3017
|
-
const shell = () => renderAdminDocument(service);
|
|
3368
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3369
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3018
3370
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3019
3371
|
return {
|
|
3020
3372
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3935,10 +4287,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3935
4287
|
};
|
|
3936
4288
|
|
|
3937
4289
|
// ../core/dist/version.js
|
|
3938
|
-
var PACKAGE_VERSION = true ? "
|
|
4290
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3939
4291
|
|
|
3940
4292
|
// ../core/dist/webhooks.js
|
|
3941
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4293
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4294
|
+
status,
|
|
4295
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4296
|
+
});
|
|
3942
4297
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3943
4298
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3944
4299
|
var parseEndpoint = (value) => {
|
|
@@ -4046,7 +4401,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4046
4401
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4047
4402
|
var DEFAULT_NAMESPACE = "default";
|
|
4048
4403
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4049
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4050
4404
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4051
4405
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4052
4406
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4085,6 +4439,22 @@ var operationMatcher = (document3) => {
|
|
|
4085
4439
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4086
4440
|
};
|
|
4087
4441
|
var createRuntime = (options) => {
|
|
4442
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4443
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4444
|
+
for (const path of internalPaths) {
|
|
4445
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4446
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4447
|
+
}
|
|
4448
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4449
|
+
const internalMatch = (path) => {
|
|
4450
|
+
for (const pattern of internalPaths) {
|
|
4451
|
+
const actual = path.split("/");
|
|
4452
|
+
const expected = pattern.split("/");
|
|
4453
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4454
|
+
return pattern;
|
|
4455
|
+
}
|
|
4456
|
+
return void 0;
|
|
4457
|
+
};
|
|
4088
4458
|
assertAdminUi(options.adminUi);
|
|
4089
4459
|
const sqlite = bootSqlite(options.sqlite);
|
|
4090
4460
|
const clock = options.clock ?? createClock();
|
|
@@ -4130,6 +4500,7 @@ var createRuntime = (options) => {
|
|
|
4130
4500
|
const created = options.create({
|
|
4131
4501
|
namespace: storageNamespace(key),
|
|
4132
4502
|
publicNamespace,
|
|
4503
|
+
adminPrefix,
|
|
4133
4504
|
sqlite,
|
|
4134
4505
|
clock: namespaceClock(publicNamespace),
|
|
4135
4506
|
rng: createdContextRng
|
|
@@ -4362,10 +4733,10 @@ var createRuntime = (options) => {
|
|
|
4362
4733
|
namespaceClock,
|
|
4363
4734
|
namespaceOf(request) {
|
|
4364
4735
|
const url = new URL(request.url);
|
|
4365
|
-
const prefixed =
|
|
4736
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4366
4737
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4367
4738
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4368
|
-
if (path
|
|
4739
|
+
if (isAdminPath(path, adminPrefix))
|
|
4369
4740
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4370
4741
|
if (selected !== void 0)
|
|
4371
4742
|
return selected;
|
|
@@ -4431,7 +4802,7 @@ var createRuntime = (options) => {
|
|
|
4431
4802
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4432
4803
|
fetch: async (incoming) => {
|
|
4433
4804
|
let request = incoming;
|
|
4434
|
-
const prefixed =
|
|
4805
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4435
4806
|
if (prefixed) {
|
|
4436
4807
|
const url2 = new URL(request.url);
|
|
4437
4808
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4447,7 +4818,15 @@ var createRuntime = (options) => {
|
|
|
4447
4818
|
signal: request.signal
|
|
4448
4819
|
});
|
|
4449
4820
|
}
|
|
4450
|
-
|
|
4821
|
+
const internalUrl = new URL(request.url);
|
|
4822
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4823
|
+
const internal = internalMatch(internalPath);
|
|
4824
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4825
|
+
if (internal) {
|
|
4826
|
+
internalUrl.pathname = internalPath;
|
|
4827
|
+
request = new Request(internalUrl, request);
|
|
4828
|
+
}
|
|
4829
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4451
4830
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4452
4831
|
const credential = options.credential(request);
|
|
4453
4832
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4467,7 +4846,9 @@ var createRuntime = (options) => {
|
|
|
4467
4846
|
return copy;
|
|
4468
4847
|
}
|
|
4469
4848
|
};
|
|
4470
|
-
|
|
4849
|
+
if (obsoleteInternalPath)
|
|
4850
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4851
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4471
4852
|
if (handled)
|
|
4472
4853
|
return stamp(handled);
|
|
4473
4854
|
const started = monotonicNow();
|
|
@@ -4508,7 +4889,7 @@ var createRuntime = (options) => {
|
|
|
4508
4889
|
type: "mockingbird_admin",
|
|
4509
4890
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4510
4891
|
}
|
|
4511
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4892
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4512
4893
|
}
|
|
4513
4894
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4514
4895
|
log(400);
|
|
@@ -4625,15 +5006,16 @@ var createRuntime = (options) => {
|
|
|
4625
5006
|
...options.admin?.(runtime) ?? {},
|
|
4626
5007
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4627
5008
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4628
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5009
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4629
5010
|
...stateAdminRoutes({
|
|
4630
5011
|
open: stateScope,
|
|
4631
5012
|
afterWrite: (namespace) => {
|
|
4632
5013
|
checkpoint(namespace, "main");
|
|
4633
5014
|
}
|
|
4634
5015
|
}),
|
|
4635
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5016
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4636
5017
|
},
|
|
5018
|
+
adminPrefix,
|
|
4637
5019
|
adminKey: options.adminKey
|
|
4638
5020
|
});
|
|
4639
5021
|
return defineMock(runtime);
|
|
@@ -4647,7 +5029,10 @@ var mutableResponse = (response) => {
|
|
|
4647
5029
|
return new Response(response.body, response);
|
|
4648
5030
|
}
|
|
4649
5031
|
};
|
|
4650
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5032
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5033
|
+
status,
|
|
5034
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5035
|
+
});
|
|
4651
5036
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4652
5037
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4653
5038
|
var credentialRoutes = (registry) => ({
|
|
@@ -4699,7 +5084,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4699
5084
|
return adminJson(200, { status: "ok" });
|
|
4700
5085
|
}
|
|
4701
5086
|
});
|
|
4702
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5087
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4703
5088
|
"GET /faults/presets": () => {
|
|
4704
5089
|
const body = {
|
|
4705
5090
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4709,7 +5094,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4709
5094
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4710
5095
|
const name = params.name;
|
|
4711
5096
|
if (!presets[name])
|
|
4712
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5097
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4713
5098
|
const overrides = isObject(body) ? body : {};
|
|
4714
5099
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4715
5100
|
}
|
|
@@ -5246,9 +5631,10 @@ function createRuntime2(options = {}) {
|
|
|
5246
5631
|
presets: OAUTH_PRESETS,
|
|
5247
5632
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5248
5633
|
...options.clock ? { clock: options.clock } : {},
|
|
5634
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5249
5635
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5250
5636
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5251
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new OAuthAPI({ ...options, sqlite, namespace, publicNamespace, now: clock.now }),
|
|
5637
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new OAuthAPI({ ...options, sqlite, namespace, publicNamespace, adminPrefix, now: clock.now }),
|
|
5252
5638
|
admin: (runtime) => ({
|
|
5253
5639
|
"GET /scenarios": () => Response.json(OAUTH_SCENARIOS),
|
|
5254
5640
|
"GET /behavior": ({ namespace }) => Response.json({
|
|
@@ -5364,7 +5750,6 @@ function createRuntime2(options = {}) {
|
|
|
5364
5750
|
|
|
5365
5751
|
// src/multi.ts
|
|
5366
5752
|
var MOUNT_PATTERN = /^\/[A-Za-z0-9](?:[A-Za-z0-9._~-]*)(?:\/[A-Za-z0-9](?:[A-Za-z0-9._~-]*))*$/;
|
|
5367
|
-
var NS_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
5368
5753
|
var json5 = (body, status = 200) => Response.json(body, { status, headers: { "cache-control": "no-store" } });
|
|
5369
5754
|
var validateMounts = (mounts) => {
|
|
5370
5755
|
if (mounts.length === 0) throw new Error("at least one OAuth mount is required");
|
|
@@ -5402,6 +5787,14 @@ var withoutMount = (request, namespacePrefix, rest) => {
|
|
|
5402
5787
|
return new Request(url, request);
|
|
5403
5788
|
};
|
|
5404
5789
|
function createMultiRuntime(options) {
|
|
5790
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5791
|
+
assertAdminPrefixAvailable(
|
|
5792
|
+
adminPrefix,
|
|
5793
|
+
options.mounts.map((mount) => mount.path)
|
|
5794
|
+
);
|
|
5795
|
+
for (const mount of options.mounts) {
|
|
5796
|
+
if (isAdminPath(adminPrefix, mount.path)) throw new Error("OAuth mount overlaps adminPrefix");
|
|
5797
|
+
}
|
|
5405
5798
|
validateMounts(options.mounts);
|
|
5406
5799
|
const clock = options.clock ?? createClock();
|
|
5407
5800
|
const runtimes = /* @__PURE__ */ new Map();
|
|
@@ -5409,6 +5802,7 @@ function createMultiRuntime(options) {
|
|
|
5409
5802
|
const { path, ...runtimeOptions } = mount;
|
|
5410
5803
|
const runtime = createRuntime2({
|
|
5411
5804
|
...runtimeOptions,
|
|
5805
|
+
adminPrefix,
|
|
5412
5806
|
clock,
|
|
5413
5807
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5414
5808
|
...runtimeOptions.adminKey ?? options.adminKey ? { adminKey: runtimeOptions.adminKey ?? options.adminKey } : {},
|
|
@@ -5429,27 +5823,27 @@ function createMultiRuntime(options) {
|
|
|
5429
5823
|
reset,
|
|
5430
5824
|
async fetch(request) {
|
|
5431
5825
|
const url = new URL(request.url);
|
|
5432
|
-
if (request.method === "GET" && url.pathname ===
|
|
5826
|
+
if (request.method === "GET" && url.pathname === `${adminPrefix}/health`) {
|
|
5433
5827
|
const health = await Promise.all(
|
|
5434
5828
|
[...runtimes.entries()].map(async ([path2, runtime]) => [
|
|
5435
5829
|
path2,
|
|
5436
|
-
await (await runtime.fetch(new Request(new URL(
|
|
5830
|
+
await (await runtime.fetch(new Request(new URL(`${adminPrefix}/health`, url), request))).json()
|
|
5437
5831
|
])
|
|
5438
5832
|
);
|
|
5439
5833
|
return json5({ status: "ok", service: "oauth", mounts: Object.fromEntries(health) });
|
|
5440
5834
|
}
|
|
5441
|
-
if (url.pathname.
|
|
5835
|
+
if (isAdminPath(url.pathname, adminPrefix) && !matchNamespacePath(url.pathname, adminPrefix) && options.adminKey !== void 0 && request.headers.get("x-mockingbird-admin-key") !== options.adminKey)
|
|
5442
5836
|
return json5({ error: { type: "mockingbird_admin", message: "invalid admin key" } }, 401);
|
|
5443
|
-
if (url.pathname ===
|
|
5837
|
+
if (url.pathname === `${adminPrefix}/mounts` && request.method === "GET")
|
|
5444
5838
|
return json5({
|
|
5445
5839
|
mounts: options.mounts.map(({ path: path2, provider, issuer }) => ({ path: path2, provider, issuer }))
|
|
5446
5840
|
});
|
|
5447
|
-
if (url.pathname ===
|
|
5841
|
+
if (url.pathname === `${adminPrefix}/reset` && request.method === "POST") {
|
|
5448
5842
|
const namespace2 = url.searchParams.get("all") === "1" ? "*" : url.searchParams.get("namespace") ?? request.headers.get("x-mockingbird-namespace") ?? "default";
|
|
5449
5843
|
await reset(namespace2);
|
|
5450
5844
|
return json5({ status: "ok", mounts: [...runtimes.keys()], namespace: namespace2 });
|
|
5451
5845
|
}
|
|
5452
|
-
if (url.pathname.
|
|
5846
|
+
if (isAdminPath(url.pathname, adminPrefix) && !matchNamespacePath(url.pathname, adminPrefix)) {
|
|
5453
5847
|
const selected = url.searchParams.get("mount");
|
|
5454
5848
|
const runtime = selected && runtimes.get(selected);
|
|
5455
5849
|
if (!runtime)
|
|
@@ -5457,8 +5851,8 @@ function createMultiRuntime(options) {
|
|
|
5457
5851
|
url.searchParams.delete("mount");
|
|
5458
5852
|
return runtime.fetch(new Request(url, request));
|
|
5459
5853
|
}
|
|
5460
|
-
const namespace =
|
|
5461
|
-
const namespacePrefix = namespace ?
|
|
5854
|
+
const namespace = matchNamespacePath(url.pathname, adminPrefix);
|
|
5855
|
+
const namespacePrefix = namespace ? `${adminPrefix}/ns/${namespace[1]}` : "";
|
|
5462
5856
|
const path = namespace ? namespace[2] ?? "/" : url.pathname;
|
|
5463
5857
|
for (const [mount, runtime] of runtimes) {
|
|
5464
5858
|
if (path !== mount && !path.startsWith(`${mount}/`)) continue;
|
|
@@ -5596,7 +5990,7 @@ var OAuthAPI = class {
|
|
|
5596
5990
|
this.seed();
|
|
5597
5991
|
}
|
|
5598
5992
|
issuer(request) {
|
|
5599
|
-
const prefix = this.options.publicNamespace && this.options.publicNamespace !== "default" ?
|
|
5993
|
+
const prefix = this.options.publicNamespace && this.options.publicNamespace !== "default" ? `${this.options.adminPrefix ?? "/__admin"}/ns/${encodeURIComponent(this.options.publicNamespace)}` : "";
|
|
5600
5994
|
return (this.options.issuer ?? `${new URL(request.url).origin}${prefix}${this.options.mountPath ?? ""}`).replace(/\/$/, "");
|
|
5601
5995
|
}
|
|
5602
5996
|
configureBehavior(input) {
|
|
@@ -6654,6 +7048,7 @@ var OAuthAPI = class {
|
|
|
6654
7048
|
|
|
6655
7049
|
export {
|
|
6656
7050
|
createClock,
|
|
7051
|
+
resolveAdminPrefix,
|
|
6657
7052
|
parseDuration,
|
|
6658
7053
|
OAUTH_SCENARIOS,
|
|
6659
7054
|
document2 as document,
|
|
@@ -6664,4 +7059,4 @@ export {
|
|
|
6664
7059
|
createMultiRuntime,
|
|
6665
7060
|
OAuthAPI
|
|
6666
7061
|
};
|
|
6667
|
-
//# sourceMappingURL=chunk-
|
|
7062
|
+
//# sourceMappingURL=chunk-I76SEED6.js.map
|