@tensorix/metahub 0.1.2 → 0.1.4
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 +50 -5
- package/dist/cli.js +3160 -196
- package/dist/cli.js.map +37 -21
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/databases.d.ts.map +1 -1
- 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/integrity.d.ts +32 -0
- package/dist/core/integrity.d.ts.map +1 -0
- package/dist/core/properties.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/search.d.ts +2 -0
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts +2 -0
- package/dist/core/snapshot.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 +20 -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 +11 -0
- package/dist/core/sync/webui.d.ts.map +1 -1
- package/dist/index.js +806 -143
- package/dist/index.js.map +21 -17
- package/dist/webui.js +33 -32
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -531,9 +531,14 @@ var init_sites = __esm(() => {
|
|
|
531
531
|
// src/core/sync/webui.ts
|
|
532
532
|
var exports_webui = {};
|
|
533
533
|
__export(exports_webui, {
|
|
534
|
+
warmWebui: () => warmWebui,
|
|
535
|
+
setWebuiBundle: () => setWebuiBundle,
|
|
534
536
|
serveWebui: () => serveWebui
|
|
535
537
|
});
|
|
536
538
|
import { fileURLToPath } from "url";
|
|
539
|
+
function setWebuiBundle(js) {
|
|
540
|
+
cachedJs = js;
|
|
541
|
+
}
|
|
537
542
|
async function getJs() {
|
|
538
543
|
if (cachedJs != null)
|
|
539
544
|
return cachedJs;
|
|
@@ -549,6 +554,11 @@ async function getJs() {
|
|
|
549
554
|
cachedJs = await res.outputs[0].text();
|
|
550
555
|
return cachedJs;
|
|
551
556
|
}
|
|
557
|
+
async function warmWebui() {
|
|
558
|
+
try {
|
|
559
|
+
await getJs();
|
|
560
|
+
} catch {}
|
|
561
|
+
}
|
|
552
562
|
async function serveWebui(req) {
|
|
553
563
|
if (req.method !== "GET")
|
|
554
564
|
return null;
|
|
@@ -575,7 +585,13 @@ var HTML = `<!doctype html>
|
|
|
575
585
|
<title>Metahub</title>
|
|
576
586
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
577
587
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
578
|
-
|
|
588
|
+
<!-- Load web fonts without blocking first paint: media="print" keeps the
|
|
589
|
+
stylesheet non-render-blocking, then onload flips it to "all". The page
|
|
590
|
+
paints instantly with the system-font fallbacks baked into --ui/--mono and
|
|
591
|
+
swaps to the web fonts when they arrive (or stays on system fonts offline).
|
|
592
|
+
This is what kept the desktop cold-start white for seconds on slow networks. -->
|
|
593
|
+
<link rel="stylesheet" media="print" onload="this.media='all'" href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
|
|
594
|
+
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"></noscript>
|
|
579
595
|
<script>try{document.documentElement.dataset.theme=localStorage.getItem('mh-theme')||'system'}catch(e){}</script>
|
|
580
596
|
<style>
|
|
581
597
|
:root {
|
|
@@ -685,6 +701,9 @@ var HTML = `<!doctype html>
|
|
|
685
701
|
.set-section { margin-top:34px; }
|
|
686
702
|
.set-section-head { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); margin-bottom:4px; }
|
|
687
703
|
.set-section-desc { color:var(--fg-soft); margin-bottom:16px; }
|
|
704
|
+
.set-footer { margin-top:40px; padding-top:20px; border-top:1px solid var(--line); display:flex;
|
|
705
|
+
justify-content:center; gap:8px; color:var(--muted); font-size:12px; }
|
|
706
|
+
.set-footer-sep { opacity:.5; }
|
|
688
707
|
.theme-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
|
|
689
708
|
.theme-card { position:relative; display:flex; flex-direction:column; align-items:flex-start; gap:10px;
|
|
690
709
|
text-align:left; padding:16px; border:1px solid var(--line-strong); border-radius:var(--radius);
|
|
@@ -704,9 +723,25 @@ var HTML = `<!doctype html>
|
|
|
704
723
|
.theme-card.sel .tc-check { opacity:1; transform:scale(1); }
|
|
705
724
|
@media (max-width:560px) { .theme-grid { grid-template-columns:1fr; } }
|
|
706
725
|
|
|
726
|
+
/* sync devices */
|
|
727
|
+
.peer-actions { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; }
|
|
728
|
+
.peer-actions .btn { display:inline-flex; align-items:center; gap:6px; }
|
|
729
|
+
.peer-list { display:flex; flex-direction:column; gap:8px; }
|
|
730
|
+
.peer-row { display:flex; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--line);
|
|
731
|
+
border-radius:10px; background:var(--surface); }
|
|
732
|
+
.peer-dot { width:9px; height:9px; border-radius:50%; flex:none; background:var(--muted); }
|
|
733
|
+
.peer-dot.on { background:#3fb950; } .peer-dot.err { background:#f85149; } .peer-dot.off { background:var(--muted); }
|
|
734
|
+
.peer-main { flex:1; min-width:0; }
|
|
735
|
+
.peer-url { font-weight:600; font-size:14px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
736
|
+
.peer-sub { font-size:12px; color:var(--muted); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
737
|
+
.peer-menu { flex:none; padding:6px; }
|
|
738
|
+
.pair-code { font-family:var(--mono); font-size:26px; font-weight:700; letter-spacing:.12em;
|
|
739
|
+
text-align:center; padding:16px; border:1px dashed var(--line); border-radius:10px; background:var(--surface); color:var(--fg); }
|
|
740
|
+
|
|
707
741
|
/* main */
|
|
708
742
|
.main { flex:1; display:flex; flex-direction:column; overflow:hidden; min-width:0; }
|
|
709
743
|
.topbar { display:flex; align-items:center; gap:6px; padding:10px 18px; border-bottom:1px solid var(--line); min-height:49px; }
|
|
744
|
+
.topbar.bare { border-bottom:0; }
|
|
710
745
|
.topbar .hamburger { display:none; }
|
|
711
746
|
.topbar .hamburger.show-collapsed { display:grid; }
|
|
712
747
|
.sidebar.collapsed { overflow:hidden; }
|
|
@@ -1031,7 +1066,71 @@ var HTML = `<!doctype html>
|
|
|
1031
1066
|
@keyframes slidein { from { opacity:0; transform:translateX(-12px); } }
|
|
1032
1067
|
|
|
1033
1068
|
/* misc */
|
|
1034
|
-
.empty { display:
|
|
1069
|
+
.empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%;
|
|
1070
|
+
color:var(--muted); text-align:center; padding:40px; }
|
|
1071
|
+
|
|
1072
|
+
/* ---- empty state: \u77E5\u8BC6\u751F\u957F\u63D2\u753B ---- */
|
|
1073
|
+
.estate-art { width:240px; height:208px; overflow:visible; animation:idleFloat 5s ease-in-out 1.6s infinite; }
|
|
1074
|
+
.estate-art .stroke-a { fill:none; stroke:var(--accent); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
|
|
1075
|
+
.estate-art .stroke { fill:none; stroke:var(--line-strong); stroke-width:2.4; stroke-linecap:round; stroke-linejoin:round; }
|
|
1076
|
+
.estate-art .fill-a { fill:var(--accent); }
|
|
1077
|
+
.estate-art .fill-soft { fill:var(--accent-soft); }
|
|
1078
|
+
.estate-art .fill-surface { fill:var(--bg); }
|
|
1079
|
+
.estate-art .ground { fill:var(--accent); }
|
|
1080
|
+
.estate-title { margin:28px 0 0; font-size:18px; font-weight:600; letter-spacing:.2px; color:var(--fg);
|
|
1081
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.0s; }
|
|
1082
|
+
.estate-hint { margin:9px 0 0; font-size:13.5px; line-height:1.6; color:var(--muted); max-width:300px;
|
|
1083
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.15s; }
|
|
1084
|
+
.estate-link { margin:18px 0 0; font-size:14px; font-weight:600; color:var(--accent);
|
|
1085
|
+
display:inline-flex; align-items:center; gap:6px; padding:9px 8px; border-radius:var(--radius-sm);
|
|
1086
|
+
opacity:0; animation:fadeUp .5s ease forwards 1.3s; transition:opacity .15s; }
|
|
1087
|
+
.estate-link:hover { opacity:.7; }
|
|
1088
|
+
/* intro */
|
|
1089
|
+
.draw { stroke-dasharray:100; stroke-dashoffset:100; }
|
|
1090
|
+
@keyframes draw { to { stroke-dashoffset:0; } }
|
|
1091
|
+
@keyframes leafGrow { from { opacity:0; transform:scale(.2) rotate(-10deg); } to { opacity:1; transform:scale(1) rotate(0); } }
|
|
1092
|
+
@keyframes cardUp { from { opacity:0; transform:translateY(16px) scale(.96); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
1093
|
+
@keyframes groundIn { to { opacity:.1; } }
|
|
1094
|
+
@keyframes glowIn { to { opacity:.6; } }
|
|
1095
|
+
@keyframes fadeUp { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
|
|
1096
|
+
/* idle loops \u2014 each 0% equals the settled rest state to avoid a jump */
|
|
1097
|
+
@keyframes idleFloat { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-5px); } }
|
|
1098
|
+
@keyframes plantSway { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-1.3deg); } 75% { transform:rotate(1.3deg); } }
|
|
1099
|
+
@keyframes leafSwayA { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(-2.6deg); } 75% { transform:rotate(2.6deg); } }
|
|
1100
|
+
@keyframes leafSwayB { 0%,100% { transform:rotate(0deg); } 25% { transform:rotate(2.6deg); } 75% { transform:rotate(-2.6deg); } }
|
|
1101
|
+
@keyframes groundBreathe { 0%,100% { transform:scale(1); opacity:.10; } 50% { transform:scale(1.07); opacity:.15; } }
|
|
1102
|
+
@keyframes glowPulse { 0%,100% { transform:scale(1); opacity:.6; } 50% { transform:scale(1.1); opacity:.85; } }
|
|
1103
|
+
@keyframes particle { 0% { opacity:0; transform:translate(0,0) scale(.4); } 35% { opacity:.85; transform:translate(2px,-4px) scale(1); } 70% { opacity:0; transform:translate(4px,-9px) scale(.5); } 100% { opacity:0; transform:translate(0,0) scale(.4); } }
|
|
1104
|
+
/* wiring */
|
|
1105
|
+
.kg-ground { transform-box:fill-box; transform-origin:center; opacity:0;
|
|
1106
|
+
animation:groundIn .5s ease forwards .1s, groundBreathe 6s ease-in-out 1.6s infinite; }
|
|
1107
|
+
.kg-glow { filter:blur(9px); transform-box:fill-box; transform-origin:center; opacity:0;
|
|
1108
|
+
animation:glowIn .6s ease forwards .1s, glowPulse 4.6s ease-in-out 1.6s infinite; }
|
|
1109
|
+
.kg-cardB { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .2s; }
|
|
1110
|
+
.kg-cardA { transform-box:fill-box; transform-origin:center; opacity:0; animation:cardUp .55s cubic-bezier(.2,.8,.2,1) forwards .32s; }
|
|
1111
|
+
.kg-plant { transform-box:fill-box; transform-origin:50% 100%; animation:plantSway 6s ease-in-out 1.7s infinite; }
|
|
1112
|
+
.kg-stem { animation:draw .9s ease-out forwards .4s; }
|
|
1113
|
+
.kg-leaf { transform-box:fill-box; }
|
|
1114
|
+
.kg-grow { transform-box:fill-box; opacity:0; }
|
|
1115
|
+
.kg-leaf.l1 { transform-origin:90% 100%; animation:leafSwayA 4.6s ease-in-out 1.7s infinite; }
|
|
1116
|
+
.kg-leaf.l2 { transform-origin:0% 80%; animation:leafSwayB 5.2s ease-in-out 1.8s infinite; }
|
|
1117
|
+
.kg-leaf.l3 { transform-origin:0% 100%; animation:leafSwayA 5.0s ease-in-out 1.9s infinite; }
|
|
1118
|
+
.kg-leaf.l1 .kg-grow { transform-origin:90% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards .85s; }
|
|
1119
|
+
.kg-leaf.l2 .kg-grow { transform-origin:0% 80%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.0s; }
|
|
1120
|
+
.kg-leaf.l3 .kg-grow { transform-origin:0% 100%; animation:leafGrow .52s cubic-bezier(.18,.9,.24,1.22) forwards 1.15s; }
|
|
1121
|
+
.kg-particle { transform-box:fill-box; transform-origin:center; opacity:0; }
|
|
1122
|
+
.kg-particle.p1 { animation:particle 4.5s ease-in-out 1.6s infinite; }
|
|
1123
|
+
.kg-particle.p2 { animation:particle 5.2s ease-in-out 2.0s infinite; }
|
|
1124
|
+
.kg-particle.p3 { animation:particle 4.8s ease-in-out 2.6s infinite; }
|
|
1125
|
+
.kg-particle.p4 { animation:particle 5.6s ease-in-out 3.1s infinite; }
|
|
1126
|
+
.kg-particle.p5 { animation:particle 4.2s ease-in-out 2.3s infinite; }
|
|
1127
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1128
|
+
.estate-art, .estate-art *, .estate-title, .estate-hint, .estate-link { animation:none !important; opacity:1 !important; transform:none !important; }
|
|
1129
|
+
.draw { stroke-dashoffset:0 !important; }
|
|
1130
|
+
.kg-ground { opacity:.1 !important; }
|
|
1131
|
+
.kg-glow { opacity:.6 !important; }
|
|
1132
|
+
.kg-particle { opacity:0 !important; }
|
|
1133
|
+
}
|
|
1035
1134
|
.error-bar { margin:12px 24px; padding:10px 14px; background:var(--danger-soft); color:var(--danger); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
|
|
1036
1135
|
.view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
|
|
1037
1136
|
.ico.flip { transform:scaleX(-1); }
|
|
@@ -1140,6 +1239,49 @@ var HTML = `<!doctype html>
|
|
|
1140
1239
|
.db { padding:16px 14px 90px; }
|
|
1141
1240
|
.peek { width:100%; }
|
|
1142
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
/* Quick Notes window (desktop, loaded at #quick). A transparent body lets the
|
|
1244
|
+
macOS vibrancy blur show through; a translucent panel keeps text readable.
|
|
1245
|
+
The top bar is the window drag region; its buttons opt back out. */
|
|
1246
|
+
body.quicknote { background:transparent; }
|
|
1247
|
+
/* The main app lays #app out as a flex row (sidebar + main); the quick-note
|
|
1248
|
+
route has a single child, so override to a full-bleed block or the panel
|
|
1249
|
+
shrinks to its content width and the window's vibrancy shows through. */
|
|
1250
|
+
body.quicknote #app { display:block; width:100vw; height:100vh; }
|
|
1251
|
+
.qn { display:flex; flex-direction:column; width:100%; height:100vh; overflow:hidden;
|
|
1252
|
+
background:color-mix(in srgb, var(--bg) 20%, transparent); }
|
|
1253
|
+
.qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px 4px 76px;
|
|
1254
|
+
user-select:none; -webkit-app-region:drag;
|
|
1255
|
+
background:color-mix(in srgb, var(--surface) 30%, transparent); }
|
|
1256
|
+
.qn-brand { flex:1; min-width:0; font-size:12px; font-weight:600; color:var(--fg-soft);
|
|
1257
|
+
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
1258
|
+
.qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
|
|
1259
|
+
.qn-actions .iconbtn { width:26px; height:26px; }
|
|
1260
|
+
.qn-actions .iconbtn.active { color:var(--accent); background:var(--hover-2); }
|
|
1261
|
+
.qn-body { flex:1; overflow:auto; -webkit-app-region:no-drag; }
|
|
1262
|
+
.qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
|
|
1263
|
+
.qn-body .doc-title { font-size:24px; margin-bottom:2px; }
|
|
1264
|
+
.qn-body .doc-meta { display:none; }
|
|
1265
|
+
/* Compact menus inside the quick-note window only (dropdown + slash command
|
|
1266
|
+
share .pop/.item/.lico with the main app; scope under body.quicknote so the
|
|
1267
|
+
main app keeps its roomier density). */
|
|
1268
|
+
body.quicknote .pop { padding:5px; border-radius:9px; }
|
|
1269
|
+
body.quicknote .pop .lbl { padding:4px 8px 2px; font-size:10px; }
|
|
1270
|
+
body.quicknote .pop .item { padding:5px 9px; gap:8px; font-size:12.5px; }
|
|
1271
|
+
body.quicknote .pop .item .t { font-size:12.5px; }
|
|
1272
|
+
body.quicknote .pop .item .d { font-size:10.5px; }
|
|
1273
|
+
body.quicknote .pop .item .lico { width:22px; height:22px; }
|
|
1274
|
+
body.quicknote .pop .item .lico.plain { width:20px; height:20px; }
|
|
1275
|
+
body.quicknote .pop .sep { margin:4px 4px; }
|
|
1276
|
+
|
|
1277
|
+
/* Quick Notes settings rows (desktop only). */
|
|
1278
|
+
.qn-set-row { display:flex; align-items:center; gap:14px; padding:12px 0; border-top:1px solid var(--line); }
|
|
1279
|
+
.qn-set-row:first-of-type { border-top:0; }
|
|
1280
|
+
.qn-set-main { flex:1; min-width:0; }
|
|
1281
|
+
.qn-set-name { font-weight:600; }
|
|
1282
|
+
.qn-set-desc { color:var(--muted); font-size:13px; margin-top:2px; }
|
|
1283
|
+
.qn-shortcut { min-width:140px; justify-content:center; font-family:var(--mono); }
|
|
1284
|
+
.qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
|
|
1143
1285
|
</style>
|
|
1144
1286
|
</head>
|
|
1145
1287
|
<body>
|
|
@@ -1207,11 +1349,41 @@ CREATE TABLE IF NOT EXISTS crdt_changes (
|
|
|
1207
1349
|
CREATE INDEX IF NOT EXISTS idx_changes_hlc ON crdt_changes(hlc);
|
|
1208
1350
|
|
|
1209
1351
|
-- Per-peer replication cursors (SQLite rowids: monotonic in insertion order,
|
|
1210
|
-
-- so no change is ever skipped regardless of HLC/clock skew).
|
|
1352
|
+
-- so no change is ever skipped regardless of HLC/clock skew). Outbound side of
|
|
1353
|
+
-- a pairing: "token" is the credential the *remote* issued to us (sent as a
|
|
1354
|
+
-- Bearer header when we sync to them). See migratePeers in db.ts for the columns
|
|
1355
|
+
-- added to legacy databases.
|
|
1211
1356
|
CREATE TABLE IF NOT EXISTS peers (
|
|
1212
|
-
url
|
|
1213
|
-
pull_cursor
|
|
1214
|
-
push_cursor
|
|
1357
|
+
url TEXT PRIMARY KEY,
|
|
1358
|
+
pull_cursor INTEGER NOT NULL DEFAULT 0,
|
|
1359
|
+
push_cursor INTEGER NOT NULL DEFAULT 0,
|
|
1360
|
+
token TEXT,
|
|
1361
|
+
label TEXT,
|
|
1362
|
+
node_id TEXT,
|
|
1363
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1364
|
+
last_sync_at INTEGER,
|
|
1365
|
+
last_status TEXT,
|
|
1366
|
+
last_error TEXT
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
-- Inbound side of a pairing: credentials *we* issued to remote devices. A peer
|
|
1370
|
+
-- presents one of these on /sync; acceptsSyncToken() checks it alongside the
|
|
1371
|
+
-- managed master token. Revoke by deleting the row.
|
|
1372
|
+
CREATE TABLE IF NOT EXISTS peer_grants (
|
|
1373
|
+
token TEXT PRIMARY KEY,
|
|
1374
|
+
peer_url TEXT,
|
|
1375
|
+
node_id TEXT,
|
|
1376
|
+
created_at INTEGER
|
|
1377
|
+
);
|
|
1378
|
+
|
|
1379
|
+
-- One-time pairing codes minted by "mh config peer code" / POST /api/pair/new.
|
|
1380
|
+
-- Short-lived and single-use: redeemed during the pairing handshake, then the
|
|
1381
|
+
-- two devices exchange durable peer_grants. used/exp gate redemption.
|
|
1382
|
+
CREATE TABLE IF NOT EXISTS pairing_codes (
|
|
1383
|
+
code TEXT PRIMARY KEY,
|
|
1384
|
+
exp INTEGER NOT NULL,
|
|
1385
|
+
used INTEGER NOT NULL DEFAULT 0,
|
|
1386
|
+
created_at INTEGER
|
|
1215
1387
|
);
|
|
1216
1388
|
|
|
1217
1389
|
CREATE TABLE IF NOT EXISTS databases (
|
|
@@ -1338,9 +1510,20 @@ function deleteDatabase(db, id) {
|
|
|
1338
1510
|
if (!getDatabase(db, id))
|
|
1339
1511
|
return false;
|
|
1340
1512
|
emit(db, "databases", id, "__deleted", 1);
|
|
1513
|
+
const tombstone = (table) => {
|
|
1514
|
+
for (const r of liveChildren(db, table, "database_id", id))
|
|
1515
|
+
emit(db, table, r, "__deleted", 1);
|
|
1516
|
+
};
|
|
1517
|
+
tombstone("properties");
|
|
1518
|
+
tombstone("records");
|
|
1519
|
+
for (const d of liveChildren(db, "documents", "database_id", id))
|
|
1520
|
+
emit(db, "documents", d, "database_id", null);
|
|
1341
1521
|
db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
|
|
1342
1522
|
return true;
|
|
1343
1523
|
}
|
|
1524
|
+
function liveChildren(db, table, col, parentId) {
|
|
1525
|
+
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
|
|
1526
|
+
}
|
|
1344
1527
|
|
|
1345
1528
|
// src/core/properties.ts
|
|
1346
1529
|
init_ids();
|
|
@@ -1469,9 +1652,13 @@ function setPropertyWidth(db, id, width) {
|
|
|
1469
1652
|
return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w } });
|
|
1470
1653
|
}
|
|
1471
1654
|
function removeProperty(db, id) {
|
|
1472
|
-
|
|
1655
|
+
const prop = getProperty(db, id);
|
|
1656
|
+
if (!prop)
|
|
1473
1657
|
return false;
|
|
1474
1658
|
emit(db, "properties", id, "__deleted", 1);
|
|
1659
|
+
const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(prop.database_id, id);
|
|
1660
|
+
for (const r of rows)
|
|
1661
|
+
emit(db, "records", r.id, id, undefined);
|
|
1475
1662
|
return true;
|
|
1476
1663
|
}
|
|
1477
1664
|
|
|
@@ -1924,12 +2111,28 @@ function migrateRecords(db) {
|
|
|
1924
2111
|
tx();
|
|
1925
2112
|
backfillRecordOrderKeys(db);
|
|
1926
2113
|
}
|
|
2114
|
+
function migratePeers(db) {
|
|
2115
|
+
const add = [
|
|
2116
|
+
["token", "TEXT"],
|
|
2117
|
+
["label", "TEXT"],
|
|
2118
|
+
["node_id", "TEXT"],
|
|
2119
|
+
["enabled", "INTEGER NOT NULL DEFAULT 1"],
|
|
2120
|
+
["last_sync_at", "INTEGER"],
|
|
2121
|
+
["last_status", "TEXT"],
|
|
2122
|
+
["last_error", "TEXT"]
|
|
2123
|
+
];
|
|
2124
|
+
for (const [col, decl] of add) {
|
|
2125
|
+
if (!hasColumn(db, "peers", col))
|
|
2126
|
+
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
1927
2129
|
function openMetahub() {
|
|
1928
2130
|
ensureDirs();
|
|
1929
2131
|
const db = new Database(dbPath(), { create: true });
|
|
1930
2132
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
1931
2133
|
runSchema(db);
|
|
1932
2134
|
migrateRecords(db);
|
|
2135
|
+
migratePeers(db);
|
|
1933
2136
|
return db;
|
|
1934
2137
|
}
|
|
1935
2138
|
|
|
@@ -2032,9 +2235,12 @@ function getDocument(db, id) {
|
|
|
2032
2235
|
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
2033
2236
|
}
|
|
2034
2237
|
function listDocuments(db, opts = {}) {
|
|
2238
|
+
const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
|
|
2239
|
+
if (opts.parent_id !== undefined)
|
|
2240
|
+
return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
|
|
2035
2241
|
if (opts.database_id)
|
|
2036
|
-
return db.query(
|
|
2037
|
-
return db.query(
|
|
2242
|
+
return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
|
|
2243
|
+
return db.query(`${cols} ORDER BY created_hlc`).all();
|
|
2038
2244
|
}
|
|
2039
2245
|
function updateDocument(db, id, fields) {
|
|
2040
2246
|
if (!getDocument(db, id))
|
|
@@ -2118,8 +2324,15 @@ function deleteDocument(db, id) {
|
|
|
2118
2324
|
if (!getDocument(db, id))
|
|
2119
2325
|
return false;
|
|
2120
2326
|
emit(db, "documents", id, "__deleted", 1);
|
|
2327
|
+
for (const b of liveChildIds(db, "doc_blocks", "doc_id", id))
|
|
2328
|
+
emit(db, "doc_blocks", b, "__deleted", 1);
|
|
2329
|
+
for (const c of liveChildIds(db, "documents", "parent_id", id))
|
|
2330
|
+
emit(db, "documents", c, "parent_id", null);
|
|
2121
2331
|
return true;
|
|
2122
2332
|
}
|
|
2333
|
+
function liveChildIds(db, table, col, parentId) {
|
|
2334
|
+
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
|
|
2335
|
+
}
|
|
2123
2336
|
function countOccurrences(haystack, needle) {
|
|
2124
2337
|
if (needle === "")
|
|
2125
2338
|
return 0;
|
|
@@ -2132,26 +2345,112 @@ function countOccurrences(haystack, needle) {
|
|
|
2132
2345
|
return n;
|
|
2133
2346
|
}
|
|
2134
2347
|
// src/core/search.ts
|
|
2348
|
+
init_crdt();
|
|
2135
2349
|
var TEXT_TYPES = "('text','url','select','multi_select','date')";
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2350
|
+
var SEARCH_INDEX_VERSION = "1";
|
|
2351
|
+
function readMeta(db, key) {
|
|
2352
|
+
return db.query("SELECT value FROM meta WHERE key = ?").get(key)?.value ?? null;
|
|
2353
|
+
}
|
|
2354
|
+
function writeMeta(db, key, value) {
|
|
2355
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
2356
|
+
}
|
|
2357
|
+
function reindexDocument(db, id) {
|
|
2358
|
+
db.query("DELETE FROM search_fts WHERE kind = 'document' AND id = ?").run(id);
|
|
2359
|
+
const d = db.query("SELECT id, title, body, database_id FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
2360
|
+
if (d)
|
|
2145
2361
|
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('document', ?, ?, ?, ?)").run(d.id, d.database_id, d.title ?? "", d.body ?? "");
|
|
2146
|
-
|
|
2147
|
-
|
|
2362
|
+
}
|
|
2363
|
+
function recordBodyRows(db, where, ...args) {
|
|
2364
|
+
return db.query(`SELECT r.id AS id, r.database_id AS database_id, group_concat(je.value, ' ') AS body
|
|
2148
2365
|
FROM records r, json_each(r.data) je
|
|
2149
2366
|
JOIN properties p ON p.id = je.key AND p.__deleted = 0
|
|
2150
|
-
WHERE r.__deleted = 0 AND p.type IN ${TEXT_TYPES}
|
|
2151
|
-
GROUP BY r.id`).all()
|
|
2152
|
-
|
|
2367
|
+
WHERE ${where} AND r.__deleted = 0 AND p.type IN ${TEXT_TYPES}
|
|
2368
|
+
GROUP BY r.id`).all(...args);
|
|
2369
|
+
}
|
|
2370
|
+
function insertRecordRow(db, r) {
|
|
2371
|
+
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('record', ?, ?, ?, ?)").run(r.id, r.database_id, r.id, r.body ?? "");
|
|
2372
|
+
}
|
|
2373
|
+
function reindexRecord(db, id) {
|
|
2374
|
+
db.query("DELETE FROM search_fts WHERE kind = 'record' AND id = ?").run(id);
|
|
2375
|
+
const r = recordBodyRows(db, "r.id = ?", id)[0];
|
|
2376
|
+
if (r)
|
|
2377
|
+
insertRecordRow(db, r);
|
|
2378
|
+
}
|
|
2379
|
+
function reindexDatabaseRecords(db, databaseId) {
|
|
2380
|
+
db.query("DELETE FROM search_fts WHERE kind = 'record' AND database_id = ?").run(databaseId);
|
|
2381
|
+
for (const r of recordBodyRows(db, "r.database_id = ?", databaseId))
|
|
2382
|
+
insertRecordRow(db, r);
|
|
2383
|
+
}
|
|
2384
|
+
function fullRebuild(db) {
|
|
2385
|
+
db.query("DELETE FROM search_fts").run();
|
|
2386
|
+
for (const d of db.query("SELECT id, title, body, database_id FROM documents WHERE __deleted = 0").all())
|
|
2387
|
+
db.query("INSERT INTO search_fts (kind, id, database_id, title, body) VALUES ('document', ?, ?, ?, ?)").run(d.id, d.database_id, d.title ?? "", d.body ?? "");
|
|
2388
|
+
for (const r of recordBodyRows(db, "1 = 1"))
|
|
2389
|
+
insertRecordRow(db, r);
|
|
2390
|
+
const max = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get().m ?? 0;
|
|
2391
|
+
writeMeta(db, "search_seq", String(max));
|
|
2392
|
+
writeMeta(db, "search_index_version", SEARCH_INDEX_VERSION);
|
|
2393
|
+
}
|
|
2394
|
+
function incrementalUpdate(db) {
|
|
2395
|
+
const cursor = Number(readMeta(db, "search_seq") ?? "0");
|
|
2396
|
+
const { changes, cursor: next } = changesAfterSeq(db, cursor);
|
|
2397
|
+
if (changes.length === 0)
|
|
2398
|
+
return;
|
|
2399
|
+
const docIds = new Set;
|
|
2400
|
+
const recordIds = new Set;
|
|
2401
|
+
const dbIds = new Set;
|
|
2402
|
+
for (const c of changes) {
|
|
2403
|
+
switch (c.dataset) {
|
|
2404
|
+
case "documents":
|
|
2405
|
+
docIds.add(c.row_id);
|
|
2406
|
+
break;
|
|
2407
|
+
case "doc_blocks": {
|
|
2408
|
+
const blk = db.query("SELECT doc_id FROM doc_blocks WHERE id = ?").get(c.row_id);
|
|
2409
|
+
if (blk?.doc_id)
|
|
2410
|
+
docIds.add(blk.doc_id);
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
case "records":
|
|
2414
|
+
recordIds.add(c.row_id);
|
|
2415
|
+
break;
|
|
2416
|
+
case "properties":
|
|
2417
|
+
if (c.col === "type" || c.col === "__deleted" || c.col === "database_id") {
|
|
2418
|
+
const p = db.query("SELECT database_id FROM properties WHERE id = ?").get(c.row_id);
|
|
2419
|
+
if (p?.database_id)
|
|
2420
|
+
dbIds.add(p.database_id);
|
|
2421
|
+
}
|
|
2422
|
+
break;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
for (const id of docIds)
|
|
2426
|
+
reindexDocument(db, id);
|
|
2427
|
+
for (const dbId of dbIds)
|
|
2428
|
+
reindexDatabaseRecords(db, dbId);
|
|
2429
|
+
for (const id of recordIds) {
|
|
2430
|
+
const r = db.query("SELECT database_id FROM records WHERE id = ?").get(id);
|
|
2431
|
+
if (r?.database_id && dbIds.has(r.database_id))
|
|
2432
|
+
continue;
|
|
2433
|
+
reindexRecord(db, id);
|
|
2153
2434
|
}
|
|
2154
|
-
db
|
|
2435
|
+
writeMeta(db, "search_seq", String(next));
|
|
2436
|
+
}
|
|
2437
|
+
function ensureIndex(db) {
|
|
2438
|
+
if (!ftsAvailable(db))
|
|
2439
|
+
return false;
|
|
2440
|
+
const version = readMeta(db, "search_index_version");
|
|
2441
|
+
const seq = readMeta(db, "search_seq");
|
|
2442
|
+
db.transaction(() => {
|
|
2443
|
+
if (version !== SEARCH_INDEX_VERSION || seq === null)
|
|
2444
|
+
fullRebuild(db);
|
|
2445
|
+
else
|
|
2446
|
+
incrementalUpdate(db);
|
|
2447
|
+
})();
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
function rebuildSearchIndex(db) {
|
|
2451
|
+
if (!ftsAvailable(db))
|
|
2452
|
+
return false;
|
|
2453
|
+
db.transaction(() => fullRebuild(db))();
|
|
2155
2454
|
return true;
|
|
2156
2455
|
}
|
|
2157
2456
|
function ftsSearch(db, query, limit) {
|
|
@@ -16517,26 +16816,41 @@ var HealthResponseSchema = exports_external.object({
|
|
|
16517
16816
|
ok: exports_external.boolean(),
|
|
16518
16817
|
node: exports_external.string().describe("Server's node id")
|
|
16519
16818
|
});
|
|
16819
|
+
var PairRequestSchema = exports_external.object({
|
|
16820
|
+
code: exports_external.string().describe("One-time pairing code minted by this server"),
|
|
16821
|
+
node_id: exports_external.string().describe("Calling device's node id"),
|
|
16822
|
+
grant: exports_external.string().describe("Durable credential the caller issues to us"),
|
|
16823
|
+
self_url: exports_external.string().optional().describe("Caller's reachable server URL (for mutual sync)")
|
|
16824
|
+
});
|
|
16825
|
+
var PairResponseSchema = exports_external.object({
|
|
16826
|
+
node_id: exports_external.string().describe("This server's node id"),
|
|
16827
|
+
grant: exports_external.string().describe("Durable credential we issue to the caller")
|
|
16828
|
+
});
|
|
16520
16829
|
var SYNC_PATH = "/sync";
|
|
16521
16830
|
var HEALTH_PATH = "/health";
|
|
16522
16831
|
var RENEW_PATH = "/auth/token";
|
|
16832
|
+
var PAIR_PATH = "/api/pair";
|
|
16523
16833
|
// src/core/sync/client.ts
|
|
16524
16834
|
init_node();
|
|
16525
16835
|
init_crdt();
|
|
16526
16836
|
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 };
|
|
16837
|
+
const row = db.query("SELECT pull_cursor, push_cursor, token FROM peers WHERE url = ?").get(url2);
|
|
16838
|
+
return row ?? { pull_cursor: 0, push_cursor: 0, token: null };
|
|
16529
16839
|
}
|
|
16530
16840
|
function setPeer(db, url2, c) {
|
|
16531
16841
|
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
16842
|
}
|
|
16533
|
-
async function syncWithPeer(db, url2) {
|
|
16843
|
+
async function syncWithPeer(db, url2, token) {
|
|
16534
16844
|
const node = getNodeId(db);
|
|
16535
16845
|
const peer = getPeer(db, url2);
|
|
16846
|
+
const bearer = token ?? peer.token ?? undefined;
|
|
16536
16847
|
const toPush = changesAfterSeq(db, peer.push_cursor);
|
|
16848
|
+
const headers = { "content-type": "application/json" };
|
|
16849
|
+
if (bearer)
|
|
16850
|
+
headers.authorization = `Bearer ${bearer}`;
|
|
16537
16851
|
const res = await fetch(new URL(SYNC_PATH, url2), {
|
|
16538
16852
|
method: "POST",
|
|
16539
|
-
headers
|
|
16853
|
+
headers,
|
|
16540
16854
|
body: JSON.stringify({ node_id: node, since: peer.pull_cursor, changes: toPush.changes })
|
|
16541
16855
|
});
|
|
16542
16856
|
if (!res.ok)
|
|
@@ -16549,10 +16863,108 @@ async function syncWithPeer(db, url2) {
|
|
|
16549
16863
|
}
|
|
16550
16864
|
// src/core/sync/server.ts
|
|
16551
16865
|
init_node();
|
|
16866
|
+
|
|
16867
|
+
// src/core/sync/token.ts
|
|
16868
|
+
init_ids();
|
|
16869
|
+
var K_TOKEN = "auth_token";
|
|
16870
|
+
var K_EXP = "auth_token_exp";
|
|
16871
|
+
var K_PREV = "auth_token_prev";
|
|
16872
|
+
var K_PREV_EXP = "auth_token_prev_exp";
|
|
16873
|
+
function parseDuration(s, fallbackMs) {
|
|
16874
|
+
if (!s)
|
|
16875
|
+
return fallbackMs;
|
|
16876
|
+
const m = /^(\d+)\s*(ms|s|m|h|d)?$/.exec(s.trim());
|
|
16877
|
+
if (!m)
|
|
16878
|
+
return fallbackMs;
|
|
16879
|
+
const n = Number(m[1]);
|
|
16880
|
+
switch (m[2]) {
|
|
16881
|
+
case "ms":
|
|
16882
|
+
return n;
|
|
16883
|
+
case "m":
|
|
16884
|
+
return n * 60000;
|
|
16885
|
+
case "h":
|
|
16886
|
+
return n * 3600000;
|
|
16887
|
+
case "d":
|
|
16888
|
+
return n * 86400000;
|
|
16889
|
+
default:
|
|
16890
|
+
return n * 1000;
|
|
16891
|
+
}
|
|
16892
|
+
}
|
|
16893
|
+
var DAY = 86400000;
|
|
16894
|
+
var DEFAULT_TTL_MS = parseDuration(process.env.METAHUB_TOKEN_TTL, 30 * DAY);
|
|
16895
|
+
var DEFAULT_GRACE_MS = parseDuration(process.env.METAHUB_TOKEN_GRACE, 7 * DAY);
|
|
16896
|
+
function getMeta(db, key) {
|
|
16897
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
16898
|
+
return row ? row.value : null;
|
|
16899
|
+
}
|
|
16900
|
+
function setMeta(db, key, value) {
|
|
16901
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
16902
|
+
}
|
|
16903
|
+
function readState(db) {
|
|
16904
|
+
const token = getMeta(db, K_TOKEN);
|
|
16905
|
+
if (!token)
|
|
16906
|
+
return null;
|
|
16907
|
+
const prev = getMeta(db, K_PREV);
|
|
16908
|
+
return {
|
|
16909
|
+
token,
|
|
16910
|
+
exp: Number(getMeta(db, K_EXP) ?? 0),
|
|
16911
|
+
prev: prev && prev.length > 0 ? prev : null,
|
|
16912
|
+
prevExp: Number(getMeta(db, K_PREV_EXP) ?? 0)
|
|
16913
|
+
};
|
|
16914
|
+
}
|
|
16915
|
+
function rotate(db, ttlMs, graceMs) {
|
|
16916
|
+
const now = Date.now();
|
|
16917
|
+
const old = getMeta(db, K_TOKEN);
|
|
16918
|
+
const next = {
|
|
16919
|
+
token: randomSuffix(24),
|
|
16920
|
+
exp: now + ttlMs,
|
|
16921
|
+
prev: old,
|
|
16922
|
+
prevExp: old ? now + graceMs : 0
|
|
16923
|
+
};
|
|
16924
|
+
setMeta(db, K_TOKEN, next.token);
|
|
16925
|
+
setMeta(db, K_EXP, String(next.exp));
|
|
16926
|
+
setMeta(db, K_PREV, next.prev ?? "");
|
|
16927
|
+
setMeta(db, K_PREV_EXP, String(next.prevExp));
|
|
16928
|
+
return next;
|
|
16929
|
+
}
|
|
16930
|
+
function loadOrRotate(db, ttlMs, graceMs) {
|
|
16931
|
+
const s = readState(db);
|
|
16932
|
+
if (!s || Date.now() > s.exp)
|
|
16933
|
+
return rotate(db, ttlMs, graceMs);
|
|
16934
|
+
return s;
|
|
16935
|
+
}
|
|
16936
|
+
|
|
16937
|
+
// src/core/config.ts
|
|
16938
|
+
var K_HOST = "cfg_host";
|
|
16939
|
+
var K_PORT = "cfg_port";
|
|
16940
|
+
var K_SYNC_INTERVAL = "cfg_sync_interval";
|
|
16941
|
+
var K_AUTO_SYNC = "cfg_auto_sync";
|
|
16942
|
+
var DEFAULT_CONFIG = {
|
|
16943
|
+
host: "127.0.0.1",
|
|
16944
|
+
port: 7777,
|
|
16945
|
+
syncIntervalMs: parseDuration(process.env.METAHUB_SYNC_INTERVAL, 30000),
|
|
16946
|
+
autoSync: true
|
|
16947
|
+
};
|
|
16948
|
+
function getMeta2(db, key) {
|
|
16949
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(key);
|
|
16950
|
+
return row ? row.value : null;
|
|
16951
|
+
}
|
|
16952
|
+
function getServerConfig(db) {
|
|
16953
|
+
const host = getMeta2(db, K_HOST);
|
|
16954
|
+
const port = getMeta2(db, K_PORT);
|
|
16955
|
+
const interval = getMeta2(db, K_SYNC_INTERVAL);
|
|
16956
|
+
const auto = getMeta2(db, K_AUTO_SYNC);
|
|
16957
|
+
return {
|
|
16958
|
+
host: host ?? DEFAULT_CONFIG.host,
|
|
16959
|
+
port: port != null ? Number(port) : DEFAULT_CONFIG.port,
|
|
16960
|
+
syncIntervalMs: interval != null ? Number(interval) : DEFAULT_CONFIG.syncIntervalMs,
|
|
16961
|
+
autoSync: auto != null ? auto === "1" : DEFAULT_CONFIG.autoSync
|
|
16962
|
+
};
|
|
16963
|
+
}
|
|
16552
16964
|
// package.json
|
|
16553
16965
|
var package_default = {
|
|
16554
16966
|
name: "@tensorix/metahub",
|
|
16555
|
-
version: "0.1.
|
|
16967
|
+
version: "0.1.4",
|
|
16556
16968
|
description: "Metahub core library and CLI",
|
|
16557
16969
|
type: "module",
|
|
16558
16970
|
main: "./dist/index.js",
|
|
@@ -16578,9 +16990,11 @@ var package_default = {
|
|
|
16578
16990
|
build: "bun run scripts/build.ts",
|
|
16579
16991
|
"build:binaries": "bun run scripts/compile-binaries.ts",
|
|
16580
16992
|
prepublishOnly: "bun run build",
|
|
16581
|
-
test: "bun test"
|
|
16993
|
+
test: "bun test",
|
|
16994
|
+
release: "bun run scripts/tag-release.ts"
|
|
16582
16995
|
},
|
|
16583
16996
|
dependencies: {
|
|
16997
|
+
"@clack/prompts": "^1.5.0",
|
|
16584
16998
|
citty: "^0.1.6",
|
|
16585
16999
|
"highlight.js": "^11.11.1",
|
|
16586
17000
|
preact: "^10.29.2",
|
|
@@ -16657,7 +17071,8 @@ var RecordValuesReq = exports_external.record(exports_external.string(), exports
|
|
|
16657
17071
|
var CreateDocumentReq = exports_external.object({
|
|
16658
17072
|
title: exports_external.string(),
|
|
16659
17073
|
body: exports_external.string().optional(),
|
|
16660
|
-
database_id: exports_external.string().optional()
|
|
17074
|
+
database_id: exports_external.string().optional(),
|
|
17075
|
+
parent_id: exports_external.string().optional()
|
|
16661
17076
|
});
|
|
16662
17077
|
var UpdateDocumentReq = exports_external.object({
|
|
16663
17078
|
title: exports_external.string().optional(),
|
|
@@ -16685,7 +17100,15 @@ function handle(fn) {
|
|
|
16685
17100
|
}
|
|
16686
17101
|
};
|
|
16687
17102
|
}
|
|
17103
|
+
var VersionSchema = exports_external.object({ version: exports_external.string() });
|
|
16688
17104
|
var webuiRoutes = [
|
|
17105
|
+
{
|
|
17106
|
+
method: "GET",
|
|
17107
|
+
path: "/api/version",
|
|
17108
|
+
summary: "Version of the running core (sidecar). Used by the desktop app's settings footer.",
|
|
17109
|
+
response: VersionSchema,
|
|
17110
|
+
handler: () => Response.json({ version: package_default.version })
|
|
17111
|
+
},
|
|
16689
17112
|
{
|
|
16690
17113
|
method: "GET",
|
|
16691
17114
|
path: "/api/databases",
|
|
@@ -16835,9 +17258,9 @@ var webuiRoutes = [
|
|
|
16835
17258
|
{
|
|
16836
17259
|
method: "GET",
|
|
16837
17260
|
path: "/api/documents",
|
|
16838
|
-
summary: "List documents. Optional query: ?db=<id>",
|
|
17261
|
+
summary: "List documents. Optional query: ?db=<id> or ?parent=<id>",
|
|
16839
17262
|
response: exports_external.array(DocumentSummarySchema),
|
|
16840
|
-
handler: handle((req, { db }) => listDocuments(db, { database_id: opt(req, "db") }))
|
|
17263
|
+
handler: handle((req, { db }) => listDocuments(db, { database_id: opt(req, "db"), parent_id: opt(req, "parent") }))
|
|
16841
17264
|
},
|
|
16842
17265
|
{
|
|
16843
17266
|
method: "POST",
|
|
@@ -16937,6 +17360,250 @@ var sitesRoutes = [
|
|
|
16937
17360
|
}
|
|
16938
17361
|
];
|
|
16939
17362
|
|
|
17363
|
+
// src/core/sync/pairing.ts
|
|
17364
|
+
init_ids();
|
|
17365
|
+
|
|
17366
|
+
// src/core/sync/peers.ts
|
|
17367
|
+
function listPeers(db) {
|
|
17368
|
+
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();
|
|
17369
|
+
}
|
|
17370
|
+
function addPeer(db, input) {
|
|
17371
|
+
db.query(`INSERT INTO peers (url, token, label, node_id, enabled, pull_cursor, push_cursor)
|
|
17372
|
+
VALUES (?, ?, ?, ?, 1, 0, 0)
|
|
17373
|
+
ON CONFLICT(url) DO UPDATE SET
|
|
17374
|
+
token = coalesce(excluded.token, peers.token),
|
|
17375
|
+
label = coalesce(excluded.label, peers.label),
|
|
17376
|
+
node_id = coalesce(excluded.node_id, peers.node_id),
|
|
17377
|
+
enabled = 1`).run(input.url, input.token ?? null, input.label ?? null, input.node_id ?? null);
|
|
17378
|
+
}
|
|
17379
|
+
function removePeer(db, url2) {
|
|
17380
|
+
const tx = db.transaction(() => {
|
|
17381
|
+
const changed = db.query("DELETE FROM peers WHERE url = ?").run(url2).changes > 0;
|
|
17382
|
+
db.query("DELETE FROM peer_grants WHERE peer_url = ?").run(url2);
|
|
17383
|
+
return changed;
|
|
17384
|
+
});
|
|
17385
|
+
return tx();
|
|
17386
|
+
}
|
|
17387
|
+
function setPeerEnabled(db, url2, enabled) {
|
|
17388
|
+
return db.query("UPDATE peers SET enabled = ? WHERE url = ?").run(enabled ? 1 : 0, url2).changes > 0;
|
|
17389
|
+
}
|
|
17390
|
+
function setPeerLabel(db, url2, label) {
|
|
17391
|
+
return db.query("UPDATE peers SET label = ? WHERE url = ?").run(label, url2).changes > 0;
|
|
17392
|
+
}
|
|
17393
|
+
function updatePeerStatus(db, url2, status, error51) {
|
|
17394
|
+
db.query("UPDATE peers SET last_sync_at = ?, last_status = ?, last_error = ? WHERE url = ?").run(Date.now(), status, error51 ?? null, url2);
|
|
17395
|
+
}
|
|
17396
|
+
async function syncPeer(db, url2) {
|
|
17397
|
+
try {
|
|
17398
|
+
const result = await syncWithPeer(db, url2);
|
|
17399
|
+
updatePeerStatus(db, url2, "ok", null);
|
|
17400
|
+
return { url: url2, ok: true, pushed: result.pushed, pulled: result.pulled };
|
|
17401
|
+
} catch (e) {
|
|
17402
|
+
const error51 = e.message;
|
|
17403
|
+
updatePeerStatus(db, url2, "error", error51);
|
|
17404
|
+
return { url: url2, ok: false, error: error51 };
|
|
17405
|
+
}
|
|
17406
|
+
}
|
|
17407
|
+
async function syncAllPeers(db) {
|
|
17408
|
+
const peers = listPeers(db).filter((p) => p.enabled);
|
|
17409
|
+
const out = [];
|
|
17410
|
+
for (const p of peers)
|
|
17411
|
+
out.push(await syncPeer(db, p.url));
|
|
17412
|
+
return out;
|
|
17413
|
+
}
|
|
17414
|
+
|
|
17415
|
+
// src/core/sync/pairing.ts
|
|
17416
|
+
var DEFAULT_PAIR_TTL_MS = parseDuration(process.env.METAHUB_PAIR_TTL, 10 * 60000);
|
|
17417
|
+
function generatePairingCode(db, ttlMs = DEFAULT_PAIR_TTL_MS) {
|
|
17418
|
+
const now = Date.now();
|
|
17419
|
+
db.query("DELETE FROM pairing_codes WHERE used = 1 OR exp < ?").run(now);
|
|
17420
|
+
const code = randomSuffix(12);
|
|
17421
|
+
const exp = now + ttlMs;
|
|
17422
|
+
db.query("INSERT INTO pairing_codes (code, exp, used, created_at) VALUES (?, ?, 0, ?)").run(code, exp, now);
|
|
17423
|
+
return { code, exp };
|
|
17424
|
+
}
|
|
17425
|
+
function redeemPairingCode(db, code) {
|
|
17426
|
+
return db.query("UPDATE pairing_codes SET used = 1 WHERE code = ? AND used = 0 AND exp >= ?").run(code, Date.now()).changes > 0;
|
|
17427
|
+
}
|
|
17428
|
+
function mintGrant(db, peerUrl, nodeId) {
|
|
17429
|
+
const token = randomSuffix(32);
|
|
17430
|
+
db.query("INSERT INTO peer_grants (token, peer_url, node_id, created_at) VALUES (?, ?, ?, ?)").run(token, peerUrl, nodeId, Date.now());
|
|
17431
|
+
return token;
|
|
17432
|
+
}
|
|
17433
|
+
function isAcceptedGrant(db, token) {
|
|
17434
|
+
return db.query("SELECT 1 FROM peer_grants WHERE token = ?").get(token) != null;
|
|
17435
|
+
}
|
|
17436
|
+
function listGrants(db) {
|
|
17437
|
+
return db.query("SELECT token, peer_url, node_id, created_at FROM peer_grants ORDER BY created_at DESC").all();
|
|
17438
|
+
}
|
|
17439
|
+
function revokeGrant(db, tokenOrPrefix) {
|
|
17440
|
+
return db.query("DELETE FROM peer_grants WHERE token = ? OR token LIKE ? || '%'").run(tokenOrPrefix, tokenOrPrefix).changes;
|
|
17441
|
+
}
|
|
17442
|
+
function handlePairRequest(db, node, body) {
|
|
17443
|
+
if (!redeemPairingCode(db, body.code)) {
|
|
17444
|
+
throw new Error("invalid or expired pairing code");
|
|
17445
|
+
}
|
|
17446
|
+
if (body.self_url) {
|
|
17447
|
+
addPeer(db, { url: body.self_url, token: body.grant, node_id: body.node_id });
|
|
17448
|
+
}
|
|
17449
|
+
const grant = mintGrant(db, body.self_url ?? null, body.node_id);
|
|
17450
|
+
return { node_id: node, grant };
|
|
17451
|
+
}
|
|
17452
|
+
async function performPairing(db, node, remoteUrl, code, selfUrl) {
|
|
17453
|
+
const ourGrant = mintGrant(db, remoteUrl, null);
|
|
17454
|
+
const reqBody = { code, node_id: node, grant: ourGrant, self_url: selfUrl };
|
|
17455
|
+
const res = await fetch(new URL(PAIR_PATH, remoteUrl), {
|
|
17456
|
+
method: "POST",
|
|
17457
|
+
headers: { "content-type": "application/json" },
|
|
17458
|
+
body: JSON.stringify(reqBody)
|
|
17459
|
+
});
|
|
17460
|
+
if (!res.ok)
|
|
17461
|
+
throw new Error(`pairing failed: ${res.status} ${await res.text()}`);
|
|
17462
|
+
const data = await res.json();
|
|
17463
|
+
addPeer(db, { url: remoteUrl, token: data.grant, node_id: data.node_id });
|
|
17464
|
+
return { node_id: data.node_id, url: remoteUrl };
|
|
17465
|
+
}
|
|
17466
|
+
|
|
17467
|
+
// src/core/sync/peers-routes.ts
|
|
17468
|
+
var PeerSchema = exports_external.object({
|
|
17469
|
+
url: exports_external.string(),
|
|
17470
|
+
pull_cursor: exports_external.number(),
|
|
17471
|
+
push_cursor: exports_external.number(),
|
|
17472
|
+
token: exports_external.string().nullable(),
|
|
17473
|
+
label: exports_external.string().nullable(),
|
|
17474
|
+
node_id: exports_external.string().nullable(),
|
|
17475
|
+
enabled: exports_external.number(),
|
|
17476
|
+
last_sync_at: exports_external.number().nullable(),
|
|
17477
|
+
last_status: exports_external.string().nullable(),
|
|
17478
|
+
last_error: exports_external.string().nullable()
|
|
17479
|
+
});
|
|
17480
|
+
var PairingCodeSchema = exports_external.object({ code: exports_external.string(), exp: exports_external.number() });
|
|
17481
|
+
var AddPeerReq = exports_external.object({
|
|
17482
|
+
url: exports_external.string(),
|
|
17483
|
+
code: exports_external.string(),
|
|
17484
|
+
self_url: exports_external.string().optional()
|
|
17485
|
+
});
|
|
17486
|
+
var AddPeerRes = exports_external.object({ node_id: exports_external.string(), url: exports_external.string() });
|
|
17487
|
+
var UpdatePeerReq = exports_external.object({
|
|
17488
|
+
enabled: exports_external.boolean().optional(),
|
|
17489
|
+
label: exports_external.string().optional()
|
|
17490
|
+
});
|
|
17491
|
+
var PeerSyncRes = exports_external.object({
|
|
17492
|
+
url: exports_external.string(),
|
|
17493
|
+
ok: exports_external.boolean(),
|
|
17494
|
+
pushed: exports_external.number().optional(),
|
|
17495
|
+
pulled: exports_external.number().optional(),
|
|
17496
|
+
error: exports_external.string().optional()
|
|
17497
|
+
});
|
|
17498
|
+
var GrantSchema = exports_external.object({
|
|
17499
|
+
token: exports_external.string(),
|
|
17500
|
+
peer_url: exports_external.string().nullable(),
|
|
17501
|
+
node_id: exports_external.string().nullable(),
|
|
17502
|
+
created_at: exports_external.number().nullable()
|
|
17503
|
+
});
|
|
17504
|
+
var RevokeRes = exports_external.object({ revoked: exports_external.number() });
|
|
17505
|
+
var OkSchema2 = exports_external.object({ ok: exports_external.boolean() });
|
|
17506
|
+
function need3(req, key) {
|
|
17507
|
+
const v = new URL(req.url).searchParams.get(key);
|
|
17508
|
+
if (!v)
|
|
17509
|
+
throw new Error(`missing query param: ${key}`);
|
|
17510
|
+
return v;
|
|
17511
|
+
}
|
|
17512
|
+
function handle3(fn) {
|
|
17513
|
+
return async (req, ctx) => {
|
|
17514
|
+
try {
|
|
17515
|
+
const out = await fn(req, ctx);
|
|
17516
|
+
if (out instanceof Response)
|
|
17517
|
+
return out;
|
|
17518
|
+
return Response.json(out ?? null);
|
|
17519
|
+
} catch (e) {
|
|
17520
|
+
return Response.json({ error: e.message }, { status: 400 });
|
|
17521
|
+
}
|
|
17522
|
+
};
|
|
17523
|
+
}
|
|
17524
|
+
var peersRoutes = [
|
|
17525
|
+
{
|
|
17526
|
+
method: "POST",
|
|
17527
|
+
path: PAIR_PATH,
|
|
17528
|
+
summary: "Pairing handshake: redeem a one-time code and exchange credentials",
|
|
17529
|
+
request: PairRequestSchema,
|
|
17530
|
+
response: PairResponseSchema,
|
|
17531
|
+
handler: handle3(async (req, { db, node }) => {
|
|
17532
|
+
const body = await req.json();
|
|
17533
|
+
return handlePairRequest(db, node, body);
|
|
17534
|
+
})
|
|
17535
|
+
},
|
|
17536
|
+
{
|
|
17537
|
+
method: "POST",
|
|
17538
|
+
path: "/api/pair/new",
|
|
17539
|
+
summary: "Mint a one-time pairing code for another device to redeem",
|
|
17540
|
+
response: PairingCodeSchema,
|
|
17541
|
+
handler: handle3((_req, { db }) => generatePairingCode(db))
|
|
17542
|
+
},
|
|
17543
|
+
{
|
|
17544
|
+
method: "POST",
|
|
17545
|
+
path: "/api/peers/pair",
|
|
17546
|
+
summary: "Pair this server with a remote one using its one-time code",
|
|
17547
|
+
request: AddPeerReq,
|
|
17548
|
+
response: AddPeerRes,
|
|
17549
|
+
handler: handle3(async (req, { db, node }) => {
|
|
17550
|
+
const body = await req.json();
|
|
17551
|
+
return performPairing(db, node, body.url, body.code, body.self_url);
|
|
17552
|
+
})
|
|
17553
|
+
},
|
|
17554
|
+
{
|
|
17555
|
+
method: "GET",
|
|
17556
|
+
path: "/api/peers",
|
|
17557
|
+
summary: "List configured sync peers with status",
|
|
17558
|
+
response: exports_external.array(PeerSchema),
|
|
17559
|
+
handler: handle3((_req, { db }) => listPeers(db))
|
|
17560
|
+
},
|
|
17561
|
+
{
|
|
17562
|
+
method: "PATCH",
|
|
17563
|
+
path: "/api/peer",
|
|
17564
|
+
summary: "Update a peer (enable/disable or label). Query: ?url=<url>",
|
|
17565
|
+
request: UpdatePeerReq,
|
|
17566
|
+
response: OkSchema2,
|
|
17567
|
+
handler: handle3(async (req, { db }) => {
|
|
17568
|
+
const url2 = need3(req, "url");
|
|
17569
|
+
const body = await req.json();
|
|
17570
|
+
if (body.enabled != null)
|
|
17571
|
+
setPeerEnabled(db, url2, body.enabled);
|
|
17572
|
+
if (body.label != null)
|
|
17573
|
+
setPeerLabel(db, url2, body.label);
|
|
17574
|
+
return { ok: true };
|
|
17575
|
+
})
|
|
17576
|
+
},
|
|
17577
|
+
{
|
|
17578
|
+
method: "DELETE",
|
|
17579
|
+
path: "/api/peer",
|
|
17580
|
+
summary: "Remove a sync peer. Query: ?url=<url>",
|
|
17581
|
+
response: OkSchema2,
|
|
17582
|
+
handler: handle3((req, { db }) => ({ ok: removePeer(db, need3(req, "url")) }))
|
|
17583
|
+
},
|
|
17584
|
+
{
|
|
17585
|
+
method: "POST",
|
|
17586
|
+
path: "/api/peer/sync",
|
|
17587
|
+
summary: "Manually sync once with a peer. Query: ?url=<url>",
|
|
17588
|
+
response: PeerSyncRes,
|
|
17589
|
+
handler: handle3((req, { db }) => syncPeer(db, need3(req, "url")))
|
|
17590
|
+
},
|
|
17591
|
+
{
|
|
17592
|
+
method: "GET",
|
|
17593
|
+
path: "/api/grants",
|
|
17594
|
+
summary: "List credentials this server issued and accepts on /sync (inbound)",
|
|
17595
|
+
response: exports_external.array(GrantSchema),
|
|
17596
|
+
handler: handle3((_req, { db }) => listGrants(db))
|
|
17597
|
+
},
|
|
17598
|
+
{
|
|
17599
|
+
method: "DELETE",
|
|
17600
|
+
path: "/api/grant",
|
|
17601
|
+
summary: "Revoke an issued credential by token or prefix. Query: ?token=<token>",
|
|
17602
|
+
response: RevokeRes,
|
|
17603
|
+
handler: handle3((req, { db }) => ({ revoked: revokeGrant(db, need3(req, "token")) }))
|
|
17604
|
+
}
|
|
17605
|
+
];
|
|
17606
|
+
|
|
16940
17607
|
// src/core/sync/routes.ts
|
|
16941
17608
|
var syncRoutes = [
|
|
16942
17609
|
{
|
|
@@ -16962,77 +17629,7 @@ var syncRoutes = [
|
|
|
16962
17629
|
}
|
|
16963
17630
|
}
|
|
16964
17631
|
];
|
|
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
|
-
}
|
|
17632
|
+
var routes = [...syncRoutes, ...webuiRoutes, ...sitesRoutes, ...peersRoutes];
|
|
17036
17633
|
|
|
17037
17634
|
// src/core/sync/openapi.ts
|
|
17038
17635
|
function buildOpenApi(version2) {
|
|
@@ -17101,6 +17698,17 @@ function hasValidToken(req, url2, cfg) {
|
|
|
17101
17698
|
return true;
|
|
17102
17699
|
return extractToken(req, url2) === cur.token;
|
|
17103
17700
|
}
|
|
17701
|
+
function acceptsSyncToken(req, url2, cfg, db) {
|
|
17702
|
+
if (!authActive(cfg))
|
|
17703
|
+
return true;
|
|
17704
|
+
const presented = extractToken(req, url2);
|
|
17705
|
+
if (!presented)
|
|
17706
|
+
return false;
|
|
17707
|
+
const cur = activeToken(cfg);
|
|
17708
|
+
if (cur && presented === cur.token)
|
|
17709
|
+
return true;
|
|
17710
|
+
return isAcceptedGrant(db, presented);
|
|
17711
|
+
}
|
|
17104
17712
|
function renewToken(req, url2, cfg) {
|
|
17105
17713
|
if (cfg.debug || cfg.staticToken != null || !cfg.db)
|
|
17106
17714
|
return null;
|
|
@@ -17228,6 +17836,16 @@ function unauthorized() {
|
|
|
17228
17836
|
}
|
|
17229
17837
|
|
|
17230
17838
|
// src/core/sync/server.ts
|
|
17839
|
+
class PortInUseError extends Error {
|
|
17840
|
+
port;
|
|
17841
|
+
host;
|
|
17842
|
+
constructor(port, host) {
|
|
17843
|
+
super(`port ${port} is already in use \u2014 another mh server may be running. ` + `Stop it, or start on a free port with --port <n>.`);
|
|
17844
|
+
this.port = port;
|
|
17845
|
+
this.host = host;
|
|
17846
|
+
this.name = "PortInUseError";
|
|
17847
|
+
}
|
|
17848
|
+
}
|
|
17231
17849
|
function scalarHtml(specUrl) {
|
|
17232
17850
|
return `<!doctype html><html><head>
|
|
17233
17851
|
<meta charset="utf-8"><title>Metahub API</title>
|
|
@@ -17240,6 +17858,11 @@ function startServer(opts = {}) {
|
|
|
17240
17858
|
const db = openMetahub();
|
|
17241
17859
|
const node = getNodeId(db);
|
|
17242
17860
|
const ctx = { db, node };
|
|
17861
|
+
const cfg = getServerConfig(db);
|
|
17862
|
+
const port = opts.port ?? cfg.port;
|
|
17863
|
+
const host = opts.host ?? cfg.host;
|
|
17864
|
+
const syncIntervalMs = opts.syncIntervalMs ?? cfg.syncIntervalMs;
|
|
17865
|
+
const autoSync = opts.autoSync ?? cfg.autoSync;
|
|
17243
17866
|
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
17867
|
debug: false,
|
|
17245
17868
|
staticToken: null,
|
|
@@ -17247,50 +17870,84 @@ function startServer(opts = {}) {
|
|
|
17247
17870
|
ttlMs: DEFAULT_TTL_MS,
|
|
17248
17871
|
graceMs: DEFAULT_GRACE_MS
|
|
17249
17872
|
};
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17262
|
-
|
|
17263
|
-
|
|
17264
|
-
|
|
17265
|
-
|
|
17266
|
-
|
|
17267
|
-
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17277
|
-
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
17284
|
-
|
|
17873
|
+
let server;
|
|
17874
|
+
try {
|
|
17875
|
+
server = Bun.serve({
|
|
17876
|
+
port,
|
|
17877
|
+
hostname: host,
|
|
17878
|
+
async fetch(req) {
|
|
17879
|
+
const url2 = new URL(req.url);
|
|
17880
|
+
if (url2.pathname === SYNC_PATH) {
|
|
17881
|
+
if (!acceptsSyncToken(req, url2, auth, db))
|
|
17882
|
+
return unauthorized();
|
|
17883
|
+
} else {
|
|
17884
|
+
const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
|
|
17885
|
+
if (!exempt && !hasValidToken(req, url2, auth)) {
|
|
17886
|
+
return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
|
|
17887
|
+
}
|
|
17888
|
+
}
|
|
17889
|
+
if (url2.pathname === RENEW_PATH) {
|
|
17890
|
+
const r = renewToken(req, url2, auth);
|
|
17891
|
+
return r ? Response.json(r) : unauthorized();
|
|
17892
|
+
}
|
|
17893
|
+
if (req.method === "GET" && url2.pathname === "/docs.json") {
|
|
17894
|
+
return Response.json(buildOpenApi(package_default.version));
|
|
17895
|
+
}
|
|
17896
|
+
if (req.method === "GET" && url2.pathname === "/docs") {
|
|
17897
|
+
return withShim(new Response(scalarHtml("/docs.json"), { headers: HTML_HEADERS }), auth);
|
|
17898
|
+
}
|
|
17899
|
+
if (req.method === "GET" && (url2.pathname === "/" || url2.pathname === "/webui.js")) {
|
|
17900
|
+
const { serveWebui: serveWebui2 } = await Promise.resolve().then(() => (init_webui(), exports_webui));
|
|
17901
|
+
const res = await serveWebui2(req);
|
|
17902
|
+
if (res)
|
|
17903
|
+
return withShim(res, auth);
|
|
17904
|
+
}
|
|
17905
|
+
if (req.method === "GET" && url2.pathname.startsWith("/sites/")) {
|
|
17906
|
+
const { serveSite: serveSite2 } = await Promise.resolve().then(() => (init_sites_serve(), exports_sites_serve));
|
|
17907
|
+
const res = await serveSite2(req, ctx);
|
|
17908
|
+
if (res)
|
|
17909
|
+
return withShim(res, auth);
|
|
17910
|
+
}
|
|
17911
|
+
const route = routes.find((r) => r.method === req.method && r.path === url2.pathname);
|
|
17912
|
+
if (route)
|
|
17913
|
+
return route.handler(req, ctx);
|
|
17914
|
+
return new Response("not found", { status: 404 });
|
|
17915
|
+
}
|
|
17916
|
+
});
|
|
17917
|
+
} catch (e) {
|
|
17918
|
+
const code = e?.code;
|
|
17919
|
+
if (code === "EADDRINUSE") {
|
|
17920
|
+
throw new PortInUseError(port, host);
|
|
17285
17921
|
}
|
|
17286
|
-
|
|
17922
|
+
if (code === "EACCES") {
|
|
17923
|
+
throw new Error(`permission denied binding ${host}:${port} \u2014 ports below 1024 need elevated privileges; pick a higher port with --port`);
|
|
17924
|
+
}
|
|
17925
|
+
throw e;
|
|
17926
|
+
}
|
|
17927
|
+
let timer = null;
|
|
17928
|
+
if (autoSync && syncIntervalMs > 0) {
|
|
17929
|
+
timer = setInterval(() => {
|
|
17930
|
+
syncAllPeers(db).catch(() => {});
|
|
17931
|
+
}, syncIntervalMs);
|
|
17932
|
+
timer.unref?.();
|
|
17933
|
+
}
|
|
17287
17934
|
const active = activeToken(auth);
|
|
17935
|
+
const authMode = opts.debug ? "disabled" : opts.token ? "static" : "managed";
|
|
17288
17936
|
return {
|
|
17289
17937
|
server,
|
|
17290
17938
|
node,
|
|
17291
|
-
port: server.port ??
|
|
17939
|
+
port: server.port ?? port,
|
|
17940
|
+
host,
|
|
17292
17941
|
token: active?.token ?? null,
|
|
17293
|
-
exp: active?.exp ?? null
|
|
17942
|
+
exp: active?.exp ?? null,
|
|
17943
|
+
authMode,
|
|
17944
|
+
autoSync: Boolean(autoSync && syncIntervalMs > 0),
|
|
17945
|
+
syncIntervalMs,
|
|
17946
|
+
stop() {
|
|
17947
|
+
if (timer)
|
|
17948
|
+
clearInterval(timer);
|
|
17949
|
+
server.stop();
|
|
17950
|
+
}
|
|
17294
17951
|
};
|
|
17295
17952
|
}
|
|
17296
17953
|
export {
|
|
@@ -17312,6 +17969,7 @@ export {
|
|
|
17312
17969
|
resolveCandidates,
|
|
17313
17970
|
removeProperty,
|
|
17314
17971
|
reconcile,
|
|
17972
|
+
rebuildSearchIndex,
|
|
17315
17973
|
randomSuffix,
|
|
17316
17974
|
putFile,
|
|
17317
17975
|
putBlob,
|
|
@@ -17324,6 +17982,7 @@ export {
|
|
|
17324
17982
|
newId,
|
|
17325
17983
|
moveRecord,
|
|
17326
17984
|
migrateRecords,
|
|
17985
|
+
migratePeers,
|
|
17327
17986
|
metahubHome,
|
|
17328
17987
|
maybeAutoIndex,
|
|
17329
17988
|
makeId,
|
|
@@ -17380,10 +18039,14 @@ export {
|
|
|
17380
18039
|
SyncRequestSchema,
|
|
17381
18040
|
SYNC_PATH,
|
|
17382
18041
|
RENEW_PATH,
|
|
18042
|
+
PortInUseError,
|
|
18043
|
+
PairResponseSchema,
|
|
18044
|
+
PairRequestSchema,
|
|
17383
18045
|
PROP_TYPES,
|
|
18046
|
+
PAIR_PATH,
|
|
17384
18047
|
HealthResponseSchema,
|
|
17385
18048
|
HEALTH_PATH,
|
|
17386
18049
|
ChangeSchema
|
|
17387
18050
|
};
|
|
17388
18051
|
|
|
17389
|
-
//# debugId=
|
|
18052
|
+
//# debugId=539C6C8168968A4864756E2164756E21
|