@promptowl/contextnest-community 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  reject,
17
17
  safePublishDocument,
18
18
  submitForReview
19
- } from "./chunk-IWA2UDAT.js";
19
+ } from "./chunk-MOXICJPD.js";
20
20
  import {
21
21
  checkConflict,
22
22
  createVersion,
@@ -25,7 +25,7 @@ import {
25
25
  getDisplayStatus,
26
26
  getVersions,
27
27
  setApprovedVersion
28
- } from "./chunk-HIH7I232.js";
28
+ } from "./chunk-VD5QX2ZQ.js";
29
29
  import {
30
30
  AppError,
31
31
  ConflictError,
@@ -35,10 +35,10 @@ import {
35
35
  ValidationError,
36
36
  canCreateInNest,
37
37
  canManageStewards,
38
- canManageWith,
39
38
  canUserAccess,
40
39
  canUserApprove,
41
40
  canUserEdit,
41
+ collabPermToRole,
42
42
  createNest,
43
43
  createStewardRecord,
44
44
  deleteNest,
@@ -65,9 +65,11 @@ import {
65
65
  listStewards,
66
66
  loadAccessConfig,
67
67
  nestAllowsSelfApprove,
68
+ nestStorageRoot,
68
69
  permissionLevel,
69
70
  removeSteward,
70
71
  renameNest,
72
+ resolveNestPath,
71
73
  resolveNestPermission,
72
74
  resolveStewardsForNode,
73
75
  resolveStewardsWithFallback,
@@ -82,13 +84,18 @@ import {
82
84
  updateSteward,
83
85
  upsertEnvVar,
84
86
  validateLicense
85
- } from "./chunk-EMOE53KX.js";
87
+ } from "./chunk-43DOX4LH.js";
86
88
  import {
87
- ANON_EMAIL,
88
- ANON_USER_ID,
89
89
  config,
90
- getDb
91
- } from "./chunk-RMU3LOPH.js";
90
+ getDb,
91
+ initDb,
92
+ insertOrIgnore,
93
+ nowExpr
94
+ } from "./chunk-QMLAXQES.js";
95
+ import {
96
+ ANON_EMAIL,
97
+ ANON_USER_ID
98
+ } from "./chunk-SLTQACJW.js";
92
99
 
93
100
  // src/index.ts
94
101
  import { serve } from "@hono/node-server";
@@ -116,37 +123,40 @@ function newSessionId() {
116
123
  function expiryIso(ttlSeconds = SESSION_TTL_SECS) {
117
124
  return new Date(Date.now() + ttlSeconds * 1e3).toISOString();
118
125
  }
119
- function createSession(userId, userAgent) {
126
+ async function createSession(userId, userAgent) {
120
127
  const db = getDb();
121
128
  const id = newSessionId();
122
- db.prepare(
129
+ await db.run(
123
130
  `INSERT INTO sessions (id, user_id, expires_at, user_agent)
124
- VALUES (?, ?, ?, ?)`
125
- ).run(id, userId, expiryIso(), userAgent || null);
131
+ VALUES (?, ?, ?, ?)`,
132
+ [id, userId, expiryIso(), userAgent || null]
133
+ );
126
134
  return id;
127
135
  }
128
- function resolveSession(id) {
136
+ async function resolveSession(id) {
129
137
  const db = getDb();
130
- const row = db.prepare(
131
- `SELECT user_id, expires_at FROM sessions WHERE id = ?`
132
- ).get(id);
138
+ const row = await db.get(
139
+ `SELECT user_id, expires_at FROM sessions WHERE id = ?`,
140
+ [id]
141
+ );
133
142
  if (!row) return null;
134
143
  if (new Date(row.expires_at).getTime() <= Date.now()) {
135
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
144
+ await db.run("DELETE FROM sessions WHERE id = ?", [id]);
136
145
  return null;
137
146
  }
138
- db.prepare(
139
- "UPDATE sessions SET last_seen_at = datetime('now') WHERE id = ?"
140
- ).run(id);
147
+ await db.run(
148
+ `UPDATE sessions SET last_seen_at = ${nowExpr(db)} WHERE id = ?`,
149
+ [id]
150
+ );
141
151
  return row.user_id;
142
152
  }
143
- function deleteSession(id) {
153
+ async function deleteSession(id) {
144
154
  const db = getDb();
145
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
155
+ await db.run("DELETE FROM sessions WHERE id = ?", [id]);
146
156
  }
147
- function deleteAllSessionsForUser(userId) {
157
+ async function deleteAllSessionsForUser(userId) {
148
158
  const db = getDb();
149
- db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
159
+ await db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
150
160
  }
151
161
  function getSessionIdFromRequest(c) {
152
162
  const cookieHeader = c.req.header("Cookie");
@@ -190,7 +200,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
190
200
  const db = getDb();
191
201
  const sessionId = getSessionIdFromRequest(c);
192
202
  if (sessionId) {
193
- const userId = resolveSession(sessionId);
203
+ const userId = await resolveSession(sessionId);
194
204
  if (userId) {
195
205
  c.set("userId", userId);
196
206
  c.set("nestScope", null);
@@ -200,11 +210,15 @@ var authMiddleware = createMiddleware(async (c, next) => {
200
210
  const key = parseBearerToken(c.req.header("Authorization"));
201
211
  if (key) {
202
212
  const keyHash = hashApiKey(key);
203
- const record = db.prepare("SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?").get(keyHash);
213
+ const record = await db.get(
214
+ "SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?",
215
+ [keyHash]
216
+ );
204
217
  if (record) {
205
- db.prepare(
206
- "UPDATE api_keys SET last_used_at = datetime('now') WHERE key_hash = ?"
207
- ).run(keyHash);
218
+ await db.run(
219
+ `UPDATE api_keys SET last_used_at = ${nowExpr(db)} WHERE key_hash = ?`,
220
+ [keyHash]
221
+ );
208
222
  c.set("userId", record.user_id);
209
223
  c.set("nestScope", record.nest_id);
210
224
  return next();
@@ -327,27 +341,30 @@ function clientIp(c) {
327
341
  }
328
342
  return "unknown";
329
343
  }
330
- function resolveCallerUserId(c) {
344
+ async function resolveCallerUserId(c) {
331
345
  const sessionId = getSessionIdFromRequest(c);
332
346
  if (sessionId) {
333
- const uid = resolveSession(sessionId);
347
+ const uid = await resolveSession(sessionId);
334
348
  if (uid) return uid;
335
349
  }
336
350
  const key = parseBearerToken(c.req.header("Authorization"));
337
351
  if (key) {
338
352
  const db = getDb();
339
- const row = db.prepare("SELECT user_id FROM api_keys WHERE key_hash = ?").get(hashApiKey(key));
353
+ const row = await db.get(
354
+ "SELECT user_id FROM api_keys WHERE key_hash = ?",
355
+ [hashApiKey(key)]
356
+ );
340
357
  if (row) return row.user_id;
341
358
  }
342
359
  return null;
343
360
  }
344
- function deviceGateBlocked(c) {
361
+ async function deviceGateBlocked(c) {
345
362
  const gate = config.PROMPTOWL_SIGN_IN_GATE;
346
363
  if (gate === "open") return null;
347
364
  const error = "PromptOwl sign-in is restricted on this server. Use email and password, or contact your admin.";
348
365
  if (gate === "disabled") return { error, gate };
349
- const callerId = resolveCallerUserId(c);
350
- if (callerId && !isLicenseAdminUserId(callerId)) return { error, gate };
366
+ const callerId = await resolveCallerUserId(c);
367
+ if (callerId && !await isLicenseAdminUserId(callerId)) return { error, gate };
351
368
  return null;
352
369
  }
353
370
  function setSessionCookie(c, sessionId) {
@@ -376,13 +393,17 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
376
393
  }
377
394
  const db = getDb();
378
395
  const meEmail = normalizeEmail(rawEmail);
379
- let user = db.prepare("SELECT id, email, name FROM users WHERE LOWER(email) = ?").get(meEmail);
396
+ let user = await db.get(
397
+ "SELECT id, email, name FROM users WHERE LOWER(email) = ?",
398
+ [meEmail]
399
+ );
380
400
  if (!user) {
381
401
  const userId = uuid();
382
402
  const placeholderHash = await hashPassword(uuid());
383
- db.prepare(
384
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
385
- ).run(userId, meEmail, rawName || null, placeholderHash);
403
+ await db.run(
404
+ "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
405
+ [userId, meEmail, rawName || null, placeholderHash]
406
+ );
386
407
  user = { id: userId, email: meEmail, name: rawName || null };
387
408
  trackEvent("user.register", {
388
409
  userId,
@@ -406,7 +427,7 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
406
427
  license_owner_email: lic.ownerEmail
407
428
  };
408
429
  }
409
- const sessionId = createSession(user.id, c.req.header("User-Agent"));
430
+ const sessionId = await createSession(user.id, c.req.header("User-Agent"));
410
431
  setSessionCookie(c, sessionId);
411
432
  return { ok: true, user, isAdmin, claimBlocked };
412
433
  }
@@ -423,25 +444,30 @@ authRoutes.post("/register", async (c) => {
423
444
  return c.json({ error: "Too many registration attempts, try again later" }, 429);
424
445
  }
425
446
  const db = getDb();
426
- const existing = db.prepare("SELECT id, is_invited FROM users WHERE LOWER(email) = ?").get(email);
447
+ const existing = await db.get(
448
+ "SELECT id, is_invited FROM users WHERE LOWER(email) = ?",
449
+ [email]
450
+ );
427
451
  let userId;
428
452
  const passwordHash = await hashPassword(body.password);
429
453
  if (existing && existing.is_invited === 1) {
430
454
  userId = existing.id;
431
- db.prepare(
432
- "UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?"
433
- ).run(passwordHash, body.name || null, userId);
455
+ await db.run(
456
+ "UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?",
457
+ [passwordHash, body.name || null, userId]
458
+ );
434
459
  trackEvent("user.register", { userId, email, claimed: true });
435
460
  } else if (existing) {
436
461
  throw new ValidationError("Email already registered");
437
462
  } else {
438
463
  userId = uuid();
439
- db.prepare(
440
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
441
- ).run(userId, email, body.name || null, passwordHash);
464
+ await db.run(
465
+ "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
466
+ [userId, email, body.name || null, passwordHash]
467
+ );
442
468
  trackEvent("user.register", { userId, email });
443
469
  }
444
- const sessionId = createSession(userId, c.req.header("User-Agent"));
470
+ const sessionId = await createSession(userId, c.req.header("User-Agent"));
445
471
  setSessionCookie(c, sessionId);
446
472
  return c.json(
447
473
  {
@@ -461,7 +487,7 @@ authRoutes.post("/login", async (c) => {
461
487
  throw new ValidationError("email and password are required");
462
488
  }
463
489
  const ip = clientIp(c);
464
- const emailLower = body.email.toLowerCase();
490
+ const emailLower = normalizeEmail(body.email);
465
491
  const hasIp = ip !== "unknown";
466
492
  const ipKey = `login:ip:${ip}`;
467
493
  const emailKey = `login:email:${emailLower}`;
@@ -469,9 +495,10 @@ authRoutes.post("/login", async (c) => {
469
495
  return c.json({ error: "Too many login attempts, try again later" }, 429);
470
496
  }
471
497
  const db = getDb();
472
- const user = db.prepare(
473
- "SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?"
474
- ).get(emailLower);
498
+ const user = await db.get(
499
+ "SELECT id, email, name, password_hash, is_admin, is_invited FROM users WHERE LOWER(email) = ?",
500
+ [emailLower]
501
+ );
475
502
  const check = user ? await verifyPassword(body.password, user.password_hash) : { ok: false, needsRehash: false };
476
503
  if (!user || !check.ok) {
477
504
  if (hasIp) recordFailure(ipKey, LOGIN_LIMIT);
@@ -482,18 +509,24 @@ authRoutes.post("/login", async (c) => {
482
509
  console.log(`[auth] login OK \u2014 counter reset ip=${ip} email=${emailLower}`);
483
510
  if (hasIp) clear(ipKey);
484
511
  clear(emailKey);
512
+ if (user.is_invited === 1) {
513
+ try {
514
+ await db.run("UPDATE users SET is_invited = 0 WHERE id = ?", [user.id]);
515
+ } catch {
516
+ }
517
+ }
485
518
  if (check.needsRehash) {
486
519
  try {
487
520
  const newHash = await hashPassword(body.password);
488
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
521
+ await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
489
522
  newHash,
490
523
  user.id
491
- );
524
+ ]);
492
525
  } catch {
493
526
  }
494
527
  }
495
528
  trackEvent("user.login", { userId: user.id });
496
- const sessionId = createSession(user.id, c.req.header("User-Agent"));
529
+ const sessionId = await createSession(user.id, c.req.header("User-Agent"));
497
530
  setSessionCookie(c, sessionId);
498
531
  return c.json({
499
532
  user: {
@@ -506,7 +539,7 @@ authRoutes.post("/login", async (c) => {
506
539
  });
507
540
  authRoutes.post("/logout", async (c) => {
508
541
  const sessionId = getSessionIdFromRequest(c);
509
- if (sessionId) deleteSession(sessionId);
542
+ if (sessionId) await deleteSession(sessionId);
510
543
  clearSessionCookie(c);
511
544
  return c.json({ ok: true });
512
545
  });
@@ -516,7 +549,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
516
549
  );
517
550
  const db = getDb();
518
551
  const userId = c.get("userId");
519
- const existing = db.prepare("SELECT key_prefix FROM api_keys WHERE user_id = ?").get(userId);
552
+ const existing = await db.get(
553
+ "SELECT key_prefix FROM api_keys WHERE user_id = ?",
554
+ [userId]
555
+ );
520
556
  if (existing) {
521
557
  return c.json(
522
558
  {
@@ -528,15 +564,16 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
528
564
  }
529
565
  const apiKey = generateApiKey();
530
566
  const keyId = uuid();
531
- db.prepare(
532
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
533
- ).run(
534
- keyId,
535
- userId,
536
- hashApiKey(apiKey),
537
- getKeyPrefix(apiKey),
538
- body.nest_id || null,
539
- body.label || null
567
+ await db.run(
568
+ "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
569
+ [
570
+ keyId,
571
+ userId,
572
+ hashApiKey(apiKey),
573
+ getKeyPrefix(apiKey),
574
+ body.nest_id || null,
575
+ body.label || null
576
+ ]
540
577
  );
541
578
  return c.json(
542
579
  { api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
@@ -551,39 +588,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
551
588
  const userId = c.get("userId");
552
589
  const apiKey = generateApiKey();
553
590
  const keyId = uuid();
554
- const prior = db.prepare("SELECT label, nest_id FROM api_keys WHERE user_id = ?").get(userId);
555
- db.transaction(() => {
556
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(userId);
557
- db.prepare(
558
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
559
- ).run(
560
- keyId,
561
- userId,
562
- hashApiKey(apiKey),
563
- getKeyPrefix(apiKey),
564
- body.nest_id ?? prior?.nest_id ?? null,
565
- body.label ?? prior?.label ?? null
591
+ const prior = await db.get(
592
+ "SELECT label, nest_id FROM api_keys WHERE user_id = ?",
593
+ [userId]
594
+ );
595
+ await db.transaction(async (tx) => {
596
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [userId]);
597
+ await tx.run(
598
+ "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
599
+ [
600
+ keyId,
601
+ userId,
602
+ hashApiKey(apiKey),
603
+ getKeyPrefix(apiKey),
604
+ body.nest_id ?? prior?.nest_id ?? null,
605
+ body.label ?? prior?.label ?? null
606
+ ]
566
607
  );
567
- })();
608
+ });
568
609
  return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
569
610
  });
570
611
  authRoutes.get("/keys", authMiddleware, async (c) => {
571
612
  const db = getDb();
572
- const keys = db.prepare(
573
- "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?"
574
- ).all(c.get("userId"));
613
+ const keys = await db.all(
614
+ "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
615
+ [c.get("userId")]
616
+ );
575
617
  return c.json({ keys });
576
618
  });
577
619
  authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
578
620
  const db = getDb();
579
- const result = db.prepare("DELETE FROM api_keys WHERE id = ? AND user_id = ?").run(c.req.param("keyId"), c.get("userId"));
621
+ const result = await db.run(
622
+ "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
623
+ [c.req.param("keyId"), c.get("userId")]
624
+ );
580
625
  if (result.changes === 0) {
581
626
  return c.json({ error: "Key not found" }, 404);
582
627
  }
583
628
  return c.json({ deleted: true });
584
629
  });
585
630
  authRoutes.post("/device", async (c) => {
586
- const blocked = deviceGateBlocked(c);
631
+ const blocked = await deviceGateBlocked(c);
587
632
  if (blocked) return c.json(blocked, 403);
588
633
  if (!tryConsume(`device:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
589
634
  return c.json({ error: "Too many device auth attempts, try again later" }, 429);
@@ -606,7 +651,7 @@ authRoutes.post("/device", async (c) => {
606
651
  return c.json(data);
607
652
  });
608
653
  authRoutes.get("/device/poll", async (c) => {
609
- const blocked = deviceGateBlocked(c);
654
+ const blocked = await deviceGateBlocked(c);
610
655
  if (blocked) return c.json(blocked, 403);
611
656
  const code = c.req.query("code");
612
657
  const clientSecret = c.req.query("client_secret");
@@ -689,12 +734,13 @@ authRoutes.get("/sso", async (c) => {
689
734
  }
690
735
  if (!claims.jti) return ssoError("invalid_ticket");
691
736
  const db = getDb();
692
- db.prepare("DELETE FROM sso_used_jti WHERE expires_at < datetime('now')").run();
737
+ await db.run(`DELETE FROM sso_used_jti WHERE expires_at < ${nowExpr(db)}`);
693
738
  const expiresAtIso = new Date((claims.exp ?? 0) * 1e3).toISOString();
694
739
  try {
695
- db.prepare(
696
- "INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)"
697
- ).run(claims.jti, expiresAtIso);
740
+ await db.run(
741
+ "INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)",
742
+ [claims.jti, expiresAtIso]
743
+ );
698
744
  } catch (err) {
699
745
  if (err?.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
700
746
  return ssoError("ticket_used");
@@ -706,7 +752,7 @@ authRoutes.get("/sso", async (c) => {
706
752
  try {
707
753
  result = await provisionPromptowlUser(c, sub, claims.name);
708
754
  } catch (err) {
709
- db.prepare("DELETE FROM sso_used_jti WHERE jti = ?").run(claims.jti);
755
+ await db.run("DELETE FROM sso_used_jti WHERE jti = ?", [claims.jti]);
710
756
  console.error("[sso] provisioning failed; released jti for retry:", err);
711
757
  return ssoError("service_error");
712
758
  }
@@ -719,13 +765,19 @@ authRoutes.get("/admin-status", async (c) => {
719
765
  const ownerEmail = lic?.valid ? lic.ownerEmail : null;
720
766
  let admin = null;
721
767
  if (ownerEmail) {
722
- const ownerRow = db.prepare("SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1").get(ownerEmail);
768
+ const ownerRow = await db.get(
769
+ "SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1",
770
+ [ownerEmail]
771
+ );
723
772
  admin = { email: ownerEmail, name: ownerRow?.name ?? null };
724
773
  }
725
- const callerId = resolveCallerUserId(c);
774
+ const callerId = await resolveCallerUserId(c);
726
775
  let me = null;
727
776
  if (callerId) {
728
- const row = db.prepare("SELECT email, name FROM users WHERE id = ?").get(callerId);
777
+ const row = await db.get(
778
+ "SELECT email, name FROM users WHERE id = ?",
779
+ [callerId]
780
+ );
729
781
  if (row) {
730
782
  me = {
731
783
  email: row.email,
@@ -748,7 +800,10 @@ authRoutes.post("/password", authMiddleware, async (c) => {
748
800
  assertValidPassword(body.next, "new password");
749
801
  const db = getDb();
750
802
  const userId = c.get("userId");
751
- const user = db.prepare("SELECT password_hash FROM users WHERE id = ?").get(userId);
803
+ const user = await db.get(
804
+ "SELECT password_hash FROM users WHERE id = ?",
805
+ [userId]
806
+ );
752
807
  if (!user) throw new ValidationError("User not found");
753
808
  const check = await verifyPassword(body.current, user.password_hash);
754
809
  if (!check.ok) {
@@ -761,20 +816,20 @@ authRoutes.post("/password", authMiddleware, async (c) => {
761
816
  );
762
817
  }
763
818
  const newHash = await hashPassword(body.next);
764
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
819
+ await db.run("UPDATE users SET password_hash = ?, is_invited = 0 WHERE id = ?", [
765
820
  newHash,
766
821
  userId
767
- );
768
- deleteAllSessionsForUser(userId);
822
+ ]);
823
+ await deleteAllSessionsForUser(userId);
769
824
  clearSessionCookie(c);
770
825
  return c.json({ ok: true });
771
826
  });
772
827
  authRoutes.post("/admin/reset-password/:userId", async (c) => {
773
- const callerId = resolveCallerUserId(c);
828
+ const callerId = await resolveCallerUserId(c);
774
829
  if (!callerId) {
775
830
  return c.json({ error: "Authentication required." }, 401);
776
831
  }
777
- if (!isLicenseAdminUserId(callerId)) {
832
+ if (!await isLicenseAdminUserId(callerId)) {
778
833
  return c.json(
779
834
  { error: "Only the license-admin user can reset passwords." },
780
835
  403
@@ -782,7 +837,10 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
782
837
  }
783
838
  const targetId = c.req.param("userId");
784
839
  const db = getDb();
785
- const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
840
+ const target = await db.get(
841
+ "SELECT id, email FROM users WHERE id = ?",
842
+ [targetId]
843
+ );
786
844
  if (!target) return c.json({ error: "User not found" }, 404);
787
845
  let supplied;
788
846
  try {
@@ -796,29 +854,28 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
796
854
  const generated = supplied ? null : uuid().replace(/-/g, "").slice(0, 16);
797
855
  const newPassword = supplied ?? generated;
798
856
  const newHash = await hashPassword(newPassword);
799
- db.transaction(() => {
800
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
857
+ await db.transaction(async (tx) => {
858
+ await tx.run("UPDATE users SET password_hash = ? WHERE id = ?", [
801
859
  newHash,
802
860
  target.id
803
- );
804
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
805
- })();
806
- deleteAllSessionsForUser(target.id);
861
+ ]);
862
+ });
863
+ await deleteAllSessionsForUser(target.id);
807
864
  trackEvent("admin.reset_password", { adminId: callerId, userId: target.id });
808
865
  return c.json({
809
866
  ok: true,
810
867
  email: target.email,
811
- keys_revoked: true,
868
+ keys_revoked: false,
812
869
  // Plaintext returned ONCE, only when the server generated it.
813
870
  temporary_password: generated ?? void 0
814
871
  });
815
872
  });
816
873
  authRoutes.delete("/users/:userId", async (c) => {
817
- const callerId = resolveCallerUserId(c);
874
+ const callerId = await resolveCallerUserId(c);
818
875
  if (!callerId) {
819
876
  return c.json({ error: "Authentication required." }, 401);
820
877
  }
821
- if (!isLicenseAdminUserId(callerId)) {
878
+ if (!await isLicenseAdminUserId(callerId)) {
822
879
  return c.json(
823
880
  { error: "Only the license-admin user can remove users." },
824
881
  403
@@ -829,9 +886,15 @@ authRoutes.delete("/users/:userId", async (c) => {
829
886
  return c.json({ error: "You can't remove your own admin account." }, 400);
830
887
  }
831
888
  const db = getDb();
832
- const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
889
+ const target = await db.get(
890
+ "SELECT id, email FROM users WHERE id = ?",
891
+ [targetId]
892
+ );
833
893
  if (!target) return c.json({ error: "User not found" }, 404);
834
- const ownedNests = db.prepare("SELECT COUNT(*) AS c FROM nests WHERE user_id = ?").get(target.id).c;
894
+ const ownedNests = (await db.get(
895
+ "SELECT COUNT(*) AS c FROM nests WHERE user_id = ?",
896
+ [target.id]
897
+ )).c;
835
898
  if (ownedNests > 0) {
836
899
  return c.json(
837
900
  {
@@ -841,17 +904,18 @@ authRoutes.delete("/users/:userId", async (c) => {
841
904
  409
842
905
  );
843
906
  }
844
- db.transaction(() => {
845
- db.prepare(
846
- "DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)"
847
- ).run(target.id, target.email);
848
- db.prepare("DELETE FROM nest_collaborators WHERE user_id = ?").run(
849
- target.id
907
+ await db.transaction(async (tx) => {
908
+ await tx.run(
909
+ "DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)",
910
+ [target.id, target.email]
850
911
  );
851
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
852
- db.prepare("DELETE FROM users WHERE id = ?").run(target.id);
853
- })();
854
- deleteAllSessionsForUser(target.id);
912
+ await tx.run("DELETE FROM nest_collaborators WHERE user_id = ?", [
913
+ target.id
914
+ ]);
915
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [target.id]);
916
+ await tx.run("DELETE FROM users WHERE id = ?", [target.id]);
917
+ });
918
+ await deleteAllSessionsForUser(target.id);
855
919
  trackEvent("admin.remove_user", { adminId: callerId, userId: target.id });
856
920
  return c.json({ ok: true, email: target.email });
857
921
  });
@@ -859,7 +923,7 @@ authRoutes.post("/invite", async (c) => {
859
923
  const body = await c.req.json();
860
924
  if (!body.email) throw new ValidationError("email is required");
861
925
  const email = normalizeEmail(body.email);
862
- const callerId = resolveCallerUserId(c);
926
+ const callerId = await resolveCallerUserId(c);
863
927
  if (!callerId) {
864
928
  return c.json(
865
929
  {
@@ -869,7 +933,7 @@ authRoutes.post("/invite", async (c) => {
869
933
  );
870
934
  }
871
935
  const db = getDb();
872
- if (!isLicenseAdminUserId(callerId)) {
936
+ if (!await isLicenseAdminUserId(callerId)) {
873
937
  return c.json(
874
938
  {
875
939
  error: "Only the license-admin user can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
@@ -877,41 +941,46 @@ authRoutes.post("/invite", async (c) => {
877
941
  403
878
942
  );
879
943
  }
880
- let user = db.prepare("SELECT id, email FROM users WHERE LOWER(email) = ?").get(email);
881
- if (!user) {
882
- const userId = uuid();
883
- const placeholderHash = await hashPassword(uuid());
884
- db.prepare(
885
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
886
- ).run(userId, email, null, placeholderHash);
887
- user = { id: userId, email };
944
+ const existing = await db.get(
945
+ "SELECT id, email, is_invited FROM users WHERE LOWER(email) = ?",
946
+ [email]
947
+ );
948
+ if (existing && existing.is_invited === 0) {
949
+ return c.json(
950
+ {
951
+ error: "A user with this email already exists. Use Reset password to issue them a new password."
952
+ },
953
+ 409
954
+ );
888
955
  }
889
- const apiKey = generateApiKey();
890
- const keyId = uuid();
891
- db.transaction(() => {
892
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(user.id);
893
- db.prepare(
894
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
895
- ).run(
896
- keyId,
897
- user.id,
898
- hashApiKey(apiKey),
899
- getKeyPrefix(apiKey),
900
- body.label || "teammate"
956
+ const tempPassword = uuid().replace(/-/g, "").slice(0, 16);
957
+ const passwordHash = await hashPassword(tempPassword);
958
+ let userId;
959
+ if (existing) {
960
+ userId = existing.id;
961
+ await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
962
+ passwordHash,
963
+ userId
964
+ ]);
965
+ } else {
966
+ userId = uuid();
967
+ await db.run(
968
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
969
+ [userId, email, null, passwordHash]
901
970
  );
902
- })();
903
- trackEvent("admin.invite", { adminId: callerId, email: body.email });
971
+ }
972
+ trackEvent("admin.invite", { adminId: callerId, email });
904
973
  return c.json(
905
974
  {
906
- api_key: apiKey,
907
- user: { id: user.id, email: user.email },
908
- message: "Copy this key and share it securely \u2014 it won't be shown again."
975
+ temporary_password: tempPassword,
976
+ user: { id: userId, email },
977
+ message: "Share this temporary password securely. Your teammate signs in with their email + this password, then can change it and create their own API key."
909
978
  },
910
979
  201
911
980
  );
912
981
  });
913
982
  authRoutes.get("/teammates", async (c) => {
914
- const callerId = resolveCallerUserId(c);
983
+ const callerId = await resolveCallerUserId(c);
915
984
  if (!callerId) {
916
985
  return c.json(
917
986
  {
@@ -921,7 +990,7 @@ authRoutes.get("/teammates", async (c) => {
921
990
  );
922
991
  }
923
992
  const db = getDb();
924
- if (!isLicenseAdminUserId(callerId)) {
993
+ if (!await isLicenseAdminUserId(callerId)) {
925
994
  return c.json(
926
995
  {
927
996
  error: "Only the license-admin user can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
@@ -929,15 +998,16 @@ authRoutes.get("/teammates", async (c) => {
929
998
  403
930
999
  );
931
1000
  }
932
- const teammates = db.prepare(
1001
+ const teammates = await db.all(
933
1002
  `SELECT u.id, u.email, u.name, u.is_invited,
934
1003
  (SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
935
1004
  (SELECT MAX(last_used_at) FROM api_keys WHERE user_id = u.id) as last_active
936
1005
  FROM users u
937
1006
  WHERE u.id != ?
938
- ORDER BY u.created_at DESC`
939
- ).all(ANON_USER_ID);
940
- const pendingStewards = db.prepare(
1007
+ ORDER BY u.created_at DESC`,
1008
+ [ANON_USER_ID]
1009
+ );
1010
+ const pendingStewards = await db.all(
941
1011
  `SELECT DISTINCT s.user_email AS email
942
1012
  FROM stewards s
943
1013
  WHERE s.is_active = 1
@@ -952,7 +1022,7 @@ authRoutes.get("/teammates", async (c) => {
952
1022
  )
953
1023
  )
954
1024
  ORDER BY s.user_email`
955
- ).all();
1025
+ );
956
1026
  const enriched = teammates.map((t) => ({ ...t, is_admin: isLicenseAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
957
1027
  return c.json({
958
1028
  teammates: enriched,
@@ -970,52 +1040,70 @@ import { serializeDocument, parseDocument as parseDocument2 } from "@promptowl/c
970
1040
  function normalizeTag(raw) {
971
1041
  return raw.trim().replace(/^#+/, "").toLowerCase();
972
1042
  }
973
- function syncNodeTags(nestId, nodeId, tags) {
1043
+ async function syncNodeTags(nestId, nodeId, tags) {
974
1044
  const db = getDb();
975
1045
  const normalized = Array.from(
976
1046
  new Set(
977
1047
  tags.filter((t) => typeof t === "string").map(normalizeTag).filter(Boolean)
978
1048
  )
979
1049
  );
980
- db.transaction(() => {
981
- db.prepare(
982
- "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?"
983
- ).run(nestId, nodeId);
984
- const insert = db.prepare(
985
- "INSERT OR IGNORE INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1050
+ const insertSql = insertOrIgnore(
1051
+ db,
1052
+ "INSERT INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1053
+ );
1054
+ await db.transaction(async (tx) => {
1055
+ await tx.run(
1056
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1057
+ [nestId, nodeId]
986
1058
  );
987
1059
  for (const tag of normalized) {
988
- insert.run(nestId, nodeId, tag);
1060
+ await tx.run(insertSql, [nestId, nodeId, tag]);
989
1061
  }
990
- })();
1062
+ });
991
1063
  }
992
- function removeNodeFromTagIndex(nestId, nodeId) {
1064
+ async function removeNodeFromTagIndex(nestId, nodeId) {
993
1065
  const db = getDb();
994
- db.prepare(
995
- "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?"
996
- ).run(nestId, nodeId);
1066
+ await db.run(
1067
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1068
+ [nestId, nodeId]
1069
+ );
997
1070
  }
998
1071
 
999
1072
  // src/governance/access-guard.ts
1000
- function resolveCallerEmail(userId) {
1073
+ async function resolveCallerEmail(userId) {
1001
1074
  if (!userId) return "admin@localhost";
1002
1075
  const db = getDb();
1003
- const row = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
1076
+ const row = await db.get(
1077
+ "SELECT email FROM users WHERE id = ?",
1078
+ [userId]
1079
+ );
1004
1080
  return row?.email || "admin@localhost";
1005
1081
  }
1006
- function canReadNode(nestId, nodeId, userId, userEmail) {
1007
- if (isPublicReader(nestId, userId)) {
1008
- return getApprovedVersion(nestId, nodeId) !== null;
1082
+ async function canReadNode(nestId, nodeId, userId, userEmail) {
1083
+ if (await isPublicReader(nestId, userId)) {
1084
+ return await getApprovedVersion(nestId, nodeId) !== null;
1009
1085
  }
1010
- if (!isStewardshipEnabled(nestId)) return true;
1011
- return canUserAccess(nestId, nodeId, userEmail).allowed;
1086
+ if (!await isStewardshipEnabled(nestId)) return true;
1087
+ return (await canUserAccess(nestId, nodeId, userEmail)).allowed;
1012
1088
  }
1013
- function filterAccessible(nestId, userId, userEmail, nodes) {
1014
- if (isPublicReader(nestId, userId)) {
1015
- return nodes.filter((n) => getApprovedVersion(nestId, n.id) !== null);
1089
+ async function filterAccessible(nestId, userId, userEmail, nodes) {
1090
+ if (await isPublicReader(nestId, userId)) {
1091
+ const filtered = [];
1092
+ for (const n of nodes) {
1093
+ if (await getApprovedVersion(nestId, n.id) !== null) {
1094
+ filtered.push(n);
1095
+ }
1096
+ }
1097
+ return filtered;
1016
1098
  }
1017
- if (!isStewardshipEnabled(nestId)) return nodes;
1018
- return nodes.filter((n) => canUserAccess(nestId, n.id, userEmail).allowed);
1099
+ if (!await isStewardshipEnabled(nestId)) return nodes;
1100
+ const accessible = [];
1101
+ for (const n of nodes) {
1102
+ if ((await canUserAccess(nestId, n.id, userEmail)).allowed) {
1103
+ accessible.push(n);
1104
+ }
1105
+ }
1106
+ return accessible;
1019
1107
  }
1020
1108
 
1021
1109
  // src/governance/external-edit-service.ts
@@ -1039,7 +1127,7 @@ var communityRbac = {
1039
1127
  isDocOwner: () => true
1040
1128
  };
1041
1129
  function docPath(nestId, documentId) {
1042
- return join(config.DATA_ROOT, "nests", nestId, `${documentId}.md`);
1130
+ return join(resolveNestPath(nestId), `${documentId}.md`);
1043
1131
  }
1044
1132
  async function readRaw(nestId, documentId) {
1045
1133
  try {
@@ -1063,7 +1151,7 @@ async function loadChainHead(storage, documentId) {
1063
1151
  }
1064
1152
  }
1065
1153
  async function loadLatestApprovedNode(nestId, documentId) {
1066
- const { storage } = engineCache.get(nestId);
1154
+ const { storage } = await engineCache.get(nestId);
1067
1155
  const head = await loadChainHead(storage, documentId);
1068
1156
  if (!head) return null;
1069
1157
  return parseDocument(docPath(nestId, documentId), head.content, documentId);
@@ -1079,7 +1167,7 @@ async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner"
1079
1167
  return res?.meta ?? null;
1080
1168
  }
1081
1169
  async function scanDocumentForDriftInternal(nestId, documentId, actor) {
1082
- const { storage } = engineCache.get(nestId);
1170
+ const { storage } = await engineCache.get(nestId);
1083
1171
  const node = await storage.readDocument(documentId).catch(() => null);
1084
1172
  if (!node) return null;
1085
1173
  const raw = await readRaw(nestId, documentId);
@@ -1106,7 +1194,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
1106
1194
  return { meta: result.meta, created: true };
1107
1195
  }
1108
1196
  async function scanNestForDrift(nestId, actor = "system:scanner") {
1109
- const { storage } = engineCache.get(nestId);
1197
+ const { storage } = await engineCache.get(nestId);
1110
1198
  const docs = await storage.discoverDocuments();
1111
1199
  const results = await Promise.all(
1112
1200
  docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
@@ -1115,7 +1203,7 @@ async function scanNestForDrift(nestId, actor = "system:scanner") {
1115
1203
  return { scanned: docs.length, staged };
1116
1204
  }
1117
1205
  async function getPendingChange(nestId, documentId) {
1118
- const { storage } = engineCache.get(nestId);
1206
+ const { storage } = await engineCache.get(nestId);
1119
1207
  const list = await listSuggestions(storage, documentId);
1120
1208
  for (let i = list.length - 1; i >= 0; i--) {
1121
1209
  const meta = list[i];
@@ -1132,7 +1220,7 @@ async function getPendingChange(nestId, documentId) {
1132
1220
  return null;
1133
1221
  }
1134
1222
  async function listNestExternalEdits(nestId) {
1135
- const { storage } = engineCache.get(nestId);
1223
+ const { storage } = await engineCache.get(nestId);
1136
1224
  const docs = await storage.discoverDocuments();
1137
1225
  const lists = await Promise.all(
1138
1226
  docs.map(async (doc) => {
@@ -1161,7 +1249,7 @@ async function listNestExternalEdits(nestId) {
1161
1249
  return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
1162
1250
  }
1163
1251
  async function getExternalEditDetail(nestId, documentId, suggestionId) {
1164
- const { storage } = engineCache.get(nestId);
1252
+ const { storage } = await engineCache.get(nestId);
1165
1253
  const found = await readSuggestion(storage, documentId, suggestionId);
1166
1254
  if (!found) return null;
1167
1255
  return {
@@ -1178,7 +1266,7 @@ async function getExternalEditDetail(nestId, documentId, suggestionId) {
1178
1266
  };
1179
1267
  }
1180
1268
  async function approveExternalEdit(input) {
1181
- const { storage } = engineCache.get(input.nestId);
1269
+ const { storage } = await engineCache.get(input.nestId);
1182
1270
  let result;
1183
1271
  try {
1184
1272
  result = await approveSuggestion({
@@ -1201,8 +1289,8 @@ async function approveExternalEdit(input) {
1201
1289
  const node = await storage.readDocument(input.documentId);
1202
1290
  const versionNum = result.versionEntry.version;
1203
1291
  const tags = node.frontmatter.tags || [];
1204
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-REGL5CUT.js");
1205
- createVersion2({
1292
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-FJWVTZKR.js");
1293
+ await createVersion2({
1206
1294
  nestId: input.nestId,
1207
1295
  nodeId: input.documentId,
1208
1296
  version: versionNum,
@@ -1212,7 +1300,12 @@ async function approveExternalEdit(input) {
1212
1300
  tags,
1213
1301
  changeNote: input.comment || "External edit approved"
1214
1302
  });
1215
- setApprovedVersion2(input.nestId, input.documentId, versionNum, input.actor);
1303
+ await setApprovedVersion2(
1304
+ input.nestId,
1305
+ input.documentId,
1306
+ versionNum,
1307
+ input.actor
1308
+ );
1216
1309
  } catch (err) {
1217
1310
  console.error(
1218
1311
  `[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
@@ -1222,7 +1315,7 @@ async function approveExternalEdit(input) {
1222
1315
  return result;
1223
1316
  }
1224
1317
  async function rejectExternalEdit(input) {
1225
- const { storage } = engineCache.get(input.nestId);
1318
+ const { storage } = await engineCache.get(input.nestId);
1226
1319
  const result = await rejectSuggestion({
1227
1320
  storage,
1228
1321
  rbac: communityRbac,
@@ -1248,7 +1341,7 @@ async function rejectExternalEdit(input) {
1248
1341
  var scannerTimer = null;
1249
1342
  async function scanAllNests() {
1250
1343
  const db = getDb();
1251
- const rows = db.prepare("SELECT id FROM nests").all();
1344
+ const rows = await db.all("SELECT id FROM nests");
1252
1345
  await Promise.all(
1253
1346
  rows.map(
1254
1347
  ({ id }) => scanNestForDrift(id).catch(
@@ -1268,9 +1361,9 @@ function startDriftScanner(intervalMs = 3e4) {
1268
1361
  }
1269
1362
 
1270
1363
  // src/nodes/service.ts
1271
- function userIdFromEmail(email) {
1364
+ async function userIdFromEmail(email) {
1272
1365
  const db = getDb();
1273
- const row = db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(email);
1366
+ const row = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [email]);
1274
1367
  return row?.id ?? ANON_USER_ID;
1275
1368
  }
1276
1369
  var normalizeTag2 = (t) => t.startsWith("#") ? t : `#${t}`;
@@ -1302,7 +1395,7 @@ function toNodeResponse(node) {
1302
1395
  };
1303
1396
  }
1304
1397
  async function listNodesForCaller(nestId, userId, filters = {}) {
1305
- const { storage, versions: versionManager } = engineCache.get(nestId);
1398
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1306
1399
  let documents = await storage.discoverDocuments();
1307
1400
  if (filters.type) {
1308
1401
  documents = documents.filter((n) => n.frontmatter.type === filters.type);
@@ -1313,14 +1406,14 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1313
1406
  (n) => (n.frontmatter.tags || []).includes(tag)
1314
1407
  );
1315
1408
  }
1316
- const userEmail = resolveCallerEmail(userId);
1317
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
1318
- const publicReader = isPublicReader(nestId, userId);
1409
+ const userEmail = await resolveCallerEmail(userId);
1410
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
1411
+ const publicReader = await isPublicReader(nestId, userId);
1319
1412
  const enriched = await Promise.all(
1320
1413
  accessible.map(async (doc) => {
1321
1414
  const r = toNodeResponse(doc);
1322
1415
  if (publicReader) {
1323
- const approved = getApprovedVersion(nestId, doc.id);
1416
+ const approved = await getApprovedVersion(nestId, doc.id);
1324
1417
  if (approved != null) {
1325
1418
  try {
1326
1419
  const raw = await versionManager.reconstructVersion(doc.id, approved);
@@ -1344,7 +1437,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1344
1437
  r.pendingChange = pending;
1345
1438
  r.status = "external_edit_pending";
1346
1439
  } else {
1347
- r.status = getDisplayStatus(nestId, r.id);
1440
+ r.status = await getDisplayStatus(nestId, r.id);
1348
1441
  }
1349
1442
  return r;
1350
1443
  })
@@ -1352,15 +1445,26 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1352
1445
  return filters.limit ? enriched.slice(0, filters.limit) : enriched;
1353
1446
  }
1354
1447
  async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
1355
- return listNodesForCaller(nestId, userIdFromEmail(userEmail), filters);
1448
+ return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
1356
1449
  }
1357
1450
  async function createNode(nestId, input, userEmail) {
1358
- const { storage, versions: versionManager } = engineCache.get(nestId);
1359
- const slug = input.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1360
- const id = input.id ?? `nodes/${slug}`;
1451
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1452
+ const slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1453
+ const slug = slugify(input.title);
1454
+ let id = input.id;
1455
+ if (!id) {
1456
+ const folderSegments = (input.folder ?? "").split("/").map(slugify).filter(Boolean);
1457
+ if (folderSegments.length > 8) {
1458
+ throw new ValidationError("folder may nest at most 8 levels deep");
1459
+ }
1460
+ if ([...folderSegments, slug].some((seg) => seg.length > 100)) {
1461
+ throw new ValidationError("each folder or title segment must be at most 100 characters");
1462
+ }
1463
+ id = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/${slug}` : `nodes/${slug}`;
1464
+ }
1361
1465
  const now = (/* @__PURE__ */ new Date()).toISOString();
1362
1466
  const tags = (input.tags || []).map(normalizeTag2);
1363
- const hasStewards = isStewardshipEnabled(nestId);
1467
+ const hasStewards = await isStewardshipEnabled(nestId);
1364
1468
  const initialStatus = hasStewards ? "draft" : "published";
1365
1469
  const initialVersion = hasStewards ? 1 : 0;
1366
1470
  let node = {
@@ -1380,7 +1484,7 @@ async function createNode(nestId, input, userEmail) {
1380
1484
  rawContent: ""
1381
1485
  };
1382
1486
  await storage.writeDocument(id, serializeDocument(node));
1383
- syncNodeTags(nestId, id, tags);
1487
+ await syncNodeTags(nestId, id, tags);
1384
1488
  let savedVersion = 1;
1385
1489
  if (hasStewards) {
1386
1490
  try {
@@ -1388,7 +1492,7 @@ async function createNode(nestId, input, userEmail) {
1388
1492
  } catch (err) {
1389
1493
  console.error("VersionManager.createVersion failed (node create)", err);
1390
1494
  }
1391
- createVersion({
1495
+ await createVersion({
1392
1496
  nestId,
1393
1497
  nodeId: id,
1394
1498
  version: 1,
@@ -1404,7 +1508,7 @@ async function createNode(nestId, input, userEmail) {
1404
1508
  note: "Auto-published on create (no stewards configured)"
1405
1509
  });
1406
1510
  savedVersion = result.node.frontmatter.version || 1;
1407
- createVersion({
1511
+ await createVersion({
1408
1512
  nestId,
1409
1513
  nodeId: id,
1410
1514
  version: savedVersion,
@@ -1413,11 +1517,11 @@ async function createNode(nestId, input, userEmail) {
1413
1517
  status: "published",
1414
1518
  tags
1415
1519
  });
1416
- setApprovedVersion(nestId, id, savedVersion, userEmail);
1520
+ await setApprovedVersion(nestId, id, savedVersion, userEmail);
1417
1521
  node = result.node;
1418
1522
  } catch (err) {
1419
1523
  console.error("publishDocument failed (node create auto-publish)", err);
1420
- createVersion({
1524
+ await createVersion({
1421
1525
  nestId,
1422
1526
  nodeId: id,
1423
1527
  version: 1,
@@ -1428,11 +1532,11 @@ async function createNode(nestId, input, userEmail) {
1428
1532
  });
1429
1533
  }
1430
1534
  }
1431
- trackEvent("node.create", { nestId, nodeId: id });
1535
+ await trackEvent("node.create", { nestId, nodeId: id });
1432
1536
  return { node, version: savedVersion };
1433
1537
  }
1434
1538
  async function registerImportedDocuments(nestId, userEmail) {
1435
- const { storage } = engineCache.get(nestId);
1539
+ const { storage } = await engineCache.get(nestId);
1436
1540
  let docs;
1437
1541
  try {
1438
1542
  docs = await storage.discoverDocuments();
@@ -1443,7 +1547,7 @@ async function registerImportedDocuments(nestId, userEmail) {
1443
1547
  let registered = 0;
1444
1548
  for (const doc of docs) {
1445
1549
  const nodeId = doc.id;
1446
- if (getCurrentVersion(nestId, nodeId) > 0) continue;
1550
+ if (await getCurrentVersion(nestId, nodeId) > 0) continue;
1447
1551
  const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
1448
1552
  const tags = rawTags.map((t) => normalizeTag2(String(t)));
1449
1553
  const fmVersion = Number(doc.frontmatter?.version);
@@ -1459,7 +1563,7 @@ async function registerImportedDocuments(nestId, userEmail) {
1459
1563
  } catch (err) {
1460
1564
  console.error("safePublishDocument failed (import register)", nodeId, err);
1461
1565
  }
1462
- createVersion({
1566
+ await createVersion({
1463
1567
  nestId,
1464
1568
  nodeId,
1465
1569
  version,
@@ -1469,15 +1573,15 @@ async function registerImportedDocuments(nestId, userEmail) {
1469
1573
  changeNote: "Imported from existing folder",
1470
1574
  tags
1471
1575
  });
1472
- setApprovedVersion(nestId, nodeId, version, userEmail);
1473
- syncNodeTags(nestId, nodeId, tags);
1576
+ await setApprovedVersion(nestId, nodeId, version, userEmail);
1577
+ await syncNodeTags(nestId, nodeId, tags);
1474
1578
  registered++;
1475
1579
  }
1476
- trackEvent("nest.import.documents", { nestId, registered });
1580
+ await trackEvent("nest.import.documents", { nestId, registered });
1477
1581
  return registered;
1478
1582
  }
1479
1583
  async function updateNode(nestId, nodeId, patch, userEmail) {
1480
- const { storage, versions: versionManager } = engineCache.get(nestId);
1584
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1481
1585
  let node;
1482
1586
  try {
1483
1587
  node = await storage.readDocument(nodeId);
@@ -1501,16 +1605,16 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1501
1605
  if (patch.title) {
1502
1606
  node = { ...node, frontmatter: { ...node.frontmatter, title: patch.title } };
1503
1607
  }
1504
- const hasStewards = isStewardshipEnabled(nestId);
1608
+ const hasStewards = await isStewardshipEnabled(nestId);
1505
1609
  const currentTags = node.frontmatter.tags || [];
1506
- if (hasStewards && getPendingReview(nestId, nodeId)) {
1610
+ if (hasStewards && await getPendingReview(nestId, nodeId)) {
1507
1611
  throw new LockedError(
1508
1612
  "This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
1509
1613
  );
1510
1614
  }
1511
1615
  let responseVersion;
1512
1616
  if (hasStewards) {
1513
- const currentVersion = getCurrentVersion(nestId, nodeId);
1617
+ const currentVersion = await getCurrentVersion(nestId, nodeId);
1514
1618
  const newVersion = currentVersion + 1;
1515
1619
  node = {
1516
1620
  ...node,
@@ -1525,13 +1629,13 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1525
1629
  };
1526
1630
  node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
1527
1631
  await storage.writeDocument(nodeId, serializeDocument(node));
1528
- syncNodeTags(nestId, nodeId, currentTags);
1632
+ await syncNodeTags(nestId, nodeId, currentTags);
1529
1633
  try {
1530
1634
  await versionManager.createVersion(node, userEmail, { note: patch.changeNote });
1531
1635
  } catch (err) {
1532
1636
  console.error("VersionManager.createVersion failed (node patch)", err);
1533
1637
  }
1534
- createVersion({
1638
+ await createVersion({
1535
1639
  nestId,
1536
1640
  nodeId,
1537
1641
  version: newVersion,
@@ -1553,7 +1657,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1553
1657
  };
1554
1658
  node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
1555
1659
  await storage.writeDocument(nodeId, serializeDocument(node));
1556
- syncNodeTags(nestId, nodeId, currentTags);
1660
+ await syncNodeTags(nestId, nodeId, currentTags);
1557
1661
  let publishedVersion = (node.frontmatter.version || 0) + 1;
1558
1662
  try {
1559
1663
  const result = await safePublishDocument(storage, nodeId, {
@@ -1565,7 +1669,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1565
1669
  } catch (err) {
1566
1670
  console.error("publishDocument failed (node patch auto-publish)", err);
1567
1671
  }
1568
- createVersion({
1672
+ await createVersion({
1569
1673
  nestId,
1570
1674
  nodeId,
1571
1675
  version: publishedVersion,
@@ -1575,7 +1679,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1575
1679
  tags: currentTags,
1576
1680
  changeNote: patch.changeNote
1577
1681
  });
1578
- setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
1682
+ await setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
1579
1683
  responseVersion = publishedVersion;
1580
1684
  }
1581
1685
  return { node, version: responseVersion };
@@ -1583,8 +1687,11 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1583
1687
 
1584
1688
  // src/nests/unsynced-service.ts
1585
1689
  import { readdirSync, readFileSync, rmSync, statSync } from "fs";
1586
- import { join as join2, relative } from "path";
1690
+ import { join as join2, relative, resolve } from "path";
1587
1691
  var RESERVED = /* @__PURE__ */ new Set(["nests"]);
1692
+ function isNestStorageRoot(absPath) {
1693
+ return resolve(absPath) === resolve(nestStorageRoot());
1694
+ }
1588
1695
  function scanMarkdown(dir) {
1589
1696
  let count = 0;
1590
1697
  let size = 0;
@@ -1664,7 +1771,9 @@ function listUnsyncedFolders() {
1664
1771
  if (!e.isDirectory()) continue;
1665
1772
  if (e.name.startsWith(".")) continue;
1666
1773
  if (RESERVED.has(e.name)) continue;
1667
- out.push(...collectLeafFolders(e.name, join2(root, e.name)));
1774
+ const abs = join2(root, e.name);
1775
+ if (isNestStorageRoot(abs)) continue;
1776
+ out.push(...collectLeafFolders(e.name, abs));
1668
1777
  }
1669
1778
  out.sort((a, b) => a.name.localeCompare(b.name));
1670
1779
  console.log(
@@ -1718,6 +1827,9 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1718
1827
  );
1719
1828
  assertSafeFolderName(folderName);
1720
1829
  const src = join2(config.DATA_ROOT, folderName);
1830
+ if (isNestStorageRoot(src)) {
1831
+ throw new ValidationError("Folder is not eligible for sync");
1832
+ }
1721
1833
  let stat;
1722
1834
  try {
1723
1835
  stat = statSync(src);
@@ -1737,7 +1849,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1737
1849
  }
1738
1850
  const segments = folderName.split("/").filter(Boolean);
1739
1851
  const baseName = segments[segments.length - 1] || folderName;
1740
- const nestName = uniqueNestName(userId, baseName);
1852
+ const nestName = await uniqueNestName(userId, baseName);
1741
1853
  if (nestName !== baseName) {
1742
1854
  console.log(
1743
1855
  `[unsynced] name "${baseName}" already in use, using "${nestName}" instead`
@@ -1761,10 +1873,13 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1761
1873
  }
1762
1874
 
1763
1875
  // src/nests/routes.ts
1764
- function effectivePermission(nestId, userId) {
1876
+ async function effectivePermission(nestId, userId) {
1765
1877
  if (config.AUTH_MODE === "open") {
1766
1878
  const db = getDb();
1767
- const nest = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
1879
+ const nest = await db.get(
1880
+ "SELECT user_id FROM nests WHERE id = ?",
1881
+ [nestId]
1882
+ );
1768
1883
  if (nest && nest.user_id === ANON_USER_ID) return "owner";
1769
1884
  }
1770
1885
  return resolveNestPermission(nestId, userId);
@@ -1772,21 +1887,30 @@ function effectivePermission(nestId, userId) {
1772
1887
  var nestRoutes = new Hono2();
1773
1888
  nestRoutes.get("/", async (c) => {
1774
1889
  const userId = c.get("userId");
1775
- const owned = listNests(userId);
1776
- const shared = listSharedNests(userId);
1777
- const publicExtras = listPublicNests(userId);
1890
+ const owned = await listNests(userId);
1891
+ const shared = await listSharedNests(userId);
1892
+ const publicExtras = await listPublicNests(userId);
1778
1893
  const db = getDb();
1779
- const ownerEmailStmt = db.prepare(
1780
- "SELECT email FROM users WHERE id = ?"
1781
- );
1782
- const callerEmail = resolveCallerEmail(userId);
1783
- const annotate = (n) => {
1784
- const permission = effectivePermission(n.id, userId);
1894
+ const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
1895
+ const callerEmail = await resolveCallerEmail(userId);
1896
+ const annotate = async (n) => {
1897
+ const permission = await effectivePermission(n.id, userId);
1785
1898
  const is_owner = permission === "owner";
1786
1899
  let owner_email = null;
1787
- const roles = is_owner ? ["owner"] : resolveUserRoles(n.id, callerEmail);
1900
+ let roles;
1901
+ if (is_owner) {
1902
+ roles = ["owner"];
1903
+ } else {
1904
+ const grantRole = collabPermToRole(
1905
+ await getCollaboratorRole(n.id, callerEmail)
1906
+ );
1907
+ const stewardRoles = await getStewardRolesForUser(n.id, callerEmail);
1908
+ roles = [
1909
+ ...new Set([grantRole, ...stewardRoles].filter(Boolean))
1910
+ ];
1911
+ }
1788
1912
  if (!is_owner && n.user_id !== ANON_USER_ID) {
1789
- const row = ownerEmailStmt.get(n.user_id);
1913
+ const row = await db.get(ownerEmailSql, [n.user_id]);
1790
1914
  owner_email = row?.email ?? null;
1791
1915
  }
1792
1916
  return { ...n, permission, is_owner, owner_email, roles };
@@ -1796,9 +1920,27 @@ nestRoutes.get("/", async (c) => {
1796
1920
  for (const n of [...owned, ...shared, ...publicExtras]) {
1797
1921
  if (seen.has(n.id)) continue;
1798
1922
  seen.add(n.id);
1799
- out.push(annotate(n));
1923
+ out.push(await annotate(n));
1800
1924
  }
1801
- return c.json({ nests: out });
1925
+ const docRows = await db.all(
1926
+ "SELECT nest_id, COUNT(DISTINCT node_id) AS c FROM node_versions GROUP BY nest_id"
1927
+ );
1928
+ const docByNest = new Map(docRows.map((r) => [r.nest_id, r.c]));
1929
+ const collabRows = await db.all(
1930
+ "SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
1931
+ );
1932
+ const collabByNest = new Map(collabRows.map((r) => [r.nest_id, r.c]));
1933
+ const stewardRows = await db.all(
1934
+ "SELECT nest_id, COUNT(DISTINCT user_email) AS c FROM stewards WHERE is_active = 1 GROUP BY nest_id"
1935
+ );
1936
+ const stewardByNest = new Map(stewardRows.map((r) => [r.nest_id, r.c]));
1937
+ const withMeta = out.map((n) => ({
1938
+ ...n,
1939
+ document_count: docByNest.get(n.id) ?? 0,
1940
+ collaborator_count: collabByNest.get(n.id) ?? 0,
1941
+ steward_count: stewardByNest.get(n.id) ?? 0
1942
+ }));
1943
+ return c.json({ nests: withMeta });
1802
1944
  });
1803
1945
  nestRoutes.post("/", async (c) => {
1804
1946
  const body = await c.req.json();
@@ -1818,20 +1960,20 @@ nestRoutes.post("/import", async (c) => {
1818
1960
  const nest = await importNest(userId, body.name, files);
1819
1961
  const documents = await registerImportedDocuments(
1820
1962
  nest.id,
1821
- resolveCallerEmail(userId)
1963
+ await resolveCallerEmail(userId)
1822
1964
  );
1823
1965
  return c.json({ nest, documents }, 201);
1824
1966
  });
1825
1967
  nestRoutes.get("/unsynced", async (c) => {
1826
1968
  const userId = c.get("userId");
1827
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
1969
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
1828
1970
  throw new ForbiddenError("Only the server admin can list unsynced folders");
1829
1971
  }
1830
1972
  return c.json({ folders: listUnsyncedFolders() });
1831
1973
  });
1832
1974
  nestRoutes.post("/unsynced/sync", async (c) => {
1833
1975
  const userId = c.get("userId");
1834
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
1976
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
1835
1977
  throw new ForbiddenError("Only the server admin can sync folders");
1836
1978
  }
1837
1979
  const body = await c.req.json();
@@ -1841,30 +1983,30 @@ nestRoutes.post("/unsynced/sync", async (c) => {
1841
1983
  const result = await syncUnsyncedFolder(
1842
1984
  userId,
1843
1985
  body.name,
1844
- resolveCallerEmail(userId)
1986
+ await resolveCallerEmail(userId)
1845
1987
  );
1846
1988
  return c.json(result, 201);
1847
1989
  });
1848
1990
  nestRoutes.get("/:nestId", async (c) => {
1849
1991
  const nestId = c.req.param("nestId");
1850
1992
  const userId = c.get("userId");
1851
- const permission = effectivePermission(nestId, userId);
1993
+ const permission = await effectivePermission(nestId, userId);
1852
1994
  if (permission === "none") {
1853
1995
  throw new NotFoundError("Nest not found");
1854
1996
  }
1855
- const email = resolveCallerEmail(userId);
1856
- let roles = resolveUserRoles(nestId, email);
1997
+ const email = await resolveCallerEmail(userId);
1998
+ let roles = await resolveUserRoles(nestId, email);
1857
1999
  if (permission === "owner" && !roles.includes("owner")) {
1858
2000
  roles = ["owner", ...roles];
1859
2001
  }
1860
- const myStewards = getStewardsForUser(nestId, email);
1861
- const nest = getNest(nestId);
2002
+ const myStewards = await getStewardsForUser(nestId, email);
2003
+ const nest = await getNest(nestId);
1862
2004
  return c.json({ nest, permission, roles, myStewards });
1863
2005
  });
1864
2006
  nestRoutes.patch("/:nestId", async (c) => {
1865
2007
  const nestId = c.req.param("nestId");
1866
2008
  const userId = c.get("userId");
1867
- const permission = effectivePermission(nestId, userId);
2009
+ const permission = await effectivePermission(nestId, userId);
1868
2010
  if (permission === "none") {
1869
2011
  throw new NotFoundError("Nest not found");
1870
2012
  }
@@ -1874,7 +2016,7 @@ nestRoutes.patch("/:nestId", async (c) => {
1874
2016
  );
1875
2017
  }
1876
2018
  const body = await c.req.json();
1877
- const nest = renameNest(nestId, {
2019
+ const nest = await renameNest(nestId, {
1878
2020
  name: body.name,
1879
2021
  description: body.description
1880
2022
  });
@@ -1883,13 +2025,13 @@ nestRoutes.patch("/:nestId", async (c) => {
1883
2025
  nestRoutes.delete("/:nestId", async (c) => {
1884
2026
  const nestId = c.req.param("nestId");
1885
2027
  const userId = c.get("userId");
1886
- const nest = getNest(nestId);
2028
+ const nest = await getNest(nestId);
1887
2029
  if (!nest) {
1888
2030
  throw new NotFoundError("Nest not found");
1889
2031
  }
1890
- const permission = effectivePermission(nestId, userId);
2032
+ const permission = await effectivePermission(nestId, userId);
1891
2033
  const isAnonOwned = nest.user_id === ANON_USER_ID;
1892
- const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && isLicenseAdminUserId(userId);
2034
+ const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && await isLicenseAdminUserId(userId);
1893
2035
  if (permission !== "owner" && !adminCaretaker) {
1894
2036
  throw new ForbiddenError(
1895
2037
  "You don't have permission to delete this nest. Only the nest owner can delete it."
@@ -1900,24 +2042,24 @@ nestRoutes.delete("/:nestId", async (c) => {
1900
2042
  });
1901
2043
  nestRoutes.get("/:nestId/settings", async (c) => {
1902
2044
  const nestId = c.req.param("nestId");
1903
- const permission = effectivePermission(nestId, c.get("userId"));
2045
+ const permission = await effectivePermission(nestId, c.get("userId"));
1904
2046
  if (permission === "none") {
1905
2047
  throw new NotFoundError("Nest not found");
1906
2048
  }
1907
2049
  return c.json({
1908
- stewardship_enabled: isStewardshipEnabled(nestId),
1909
- allow_self_approve: nestAllowsSelfApprove(nestId)
2050
+ stewardship_enabled: await isStewardshipEnabled(nestId),
2051
+ allow_self_approve: await nestAllowsSelfApprove(nestId)
1910
2052
  });
1911
2053
  });
1912
2054
  nestRoutes.patch("/:nestId/settings", async (c) => {
1913
2055
  const nestId = c.req.param("nestId");
1914
2056
  const userId = c.get("userId");
1915
- const isServerAdmin = isLicenseAdminUserId(userId);
1916
- const permission = effectivePermission(nestId, userId);
1917
- if (!isServerAdmin && permission !== "owner") {
2057
+ const isServerAdmin = await isLicenseAdminUserId(userId);
2058
+ const permission = await effectivePermission(nestId, userId);
2059
+ if (!isServerAdmin && permission !== "owner" && permission !== "admin") {
1918
2060
  return c.json(
1919
2061
  {
1920
- error: "Only the nest owner or the server license-admin can update nest settings."
2062
+ error: "Only a nest admin, the nest owner, or the server license-admin can update nest settings."
1921
2063
  },
1922
2064
  403
1923
2065
  );
@@ -1926,17 +2068,25 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
1926
2068
  let wiped = null;
1927
2069
  if (typeof body.stewardship_enabled === "boolean") {
1928
2070
  if (body.stewardship_enabled) {
1929
- setStewardshipEnabled(nestId, true);
2071
+ await setStewardshipEnabled(nestId, true);
1930
2072
  } else {
1931
- wiped = disableStewardshipAndWipeGovernance(nestId);
2073
+ if (!isServerAdmin && permission !== "owner") {
2074
+ return c.json(
2075
+ {
2076
+ error: "Only the nest owner or the server license-admin can disable stewardship (this permanently wipes stewards and pending reviews)."
2077
+ },
2078
+ 403
2079
+ );
2080
+ }
2081
+ wiped = await disableStewardshipAndWipeGovernance(nestId);
1932
2082
  }
1933
2083
  }
1934
2084
  if (typeof body.allow_self_approve === "boolean") {
1935
- setAllowSelfApprove(nestId, body.allow_self_approve);
2085
+ await setAllowSelfApprove(nestId, body.allow_self_approve);
1936
2086
  }
1937
2087
  return c.json({
1938
- stewardship_enabled: isStewardshipEnabled(nestId),
1939
- allow_self_approve: nestAllowsSelfApprove(nestId),
2088
+ stewardship_enabled: await isStewardshipEnabled(nestId),
2089
+ allow_self_approve: await nestAllowsSelfApprove(nestId),
1940
2090
  wiped
1941
2091
  });
1942
2092
  });
@@ -1961,21 +2111,28 @@ async function addCollaborator(params) {
1961
2111
  let userId = params.userId;
1962
2112
  if (!userId && params.email) {
1963
2113
  const email = normalizeEmail(params.email);
1964
- const existing = db.prepare("SELECT id FROM users WHERE LOWER(email) = ?").get(email);
2114
+ const existing = await db.get(
2115
+ "SELECT id FROM users WHERE LOWER(email) = ?",
2116
+ [email]
2117
+ );
1965
2118
  if (existing) {
1966
2119
  userId = existing.id;
1967
2120
  } else {
1968
2121
  const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
1969
2122
  userId = uuid2();
1970
- db.prepare(
1971
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
1972
- ).run(userId, email, null, await hashPassword2(uuid2()));
2123
+ await db.run(
2124
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
2125
+ [userId, email, null, await hashPassword2(uuid2())]
2126
+ );
1973
2127
  }
1974
2128
  }
1975
2129
  if (!userId) {
1976
2130
  throw new ValidationError("user_id or email is required");
1977
2131
  }
1978
- const ownerRow = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
2132
+ const ownerRow = await db.get(
2133
+ "SELECT user_id FROM nests WHERE id = ?",
2134
+ [nestId]
2135
+ );
1979
2136
  if (!ownerRow) {
1980
2137
  throw new ValidationError("Nest not found");
1981
2138
  }
@@ -1989,19 +2146,29 @@ async function addCollaborator(params) {
1989
2146
  if (selfByEmail || selfById) {
1990
2147
  throw new ValidationError("You can't add yourself as a collaborator.");
1991
2148
  }
1992
- const dupe = db.prepare("SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?").get(nestId, userId);
2149
+ const dupe = await db.get(
2150
+ "SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
2151
+ [nestId, userId]
2152
+ );
1993
2153
  if (dupe) {
1994
2154
  throw new ConflictError(
1995
2155
  `${params.email || "This user"} already has access to this nest. Change their permission instead of adding them again.`
1996
2156
  );
1997
2157
  }
1998
- const granterByEmail = !params.grantedByUserId && params.grantedByEmail ? db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(params.grantedByEmail)?.id : void 0;
2158
+ const granterByEmail = !params.grantedByUserId && params.grantedByEmail ? (await db.get(
2159
+ "SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
2160
+ [params.grantedByEmail]
2161
+ ))?.id : void 0;
1999
2162
  const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
2000
2163
  const collabId = uuid2();
2001
- db.prepare(
2002
- "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)"
2003
- ).run(collabId, nestId, userId, params.permission, granterId);
2004
- return db.prepare("SELECT * FROM nest_collaborators WHERE id = ?").get(collabId);
2164
+ await db.run(
2165
+ "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
2166
+ [collabId, nestId, userId, params.permission, granterId]
2167
+ );
2168
+ return await db.get(
2169
+ "SELECT * FROM nest_collaborators WHERE id = ?",
2170
+ [collabId]
2171
+ );
2005
2172
  }
2006
2173
 
2007
2174
  // src/nests/sharing-routes.ts
@@ -2009,20 +2176,25 @@ var sharingRoutes = new Hono3();
2009
2176
  sharingRoutes.get("/collaborators", async (c) => {
2010
2177
  const db = getDb();
2011
2178
  const nestId = c.req.param("nestId");
2012
- const collabs = db.prepare(
2179
+ const collabs = await db.all(
2013
2180
  `SELECT nc.*, u.email FROM nest_collaborators nc
2014
2181
  LEFT JOIN users u ON nc.user_id = u.id
2015
2182
  WHERE nc.nest_id = ?
2016
- ORDER BY nc.granted_at`
2017
- ).all(nestId);
2018
- const enriched = collabs.map((collab) => {
2019
- if (!collab.email) return { ...collab, stewardRoles: [], roles: [] };
2020
- return {
2183
+ ORDER BY nc.granted_at`,
2184
+ [nestId]
2185
+ );
2186
+ const enriched = [];
2187
+ for (const collab of collabs) {
2188
+ if (!collab.email) {
2189
+ enriched.push({ ...collab, stewardRoles: [], roles: [] });
2190
+ continue;
2191
+ }
2192
+ enriched.push({
2021
2193
  ...collab,
2022
- stewardRoles: getStewardRolesForUser(nestId, collab.email),
2023
- roles: resolveUserRoles(nestId, collab.email)
2024
- };
2025
- });
2194
+ stewardRoles: await getStewardRolesForUser(nestId, collab.email),
2195
+ roles: await resolveUserRoles(nestId, collab.email)
2196
+ });
2197
+ }
2026
2198
  return c.json({ collaborators: enriched });
2027
2199
  });
2028
2200
  sharingRoutes.post("/collaborators", async (c) => {
@@ -2045,9 +2217,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
2045
2217
  throw new ValidationError("permission must be read, write, or admin");
2046
2218
  }
2047
2219
  const db = getDb();
2048
- const info = db.prepare(
2049
- "UPDATE nest_collaborators SET permission = ? WHERE id = ? AND nest_id = ?"
2050
- ).run(body.permission, c.req.param("collabId"), c.req.param("nestId"));
2220
+ const info = await db.run(
2221
+ "UPDATE nest_collaborators SET permission = ? WHERE id = ? AND nest_id = ?",
2222
+ [body.permission, c.req.param("collabId"), c.req.param("nestId")]
2223
+ );
2051
2224
  if (info.changes === 0) {
2052
2225
  throw new NotFoundError("Collaborator not found");
2053
2226
  }
@@ -2055,9 +2228,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
2055
2228
  });
2056
2229
  sharingRoutes.delete("/collaborators/:collabId", async (c) => {
2057
2230
  const db = getDb();
2058
- const info = db.prepare(
2059
- "DELETE FROM nest_collaborators WHERE id = ? AND nest_id = ?"
2060
- ).run(c.req.param("collabId"), c.req.param("nestId"));
2231
+ const info = await db.run(
2232
+ "DELETE FROM nest_collaborators WHERE id = ? AND nest_id = ?",
2233
+ [c.req.param("collabId"), c.req.param("nestId")]
2234
+ );
2061
2235
  if (info.changes === 0) {
2062
2236
  throw new NotFoundError("Collaborator not found");
2063
2237
  }
@@ -2069,10 +2243,10 @@ sharingRoutes.patch("/visibility", async (c) => {
2069
2243
  throw new ValidationError("visibility must be private or public");
2070
2244
  }
2071
2245
  const db = getDb();
2072
- db.prepare("UPDATE nests SET visibility = ? WHERE id = ?").run(
2246
+ await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
2073
2247
  body.visibility,
2074
2248
  c.req.param("nestId")
2075
- );
2249
+ ]);
2076
2250
  return c.json({ visibility: body.visibility });
2077
2251
  });
2078
2252
 
@@ -2099,372 +2273,74 @@ function isMarkdownFormat(c) {
2099
2273
  return c.req.query("format") === "markdown";
2100
2274
  }
2101
2275
 
2102
- // src/nodes/routes.ts
2103
- var nodeRoutes = new Hono4();
2104
- function nodeAsMarkdown(response, nodeId) {
2105
- return nodeToMarkdown({
2106
- id: nodeId,
2107
- title: response.title,
2108
- tags: response.tags,
2109
- status: response.status,
2110
- body: response.content
2111
- });
2276
+ // src/annotations/service.ts
2277
+ import { v4 as uuid3 } from "uuid";
2278
+
2279
+ // src/annotations/projection.ts
2280
+ var MAX_CONTEXT_CHARS = 250;
2281
+ var MAX_QUOTE_CHARS = 1e3;
2282
+ function clampAnchor(raw) {
2283
+ if (!raw) return null;
2284
+ const quote = (raw.quote ?? "").trim().slice(0, MAX_QUOTE_CHARS);
2285
+ if (!quote) return null;
2286
+ const before = (raw.before ?? "").slice(-MAX_CONTEXT_CHARS);
2287
+ const after = (raw.after ?? "").slice(0, MAX_CONTEXT_CHARS);
2288
+ const line = typeof raw.line === "number" && Number.isFinite(raw.line) && raw.line > 0 ? Math.floor(raw.line) : void 0;
2289
+ return { quote, before, after, ...line !== void 0 ? { line } : {} };
2112
2290
  }
2113
- nodeRoutes.get("/", async (c) => {
2114
- const nestId = c.req.param("nestId");
2115
- const userId = c.get("userId");
2116
- const nodes = await listNodesForCaller(nestId, userId);
2117
- return c.json({ count: nodes.length, nodes });
2118
- });
2119
- nodeRoutes.post("/", async (c) => {
2120
- const body = await c.req.json();
2121
- if (!body.title || !body.content) {
2122
- throw new ValidationError("title and content are required");
2291
+ function oneLine(s) {
2292
+ return s.replace(/\s+/g, " ").replace(/--+>/g, "-\u2192").trim();
2293
+ }
2294
+ function renderAnchor(anchor) {
2295
+ if (!anchor) {
2296
+ return "<!-- anchor: whole-artifact -->";
2123
2297
  }
2124
- const nestId = c.req.param("nestId");
2125
- const authorEmail = getUserEmail(c);
2126
- const { node } = await createNode(
2127
- nestId,
2128
- {
2129
- title: body.title,
2130
- content: body.content,
2131
- type: body.type,
2132
- tags: body.tags,
2133
- scope: body.scope,
2134
- status: body.status
2135
- },
2136
- authorEmail
2137
- );
2138
- const resolved = resolveStewardsForNode(nestId, node.id);
2139
- return c.json({
2140
- node: toNodeResponse(node),
2141
- stewards: resolved.length > 0 ? resolved.map((r) => ({
2142
- email: r.steward.userEmail,
2143
- role: r.steward.role,
2144
- source: r.source
2145
- })) : void 0
2146
- }, 201);
2147
- });
2148
- nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2149
- const nestId = c.req.param("nestId");
2150
- const nodeId = c.req.param("nodeId");
2151
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-P4M5UEJW.js");
2152
- const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
2153
- nestId,
2154
- nodeId
2155
- );
2156
- return c.json({
2157
- nodeId,
2158
- stewards: stewards.map((r) => ({
2159
- email: r.steward.userEmail,
2160
- role: r.steward.role,
2161
- scope: r.steward.scope,
2162
- source: r.source,
2163
- priority: r.priority
2164
- })),
2165
- fallbackToOwner,
2166
- ownerEmail
2298
+ const linePart = anchor.line !== void 0 ? `line ${anchor.line} \xB7 ` : "";
2299
+ const context = `\u2026${oneLine(anchor.before)}\u3008${oneLine(anchor.quote)}\u3009${oneLine(
2300
+ anchor.after
2301
+ )}\u2026`;
2302
+ return `<!-- anchor: ${linePart}quote "${oneLine(anchor.quote)}" \xB7 context ${context} -->`;
2303
+ }
2304
+ function statusHeading(thread) {
2305
+ if (thread.status === "resolved") {
2306
+ const who = thread.resolvedBy ? `${thread.resolvedBy}, ` : "";
2307
+ const when = thread.resolvedAt ? `${thread.resolvedAt}` : "";
2308
+ const meta = who || when ? ` (${who}${when})` : "";
2309
+ return `\u2705 RESOLVED${meta}`;
2310
+ }
2311
+ return "\u{1F7E0} OPEN";
2312
+ }
2313
+ function snapshotLabel(v) {
2314
+ return v == null ? "Unpinned" : `Snapshot v${v}`;
2315
+ }
2316
+ function orderThreads(threads) {
2317
+ return [...threads].sort((a, b) => {
2318
+ if (a.status !== b.status) return a.status === "open" ? -1 : 1;
2319
+ return a.createdAt.localeCompare(b.createdAt);
2167
2320
  });
2168
- });
2169
- nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2170
- const nestId = c.req.param("nestId");
2171
- const nodeId = c.req.param("nodeId");
2172
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-REGL5CUT.js");
2173
- const allVersions = getVersions2(nestId, nodeId);
2174
- const approved = getApprovedVersion2(nestId, nodeId);
2175
- const db = getDb();
2176
- const resolutions = db.prepare(
2177
- `SELECT version, status, resolved_by, resolved_at
2178
- FROM review_requests
2179
- WHERE nest_id = ? AND node_id = ?
2180
- AND status IN ('approved', 'rejected')
2181
- AND resolved_by IS NOT NULL
2182
- ORDER BY resolved_at DESC`
2183
- ).all(nestId, nodeId);
2184
- const byVersion = /* @__PURE__ */ new Map();
2185
- for (const r of resolutions) {
2186
- if (!byVersion.has(r.version)) {
2187
- byVersion.set(r.version, { status: r.status, resolvedBy: r.resolved_by });
2188
- }
2321
+ }
2322
+ function projectThreadsToMarkdown(artifactTitle, threads) {
2323
+ const lines = [];
2324
+ lines.push(`# ${artifactTitle} \u2014 Annotations`);
2325
+ lines.push("");
2326
+ lines.push(
2327
+ "> Auto-generated from review comments, grouped by the artifact snapshot each was anchored to. **Open** threads are unresolved feedback for the next iteration; **resolved** threads were already addressed (don't re-break them)."
2328
+ );
2329
+ lines.push("");
2330
+ if (threads.length === 0) {
2331
+ lines.push("_No annotations yet._");
2332
+ lines.push("");
2333
+ return lines.join("\n");
2189
2334
  }
2190
- const enriched = allVersions.map((v) => {
2191
- const r = byVersion.get(v.version);
2192
- return r ? { ...v, resolvedBy: r.resolvedBy, resolutionStatus: r.status } : v;
2193
- });
2194
- return c.json({
2195
- versions: enriched,
2196
- approvedVersion: approved,
2197
- currentVersion: allVersions[0]?.version || 0
2198
- });
2199
- });
2200
- nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
2201
- const nestId = c.req.param("nestId");
2202
- const nodeId = c.req.param("nodeId");
2203
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-XNXHF6Q7.js");
2204
- const history = getReviewHistory2(nestId, nodeId);
2205
- return c.json({ reviews: history });
2206
- });
2207
- nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
2208
- const nestId = c.req.param("nestId");
2209
- const nodeId = c.req.param("nodeId");
2210
- const { versions: versionManager } = engineCache.get(nestId);
2211
- const userId = c.get("userId");
2212
- const userEmail = resolveCallerEmail(userId);
2213
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2214
- return c.json(
2215
- { error: "Access denied \u2014 no steward assignment for this node" },
2216
- 403
2217
- );
2218
- }
2219
- const body = await c.req.json().catch(() => ({}));
2220
- const targetVersion = Number(body.targetVersion);
2221
- if (!Number.isInteger(targetVersion) || targetVersion < 1) {
2222
- throw new ValidationError("targetVersion (a positive integer) is required");
2223
- }
2224
- let raw;
2225
- try {
2226
- raw = await versionManager.reconstructVersion(nodeId, targetVersion);
2227
- } catch {
2228
- throw new NotFoundError(
2229
- `Version ${targetVersion} not found for ${nodeId}`
2230
- );
2231
- }
2232
- const content = bodyOnly(nodeId, raw);
2233
- const { node, version } = await updateNode(
2234
- nestId,
2235
- nodeId,
2236
- { content, changeNote: `Restored from version ${targetVersion}` },
2237
- userEmail
2238
- );
2239
- trackEvent("node.revert", { nestId, nodeId, targetVersion });
2240
- return c.json({ ok: true, version, node: toNodeResponse(node) });
2241
- });
2242
- nodeRoutes.get("/:nodeId{.+}", async (c) => {
2243
- const nestId = c.req.param("nestId");
2244
- const nodeId = c.req.param("nodeId");
2245
- const { storage, versions: versionManager } = engineCache.get(nestId);
2246
- const userId = c.get("userId");
2247
- const userEmail = resolveCallerEmail(userId);
2248
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2249
- return c.json(
2250
- { error: "Access denied \u2014 no steward assignment for this node" },
2251
- 403
2252
- );
2253
- }
2254
- let node;
2255
- try {
2256
- node = await storage.readDocument(nodeId, { verifyChecksum: true });
2257
- } catch {
2258
- throw new NotFoundError(`Node not found: ${nodeId}`);
2259
- }
2260
- if (node.pendingChange) {
2261
- try {
2262
- await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
2263
- const refreshed = await getPendingChange(nestId, nodeId);
2264
- if (refreshed) node.pendingChange = refreshed;
2265
- } catch (err) {
2266
- console.error("[external-edit] stage-on-read failed:", err);
2267
- }
2268
- try {
2269
- const latest = await loadLatestApprovedNode(nestId, nodeId);
2270
- if (latest) {
2271
- node = { ...latest, pendingChange: node.pendingChange };
2272
- }
2273
- } catch (err) {
2274
- console.error("[external-edit] reconstruct-latest failed:", err);
2275
- }
2276
- }
2277
- const response = toNodeResponse(node);
2278
- if (isPublicReader(nestId, userId)) {
2279
- const approved = getApprovedVersion(nestId, nodeId);
2280
- if (approved != null) {
2281
- try {
2282
- const raw = await versionManager.reconstructVersion(
2283
- nodeId,
2284
- approved
2285
- );
2286
- response.content = bodyOnly(nodeId, raw);
2287
- } catch (err) {
2288
- console.error(
2289
- "reconstructVersion failed (public single)",
2290
- nodeId,
2291
- approved,
2292
- err
2293
- );
2294
- response.content = "";
2295
- }
2296
- response.version = approved;
2297
- response.status = "published";
2298
- if (isMarkdownFormat(c)) {
2299
- return c.body(nodeAsMarkdown(response, nodeId), 200, {
2300
- "Content-Type": "text/markdown; charset=utf-8"
2301
- });
2302
- }
2303
- return c.json({ node: response });
2304
- }
2305
- }
2306
- response.status = node.pendingChange ? "external_edit_pending" : getDisplayStatus(nestId, nodeId);
2307
- if (response.status === "pending_review") {
2308
- const pending = getPendingReview(nestId, nodeId);
2309
- response.pendingReviewBy = pending?.requestedBy ?? null;
2310
- }
2311
- if (isMarkdownFormat(c)) {
2312
- return c.body(nodeAsMarkdown(response, nodeId), 200, {
2313
- "Content-Type": "text/markdown; charset=utf-8"
2314
- });
2315
- }
2316
- return c.json({ node: response });
2317
- });
2318
- nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2319
- const nestId = c.req.param("nestId");
2320
- const nodeId = c.req.param("nodeId");
2321
- const body = await c.req.json();
2322
- const baseVersionHeader = c.req.header("X-Base-Version");
2323
- if (baseVersionHeader) {
2324
- const baseVersion = parseInt(baseVersionHeader, 10);
2325
- const conflict = checkConflict(nestId, nodeId, baseVersion);
2326
- if (conflict.conflict) {
2327
- return c.json(
2328
- {
2329
- error: "Version conflict",
2330
- your_version: baseVersion,
2331
- current_version: conflict.currentVersion,
2332
- updated_by: conflict.updatedBy,
2333
- updated_at: conflict.updatedAt,
2334
- rejected_content: body.content || body.append || null
2335
- },
2336
- 409
2337
- );
2338
- }
2339
- }
2340
- const authorEmail = getUserEmail(c);
2341
- const { node, version: responseVersion } = await updateNode(
2342
- nestId,
2343
- nodeId,
2344
- {
2345
- content: body.content,
2346
- append: body.append,
2347
- tags: body.tags,
2348
- title: body.title,
2349
- status: body.status,
2350
- changeNote: body.changeNote
2351
- },
2352
- authorEmail
2353
- );
2354
- return c.json({ node: toNodeResponse(node), version: responseVersion });
2355
- });
2356
- nodeRoutes.delete("/:nodeId{.+}", async (c) => {
2357
- const nestId = c.req.param("nestId");
2358
- const nodeId = c.req.param("nodeId");
2359
- const { storage } = engineCache.get(nestId);
2360
- if (isStewardshipEnabled(nestId) && getPendingReview(nestId, nodeId)) {
2361
- throw new LockedError(
2362
- "This document is awaiting steward review and is locked. Approve or reject the pending review before deleting."
2363
- );
2364
- }
2365
- try {
2366
- await storage.deleteDocument(nodeId);
2367
- } catch {
2368
- throw new NotFoundError(`Node not found: ${nodeId}`);
2369
- }
2370
- removeNodeFromTagIndex(nestId, nodeId);
2371
- const db = getDb();
2372
- db.transaction(() => {
2373
- db.prepare(
2374
- "DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?"
2375
- ).run(nestId, nodeId);
2376
- db.prepare(
2377
- "DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?"
2378
- ).run(nestId, nodeId);
2379
- db.prepare(
2380
- "DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?"
2381
- ).run(nestId, nodeId);
2382
- db.prepare(
2383
- `DELETE FROM stewards
2384
- WHERE nest_id = ? AND scope = 'document' AND node_pattern = ?`
2385
- ).run(nestId, nodeId);
2386
- })();
2387
- trackEvent("node.delete", { nestId, nodeId });
2388
- return c.json({ deleted: true });
2389
- });
2390
- function getUserEmail(c) {
2391
- const userId = c.get("userId");
2392
- const db = getDb();
2393
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
2394
- return user?.email || "anonymous@localhost";
2395
- }
2396
-
2397
- // src/annotations/routes.ts
2398
- import { Hono as Hono5 } from "hono";
2399
-
2400
- // src/annotations/service.ts
2401
- import { v4 as uuid3 } from "uuid";
2402
-
2403
- // src/annotations/projection.ts
2404
- var MAX_CONTEXT_CHARS = 250;
2405
- var MAX_QUOTE_CHARS = 1e3;
2406
- function clampAnchor(raw) {
2407
- if (!raw) return null;
2408
- const quote = (raw.quote ?? "").trim().slice(0, MAX_QUOTE_CHARS);
2409
- if (!quote) return null;
2410
- const before = (raw.before ?? "").slice(-MAX_CONTEXT_CHARS);
2411
- const after = (raw.after ?? "").slice(0, MAX_CONTEXT_CHARS);
2412
- const line = typeof raw.line === "number" && Number.isFinite(raw.line) && raw.line > 0 ? Math.floor(raw.line) : void 0;
2413
- return { quote, before, after, ...line !== void 0 ? { line } : {} };
2414
- }
2415
- function oneLine(s) {
2416
- return s.replace(/\s+/g, " ").replace(/--+>/g, "-\u2192").trim();
2417
- }
2418
- function renderAnchor(anchor) {
2419
- if (!anchor) {
2420
- return "<!-- anchor: whole-artifact -->";
2421
- }
2422
- const linePart = anchor.line !== void 0 ? `line ${anchor.line} \xB7 ` : "";
2423
- const context = `\u2026${oneLine(anchor.before)}\u3008${oneLine(anchor.quote)}\u3009${oneLine(
2424
- anchor.after
2425
- )}\u2026`;
2426
- return `<!-- anchor: ${linePart}quote "${oneLine(anchor.quote)}" \xB7 context ${context} -->`;
2427
- }
2428
- function statusHeading(thread) {
2429
- if (thread.status === "resolved") {
2430
- const who = thread.resolvedBy ? `${thread.resolvedBy}, ` : "";
2431
- const when = thread.resolvedAt ? `${thread.resolvedAt}` : "";
2432
- const meta = who || when ? ` (${who}${when})` : "";
2433
- return `\u2705 RESOLVED${meta}`;
2434
- }
2435
- return "\u{1F7E0} OPEN";
2436
- }
2437
- function snapshotLabel(v) {
2438
- return v == null ? "Unpinned" : `Snapshot v${v}`;
2439
- }
2440
- function orderThreads(threads) {
2441
- return [...threads].sort((a, b) => {
2442
- if (a.status !== b.status) return a.status === "open" ? -1 : 1;
2443
- return a.createdAt.localeCompare(b.createdAt);
2444
- });
2445
- }
2446
- function projectThreadsToMarkdown(artifactTitle, threads) {
2447
- const lines = [];
2448
- lines.push(`# ${artifactTitle} \u2014 Annotations`);
2449
- lines.push("");
2450
- lines.push(
2451
- "> Auto-generated from review comments, grouped by the artifact snapshot each was anchored to. **Open** threads are unresolved feedback for the next iteration; **resolved** threads were already addressed (don't re-break them)."
2452
- );
2453
- lines.push("");
2454
- if (threads.length === 0) {
2455
- lines.push("_No annotations yet._");
2456
- lines.push("");
2457
- return lines.join("\n");
2458
- }
2459
- const buckets2 = /* @__PURE__ */ new Map();
2460
- for (const t of threads) {
2461
- const key = t.snapshotVersion ?? null;
2462
- (buckets2.get(key) ?? buckets2.set(key, []).get(key)).push(t);
2463
- }
2464
- const keys = [...buckets2.keys()].sort((a, b) => {
2465
- if (a == null) return 1;
2466
- if (b == null) return -1;
2467
- return b - a;
2335
+ const buckets2 = /* @__PURE__ */ new Map();
2336
+ for (const t of threads) {
2337
+ const key = t.snapshotVersion ?? null;
2338
+ (buckets2.get(key) ?? buckets2.set(key, []).get(key)).push(t);
2339
+ }
2340
+ const keys = [...buckets2.keys()].sort((a, b) => {
2341
+ if (a == null) return 1;
2342
+ if (b == null) return -1;
2343
+ return b - a;
2468
2344
  });
2469
2345
  for (const key of keys) {
2470
2346
  lines.push(`## ${snapshotLabel(key)}`);
@@ -2484,11 +2360,12 @@ function projectThreadsToMarkdown(artifactTitle, threads) {
2484
2360
  }
2485
2361
 
2486
2362
  // src/annotations/service.ts
2487
- function loadComments(threadId) {
2363
+ async function loadComments(threadId) {
2488
2364
  const db = getDb();
2489
- const rows = db.prepare(
2490
- "SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, rowid ASC"
2491
- ).all(threadId);
2365
+ const rows = await db.all(
2366
+ "SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, id ASC",
2367
+ [threadId]
2368
+ );
2492
2369
  return rows.map((r) => ({
2493
2370
  id: r.id,
2494
2371
  author: r.author,
@@ -2496,7 +2373,7 @@ function loadComments(threadId) {
2496
2373
  createdAt: r.created_at
2497
2374
  }));
2498
2375
  }
2499
- function rowToThread(row) {
2376
+ async function rowToThread(row) {
2500
2377
  let anchor = null;
2501
2378
  if (row.anchor_json) {
2502
2379
  try {
@@ -2516,19 +2393,32 @@ function rowToThread(row) {
2516
2393
  createdAt: row.created_at,
2517
2394
  resolvedBy: row.resolved_by,
2518
2395
  resolvedAt: row.resolved_at,
2519
- comments: loadComments(row.id)
2396
+ comments: await loadComments(row.id)
2520
2397
  };
2521
2398
  }
2522
- function getThreadRow(threadId) {
2523
- return getDb().prepare("SELECT * FROM annotation_threads WHERE id = ?").get(threadId);
2399
+ async function getThreadRow(threadId) {
2400
+ return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
2401
+ }
2402
+ async function countThreadsByNode(nestId) {
2403
+ const rows = await getDb().all(
2404
+ `SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
2405
+ FROM annotation_threads
2406
+ WHERE nest_id = ?
2407
+ GROUP BY node_id`,
2408
+ [nestId]
2409
+ );
2410
+ const out = {};
2411
+ for (const r of rows) out[r.node_id] = { count: r.count, lastAt: r.last_at };
2412
+ return out;
2524
2413
  }
2525
- function listThreads(nestId, nodeId) {
2526
- const rows = getDb().prepare(
2527
- "SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, rowid ASC"
2528
- ).all(nestId, nodeId);
2529
- return rows.map(rowToThread);
2414
+ async function listThreads(nestId, nodeId) {
2415
+ const rows = await getDb().all(
2416
+ "SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, id ASC",
2417
+ [nestId, nodeId]
2418
+ );
2419
+ return Promise.all(rows.map(rowToThread));
2530
2420
  }
2531
- function createThread(nestId, nodeId, input, authorEmail) {
2421
+ async function createThread(nestId, nodeId, input, authorEmail) {
2532
2422
  const body = (input.body ?? "").trim();
2533
2423
  if (!body) {
2534
2424
  throw new Error("comment body is required");
@@ -2536,61 +2426,66 @@ function createThread(nestId, nodeId, input, authorEmail) {
2536
2426
  const db = getDb();
2537
2427
  const id = uuid3();
2538
2428
  const anchor = clampAnchor(input.anchor);
2539
- const snapshot = input.snapshotVersion ?? getApprovedVersion(nestId, nodeId) ?? null;
2540
- const tx = db.transaction(() => {
2541
- db.prepare(
2429
+ const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
2430
+ await db.transaction(async (tx) => {
2431
+ await tx.run(
2542
2432
  `INSERT INTO annotation_threads
2543
2433
  (id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
2544
- VALUES (?, ?, ?, ?, ?, 'open', ?)`
2545
- ).run(
2546
- id,
2547
- nestId,
2548
- nodeId,
2549
- snapshot,
2550
- anchor ? JSON.stringify(anchor) : null,
2551
- authorEmail
2434
+ VALUES (?, ?, ?, ?, ?, 'open', ?)`,
2435
+ [
2436
+ id,
2437
+ nestId,
2438
+ nodeId,
2439
+ snapshot,
2440
+ anchor ? JSON.stringify(anchor) : null,
2441
+ authorEmail
2442
+ ]
2443
+ );
2444
+ await tx.run(
2445
+ "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2446
+ [uuid3(), id, authorEmail, body]
2552
2447
  );
2553
- db.prepare(
2554
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
2555
- ).run(uuid3(), id, authorEmail, body);
2556
2448
  });
2557
- tx();
2558
- return rowToThread(getThreadRow(id));
2449
+ return rowToThread(await getThreadRow(id));
2559
2450
  }
2560
- function getScopedThreadRow(threadId, nestId, nodeId) {
2561
- const row = getThreadRow(threadId);
2451
+ async function getScopedThreadRow(threadId, nestId, nodeId) {
2452
+ const row = await getThreadRow(threadId);
2562
2453
  if (!row || row.nest_id !== nestId || row.node_id !== nodeId) {
2563
2454
  throw new NotFoundError(`Thread not found: ${threadId}`);
2564
2455
  }
2565
2456
  return row;
2566
2457
  }
2567
- function addComment(nestId, nodeId, threadId, authorEmail, body) {
2458
+ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
2568
2459
  const trimmed = (body ?? "").trim();
2569
2460
  if (!trimmed) {
2570
2461
  throw new Error("comment body is required");
2571
2462
  }
2572
- getScopedThreadRow(threadId, nestId, nodeId);
2573
- getDb().prepare(
2574
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
2575
- ).run(uuid3(), threadId, authorEmail, trimmed);
2576
- return rowToThread(getThreadRow(threadId));
2463
+ await getScopedThreadRow(threadId, nestId, nodeId);
2464
+ await getDb().run(
2465
+ "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2466
+ [uuid3(), threadId, authorEmail, trimmed]
2467
+ );
2468
+ return rowToThread(await getThreadRow(threadId));
2577
2469
  }
2578
- function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
2579
- getScopedThreadRow(threadId, nestId, nodeId);
2470
+ async function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
2471
+ await getScopedThreadRow(threadId, nestId, nodeId);
2472
+ const db = getDb();
2580
2473
  if (status === "resolved") {
2581
- getDb().prepare(
2582
- "UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = datetime('now') WHERE id = ?"
2583
- ).run(byEmail, threadId);
2474
+ await db.run(
2475
+ `UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)} WHERE id = ?`,
2476
+ [byEmail, threadId]
2477
+ );
2584
2478
  } else {
2585
- getDb().prepare(
2586
- "UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?"
2587
- ).run(threadId);
2479
+ await db.run(
2480
+ "UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?",
2481
+ [threadId]
2482
+ );
2588
2483
  }
2589
- return rowToThread(getThreadRow(threadId));
2484
+ return rowToThread(await getThreadRow(threadId));
2590
2485
  }
2591
2486
  async function readArtifact(nestId, nodeId, version) {
2592
- const { storage, versions: versionManager } = engineCache.get(nestId);
2593
- const target = version ?? getApprovedVersion(nestId, nodeId) ?? null;
2487
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
2488
+ const target = version ?? await getApprovedVersion(nestId, nodeId) ?? null;
2594
2489
  let title = nodeId;
2595
2490
  let type = null;
2596
2491
  let liveHtml = "";
@@ -2617,52 +2512,388 @@ async function readArtifact(nestId, nodeId, version) {
2617
2512
  if (!liveExists) {
2618
2513
  throw new NotFoundError(`Artifact not found: ${nodeId}`);
2619
2514
  }
2620
- return { title, html: liveHtml, version: target, type };
2621
- }
2622
- function derivedAnnotationsId(sourceNodeId) {
2623
- const base = sourceNodeId.replace(/^nodes\//, "");
2624
- return `nodes/${base}--annotations`;
2625
- }
2626
- async function syncAnnotationsNode(nestId, nodeId, userEmail) {
2515
+ return { title, html: liveHtml, version: target, type };
2516
+ }
2517
+ function derivedAnnotationsId(sourceNodeId) {
2518
+ const base = sourceNodeId.replace(/^nodes\//, "");
2519
+ return `nodes/${base}--annotations`;
2520
+ }
2521
+ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
2522
+ try {
2523
+ let title = nodeId;
2524
+ let type = null;
2525
+ try {
2526
+ const { storage } = await engineCache.get(nestId);
2527
+ const node = await storage.readDocument(nodeId);
2528
+ title = node.frontmatter?.title || nodeId;
2529
+ type = node.frontmatter?.type || "document";
2530
+ } catch {
2531
+ }
2532
+ if (type !== null && type !== "artifact") return;
2533
+ const threads = await listThreads(nestId, nodeId);
2534
+ const derivedTitle = `${title} \u2014 Annotations`;
2535
+ const markdown = projectThreadsToMarkdown(title, threads);
2536
+ const derivedId = derivedAnnotationsId(nodeId);
2537
+ try {
2538
+ await updateNode(nestId, derivedId, { content: markdown }, userEmail);
2539
+ } catch (err) {
2540
+ if (err instanceof NotFoundError) {
2541
+ await createNode(
2542
+ nestId,
2543
+ {
2544
+ id: derivedId,
2545
+ title: derivedTitle,
2546
+ content: markdown,
2547
+ type: "document",
2548
+ tags: ["annotations"]
2549
+ },
2550
+ userEmail
2551
+ );
2552
+ } else {
2553
+ throw err;
2554
+ }
2555
+ }
2556
+ } catch (err) {
2557
+ console.warn(
2558
+ `[annotations] failed to sync derived node for ${nestId}/${nodeId}:`,
2559
+ err
2560
+ );
2561
+ }
2562
+ }
2563
+
2564
+ // src/nodes/routes.ts
2565
+ var nodeRoutes = new Hono4();
2566
+ function nodeAsMarkdown(response, nodeId) {
2567
+ return nodeToMarkdown({
2568
+ id: nodeId,
2569
+ title: response.title,
2570
+ tags: response.tags,
2571
+ status: response.status,
2572
+ body: response.content
2573
+ });
2574
+ }
2575
+ nodeRoutes.get("/", async (c) => {
2576
+ const nestId = c.req.param("nestId");
2577
+ const userId = c.get("userId");
2578
+ const nodes = await listNodesForCaller(nestId, userId);
2579
+ return c.json({ count: nodes.length, nodes });
2580
+ });
2581
+ nodeRoutes.post("/", async (c) => {
2582
+ const body = await c.req.json();
2583
+ if (!body.title || !body.content) {
2584
+ throw new ValidationError("title and content are required");
2585
+ }
2586
+ if (body.folder !== void 0 && typeof body.folder !== "string") {
2587
+ throw new ValidationError('folder must be a string path like "gtm/deals"');
2588
+ }
2589
+ const nestId = c.req.param("nestId");
2590
+ const authorEmail = await getUserEmail(c);
2591
+ const { node } = await createNode(
2592
+ nestId,
2593
+ {
2594
+ title: body.title,
2595
+ content: body.content,
2596
+ type: body.type,
2597
+ tags: body.tags,
2598
+ scope: body.scope,
2599
+ status: body.status,
2600
+ folder: body.folder
2601
+ },
2602
+ authorEmail
2603
+ );
2604
+ const resolved = await resolveStewardsForNode(nestId, node.id);
2605
+ return c.json({
2606
+ node: toNodeResponse(node),
2607
+ stewards: resolved.length > 0 ? resolved.map((r) => ({
2608
+ email: r.steward.userEmail,
2609
+ role: r.steward.role,
2610
+ source: r.source
2611
+ })) : void 0
2612
+ }, 201);
2613
+ });
2614
+ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2615
+ const nestId = c.req.param("nestId");
2616
+ const nodeId = c.req.param("nodeId");
2617
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-I2JAFYJU.js");
2618
+ const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
2619
+ nestId,
2620
+ nodeId
2621
+ );
2622
+ return c.json({
2623
+ nodeId,
2624
+ stewards: stewards.map((r) => ({
2625
+ email: r.steward.userEmail,
2626
+ role: r.steward.role,
2627
+ scope: r.steward.scope,
2628
+ source: r.source,
2629
+ priority: r.priority
2630
+ })),
2631
+ fallbackToOwner,
2632
+ ownerEmail
2633
+ });
2634
+ });
2635
+ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2636
+ const nestId = c.req.param("nestId");
2637
+ const nodeId = c.req.param("nodeId");
2638
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-FJWVTZKR.js");
2639
+ const allVersions = await getVersions2(nestId, nodeId);
2640
+ const approved = await getApprovedVersion2(nestId, nodeId);
2641
+ const db = getDb();
2642
+ const resolutions = await db.all(
2643
+ `SELECT version, status, resolved_by, resolved_at
2644
+ FROM review_requests
2645
+ WHERE nest_id = ? AND node_id = ?
2646
+ AND status IN ('approved', 'rejected')
2647
+ AND resolved_by IS NOT NULL
2648
+ ORDER BY resolved_at DESC`,
2649
+ [nestId, nodeId]
2650
+ );
2651
+ const byVersion = /* @__PURE__ */ new Map();
2652
+ for (const r of resolutions) {
2653
+ if (!byVersion.has(r.version)) {
2654
+ byVersion.set(r.version, { status: r.status, resolvedBy: r.resolved_by });
2655
+ }
2656
+ }
2657
+ const enriched = allVersions.map((v) => {
2658
+ const r = byVersion.get(v.version);
2659
+ return r ? { ...v, resolvedBy: r.resolvedBy, resolutionStatus: r.status } : v;
2660
+ });
2661
+ return c.json({
2662
+ versions: enriched,
2663
+ approvedVersion: approved,
2664
+ currentVersion: allVersions[0]?.version || 0
2665
+ });
2666
+ });
2667
+ nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
2668
+ const nestId = c.req.param("nestId");
2669
+ const nodeId = c.req.param("nodeId");
2670
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2FSKW425.js");
2671
+ const history = await getReviewHistory2(nestId, nodeId);
2672
+ return c.json({ reviews: history });
2673
+ });
2674
+ nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
2675
+ const nestId = c.req.param("nestId");
2676
+ const nodeId = c.req.param("nodeId");
2677
+ const { versions: versionManager } = await engineCache.get(nestId);
2678
+ const userId = c.get("userId");
2679
+ const userEmail = await resolveCallerEmail(userId);
2680
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2681
+ return c.json(
2682
+ { error: "Access denied \u2014 no steward assignment for this node" },
2683
+ 403
2684
+ );
2685
+ }
2686
+ const body = await c.req.json().catch(() => ({}));
2687
+ const targetVersion = Number(body.targetVersion);
2688
+ if (!Number.isInteger(targetVersion) || targetVersion < 1) {
2689
+ throw new ValidationError("targetVersion (a positive integer) is required");
2690
+ }
2691
+ let raw;
2692
+ try {
2693
+ raw = await versionManager.reconstructVersion(nodeId, targetVersion);
2694
+ } catch {
2695
+ throw new NotFoundError(
2696
+ `Version ${targetVersion} not found for ${nodeId}`
2697
+ );
2698
+ }
2699
+ const content = bodyOnly(nodeId, raw);
2700
+ const { node, version } = await updateNode(
2701
+ nestId,
2702
+ nodeId,
2703
+ { content, changeNote: `Restored from version ${targetVersion}` },
2704
+ userEmail
2705
+ );
2706
+ await trackEvent("node.revert", { nestId, nodeId, targetVersion });
2707
+ return c.json({ ok: true, version, node: toNodeResponse(node) });
2708
+ });
2709
+ nodeRoutes.get("/:nodeId{.+}", async (c) => {
2710
+ const nestId = c.req.param("nestId");
2711
+ const nodeId = c.req.param("nodeId");
2712
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
2713
+ const userId = c.get("userId");
2714
+ const userEmail = await resolveCallerEmail(userId);
2715
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2716
+ return c.json(
2717
+ { error: "Access denied \u2014 no steward assignment for this node" },
2718
+ 403
2719
+ );
2720
+ }
2721
+ let node;
2627
2722
  try {
2628
- let title = nodeId;
2723
+ node = await storage.readDocument(nodeId, { verifyChecksum: true });
2724
+ } catch {
2725
+ throw new NotFoundError(`Node not found: ${nodeId}`);
2726
+ }
2727
+ if (node.pendingChange) {
2629
2728
  try {
2630
- const { storage } = engineCache.get(nestId);
2631
- const node = await storage.readDocument(nodeId);
2632
- title = node.frontmatter?.title || nodeId;
2633
- } catch {
2729
+ await scanDocumentForDrift(nestId, nodeId, userEmail || "system:read");
2730
+ const refreshed = await getPendingChange(nestId, nodeId);
2731
+ if (refreshed) node.pendingChange = refreshed;
2732
+ } catch (err) {
2733
+ console.error("[external-edit] stage-on-read failed:", err);
2634
2734
  }
2635
- const threads = listThreads(nestId, nodeId);
2636
- const derivedTitle = `${title} \u2014 Annotations`;
2637
- const markdown = projectThreadsToMarkdown(title, threads);
2638
- const derivedId = derivedAnnotationsId(nodeId);
2639
2735
  try {
2640
- await updateNode(nestId, derivedId, { content: markdown }, userEmail);
2736
+ const latest = await loadLatestApprovedNode(nestId, nodeId);
2737
+ if (latest) {
2738
+ node = { ...latest, pendingChange: node.pendingChange };
2739
+ }
2641
2740
  } catch (err) {
2642
- if (err instanceof NotFoundError) {
2643
- await createNode(
2644
- nestId,
2645
- {
2646
- id: derivedId,
2647
- title: derivedTitle,
2648
- content: markdown,
2649
- type: "document",
2650
- tags: ["annotations"]
2651
- },
2652
- userEmail
2741
+ console.error("[external-edit] reconstruct-latest failed:", err);
2742
+ }
2743
+ }
2744
+ const response = toNodeResponse(node);
2745
+ if (await isPublicReader(nestId, userId)) {
2746
+ const approved = await getApprovedVersion(nestId, nodeId);
2747
+ if (approved != null) {
2748
+ try {
2749
+ const raw = await versionManager.reconstructVersion(
2750
+ nodeId,
2751
+ approved
2653
2752
  );
2654
- } else {
2655
- throw err;
2753
+ response.content = bodyOnly(nodeId, raw);
2754
+ } catch (err) {
2755
+ console.error(
2756
+ "reconstructVersion failed (public single)",
2757
+ nodeId,
2758
+ approved,
2759
+ err
2760
+ );
2761
+ response.content = "";
2656
2762
  }
2763
+ response.version = approved;
2764
+ response.status = "published";
2765
+ if (isMarkdownFormat(c)) {
2766
+ return c.body(nodeAsMarkdown(response, nodeId), 200, {
2767
+ "Content-Type": "text/markdown; charset=utf-8"
2768
+ });
2769
+ }
2770
+ return c.json({ node: response });
2657
2771
  }
2658
- } catch (err) {
2659
- console.warn(
2660
- `[annotations] failed to sync derived node for ${nestId}/${nodeId}:`,
2661
- err
2772
+ }
2773
+ response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
2774
+ if (response.status === "pending_review") {
2775
+ const pending = await getPendingReview(nestId, nodeId);
2776
+ response.pendingReviewBy = pending?.requestedBy ?? null;
2777
+ }
2778
+ if (isMarkdownFormat(c)) {
2779
+ return c.body(nodeAsMarkdown(response, nodeId), 200, {
2780
+ "Content-Type": "text/markdown; charset=utf-8"
2781
+ });
2782
+ }
2783
+ return c.json({ node: response });
2784
+ });
2785
+ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2786
+ const nestId = c.req.param("nestId");
2787
+ const nodeId = c.req.param("nodeId");
2788
+ const body = await c.req.json();
2789
+ const baseVersionHeader = c.req.header("X-Base-Version");
2790
+ if (baseVersionHeader) {
2791
+ const baseVersion = parseInt(baseVersionHeader, 10);
2792
+ const conflict = await checkConflict(nestId, nodeId, baseVersion);
2793
+ if (conflict.conflict) {
2794
+ return c.json(
2795
+ {
2796
+ error: "Version conflict",
2797
+ your_version: baseVersion,
2798
+ current_version: conflict.currentVersion,
2799
+ updated_by: conflict.updatedBy,
2800
+ updated_at: conflict.updatedAt,
2801
+ rejected_content: body.content || body.append || null
2802
+ },
2803
+ 409
2804
+ );
2805
+ }
2806
+ }
2807
+ const authorEmail = await getUserEmail(c);
2808
+ const { node, version: responseVersion } = await updateNode(
2809
+ nestId,
2810
+ nodeId,
2811
+ {
2812
+ content: body.content,
2813
+ append: body.append,
2814
+ tags: body.tags,
2815
+ title: body.title,
2816
+ status: body.status,
2817
+ changeNote: body.changeNote
2818
+ },
2819
+ authorEmail
2820
+ );
2821
+ return c.json({ node: toNodeResponse(node), version: responseVersion });
2822
+ });
2823
+ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
2824
+ const nestId = c.req.param("nestId");
2825
+ const nodeId = c.req.param("nodeId");
2826
+ const { storage } = await engineCache.get(nestId);
2827
+ if (await isStewardshipEnabled(nestId) && await getPendingReview(nestId, nodeId)) {
2828
+ throw new LockedError(
2829
+ "This document is awaiting steward review and is locked. Approve or reject the pending review before deleting."
2662
2830
  );
2663
2831
  }
2832
+ try {
2833
+ await storage.deleteDocument(nodeId);
2834
+ } catch {
2835
+ throw new NotFoundError(`Node not found: ${nodeId}`);
2836
+ }
2837
+ await removeNodeFromTagIndex(nestId, nodeId);
2838
+ const derivedId = derivedAnnotationsId(nodeId);
2839
+ try {
2840
+ await storage.deleteDocument(derivedId);
2841
+ await removeNodeFromTagIndex(nestId, derivedId);
2842
+ } catch {
2843
+ }
2844
+ const db = getDb();
2845
+ await db.transaction(async (tx) => {
2846
+ await tx.run(
2847
+ "DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?",
2848
+ [nestId, nodeId]
2849
+ );
2850
+ await tx.run(
2851
+ "DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?",
2852
+ [nestId, nodeId]
2853
+ );
2854
+ await tx.run(
2855
+ "DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?",
2856
+ [nestId, nodeId]
2857
+ );
2858
+ await tx.run(
2859
+ `DELETE FROM stewards
2860
+ WHERE nest_id = ? AND scope = 'document' AND node_pattern = ?`,
2861
+ [nestId, nodeId]
2862
+ );
2863
+ await tx.run(
2864
+ "DELETE FROM annotation_threads WHERE nest_id = ? AND node_id = ?",
2865
+ [nestId, nodeId]
2866
+ );
2867
+ await tx.run(
2868
+ "DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?",
2869
+ [nestId, derivedId]
2870
+ );
2871
+ await tx.run(
2872
+ "DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?",
2873
+ [nestId, derivedId]
2874
+ );
2875
+ await tx.run(
2876
+ "DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?",
2877
+ [nestId, derivedId]
2878
+ );
2879
+ await tx.run(
2880
+ "DELETE FROM annotation_threads WHERE nest_id = ? AND node_id = ?",
2881
+ [nestId, derivedId]
2882
+ );
2883
+ });
2884
+ await trackEvent("node.delete", { nestId, nodeId });
2885
+ return c.json({ deleted: true });
2886
+ });
2887
+ async function getUserEmail(c) {
2888
+ const userId = c.get("userId");
2889
+ const db = getDb();
2890
+ const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
2891
+ return user?.email || "anonymous@localhost";
2664
2892
  }
2665
2893
 
2894
+ // src/annotations/routes.ts
2895
+ import { Hono as Hono5 } from "hono";
2896
+
2666
2897
  // src/annotations/types.ts
2667
2898
  var ARTIFACT_NODE_TYPE = "artifact";
2668
2899
 
@@ -2680,25 +2911,25 @@ annotationRoutes.get("/:nodeId{.+}/annotations", async (c) => {
2680
2911
  const nestId = c.req.param("nestId");
2681
2912
  const nodeId = getNodeId(c);
2682
2913
  const userId = c.get("userId");
2683
- const userEmail = resolveCallerEmail(userId);
2684
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2914
+ const userEmail = await resolveCallerEmail(userId);
2915
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2685
2916
  return c.json({ error: "Access denied" }, 403);
2686
2917
  }
2687
- return c.json({ threads: listThreads(nestId, nodeId) });
2918
+ return c.json({ threads: await listThreads(nestId, nodeId) });
2688
2919
  });
2689
2920
  annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
2690
2921
  const nestId = c.req.param("nestId");
2691
2922
  const nodeId = getNodeId(c);
2692
2923
  const userId = c.get("userId");
2693
- const userEmail = resolveCallerEmail(userId);
2694
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2924
+ const userEmail = await resolveCallerEmail(userId);
2925
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2695
2926
  return c.json({ error: "Access denied" }, 403);
2696
2927
  }
2697
2928
  const input = await c.req.json();
2698
2929
  if (!input.body || !input.body.trim()) {
2699
2930
  throw new ValidationError("body is required");
2700
2931
  }
2701
- const thread = createThread(
2932
+ const thread = await createThread(
2702
2933
  nestId,
2703
2934
  nodeId,
2704
2935
  {
@@ -2716,15 +2947,15 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/comments", async (c) =
2716
2947
  const nodeId = getNodeId(c);
2717
2948
  const threadId = c.req.param("threadId");
2718
2949
  const userId = c.get("userId");
2719
- const userEmail = resolveCallerEmail(userId);
2720
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2950
+ const userEmail = await resolveCallerEmail(userId);
2951
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2721
2952
  return c.json({ error: "Access denied" }, 403);
2722
2953
  }
2723
2954
  const input = await c.req.json();
2724
2955
  if (!input.body || !input.body.trim()) {
2725
2956
  throw new ValidationError("body is required");
2726
2957
  }
2727
- const thread = addComment(nestId, nodeId, threadId, userEmail, input.body);
2958
+ const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
2728
2959
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2729
2960
  return c.json({ thread });
2730
2961
  });
@@ -2733,11 +2964,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/resolve", async (c) =>
2733
2964
  const nodeId = getNodeId(c);
2734
2965
  const threadId = c.req.param("threadId");
2735
2966
  const userId = c.get("userId");
2736
- const userEmail = resolveCallerEmail(userId);
2737
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2967
+ const userEmail = await resolveCallerEmail(userId);
2968
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2738
2969
  return c.json({ error: "Access denied" }, 403);
2739
2970
  }
2740
- const thread = setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
2971
+ const thread = await setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
2741
2972
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2742
2973
  return c.json({ thread });
2743
2974
  });
@@ -2746,11 +2977,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/reopen", async (c) =>
2746
2977
  const nodeId = getNodeId(c);
2747
2978
  const threadId = c.req.param("threadId");
2748
2979
  const userId = c.get("userId");
2749
- const userEmail = resolveCallerEmail(userId);
2750
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2980
+ const userEmail = await resolveCallerEmail(userId);
2981
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2751
2982
  return c.json({ error: "Access denied" }, 403);
2752
2983
  }
2753
- const thread = setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
2984
+ const thread = await setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
2754
2985
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2755
2986
  return c.json({ thread });
2756
2987
  });
@@ -2758,8 +2989,8 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
2758
2989
  const nestId = c.req.param("nestId");
2759
2990
  const nodeId = getNodeId(c);
2760
2991
  const userId = c.get("userId");
2761
- const userEmail = resolveCallerEmail(userId);
2762
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2992
+ const userEmail = await resolveCallerEmail(userId);
2993
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2763
2994
  return c.json({ error: "Access denied" }, 403);
2764
2995
  }
2765
2996
  const vRaw = c.req.query("v");
@@ -2950,15 +3181,16 @@ function tokenizePrompt(prompt) {
2950
3181
  const words = prompt.toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
2951
3182
  return Array.from(new Set(words));
2952
3183
  }
2953
- function compilePrompt(prompt, nestId, titles) {
3184
+ async function compilePrompt(prompt, nestId, titles) {
2954
3185
  const tokens = tokenizePrompt(prompt);
2955
3186
  if (tokens.length === 0) {
2956
3187
  return { selector: null, matchedTags: [], matchedTitles: [], unmatched: [] };
2957
3188
  }
2958
3189
  const db = getDb();
2959
- const tagRows = db.prepare(
2960
- "SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?"
2961
- ).all(nestId);
3190
+ const tagRows = await db.all(
3191
+ "SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?",
3192
+ [nestId]
3193
+ );
2962
3194
  const knownTags = new Set(tagRows.map((r) => r.tag_name));
2963
3195
  const matchedTags = /* @__PURE__ */ new Set();
2964
3196
  for (const token of tokens) {
@@ -2988,11 +3220,11 @@ function compilePrompt(prompt, nestId, titles) {
2988
3220
 
2989
3221
  // src/nodes/readable-body.ts
2990
3222
  async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
2991
- if (!isPublicReader(nestId, userId)) return workingBody;
2992
- const approved = getApprovedVersion(nestId, nodeId);
3223
+ if (!await isPublicReader(nestId, userId)) return workingBody;
3224
+ const approved = await getApprovedVersion(nestId, nodeId);
2993
3225
  if (approved == null) return "";
2994
3226
  try {
2995
- const { versions } = engineCache.get(nestId);
3227
+ const { versions } = await engineCache.get(nestId);
2996
3228
  const raw = await versions.reconstructVersion(nodeId, approved);
2997
3229
  return bodyOnly(nodeId, raw);
2998
3230
  } catch {
@@ -3000,11 +3232,11 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
3000
3232
  }
3001
3233
  }
3002
3234
  async function resolveExportBody(nestId, nodeId, workingBody) {
3003
- if (!isStewardshipEnabled(nestId)) return workingBody;
3004
- const approved = getApprovedVersion(nestId, nodeId);
3235
+ if (!await isStewardshipEnabled(nestId)) return workingBody;
3236
+ const approved = await getApprovedVersion(nestId, nodeId);
3005
3237
  if (approved == null) return null;
3006
3238
  try {
3007
- const { versions } = engineCache.get(nestId);
3239
+ const { versions } = await engineCache.get(nestId);
3008
3240
  const raw = await versions.reconstructVersion(nodeId, approved);
3009
3241
  return bodyOnly(nodeId, raw);
3010
3242
  } catch {
@@ -3027,9 +3259,9 @@ function extractWikiTargets(body) {
3027
3259
  return out;
3028
3260
  }
3029
3261
  async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
3030
- const { storage } = engineCache.get(nestId);
3262
+ const { storage } = await engineCache.get(nestId);
3031
3263
  const docs = await storage.discoverDocuments();
3032
- const accessible = filterAccessible(nestId, userId, userEmail, docs);
3264
+ const accessible = await filterAccessible(nestId, userId, userEmail, docs);
3033
3265
  const idSet = new Set(accessible.map((d) => d.id));
3034
3266
  const titleToId = /* @__PURE__ */ new Map();
3035
3267
  for (const d of accessible) {
@@ -3065,7 +3297,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
3065
3297
  }
3066
3298
  }
3067
3299
  const tags = [...tagCounts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
3068
- const exposeStewards = isStewardshipEnabled(nestId) && !isPublicReader(nestId, userId);
3300
+ const exposeStewards = await isStewardshipEnabled(nestId) && !await isPublicReader(nestId, userId);
3069
3301
  const stewardRows = exposeStewards ? (await listStewards({ nestId })).filter(
3070
3302
  // Drop document-scoped rows whose node the caller can't see.
3071
3303
  (s) => s.scope !== "document" || idSet.has(s.nodePattern || "")
@@ -3159,7 +3391,7 @@ var queryRoutes = new Hono6();
3159
3391
  queryRoutes.get("/graph", async (c) => {
3160
3392
  const nestId = c.req.param("nestId");
3161
3393
  const userId = c.get("userId");
3162
- const userEmail = resolveCallerEmail(userId);
3394
+ const userEmail = await resolveCallerEmail(userId);
3163
3395
  const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
3164
3396
  const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
3165
3397
  return c.json(graph);
@@ -3184,17 +3416,36 @@ queryRoutes.post("/context", async (c) => {
3184
3416
  throw new ValidationError("prompt or selector is required");
3185
3417
  }
3186
3418
  const nestId = c.req.param("nestId");
3187
- const { query: queryEngine, storage } = engineCache.get(nestId);
3419
+ const { query: queryEngine, storage } = await engineCache.get(nestId);
3188
3420
  const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
3189
3421
  const hops = body.hops ?? 2;
3190
3422
  const includeDrafts = body.include_drafts === true;
3423
+ const isVisible = (d) => includeDrafts || d.frontmatter.status === "published";
3191
3424
  let selector = body.selector?.trim() || null;
3192
3425
  let compileDetail = null;
3193
3426
  let titleMatches = [];
3427
+ let resolvedTitleSelector = false;
3194
3428
  const allDocs = await storage.discoverDocuments();
3195
- if (!selector && body.prompt) {
3429
+ if (selector && selector.includes("[[")) {
3430
+ const parts = selector.split("|").map((p) => p.trim()).filter(Boolean);
3431
+ const wantedTitles = /* @__PURE__ */ new Set();
3432
+ const rest = [];
3433
+ for (const p of parts) {
3434
+ const m = /^\[\[(.+)\]\]$/.exec(p);
3435
+ if (m) wantedTitles.add(m[1].trim().toLowerCase());
3436
+ else rest.push(p);
3437
+ }
3438
+ if (wantedTitles.size > 0) {
3439
+ selector = rest.join("|") || null;
3440
+ resolvedTitleSelector = true;
3441
+ titleMatches = allDocs.filter(
3442
+ (d) => wantedTitles.has(String(d.frontmatter.title || "").toLowerCase()) && isVisible(d)
3443
+ );
3444
+ }
3445
+ }
3446
+ if (!selector && !resolvedTitleSelector && body.prompt) {
3196
3447
  const titles = allDocs.map((d) => d.frontmatter.title);
3197
- compileDetail = compilePrompt(body.prompt, nestId, titles);
3448
+ compileDetail = await compilePrompt(body.prompt, nestId, titles);
3198
3449
  selector = compileDetail.selector;
3199
3450
  if (compileDetail.matchedTitles.length > 0) {
3200
3451
  const matchedLower = new Set(
@@ -3218,6 +3469,44 @@ queryRoutes.post("/context", async (c) => {
3218
3469
  hopsUsed = result.hopsUsed;
3219
3470
  nodesTraversed = result.nodesTraversed;
3220
3471
  }
3472
+ if (titleMatches.length > 0 && hops > 0) {
3473
+ const byTitle = new Map(
3474
+ allDocs.map((d) => [
3475
+ String(d.frontmatter.title || "").toLowerCase(),
3476
+ d
3477
+ ])
3478
+ );
3479
+ const byId = new Map(allDocs.map((d) => [d.id, d]));
3480
+ const adj = /* @__PURE__ */ new Map();
3481
+ const link = (a, b) => {
3482
+ (adj.get(a) ?? adj.set(a, /* @__PURE__ */ new Set()).get(a)).add(b);
3483
+ (adj.get(b) ?? adj.set(b, /* @__PURE__ */ new Set()).get(b)).add(a);
3484
+ };
3485
+ for (const d of allDocs) {
3486
+ for (const target of extractWikiTargets(d.body || "")) {
3487
+ const t = byId.get(target) ?? byTitle.get(target.toLowerCase());
3488
+ if (t && t.id !== d.id) link(d.id, t.id);
3489
+ }
3490
+ }
3491
+ const seen = new Set(titleMatches.map((d) => d.id));
3492
+ let frontier = titleMatches.map((d) => d.id);
3493
+ for (let depth = 0; depth < hops && frontier.length; depth++) {
3494
+ const next = [];
3495
+ for (const id of frontier) {
3496
+ for (const nb of adj.get(id) ?? []) {
3497
+ if (seen.has(nb)) continue;
3498
+ seen.add(nb);
3499
+ const doc = byId.get(nb);
3500
+ if (!doc || !isVisible(doc)) continue;
3501
+ next.push(nb);
3502
+ titleMatches.push(doc);
3503
+ }
3504
+ }
3505
+ frontier = next;
3506
+ hopsUsed = Math.max(hopsUsed, depth + 1);
3507
+ }
3508
+ nodesTraversed += seen.size;
3509
+ }
3221
3510
  if (titleMatches.length > 0) {
3222
3511
  const seen = new Set(documents.map((d) => d.id));
3223
3512
  for (const t of titleMatches) {
@@ -3228,9 +3517,9 @@ queryRoutes.post("/context", async (c) => {
3228
3517
  }
3229
3518
  }
3230
3519
  const userId = c.get("userId");
3231
- const userEmail = resolveCallerEmail(userId);
3520
+ const userEmail = await resolveCallerEmail(userId);
3232
3521
  const beforePermission = documents.length;
3233
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
3522
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
3234
3523
  const permissionFiltered = beforePermission - accessible.length;
3235
3524
  const readable = await Promise.all(
3236
3525
  accessible.map(async (doc) => ({
@@ -3289,13 +3578,13 @@ queryRoutes.post("/query", async (c) => {
3289
3578
  throw new ValidationError("query is required");
3290
3579
  }
3291
3580
  const nestId = c.req.param("nestId");
3292
- const { query: queryEngine } = engineCache.get(nestId);
3581
+ const { query: queryEngine } = await engineCache.get(nestId);
3293
3582
  const result = await queryEngine.query(body.query, {
3294
3583
  hops: body.hops ?? 2
3295
3584
  });
3296
3585
  const userId = c.get("userId");
3297
- const userEmail = resolveCallerEmail(userId);
3298
- const accessible = filterAccessible(nestId, userId, userEmail, result.documents);
3586
+ const userEmail = await resolveCallerEmail(userId);
3587
+ const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
3299
3588
  return c.json({
3300
3589
  query: body.query,
3301
3590
  count: accessible.length,
@@ -3314,7 +3603,7 @@ queryRoutes.get("/search", async (c) => {
3314
3603
  throw new ValidationError("q query parameter is required");
3315
3604
  }
3316
3605
  const nestId = c.req.param("nestId");
3317
- const { storage } = engineCache.get(nestId);
3606
+ const { storage } = await engineCache.get(nestId);
3318
3607
  const documents = await storage.discoverDocuments();
3319
3608
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
3320
3609
  const matches = documents.filter((node) => {
@@ -3327,8 +3616,8 @@ queryRoutes.get("/search", async (c) => {
3327
3616
  return terms.every((term) => haystack.includes(term));
3328
3617
  });
3329
3618
  const userId = c.get("userId");
3330
- const userEmail = resolveCallerEmail(userId);
3331
- const accessible = filterAccessible(nestId, userId, userEmail, matches);
3619
+ const userEmail = await resolveCallerEmail(userId);
3620
+ const accessible = await filterAccessible(nestId, userId, userEmail, matches);
3332
3621
  return c.json({
3333
3622
  query: q,
3334
3623
  count: accessible.length,
@@ -3342,7 +3631,7 @@ queryRoutes.get("/search", async (c) => {
3342
3631
  });
3343
3632
  });
3344
3633
  queryRoutes.get("/overview", async (c) => {
3345
- const { storage } = engineCache.get(c.req.param("nestId"));
3634
+ const { storage } = await engineCache.get(c.req.param("nestId"));
3346
3635
  const documents = await storage.discoverDocuments();
3347
3636
  const types = {};
3348
3637
  const tags = {};
@@ -3366,8 +3655,12 @@ queryRoutes.get("/overview", async (c) => {
3366
3655
  }))
3367
3656
  });
3368
3657
  });
3658
+ queryRoutes.get("/comment-counts", async (c) => {
3659
+ const counts = await countThreadsByNode(c.req.param("nestId"));
3660
+ return c.json({ counts });
3661
+ });
3369
3662
  queryRoutes.get("/context", async (c) => {
3370
- const { storage } = engineCache.get(c.req.param("nestId"));
3663
+ const { storage } = await engineCache.get(c.req.param("nestId"));
3371
3664
  const content = await storage.readContextMd();
3372
3665
  return c.json({ content: content || "" });
3373
3666
  });
@@ -3376,7 +3669,7 @@ queryRoutes.get("/export", async (c) => {
3376
3669
  throw new ValidationError("format=markdown is required");
3377
3670
  }
3378
3671
  const nestId = c.req.param("nestId");
3379
- const { storage, query: queryEngine } = engineCache.get(nestId);
3672
+ const { storage, query: queryEngine } = await engineCache.get(nestId);
3380
3673
  const selector = c.req.query("selector")?.trim() || null;
3381
3674
  let documents;
3382
3675
  if (selector) {
@@ -3386,9 +3679,9 @@ queryRoutes.get("/export", async (c) => {
3386
3679
  documents = await storage.discoverDocuments();
3387
3680
  }
3388
3681
  const userId = c.get("userId");
3389
- const userEmail = resolveCallerEmail(userId);
3390
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
3391
- const governed = isStewardshipEnabled(nestId);
3682
+ const userEmail = await resolveCallerEmail(userId);
3683
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
3684
+ const governed = await isStewardshipEnabled(nestId);
3392
3685
  const resolved = await Promise.all(
3393
3686
  accessible.map(async (n) => {
3394
3687
  const body = await resolveExportBody(nestId, n.id, n.body || "");
@@ -3435,7 +3728,7 @@ queryRoutes.post("/publish", async (c) => {
3435
3728
  throw new ValidationError("documents array or context_md is required");
3436
3729
  }
3437
3730
  const nestId = c.req.param("nestId");
3438
- const { storage } = engineCache.get(nestId);
3731
+ const { storage } = await engineCache.get(nestId);
3439
3732
  const created = [];
3440
3733
  if (body.context_md) {
3441
3734
  await storage.writeContextMd(body.context_md);
@@ -3467,7 +3760,7 @@ queryRoutes.post("/publish", async (c) => {
3467
3760
  };
3468
3761
  const serialized = serializeDocument2(node);
3469
3762
  await storage.writeDocument(id, serialized);
3470
- syncNodeTags(nestId, id, tags);
3763
+ await syncNodeTags(nestId, id, tags);
3471
3764
  created.push(id);
3472
3765
  }
3473
3766
  trackEvent("nest.publish", { nestId, count: created.length });
@@ -3487,6 +3780,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3487
3780
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3488
3781
 
3489
3782
  // src/mcp/tools.ts
3783
+ var MAX_HOPS = 10;
3784
+ function normalizeHops(raw) {
3785
+ if (raw == null) return 2;
3786
+ const n = Number(raw);
3787
+ if (!Number.isFinite(n)) return 2;
3788
+ return Math.max(0, Math.min(MAX_HOPS, Math.floor(n)));
3789
+ }
3490
3790
  var TOOL_DEFINITIONS = [
3491
3791
  {
3492
3792
  name: "context_init",
@@ -3515,7 +3815,11 @@ var TOOL_DEFINITIONS = [
3515
3815
  inputSchema: {
3516
3816
  type: "object",
3517
3817
  properties: {
3518
- query: { type: "string", description: "Selector query" }
3818
+ query: { type: "string", description: "Selector query" },
3819
+ hops: {
3820
+ type: "number",
3821
+ description: "Graph traversal depth from the matched nodes (default: 2). Use 1 for just the matches + direct links, higher to pull in more of the neighborhood."
3822
+ }
3519
3823
  },
3520
3824
  required: ["query"]
3521
3825
  }
@@ -3556,11 +3860,39 @@ var TOOL_DEFINITIONS = [
3556
3860
  max_tokens: {
3557
3861
  type: "number",
3558
3862
  description: "Approximate token budget (default: 8000)"
3863
+ },
3864
+ hops: {
3865
+ type: "number",
3866
+ description: "Graph traversal depth from the matched nodes (default: 2)"
3559
3867
  }
3560
3868
  },
3561
3869
  required: ["selector"]
3562
3870
  }
3563
3871
  },
3872
+ {
3873
+ name: "context_export",
3874
+ description: "Export the ENTIRE nest as one markdown bundle \u2014 every accessible document's FULL content. Use when you need the whole nest as context rather than a targeted query. Respects an optional token budget.",
3875
+ inputSchema: {
3876
+ type: "object",
3877
+ properties: {
3878
+ max_tokens: {
3879
+ type: "number",
3880
+ description: "Approximate token budget; documents are included until it's reached (omit for no cap)."
3881
+ }
3882
+ }
3883
+ }
3884
+ },
3885
+ {
3886
+ name: "context_comments",
3887
+ description: "Get the review comments/annotations on a document \u2014 anchored quotes, threaded replies, and open/resolved status \u2014 by title or ID. Use to pull human feedback into context.",
3888
+ inputSchema: {
3889
+ type: "object",
3890
+ properties: {
3891
+ title: { type: "string", description: "Title of the node" },
3892
+ id: { type: "string", description: "ID of the node" }
3893
+ }
3894
+ }
3895
+ },
3564
3896
  {
3565
3897
  name: "context_create",
3566
3898
  description: "Create a new knowledge node in the vault.",
@@ -3581,7 +3913,11 @@ var TOOL_DEFINITIONS = [
3581
3913
  items: { type: "string" },
3582
3914
  description: "Tags"
3583
3915
  },
3584
- scope: { type: "string", description: "Visibility scope" }
3916
+ scope: { type: "string", description: "Visibility scope" },
3917
+ folder: {
3918
+ type: "string",
3919
+ description: 'Folder path under nodes/ (e.g. "gtm/deals"); segments are slugified'
3920
+ }
3585
3921
  },
3586
3922
  required: ["title", "content"]
3587
3923
  }
@@ -3737,8 +4073,8 @@ var TOOL_DEFINITIONS = [
3737
4073
  }
3738
4074
  ];
3739
4075
  async function resolveLlmBody(ctx, node) {
3740
- if (!isStewardshipEnabled(ctx.nestId)) return node.body || "";
3741
- const approved = getApprovedVersion(ctx.nestId, node.id);
4076
+ if (!await isStewardshipEnabled(ctx.nestId)) return node.body || "";
4077
+ const approved = await getApprovedVersion(ctx.nestId, node.id);
3742
4078
  if (approved == null) return null;
3743
4079
  try {
3744
4080
  return await ctx.versionManager.reconstructVersion(node.id, approved);
@@ -3812,8 +4148,16 @@ ${nodeList}`;
3812
4148
  ${results}`;
3813
4149
  }
3814
4150
  case "context_query": {
3815
- const result = await queryEngine.query(args.query, { hops: 2 });
3816
- const nodes = result.documents;
4151
+ const result = await queryEngine.query(args.query, {
4152
+ hops: normalizeHops(args.hops)
4153
+ });
4154
+ const visibility = await Promise.all(
4155
+ result.documents.map(async (n) => ({
4156
+ node: n,
4157
+ visible: await resolveLlmBody(ctx, n) !== null
4158
+ }))
4159
+ );
4160
+ const nodes = visibility.filter((e) => e.visible).map((e) => e.node);
3817
4161
  if (!nodes.length) return `No nodes matched: ${args.query}`;
3818
4162
  const list = nodes.map(
3819
4163
  (n, i) => `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).join(" ")}`
@@ -3861,23 +4205,101 @@ ${body || "(no content)"}`;
3861
4205
  ${list}`;
3862
4206
  }
3863
4207
  case "context_resolve": {
3864
- const result = await queryEngine.query(args.selector, { hops: 2 });
4208
+ const result = await queryEngine.query(args.selector, {
4209
+ hops: normalizeHops(args.hops)
4210
+ });
3865
4211
  const maxTokens = args.max_tokens || 8e3;
3866
4212
  const approxChars = maxTokens * 4;
4213
+ const resolvedBodies = await Promise.all(
4214
+ result.documents.map(async (n) => ({
4215
+ node: n,
4216
+ body: await resolveLlmBody(ctx, n)
4217
+ }))
4218
+ );
3867
4219
  let total = 0;
3868
4220
  const resolved = [];
3869
- for (const n of result.documents) {
4221
+ for (const { node: n, body } of resolvedBodies) {
4222
+ if (body === null) continue;
3870
4223
  const entry = `## ${n.frontmatter.title}
3871
4224
 
3872
- ${n.body || ""}`;
4225
+ ${body}`;
3873
4226
  if (total + entry.length > approxChars) break;
3874
4227
  resolved.push(entry);
3875
4228
  total += entry.length;
3876
4229
  }
3877
4230
  return resolved.join("\n\n---\n\n") || "No nodes resolved.";
3878
4231
  }
4232
+ case "context_export": {
4233
+ const docs = await storage.discoverDocuments();
4234
+ const approxChars = args.max_tokens ? args.max_tokens * 4 : Infinity;
4235
+ const resolved = await Promise.all(
4236
+ docs.map(async (n) => ({ node: n, body: await resolveLlmBody(ctx, n) }))
4237
+ );
4238
+ let total = 0;
4239
+ let budgetHit = false;
4240
+ const parts = [];
4241
+ for (const { node: n, body } of resolved) {
4242
+ if (body === null) continue;
4243
+ const meta = [
4244
+ `**Title:** ${n.frontmatter.title}`,
4245
+ `**Type:** ${n.frontmatter.type || "document"}`,
4246
+ n.frontmatter.tags?.length ? `**Tags:** ${n.frontmatter.tags.join(" ")}` : null
4247
+ ].filter(Boolean).join("\n");
4248
+ const entry = `${meta}
4249
+
4250
+ ${body || "(no content)"}`;
4251
+ if (total + entry.length > approxChars) {
4252
+ budgetHit = true;
4253
+ break;
4254
+ }
4255
+ parts.push(entry);
4256
+ total += entry.length;
4257
+ }
4258
+ if (!parts.length) {
4259
+ return budgetHit ? "Token budget reached before any document fit \u2014 raise max_tokens, or use context_query/context_resolve to target." : "No documents available to export.";
4260
+ }
4261
+ const note = budgetHit ? `
4262
+
4263
+ _(Token budget reached \u2014 ${parts.length} of ${docs.length} documents included. Raise max_tokens, or use context_query/context_resolve to target.)_` : "";
4264
+ return `# Nest export \u2014 ${parts.length} document(s)
4265
+
4266
+ ${parts.join(
4267
+ "\n\n---\n\n"
4268
+ )}${note}`;
4269
+ }
4270
+ case "context_comments": {
4271
+ const docs = await storage.discoverDocuments();
4272
+ let node;
4273
+ if (args.title) {
4274
+ node = docs.find(
4275
+ (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4276
+ );
4277
+ } else if (args.id) {
4278
+ node = docs.find((n) => n.id === args.id);
4279
+ }
4280
+ if (!node) return `Node not found: ${args.title || args.id}`;
4281
+ if (await resolveLlmBody(ctx, node) === null) {
4282
+ return `Node "${node.frontmatter.title}" has no approved version yet \u2014 not available to AI.`;
4283
+ }
4284
+ const threads = await listThreads(nestId, node.id);
4285
+ if (!threads.length)
4286
+ return `No comments on "${node.frontmatter.title}".`;
4287
+ const openCount = threads.filter((t) => t.status === "open").length;
4288
+ const sections = threads.map((t, i) => {
4289
+ const quote = t.anchor?.quote ? `> ${t.anchor.quote.replace(/\n/g, " ")}
4290
+
4291
+ ` : "";
4292
+ const head = `### ${i + 1}. [${t.status}]${t.anchor?.quote ? "" : " (whole-document)"}`;
4293
+ const comments = t.comments.map((c) => `- **${c.author}** (${c.createdAt}): ${c.body}`).join("\n");
4294
+ return `${head}
4295
+ ${quote}${comments}`;
4296
+ }).join("\n\n");
4297
+ return `# Comments on "${node.frontmatter.title}" \u2014 ${threads.length} thread(s), ${openCount} open
4298
+
4299
+ ${sections}`;
4300
+ }
3879
4301
  case "context_create": {
3880
- if (!canCreateInNest(nestId, userEmail)) {
4302
+ if (!await canCreateInNest(nestId, userEmail)) {
3881
4303
  return "You don't have permission to create documents in this nest.";
3882
4304
  }
3883
4305
  const { node } = await createNode(
@@ -3887,7 +4309,8 @@ ${n.body || ""}`;
3887
4309
  content: args.content,
3888
4310
  type: args.type,
3889
4311
  tags: args.tags,
3890
- scope: args.scope
4312
+ scope: args.scope,
4313
+ folder: args.folder
3891
4314
  },
3892
4315
  userEmail
3893
4316
  );
@@ -3899,7 +4322,7 @@ ${n.body || ""}`;
3899
4322
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3900
4323
  );
3901
4324
  if (!node) return `Node not found: ${args.title}`;
3902
- const editCheck = canUserEdit(nestId, node.id, userEmail);
4325
+ const editCheck = await canUserEdit(nestId, node.id, userEmail);
3903
4326
  if (!editCheck.allowed) {
3904
4327
  return `You don't have permission to edit "${args.title}": ${editCheck.reason}`;
3905
4328
  }
@@ -3923,7 +4346,7 @@ ${n.body || ""}`;
3923
4346
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3924
4347
  );
3925
4348
  const nodeId = node?.id || "";
3926
- const resolved = resolveStewardsForNode(ctx.nestId, nodeId);
4349
+ const resolved = await resolveStewardsForNode(ctx.nestId, nodeId);
3927
4350
  if (resolved.length === 0) {
3928
4351
  return `No stewards configured for "${args.title}". Changes are auto-approved.`;
3929
4352
  }
@@ -3934,8 +4357,8 @@ ${n.body || ""}`;
3934
4357
 
3935
4358
  ${list}`;
3936
4359
  }
3937
- if (!canManageStewards(ctx.userEmail)) {
3938
- return "You don't have permission to list stewards. Only the super admin can do this.";
4360
+ if (!await canManageStewards(ctx.nestId, ctx.userId)) {
4361
+ return "You don't have permission to list stewards. Only a nest admin, the nest owner, or the server admin can do this.";
3939
4362
  }
3940
4363
  const allStewards = await getStewardsForNest(ctx.nestId);
3941
4364
  if (allStewards.length === 0) {
@@ -3979,14 +4402,14 @@ ${list}`;
3979
4402
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3980
4403
  );
3981
4404
  if (!node) return `Node not found: ${args.title}`;
3982
- const submitCheck = canUserEdit(ctx.nestId, node.id, userEmail);
4405
+ const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
3983
4406
  if (!submitCheck.allowed) {
3984
4407
  return `You don't have permission to submit "${args.title}" for review: ${submitCheck.reason}`;
3985
4408
  }
3986
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4409
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
3987
4410
  if (currentVersion === 0) return `No versions found for "${args.title}"`;
3988
4411
  try {
3989
- const request = submitForReview({
4412
+ const request = await submitForReview({
3990
4413
  nestId: ctx.nestId,
3991
4414
  nodeId: node.id,
3992
4415
  version: currentVersion,
@@ -3994,7 +4417,7 @@ ${list}`;
3994
4417
  note: args.note,
3995
4418
  priority: args.priority
3996
4419
  });
3997
- const resolved = resolveStewardsForNode(
4420
+ const resolved = await resolveStewardsForNode(
3998
4421
  ctx.nestId,
3999
4422
  node.id
4000
4423
  );
@@ -4013,7 +4436,7 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
4013
4436
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4014
4437
  );
4015
4438
  if (!node) return `Node not found: ${args.title}`;
4016
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4439
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
4017
4440
  try {
4018
4441
  const request = await approve({
4019
4442
  nestId: ctx.nestId,
@@ -4034,9 +4457,9 @@ Note: ${args.note}` : ""}`;
4034
4457
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4035
4458
  );
4036
4459
  if (!node) return `Node not found: ${args.title}`;
4037
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4460
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
4038
4461
  try {
4039
- const request = reject({
4462
+ const request = await reject({
4040
4463
  nestId: ctx.nestId,
4041
4464
  nodeId: node.id,
4042
4465
  version: currentVersion,
@@ -4055,8 +4478,8 @@ Reason: ${args.note}`;
4055
4478
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4056
4479
  );
4057
4480
  if (!node) return `Node not found: ${args.title}`;
4058
- const allVersions = getVersions(ctx.nestId, node.id);
4059
- const approved = getApprovedVersion(ctx.nestId, node.id);
4481
+ const allVersions = await getVersions(ctx.nestId, node.id);
4482
+ const approved = await getApprovedVersion(ctx.nestId, node.id);
4060
4483
  if (allVersions.length === 0) {
4061
4484
  return `No version history for "${args.title}".`;
4062
4485
  }
@@ -4073,8 +4496,8 @@ ${list}`;
4073
4496
  if (!["nest", "tag", "document"].includes(scope)) {
4074
4497
  return `Invalid scope "${args.scope}". Use: nest, tag, or document.`;
4075
4498
  }
4076
- if (!canManageStewards(ctx.userEmail)) {
4077
- return "You don't have permission to manage stewards. Only the super admin can do this.";
4499
+ if (!await canManageStewards(ctx.nestId, ctx.userId)) {
4500
+ return "You don't have permission to manage stewards. Only a nest admin, the nest owner, or the server admin can do this.";
4078
4501
  }
4079
4502
  try {
4080
4503
  await createStewardRecord({
@@ -4092,9 +4515,9 @@ ${list}`;
4092
4515
  }
4093
4516
  }
4094
4517
  case "context_share_nest": {
4095
- const roles = resolveUserRoles(ctx.nestId, ctx.userEmail);
4096
- if (!canManageWith(roles)) {
4097
- return "You don't have permission to share this nest via this tool (admin only). You can still invite from the UI with write access, or ask a nest admin.";
4518
+ const callerPermission = config.AUTH_MODE === "open" ? "owner" : await resolveNestPermission(ctx.nestId, ctx.userId);
4519
+ if (permissionLevel(callerPermission) < permissionLevel("write")) {
4520
+ return "You don't have permission to share this nest. Sharing needs write access \u2014 ask a nest admin or the owner.";
4098
4521
  }
4099
4522
  const permission = args.permission || "read";
4100
4523
  try {
@@ -4102,7 +4525,8 @@ ${list}`;
4102
4525
  nestId: ctx.nestId,
4103
4526
  email: args.email,
4104
4527
  permission,
4105
- grantedByEmail: ctx.userEmail
4528
+ grantedByEmail: ctx.userEmail,
4529
+ callerPermission
4106
4530
  });
4107
4531
  const label = permission === "admin" ? "admin" : permission === "write" ? "editor" : "viewer";
4108
4532
  return `Shared this nest with **${args.email}** as ${label}.`;
@@ -4111,7 +4535,7 @@ ${list}`;
4111
4535
  }
4112
4536
  }
4113
4537
  case "context_unsynced_list": {
4114
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
4538
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
4115
4539
  return "You don't have permission to list unsynced folders. Server admin only.";
4116
4540
  }
4117
4541
  const folders = listUnsyncedFolders();
@@ -4124,7 +4548,7 @@ ${list}`;
4124
4548
  ${list}`;
4125
4549
  }
4126
4550
  case "context_sync_folder": {
4127
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
4551
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
4128
4552
  return "You don't have permission to sync folders. Server admin only.";
4129
4553
  }
4130
4554
  try {
@@ -4146,9 +4570,9 @@ ${list}`;
4146
4570
  // src/mcp/routes.ts
4147
4571
  import { z } from "zod";
4148
4572
  var mcpRoutes = new Hono7();
4149
- function getUserEmail2(userId) {
4573
+ async function getUserEmail2(userId) {
4150
4574
  const db = getDb();
4151
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
4575
+ const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
4152
4576
  return user?.email || "anonymous@localhost";
4153
4577
  }
4154
4578
  function createMcpServerForNest(nestId, userId, userEmail) {
@@ -4156,7 +4580,6 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4156
4580
  { name: `contextnest-${nestId}`, version: "1.0.0" },
4157
4581
  { capabilities: { tools: {} } }
4158
4582
  );
4159
- const engine = engineCache.get(nestId);
4160
4583
  for (const tool of TOOL_DEFINITIONS) {
4161
4584
  const props = tool.inputSchema.properties || {};
4162
4585
  const required = tool.inputSchema.required || [];
@@ -4171,6 +4594,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4171
4594
  shape[key] = field;
4172
4595
  }
4173
4596
  server.tool(tool.name, tool.description, shape, async (args) => {
4597
+ const engine = await engineCache.get(nestId);
4174
4598
  const text = await handleToolCall(tool.name, args, {
4175
4599
  storage: engine.storage,
4176
4600
  queryEngine: engine.query,
@@ -4187,7 +4611,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4187
4611
  mcpRoutes.all("/", async (c) => {
4188
4612
  const nestId = c.req.param("nestId");
4189
4613
  const userId = c.get("userId");
4190
- const userEmail = getUserEmail2(userId);
4614
+ const userEmail = await getUserEmail2(userId);
4191
4615
  const server = createMcpServerForNest(nestId, userId, userEmail);
4192
4616
  const transport = new WebStandardStreamableHTTPServerTransport({
4193
4617
  sessionIdGenerator: void 0,
@@ -4208,41 +4632,43 @@ import { Hono as Hono8 } from "hono";
4208
4632
 
4209
4633
  // src/governance/comment-service.ts
4210
4634
  import { v4 as uuid4 } from "uuid";
4211
- function createComment(params) {
4635
+ async function createComment(params) {
4212
4636
  const db = getDb();
4213
4637
  const body = (params.body ?? "").trim();
4214
4638
  if (!body) {
4215
4639
  throw new Error("Comment body is required");
4216
4640
  }
4217
4641
  if (params.parentId) {
4218
- const parent = db.prepare(
4219
- "SELECT id FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
4220
- ).get(params.parentId, params.nestId, params.nodeId);
4642
+ const parent = await db.get(
4643
+ "SELECT id FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?",
4644
+ [params.parentId, params.nestId, params.nodeId]
4645
+ );
4221
4646
  if (!parent) {
4222
4647
  throw new Error("Parent comment not found on this node");
4223
4648
  }
4224
4649
  }
4225
4650
  const id = uuid4();
4226
- db.prepare(
4651
+ await db.run(
4227
4652
  `INSERT INTO comments
4228
4653
  (id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
4229
4654
  parent_id, author, body)
4230
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
4231
- ).run(
4232
- id,
4233
- params.nestId,
4234
- params.nodeId,
4235
- params.version ?? null,
4236
- params.anchor?.start ?? null,
4237
- params.anchor?.end ?? null,
4238
- params.anchor?.text ?? null,
4239
- params.parentId ?? null,
4240
- params.author,
4241
- body
4655
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4656
+ [
4657
+ id,
4658
+ params.nestId,
4659
+ params.nodeId,
4660
+ params.version ?? null,
4661
+ params.anchor?.start ?? null,
4662
+ params.anchor?.end ?? null,
4663
+ params.anchor?.text ?? null,
4664
+ params.parentId ?? null,
4665
+ params.author,
4666
+ body
4667
+ ]
4242
4668
  );
4243
- return getComment(id);
4669
+ return await getComment(id);
4244
4670
  }
4245
- function listComments(nestId, nodeId, opts = {}) {
4671
+ async function listComments(nestId, nodeId, opts = {}) {
4246
4672
  const db = getDb();
4247
4673
  const args = [nestId, nodeId];
4248
4674
  let statusClause = "";
@@ -4250,35 +4676,38 @@ function listComments(nestId, nodeId, opts = {}) {
4250
4676
  statusClause = " AND status = ?";
4251
4677
  args.push(opts.status);
4252
4678
  }
4253
- const rows = db.prepare(
4679
+ const rows = await db.all(
4254
4680
  `SELECT * FROM comments
4255
4681
  WHERE nest_id = ? AND node_id = ?${statusClause}
4256
- ORDER BY created_at ASC`
4257
- ).all(...args);
4682
+ ORDER BY created_at ASC`,
4683
+ args
4684
+ );
4258
4685
  return rows.map(rowToComment);
4259
4686
  }
4260
- function getComment(id) {
4687
+ async function getComment(id) {
4261
4688
  const db = getDb();
4262
- const row = db.prepare("SELECT * FROM comments WHERE id = ?").get(id);
4689
+ const row = await db.get("SELECT * FROM comments WHERE id = ?", [id]);
4263
4690
  return row ? rowToComment(row) : null;
4264
4691
  }
4265
- function resolveComment(params) {
4692
+ async function resolveComment(params) {
4266
4693
  const db = getDb();
4267
- const existing = db.prepare(
4268
- "SELECT id, status FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
4269
- ).get(params.commentId, params.nestId, params.nodeId);
4694
+ const existing = await db.get(
4695
+ "SELECT id, status FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?",
4696
+ [params.commentId, params.nestId, params.nodeId]
4697
+ );
4270
4698
  if (!existing) {
4271
4699
  throw new Error("Comment not found");
4272
4700
  }
4273
4701
  if (existing.status === "resolved") {
4274
4702
  throw new Error("Comment is already resolved");
4275
4703
  }
4276
- db.prepare(
4704
+ await db.run(
4277
4705
  `UPDATE comments
4278
- SET status = 'resolved', resolved_by = ?, resolved_at = datetime('now')
4279
- WHERE id = ?`
4280
- ).run(params.resolvedBy, params.commentId);
4281
- return getComment(params.commentId);
4706
+ SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)}
4707
+ WHERE id = ?`,
4708
+ [params.resolvedBy, params.commentId]
4709
+ );
4710
+ return await getComment(params.commentId);
4282
4711
  }
4283
4712
  async function getActivity(params) {
4284
4713
  const db = getDb();
@@ -4286,13 +4715,14 @@ async function getActivity(params) {
4286
4715
  const entries = [];
4287
4716
  const nodeFilter = params.nodeId ? " AND node_id = ?" : "";
4288
4717
  const baseArgs = params.nodeId ? [params.nestId, params.nodeId] : [params.nestId];
4289
- const commentRows = db.prepare(
4718
+ const commentRows = await db.all(
4290
4719
  `SELECT id, node_id, author, body, status, created_at, resolved_by, resolved_at
4291
4720
  FROM comments
4292
4721
  WHERE nest_id = ?${nodeFilter}
4293
4722
  ORDER BY COALESCE(resolved_at, created_at) DESC
4294
- LIMIT ?`
4295
- ).all(...baseArgs, limit);
4723
+ LIMIT ?`,
4724
+ [...baseArgs, limit]
4725
+ );
4296
4726
  for (const r of commentRows) {
4297
4727
  entries.push({
4298
4728
  type: "comment",
@@ -4313,13 +4743,14 @@ async function getActivity(params) {
4313
4743
  });
4314
4744
  }
4315
4745
  }
4316
- const versionRows = db.prepare(
4746
+ const versionRows = await db.all(
4317
4747
  `SELECT node_id, version, author, change_note, created_at
4318
4748
  FROM node_versions
4319
4749
  WHERE nest_id = ?${nodeFilter}
4320
4750
  ORDER BY created_at DESC
4321
- LIMIT ?`
4322
- ).all(...baseArgs, limit);
4751
+ LIMIT ?`,
4752
+ [...baseArgs, limit]
4753
+ );
4323
4754
  for (const r of versionRows) {
4324
4755
  entries.push({
4325
4756
  type: "edit",
@@ -4330,13 +4761,14 @@ async function getActivity(params) {
4330
4761
  refId: String(r.version)
4331
4762
  });
4332
4763
  }
4333
- const reviewRows = db.prepare(
4764
+ const reviewRows = await db.all(
4334
4765
  `SELECT id, node_id, requested_by, requested_at, status, resolved_by, resolved_at
4335
4766
  FROM review_requests
4336
4767
  WHERE nest_id = ?${nodeFilter}
4337
4768
  ORDER BY COALESCE(resolved_at, requested_at) DESC
4338
- LIMIT ?`
4339
- ).all(...baseArgs, limit);
4769
+ LIMIT ?`,
4770
+ [...baseArgs, limit]
4771
+ );
4340
4772
  for (const r of reviewRows) {
4341
4773
  entries.push({
4342
4774
  type: "review_requested",
@@ -4479,8 +4911,7 @@ function parseEntry(str) {
4479
4911
  return entry;
4480
4912
  }
4481
4913
  function loadStewardsConfig(nestId) {
4482
- const dataRoot = config.DATA_ROOT;
4483
- const nestPath = join3(dataRoot, "nests", nestId);
4914
+ const nestPath = resolveNestPath(nestId);
4484
4915
  const candidates = [
4485
4916
  join3(nestPath, "stewards.yaml"),
4486
4917
  join3(nestPath, "stewards.yml"),
@@ -4512,24 +4943,25 @@ governanceRoutes.get("/stewards", async (c) => {
4512
4943
  search: search || void 0
4513
4944
  });
4514
4945
  const cache = /* @__PURE__ */ new Map();
4515
- const enriched = stewards.map((s) => {
4946
+ const enriched = [];
4947
+ for (const s of stewards) {
4516
4948
  const key = s.userEmail.toLowerCase();
4517
4949
  let merged = cache.get(key);
4518
4950
  if (!merged) {
4519
4951
  merged = {
4520
- collaboratorRole: getCollaboratorRole(nestId, s.userEmail),
4521
- roles: resolveUserRoles(nestId, s.userEmail)
4952
+ collaboratorRole: await getCollaboratorRole(nestId, s.userEmail),
4953
+ roles: await resolveUserRoles(nestId, s.userEmail)
4522
4954
  };
4523
4955
  cache.set(key, merged);
4524
4956
  }
4525
- return { ...s, ...merged };
4526
- });
4957
+ enriched.push({ ...s, ...merged });
4958
+ }
4527
4959
  return c.json({ stewards: enriched });
4528
4960
  });
4529
4961
  governanceRoutes.post("/stewards", async (c) => {
4530
4962
  const nestId = c.req.param("nestId");
4531
4963
  const body = await c.req.json();
4532
- const assignedBy = getUserEmail3(c);
4964
+ const assignedBy = await getUserEmail3(c);
4533
4965
  if (!body.scope) throw new ValidationError("scope is required");
4534
4966
  if (body.scope === "folder") {
4535
4967
  throw new ValidationError(
@@ -4571,7 +5003,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
4571
5003
  if (!body.role && !body.scope) {
4572
5004
  throw new ValidationError("role or scope is required");
4573
5005
  }
4574
- const steward = updateSteward(stewardId, {
5006
+ const steward = await updateSteward(stewardId, {
4575
5007
  role: body.role,
4576
5008
  scope: body.scope,
4577
5009
  documentId: body.nodePattern,
@@ -4581,7 +5013,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
4581
5013
  });
4582
5014
  governanceRoutes.delete("/stewards/:stewardId", async (c) => {
4583
5015
  const stewardId = c.req.param("stewardId");
4584
- removeSteward(stewardId);
5016
+ await removeSteward(stewardId);
4585
5017
  return c.json({ removed: true });
4586
5018
  });
4587
5019
  governanceRoutes.post("/stewards/sync", async (c) => {
@@ -4590,7 +5022,7 @@ governanceRoutes.post("/stewards/sync", async (c) => {
4590
5022
  if (!stewardsConfig) {
4591
5023
  return c.json({ synced: 0, message: "No stewards.yaml found" });
4592
5024
  }
4593
- const count = syncFromConfig(nestId, stewardsConfig);
5025
+ const count = await syncFromConfig(nestId, stewardsConfig);
4594
5026
  return c.json({ synced: count });
4595
5027
  });
4596
5028
  governanceRoutes.get("/review-queue", async (c) => {
@@ -4604,16 +5036,17 @@ governanceRoutes.get("/review-queue", async (c) => {
4604
5036
  limit,
4605
5037
  offset
4606
5038
  });
4607
- const email = getUserEmail3(c);
5039
+ const email = await getUserEmail3(c);
4608
5040
  const canReviewCache = /* @__PURE__ */ new Map();
4609
- const requests = result.requests.map((r) => {
5041
+ const requests = [];
5042
+ for (const r of result.requests) {
4610
5043
  let canReview = canReviewCache.get(r.nodeId);
4611
5044
  if (canReview === void 0) {
4612
- canReview = canUserApprove(nestId, r.nodeId, email).allowed;
5045
+ canReview = (await canUserApprove(nestId, r.nodeId, email)).allowed;
4613
5046
  canReviewCache.set(r.nodeId, canReview);
4614
5047
  }
4615
- return { ...r, canReview };
4616
- });
5048
+ requests.push({ ...r, canReview });
5049
+ }
4617
5050
  return c.json({ ...result, requests });
4618
5051
  });
4619
5052
  governanceRoutes.get("/external-edits", async (c) => {
@@ -4627,7 +5060,7 @@ governanceRoutes.get("/external-edits", async (c) => {
4627
5060
  });
4628
5061
  governanceRoutes.post("/external-edits/scan", async (c) => {
4629
5062
  const nestId = c.req.param("nestId");
4630
- const actor = getUserEmail3(c);
5063
+ const actor = await getUserEmail3(c);
4631
5064
  const result = await scanNestForDrift(nestId, actor);
4632
5065
  return c.json(result);
4633
5066
  });
@@ -4641,7 +5074,7 @@ var governanceNodeRoutes = new Hono8();
4641
5074
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4642
5075
  const nestId = c.req.param("nestId");
4643
5076
  const nodeId = c.req.param("nodeId");
4644
- const { stewards: resolved, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback(nestId, nodeId);
5077
+ const { stewards: resolved, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback(nestId, nodeId);
4645
5078
  return c.json({
4646
5079
  nodeId,
4647
5080
  stewards: resolved.map((r) => ({
@@ -4658,9 +5091,9 @@ governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4658
5091
  governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4659
5092
  const nestId = c.req.param("nestId");
4660
5093
  const nodeId = c.req.param("nodeId");
4661
- const allVersions = getVersions(nestId, nodeId);
4662
- const approved = getApprovedVersion(nestId, nodeId);
4663
- const { versions: versionManager } = engineCache.get(nestId);
5094
+ const allVersions = await getVersions(nestId, nodeId);
5095
+ const approved = await getApprovedVersion(nestId, nodeId);
5096
+ const { versions: versionManager } = await engineCache.get(nestId);
4664
5097
  const withContent = await Promise.all(
4665
5098
  allVersions.map(async (v) => {
4666
5099
  try {
@@ -4680,14 +5113,14 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4680
5113
  governanceNodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4681
5114
  const nestId = c.req.param("nestId");
4682
5115
  const nodeId = c.req.param("nodeId");
4683
- const history = getReviewHistory(nestId, nodeId);
5116
+ const history = await getReviewHistory(nestId, nodeId);
4684
5117
  return c.json({ reviews: history });
4685
5118
  });
4686
5119
  governanceNodeRoutes.get("/:nodeId{.+}/comments", async (c) => {
4687
5120
  const nestId = c.req.param("nestId");
4688
5121
  const nodeId = c.req.param("nodeId");
4689
5122
  const status = c.req.query("status");
4690
- const list = listComments(nestId, nodeId, {
5123
+ const list = await listComments(nestId, nodeId, {
4691
5124
  status: status === "open" || status === "resolved" ? status : void 0
4692
5125
  });
4693
5126
  return c.json({ comments: list });
@@ -4696,9 +5129,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
4696
5129
  const nestId = c.req.param("nestId");
4697
5130
  const nodeId = c.req.param("nodeId");
4698
5131
  const body = await c.req.json();
4699
- const author = getUserEmail3(c);
5132
+ const author = await getUserEmail3(c);
4700
5133
  try {
4701
- const comment = createComment({
5134
+ const comment = await createComment({
4702
5135
  nestId,
4703
5136
  nodeId,
4704
5137
  author,
@@ -4720,9 +5153,9 @@ governanceNodeRoutes.post(
4720
5153
  const nestId = c.req.param("nestId");
4721
5154
  const nodeId = c.req.param("nodeId");
4722
5155
  const commentId = c.req.param("commentId");
4723
- const resolvedBy = getUserEmail3(c);
5156
+ const resolvedBy = await getUserEmail3(c);
4724
5157
  try {
4725
- const comment = resolveComment({
5158
+ const comment = await resolveComment({
4726
5159
  nestId,
4727
5160
  nodeId,
4728
5161
  commentId,
@@ -4749,14 +5182,14 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
4749
5182
  const nestId = c.req.param("nestId");
4750
5183
  const nodeId = c.req.param("nodeId");
4751
5184
  const body = await c.req.json();
4752
- const currentVersion = getCurrentVersion(nestId, nodeId);
5185
+ const currentVersion = await getCurrentVersion(nestId, nodeId);
4753
5186
  if (currentVersion === 0) {
4754
5187
  throw new ValidationError("Node has no versions to review");
4755
5188
  }
4756
- const userEmail = getUserEmail3(c);
5189
+ const userEmail = await getUserEmail3(c);
4757
5190
  let request;
4758
5191
  try {
4759
- request = submitForReview({
5192
+ request = await submitForReview({
4760
5193
  nestId,
4761
5194
  nodeId,
4762
5195
  version: currentVersion,
@@ -4771,7 +5204,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
4771
5204
  }
4772
5205
  throw err;
4773
5206
  }
4774
- const resolved = resolveStewardsForNode(nestId, nodeId);
5207
+ const resolved = await resolveStewardsForNode(nestId, nodeId);
4775
5208
  return c.json(
4776
5209
  {
4777
5210
  review: request,
@@ -4788,13 +5221,13 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
4788
5221
  const nestId = c.req.param("nestId");
4789
5222
  const nodeId = c.req.param("nodeId");
4790
5223
  const body = await c.req.json();
4791
- const userEmail = getUserEmail3(c);
5224
+ const userEmail = await getUserEmail3(c);
4792
5225
  const isAdmin = isSuperAdmin(userEmail);
4793
5226
  try {
4794
5227
  const request = await approve({
4795
5228
  nestId,
4796
5229
  nodeId,
4797
- version: getCurrentVersion(nestId, nodeId),
5230
+ version: await getCurrentVersion(nestId, nodeId),
4798
5231
  approvedBy: userEmail,
4799
5232
  note: body.note,
4800
5233
  override: body.override && isAdmin
@@ -4811,12 +5244,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
4811
5244
  if (!body.note) {
4812
5245
  throw new ValidationError("Rejection note is required");
4813
5246
  }
4814
- const userEmail = getUserEmail3(c);
5247
+ const userEmail = await getUserEmail3(c);
4815
5248
  try {
4816
- const request = reject({
5249
+ const request = await reject({
4817
5250
  nestId,
4818
5251
  nodeId,
4819
- version: getCurrentVersion(nestId, nodeId),
5252
+ version: await getCurrentVersion(nestId, nodeId),
4820
5253
  rejectedBy: userEmail,
4821
5254
  note: body.note
4822
5255
  });
@@ -4828,20 +5261,20 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
4828
5261
  governanceNodeRoutes.get("/:nodeId{.+}/can-access", async (c) => {
4829
5262
  const nestId = c.req.param("nestId");
4830
5263
  const nodeId = c.req.param("nodeId");
4831
- const userEmail = getUserEmail3(c);
4832
- return c.json(canUserAccess(nestId, nodeId, userEmail));
5264
+ const userEmail = await getUserEmail3(c);
5265
+ return c.json(await canUserAccess(nestId, nodeId, userEmail));
4833
5266
  });
4834
5267
  governanceNodeRoutes.get("/:nodeId{.+}/can-approve", async (c) => {
4835
5268
  const nestId = c.req.param("nestId");
4836
5269
  const nodeId = c.req.param("nodeId");
4837
- const userEmail = getUserEmail3(c);
4838
- return c.json(canUserApprove(nestId, nodeId, userEmail));
5270
+ const userEmail = await getUserEmail3(c);
5271
+ return c.json(await canUserApprove(nestId, nodeId, userEmail));
4839
5272
  });
4840
5273
  governanceNodeRoutes.get("/:nodeId{.+}/can-edit", async (c) => {
4841
5274
  const nestId = c.req.param("nestId");
4842
5275
  const nodeId = c.req.param("nodeId");
4843
- const userEmail = getUserEmail3(c);
4844
- return c.json(canUserEdit(nestId, nodeId, userEmail));
5276
+ const userEmail = await getUserEmail3(c);
5277
+ return c.json(await canUserEdit(nestId, nodeId, userEmail));
4845
5278
  });
4846
5279
  governanceNodeRoutes.get("/:nodeId{.+?}/external-edits", async (c) => {
4847
5280
  const nestId = c.req.param("nestId");
@@ -4873,7 +5306,7 @@ governanceNodeRoutes.post(
4873
5306
  const nodeId = c.req.param("nodeId");
4874
5307
  const suggestionId = c.req.param("suggestionId");
4875
5308
  const body = await c.req.json().catch(() => ({}));
4876
- const actor = getUserEmail3(c);
5309
+ const actor = await getUserEmail3(c);
4877
5310
  try {
4878
5311
  const result = await approveExternalEdit({
4879
5312
  nestId,
@@ -4909,7 +5342,7 @@ governanceNodeRoutes.post(
4909
5342
  if (!body.reason) {
4910
5343
  throw new ValidationError("Rejection reason is required");
4911
5344
  }
4912
- const actor = getUserEmail3(c);
5345
+ const actor = await getUserEmail3(c);
4913
5346
  try {
4914
5347
  const result = await rejectExternalEdit({
4915
5348
  nestId,
@@ -4930,31 +5363,38 @@ governanceNodeRoutes.post(
4930
5363
  governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
4931
5364
  const nestId = c.req.param("nestId");
4932
5365
  const nodeId = c.req.param("nodeId");
4933
- const userEmail = getUserEmail3(c);
4934
- const request = cancelReview({
5366
+ const userEmail = await getUserEmail3(c);
5367
+ const request = await cancelReview({
4935
5368
  nestId,
4936
5369
  nodeId,
4937
5370
  cancelledBy: userEmail
4938
5371
  });
4939
5372
  return c.json({ review: request });
4940
5373
  });
4941
- function getUserEmail3(c) {
5374
+ async function getUserEmail3(c) {
4942
5375
  const userId = c.get("userId");
4943
5376
  const db = getDb();
4944
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
5377
+ const user = await db.get(
5378
+ "SELECT email FROM users WHERE id = ?",
5379
+ [userId]
5380
+ );
4945
5381
  return user?.email || "anonymous@localhost";
4946
5382
  }
4947
5383
 
4948
5384
  // src/auth/anonymous.ts
4949
5385
  import bcrypt from "bcryptjs";
4950
- function ensureAnonymousUser() {
5386
+ async function ensureAnonymousUser() {
4951
5387
  const db = getDb();
4952
- const exists = db.prepare("SELECT id FROM users WHERE id = ?").get(ANON_USER_ID);
5388
+ const exists = await db.get(
5389
+ "SELECT id FROM users WHERE id = ?",
5390
+ [ANON_USER_ID]
5391
+ );
4953
5392
  if (!exists) {
4954
5393
  const placeholder = bcrypt.hashSync("anon-no-login", 4);
4955
- db.prepare(
4956
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
4957
- ).run(ANON_USER_ID, ANON_EMAIL, "Admin", placeholder);
5394
+ await db.run(
5395
+ "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
5396
+ [ANON_USER_ID, ANON_EMAIL, "Admin", placeholder]
5397
+ );
4958
5398
  }
4959
5399
  return ANON_USER_ID;
4960
5400
  }
@@ -4972,7 +5412,7 @@ var UI_DIR_CANDIDATES = [
4972
5412
  var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
4973
5413
  var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
4974
5414
  var openModeMiddleware = createMiddleware2(async (c, next) => {
4975
- const anonId = ensureAnonymousUser();
5415
+ const anonId = await ensureAnonymousUser();
4976
5416
  c.set("userId", anonId);
4977
5417
  c.set("nestScope", null);
4978
5418
  await next();
@@ -4980,6 +5420,7 @@ var openModeMiddleware = createMiddleware2(async (c, next) => {
4980
5420
  function isPublicReadEligiblePath(method, path) {
4981
5421
  if (method !== "GET") return false;
4982
5422
  if (!/^\/nests\/[^/]+(\/.*)?$/.test(path)) return false;
5423
+ if (/^\/nests\/[^/]+\/stewards/.test(path)) return false;
4983
5424
  return !/\/(collaborators|visibility|settings|mcp)/.test(path);
4984
5425
  }
4985
5426
  var flexAuthMiddleware = createMiddleware2(async (c, next) => {
@@ -4989,13 +5430,13 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
4989
5430
  return authMiddleware(c, next);
4990
5431
  }
4991
5432
  if (config.AUTH_MODE === "open") {
4992
- const anonId = ensureAnonymousUser();
5433
+ const anonId = await ensureAnonymousUser();
4993
5434
  c.set("userId", anonId);
4994
5435
  c.set("nestScope", null);
4995
5436
  return next();
4996
5437
  }
4997
5438
  if (isPublicReadEligiblePath(c.req.method, c.req.path)) {
4998
- const anonId = ensureAnonymousUser();
5439
+ const anonId = await ensureAnonymousUser();
4999
5440
  c.set("userId", anonId);
5000
5441
  c.set("nestScope", null);
5001
5442
  return next();
@@ -5117,7 +5558,7 @@ function createApp() {
5117
5558
  }
5118
5559
  });
5119
5560
  app.use("/admin/settings", flexAuthMiddleware);
5120
- const adminSettingsAllowed = (c) => config.AUTH_MODE === "open" || isLicenseAdminUserId(c.get("userId"));
5561
+ const adminSettingsAllowed = async (c) => config.AUTH_MODE === "open" || await isLicenseAdminUserId(c.get("userId"));
5121
5562
  const currentServerSettings = () => ({
5122
5563
  promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
5123
5564
  logo_url: config.LOGO_URL,
@@ -5125,13 +5566,13 @@ function createApp() {
5125
5566
  public_base_url: config.PUBLIC_BASE_URL,
5126
5567
  max_body_bytes: config.MAX_BODY_BYTES
5127
5568
  });
5128
- app.get("/admin/settings", (c) => {
5129
- if (!adminSettingsAllowed(c))
5569
+ app.get("/admin/settings", async (c) => {
5570
+ if (!await adminSettingsAllowed(c))
5130
5571
  return c.json({ error: "Only the server admin can view this." }, 403);
5131
5572
  return c.json(currentServerSettings());
5132
5573
  });
5133
5574
  app.patch("/admin/settings", async (c) => {
5134
- if (!adminSettingsAllowed(c))
5575
+ if (!await adminSettingsAllowed(c))
5135
5576
  return c.json({ error: "Only the server admin can change this." }, 403);
5136
5577
  let body;
5137
5578
  try {
@@ -5187,18 +5628,22 @@ function createApp() {
5187
5628
  app.get("/stats", async (c) => {
5188
5629
  const db = getDb();
5189
5630
  const userId = c.get("userId");
5190
- const userEmail = resolveCallerEmail(userId);
5191
- const visibleNests = [...listNests(userId), ...listSharedNests(userId)];
5631
+ const userEmail = await resolveCallerEmail(userId);
5632
+ const owned = await listNests(userId);
5633
+ const sharedNests = await listSharedNests(userId);
5634
+ const visibleNests = [...owned, ...sharedNests];
5192
5635
  let documents = 0;
5193
5636
  for (const nest of visibleNests) {
5194
5637
  try {
5195
- const { storage } = engineCache.get(nest.id);
5638
+ const { storage } = await engineCache.get(nest.id);
5196
5639
  const docs = await storage.discoverDocuments();
5197
- documents += filterAccessible(nest.id, userId, userEmail, docs).length;
5640
+ documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
5198
5641
  } catch {
5199
5642
  }
5200
5643
  }
5201
- const usersRow = db.prepare("SELECT COUNT(*) as c FROM users").get();
5644
+ const usersRow = await db.get(
5645
+ "SELECT COUNT(*) as c FROM users"
5646
+ );
5202
5647
  return c.json({
5203
5648
  nests: visibleNests.length,
5204
5649
  documents,
@@ -5252,7 +5697,7 @@ function createApp() {
5252
5697
  c.set("nestPermission", "owner");
5253
5698
  return next();
5254
5699
  }
5255
- const permission = resolveNestPermission(nestId, userId);
5700
+ const permission = await resolveNestPermission(nestId, userId);
5256
5701
  if (permission === "none") {
5257
5702
  return c.json({ error: "Nest not found" }, 404);
5258
5703
  }
@@ -5262,10 +5707,10 @@ function createApp() {
5262
5707
  const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
5263
5708
  const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
5264
5709
  const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
5265
- if (isStewardRoster && !canManageStewards(resolveCallerEmail(userId))) {
5710
+ if (isStewardRoster && permission !== "owner" && permission !== "admin") {
5266
5711
  return c.json(
5267
5712
  {
5268
- error: "You don't have permission to manage stewards. Only the super admin can do this."
5713
+ error: "You don't have permission to manage stewards. Only a nest admin, the nest owner, or the server admin can do this."
5269
5714
  },
5270
5715
  403
5271
5716
  );
@@ -5281,7 +5726,7 @@ function createApp() {
5281
5726
  const isNodeRevert = c.req.method === "POST" && parts.length >= 4 && parts[parts.length - 1] === "revert";
5282
5727
  let stewardEditorBypass = false;
5283
5728
  if (required === "write" && permission === "read" && parts[1] === "nodes") {
5284
- const userEmail = resolveCallerEmail(userId);
5729
+ const userEmail = await resolveCallerEmail(userId);
5285
5730
  if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE" || isNodeRevert)) {
5286
5731
  const idParts = isNodeRevert ? parts.slice(2, -1) : parts.slice(2);
5287
5732
  const rawNodeId = idParts.join("/");
@@ -5290,12 +5735,12 @@ function createApp() {
5290
5735
  nodeId = decodeURIComponent(rawNodeId);
5291
5736
  } catch {
5292
5737
  }
5293
- const resolved = resolveStewardsForNode(nestId, nodeId);
5738
+ const resolved = await resolveStewardsForNode(nestId, nodeId);
5294
5739
  stewardEditorBypass = resolved.some(
5295
5740
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor"
5296
5741
  );
5297
5742
  } else if (parts.length === 2 && c.req.method === "POST") {
5298
- const resolved = resolveStewardsForNode(nestId, "");
5743
+ const resolved = await resolveStewardsForNode(nestId, "");
5299
5744
  stewardEditorBypass = resolved.some(
5300
5745
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && r.steward.scope === "nest"
5301
5746
  );
@@ -5403,28 +5848,30 @@ function createApp() {
5403
5848
 
5404
5849
  // src/db/backfill.ts
5405
5850
  import { NestStorage } from "@promptowl/contextnest-engine";
5406
- import { join as join5 } from "path";
5407
5851
  var MIGRATION_ID = "005_backfill_node_versions_from_history";
5408
5852
  async function backfillNodeVersionsFromHistory(db) {
5409
- const already = db.prepare("SELECT id FROM schema_migrations WHERE id = ?").get(MIGRATION_ID);
5853
+ const already = await db.get(
5854
+ "SELECT id FROM schema_migrations WHERE id = ?",
5855
+ [MIGRATION_ID]
5856
+ );
5410
5857
  if (already) return;
5411
- const nests = db.prepare("SELECT id FROM nests").all();
5412
- const insert = db.prepare(
5413
- `INSERT OR IGNORE INTO node_versions
5858
+ const nests = await db.all("SELECT id FROM nests");
5859
+ const insertSql = insertOrIgnore(
5860
+ db,
5861
+ `INSERT INTO node_versions
5414
5862
  (nest_id, node_id, version, content_hash, author, status, change_note, created_at)
5415
5863
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
5416
5864
  );
5417
- const approvedPin = db.prepare(
5418
- `INSERT OR REPLACE INTO approved_versions
5865
+ const approvedPinSql = `INSERT INTO approved_versions
5419
5866
  (nest_id, node_id, approved_version, approved_by, approved_at)
5420
- VALUES (?, ?, ?, ?, COALESCE(
5421
- (SELECT approved_at FROM approved_versions WHERE nest_id = ? AND node_id = ?),
5422
- datetime('now')))`
5423
- );
5867
+ VALUES (?, ?, ?, ?, ${nowExpr(db)})
5868
+ ON CONFLICT (nest_id, node_id) DO UPDATE SET
5869
+ approved_version = excluded.approved_version,
5870
+ approved_by = excluded.approved_by`;
5424
5871
  let totalInserted = 0;
5425
5872
  let totalDocs = 0;
5426
5873
  for (const { id: nestId } of nests) {
5427
- const nestPath = join5(config.DATA_ROOT, "nests", nestId);
5874
+ const nestPath = resolveNestPath(nestId);
5428
5875
  const storage = new NestStorage(nestPath);
5429
5876
  let docs;
5430
5877
  try {
@@ -5445,15 +5892,16 @@ async function backfillNodeVersionsFromHistory(db) {
5445
5892
  history = null;
5446
5893
  }
5447
5894
  if (!history || history.versions.length === 0) continue;
5448
- const existing = db.prepare(
5449
- `SELECT version FROM node_versions WHERE nest_id = ? AND node_id = ?`
5450
- ).all(nestId, doc.id);
5895
+ const existing = await db.all(
5896
+ `SELECT version FROM node_versions WHERE nest_id = ? AND node_id = ?`,
5897
+ [nestId, doc.id]
5898
+ );
5451
5899
  const known = new Set(existing.map((r) => r.version));
5452
5900
  const tagsJson = doc.frontmatter.tags ? JSON.stringify(doc.frontmatter.tags) : null;
5453
5901
  const latestVersion = history.versions[history.versions.length - 1].version;
5454
5902
  for (const entry of history.versions) {
5455
5903
  if (known.has(entry.version)) continue;
5456
- insert.run(
5904
+ await db.run(insertSql, [
5457
5905
  nestId,
5458
5906
  doc.id,
5459
5907
  entry.version,
@@ -5462,33 +5910,33 @@ async function backfillNodeVersionsFromHistory(db) {
5462
5910
  "approved",
5463
5911
  entry.note || null,
5464
5912
  entry.edited_at || (/* @__PURE__ */ new Date()).toISOString()
5465
- );
5913
+ ]);
5466
5914
  totalInserted += 1;
5467
5915
  }
5468
- const pin = db.prepare(
5469
- `SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?`
5470
- ).get(nestId, doc.id);
5916
+ const pin = await db.get(
5917
+ `SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?`,
5918
+ [nestId, doc.id]
5919
+ );
5471
5920
  if (!pin || pin.approved_version < latestVersion) {
5472
- approvedPin.run(
5921
+ await db.run(approvedPinSql, [
5473
5922
  nestId,
5474
5923
  doc.id,
5475
5924
  latestVersion,
5476
- history.versions[history.versions.length - 1].edited_by || "system:backfill",
5477
- nestId,
5478
- doc.id
5479
- );
5925
+ history.versions[history.versions.length - 1].edited_by || "system:backfill"
5926
+ ]);
5480
5927
  }
5481
5928
  if (tagsJson) {
5482
- const updateTags = db.prepare(
5929
+ await db.run(
5483
5930
  `UPDATE node_versions SET tags_json = ?
5484
- WHERE nest_id = ? AND node_id = ? AND version = ? AND tags_json IS NULL`
5931
+ WHERE nest_id = ? AND node_id = ? AND version = ? AND tags_json IS NULL`,
5932
+ [tagsJson, nestId, doc.id, latestVersion]
5485
5933
  );
5486
- updateTags.run(tagsJson, nestId, doc.id, latestVersion);
5487
5934
  }
5488
5935
  }
5489
5936
  }
5490
- db.prepare("INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)").run(
5491
- MIGRATION_ID
5937
+ await db.run(
5938
+ insertOrIgnore(db, "INSERT INTO schema_migrations (id) VALUES (?)"),
5939
+ [MIGRATION_ID]
5492
5940
  );
5493
5941
  console.log(
5494
5942
  `[backfill] node_versions: scanned ${totalDocs} docs across ${nests.length} nests, inserted ${totalInserted} rows`
@@ -5497,7 +5945,7 @@ async function backfillNodeVersionsFromHistory(db) {
5497
5945
 
5498
5946
  // src/index.ts
5499
5947
  async function main() {
5500
- const db = getDb();
5948
+ const db = await initDb();
5501
5949
  try {
5502
5950
  await backfillNodeVersionsFromHistory(db);
5503
5951
  } catch (err) {
@@ -5531,7 +5979,8 @@ async function main() {
5531
5979
  }
5532
5980
  const app = createApp();
5533
5981
  startLicenseSafetyPoll();
5534
- const driftScanIntervalMs = Number(process.env.DRIFT_SCAN_INTERVAL_MS) || 3e4;
5982
+ const driftRaw = process.env.DRIFT_SCAN_INTERVAL_MS;
5983
+ const driftScanIntervalMs = driftRaw != null && driftRaw !== "" && Number.isFinite(Number(driftRaw)) ? Number(driftRaw) : 3e4;
5535
5984
  if (driftScanIntervalMs > 0) {
5536
5985
  startDriftScanner(driftScanIntervalMs);
5537
5986
  }
@@ -5542,11 +5991,13 @@ async function main() {
5542
5991
  licensed: license.valid
5543
5992
  });
5544
5993
  const authDesc = config.AUTH_MODE === "open" ? "open (no auth \u2014 single-user / LAN only)" : "key (API key required on every request)";
5994
+ const dbDesc = db.dialect === "postgres" ? `postgres (${config.CLOUD_SQL_CONNECTION_NAME ? `cloudsql ${config.CLOUD_SQL_CONNECTION_NAME}` : config.DATABASE_URL ? "DATABASE_URL" : `${config.DB_HOST || "localhost"}:${config.DB_PORT}/${config.DB_NAME}`})` : `sqlite (${config.DATABASE_PATH})`;
5545
5995
  console.log(`
5546
5996
  ContextNest Community Server v0.1.0
5547
5997
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5548
5998
  Port: ${config.PORT}
5549
5999
  Data: ${config.DATA_ROOT}
6000
+ Database: ${dbDesc}
5550
6001
  License: ${license.valid ? `${license.tier}${license.org ? ` (${license.org})` : ""}` : "unlicensed (register at promptowl.ai)"}
5551
6002
  Auth: ${authDesc}
5552
6003
  Telemetry: ${config.TELEMETRY_ENABLED ? "on" : "off"}