@hatk/hatk 0.0.1-alpha.69 → 0.0.1-alpha.70
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/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +25 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -9
- package/package.json +1 -1
- package/public/admin-auth.js +271 -306
- package/public/admin.html +26 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAkB,MAAM,aAAa,CAAA;AAC9D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAA;AAI1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAM1D,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AACD,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAMD,wBAAsB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnG;AAMD,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,WAAW,EAAE,EAC3B,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":"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"}
|
package/dist/database/db.js
CHANGED
|
@@ -65,6 +65,17 @@ export async function initDatabase(adapter, dbPath, tableSchemas, ddlStatements)
|
|
|
65
65
|
catch { }
|
|
66
66
|
// Re-queue repos with missing handles so backfill populates them
|
|
67
67
|
await run(`UPDATE _repos SET status = 'pending' WHERE handle IS NULL`);
|
|
68
|
+
// Covering index for the status rollups the admin overview and backfill queue
|
|
69
|
+
// read. Skipped on DuckDB, which scans the column just as fast and would only
|
|
70
|
+
// pay the ART index's insert cost.
|
|
71
|
+
if (port.dialect !== 'duckdb') {
|
|
72
|
+
await run(`CREATE INDEX IF NOT EXISTS idx_repos_status ON _repos(status)`);
|
|
73
|
+
}
|
|
74
|
+
// resolveHandleToDid runs on every feed request that takes an `actor` handle,
|
|
75
|
+
// and without this it scans all of _repos. Not UNIQUE: during a handle change
|
|
76
|
+
// two rows can briefly claim the same handle, and a constraint violation there
|
|
77
|
+
// would fail the indexer write.
|
|
78
|
+
await run(`CREATE INDEX IF NOT EXISTS idx_repos_handle ON _repos(handle)`);
|
|
68
79
|
await run(`CREATE TABLE IF NOT EXISTS _cursor (
|
|
69
80
|
key TEXT PRIMARY KEY,
|
|
70
81
|
value TEXT NOT NULL
|
|
@@ -422,11 +433,21 @@ export async function listReposPaginated(opts = {}) {
|
|
|
422
433
|
return { repos: rows, total };
|
|
423
434
|
}
|
|
424
435
|
export async function getCollectionCounts() {
|
|
436
|
+
const entries = [...schemas];
|
|
425
437
|
const counts = {};
|
|
426
|
-
for (const [collection
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
438
|
+
for (const [collection] of entries)
|
|
439
|
+
counts[collection] = 0;
|
|
440
|
+
if (entries.length === 0)
|
|
441
|
+
return counts;
|
|
442
|
+
// One round trip instead of one per collection — reads are serialized on a
|
|
443
|
+
// single connection, so N queries means N waits behind whatever else is
|
|
444
|
+
// reading.
|
|
445
|
+
const sql = entries
|
|
446
|
+
.map(([collection, schema]) => `SELECT '${collection.replace(/'/g, "''")}' as collection, ${dialect.countAsInteger} as count FROM ${schema.tableName}`)
|
|
447
|
+
.join(' UNION ALL ');
|
|
448
|
+
const rows = await all(sql);
|
|
449
|
+
for (const row of rows)
|
|
450
|
+
counts[row.collection] = Number(row.count || 0);
|
|
430
451
|
return counts;
|
|
431
452
|
}
|
|
432
453
|
export async function getRepoStatusCounts() {
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AA4B9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAqM3F;AAgDD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAy5B5F;AAGD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,GAAE,MAAM,EAAO,EACrB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,GACpB,OAAO,WAAW,EAAE,MAAM,CAG5B"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { join, extname } from 'node:path';
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
4
6
|
import { queryRecords, getRecordByUri, searchRecords, getSchema, reshapeRow, setRepoStatus, getRepoStatus, getRepoRetryInfo, queryLabelsForUris, insertLabels, searchAccounts, listReposPaginated, getCollectionCounts, getRepoStatusCounts, getDatabaseSize, deleteLabels, getRecentRecords, listActiveRepoDids, removeRepo, getRepoHandle, getPreferences, putPreference, insertReport, queryReports, resolveReport, getOpenReportCount, } from "./database/db.js";
|
|
5
7
|
import { executeFeed, listFeeds } from "./feeds.js";
|
|
6
8
|
import { executeXrpc, InvalidRequestError, NotFoundError, registerCoreXrpcHandler } from "./xrpc.js";
|
|
@@ -240,6 +242,37 @@ export function registerCoreHandlers(collections, oauth) {
|
|
|
240
242
|
});
|
|
241
243
|
}
|
|
242
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Assets we ship inside the package never change while the process is alive, so
|
|
247
|
+
* read, gzip and hash them once. Repeat visits then cost a 304 instead of the
|
|
248
|
+
* full (uncompressed) payload.
|
|
249
|
+
*/
|
|
250
|
+
const bundledAssets = new Map();
|
|
251
|
+
async function serveBundledAsset(path, contentType, request, devMode) {
|
|
252
|
+
let asset = devMode ? undefined : bundledAssets.get(path);
|
|
253
|
+
if (!asset) {
|
|
254
|
+
const raw = await readFile(path);
|
|
255
|
+
asset = {
|
|
256
|
+
raw,
|
|
257
|
+
gzip: gzipSync(raw),
|
|
258
|
+
etag: `"${createHash('sha256').update(raw).digest('base64url').slice(0, 16)}"`,
|
|
259
|
+
};
|
|
260
|
+
if (!devMode)
|
|
261
|
+
bundledAssets.set(path, asset);
|
|
262
|
+
}
|
|
263
|
+
const headers = { ETag: asset.etag, 'Cache-Control': 'no-cache' };
|
|
264
|
+
if (request.headers.get('if-none-match') === asset.etag) {
|
|
265
|
+
return new Response(null, { status: 304, headers });
|
|
266
|
+
}
|
|
267
|
+
const acceptEncoding = request.headers.get('accept-encoding');
|
|
268
|
+
const useGzip = !!acceptEncoding && /\bgzip\b/.test(acceptEncoding);
|
|
269
|
+
if (useGzip) {
|
|
270
|
+
headers['Content-Encoding'] = 'gzip';
|
|
271
|
+
headers.Vary = 'Accept-Encoding';
|
|
272
|
+
}
|
|
273
|
+
headers['Content-Type'] = contentType;
|
|
274
|
+
return new Response(Buffer.from(useGzip ? asset.gzip : asset.raw), { status: 200, headers });
|
|
275
|
+
}
|
|
243
276
|
/**
|
|
244
277
|
* Create a Web Standard request handler for all hatk routes.
|
|
245
278
|
* Returns a pure function: (Request) → Promise<Response>
|
|
@@ -255,6 +288,37 @@ export function createHandler(config) {
|
|
|
255
288
|
return withCors(jsonError(403, 'Admin access required', acceptEncoding));
|
|
256
289
|
return null; // auth OK
|
|
257
290
|
}
|
|
291
|
+
// The /admin/info rollups count every row in every collection table, so they get
|
|
292
|
+
// slower as the index grows. Cache them for a few seconds — that collapses the
|
|
293
|
+
// overview's repeat loads and any concurrent requests into one scan — and drop
|
|
294
|
+
// the cache whenever an admin action changes what they report.
|
|
295
|
+
const ADMIN_STATS_TTL_MS = 10_000;
|
|
296
|
+
let adminStats = null;
|
|
297
|
+
function getAdminStats() {
|
|
298
|
+
if (adminStats && Date.now() - adminStats.at < ADMIN_STATS_TTL_MS)
|
|
299
|
+
return adminStats.data;
|
|
300
|
+
const data = (async () => {
|
|
301
|
+
const [repos, duckdb, collections, openReports] = await Promise.all([
|
|
302
|
+
getRepoStatusCounts(),
|
|
303
|
+
getDatabaseSize(),
|
|
304
|
+
getCollectionCounts(),
|
|
305
|
+
getOpenReportCount(),
|
|
306
|
+
]);
|
|
307
|
+
return { repos, duckdb, collections, openReports };
|
|
308
|
+
})();
|
|
309
|
+
const entry = { at: Date.now(), data };
|
|
310
|
+
adminStats = entry;
|
|
311
|
+
// Don't cache a failure — and keep the rejection handled so it surfaces only
|
|
312
|
+
// at the awaiting request.
|
|
313
|
+
data.catch(() => {
|
|
314
|
+
if (adminStats === entry)
|
|
315
|
+
adminStats = null;
|
|
316
|
+
});
|
|
317
|
+
return data;
|
|
318
|
+
}
|
|
319
|
+
function invalidateAdminStats() {
|
|
320
|
+
adminStats = null;
|
|
321
|
+
}
|
|
258
322
|
return async (request) => {
|
|
259
323
|
const url = new URL(request.url);
|
|
260
324
|
const acceptEncoding = request.headers.get('accept-encoding');
|
|
@@ -425,6 +489,7 @@ export function createHandler(config) {
|
|
|
425
489
|
await setRepoStatus(did, 'pending');
|
|
426
490
|
triggerAutoBackfill(did);
|
|
427
491
|
}
|
|
492
|
+
invalidateAdminStats();
|
|
428
493
|
return withCors(json({ added: dids.length }, 200, acceptEncoding));
|
|
429
494
|
}
|
|
430
495
|
// POST /admin/labels/rescan — retroactively apply label rules
|
|
@@ -492,6 +557,7 @@ export function createHandler(config) {
|
|
|
492
557
|
if (!did)
|
|
493
558
|
return withCors(jsonError(400, 'Missing did', acceptEncoding));
|
|
494
559
|
await setRepoStatus(did, 'takendown');
|
|
560
|
+
invalidateAdminStats();
|
|
495
561
|
return withCors(json({ ok: true }, 200, acceptEncoding));
|
|
496
562
|
}
|
|
497
563
|
// POST /admin/reverse-takedown — reverse a takedown
|
|
@@ -503,6 +569,7 @@ export function createHandler(config) {
|
|
|
503
569
|
if (!did)
|
|
504
570
|
return withCors(jsonError(400, 'Missing did', acceptEncoding));
|
|
505
571
|
await setRepoStatus(did, 'active');
|
|
572
|
+
invalidateAdminStats();
|
|
506
573
|
return withCors(json({ ok: true }, 200, acceptEncoding));
|
|
507
574
|
}
|
|
508
575
|
// GET /admin/search — search records or accounts
|
|
@@ -623,6 +690,7 @@ export function createHandler(config) {
|
|
|
623
690
|
triggerAutoBackfill(did);
|
|
624
691
|
}
|
|
625
692
|
}
|
|
693
|
+
invalidateAdminStats();
|
|
626
694
|
return withCors(json({ resyncing: repoList.length }, 200, acceptEncoding));
|
|
627
695
|
}
|
|
628
696
|
// POST /admin/repos/remove — remove DIDs from tracking
|
|
@@ -636,6 +704,7 @@ export function createHandler(config) {
|
|
|
636
704
|
for (const did of dids) {
|
|
637
705
|
await removeRepo(did);
|
|
638
706
|
}
|
|
707
|
+
invalidateAdminStats();
|
|
639
708
|
return withCors(json({ removed: dids.length }, 200, acceptEncoding));
|
|
640
709
|
}
|
|
641
710
|
// GET /admin/info — aggregate status + db size + collection counts
|
|
@@ -643,9 +712,7 @@ export function createHandler(config) {
|
|
|
643
712
|
const denied = requireAdmin(viewer, acceptEncoding);
|
|
644
713
|
if (denied)
|
|
645
714
|
return denied;
|
|
646
|
-
const
|
|
647
|
-
const dbInfo = await getDatabaseSize();
|
|
648
|
-
const collectionCounts = await getCollectionCounts();
|
|
715
|
+
const stats = await getAdminStats();
|
|
649
716
|
const mem = process.memoryUsage();
|
|
650
717
|
const node = {
|
|
651
718
|
rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MiB`,
|
|
@@ -653,8 +720,13 @@ export function createHandler(config) {
|
|
|
653
720
|
heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(1)} MiB`,
|
|
654
721
|
external: `${(mem.external / 1024 / 1024).toFixed(1)} MiB`,
|
|
655
722
|
};
|
|
656
|
-
|
|
657
|
-
|
|
723
|
+
return withCors(json({
|
|
724
|
+
repos: stats.repos,
|
|
725
|
+
duckdb: stats.duckdb,
|
|
726
|
+
node,
|
|
727
|
+
collections: stats.collections,
|
|
728
|
+
openReports: stats.openReports,
|
|
729
|
+
}, 200, acceptEncoding));
|
|
658
730
|
}
|
|
659
731
|
// GET /admin/reports — list reports
|
|
660
732
|
if (url.pathname === '/admin/reports' && request.method === 'GET') {
|
|
@@ -684,6 +756,7 @@ export function createHandler(config) {
|
|
|
684
756
|
if (action === 'resolve') {
|
|
685
757
|
await insertLabels([{ src: 'admin', uri: report.subjectUri, val: report.label }]);
|
|
686
758
|
}
|
|
759
|
+
invalidateAdminStats();
|
|
687
760
|
return withCors(json({ ok: true }, 200, acceptEncoding));
|
|
688
761
|
}
|
|
689
762
|
// GET /admin/info/:did — repo status info
|
|
@@ -953,8 +1026,7 @@ export function createHandler(config) {
|
|
|
953
1026
|
if (url.pathname === '/admin' || url.pathname === '/admin/') {
|
|
954
1027
|
const adminPath = join(import.meta.dirname, '../public/admin.html');
|
|
955
1028
|
try {
|
|
956
|
-
|
|
957
|
-
return withCors(file(content, 'text/html'));
|
|
1029
|
+
return withCors(await serveBundledAsset(adminPath, 'text/html', request, devMode));
|
|
958
1030
|
}
|
|
959
1031
|
catch {
|
|
960
1032
|
return withCors(new Response('Admin page not found', { status: 404 }));
|
|
@@ -964,8 +1036,7 @@ export function createHandler(config) {
|
|
|
964
1036
|
if (url.pathname === '/admin/admin-auth.js') {
|
|
965
1037
|
const authPath = join(import.meta.dirname, '../public/admin-auth.js');
|
|
966
1038
|
try {
|
|
967
|
-
|
|
968
|
-
return withCors(file(content, 'application/javascript'));
|
|
1039
|
+
return withCors(await serveBundledAsset(authPath, 'application/javascript', request, devMode));
|
|
969
1040
|
}
|
|
970
1041
|
catch {
|
|
971
1042
|
return notFound();
|
package/package.json
CHANGED
package/public/admin-auth.js
CHANGED
|
@@ -1,320 +1,285 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
//#region ../oauth-client/src/crypto.js
|
|
2
|
+
function e(e) {
|
|
3
|
+
let t = e instanceof Uint8Array ? e : new Uint8Array(e), n = "";
|
|
4
|
+
for (let e = 0; e < t.length; e++) n += String.fromCharCode(t[e]);
|
|
5
|
+
return btoa(n).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
7
|
+
function t(t = 32) {
|
|
8
|
+
return e(crypto.getRandomValues(new Uint8Array(t)));
|
|
9
9
|
}
|
|
10
|
-
async function
|
|
11
|
-
|
|
10
|
+
async function n(e) {
|
|
11
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(e)));
|
|
12
12
|
}
|
|
13
|
-
async function
|
|
14
|
-
|
|
13
|
+
async function r(t) {
|
|
14
|
+
return e(await n(t));
|
|
15
15
|
}
|
|
16
|
-
async function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `${a}.${g(i)}`
|
|
16
|
+
async function i(t, n, r) {
|
|
17
|
+
let i = new TextEncoder(), a = `${e(i.encode(JSON.stringify(t)))}.${e(i.encode(JSON.stringify(n)))}`;
|
|
18
|
+
return `${a}.${e(await crypto.subtle.sign({
|
|
19
|
+
name: "ECDSA",
|
|
20
|
+
hash: "SHA-256"
|
|
21
|
+
}, r, i.encode(a)))}`;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}))
|
|
39
|
-
})
|
|
40
|
-
return (y.set(s, o), o)
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region ../oauth-client/src/dpop.js
|
|
25
|
+
var a = "dpop-keys", o = "dpop-key", s = /* @__PURE__ */ new Map();
|
|
26
|
+
function c(e) {
|
|
27
|
+
let t = s.get(e);
|
|
28
|
+
if (t) return t;
|
|
29
|
+
let n = new Promise((t, n) => {
|
|
30
|
+
let r = indexedDB.open(`appview-oauth-${e}`, 1);
|
|
31
|
+
r.onerror = () => n(r.error), r.onsuccess = () => t(r.result), r.onupgradeneeded = (e) => {
|
|
32
|
+
let t = e.target.result;
|
|
33
|
+
t.objectStoreNames.contains(a) || t.createObjectStore(a, { keyPath: "id" });
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
return s.set(e, n), n;
|
|
41
37
|
}
|
|
42
|
-
async function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
async function l(e) {
|
|
39
|
+
let t = await c(e);
|
|
40
|
+
return new Promise((e, n) => {
|
|
41
|
+
let r = t.transaction(a, "readonly").objectStore(a).get(o);
|
|
42
|
+
r.onsuccess = () => e(r.result || null), r.onerror = () => n(r.error);
|
|
43
|
+
});
|
|
48
44
|
}
|
|
49
|
-
async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
async function u(e, t, n) {
|
|
46
|
+
let r = await c(e);
|
|
47
|
+
return new Promise((e, i) => {
|
|
48
|
+
let s = r.transaction(a, "readwrite").objectStore(a).put({
|
|
49
|
+
id: o,
|
|
50
|
+
privateKey: t,
|
|
51
|
+
publicJwk: n,
|
|
52
|
+
createdAt: Date.now()
|
|
53
|
+
});
|
|
54
|
+
s.onsuccess = () => e(), s.onerror = () => i(s.error);
|
|
55
|
+
});
|
|
60
56
|
}
|
|
61
|
-
async function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
57
|
+
async function d(e) {
|
|
58
|
+
let t = await l(e);
|
|
59
|
+
if (t) return t;
|
|
60
|
+
let n = await crypto.subtle.generateKey({
|
|
61
|
+
name: "ECDSA",
|
|
62
|
+
namedCurve: "P-256"
|
|
63
|
+
}, !1, ["sign"]), r = await crypto.subtle.exportKey("jwk", n.publicKey);
|
|
64
|
+
return await u(e, n.privateKey, r), {
|
|
65
|
+
id: o,
|
|
66
|
+
privateKey: n.privateKey,
|
|
67
|
+
publicJwk: r,
|
|
68
|
+
createdAt: Date.now()
|
|
69
|
+
};
|
|
72
70
|
}
|
|
73
|
-
async function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
async function f(e) {
|
|
72
|
+
let t = await c(e);
|
|
73
|
+
return new Promise((e, n) => {
|
|
74
|
+
let r = t.transaction(a, "readwrite").objectStore(a).delete(o);
|
|
75
|
+
r.onsuccess = () => e(), r.onerror = () => n(r.error);
|
|
76
|
+
});
|
|
79
77
|
}
|
|
80
|
-
async function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
async function p(e, n, a, o) {
|
|
79
|
+
let s = await d(e), { kty: c, crv: l, x: u, y: f } = s.publicJwk, p = {
|
|
80
|
+
alg: "ES256",
|
|
81
|
+
typ: "dpop+jwt",
|
|
82
|
+
jwk: {
|
|
83
|
+
kty: c,
|
|
84
|
+
crv: l,
|
|
85
|
+
x: u,
|
|
86
|
+
y: f
|
|
87
|
+
}
|
|
88
|
+
}, m = {
|
|
89
|
+
jti: t(16),
|
|
90
|
+
htm: n,
|
|
91
|
+
htu: a.split("?")[0],
|
|
92
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
93
|
+
};
|
|
94
|
+
return o && (m.ath = await r(o)), i(p, m, s.privateKey);
|
|
91
95
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region ../oauth-client/src/storage.js
|
|
98
|
+
function m(e) {
|
|
99
|
+
let t = (t) => `appview_${e}_${t}`, n = /* @__PURE__ */ new Set([
|
|
100
|
+
"codeVerifier",
|
|
101
|
+
"oauthState",
|
|
102
|
+
"redirectUri"
|
|
103
|
+
]);
|
|
104
|
+
return {
|
|
105
|
+
get(e) {
|
|
106
|
+
return (n.has(e) ? sessionStorage : localStorage).getItem(t(e));
|
|
107
|
+
},
|
|
108
|
+
set(e, r) {
|
|
109
|
+
(n.has(e) ? sessionStorage : localStorage).setItem(t(e), r);
|
|
110
|
+
},
|
|
111
|
+
remove(e) {
|
|
112
|
+
(n.has(e) ? sessionStorage : localStorage).removeItem(t(e));
|
|
113
|
+
},
|
|
114
|
+
clear() {
|
|
115
|
+
for (let e of n) sessionStorage.removeItem(t(e));
|
|
116
|
+
for (let e of [
|
|
117
|
+
"accessToken",
|
|
118
|
+
"refreshToken",
|
|
119
|
+
"tokenExpiresAt",
|
|
120
|
+
"userDid",
|
|
121
|
+
"clientId"
|
|
122
|
+
]) localStorage.removeItem(t(e));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
111
125
|
}
|
|
112
|
-
|
|
113
|
-
async function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return e
|
|
129
|
-
}
|
|
130
|
-
return null
|
|
126
|
+
var h = 5e3;
|
|
127
|
+
async function g(e, t) {
|
|
128
|
+
let n = `appview_${e}_lock_${t}`, r = `${Date.now()}_${Math.random()}`, i = Date.now() + h;
|
|
129
|
+
for (; Date.now() < i;) {
|
|
130
|
+
let e = localStorage.getItem(n);
|
|
131
|
+
if (e) {
|
|
132
|
+
let t = parseInt(e.split("_")[0]);
|
|
133
|
+
if (Date.now() - t > h) localStorage.removeItem(n);
|
|
134
|
+
else {
|
|
135
|
+
await new Promise((e) => setTimeout(e, 50));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (localStorage.setItem(n, r), await new Promise((e) => setTimeout(e, 10)), localStorage.getItem(n) === r) return r;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
131
142
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
function _(e, t, n) {
|
|
144
|
+
let r = `appview_${e}_lock_${t}`;
|
|
145
|
+
localStorage.getItem(r) === n && localStorage.removeItem(r);
|
|
135
146
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
async logout() {
|
|
276
|
-
;(this.storage.clear(), await v(this.namespace), (this._initPromise = null))
|
|
277
|
-
}
|
|
278
|
-
// --- Private ---
|
|
279
|
-
_storeTokens(t) {
|
|
280
|
-
;(this.storage.set('accessToken', t.access_token),
|
|
281
|
-
t.refresh_token && this.storage.set('refreshToken', t.refresh_token),
|
|
282
|
-
t.sub && this.storage.set('userDid', t.sub))
|
|
283
|
-
const o = Date.now() + (t.expires_in || 3600) * 1e3
|
|
284
|
-
this.storage.set('tokenExpiresAt', o.toString())
|
|
285
|
-
}
|
|
286
|
-
async _getValidToken() {
|
|
287
|
-
const t = this.storage.get('accessToken'),
|
|
288
|
-
o = parseInt(this.storage.get('tokenExpiresAt') || '0')
|
|
289
|
-
if (t && Date.now() < o - _) return t
|
|
290
|
-
const e = this.storage.get('refreshToken')
|
|
291
|
-
if (!e) return null
|
|
292
|
-
const n = await A(this.namespace, 'refresh')
|
|
293
|
-
if (!n) return (await new Promise((r) => setTimeout(r, 150)), this.storage.get('accessToken'))
|
|
294
|
-
try {
|
|
295
|
-
const r = this.storage.get('accessToken'),
|
|
296
|
-
a = parseInt(this.storage.get('tokenExpiresAt') || '0')
|
|
297
|
-
if (r && Date.now() < a - _) return r
|
|
298
|
-
const i = `${this.server}/oauth/token`,
|
|
299
|
-
c = await w(this.namespace, 'POST', i),
|
|
300
|
-
h = await fetch(i, {
|
|
301
|
-
method: 'POST',
|
|
302
|
-
headers: {
|
|
303
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
304
|
-
DPoP: c,
|
|
305
|
-
},
|
|
306
|
-
body: new URLSearchParams({
|
|
307
|
-
grant_type: 'refresh_token',
|
|
308
|
-
refresh_token: e,
|
|
309
|
-
client_id: this.storage.get('clientId') || this.clientId,
|
|
310
|
-
}),
|
|
311
|
-
})
|
|
312
|
-
if (!h.ok) return (this.storage.clear(), null)
|
|
313
|
-
const l = await h.json()
|
|
314
|
-
return (this._storeTokens(l), l.access_token)
|
|
315
|
-
} finally {
|
|
316
|
-
O(this.namespace, 'refresh', n)
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
export { K as OAuthClient }
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region ../oauth-client/src/client.js
|
|
149
|
+
var v = 6e4, y = class {
|
|
150
|
+
constructor({ server: e, clientId: t, redirectUri: n, scope: r }) {
|
|
151
|
+
this.server = e.replace(/\/$/, ""), this.clientId = t || window.location.origin, this.redirectUri = n || window.location.origin + window.location.pathname, this.scope = r || "atproto", this.namespace = this.clientId.replace(/[^a-z0-9]/gi, "_").slice(0, 32), this.storage = m(this.namespace), this._initPromise = null, this._refreshPromise = null;
|
|
152
|
+
}
|
|
153
|
+
async init() {
|
|
154
|
+
return this._initPromise ||= d(this.namespace), this._initPromise;
|
|
155
|
+
}
|
|
156
|
+
async login(e) {
|
|
157
|
+
await this.init();
|
|
158
|
+
let n = t(32), i = await r(n), a = t(16);
|
|
159
|
+
this.storage.set("codeVerifier", n), this.storage.set("oauthState", a), this.storage.set("clientId", this.clientId), this.storage.set("redirectUri", this.redirectUri);
|
|
160
|
+
let o = `${this.server}/oauth/par`, s = await p(this.namespace, "POST", o), c = new URLSearchParams({
|
|
161
|
+
client_id: this.clientId,
|
|
162
|
+
redirect_uri: this.redirectUri,
|
|
163
|
+
response_type: "code",
|
|
164
|
+
code_challenge: i,
|
|
165
|
+
code_challenge_method: "S256",
|
|
166
|
+
scope: this.scope,
|
|
167
|
+
state: a
|
|
168
|
+
});
|
|
169
|
+
e && c.set("login_hint", e);
|
|
170
|
+
let l = await fetch(o, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
174
|
+
DPoP: s
|
|
175
|
+
},
|
|
176
|
+
body: c.toString()
|
|
177
|
+
});
|
|
178
|
+
if (!l.ok) {
|
|
179
|
+
let e = await l.json().catch(() => ({}));
|
|
180
|
+
throw Error(`PAR failed: ${e.error || l.status}`);
|
|
181
|
+
}
|
|
182
|
+
let { request_uri: u } = await l.json(), d = new URLSearchParams({
|
|
183
|
+
request_uri: u,
|
|
184
|
+
client_id: this.clientId
|
|
185
|
+
});
|
|
186
|
+
window.location.href = `${this.server}/oauth/authorize?${d}`;
|
|
187
|
+
}
|
|
188
|
+
async handleCallback() {
|
|
189
|
+
let e = new URLSearchParams(window.location.search), t = e.get("code"), n = e.get("state"), r = e.get("error");
|
|
190
|
+
if (r) throw Error(`OAuth error: ${r} - ${e.get("error_description") || ""}`);
|
|
191
|
+
if (!t || !n) return !1;
|
|
192
|
+
if (n !== this.storage.get("oauthState")) throw Error("OAuth state mismatch");
|
|
193
|
+
let i = this.storage.get("codeVerifier"), a = this.storage.get("clientId"), o = this.storage.get("redirectUri");
|
|
194
|
+
if (!i || !a || !o) throw Error("Missing OAuth session data");
|
|
195
|
+
await this.init();
|
|
196
|
+
let s = `${this.server}/oauth/token`, c = await p(this.namespace, "POST", s), l = await fetch(s, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
200
|
+
DPoP: c
|
|
201
|
+
},
|
|
202
|
+
body: new URLSearchParams({
|
|
203
|
+
grant_type: "authorization_code",
|
|
204
|
+
code: t,
|
|
205
|
+
redirect_uri: o,
|
|
206
|
+
client_id: a,
|
|
207
|
+
code_verifier: i
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
if (!l.ok) {
|
|
211
|
+
let e = await l.json().catch(() => ({}));
|
|
212
|
+
throw Error(`Token exchange failed: ${e.error_description || l.statusText}`);
|
|
213
|
+
}
|
|
214
|
+
let u = await l.json();
|
|
215
|
+
return this._storeTokens(u), this.storage.remove("codeVerifier"), this.storage.remove("oauthState"), this.storage.remove("redirectUri"), window.history.replaceState({}, document.title, window.location.pathname), !0;
|
|
216
|
+
}
|
|
217
|
+
get isLoggedIn() {
|
|
218
|
+
return !!this.storage.get("accessToken") && !!this.storage.get("userDid");
|
|
219
|
+
}
|
|
220
|
+
get did() {
|
|
221
|
+
return this.storage.get("userDid");
|
|
222
|
+
}
|
|
223
|
+
get handle() {
|
|
224
|
+
return this.storage.get("userHandle");
|
|
225
|
+
}
|
|
226
|
+
async fetch(e, t = {}) {
|
|
227
|
+
await this.init();
|
|
228
|
+
let n = e.startsWith("http") ? e : `${this.server}${e}`, r = (t.method || "GET").toUpperCase(), i = await this._getValidToken();
|
|
229
|
+
if (!i) throw Error("Not authenticated");
|
|
230
|
+
let a = await p(this.namespace, r, n, i), o = {
|
|
231
|
+
...t.headers,
|
|
232
|
+
Authorization: `DPoP ${i}`,
|
|
233
|
+
DPoP: a
|
|
234
|
+
}, s = await fetch(n, {
|
|
235
|
+
...t,
|
|
236
|
+
method: r,
|
|
237
|
+
headers: o
|
|
238
|
+
});
|
|
239
|
+
if (s.status === 403 && (await s.clone().json().catch(() => ({}))).error === "ScopeMissingError") throw this.login(this.did), Error("Re-authenticating with updated scopes");
|
|
240
|
+
return s;
|
|
241
|
+
}
|
|
242
|
+
async logout() {
|
|
243
|
+
this.storage.clear(), await f(this.namespace), this._initPromise = null;
|
|
244
|
+
}
|
|
245
|
+
_storeTokens(e) {
|
|
246
|
+
this.storage.set("accessToken", e.access_token), e.refresh_token && this.storage.set("refreshToken", e.refresh_token), e.sub && this.storage.set("userDid", e.sub), e.handle && this.storage.set("userHandle", e.handle);
|
|
247
|
+
let t = Date.now() + (e.expires_in || 3600) * 1e3;
|
|
248
|
+
this.storage.set("tokenExpiresAt", t.toString());
|
|
249
|
+
}
|
|
250
|
+
async _getValidToken() {
|
|
251
|
+
let e = this.storage.get("accessToken"), t = parseInt(this.storage.get("tokenExpiresAt") || "0");
|
|
252
|
+
return e && Date.now() < t - v ? e : (this._refreshPromise ||= this._refresh().finally(() => {
|
|
253
|
+
this._refreshPromise = null;
|
|
254
|
+
}), this._refreshPromise);
|
|
255
|
+
}
|
|
256
|
+
async _refresh() {
|
|
257
|
+
let e = this.storage.get("refreshToken");
|
|
258
|
+
if (!e) return null;
|
|
259
|
+
let t = await g(this.namespace, "refresh");
|
|
260
|
+
if (!t) return await new Promise((e) => setTimeout(e, 150)), this.storage.get("accessToken");
|
|
261
|
+
try {
|
|
262
|
+
let t = this.storage.get("accessToken"), n = parseInt(this.storage.get("tokenExpiresAt") || "0");
|
|
263
|
+
if (t && Date.now() < n - v) return t;
|
|
264
|
+
let r = `${this.server}/oauth/token`, i = await p(this.namespace, "POST", r), a = await fetch(r, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
268
|
+
DPoP: i
|
|
269
|
+
},
|
|
270
|
+
body: new URLSearchParams({
|
|
271
|
+
grant_type: "refresh_token",
|
|
272
|
+
refresh_token: e,
|
|
273
|
+
client_id: this.storage.get("clientId") || this.clientId
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
if (!a.ok) return this.storage.clear(), null;
|
|
277
|
+
let o = await a.json();
|
|
278
|
+
return this._storeTokens(o), o.access_token;
|
|
279
|
+
} finally {
|
|
280
|
+
_(this.namespace, "refresh", t);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
//#endregion
|
|
285
|
+
export { y as OAuthClient };
|
package/public/admin.html
CHANGED
|
@@ -6,10 +6,20 @@
|
|
|
6
6
|
<title>Admin</title>
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<!-- Loaded off the critical path: media="print" keeps it from blocking the
|
|
10
|
+
first render, and the system-ui/Menlo fallbacks carry until it swaps in. -->
|
|
9
11
|
<link
|
|
10
12
|
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Figtree:wght@400;500;600;700&display=swap"
|
|
11
13
|
rel="stylesheet"
|
|
14
|
+
media="print"
|
|
15
|
+
onload="this.media = 'all'"
|
|
12
16
|
/>
|
|
17
|
+
<noscript>
|
|
18
|
+
<link
|
|
19
|
+
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Figtree:wght@400;500;600;700&display=swap"
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
/>
|
|
22
|
+
</noscript>
|
|
13
23
|
<style>
|
|
14
24
|
*,
|
|
15
25
|
*::before,
|
|
@@ -1318,6 +1328,8 @@
|
|
|
1318
1328
|
|
|
1319
1329
|
window.auth = auth
|
|
1320
1330
|
let labelDefinitions = []
|
|
1331
|
+
// Started during boot so the overview's data is in flight while we auth
|
|
1332
|
+
let overviewPrefetch = null
|
|
1321
1333
|
|
|
1322
1334
|
function toast(msg, type = '') {
|
|
1323
1335
|
const el = document.createElement('div')
|
|
@@ -1397,8 +1409,17 @@
|
|
|
1397
1409
|
}
|
|
1398
1410
|
loginScreen.style.display = 'none'
|
|
1399
1411
|
|
|
1412
|
+
// Fire these together — awaiting them one at a time puts three round
|
|
1413
|
+
// trips in front of the first render.
|
|
1414
|
+
const whoamiPromise = api('/admin/whoami')
|
|
1415
|
+
const definitionsPromise = api('/admin/labels/definitions').catch(() => null)
|
|
1416
|
+
if (getURLState().tab === 'overview') {
|
|
1417
|
+
overviewPrefetch = api('/admin/info')
|
|
1418
|
+
overviewPrefetch.catch(() => {}) // loadOverview reports the failure
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1400
1421
|
try {
|
|
1401
|
-
const whoami = await
|
|
1422
|
+
const whoami = await whoamiPromise
|
|
1402
1423
|
document.getElementById('viewer-did').textContent = whoami.did
|
|
1403
1424
|
} catch (e) {
|
|
1404
1425
|
document.getElementById('error-msg').textContent =
|
|
@@ -1409,10 +1430,7 @@
|
|
|
1409
1430
|
return
|
|
1410
1431
|
}
|
|
1411
1432
|
|
|
1412
|
-
|
|
1413
|
-
const result = await api('/admin/labels/definitions')
|
|
1414
|
-
labelDefinitions = result.definitions || []
|
|
1415
|
-
} catch {}
|
|
1433
|
+
labelDefinitions = (await definitionsPromise)?.definitions || []
|
|
1416
1434
|
|
|
1417
1435
|
appEl.classList.add('visible')
|
|
1418
1436
|
restoreFromURL()
|
|
@@ -1499,7 +1517,9 @@
|
|
|
1499
1517
|
|
|
1500
1518
|
async function loadOverview() {
|
|
1501
1519
|
try {
|
|
1502
|
-
const
|
|
1520
|
+
const pending = overviewPrefetch
|
|
1521
|
+
overviewPrefetch = null
|
|
1522
|
+
const info = await (pending || api('/admin/info'))
|
|
1503
1523
|
const statusCards = document.getElementById('status-cards')
|
|
1504
1524
|
const repoStatuses = info.repos || {}
|
|
1505
1525
|
const total = Object.values(repoStatuses).reduce((a, b) => a + b, 0)
|