@tensorix/metahub 0.1.2 → 0.1.3
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/README.md +30 -5
- package/dist/cli.js +2445 -171
- package/dist/cli.js.map +28 -16
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/db.d.ts +6 -0
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts +1 -0
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/sync/auth.d.ts +6 -0
- package/dist/core/sync/auth.d.ts.map +1 -1
- package/dist/core/sync/client.d.ts +7 -2
- package/dist/core/sync/client.d.ts.map +1 -1
- package/dist/core/sync/pairing.d.ts +50 -0
- package/dist/core/sync/pairing.d.ts.map +1 -0
- package/dist/core/sync/peers-routes.d.ts +3 -0
- package/dist/core/sync/peers-routes.d.ts.map +1 -0
- package/dist/core/sync/peers.d.ts +45 -0
- package/dist/core/sync/peers.d.ts.map +1 -0
- package/dist/core/sync/protocol.d.ts +13 -0
- package/dist/core/sync/protocol.d.ts.map +1 -1
- package/dist/core/sync/routes.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts +6 -0
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/webui-routes.d.ts.map +1 -1
- package/dist/core/sync/webui.d.ts.map +1 -1
- package/dist/index.js +526 -93
- package/dist/index.js.map +18 -14
- package/dist/webui.js +31 -30
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -704,6 +704,21 @@ var HTML = `<!doctype html>
|
|
|
704
704
|
.theme-card.sel .tc-check { opacity:1; transform:scale(1); }
|
|
705
705
|
@media (max-width:560px) { .theme-grid { grid-template-columns:1fr; } }
|
|
706
706
|
|
|
707
|
+
/* sync devices */
|
|
708
|
+
.peer-actions { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; }
|
|
709
|
+
.peer-actions .btn { display:inline-flex; align-items:center; gap:6px; }
|
|
710
|
+
.peer-list { display:flex; flex-direction:column; gap:8px; }
|
|
711
|
+
.peer-row { display:flex; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--line);
|
|
712
|
+
border-radius:10px; background:var(--surface); }
|
|
713
|
+
.peer-dot { width:9px; height:9px; border-radius:50%; flex:none; background:var(--muted); }
|
|
714
|
+
.peer-dot.on { background:#3fb950; } .peer-dot.err { background:#f85149; } .peer-dot.off { background:var(--muted); }
|
|
715
|
+
.peer-main { flex:1; min-width:0; }
|
|
716
|
+
.peer-url { font-weight:600; font-size:14px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
717
|
+
.peer-sub { font-size:12px; color:var(--muted); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
718
|
+
.peer-menu { flex:none; padding:6px; }
|
|
719
|
+
.pair-code { font-family:var(--mono); font-size:26px; font-weight:700; letter-spacing:.12em;
|
|
720
|
+
text-align:center; padding:16px; border:1px dashed var(--line); border-radius:10px; background:var(--surface); color:var(--fg); }
|
|
721
|
+
|
|
707
722
|
/* main */
|
|
708
723
|
.main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
|
|
709
724
|
.topbar { display:flex; align-items:center; gap:6px; padding:10px 18px; border-bottom:1px solid var(--line); min-height:49px; }
|
|
@@ -1140,6 +1155,49 @@ var HTML = `<!doctype html>
|
|
|
1140
1155
|
.db { padding:16px 14px 90px; }
|
|
1141
1156
|
.peek { width:100%; }
|
|
1142
1157
|
}
|
|
1158
|
+
|
|
1159
|
+
/* Quick Notes window (desktop, loaded at #quick). A transparent body lets the
|
|
1160
|
+
macOS vibrancy blur show through; a translucent panel keeps text readable.
|
|
1161
|
+
The top bar is the window drag region; its buttons opt back out. */
|
|
1162
|
+
body.quicknote { background:transparent; }
|
|
1163
|
+
/* The main app lays #app out as a flex row (sidebar + main); the quick-note
|
|
1164
|
+
route has a single child, so override to a full-bleed block or the panel
|
|
1165
|
+
shrinks to its content width and the window's vibrancy shows through. */
|
|
1166
|
+
body.quicknote #app { display:block; width:100vw; height:100vh; }
|
|
1167
|
+
.qn { display:flex; flex-direction:column; width:100%; height:100vh; overflow:hidden;
|
|
1168
|
+
background:color-mix(in srgb, var(--bg) 20%, transparent); }
|
|
1169
|
+
.qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px 4px 76px;
|
|
1170
|
+
user-select:none; -webkit-app-region:drag;
|
|
1171
|
+
background:color-mix(in srgb, var(--surface) 30%, transparent); }
|
|
1172
|
+
.qn-brand { flex:1; min-width:0; font-size:12px; font-weight:600; color:var(--fg-soft);
|
|
1173
|
+
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
1174
|
+
.qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
|
|
1175
|
+
.qn-actions .iconbtn { width:26px; height:26px; }
|
|
1176
|
+
.qn-actions .iconbtn.active { color:var(--accent); background:var(--hover-2); }
|
|
1177
|
+
.qn-body { flex:1; overflow:auto; -webkit-app-region:no-drag; }
|
|
1178
|
+
.qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
|
|
1179
|
+
.qn-body .doc-title { font-size:24px; margin-bottom:2px; }
|
|
1180
|
+
.qn-body .doc-meta { display:none; }
|
|
1181
|
+
/* Compact menus inside the quick-note window only (dropdown + slash command
|
|
1182
|
+
share .pop/.item/.lico with the main app; scope under body.quicknote so the
|
|
1183
|
+
main app keeps its roomier density). */
|
|
1184
|
+
body.quicknote .pop { padding:5px; border-radius:9px; }
|
|
1185
|
+
body.quicknote .pop .lbl { padding:4px 8px 2px; font-size:10px; }
|
|
1186
|
+
body.quicknote .pop .item { padding:5px 9px; gap:8px; font-size:12.5px; }
|
|
1187
|
+
body.quicknote .pop .item .t { font-size:12.5px; }
|
|
1188
|
+
body.quicknote .pop .item .d { font-size:10.5px; }
|
|
1189
|
+
body.quicknote .pop .item .lico { width:22px; height:22px; }
|
|
1190
|
+
body.quicknote .pop .item .lico.plain { width:20px; height:20px; }
|
|
1191
|
+
body.quicknote .pop .sep { margin:4px 4px; }
|
|
1192
|
+
|
|
1193
|
+
/* Quick Notes settings rows (desktop only). */
|
|
1194
|
+
.qn-set-row { display:flex; align-items:center; gap:14px; padding:12px 0; border-top:1px solid var(--line); }
|
|
1195
|
+
.qn-set-row:first-of-type { border-top:0; }
|
|
1196
|
+
.qn-set-main { flex:1; min-width:0; }
|
|
1197
|
+
.qn-set-name { font-weight:600; }
|
|
1198
|
+
.qn-set-desc { color:var(--muted); font-size:13px; margin-top:2px; }
|
|
1199
|
+
.qn-shortcut { min-width:140px; justify-content:center; font-family:var(--mono); }
|
|
1200
|
+
.qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
|
|
1143
1201
|
</style>
|
|
1144
1202
|
</head>
|
|
1145
1203
|
<body>
|
|
@@ -1207,11 +1265,41 @@ CREATE TABLE IF NOT EXISTS crdt_changes (
|
|
|
1207
1265
|
CREATE INDEX IF NOT EXISTS idx_changes_hlc ON crdt_changes(hlc);
|
|
1208
1266
|
|
|
1209
1267
|
-- Per-peer replication cursors (SQLite rowids: monotonic in insertion order,
|
|
1210
|
-
-- so no change is ever skipped regardless of HLC/clock skew).
|
|
1268
|
+
-- so no change is ever skipped regardless of HLC/clock skew). Outbound side of
|
|
1269
|
+
-- a pairing: "token" is the credential the *remote* issued to us (sent as a
|
|
1270
|
+
-- Bearer header when we sync to them). See migratePeers in db.ts for the columns
|
|
1271
|
+
-- added to legacy databases.
|
|
1211
1272
|
CREATE TABLE IF NOT EXISTS peers (
|
|
1212
|
-
url
|
|
1213
|
-
pull_cursor
|
|
1214
|
-
push_cursor
|
|
1273
|
+
url TEXT PRIMARY KEY,
|
|
1274
|
+
pull_cursor INTEGER NOT NULL DEFAULT 0,
|
|
1275
|
+
push_cursor INTEGER NOT NULL DEFAULT 0,
|
|
1276
|
+
token TEXT,
|
|
1277
|
+
label TEXT,
|
|
1278
|
+
node_id TEXT,
|
|
1279
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1280
|
+
last_sync_at INTEGER,
|
|
1281
|
+
last_status TEXT,
|
|
1282
|
+
last_error TEXT
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
-- Inbound side of a pairing: credentials *we* issued to remote devices. A peer
|
|
1286
|
+
-- presents one of these on /sync; acceptsSyncToken() checks it alongside the
|
|
1287
|
+
-- managed master token. Revoke by deleting the row.
|
|
1288
|
+
CREATE TABLE IF NOT EXISTS peer_grants (
|
|
1289
|
+
token TEXT PRIMARY KEY,
|
|
1290
|
+
peer_url TEXT,
|
|
1291
|
+
node_id TEXT,
|
|
1292
|
+
created_at INTEGER
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
-- One-time pairing codes minted by "mh config peer code" / POST /api/pair/new.
|
|
1296
|
+
-- Short-lived and single-use: redeemed during the pairing handshake, then the
|
|
1297
|
+
-- two devices exchange durable peer_grants. used/exp gate redemption.
|
|
1298
|
+
CREATE TABLE IF NOT EXISTS pairing_codes (
|
|
1299
|
+
code TEXT PRIMARY KEY,
|
|
1300
|
+
exp INTEGER NOT NULL,
|
|
1301
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
1302
|
+
created_at INTEGER
|
|
1215
1303
|
);
|
|
1216
1304
|
|
|
1217
1305
|
CREATE TABLE IF NOT EXISTS databases (
|
|
@@ -1924,12 +2012,28 @@ function migrateRecords(db) {
|
|
|
1924
2012
|
tx();
|
|
1925
2013
|
backfillRecordOrderKeys(db);
|
|
1926
2014
|
}
|
|
2015
|
+
function migratePeers(db) {
|
|
2016
|
+
const add = [
|
|
2017
|
+
["token", "TEXT"],
|
|
2018
|
+
["label", "TEXT"],
|
|
2019
|
+
["node_id", "TEXT"],
|
|
2020
|
+
["enabled", "INTEGER NOT NULL DEFAULT 1"],
|
|
2021
|
+
["last_sync_at", "INTEGER"],
|
|
2022
|
+
["last_status", "TEXT"],
|
|
2023
|
+
["last_error", "TEXT"]
|
|
2024
|
+
];
|
|
2025
|
+
for (const [col, decl] of add) {
|
|
2026
|
+
if (!hasColumn(db, "peers", col))
|
|
2027
|
+
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
1927
2030
|
function openMetahub() {
|
|
1928
2031
|
ensureDirs();
|
|
1929
2032
|
const db = new Database(dbPath(), { create: true });
|
|
1930
2033
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
1931
2034
|
runSchema(db);
|
|
1932
2035
|
migrateRecords(db);
|
|
2036
|
+
migratePeers(db);
|
|
1933
2037
|
return db;
|
|
1934
2038
|
}
|
|
1935
2039
|
|
|
@@ -2032,9 +2136,12 @@ function getDocument(db, id) {
|
|
|
2032
2136
|
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
2033
2137
|
}
|
|
2034
2138
|
function listDocuments(db, opts = {}) {
|
|
2139
|
+
const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
|
|
2140
|
+
if (opts.parent_id !== undefined)
|
|
2141
|
+
return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
|
|
2035
2142
|
if (opts.database_id)
|
|
2036
|
-
return db.query(
|
|
2037
|
-
return db.query(
|
|
2143
|
+
return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
|
|
2144
|
+
return db.query(`${cols} ORDER BY created_hlc`).all();
|
|
2038
2145
|
}
|
|
2039
2146
|
function updateDocument(db, id, fields) {
|
|
2040
2147
|
if (!getDocument(db, id))
|
|
@@ -16517,26 +16624,41 @@ var HealthResponseSchema = exports_external.object({
|
|
|
16517
16624
|
ok: exports_external.boolean(),
|
|
16518
16625
|
node: exports_external.string().describe("Server's node id")
|
|
16519
16626
|
});
|
|
16627
|
+
var PairRequestSchema = exports_external.object({
|
|
16628
|
+
code: exports_external.string().describe("One-time pairing code minted by this server"),
|
|
16629
|
+
node_id: exports_external.string().describe("Calling device's node id"),
|
|
16630
|
+
grant: exports_external.string().describe("Durable credential the caller issues to us"),
|
|
16631
|
+
self_url: exports_external.string().optional().describe("Caller's reachable server URL (for mutual sync)")
|
|
16632
|
+
});
|
|
16633
|
+
var PairResponseSchema = exports_external.object({
|
|
16634
|
+
node_id: exports_external.string().describe("This server's node id"),
|
|
16635
|
+
grant: exports_external.string().describe("Durable credential we issue to the caller")
|
|
16636
|
+
});
|
|
16520
16637
|
var SYNC_PATH = "/sync";
|
|
16521
16638
|
var HEALTH_PATH = "/health";
|
|
16522
16639
|
var RENEW_PATH = "/auth/token";
|
|
16640
|
+
var PAIR_PATH = "/api/pair";
|
|
16523
16641
|
// src/core/sync/client.ts
|
|
16524
16642
|
init_node();
|
|
16525
16643
|
init_crdt();
|
|
16526
16644
|
function getPeer(db, url2) {
|
|
16527
|
-
const row = db.query("SELECT pull_cursor, push_cursor FROM peers WHERE url = ?").get(url2);
|
|
16528
|
-
return row ?? { pull_cursor: 0, push_cursor: 0 };
|
|
16645
|
+
const row = db.query("SELECT pull_cursor, push_cursor, token FROM peers WHERE url = ?").get(url2);
|
|
16646
|
+
return row ?? { pull_cursor: 0, push_cursor: 0, token: null };
|
|
16529
16647
|
}
|
|
16530
16648
|
function setPeer(db, url2, c) {
|
|
16531
16649
|
db.query("INSERT INTO peers (url, pull_cursor, push_cursor) VALUES (?, ?, ?) ON CONFLICT(url) DO UPDATE SET pull_cursor = excluded.pull_cursor, push_cursor = excluded.push_cursor").run(url2, c.pull_cursor, c.push_cursor);
|
|
16532
16650
|
}
|
|
16533
|
-
async function syncWithPeer(db, url2) {
|
|
16651
|
+
async function syncWithPeer(db, url2, token) {
|
|
16534
16652
|
const node = getNodeId(db);
|
|
16535
16653
|
const peer = getPeer(db, url2);
|
|
16654
|
+
const bearer = token ?? peer.token ?? undefined;
|
|
16536
16655
|
const toPush = changesAfterSeq(db, peer.push_cursor);
|
|
16656
|
+
const headers = { "content-type": "application/json" };
|
|
16657
|
+
if (bearer)
|
|
16658
|
+
headers.authorization = `Bearer ${bearer}`;
|
|
16537
16659
|
const res = await fetch(new URL(SYNC_PATH, url2), {
|
|
16538
16660
|
method: "POST",
|
|
16539
|
-
headers
|
|
16661
|
+
headers,
|
|
16540
16662
|
body: JSON.stringify({ node_id: node, since: peer.pull_cursor, changes: toPush.changes })
|
|
16541
16663
|
});
|
|
16542
16664
|
if (!res.ok)
|
|
@@ -16549,10 +16671,108 @@ async function syncWithPeer(db, url2) {
|
|
|
16549
16671
|
}
|
|
16550
16672
|
// src/core/sync/server.ts
|
|
16551
16673
|
init_node();
|
|
16674
|
+
|
|
16675
|
+
// src/core/sync/token.ts
|
|
16676
|
+
init_ids();
|
|
16677
|
+
var K_TOKEN = "auth_token";
|
|
16678
|
+
var K_EXP = "auth_token_exp";
|
|
16679
|
+
var K_PREV = "auth_token_prev";
|
|
16680
|
+
var K_PREV_EXP = "auth_token_prev_exp";
|
|
16681
|
+
function parseDuration(s, fallbackMs) {
|
|
16682
|
+
if (!s)
|
|
16683
|
+
return fallbackMs;
|
|
16684
|
+
const m = /^(\d+)\s*(ms|s|m|h|d)?$/.exec(s.trim());
|
|
16685
|
+
if (!m)
|
|
16686
|
+
return fallbackMs;
|
|
16687
|
+
const n = Number(m[1]);
|
|
16688
|
+
switch (m[2]) {
|
|
16689
|
+
case "ms":
|
|
16690
|
+
return n;
|
|
16691
|
+
case "m":
|
|
16692
|
+
return n * 60000;
|
|
16693
|
+
case "h":
|
|
16694
|
+
return n * 3600000;
|
|
16695
|
+
case "d":
|
|
16696
|
+
return n * 86400000;
|
|
16697
|
+
default:
|
|
16698
|
+
return n * 1000;
|
|
16699
|
+
}
|
|
16700
|
+
}
|
|
16701
|
+
var DAY = 86400000;
|
|
16702
|
+
var DEFAULT_TTL_MS = parseDuration(process.env.METAHUB_TOKEN_TTL, 30 * DAY);
|
|
16703
|
+
var DEFAULT_GRACE_MS = parseDuration(process.env.METAHUB_TOKEN_GRACE, 7 * DAY);
|
|
16704
|
+
function getMeta(db, key) {
|
|
16705
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
16706
|
+
return row ? row.value : null;
|
|
16707
|
+
}
|
|
16708
|
+
function setMeta(db, key, value) {
|
|
16709
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
16710
|
+
}
|
|
16711
|
+
function readState(db) {
|
|
16712
|
+
const token = getMeta(db, K_TOKEN);
|
|
16713
|
+
if (!token)
|
|
16714
|
+
return null;
|
|
16715
|
+
const prev = getMeta(db, K_PREV);
|
|
16716
|
+
return {
|
|
16717
|
+
token,
|
|
16718
|
+
exp: Number(getMeta(db, K_EXP) ?? 0),
|
|
16719
|
+
prev: prev && prev.length > 0 ? prev : null,
|
|
16720
|
+
prevExp: Number(getMeta(db, K_PREV_EXP) ?? 0)
|
|
16721
|
+
};
|
|
16722
|
+
}
|
|
16723
|
+
function rotate(db, ttlMs, graceMs) {
|
|
16724
|
+
const now = Date.now();
|
|
16725
|
+
const old = getMeta(db, K_TOKEN);
|
|
16726
|
+
const next = {
|
|
16727
|
+
token: randomSuffix(24),
|
|
16728
|
+
exp: now + ttlMs,
|
|
16729
|
+
prev: old,
|
|
16730
|
+
prevExp: old ? now + graceMs : 0
|
|
16731
|
+
};
|
|
16732
|
+
setMeta(db, K_TOKEN, next.token);
|
|
16733
|
+
setMeta(db, K_EXP, String(next.exp));
|
|
16734
|
+
setMeta(db, K_PREV, next.prev ?? "");
|
|
16735
|
+
setMeta(db, K_PREV_EXP, String(next.prevExp));
|
|
16736
|
+
return next;
|
|
16737
|
+
}
|
|
16738
|
+
function loadOrRotate(db, ttlMs, graceMs) {
|
|
16739
|
+
const s = readState(db);
|
|
16740
|
+
if (!s || Date.now() > s.exp)
|
|
16741
|
+
return rotate(db, ttlMs, graceMs);
|
|
16742
|
+
return s;
|
|
16743
|
+
}
|
|
16744
|
+
|
|
16745
|
+
// src/core/config.ts
|
|
16746
|
+
var K_HOST = "cfg_host";
|
|
16747
|
+
var K_PORT = "cfg_port";
|
|
16748
|
+
var K_SYNC_INTERVAL = "cfg_sync_interval";
|
|
16749
|
+
var K_AUTO_SYNC = "cfg_auto_sync";
|
|
16750
|
+
var DEFAULT_CONFIG = {
|
|
16751
|
+
host: "127.0.0.1",
|
|
16752
|
+
port: 7777,
|
|
16753
|
+
syncIntervalMs: parseDuration(process.env.METAHUB_SYNC_INTERVAL, 30000),
|
|
16754
|
+
autoSync: true
|
|
16755
|
+
};
|
|
16756
|
+
function getMeta2(db, key) {
|
|
16757
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
16758
|
+
return row ? row.value : null;
|
|
16759
|
+
}
|
|
16760
|
+
function getServerConfig(db) {
|
|
16761
|
+
const host = getMeta2(db, K_HOST);
|
|
16762
|
+
const port = getMeta2(db, K_PORT);
|
|
16763
|
+
const interval = getMeta2(db, K_SYNC_INTERVAL);
|
|
16764
|
+
const auto = getMeta2(db, K_AUTO_SYNC);
|
|
16765
|
+
return {
|
|
16766
|
+
host: host ?? DEFAULT_CONFIG.host,
|
|
16767
|
+
port: port != null ? Number(port) : DEFAULT_CONFIG.port,
|
|
16768
|
+
syncIntervalMs: interval != null ? Number(interval) : DEFAULT_CONFIG.syncIntervalMs,
|
|
16769
|
+
autoSync: auto != null ? auto === "1" : DEFAULT_CONFIG.autoSync
|
|
16770
|
+
};
|
|
16771
|
+
}
|
|
16552
16772
|
// package.json
|
|
16553
16773
|
var package_default = {
|
|
16554
16774
|
name: "@tensorix/metahub",
|
|
16555
|
-
version: "0.1.
|
|
16775
|
+
version: "0.1.3",
|
|
16556
16776
|
description: "Metahub core library and CLI",
|
|
16557
16777
|
type: "module",
|
|
16558
16778
|
main: "./dist/index.js",
|
|
@@ -16581,6 +16801,7 @@ var package_default = {
|
|
|
16581
16801
|
test: "bun test"
|
|
16582
16802
|
},
|
|
16583
16803
|
dependencies: {
|
|
16804
|
+
"@clack/prompts": "^1.5.0",
|
|
16584
16805
|
citty: "^0.1.6",
|
|
16585
16806
|
"highlight.js": "^11.11.1",
|
|
16586
16807
|
preact: "^10.29.2",
|
|
@@ -16657,7 +16878,8 @@ var RecordValuesReq = exports_external.record(exports_external.string(), exports
|
|
|
16657
16878
|
var CreateDocumentReq = exports_external.object({
|
|
16658
16879
|
title: exports_external.string(),
|
|
16659
16880
|
body: exports_external.string().optional(),
|
|
16660
|
-
database_id: exports_external.string().optional()
|
|
16881
|
+
database_id: exports_external.string().optional(),
|
|
16882
|
+
parent_id: exports_external.string().optional()
|
|
16661
16883
|
});
|
|
16662
16884
|
var UpdateDocumentReq = exports_external.object({
|
|
16663
16885
|
title: exports_external.string().optional(),
|
|
@@ -16835,9 +17057,9 @@ var webuiRoutes = [
|
|
|
16835
17057
|
{
|
|
16836
17058
|
method: "GET",
|
|
16837
17059
|
path: "/api/documents",
|
|
16838
|
-
summary: "List documents. Optional query: ?db=<id>",
|
|
17060
|
+
summary: "List documents. Optional query: ?db=<id> or ?parent=<id>",
|
|
16839
17061
|
response: exports_external.array(DocumentSummarySchema),
|
|
16840
|
-
handler: handle((req, { db }) => listDocuments(db, { database_id: opt(req, "db") }))
|
|
17062
|
+
handler: handle((req, { db }) => listDocuments(db, { database_id: opt(req, "db"), parent_id: opt(req, "parent") }))
|
|
16841
17063
|
},
|
|
16842
17064
|
{
|
|
16843
17065
|
method: "POST",
|
|
@@ -16937,6 +17159,250 @@ var sitesRoutes = [
|
|
|
16937
17159
|
}
|
|
16938
17160
|
];
|
|
16939
17161
|
|
|
17162
|
+
// src/core/sync/pairing.ts
|
|
17163
|
+
init_ids();
|
|
17164
|
+
|
|
17165
|
+
// src/core/sync/peers.ts
|
|
17166
|
+
function listPeers(db) {
|
|
17167
|
+
return db.query("SELECT url, pull_cursor, push_cursor, token, label, node_id, enabled, last_sync_at, last_status, last_error FROM peers ORDER BY url").all();
|
|
17168
|
+
}
|
|
17169
|
+
function addPeer(db, input) {
|
|
17170
|
+
db.query(`INSERT INTO peers (url, token, label, node_id, enabled, pull_cursor, push_cursor)
|
|
17171
|
+
VALUES (?, ?, ?, ?, 1, 0, 0)
|
|
17172
|
+
ON CONFLICT(url) DO UPDATE SET
|
|
17173
|
+
token = coalesce(excluded.token, peers.token),
|
|
17174
|
+
label = coalesce(excluded.label, peers.label),
|
|
17175
|
+
node_id = coalesce(excluded.node_id, peers.node_id),
|
|
17176
|
+
enabled = 1`).run(input.url, input.token ?? null, input.label ?? null, input.node_id ?? null);
|
|
17177
|
+
}
|
|
17178
|
+
function removePeer(db, url2) {
|
|
17179
|
+
const tx = db.transaction(() => {
|
|
17180
|
+
const changed = db.query("DELETE FROM peers WHERE url = ?").run(url2).changes > 0;
|
|
17181
|
+
db.query("DELETE FROM peer_grants WHERE peer_url = ?").run(url2);
|
|
17182
|
+
return changed;
|
|
17183
|
+
});
|
|
17184
|
+
return tx();
|
|
17185
|
+
}
|
|
17186
|
+
function setPeerEnabled(db, url2, enabled) {
|
|
17187
|
+
return db.query("UPDATE peers SET enabled = ? WHERE url = ?").run(enabled ? 1 : 0, url2).changes > 0;
|
|
17188
|
+
}
|
|
17189
|
+
function setPeerLabel(db, url2, label) {
|
|
17190
|
+
return db.query("UPDATE peers SET label = ? WHERE url = ?").run(label, url2).changes > 0;
|
|
17191
|
+
}
|
|
17192
|
+
function updatePeerStatus(db, url2, status, error51) {
|
|
17193
|
+
db.query("UPDATE peers SET last_sync_at = ?, last_status = ?, last_error = ? WHERE url = ?").run(Date.now(), status, error51 ?? null, url2);
|
|
17194
|
+
}
|
|
17195
|
+
async function syncPeer(db, url2) {
|
|
17196
|
+
try {
|
|
17197
|
+
const result = await syncWithPeer(db, url2);
|
|
17198
|
+
updatePeerStatus(db, url2, "ok", null);
|
|
17199
|
+
return { url: url2, ok: true, pushed: result.pushed, pulled: result.pulled };
|
|
17200
|
+
} catch (e) {
|
|
17201
|
+
const error51 = e.message;
|
|
17202
|
+
updatePeerStatus(db, url2, "error", error51);
|
|
17203
|
+
return { url: url2, ok: false, error: error51 };
|
|
17204
|
+
}
|
|
17205
|
+
}
|
|
17206
|
+
async function syncAllPeers(db) {
|
|
17207
|
+
const peers = listPeers(db).filter((p) => p.enabled);
|
|
17208
|
+
const out = [];
|
|
17209
|
+
for (const p of peers)
|
|
17210
|
+
out.push(await syncPeer(db, p.url));
|
|
17211
|
+
return out;
|
|
17212
|
+
}
|
|
17213
|
+
|
|
17214
|
+
// src/core/sync/pairing.ts
|
|
17215
|
+
var DEFAULT_PAIR_TTL_MS = parseDuration(process.env.METAHUB_PAIR_TTL, 10 * 60000);
|
|
17216
|
+
function generatePairingCode(db, ttlMs = DEFAULT_PAIR_TTL_MS) {
|
|
17217
|
+
const now = Date.now();
|
|
17218
|
+
db.query("DELETE FROM pairing_codes WHERE used = 1 OR exp < ?").run(now);
|
|
17219
|
+
const code = randomSuffix(12);
|
|
17220
|
+
const exp = now + ttlMs;
|
|
17221
|
+
db.query("INSERT INTO pairing_codes (code, exp, used, created_at) VALUES (?, ?, 0, ?)").run(code, exp, now);
|
|
17222
|
+
return { code, exp };
|
|
17223
|
+
}
|
|
17224
|
+
function redeemPairingCode(db, code) {
|
|
17225
|
+
return db.query("UPDATE pairing_codes SET used = 1 WHERE code = ? AND used = 0 AND exp >= ?").run(code, Date.now()).changes > 0;
|
|
17226
|
+
}
|
|
17227
|
+
function mintGrant(db, peerUrl, nodeId) {
|
|
17228
|
+
const token = randomSuffix(32);
|
|
17229
|
+
db.query("INSERT INTO peer_grants (token, peer_url, node_id, created_at) VALUES (?, ?, ?, ?)").run(token, peerUrl, nodeId, Date.now());
|
|
17230
|
+
return token;
|
|
17231
|
+
}
|
|
17232
|
+
function isAcceptedGrant(db, token) {
|
|
17233
|
+
return db.query("SELECT 1 FROM peer_grants WHERE token = ?").get(token) != null;
|
|
17234
|
+
}
|
|
17235
|
+
function listGrants(db) {
|
|
17236
|
+
return db.query("SELECT token, peer_url, node_id, created_at FROM peer_grants ORDER BY created_at DESC").all();
|
|
17237
|
+
}
|
|
17238
|
+
function revokeGrant(db, tokenOrPrefix) {
|
|
17239
|
+
return db.query("DELETE FROM peer_grants WHERE token = ? OR token LIKE ? || '%'").run(tokenOrPrefix, tokenOrPrefix).changes;
|
|
17240
|
+
}
|
|
17241
|
+
function handlePairRequest(db, node, body) {
|
|
17242
|
+
if (!redeemPairingCode(db, body.code)) {
|
|
17243
|
+
throw new Error("invalid or expired pairing code");
|
|
17244
|
+
}
|
|
17245
|
+
if (body.self_url) {
|
|
17246
|
+
addPeer(db, { url: body.self_url, token: body.grant, node_id: body.node_id });
|
|
17247
|
+
}
|
|
17248
|
+
const grant = mintGrant(db, body.self_url ?? null, body.node_id);
|
|
17249
|
+
return { node_id: node, grant };
|
|
17250
|
+
}
|
|
17251
|
+
async function performPairing(db, node, remoteUrl, code, selfUrl) {
|
|
17252
|
+
const ourGrant = mintGrant(db, remoteUrl, null);
|
|
17253
|
+
const reqBody = { code, node_id: node, grant: ourGrant, self_url: selfUrl };
|
|
17254
|
+
const res = await fetch(new URL(PAIR_PATH, remoteUrl), {
|
|
17255
|
+
method: "POST",
|
|
17256
|
+
headers: { "content-type": "application/json" },
|
|
17257
|
+
body: JSON.stringify(reqBody)
|
|
17258
|
+
});
|
|
17259
|
+
if (!res.ok)
|
|
17260
|
+
throw new Error(`pairing failed: ${res.status} ${await res.text()}`);
|
|
17261
|
+
const data = await res.json();
|
|
17262
|
+
addPeer(db, { url: remoteUrl, token: data.grant, node_id: data.node_id });
|
|
17263
|
+
return { node_id: data.node_id, url: remoteUrl };
|
|
17264
|
+
}
|
|
17265
|
+
|
|
17266
|
+
// src/core/sync/peers-routes.ts
|
|
17267
|
+
var PeerSchema = exports_external.object({
|
|
17268
|
+
url: exports_external.string(),
|
|
17269
|
+
pull_cursor: exports_external.number(),
|
|
17270
|
+
push_cursor: exports_external.number(),
|
|
17271
|
+
token: exports_external.string().nullable(),
|
|
17272
|
+
label: exports_external.string().nullable(),
|
|
17273
|
+
node_id: exports_external.string().nullable(),
|
|
17274
|
+
enabled: exports_external.number(),
|
|
17275
|
+
last_sync_at: exports_external.number().nullable(),
|
|
17276
|
+
last_status: exports_external.string().nullable(),
|
|
17277
|
+
last_error: exports_external.string().nullable()
|
|
17278
|
+
});
|
|
17279
|
+
var PairingCodeSchema = exports_external.object({ code: exports_external.string(), exp: exports_external.number() });
|
|
17280
|
+
var AddPeerReq = exports_external.object({
|
|
17281
|
+
url: exports_external.string(),
|
|
17282
|
+
code: exports_external.string(),
|
|
17283
|
+
self_url: exports_external.string().optional()
|
|
17284
|
+
});
|
|
17285
|
+
var AddPeerRes = exports_external.object({ node_id: exports_external.string(), url: exports_external.string() });
|
|
17286
|
+
var UpdatePeerReq = exports_external.object({
|
|
17287
|
+
enabled: exports_external.boolean().optional(),
|
|
17288
|
+
label: exports_external.string().optional()
|
|
17289
|
+
});
|
|
17290
|
+
var PeerSyncRes = exports_external.object({
|
|
17291
|
+
url: exports_external.string(),
|
|
17292
|
+
ok: exports_external.boolean(),
|
|
17293
|
+
pushed: exports_external.number().optional(),
|
|
17294
|
+
pulled: exports_external.number().optional(),
|
|
17295
|
+
error: exports_external.string().optional()
|
|
17296
|
+
});
|
|
17297
|
+
var GrantSchema = exports_external.object({
|
|
17298
|
+
token: exports_external.string(),
|
|
17299
|
+
peer_url: exports_external.string().nullable(),
|
|
17300
|
+
node_id: exports_external.string().nullable(),
|
|
17301
|
+
created_at: exports_external.number().nullable()
|
|
17302
|
+
});
|
|
17303
|
+
var RevokeRes = exports_external.object({ revoked: exports_external.number() });
|
|
17304
|
+
var OkSchema2 = exports_external.object({ ok: exports_external.boolean() });
|
|
17305
|
+
function need3(req, key) {
|
|
17306
|
+
const v = new URL(req.url).searchParams.get(key);
|
|
17307
|
+
if (!v)
|
|
17308
|
+
throw new Error(`missing query param: ${key}`);
|
|
17309
|
+
return v;
|
|
17310
|
+
}
|
|
17311
|
+
function handle3(fn) {
|
|
17312
|
+
return async (req, ctx) => {
|
|
17313
|
+
try {
|
|
17314
|
+
const out = await fn(req, ctx);
|
|
17315
|
+
if (out instanceof Response)
|
|
17316
|
+
return out;
|
|
17317
|
+
return Response.json(out ?? null);
|
|
17318
|
+
} catch (e) {
|
|
17319
|
+
return Response.json({ error: e.message }, { status: 400 });
|
|
17320
|
+
}
|
|
17321
|
+
};
|
|
17322
|
+
}
|
|
17323
|
+
var peersRoutes = [
|
|
17324
|
+
{
|
|
17325
|
+
method: "POST",
|
|
17326
|
+
path: PAIR_PATH,
|
|
17327
|
+
summary: "Pairing handshake: redeem a one-time code and exchange credentials",
|
|
17328
|
+
request: PairRequestSchema,
|
|
17329
|
+
response: PairResponseSchema,
|
|
17330
|
+
handler: handle3(async (req, { db, node }) => {
|
|
17331
|
+
const body = await req.json();
|
|
17332
|
+
return handlePairRequest(db, node, body);
|
|
17333
|
+
})
|
|
17334
|
+
},
|
|
17335
|
+
{
|
|
17336
|
+
method: "POST",
|
|
17337
|
+
path: "/api/pair/new",
|
|
17338
|
+
summary: "Mint a one-time pairing code for another device to redeem",
|
|
17339
|
+
response: PairingCodeSchema,
|
|
17340
|
+
handler: handle3((_req, { db }) => generatePairingCode(db))
|
|
17341
|
+
},
|
|
17342
|
+
{
|
|
17343
|
+
method: "POST",
|
|
17344
|
+
path: "/api/peers/pair",
|
|
17345
|
+
summary: "Pair this server with a remote one using its one-time code",
|
|
17346
|
+
request: AddPeerReq,
|
|
17347
|
+
response: AddPeerRes,
|
|
17348
|
+
handler: handle3(async (req, { db, node }) => {
|
|
17349
|
+
const body = await req.json();
|
|
17350
|
+
return performPairing(db, node, body.url, body.code, body.self_url);
|
|
17351
|
+
})
|
|
17352
|
+
},
|
|
17353
|
+
{
|
|
17354
|
+
method: "GET",
|
|
17355
|
+
path: "/api/peers",
|
|
17356
|
+
summary: "List configured sync peers with status",
|
|
17357
|
+
response: exports_external.array(PeerSchema),
|
|
17358
|
+
handler: handle3((_req, { db }) => listPeers(db))
|
|
17359
|
+
},
|
|
17360
|
+
{
|
|
17361
|
+
method: "PATCH",
|
|
17362
|
+
path: "/api/peer",
|
|
17363
|
+
summary: "Update a peer (enable/disable or label). Query: ?url=<url>",
|
|
17364
|
+
request: UpdatePeerReq,
|
|
17365
|
+
response: OkSchema2,
|
|
17366
|
+
handler: handle3(async (req, { db }) => {
|
|
17367
|
+
const url2 = need3(req, "url");
|
|
17368
|
+
const body = await req.json();
|
|
17369
|
+
if (body.enabled != null)
|
|
17370
|
+
setPeerEnabled(db, url2, body.enabled);
|
|
17371
|
+
if (body.label != null)
|
|
17372
|
+
setPeerLabel(db, url2, body.label);
|
|
17373
|
+
return { ok: true };
|
|
17374
|
+
})
|
|
17375
|
+
},
|
|
17376
|
+
{
|
|
17377
|
+
method: "DELETE",
|
|
17378
|
+
path: "/api/peer",
|
|
17379
|
+
summary: "Remove a sync peer. Query: ?url=<url>",
|
|
17380
|
+
response: OkSchema2,
|
|
17381
|
+
handler: handle3((req, { db }) => ({ ok: removePeer(db, need3(req, "url")) }))
|
|
17382
|
+
},
|
|
17383
|
+
{
|
|
17384
|
+
method: "POST",
|
|
17385
|
+
path: "/api/peer/sync",
|
|
17386
|
+
summary: "Manually sync once with a peer. Query: ?url=<url>",
|
|
17387
|
+
response: PeerSyncRes,
|
|
17388
|
+
handler: handle3((req, { db }) => syncPeer(db, need3(req, "url")))
|
|
17389
|
+
},
|
|
17390
|
+
{
|
|
17391
|
+
method: "GET",
|
|
17392
|
+
path: "/api/grants",
|
|
17393
|
+
summary: "List credentials this server issued and accepts on /sync (inbound)",
|
|
17394
|
+
response: exports_external.array(GrantSchema),
|
|
17395
|
+
handler: handle3((_req, { db }) => listGrants(db))
|
|
17396
|
+
},
|
|
17397
|
+
{
|
|
17398
|
+
method: "DELETE",
|
|
17399
|
+
path: "/api/grant",
|
|
17400
|
+
summary: "Revoke an issued credential by token or prefix. Query: ?token=<token>",
|
|
17401
|
+
response: RevokeRes,
|
|
17402
|
+
handler: handle3((req, { db }) => ({ revoked: revokeGrant(db, need3(req, "token")) }))
|
|
17403
|
+
}
|
|
17404
|
+
];
|
|
17405
|
+
|
|
16940
17406
|
// src/core/sync/routes.ts
|
|
16941
17407
|
var syncRoutes = [
|
|
16942
17408
|
{
|
|
@@ -16962,77 +17428,7 @@ var syncRoutes = [
|
|
|
16962
17428
|
}
|
|
16963
17429
|
}
|
|
16964
17430
|
];
|
|
16965
|
-
var routes = [...syncRoutes, ...webuiRoutes, ...sitesRoutes];
|
|
16966
|
-
|
|
16967
|
-
// src/core/sync/token.ts
|
|
16968
|
-
init_ids();
|
|
16969
|
-
var K_TOKEN = "auth_token";
|
|
16970
|
-
var K_EXP = "auth_token_exp";
|
|
16971
|
-
var K_PREV = "auth_token_prev";
|
|
16972
|
-
var K_PREV_EXP = "auth_token_prev_exp";
|
|
16973
|
-
function parseDuration(s, fallbackMs) {
|
|
16974
|
-
if (!s)
|
|
16975
|
-
return fallbackMs;
|
|
16976
|
-
const m = /^(\d+)\s*(ms|s|m|h|d)?$/.exec(s.trim());
|
|
16977
|
-
if (!m)
|
|
16978
|
-
return fallbackMs;
|
|
16979
|
-
const n = Number(m[1]);
|
|
16980
|
-
switch (m[2]) {
|
|
16981
|
-
case "ms":
|
|
16982
|
-
return n;
|
|
16983
|
-
case "m":
|
|
16984
|
-
return n * 60000;
|
|
16985
|
-
case "h":
|
|
16986
|
-
return n * 3600000;
|
|
16987
|
-
case "d":
|
|
16988
|
-
return n * 86400000;
|
|
16989
|
-
default:
|
|
16990
|
-
return n * 1000;
|
|
16991
|
-
}
|
|
16992
|
-
}
|
|
16993
|
-
var DAY = 86400000;
|
|
16994
|
-
var DEFAULT_TTL_MS = parseDuration(process.env.METAHUB_TOKEN_TTL, 30 * DAY);
|
|
16995
|
-
var DEFAULT_GRACE_MS = parseDuration(process.env.METAHUB_TOKEN_GRACE, 7 * DAY);
|
|
16996
|
-
function getMeta(db, key) {
|
|
16997
|
-
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
16998
|
-
return row ? row.value : null;
|
|
16999
|
-
}
|
|
17000
|
-
function setMeta(db, key, value) {
|
|
17001
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
17002
|
-
}
|
|
17003
|
-
function readState(db) {
|
|
17004
|
-
const token = getMeta(db, K_TOKEN);
|
|
17005
|
-
if (!token)
|
|
17006
|
-
return null;
|
|
17007
|
-
const prev = getMeta(db, K_PREV);
|
|
17008
|
-
return {
|
|
17009
|
-
token,
|
|
17010
|
-
exp: Number(getMeta(db, K_EXP) ?? 0),
|
|
17011
|
-
prev: prev && prev.length > 0 ? prev : null,
|
|
17012
|
-
prevExp: Number(getMeta(db, K_PREV_EXP) ?? 0)
|
|
17013
|
-
};
|
|
17014
|
-
}
|
|
17015
|
-
function rotate(db, ttlMs, graceMs) {
|
|
17016
|
-
const now = Date.now();
|
|
17017
|
-
const old = getMeta(db, K_TOKEN);
|
|
17018
|
-
const next = {
|
|
17019
|
-
token: randomSuffix(24),
|
|
17020
|
-
exp: now + ttlMs,
|
|
17021
|
-
prev: old,
|
|
17022
|
-
prevExp: old ? now + graceMs : 0
|
|
17023
|
-
};
|
|
17024
|
-
setMeta(db, K_TOKEN, next.token);
|
|
17025
|
-
setMeta(db, K_EXP, String(next.exp));
|
|
17026
|
-
setMeta(db, K_PREV, next.prev ?? "");
|
|
17027
|
-
setMeta(db, K_PREV_EXP, String(next.prevExp));
|
|
17028
|
-
return next;
|
|
17029
|
-
}
|
|
17030
|
-
function loadOrRotate(db, ttlMs, graceMs) {
|
|
17031
|
-
const s = readState(db);
|
|
17032
|
-
if (!s || Date.now() > s.exp)
|
|
17033
|
-
return rotate(db, ttlMs, graceMs);
|
|
17034
|
-
return s;
|
|
17035
|
-
}
|
|
17431
|
+
var routes = [...syncRoutes, ...webuiRoutes, ...sitesRoutes, ...peersRoutes];
|
|
17036
17432
|
|
|
17037
17433
|
// src/core/sync/openapi.ts
|
|
17038
17434
|
function buildOpenApi(version2) {
|
|
@@ -17101,6 +17497,17 @@ function hasValidToken(req, url2, cfg) {
|
|
|
17101
17497
|
return true;
|
|
17102
17498
|
return extractToken(req, url2) === cur.token;
|
|
17103
17499
|
}
|
|
17500
|
+
function acceptsSyncToken(req, url2, cfg, db) {
|
|
17501
|
+
if (!authActive(cfg))
|
|
17502
|
+
return true;
|
|
17503
|
+
const presented = extractToken(req, url2);
|
|
17504
|
+
if (!presented)
|
|
17505
|
+
return false;
|
|
17506
|
+
const cur = activeToken(cfg);
|
|
17507
|
+
if (cur && presented === cur.token)
|
|
17508
|
+
return true;
|
|
17509
|
+
return isAcceptedGrant(db, presented);
|
|
17510
|
+
}
|
|
17104
17511
|
function renewToken(req, url2, cfg) {
|
|
17105
17512
|
if (cfg.debug || cfg.staticToken != null || !cfg.db)
|
|
17106
17513
|
return null;
|
|
@@ -17240,6 +17647,11 @@ function startServer(opts = {}) {
|
|
|
17240
17647
|
const db = openMetahub();
|
|
17241
17648
|
const node = getNodeId(db);
|
|
17242
17649
|
const ctx = { db, node };
|
|
17650
|
+
const cfg = getServerConfig(db);
|
|
17651
|
+
const port = opts.port ?? cfg.port;
|
|
17652
|
+
const host = opts.host ?? cfg.host;
|
|
17653
|
+
const syncIntervalMs = opts.syncIntervalMs ?? cfg.syncIntervalMs;
|
|
17654
|
+
const autoSync = opts.autoSync ?? cfg.autoSync;
|
|
17243
17655
|
const auth = opts.debug ? { debug: true, staticToken: null, db: null, ttlMs: 0, graceMs: 0 } : opts.token ? { debug: false, staticToken: opts.token, db: null, ttlMs: 0, graceMs: 0 } : {
|
|
17244
17656
|
debug: false,
|
|
17245
17657
|
staticToken: null,
|
|
@@ -17248,13 +17660,18 @@ function startServer(opts = {}) {
|
|
|
17248
17660
|
graceMs: DEFAULT_GRACE_MS
|
|
17249
17661
|
};
|
|
17250
17662
|
const server = Bun.serve({
|
|
17251
|
-
port
|
|
17252
|
-
hostname:
|
|
17663
|
+
port,
|
|
17664
|
+
hostname: host,
|
|
17253
17665
|
async fetch(req) {
|
|
17254
17666
|
const url2 = new URL(req.url);
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17667
|
+
if (url2.pathname === SYNC_PATH) {
|
|
17668
|
+
if (!acceptsSyncToken(req, url2, auth, db))
|
|
17669
|
+
return unauthorized();
|
|
17670
|
+
} else {
|
|
17671
|
+
const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
|
|
17672
|
+
if (!exempt && !hasValidToken(req, url2, auth)) {
|
|
17673
|
+
return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
|
|
17674
|
+
}
|
|
17258
17675
|
}
|
|
17259
17676
|
if (url2.pathname === RENEW_PATH) {
|
|
17260
17677
|
const r = renewToken(req, url2, auth);
|
|
@@ -17284,13 +17701,25 @@ function startServer(opts = {}) {
|
|
|
17284
17701
|
return new Response("not found", { status: 404 });
|
|
17285
17702
|
}
|
|
17286
17703
|
});
|
|
17704
|
+
let timer = null;
|
|
17705
|
+
if (autoSync && syncIntervalMs > 0) {
|
|
17706
|
+
timer = setInterval(() => {
|
|
17707
|
+
syncAllPeers(db).catch(() => {});
|
|
17708
|
+
}, syncIntervalMs);
|
|
17709
|
+
timer.unref?.();
|
|
17710
|
+
}
|
|
17287
17711
|
const active = activeToken(auth);
|
|
17288
17712
|
return {
|
|
17289
17713
|
server,
|
|
17290
17714
|
node,
|
|
17291
|
-
port: server.port ??
|
|
17715
|
+
port: server.port ?? port,
|
|
17292
17716
|
token: active?.token ?? null,
|
|
17293
|
-
exp: active?.exp ?? null
|
|
17717
|
+
exp: active?.exp ?? null,
|
|
17718
|
+
stop() {
|
|
17719
|
+
if (timer)
|
|
17720
|
+
clearInterval(timer);
|
|
17721
|
+
server.stop();
|
|
17722
|
+
}
|
|
17294
17723
|
};
|
|
17295
17724
|
}
|
|
17296
17725
|
export {
|
|
@@ -17324,6 +17753,7 @@ export {
|
|
|
17324
17753
|
newId,
|
|
17325
17754
|
moveRecord,
|
|
17326
17755
|
migrateRecords,
|
|
17756
|
+
migratePeers,
|
|
17327
17757
|
metahubHome,
|
|
17328
17758
|
maybeAutoIndex,
|
|
17329
17759
|
makeId,
|
|
@@ -17380,10 +17810,13 @@ export {
|
|
|
17380
17810
|
SyncRequestSchema,
|
|
17381
17811
|
SYNC_PATH,
|
|
17382
17812
|
RENEW_PATH,
|
|
17813
|
+
PairResponseSchema,
|
|
17814
|
+
PairRequestSchema,
|
|
17383
17815
|
PROP_TYPES,
|
|
17816
|
+
PAIR_PATH,
|
|
17384
17817
|
HealthResponseSchema,
|
|
17385
17818
|
HEALTH_PATH,
|
|
17386
17819
|
ChangeSchema
|
|
17387
17820
|
};
|
|
17388
17821
|
|
|
17389
|
-
//# debugId=
|
|
17822
|
+
//# debugId=52E20C0E56A04E8A64756E2164756E21
|