@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
|
@@ -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-7V33Z6CS.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
|
}
|
|
@@ -964,35 +1021,39 @@ function primaryRole(roles) {
|
|
|
964
1021
|
}
|
|
965
1022
|
|
|
966
1023
|
// src/governance/stewardship-service.ts
|
|
967
|
-
function assignSteward(data) {
|
|
1024
|
+
async function assignSteward(data) {
|
|
968
1025
|
const db = getDb();
|
|
969
1026
|
const id = uuid2();
|
|
970
|
-
db.
|
|
1027
|
+
await db.run(
|
|
971
1028
|
`INSERT INTO stewards
|
|
972
1029
|
(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
|
-
|
|
1030
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1031
|
+
[
|
|
1032
|
+
id,
|
|
1033
|
+
data.nestId,
|
|
1034
|
+
data.scope,
|
|
1035
|
+
data.nodePattern || null,
|
|
1036
|
+
data.tagName || null,
|
|
1037
|
+
data.userEmail,
|
|
1038
|
+
data.userId || null,
|
|
1039
|
+
data.role,
|
|
1040
|
+
data.assignedBy,
|
|
1041
|
+
data.isActive ? 1 : 0
|
|
1042
|
+
]
|
|
985
1043
|
);
|
|
986
1044
|
return { ...data, id };
|
|
987
1045
|
}
|
|
988
|
-
function removeSteward(id) {
|
|
1046
|
+
async function removeSteward(id) {
|
|
989
1047
|
const db = getDb();
|
|
990
|
-
db.
|
|
1048
|
+
await db.run("DELETE FROM stewards WHERE id = ?", [id]);
|
|
991
1049
|
}
|
|
992
1050
|
var VALID_STEWARD_ROLES = ["editor", "reviewer", "viewer"];
|
|
993
|
-
function updateSteward(id, update) {
|
|
1051
|
+
async function updateSteward(id, update) {
|
|
994
1052
|
const db = getDb();
|
|
995
|
-
const current = db.
|
|
1053
|
+
const current = await db.get(
|
|
1054
|
+
"SELECT * FROM stewards WHERE id = ? AND is_active = 1",
|
|
1055
|
+
[id]
|
|
1056
|
+
);
|
|
996
1057
|
if (!current) {
|
|
997
1058
|
throw new ValidationError("Steward not found");
|
|
998
1059
|
}
|
|
@@ -1023,13 +1084,14 @@ function updateSteward(id, update) {
|
|
|
1023
1084
|
case "nest":
|
|
1024
1085
|
break;
|
|
1025
1086
|
}
|
|
1026
|
-
const dup = db.
|
|
1087
|
+
const dup = await db.get(
|
|
1027
1088
|
`SELECT role FROM stewards
|
|
1028
1089
|
WHERE nest_id = ? AND is_active = 1 AND id != ? AND scope = ?
|
|
1029
1090
|
AND user_email = ?
|
|
1030
1091
|
AND COALESCE(node_pattern, '') = COALESCE(?, '')
|
|
1031
|
-
AND COALESCE(tag_name, '') = COALESCE(?, '')
|
|
1032
|
-
|
|
1092
|
+
AND COALESCE(tag_name, '') = COALESCE(?, '')`,
|
|
1093
|
+
[current.nest_id, id, scope, current.user_email, nodePattern, tagName]
|
|
1094
|
+
);
|
|
1033
1095
|
if (dup) {
|
|
1034
1096
|
const scopeLabel = scope === "document" ? `document "${nodePattern}"` : scope === "tag" ? `tag "#${tagName}"` : "this nest";
|
|
1035
1097
|
throw new ConflictError(
|
|
@@ -1037,26 +1099,30 @@ function updateSteward(id, update) {
|
|
|
1037
1099
|
);
|
|
1038
1100
|
}
|
|
1039
1101
|
}
|
|
1040
|
-
db.
|
|
1041
|
-
"UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?"
|
|
1042
|
-
|
|
1043
|
-
|
|
1102
|
+
await db.run(
|
|
1103
|
+
"UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?",
|
|
1104
|
+
[role, scope, nodePattern, tagName, id]
|
|
1105
|
+
);
|
|
1106
|
+
const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
|
|
1044
1107
|
return rowToSteward(row);
|
|
1045
1108
|
}
|
|
1046
|
-
function updateStewardRole(id, role) {
|
|
1109
|
+
async function updateStewardRole(id, role) {
|
|
1047
1110
|
return updateSteward(id, { role });
|
|
1048
1111
|
}
|
|
1049
|
-
function getSteward(id) {
|
|
1112
|
+
async function getSteward(id) {
|
|
1050
1113
|
const db = getDb();
|
|
1051
|
-
const row = db.
|
|
1114
|
+
const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
|
|
1052
1115
|
return row ? rowToSteward(row) : null;
|
|
1053
1116
|
}
|
|
1054
1117
|
async function getStewardsForNest(nestId) {
|
|
1055
1118
|
const db = getDb();
|
|
1056
|
-
const rows = db.
|
|
1119
|
+
const rows = await db.all(
|
|
1120
|
+
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1",
|
|
1121
|
+
[nestId]
|
|
1122
|
+
);
|
|
1057
1123
|
return enrichWithTitles(nestId, rows.map(rowToSteward));
|
|
1058
1124
|
}
|
|
1059
|
-
function getStewardsForScope(params) {
|
|
1125
|
+
async function getStewardsForScope(params) {
|
|
1060
1126
|
const db = getDb();
|
|
1061
1127
|
let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
|
|
1062
1128
|
const args = [params.nestId];
|
|
@@ -1068,7 +1134,7 @@ function getStewardsForScope(params) {
|
|
|
1068
1134
|
sql += " AND (node_pattern = ? OR tag_name = ?)";
|
|
1069
1135
|
args.push(params.scopeTarget, params.scopeTarget);
|
|
1070
1136
|
}
|
|
1071
|
-
return db.
|
|
1137
|
+
return (await db.all(sql, args)).map(rowToSteward);
|
|
1072
1138
|
}
|
|
1073
1139
|
async function listStewards(params) {
|
|
1074
1140
|
const db = getDb();
|
|
@@ -1084,7 +1150,7 @@ async function listStewards(params) {
|
|
|
1084
1150
|
args.push(like, like, like);
|
|
1085
1151
|
}
|
|
1086
1152
|
sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
|
|
1087
|
-
const stewards = db.
|
|
1153
|
+
const stewards = (await db.all(sql, args)).map(rowToSteward);
|
|
1088
1154
|
return enrichWithTitles(params.nestId, stewards);
|
|
1089
1155
|
}
|
|
1090
1156
|
async function enrichWithTitles(nestId, stewards) {
|
|
@@ -1118,7 +1184,7 @@ async function createStewardRecord(params) {
|
|
|
1118
1184
|
const db = getDb();
|
|
1119
1185
|
const results = [];
|
|
1120
1186
|
const actor = (params.assignedBy || "").trim().toLowerCase();
|
|
1121
|
-
const ownerEmail = (getNestOwnerEmail(params.nestId) || "").toLowerCase();
|
|
1187
|
+
const ownerEmail = (await getNestOwnerEmail(params.nestId) || "").toLowerCase();
|
|
1122
1188
|
for (const user of params.users) {
|
|
1123
1189
|
const email = user.email.trim().toLowerCase();
|
|
1124
1190
|
if (!email) continue;
|
|
@@ -1132,17 +1198,18 @@ async function createStewardRecord(params) {
|
|
|
1132
1198
|
"The nest owner already has full access and doesn't need a steward role."
|
|
1133
1199
|
);
|
|
1134
1200
|
}
|
|
1135
|
-
const existing = db.
|
|
1201
|
+
const existing = await db.get(
|
|
1136
1202
|
`SELECT * FROM stewards
|
|
1137
1203
|
WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
|
|
1138
1204
|
AND COALESCE(node_pattern, '') = COALESCE(?, '')
|
|
1139
|
-
AND COALESCE(tag_name, '') = COALESCE(?, '')
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1205
|
+
AND COALESCE(tag_name, '') = COALESCE(?, '')`,
|
|
1206
|
+
[
|
|
1207
|
+
params.nestId,
|
|
1208
|
+
params.scope,
|
|
1209
|
+
email,
|
|
1210
|
+
nodePattern ?? null,
|
|
1211
|
+
tagName ?? null
|
|
1212
|
+
]
|
|
1146
1213
|
);
|
|
1147
1214
|
if (existing) {
|
|
1148
1215
|
const scopeLabel = params.scope === "document" ? `document "${nodePattern}"` : params.scope === "tag" ? `tag "#${tagName}"` : "this nest";
|
|
@@ -1150,8 +1217,11 @@ async function createStewardRecord(params) {
|
|
|
1150
1217
|
`"${email}" is already a steward of ${scopeLabel} with the "${existing.role}" role. Remove the existing assignment first to change the role.`
|
|
1151
1218
|
);
|
|
1152
1219
|
}
|
|
1153
|
-
const userRow = db.
|
|
1154
|
-
|
|
1220
|
+
const userRow = await db.get(
|
|
1221
|
+
"SELECT id FROM users WHERE email = ?",
|
|
1222
|
+
[email]
|
|
1223
|
+
);
|
|
1224
|
+
const created = await assignSteward({
|
|
1155
1225
|
nestId: params.nestId,
|
|
1156
1226
|
scope: params.scope,
|
|
1157
1227
|
nodePattern,
|
|
@@ -1165,20 +1235,21 @@ async function createStewardRecord(params) {
|
|
|
1165
1235
|
});
|
|
1166
1236
|
results.push(created);
|
|
1167
1237
|
}
|
|
1168
|
-
db.
|
|
1169
|
-
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0"
|
|
1170
|
-
|
|
1238
|
+
await db.run(
|
|
1239
|
+
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
|
|
1240
|
+
[params.nestId]
|
|
1241
|
+
);
|
|
1171
1242
|
return results;
|
|
1172
1243
|
}
|
|
1173
|
-
function resolveStewardsForNode(nestId, nodeId) {
|
|
1174
|
-
return resolve(nestId, nodeId).stewards;
|
|
1244
|
+
async function resolveStewardsForNode(nestId, nodeId) {
|
|
1245
|
+
return (await resolve(nestId, nodeId)).stewards;
|
|
1175
1246
|
}
|
|
1176
|
-
function resolveStewardsWithFallback(nestId, nodeId) {
|
|
1247
|
+
async function resolveStewardsWithFallback(nestId, nodeId) {
|
|
1177
1248
|
return resolve(nestId, nodeId);
|
|
1178
1249
|
}
|
|
1179
|
-
function resolve(nestId, nodeId) {
|
|
1250
|
+
async function resolve(nestId, nodeId) {
|
|
1180
1251
|
const db = getDb();
|
|
1181
|
-
const rows = db.
|
|
1252
|
+
const rows = await db.all(
|
|
1182
1253
|
`
|
|
1183
1254
|
SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
|
|
1184
1255
|
FROM stewards s
|
|
@@ -1197,16 +1268,17 @@ function resolve(nestId, nodeId) {
|
|
|
1197
1268
|
FROM stewards s
|
|
1198
1269
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
|
|
1199
1270
|
ORDER BY priority ASC, user_email ASC
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1271
|
+
`,
|
|
1272
|
+
[
|
|
1273
|
+
nestId,
|
|
1274
|
+
nodeId,
|
|
1275
|
+
// document branch
|
|
1276
|
+
nestId,
|
|
1277
|
+
nodeId,
|
|
1278
|
+
// tag branch
|
|
1279
|
+
nestId
|
|
1280
|
+
// nest branch
|
|
1281
|
+
]
|
|
1210
1282
|
);
|
|
1211
1283
|
const resolved = rows.map((row) => ({
|
|
1212
1284
|
steward: rowToSteward(row),
|
|
@@ -1216,60 +1288,67 @@ function resolve(nestId, nodeId) {
|
|
|
1216
1288
|
if (resolved.length > 0) {
|
|
1217
1289
|
return { stewards: resolved, fallbackToOwner: false };
|
|
1218
1290
|
}
|
|
1219
|
-
const owner = db.
|
|
1291
|
+
const owner = await db.get(
|
|
1220
1292
|
`SELECT u.email FROM nests n
|
|
1221
1293
|
JOIN users u ON u.id = n.user_id
|
|
1222
|
-
WHERE n.id =
|
|
1223
|
-
|
|
1294
|
+
WHERE n.id = ?`,
|
|
1295
|
+
[nestId]
|
|
1296
|
+
);
|
|
1224
1297
|
return {
|
|
1225
1298
|
stewards: [],
|
|
1226
1299
|
fallbackToOwner: true,
|
|
1227
1300
|
ownerEmail: owner?.email
|
|
1228
1301
|
};
|
|
1229
1302
|
}
|
|
1230
|
-
function userIdForEmail(email) {
|
|
1303
|
+
async function userIdForEmail(email) {
|
|
1231
1304
|
const db = getDb();
|
|
1232
|
-
const row = db.
|
|
1305
|
+
const row = await db.get(
|
|
1306
|
+
"SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
|
|
1307
|
+
[email]
|
|
1308
|
+
);
|
|
1233
1309
|
return row?.id;
|
|
1234
1310
|
}
|
|
1235
|
-
function getStewardRolesForUser(nestId, userEmail) {
|
|
1311
|
+
async function getStewardRolesForUser(nestId, userEmail) {
|
|
1236
1312
|
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
|
-
|
|
1313
|
+
const rows = await db.all(
|
|
1314
|
+
"SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
|
|
1315
|
+
[nestId, userEmail]
|
|
1316
|
+
);
|
|
1240
1317
|
return rows.map((r) => r.role);
|
|
1241
1318
|
}
|
|
1242
|
-
function getStewardsForUser(nestId, userEmail) {
|
|
1319
|
+
async function getStewardsForUser(nestId, userEmail) {
|
|
1243
1320
|
const db = getDb();
|
|
1244
|
-
const rows = db.
|
|
1245
|
-
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)"
|
|
1246
|
-
|
|
1321
|
+
const rows = await db.all(
|
|
1322
|
+
"SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
|
|
1323
|
+
[nestId, userEmail]
|
|
1324
|
+
);
|
|
1247
1325
|
return rows.map(rowToSteward);
|
|
1248
1326
|
}
|
|
1249
|
-
function getCollaboratorRole(nestId, userEmail) {
|
|
1250
|
-
const userId = userIdForEmail(userEmail);
|
|
1327
|
+
async function getCollaboratorRole(nestId, userEmail) {
|
|
1328
|
+
const userId = await userIdForEmail(userEmail);
|
|
1251
1329
|
if (!userId) return null;
|
|
1252
1330
|
const db = getDb();
|
|
1253
|
-
const row = db.
|
|
1254
|
-
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
1255
|
-
|
|
1331
|
+
const row = await db.get(
|
|
1332
|
+
"SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
1333
|
+
[nestId, userId]
|
|
1334
|
+
);
|
|
1256
1335
|
return row?.permission ?? null;
|
|
1257
1336
|
}
|
|
1258
|
-
function resolveUserRoles(nestId, userEmail, opts) {
|
|
1337
|
+
async function resolveUserRoles(nestId, userEmail, opts) {
|
|
1259
1338
|
const roles = /* @__PURE__ */ new Set();
|
|
1260
1339
|
if (isSuperAdmin(userEmail)) roles.add("admin");
|
|
1261
|
-
const owner = getNestOwnerEmail(nestId);
|
|
1340
|
+
const owner = await getNestOwnerEmail(nestId);
|
|
1262
1341
|
if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
|
|
1263
1342
|
roles.add("owner");
|
|
1264
1343
|
}
|
|
1265
|
-
const userId = userIdForEmail(userEmail);
|
|
1344
|
+
const userId = await userIdForEmail(userEmail);
|
|
1266
1345
|
if (userId) {
|
|
1267
|
-
const collabRole = collabPermToRole(resolveNestPermission(nestId, userId));
|
|
1346
|
+
const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
|
|
1268
1347
|
if (collabRole) roles.add(collabRole);
|
|
1269
1348
|
}
|
|
1270
|
-
const stewardRoles = opts?.nodeId ? resolveStewardsForNode(nestId, opts.nodeId).filter(
|
|
1349
|
+
const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
|
|
1271
1350
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
|
|
1272
|
-
).map((r) => r.steward.role) : getStewardRolesForUser(nestId, userEmail);
|
|
1351
|
+
).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
|
|
1273
1352
|
for (const role of stewardRoles) roles.add(role);
|
|
1274
1353
|
return [...roles];
|
|
1275
1354
|
}
|
|
@@ -1277,28 +1356,29 @@ function canManageStewards(userEmail) {
|
|
|
1277
1356
|
if (config.AUTH_MODE === "open") return true;
|
|
1278
1357
|
return isLicenseAdminEmail(userEmail) || isSuperAdmin(userEmail);
|
|
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;
|
|
@@ -1464,6 +1551,7 @@ export {
|
|
|
1464
1551
|
getCurrentLicense,
|
|
1465
1552
|
isLicenseAdminEmail,
|
|
1466
1553
|
isLicenseAdminUserId,
|
|
1554
|
+
upsertEnvVar,
|
|
1467
1555
|
installLicenseKey,
|
|
1468
1556
|
startLicenseSafetyPoll,
|
|
1469
1557
|
isSuspended,
|
|
@@ -1474,6 +1562,8 @@ export {
|
|
|
1474
1562
|
resolveNestPermission,
|
|
1475
1563
|
permissionLevel,
|
|
1476
1564
|
isPublicReader,
|
|
1565
|
+
nestStorageRoot,
|
|
1566
|
+
resolveNestPath,
|
|
1477
1567
|
uniqueNestName,
|
|
1478
1568
|
isStewardshipEnabled,
|
|
1479
1569
|
setStewardshipEnabled,
|