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