@promptowl/contextnest-community 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +181 -123
- package/LICENSE.md +142 -142
- package/README.md +224 -211
- package/dist/adapter.postgres-YOODX2BI.js +71 -0
- package/dist/{chunk-EMOE53KX.js → chunk-43DOX4LH.js} +356 -267
- package/dist/{chunk-IWA2UDAT.js → chunk-MOXICJPD.js} +116 -90
- package/dist/{chunk-RMU3LOPH.js → chunk-QMLAXQES.js} +383 -69
- package/dist/chunk-SLTQACJW.js +8 -0
- package/dist/{chunk-HIH7I232.js → chunk-VD5QX2ZQ.js} +69 -53
- package/dist/index.js +1427 -976
- package/dist/migrations.postgres-3HMGLKOV.js +279 -0
- package/dist/{review-service-XNXHF6Q7.js → review-service-2FSKW425.js} +5 -4
- package/dist/{stewardship-service-P4M5UEJW.js → stewardship-service-I2JAFYJU.js} +3 -2
- package/dist/{version-service-REGL5CUT.js → version-service-FJWVTZKR.js} +3 -2
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-24dBhQSQ.css +1 -0
- package/dist/web3/assets/index-DpM8J4RN.js +889 -0
- package/dist/web3/index.html +14 -14
- package/package.json +152 -150
- package/dist/web3/assets/index-C2dfT3Et.css +0 -1
- package/dist/web3/assets/index-DEKnE-ty.js +0 -887
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ANON_USER_ID,
|
|
3
2
|
config,
|
|
4
|
-
getDb
|
|
5
|
-
|
|
3
|
+
getDb,
|
|
4
|
+
insertOrReplace,
|
|
5
|
+
nowExpr
|
|
6
|
+
} from "./chunk-QMLAXQES.js";
|
|
7
|
+
import {
|
|
8
|
+
ANON_USER_ID
|
|
9
|
+
} from "./chunk-SLTQACJW.js";
|
|
6
10
|
|
|
7
11
|
// src/governance/stewardship-service.ts
|
|
8
12
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -148,7 +152,6 @@ import {
|
|
|
148
152
|
} from "@promptowl/contextnest-engine";
|
|
149
153
|
|
|
150
154
|
// src/nests/service.ts
|
|
151
|
-
import { join as join3 } from "path";
|
|
152
155
|
import { rmSync as rmSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
153
156
|
import { v4 as uuid } from "uuid";
|
|
154
157
|
import { NestStorage } from "@promptowl/contextnest-engine";
|
|
@@ -188,54 +191,59 @@ function discardImportDir(dest) {
|
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
// src/shared/paths.ts
|
|
195
|
+
import { join as join3 } from "path";
|
|
196
|
+
function nestStorageRoot() {
|
|
197
|
+
return config.NEST_STORAGE_ROOT;
|
|
198
|
+
}
|
|
199
|
+
function resolveNestPath(nestId) {
|
|
200
|
+
return join3(nestStorageRoot(), nestId);
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
// src/telemetry/tracker.ts
|
|
192
204
|
import { createHash } from "crypto";
|
|
193
205
|
function computeBatchId(ids) {
|
|
194
206
|
const key = ids.slice().sort((a, b) => a - b).join(",");
|
|
195
207
|
return createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
196
208
|
}
|
|
197
|
-
function trackEvent(event, data) {
|
|
209
|
+
async function trackEvent(event, data) {
|
|
198
210
|
if (!config.TELEMETRY_ENABLED) return;
|
|
199
211
|
try {
|
|
200
212
|
const db = getDb();
|
|
201
|
-
db.
|
|
202
|
-
"INSERT INTO telemetry_events (event, data_json) VALUES (?, ?)"
|
|
203
|
-
|
|
213
|
+
await db.run(
|
|
214
|
+
"INSERT INTO telemetry_events (event, data_json) VALUES (?, ?)",
|
|
215
|
+
[event, data ? JSON.stringify(data) : null]
|
|
216
|
+
);
|
|
204
217
|
} catch {
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
async function flushTelemetry() {
|
|
208
221
|
if (!config.TELEMETRY_ENABLED || !config.PROMPTOWL_KEY) return;
|
|
209
222
|
const db = getDb();
|
|
210
|
-
const userCount = db.
|
|
211
|
-
const nestCount = db.
|
|
212
|
-
const events = db.
|
|
223
|
+
const userCount = (await db.get("SELECT COUNT(*) as c FROM users"))?.c || 0;
|
|
224
|
+
const nestCount = (await db.get("SELECT COUNT(*) as c FROM nests"))?.c || 0;
|
|
225
|
+
const events = await db.all(
|
|
213
226
|
"SELECT id, event, data_json, created_at FROM telemetry_events WHERE sent = 0 ORDER BY id LIMIT 100"
|
|
214
|
-
)
|
|
227
|
+
);
|
|
215
228
|
if (events.length === 0 && userCount === 0) {
|
|
216
229
|
console.log("[telemetry] skip flush: no events and no users");
|
|
217
230
|
return;
|
|
218
231
|
}
|
|
219
|
-
const emailStmt = db.prepare("SELECT email FROM users WHERE id = ?");
|
|
220
232
|
const emailCache = /* @__PURE__ */ new Map();
|
|
221
|
-
const resolveEmail = (userId) => {
|
|
233
|
+
const resolveEmail = async (userId) => {
|
|
222
234
|
if (typeof userId !== "string" || !userId) return null;
|
|
223
235
|
if (emailCache.has(userId)) return emailCache.get(userId) ?? null;
|
|
224
|
-
const row =
|
|
236
|
+
const row = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
225
237
|
const email = row?.email ?? null;
|
|
226
238
|
emailCache.set(userId, email);
|
|
227
239
|
return email;
|
|
228
240
|
};
|
|
229
241
|
const batchId = events.length > 0 ? computeBatchId(events.map((e) => e.id)) : null;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
batch_id: batchId,
|
|
233
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
234
|
-
stats: { users: userCount, nests: nestCount },
|
|
235
|
-
events: events.map((e) => {
|
|
242
|
+
const eventPayload = await Promise.all(
|
|
243
|
+
events.map(async (e) => {
|
|
236
244
|
const data = e.data_json ? JSON.parse(e.data_json) : null;
|
|
237
245
|
if (data && data.userId && !data.userEmail) {
|
|
238
|
-
const email = resolveEmail(data.userId);
|
|
246
|
+
const email = await resolveEmail(data.userId);
|
|
239
247
|
if (email) data.userEmail = email;
|
|
240
248
|
}
|
|
241
249
|
return {
|
|
@@ -244,6 +252,13 @@ async function flushTelemetry() {
|
|
|
244
252
|
at: e.created_at
|
|
245
253
|
};
|
|
246
254
|
})
|
|
255
|
+
);
|
|
256
|
+
const payload = {
|
|
257
|
+
server_key: config.PROMPTOWL_KEY,
|
|
258
|
+
batch_id: batchId,
|
|
259
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
+
stats: { users: userCount, nests: nestCount },
|
|
261
|
+
events: eventPayload
|
|
247
262
|
};
|
|
248
263
|
const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
|
|
249
264
|
const url = `${promptowlUrl}/api/telemetry/ingest`;
|
|
@@ -260,9 +275,10 @@ async function flushTelemetry() {
|
|
|
260
275
|
console.log(`[telemetry] response: ${res.status} ${res.statusText}`);
|
|
261
276
|
if (res.ok && events.length > 0) {
|
|
262
277
|
const ids = events.map((e) => e.id);
|
|
263
|
-
db.
|
|
264
|
-
`UPDATE telemetry_events SET sent = 1 WHERE id IN (${ids.map(() => "?").join(",")})
|
|
265
|
-
|
|
278
|
+
await db.run(
|
|
279
|
+
`UPDATE telemetry_events SET sent = 1 WHERE id IN (${ids.map(() => "?").join(",")})`,
|
|
280
|
+
ids
|
|
281
|
+
);
|
|
266
282
|
console.log(`[telemetry] marked ${ids.length} events as sent`);
|
|
267
283
|
}
|
|
268
284
|
} catch (err) {
|
|
@@ -291,9 +307,12 @@ function isLicenseAdminEmail(email) {
|
|
|
291
307
|
if (!lic?.valid || !lic.ownerEmail) return false;
|
|
292
308
|
return lic.ownerEmail.toLowerCase() === email.toLowerCase();
|
|
293
309
|
}
|
|
294
|
-
function isLicenseAdminUserId(userId) {
|
|
310
|
+
async function isLicenseAdminUserId(userId) {
|
|
295
311
|
try {
|
|
296
|
-
const row = getDb().
|
|
312
|
+
const row = await getDb().get(
|
|
313
|
+
"SELECT email FROM users WHERE id = ?",
|
|
314
|
+
[userId]
|
|
315
|
+
);
|
|
297
316
|
return isLicenseAdminEmail(row?.email);
|
|
298
317
|
} catch {
|
|
299
318
|
return false;
|
|
@@ -352,7 +371,7 @@ function startLicenseSafetyPoll() {
|
|
|
352
371
|
const isValid = !!currentLicense?.valid;
|
|
353
372
|
if (wasValid && !isValid) {
|
|
354
373
|
console.log("[license] safety poll detected revocation");
|
|
355
|
-
handleLicenseRevoked();
|
|
374
|
+
await handleLicenseRevoked();
|
|
356
375
|
}
|
|
357
376
|
} catch (err) {
|
|
358
377
|
console.warn(
|
|
@@ -364,10 +383,10 @@ function startLicenseSafetyPoll() {
|
|
|
364
383
|
safetyPollHandle.unref();
|
|
365
384
|
}
|
|
366
385
|
}
|
|
367
|
-
function handleLicenseRevoked() {
|
|
386
|
+
async function handleLicenseRevoked() {
|
|
368
387
|
try {
|
|
369
388
|
const db = getDb();
|
|
370
|
-
const result = db.
|
|
389
|
+
const result = await db.run("DELETE FROM sessions");
|
|
371
390
|
console.warn(
|
|
372
391
|
`[license] revoked \u2014 wiped ${result.changes} active session(s).`
|
|
373
392
|
);
|
|
@@ -423,7 +442,10 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
423
442
|
};
|
|
424
443
|
}
|
|
425
444
|
const db = getDb();
|
|
426
|
-
const cached = db.
|
|
445
|
+
const cached = await db.get(
|
|
446
|
+
"SELECT * FROM license_cache WHERE key = ?",
|
|
447
|
+
[key]
|
|
448
|
+
);
|
|
427
449
|
if (cached && !forceFresh) {
|
|
428
450
|
const age = Date.now() - (/* @__PURE__ */ new Date(cached.validated_at + "Z")).getTime();
|
|
429
451
|
if (age < CACHE_TTL_MS) {
|
|
@@ -513,7 +535,7 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
513
535
|
suspensionReason = null;
|
|
514
536
|
}
|
|
515
537
|
if (!data.valid && !data.suspended) {
|
|
516
|
-
db.
|
|
538
|
+
await db.run("DELETE FROM license_cache WHERE key = ?", [key]);
|
|
517
539
|
return {
|
|
518
540
|
valid: false,
|
|
519
541
|
tier: "none",
|
|
@@ -526,15 +548,21 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
526
548
|
}
|
|
527
549
|
if (data.valid) {
|
|
528
550
|
const limitsJson = data.limits ? JSON.stringify(data.limits) : null;
|
|
529
|
-
db.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
551
|
+
await db.run(
|
|
552
|
+
insertOrReplace(
|
|
553
|
+
db,
|
|
554
|
+
`INSERT INTO license_cache (key, tier, org, limits_json, owner_email, validated_at)
|
|
555
|
+
VALUES (?, ?, ?, ?, ?, ${nowExpr(db)})`,
|
|
556
|
+
["key"],
|
|
557
|
+
`tier = excluded.tier, org = excluded.org, limits_json = excluded.limits_json, owner_email = excluded.owner_email, validated_at = ${nowExpr(db)}`
|
|
558
|
+
),
|
|
559
|
+
[
|
|
560
|
+
key,
|
|
561
|
+
data.tier || "community",
|
|
562
|
+
data.org || null,
|
|
563
|
+
limitsJson,
|
|
564
|
+
data.owner_email || null
|
|
565
|
+
]
|
|
538
566
|
);
|
|
539
567
|
}
|
|
540
568
|
return {
|
|
@@ -574,71 +602,80 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
574
602
|
}
|
|
575
603
|
|
|
576
604
|
// src/nests/service.ts
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
async function isImportedNest(nestId) {
|
|
606
|
+
const row = await getDb().get(
|
|
607
|
+
"SELECT is_imported FROM nests WHERE id = ?",
|
|
608
|
+
[nestId]
|
|
609
|
+
);
|
|
582
610
|
return !!row?.is_imported;
|
|
583
611
|
}
|
|
584
612
|
function toSlug(name) {
|
|
585
613
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
586
614
|
}
|
|
587
|
-
function uniqueNestName(userId, baseName) {
|
|
615
|
+
async function uniqueNestName(userId, baseName) {
|
|
588
616
|
const db = getDb();
|
|
589
|
-
const slugTaken = db.prepare(
|
|
590
|
-
"SELECT 1 FROM nests WHERE user_id = ? AND slug = ? LIMIT 1"
|
|
591
|
-
);
|
|
592
617
|
let candidate = baseName;
|
|
593
618
|
let n = 0;
|
|
594
619
|
while (n < 1e3) {
|
|
595
620
|
const slug = toSlug(candidate);
|
|
596
|
-
const hit =
|
|
621
|
+
const hit = await db.get(
|
|
622
|
+
"SELECT 1 FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
|
|
623
|
+
[userId, slug]
|
|
624
|
+
);
|
|
597
625
|
if (!hit) return candidate;
|
|
598
626
|
n++;
|
|
599
627
|
candidate = `${baseName} (${n})`;
|
|
600
628
|
}
|
|
601
629
|
return candidate;
|
|
602
630
|
}
|
|
603
|
-
function isStewardshipEnabled(nestId) {
|
|
631
|
+
async function isStewardshipEnabled(nestId) {
|
|
604
632
|
const db = getDb();
|
|
605
|
-
const row = db.
|
|
633
|
+
const row = await db.get(
|
|
634
|
+
"SELECT stewardship_enabled FROM nests WHERE id = ?",
|
|
635
|
+
[nestId]
|
|
636
|
+
);
|
|
606
637
|
return !!row?.stewardship_enabled;
|
|
607
638
|
}
|
|
608
|
-
function setStewardshipEnabled(nestId, enabled) {
|
|
639
|
+
async function setStewardshipEnabled(nestId, enabled) {
|
|
609
640
|
const db = getDb();
|
|
610
|
-
db.
|
|
641
|
+
await db.run("UPDATE nests SET stewardship_enabled = ? WHERE id = ?", [
|
|
611
642
|
enabled ? 1 : 0,
|
|
612
643
|
nestId
|
|
613
|
-
);
|
|
644
|
+
]);
|
|
614
645
|
}
|
|
615
|
-
function nestAllowsSelfApprove(nestId) {
|
|
646
|
+
async function nestAllowsSelfApprove(nestId) {
|
|
616
647
|
const db = getDb();
|
|
617
|
-
const row = db.
|
|
648
|
+
const row = await db.get(
|
|
649
|
+
"SELECT allow_self_approve FROM nests WHERE id = ?",
|
|
650
|
+
[nestId]
|
|
651
|
+
);
|
|
618
652
|
return !!row?.allow_self_approve;
|
|
619
653
|
}
|
|
620
|
-
function setAllowSelfApprove(nestId, allow) {
|
|
654
|
+
async function setAllowSelfApprove(nestId, allow) {
|
|
621
655
|
const db = getDb();
|
|
622
|
-
db.
|
|
656
|
+
await db.run("UPDATE nests SET allow_self_approve = ? WHERE id = ?", [
|
|
623
657
|
allow ? 1 : 0,
|
|
624
658
|
nestId
|
|
625
|
-
);
|
|
659
|
+
]);
|
|
626
660
|
}
|
|
627
|
-
function disableStewardshipAndWipeGovernance(nestId) {
|
|
661
|
+
async function disableStewardshipAndWipeGovernance(nestId) {
|
|
628
662
|
const db = getDb();
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
663
|
+
return db.transaction(async (tx) => {
|
|
664
|
+
const id = nestId;
|
|
665
|
+
const stewards = (await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id])).changes;
|
|
666
|
+
const pendingReviews = (await tx.run(
|
|
667
|
+
"DELETE FROM review_requests WHERE nest_id = ? AND status = 'pending'",
|
|
668
|
+
[id]
|
|
669
|
+
)).changes;
|
|
670
|
+
await tx.run("UPDATE nests SET stewardship_enabled = 0 WHERE id = ?", [id]);
|
|
635
671
|
return { stewards, pendingReviews };
|
|
636
672
|
});
|
|
637
|
-
return wipe(nestId);
|
|
638
673
|
}
|
|
639
|
-
function renameNest(nestId, patch) {
|
|
674
|
+
async function renameNest(nestId, patch) {
|
|
640
675
|
const db = getDb();
|
|
641
|
-
const existing = db.
|
|
676
|
+
const existing = await db.get("SELECT * FROM nests WHERE id = ?", [
|
|
677
|
+
nestId
|
|
678
|
+
]);
|
|
642
679
|
if (!existing) {
|
|
643
680
|
throw new ValidationError("Nest not found");
|
|
644
681
|
}
|
|
@@ -649,9 +686,10 @@ function renameNest(nestId, patch) {
|
|
|
649
686
|
if (!trimmed) throw new ValidationError("Name cannot be empty");
|
|
650
687
|
const slug = toSlug(trimmed);
|
|
651
688
|
if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
|
|
652
|
-
const conflict = db.
|
|
653
|
-
"SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1"
|
|
654
|
-
|
|
689
|
+
const conflict = await db.get(
|
|
690
|
+
"SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1",
|
|
691
|
+
[existing.user_id, slug, nestId]
|
|
692
|
+
);
|
|
655
693
|
if (conflict) {
|
|
656
694
|
throw new ValidationError(
|
|
657
695
|
`Another nest already uses this name. Pick a different one.`
|
|
@@ -668,25 +706,27 @@ function renameNest(nestId, patch) {
|
|
|
668
706
|
return existing;
|
|
669
707
|
}
|
|
670
708
|
params.push(nestId);
|
|
671
|
-
db.
|
|
672
|
-
|
|
709
|
+
await db.run(
|
|
710
|
+
`UPDATE nests SET ${updates.join(", ")} WHERE id = ?`,
|
|
711
|
+
params
|
|
673
712
|
);
|
|
674
|
-
return db.
|
|
713
|
+
return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
|
|
675
714
|
}
|
|
676
715
|
async function createNest(userId, name, description) {
|
|
677
716
|
const id = uuid();
|
|
678
717
|
const slug = toSlug(name);
|
|
679
718
|
const db = getDb();
|
|
680
719
|
const visibility = userId === ANON_USER_ID ? "public" : "private";
|
|
681
|
-
db.
|
|
682
|
-
"INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)"
|
|
683
|
-
|
|
720
|
+
await db.run(
|
|
721
|
+
"INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
|
|
722
|
+
[id, userId, name, slug, description || null, visibility]
|
|
723
|
+
);
|
|
684
724
|
const path = resolveNestPath(id);
|
|
685
725
|
mkdirSync2(path, { recursive: true });
|
|
686
726
|
const storage = new NestStorage(path);
|
|
687
727
|
await storage.init(name);
|
|
688
728
|
trackEvent("nest.create", { nestId: id, userId });
|
|
689
|
-
return db.
|
|
729
|
+
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
690
730
|
}
|
|
691
731
|
async function importNest(userId, name, files) {
|
|
692
732
|
const nm = (name || "").trim();
|
|
@@ -709,35 +749,46 @@ async function importNest(userId, name, files) {
|
|
|
709
749
|
discardImportDir(dest);
|
|
710
750
|
throw new ValidationError("Failed to import folder");
|
|
711
751
|
}
|
|
712
|
-
db.
|
|
713
|
-
"INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)"
|
|
714
|
-
|
|
752
|
+
await db.run(
|
|
753
|
+
"INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
|
|
754
|
+
[id, userId, nm, slug, null, visibility]
|
|
755
|
+
);
|
|
715
756
|
trackEvent("nest.import", { nestId: id, userId });
|
|
716
|
-
return db.
|
|
757
|
+
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
717
758
|
}
|
|
718
|
-
function listNests(userId) {
|
|
759
|
+
async function listNests(userId) {
|
|
719
760
|
const db = getDb();
|
|
720
761
|
if (userId === ANON_USER_ID) {
|
|
721
|
-
return db.
|
|
762
|
+
return await db.all(
|
|
763
|
+
"SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
|
|
764
|
+
[ANON_USER_ID]
|
|
765
|
+
);
|
|
722
766
|
}
|
|
723
|
-
const includeAnon = config.AUTH_MODE === "open" || isLicenseAdminUserId(userId);
|
|
767
|
+
const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
|
|
724
768
|
if (!includeAnon) {
|
|
725
|
-
return db.
|
|
769
|
+
return await db.all(
|
|
770
|
+
"SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
|
|
771
|
+
[userId]
|
|
772
|
+
);
|
|
726
773
|
}
|
|
727
|
-
return db.
|
|
728
|
-
"SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC"
|
|
729
|
-
|
|
774
|
+
return await db.all(
|
|
775
|
+
"SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC",
|
|
776
|
+
[userId, ANON_USER_ID]
|
|
777
|
+
);
|
|
730
778
|
}
|
|
731
|
-
function listSharedNests(userId) {
|
|
779
|
+
async function listSharedNests(userId) {
|
|
732
780
|
const db = getDb();
|
|
733
|
-
const caller = db.
|
|
734
|
-
|
|
781
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
782
|
+
userId
|
|
783
|
+
]);
|
|
784
|
+
const isServerAdmin = await isLicenseAdminUserId(userId) || (caller?.email ? isSuperAdmin(caller.email) : false);
|
|
735
785
|
if (isServerAdmin) {
|
|
736
|
-
return db.
|
|
737
|
-
"SELECT * FROM nests WHERE user_id != ? ORDER BY created_at DESC"
|
|
738
|
-
|
|
786
|
+
return await db.all(
|
|
787
|
+
"SELECT * FROM nests WHERE user_id != ? ORDER BY created_at DESC",
|
|
788
|
+
[userId]
|
|
789
|
+
);
|
|
739
790
|
}
|
|
740
|
-
return db.
|
|
791
|
+
return await db.all(
|
|
741
792
|
`SELECT DISTINCT n.* FROM nests n
|
|
742
793
|
LEFT JOIN nest_collaborators nc
|
|
743
794
|
ON nc.nest_id = n.id AND nc.user_id = ?
|
|
@@ -747,12 +798,13 @@ function listSharedNests(userId) {
|
|
|
747
798
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
748
799
|
WHERE n.user_id != ?
|
|
749
800
|
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
|
|
750
|
-
ORDER BY n.created_at DESC
|
|
751
|
-
|
|
801
|
+
ORDER BY n.created_at DESC`,
|
|
802
|
+
[userId, userId, userId]
|
|
803
|
+
);
|
|
752
804
|
}
|
|
753
|
-
function listPublicNests(userId) {
|
|
805
|
+
async function listPublicNests(userId) {
|
|
754
806
|
const db = getDb();
|
|
755
|
-
return db.
|
|
807
|
+
return await db.all(
|
|
756
808
|
`SELECT n.* FROM nests n
|
|
757
809
|
WHERE n.visibility = 'public'
|
|
758
810
|
AND n.user_id != ?
|
|
@@ -760,29 +812,30 @@ function listPublicNests(userId) {
|
|
|
760
812
|
SELECT 1 FROM nest_collaborators nc
|
|
761
813
|
WHERE nc.nest_id = n.id AND nc.user_id = ?
|
|
762
814
|
)
|
|
763
|
-
ORDER BY n.created_at DESC
|
|
764
|
-
|
|
815
|
+
ORDER BY n.created_at DESC`,
|
|
816
|
+
[userId, userId]
|
|
817
|
+
);
|
|
765
818
|
}
|
|
766
|
-
function getNest(nestId) {
|
|
819
|
+
async function getNest(nestId) {
|
|
767
820
|
const db = getDb();
|
|
768
|
-
return db.
|
|
821
|
+
return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]) || null;
|
|
769
822
|
}
|
|
770
823
|
async function deleteNest(nestId) {
|
|
771
824
|
const db = getDb();
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
825
|
+
await db.transaction(async (tx) => {
|
|
826
|
+
const id = nestId;
|
|
827
|
+
await tx.run("DELETE FROM approved_versions WHERE nest_id = ?", [id]);
|
|
828
|
+
await tx.run("DELETE FROM node_versions WHERE nest_id = ?", [id]);
|
|
829
|
+
await tx.run("DELETE FROM review_requests WHERE nest_id = ?", [id]);
|
|
830
|
+
await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id]);
|
|
831
|
+
await tx.run("DELETE FROM nest_collaborators WHERE nest_id = ?", [id]);
|
|
778
832
|
try {
|
|
779
|
-
|
|
833
|
+
await tx.run("DELETE FROM node_tag_index WHERE nest_id = ?", [id]);
|
|
780
834
|
} catch {
|
|
781
835
|
}
|
|
782
|
-
|
|
783
|
-
|
|
836
|
+
await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
|
|
837
|
+
await tx.run("DELETE FROM nests WHERE id = ?", [id]);
|
|
784
838
|
});
|
|
785
|
-
wipe(nestId);
|
|
786
839
|
const path = resolveNestPath(nestId);
|
|
787
840
|
try {
|
|
788
841
|
rmSync2(path, { recursive: true, force: true });
|
|
@@ -840,11 +893,11 @@ var FlatNestStorage = class extends NestStorage2 {
|
|
|
840
893
|
// src/nodes/engine.ts
|
|
841
894
|
var NestEngineCache = class {
|
|
842
895
|
cache = /* @__PURE__ */ new Map();
|
|
843
|
-
get(nestId) {
|
|
896
|
+
async get(nestId) {
|
|
844
897
|
let engine = this.cache.get(nestId);
|
|
845
898
|
if (!engine) {
|
|
846
899
|
const nestPath = resolveNestPath(nestId);
|
|
847
|
-
const storage = isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath);
|
|
900
|
+
const storage = await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath);
|
|
848
901
|
const query = new GraphQueryEngine(storage);
|
|
849
902
|
const versions = new VersionManager(storage);
|
|
850
903
|
engine = { storage, query, versions };
|
|
@@ -861,7 +914,7 @@ var engineCache = new NestEngineCache();
|
|
|
861
914
|
// src/governance/title-resolver.ts
|
|
862
915
|
async function buildTitleMap(nestId) {
|
|
863
916
|
try {
|
|
864
|
-
const { storage } = engineCache.get(nestId);
|
|
917
|
+
const { storage } = await engineCache.get(nestId);
|
|
865
918
|
const docs = await storage.discoverDocuments();
|
|
866
919
|
return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
|
|
867
920
|
} catch {
|
|
@@ -877,30 +930,32 @@ var PERMISSION_LEVELS = {
|
|
|
877
930
|
admin: 3,
|
|
878
931
|
owner: 4
|
|
879
932
|
};
|
|
880
|
-
function resolveNestPermission(nestId, userId) {
|
|
933
|
+
async function resolveNestPermission(nestId, userId) {
|
|
881
934
|
const db = getDb();
|
|
882
|
-
const nest = db.
|
|
935
|
+
const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
|
|
883
936
|
if (!nest) return "none";
|
|
884
937
|
if (nest.user_id === userId) return "owner";
|
|
885
|
-
if (nest.user_id === ANON_USER_ID && isLicenseAdminUserId(userId)) {
|
|
938
|
+
if (nest.user_id === ANON_USER_ID && await isLicenseAdminUserId(userId)) {
|
|
886
939
|
return "owner";
|
|
887
940
|
}
|
|
888
|
-
if (isLicenseAdminUserId(userId)) return "admin";
|
|
889
|
-
const caller = db.
|
|
941
|
+
if (await isLicenseAdminUserId(userId)) return "admin";
|
|
942
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
890
943
|
if (caller?.email && isSuperAdmin(caller.email)) {
|
|
891
944
|
return "admin";
|
|
892
945
|
}
|
|
893
|
-
const directGrant = db.
|
|
894
|
-
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
895
|
-
|
|
946
|
+
const directGrant = await db.get(
|
|
947
|
+
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
948
|
+
[nestId, userId]
|
|
949
|
+
);
|
|
896
950
|
if (directGrant) return directGrant.permission;
|
|
897
|
-
const stewardGrant = db.
|
|
951
|
+
const stewardGrant = await db.get(
|
|
898
952
|
`SELECT 1 FROM stewards s
|
|
899
953
|
JOIN users u ON u.id = ?
|
|
900
954
|
WHERE s.nest_id = ? AND s.is_active = 1
|
|
901
955
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
902
|
-
LIMIT 1
|
|
903
|
-
|
|
956
|
+
LIMIT 1`,
|
|
957
|
+
[userId, nestId]
|
|
958
|
+
);
|
|
904
959
|
if (stewardGrant) return "read";
|
|
905
960
|
if (nest.visibility === "public") return "read";
|
|
906
961
|
return "none";
|
|
@@ -908,26 +963,28 @@ function resolveNestPermission(nestId, userId) {
|
|
|
908
963
|
function permissionLevel(p) {
|
|
909
964
|
return PERMISSION_LEVELS[p] ?? 0;
|
|
910
965
|
}
|
|
911
|
-
function isPublicReader(nestId, userId) {
|
|
966
|
+
async function isPublicReader(nestId, userId) {
|
|
912
967
|
const db = getDb();
|
|
913
|
-
const nest = db.
|
|
968
|
+
const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
|
|
914
969
|
if (!nest) return false;
|
|
915
970
|
if (nest.visibility !== "public") return false;
|
|
916
971
|
if (nest.user_id === userId) return false;
|
|
917
|
-
const directGrant = db.
|
|
918
|
-
"SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
919
|
-
|
|
972
|
+
const directGrant = await db.get(
|
|
973
|
+
"SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
974
|
+
[nestId, userId]
|
|
975
|
+
);
|
|
920
976
|
if (directGrant) return false;
|
|
921
|
-
if (isLicenseAdminUserId(userId)) return false;
|
|
922
|
-
const caller = db.
|
|
977
|
+
if (await isLicenseAdminUserId(userId)) return false;
|
|
978
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
923
979
|
if (caller?.email && isSuperAdmin(caller.email)) return false;
|
|
924
|
-
const stewardGrant = db.
|
|
980
|
+
const stewardGrant = await db.get(
|
|
925
981
|
`SELECT 1 FROM stewards s
|
|
926
982
|
JOIN users u ON u.id = ?
|
|
927
983
|
WHERE s.nest_id = ? AND s.is_active = 1
|
|
928
984
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
929
|
-
LIMIT 1
|
|
930
|
-
|
|
985
|
+
LIMIT 1`,
|
|
986
|
+
[userId, nestId]
|
|
987
|
+
);
|
|
931
988
|
if (stewardGrant) return false;
|
|
932
989
|
return true;
|
|
933
990
|
}
|
|
@@ -950,7 +1007,6 @@ function collabPermToRole(permission) {
|
|
|
950
1007
|
var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
|
|
951
1008
|
var canViewWith = (roles) => roles.length > 0;
|
|
952
1009
|
var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
|
|
953
|
-
var canManageWith = (roles) => includesAny(roles, ["owner", "admin"]);
|
|
954
1010
|
var PRECEDENCE = [
|
|
955
1011
|
"owner",
|
|
956
1012
|
"admin",
|
|
@@ -964,35 +1020,39 @@ function primaryRole(roles) {
|
|
|
964
1020
|
}
|
|
965
1021
|
|
|
966
1022
|
// src/governance/stewardship-service.ts
|
|
967
|
-
function assignSteward(data) {
|
|
1023
|
+
async function assignSteward(data) {
|
|
968
1024
|
const db = getDb();
|
|
969
1025
|
const id = uuid2();
|
|
970
|
-
db.
|
|
1026
|
+
await db.run(
|
|
971
1027
|
`INSERT INTO stewards
|
|
972
1028
|
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
|
|
973
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1029
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1030
|
+
[
|
|
1031
|
+
id,
|
|
1032
|
+
data.nestId,
|
|
1033
|
+
data.scope,
|
|
1034
|
+
data.nodePattern || null,
|
|
1035
|
+
data.tagName || null,
|
|
1036
|
+
data.userEmail,
|
|
1037
|
+
data.userId || null,
|
|
1038
|
+
data.role,
|
|
1039
|
+
data.assignedBy,
|
|
1040
|
+
data.isActive ? 1 : 0
|
|
1041
|
+
]
|
|
985
1042
|
);
|
|
986
1043
|
return { ...data, id };
|
|
987
1044
|
}
|
|
988
|
-
function removeSteward(id) {
|
|
1045
|
+
async function removeSteward(id) {
|
|
989
1046
|
const db = getDb();
|
|
990
|
-
db.
|
|
1047
|
+
await db.run("DELETE FROM stewards WHERE id = ?", [id]);
|
|
991
1048
|
}
|
|
992
1049
|
var VALID_STEWARD_ROLES = ["editor", "reviewer", "viewer"];
|
|
993
|
-
function updateSteward(id, update) {
|
|
1050
|
+
async function updateSteward(id, update) {
|
|
994
1051
|
const db = getDb();
|
|
995
|
-
const current = db.
|
|
1052
|
+
const current = await db.get(
|
|
1053
|
+
"SELECT * FROM stewards WHERE id = ? AND is_active = 1",
|
|
1054
|
+
[id]
|
|
1055
|
+
);
|
|
996
1056
|
if (!current) {
|
|
997
1057
|
throw new ValidationError("Steward not found");
|
|
998
1058
|
}
|
|
@@ -1023,13 +1083,14 @@ function updateSteward(id, update) {
|
|
|
1023
1083
|
case "nest":
|
|
1024
1084
|
break;
|
|
1025
1085
|
}
|
|
1026
|
-
const dup = db.
|
|
1086
|
+
const dup = await db.get(
|
|
1027
1087
|
`SELECT role FROM stewards
|
|
1028
1088
|
WHERE nest_id = ? AND is_active = 1 AND id != ? AND scope = ?
|
|
1029
1089
|
AND user_email = ?
|
|
1030
1090
|
AND COALESCE(node_pattern, '') = COALESCE(?, '')
|
|
1031
|
-
AND COALESCE(tag_name, '') = COALESCE(?, '')
|
|
1032
|
-
|
|
1091
|
+
AND COALESCE(tag_name, '') = COALESCE(?, '')`,
|
|
1092
|
+
[current.nest_id, id, scope, current.user_email, nodePattern, tagName]
|
|
1093
|
+
);
|
|
1033
1094
|
if (dup) {
|
|
1034
1095
|
const scopeLabel = scope === "document" ? `document "${nodePattern}"` : scope === "tag" ? `tag "#${tagName}"` : "this nest";
|
|
1035
1096
|
throw new ConflictError(
|
|
@@ -1037,26 +1098,30 @@ function updateSteward(id, update) {
|
|
|
1037
1098
|
);
|
|
1038
1099
|
}
|
|
1039
1100
|
}
|
|
1040
|
-
db.
|
|
1041
|
-
"UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?"
|
|
1042
|
-
|
|
1043
|
-
|
|
1101
|
+
await db.run(
|
|
1102
|
+
"UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?",
|
|
1103
|
+
[role, scope, nodePattern, tagName, id]
|
|
1104
|
+
);
|
|
1105
|
+
const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
|
|
1044
1106
|
return rowToSteward(row);
|
|
1045
1107
|
}
|
|
1046
|
-
function updateStewardRole(id, role) {
|
|
1108
|
+
async function updateStewardRole(id, role) {
|
|
1047
1109
|
return updateSteward(id, { role });
|
|
1048
1110
|
}
|
|
1049
|
-
function getSteward(id) {
|
|
1111
|
+
async function getSteward(id) {
|
|
1050
1112
|
const db = getDb();
|
|
1051
|
-
const row = db.
|
|
1113
|
+
const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
|
|
1052
1114
|
return row ? rowToSteward(row) : null;
|
|
1053
1115
|
}
|
|
1054
1116
|
async function getStewardsForNest(nestId) {
|
|
1055
1117
|
const db = getDb();
|
|
1056
|
-
const rows = db.
|
|
1118
|
+
const rows = await db.all(
|
|
1119
|
+
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1",
|
|
1120
|
+
[nestId]
|
|
1121
|
+
);
|
|
1057
1122
|
return enrichWithTitles(nestId, rows.map(rowToSteward));
|
|
1058
1123
|
}
|
|
1059
|
-
function getStewardsForScope(params) {
|
|
1124
|
+
async function getStewardsForScope(params) {
|
|
1060
1125
|
const db = getDb();
|
|
1061
1126
|
let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
|
|
1062
1127
|
const args = [params.nestId];
|
|
@@ -1068,7 +1133,7 @@ function getStewardsForScope(params) {
|
|
|
1068
1133
|
sql += " AND (node_pattern = ? OR tag_name = ?)";
|
|
1069
1134
|
args.push(params.scopeTarget, params.scopeTarget);
|
|
1070
1135
|
}
|
|
1071
|
-
return db.
|
|
1136
|
+
return (await db.all(sql, args)).map(rowToSteward);
|
|
1072
1137
|
}
|
|
1073
1138
|
async function listStewards(params) {
|
|
1074
1139
|
const db = getDb();
|
|
@@ -1084,7 +1149,7 @@ async function listStewards(params) {
|
|
|
1084
1149
|
args.push(like, like, like);
|
|
1085
1150
|
}
|
|
1086
1151
|
sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
|
|
1087
|
-
const stewards = db.
|
|
1152
|
+
const stewards = (await db.all(sql, args)).map(rowToSteward);
|
|
1088
1153
|
return enrichWithTitles(params.nestId, stewards);
|
|
1089
1154
|
}
|
|
1090
1155
|
async function enrichWithTitles(nestId, stewards) {
|
|
@@ -1118,7 +1183,7 @@ async function createStewardRecord(params) {
|
|
|
1118
1183
|
const db = getDb();
|
|
1119
1184
|
const results = [];
|
|
1120
1185
|
const actor = (params.assignedBy || "").trim().toLowerCase();
|
|
1121
|
-
const ownerEmail = (getNestOwnerEmail(params.nestId) || "").toLowerCase();
|
|
1186
|
+
const ownerEmail = (await getNestOwnerEmail(params.nestId) || "").toLowerCase();
|
|
1122
1187
|
for (const user of params.users) {
|
|
1123
1188
|
const email = user.email.trim().toLowerCase();
|
|
1124
1189
|
if (!email) continue;
|
|
@@ -1132,17 +1197,18 @@ async function createStewardRecord(params) {
|
|
|
1132
1197
|
"The nest owner already has full access and doesn't need a steward role."
|
|
1133
1198
|
);
|
|
1134
1199
|
}
|
|
1135
|
-
const existing = db.
|
|
1200
|
+
const existing = await db.get(
|
|
1136
1201
|
`SELECT * FROM stewards
|
|
1137
1202
|
WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
|
|
1138
1203
|
AND COALESCE(node_pattern, '') = COALESCE(?, '')
|
|
1139
|
-
AND COALESCE(tag_name, '') = COALESCE(?, '')
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1204
|
+
AND COALESCE(tag_name, '') = COALESCE(?, '')`,
|
|
1205
|
+
[
|
|
1206
|
+
params.nestId,
|
|
1207
|
+
params.scope,
|
|
1208
|
+
email,
|
|
1209
|
+
nodePattern ?? null,
|
|
1210
|
+
tagName ?? null
|
|
1211
|
+
]
|
|
1146
1212
|
);
|
|
1147
1213
|
if (existing) {
|
|
1148
1214
|
const scopeLabel = params.scope === "document" ? `document "${nodePattern}"` : params.scope === "tag" ? `tag "#${tagName}"` : "this nest";
|
|
@@ -1150,8 +1216,11 @@ async function createStewardRecord(params) {
|
|
|
1150
1216
|
`"${email}" is already a steward of ${scopeLabel} with the "${existing.role}" role. Remove the existing assignment first to change the role.`
|
|
1151
1217
|
);
|
|
1152
1218
|
}
|
|
1153
|
-
const userRow = db.
|
|
1154
|
-
|
|
1219
|
+
const userRow = await db.get(
|
|
1220
|
+
"SELECT id FROM users WHERE email = ?",
|
|
1221
|
+
[email]
|
|
1222
|
+
);
|
|
1223
|
+
const created = await assignSteward({
|
|
1155
1224
|
nestId: params.nestId,
|
|
1156
1225
|
scope: params.scope,
|
|
1157
1226
|
nodePattern,
|
|
@@ -1165,20 +1234,21 @@ async function createStewardRecord(params) {
|
|
|
1165
1234
|
});
|
|
1166
1235
|
results.push(created);
|
|
1167
1236
|
}
|
|
1168
|
-
db.
|
|
1169
|
-
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0"
|
|
1170
|
-
|
|
1237
|
+
await db.run(
|
|
1238
|
+
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
|
|
1239
|
+
[params.nestId]
|
|
1240
|
+
);
|
|
1171
1241
|
return results;
|
|
1172
1242
|
}
|
|
1173
|
-
function resolveStewardsForNode(nestId, nodeId) {
|
|
1174
|
-
return resolve(nestId, nodeId).stewards;
|
|
1243
|
+
async function resolveStewardsForNode(nestId, nodeId) {
|
|
1244
|
+
return (await resolve(nestId, nodeId)).stewards;
|
|
1175
1245
|
}
|
|
1176
|
-
function resolveStewardsWithFallback(nestId, nodeId) {
|
|
1246
|
+
async function resolveStewardsWithFallback(nestId, nodeId) {
|
|
1177
1247
|
return resolve(nestId, nodeId);
|
|
1178
1248
|
}
|
|
1179
|
-
function resolve(nestId, nodeId) {
|
|
1249
|
+
async function resolve(nestId, nodeId) {
|
|
1180
1250
|
const db = getDb();
|
|
1181
|
-
const rows = db.
|
|
1251
|
+
const rows = await db.all(
|
|
1182
1252
|
`
|
|
1183
1253
|
SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
|
|
1184
1254
|
FROM stewards s
|
|
@@ -1197,16 +1267,17 @@ function resolve(nestId, nodeId) {
|
|
|
1197
1267
|
FROM stewards s
|
|
1198
1268
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
|
|
1199
1269
|
ORDER BY priority ASC, user_email ASC
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1270
|
+
`,
|
|
1271
|
+
[
|
|
1272
|
+
nestId,
|
|
1273
|
+
nodeId,
|
|
1274
|
+
// document branch
|
|
1275
|
+
nestId,
|
|
1276
|
+
nodeId,
|
|
1277
|
+
// tag branch
|
|
1278
|
+
nestId
|
|
1279
|
+
// nest branch
|
|
1280
|
+
]
|
|
1210
1281
|
);
|
|
1211
1282
|
const resolved = rows.map((row) => ({
|
|
1212
1283
|
steward: rowToSteward(row),
|
|
@@ -1216,89 +1287,98 @@ function resolve(nestId, nodeId) {
|
|
|
1216
1287
|
if (resolved.length > 0) {
|
|
1217
1288
|
return { stewards: resolved, fallbackToOwner: false };
|
|
1218
1289
|
}
|
|
1219
|
-
const owner = db.
|
|
1290
|
+
const owner = await db.get(
|
|
1220
1291
|
`SELECT u.email FROM nests n
|
|
1221
1292
|
JOIN users u ON u.id = n.user_id
|
|
1222
|
-
WHERE n.id =
|
|
1223
|
-
|
|
1293
|
+
WHERE n.id = ?`,
|
|
1294
|
+
[nestId]
|
|
1295
|
+
);
|
|
1224
1296
|
return {
|
|
1225
1297
|
stewards: [],
|
|
1226
1298
|
fallbackToOwner: true,
|
|
1227
1299
|
ownerEmail: owner?.email
|
|
1228
1300
|
};
|
|
1229
1301
|
}
|
|
1230
|
-
function userIdForEmail(email) {
|
|
1302
|
+
async function userIdForEmail(email) {
|
|
1231
1303
|
const db = getDb();
|
|
1232
|
-
const row = db.
|
|
1304
|
+
const row = await db.get(
|
|
1305
|
+
"SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
|
|
1306
|
+
[email]
|
|
1307
|
+
);
|
|
1233
1308
|
return row?.id;
|
|
1234
1309
|
}
|
|
1235
|
-
function getStewardRolesForUser(nestId, userEmail) {
|
|
1310
|
+
async function getStewardRolesForUser(nestId, userEmail) {
|
|
1236
1311
|
const db = getDb();
|
|
1237
|
-
const rows = db.
|
|
1238
|
-
"SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)"
|
|
1239
|
-
|
|
1312
|
+
const rows = await db.all(
|
|
1313
|
+
"SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
|
|
1314
|
+
[nestId, userEmail]
|
|
1315
|
+
);
|
|
1240
1316
|
return rows.map((r) => r.role);
|
|
1241
1317
|
}
|
|
1242
|
-
function getStewardsForUser(nestId, userEmail) {
|
|
1318
|
+
async function getStewardsForUser(nestId, userEmail) {
|
|
1243
1319
|
const db = getDb();
|
|
1244
|
-
const rows = db.
|
|
1245
|
-
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)"
|
|
1246
|
-
|
|
1320
|
+
const rows = await db.all(
|
|
1321
|
+
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
|
|
1322
|
+
[nestId, userEmail]
|
|
1323
|
+
);
|
|
1247
1324
|
return rows.map(rowToSteward);
|
|
1248
1325
|
}
|
|
1249
|
-
function getCollaboratorRole(nestId, userEmail) {
|
|
1250
|
-
const userId = userIdForEmail(userEmail);
|
|
1326
|
+
async function getCollaboratorRole(nestId, userEmail) {
|
|
1327
|
+
const userId = await userIdForEmail(userEmail);
|
|
1251
1328
|
if (!userId) return null;
|
|
1252
1329
|
const db = getDb();
|
|
1253
|
-
const row = db.
|
|
1254
|
-
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
1255
|
-
|
|
1330
|
+
const row = await db.get(
|
|
1331
|
+
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
1332
|
+
[nestId, userId]
|
|
1333
|
+
);
|
|
1256
1334
|
return row?.permission ?? null;
|
|
1257
1335
|
}
|
|
1258
|
-
function resolveUserRoles(nestId, userEmail, opts) {
|
|
1336
|
+
async function resolveUserRoles(nestId, userEmail, opts) {
|
|
1259
1337
|
const roles = /* @__PURE__ */ new Set();
|
|
1260
1338
|
if (isSuperAdmin(userEmail)) roles.add("admin");
|
|
1261
|
-
const owner = getNestOwnerEmail(nestId);
|
|
1339
|
+
const owner = await getNestOwnerEmail(nestId);
|
|
1262
1340
|
if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
|
|
1263
1341
|
roles.add("owner");
|
|
1264
1342
|
}
|
|
1265
|
-
const userId = userIdForEmail(userEmail);
|
|
1343
|
+
const userId = await userIdForEmail(userEmail);
|
|
1266
1344
|
if (userId) {
|
|
1267
|
-
const collabRole = collabPermToRole(resolveNestPermission(nestId, userId));
|
|
1345
|
+
const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
|
|
1268
1346
|
if (collabRole) roles.add(collabRole);
|
|
1269
1347
|
}
|
|
1270
|
-
const stewardRoles = opts?.nodeId ? resolveStewardsForNode(nestId, opts.nodeId).filter(
|
|
1348
|
+
const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
|
|
1271
1349
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
|
|
1272
|
-
).map((r) => r.steward.role) : getStewardRolesForUser(nestId, userEmail);
|
|
1350
|
+
).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
|
|
1273
1351
|
for (const role of stewardRoles) roles.add(role);
|
|
1274
1352
|
return [...roles];
|
|
1275
1353
|
}
|
|
1276
|
-
function canManageStewards(
|
|
1354
|
+
async function canManageStewards(nestId, userId) {
|
|
1277
1355
|
if (config.AUTH_MODE === "open") return true;
|
|
1278
|
-
|
|
1356
|
+
const perm = await resolveNestPermission(nestId, userId);
|
|
1357
|
+
return perm === "owner" || perm === "admin";
|
|
1279
1358
|
}
|
|
1280
|
-
function canCreateInNest(nestId, userEmail) {
|
|
1359
|
+
async function canCreateInNest(nestId, userEmail) {
|
|
1281
1360
|
if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
|
|
1282
|
-
const userId = userIdForEmail(userEmail);
|
|
1361
|
+
const userId = await userIdForEmail(userEmail);
|
|
1283
1362
|
if (userId) {
|
|
1284
|
-
const perm = resolveNestPermission(nestId, userId);
|
|
1363
|
+
const perm = await resolveNestPermission(nestId, userId);
|
|
1285
1364
|
if (perm === "owner" || perm === "admin" || perm === "write") return true;
|
|
1286
1365
|
}
|
|
1287
|
-
return getStewardsForUser(nestId, userEmail).some(
|
|
1366
|
+
return (await getStewardsForUser(nestId, userEmail)).some(
|
|
1288
1367
|
(s) => s.role === "editor" && s.scope === "nest"
|
|
1289
1368
|
);
|
|
1290
1369
|
}
|
|
1291
|
-
function getNestOwnerEmail(nestId) {
|
|
1370
|
+
async function getNestOwnerEmail(nestId) {
|
|
1292
1371
|
const db = getDb();
|
|
1293
|
-
const row = db.
|
|
1372
|
+
const row = await db.get(
|
|
1294
1373
|
`SELECT u.email FROM nests n
|
|
1295
1374
|
JOIN users u ON u.id = n.user_id
|
|
1296
|
-
WHERE n.id =
|
|
1297
|
-
|
|
1375
|
+
WHERE n.id = ?`,
|
|
1376
|
+
[nestId]
|
|
1377
|
+
);
|
|
1298
1378
|
return row?.email ?? null;
|
|
1299
1379
|
}
|
|
1300
|
-
function canUserEdit(nestId, nodeId, userEmail) {
|
|
1301
|
-
const roles = resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1380
|
+
async function canUserEdit(nestId, nodeId, userEmail) {
|
|
1381
|
+
const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1302
1382
|
if (roles.includes("owner")) {
|
|
1303
1383
|
return { allowed: true, reason: "nest owner", role: "owner" };
|
|
1304
1384
|
}
|
|
@@ -1314,31 +1394,33 @@ function canUserEdit(nestId, nodeId, userEmail) {
|
|
|
1314
1394
|
}
|
|
1315
1395
|
return { allowed: false, reason: "no editor role on this node", role: null };
|
|
1316
1396
|
}
|
|
1317
|
-
function getCurrentVersionAuthor(nestId, nodeId) {
|
|
1397
|
+
async function getCurrentVersionAuthor(nestId, nodeId) {
|
|
1318
1398
|
const db = getDb();
|
|
1319
|
-
const row = db.
|
|
1399
|
+
const row = await db.get(
|
|
1320
1400
|
`SELECT author FROM node_versions
|
|
1321
1401
|
WHERE nest_id = ? AND node_id = ?
|
|
1322
|
-
ORDER BY version DESC LIMIT 1
|
|
1323
|
-
|
|
1402
|
+
ORDER BY version DESC LIMIT 1`,
|
|
1403
|
+
[nestId, nodeId]
|
|
1404
|
+
);
|
|
1324
1405
|
return row?.author ?? null;
|
|
1325
1406
|
}
|
|
1326
|
-
function getPendingReviewRequester(nestId, nodeId) {
|
|
1407
|
+
async function getPendingReviewRequester(nestId, nodeId) {
|
|
1327
1408
|
const db = getDb();
|
|
1328
|
-
const row = db.
|
|
1409
|
+
const row = await db.get(
|
|
1329
1410
|
`SELECT requested_by FROM review_requests
|
|
1330
1411
|
WHERE nest_id = ? AND node_id = ? AND status = 'pending'
|
|
1331
|
-
ORDER BY requested_at DESC LIMIT 1
|
|
1332
|
-
|
|
1412
|
+
ORDER BY requested_at DESC LIMIT 1`,
|
|
1413
|
+
[nestId, nodeId]
|
|
1414
|
+
);
|
|
1333
1415
|
return row?.requested_by ?? null;
|
|
1334
1416
|
}
|
|
1335
|
-
function canUserApprove(nestId, nodeId, userEmail) {
|
|
1336
|
-
const roles = resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1417
|
+
async function canUserApprove(nestId, nodeId, userEmail) {
|
|
1418
|
+
const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1337
1419
|
const isOwner = roles.includes("owner");
|
|
1338
1420
|
const isSuper = isSuperAdmin(userEmail);
|
|
1339
|
-
const allowSelf = nestAllowsSelfApprove(nestId);
|
|
1421
|
+
const allowSelf = await nestAllowsSelfApprove(nestId);
|
|
1340
1422
|
const hasStewardApprove = roles.includes("admin") || roles.includes("reviewer");
|
|
1341
|
-
const actor = getPendingReviewRequester(nestId, nodeId) ?? getCurrentVersionAuthor(nestId, nodeId);
|
|
1423
|
+
const actor = await getPendingReviewRequester(nestId, nodeId) ?? await getCurrentVersionAuthor(nestId, nodeId);
|
|
1342
1424
|
const isOwnSubmission = !!actor && actor.toLowerCase() === userEmail.toLowerCase();
|
|
1343
1425
|
if (hasStewardApprove) {
|
|
1344
1426
|
if (isOwnSubmission && !((isOwner || isSuper) && allowSelf)) {
|
|
@@ -1375,8 +1457,8 @@ function canUserApprove(nestId, nodeId, userEmail) {
|
|
|
1375
1457
|
role: held
|
|
1376
1458
|
};
|
|
1377
1459
|
}
|
|
1378
|
-
function canUserAccess(nestId, nodeId, userEmail) {
|
|
1379
|
-
const roles = resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1460
|
+
async function canUserAccess(nestId, nodeId, userEmail) {
|
|
1461
|
+
const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
|
|
1380
1462
|
if (roles.includes("owner")) {
|
|
1381
1463
|
return { allowed: true, reason: "nest owner", role: "owner" };
|
|
1382
1464
|
}
|
|
@@ -1392,21 +1474,26 @@ function canUserAccess(nestId, nodeId, userEmail) {
|
|
|
1392
1474
|
}
|
|
1393
1475
|
return { allowed: false, reason: "no collaborator or steward access", role: null };
|
|
1394
1476
|
}
|
|
1395
|
-
function syncFromConfig(nestId, config2) {
|
|
1477
|
+
async function syncFromConfig(nestId, config2) {
|
|
1396
1478
|
const db = getDb();
|
|
1397
1479
|
let count = 0;
|
|
1398
|
-
db.
|
|
1399
|
-
"UPDATE stewards SET is_active = 0 WHERE nest_id = ?"
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1480
|
+
await db.run(
|
|
1481
|
+
"UPDATE stewards SET is_active = 0 WHERE nest_id = ?",
|
|
1482
|
+
[nestId]
|
|
1483
|
+
);
|
|
1484
|
+
await db.run(
|
|
1485
|
+
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ?",
|
|
1486
|
+
[nestId]
|
|
1487
|
+
);
|
|
1488
|
+
const addEntries = async (scope, entries, target) => {
|
|
1405
1489
|
for (const entry of entries) {
|
|
1406
|
-
const user = db.
|
|
1490
|
+
const user = await db.get(
|
|
1491
|
+
"SELECT id FROM users WHERE email = ?",
|
|
1492
|
+
[entry.email]
|
|
1493
|
+
);
|
|
1407
1494
|
const rawRole = entry.role || "reviewer";
|
|
1408
1495
|
const role = rawRole === "admin" ? "reviewer" : rawRole;
|
|
1409
|
-
assignSteward({
|
|
1496
|
+
await assignSteward({
|
|
1410
1497
|
nestId,
|
|
1411
1498
|
scope,
|
|
1412
1499
|
nodePattern: target?.nodePattern,
|
|
@@ -1422,16 +1509,16 @@ function syncFromConfig(nestId, config2) {
|
|
|
1422
1509
|
}
|
|
1423
1510
|
};
|
|
1424
1511
|
if (config2.nest) {
|
|
1425
|
-
addEntries("nest", config2.nest);
|
|
1512
|
+
await addEntries("nest", config2.nest);
|
|
1426
1513
|
}
|
|
1427
1514
|
if (config2.tags) {
|
|
1428
1515
|
for (const [tagName, entries] of Object.entries(config2.tags)) {
|
|
1429
|
-
addEntries("tag", entries, { tagName });
|
|
1516
|
+
await addEntries("tag", entries, { tagName });
|
|
1430
1517
|
}
|
|
1431
1518
|
}
|
|
1432
1519
|
if (config2.documents) {
|
|
1433
1520
|
for (const [docPattern, entries] of Object.entries(config2.documents)) {
|
|
1434
|
-
addEntries("document", entries, { nodePattern: docPattern });
|
|
1521
|
+
await addEntries("document", entries, { nodePattern: docPattern });
|
|
1435
1522
|
}
|
|
1436
1523
|
}
|
|
1437
1524
|
return count;
|
|
@@ -1475,6 +1562,8 @@ export {
|
|
|
1475
1562
|
resolveNestPermission,
|
|
1476
1563
|
permissionLevel,
|
|
1477
1564
|
isPublicReader,
|
|
1565
|
+
nestStorageRoot,
|
|
1566
|
+
resolveNestPath,
|
|
1478
1567
|
uniqueNestName,
|
|
1479
1568
|
isStewardshipEnabled,
|
|
1480
1569
|
setStewardshipEnabled,
|
|
@@ -1491,7 +1580,7 @@ export {
|
|
|
1491
1580
|
deleteNest,
|
|
1492
1581
|
engineCache,
|
|
1493
1582
|
buildTitleMap,
|
|
1494
|
-
|
|
1583
|
+
collabPermToRole,
|
|
1495
1584
|
assignSteward,
|
|
1496
1585
|
removeSteward,
|
|
1497
1586
|
updateSteward,
|