@promptowl/contextnest-community 1.5.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/CONFIGURATION.md +60 -2
- package/README.md +13 -0
- package/dist/adapter.postgres-YOODX2BI.js +71 -0
- package/dist/{chunk-EMOE53KX.js → chunk-2TUMMVBG.js} +352 -263
- package/dist/{chunk-RMU3LOPH.js → chunk-7V33Z6CS.js} +258 -69
- package/dist/{chunk-IWA2UDAT.js → chunk-MZGFKBOK.js} +116 -90
- package/dist/chunk-SLTQACJW.js +8 -0
- package/dist/{chunk-HIH7I232.js → chunk-T5L4LYU4.js} +69 -53
- package/dist/index.js +752 -592
- package/dist/migrations.postgres-NVJAGBSF.js +273 -0
- package/dist/{review-service-XNXHF6Q7.js → review-service-QJ3FBOML.js} +5 -4
- package/dist/{stewardship-service-P4M5UEJW.js → stewardship-service-VIKF3VZB.js} +3 -2
- package/dist/{version-service-REGL5CUT.js → version-service-MUZYTYMW.js} +3 -2
- package/dist/web3/assets/{index-DEKnE-ty.js → index-C2Tf2ZFz.js} +97 -97
- package/dist/web3/index.html +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
reject,
|
|
17
17
|
safePublishDocument,
|
|
18
18
|
submitForReview
|
|
19
|
-
} from "./chunk-
|
|
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-
|
|
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,
|
|
@@ -82,13 +84,18 @@ import {
|
|
|
82
84
|
updateSteward,
|
|
83
85
|
upsertEnvVar,
|
|
84
86
|
validateLicense
|
|
85
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-2TUMMVBG.js";
|
|
86
88
|
import {
|
|
87
|
-
ANON_EMAIL,
|
|
88
|
-
ANON_USER_ID,
|
|
89
89
|
config,
|
|
90
|
-
getDb
|
|
91
|
-
|
|
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";
|
|
92
99
|
|
|
93
100
|
// src/index.ts
|
|
94
101
|
import { serve } from "@hono/node-server";
|
|
@@ -116,37 +123,40 @@ function newSessionId() {
|
|
|
116
123
|
function expiryIso(ttlSeconds = SESSION_TTL_SECS) {
|
|
117
124
|
return new Date(Date.now() + ttlSeconds * 1e3).toISOString();
|
|
118
125
|
}
|
|
119
|
-
function createSession(userId, userAgent) {
|
|
126
|
+
async function createSession(userId, userAgent) {
|
|
120
127
|
const db = getDb();
|
|
121
128
|
const id = newSessionId();
|
|
122
|
-
db.
|
|
129
|
+
await db.run(
|
|
123
130
|
`INSERT INTO sessions (id, user_id, expires_at, user_agent)
|
|
124
|
-
VALUES (?, ?, ?, ?)
|
|
125
|
-
|
|
131
|
+
VALUES (?, ?, ?, ?)`,
|
|
132
|
+
[id, userId, expiryIso(), userAgent || null]
|
|
133
|
+
);
|
|
126
134
|
return id;
|
|
127
135
|
}
|
|
128
|
-
function resolveSession(id) {
|
|
136
|
+
async function resolveSession(id) {
|
|
129
137
|
const db = getDb();
|
|
130
|
-
const row = db.
|
|
131
|
-
`SELECT user_id, expires_at FROM sessions WHERE id =
|
|
132
|
-
|
|
138
|
+
const row = await db.get(
|
|
139
|
+
`SELECT user_id, expires_at FROM sessions WHERE id = ?`,
|
|
140
|
+
[id]
|
|
141
|
+
);
|
|
133
142
|
if (!row) return null;
|
|
134
143
|
if (new Date(row.expires_at).getTime() <= Date.now()) {
|
|
135
|
-
db.
|
|
144
|
+
await db.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
136
145
|
return null;
|
|
137
146
|
}
|
|
138
|
-
db.
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
await db.run(
|
|
148
|
+
`UPDATE sessions SET last_seen_at = ${nowExpr(db)} WHERE id = ?`,
|
|
149
|
+
[id]
|
|
150
|
+
);
|
|
141
151
|
return row.user_id;
|
|
142
152
|
}
|
|
143
|
-
function deleteSession(id) {
|
|
153
|
+
async function deleteSession(id) {
|
|
144
154
|
const db = getDb();
|
|
145
|
-
db.
|
|
155
|
+
await db.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
146
156
|
}
|
|
147
|
-
function deleteAllSessionsForUser(userId) {
|
|
157
|
+
async function deleteAllSessionsForUser(userId) {
|
|
148
158
|
const db = getDb();
|
|
149
|
-
db.
|
|
159
|
+
await db.run("DELETE FROM sessions WHERE user_id = ?", [userId]);
|
|
150
160
|
}
|
|
151
161
|
function getSessionIdFromRequest(c) {
|
|
152
162
|
const cookieHeader = c.req.header("Cookie");
|
|
@@ -190,7 +200,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
190
200
|
const db = getDb();
|
|
191
201
|
const sessionId = getSessionIdFromRequest(c);
|
|
192
202
|
if (sessionId) {
|
|
193
|
-
const userId = resolveSession(sessionId);
|
|
203
|
+
const userId = await resolveSession(sessionId);
|
|
194
204
|
if (userId) {
|
|
195
205
|
c.set("userId", userId);
|
|
196
206
|
c.set("nestScope", null);
|
|
@@ -200,11 +210,15 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
200
210
|
const key = parseBearerToken(c.req.header("Authorization"));
|
|
201
211
|
if (key) {
|
|
202
212
|
const keyHash = hashApiKey(key);
|
|
203
|
-
const record = db.
|
|
213
|
+
const record = await db.get(
|
|
214
|
+
"SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?",
|
|
215
|
+
[keyHash]
|
|
216
|
+
);
|
|
204
217
|
if (record) {
|
|
205
|
-
db.
|
|
206
|
-
|
|
207
|
-
|
|
218
|
+
await db.run(
|
|
219
|
+
`UPDATE api_keys SET last_used_at = ${nowExpr(db)} WHERE key_hash = ?`,
|
|
220
|
+
[keyHash]
|
|
221
|
+
);
|
|
208
222
|
c.set("userId", record.user_id);
|
|
209
223
|
c.set("nestScope", record.nest_id);
|
|
210
224
|
return next();
|
|
@@ -327,27 +341,30 @@ function clientIp(c) {
|
|
|
327
341
|
}
|
|
328
342
|
return "unknown";
|
|
329
343
|
}
|
|
330
|
-
function resolveCallerUserId(c) {
|
|
344
|
+
async function resolveCallerUserId(c) {
|
|
331
345
|
const sessionId = getSessionIdFromRequest(c);
|
|
332
346
|
if (sessionId) {
|
|
333
|
-
const uid = resolveSession(sessionId);
|
|
347
|
+
const uid = await resolveSession(sessionId);
|
|
334
348
|
if (uid) return uid;
|
|
335
349
|
}
|
|
336
350
|
const key = parseBearerToken(c.req.header("Authorization"));
|
|
337
351
|
if (key) {
|
|
338
352
|
const db = getDb();
|
|
339
|
-
const row = db.
|
|
353
|
+
const row = await db.get(
|
|
354
|
+
"SELECT user_id FROM api_keys WHERE key_hash = ?",
|
|
355
|
+
[hashApiKey(key)]
|
|
356
|
+
);
|
|
340
357
|
if (row) return row.user_id;
|
|
341
358
|
}
|
|
342
359
|
return null;
|
|
343
360
|
}
|
|
344
|
-
function deviceGateBlocked(c) {
|
|
361
|
+
async function deviceGateBlocked(c) {
|
|
345
362
|
const gate = config.PROMPTOWL_SIGN_IN_GATE;
|
|
346
363
|
if (gate === "open") return null;
|
|
347
364
|
const error = "PromptOwl sign-in is restricted on this server. Use email and password, or contact your admin.";
|
|
348
365
|
if (gate === "disabled") return { error, gate };
|
|
349
|
-
const callerId = resolveCallerUserId(c);
|
|
350
|
-
if (callerId && !isLicenseAdminUserId(callerId)) return { error, gate };
|
|
366
|
+
const callerId = await resolveCallerUserId(c);
|
|
367
|
+
if (callerId && !await isLicenseAdminUserId(callerId)) return { error, gate };
|
|
351
368
|
return null;
|
|
352
369
|
}
|
|
353
370
|
function setSessionCookie(c, sessionId) {
|
|
@@ -376,13 +393,17 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
376
393
|
}
|
|
377
394
|
const db = getDb();
|
|
378
395
|
const meEmail = normalizeEmail(rawEmail);
|
|
379
|
-
let user = db.
|
|
396
|
+
let user = await db.get(
|
|
397
|
+
"SELECT id, email, name FROM users WHERE LOWER(email) = ?",
|
|
398
|
+
[meEmail]
|
|
399
|
+
);
|
|
380
400
|
if (!user) {
|
|
381
401
|
const userId = uuid();
|
|
382
402
|
const placeholderHash = await hashPassword(uuid());
|
|
383
|
-
db.
|
|
384
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
385
|
-
|
|
403
|
+
await db.run(
|
|
404
|
+
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
405
|
+
[userId, meEmail, rawName || null, placeholderHash]
|
|
406
|
+
);
|
|
386
407
|
user = { id: userId, email: meEmail, name: rawName || null };
|
|
387
408
|
trackEvent("user.register", {
|
|
388
409
|
userId,
|
|
@@ -406,7 +427,7 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
406
427
|
license_owner_email: lic.ownerEmail
|
|
407
428
|
};
|
|
408
429
|
}
|
|
409
|
-
const sessionId = createSession(user.id, c.req.header("User-Agent"));
|
|
430
|
+
const sessionId = await createSession(user.id, c.req.header("User-Agent"));
|
|
410
431
|
setSessionCookie(c, sessionId);
|
|
411
432
|
return { ok: true, user, isAdmin, claimBlocked };
|
|
412
433
|
}
|
|
@@ -423,25 +444,30 @@ authRoutes.post("/register", async (c) => {
|
|
|
423
444
|
return c.json({ error: "Too many registration attempts, try again later" }, 429);
|
|
424
445
|
}
|
|
425
446
|
const db = getDb();
|
|
426
|
-
const existing = db.
|
|
447
|
+
const existing = await db.get(
|
|
448
|
+
"SELECT id, is_invited FROM users WHERE LOWER(email) = ?",
|
|
449
|
+
[email]
|
|
450
|
+
);
|
|
427
451
|
let userId;
|
|
428
452
|
const passwordHash = await hashPassword(body.password);
|
|
429
453
|
if (existing && existing.is_invited === 1) {
|
|
430
454
|
userId = existing.id;
|
|
431
|
-
db.
|
|
432
|
-
"UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?"
|
|
433
|
-
|
|
455
|
+
await db.run(
|
|
456
|
+
"UPDATE users SET password_hash = ?, name = COALESCE(?, name), is_invited = 0 WHERE id = ?",
|
|
457
|
+
[passwordHash, body.name || null, userId]
|
|
458
|
+
);
|
|
434
459
|
trackEvent("user.register", { userId, email, claimed: true });
|
|
435
460
|
} else if (existing) {
|
|
436
461
|
throw new ValidationError("Email already registered");
|
|
437
462
|
} else {
|
|
438
463
|
userId = uuid();
|
|
439
|
-
db.
|
|
440
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
441
|
-
|
|
464
|
+
await db.run(
|
|
465
|
+
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
466
|
+
[userId, email, body.name || null, passwordHash]
|
|
467
|
+
);
|
|
442
468
|
trackEvent("user.register", { userId, email });
|
|
443
469
|
}
|
|
444
|
-
const sessionId = createSession(userId, c.req.header("User-Agent"));
|
|
470
|
+
const sessionId = await createSession(userId, c.req.header("User-Agent"));
|
|
445
471
|
setSessionCookie(c, sessionId);
|
|
446
472
|
return c.json(
|
|
447
473
|
{
|
|
@@ -469,9 +495,10 @@ authRoutes.post("/login", async (c) => {
|
|
|
469
495
|
return c.json({ error: "Too many login attempts, try again later" }, 429);
|
|
470
496
|
}
|
|
471
497
|
const db = getDb();
|
|
472
|
-
const user = db.
|
|
473
|
-
"SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?"
|
|
474
|
-
|
|
498
|
+
const user = await db.get(
|
|
499
|
+
"SELECT id, email, name, password_hash, is_admin FROM users WHERE LOWER(email) = ?",
|
|
500
|
+
[emailLower]
|
|
501
|
+
);
|
|
475
502
|
const check = user ? await verifyPassword(body.password, user.password_hash) : { ok: false, needsRehash: false };
|
|
476
503
|
if (!user || !check.ok) {
|
|
477
504
|
if (hasIp) recordFailure(ipKey, LOGIN_LIMIT);
|
|
@@ -485,15 +512,15 @@ authRoutes.post("/login", async (c) => {
|
|
|
485
512
|
if (check.needsRehash) {
|
|
486
513
|
try {
|
|
487
514
|
const newHash = await hashPassword(body.password);
|
|
488
|
-
db.
|
|
515
|
+
await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
489
516
|
newHash,
|
|
490
517
|
user.id
|
|
491
|
-
);
|
|
518
|
+
]);
|
|
492
519
|
} catch {
|
|
493
520
|
}
|
|
494
521
|
}
|
|
495
522
|
trackEvent("user.login", { userId: user.id });
|
|
496
|
-
const sessionId = createSession(user.id, c.req.header("User-Agent"));
|
|
523
|
+
const sessionId = await createSession(user.id, c.req.header("User-Agent"));
|
|
497
524
|
setSessionCookie(c, sessionId);
|
|
498
525
|
return c.json({
|
|
499
526
|
user: {
|
|
@@ -506,7 +533,7 @@ authRoutes.post("/login", async (c) => {
|
|
|
506
533
|
});
|
|
507
534
|
authRoutes.post("/logout", async (c) => {
|
|
508
535
|
const sessionId = getSessionIdFromRequest(c);
|
|
509
|
-
if (sessionId) deleteSession(sessionId);
|
|
536
|
+
if (sessionId) await deleteSession(sessionId);
|
|
510
537
|
clearSessionCookie(c);
|
|
511
538
|
return c.json({ ok: true });
|
|
512
539
|
});
|
|
@@ -516,7 +543,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
516
543
|
);
|
|
517
544
|
const db = getDb();
|
|
518
545
|
const userId = c.get("userId");
|
|
519
|
-
const existing = db.
|
|
546
|
+
const existing = await db.get(
|
|
547
|
+
"SELECT key_prefix FROM api_keys WHERE user_id = ?",
|
|
548
|
+
[userId]
|
|
549
|
+
);
|
|
520
550
|
if (existing) {
|
|
521
551
|
return c.json(
|
|
522
552
|
{
|
|
@@ -528,15 +558,16 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
528
558
|
}
|
|
529
559
|
const apiKey = generateApiKey();
|
|
530
560
|
const keyId = uuid();
|
|
531
|
-
db.
|
|
532
|
-
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)"
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
+
]
|
|
540
571
|
);
|
|
541
572
|
return c.json(
|
|
542
573
|
{ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
|
|
@@ -551,39 +582,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
551
582
|
const userId = c.get("userId");
|
|
552
583
|
const apiKey = generateApiKey();
|
|
553
584
|
const keyId = uuid();
|
|
554
|
-
const prior = db.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
]
|
|
566
601
|
);
|
|
567
|
-
})
|
|
602
|
+
});
|
|
568
603
|
return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
|
|
569
604
|
});
|
|
570
605
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
571
606
|
const db = getDb();
|
|
572
|
-
const keys = db.
|
|
573
|
-
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?"
|
|
574
|
-
|
|
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
|
+
);
|
|
575
611
|
return c.json({ keys });
|
|
576
612
|
});
|
|
577
613
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
578
614
|
const db = getDb();
|
|
579
|
-
const result = db.
|
|
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
|
+
);
|
|
580
619
|
if (result.changes === 0) {
|
|
581
620
|
return c.json({ error: "Key not found" }, 404);
|
|
582
621
|
}
|
|
583
622
|
return c.json({ deleted: true });
|
|
584
623
|
});
|
|
585
624
|
authRoutes.post("/device", async (c) => {
|
|
586
|
-
const blocked = deviceGateBlocked(c);
|
|
625
|
+
const blocked = await deviceGateBlocked(c);
|
|
587
626
|
if (blocked) return c.json(blocked, 403);
|
|
588
627
|
if (!tryConsume(`device:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
|
|
589
628
|
return c.json({ error: "Too many device auth attempts, try again later" }, 429);
|
|
@@ -606,7 +645,7 @@ authRoutes.post("/device", async (c) => {
|
|
|
606
645
|
return c.json(data);
|
|
607
646
|
});
|
|
608
647
|
authRoutes.get("/device/poll", async (c) => {
|
|
609
|
-
const blocked = deviceGateBlocked(c);
|
|
648
|
+
const blocked = await deviceGateBlocked(c);
|
|
610
649
|
if (blocked) return c.json(blocked, 403);
|
|
611
650
|
const code = c.req.query("code");
|
|
612
651
|
const clientSecret = c.req.query("client_secret");
|
|
@@ -689,12 +728,13 @@ authRoutes.get("/sso", async (c) => {
|
|
|
689
728
|
}
|
|
690
729
|
if (!claims.jti) return ssoError("invalid_ticket");
|
|
691
730
|
const db = getDb();
|
|
692
|
-
db.
|
|
731
|
+
await db.run(`DELETE FROM sso_used_jti WHERE expires_at < ${nowExpr(db)}`);
|
|
693
732
|
const expiresAtIso = new Date((claims.exp ?? 0) * 1e3).toISOString();
|
|
694
733
|
try {
|
|
695
|
-
db.
|
|
696
|
-
"INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)"
|
|
697
|
-
|
|
734
|
+
await db.run(
|
|
735
|
+
"INSERT INTO sso_used_jti (jti, expires_at) VALUES (?, ?)",
|
|
736
|
+
[claims.jti, expiresAtIso]
|
|
737
|
+
);
|
|
698
738
|
} catch (err) {
|
|
699
739
|
if (err?.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
700
740
|
return ssoError("ticket_used");
|
|
@@ -706,7 +746,7 @@ authRoutes.get("/sso", async (c) => {
|
|
|
706
746
|
try {
|
|
707
747
|
result = await provisionPromptowlUser(c, sub, claims.name);
|
|
708
748
|
} catch (err) {
|
|
709
|
-
db.
|
|
749
|
+
await db.run("DELETE FROM sso_used_jti WHERE jti = ?", [claims.jti]);
|
|
710
750
|
console.error("[sso] provisioning failed; released jti for retry:", err);
|
|
711
751
|
return ssoError("service_error");
|
|
712
752
|
}
|
|
@@ -719,13 +759,19 @@ authRoutes.get("/admin-status", async (c) => {
|
|
|
719
759
|
const ownerEmail = lic?.valid ? lic.ownerEmail : null;
|
|
720
760
|
let admin = null;
|
|
721
761
|
if (ownerEmail) {
|
|
722
|
-
const ownerRow = db.
|
|
762
|
+
const ownerRow = await db.get(
|
|
763
|
+
"SELECT name FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1",
|
|
764
|
+
[ownerEmail]
|
|
765
|
+
);
|
|
723
766
|
admin = { email: ownerEmail, name: ownerRow?.name ?? null };
|
|
724
767
|
}
|
|
725
|
-
const callerId = resolveCallerUserId(c);
|
|
768
|
+
const callerId = await resolveCallerUserId(c);
|
|
726
769
|
let me = null;
|
|
727
770
|
if (callerId) {
|
|
728
|
-
const row = db.
|
|
771
|
+
const row = await db.get(
|
|
772
|
+
"SELECT email, name FROM users WHERE id = ?",
|
|
773
|
+
[callerId]
|
|
774
|
+
);
|
|
729
775
|
if (row) {
|
|
730
776
|
me = {
|
|
731
777
|
email: row.email,
|
|
@@ -748,7 +794,10 @@ authRoutes.post("/password", authMiddleware, async (c) => {
|
|
|
748
794
|
assertValidPassword(body.next, "new password");
|
|
749
795
|
const db = getDb();
|
|
750
796
|
const userId = c.get("userId");
|
|
751
|
-
const user = db.
|
|
797
|
+
const user = await db.get(
|
|
798
|
+
"SELECT password_hash FROM users WHERE id = ?",
|
|
799
|
+
[userId]
|
|
800
|
+
);
|
|
752
801
|
if (!user) throw new ValidationError("User not found");
|
|
753
802
|
const check = await verifyPassword(body.current, user.password_hash);
|
|
754
803
|
if (!check.ok) {
|
|
@@ -761,20 +810,20 @@ authRoutes.post("/password", authMiddleware, async (c) => {
|
|
|
761
810
|
);
|
|
762
811
|
}
|
|
763
812
|
const newHash = await hashPassword(body.next);
|
|
764
|
-
db.
|
|
813
|
+
await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
765
814
|
newHash,
|
|
766
815
|
userId
|
|
767
|
-
);
|
|
768
|
-
deleteAllSessionsForUser(userId);
|
|
816
|
+
]);
|
|
817
|
+
await deleteAllSessionsForUser(userId);
|
|
769
818
|
clearSessionCookie(c);
|
|
770
819
|
return c.json({ ok: true });
|
|
771
820
|
});
|
|
772
821
|
authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
773
|
-
const callerId = resolveCallerUserId(c);
|
|
822
|
+
const callerId = await resolveCallerUserId(c);
|
|
774
823
|
if (!callerId) {
|
|
775
824
|
return c.json({ error: "Authentication required." }, 401);
|
|
776
825
|
}
|
|
777
|
-
if (!isLicenseAdminUserId(callerId)) {
|
|
826
|
+
if (!await isLicenseAdminUserId(callerId)) {
|
|
778
827
|
return c.json(
|
|
779
828
|
{ error: "Only the license-admin user can reset passwords." },
|
|
780
829
|
403
|
|
@@ -782,7 +831,10 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
782
831
|
}
|
|
783
832
|
const targetId = c.req.param("userId");
|
|
784
833
|
const db = getDb();
|
|
785
|
-
const target = db.
|
|
834
|
+
const target = await db.get(
|
|
835
|
+
"SELECT id, email FROM users WHERE id = ?",
|
|
836
|
+
[targetId]
|
|
837
|
+
);
|
|
786
838
|
if (!target) return c.json({ error: "User not found" }, 404);
|
|
787
839
|
let supplied;
|
|
788
840
|
try {
|
|
@@ -796,14 +848,14 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
796
848
|
const generated = supplied ? null : uuid().replace(/-/g, "").slice(0, 16);
|
|
797
849
|
const newPassword = supplied ?? generated;
|
|
798
850
|
const newHash = await hashPassword(newPassword);
|
|
799
|
-
db.transaction(() => {
|
|
800
|
-
|
|
851
|
+
await db.transaction(async (tx) => {
|
|
852
|
+
await tx.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
801
853
|
newHash,
|
|
802
854
|
target.id
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
})
|
|
806
|
-
deleteAllSessionsForUser(target.id);
|
|
855
|
+
]);
|
|
856
|
+
await tx.run("DELETE FROM api_keys WHERE user_id = ?", [target.id]);
|
|
857
|
+
});
|
|
858
|
+
await deleteAllSessionsForUser(target.id);
|
|
807
859
|
trackEvent("admin.reset_password", { adminId: callerId, userId: target.id });
|
|
808
860
|
return c.json({
|
|
809
861
|
ok: true,
|
|
@@ -814,11 +866,11 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
814
866
|
});
|
|
815
867
|
});
|
|
816
868
|
authRoutes.delete("/users/:userId", async (c) => {
|
|
817
|
-
const callerId = resolveCallerUserId(c);
|
|
869
|
+
const callerId = await resolveCallerUserId(c);
|
|
818
870
|
if (!callerId) {
|
|
819
871
|
return c.json({ error: "Authentication required." }, 401);
|
|
820
872
|
}
|
|
821
|
-
if (!isLicenseAdminUserId(callerId)) {
|
|
873
|
+
if (!await isLicenseAdminUserId(callerId)) {
|
|
822
874
|
return c.json(
|
|
823
875
|
{ error: "Only the license-admin user can remove users." },
|
|
824
876
|
403
|
|
@@ -829,9 +881,15 @@ authRoutes.delete("/users/:userId", async (c) => {
|
|
|
829
881
|
return c.json({ error: "You can't remove your own admin account." }, 400);
|
|
830
882
|
}
|
|
831
883
|
const db = getDb();
|
|
832
|
-
const target = db.
|
|
884
|
+
const target = await db.get(
|
|
885
|
+
"SELECT id, email FROM users WHERE id = ?",
|
|
886
|
+
[targetId]
|
|
887
|
+
);
|
|
833
888
|
if (!target) return c.json({ error: "User not found" }, 404);
|
|
834
|
-
const ownedNests =
|
|
889
|
+
const ownedNests = (await db.get(
|
|
890
|
+
"SELECT COUNT(*) AS c FROM nests WHERE user_id = ?",
|
|
891
|
+
[target.id]
|
|
892
|
+
)).c;
|
|
835
893
|
if (ownedNests > 0) {
|
|
836
894
|
return c.json(
|
|
837
895
|
{
|
|
@@ -841,17 +899,18 @@ authRoutes.delete("/users/:userId", async (c) => {
|
|
|
841
899
|
409
|
|
842
900
|
);
|
|
843
901
|
}
|
|
844
|
-
db.transaction(() => {
|
|
845
|
-
|
|
846
|
-
"DELETE FROM stewards WHERE user_id = ? OR lower(user_email) = lower(?)"
|
|
847
|
-
|
|
848
|
-
db.prepare("DELETE FROM nest_collaborators WHERE user_id = ?").run(
|
|
849
|
-
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]
|
|
850
906
|
);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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);
|
|
855
914
|
trackEvent("admin.remove_user", { adminId: callerId, userId: target.id });
|
|
856
915
|
return c.json({ ok: true, email: target.email });
|
|
857
916
|
});
|
|
@@ -859,7 +918,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
859
918
|
const body = await c.req.json();
|
|
860
919
|
if (!body.email) throw new ValidationError("email is required");
|
|
861
920
|
const email = normalizeEmail(body.email);
|
|
862
|
-
const callerId = resolveCallerUserId(c);
|
|
921
|
+
const callerId = await resolveCallerUserId(c);
|
|
863
922
|
if (!callerId) {
|
|
864
923
|
return c.json(
|
|
865
924
|
{
|
|
@@ -869,7 +928,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
869
928
|
);
|
|
870
929
|
}
|
|
871
930
|
const db = getDb();
|
|
872
|
-
if (!isLicenseAdminUserId(callerId)) {
|
|
931
|
+
if (!await isLicenseAdminUserId(callerId)) {
|
|
873
932
|
return c.json(
|
|
874
933
|
{
|
|
875
934
|
error: "Only the license-admin user can invite teammates. Contact the admin who installed the PromptOwl license on this server to issue invitations."
|
|
@@ -877,29 +936,34 @@ authRoutes.post("/invite", async (c) => {
|
|
|
877
936
|
403
|
|
878
937
|
);
|
|
879
938
|
}
|
|
880
|
-
let user = db.
|
|
939
|
+
let user = await db.get(
|
|
940
|
+
"SELECT id, email FROM users WHERE LOWER(email) = ?",
|
|
941
|
+
[email]
|
|
942
|
+
);
|
|
881
943
|
if (!user) {
|
|
882
944
|
const userId = uuid();
|
|
883
945
|
const placeholderHash = await hashPassword(uuid());
|
|
884
|
-
db.
|
|
885
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
886
|
-
|
|
946
|
+
await db.run(
|
|
947
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
948
|
+
[userId, email, null, placeholderHash]
|
|
949
|
+
);
|
|
887
950
|
user = { id: userId, email };
|
|
888
951
|
}
|
|
889
952
|
const apiKey = generateApiKey();
|
|
890
953
|
const keyId = uuid();
|
|
891
|
-
db.transaction(() => {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, label) VALUES (?, ?, ?, ?, ?)"
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
]
|
|
901
965
|
);
|
|
902
|
-
})
|
|
966
|
+
});
|
|
903
967
|
trackEvent("admin.invite", { adminId: callerId, email: body.email });
|
|
904
968
|
return c.json(
|
|
905
969
|
{
|
|
@@ -911,7 +975,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
911
975
|
);
|
|
912
976
|
});
|
|
913
977
|
authRoutes.get("/teammates", async (c) => {
|
|
914
|
-
const callerId = resolveCallerUserId(c);
|
|
978
|
+
const callerId = await resolveCallerUserId(c);
|
|
915
979
|
if (!callerId) {
|
|
916
980
|
return c.json(
|
|
917
981
|
{
|
|
@@ -921,7 +985,7 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
921
985
|
);
|
|
922
986
|
}
|
|
923
987
|
const db = getDb();
|
|
924
|
-
if (!isLicenseAdminUserId(callerId)) {
|
|
988
|
+
if (!await isLicenseAdminUserId(callerId)) {
|
|
925
989
|
return c.json(
|
|
926
990
|
{
|
|
927
991
|
error: "Only the license-admin user can view the teammates list. Contact the admin who installed the PromptOwl license on this server."
|
|
@@ -929,15 +993,16 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
929
993
|
403
|
|
930
994
|
);
|
|
931
995
|
}
|
|
932
|
-
const teammates = db.
|
|
996
|
+
const teammates = await db.all(
|
|
933
997
|
`SELECT u.id, u.email, u.name, u.is_invited,
|
|
934
998
|
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
|
|
935
999
|
(SELECT MAX(last_used_at) FROM api_keys WHERE user_id = u.id) as last_active
|
|
936
1000
|
FROM users u
|
|
937
1001
|
WHERE u.id != ?
|
|
938
|
-
ORDER BY u.created_at DESC
|
|
939
|
-
|
|
940
|
-
|
|
1002
|
+
ORDER BY u.created_at DESC`,
|
|
1003
|
+
[ANON_USER_ID]
|
|
1004
|
+
);
|
|
1005
|
+
const pendingStewards = await db.all(
|
|
941
1006
|
`SELECT DISTINCT s.user_email AS email
|
|
942
1007
|
FROM stewards s
|
|
943
1008
|
WHERE s.is_active = 1
|
|
@@ -952,7 +1017,7 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
952
1017
|
)
|
|
953
1018
|
)
|
|
954
1019
|
ORDER BY s.user_email`
|
|
955
|
-
)
|
|
1020
|
+
);
|
|
956
1021
|
const enriched = teammates.map((t) => ({ ...t, is_admin: isLicenseAdminEmail(t.email) })).sort((a, b) => Number(b.is_admin) - Number(a.is_admin));
|
|
957
1022
|
return c.json({
|
|
958
1023
|
teammates: enriched,
|
|
@@ -970,52 +1035,70 @@ import { serializeDocument, parseDocument as parseDocument2 } from "@promptowl/c
|
|
|
970
1035
|
function normalizeTag(raw) {
|
|
971
1036
|
return raw.trim().replace(/^#+/, "").toLowerCase();
|
|
972
1037
|
}
|
|
973
|
-
function syncNodeTags(nestId, nodeId, tags) {
|
|
1038
|
+
async function syncNodeTags(nestId, nodeId, tags) {
|
|
974
1039
|
const db = getDb();
|
|
975
1040
|
const normalized = Array.from(
|
|
976
1041
|
new Set(
|
|
977
1042
|
tags.filter((t) => typeof t === "string").map(normalizeTag).filter(Boolean)
|
|
978
1043
|
)
|
|
979
1044
|
);
|
|
980
|
-
|
|
981
|
-
db
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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]
|
|
986
1053
|
);
|
|
987
1054
|
for (const tag of normalized) {
|
|
988
|
-
|
|
1055
|
+
await tx.run(insertSql, [nestId, nodeId, tag]);
|
|
989
1056
|
}
|
|
990
|
-
})
|
|
1057
|
+
});
|
|
991
1058
|
}
|
|
992
|
-
function removeNodeFromTagIndex(nestId, nodeId) {
|
|
1059
|
+
async function removeNodeFromTagIndex(nestId, nodeId) {
|
|
993
1060
|
const db = getDb();
|
|
994
|
-
db.
|
|
995
|
-
"DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?"
|
|
996
|
-
|
|
1061
|
+
await db.run(
|
|
1062
|
+
"DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
|
|
1063
|
+
[nestId, nodeId]
|
|
1064
|
+
);
|
|
997
1065
|
}
|
|
998
1066
|
|
|
999
1067
|
// src/governance/access-guard.ts
|
|
1000
|
-
function resolveCallerEmail(userId) {
|
|
1068
|
+
async function resolveCallerEmail(userId) {
|
|
1001
1069
|
if (!userId) return "admin@localhost";
|
|
1002
1070
|
const db = getDb();
|
|
1003
|
-
const row = db.
|
|
1071
|
+
const row = await db.get(
|
|
1072
|
+
"SELECT email FROM users WHERE id = ?",
|
|
1073
|
+
[userId]
|
|
1074
|
+
);
|
|
1004
1075
|
return row?.email || "admin@localhost";
|
|
1005
1076
|
}
|
|
1006
|
-
function canReadNode(nestId, nodeId, userId, userEmail) {
|
|
1007
|
-
if (isPublicReader(nestId, userId)) {
|
|
1008
|
-
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;
|
|
1009
1080
|
}
|
|
1010
|
-
if (!isStewardshipEnabled(nestId)) return true;
|
|
1011
|
-
return canUserAccess(nestId, nodeId, userEmail).allowed;
|
|
1081
|
+
if (!await isStewardshipEnabled(nestId)) return true;
|
|
1082
|
+
return (await canUserAccess(nestId, nodeId, userEmail)).allowed;
|
|
1012
1083
|
}
|
|
1013
|
-
function filterAccessible(nestId, userId, userEmail, nodes) {
|
|
1014
|
-
if (isPublicReader(nestId, userId)) {
|
|
1015
|
-
|
|
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;
|
|
1093
|
+
}
|
|
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
|
+
}
|
|
1016
1100
|
}
|
|
1017
|
-
|
|
1018
|
-
return nodes.filter((n) => canUserAccess(nestId, n.id, userEmail).allowed);
|
|
1101
|
+
return accessible;
|
|
1019
1102
|
}
|
|
1020
1103
|
|
|
1021
1104
|
// src/governance/external-edit-service.ts
|
|
@@ -1039,7 +1122,7 @@ var communityRbac = {
|
|
|
1039
1122
|
isDocOwner: () => true
|
|
1040
1123
|
};
|
|
1041
1124
|
function docPath(nestId, documentId) {
|
|
1042
|
-
return join(
|
|
1125
|
+
return join(resolveNestPath(nestId), `${documentId}.md`);
|
|
1043
1126
|
}
|
|
1044
1127
|
async function readRaw(nestId, documentId) {
|
|
1045
1128
|
try {
|
|
@@ -1063,7 +1146,7 @@ async function loadChainHead(storage, documentId) {
|
|
|
1063
1146
|
}
|
|
1064
1147
|
}
|
|
1065
1148
|
async function loadLatestApprovedNode(nestId, documentId) {
|
|
1066
|
-
const { storage } = engineCache.get(nestId);
|
|
1149
|
+
const { storage } = await engineCache.get(nestId);
|
|
1067
1150
|
const head = await loadChainHead(storage, documentId);
|
|
1068
1151
|
if (!head) return null;
|
|
1069
1152
|
return parseDocument(docPath(nestId, documentId), head.content, documentId);
|
|
@@ -1079,7 +1162,7 @@ async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner"
|
|
|
1079
1162
|
return res?.meta ?? null;
|
|
1080
1163
|
}
|
|
1081
1164
|
async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
1082
|
-
const { storage } = engineCache.get(nestId);
|
|
1165
|
+
const { storage } = await engineCache.get(nestId);
|
|
1083
1166
|
const node = await storage.readDocument(documentId).catch(() => null);
|
|
1084
1167
|
if (!node) return null;
|
|
1085
1168
|
const raw = await readRaw(nestId, documentId);
|
|
@@ -1106,7 +1189,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
1106
1189
|
return { meta: result.meta, created: true };
|
|
1107
1190
|
}
|
|
1108
1191
|
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
1109
|
-
const { storage } = engineCache.get(nestId);
|
|
1192
|
+
const { storage } = await engineCache.get(nestId);
|
|
1110
1193
|
const docs = await storage.discoverDocuments();
|
|
1111
1194
|
const results = await Promise.all(
|
|
1112
1195
|
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
@@ -1115,7 +1198,7 @@ async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
|
1115
1198
|
return { scanned: docs.length, staged };
|
|
1116
1199
|
}
|
|
1117
1200
|
async function getPendingChange(nestId, documentId) {
|
|
1118
|
-
const { storage } = engineCache.get(nestId);
|
|
1201
|
+
const { storage } = await engineCache.get(nestId);
|
|
1119
1202
|
const list = await listSuggestions(storage, documentId);
|
|
1120
1203
|
for (let i = list.length - 1; i >= 0; i--) {
|
|
1121
1204
|
const meta = list[i];
|
|
@@ -1132,7 +1215,7 @@ async function getPendingChange(nestId, documentId) {
|
|
|
1132
1215
|
return null;
|
|
1133
1216
|
}
|
|
1134
1217
|
async function listNestExternalEdits(nestId) {
|
|
1135
|
-
const { storage } = engineCache.get(nestId);
|
|
1218
|
+
const { storage } = await engineCache.get(nestId);
|
|
1136
1219
|
const docs = await storage.discoverDocuments();
|
|
1137
1220
|
const lists = await Promise.all(
|
|
1138
1221
|
docs.map(async (doc) => {
|
|
@@ -1161,7 +1244,7 @@ async function listNestExternalEdits(nestId) {
|
|
|
1161
1244
|
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
1162
1245
|
}
|
|
1163
1246
|
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
1164
|
-
const { storage } = engineCache.get(nestId);
|
|
1247
|
+
const { storage } = await engineCache.get(nestId);
|
|
1165
1248
|
const found = await readSuggestion(storage, documentId, suggestionId);
|
|
1166
1249
|
if (!found) return null;
|
|
1167
1250
|
return {
|
|
@@ -1178,7 +1261,7 @@ async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
|
1178
1261
|
};
|
|
1179
1262
|
}
|
|
1180
1263
|
async function approveExternalEdit(input) {
|
|
1181
|
-
const { storage } = engineCache.get(input.nestId);
|
|
1264
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
1182
1265
|
let result;
|
|
1183
1266
|
try {
|
|
1184
1267
|
result = await approveSuggestion({
|
|
@@ -1201,8 +1284,8 @@ async function approveExternalEdit(input) {
|
|
|
1201
1284
|
const node = await storage.readDocument(input.documentId);
|
|
1202
1285
|
const versionNum = result.versionEntry.version;
|
|
1203
1286
|
const tags = node.frontmatter.tags || [];
|
|
1204
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1205
|
-
createVersion2({
|
|
1287
|
+
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-MUZYTYMW.js");
|
|
1288
|
+
await createVersion2({
|
|
1206
1289
|
nestId: input.nestId,
|
|
1207
1290
|
nodeId: input.documentId,
|
|
1208
1291
|
version: versionNum,
|
|
@@ -1212,7 +1295,12 @@ async function approveExternalEdit(input) {
|
|
|
1212
1295
|
tags,
|
|
1213
1296
|
changeNote: input.comment || "External edit approved"
|
|
1214
1297
|
});
|
|
1215
|
-
setApprovedVersion2(
|
|
1298
|
+
await setApprovedVersion2(
|
|
1299
|
+
input.nestId,
|
|
1300
|
+
input.documentId,
|
|
1301
|
+
versionNum,
|
|
1302
|
+
input.actor
|
|
1303
|
+
);
|
|
1216
1304
|
} catch (err) {
|
|
1217
1305
|
console.error(
|
|
1218
1306
|
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
@@ -1222,7 +1310,7 @@ async function approveExternalEdit(input) {
|
|
|
1222
1310
|
return result;
|
|
1223
1311
|
}
|
|
1224
1312
|
async function rejectExternalEdit(input) {
|
|
1225
|
-
const { storage } = engineCache.get(input.nestId);
|
|
1313
|
+
const { storage } = await engineCache.get(input.nestId);
|
|
1226
1314
|
const result = await rejectSuggestion({
|
|
1227
1315
|
storage,
|
|
1228
1316
|
rbac: communityRbac,
|
|
@@ -1248,7 +1336,7 @@ async function rejectExternalEdit(input) {
|
|
|
1248
1336
|
var scannerTimer = null;
|
|
1249
1337
|
async function scanAllNests() {
|
|
1250
1338
|
const db = getDb();
|
|
1251
|
-
const rows = db.
|
|
1339
|
+
const rows = await db.all("SELECT id FROM nests");
|
|
1252
1340
|
await Promise.all(
|
|
1253
1341
|
rows.map(
|
|
1254
1342
|
({ id }) => scanNestForDrift(id).catch(
|
|
@@ -1268,9 +1356,9 @@ function startDriftScanner(intervalMs = 3e4) {
|
|
|
1268
1356
|
}
|
|
1269
1357
|
|
|
1270
1358
|
// src/nodes/service.ts
|
|
1271
|
-
function userIdFromEmail(email) {
|
|
1359
|
+
async function userIdFromEmail(email) {
|
|
1272
1360
|
const db = getDb();
|
|
1273
|
-
const row = db.
|
|
1361
|
+
const row = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [email]);
|
|
1274
1362
|
return row?.id ?? ANON_USER_ID;
|
|
1275
1363
|
}
|
|
1276
1364
|
var normalizeTag2 = (t) => t.startsWith("#") ? t : `#${t}`;
|
|
@@ -1302,7 +1390,7 @@ function toNodeResponse(node) {
|
|
|
1302
1390
|
};
|
|
1303
1391
|
}
|
|
1304
1392
|
async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
1305
|
-
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
1393
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
1306
1394
|
let documents = await storage.discoverDocuments();
|
|
1307
1395
|
if (filters.type) {
|
|
1308
1396
|
documents = documents.filter((n) => n.frontmatter.type === filters.type);
|
|
@@ -1313,14 +1401,14 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1313
1401
|
(n) => (n.frontmatter.tags || []).includes(tag)
|
|
1314
1402
|
);
|
|
1315
1403
|
}
|
|
1316
|
-
const userEmail = resolveCallerEmail(userId);
|
|
1317
|
-
const accessible = filterAccessible(nestId, userId, userEmail, documents);
|
|
1318
|
-
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);
|
|
1319
1407
|
const enriched = await Promise.all(
|
|
1320
1408
|
accessible.map(async (doc) => {
|
|
1321
1409
|
const r = toNodeResponse(doc);
|
|
1322
1410
|
if (publicReader) {
|
|
1323
|
-
const approved = getApprovedVersion(nestId, doc.id);
|
|
1411
|
+
const approved = await getApprovedVersion(nestId, doc.id);
|
|
1324
1412
|
if (approved != null) {
|
|
1325
1413
|
try {
|
|
1326
1414
|
const raw = await versionManager.reconstructVersion(doc.id, approved);
|
|
@@ -1344,7 +1432,7 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1344
1432
|
r.pendingChange = pending;
|
|
1345
1433
|
r.status = "external_edit_pending";
|
|
1346
1434
|
} else {
|
|
1347
|
-
r.status = getDisplayStatus(nestId, r.id);
|
|
1435
|
+
r.status = await getDisplayStatus(nestId, r.id);
|
|
1348
1436
|
}
|
|
1349
1437
|
return r;
|
|
1350
1438
|
})
|
|
@@ -1352,15 +1440,15 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1352
1440
|
return filters.limit ? enriched.slice(0, filters.limit) : enriched;
|
|
1353
1441
|
}
|
|
1354
1442
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
1355
|
-
return listNodesForCaller(nestId, userIdFromEmail(userEmail), filters);
|
|
1443
|
+
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
1356
1444
|
}
|
|
1357
1445
|
async function createNode(nestId, input, userEmail) {
|
|
1358
|
-
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
1446
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
1359
1447
|
const slug = input.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1360
1448
|
const id = input.id ?? `nodes/${slug}`;
|
|
1361
1449
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1362
1450
|
const tags = (input.tags || []).map(normalizeTag2);
|
|
1363
|
-
const hasStewards = isStewardshipEnabled(nestId);
|
|
1451
|
+
const hasStewards = await isStewardshipEnabled(nestId);
|
|
1364
1452
|
const initialStatus = hasStewards ? "draft" : "published";
|
|
1365
1453
|
const initialVersion = hasStewards ? 1 : 0;
|
|
1366
1454
|
let node = {
|
|
@@ -1380,7 +1468,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
1380
1468
|
rawContent: ""
|
|
1381
1469
|
};
|
|
1382
1470
|
await storage.writeDocument(id, serializeDocument(node));
|
|
1383
|
-
syncNodeTags(nestId, id, tags);
|
|
1471
|
+
await syncNodeTags(nestId, id, tags);
|
|
1384
1472
|
let savedVersion = 1;
|
|
1385
1473
|
if (hasStewards) {
|
|
1386
1474
|
try {
|
|
@@ -1388,7 +1476,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
1388
1476
|
} catch (err) {
|
|
1389
1477
|
console.error("VersionManager.createVersion failed (node create)", err);
|
|
1390
1478
|
}
|
|
1391
|
-
createVersion({
|
|
1479
|
+
await createVersion({
|
|
1392
1480
|
nestId,
|
|
1393
1481
|
nodeId: id,
|
|
1394
1482
|
version: 1,
|
|
@@ -1404,7 +1492,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
1404
1492
|
note: "Auto-published on create (no stewards configured)"
|
|
1405
1493
|
});
|
|
1406
1494
|
savedVersion = result.node.frontmatter.version || 1;
|
|
1407
|
-
createVersion({
|
|
1495
|
+
await createVersion({
|
|
1408
1496
|
nestId,
|
|
1409
1497
|
nodeId: id,
|
|
1410
1498
|
version: savedVersion,
|
|
@@ -1413,11 +1501,11 @@ async function createNode(nestId, input, userEmail) {
|
|
|
1413
1501
|
status: "published",
|
|
1414
1502
|
tags
|
|
1415
1503
|
});
|
|
1416
|
-
setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
1504
|
+
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
1417
1505
|
node = result.node;
|
|
1418
1506
|
} catch (err) {
|
|
1419
1507
|
console.error("publishDocument failed (node create auto-publish)", err);
|
|
1420
|
-
createVersion({
|
|
1508
|
+
await createVersion({
|
|
1421
1509
|
nestId,
|
|
1422
1510
|
nodeId: id,
|
|
1423
1511
|
version: 1,
|
|
@@ -1428,11 +1516,11 @@ async function createNode(nestId, input, userEmail) {
|
|
|
1428
1516
|
});
|
|
1429
1517
|
}
|
|
1430
1518
|
}
|
|
1431
|
-
trackEvent("node.create", { nestId, nodeId: id });
|
|
1519
|
+
await trackEvent("node.create", { nestId, nodeId: id });
|
|
1432
1520
|
return { node, version: savedVersion };
|
|
1433
1521
|
}
|
|
1434
1522
|
async function registerImportedDocuments(nestId, userEmail) {
|
|
1435
|
-
const { storage } = engineCache.get(nestId);
|
|
1523
|
+
const { storage } = await engineCache.get(nestId);
|
|
1436
1524
|
let docs;
|
|
1437
1525
|
try {
|
|
1438
1526
|
docs = await storage.discoverDocuments();
|
|
@@ -1443,7 +1531,7 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
1443
1531
|
let registered = 0;
|
|
1444
1532
|
for (const doc of docs) {
|
|
1445
1533
|
const nodeId = doc.id;
|
|
1446
|
-
if (getCurrentVersion(nestId, nodeId) > 0) continue;
|
|
1534
|
+
if (await getCurrentVersion(nestId, nodeId) > 0) continue;
|
|
1447
1535
|
const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
|
|
1448
1536
|
const tags = rawTags.map((t) => normalizeTag2(String(t)));
|
|
1449
1537
|
const fmVersion = Number(doc.frontmatter?.version);
|
|
@@ -1459,7 +1547,7 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
1459
1547
|
} catch (err) {
|
|
1460
1548
|
console.error("safePublishDocument failed (import register)", nodeId, err);
|
|
1461
1549
|
}
|
|
1462
|
-
createVersion({
|
|
1550
|
+
await createVersion({
|
|
1463
1551
|
nestId,
|
|
1464
1552
|
nodeId,
|
|
1465
1553
|
version,
|
|
@@ -1469,15 +1557,15 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
1469
1557
|
changeNote: "Imported from existing folder",
|
|
1470
1558
|
tags
|
|
1471
1559
|
});
|
|
1472
|
-
setApprovedVersion(nestId, nodeId, version, userEmail);
|
|
1473
|
-
syncNodeTags(nestId, nodeId, tags);
|
|
1560
|
+
await setApprovedVersion(nestId, nodeId, version, userEmail);
|
|
1561
|
+
await syncNodeTags(nestId, nodeId, tags);
|
|
1474
1562
|
registered++;
|
|
1475
1563
|
}
|
|
1476
|
-
trackEvent("nest.import.documents", { nestId, registered });
|
|
1564
|
+
await trackEvent("nest.import.documents", { nestId, registered });
|
|
1477
1565
|
return registered;
|
|
1478
1566
|
}
|
|
1479
1567
|
async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
1480
|
-
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
1568
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
1481
1569
|
let node;
|
|
1482
1570
|
try {
|
|
1483
1571
|
node = await storage.readDocument(nodeId);
|
|
@@ -1501,16 +1589,16 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1501
1589
|
if (patch.title) {
|
|
1502
1590
|
node = { ...node, frontmatter: { ...node.frontmatter, title: patch.title } };
|
|
1503
1591
|
}
|
|
1504
|
-
const hasStewards = isStewardshipEnabled(nestId);
|
|
1592
|
+
const hasStewards = await isStewardshipEnabled(nestId);
|
|
1505
1593
|
const currentTags = node.frontmatter.tags || [];
|
|
1506
|
-
if (hasStewards && getPendingReview(nestId, nodeId)) {
|
|
1594
|
+
if (hasStewards && await getPendingReview(nestId, nodeId)) {
|
|
1507
1595
|
throw new LockedError(
|
|
1508
1596
|
"This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
|
|
1509
1597
|
);
|
|
1510
1598
|
}
|
|
1511
1599
|
let responseVersion;
|
|
1512
1600
|
if (hasStewards) {
|
|
1513
|
-
const currentVersion = getCurrentVersion(nestId, nodeId);
|
|
1601
|
+
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
1514
1602
|
const newVersion = currentVersion + 1;
|
|
1515
1603
|
node = {
|
|
1516
1604
|
...node,
|
|
@@ -1525,13 +1613,13 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1525
1613
|
};
|
|
1526
1614
|
node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
|
|
1527
1615
|
await storage.writeDocument(nodeId, serializeDocument(node));
|
|
1528
|
-
syncNodeTags(nestId, nodeId, currentTags);
|
|
1616
|
+
await syncNodeTags(nestId, nodeId, currentTags);
|
|
1529
1617
|
try {
|
|
1530
1618
|
await versionManager.createVersion(node, userEmail, { note: patch.changeNote });
|
|
1531
1619
|
} catch (err) {
|
|
1532
1620
|
console.error("VersionManager.createVersion failed (node patch)", err);
|
|
1533
1621
|
}
|
|
1534
|
-
createVersion({
|
|
1622
|
+
await createVersion({
|
|
1535
1623
|
nestId,
|
|
1536
1624
|
nodeId,
|
|
1537
1625
|
version: newVersion,
|
|
@@ -1553,7 +1641,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1553
1641
|
};
|
|
1554
1642
|
node = { ...node, frontmatter: stripUndefined(node.frontmatter) };
|
|
1555
1643
|
await storage.writeDocument(nodeId, serializeDocument(node));
|
|
1556
|
-
syncNodeTags(nestId, nodeId, currentTags);
|
|
1644
|
+
await syncNodeTags(nestId, nodeId, currentTags);
|
|
1557
1645
|
let publishedVersion = (node.frontmatter.version || 0) + 1;
|
|
1558
1646
|
try {
|
|
1559
1647
|
const result = await safePublishDocument(storage, nodeId, {
|
|
@@ -1565,7 +1653,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1565
1653
|
} catch (err) {
|
|
1566
1654
|
console.error("publishDocument failed (node patch auto-publish)", err);
|
|
1567
1655
|
}
|
|
1568
|
-
createVersion({
|
|
1656
|
+
await createVersion({
|
|
1569
1657
|
nestId,
|
|
1570
1658
|
nodeId,
|
|
1571
1659
|
version: publishedVersion,
|
|
@@ -1575,7 +1663,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1575
1663
|
tags: currentTags,
|
|
1576
1664
|
changeNote: patch.changeNote
|
|
1577
1665
|
});
|
|
1578
|
-
setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
|
|
1666
|
+
await setApprovedVersion(nestId, nodeId, publishedVersion, userEmail);
|
|
1579
1667
|
responseVersion = publishedVersion;
|
|
1580
1668
|
}
|
|
1581
1669
|
return { node, version: responseVersion };
|
|
@@ -1583,8 +1671,11 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
1583
1671
|
|
|
1584
1672
|
// src/nests/unsynced-service.ts
|
|
1585
1673
|
import { readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
1586
|
-
import { join as join2, relative } from "path";
|
|
1674
|
+
import { join as join2, relative, resolve } from "path";
|
|
1587
1675
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
1676
|
+
function isNestStorageRoot(absPath) {
|
|
1677
|
+
return resolve(absPath) === resolve(nestStorageRoot());
|
|
1678
|
+
}
|
|
1588
1679
|
function scanMarkdown(dir) {
|
|
1589
1680
|
let count = 0;
|
|
1590
1681
|
let size = 0;
|
|
@@ -1664,7 +1755,9 @@ function listUnsyncedFolders() {
|
|
|
1664
1755
|
if (!e.isDirectory()) continue;
|
|
1665
1756
|
if (e.name.startsWith(".")) continue;
|
|
1666
1757
|
if (RESERVED.has(e.name)) continue;
|
|
1667
|
-
|
|
1758
|
+
const abs = join2(root, e.name);
|
|
1759
|
+
if (isNestStorageRoot(abs)) continue;
|
|
1760
|
+
out.push(...collectLeafFolders(e.name, abs));
|
|
1668
1761
|
}
|
|
1669
1762
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1670
1763
|
console.log(
|
|
@@ -1718,6 +1811,9 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
1718
1811
|
);
|
|
1719
1812
|
assertSafeFolderName(folderName);
|
|
1720
1813
|
const src = join2(config.DATA_ROOT, folderName);
|
|
1814
|
+
if (isNestStorageRoot(src)) {
|
|
1815
|
+
throw new ValidationError("Folder is not eligible for sync");
|
|
1816
|
+
}
|
|
1721
1817
|
let stat;
|
|
1722
1818
|
try {
|
|
1723
1819
|
stat = statSync(src);
|
|
@@ -1737,7 +1833,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
1737
1833
|
}
|
|
1738
1834
|
const segments = folderName.split("/").filter(Boolean);
|
|
1739
1835
|
const baseName = segments[segments.length - 1] || folderName;
|
|
1740
|
-
const nestName = uniqueNestName(userId, baseName);
|
|
1836
|
+
const nestName = await uniqueNestName(userId, baseName);
|
|
1741
1837
|
if (nestName !== baseName) {
|
|
1742
1838
|
console.log(
|
|
1743
1839
|
`[unsynced] name "${baseName}" already in use, using "${nestName}" instead`
|
|
@@ -1761,10 +1857,13 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
1761
1857
|
}
|
|
1762
1858
|
|
|
1763
1859
|
// src/nests/routes.ts
|
|
1764
|
-
function effectivePermission(nestId, userId) {
|
|
1860
|
+
async function effectivePermission(nestId, userId) {
|
|
1765
1861
|
if (config.AUTH_MODE === "open") {
|
|
1766
1862
|
const db = getDb();
|
|
1767
|
-
const nest = db.
|
|
1863
|
+
const nest = await db.get(
|
|
1864
|
+
"SELECT user_id FROM nests WHERE id = ?",
|
|
1865
|
+
[nestId]
|
|
1866
|
+
);
|
|
1768
1867
|
if (nest && nest.user_id === ANON_USER_ID) return "owner";
|
|
1769
1868
|
}
|
|
1770
1869
|
return resolveNestPermission(nestId, userId);
|
|
@@ -1772,21 +1871,19 @@ function effectivePermission(nestId, userId) {
|
|
|
1772
1871
|
var nestRoutes = new Hono2();
|
|
1773
1872
|
nestRoutes.get("/", async (c) => {
|
|
1774
1873
|
const userId = c.get("userId");
|
|
1775
|
-
const owned = listNests(userId);
|
|
1776
|
-
const shared = listSharedNests(userId);
|
|
1777
|
-
const publicExtras = listPublicNests(userId);
|
|
1874
|
+
const owned = await listNests(userId);
|
|
1875
|
+
const shared = await listSharedNests(userId);
|
|
1876
|
+
const publicExtras = await listPublicNests(userId);
|
|
1778
1877
|
const db = getDb();
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
)
|
|
1782
|
-
|
|
1783
|
-
const annotate = (n) => {
|
|
1784
|
-
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);
|
|
1785
1882
|
const is_owner = permission === "owner";
|
|
1786
1883
|
let owner_email = null;
|
|
1787
|
-
const roles = is_owner ? ["owner"] : resolveUserRoles(n.id, callerEmail);
|
|
1884
|
+
const roles = is_owner ? ["owner"] : await resolveUserRoles(n.id, callerEmail);
|
|
1788
1885
|
if (!is_owner && n.user_id !== ANON_USER_ID) {
|
|
1789
|
-
const row =
|
|
1886
|
+
const row = await db.get(ownerEmailSql, [n.user_id]);
|
|
1790
1887
|
owner_email = row?.email ?? null;
|
|
1791
1888
|
}
|
|
1792
1889
|
return { ...n, permission, is_owner, owner_email, roles };
|
|
@@ -1796,7 +1893,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
1796
1893
|
for (const n of [...owned, ...shared, ...publicExtras]) {
|
|
1797
1894
|
if (seen.has(n.id)) continue;
|
|
1798
1895
|
seen.add(n.id);
|
|
1799
|
-
out.push(annotate(n));
|
|
1896
|
+
out.push(await annotate(n));
|
|
1800
1897
|
}
|
|
1801
1898
|
return c.json({ nests: out });
|
|
1802
1899
|
});
|
|
@@ -1818,20 +1915,20 @@ nestRoutes.post("/import", async (c) => {
|
|
|
1818
1915
|
const nest = await importNest(userId, body.name, files);
|
|
1819
1916
|
const documents = await registerImportedDocuments(
|
|
1820
1917
|
nest.id,
|
|
1821
|
-
resolveCallerEmail(userId)
|
|
1918
|
+
await resolveCallerEmail(userId)
|
|
1822
1919
|
);
|
|
1823
1920
|
return c.json({ nest, documents }, 201);
|
|
1824
1921
|
});
|
|
1825
1922
|
nestRoutes.get("/unsynced", async (c) => {
|
|
1826
1923
|
const userId = c.get("userId");
|
|
1827
|
-
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
1924
|
+
if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
|
|
1828
1925
|
throw new ForbiddenError("Only the server admin can list unsynced folders");
|
|
1829
1926
|
}
|
|
1830
1927
|
return c.json({ folders: listUnsyncedFolders() });
|
|
1831
1928
|
});
|
|
1832
1929
|
nestRoutes.post("/unsynced/sync", async (c) => {
|
|
1833
1930
|
const userId = c.get("userId");
|
|
1834
|
-
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
1931
|
+
if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
|
|
1835
1932
|
throw new ForbiddenError("Only the server admin can sync folders");
|
|
1836
1933
|
}
|
|
1837
1934
|
const body = await c.req.json();
|
|
@@ -1841,30 +1938,30 @@ nestRoutes.post("/unsynced/sync", async (c) => {
|
|
|
1841
1938
|
const result = await syncUnsyncedFolder(
|
|
1842
1939
|
userId,
|
|
1843
1940
|
body.name,
|
|
1844
|
-
resolveCallerEmail(userId)
|
|
1941
|
+
await resolveCallerEmail(userId)
|
|
1845
1942
|
);
|
|
1846
1943
|
return c.json(result, 201);
|
|
1847
1944
|
});
|
|
1848
1945
|
nestRoutes.get("/:nestId", async (c) => {
|
|
1849
1946
|
const nestId = c.req.param("nestId");
|
|
1850
1947
|
const userId = c.get("userId");
|
|
1851
|
-
const permission = effectivePermission(nestId, userId);
|
|
1948
|
+
const permission = await effectivePermission(nestId, userId);
|
|
1852
1949
|
if (permission === "none") {
|
|
1853
1950
|
throw new NotFoundError("Nest not found");
|
|
1854
1951
|
}
|
|
1855
|
-
const email = resolveCallerEmail(userId);
|
|
1856
|
-
let roles = resolveUserRoles(nestId, email);
|
|
1952
|
+
const email = await resolveCallerEmail(userId);
|
|
1953
|
+
let roles = await resolveUserRoles(nestId, email);
|
|
1857
1954
|
if (permission === "owner" && !roles.includes("owner")) {
|
|
1858
1955
|
roles = ["owner", ...roles];
|
|
1859
1956
|
}
|
|
1860
|
-
const myStewards = getStewardsForUser(nestId, email);
|
|
1861
|
-
const nest = getNest(nestId);
|
|
1957
|
+
const myStewards = await getStewardsForUser(nestId, email);
|
|
1958
|
+
const nest = await getNest(nestId);
|
|
1862
1959
|
return c.json({ nest, permission, roles, myStewards });
|
|
1863
1960
|
});
|
|
1864
1961
|
nestRoutes.patch("/:nestId", async (c) => {
|
|
1865
1962
|
const nestId = c.req.param("nestId");
|
|
1866
1963
|
const userId = c.get("userId");
|
|
1867
|
-
const permission = effectivePermission(nestId, userId);
|
|
1964
|
+
const permission = await effectivePermission(nestId, userId);
|
|
1868
1965
|
if (permission === "none") {
|
|
1869
1966
|
throw new NotFoundError("Nest not found");
|
|
1870
1967
|
}
|
|
@@ -1874,7 +1971,7 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
1874
1971
|
);
|
|
1875
1972
|
}
|
|
1876
1973
|
const body = await c.req.json();
|
|
1877
|
-
const nest = renameNest(nestId, {
|
|
1974
|
+
const nest = await renameNest(nestId, {
|
|
1878
1975
|
name: body.name,
|
|
1879
1976
|
description: body.description
|
|
1880
1977
|
});
|
|
@@ -1883,13 +1980,13 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
1883
1980
|
nestRoutes.delete("/:nestId", async (c) => {
|
|
1884
1981
|
const nestId = c.req.param("nestId");
|
|
1885
1982
|
const userId = c.get("userId");
|
|
1886
|
-
const nest = getNest(nestId);
|
|
1983
|
+
const nest = await getNest(nestId);
|
|
1887
1984
|
if (!nest) {
|
|
1888
1985
|
throw new NotFoundError("Nest not found");
|
|
1889
1986
|
}
|
|
1890
|
-
const permission = effectivePermission(nestId, userId);
|
|
1987
|
+
const permission = await effectivePermission(nestId, userId);
|
|
1891
1988
|
const isAnonOwned = nest.user_id === ANON_USER_ID;
|
|
1892
|
-
const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && isLicenseAdminUserId(userId);
|
|
1989
|
+
const adminCaretaker = config.AUTH_MODE !== "open" && isAnonOwned && await isLicenseAdminUserId(userId);
|
|
1893
1990
|
if (permission !== "owner" && !adminCaretaker) {
|
|
1894
1991
|
throw new ForbiddenError(
|
|
1895
1992
|
"You don't have permission to delete this nest. Only the nest owner can delete it."
|
|
@@ -1900,20 +1997,20 @@ nestRoutes.delete("/:nestId", async (c) => {
|
|
|
1900
1997
|
});
|
|
1901
1998
|
nestRoutes.get("/:nestId/settings", async (c) => {
|
|
1902
1999
|
const nestId = c.req.param("nestId");
|
|
1903
|
-
const permission = effectivePermission(nestId, c.get("userId"));
|
|
2000
|
+
const permission = await effectivePermission(nestId, c.get("userId"));
|
|
1904
2001
|
if (permission === "none") {
|
|
1905
2002
|
throw new NotFoundError("Nest not found");
|
|
1906
2003
|
}
|
|
1907
2004
|
return c.json({
|
|
1908
|
-
stewardship_enabled: isStewardshipEnabled(nestId),
|
|
1909
|
-
allow_self_approve: nestAllowsSelfApprove(nestId)
|
|
2005
|
+
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
2006
|
+
allow_self_approve: await nestAllowsSelfApprove(nestId)
|
|
1910
2007
|
});
|
|
1911
2008
|
});
|
|
1912
2009
|
nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
1913
2010
|
const nestId = c.req.param("nestId");
|
|
1914
2011
|
const userId = c.get("userId");
|
|
1915
|
-
const isServerAdmin = isLicenseAdminUserId(userId);
|
|
1916
|
-
const permission = effectivePermission(nestId, userId);
|
|
2012
|
+
const isServerAdmin = await isLicenseAdminUserId(userId);
|
|
2013
|
+
const permission = await effectivePermission(nestId, userId);
|
|
1917
2014
|
if (!isServerAdmin && permission !== "owner") {
|
|
1918
2015
|
return c.json(
|
|
1919
2016
|
{
|
|
@@ -1926,17 +2023,17 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
1926
2023
|
let wiped = null;
|
|
1927
2024
|
if (typeof body.stewardship_enabled === "boolean") {
|
|
1928
2025
|
if (body.stewardship_enabled) {
|
|
1929
|
-
setStewardshipEnabled(nestId, true);
|
|
2026
|
+
await setStewardshipEnabled(nestId, true);
|
|
1930
2027
|
} else {
|
|
1931
|
-
wiped = disableStewardshipAndWipeGovernance(nestId);
|
|
2028
|
+
wiped = await disableStewardshipAndWipeGovernance(nestId);
|
|
1932
2029
|
}
|
|
1933
2030
|
}
|
|
1934
2031
|
if (typeof body.allow_self_approve === "boolean") {
|
|
1935
|
-
setAllowSelfApprove(nestId, body.allow_self_approve);
|
|
2032
|
+
await setAllowSelfApprove(nestId, body.allow_self_approve);
|
|
1936
2033
|
}
|
|
1937
2034
|
return c.json({
|
|
1938
|
-
stewardship_enabled: isStewardshipEnabled(nestId),
|
|
1939
|
-
allow_self_approve: nestAllowsSelfApprove(nestId),
|
|
2035
|
+
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
2036
|
+
allow_self_approve: await nestAllowsSelfApprove(nestId),
|
|
1940
2037
|
wiped
|
|
1941
2038
|
});
|
|
1942
2039
|
});
|
|
@@ -1961,21 +2058,28 @@ async function addCollaborator(params) {
|
|
|
1961
2058
|
let userId = params.userId;
|
|
1962
2059
|
if (!userId && params.email) {
|
|
1963
2060
|
const email = normalizeEmail(params.email);
|
|
1964
|
-
const existing = db.
|
|
2061
|
+
const existing = await db.get(
|
|
2062
|
+
"SELECT id FROM users WHERE LOWER(email) = ?",
|
|
2063
|
+
[email]
|
|
2064
|
+
);
|
|
1965
2065
|
if (existing) {
|
|
1966
2066
|
userId = existing.id;
|
|
1967
2067
|
} else {
|
|
1968
2068
|
const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
|
|
1969
2069
|
userId = uuid2();
|
|
1970
|
-
db.
|
|
1971
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
1972
|
-
|
|
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
|
+
);
|
|
1973
2074
|
}
|
|
1974
2075
|
}
|
|
1975
2076
|
if (!userId) {
|
|
1976
2077
|
throw new ValidationError("user_id or email is required");
|
|
1977
2078
|
}
|
|
1978
|
-
const ownerRow = db.
|
|
2079
|
+
const ownerRow = await db.get(
|
|
2080
|
+
"SELECT user_id FROM nests WHERE id = ?",
|
|
2081
|
+
[nestId]
|
|
2082
|
+
);
|
|
1979
2083
|
if (!ownerRow) {
|
|
1980
2084
|
throw new ValidationError("Nest not found");
|
|
1981
2085
|
}
|
|
@@ -1989,19 +2093,29 @@ async function addCollaborator(params) {
|
|
|
1989
2093
|
if (selfByEmail || selfById) {
|
|
1990
2094
|
throw new ValidationError("You can't add yourself as a collaborator.");
|
|
1991
2095
|
}
|
|
1992
|
-
const dupe = db.
|
|
2096
|
+
const dupe = await db.get(
|
|
2097
|
+
"SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
2098
|
+
[nestId, userId]
|
|
2099
|
+
);
|
|
1993
2100
|
if (dupe) {
|
|
1994
2101
|
throw new ConflictError(
|
|
1995
2102
|
`${params.email || "This user"} already has access to this nest. Change their permission instead of adding them again.`
|
|
1996
2103
|
);
|
|
1997
2104
|
}
|
|
1998
|
-
const granterByEmail = !params.grantedByUserId && params.grantedByEmail ?
|
|
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;
|
|
1999
2109
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
2000
2110
|
const collabId = uuid2();
|
|
2001
|
-
db.
|
|
2002
|
-
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)"
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
+
);
|
|
2005
2119
|
}
|
|
2006
2120
|
|
|
2007
2121
|
// src/nests/sharing-routes.ts
|
|
@@ -2009,20 +2123,25 @@ var sharingRoutes = new Hono3();
|
|
|
2009
2123
|
sharingRoutes.get("/collaborators", async (c) => {
|
|
2010
2124
|
const db = getDb();
|
|
2011
2125
|
const nestId = c.req.param("nestId");
|
|
2012
|
-
const collabs = db.
|
|
2126
|
+
const collabs = await db.all(
|
|
2013
2127
|
`SELECT nc.*, u.email FROM nest_collaborators nc
|
|
2014
2128
|
LEFT JOIN users u ON nc.user_id = u.id
|
|
2015
2129
|
WHERE nc.nest_id = ?
|
|
2016
|
-
ORDER BY nc.granted_at
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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({
|
|
2021
2140
|
...collab,
|
|
2022
|
-
stewardRoles: getStewardRolesForUser(nestId, collab.email),
|
|
2023
|
-
roles: resolveUserRoles(nestId, collab.email)
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2141
|
+
stewardRoles: await getStewardRolesForUser(nestId, collab.email),
|
|
2142
|
+
roles: await resolveUserRoles(nestId, collab.email)
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2026
2145
|
return c.json({ collaborators: enriched });
|
|
2027
2146
|
});
|
|
2028
2147
|
sharingRoutes.post("/collaborators", async (c) => {
|
|
@@ -2045,9 +2164,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
|
|
|
2045
2164
|
throw new ValidationError("permission must be read, write, or admin");
|
|
2046
2165
|
}
|
|
2047
2166
|
const db = getDb();
|
|
2048
|
-
const info = db.
|
|
2049
|
-
"UPDATE nest_collaborators SET permission = ? WHERE id = ? AND nest_id = ?"
|
|
2050
|
-
|
|
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
|
+
);
|
|
2051
2171
|
if (info.changes === 0) {
|
|
2052
2172
|
throw new NotFoundError("Collaborator not found");
|
|
2053
2173
|
}
|
|
@@ -2055,9 +2175,10 @@ sharingRoutes.patch("/collaborators/:collabId", async (c) => {
|
|
|
2055
2175
|
});
|
|
2056
2176
|
sharingRoutes.delete("/collaborators/:collabId", async (c) => {
|
|
2057
2177
|
const db = getDb();
|
|
2058
|
-
const info = db.
|
|
2059
|
-
"DELETE FROM nest_collaborators WHERE id = ? AND nest_id = ?"
|
|
2060
|
-
|
|
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
|
+
);
|
|
2061
2182
|
if (info.changes === 0) {
|
|
2062
2183
|
throw new NotFoundError("Collaborator not found");
|
|
2063
2184
|
}
|
|
@@ -2069,10 +2190,10 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
2069
2190
|
throw new ValidationError("visibility must be private or public");
|
|
2070
2191
|
}
|
|
2071
2192
|
const db = getDb();
|
|
2072
|
-
db.
|
|
2193
|
+
await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
|
|
2073
2194
|
body.visibility,
|
|
2074
2195
|
c.req.param("nestId")
|
|
2075
|
-
);
|
|
2196
|
+
]);
|
|
2076
2197
|
return c.json({ visibility: body.visibility });
|
|
2077
2198
|
});
|
|
2078
2199
|
|
|
@@ -2122,7 +2243,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
2122
2243
|
throw new ValidationError("title and content are required");
|
|
2123
2244
|
}
|
|
2124
2245
|
const nestId = c.req.param("nestId");
|
|
2125
|
-
const authorEmail = getUserEmail(c);
|
|
2246
|
+
const authorEmail = await getUserEmail(c);
|
|
2126
2247
|
const { node } = await createNode(
|
|
2127
2248
|
nestId,
|
|
2128
2249
|
{
|
|
@@ -2135,7 +2256,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
2135
2256
|
},
|
|
2136
2257
|
authorEmail
|
|
2137
2258
|
);
|
|
2138
|
-
const resolved = resolveStewardsForNode(nestId, node.id);
|
|
2259
|
+
const resolved = await resolveStewardsForNode(nestId, node.id);
|
|
2139
2260
|
return c.json({
|
|
2140
2261
|
node: toNodeResponse(node),
|
|
2141
2262
|
stewards: resolved.length > 0 ? resolved.map((r) => ({
|
|
@@ -2148,8 +2269,8 @@ nodeRoutes.post("/", async (c) => {
|
|
|
2148
2269
|
nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
2149
2270
|
const nestId = c.req.param("nestId");
|
|
2150
2271
|
const nodeId = c.req.param("nodeId");
|
|
2151
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
2152
|
-
const { stewards, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback2(
|
|
2272
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-VIKF3VZB.js");
|
|
2273
|
+
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
2153
2274
|
nestId,
|
|
2154
2275
|
nodeId
|
|
2155
2276
|
);
|
|
@@ -2169,18 +2290,19 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
|
2169
2290
|
nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
2170
2291
|
const nestId = c.req.param("nestId");
|
|
2171
2292
|
const nodeId = c.req.param("nodeId");
|
|
2172
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
2173
|
-
const allVersions = getVersions2(nestId, nodeId);
|
|
2174
|
-
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);
|
|
2175
2296
|
const db = getDb();
|
|
2176
|
-
const resolutions = db.
|
|
2297
|
+
const resolutions = await db.all(
|
|
2177
2298
|
`SELECT version, status, resolved_by, resolved_at
|
|
2178
2299
|
FROM review_requests
|
|
2179
2300
|
WHERE nest_id = ? AND node_id = ?
|
|
2180
2301
|
AND status IN ('approved', 'rejected')
|
|
2181
2302
|
AND resolved_by IS NOT NULL
|
|
2182
|
-
ORDER BY resolved_at DESC
|
|
2183
|
-
|
|
2303
|
+
ORDER BY resolved_at DESC`,
|
|
2304
|
+
[nestId, nodeId]
|
|
2305
|
+
);
|
|
2184
2306
|
const byVersion = /* @__PURE__ */ new Map();
|
|
2185
2307
|
for (const r of resolutions) {
|
|
2186
2308
|
if (!byVersion.has(r.version)) {
|
|
@@ -2200,17 +2322,17 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
|
2200
2322
|
nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
|
|
2201
2323
|
const nestId = c.req.param("nestId");
|
|
2202
2324
|
const nodeId = c.req.param("nodeId");
|
|
2203
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
2204
|
-
const history = getReviewHistory2(nestId, nodeId);
|
|
2325
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-QJ3FBOML.js");
|
|
2326
|
+
const history = await getReviewHistory2(nestId, nodeId);
|
|
2205
2327
|
return c.json({ reviews: history });
|
|
2206
2328
|
});
|
|
2207
2329
|
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
2208
2330
|
const nestId = c.req.param("nestId");
|
|
2209
2331
|
const nodeId = c.req.param("nodeId");
|
|
2210
|
-
const { versions: versionManager } = engineCache.get(nestId);
|
|
2332
|
+
const { versions: versionManager } = await engineCache.get(nestId);
|
|
2211
2333
|
const userId = c.get("userId");
|
|
2212
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2213
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2334
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2335
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2214
2336
|
return c.json(
|
|
2215
2337
|
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
2216
2338
|
403
|
|
@@ -2236,16 +2358,16 @@ nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
|
2236
2358
|
{ content, changeNote: `Restored from version ${targetVersion}` },
|
|
2237
2359
|
userEmail
|
|
2238
2360
|
);
|
|
2239
|
-
trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
2361
|
+
await trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
2240
2362
|
return c.json({ ok: true, version, node: toNodeResponse(node) });
|
|
2241
2363
|
});
|
|
2242
2364
|
nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
2243
2365
|
const nestId = c.req.param("nestId");
|
|
2244
2366
|
const nodeId = c.req.param("nodeId");
|
|
2245
|
-
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
2367
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2246
2368
|
const userId = c.get("userId");
|
|
2247
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2248
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2369
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2370
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2249
2371
|
return c.json(
|
|
2250
2372
|
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
2251
2373
|
403
|
|
@@ -2275,8 +2397,8 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
2275
2397
|
}
|
|
2276
2398
|
}
|
|
2277
2399
|
const response = toNodeResponse(node);
|
|
2278
|
-
if (isPublicReader(nestId, userId)) {
|
|
2279
|
-
const approved = getApprovedVersion(nestId, nodeId);
|
|
2400
|
+
if (await isPublicReader(nestId, userId)) {
|
|
2401
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
2280
2402
|
if (approved != null) {
|
|
2281
2403
|
try {
|
|
2282
2404
|
const raw = await versionManager.reconstructVersion(
|
|
@@ -2303,9 +2425,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
2303
2425
|
return c.json({ node: response });
|
|
2304
2426
|
}
|
|
2305
2427
|
}
|
|
2306
|
-
response.status = node.pendingChange ? "external_edit_pending" : getDisplayStatus(nestId, nodeId);
|
|
2428
|
+
response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
|
|
2307
2429
|
if (response.status === "pending_review") {
|
|
2308
|
-
const pending = getPendingReview(nestId, nodeId);
|
|
2430
|
+
const pending = await getPendingReview(nestId, nodeId);
|
|
2309
2431
|
response.pendingReviewBy = pending?.requestedBy ?? null;
|
|
2310
2432
|
}
|
|
2311
2433
|
if (isMarkdownFormat(c)) {
|
|
@@ -2322,7 +2444,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
2322
2444
|
const baseVersionHeader = c.req.header("X-Base-Version");
|
|
2323
2445
|
if (baseVersionHeader) {
|
|
2324
2446
|
const baseVersion = parseInt(baseVersionHeader, 10);
|
|
2325
|
-
const conflict = checkConflict(nestId, nodeId, baseVersion);
|
|
2447
|
+
const conflict = await checkConflict(nestId, nodeId, baseVersion);
|
|
2326
2448
|
if (conflict.conflict) {
|
|
2327
2449
|
return c.json(
|
|
2328
2450
|
{
|
|
@@ -2337,7 +2459,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
2337
2459
|
);
|
|
2338
2460
|
}
|
|
2339
2461
|
}
|
|
2340
|
-
const authorEmail = getUserEmail(c);
|
|
2462
|
+
const authorEmail = await getUserEmail(c);
|
|
2341
2463
|
const { node, version: responseVersion } = await updateNode(
|
|
2342
2464
|
nestId,
|
|
2343
2465
|
nodeId,
|
|
@@ -2356,8 +2478,8 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
2356
2478
|
nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
2357
2479
|
const nestId = c.req.param("nestId");
|
|
2358
2480
|
const nodeId = c.req.param("nodeId");
|
|
2359
|
-
const { storage } = engineCache.get(nestId);
|
|
2360
|
-
if (isStewardshipEnabled(nestId) && getPendingReview(nestId, nodeId)) {
|
|
2481
|
+
const { storage } = await engineCache.get(nestId);
|
|
2482
|
+
if (await isStewardshipEnabled(nestId) && await getPendingReview(nestId, nodeId)) {
|
|
2361
2483
|
throw new LockedError(
|
|
2362
2484
|
"This document is awaiting steward review and is locked. Approve or reject the pending review before deleting."
|
|
2363
2485
|
);
|
|
@@ -2367,30 +2489,34 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
|
2367
2489
|
} catch {
|
|
2368
2490
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
2369
2491
|
}
|
|
2370
|
-
removeNodeFromTagIndex(nestId, nodeId);
|
|
2492
|
+
await removeNodeFromTagIndex(nestId, nodeId);
|
|
2371
2493
|
const db = getDb();
|
|
2372
|
-
db.transaction(() => {
|
|
2373
|
-
|
|
2374
|
-
"DELETE FROM node_versions WHERE nest_id = ? AND node_id = ?"
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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(
|
|
2383
2508
|
`DELETE FROM stewards
|
|
2384
|
-
WHERE nest_id = ? AND scope = 'document' AND node_pattern =
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2509
|
+
WHERE nest_id = ? AND scope = 'document' AND node_pattern = ?`,
|
|
2510
|
+
[nestId, nodeId]
|
|
2511
|
+
);
|
|
2512
|
+
});
|
|
2513
|
+
await trackEvent("node.delete", { nestId, nodeId });
|
|
2388
2514
|
return c.json({ deleted: true });
|
|
2389
2515
|
});
|
|
2390
|
-
function getUserEmail(c) {
|
|
2516
|
+
async function getUserEmail(c) {
|
|
2391
2517
|
const userId = c.get("userId");
|
|
2392
2518
|
const db = getDb();
|
|
2393
|
-
const user = db.
|
|
2519
|
+
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
2394
2520
|
return user?.email || "anonymous@localhost";
|
|
2395
2521
|
}
|
|
2396
2522
|
|
|
@@ -2484,11 +2610,12 @@ function projectThreadsToMarkdown(artifactTitle, threads) {
|
|
|
2484
2610
|
}
|
|
2485
2611
|
|
|
2486
2612
|
// src/annotations/service.ts
|
|
2487
|
-
function loadComments(threadId) {
|
|
2613
|
+
async function loadComments(threadId) {
|
|
2488
2614
|
const db = getDb();
|
|
2489
|
-
const rows = db.
|
|
2490
|
-
"SELECT id, author, body, created_at FROM annotation_comments WHERE thread_id = ? ORDER BY created_at ASC, rowid ASC"
|
|
2491
|
-
|
|
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
|
+
);
|
|
2492
2619
|
return rows.map((r) => ({
|
|
2493
2620
|
id: r.id,
|
|
2494
2621
|
author: r.author,
|
|
@@ -2496,7 +2623,7 @@ function loadComments(threadId) {
|
|
|
2496
2623
|
createdAt: r.created_at
|
|
2497
2624
|
}));
|
|
2498
2625
|
}
|
|
2499
|
-
function rowToThread(row) {
|
|
2626
|
+
async function rowToThread(row) {
|
|
2500
2627
|
let anchor = null;
|
|
2501
2628
|
if (row.anchor_json) {
|
|
2502
2629
|
try {
|
|
@@ -2516,19 +2643,20 @@ function rowToThread(row) {
|
|
|
2516
2643
|
createdAt: row.created_at,
|
|
2517
2644
|
resolvedBy: row.resolved_by,
|
|
2518
2645
|
resolvedAt: row.resolved_at,
|
|
2519
|
-
comments: loadComments(row.id)
|
|
2646
|
+
comments: await loadComments(row.id)
|
|
2520
2647
|
};
|
|
2521
2648
|
}
|
|
2522
|
-
function getThreadRow(threadId) {
|
|
2523
|
-
return getDb().
|
|
2649
|
+
async function getThreadRow(threadId) {
|
|
2650
|
+
return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
|
|
2524
2651
|
}
|
|
2525
|
-
function listThreads(nestId, nodeId) {
|
|
2526
|
-
const rows = getDb().
|
|
2527
|
-
"SELECT * FROM annotation_threads WHERE nest_id = ? AND node_id = ? ORDER BY created_at ASC, rowid ASC"
|
|
2528
|
-
|
|
2529
|
-
|
|
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));
|
|
2530
2658
|
}
|
|
2531
|
-
function createThread(nestId, nodeId, input, authorEmail) {
|
|
2659
|
+
async function createThread(nestId, nodeId, input, authorEmail) {
|
|
2532
2660
|
const body = (input.body ?? "").trim();
|
|
2533
2661
|
if (!body) {
|
|
2534
2662
|
throw new Error("comment body is required");
|
|
@@ -2536,61 +2664,66 @@ function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
2536
2664
|
const db = getDb();
|
|
2537
2665
|
const id = uuid3();
|
|
2538
2666
|
const anchor = clampAnchor(input.anchor);
|
|
2539
|
-
const snapshot = input.snapshotVersion ?? getApprovedVersion(nestId, nodeId) ?? null;
|
|
2540
|
-
|
|
2541
|
-
|
|
2667
|
+
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
2668
|
+
await db.transaction(async (tx) => {
|
|
2669
|
+
await tx.run(
|
|
2542
2670
|
`INSERT INTO annotation_threads
|
|
2543
2671
|
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
|
|
2544
|
-
VALUES (?, ?, ?, ?, ?, 'open', ?)
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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]
|
|
2552
2685
|
);
|
|
2553
|
-
db.prepare(
|
|
2554
|
-
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
|
|
2555
|
-
).run(uuid3(), id, authorEmail, body);
|
|
2556
2686
|
});
|
|
2557
|
-
|
|
2558
|
-
return rowToThread(getThreadRow(id));
|
|
2687
|
+
return rowToThread(await getThreadRow(id));
|
|
2559
2688
|
}
|
|
2560
|
-
function getScopedThreadRow(threadId, nestId, nodeId) {
|
|
2561
|
-
const row = getThreadRow(threadId);
|
|
2689
|
+
async function getScopedThreadRow(threadId, nestId, nodeId) {
|
|
2690
|
+
const row = await getThreadRow(threadId);
|
|
2562
2691
|
if (!row || row.nest_id !== nestId || row.node_id !== nodeId) {
|
|
2563
2692
|
throw new NotFoundError(`Thread not found: ${threadId}`);
|
|
2564
2693
|
}
|
|
2565
2694
|
return row;
|
|
2566
2695
|
}
|
|
2567
|
-
function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
2696
|
+
async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
2568
2697
|
const trimmed = (body ?? "").trim();
|
|
2569
2698
|
if (!trimmed) {
|
|
2570
2699
|
throw new Error("comment body is required");
|
|
2571
2700
|
}
|
|
2572
|
-
getScopedThreadRow(threadId, nestId, nodeId);
|
|
2573
|
-
getDb().
|
|
2574
|
-
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)"
|
|
2575
|
-
|
|
2576
|
-
|
|
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));
|
|
2577
2707
|
}
|
|
2578
|
-
function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
|
|
2579
|
-
getScopedThreadRow(threadId, nestId, nodeId);
|
|
2708
|
+
async function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
|
|
2709
|
+
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
2710
|
+
const db = getDb();
|
|
2580
2711
|
if (status === "resolved") {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2712
|
+
await db.run(
|
|
2713
|
+
`UPDATE annotation_threads SET status = 'resolved', resolved_by = ?, resolved_at = ${nowExpr(db)} WHERE id = ?`,
|
|
2714
|
+
[byEmail, threadId]
|
|
2715
|
+
);
|
|
2584
2716
|
} else {
|
|
2585
|
-
|
|
2586
|
-
"UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?"
|
|
2587
|
-
|
|
2717
|
+
await db.run(
|
|
2718
|
+
"UPDATE annotation_threads SET status = 'open', resolved_by = NULL, resolved_at = NULL WHERE id = ?",
|
|
2719
|
+
[threadId]
|
|
2720
|
+
);
|
|
2588
2721
|
}
|
|
2589
|
-
return rowToThread(getThreadRow(threadId));
|
|
2722
|
+
return rowToThread(await getThreadRow(threadId));
|
|
2590
2723
|
}
|
|
2591
2724
|
async function readArtifact(nestId, nodeId, version) {
|
|
2592
|
-
const { storage, versions: versionManager } = engineCache.get(nestId);
|
|
2593
|
-
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;
|
|
2594
2727
|
let title = nodeId;
|
|
2595
2728
|
let type = null;
|
|
2596
2729
|
let liveHtml = "";
|
|
@@ -2627,12 +2760,12 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
2627
2760
|
try {
|
|
2628
2761
|
let title = nodeId;
|
|
2629
2762
|
try {
|
|
2630
|
-
const { storage } = engineCache.get(nestId);
|
|
2763
|
+
const { storage } = await engineCache.get(nestId);
|
|
2631
2764
|
const node = await storage.readDocument(nodeId);
|
|
2632
2765
|
title = node.frontmatter?.title || nodeId;
|
|
2633
2766
|
} catch {
|
|
2634
2767
|
}
|
|
2635
|
-
const threads = listThreads(nestId, nodeId);
|
|
2768
|
+
const threads = await listThreads(nestId, nodeId);
|
|
2636
2769
|
const derivedTitle = `${title} \u2014 Annotations`;
|
|
2637
2770
|
const markdown = projectThreadsToMarkdown(title, threads);
|
|
2638
2771
|
const derivedId = derivedAnnotationsId(nodeId);
|
|
@@ -2680,25 +2813,25 @@ annotationRoutes.get("/:nodeId{.+}/annotations", async (c) => {
|
|
|
2680
2813
|
const nestId = c.req.param("nestId");
|
|
2681
2814
|
const nodeId = getNodeId(c);
|
|
2682
2815
|
const userId = c.get("userId");
|
|
2683
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2684
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2816
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2817
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2685
2818
|
return c.json({ error: "Access denied" }, 403);
|
|
2686
2819
|
}
|
|
2687
|
-
return c.json({ threads: listThreads(nestId, nodeId) });
|
|
2820
|
+
return c.json({ threads: await listThreads(nestId, nodeId) });
|
|
2688
2821
|
});
|
|
2689
2822
|
annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
|
|
2690
2823
|
const nestId = c.req.param("nestId");
|
|
2691
2824
|
const nodeId = getNodeId(c);
|
|
2692
2825
|
const userId = c.get("userId");
|
|
2693
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2694
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2826
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2827
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2695
2828
|
return c.json({ error: "Access denied" }, 403);
|
|
2696
2829
|
}
|
|
2697
2830
|
const input = await c.req.json();
|
|
2698
2831
|
if (!input.body || !input.body.trim()) {
|
|
2699
2832
|
throw new ValidationError("body is required");
|
|
2700
2833
|
}
|
|
2701
|
-
const thread = createThread(
|
|
2834
|
+
const thread = await createThread(
|
|
2702
2835
|
nestId,
|
|
2703
2836
|
nodeId,
|
|
2704
2837
|
{
|
|
@@ -2716,15 +2849,15 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/comments", async (c) =
|
|
|
2716
2849
|
const nodeId = getNodeId(c);
|
|
2717
2850
|
const threadId = c.req.param("threadId");
|
|
2718
2851
|
const userId = c.get("userId");
|
|
2719
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2720
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2852
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2853
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2721
2854
|
return c.json({ error: "Access denied" }, 403);
|
|
2722
2855
|
}
|
|
2723
2856
|
const input = await c.req.json();
|
|
2724
2857
|
if (!input.body || !input.body.trim()) {
|
|
2725
2858
|
throw new ValidationError("body is required");
|
|
2726
2859
|
}
|
|
2727
|
-
const thread = addComment(nestId, nodeId, threadId, userEmail, input.body);
|
|
2860
|
+
const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
|
|
2728
2861
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
2729
2862
|
return c.json({ thread });
|
|
2730
2863
|
});
|
|
@@ -2733,11 +2866,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/resolve", async (c) =>
|
|
|
2733
2866
|
const nodeId = getNodeId(c);
|
|
2734
2867
|
const threadId = c.req.param("threadId");
|
|
2735
2868
|
const userId = c.get("userId");
|
|
2736
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2737
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2869
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2870
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2738
2871
|
return c.json({ error: "Access denied" }, 403);
|
|
2739
2872
|
}
|
|
2740
|
-
const thread = setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
|
|
2873
|
+
const thread = await setThreadStatus(nestId, nodeId, threadId, "resolved", userEmail);
|
|
2741
2874
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
2742
2875
|
return c.json({ thread });
|
|
2743
2876
|
});
|
|
@@ -2746,11 +2879,11 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/reopen", async (c) =>
|
|
|
2746
2879
|
const nodeId = getNodeId(c);
|
|
2747
2880
|
const threadId = c.req.param("threadId");
|
|
2748
2881
|
const userId = c.get("userId");
|
|
2749
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2750
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2882
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2883
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2751
2884
|
return c.json({ error: "Access denied" }, 403);
|
|
2752
2885
|
}
|
|
2753
|
-
const thread = setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
|
|
2886
|
+
const thread = await setThreadStatus(nestId, nodeId, threadId, "open", userEmail);
|
|
2754
2887
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
2755
2888
|
return c.json({ thread });
|
|
2756
2889
|
});
|
|
@@ -2758,8 +2891,8 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
2758
2891
|
const nestId = c.req.param("nestId");
|
|
2759
2892
|
const nodeId = getNodeId(c);
|
|
2760
2893
|
const userId = c.get("userId");
|
|
2761
|
-
const userEmail = resolveCallerEmail(userId);
|
|
2762
|
-
if (!canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2894
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2895
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
2763
2896
|
return c.json({ error: "Access denied" }, 403);
|
|
2764
2897
|
}
|
|
2765
2898
|
const vRaw = c.req.query("v");
|
|
@@ -2950,15 +3083,16 @@ function tokenizePrompt(prompt) {
|
|
|
2950
3083
|
const words = prompt.toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
|
|
2951
3084
|
return Array.from(new Set(words));
|
|
2952
3085
|
}
|
|
2953
|
-
function compilePrompt(prompt, nestId, titles) {
|
|
3086
|
+
async function compilePrompt(prompt, nestId, titles) {
|
|
2954
3087
|
const tokens = tokenizePrompt(prompt);
|
|
2955
3088
|
if (tokens.length === 0) {
|
|
2956
3089
|
return { selector: null, matchedTags: [], matchedTitles: [], unmatched: [] };
|
|
2957
3090
|
}
|
|
2958
3091
|
const db = getDb();
|
|
2959
|
-
const tagRows = db.
|
|
2960
|
-
"SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?"
|
|
2961
|
-
|
|
3092
|
+
const tagRows = await db.all(
|
|
3093
|
+
"SELECT DISTINCT tag_name FROM node_tag_index WHERE nest_id = ?",
|
|
3094
|
+
[nestId]
|
|
3095
|
+
);
|
|
2962
3096
|
const knownTags = new Set(tagRows.map((r) => r.tag_name));
|
|
2963
3097
|
const matchedTags = /* @__PURE__ */ new Set();
|
|
2964
3098
|
for (const token of tokens) {
|
|
@@ -2988,11 +3122,11 @@ function compilePrompt(prompt, nestId, titles) {
|
|
|
2988
3122
|
|
|
2989
3123
|
// src/nodes/readable-body.ts
|
|
2990
3124
|
async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
2991
|
-
if (!isPublicReader(nestId, userId)) return workingBody;
|
|
2992
|
-
const approved = getApprovedVersion(nestId, nodeId);
|
|
3125
|
+
if (!await isPublicReader(nestId, userId)) return workingBody;
|
|
3126
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
2993
3127
|
if (approved == null) return "";
|
|
2994
3128
|
try {
|
|
2995
|
-
const { versions } = engineCache.get(nestId);
|
|
3129
|
+
const { versions } = await engineCache.get(nestId);
|
|
2996
3130
|
const raw = await versions.reconstructVersion(nodeId, approved);
|
|
2997
3131
|
return bodyOnly(nodeId, raw);
|
|
2998
3132
|
} catch {
|
|
@@ -3000,11 +3134,11 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
|
3000
3134
|
}
|
|
3001
3135
|
}
|
|
3002
3136
|
async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
3003
|
-
if (!isStewardshipEnabled(nestId)) return workingBody;
|
|
3004
|
-
const approved = getApprovedVersion(nestId, nodeId);
|
|
3137
|
+
if (!await isStewardshipEnabled(nestId)) return workingBody;
|
|
3138
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
3005
3139
|
if (approved == null) return null;
|
|
3006
3140
|
try {
|
|
3007
|
-
const { versions } = engineCache.get(nestId);
|
|
3141
|
+
const { versions } = await engineCache.get(nestId);
|
|
3008
3142
|
const raw = await versions.reconstructVersion(nodeId, approved);
|
|
3009
3143
|
return bodyOnly(nodeId, raw);
|
|
3010
3144
|
} catch {
|
|
@@ -3027,9 +3161,9 @@ function extractWikiTargets(body) {
|
|
|
3027
3161
|
return out;
|
|
3028
3162
|
}
|
|
3029
3163
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
3030
|
-
const { storage } = engineCache.get(nestId);
|
|
3164
|
+
const { storage } = await engineCache.get(nestId);
|
|
3031
3165
|
const docs = await storage.discoverDocuments();
|
|
3032
|
-
const accessible = filterAccessible(nestId, userId, userEmail, docs);
|
|
3166
|
+
const accessible = await filterAccessible(nestId, userId, userEmail, docs);
|
|
3033
3167
|
const idSet = new Set(accessible.map((d) => d.id));
|
|
3034
3168
|
const titleToId = /* @__PURE__ */ new Map();
|
|
3035
3169
|
for (const d of accessible) {
|
|
@@ -3065,7 +3199,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
3065
3199
|
}
|
|
3066
3200
|
}
|
|
3067
3201
|
const tags = [...tagCounts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
|
|
3068
|
-
const exposeStewards = isStewardshipEnabled(nestId) && !isPublicReader(nestId, userId);
|
|
3202
|
+
const exposeStewards = await isStewardshipEnabled(nestId) && !await isPublicReader(nestId, userId);
|
|
3069
3203
|
const stewardRows = exposeStewards ? (await listStewards({ nestId })).filter(
|
|
3070
3204
|
// Drop document-scoped rows whose node the caller can't see.
|
|
3071
3205
|
(s) => s.scope !== "document" || idSet.has(s.nodePattern || "")
|
|
@@ -3159,7 +3293,7 @@ var queryRoutes = new Hono6();
|
|
|
3159
3293
|
queryRoutes.get("/graph", async (c) => {
|
|
3160
3294
|
const nestId = c.req.param("nestId");
|
|
3161
3295
|
const userId = c.get("userId");
|
|
3162
|
-
const userEmail = resolveCallerEmail(userId);
|
|
3296
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
3163
3297
|
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
3164
3298
|
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
3165
3299
|
return c.json(graph);
|
|
@@ -3184,7 +3318,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
3184
3318
|
throw new ValidationError("prompt or selector is required");
|
|
3185
3319
|
}
|
|
3186
3320
|
const nestId = c.req.param("nestId");
|
|
3187
|
-
const { query: queryEngine, storage } = engineCache.get(nestId);
|
|
3321
|
+
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
3188
3322
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
3189
3323
|
const hops = body.hops ?? 2;
|
|
3190
3324
|
const includeDrafts = body.include_drafts === true;
|
|
@@ -3194,7 +3328,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
3194
3328
|
const allDocs = await storage.discoverDocuments();
|
|
3195
3329
|
if (!selector && body.prompt) {
|
|
3196
3330
|
const titles = allDocs.map((d) => d.frontmatter.title);
|
|
3197
|
-
compileDetail = compilePrompt(body.prompt, nestId, titles);
|
|
3331
|
+
compileDetail = await compilePrompt(body.prompt, nestId, titles);
|
|
3198
3332
|
selector = compileDetail.selector;
|
|
3199
3333
|
if (compileDetail.matchedTitles.length > 0) {
|
|
3200
3334
|
const matchedLower = new Set(
|
|
@@ -3228,9 +3362,9 @@ queryRoutes.post("/context", async (c) => {
|
|
|
3228
3362
|
}
|
|
3229
3363
|
}
|
|
3230
3364
|
const userId = c.get("userId");
|
|
3231
|
-
const userEmail = resolveCallerEmail(userId);
|
|
3365
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
3232
3366
|
const beforePermission = documents.length;
|
|
3233
|
-
const accessible = filterAccessible(nestId, userId, userEmail, documents);
|
|
3367
|
+
const accessible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
3234
3368
|
const permissionFiltered = beforePermission - accessible.length;
|
|
3235
3369
|
const readable = await Promise.all(
|
|
3236
3370
|
accessible.map(async (doc) => ({
|
|
@@ -3289,13 +3423,13 @@ queryRoutes.post("/query", async (c) => {
|
|
|
3289
3423
|
throw new ValidationError("query is required");
|
|
3290
3424
|
}
|
|
3291
3425
|
const nestId = c.req.param("nestId");
|
|
3292
|
-
const { query: queryEngine } = engineCache.get(nestId);
|
|
3426
|
+
const { query: queryEngine } = await engineCache.get(nestId);
|
|
3293
3427
|
const result = await queryEngine.query(body.query, {
|
|
3294
3428
|
hops: body.hops ?? 2
|
|
3295
3429
|
});
|
|
3296
3430
|
const userId = c.get("userId");
|
|
3297
|
-
const userEmail = resolveCallerEmail(userId);
|
|
3298
|
-
const accessible = filterAccessible(nestId, userId, userEmail, result.documents);
|
|
3431
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
3432
|
+
const accessible = await filterAccessible(nestId, userId, userEmail, result.documents);
|
|
3299
3433
|
return c.json({
|
|
3300
3434
|
query: body.query,
|
|
3301
3435
|
count: accessible.length,
|
|
@@ -3314,7 +3448,7 @@ queryRoutes.get("/search", async (c) => {
|
|
|
3314
3448
|
throw new ValidationError("q query parameter is required");
|
|
3315
3449
|
}
|
|
3316
3450
|
const nestId = c.req.param("nestId");
|
|
3317
|
-
const { storage } = engineCache.get(nestId);
|
|
3451
|
+
const { storage } = await engineCache.get(nestId);
|
|
3318
3452
|
const documents = await storage.discoverDocuments();
|
|
3319
3453
|
const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
3320
3454
|
const matches = documents.filter((node) => {
|
|
@@ -3327,8 +3461,8 @@ queryRoutes.get("/search", async (c) => {
|
|
|
3327
3461
|
return terms.every((term) => haystack.includes(term));
|
|
3328
3462
|
});
|
|
3329
3463
|
const userId = c.get("userId");
|
|
3330
|
-
const userEmail = resolveCallerEmail(userId);
|
|
3331
|
-
const accessible = filterAccessible(nestId, userId, userEmail, matches);
|
|
3464
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
3465
|
+
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
3332
3466
|
return c.json({
|
|
3333
3467
|
query: q,
|
|
3334
3468
|
count: accessible.length,
|
|
@@ -3342,7 +3476,7 @@ queryRoutes.get("/search", async (c) => {
|
|
|
3342
3476
|
});
|
|
3343
3477
|
});
|
|
3344
3478
|
queryRoutes.get("/overview", async (c) => {
|
|
3345
|
-
const { storage } = engineCache.get(c.req.param("nestId"));
|
|
3479
|
+
const { storage } = await engineCache.get(c.req.param("nestId"));
|
|
3346
3480
|
const documents = await storage.discoverDocuments();
|
|
3347
3481
|
const types = {};
|
|
3348
3482
|
const tags = {};
|
|
@@ -3367,7 +3501,7 @@ queryRoutes.get("/overview", async (c) => {
|
|
|
3367
3501
|
});
|
|
3368
3502
|
});
|
|
3369
3503
|
queryRoutes.get("/context", async (c) => {
|
|
3370
|
-
const { storage } = engineCache.get(c.req.param("nestId"));
|
|
3504
|
+
const { storage } = await engineCache.get(c.req.param("nestId"));
|
|
3371
3505
|
const content = await storage.readContextMd();
|
|
3372
3506
|
return c.json({ content: content || "" });
|
|
3373
3507
|
});
|
|
@@ -3376,7 +3510,7 @@ queryRoutes.get("/export", async (c) => {
|
|
|
3376
3510
|
throw new ValidationError("format=markdown is required");
|
|
3377
3511
|
}
|
|
3378
3512
|
const nestId = c.req.param("nestId");
|
|
3379
|
-
const { storage, query: queryEngine } = engineCache.get(nestId);
|
|
3513
|
+
const { storage, query: queryEngine } = await engineCache.get(nestId);
|
|
3380
3514
|
const selector = c.req.query("selector")?.trim() || null;
|
|
3381
3515
|
let documents;
|
|
3382
3516
|
if (selector) {
|
|
@@ -3386,9 +3520,9 @@ queryRoutes.get("/export", async (c) => {
|
|
|
3386
3520
|
documents = await storage.discoverDocuments();
|
|
3387
3521
|
}
|
|
3388
3522
|
const userId = c.get("userId");
|
|
3389
|
-
const userEmail = resolveCallerEmail(userId);
|
|
3390
|
-
const accessible = filterAccessible(nestId, userId, userEmail, documents);
|
|
3391
|
-
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);
|
|
3392
3526
|
const resolved = await Promise.all(
|
|
3393
3527
|
accessible.map(async (n) => {
|
|
3394
3528
|
const body = await resolveExportBody(nestId, n.id, n.body || "");
|
|
@@ -3435,7 +3569,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
3435
3569
|
throw new ValidationError("documents array or context_md is required");
|
|
3436
3570
|
}
|
|
3437
3571
|
const nestId = c.req.param("nestId");
|
|
3438
|
-
const { storage } = engineCache.get(nestId);
|
|
3572
|
+
const { storage } = await engineCache.get(nestId);
|
|
3439
3573
|
const created = [];
|
|
3440
3574
|
if (body.context_md) {
|
|
3441
3575
|
await storage.writeContextMd(body.context_md);
|
|
@@ -3467,7 +3601,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
3467
3601
|
};
|
|
3468
3602
|
const serialized = serializeDocument2(node);
|
|
3469
3603
|
await storage.writeDocument(id, serialized);
|
|
3470
|
-
syncNodeTags(nestId, id, tags);
|
|
3604
|
+
await syncNodeTags(nestId, id, tags);
|
|
3471
3605
|
created.push(id);
|
|
3472
3606
|
}
|
|
3473
3607
|
trackEvent("nest.publish", { nestId, count: created.length });
|
|
@@ -3737,8 +3871,8 @@ var TOOL_DEFINITIONS = [
|
|
|
3737
3871
|
}
|
|
3738
3872
|
];
|
|
3739
3873
|
async function resolveLlmBody(ctx, node) {
|
|
3740
|
-
if (!isStewardshipEnabled(ctx.nestId)) return node.body || "";
|
|
3741
|
-
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);
|
|
3742
3876
|
if (approved == null) return null;
|
|
3743
3877
|
try {
|
|
3744
3878
|
return await ctx.versionManager.reconstructVersion(node.id, approved);
|
|
@@ -3877,7 +4011,7 @@ ${n.body || ""}`;
|
|
|
3877
4011
|
return resolved.join("\n\n---\n\n") || "No nodes resolved.";
|
|
3878
4012
|
}
|
|
3879
4013
|
case "context_create": {
|
|
3880
|
-
if (!canCreateInNest(nestId, userEmail)) {
|
|
4014
|
+
if (!await canCreateInNest(nestId, userEmail)) {
|
|
3881
4015
|
return "You don't have permission to create documents in this nest.";
|
|
3882
4016
|
}
|
|
3883
4017
|
const { node } = await createNode(
|
|
@@ -3899,7 +4033,7 @@ ${n.body || ""}`;
|
|
|
3899
4033
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
3900
4034
|
);
|
|
3901
4035
|
if (!node) return `Node not found: ${args.title}`;
|
|
3902
|
-
const editCheck = canUserEdit(nestId, node.id, userEmail);
|
|
4036
|
+
const editCheck = await canUserEdit(nestId, node.id, userEmail);
|
|
3903
4037
|
if (!editCheck.allowed) {
|
|
3904
4038
|
return `You don't have permission to edit "${args.title}": ${editCheck.reason}`;
|
|
3905
4039
|
}
|
|
@@ -3923,7 +4057,7 @@ ${n.body || ""}`;
|
|
|
3923
4057
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
3924
4058
|
);
|
|
3925
4059
|
const nodeId = node?.id || "";
|
|
3926
|
-
const resolved = resolveStewardsForNode(ctx.nestId, nodeId);
|
|
4060
|
+
const resolved = await resolveStewardsForNode(ctx.nestId, nodeId);
|
|
3927
4061
|
if (resolved.length === 0) {
|
|
3928
4062
|
return `No stewards configured for "${args.title}". Changes are auto-approved.`;
|
|
3929
4063
|
}
|
|
@@ -3979,14 +4113,14 @@ ${list}`;
|
|
|
3979
4113
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
3980
4114
|
);
|
|
3981
4115
|
if (!node) return `Node not found: ${args.title}`;
|
|
3982
|
-
const submitCheck = canUserEdit(ctx.nestId, node.id, userEmail);
|
|
4116
|
+
const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
|
|
3983
4117
|
if (!submitCheck.allowed) {
|
|
3984
4118
|
return `You don't have permission to submit "${args.title}" for review: ${submitCheck.reason}`;
|
|
3985
4119
|
}
|
|
3986
|
-
const currentVersion = getCurrentVersion(ctx.nestId, node.id);
|
|
4120
|
+
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
3987
4121
|
if (currentVersion === 0) return `No versions found for "${args.title}"`;
|
|
3988
4122
|
try {
|
|
3989
|
-
const request = submitForReview({
|
|
4123
|
+
const request = await submitForReview({
|
|
3990
4124
|
nestId: ctx.nestId,
|
|
3991
4125
|
nodeId: node.id,
|
|
3992
4126
|
version: currentVersion,
|
|
@@ -3994,7 +4128,7 @@ ${list}`;
|
|
|
3994
4128
|
note: args.note,
|
|
3995
4129
|
priority: args.priority
|
|
3996
4130
|
});
|
|
3997
|
-
const resolved = resolveStewardsForNode(
|
|
4131
|
+
const resolved = await resolveStewardsForNode(
|
|
3998
4132
|
ctx.nestId,
|
|
3999
4133
|
node.id
|
|
4000
4134
|
);
|
|
@@ -4013,7 +4147,7 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
|
|
|
4013
4147
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
4014
4148
|
);
|
|
4015
4149
|
if (!node) return `Node not found: ${args.title}`;
|
|
4016
|
-
const currentVersion = getCurrentVersion(ctx.nestId, node.id);
|
|
4150
|
+
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
4017
4151
|
try {
|
|
4018
4152
|
const request = await approve({
|
|
4019
4153
|
nestId: ctx.nestId,
|
|
@@ -4034,9 +4168,9 @@ Note: ${args.note}` : ""}`;
|
|
|
4034
4168
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
4035
4169
|
);
|
|
4036
4170
|
if (!node) return `Node not found: ${args.title}`;
|
|
4037
|
-
const currentVersion = getCurrentVersion(ctx.nestId, node.id);
|
|
4171
|
+
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
4038
4172
|
try {
|
|
4039
|
-
const request = reject({
|
|
4173
|
+
const request = await reject({
|
|
4040
4174
|
nestId: ctx.nestId,
|
|
4041
4175
|
nodeId: node.id,
|
|
4042
4176
|
version: currentVersion,
|
|
@@ -4055,8 +4189,8 @@ Reason: ${args.note}`;
|
|
|
4055
4189
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
4056
4190
|
);
|
|
4057
4191
|
if (!node) return `Node not found: ${args.title}`;
|
|
4058
|
-
const allVersions = getVersions(ctx.nestId, node.id);
|
|
4059
|
-
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);
|
|
4060
4194
|
if (allVersions.length === 0) {
|
|
4061
4195
|
return `No version history for "${args.title}".`;
|
|
4062
4196
|
}
|
|
@@ -4092,7 +4226,7 @@ ${list}`;
|
|
|
4092
4226
|
}
|
|
4093
4227
|
}
|
|
4094
4228
|
case "context_share_nest": {
|
|
4095
|
-
const roles = resolveUserRoles(ctx.nestId, ctx.userEmail);
|
|
4229
|
+
const roles = await resolveUserRoles(ctx.nestId, ctx.userEmail);
|
|
4096
4230
|
if (!canManageWith(roles)) {
|
|
4097
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.";
|
|
4098
4232
|
}
|
|
@@ -4111,7 +4245,7 @@ ${list}`;
|
|
|
4111
4245
|
}
|
|
4112
4246
|
}
|
|
4113
4247
|
case "context_unsynced_list": {
|
|
4114
|
-
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
4248
|
+
if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
|
|
4115
4249
|
return "You don't have permission to list unsynced folders. Server admin only.";
|
|
4116
4250
|
}
|
|
4117
4251
|
const folders = listUnsyncedFolders();
|
|
@@ -4124,7 +4258,7 @@ ${list}`;
|
|
|
4124
4258
|
${list}`;
|
|
4125
4259
|
}
|
|
4126
4260
|
case "context_sync_folder": {
|
|
4127
|
-
if (config.AUTH_MODE !== "open" && !isLicenseAdminUserId(userId)) {
|
|
4261
|
+
if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
|
|
4128
4262
|
return "You don't have permission to sync folders. Server admin only.";
|
|
4129
4263
|
}
|
|
4130
4264
|
try {
|
|
@@ -4146,9 +4280,9 @@ ${list}`;
|
|
|
4146
4280
|
// src/mcp/routes.ts
|
|
4147
4281
|
import { z } from "zod";
|
|
4148
4282
|
var mcpRoutes = new Hono7();
|
|
4149
|
-
function getUserEmail2(userId) {
|
|
4283
|
+
async function getUserEmail2(userId) {
|
|
4150
4284
|
const db = getDb();
|
|
4151
|
-
const user = db.
|
|
4285
|
+
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
4152
4286
|
return user?.email || "anonymous@localhost";
|
|
4153
4287
|
}
|
|
4154
4288
|
function createMcpServerForNest(nestId, userId, userEmail) {
|
|
@@ -4156,7 +4290,6 @@ function createMcpServerForNest(nestId, userId, userEmail) {
|
|
|
4156
4290
|
{ name: `contextnest-${nestId}`, version: "1.0.0" },
|
|
4157
4291
|
{ capabilities: { tools: {} } }
|
|
4158
4292
|
);
|
|
4159
|
-
const engine = engineCache.get(nestId);
|
|
4160
4293
|
for (const tool of TOOL_DEFINITIONS) {
|
|
4161
4294
|
const props = tool.inputSchema.properties || {};
|
|
4162
4295
|
const required = tool.inputSchema.required || [];
|
|
@@ -4171,6 +4304,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
|
|
|
4171
4304
|
shape[key] = field;
|
|
4172
4305
|
}
|
|
4173
4306
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
4307
|
+
const engine = await engineCache.get(nestId);
|
|
4174
4308
|
const text = await handleToolCall(tool.name, args, {
|
|
4175
4309
|
storage: engine.storage,
|
|
4176
4310
|
queryEngine: engine.query,
|
|
@@ -4187,7 +4321,7 @@ function createMcpServerForNest(nestId, userId, userEmail) {
|
|
|
4187
4321
|
mcpRoutes.all("/", async (c) => {
|
|
4188
4322
|
const nestId = c.req.param("nestId");
|
|
4189
4323
|
const userId = c.get("userId");
|
|
4190
|
-
const userEmail = getUserEmail2(userId);
|
|
4324
|
+
const userEmail = await getUserEmail2(userId);
|
|
4191
4325
|
const server = createMcpServerForNest(nestId, userId, userEmail);
|
|
4192
4326
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
4193
4327
|
sessionIdGenerator: void 0,
|
|
@@ -4208,41 +4342,43 @@ import { Hono as Hono8 } from "hono";
|
|
|
4208
4342
|
|
|
4209
4343
|
// src/governance/comment-service.ts
|
|
4210
4344
|
import { v4 as uuid4 } from "uuid";
|
|
4211
|
-
function createComment(params) {
|
|
4345
|
+
async function createComment(params) {
|
|
4212
4346
|
const db = getDb();
|
|
4213
4347
|
const body = (params.body ?? "").trim();
|
|
4214
4348
|
if (!body) {
|
|
4215
4349
|
throw new Error("Comment body is required");
|
|
4216
4350
|
}
|
|
4217
4351
|
if (params.parentId) {
|
|
4218
|
-
const parent = db.
|
|
4219
|
-
"SELECT id FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
|
|
4220
|
-
|
|
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
|
+
);
|
|
4221
4356
|
if (!parent) {
|
|
4222
4357
|
throw new Error("Parent comment not found on this node");
|
|
4223
4358
|
}
|
|
4224
4359
|
}
|
|
4225
4360
|
const id = uuid4();
|
|
4226
|
-
db.
|
|
4361
|
+
await db.run(
|
|
4227
4362
|
`INSERT INTO comments
|
|
4228
4363
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
4229
4364
|
parent_id, author, body)
|
|
4230
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
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
|
+
]
|
|
4242
4378
|
);
|
|
4243
|
-
return getComment(id);
|
|
4379
|
+
return await getComment(id);
|
|
4244
4380
|
}
|
|
4245
|
-
function listComments(nestId, nodeId, opts = {}) {
|
|
4381
|
+
async function listComments(nestId, nodeId, opts = {}) {
|
|
4246
4382
|
const db = getDb();
|
|
4247
4383
|
const args = [nestId, nodeId];
|
|
4248
4384
|
let statusClause = "";
|
|
@@ -4250,35 +4386,38 @@ function listComments(nestId, nodeId, opts = {}) {
|
|
|
4250
4386
|
statusClause = " AND status = ?";
|
|
4251
4387
|
args.push(opts.status);
|
|
4252
4388
|
}
|
|
4253
|
-
const rows = db.
|
|
4389
|
+
const rows = await db.all(
|
|
4254
4390
|
`SELECT * FROM comments
|
|
4255
4391
|
WHERE nest_id = ? AND node_id = ?${statusClause}
|
|
4256
|
-
ORDER BY created_at ASC
|
|
4257
|
-
|
|
4392
|
+
ORDER BY created_at ASC`,
|
|
4393
|
+
args
|
|
4394
|
+
);
|
|
4258
4395
|
return rows.map(rowToComment);
|
|
4259
4396
|
}
|
|
4260
|
-
function getComment(id) {
|
|
4397
|
+
async function getComment(id) {
|
|
4261
4398
|
const db = getDb();
|
|
4262
|
-
const row = db.
|
|
4399
|
+
const row = await db.get("SELECT * FROM comments WHERE id = ?", [id]);
|
|
4263
4400
|
return row ? rowToComment(row) : null;
|
|
4264
4401
|
}
|
|
4265
|
-
function resolveComment(params) {
|
|
4402
|
+
async function resolveComment(params) {
|
|
4266
4403
|
const db = getDb();
|
|
4267
|
-
const existing = db.
|
|
4268
|
-
"SELECT id, status FROM comments WHERE id = ? AND nest_id = ? AND node_id = ?"
|
|
4269
|
-
|
|
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
|
+
);
|
|
4270
4408
|
if (!existing) {
|
|
4271
4409
|
throw new Error("Comment not found");
|
|
4272
4410
|
}
|
|
4273
4411
|
if (existing.status === "resolved") {
|
|
4274
4412
|
throw new Error("Comment is already resolved");
|
|
4275
4413
|
}
|
|
4276
|
-
db.
|
|
4414
|
+
await db.run(
|
|
4277
4415
|
`UPDATE comments
|
|
4278
|
-
SET status = 'resolved', resolved_by = ?, resolved_at =
|
|
4279
|
-
WHERE id =
|
|
4280
|
-
|
|
4281
|
-
|
|
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);
|
|
4282
4421
|
}
|
|
4283
4422
|
async function getActivity(params) {
|
|
4284
4423
|
const db = getDb();
|
|
@@ -4286,13 +4425,14 @@ async function getActivity(params) {
|
|
|
4286
4425
|
const entries = [];
|
|
4287
4426
|
const nodeFilter = params.nodeId ? " AND node_id = ?" : "";
|
|
4288
4427
|
const baseArgs = params.nodeId ? [params.nestId, params.nodeId] : [params.nestId];
|
|
4289
|
-
const commentRows = db.
|
|
4428
|
+
const commentRows = await db.all(
|
|
4290
4429
|
`SELECT id, node_id, author, body, status, created_at, resolved_by, resolved_at
|
|
4291
4430
|
FROM comments
|
|
4292
4431
|
WHERE nest_id = ?${nodeFilter}
|
|
4293
4432
|
ORDER BY COALESCE(resolved_at, created_at) DESC
|
|
4294
|
-
LIMIT
|
|
4295
|
-
|
|
4433
|
+
LIMIT ?`,
|
|
4434
|
+
[...baseArgs, limit]
|
|
4435
|
+
);
|
|
4296
4436
|
for (const r of commentRows) {
|
|
4297
4437
|
entries.push({
|
|
4298
4438
|
type: "comment",
|
|
@@ -4313,13 +4453,14 @@ async function getActivity(params) {
|
|
|
4313
4453
|
});
|
|
4314
4454
|
}
|
|
4315
4455
|
}
|
|
4316
|
-
const versionRows = db.
|
|
4456
|
+
const versionRows = await db.all(
|
|
4317
4457
|
`SELECT node_id, version, author, change_note, created_at
|
|
4318
4458
|
FROM node_versions
|
|
4319
4459
|
WHERE nest_id = ?${nodeFilter}
|
|
4320
4460
|
ORDER BY created_at DESC
|
|
4321
|
-
LIMIT
|
|
4322
|
-
|
|
4461
|
+
LIMIT ?`,
|
|
4462
|
+
[...baseArgs, limit]
|
|
4463
|
+
);
|
|
4323
4464
|
for (const r of versionRows) {
|
|
4324
4465
|
entries.push({
|
|
4325
4466
|
type: "edit",
|
|
@@ -4330,13 +4471,14 @@ async function getActivity(params) {
|
|
|
4330
4471
|
refId: String(r.version)
|
|
4331
4472
|
});
|
|
4332
4473
|
}
|
|
4333
|
-
const reviewRows = db.
|
|
4474
|
+
const reviewRows = await db.all(
|
|
4334
4475
|
`SELECT id, node_id, requested_by, requested_at, status, resolved_by, resolved_at
|
|
4335
4476
|
FROM review_requests
|
|
4336
4477
|
WHERE nest_id = ?${nodeFilter}
|
|
4337
4478
|
ORDER BY COALESCE(resolved_at, requested_at) DESC
|
|
4338
|
-
LIMIT
|
|
4339
|
-
|
|
4479
|
+
LIMIT ?`,
|
|
4480
|
+
[...baseArgs, limit]
|
|
4481
|
+
);
|
|
4340
4482
|
for (const r of reviewRows) {
|
|
4341
4483
|
entries.push({
|
|
4342
4484
|
type: "review_requested",
|
|
@@ -4479,8 +4621,7 @@ function parseEntry(str) {
|
|
|
4479
4621
|
return entry;
|
|
4480
4622
|
}
|
|
4481
4623
|
function loadStewardsConfig(nestId) {
|
|
4482
|
-
const
|
|
4483
|
-
const nestPath = join3(dataRoot, "nests", nestId);
|
|
4624
|
+
const nestPath = resolveNestPath(nestId);
|
|
4484
4625
|
const candidates = [
|
|
4485
4626
|
join3(nestPath, "stewards.yaml"),
|
|
4486
4627
|
join3(nestPath, "stewards.yml"),
|
|
@@ -4512,24 +4653,25 @@ governanceRoutes.get("/stewards", async (c) => {
|
|
|
4512
4653
|
search: search || void 0
|
|
4513
4654
|
});
|
|
4514
4655
|
const cache = /* @__PURE__ */ new Map();
|
|
4515
|
-
const enriched =
|
|
4656
|
+
const enriched = [];
|
|
4657
|
+
for (const s of stewards) {
|
|
4516
4658
|
const key = s.userEmail.toLowerCase();
|
|
4517
4659
|
let merged = cache.get(key);
|
|
4518
4660
|
if (!merged) {
|
|
4519
4661
|
merged = {
|
|
4520
|
-
collaboratorRole: getCollaboratorRole(nestId, s.userEmail),
|
|
4521
|
-
roles: resolveUserRoles(nestId, s.userEmail)
|
|
4662
|
+
collaboratorRole: await getCollaboratorRole(nestId, s.userEmail),
|
|
4663
|
+
roles: await resolveUserRoles(nestId, s.userEmail)
|
|
4522
4664
|
};
|
|
4523
4665
|
cache.set(key, merged);
|
|
4524
4666
|
}
|
|
4525
|
-
|
|
4526
|
-
}
|
|
4667
|
+
enriched.push({ ...s, ...merged });
|
|
4668
|
+
}
|
|
4527
4669
|
return c.json({ stewards: enriched });
|
|
4528
4670
|
});
|
|
4529
4671
|
governanceRoutes.post("/stewards", async (c) => {
|
|
4530
4672
|
const nestId = c.req.param("nestId");
|
|
4531
4673
|
const body = await c.req.json();
|
|
4532
|
-
const assignedBy = getUserEmail3(c);
|
|
4674
|
+
const assignedBy = await getUserEmail3(c);
|
|
4533
4675
|
if (!body.scope) throw new ValidationError("scope is required");
|
|
4534
4676
|
if (body.scope === "folder") {
|
|
4535
4677
|
throw new ValidationError(
|
|
@@ -4571,7 +4713,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
|
|
|
4571
4713
|
if (!body.role && !body.scope) {
|
|
4572
4714
|
throw new ValidationError("role or scope is required");
|
|
4573
4715
|
}
|
|
4574
|
-
const steward = updateSteward(stewardId, {
|
|
4716
|
+
const steward = await updateSteward(stewardId, {
|
|
4575
4717
|
role: body.role,
|
|
4576
4718
|
scope: body.scope,
|
|
4577
4719
|
documentId: body.nodePattern,
|
|
@@ -4581,7 +4723,7 @@ governanceRoutes.patch("/stewards/:stewardId", async (c) => {
|
|
|
4581
4723
|
});
|
|
4582
4724
|
governanceRoutes.delete("/stewards/:stewardId", async (c) => {
|
|
4583
4725
|
const stewardId = c.req.param("stewardId");
|
|
4584
|
-
removeSteward(stewardId);
|
|
4726
|
+
await removeSteward(stewardId);
|
|
4585
4727
|
return c.json({ removed: true });
|
|
4586
4728
|
});
|
|
4587
4729
|
governanceRoutes.post("/stewards/sync", async (c) => {
|
|
@@ -4590,7 +4732,7 @@ governanceRoutes.post("/stewards/sync", async (c) => {
|
|
|
4590
4732
|
if (!stewardsConfig) {
|
|
4591
4733
|
return c.json({ synced: 0, message: "No stewards.yaml found" });
|
|
4592
4734
|
}
|
|
4593
|
-
const count = syncFromConfig(nestId, stewardsConfig);
|
|
4735
|
+
const count = await syncFromConfig(nestId, stewardsConfig);
|
|
4594
4736
|
return c.json({ synced: count });
|
|
4595
4737
|
});
|
|
4596
4738
|
governanceRoutes.get("/review-queue", async (c) => {
|
|
@@ -4604,16 +4746,17 @@ governanceRoutes.get("/review-queue", async (c) => {
|
|
|
4604
4746
|
limit,
|
|
4605
4747
|
offset
|
|
4606
4748
|
});
|
|
4607
|
-
const email = getUserEmail3(c);
|
|
4749
|
+
const email = await getUserEmail3(c);
|
|
4608
4750
|
const canReviewCache = /* @__PURE__ */ new Map();
|
|
4609
|
-
const requests =
|
|
4751
|
+
const requests = [];
|
|
4752
|
+
for (const r of result.requests) {
|
|
4610
4753
|
let canReview = canReviewCache.get(r.nodeId);
|
|
4611
4754
|
if (canReview === void 0) {
|
|
4612
|
-
canReview = canUserApprove(nestId, r.nodeId, email).allowed;
|
|
4755
|
+
canReview = (await canUserApprove(nestId, r.nodeId, email)).allowed;
|
|
4613
4756
|
canReviewCache.set(r.nodeId, canReview);
|
|
4614
4757
|
}
|
|
4615
|
-
|
|
4616
|
-
}
|
|
4758
|
+
requests.push({ ...r, canReview });
|
|
4759
|
+
}
|
|
4617
4760
|
return c.json({ ...result, requests });
|
|
4618
4761
|
});
|
|
4619
4762
|
governanceRoutes.get("/external-edits", async (c) => {
|
|
@@ -4627,7 +4770,7 @@ governanceRoutes.get("/external-edits", async (c) => {
|
|
|
4627
4770
|
});
|
|
4628
4771
|
governanceRoutes.post("/external-edits/scan", async (c) => {
|
|
4629
4772
|
const nestId = c.req.param("nestId");
|
|
4630
|
-
const actor = getUserEmail3(c);
|
|
4773
|
+
const actor = await getUserEmail3(c);
|
|
4631
4774
|
const result = await scanNestForDrift(nestId, actor);
|
|
4632
4775
|
return c.json(result);
|
|
4633
4776
|
});
|
|
@@ -4641,7 +4784,7 @@ var governanceNodeRoutes = new Hono8();
|
|
|
4641
4784
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4642
4785
|
const nestId = c.req.param("nestId");
|
|
4643
4786
|
const nodeId = c.req.param("nodeId");
|
|
4644
|
-
const { stewards: resolved, fallbackToOwner, ownerEmail } = resolveStewardsWithFallback(nestId, nodeId);
|
|
4787
|
+
const { stewards: resolved, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback(nestId, nodeId);
|
|
4645
4788
|
return c.json({
|
|
4646
4789
|
nodeId,
|
|
4647
4790
|
stewards: resolved.map((r) => ({
|
|
@@ -4658,9 +4801,9 @@ governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4658
4801
|
governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
4659
4802
|
const nestId = c.req.param("nestId");
|
|
4660
4803
|
const nodeId = c.req.param("nodeId");
|
|
4661
|
-
const allVersions = getVersions(nestId, nodeId);
|
|
4662
|
-
const approved = getApprovedVersion(nestId, nodeId);
|
|
4663
|
-
const { versions: versionManager } = engineCache.get(nestId);
|
|
4804
|
+
const allVersions = await getVersions(nestId, nodeId);
|
|
4805
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
4806
|
+
const { versions: versionManager } = await engineCache.get(nestId);
|
|
4664
4807
|
const withContent = await Promise.all(
|
|
4665
4808
|
allVersions.map(async (v) => {
|
|
4666
4809
|
try {
|
|
@@ -4680,14 +4823,14 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
4680
4823
|
governanceNodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4681
4824
|
const nestId = c.req.param("nestId");
|
|
4682
4825
|
const nodeId = c.req.param("nodeId");
|
|
4683
|
-
const history = getReviewHistory(nestId, nodeId);
|
|
4826
|
+
const history = await getReviewHistory(nestId, nodeId);
|
|
4684
4827
|
return c.json({ reviews: history });
|
|
4685
4828
|
});
|
|
4686
4829
|
governanceNodeRoutes.get("/:nodeId{.+}/comments", async (c) => {
|
|
4687
4830
|
const nestId = c.req.param("nestId");
|
|
4688
4831
|
const nodeId = c.req.param("nodeId");
|
|
4689
4832
|
const status = c.req.query("status");
|
|
4690
|
-
const list = listComments(nestId, nodeId, {
|
|
4833
|
+
const list = await listComments(nestId, nodeId, {
|
|
4691
4834
|
status: status === "open" || status === "resolved" ? status : void 0
|
|
4692
4835
|
});
|
|
4693
4836
|
return c.json({ comments: list });
|
|
@@ -4696,9 +4839,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
|
|
|
4696
4839
|
const nestId = c.req.param("nestId");
|
|
4697
4840
|
const nodeId = c.req.param("nodeId");
|
|
4698
4841
|
const body = await c.req.json();
|
|
4699
|
-
const author = getUserEmail3(c);
|
|
4842
|
+
const author = await getUserEmail3(c);
|
|
4700
4843
|
try {
|
|
4701
|
-
const comment = createComment({
|
|
4844
|
+
const comment = await createComment({
|
|
4702
4845
|
nestId,
|
|
4703
4846
|
nodeId,
|
|
4704
4847
|
author,
|
|
@@ -4720,9 +4863,9 @@ governanceNodeRoutes.post(
|
|
|
4720
4863
|
const nestId = c.req.param("nestId");
|
|
4721
4864
|
const nodeId = c.req.param("nodeId");
|
|
4722
4865
|
const commentId = c.req.param("commentId");
|
|
4723
|
-
const resolvedBy = getUserEmail3(c);
|
|
4866
|
+
const resolvedBy = await getUserEmail3(c);
|
|
4724
4867
|
try {
|
|
4725
|
-
const comment = resolveComment({
|
|
4868
|
+
const comment = await resolveComment({
|
|
4726
4869
|
nestId,
|
|
4727
4870
|
nodeId,
|
|
4728
4871
|
commentId,
|
|
@@ -4749,14 +4892,14 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
4749
4892
|
const nestId = c.req.param("nestId");
|
|
4750
4893
|
const nodeId = c.req.param("nodeId");
|
|
4751
4894
|
const body = await c.req.json();
|
|
4752
|
-
const currentVersion = getCurrentVersion(nestId, nodeId);
|
|
4895
|
+
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
4753
4896
|
if (currentVersion === 0) {
|
|
4754
4897
|
throw new ValidationError("Node has no versions to review");
|
|
4755
4898
|
}
|
|
4756
|
-
const userEmail = getUserEmail3(c);
|
|
4899
|
+
const userEmail = await getUserEmail3(c);
|
|
4757
4900
|
let request;
|
|
4758
4901
|
try {
|
|
4759
|
-
request = submitForReview({
|
|
4902
|
+
request = await submitForReview({
|
|
4760
4903
|
nestId,
|
|
4761
4904
|
nodeId,
|
|
4762
4905
|
version: currentVersion,
|
|
@@ -4771,7 +4914,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
4771
4914
|
}
|
|
4772
4915
|
throw err;
|
|
4773
4916
|
}
|
|
4774
|
-
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
4917
|
+
const resolved = await resolveStewardsForNode(nestId, nodeId);
|
|
4775
4918
|
return c.json(
|
|
4776
4919
|
{
|
|
4777
4920
|
review: request,
|
|
@@ -4788,13 +4931,13 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
|
|
|
4788
4931
|
const nestId = c.req.param("nestId");
|
|
4789
4932
|
const nodeId = c.req.param("nodeId");
|
|
4790
4933
|
const body = await c.req.json();
|
|
4791
|
-
const userEmail = getUserEmail3(c);
|
|
4934
|
+
const userEmail = await getUserEmail3(c);
|
|
4792
4935
|
const isAdmin = isSuperAdmin(userEmail);
|
|
4793
4936
|
try {
|
|
4794
4937
|
const request = await approve({
|
|
4795
4938
|
nestId,
|
|
4796
4939
|
nodeId,
|
|
4797
|
-
version: getCurrentVersion(nestId, nodeId),
|
|
4940
|
+
version: await getCurrentVersion(nestId, nodeId),
|
|
4798
4941
|
approvedBy: userEmail,
|
|
4799
4942
|
note: body.note,
|
|
4800
4943
|
override: body.override && isAdmin
|
|
@@ -4811,12 +4954,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
4811
4954
|
if (!body.note) {
|
|
4812
4955
|
throw new ValidationError("Rejection note is required");
|
|
4813
4956
|
}
|
|
4814
|
-
const userEmail = getUserEmail3(c);
|
|
4957
|
+
const userEmail = await getUserEmail3(c);
|
|
4815
4958
|
try {
|
|
4816
|
-
const request = reject({
|
|
4959
|
+
const request = await reject({
|
|
4817
4960
|
nestId,
|
|
4818
4961
|
nodeId,
|
|
4819
|
-
version: getCurrentVersion(nestId, nodeId),
|
|
4962
|
+
version: await getCurrentVersion(nestId, nodeId),
|
|
4820
4963
|
rejectedBy: userEmail,
|
|
4821
4964
|
note: body.note
|
|
4822
4965
|
});
|
|
@@ -4828,20 +4971,20 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
4828
4971
|
governanceNodeRoutes.get("/:nodeId{.+}/can-access", async (c) => {
|
|
4829
4972
|
const nestId = c.req.param("nestId");
|
|
4830
4973
|
const nodeId = c.req.param("nodeId");
|
|
4831
|
-
const userEmail = getUserEmail3(c);
|
|
4832
|
-
return c.json(canUserAccess(nestId, nodeId, userEmail));
|
|
4974
|
+
const userEmail = await getUserEmail3(c);
|
|
4975
|
+
return c.json(await canUserAccess(nestId, nodeId, userEmail));
|
|
4833
4976
|
});
|
|
4834
4977
|
governanceNodeRoutes.get("/:nodeId{.+}/can-approve", async (c) => {
|
|
4835
4978
|
const nestId = c.req.param("nestId");
|
|
4836
4979
|
const nodeId = c.req.param("nodeId");
|
|
4837
|
-
const userEmail = getUserEmail3(c);
|
|
4838
|
-
return c.json(canUserApprove(nestId, nodeId, userEmail));
|
|
4980
|
+
const userEmail = await getUserEmail3(c);
|
|
4981
|
+
return c.json(await canUserApprove(nestId, nodeId, userEmail));
|
|
4839
4982
|
});
|
|
4840
4983
|
governanceNodeRoutes.get("/:nodeId{.+}/can-edit", async (c) => {
|
|
4841
4984
|
const nestId = c.req.param("nestId");
|
|
4842
4985
|
const nodeId = c.req.param("nodeId");
|
|
4843
|
-
const userEmail = getUserEmail3(c);
|
|
4844
|
-
return c.json(canUserEdit(nestId, nodeId, userEmail));
|
|
4986
|
+
const userEmail = await getUserEmail3(c);
|
|
4987
|
+
return c.json(await canUserEdit(nestId, nodeId, userEmail));
|
|
4845
4988
|
});
|
|
4846
4989
|
governanceNodeRoutes.get("/:nodeId{.+?}/external-edits", async (c) => {
|
|
4847
4990
|
const nestId = c.req.param("nestId");
|
|
@@ -4873,7 +5016,7 @@ governanceNodeRoutes.post(
|
|
|
4873
5016
|
const nodeId = c.req.param("nodeId");
|
|
4874
5017
|
const suggestionId = c.req.param("suggestionId");
|
|
4875
5018
|
const body = await c.req.json().catch(() => ({}));
|
|
4876
|
-
const actor = getUserEmail3(c);
|
|
5019
|
+
const actor = await getUserEmail3(c);
|
|
4877
5020
|
try {
|
|
4878
5021
|
const result = await approveExternalEdit({
|
|
4879
5022
|
nestId,
|
|
@@ -4909,7 +5052,7 @@ governanceNodeRoutes.post(
|
|
|
4909
5052
|
if (!body.reason) {
|
|
4910
5053
|
throw new ValidationError("Rejection reason is required");
|
|
4911
5054
|
}
|
|
4912
|
-
const actor = getUserEmail3(c);
|
|
5055
|
+
const actor = await getUserEmail3(c);
|
|
4913
5056
|
try {
|
|
4914
5057
|
const result = await rejectExternalEdit({
|
|
4915
5058
|
nestId,
|
|
@@ -4930,31 +5073,38 @@ governanceNodeRoutes.post(
|
|
|
4930
5073
|
governanceNodeRoutes.post("/:nodeId{.+}/cancel-review", async (c) => {
|
|
4931
5074
|
const nestId = c.req.param("nestId");
|
|
4932
5075
|
const nodeId = c.req.param("nodeId");
|
|
4933
|
-
const userEmail = getUserEmail3(c);
|
|
4934
|
-
const request = cancelReview({
|
|
5076
|
+
const userEmail = await getUserEmail3(c);
|
|
5077
|
+
const request = await cancelReview({
|
|
4935
5078
|
nestId,
|
|
4936
5079
|
nodeId,
|
|
4937
5080
|
cancelledBy: userEmail
|
|
4938
5081
|
});
|
|
4939
5082
|
return c.json({ review: request });
|
|
4940
5083
|
});
|
|
4941
|
-
function getUserEmail3(c) {
|
|
5084
|
+
async function getUserEmail3(c) {
|
|
4942
5085
|
const userId = c.get("userId");
|
|
4943
5086
|
const db = getDb();
|
|
4944
|
-
const user = db.
|
|
5087
|
+
const user = await db.get(
|
|
5088
|
+
"SELECT email FROM users WHERE id = ?",
|
|
5089
|
+
[userId]
|
|
5090
|
+
);
|
|
4945
5091
|
return user?.email || "anonymous@localhost";
|
|
4946
5092
|
}
|
|
4947
5093
|
|
|
4948
5094
|
// src/auth/anonymous.ts
|
|
4949
5095
|
import bcrypt from "bcryptjs";
|
|
4950
|
-
function ensureAnonymousUser() {
|
|
5096
|
+
async function ensureAnonymousUser() {
|
|
4951
5097
|
const db = getDb();
|
|
4952
|
-
const exists = db.
|
|
5098
|
+
const exists = await db.get(
|
|
5099
|
+
"SELECT id FROM users WHERE id = ?",
|
|
5100
|
+
[ANON_USER_ID]
|
|
5101
|
+
);
|
|
4953
5102
|
if (!exists) {
|
|
4954
5103
|
const placeholder = bcrypt.hashSync("anon-no-login", 4);
|
|
4955
|
-
db.
|
|
4956
|
-
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)"
|
|
4957
|
-
|
|
5104
|
+
await db.run(
|
|
5105
|
+
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
5106
|
+
[ANON_USER_ID, ANON_EMAIL, "Admin", placeholder]
|
|
5107
|
+
);
|
|
4958
5108
|
}
|
|
4959
5109
|
return ANON_USER_ID;
|
|
4960
5110
|
}
|
|
@@ -4972,7 +5122,7 @@ var UI_DIR_CANDIDATES = [
|
|
|
4972
5122
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
4973
5123
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
4974
5124
|
var openModeMiddleware = createMiddleware2(async (c, next) => {
|
|
4975
|
-
const anonId = ensureAnonymousUser();
|
|
5125
|
+
const anonId = await ensureAnonymousUser();
|
|
4976
5126
|
c.set("userId", anonId);
|
|
4977
5127
|
c.set("nestScope", null);
|
|
4978
5128
|
await next();
|
|
@@ -4989,13 +5139,13 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
4989
5139
|
return authMiddleware(c, next);
|
|
4990
5140
|
}
|
|
4991
5141
|
if (config.AUTH_MODE === "open") {
|
|
4992
|
-
const anonId = ensureAnonymousUser();
|
|
5142
|
+
const anonId = await ensureAnonymousUser();
|
|
4993
5143
|
c.set("userId", anonId);
|
|
4994
5144
|
c.set("nestScope", null);
|
|
4995
5145
|
return next();
|
|
4996
5146
|
}
|
|
4997
5147
|
if (isPublicReadEligiblePath(c.req.method, c.req.path)) {
|
|
4998
|
-
const anonId = ensureAnonymousUser();
|
|
5148
|
+
const anonId = await ensureAnonymousUser();
|
|
4999
5149
|
c.set("userId", anonId);
|
|
5000
5150
|
c.set("nestScope", null);
|
|
5001
5151
|
return next();
|
|
@@ -5117,7 +5267,7 @@ function createApp() {
|
|
|
5117
5267
|
}
|
|
5118
5268
|
});
|
|
5119
5269
|
app.use("/admin/settings", flexAuthMiddleware);
|
|
5120
|
-
const adminSettingsAllowed = (c) => config.AUTH_MODE === "open" || isLicenseAdminUserId(c.get("userId"));
|
|
5270
|
+
const adminSettingsAllowed = async (c) => config.AUTH_MODE === "open" || await isLicenseAdminUserId(c.get("userId"));
|
|
5121
5271
|
const currentServerSettings = () => ({
|
|
5122
5272
|
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
5123
5273
|
logo_url: config.LOGO_URL,
|
|
@@ -5125,13 +5275,13 @@ function createApp() {
|
|
|
5125
5275
|
public_base_url: config.PUBLIC_BASE_URL,
|
|
5126
5276
|
max_body_bytes: config.MAX_BODY_BYTES
|
|
5127
5277
|
});
|
|
5128
|
-
app.get("/admin/settings", (c) => {
|
|
5129
|
-
if (!adminSettingsAllowed(c))
|
|
5278
|
+
app.get("/admin/settings", async (c) => {
|
|
5279
|
+
if (!await adminSettingsAllowed(c))
|
|
5130
5280
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
5131
5281
|
return c.json(currentServerSettings());
|
|
5132
5282
|
});
|
|
5133
5283
|
app.patch("/admin/settings", async (c) => {
|
|
5134
|
-
if (!adminSettingsAllowed(c))
|
|
5284
|
+
if (!await adminSettingsAllowed(c))
|
|
5135
5285
|
return c.json({ error: "Only the server admin can change this." }, 403);
|
|
5136
5286
|
let body;
|
|
5137
5287
|
try {
|
|
@@ -5187,18 +5337,22 @@ function createApp() {
|
|
|
5187
5337
|
app.get("/stats", async (c) => {
|
|
5188
5338
|
const db = getDb();
|
|
5189
5339
|
const userId = c.get("userId");
|
|
5190
|
-
const userEmail = resolveCallerEmail(userId);
|
|
5191
|
-
const
|
|
5340
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5341
|
+
const owned = await listNests(userId);
|
|
5342
|
+
const sharedNests = await listSharedNests(userId);
|
|
5343
|
+
const visibleNests = [...owned, ...sharedNests];
|
|
5192
5344
|
let documents = 0;
|
|
5193
5345
|
for (const nest of visibleNests) {
|
|
5194
5346
|
try {
|
|
5195
|
-
const { storage } = engineCache.get(nest.id);
|
|
5347
|
+
const { storage } = await engineCache.get(nest.id);
|
|
5196
5348
|
const docs = await storage.discoverDocuments();
|
|
5197
|
-
documents += filterAccessible(nest.id, userId, userEmail, docs).length;
|
|
5349
|
+
documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
|
|
5198
5350
|
} catch {
|
|
5199
5351
|
}
|
|
5200
5352
|
}
|
|
5201
|
-
const usersRow = db.
|
|
5353
|
+
const usersRow = await db.get(
|
|
5354
|
+
"SELECT COUNT(*) as c FROM users"
|
|
5355
|
+
);
|
|
5202
5356
|
return c.json({
|
|
5203
5357
|
nests: visibleNests.length,
|
|
5204
5358
|
documents,
|
|
@@ -5252,7 +5406,7 @@ function createApp() {
|
|
|
5252
5406
|
c.set("nestPermission", "owner");
|
|
5253
5407
|
return next();
|
|
5254
5408
|
}
|
|
5255
|
-
const permission = resolveNestPermission(nestId, userId);
|
|
5409
|
+
const permission = await resolveNestPermission(nestId, userId);
|
|
5256
5410
|
if (permission === "none") {
|
|
5257
5411
|
return c.json({ error: "Nest not found" }, 404);
|
|
5258
5412
|
}
|
|
@@ -5262,7 +5416,7 @@ function createApp() {
|
|
|
5262
5416
|
const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
|
|
5263
5417
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
5264
5418
|
const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
|
|
5265
|
-
if (isStewardRoster && !canManageStewards(resolveCallerEmail(userId))) {
|
|
5419
|
+
if (isStewardRoster && !canManageStewards(await resolveCallerEmail(userId))) {
|
|
5266
5420
|
return c.json(
|
|
5267
5421
|
{
|
|
5268
5422
|
error: "You don't have permission to manage stewards. Only the super admin can do this."
|
|
@@ -5281,7 +5435,7 @@ function createApp() {
|
|
|
5281
5435
|
const isNodeRevert = c.req.method === "POST" && parts.length >= 4 && parts[parts.length - 1] === "revert";
|
|
5282
5436
|
let stewardEditorBypass = false;
|
|
5283
5437
|
if (required === "write" && permission === "read" && parts[1] === "nodes") {
|
|
5284
|
-
const userEmail = resolveCallerEmail(userId);
|
|
5438
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5285
5439
|
if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE" || isNodeRevert)) {
|
|
5286
5440
|
const idParts = isNodeRevert ? parts.slice(2, -1) : parts.slice(2);
|
|
5287
5441
|
const rawNodeId = idParts.join("/");
|
|
@@ -5290,12 +5444,12 @@ function createApp() {
|
|
|
5290
5444
|
nodeId = decodeURIComponent(rawNodeId);
|
|
5291
5445
|
} catch {
|
|
5292
5446
|
}
|
|
5293
|
-
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
5447
|
+
const resolved = await resolveStewardsForNode(nestId, nodeId);
|
|
5294
5448
|
stewardEditorBypass = resolved.some(
|
|
5295
5449
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor"
|
|
5296
5450
|
);
|
|
5297
5451
|
} else if (parts.length === 2 && c.req.method === "POST") {
|
|
5298
|
-
const resolved = resolveStewardsForNode(nestId, "");
|
|
5452
|
+
const resolved = await resolveStewardsForNode(nestId, "");
|
|
5299
5453
|
stewardEditorBypass = resolved.some(
|
|
5300
5454
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && r.steward.scope === "nest"
|
|
5301
5455
|
);
|
|
@@ -5403,28 +5557,30 @@ function createApp() {
|
|
|
5403
5557
|
|
|
5404
5558
|
// src/db/backfill.ts
|
|
5405
5559
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
5406
|
-
import { join as join5 } from "path";
|
|
5407
5560
|
var MIGRATION_ID = "005_backfill_node_versions_from_history";
|
|
5408
5561
|
async function backfillNodeVersionsFromHistory(db) {
|
|
5409
|
-
const already = db.
|
|
5562
|
+
const already = await db.get(
|
|
5563
|
+
"SELECT id FROM schema_migrations WHERE id = ?",
|
|
5564
|
+
[MIGRATION_ID]
|
|
5565
|
+
);
|
|
5410
5566
|
if (already) return;
|
|
5411
|
-
const nests = db.
|
|
5412
|
-
const
|
|
5413
|
-
|
|
5567
|
+
const nests = await db.all("SELECT id FROM nests");
|
|
5568
|
+
const insertSql = insertOrIgnore(
|
|
5569
|
+
db,
|
|
5570
|
+
`INSERT INTO node_versions
|
|
5414
5571
|
(nest_id, node_id, version, content_hash, author, status, change_note, created_at)
|
|
5415
5572
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5416
5573
|
);
|
|
5417
|
-
const
|
|
5418
|
-
`INSERT OR REPLACE INTO approved_versions
|
|
5574
|
+
const approvedPinSql = `INSERT INTO approved_versions
|
|
5419
5575
|
(nest_id, node_id, approved_version, approved_by, approved_at)
|
|
5420
|
-
VALUES (?, ?, ?, ?,
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
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`;
|
|
5424
5580
|
let totalInserted = 0;
|
|
5425
5581
|
let totalDocs = 0;
|
|
5426
5582
|
for (const { id: nestId } of nests) {
|
|
5427
|
-
const nestPath =
|
|
5583
|
+
const nestPath = resolveNestPath(nestId);
|
|
5428
5584
|
const storage = new NestStorage(nestPath);
|
|
5429
5585
|
let docs;
|
|
5430
5586
|
try {
|
|
@@ -5445,15 +5601,16 @@ async function backfillNodeVersionsFromHistory(db) {
|
|
|
5445
5601
|
history = null;
|
|
5446
5602
|
}
|
|
5447
5603
|
if (!history || history.versions.length === 0) continue;
|
|
5448
|
-
const existing = db.
|
|
5449
|
-
`SELECT version FROM node_versions WHERE nest_id = ? AND node_id =
|
|
5450
|
-
|
|
5604
|
+
const existing = await db.all(
|
|
5605
|
+
`SELECT version FROM node_versions WHERE nest_id = ? AND node_id = ?`,
|
|
5606
|
+
[nestId, doc.id]
|
|
5607
|
+
);
|
|
5451
5608
|
const known = new Set(existing.map((r) => r.version));
|
|
5452
5609
|
const tagsJson = doc.frontmatter.tags ? JSON.stringify(doc.frontmatter.tags) : null;
|
|
5453
5610
|
const latestVersion = history.versions[history.versions.length - 1].version;
|
|
5454
5611
|
for (const entry of history.versions) {
|
|
5455
5612
|
if (known.has(entry.version)) continue;
|
|
5456
|
-
|
|
5613
|
+
await db.run(insertSql, [
|
|
5457
5614
|
nestId,
|
|
5458
5615
|
doc.id,
|
|
5459
5616
|
entry.version,
|
|
@@ -5462,33 +5619,33 @@ async function backfillNodeVersionsFromHistory(db) {
|
|
|
5462
5619
|
"approved",
|
|
5463
5620
|
entry.note || null,
|
|
5464
5621
|
entry.edited_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
5465
|
-
);
|
|
5622
|
+
]);
|
|
5466
5623
|
totalInserted += 1;
|
|
5467
5624
|
}
|
|
5468
|
-
const pin = db.
|
|
5469
|
-
`SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id =
|
|
5470
|
-
|
|
5625
|
+
const pin = await db.get(
|
|
5626
|
+
`SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?`,
|
|
5627
|
+
[nestId, doc.id]
|
|
5628
|
+
);
|
|
5471
5629
|
if (!pin || pin.approved_version < latestVersion) {
|
|
5472
|
-
|
|
5630
|
+
await db.run(approvedPinSql, [
|
|
5473
5631
|
nestId,
|
|
5474
5632
|
doc.id,
|
|
5475
5633
|
latestVersion,
|
|
5476
|
-
history.versions[history.versions.length - 1].edited_by || "system:backfill"
|
|
5477
|
-
|
|
5478
|
-
doc.id
|
|
5479
|
-
);
|
|
5634
|
+
history.versions[history.versions.length - 1].edited_by || "system:backfill"
|
|
5635
|
+
]);
|
|
5480
5636
|
}
|
|
5481
5637
|
if (tagsJson) {
|
|
5482
|
-
|
|
5638
|
+
await db.run(
|
|
5483
5639
|
`UPDATE node_versions SET tags_json = ?
|
|
5484
|
-
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]
|
|
5485
5642
|
);
|
|
5486
|
-
updateTags.run(tagsJson, nestId, doc.id, latestVersion);
|
|
5487
5643
|
}
|
|
5488
5644
|
}
|
|
5489
5645
|
}
|
|
5490
|
-
db.
|
|
5491
|
-
|
|
5646
|
+
await db.run(
|
|
5647
|
+
insertOrIgnore(db, "INSERT INTO schema_migrations (id) VALUES (?)"),
|
|
5648
|
+
[MIGRATION_ID]
|
|
5492
5649
|
);
|
|
5493
5650
|
console.log(
|
|
5494
5651
|
`[backfill] node_versions: scanned ${totalDocs} docs across ${nests.length} nests, inserted ${totalInserted} rows`
|
|
@@ -5497,7 +5654,7 @@ async function backfillNodeVersionsFromHistory(db) {
|
|
|
5497
5654
|
|
|
5498
5655
|
// src/index.ts
|
|
5499
5656
|
async function main() {
|
|
5500
|
-
const db =
|
|
5657
|
+
const db = await initDb();
|
|
5501
5658
|
try {
|
|
5502
5659
|
await backfillNodeVersionsFromHistory(db);
|
|
5503
5660
|
} catch (err) {
|
|
@@ -5531,7 +5688,8 @@ async function main() {
|
|
|
5531
5688
|
}
|
|
5532
5689
|
const app = createApp();
|
|
5533
5690
|
startLicenseSafetyPoll();
|
|
5534
|
-
const
|
|
5691
|
+
const driftRaw = process.env.DRIFT_SCAN_INTERVAL_MS;
|
|
5692
|
+
const driftScanIntervalMs = driftRaw != null && driftRaw !== "" && Number.isFinite(Number(driftRaw)) ? Number(driftRaw) : 3e4;
|
|
5535
5693
|
if (driftScanIntervalMs > 0) {
|
|
5536
5694
|
startDriftScanner(driftScanIntervalMs);
|
|
5537
5695
|
}
|
|
@@ -5542,11 +5700,13 @@ async function main() {
|
|
|
5542
5700
|
licensed: license.valid
|
|
5543
5701
|
});
|
|
5544
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})`;
|
|
5545
5704
|
console.log(`
|
|
5546
5705
|
ContextNest Community Server v0.1.0
|
|
5547
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
|
|
5548
5707
|
Port: ${config.PORT}
|
|
5549
5708
|
Data: ${config.DATA_ROOT}
|
|
5709
|
+
Database: ${dbDesc}
|
|
5550
5710
|
License: ${license.valid ? `${license.tier}${license.org ? ` (${license.org})` : ""}` : "unlicensed (register at promptowl.ai)"}
|
|
5551
5711
|
Auth: ${authDesc}
|
|
5552
5712
|
Telemetry: ${config.TELEMETRY_ENABLED ? "on" : "off"}
|