@promptowl/contextnest-community 1.4.0 → 1.6.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-TTXTEUFT.js";
19
+ } from "./chunk-MZGFKBOK.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-T5L4LYU4.js";
29
29
  import {
30
30
  AppError,
31
31
  ConflictError,
@@ -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,
@@ -80,14 +82,20 @@ import {
80
82
  trackEvent,
81
83
  uniqueNestName,
82
84
  updateSteward,
85
+ upsertEnvVar,
83
86
  validateLicense
84
- } from "./chunk-XNPD2Q6E.js";
87
+ } from "./chunk-2TUMMVBG.js";
85
88
  import {
86
- ANON_EMAIL,
87
- ANON_USER_ID,
88
89
  config,
89
- getDb
90
- } from "./chunk-RMU3LOPH.js";
90
+ getDb,
91
+ initDb,
92
+ insertOrIgnore,
93
+ nowExpr
94
+ } from "./chunk-7V33Z6CS.js";
95
+ import {
96
+ ANON_EMAIL,
97
+ ANON_USER_ID
98
+ } from "./chunk-SLTQACJW.js";
91
99
 
92
100
  // src/index.ts
93
101
  import { serve } from "@hono/node-server";
@@ -115,37 +123,40 @@ function newSessionId() {
115
123
  function expiryIso(ttlSeconds = SESSION_TTL_SECS) {
116
124
  return new Date(Date.now() + ttlSeconds * 1e3).toISOString();
117
125
  }
118
- function createSession(userId, userAgent) {
126
+ async function createSession(userId, userAgent) {
119
127
  const db = getDb();
120
128
  const id = newSessionId();
121
- db.prepare(
129
+ await db.run(
122
130
  `INSERT INTO sessions (id, user_id, expires_at, user_agent)
123
- VALUES (?, ?, ?, ?)`
124
- ).run(id, userId, expiryIso(), userAgent || null);
131
+ VALUES (?, ?, ?, ?)`,
132
+ [id, userId, expiryIso(), userAgent || null]
133
+ );
125
134
  return id;
126
135
  }
127
- function resolveSession(id) {
136
+ async function resolveSession(id) {
128
137
  const db = getDb();
129
- const row = db.prepare(
130
- `SELECT user_id, expires_at FROM sessions WHERE id = ?`
131
- ).get(id);
138
+ const row = await db.get(
139
+ `SELECT user_id, expires_at FROM sessions WHERE id = ?`,
140
+ [id]
141
+ );
132
142
  if (!row) return null;
133
143
  if (new Date(row.expires_at).getTime() <= Date.now()) {
134
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
144
+ await db.run("DELETE FROM sessions WHERE id = ?", [id]);
135
145
  return null;
136
146
  }
137
- db.prepare(
138
- "UPDATE sessions SET last_seen_at = datetime('now') WHERE id = ?"
139
- ).run(id);
147
+ await db.run(
148
+ `UPDATE sessions SET last_seen_at = ${nowExpr(db)} WHERE id = ?`,
149
+ [id]
150
+ );
140
151
  return row.user_id;
141
152
  }
142
- function deleteSession(id) {
153
+ async function deleteSession(id) {
143
154
  const db = getDb();
144
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
155
+ await db.run("DELETE FROM sessions WHERE id = ?", [id]);
145
156
  }
146
- function deleteAllSessionsForUser(userId) {
157
+ async function deleteAllSessionsForUser(userId) {
147
158
  const db = getDb();
148
- db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
159
+ await db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
149
160
  }
150
161
  function getSessionIdFromRequest(c) {
151
162
  const cookieHeader = c.req.header("Cookie");
@@ -189,7 +200,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
189
200
  const db = getDb();
190
201
  const sessionId = getSessionIdFromRequest(c);
191
202
  if (sessionId) {
192
- const userId = resolveSession(sessionId);
203
+ const userId = await resolveSession(sessionId);
193
204
  if (userId) {
194
205
  c.set("userId", userId);
195
206
  c.set("nestScope", null);
@@ -199,11 +210,15 @@ var authMiddleware = createMiddleware(async (c, next) => {
199
210
  const key = parseBearerToken(c.req.header("Authorization"));
200
211
  if (key) {
201
212
  const keyHash = hashApiKey(key);
202
- 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
+ );
203
217
  if (record) {
204
- db.prepare(
205
- "UPDATE api_keys SET last_used_at = datetime('now') WHERE key_hash = ?"
206
- ).run(keyHash);
218
+ await db.run(
219
+ `UPDATE api_keys SET last_used_at = ${nowExpr(db)} WHERE key_hash = ?`,
220
+ [keyHash]
221
+ );
207
222
  c.set("userId", record.user_id);
208
223
  c.set("nestScope", record.nest_id);
209
224
  return next();
@@ -303,6 +318,17 @@ var LOGIN_LIMIT = { max: 5, windowMs: 15 * 6e4 };
303
318
  var REGISTER_LIMIT = { max: 3, windowMs: 60 * 6e4 };
304
319
  var DEVICE_LIMIT = { max: 10, windowMs: 15 * 6e4 };
305
320
  var warnedMissingSsoAudience = false;
321
+ var MIN_PASSWORD_LENGTH = 8;
322
+ function assertValidPassword(password, label = "password") {
323
+ if (password.trim().length === 0) {
324
+ throw new ValidationError(`${label} cannot be empty or only whitespace`);
325
+ }
326
+ if (password.length < MIN_PASSWORD_LENGTH) {
327
+ throw new ValidationError(
328
+ `${label} must be at least ${MIN_PASSWORD_LENGTH} characters`
329
+ );
330
+ }
331
+ }
306
332
  function clientIp(c) {
307
333
  const xff = c.req.header("x-forwarded-for");
308
334
  if (xff) return xff.split(",")[0].trim();
@@ -315,27 +341,30 @@ function clientIp(c) {
315
341
  }
316
342
  return "unknown";
317
343
  }
318
- function resolveCallerUserId(c) {
344
+ async function resolveCallerUserId(c) {
319
345
  const sessionId = getSessionIdFromRequest(c);
320
346
  if (sessionId) {
321
- const uid = resolveSession(sessionId);
347
+ const uid = await resolveSession(sessionId);
322
348
  if (uid) return uid;
323
349
  }
324
350
  const key = parseBearerToken(c.req.header("Authorization"));
325
351
  if (key) {
326
352
  const db = getDb();
327
- 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
+ );
328
357
  if (row) return row.user_id;
329
358
  }
330
359
  return null;
331
360
  }
332
- function deviceGateBlocked(c) {
361
+ async function deviceGateBlocked(c) {
333
362
  const gate = config.PROMPTOWL_SIGN_IN_GATE;
334
363
  if (gate === "open") return null;
335
364
  const error = "PromptOwl sign-in is restricted on this server. Use email and password, or contact your admin.";
336
365
  if (gate === "disabled") return { error, gate };
337
- const callerId = resolveCallerUserId(c);
338
- if (callerId && !isLicenseAdminUserId(callerId)) return { error, gate };
366
+ const callerId = await resolveCallerUserId(c);
367
+ if (callerId && !await isLicenseAdminUserId(callerId)) return { error, gate };
339
368
  return null;
340
369
  }
341
370
  function setSessionCookie(c, sessionId) {
@@ -364,13 +393,17 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
364
393
  }
365
394
  const db = getDb();
366
395
  const meEmail = normalizeEmail(rawEmail);
367
- 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
+ );
368
400
  if (!user) {
369
401
  const userId = uuid();
370
402
  const placeholderHash = await hashPassword(uuid());
371
- db.prepare(
372
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
373
- ).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
+ );
374
407
  user = { id: userId, email: meEmail, name: rawName || null };
375
408
  trackEvent("user.register", {
376
409
  userId,
@@ -394,7 +427,7 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
394
427
  license_owner_email: lic.ownerEmail
395
428
  };
396
429
  }
397
- const sessionId = createSession(user.id, c.req.header("User-Agent"));
430
+ const sessionId = await createSession(user.id, c.req.header("User-Agent"));
398
431
  setSessionCookie(c, sessionId);
399
432
  return { ok: true, user, isAdmin, claimBlocked };
400
433
  }
@@ -404,31 +437,37 @@ authRoutes.post("/register", async (c) => {
404
437
  if (!body.email || !body.password) {
405
438
  throw new ValidationError("email and password are required");
406
439
  }
440
+ assertValidPassword(body.password);
407
441
  const email = normalizeEmail(body.email);
408
442
  const ip = clientIp(c);
409
443
  if (!tryConsume(`register:ip:${ip}`, REGISTER_LIMIT)) {
410
444
  return c.json({ error: "Too many registration attempts, try again later" }, 429);
411
445
  }
412
446
  const db = getDb();
413
- 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
+ );
414
451
  let userId;
415
452
  const passwordHash = await hashPassword(body.password);
416
453
  if (existing && existing.is_invited === 1) {
417
454
  userId = existing.id;
418
- db.prepare(
419
- "UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?"
420
- ).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
+ );
421
459
  trackEvent("user.register", { userId, email, claimed: true });
422
460
  } else if (existing) {
423
461
  throw new ValidationError("Email already registered");
424
462
  } else {
425
463
  userId = uuid();
426
- db.prepare(
427
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
428
- ).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
+ );
429
468
  trackEvent("user.register", { userId, email });
430
469
  }
431
- const sessionId = createSession(userId, c.req.header("User-Agent"));
470
+ const sessionId = await createSession(userId, c.req.header("User-Agent"));
432
471
  setSessionCookie(c, sessionId);
433
472
  return c.json(
434
473
  {
@@ -456,9 +495,10 @@ authRoutes.post("/login", async (c) => {
456
495
  return c.json({ error: "Too many login attempts, try again later" }, 429);
457
496
  }
458
497
  const db = getDb();
459
- const user = db.prepare(
460
- "SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?"
461
- ).get(emailLower);
498
+ const user = await db.get(
499
+ "SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?",
500
+ [emailLower]
501
+ );
462
502
  const check = user ? await verifyPassword(body.password, user.password_hash) : { ok: false, needsRehash: false };
463
503
  if (!user || !check.ok) {
464
504
  if (hasIp) recordFailure(ipKey, LOGIN_LIMIT);
@@ -472,15 +512,15 @@ authRoutes.post("/login", async (c) => {
472
512
  if (check.needsRehash) {
473
513
  try {
474
514
  const newHash = await hashPassword(body.password);
475
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
515
+ await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
476
516
  newHash,
477
517
  user.id
478
- );
518
+ ]);
479
519
  } catch {
480
520
  }
481
521
  }
482
522
  trackEvent("user.login", { userId: user.id });
483
- const sessionId = createSession(user.id, c.req.header("User-Agent"));
523
+ const sessionId = await createSession(user.id, c.req.header("User-Agent"));
484
524
  setSessionCookie(c, sessionId);
485
525
  return c.json({
486
526
  user: {
@@ -493,7 +533,7 @@ authRoutes.post("/login", async (c) => {
493
533
  });
494
534
  authRoutes.post("/logout", async (c) => {
495
535
  const sessionId = getSessionIdFromRequest(c);
496
- if (sessionId) deleteSession(sessionId);
536
+ if (sessionId) await deleteSession(sessionId);
497
537
  clearSessionCookie(c);
498
538
  return c.json({ ok: true });
499
539
  });
@@ -503,7 +543,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
503
543
  );
504
544
  const db = getDb();
505
545
  const userId = c.get("userId");
506
- const existing = db.prepare("SELECT key_prefix FROM api_keys WHERE user_id = ?").get(userId);
546
+ const existing = await db.get(
547
+ "SELECT key_prefix FROM api_keys WHERE user_id = ?",
548
+ [userId]
549
+ );
507
550
  if (existing) {
508
551
  return c.json(
509
552
  {
@@ -515,15 +558,16 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
515
558
  }
516
559
  const apiKey = generateApiKey();
517
560
  const keyId = uuid();
518
- db.prepare(
519
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
520
- ).run(
521
- keyId,
522
- userId,
523
- hashApiKey(apiKey),
524
- getKeyPrefix(apiKey),
525
- body.nest_id || null,
526
- body.label || null
561
+ await db.run(
562
+ "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
563
+ [
564
+ keyId,
565
+ userId,
566
+ hashApiKey(apiKey),
567
+ getKeyPrefix(apiKey),
568
+ body.nest_id || null,
569
+ body.label || null
570
+ ]
527
571
  );
528
572
  return c.json(
529
573
  { api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
@@ -538,39 +582,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
538
582
  const userId = c.get("userId");
539
583
  const apiKey = generateApiKey();
540
584
  const keyId = uuid();
541
- const prior = db.prepare("SELECT label, nest_id FROM api_keys WHERE user_id = ?").get(userId);
542
- db.transaction(() => {
543
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(userId);
544
- db.prepare(
545
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
546
- ).run(
547
- keyId,
548
- userId,
549
- hashApiKey(apiKey),
550
- getKeyPrefix(apiKey),
551
- body.nest_id ?? prior?.nest_id ?? null,
552
- body.label ?? prior?.label ?? null
585
+ const prior = await db.get(
586
+ "SELECT label, nest_id FROM api_keys WHERE user_id = ?",
587
+ [userId]
588
+ );
589
+ await db.transaction(async (tx) => {
590
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [userId]);
591
+ await tx.run(
592
+ "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
593
+ [
594
+ keyId,
595
+ userId,
596
+ hashApiKey(apiKey),
597
+ getKeyPrefix(apiKey),
598
+ body.nest_id ?? prior?.nest_id ?? null,
599
+ body.label ?? prior?.label ?? null
600
+ ]
553
601
  );
554
- })();
602
+ });
555
603
  return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
556
604
  });
557
605
  authRoutes.get("/keys", authMiddleware, async (c) => {
558
606
  const db = getDb();
559
- const keys = db.prepare(
560
- "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?"
561
- ).all(c.get("userId"));
607
+ const keys = await db.all(
608
+ "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
609
+ [c.get("userId")]
610
+ );
562
611
  return c.json({ keys });
563
612
  });
564
613
  authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
565
614
  const db = getDb();
566
- const result = db.prepare("DELETE FROM api_keys WHERE id = ? AND user_id = ?").run(c.req.param("keyId"), c.get("userId"));
615
+ const result = await db.run(
616
+ "DELETE FROM api_keys WHERE id = ? AND user_id = ?",
617
+ [c.req.param("keyId"), c.get("userId")]
618
+ );
567
619
  if (result.changes === 0) {
568
620
  return c.json({ error: "Key not found" }, 404);
569
621
  }
570
622
  return c.json({ deleted: true });
571
623
  });
572
624
  authRoutes.post("/device", async (c) => {
573
- const blocked = deviceGateBlocked(c);
625
+ const blocked = await deviceGateBlocked(c);
574
626
  if (blocked) return c.json(blocked, 403);
575
627
  if (!tryConsume(`device:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
576
628
  return c.json({ error: "Too many device auth attempts, try again later" }, 429);
@@ -593,7 +645,7 @@ authRoutes.post("/device", async (c) => {
593
645
  return c.json(data);
594
646
  });
595
647
  authRoutes.get("/device/poll", async (c) => {
596
- const blocked = deviceGateBlocked(c);
648
+ const blocked = await deviceGateBlocked(c);
597
649
  if (blocked) return c.json(blocked, 403);
598
650
  const code = c.req.query("code");
599
651
  const clientSecret = c.req.query("client_secret");
@@ -676,12 +728,13 @@ authRoutes.get("/sso", async (c) => {
676
728
  }
677
729
  if (!claims.jti) return ssoError("invalid_ticket");
678
730
  const db = getDb();
679
- db.prepare("DELETE FROM sso_used_jti WHERE expires_at < datetime('now')").run();
731
+ await db.run(`DELETE FROM sso_used_jti WHERE expires_at < ${nowExpr(db)}`);
680
732
  const expiresAtIso = new Date((claims.exp ?? 0) * 1e3).toISOString();
681
733
  try {
682
- db.prepare(
683
- "INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)"
684
- ).run(claims.jti, expiresAtIso);
734
+ await db.run(
735
+ "INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)",
736
+ [claims.jti, expiresAtIso]
737
+ );
685
738
  } catch (err) {
686
739
  if (err?.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
687
740
  return ssoError("ticket_used");
@@ -693,7 +746,7 @@ authRoutes.get("/sso", async (c) => {
693
746
  try {
694
747
  result = await provisionPromptowlUser(c, sub, claims.name);
695
748
  } catch (err) {
696
- db.prepare("DELETE FROM sso_used_jti WHERE jti = ?").run(claims.jti);
749
+ await db.run("DELETE FROM sso_used_jti WHERE jti = ?", [claims.jti]);
697
750
  console.error("[sso] provisioning failed; released jti for retry:", err);
698
751
  return ssoError("service_error");
699
752
  }
@@ -706,13 +759,19 @@ authRoutes.get("/admin-status", async (c) => {
706
759
  const ownerEmail = lic?.valid ? lic.ownerEmail : null;
707
760
  let admin = null;
708
761
  if (ownerEmail) {
709
- const ownerRow = db.prepare("SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1").get(ownerEmail);
762
+ const ownerRow = await db.get(
763
+ "SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1",
764
+ [ownerEmail]
765
+ );
710
766
  admin = { email: ownerEmail, name: ownerRow?.name ?? null };
711
767
  }
712
- const callerId = resolveCallerUserId(c);
768
+ const callerId = await resolveCallerUserId(c);
713
769
  let me = null;
714
770
  if (callerId) {
715
- const row = db.prepare("SELECT email, name FROM users WHERE id = ?").get(callerId);
771
+ const row = await db.get(
772
+ "SELECT email, name FROM users WHERE id = ?",
773
+ [callerId]
774
+ );
716
775
  if (row) {
717
776
  me = {
718
777
  email: row.email,
@@ -732,12 +791,13 @@ authRoutes.post("/password", authMiddleware, async (c) => {
732
791
  if (!body.current || !body.next) {
733
792
  throw new ValidationError("current and next are required");
734
793
  }
735
- if (body.next.length < 8) {
736
- throw new ValidationError("new password must be at least 8 characters");
737
- }
794
+ assertValidPassword(body.next, "new password");
738
795
  const db = getDb();
739
796
  const userId = c.get("userId");
740
- const user = db.prepare("SELECT password_hash FROM users WHERE id = ?").get(userId);
797
+ const user = await db.get(
798
+ "SELECT password_hash FROM users WHERE id = ?",
799
+ [userId]
800
+ );
741
801
  if (!user) throw new ValidationError("User not found");
742
802
  const check = await verifyPassword(body.current, user.password_hash);
743
803
  if (!check.ok) {
@@ -750,20 +810,20 @@ authRoutes.post("/password", authMiddleware, async (c) => {
750
810
  );
751
811
  }
752
812
  const newHash = await hashPassword(body.next);
753
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
813
+ await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
754
814
  newHash,
755
815
  userId
756
- );
757
- deleteAllSessionsForUser(userId);
816
+ ]);
817
+ await deleteAllSessionsForUser(userId);
758
818
  clearSessionCookie(c);
759
819
  return c.json({ ok: true });
760
820
  });
761
821
  authRoutes.post("/admin/reset-password/:userId", async (c) => {
762
- const callerId = resolveCallerUserId(c);
822
+ const callerId = await resolveCallerUserId(c);
763
823
  if (!callerId) {
764
824
  return c.json({ error: "Authentication required." }, 401);
765
825
  }
766
- if (!isLicenseAdminUserId(callerId)) {
826
+ if (!await isLicenseAdminUserId(callerId)) {
767
827
  return c.json(
768
828
  { error: "Only the license-admin user can reset passwords." },
769
829
  403
@@ -771,7 +831,10 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
771
831
  }
772
832
  const targetId = c.req.param("userId");
773
833
  const db = getDb();
774
- const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
834
+ const target = await db.get(
835
+ "SELECT id, email FROM users WHERE id = ?",
836
+ [targetId]
837
+ );
775
838
  if (!target) return c.json({ error: "User not found" }, 404);
776
839
  let supplied;
777
840
  try {
@@ -779,20 +842,20 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
779
842
  } catch {
780
843
  supplied = void 0;
781
844
  }
782
- if (supplied && supplied.length < 8) {
783
- throw new ValidationError("password must be at least 8 characters");
845
+ if (supplied !== void 0) {
846
+ assertValidPassword(supplied);
784
847
  }
785
848
  const generated = supplied ? null : uuid().replace(/-/g, "").slice(0, 16);
786
849
  const newPassword = supplied ?? generated;
787
850
  const newHash = await hashPassword(newPassword);
788
- db.transaction(() => {
789
- db.prepare("UPDATE users SET password_hash = ? WHERE id = ?").run(
851
+ await db.transaction(async (tx) => {
852
+ await tx.run("UPDATE users SET password_hash = ? WHERE id = ?", [
790
853
  newHash,
791
854
  target.id
792
- );
793
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
794
- })();
795
- deleteAllSessionsForUser(target.id);
855
+ ]);
856
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [target.id]);
857
+ });
858
+ await deleteAllSessionsForUser(target.id);
796
859
  trackEvent("admin.reset_password", { adminId: callerId, userId: target.id });
797
860
  return c.json({
798
861
  ok: true,
@@ -803,11 +866,11 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
803
866
  });
804
867
  });
805
868
  authRoutes.delete("/users/:userId", async (c) => {
806
- const callerId = resolveCallerUserId(c);
869
+ const callerId = await resolveCallerUserId(c);
807
870
  if (!callerId) {
808
871
  return c.json({ error: "Authentication required." }, 401);
809
872
  }
810
- if (!isLicenseAdminUserId(callerId)) {
873
+ if (!await isLicenseAdminUserId(callerId)) {
811
874
  return c.json(
812
875
  { error: "Only the license-admin user can remove users." },
813
876
  403
@@ -818,9 +881,15 @@ authRoutes.delete("/users/:userId", async (c) => {
818
881
  return c.json({ error: "You can't remove your own admin account." }, 400);
819
882
  }
820
883
  const db = getDb();
821
- const target = db.prepare("SELECT id, email FROM users WHERE id = ?").get(targetId);
884
+ const target = await db.get(
885
+ "SELECT id, email FROM users WHERE id = ?",
886
+ [targetId]
887
+ );
822
888
  if (!target) return c.json({ error: "User not found" }, 404);
823
- const ownedNests = db.prepare("SELECT COUNT(*) AS c FROM nests WHERE user_id = ?").get(target.id).c;
889
+ const ownedNests = (await db.get(
890
+ "SELECT COUNT(*) AS c FROM nests WHERE user_id = ?",
891
+ [target.id]
892
+ )).c;
824
893
  if (ownedNests > 0) {
825
894
  return c.json(
826
895
  {
@@ -830,17 +899,18 @@ authRoutes.delete("/users/:userId", async (c) => {
830
899
  409
831
900
  );
832
901
  }
833
- db.transaction(() => {
834
- db.prepare(
835
- "DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)"
836
- ).run(target.id, target.email);
837
- db.prepare("DELETE FROM nest_collaborators WHERE user_id = ?").run(
838
- target.id
902
+ await db.transaction(async (tx) => {
903
+ await tx.run(
904
+ "DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)",
905
+ [target.id, target.email]
839
906
  );
840
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(target.id);
841
- db.prepare("DELETE FROM users WHERE id = ?").run(target.id);
842
- })();
843
- deleteAllSessionsForUser(target.id);
907
+ await tx.run("DELETE FROM nest_collaborators WHERE user_id = ?", [
908
+ target.id
909
+ ]);
910
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [target.id]);
911
+ await tx.run("DELETE FROM users WHERE id = ?", [target.id]);
912
+ });
913
+ await deleteAllSessionsForUser(target.id);
844
914
  trackEvent("admin.remove_user", { adminId: callerId, userId: target.id });
845
915
  return c.json({ ok: true, email: target.email });
846
916
  });
@@ -848,7 +918,7 @@ authRoutes.post("/invite", async (c) => {
848
918
  const body = await c.req.json();
849
919
  if (!body.email) throw new ValidationError("email is required");
850
920
  const email = normalizeEmail(body.email);
851
- const callerId = resolveCallerUserId(c);
921
+ const callerId = await resolveCallerUserId(c);
852
922
  if (!callerId) {
853
923
  return c.json(
854
924
  {
@@ -858,7 +928,7 @@ authRoutes.post("/invite", async (c) => {
858
928
  );
859
929
  }
860
930
  const db = getDb();
861
- if (!isLicenseAdminUserId(callerId)) {
931
+ if (!await isLicenseAdminUserId(callerId)) {
862
932
  return c.json(
863
933
  {
864
934
  error: "Only the license-admin user can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
@@ -866,29 +936,34 @@ authRoutes.post("/invite", async (c) => {
866
936
  403
867
937
  );
868
938
  }
869
- let user = db.prepare("SELECT id, email FROM users WHERE LOWER(email) = ?").get(email);
939
+ let user = await db.get(
940
+ "SELECT id, email FROM users WHERE LOWER(email) = ?",
941
+ [email]
942
+ );
870
943
  if (!user) {
871
944
  const userId = uuid();
872
945
  const placeholderHash = await hashPassword(uuid());
873
- db.prepare(
874
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
875
- ).run(userId, email, null, placeholderHash);
946
+ await db.run(
947
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
948
+ [userId, email, null, placeholderHash]
949
+ );
876
950
  user = { id: userId, email };
877
951
  }
878
952
  const apiKey = generateApiKey();
879
953
  const keyId = uuid();
880
- db.transaction(() => {
881
- db.prepare("DELETE FROM api_keys WHERE user_id = ?").run(user.id);
882
- db.prepare(
883
- "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
884
- ).run(
885
- keyId,
886
- user.id,
887
- hashApiKey(apiKey),
888
- getKeyPrefix(apiKey),
889
- body.label || "teammate"
954
+ await db.transaction(async (tx) => {
955
+ await tx.run("DELETE FROM api_keys WHERE user_id = ?", [user.id]);
956
+ await tx.run(
957
+ "INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)",
958
+ [
959
+ keyId,
960
+ user.id,
961
+ hashApiKey(apiKey),
962
+ getKeyPrefix(apiKey),
963
+ body.label || "teammate"
964
+ ]
890
965
  );
891
- })();
966
+ });
892
967
  trackEvent("admin.invite", { adminId: callerId, email: body.email });
893
968
  return c.json(
894
969
  {
@@ -900,7 +975,7 @@ authRoutes.post("/invite", async (c) => {
900
975
  );
901
976
  });
902
977
  authRoutes.get("/teammates", async (c) => {
903
- const callerId = resolveCallerUserId(c);
978
+ const callerId = await resolveCallerUserId(c);
904
979
  if (!callerId) {
905
980
  return c.json(
906
981
  {
@@ -910,7 +985,7 @@ authRoutes.get("/teammates", async (c) => {
910
985
  );
911
986
  }
912
987
  const db = getDb();
913
- if (!isLicenseAdminUserId(callerId)) {
988
+ if (!await isLicenseAdminUserId(callerId)) {
914
989
  return c.json(
915
990
  {
916
991
  error: "Only the license-admin user can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
@@ -918,15 +993,16 @@ authRoutes.get("/teammates", async (c) => {
918
993
  403
919
994
  );
920
995
  }
921
- const teammates = db.prepare(
996
+ const teammates = await db.all(
922
997
  `SELECT u.id, u.email, u.name, u.is_invited,
923
998
  (SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
924
999
  (SELECT MAX(last_used_at) FROM api_keys WHERE user_id = u.id) as last_active
925
1000
  FROM users u
926
1001
  WHERE u.id != ?
927
- ORDER BY u.created_at DESC`
928
- ).all(ANON_USER_ID);
929
- const pendingStewards = db.prepare(
1002
+ ORDER BY u.created_at DESC`,
1003
+ [ANON_USER_ID]
1004
+ );
1005
+ const pendingStewards = await db.all(
930
1006
  `SELECT DISTINCT s.user_email AS email
931
1007
  FROM stewards s
932
1008
  WHERE s.is_active = 1
@@ -941,7 +1017,7 @@ authRoutes.get("/teammates", async (c) => {
941
1017
  )
942
1018
  )
943
1019
  ORDER BY s.user_email`
944
- ).all();
1020
+ );
945
1021
  const enriched = teammates.map((t) => ({ ...t, is_admin: isLicenseAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
946
1022
  return c.json({
947
1023
  teammates: enriched,
@@ -959,52 +1035,70 @@ import { serializeDocument, parseDocument as parseDocument2 } from "@promptowl/c
959
1035
  function normalizeTag(raw) {
960
1036
  return raw.trim().replace(/^#+/, "").toLowerCase();
961
1037
  }
962
- function syncNodeTags(nestId, nodeId, tags) {
1038
+ async function syncNodeTags(nestId, nodeId, tags) {
963
1039
  const db = getDb();
964
1040
  const normalized = Array.from(
965
1041
  new Set(
966
1042
  tags.filter((t) => typeof t === "string").map(normalizeTag).filter(Boolean)
967
1043
  )
968
1044
  );
969
- db.transaction(() => {
970
- db.prepare(
971
- "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?"
972
- ).run(nestId, nodeId);
973
- const insert = db.prepare(
974
- "INSERT OR IGNORE INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1045
+ const insertSql = insertOrIgnore(
1046
+ db,
1047
+ "INSERT INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1048
+ );
1049
+ await db.transaction(async (tx) => {
1050
+ await tx.run(
1051
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1052
+ [nestId, nodeId]
975
1053
  );
976
1054
  for (const tag of normalized) {
977
- insert.run(nestId, nodeId, tag);
1055
+ await tx.run(insertSql, [nestId, nodeId, tag]);
978
1056
  }
979
- })();
1057
+ });
980
1058
  }
981
- function removeNodeFromTagIndex(nestId, nodeId) {
1059
+ async function removeNodeFromTagIndex(nestId, nodeId) {
982
1060
  const db = getDb();
983
- db.prepare(
984
- "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?"
985
- ).run(nestId, nodeId);
1061
+ await db.run(
1062
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1063
+ [nestId, nodeId]
1064
+ );
986
1065
  }
987
1066
 
988
1067
  // src/governance/access-guard.ts
989
- function resolveCallerEmail(userId) {
1068
+ async function resolveCallerEmail(userId) {
990
1069
  if (!userId) return "admin@localhost";
991
1070
  const db = getDb();
992
- const row = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
1071
+ const row = await db.get(
1072
+ "SELECT email FROM users WHERE id = ?",
1073
+ [userId]
1074
+ );
993
1075
  return row?.email || "admin@localhost";
994
1076
  }
995
- function canReadNode(nestId, nodeId, userId, userEmail) {
996
- if (isPublicReader(nestId, userId)) {
997
- return getApprovedVersion(nestId, nodeId) !== null;
1077
+ async function canReadNode(nestId, nodeId, userId, userEmail) {
1078
+ if (await isPublicReader(nestId, userId)) {
1079
+ return await getApprovedVersion(nestId, nodeId) !== null;
998
1080
  }
999
- if (!isStewardshipEnabled(nestId)) return true;
1000
- return canUserAccess(nestId, nodeId, userEmail).allowed;
1081
+ if (!await isStewardshipEnabled(nestId)) return true;
1082
+ return (await canUserAccess(nestId, nodeId, userEmail)).allowed;
1001
1083
  }
1002
- function filterAccessible(nestId, userId, userEmail, nodes) {
1003
- if (isPublicReader(nestId, userId)) {
1004
- return nodes.filter((n) => getApprovedVersion(nestId, n.id) !== null);
1084
+ async function filterAccessible(nestId, userId, userEmail, nodes) {
1085
+ if (await isPublicReader(nestId, userId)) {
1086
+ const filtered = [];
1087
+ for (const n of nodes) {
1088
+ if (await getApprovedVersion(nestId, n.id) !== null) {
1089
+ filtered.push(n);
1090
+ }
1091
+ }
1092
+ return filtered;
1005
1093
  }
1006
- if (!isStewardshipEnabled(nestId)) return nodes;
1007
- return nodes.filter((n) => canUserAccess(nestId, n.id, userEmail).allowed);
1094
+ if (!await isStewardshipEnabled(nestId)) return nodes;
1095
+ const accessible = [];
1096
+ for (const n of nodes) {
1097
+ if ((await canUserAccess(nestId, n.id, userEmail)).allowed) {
1098
+ accessible.push(n);
1099
+ }
1100
+ }
1101
+ return accessible;
1008
1102
  }
1009
1103
 
1010
1104
  // src/governance/external-edit-service.ts
@@ -1028,7 +1122,7 @@ var communityRbac = {
1028
1122
  isDocOwner: () => true
1029
1123
  };
1030
1124
  function docPath(nestId, documentId) {
1031
- return join(config.DATA_ROOT, "nests", nestId, `${documentId}.md`);
1125
+ return join(resolveNestPath(nestId), `${documentId}.md`);
1032
1126
  }
1033
1127
  async function readRaw(nestId, documentId) {
1034
1128
  try {
@@ -1052,7 +1146,7 @@ async function loadChainHead(storage, documentId) {
1052
1146
  }
1053
1147
  }
1054
1148
  async function loadLatestApprovedNode(nestId, documentId) {
1055
- const { storage } = engineCache.get(nestId);
1149
+ const { storage } = await engineCache.get(nestId);
1056
1150
  const head = await loadChainHead(storage, documentId);
1057
1151
  if (!head) return null;
1058
1152
  return parseDocument(docPath(nestId, documentId), head.content, documentId);
@@ -1068,7 +1162,7 @@ async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner"
1068
1162
  return res?.meta ?? null;
1069
1163
  }
1070
1164
  async function scanDocumentForDriftInternal(nestId, documentId, actor) {
1071
- const { storage } = engineCache.get(nestId);
1165
+ const { storage } = await engineCache.get(nestId);
1072
1166
  const node = await storage.readDocument(documentId).catch(() => null);
1073
1167
  if (!node) return null;
1074
1168
  const raw = await readRaw(nestId, documentId);
@@ -1095,7 +1189,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
1095
1189
  return { meta: result.meta, created: true };
1096
1190
  }
1097
1191
  async function scanNestForDrift(nestId, actor = "system:scanner") {
1098
- const { storage } = engineCache.get(nestId);
1192
+ const { storage } = await engineCache.get(nestId);
1099
1193
  const docs = await storage.discoverDocuments();
1100
1194
  const results = await Promise.all(
1101
1195
  docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
@@ -1104,7 +1198,7 @@ async function scanNestForDrift(nestId, actor = "system:scanner") {
1104
1198
  return { scanned: docs.length, staged };
1105
1199
  }
1106
1200
  async function getPendingChange(nestId, documentId) {
1107
- const { storage } = engineCache.get(nestId);
1201
+ const { storage } = await engineCache.get(nestId);
1108
1202
  const list = await listSuggestions(storage, documentId);
1109
1203
  for (let i = list.length - 1; i >= 0; i--) {
1110
1204
  const meta = list[i];
@@ -1121,7 +1215,7 @@ async function getPendingChange(nestId, documentId) {
1121
1215
  return null;
1122
1216
  }
1123
1217
  async function listNestExternalEdits(nestId) {
1124
- const { storage } = engineCache.get(nestId);
1218
+ const { storage } = await engineCache.get(nestId);
1125
1219
  const docs = await storage.discoverDocuments();
1126
1220
  const lists = await Promise.all(
1127
1221
  docs.map(async (doc) => {
@@ -1150,7 +1244,7 @@ async function listNestExternalEdits(nestId) {
1150
1244
  return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
1151
1245
  }
1152
1246
  async function getExternalEditDetail(nestId, documentId, suggestionId) {
1153
- const { storage } = engineCache.get(nestId);
1247
+ const { storage } = await engineCache.get(nestId);
1154
1248
  const found = await readSuggestion(storage, documentId, suggestionId);
1155
1249
  if (!found) return null;
1156
1250
  return {
@@ -1167,7 +1261,7 @@ async function getExternalEditDetail(nestId, documentId, suggestionId) {
1167
1261
  };
1168
1262
  }
1169
1263
  async function approveExternalEdit(input) {
1170
- const { storage } = engineCache.get(input.nestId);
1264
+ const { storage } = await engineCache.get(input.nestId);
1171
1265
  let result;
1172
1266
  try {
1173
1267
  result = await approveSuggestion({
@@ -1190,8 +1284,8 @@ async function approveExternalEdit(input) {
1190
1284
  const node = await storage.readDocument(input.documentId);
1191
1285
  const versionNum = result.versionEntry.version;
1192
1286
  const tags = node.frontmatter.tags || [];
1193
- const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-REGL5CUT.js");
1194
- createVersion2({
1287
+ const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-MUZYTYMW.js");
1288
+ await createVersion2({
1195
1289
  nestId: input.nestId,
1196
1290
  nodeId: input.documentId,
1197
1291
  version: versionNum,
@@ -1201,7 +1295,12 @@ async function approveExternalEdit(input) {
1201
1295
  tags,
1202
1296
  changeNote: input.comment || "External edit approved"
1203
1297
  });
1204
- setApprovedVersion2(input.nestId, input.documentId, versionNum, input.actor);
1298
+ await setApprovedVersion2(
1299
+ input.nestId,
1300
+ input.documentId,
1301
+ versionNum,
1302
+ input.actor
1303
+ );
1205
1304
  } catch (err) {
1206
1305
  console.error(
1207
1306
  `[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
@@ -1211,7 +1310,7 @@ async function approveExternalEdit(input) {
1211
1310
  return result;
1212
1311
  }
1213
1312
  async function rejectExternalEdit(input) {
1214
- const { storage } = engineCache.get(input.nestId);
1313
+ const { storage } = await engineCache.get(input.nestId);
1215
1314
  const result = await rejectSuggestion({
1216
1315
  storage,
1217
1316
  rbac: communityRbac,
@@ -1237,7 +1336,7 @@ async function rejectExternalEdit(input) {
1237
1336
  var scannerTimer = null;
1238
1337
  async function scanAllNests() {
1239
1338
  const db = getDb();
1240
- const rows = db.prepare("SELECT id FROM nests").all();
1339
+ const rows = await db.all("SELECT id FROM nests");
1241
1340
  await Promise.all(
1242
1341
  rows.map(
1243
1342
  ({ id }) => scanNestForDrift(id).catch(
@@ -1257,9 +1356,9 @@ function startDriftScanner(intervalMs = 3e4) {
1257
1356
  }
1258
1357
 
1259
1358
  // src/nodes/service.ts
1260
- function userIdFromEmail(email) {
1359
+ async function userIdFromEmail(email) {
1261
1360
  const db = getDb();
1262
- const row = db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(email);
1361
+ const row = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [email]);
1263
1362
  return row?.id ?? ANON_USER_ID;
1264
1363
  }
1265
1364
  var normalizeTag2 = (t) => t.startsWith("#") ? t : `#${t}`;
@@ -1291,7 +1390,7 @@ function toNodeResponse(node) {
1291
1390
  };
1292
1391
  }
1293
1392
  async function listNodesForCaller(nestId, userId, filters = {}) {
1294
- const { storage, versions: versionManager } = engineCache.get(nestId);
1393
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1295
1394
  let documents = await storage.discoverDocuments();
1296
1395
  if (filters.type) {
1297
1396
  documents = documents.filter((n) => n.frontmatter.type === filters.type);
@@ -1302,14 +1401,14 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1302
1401
  (n) => (n.frontmatter.tags || []).includes(tag)
1303
1402
  );
1304
1403
  }
1305
- const userEmail = resolveCallerEmail(userId);
1306
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
1307
- const publicReader = isPublicReader(nestId, userId);
1404
+ const userEmail = await resolveCallerEmail(userId);
1405
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
1406
+ const publicReader = await isPublicReader(nestId, userId);
1308
1407
  const enriched = await Promise.all(
1309
1408
  accessible.map(async (doc) => {
1310
1409
  const r = toNodeResponse(doc);
1311
1410
  if (publicReader) {
1312
- const approved = getApprovedVersion(nestId, doc.id);
1411
+ const approved = await getApprovedVersion(nestId, doc.id);
1313
1412
  if (approved != null) {
1314
1413
  try {
1315
1414
  const raw = await versionManager.reconstructVersion(doc.id, approved);
@@ -1333,7 +1432,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1333
1432
  r.pendingChange = pending;
1334
1433
  r.status = "external_edit_pending";
1335
1434
  } else {
1336
- r.status = getDisplayStatus(nestId, r.id);
1435
+ r.status = await getDisplayStatus(nestId, r.id);
1337
1436
  }
1338
1437
  return r;
1339
1438
  })
@@ -1341,15 +1440,15 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
1341
1440
  return filters.limit ? enriched.slice(0, filters.limit) : enriched;
1342
1441
  }
1343
1442
  async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
1344
- return listNodesForCaller(nestId, userIdFromEmail(userEmail), filters);
1443
+ return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
1345
1444
  }
1346
1445
  async function createNode(nestId, input, userEmail) {
1347
- const { storage, versions: versionManager } = engineCache.get(nestId);
1446
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1348
1447
  const slug = input.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1349
1448
  const id = input.id ?? `nodes/${slug}`;
1350
1449
  const now = (/* @__PURE__ */ new Date()).toISOString();
1351
1450
  const tags = (input.tags || []).map(normalizeTag2);
1352
- const hasStewards = isStewardshipEnabled(nestId);
1451
+ const hasStewards = await isStewardshipEnabled(nestId);
1353
1452
  const initialStatus = hasStewards ? "draft" : "published";
1354
1453
  const initialVersion = hasStewards ? 1 : 0;
1355
1454
  let node = {
@@ -1369,7 +1468,7 @@ async function createNode(nestId, input, userEmail) {
1369
1468
  rawContent: ""
1370
1469
  };
1371
1470
  await storage.writeDocument(id, serializeDocument(node));
1372
- syncNodeTags(nestId, id, tags);
1471
+ await syncNodeTags(nestId, id, tags);
1373
1472
  let savedVersion = 1;
1374
1473
  if (hasStewards) {
1375
1474
  try {
@@ -1377,7 +1476,7 @@ async function createNode(nestId, input, userEmail) {
1377
1476
  } catch (err) {
1378
1477
  console.error("VersionManager.createVersion failed (node create)", err);
1379
1478
  }
1380
- createVersion({
1479
+ await createVersion({
1381
1480
  nestId,
1382
1481
  nodeId: id,
1383
1482
  version: 1,
@@ -1393,7 +1492,7 @@ async function createNode(nestId, input, userEmail) {
1393
1492
  note: "Auto-published on create (no stewards configured)"
1394
1493
  });
1395
1494
  savedVersion = result.node.frontmatter.version || 1;
1396
- createVersion({
1495
+ await createVersion({
1397
1496
  nestId,
1398
1497
  nodeId: id,
1399
1498
  version: savedVersion,
@@ -1402,11 +1501,11 @@ async function createNode(nestId, input, userEmail) {
1402
1501
  status: "published",
1403
1502
  tags
1404
1503
  });
1405
- setApprovedVersion(nestId, id, savedVersion, userEmail);
1504
+ await setApprovedVersion(nestId, id, savedVersion, userEmail);
1406
1505
  node = result.node;
1407
1506
  } catch (err) {
1408
1507
  console.error("publishDocument failed (node create auto-publish)", err);
1409
- createVersion({
1508
+ await createVersion({
1410
1509
  nestId,
1411
1510
  nodeId: id,
1412
1511
  version: 1,
@@ -1417,11 +1516,11 @@ async function createNode(nestId, input, userEmail) {
1417
1516
  });
1418
1517
  }
1419
1518
  }
1420
- trackEvent("node.create", { nestId, nodeId: id });
1519
+ await trackEvent("node.create", { nestId, nodeId: id });
1421
1520
  return { node, version: savedVersion };
1422
1521
  }
1423
1522
  async function registerImportedDocuments(nestId, userEmail) {
1424
- const { storage } = engineCache.get(nestId);
1523
+ const { storage } = await engineCache.get(nestId);
1425
1524
  let docs;
1426
1525
  try {
1427
1526
  docs = await storage.discoverDocuments();
@@ -1432,7 +1531,7 @@ async function registerImportedDocuments(nestId, userEmail) {
1432
1531
  let registered = 0;
1433
1532
  for (const doc of docs) {
1434
1533
  const nodeId = doc.id;
1435
- if (getCurrentVersion(nestId, nodeId) > 0) continue;
1534
+ if (await getCurrentVersion(nestId, nodeId) > 0) continue;
1436
1535
  const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
1437
1536
  const tags = rawTags.map((t) => normalizeTag2(String(t)));
1438
1537
  const fmVersion = Number(doc.frontmatter?.version);
@@ -1448,7 +1547,7 @@ async function registerImportedDocuments(nestId, userEmail) {
1448
1547
  } catch (err) {
1449
1548
  console.error("safePublishDocument failed (import register)", nodeId, err);
1450
1549
  }
1451
- createVersion({
1550
+ await createVersion({
1452
1551
  nestId,
1453
1552
  nodeId,
1454
1553
  version,
@@ -1458,15 +1557,15 @@ async function registerImportedDocuments(nestId, userEmail) {
1458
1557
  changeNote: "Imported from existing folder",
1459
1558
  tags
1460
1559
  });
1461
- setApprovedVersion(nestId, nodeId, version, userEmail);
1462
- syncNodeTags(nestId, nodeId, tags);
1560
+ await setApprovedVersion(nestId, nodeId, version, userEmail);
1561
+ await syncNodeTags(nestId, nodeId, tags);
1463
1562
  registered++;
1464
1563
  }
1465
- trackEvent("nest.import.documents", { nestId, registered });
1564
+ await trackEvent("nest.import.documents", { nestId, registered });
1466
1565
  return registered;
1467
1566
  }
1468
1567
  async function updateNode(nestId, nodeId, patch, userEmail) {
1469
- const { storage, versions: versionManager } = engineCache.get(nestId);
1568
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
1470
1569
  let node;
1471
1570
  try {
1472
1571
  node = await storage.readDocument(nodeId);
@@ -1490,16 +1589,16 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1490
1589
  if (patch.title) {
1491
1590
  node = { ...node, frontmatter: { ...node.frontmatter, title: patch.title } };
1492
1591
  }
1493
- const hasStewards = isStewardshipEnabled(nestId);
1592
+ const hasStewards = await isStewardshipEnabled(nestId);
1494
1593
  const currentTags = node.frontmatter.tags || [];
1495
- if (hasStewards && getPendingReview(nestId, nodeId)) {
1594
+ if (hasStewards && await getPendingReview(nestId, nodeId)) {
1496
1595
  throw new LockedError(
1497
1596
  "This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
1498
1597
  );
1499
1598
  }
1500
1599
  let responseVersion;
1501
1600
  if (hasStewards) {
1502
- const currentVersion = getCurrentVersion(nestId, nodeId);
1601
+ const currentVersion = await getCurrentVersion(nestId, nodeId);
1503
1602
  const newVersion = currentVersion + 1;
1504
1603
  node = {
1505
1604
  ...node,
@@ -1514,13 +1613,13 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1514
1613
  };
1515
1614
  node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
1516
1615
  await storage.writeDocument(nodeId, serializeDocument(node));
1517
- syncNodeTags(nestId, nodeId, currentTags);
1616
+ await syncNodeTags(nestId, nodeId, currentTags);
1518
1617
  try {
1519
1618
  await versionManager.createVersion(node, userEmail, { note: patch.changeNote });
1520
1619
  } catch (err) {
1521
1620
  console.error("VersionManager.createVersion failed (node patch)", err);
1522
1621
  }
1523
- createVersion({
1622
+ await createVersion({
1524
1623
  nestId,
1525
1624
  nodeId,
1526
1625
  version: newVersion,
@@ -1542,7 +1641,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1542
1641
  };
1543
1642
  node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
1544
1643
  await storage.writeDocument(nodeId, serializeDocument(node));
1545
- syncNodeTags(nestId, nodeId, currentTags);
1644
+ await syncNodeTags(nestId, nodeId, currentTags);
1546
1645
  let publishedVersion = (node.frontmatter.version || 0) + 1;
1547
1646
  try {
1548
1647
  const result = await safePublishDocument(storage, nodeId, {
@@ -1554,7 +1653,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1554
1653
  } catch (err) {
1555
1654
  console.error("publishDocument failed (node patch auto-publish)", err);
1556
1655
  }
1557
- createVersion({
1656
+ await createVersion({
1558
1657
  nestId,
1559
1658
  nodeId,
1560
1659
  version: publishedVersion,
@@ -1564,7 +1663,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1564
1663
  tags: currentTags,
1565
1664
  changeNote: patch.changeNote
1566
1665
  });
1567
- setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
1666
+ await setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
1568
1667
  responseVersion = publishedVersion;
1569
1668
  }
1570
1669
  return { node, version: responseVersion };
@@ -1572,8 +1671,11 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
1572
1671
 
1573
1672
  // src/nests/unsynced-service.ts
1574
1673
  import { readdirSync, readFileSync, rmSync, statSync } from "fs";
1575
- import { join as join2, relative } from "path";
1674
+ import { join as join2, relative, resolve } from "path";
1576
1675
  var RESERVED = /* @__PURE__ */ new Set(["nests"]);
1676
+ function isNestStorageRoot(absPath) {
1677
+ return resolve(absPath) === resolve(nestStorageRoot());
1678
+ }
1577
1679
  function scanMarkdown(dir) {
1578
1680
  let count = 0;
1579
1681
  let size = 0;
@@ -1653,7 +1755,9 @@ function listUnsyncedFolders() {
1653
1755
  if (!e.isDirectory()) continue;
1654
1756
  if (e.name.startsWith(".")) continue;
1655
1757
  if (RESERVED.has(e.name)) continue;
1656
- out.push(...collectLeafFolders(e.name, join2(root, e.name)));
1758
+ const abs = join2(root, e.name);
1759
+ if (isNestStorageRoot(abs)) continue;
1760
+ out.push(...collectLeafFolders(e.name, abs));
1657
1761
  }
1658
1762
  out.sort((a, b) => a.name.localeCompare(b.name));
1659
1763
  console.log(
@@ -1707,6 +1811,9 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1707
1811
  );
1708
1812
  assertSafeFolderName(folderName);
1709
1813
  const src = join2(config.DATA_ROOT, folderName);
1814
+ if (isNestStorageRoot(src)) {
1815
+ throw new ValidationError("Folder is not eligible for sync");
1816
+ }
1710
1817
  let stat;
1711
1818
  try {
1712
1819
  stat = statSync(src);
@@ -1726,7 +1833,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1726
1833
  }
1727
1834
  const segments = folderName.split("/").filter(Boolean);
1728
1835
  const baseName = segments[segments.length - 1] || folderName;
1729
- const nestName = uniqueNestName(userId, baseName);
1836
+ const nestName = await uniqueNestName(userId, baseName);
1730
1837
  if (nestName !== baseName) {
1731
1838
  console.log(
1732
1839
  `[unsynced] name "${baseName}" already in use, using "${nestName}" instead`
@@ -1750,10 +1857,13 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
1750
1857
  }
1751
1858
 
1752
1859
  // src/nests/routes.ts
1753
- function effectivePermission(nestId, userId) {
1860
+ async function effectivePermission(nestId, userId) {
1754
1861
  if (config.AUTH_MODE === "open") {
1755
1862
  const db = getDb();
1756
- const nest = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
1863
+ const nest = await db.get(
1864
+ "SELECT user_id FROM nests WHERE id = ?",
1865
+ [nestId]
1866
+ );
1757
1867
  if (nest && nest.user_id === ANON_USER_ID) return "owner";
1758
1868
  }
1759
1869
  return resolveNestPermission(nestId, userId);
@@ -1761,21 +1871,19 @@ function effectivePermission(nestId, userId) {
1761
1871
  var nestRoutes = new Hono2();
1762
1872
  nestRoutes.get("/", async (c) => {
1763
1873
  const userId = c.get("userId");
1764
- const owned = listNests(userId);
1765
- const shared = listSharedNests(userId);
1766
- const publicExtras = listPublicNests(userId);
1874
+ const owned = await listNests(userId);
1875
+ const shared = await listSharedNests(userId);
1876
+ const publicExtras = await listPublicNests(userId);
1767
1877
  const db = getDb();
1768
- const ownerEmailStmt = db.prepare(
1769
- "SELECT email FROM users WHERE id = ?"
1770
- );
1771
- const callerEmail = resolveCallerEmail(userId);
1772
- const annotate = (n) => {
1773
- const permission = effectivePermission(n.id, userId);
1878
+ const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
1879
+ const callerEmail = await resolveCallerEmail(userId);
1880
+ const annotate = async (n) => {
1881
+ const permission = await effectivePermission(n.id, userId);
1774
1882
  const is_owner = permission === "owner";
1775
1883
  let owner_email = null;
1776
- const roles = is_owner ? ["owner"] : resolveUserRoles(n.id, callerEmail);
1884
+ const roles = is_owner ? ["owner"] : await resolveUserRoles(n.id, callerEmail);
1777
1885
  if (!is_owner && n.user_id !== ANON_USER_ID) {
1778
- const row = ownerEmailStmt.get(n.user_id);
1886
+ const row = await db.get(ownerEmailSql, [n.user_id]);
1779
1887
  owner_email = row?.email ?? null;
1780
1888
  }
1781
1889
  return { ...n, permission, is_owner, owner_email, roles };
@@ -1785,7 +1893,7 @@ nestRoutes.get("/", async (c) => {
1785
1893
  for (const n of [...owned, ...shared, ...publicExtras]) {
1786
1894
  if (seen.has(n.id)) continue;
1787
1895
  seen.add(n.id);
1788
- out.push(annotate(n));
1896
+ out.push(await annotate(n));
1789
1897
  }
1790
1898
  return c.json({ nests: out });
1791
1899
  });
@@ -1807,20 +1915,20 @@ nestRoutes.post("/import", async (c) => {
1807
1915
  const nest = await importNest(userId, body.name, files);
1808
1916
  const documents = await registerImportedDocuments(
1809
1917
  nest.id,
1810
- resolveCallerEmail(userId)
1918
+ await resolveCallerEmail(userId)
1811
1919
  );
1812
1920
  return c.json({ nest, documents }, 201);
1813
1921
  });
1814
1922
  nestRoutes.get("/unsynced", async (c) => {
1815
1923
  const userId = c.get("userId");
1816
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
1924
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
1817
1925
  throw new ForbiddenError("Only the server admin can list unsynced folders");
1818
1926
  }
1819
1927
  return c.json({ folders: listUnsyncedFolders() });
1820
1928
  });
1821
1929
  nestRoutes.post("/unsynced/sync", async (c) => {
1822
1930
  const userId = c.get("userId");
1823
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
1931
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
1824
1932
  throw new ForbiddenError("Only the server admin can sync folders");
1825
1933
  }
1826
1934
  const body = await c.req.json();
@@ -1830,30 +1938,30 @@ nestRoutes.post("/unsynced/sync", async (c) => {
1830
1938
  const result = await syncUnsyncedFolder(
1831
1939
  userId,
1832
1940
  body.name,
1833
- resolveCallerEmail(userId)
1941
+ await resolveCallerEmail(userId)
1834
1942
  );
1835
1943
  return c.json(result, 201);
1836
1944
  });
1837
1945
  nestRoutes.get("/:nestId", async (c) => {
1838
1946
  const nestId = c.req.param("nestId");
1839
1947
  const userId = c.get("userId");
1840
- const permission = effectivePermission(nestId, userId);
1948
+ const permission = await effectivePermission(nestId, userId);
1841
1949
  if (permission === "none") {
1842
1950
  throw new NotFoundError("Nest not found");
1843
1951
  }
1844
- const email = resolveCallerEmail(userId);
1845
- let roles = resolveUserRoles(nestId, email);
1952
+ const email = await resolveCallerEmail(userId);
1953
+ let roles = await resolveUserRoles(nestId, email);
1846
1954
  if (permission === "owner" && !roles.includes("owner")) {
1847
1955
  roles = ["owner", ...roles];
1848
1956
  }
1849
- const myStewards = getStewardsForUser(nestId, email);
1850
- const nest = getNest(nestId);
1957
+ const myStewards = await getStewardsForUser(nestId, email);
1958
+ const nest = await getNest(nestId);
1851
1959
  return c.json({ nest, permission, roles, myStewards });
1852
1960
  });
1853
1961
  nestRoutes.patch("/:nestId", async (c) => {
1854
1962
  const nestId = c.req.param("nestId");
1855
1963
  const userId = c.get("userId");
1856
- const permission = effectivePermission(nestId, userId);
1964
+ const permission = await effectivePermission(nestId, userId);
1857
1965
  if (permission === "none") {
1858
1966
  throw new NotFoundError("Nest not found");
1859
1967
  }
@@ -1863,7 +1971,7 @@ nestRoutes.patch("/:nestId", async (c) => {
1863
1971
  );
1864
1972
  }
1865
1973
  const body = await c.req.json();
1866
- const nest = renameNest(nestId, {
1974
+ const nest = await renameNest(nestId, {
1867
1975
  name: body.name,
1868
1976
  description: body.description
1869
1977
  });
@@ -1872,13 +1980,13 @@ nestRoutes.patch("/:nestId", async (c) => {
1872
1980
  nestRoutes.delete("/:nestId", async (c) => {
1873
1981
  const nestId = c.req.param("nestId");
1874
1982
  const userId = c.get("userId");
1875
- const nest = getNest(nestId);
1983
+ const nest = await getNest(nestId);
1876
1984
  if (!nest) {
1877
1985
  throw new NotFoundError("Nest not found");
1878
1986
  }
1879
- const permission = effectivePermission(nestId, userId);
1987
+ const permission = await effectivePermission(nestId, userId);
1880
1988
  const isAnonOwned = nest.user_id === ANON_USER_ID;
1881
- const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && isLicenseAdminUserId(userId);
1989
+ const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && await isLicenseAdminUserId(userId);
1882
1990
  if (permission !== "owner" && !adminCaretaker) {
1883
1991
  throw new ForbiddenError(
1884
1992
  "You don't have permission to delete this nest. Only the nest owner can delete it."
@@ -1889,20 +1997,20 @@ nestRoutes.delete("/:nestId", async (c) => {
1889
1997
  });
1890
1998
  nestRoutes.get("/:nestId/settings", async (c) => {
1891
1999
  const nestId = c.req.param("nestId");
1892
- const permission = effectivePermission(nestId, c.get("userId"));
2000
+ const permission = await effectivePermission(nestId, c.get("userId"));
1893
2001
  if (permission === "none") {
1894
2002
  throw new NotFoundError("Nest not found");
1895
2003
  }
1896
2004
  return c.json({
1897
- stewardship_enabled: isStewardshipEnabled(nestId),
1898
- allow_self_approve: nestAllowsSelfApprove(nestId)
2005
+ stewardship_enabled: await isStewardshipEnabled(nestId),
2006
+ allow_self_approve: await nestAllowsSelfApprove(nestId)
1899
2007
  });
1900
2008
  });
1901
2009
  nestRoutes.patch("/:nestId/settings", async (c) => {
1902
2010
  const nestId = c.req.param("nestId");
1903
2011
  const userId = c.get("userId");
1904
- const isServerAdmin = isLicenseAdminUserId(userId);
1905
- const permission = effectivePermission(nestId, userId);
2012
+ const isServerAdmin = await isLicenseAdminUserId(userId);
2013
+ const permission = await effectivePermission(nestId, userId);
1906
2014
  if (!isServerAdmin && permission !== "owner") {
1907
2015
  return c.json(
1908
2016
  {
@@ -1915,17 +2023,17 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
1915
2023
  let wiped = null;
1916
2024
  if (typeof body.stewardship_enabled === "boolean") {
1917
2025
  if (body.stewardship_enabled) {
1918
- setStewardshipEnabled(nestId, true);
2026
+ await setStewardshipEnabled(nestId, true);
1919
2027
  } else {
1920
- wiped = disableStewardshipAndWipeGovernance(nestId);
2028
+ wiped = await disableStewardshipAndWipeGovernance(nestId);
1921
2029
  }
1922
2030
  }
1923
2031
  if (typeof body.allow_self_approve === "boolean") {
1924
- setAllowSelfApprove(nestId, body.allow_self_approve);
2032
+ await setAllowSelfApprove(nestId, body.allow_self_approve);
1925
2033
  }
1926
2034
  return c.json({
1927
- stewardship_enabled: isStewardshipEnabled(nestId),
1928
- allow_self_approve: nestAllowsSelfApprove(nestId),
2035
+ stewardship_enabled: await isStewardshipEnabled(nestId),
2036
+ allow_self_approve: await nestAllowsSelfApprove(nestId),
1929
2037
  wiped
1930
2038
  });
1931
2039
  });
@@ -1950,21 +2058,28 @@ async function addCollaborator(params) {
1950
2058
  let userId = params.userId;
1951
2059
  if (!userId && params.email) {
1952
2060
  const email = normalizeEmail(params.email);
1953
- const existing = db.prepare("SELECT id FROM users WHERE LOWER(email) = ?").get(email);
2061
+ const existing = await db.get(
2062
+ "SELECT id FROM users WHERE LOWER(email) = ?",
2063
+ [email]
2064
+ );
1954
2065
  if (existing) {
1955
2066
  userId = existing.id;
1956
2067
  } else {
1957
2068
  const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
1958
2069
  userId = uuid2();
1959
- db.prepare(
1960
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
1961
- ).run(userId, email, null, await hashPassword2(uuid2()));
2070
+ await db.run(
2071
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
2072
+ [userId, email, null, await hashPassword2(uuid2())]
2073
+ );
1962
2074
  }
1963
2075
  }
1964
2076
  if (!userId) {
1965
2077
  throw new ValidationError("user_id or email is required");
1966
2078
  }
1967
- const ownerRow = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
2079
+ const ownerRow = await db.get(
2080
+ "SELECT user_id FROM nests WHERE id = ?",
2081
+ [nestId]
2082
+ );
1968
2083
  if (!ownerRow) {
1969
2084
  throw new ValidationError("Nest not found");
1970
2085
  }
@@ -1978,19 +2093,29 @@ async function addCollaborator(params) {
1978
2093
  if (selfByEmail || selfById) {
1979
2094
  throw new ValidationError("You can't add yourself as a collaborator.");
1980
2095
  }
1981
- const dupe = db.prepare("SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?").get(nestId, userId);
2096
+ const dupe = await db.get(
2097
+ "SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
2098
+ [nestId, userId]
2099
+ );
1982
2100
  if (dupe) {
1983
2101
  throw new ConflictError(
1984
2102
  `${params.email || "This user"} already has access to this nest. Change their permission instead of adding them again.`
1985
2103
  );
1986
2104
  }
1987
- const granterByEmail = !params.grantedByUserId && params.grantedByEmail ? db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(params.grantedByEmail)?.id : void 0;
2105
+ const granterByEmail = !params.grantedByUserId && params.grantedByEmail ? (await db.get(
2106
+ "SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
2107
+ [params.grantedByEmail]
2108
+ ))?.id : void 0;
1988
2109
  const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
1989
2110
  const collabId = uuid2();
1990
- db.prepare(
1991
- "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)"
1992
- ).run(collabId, nestId, userId, params.permission, granterId);
1993
- return db.prepare("SELECT * FROM nest_collaborators WHERE id = ?").get(collabId);
2111
+ await db.run(
2112
+ "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
2113
+ [collabId, nestId, userId, params.permission, granterId]
2114
+ );
2115
+ return await db.get(
2116
+ "SELECT * FROM nest_collaborators WHERE id = ?",
2117
+ [collabId]
2118
+ );
1994
2119
  }
1995
2120
 
1996
2121
  // src/nests/sharing-routes.ts
@@ -1998,20 +2123,25 @@ var sharingRoutes = new Hono3();
1998
2123
  sharingRoutes.get("/collaborators", async (c) => {
1999
2124
  const db = getDb();
2000
2125
  const nestId = c.req.param("nestId");
2001
- const collabs = db.prepare(
2126
+ const collabs = await db.all(
2002
2127
  `SELECT nc.*, u.email FROM nest_collaborators nc
2003
2128
  LEFT JOIN users u ON nc.user_id = u.id
2004
2129
  WHERE nc.nest_id = ?
2005
- ORDER BY nc.granted_at`
2006
- ).all(nestId);
2007
- const enriched = collabs.map((collab) => {
2008
- if (!collab.email) return { ...collab, stewardRoles: [], roles: [] };
2009
- return {
2130
+ ORDER BY nc.granted_at`,
2131
+ [nestId]
2132
+ );
2133
+ const enriched = [];
2134
+ for (const collab of collabs) {
2135
+ if (!collab.email) {
2136
+ enriched.push({ ...collab, stewardRoles: [], roles: [] });
2137
+ continue;
2138
+ }
2139
+ enriched.push({
2010
2140
  ...collab,
2011
- stewardRoles: getStewardRolesForUser(nestId, collab.email),
2012
- roles: resolveUserRoles(nestId, collab.email)
2013
- };
2014
- });
2141
+ stewardRoles: await getStewardRolesForUser(nestId, collab.email),
2142
+ roles: await resolveUserRoles(nestId, collab.email)
2143
+ });
2144
+ }
2015
2145
  return c.json({ collaborators: enriched });
2016
2146
  });
2017
2147
  sharingRoutes.post("/collaborators", async (c) => {
@@ -2034,9 +2164,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
2034
2164
  throw new ValidationError("permission must be read, write, or admin");
2035
2165
  }
2036
2166
  const db = getDb();
2037
- const info = db.prepare(
2038
- "UPDATE nest_collaborators SET permission = ? WHERE id = ? AND nest_id = ?"
2039
- ).run(body.permission, c.req.param("collabId"), c.req.param("nestId"));
2167
+ const info = await db.run(
2168
+ "UPDATE nest_collaborators SET permission = ? WHERE id = ? AND nest_id = ?",
2169
+ [body.permission, c.req.param("collabId"), c.req.param("nestId")]
2170
+ );
2040
2171
  if (info.changes === 0) {
2041
2172
  throw new NotFoundError("Collaborator not found");
2042
2173
  }
@@ -2044,9 +2175,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
2044
2175
  });
2045
2176
  sharingRoutes.delete("/collaborators/:collabId", async (c) => {
2046
2177
  const db = getDb();
2047
- const info = db.prepare(
2048
- "DELETE FROM nest_collaborators WHERE id = ? AND nest_id = ?"
2049
- ).run(c.req.param("collabId"), c.req.param("nestId"));
2178
+ const info = await db.run(
2179
+ "DELETE FROM nest_collaborators WHERE id = ? AND nest_id = ?",
2180
+ [c.req.param("collabId"), c.req.param("nestId")]
2181
+ );
2050
2182
  if (info.changes === 0) {
2051
2183
  throw new NotFoundError("Collaborator not found");
2052
2184
  }
@@ -2058,10 +2190,10 @@ sharingRoutes.patch("/visibility", async (c) => {
2058
2190
  throw new ValidationError("visibility must be private or public");
2059
2191
  }
2060
2192
  const db = getDb();
2061
- db.prepare("UPDATE nests SET visibility = ? WHERE id = ?").run(
2193
+ await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
2062
2194
  body.visibility,
2063
2195
  c.req.param("nestId")
2064
- );
2196
+ ]);
2065
2197
  return c.json({ visibility: body.visibility });
2066
2198
  });
2067
2199
 
@@ -2111,7 +2243,7 @@ nodeRoutes.post("/", async (c) => {
2111
2243
  throw new ValidationError("title and content are required");
2112
2244
  }
2113
2245
  const nestId = c.req.param("nestId");
2114
- const authorEmail = getUserEmail(c);
2246
+ const authorEmail = await getUserEmail(c);
2115
2247
  const { node } = await createNode(
2116
2248
  nestId,
2117
2249
  {
@@ -2124,7 +2256,7 @@ nodeRoutes.post("/", async (c) => {
2124
2256
  },
2125
2257
  authorEmail
2126
2258
  );
2127
- const resolved = resolveStewardsForNode(nestId, node.id);
2259
+ const resolved = await resolveStewardsForNode(nestId, node.id);
2128
2260
  return c.json({
2129
2261
  node: toNodeResponse(node),
2130
2262
  stewards: resolved.length > 0 ? resolved.map((r) => ({
@@ -2137,8 +2269,8 @@ nodeRoutes.post("/", async (c) => {
2137
2269
  nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2138
2270
  const nestId = c.req.param("nestId");
2139
2271
  const nodeId = c.req.param("nodeId");
2140
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-HFQPGASU.js");
2141
- const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
2272
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-VIKF3VZB.js");
2273
+ const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
2142
2274
  nestId,
2143
2275
  nodeId
2144
2276
  );
@@ -2158,18 +2290,19 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
2158
2290
  nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2159
2291
  const nestId = c.req.param("nestId");
2160
2292
  const nodeId = c.req.param("nodeId");
2161
- const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-REGL5CUT.js");
2162
- const allVersions = getVersions2(nestId, nodeId);
2163
- const approved = getApprovedVersion2(nestId, nodeId);
2293
+ const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-MUZYTYMW.js");
2294
+ const allVersions = await getVersions2(nestId, nodeId);
2295
+ const approved = await getApprovedVersion2(nestId, nodeId);
2164
2296
  const db = getDb();
2165
- const resolutions = db.prepare(
2297
+ const resolutions = await db.all(
2166
2298
  `SELECT version, status, resolved_by, resolved_at
2167
2299
  FROM review_requests
2168
2300
  WHERE nest_id = ? AND node_id = ?
2169
2301
  AND status IN ('approved', 'rejected')
2170
2302
  AND resolved_by IS NOT NULL
2171
- ORDER BY resolved_at DESC`
2172
- ).all(nestId, nodeId);
2303
+ ORDER BY resolved_at DESC`,
2304
+ [nestId, nodeId]
2305
+ );
2173
2306
  const byVersion = /* @__PURE__ */ new Map();
2174
2307
  for (const r of resolutions) {
2175
2308
  if (!byVersion.has(r.version)) {
@@ -2189,17 +2322,17 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
2189
2322
  nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
2190
2323
  const nestId = c.req.param("nestId");
2191
2324
  const nodeId = c.req.param("nodeId");
2192
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-ZBYZYH5H.js");
2193
- const history = getReviewHistory2(nestId, nodeId);
2325
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-QJ3FBOML.js");
2326
+ const history = await getReviewHistory2(nestId, nodeId);
2194
2327
  return c.json({ reviews: history });
2195
2328
  });
2196
2329
  nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
2197
2330
  const nestId = c.req.param("nestId");
2198
2331
  const nodeId = c.req.param("nodeId");
2199
- const { versions: versionManager } = engineCache.get(nestId);
2332
+ const { versions: versionManager } = await engineCache.get(nestId);
2200
2333
  const userId = c.get("userId");
2201
- const userEmail = resolveCallerEmail(userId);
2202
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2334
+ const userEmail = await resolveCallerEmail(userId);
2335
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2203
2336
  return c.json(
2204
2337
  { error: "Access denied \u2014 no steward assignment for this node" },
2205
2338
  403
@@ -2225,16 +2358,16 @@ nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
2225
2358
  { content, changeNote: `Restored from version ${targetVersion}` },
2226
2359
  userEmail
2227
2360
  );
2228
- trackEvent("node.revert", { nestId, nodeId, targetVersion });
2361
+ await trackEvent("node.revert", { nestId, nodeId, targetVersion });
2229
2362
  return c.json({ ok: true, version, node: toNodeResponse(node) });
2230
2363
  });
2231
2364
  nodeRoutes.get("/:nodeId{.+}", async (c) => {
2232
2365
  const nestId = c.req.param("nestId");
2233
2366
  const nodeId = c.req.param("nodeId");
2234
- const { storage, versions: versionManager } = engineCache.get(nestId);
2367
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
2235
2368
  const userId = c.get("userId");
2236
- const userEmail = resolveCallerEmail(userId);
2237
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2369
+ const userEmail = await resolveCallerEmail(userId);
2370
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2238
2371
  return c.json(
2239
2372
  { error: "Access denied \u2014 no steward assignment for this node" },
2240
2373
  403
@@ -2264,8 +2397,8 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
2264
2397
  }
2265
2398
  }
2266
2399
  const response = toNodeResponse(node);
2267
- if (isPublicReader(nestId, userId)) {
2268
- const approved = getApprovedVersion(nestId, nodeId);
2400
+ if (await isPublicReader(nestId, userId)) {
2401
+ const approved = await getApprovedVersion(nestId, nodeId);
2269
2402
  if (approved != null) {
2270
2403
  try {
2271
2404
  const raw = await versionManager.reconstructVersion(
@@ -2292,9 +2425,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
2292
2425
  return c.json({ node: response });
2293
2426
  }
2294
2427
  }
2295
- response.status = node.pendingChange ? "external_edit_pending" : getDisplayStatus(nestId, nodeId);
2428
+ response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
2296
2429
  if (response.status === "pending_review") {
2297
- const pending = getPendingReview(nestId, nodeId);
2430
+ const pending = await getPendingReview(nestId, nodeId);
2298
2431
  response.pendingReviewBy = pending?.requestedBy ?? null;
2299
2432
  }
2300
2433
  if (isMarkdownFormat(c)) {
@@ -2311,7 +2444,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2311
2444
  const baseVersionHeader = c.req.header("X-Base-Version");
2312
2445
  if (baseVersionHeader) {
2313
2446
  const baseVersion = parseInt(baseVersionHeader, 10);
2314
- const conflict = checkConflict(nestId, nodeId, baseVersion);
2447
+ const conflict = await checkConflict(nestId, nodeId, baseVersion);
2315
2448
  if (conflict.conflict) {
2316
2449
  return c.json(
2317
2450
  {
@@ -2326,7 +2459,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2326
2459
  );
2327
2460
  }
2328
2461
  }
2329
- const authorEmail = getUserEmail(c);
2462
+ const authorEmail = await getUserEmail(c);
2330
2463
  const { node, version: responseVersion } = await updateNode(
2331
2464
  nestId,
2332
2465
  nodeId,
@@ -2345,8 +2478,8 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
2345
2478
  nodeRoutes.delete("/:nodeId{.+}", async (c) => {
2346
2479
  const nestId = c.req.param("nestId");
2347
2480
  const nodeId = c.req.param("nodeId");
2348
- const { storage } = engineCache.get(nestId);
2349
- if (isStewardshipEnabled(nestId) && getPendingReview(nestId, nodeId)) {
2481
+ const { storage } = await engineCache.get(nestId);
2482
+ if (await isStewardshipEnabled(nestId) && await getPendingReview(nestId, nodeId)) {
2350
2483
  throw new LockedError(
2351
2484
  "This document is awaiting steward review and is locked. Approve or reject the pending review before deleting."
2352
2485
  );
@@ -2356,30 +2489,34 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
2356
2489
  } catch {
2357
2490
  throw new NotFoundError(`Node not found: ${nodeId}`);
2358
2491
  }
2359
- removeNodeFromTagIndex(nestId, nodeId);
2492
+ await removeNodeFromTagIndex(nestId, nodeId);
2360
2493
  const db = getDb();
2361
- db.transaction(() => {
2362
- db.prepare(
2363
- "DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?"
2364
- ).run(nestId, nodeId);
2365
- db.prepare(
2366
- "DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?"
2367
- ).run(nestId, nodeId);
2368
- db.prepare(
2369
- "DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?"
2370
- ).run(nestId, nodeId);
2371
- db.prepare(
2494
+ await db.transaction(async (tx) => {
2495
+ await tx.run(
2496
+ "DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?",
2497
+ [nestId, nodeId]
2498
+ );
2499
+ await tx.run(
2500
+ "DELETE FROM review_requests WHERE nest_id = ? AND node_id = ?",
2501
+ [nestId, nodeId]
2502
+ );
2503
+ await tx.run(
2504
+ "DELETE FROM approved_versions WHERE nest_id = ? AND node_id = ?",
2505
+ [nestId, nodeId]
2506
+ );
2507
+ await tx.run(
2372
2508
  `DELETE FROM stewards
2373
- WHERE nest_id = ? AND scope = 'document' AND node_pattern = ?`
2374
- ).run(nestId, nodeId);
2375
- })();
2376
- trackEvent("node.delete", { nestId, nodeId });
2509
+ WHERE nest_id = ? AND scope = 'document' AND node_pattern = ?`,
2510
+ [nestId, nodeId]
2511
+ );
2512
+ });
2513
+ await trackEvent("node.delete", { nestId, nodeId });
2377
2514
  return c.json({ deleted: true });
2378
2515
  });
2379
- function getUserEmail(c) {
2516
+ async function getUserEmail(c) {
2380
2517
  const userId = c.get("userId");
2381
2518
  const db = getDb();
2382
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
2519
+ const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
2383
2520
  return user?.email || "anonymous@localhost";
2384
2521
  }
2385
2522
 
@@ -2473,11 +2610,12 @@ function projectThreadsToMarkdown(artifactTitle, threads) {
2473
2610
  }
2474
2611
 
2475
2612
  // src/annotations/service.ts
2476
- function loadComments(threadId) {
2613
+ async function loadComments(threadId) {
2477
2614
  const db = getDb();
2478
- const rows = db.prepare(
2479
- "SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, rowid ASC"
2480
- ).all(threadId);
2615
+ const rows = await db.all(
2616
+ "SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, rowid ASC",
2617
+ [threadId]
2618
+ );
2481
2619
  return rows.map((r) => ({
2482
2620
  id: r.id,
2483
2621
  author: r.author,
@@ -2485,7 +2623,7 @@ function loadComments(threadId) {
2485
2623
  createdAt: r.created_at
2486
2624
  }));
2487
2625
  }
2488
- function rowToThread(row) {
2626
+ async function rowToThread(row) {
2489
2627
  let anchor = null;
2490
2628
  if (row.anchor_json) {
2491
2629
  try {
@@ -2505,19 +2643,20 @@ function rowToThread(row) {
2505
2643
  createdAt: row.created_at,
2506
2644
  resolvedBy: row.resolved_by,
2507
2645
  resolvedAt: row.resolved_at,
2508
- comments: loadComments(row.id)
2646
+ comments: await loadComments(row.id)
2509
2647
  };
2510
2648
  }
2511
- function getThreadRow(threadId) {
2512
- return getDb().prepare("SELECT * FROM annotation_threads WHERE id = ?").get(threadId);
2649
+ async function getThreadRow(threadId) {
2650
+ return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
2513
2651
  }
2514
- function listThreads(nestId, nodeId) {
2515
- const rows = getDb().prepare(
2516
- "SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, rowid ASC"
2517
- ).all(nestId, nodeId);
2518
- return rows.map(rowToThread);
2652
+ async function listThreads(nestId, nodeId) {
2653
+ const rows = await getDb().all(
2654
+ "SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, rowid ASC",
2655
+ [nestId, nodeId]
2656
+ );
2657
+ return Promise.all(rows.map(rowToThread));
2519
2658
  }
2520
- function createThread(nestId, nodeId, input, authorEmail) {
2659
+ async function createThread(nestId, nodeId, input, authorEmail) {
2521
2660
  const body = (input.body ?? "").trim();
2522
2661
  if (!body) {
2523
2662
  throw new Error("comment body is required");
@@ -2525,61 +2664,66 @@ function createThread(nestId, nodeId, input, authorEmail) {
2525
2664
  const db = getDb();
2526
2665
  const id = uuid3();
2527
2666
  const anchor = clampAnchor(input.anchor);
2528
- const snapshot = input.snapshotVersion ?? getApprovedVersion(nestId, nodeId) ?? null;
2529
- const tx = db.transaction(() => {
2530
- db.prepare(
2667
+ const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
2668
+ await db.transaction(async (tx) => {
2669
+ await tx.run(
2531
2670
  `INSERT INTO annotation_threads
2532
2671
  (id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
2533
- VALUES (?, ?, ?, ?, ?, 'open', ?)`
2534
- ).run(
2535
- id,
2536
- nestId,
2537
- nodeId,
2538
- snapshot,
2539
- anchor ? JSON.stringify(anchor) : null,
2540
- authorEmail
2672
+ VALUES (?, ?, ?, ?, ?, 'open', ?)`,
2673
+ [
2674
+ id,
2675
+ nestId,
2676
+ nodeId,
2677
+ snapshot,
2678
+ anchor ? JSON.stringify(anchor) : null,
2679
+ authorEmail
2680
+ ]
2681
+ );
2682
+ await tx.run(
2683
+ "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2684
+ [uuid3(), id, authorEmail, body]
2541
2685
  );
2542
- db.prepare(
2543
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
2544
- ).run(uuid3(), id, authorEmail, body);
2545
2686
  });
2546
- tx();
2547
- return rowToThread(getThreadRow(id));
2687
+ return rowToThread(await getThreadRow(id));
2548
2688
  }
2549
- function getScopedThreadRow(threadId, nestId, nodeId) {
2550
- const row = getThreadRow(threadId);
2689
+ async function getScopedThreadRow(threadId, nestId, nodeId) {
2690
+ const row = await getThreadRow(threadId);
2551
2691
  if (!row || row.nest_id !== nestId || row.node_id !== nodeId) {
2552
2692
  throw new NotFoundError(`Thread not found: ${threadId}`);
2553
2693
  }
2554
2694
  return row;
2555
2695
  }
2556
- function addComment(nestId, nodeId, threadId, authorEmail, body) {
2696
+ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
2557
2697
  const trimmed = (body ?? "").trim();
2558
2698
  if (!trimmed) {
2559
2699
  throw new Error("comment body is required");
2560
2700
  }
2561
- getScopedThreadRow(threadId, nestId, nodeId);
2562
- getDb().prepare(
2563
- "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
2564
- ).run(uuid3(), threadId, authorEmail, trimmed);
2565
- return rowToThread(getThreadRow(threadId));
2701
+ await getScopedThreadRow(threadId, nestId, nodeId);
2702
+ await getDb().run(
2703
+ "INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
2704
+ [uuid3(), threadId, authorEmail, trimmed]
2705
+ );
2706
+ return rowToThread(await getThreadRow(threadId));
2566
2707
  }
2567
- function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
2568
- getScopedThreadRow(threadId, nestId, nodeId);
2708
+ async function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
2709
+ await getScopedThreadRow(threadId, nestId, nodeId);
2710
+ const db = getDb();
2569
2711
  if (status === "resolved") {
2570
- getDb().prepare(
2571
- "UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = datetime('now') WHERE id = ?"
2572
- ).run(byEmail, threadId);
2712
+ await db.run(
2713
+ `UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)} WHERE id = ?`,
2714
+ [byEmail, threadId]
2715
+ );
2573
2716
  } else {
2574
- getDb().prepare(
2575
- "UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?"
2576
- ).run(threadId);
2717
+ await db.run(
2718
+ "UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?",
2719
+ [threadId]
2720
+ );
2577
2721
  }
2578
- return rowToThread(getThreadRow(threadId));
2722
+ return rowToThread(await getThreadRow(threadId));
2579
2723
  }
2580
2724
  async function readArtifact(nestId, nodeId, version) {
2581
- const { storage, versions: versionManager } = engineCache.get(nestId);
2582
- const target = version ?? getApprovedVersion(nestId, nodeId) ?? null;
2725
+ const { storage, versions: versionManager } = await engineCache.get(nestId);
2726
+ const target = version ?? await getApprovedVersion(nestId, nodeId) ?? null;
2583
2727
  let title = nodeId;
2584
2728
  let type = null;
2585
2729
  let liveHtml = "";
@@ -2616,12 +2760,12 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
2616
2760
  try {
2617
2761
  let title = nodeId;
2618
2762
  try {
2619
- const { storage } = engineCache.get(nestId);
2763
+ const { storage } = await engineCache.get(nestId);
2620
2764
  const node = await storage.readDocument(nodeId);
2621
2765
  title = node.frontmatter?.title || nodeId;
2622
2766
  } catch {
2623
2767
  }
2624
- const threads = listThreads(nestId, nodeId);
2768
+ const threads = await listThreads(nestId, nodeId);
2625
2769
  const derivedTitle = `${title} \u2014 Annotations`;
2626
2770
  const markdown = projectThreadsToMarkdown(title, threads);
2627
2771
  const derivedId = derivedAnnotationsId(nodeId);
@@ -2669,25 +2813,25 @@ annotationRoutes.get("/:nodeId{.+}/annotations", async (c) => {
2669
2813
  const nestId = c.req.param("nestId");
2670
2814
  const nodeId = getNodeId(c);
2671
2815
  const userId = c.get("userId");
2672
- const userEmail = resolveCallerEmail(userId);
2673
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2816
+ const userEmail = await resolveCallerEmail(userId);
2817
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2674
2818
  return c.json({ error: "Access denied" }, 403);
2675
2819
  }
2676
- return c.json({ threads: listThreads(nestId, nodeId) });
2820
+ return c.json({ threads: await listThreads(nestId, nodeId) });
2677
2821
  });
2678
2822
  annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
2679
2823
  const nestId = c.req.param("nestId");
2680
2824
  const nodeId = getNodeId(c);
2681
2825
  const userId = c.get("userId");
2682
- const userEmail = resolveCallerEmail(userId);
2683
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2826
+ const userEmail = await resolveCallerEmail(userId);
2827
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2684
2828
  return c.json({ error: "Access denied" }, 403);
2685
2829
  }
2686
2830
  const input = await c.req.json();
2687
2831
  if (!input.body || !input.body.trim()) {
2688
2832
  throw new ValidationError("body is required");
2689
2833
  }
2690
- const thread = createThread(
2834
+ const thread = await createThread(
2691
2835
  nestId,
2692
2836
  nodeId,
2693
2837
  {
@@ -2705,15 +2849,15 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/comments", async (c) =
2705
2849
  const nodeId = getNodeId(c);
2706
2850
  const threadId = c.req.param("threadId");
2707
2851
  const userId = c.get("userId");
2708
- const userEmail = resolveCallerEmail(userId);
2709
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2852
+ const userEmail = await resolveCallerEmail(userId);
2853
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2710
2854
  return c.json({ error: "Access denied" }, 403);
2711
2855
  }
2712
2856
  const input = await c.req.json();
2713
2857
  if (!input.body || !input.body.trim()) {
2714
2858
  throw new ValidationError("body is required");
2715
2859
  }
2716
- const thread = addComment(nestId, nodeId, threadId, userEmail, input.body);
2860
+ const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
2717
2861
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2718
2862
  return c.json({ thread });
2719
2863
  });
@@ -2722,11 +2866,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/resolve", async (c) =>
2722
2866
  const nodeId = getNodeId(c);
2723
2867
  const threadId = c.req.param("threadId");
2724
2868
  const userId = c.get("userId");
2725
- const userEmail = resolveCallerEmail(userId);
2726
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2869
+ const userEmail = await resolveCallerEmail(userId);
2870
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2727
2871
  return c.json({ error: "Access denied" }, 403);
2728
2872
  }
2729
- const thread = setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
2873
+ const thread = await setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
2730
2874
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2731
2875
  return c.json({ thread });
2732
2876
  });
@@ -2735,11 +2879,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/reopen", async (c) =>
2735
2879
  const nodeId = getNodeId(c);
2736
2880
  const threadId = c.req.param("threadId");
2737
2881
  const userId = c.get("userId");
2738
- const userEmail = resolveCallerEmail(userId);
2739
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2882
+ const userEmail = await resolveCallerEmail(userId);
2883
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2740
2884
  return c.json({ error: "Access denied" }, 403);
2741
2885
  }
2742
- const thread = setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
2886
+ const thread = await setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
2743
2887
  await syncAnnotationsNode(nestId, nodeId, userEmail);
2744
2888
  return c.json({ thread });
2745
2889
  });
@@ -2747,8 +2891,8 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
2747
2891
  const nestId = c.req.param("nestId");
2748
2892
  const nodeId = getNodeId(c);
2749
2893
  const userId = c.get("userId");
2750
- const userEmail = resolveCallerEmail(userId);
2751
- if (!canReadNode(nestId, nodeId, userId, userEmail)) {
2894
+ const userEmail = await resolveCallerEmail(userId);
2895
+ if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
2752
2896
  return c.json({ error: "Access denied" }, 403);
2753
2897
  }
2754
2898
  const vRaw = c.req.query("v");
@@ -2939,15 +3083,16 @@ function tokenizePrompt(prompt) {
2939
3083
  const words = prompt.toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
2940
3084
  return Array.from(new Set(words));
2941
3085
  }
2942
- function compilePrompt(prompt, nestId, titles) {
3086
+ async function compilePrompt(prompt, nestId, titles) {
2943
3087
  const tokens = tokenizePrompt(prompt);
2944
3088
  if (tokens.length === 0) {
2945
3089
  return { selector: null, matchedTags: [], matchedTitles: [], unmatched: [] };
2946
3090
  }
2947
3091
  const db = getDb();
2948
- const tagRows = db.prepare(
2949
- "SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?"
2950
- ).all(nestId);
3092
+ const tagRows = await db.all(
3093
+ "SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?",
3094
+ [nestId]
3095
+ );
2951
3096
  const knownTags = new Set(tagRows.map((r) => r.tag_name));
2952
3097
  const matchedTags = /* @__PURE__ */ new Set();
2953
3098
  for (const token of tokens) {
@@ -2977,11 +3122,11 @@ function compilePrompt(prompt, nestId, titles) {
2977
3122
 
2978
3123
  // src/nodes/readable-body.ts
2979
3124
  async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
2980
- if (!isPublicReader(nestId, userId)) return workingBody;
2981
- const approved = getApprovedVersion(nestId, nodeId);
3125
+ if (!await isPublicReader(nestId, userId)) return workingBody;
3126
+ const approved = await getApprovedVersion(nestId, nodeId);
2982
3127
  if (approved == null) return "";
2983
3128
  try {
2984
- const { versions } = engineCache.get(nestId);
3129
+ const { versions } = await engineCache.get(nestId);
2985
3130
  const raw = await versions.reconstructVersion(nodeId, approved);
2986
3131
  return bodyOnly(nodeId, raw);
2987
3132
  } catch {
@@ -2989,11 +3134,11 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
2989
3134
  }
2990
3135
  }
2991
3136
  async function resolveExportBody(nestId, nodeId, workingBody) {
2992
- if (!isStewardshipEnabled(nestId)) return workingBody;
2993
- const approved = getApprovedVersion(nestId, nodeId);
3137
+ if (!await isStewardshipEnabled(nestId)) return workingBody;
3138
+ const approved = await getApprovedVersion(nestId, nodeId);
2994
3139
  if (approved == null) return null;
2995
3140
  try {
2996
- const { versions } = engineCache.get(nestId);
3141
+ const { versions } = await engineCache.get(nestId);
2997
3142
  const raw = await versions.reconstructVersion(nodeId, approved);
2998
3143
  return bodyOnly(nodeId, raw);
2999
3144
  } catch {
@@ -3016,9 +3161,9 @@ function extractWikiTargets(body) {
3016
3161
  return out;
3017
3162
  }
3018
3163
  async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
3019
- const { storage } = engineCache.get(nestId);
3164
+ const { storage } = await engineCache.get(nestId);
3020
3165
  const docs = await storage.discoverDocuments();
3021
- const accessible = filterAccessible(nestId, userId, userEmail, docs);
3166
+ const accessible = await filterAccessible(nestId, userId, userEmail, docs);
3022
3167
  const idSet = new Set(accessible.map((d) => d.id));
3023
3168
  const titleToId = /* @__PURE__ */ new Map();
3024
3169
  for (const d of accessible) {
@@ -3054,7 +3199,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
3054
3199
  }
3055
3200
  }
3056
3201
  const tags = [...tagCounts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
3057
- const exposeStewards = isStewardshipEnabled(nestId) && !isPublicReader(nestId, userId);
3202
+ const exposeStewards = await isStewardshipEnabled(nestId) && !await isPublicReader(nestId, userId);
3058
3203
  const stewardRows = exposeStewards ? (await listStewards({ nestId })).filter(
3059
3204
  // Drop document-scoped rows whose node the caller can't see.
3060
3205
  (s) => s.scope !== "document" || idSet.has(s.nodePattern || "")
@@ -3148,7 +3293,7 @@ var queryRoutes = new Hono6();
3148
3293
  queryRoutes.get("/graph", async (c) => {
3149
3294
  const nestId = c.req.param("nestId");
3150
3295
  const userId = c.get("userId");
3151
- const userEmail = resolveCallerEmail(userId);
3296
+ const userEmail = await resolveCallerEmail(userId);
3152
3297
  const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
3153
3298
  const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
3154
3299
  return c.json(graph);
@@ -3173,7 +3318,7 @@ queryRoutes.post("/context", async (c) => {
3173
3318
  throw new ValidationError("prompt or selector is required");
3174
3319
  }
3175
3320
  const nestId = c.req.param("nestId");
3176
- const { query: queryEngine, storage } = engineCache.get(nestId);
3321
+ const { query: queryEngine, storage } = await engineCache.get(nestId);
3177
3322
  const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
3178
3323
  const hops = body.hops ?? 2;
3179
3324
  const includeDrafts = body.include_drafts === true;
@@ -3183,7 +3328,7 @@ queryRoutes.post("/context", async (c) => {
3183
3328
  const allDocs = await storage.discoverDocuments();
3184
3329
  if (!selector && body.prompt) {
3185
3330
  const titles = allDocs.map((d) => d.frontmatter.title);
3186
- compileDetail = compilePrompt(body.prompt, nestId, titles);
3331
+ compileDetail = await compilePrompt(body.prompt, nestId, titles);
3187
3332
  selector = compileDetail.selector;
3188
3333
  if (compileDetail.matchedTitles.length > 0) {
3189
3334
  const matchedLower = new Set(
@@ -3217,9 +3362,9 @@ queryRoutes.post("/context", async (c) => {
3217
3362
  }
3218
3363
  }
3219
3364
  const userId = c.get("userId");
3220
- const userEmail = resolveCallerEmail(userId);
3365
+ const userEmail = await resolveCallerEmail(userId);
3221
3366
  const beforePermission = documents.length;
3222
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
3367
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
3223
3368
  const permissionFiltered = beforePermission - accessible.length;
3224
3369
  const readable = await Promise.all(
3225
3370
  accessible.map(async (doc) => ({
@@ -3278,13 +3423,13 @@ queryRoutes.post("/query", async (c) => {
3278
3423
  throw new ValidationError("query is required");
3279
3424
  }
3280
3425
  const nestId = c.req.param("nestId");
3281
- const { query: queryEngine } = engineCache.get(nestId);
3426
+ const { query: queryEngine } = await engineCache.get(nestId);
3282
3427
  const result = await queryEngine.query(body.query, {
3283
3428
  hops: body.hops ?? 2
3284
3429
  });
3285
3430
  const userId = c.get("userId");
3286
- const userEmail = resolveCallerEmail(userId);
3287
- const accessible = filterAccessible(nestId, userId, userEmail, result.documents);
3431
+ const userEmail = await resolveCallerEmail(userId);
3432
+ const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
3288
3433
  return c.json({
3289
3434
  query: body.query,
3290
3435
  count: accessible.length,
@@ -3303,7 +3448,7 @@ queryRoutes.get("/search", async (c) => {
3303
3448
  throw new ValidationError("q query parameter is required");
3304
3449
  }
3305
3450
  const nestId = c.req.param("nestId");
3306
- const { storage } = engineCache.get(nestId);
3451
+ const { storage } = await engineCache.get(nestId);
3307
3452
  const documents = await storage.discoverDocuments();
3308
3453
  const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
3309
3454
  const matches = documents.filter((node) => {
@@ -3316,8 +3461,8 @@ queryRoutes.get("/search", async (c) => {
3316
3461
  return terms.every((term) => haystack.includes(term));
3317
3462
  });
3318
3463
  const userId = c.get("userId");
3319
- const userEmail = resolveCallerEmail(userId);
3320
- const accessible = filterAccessible(nestId, userId, userEmail, matches);
3464
+ const userEmail = await resolveCallerEmail(userId);
3465
+ const accessible = await filterAccessible(nestId, userId, userEmail, matches);
3321
3466
  return c.json({
3322
3467
  query: q,
3323
3468
  count: accessible.length,
@@ -3331,7 +3476,7 @@ queryRoutes.get("/search", async (c) => {
3331
3476
  });
3332
3477
  });
3333
3478
  queryRoutes.get("/overview", async (c) => {
3334
- const { storage } = engineCache.get(c.req.param("nestId"));
3479
+ const { storage } = await engineCache.get(c.req.param("nestId"));
3335
3480
  const documents = await storage.discoverDocuments();
3336
3481
  const types = {};
3337
3482
  const tags = {};
@@ -3356,7 +3501,7 @@ queryRoutes.get("/overview", async (c) => {
3356
3501
  });
3357
3502
  });
3358
3503
  queryRoutes.get("/context", async (c) => {
3359
- const { storage } = engineCache.get(c.req.param("nestId"));
3504
+ const { storage } = await engineCache.get(c.req.param("nestId"));
3360
3505
  const content = await storage.readContextMd();
3361
3506
  return c.json({ content: content || "" });
3362
3507
  });
@@ -3365,7 +3510,7 @@ queryRoutes.get("/export", async (c) => {
3365
3510
  throw new ValidationError("format=markdown is required");
3366
3511
  }
3367
3512
  const nestId = c.req.param("nestId");
3368
- const { storage, query: queryEngine } = engineCache.get(nestId);
3513
+ const { storage, query: queryEngine } = await engineCache.get(nestId);
3369
3514
  const selector = c.req.query("selector")?.trim() || null;
3370
3515
  let documents;
3371
3516
  if (selector) {
@@ -3375,9 +3520,9 @@ queryRoutes.get("/export", async (c) => {
3375
3520
  documents = await storage.discoverDocuments();
3376
3521
  }
3377
3522
  const userId = c.get("userId");
3378
- const userEmail = resolveCallerEmail(userId);
3379
- const accessible = filterAccessible(nestId, userId, userEmail, documents);
3380
- const governed = isStewardshipEnabled(nestId);
3523
+ const userEmail = await resolveCallerEmail(userId);
3524
+ const accessible = await filterAccessible(nestId, userId, userEmail, documents);
3525
+ const governed = await isStewardshipEnabled(nestId);
3381
3526
  const resolved = await Promise.all(
3382
3527
  accessible.map(async (n) => {
3383
3528
  const body = await resolveExportBody(nestId, n.id, n.body || "");
@@ -3424,7 +3569,7 @@ queryRoutes.post("/publish", async (c) => {
3424
3569
  throw new ValidationError("documents array or context_md is required");
3425
3570
  }
3426
3571
  const nestId = c.req.param("nestId");
3427
- const { storage } = engineCache.get(nestId);
3572
+ const { storage } = await engineCache.get(nestId);
3428
3573
  const created = [];
3429
3574
  if (body.context_md) {
3430
3575
  await storage.writeContextMd(body.context_md);
@@ -3456,7 +3601,7 @@ queryRoutes.post("/publish", async (c) => {
3456
3601
  };
3457
3602
  const serialized = serializeDocument2(node);
3458
3603
  await storage.writeDocument(id, serialized);
3459
- syncNodeTags(nestId, id, tags);
3604
+ await syncNodeTags(nestId, id, tags);
3460
3605
  created.push(id);
3461
3606
  }
3462
3607
  trackEvent("nest.publish", { nestId, count: created.length });
@@ -3726,8 +3871,8 @@ var TOOL_DEFINITIONS = [
3726
3871
  }
3727
3872
  ];
3728
3873
  async function resolveLlmBody(ctx, node) {
3729
- if (!isStewardshipEnabled(ctx.nestId)) return node.body || "";
3730
- const approved = getApprovedVersion(ctx.nestId, node.id);
3874
+ if (!await isStewardshipEnabled(ctx.nestId)) return node.body || "";
3875
+ const approved = await getApprovedVersion(ctx.nestId, node.id);
3731
3876
  if (approved == null) return null;
3732
3877
  try {
3733
3878
  return await ctx.versionManager.reconstructVersion(node.id, approved);
@@ -3866,7 +4011,7 @@ ${n.body || ""}`;
3866
4011
  return resolved.join("\n\n---\n\n") || "No nodes resolved.";
3867
4012
  }
3868
4013
  case "context_create": {
3869
- if (!canCreateInNest(nestId, userEmail)) {
4014
+ if (!await canCreateInNest(nestId, userEmail)) {
3870
4015
  return "You don't have permission to create documents in this nest.";
3871
4016
  }
3872
4017
  const { node } = await createNode(
@@ -3888,7 +4033,7 @@ ${n.body || ""}`;
3888
4033
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3889
4034
  );
3890
4035
  if (!node) return `Node not found: ${args.title}`;
3891
- const editCheck = canUserEdit(nestId, node.id, userEmail);
4036
+ const editCheck = await canUserEdit(nestId, node.id, userEmail);
3892
4037
  if (!editCheck.allowed) {
3893
4038
  return `You don't have permission to edit "${args.title}": ${editCheck.reason}`;
3894
4039
  }
@@ -3912,7 +4057,7 @@ ${n.body || ""}`;
3912
4057
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3913
4058
  );
3914
4059
  const nodeId = node?.id || "";
3915
- const resolved = resolveStewardsForNode(ctx.nestId, nodeId);
4060
+ const resolved = await resolveStewardsForNode(ctx.nestId, nodeId);
3916
4061
  if (resolved.length === 0) {
3917
4062
  return `No stewards configured for "${args.title}". Changes are auto-approved.`;
3918
4063
  }
@@ -3968,14 +4113,14 @@ ${list}`;
3968
4113
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
3969
4114
  );
3970
4115
  if (!node) return `Node not found: ${args.title}`;
3971
- const submitCheck = canUserEdit(ctx.nestId, node.id, userEmail);
4116
+ const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
3972
4117
  if (!submitCheck.allowed) {
3973
4118
  return `You don't have permission to submit "${args.title}" for review: ${submitCheck.reason}`;
3974
4119
  }
3975
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4120
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
3976
4121
  if (currentVersion === 0) return `No versions found for "${args.title}"`;
3977
4122
  try {
3978
- const request = submitForReview({
4123
+ const request = await submitForReview({
3979
4124
  nestId: ctx.nestId,
3980
4125
  nodeId: node.id,
3981
4126
  version: currentVersion,
@@ -3983,7 +4128,7 @@ ${list}`;
3983
4128
  note: args.note,
3984
4129
  priority: args.priority
3985
4130
  });
3986
- const resolved = resolveStewardsForNode(
4131
+ const resolved = await resolveStewardsForNode(
3987
4132
  ctx.nestId,
3988
4133
  node.id
3989
4134
  );
@@ -4002,7 +4147,7 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
4002
4147
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4003
4148
  );
4004
4149
  if (!node) return `Node not found: ${args.title}`;
4005
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4150
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
4006
4151
  try {
4007
4152
  const request = await approve({
4008
4153
  nestId: ctx.nestId,
@@ -4023,9 +4168,9 @@ Note: ${args.note}` : ""}`;
4023
4168
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4024
4169
  );
4025
4170
  if (!node) return `Node not found: ${args.title}`;
4026
- const currentVersion = getCurrentVersion(ctx.nestId, node.id);
4171
+ const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
4027
4172
  try {
4028
- const request = reject({
4173
+ const request = await reject({
4029
4174
  nestId: ctx.nestId,
4030
4175
  nodeId: node.id,
4031
4176
  version: currentVersion,
@@ -4044,8 +4189,8 @@ Reason: ${args.note}`;
4044
4189
  (n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
4045
4190
  );
4046
4191
  if (!node) return `Node not found: ${args.title}`;
4047
- const allVersions = getVersions(ctx.nestId, node.id);
4048
- const approved = getApprovedVersion(ctx.nestId, node.id);
4192
+ const allVersions = await getVersions(ctx.nestId, node.id);
4193
+ const approved = await getApprovedVersion(ctx.nestId, node.id);
4049
4194
  if (allVersions.length === 0) {
4050
4195
  return `No version history for "${args.title}".`;
4051
4196
  }
@@ -4081,7 +4226,7 @@ ${list}`;
4081
4226
  }
4082
4227
  }
4083
4228
  case "context_share_nest": {
4084
- const roles = resolveUserRoles(ctx.nestId, ctx.userEmail);
4229
+ const roles = await resolveUserRoles(ctx.nestId, ctx.userEmail);
4085
4230
  if (!canManageWith(roles)) {
4086
4231
  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.";
4087
4232
  }
@@ -4100,7 +4245,7 @@ ${list}`;
4100
4245
  }
4101
4246
  }
4102
4247
  case "context_unsynced_list": {
4103
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
4248
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
4104
4249
  return "You don't have permission to list unsynced folders. Server admin only.";
4105
4250
  }
4106
4251
  const folders = listUnsyncedFolders();
@@ -4113,7 +4258,7 @@ ${list}`;
4113
4258
  ${list}`;
4114
4259
  }
4115
4260
  case "context_sync_folder": {
4116
- if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
4261
+ if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
4117
4262
  return "You don't have permission to sync folders. Server admin only.";
4118
4263
  }
4119
4264
  try {
@@ -4135,9 +4280,9 @@ ${list}`;
4135
4280
  // src/mcp/routes.ts
4136
4281
  import { z } from "zod";
4137
4282
  var mcpRoutes = new Hono7();
4138
- function getUserEmail2(userId) {
4283
+ async function getUserEmail2(userId) {
4139
4284
  const db = getDb();
4140
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
4285
+ const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
4141
4286
  return user?.email || "anonymous@localhost";
4142
4287
  }
4143
4288
  function createMcpServerForNest(nestId, userId, userEmail) {
@@ -4145,7 +4290,6 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4145
4290
  { name: `contextnest-${nestId}`, version: "1.0.0" },
4146
4291
  { capabilities: { tools: {} } }
4147
4292
  );
4148
- const engine = engineCache.get(nestId);
4149
4293
  for (const tool of TOOL_DEFINITIONS) {
4150
4294
  const props = tool.inputSchema.properties || {};
4151
4295
  const required = tool.inputSchema.required || [];
@@ -4160,6 +4304,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4160
4304
  shape[key] = field;
4161
4305
  }
4162
4306
  server.tool(tool.name, tool.description, shape, async (args) => {
4307
+ const engine = await engineCache.get(nestId);
4163
4308
  const text = await handleToolCall(tool.name, args, {
4164
4309
  storage: engine.storage,
4165
4310
  queryEngine: engine.query,
@@ -4176,7 +4321,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
4176
4321
  mcpRoutes.all("/", async (c) => {
4177
4322
  const nestId = c.req.param("nestId");
4178
4323
  const userId = c.get("userId");
4179
- const userEmail = getUserEmail2(userId);
4324
+ const userEmail = await getUserEmail2(userId);
4180
4325
  const server = createMcpServerForNest(nestId, userId, userEmail);
4181
4326
  const transport = new WebStandardStreamableHTTPServerTransport({
4182
4327
  sessionIdGenerator: void 0,
@@ -4197,41 +4342,43 @@ import { Hono as Hono8 } from "hono";
4197
4342
 
4198
4343
  // src/governance/comment-service.ts
4199
4344
  import { v4 as uuid4 } from "uuid";
4200
- function createComment(params) {
4345
+ async function createComment(params) {
4201
4346
  const db = getDb();
4202
4347
  const body = (params.body ?? "").trim();
4203
4348
  if (!body) {
4204
4349
  throw new Error("Comment body is required");
4205
4350
  }
4206
4351
  if (params.parentId) {
4207
- const parent = db.prepare(
4208
- "SELECT id FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
4209
- ).get(params.parentId, params.nestId, params.nodeId);
4352
+ const parent = await db.get(
4353
+ "SELECT id FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?",
4354
+ [params.parentId, params.nestId, params.nodeId]
4355
+ );
4210
4356
  if (!parent) {
4211
4357
  throw new Error("Parent comment not found on this node");
4212
4358
  }
4213
4359
  }
4214
4360
  const id = uuid4();
4215
- db.prepare(
4361
+ await db.run(
4216
4362
  `INSERT INTO comments
4217
4363
  (id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
4218
4364
  parent_id, author, body)
4219
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
4220
- ).run(
4221
- id,
4222
- params.nestId,
4223
- params.nodeId,
4224
- params.version ?? null,
4225
- params.anchor?.start ?? null,
4226
- params.anchor?.end ?? null,
4227
- params.anchor?.text ?? null,
4228
- params.parentId ?? null,
4229
- params.author,
4230
- body
4365
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4366
+ [
4367
+ id,
4368
+ params.nestId,
4369
+ params.nodeId,
4370
+ params.version ?? null,
4371
+ params.anchor?.start ?? null,
4372
+ params.anchor?.end ?? null,
4373
+ params.anchor?.text ?? null,
4374
+ params.parentId ?? null,
4375
+ params.author,
4376
+ body
4377
+ ]
4231
4378
  );
4232
- return getComment(id);
4379
+ return await getComment(id);
4233
4380
  }
4234
- function listComments(nestId, nodeId, opts = {}) {
4381
+ async function listComments(nestId, nodeId, opts = {}) {
4235
4382
  const db = getDb();
4236
4383
  const args = [nestId, nodeId];
4237
4384
  let statusClause = "";
@@ -4239,35 +4386,38 @@ function listComments(nestId, nodeId, opts = {}) {
4239
4386
  statusClause = " AND status = ?";
4240
4387
  args.push(opts.status);
4241
4388
  }
4242
- const rows = db.prepare(
4389
+ const rows = await db.all(
4243
4390
  `SELECT * FROM comments
4244
4391
  WHERE nest_id = ? AND node_id = ?${statusClause}
4245
- ORDER BY created_at ASC`
4246
- ).all(...args);
4392
+ ORDER BY created_at ASC`,
4393
+ args
4394
+ );
4247
4395
  return rows.map(rowToComment);
4248
4396
  }
4249
- function getComment(id) {
4397
+ async function getComment(id) {
4250
4398
  const db = getDb();
4251
- const row = db.prepare("SELECT * FROM comments WHERE id = ?").get(id);
4399
+ const row = await db.get("SELECT * FROM comments WHERE id = ?", [id]);
4252
4400
  return row ? rowToComment(row) : null;
4253
4401
  }
4254
- function resolveComment(params) {
4402
+ async function resolveComment(params) {
4255
4403
  const db = getDb();
4256
- const existing = db.prepare(
4257
- "SELECT id, status FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
4258
- ).get(params.commentId, params.nestId, params.nodeId);
4404
+ const existing = await db.get(
4405
+ "SELECT id, status FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?",
4406
+ [params.commentId, params.nestId, params.nodeId]
4407
+ );
4259
4408
  if (!existing) {
4260
4409
  throw new Error("Comment not found");
4261
4410
  }
4262
4411
  if (existing.status === "resolved") {
4263
4412
  throw new Error("Comment is already resolved");
4264
4413
  }
4265
- db.prepare(
4414
+ await db.run(
4266
4415
  `UPDATE comments
4267
- SET status = 'resolved', resolved_by = ?, resolved_at = datetime('now')
4268
- WHERE id = ?`
4269
- ).run(params.resolvedBy, params.commentId);
4270
- return getComment(params.commentId);
4416
+ SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)}
4417
+ WHERE id = ?`,
4418
+ [params.resolvedBy, params.commentId]
4419
+ );
4420
+ return await getComment(params.commentId);
4271
4421
  }
4272
4422
  async function getActivity(params) {
4273
4423
  const db = getDb();
@@ -4275,13 +4425,14 @@ async function getActivity(params) {
4275
4425
  const entries = [];
4276
4426
  const nodeFilter = params.nodeId ? " AND node_id = ?" : "";
4277
4427
  const baseArgs = params.nodeId ? [params.nestId, params.nodeId] : [params.nestId];
4278
- const commentRows = db.prepare(
4428
+ const commentRows = await db.all(
4279
4429
  `SELECT id, node_id, author, body, status, created_at, resolved_by, resolved_at
4280
4430
  FROM comments
4281
4431
  WHERE nest_id = ?${nodeFilter}
4282
4432
  ORDER BY COALESCE(resolved_at, created_at) DESC
4283
- LIMIT ?`
4284
- ).all(...baseArgs, limit);
4433
+ LIMIT ?`,
4434
+ [...baseArgs, limit]
4435
+ );
4285
4436
  for (const r of commentRows) {
4286
4437
  entries.push({
4287
4438
  type: "comment",
@@ -4302,13 +4453,14 @@ async function getActivity(params) {
4302
4453
  });
4303
4454
  }
4304
4455
  }
4305
- const versionRows = db.prepare(
4456
+ const versionRows = await db.all(
4306
4457
  `SELECT node_id, version, author, change_note, created_at
4307
4458
  FROM node_versions
4308
4459
  WHERE nest_id = ?${nodeFilter}
4309
4460
  ORDER BY created_at DESC
4310
- LIMIT ?`
4311
- ).all(...baseArgs, limit);
4461
+ LIMIT ?`,
4462
+ [...baseArgs, limit]
4463
+ );
4312
4464
  for (const r of versionRows) {
4313
4465
  entries.push({
4314
4466
  type: "edit",
@@ -4319,13 +4471,14 @@ async function getActivity(params) {
4319
4471
  refId: String(r.version)
4320
4472
  });
4321
4473
  }
4322
- const reviewRows = db.prepare(
4474
+ const reviewRows = await db.all(
4323
4475
  `SELECT id, node_id, requested_by, requested_at, status, resolved_by, resolved_at
4324
4476
  FROM review_requests
4325
4477
  WHERE nest_id = ?${nodeFilter}
4326
4478
  ORDER BY COALESCE(resolved_at, requested_at) DESC
4327
- LIMIT ?`
4328
- ).all(...baseArgs, limit);
4479
+ LIMIT ?`,
4480
+ [...baseArgs, limit]
4481
+ );
4329
4482
  for (const r of reviewRows) {
4330
4483
  entries.push({
4331
4484
  type: "review_requested",
@@ -4468,8 +4621,7 @@ function parseEntry(str) {
4468
4621
  return entry;
4469
4622
  }
4470
4623
  function loadStewardsConfig(nestId) {
4471
- const dataRoot = config.DATA_ROOT;
4472
- const nestPath = join3(dataRoot, "nests", nestId);
4624
+ const nestPath = resolveNestPath(nestId);
4473
4625
  const candidates = [
4474
4626
  join3(nestPath, "stewards.yaml"),
4475
4627
  join3(nestPath, "stewards.yml"),
@@ -4501,24 +4653,25 @@ governanceRoutes.get("/stewards", async (c) => {
4501
4653
  search: search || void 0
4502
4654
  });
4503
4655
  const cache = /* @__PURE__ */ new Map();
4504
- const enriched = stewards.map((s) => {
4656
+ const enriched = [];
4657
+ for (const s of stewards) {
4505
4658
  const key = s.userEmail.toLowerCase();
4506
4659
  let merged = cache.get(key);
4507
4660
  if (!merged) {
4508
4661
  merged = {
4509
- collaboratorRole: getCollaboratorRole(nestId, s.userEmail),
4510
- roles: resolveUserRoles(nestId, s.userEmail)
4662
+ collaboratorRole: await getCollaboratorRole(nestId, s.userEmail),
4663
+ roles: await resolveUserRoles(nestId, s.userEmail)
4511
4664
  };
4512
4665
  cache.set(key, merged);
4513
4666
  }
4514
- return { ...s, ...merged };
4515
- });
4667
+ enriched.push({ ...s, ...merged });
4668
+ }
4516
4669
  return c.json({ stewards: enriched });
4517
4670
  });
4518
4671
  governanceRoutes.post("/stewards", async (c) => {
4519
4672
  const nestId = c.req.param("nestId");
4520
4673
  const body = await c.req.json();
4521
- const assignedBy = getUserEmail3(c);
4674
+ const assignedBy = await getUserEmail3(c);
4522
4675
  if (!body.scope) throw new ValidationError("scope is required");
4523
4676
  if (body.scope === "folder") {
4524
4677
  throw new ValidationError(
@@ -4560,7 +4713,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
4560
4713
  if (!body.role && !body.scope) {
4561
4714
  throw new ValidationError("role or scope is required");
4562
4715
  }
4563
- const steward = updateSteward(stewardId, {
4716
+ const steward = await updateSteward(stewardId, {
4564
4717
  role: body.role,
4565
4718
  scope: body.scope,
4566
4719
  documentId: body.nodePattern,
@@ -4570,7 +4723,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
4570
4723
  });
4571
4724
  governanceRoutes.delete("/stewards/:stewardId", async (c) => {
4572
4725
  const stewardId = c.req.param("stewardId");
4573
- removeSteward(stewardId);
4726
+ await removeSteward(stewardId);
4574
4727
  return c.json({ removed: true });
4575
4728
  });
4576
4729
  governanceRoutes.post("/stewards/sync", async (c) => {
@@ -4579,7 +4732,7 @@ governanceRoutes.post("/stewards/sync", async (c) => {
4579
4732
  if (!stewardsConfig) {
4580
4733
  return c.json({ synced: 0, message: "No stewards.yaml found" });
4581
4734
  }
4582
- const count = syncFromConfig(nestId, stewardsConfig);
4735
+ const count = await syncFromConfig(nestId, stewardsConfig);
4583
4736
  return c.json({ synced: count });
4584
4737
  });
4585
4738
  governanceRoutes.get("/review-queue", async (c) => {
@@ -4593,16 +4746,17 @@ governanceRoutes.get("/review-queue", async (c) => {
4593
4746
  limit,
4594
4747
  offset
4595
4748
  });
4596
- const email = getUserEmail3(c);
4749
+ const email = await getUserEmail3(c);
4597
4750
  const canReviewCache = /* @__PURE__ */ new Map();
4598
- const requests = result.requests.map((r) => {
4751
+ const requests = [];
4752
+ for (const r of result.requests) {
4599
4753
  let canReview = canReviewCache.get(r.nodeId);
4600
4754
  if (canReview === void 0) {
4601
- canReview = canUserApprove(nestId, r.nodeId, email).allowed;
4755
+ canReview = (await canUserApprove(nestId, r.nodeId, email)).allowed;
4602
4756
  canReviewCache.set(r.nodeId, canReview);
4603
4757
  }
4604
- return { ...r, canReview };
4605
- });
4758
+ requests.push({ ...r, canReview });
4759
+ }
4606
4760
  return c.json({ ...result, requests });
4607
4761
  });
4608
4762
  governanceRoutes.get("/external-edits", async (c) => {
@@ -4616,7 +4770,7 @@ governanceRoutes.get("/external-edits", async (c) => {
4616
4770
  });
4617
4771
  governanceRoutes.post("/external-edits/scan", async (c) => {
4618
4772
  const nestId = c.req.param("nestId");
4619
- const actor = getUserEmail3(c);
4773
+ const actor = await getUserEmail3(c);
4620
4774
  const result = await scanNestForDrift(nestId, actor);
4621
4775
  return c.json(result);
4622
4776
  });
@@ -4630,7 +4784,7 @@ var governanceNodeRoutes = new Hono8();
4630
4784
  governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4631
4785
  const nestId = c.req.param("nestId");
4632
4786
  const nodeId = c.req.param("nodeId");
4633
- const { stewards: resolved, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback(nestId, nodeId);
4787
+ const { stewards: resolved, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback(nestId, nodeId);
4634
4788
  return c.json({
4635
4789
  nodeId,
4636
4790
  stewards: resolved.map((r) => ({
@@ -4647,9 +4801,9 @@ governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
4647
4801
  governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4648
4802
  const nestId = c.req.param("nestId");
4649
4803
  const nodeId = c.req.param("nodeId");
4650
- const allVersions = getVersions(nestId, nodeId);
4651
- const approved = getApprovedVersion(nestId, nodeId);
4652
- const { versions: versionManager } = engineCache.get(nestId);
4804
+ const allVersions = await getVersions(nestId, nodeId);
4805
+ const approved = await getApprovedVersion(nestId, nodeId);
4806
+ const { versions: versionManager } = await engineCache.get(nestId);
4653
4807
  const withContent = await Promise.all(
4654
4808
  allVersions.map(async (v) => {
4655
4809
  try {
@@ -4669,14 +4823,14 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
4669
4823
  governanceNodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
4670
4824
  const nestId = c.req.param("nestId");
4671
4825
  const nodeId = c.req.param("nodeId");
4672
- const history = getReviewHistory(nestId, nodeId);
4826
+ const history = await getReviewHistory(nestId, nodeId);
4673
4827
  return c.json({ reviews: history });
4674
4828
  });
4675
4829
  governanceNodeRoutes.get("/:nodeId{.+}/comments", async (c) => {
4676
4830
  const nestId = c.req.param("nestId");
4677
4831
  const nodeId = c.req.param("nodeId");
4678
4832
  const status = c.req.query("status");
4679
- const list = listComments(nestId, nodeId, {
4833
+ const list = await listComments(nestId, nodeId, {
4680
4834
  status: status === "open" || status === "resolved" ? status : void 0
4681
4835
  });
4682
4836
  return c.json({ comments: list });
@@ -4685,9 +4839,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
4685
4839
  const nestId = c.req.param("nestId");
4686
4840
  const nodeId = c.req.param("nodeId");
4687
4841
  const body = await c.req.json();
4688
- const author = getUserEmail3(c);
4842
+ const author = await getUserEmail3(c);
4689
4843
  try {
4690
- const comment = createComment({
4844
+ const comment = await createComment({
4691
4845
  nestId,
4692
4846
  nodeId,
4693
4847
  author,
@@ -4709,9 +4863,9 @@ governanceNodeRoutes.post(
4709
4863
  const nestId = c.req.param("nestId");
4710
4864
  const nodeId = c.req.param("nodeId");
4711
4865
  const commentId = c.req.param("commentId");
4712
- const resolvedBy = getUserEmail3(c);
4866
+ const resolvedBy = await getUserEmail3(c);
4713
4867
  try {
4714
- const comment = resolveComment({
4868
+ const comment = await resolveComment({
4715
4869
  nestId,
4716
4870
  nodeId,
4717
4871
  commentId,
@@ -4738,14 +4892,14 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
4738
4892
  const nestId = c.req.param("nestId");
4739
4893
  const nodeId = c.req.param("nodeId");
4740
4894
  const body = await c.req.json();
4741
- const currentVersion = getCurrentVersion(nestId, nodeId);
4895
+ const currentVersion = await getCurrentVersion(nestId, nodeId);
4742
4896
  if (currentVersion === 0) {
4743
4897
  throw new ValidationError("Node has no versions to review");
4744
4898
  }
4745
- const userEmail = getUserEmail3(c);
4899
+ const userEmail = await getUserEmail3(c);
4746
4900
  let request;
4747
4901
  try {
4748
- request = submitForReview({
4902
+ request = await submitForReview({
4749
4903
  nestId,
4750
4904
  nodeId,
4751
4905
  version: currentVersion,
@@ -4760,7 +4914,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
4760
4914
  }
4761
4915
  throw err;
4762
4916
  }
4763
- const resolved = resolveStewardsForNode(nestId, nodeId);
4917
+ const resolved = await resolveStewardsForNode(nestId, nodeId);
4764
4918
  return c.json(
4765
4919
  {
4766
4920
  review: request,
@@ -4777,13 +4931,13 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
4777
4931
  const nestId = c.req.param("nestId");
4778
4932
  const nodeId = c.req.param("nodeId");
4779
4933
  const body = await c.req.json();
4780
- const userEmail = getUserEmail3(c);
4934
+ const userEmail = await getUserEmail3(c);
4781
4935
  const isAdmin = isSuperAdmin(userEmail);
4782
4936
  try {
4783
4937
  const request = await approve({
4784
4938
  nestId,
4785
4939
  nodeId,
4786
- version: getCurrentVersion(nestId, nodeId),
4940
+ version: await getCurrentVersion(nestId, nodeId),
4787
4941
  approvedBy: userEmail,
4788
4942
  note: body.note,
4789
4943
  override: body.override && isAdmin
@@ -4800,12 +4954,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
4800
4954
  if (!body.note) {
4801
4955
  throw new ValidationError("Rejection note is required");
4802
4956
  }
4803
- const userEmail = getUserEmail3(c);
4957
+ const userEmail = await getUserEmail3(c);
4804
4958
  try {
4805
- const request = reject({
4959
+ const request = await reject({
4806
4960
  nestId,
4807
4961
  nodeId,
4808
- version: getCurrentVersion(nestId, nodeId),
4962
+ version: await getCurrentVersion(nestId, nodeId),
4809
4963
  rejectedBy: userEmail,
4810
4964
  note: body.note
4811
4965
  });
@@ -4817,20 +4971,20 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
4817
4971
  governanceNodeRoutes.get("/:nodeId{.+}/can-access", async (c) => {
4818
4972
  const nestId = c.req.param("nestId");
4819
4973
  const nodeId = c.req.param("nodeId");
4820
- const userEmail = getUserEmail3(c);
4821
- return c.json(canUserAccess(nestId, nodeId, userEmail));
4974
+ const userEmail = await getUserEmail3(c);
4975
+ return c.json(await canUserAccess(nestId, nodeId, userEmail));
4822
4976
  });
4823
4977
  governanceNodeRoutes.get("/:nodeId{.+}/can-approve", async (c) => {
4824
4978
  const nestId = c.req.param("nestId");
4825
4979
  const nodeId = c.req.param("nodeId");
4826
- const userEmail = getUserEmail3(c);
4827
- return c.json(canUserApprove(nestId, nodeId, userEmail));
4980
+ const userEmail = await getUserEmail3(c);
4981
+ return c.json(await canUserApprove(nestId, nodeId, userEmail));
4828
4982
  });
4829
4983
  governanceNodeRoutes.get("/:nodeId{.+}/can-edit", async (c) => {
4830
4984
  const nestId = c.req.param("nestId");
4831
4985
  const nodeId = c.req.param("nodeId");
4832
- const userEmail = getUserEmail3(c);
4833
- return c.json(canUserEdit(nestId, nodeId, userEmail));
4986
+ const userEmail = await getUserEmail3(c);
4987
+ return c.json(await canUserEdit(nestId, nodeId, userEmail));
4834
4988
  });
4835
4989
  governanceNodeRoutes.get("/:nodeId{.+?}/external-edits", async (c) => {
4836
4990
  const nestId = c.req.param("nestId");
@@ -4862,7 +5016,7 @@ governanceNodeRoutes.post(
4862
5016
  const nodeId = c.req.param("nodeId");
4863
5017
  const suggestionId = c.req.param("suggestionId");
4864
5018
  const body = await c.req.json().catch(() => ({}));
4865
- const actor = getUserEmail3(c);
5019
+ const actor = await getUserEmail3(c);
4866
5020
  try {
4867
5021
  const result = await approveExternalEdit({
4868
5022
  nestId,
@@ -4898,7 +5052,7 @@ governanceNodeRoutes.post(
4898
5052
  if (!body.reason) {
4899
5053
  throw new ValidationError("Rejection reason is required");
4900
5054
  }
4901
- const actor = getUserEmail3(c);
5055
+ const actor = await getUserEmail3(c);
4902
5056
  try {
4903
5057
  const result = await rejectExternalEdit({
4904
5058
  nestId,
@@ -4919,31 +5073,38 @@ governanceNodeRoutes.post(
4919
5073
  governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
4920
5074
  const nestId = c.req.param("nestId");
4921
5075
  const nodeId = c.req.param("nodeId");
4922
- const userEmail = getUserEmail3(c);
4923
- const request = cancelReview({
5076
+ const userEmail = await getUserEmail3(c);
5077
+ const request = await cancelReview({
4924
5078
  nestId,
4925
5079
  nodeId,
4926
5080
  cancelledBy: userEmail
4927
5081
  });
4928
5082
  return c.json({ review: request });
4929
5083
  });
4930
- function getUserEmail3(c) {
5084
+ async function getUserEmail3(c) {
4931
5085
  const userId = c.get("userId");
4932
5086
  const db = getDb();
4933
- const user = db.prepare("SELECT email FROM users WHERE id = ?").get(userId);
5087
+ const user = await db.get(
5088
+ "SELECT email FROM users WHERE id = ?",
5089
+ [userId]
5090
+ );
4934
5091
  return user?.email || "anonymous@localhost";
4935
5092
  }
4936
5093
 
4937
5094
  // src/auth/anonymous.ts
4938
5095
  import bcrypt from "bcryptjs";
4939
- function ensureAnonymousUser() {
5096
+ async function ensureAnonymousUser() {
4940
5097
  const db = getDb();
4941
- const exists = db.prepare("SELECT id FROM users WHERE id = ?").get(ANON_USER_ID);
5098
+ const exists = await db.get(
5099
+ "SELECT id FROM users WHERE id = ?",
5100
+ [ANON_USER_ID]
5101
+ );
4942
5102
  if (!exists) {
4943
5103
  const placeholder = bcrypt.hashSync("anon-no-login", 4);
4944
- db.prepare(
4945
- "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
4946
- ).run(ANON_USER_ID, ANON_EMAIL, "Admin", placeholder);
5104
+ await db.run(
5105
+ "INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
5106
+ [ANON_USER_ID, ANON_EMAIL, "Admin", placeholder]
5107
+ );
4947
5108
  }
4948
5109
  return ANON_USER_ID;
4949
5110
  }
@@ -4961,7 +5122,7 @@ var UI_DIR_CANDIDATES = [
4961
5122
  var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
4962
5123
  var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
4963
5124
  var openModeMiddleware = createMiddleware2(async (c, next) => {
4964
- const anonId = ensureAnonymousUser();
5125
+ const anonId = await ensureAnonymousUser();
4965
5126
  c.set("userId", anonId);
4966
5127
  c.set("nestScope", null);
4967
5128
  await next();
@@ -4978,13 +5139,13 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
4978
5139
  return authMiddleware(c, next);
4979
5140
  }
4980
5141
  if (config.AUTH_MODE === "open") {
4981
- const anonId = ensureAnonymousUser();
5142
+ const anonId = await ensureAnonymousUser();
4982
5143
  c.set("userId", anonId);
4983
5144
  c.set("nestScope", null);
4984
5145
  return next();
4985
5146
  }
4986
5147
  if (isPublicReadEligiblePath(c.req.method, c.req.path)) {
4987
- const anonId = ensureAnonymousUser();
5148
+ const anonId = await ensureAnonymousUser();
4988
5149
  c.set("userId", anonId);
4989
5150
  c.set("nestScope", null);
4990
5151
  return next();
@@ -5105,22 +5266,93 @@ function createApp() {
5105
5266
  return c.json({ error: msg }, 500);
5106
5267
  }
5107
5268
  });
5269
+ app.use("/admin/settings", flexAuthMiddleware);
5270
+ const adminSettingsAllowed = async (c) => config.AUTH_MODE === "open" || await isLicenseAdminUserId(c.get("userId"));
5271
+ const currentServerSettings = () => ({
5272
+ promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
5273
+ logo_url: config.LOGO_URL,
5274
+ telemetry_enabled: config.TELEMETRY_ENABLED,
5275
+ public_base_url: config.PUBLIC_BASE_URL,
5276
+ max_body_bytes: config.MAX_BODY_BYTES
5277
+ });
5278
+ app.get("/admin/settings", async (c) => {
5279
+ if (!await adminSettingsAllowed(c))
5280
+ return c.json({ error: "Only the server admin can view this." }, 403);
5281
+ return c.json(currentServerSettings());
5282
+ });
5283
+ app.patch("/admin/settings", async (c) => {
5284
+ if (!await adminSettingsAllowed(c))
5285
+ return c.json({ error: "Only the server admin can change this." }, 403);
5286
+ let body;
5287
+ try {
5288
+ body = await c.req.json();
5289
+ } catch {
5290
+ return c.json({ error: "Invalid JSON body" }, 400);
5291
+ }
5292
+ const errors = [];
5293
+ const pending = [];
5294
+ if ("promptowl_sign_in_gate" in body) {
5295
+ const v = String(body.promptowl_sign_in_gate ?? "").trim().toLowerCase();
5296
+ if (!["open", "admin-only", "disabled"].includes(v))
5297
+ errors.push("promptowl_sign_in_gate must be open | admin-only | disabled");
5298
+ else pending.push({ name: "PROMPTOWL_SIGN_IN_GATE", value: v });
5299
+ }
5300
+ if ("logo_url" in body) {
5301
+ const v = String(body.logo_url ?? "").trim();
5302
+ pending.push({ name: "LOGO_URL", value: v || null });
5303
+ }
5304
+ if ("public_base_url" in body) {
5305
+ const v = String(body.public_base_url ?? "").trim().replace(/\/$/, "");
5306
+ pending.push({ name: "PUBLIC_BASE_URL", value: v || null });
5307
+ }
5308
+ if ("telemetry_enabled" in body) {
5309
+ pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
5310
+ }
5311
+ if ("max_body_bytes" in body) {
5312
+ const n = Number(body.max_body_bytes);
5313
+ if (!Number.isFinite(n) || n < 1024 * 1024 || n > 500 * 1024 * 1024)
5314
+ errors.push("max_body_bytes must be between 1MB and 500MB");
5315
+ else pending.push({ name: "MAX_BODY_BYTES", value: String(Math.floor(n)) });
5316
+ }
5317
+ if (errors.length)
5318
+ return c.json({ error: errors.join("; "), settings: currentServerSettings() }, 400);
5319
+ for (const { name, value } of pending) {
5320
+ try {
5321
+ upsertEnvVar(config.ENV_FILE_PATH, name, value);
5322
+ } catch (e) {
5323
+ return c.json(
5324
+ {
5325
+ error: `Could not persist ${name}: ${e instanceof Error ? e.message : String(e)}`,
5326
+ settings: currentServerSettings()
5327
+ },
5328
+ 500
5329
+ );
5330
+ }
5331
+ if (value === null) delete process.env[name];
5332
+ else process.env[name] = value;
5333
+ }
5334
+ return c.json({ settings: currentServerSettings() });
5335
+ });
5108
5336
  app.use("/stats", flexAuthMiddleware);
5109
5337
  app.get("/stats", async (c) => {
5110
5338
  const db = getDb();
5111
5339
  const userId = c.get("userId");
5112
- const userEmail = resolveCallerEmail(userId);
5113
- const visibleNests = [...listNests(userId), ...listSharedNests(userId)];
5340
+ const userEmail = await resolveCallerEmail(userId);
5341
+ const owned = await listNests(userId);
5342
+ const sharedNests = await listSharedNests(userId);
5343
+ const visibleNests = [...owned, ...sharedNests];
5114
5344
  let documents = 0;
5115
5345
  for (const nest of visibleNests) {
5116
5346
  try {
5117
- const { storage } = engineCache.get(nest.id);
5347
+ const { storage } = await engineCache.get(nest.id);
5118
5348
  const docs = await storage.discoverDocuments();
5119
- documents += filterAccessible(nest.id, userId, userEmail, docs).length;
5349
+ documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
5120
5350
  } catch {
5121
5351
  }
5122
5352
  }
5123
- const usersRow = db.prepare("SELECT COUNT(*) as c FROM users").get();
5353
+ const usersRow = await db.get(
5354
+ "SELECT COUNT(*) as c FROM users"
5355
+ );
5124
5356
  return c.json({
5125
5357
  nests: visibleNests.length,
5126
5358
  documents,
@@ -5174,7 +5406,7 @@ function createApp() {
5174
5406
  c.set("nestPermission", "owner");
5175
5407
  return next();
5176
5408
  }
5177
- const permission = resolveNestPermission(nestId, userId);
5409
+ const permission = await resolveNestPermission(nestId, userId);
5178
5410
  if (permission === "none") {
5179
5411
  return c.json({ error: "Nest not found" }, 404);
5180
5412
  }
@@ -5184,7 +5416,7 @@ function createApp() {
5184
5416
  const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
5185
5417
  const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
5186
5418
  const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
5187
- if (isStewardRoster && !canManageStewards(resolveCallerEmail(userId))) {
5419
+ if (isStewardRoster && !canManageStewards(await resolveCallerEmail(userId))) {
5188
5420
  return c.json(
5189
5421
  {
5190
5422
  error: "You don't have permission to manage stewards. Only the super admin can do this."
@@ -5203,7 +5435,7 @@ function createApp() {
5203
5435
  const isNodeRevert = c.req.method === "POST" && parts.length >= 4 && parts[parts.length - 1] === "revert";
5204
5436
  let stewardEditorBypass = false;
5205
5437
  if (required === "write" && permission === "read" && parts[1] === "nodes") {
5206
- const userEmail = resolveCallerEmail(userId);
5438
+ const userEmail = await resolveCallerEmail(userId);
5207
5439
  if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE" || isNodeRevert)) {
5208
5440
  const idParts = isNodeRevert ? parts.slice(2, -1) : parts.slice(2);
5209
5441
  const rawNodeId = idParts.join("/");
@@ -5212,12 +5444,12 @@ function createApp() {
5212
5444
  nodeId = decodeURIComponent(rawNodeId);
5213
5445
  } catch {
5214
5446
  }
5215
- const resolved = resolveStewardsForNode(nestId, nodeId);
5447
+ const resolved = await resolveStewardsForNode(nestId, nodeId);
5216
5448
  stewardEditorBypass = resolved.some(
5217
5449
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor"
5218
5450
  );
5219
5451
  } else if (parts.length === 2 && c.req.method === "POST") {
5220
- const resolved = resolveStewardsForNode(nestId, "");
5452
+ const resolved = await resolveStewardsForNode(nestId, "");
5221
5453
  stewardEditorBypass = resolved.some(
5222
5454
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && r.steward.scope === "nest"
5223
5455
  );
@@ -5325,28 +5557,30 @@ function createApp() {
5325
5557
 
5326
5558
  // src/db/backfill.ts
5327
5559
  import { NestStorage } from "@promptowl/contextnest-engine";
5328
- import { join as join5 } from "path";
5329
5560
  var MIGRATION_ID = "005_backfill_node_versions_from_history";
5330
5561
  async function backfillNodeVersionsFromHistory(db) {
5331
- const already = db.prepare("SELECT id FROM schema_migrations WHERE id = ?").get(MIGRATION_ID);
5562
+ const already = await db.get(
5563
+ "SELECT id FROM schema_migrations WHERE id = ?",
5564
+ [MIGRATION_ID]
5565
+ );
5332
5566
  if (already) return;
5333
- const nests = db.prepare("SELECT id FROM nests").all();
5334
- const insert = db.prepare(
5335
- `INSERT OR IGNORE INTO node_versions
5567
+ const nests = await db.all("SELECT id FROM nests");
5568
+ const insertSql = insertOrIgnore(
5569
+ db,
5570
+ `INSERT INTO node_versions
5336
5571
  (nest_id, node_id, version, content_hash, author, status, change_note, created_at)
5337
5572
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
5338
5573
  );
5339
- const approvedPin = db.prepare(
5340
- `INSERT OR REPLACE INTO approved_versions
5574
+ const approvedPinSql = `INSERT INTO approved_versions
5341
5575
  (nest_id, node_id, approved_version, approved_by, approved_at)
5342
- VALUES (?, ?, ?, ?, COALESCE(
5343
- (SELECT approved_at FROM approved_versions WHERE nest_id = ? AND node_id = ?),
5344
- datetime('now')))`
5345
- );
5576
+ VALUES (?, ?, ?, ?, ${nowExpr(db)})
5577
+ ON CONFLICT (nest_id, node_id) DO UPDATE SET
5578
+ approved_version = excluded.approved_version,
5579
+ approved_by = excluded.approved_by`;
5346
5580
  let totalInserted = 0;
5347
5581
  let totalDocs = 0;
5348
5582
  for (const { id: nestId } of nests) {
5349
- const nestPath = join5(config.DATA_ROOT, "nests", nestId);
5583
+ const nestPath = resolveNestPath(nestId);
5350
5584
  const storage = new NestStorage(nestPath);
5351
5585
  let docs;
5352
5586
  try {
@@ -5367,15 +5601,16 @@ async function backfillNodeVersionsFromHistory(db) {
5367
5601
  history = null;
5368
5602
  }
5369
5603
  if (!history || history.versions.length === 0) continue;
5370
- const existing = db.prepare(
5371
- `SELECT version FROM node_versions WHERE nest_id = ? AND node_id = ?`
5372
- ).all(nestId, doc.id);
5604
+ const existing = await db.all(
5605
+ `SELECT version FROM node_versions WHERE nest_id = ? AND node_id = ?`,
5606
+ [nestId, doc.id]
5607
+ );
5373
5608
  const known = new Set(existing.map((r) => r.version));
5374
5609
  const tagsJson = doc.frontmatter.tags ? JSON.stringify(doc.frontmatter.tags) : null;
5375
5610
  const latestVersion = history.versions[history.versions.length - 1].version;
5376
5611
  for (const entry of history.versions) {
5377
5612
  if (known.has(entry.version)) continue;
5378
- insert.run(
5613
+ await db.run(insertSql, [
5379
5614
  nestId,
5380
5615
  doc.id,
5381
5616
  entry.version,
@@ -5384,33 +5619,33 @@ async function backfillNodeVersionsFromHistory(db) {
5384
5619
  "approved",
5385
5620
  entry.note || null,
5386
5621
  entry.edited_at || (/* @__PURE__ */ new Date()).toISOString()
5387
- );
5622
+ ]);
5388
5623
  totalInserted += 1;
5389
5624
  }
5390
- const pin = db.prepare(
5391
- `SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?`
5392
- ).get(nestId, doc.id);
5625
+ const pin = await db.get(
5626
+ `SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?`,
5627
+ [nestId, doc.id]
5628
+ );
5393
5629
  if (!pin || pin.approved_version < latestVersion) {
5394
- approvedPin.run(
5630
+ await db.run(approvedPinSql, [
5395
5631
  nestId,
5396
5632
  doc.id,
5397
5633
  latestVersion,
5398
- history.versions[history.versions.length - 1].edited_by || "system:backfill",
5399
- nestId,
5400
- doc.id
5401
- );
5634
+ history.versions[history.versions.length - 1].edited_by || "system:backfill"
5635
+ ]);
5402
5636
  }
5403
5637
  if (tagsJson) {
5404
- const updateTags = db.prepare(
5638
+ await db.run(
5405
5639
  `UPDATE node_versions SET tags_json = ?
5406
- WHERE nest_id = ? AND node_id = ? AND version = ? AND tags_json IS NULL`
5640
+ WHERE nest_id = ? AND node_id = ? AND version = ? AND tags_json IS NULL`,
5641
+ [tagsJson, nestId, doc.id, latestVersion]
5407
5642
  );
5408
- updateTags.run(tagsJson, nestId, doc.id, latestVersion);
5409
5643
  }
5410
5644
  }
5411
5645
  }
5412
- db.prepare("INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)").run(
5413
- MIGRATION_ID
5646
+ await db.run(
5647
+ insertOrIgnore(db, "INSERT INTO schema_migrations (id) VALUES (?)"),
5648
+ [MIGRATION_ID]
5414
5649
  );
5415
5650
  console.log(
5416
5651
  `[backfill] node_versions: scanned ${totalDocs} docs across ${nests.length} nests, inserted ${totalInserted} rows`
@@ -5419,7 +5654,7 @@ async function backfillNodeVersionsFromHistory(db) {
5419
5654
 
5420
5655
  // src/index.ts
5421
5656
  async function main() {
5422
- const db = getDb();
5657
+ const db = await initDb();
5423
5658
  try {
5424
5659
  await backfillNodeVersionsFromHistory(db);
5425
5660
  } catch (err) {
@@ -5453,7 +5688,8 @@ async function main() {
5453
5688
  }
5454
5689
  const app = createApp();
5455
5690
  startLicenseSafetyPoll();
5456
- const driftScanIntervalMs = Number(process.env.DRIFT_SCAN_INTERVAL_MS) || 3e4;
5691
+ const driftRaw = process.env.DRIFT_SCAN_INTERVAL_MS;
5692
+ const driftScanIntervalMs = driftRaw != null && driftRaw !== "" && Number.isFinite(Number(driftRaw)) ? Number(driftRaw) : 3e4;
5457
5693
  if (driftScanIntervalMs > 0) {
5458
5694
  startDriftScanner(driftScanIntervalMs);
5459
5695
  }
@@ -5464,11 +5700,13 @@ async function main() {
5464
5700
  licensed: license.valid
5465
5701
  });
5466
5702
  const authDesc = config.AUTH_MODE === "open" ? "open (no auth \u2014 single-user / LAN only)" : "key (API key required on every request)";
5703
+ 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})`;
5467
5704
  console.log(`
5468
5705
  ContextNest Community Server v0.1.0
5469
5706
  \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
5470
5707
  Port: ${config.PORT}
5471
5708
  Data: ${config.DATA_ROOT}
5709
+ Database: ${dbDesc}
5472
5710
  License: ${license.valid ? `${license.tier}${license.org ? ` (${license.org})` : ""}` : "unlicensed (register at promptowl.ai)"}
5473
5711
  Auth: ${authDesc}
5474
5712
  Telemetry: ${config.TELEMETRY_ENABLED ? "on" : "off"}