@promptowl/contextnest-community 1.20.0 → 1.21.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/README.md +1 -1
- package/dist/{chunk-GVUBZTI3.js → chunk-4KJGQHWB.js} +93 -40
- package/dist/{chunk-7CPP6FIU.js → chunk-6JGQX4GA.js} +173 -34
- package/dist/{chunk-EY7WOKJR.js → chunk-6PJQSKKV.js} +12 -4
- package/dist/{chunk-KHMSDTJJ.js → chunk-IGPJ74O4.js} +60 -12
- package/dist/{chunk-XCFKS65Y.js → chunk-LPPKPEYI.js} +25 -1
- package/dist/{chunk-O5DYL7ZG.js → chunk-N2DTJFCD.js} +2 -2
- package/dist/{chunk-3LUUJUWS.js → chunk-XWGEXQU3.js} +6 -2
- package/dist/{client-7Z2IVCPW.js → client-BD6PAUCZ.js} +1 -1
- package/dist/{engine-VZTSDUPB.js → engine-IKZQ46P7.js} +2 -2
- package/dist/{external-edit-service-3FVK3OD7.js → external-edit-service-LTON57HO.js} +3 -3
- package/dist/{grants-service-WC27TRY3.js → grants-service-LSNZN6NG.js} +2 -2
- package/dist/index.js +551 -150
- package/dist/{migrations.postgres-L42OXR7K.js → migrations.postgres-JA2YGLS3.js} +20 -1
- package/dist/{review-service-TJRHRMMZ.js → review-service-6JK64IRT.js} +8 -6
- package/dist/{stewardship-service-HRVD7SIS.js → stewardship-service-2YKGBP63.js} +3 -3
- package/dist/{version-service-5PIFT2OY.js → version-service-PJV74PVQ.js} +11 -3
- package/dist/web3/assets/index-T4pilFNL.css +1 -0
- package/dist/web3/assets/index-deyhV93k.js +1382 -0
- package/dist/web3/index.html +2 -2
- package/package.json +1 -1
- package/dist/web3/assets/index-D4UG3T04.js +0 -1365
- package/dist/web3/assets/index-Dw1UTDIk.css +0 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ ContextNest Community Edition is a self-hosted server that lets you:
|
|
|
15
15
|
- Export a nest as a portable bundle and re-import it on another self-hosted host
|
|
16
16
|
- Apply stewardship workflows — draft, pending review, approved
|
|
17
17
|
- Share nests with collaborators or publish them read-only to the public
|
|
18
|
-
- Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`
|
|
18
|
+
- Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`, or register the nest in the ctx CLI (`ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`)
|
|
19
19
|
- Sync with the PromptOwl hosted platform for multi-user collaboration
|
|
20
20
|
|
|
21
21
|
The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
resolveNestWideRoles,
|
|
5
5
|
resolveStewardsForNode,
|
|
6
6
|
stewardCoverageForUser
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-N2DTJFCD.js";
|
|
8
8
|
import {
|
|
9
9
|
grantCoversNode,
|
|
10
10
|
listUserGrants,
|
|
11
11
|
resolveNodeGrant
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XWGEXQU3.js";
|
|
13
13
|
import {
|
|
14
14
|
createVersion,
|
|
15
15
|
getApprovedVersion,
|
|
16
16
|
getApprovedVersions,
|
|
17
17
|
getCurrentVersion,
|
|
18
18
|
setApprovedVersion
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-IGPJ74O4.js";
|
|
20
20
|
import {
|
|
21
21
|
buildDocContext,
|
|
22
22
|
buildTitleMap,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
resolveNestPermission,
|
|
37
37
|
sendEmailToRecipient,
|
|
38
38
|
titleForNode
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-6JGQX4GA.js";
|
|
40
40
|
import {
|
|
41
41
|
ConflictError,
|
|
42
42
|
NotFoundError,
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
config,
|
|
47
47
|
getDb
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-LPPKPEYI.js";
|
|
49
49
|
|
|
50
50
|
// src/governance/review-service.ts
|
|
51
51
|
import { v4 as uuid2 } from "uuid";
|
|
@@ -119,6 +119,20 @@ var safeJson = (s) => {
|
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
// src/workflow/flow-graph.ts
|
|
122
|
+
async function nodeTrail(db, nestId, ids) {
|
|
123
|
+
const unique = [...new Set(ids)];
|
|
124
|
+
const titles = /* @__PURE__ */ new Map();
|
|
125
|
+
try {
|
|
126
|
+
const rows = await db.all(
|
|
127
|
+
`SELECT node_id, title FROM node_index
|
|
128
|
+
WHERE nest_id = ? AND node_id IN (${unique.map(() => "?").join(",")})`,
|
|
129
|
+
[nestId, ...unique]
|
|
130
|
+
);
|
|
131
|
+
for (const r of rows) if (r.title) titles.set(r.node_id, r.title);
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
return ids.map((id) => `"${titles.get(id) ?? id}"`).join(" \u2192 ");
|
|
135
|
+
}
|
|
122
136
|
async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
123
137
|
const db = getDb();
|
|
124
138
|
const rows = await db.all(
|
|
@@ -133,7 +147,7 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
|
|
|
133
147
|
for (const r of rows) {
|
|
134
148
|
if (r.from_node === r.to_node) {
|
|
135
149
|
throw new ConflictError(
|
|
136
|
-
`cycle:
|
|
150
|
+
`cycle: ${await nodeTrail(db, nestId, [r.from_node])} cannot flow into itself`
|
|
137
151
|
);
|
|
138
152
|
}
|
|
139
153
|
nodes.add(r.from_node);
|
|
@@ -698,31 +712,60 @@ function withNodeWriteLock(key, fn) {
|
|
|
698
712
|
// src/governance/review-service.ts
|
|
699
713
|
async function submitForReview(params) {
|
|
700
714
|
const db = getDb();
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
715
|
+
const { id, version } = await withNodeWriteLock(
|
|
716
|
+
nodeWriteKey(params.nestId, params.nodeId),
|
|
717
|
+
async () => {
|
|
718
|
+
const existing = await db.get(
|
|
719
|
+
"SELECT id FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending'",
|
|
720
|
+
[params.nestId, params.nodeId]
|
|
721
|
+
);
|
|
722
|
+
if (existing) {
|
|
723
|
+
throw new Error("A review is already pending for this node");
|
|
724
|
+
}
|
|
725
|
+
const version2 = await getCurrentVersion(params.nestId, params.nodeId) || params.version;
|
|
726
|
+
const id2 = uuid2();
|
|
727
|
+
await db.run(
|
|
728
|
+
`INSERT INTO review_requests
|
|
711
729
|
(id, nest_id, node_id, version, requested_by, request_note, priority)
|
|
712
730
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
731
|
+
[
|
|
732
|
+
id2,
|
|
733
|
+
params.nestId,
|
|
734
|
+
params.nodeId,
|
|
735
|
+
version2,
|
|
736
|
+
params.requestedBy,
|
|
737
|
+
params.note || null,
|
|
738
|
+
params.priority || "normal"
|
|
739
|
+
]
|
|
740
|
+
);
|
|
741
|
+
await db.run(
|
|
742
|
+
"UPDATE node_versions SET status = 'pending_review' WHERE nest_id = ? AND node_id = ? AND version = ?",
|
|
743
|
+
[params.nestId, params.nodeId, version2]
|
|
744
|
+
);
|
|
745
|
+
try {
|
|
746
|
+
const { storage, versions: versionManager } = await engineCache.get(
|
|
747
|
+
params.nestId
|
|
748
|
+
);
|
|
749
|
+
const node = await storage.readDocument(params.nodeId);
|
|
750
|
+
const history = await versionManager.getHistory(params.nodeId);
|
|
751
|
+
const alreadySealed = (history?.versions ?? []).some(
|
|
752
|
+
(v) => v.version === (node.frontmatter.version ?? 1)
|
|
753
|
+
);
|
|
754
|
+
if (!alreadySealed) {
|
|
755
|
+
await versionManager.createVersion(node, params.requestedBy, {
|
|
756
|
+
note: params.note || "Submitted for review"
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.error(
|
|
761
|
+
"VersionManager.createVersion failed (submit for review)",
|
|
762
|
+
params.nestId,
|
|
763
|
+
params.nodeId,
|
|
764
|
+
err
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
return { id: id2, version: version2 };
|
|
768
|
+
}
|
|
726
769
|
);
|
|
727
770
|
const reviewCtx = await buildDocContext(params.nestId, params.nodeId, params.baseUrl);
|
|
728
771
|
void dispatchEvent({
|
|
@@ -730,18 +773,18 @@ async function submitForReview(params) {
|
|
|
730
773
|
nestId: params.nestId,
|
|
731
774
|
subjectId: params.nodeId,
|
|
732
775
|
actor: params.requestedBy,
|
|
733
|
-
message: `Review requested on *${reviewCtx.label}* (v${
|
|
776
|
+
message: `Review requested on *${reviewCtx.label}* (v${version}) by ${params.requestedBy}${params.note ? ` \u2014 "${params.note}"` : ""}`
|
|
734
777
|
});
|
|
735
778
|
void notifyNestEvent(
|
|
736
779
|
params.nestId,
|
|
737
|
-
`:memo: Review requested on *${reviewCtx.label}* (v${
|
|
780
|
+
`:memo: Review requested on *${reviewCtx.label}* (v${version}) by ${params.requestedBy}${params.note ? ` \u2014 "${params.note}"` : ""}`,
|
|
738
781
|
{
|
|
739
782
|
status: "pending_review",
|
|
740
783
|
docTitle: reviewCtx.docTitle,
|
|
741
784
|
path: reviewCtx.path,
|
|
742
785
|
link: reviewCtx.link,
|
|
743
786
|
actor: params.requestedBy,
|
|
744
|
-
version
|
|
787
|
+
version,
|
|
745
788
|
note: params.note || null
|
|
746
789
|
}
|
|
747
790
|
);
|
|
@@ -765,7 +808,7 @@ async function submitForReview(params) {
|
|
|
765
808
|
path: reviewCtx.path,
|
|
766
809
|
link: reviewCtx.link,
|
|
767
810
|
actor: params.requestedBy,
|
|
768
|
-
version
|
|
811
|
+
version,
|
|
769
812
|
note: params.note || null
|
|
770
813
|
});
|
|
771
814
|
}
|
|
@@ -1106,9 +1149,12 @@ async function getPendingReview(nestId, nodeId) {
|
|
|
1106
1149
|
);
|
|
1107
1150
|
return row ? rowToReviewRequest(row) : null;
|
|
1108
1151
|
}
|
|
1152
|
+
function isNestTarget(t) {
|
|
1153
|
+
return t === "nest" || t === "nest_archive";
|
|
1154
|
+
}
|
|
1109
1155
|
async function deletionTargetTitle(request, titles) {
|
|
1110
1156
|
if (request.targetType === "folder") return prettyFolderPath(request.nodeId);
|
|
1111
|
-
if (request.targetType
|
|
1157
|
+
if (isNestTarget(request.targetType)) return await nestName(request.nestId);
|
|
1112
1158
|
return titles?.get(request.nodeId) ?? await titleForNode(request.nestId, request.nodeId);
|
|
1113
1159
|
}
|
|
1114
1160
|
async function nestAdminEmails(nestId) {
|
|
@@ -1145,7 +1191,7 @@ async function deletionTargetContext(request, baseUrl) {
|
|
|
1145
1191
|
const name = await nestName(request.nestId);
|
|
1146
1192
|
const label = request.targetType === "folder" ? prettyFolderPath(request.nodeId) : name;
|
|
1147
1193
|
return {
|
|
1148
|
-
noun: request.targetType,
|
|
1194
|
+
noun: request.targetType === "nest_archive" ? "nest" : request.targetType,
|
|
1149
1195
|
label,
|
|
1150
1196
|
docTitle: label,
|
|
1151
1197
|
path: name,
|
|
@@ -1210,7 +1256,8 @@ async function notifyDeletionRequested(request, baseUrl) {
|
|
|
1210
1256
|
async function notifyDeletionResolved(params) {
|
|
1211
1257
|
try {
|
|
1212
1258
|
const { outcome, resolvedBy, note } = params;
|
|
1213
|
-
const
|
|
1259
|
+
const archivedInstead = outcome === "deleted" && params.targetType === "nest_archive";
|
|
1260
|
+
const verb = archivedInstead ? "archived" : outcome === "deleted" ? "deleted" : "rejected the deletion of";
|
|
1214
1261
|
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1215
1262
|
const recipient = params.requestedBy.toLowerCase();
|
|
1216
1263
|
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
@@ -1220,7 +1267,7 @@ async function notifyDeletionResolved(params) {
|
|
|
1220
1267
|
[recipient],
|
|
1221
1268
|
`deletion_${outcome}`,
|
|
1222
1269
|
params.requestId,
|
|
1223
|
-
outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1270
|
+
outcome === "deleted" ? `"${params.docTitle}" was ${archivedInstead ? "archived" : "deleted"} by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1224
1271
|
);
|
|
1225
1272
|
}
|
|
1226
1273
|
void dispatchEvent({
|
|
@@ -1240,7 +1287,10 @@ async function notifyDeletionResolved(params) {
|
|
|
1240
1287
|
if (recipient !== resolvedBy.toLowerCase()) {
|
|
1241
1288
|
const link = params.baseUrl ? `${params.baseUrl}/?nest=${encodeURIComponent(params.nestId)}` : "";
|
|
1242
1289
|
void sendEmailToRecipient(recipient, {
|
|
1243
|
-
|
|
1290
|
+
// The email has to agree with the inbox row and the channel line above:
|
|
1291
|
+
// an honoured archive is not a deletion, and the deletion template says
|
|
1292
|
+
// the nest is gone.
|
|
1293
|
+
status: archivedInstead ? "deletion_archived" : outcome === "deleted" ? "deletion_deleted" : "deletion_declined",
|
|
1244
1294
|
docTitle: params.docTitle,
|
|
1245
1295
|
path: params.nestId,
|
|
1246
1296
|
link: outcome === "deleted" ? link : void 0,
|
|
@@ -1261,12 +1311,13 @@ async function requestDeletion(params) {
|
|
|
1261
1311
|
const db = getDb();
|
|
1262
1312
|
const targetType = params.targetType || "document";
|
|
1263
1313
|
const noun = targetType === "document" ? "node" : targetType === "folder" ? "folder" : "nest";
|
|
1314
|
+
const kind = targetType === "nest_archive" ? "An archive request" : "A deletion request";
|
|
1264
1315
|
const existing = await db.get(
|
|
1265
1316
|
"SELECT id FROM deletion_requests WHERE nest_id = ? AND node_id = ? AND target_type = ? AND status = 'pending'",
|
|
1266
1317
|
[params.nestId, params.nodeId, targetType]
|
|
1267
1318
|
);
|
|
1268
1319
|
if (existing) {
|
|
1269
|
-
throw new ConflictError(
|
|
1320
|
+
throw new ConflictError(`${kind} is already pending for this ${noun}`);
|
|
1270
1321
|
}
|
|
1271
1322
|
const id = uuid2();
|
|
1272
1323
|
try {
|
|
@@ -1289,7 +1340,7 @@ async function requestDeletion(params) {
|
|
|
1289
1340
|
params.nodeId,
|
|
1290
1341
|
err
|
|
1291
1342
|
);
|
|
1292
|
-
throw new ConflictError(
|
|
1343
|
+
throw new ConflictError(`${kind} is already pending for this ${noun}`);
|
|
1293
1344
|
}
|
|
1294
1345
|
const request = await getDeletionRequest(id);
|
|
1295
1346
|
await notifyDeletionRequested(request, params.baseUrl);
|
|
@@ -1437,6 +1488,7 @@ export {
|
|
|
1437
1488
|
canRequestDeletion,
|
|
1438
1489
|
filterAccessible,
|
|
1439
1490
|
safeJson,
|
|
1491
|
+
nodeTrail,
|
|
1440
1492
|
requireWorkflowPlane,
|
|
1441
1493
|
seedDefaultEdgeTypes,
|
|
1442
1494
|
parseConditionSchema,
|
|
@@ -1458,6 +1510,7 @@ export {
|
|
|
1458
1510
|
getReviewQueue,
|
|
1459
1511
|
getReviewHistory,
|
|
1460
1512
|
getPendingReview,
|
|
1513
|
+
isNestTarget,
|
|
1461
1514
|
deletionTargetTitle,
|
|
1462
1515
|
notifyDeletionResolved,
|
|
1463
1516
|
requestDeletion,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LPPKPEYI.js";
|
|
12
12
|
import {
|
|
13
13
|
ANON_USER_ID
|
|
14
14
|
} from "./chunk-YB3LKF7U.js";
|
|
@@ -680,6 +680,14 @@ var STATUS_META = {
|
|
|
680
680
|
subjectWord: "deleted",
|
|
681
681
|
label: "Deleted",
|
|
682
682
|
color: "#dc2626"
|
|
683
|
+
},
|
|
684
|
+
// An honoured ARCHIVE request. Deliberately not the deletion palette: the
|
|
685
|
+
// nest is intact and restorable, and red "Deleted" would tell the requester
|
|
686
|
+
// the opposite of what happened.
|
|
687
|
+
deletion_archived: {
|
|
688
|
+
subjectWord: "archived",
|
|
689
|
+
label: "Archived",
|
|
690
|
+
color: "#0891b2"
|
|
683
691
|
}
|
|
684
692
|
};
|
|
685
693
|
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
@@ -777,6 +785,14 @@ var TEMPLATES = {
|
|
|
777
785
|
lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
|
|
778
786
|
rows: governanceRows
|
|
779
787
|
},
|
|
788
|
+
deletion_archived: {
|
|
789
|
+
heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
|
|
790
|
+
detailsHeader: "Nest details",
|
|
791
|
+
linkLabel: "Open nest",
|
|
792
|
+
lineT: (x) => `${x.title} has been archived${x.byT}${x.noteT} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
793
|
+
lineH: (x) => `${x.doc} has been archived${x.byH}${x.noteH} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
794
|
+
rows: governanceRows
|
|
795
|
+
},
|
|
780
796
|
steward: {
|
|
781
797
|
heading: (x) => `You're now a steward of ${x.title}`,
|
|
782
798
|
detailsHeader: "Details",
|
|
@@ -1259,7 +1275,9 @@ async function listNotifications(userEmail, opts = {}) {
|
|
|
1259
1275
|
`SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
|
|
1260
1276
|
COALESCE(
|
|
1261
1277
|
rr.node_id,
|
|
1262
|
-
CASE WHEN dr.target_type = 'document' THEN dr.node_id END
|
|
1278
|
+
CASE WHEN dr.target_type = 'document' THEN dr.node_id END,
|
|
1279
|
+
-- A mention names the document directly; there's no request row.
|
|
1280
|
+
CASE WHEN n.kind = 'mention' THEN n.subject_id END
|
|
1263
1281
|
) AS node_id
|
|
1264
1282
|
FROM notifications n
|
|
1265
1283
|
LEFT JOIN review_requests rr ON rr.id = n.subject_id
|
|
@@ -1566,6 +1584,23 @@ function parseSharedTeams(json) {
|
|
|
1566
1584
|
return [];
|
|
1567
1585
|
}
|
|
1568
1586
|
}
|
|
1587
|
+
async function listSharedTeamMembers(refs) {
|
|
1588
|
+
const ids = [...new Set(refs.map((r) => r.teamId).filter(Boolean))];
|
|
1589
|
+
if (ids.length === 0) return [];
|
|
1590
|
+
const rows = await getDb().all(
|
|
1591
|
+
`SELECT members FROM teams WHERE id IN (${ids.map(() => "?").join(",")})`,
|
|
1592
|
+
ids
|
|
1593
|
+
);
|
|
1594
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
1595
|
+
for (const row of rows) {
|
|
1596
|
+
for (const member of parseMembers(row.members)) {
|
|
1597
|
+
const key = (member.email || "").toLowerCase();
|
|
1598
|
+
if (!key || byEmail.has(key)) continue;
|
|
1599
|
+
byEmail.set(key, { userId: member.userId, email: member.email, name: null });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return [...byEmail.values()];
|
|
1603
|
+
}
|
|
1569
1604
|
async function listNestTeams(nestId) {
|
|
1570
1605
|
const db = getDb();
|
|
1571
1606
|
const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
|
|
@@ -1750,9 +1785,29 @@ var PERMISSION_LEVELS = {
|
|
|
1750
1785
|
};
|
|
1751
1786
|
var higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
|
|
1752
1787
|
async function resolveNestAccess(nestId, userId) {
|
|
1788
|
+
const access = await resolveNestAccessRaw(nestId, userId);
|
|
1789
|
+
return {
|
|
1790
|
+
...access,
|
|
1791
|
+
rawPermission: access.permission,
|
|
1792
|
+
permission: capForArchive(access.permission, access.archived)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async function isNestArchived(nestId) {
|
|
1796
|
+
const row = await getDb().get(
|
|
1797
|
+
"SELECT archived_at FROM nests WHERE id = ?",
|
|
1798
|
+
[nestId]
|
|
1799
|
+
);
|
|
1800
|
+
return !!row?.archived_at;
|
|
1801
|
+
}
|
|
1802
|
+
function capForArchive(permission, archived) {
|
|
1803
|
+
if (!archived) return permission;
|
|
1804
|
+
return PERMISSION_LEVELS[permission] > PERMISSION_LEVELS.read ? "read" : permission;
|
|
1805
|
+
}
|
|
1806
|
+
async function resolveNestAccessRaw(nestId, userId) {
|
|
1753
1807
|
const row = await getDb().get(
|
|
1754
1808
|
`SELECT n.user_id AS owner_id,
|
|
1755
1809
|
n.visibility AS visibility,
|
|
1810
|
+
n.archived_at AS archived_at,
|
|
1756
1811
|
n.shared_teams AS shared_teams,
|
|
1757
1812
|
u.email AS caller_email,
|
|
1758
1813
|
nc.permission AS collab_permission,
|
|
@@ -1779,7 +1834,9 @@ async function resolveNestAccess(nestId, userId) {
|
|
|
1779
1834
|
isSteward: false,
|
|
1780
1835
|
hasGrant: false,
|
|
1781
1836
|
permission: "none",
|
|
1782
|
-
|
|
1837
|
+
rawPermission: "none",
|
|
1838
|
+
isPublicReader: false,
|
|
1839
|
+
archived: false
|
|
1783
1840
|
};
|
|
1784
1841
|
if (!row) return absent;
|
|
1785
1842
|
const base = {
|
|
@@ -1791,7 +1848,8 @@ async function resolveNestAccess(nestId, userId) {
|
|
|
1791
1848
|
// Postgres returns integer 1, SQLite returns 1 — both truthy, neither a
|
|
1792
1849
|
// boolean, so normalize rather than leaking the driver's shape.
|
|
1793
1850
|
isSteward: !!row.steward_hit,
|
|
1794
|
-
hasGrant: !!row.grant_hit
|
|
1851
|
+
hasGrant: !!row.grant_hit,
|
|
1852
|
+
archived: !!row.archived_at
|
|
1795
1853
|
};
|
|
1796
1854
|
if (row.owner_id === userId) {
|
|
1797
1855
|
return { ...base, permission: "owner", isPublicReader: false };
|
|
@@ -1877,20 +1935,32 @@ async function isImportedNest(nestId) {
|
|
|
1877
1935
|
);
|
|
1878
1936
|
return !!row?.is_imported;
|
|
1879
1937
|
}
|
|
1938
|
+
var MAX_NEST_NAME_LENGTH = 100;
|
|
1880
1939
|
function plainName(name, emptyMessage = "Name cannot be empty") {
|
|
1881
1940
|
const typed = (name ?? "").trim();
|
|
1882
1941
|
if (!typed) throw new ValidationError(emptyMessage);
|
|
1883
|
-
|
|
1884
|
-
|
|
1942
|
+
if (typed.length > MAX_NEST_NAME_LENGTH) {
|
|
1943
|
+
throw new ValidationError(
|
|
1944
|
+
`Nest names are capped at ${MAX_NEST_NAME_LENGTH} characters`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
if (/<\/?[a-zA-Z][^>]*>/.test(typed)) {
|
|
1885
1948
|
throw new ValidationError(
|
|
1886
|
-
"
|
|
1949
|
+
"Nest names are plain text \u2014 remove the HTML tags."
|
|
1887
1950
|
);
|
|
1888
1951
|
}
|
|
1889
|
-
return
|
|
1952
|
+
return typed;
|
|
1890
1953
|
}
|
|
1891
1954
|
function toSlug(name) {
|
|
1892
1955
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1893
1956
|
}
|
|
1957
|
+
async function assertSlugFree(userId, slug, message, exceptId) {
|
|
1958
|
+
const conflict = await getDb().get(
|
|
1959
|
+
exceptId ? "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1" : "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
|
|
1960
|
+
exceptId ? [userId, slug, exceptId] : [userId, slug]
|
|
1961
|
+
);
|
|
1962
|
+
if (conflict) throw new ValidationError(message);
|
|
1963
|
+
}
|
|
1894
1964
|
async function uniqueNestName(userId, baseName) {
|
|
1895
1965
|
const db = getDb();
|
|
1896
1966
|
let candidate = baseName;
|
|
@@ -2017,15 +2087,12 @@ async function renameNest(nestId, patch) {
|
|
|
2017
2087
|
const trimmed = plainName(patch.name);
|
|
2018
2088
|
const slug = toSlug(trimmed);
|
|
2019
2089
|
if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2090
|
+
await assertSlugFree(
|
|
2091
|
+
existing.user_id,
|
|
2092
|
+
slug,
|
|
2093
|
+
"Another nest already uses this name. Pick a different one.",
|
|
2094
|
+
nestId
|
|
2023
2095
|
);
|
|
2024
|
-
if (conflict) {
|
|
2025
|
-
throw new ValidationError(
|
|
2026
|
-
`Another nest already uses this name. Pick a different one.`
|
|
2027
|
-
);
|
|
2028
|
-
}
|
|
2029
2096
|
updates.push("name = ?", "slug = ?");
|
|
2030
2097
|
params.push(trimmed, slug);
|
|
2031
2098
|
}
|
|
@@ -2053,15 +2120,11 @@ async function createNest(userId, name, description) {
|
|
|
2053
2120
|
"Name must contain at least one alphanumeric character"
|
|
2054
2121
|
);
|
|
2055
2122
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2123
|
+
await assertSlugFree(
|
|
2124
|
+
userId,
|
|
2125
|
+
slug,
|
|
2126
|
+
"You already have a nest with this name. Pick a different one."
|
|
2059
2127
|
);
|
|
2060
|
-
if (conflict) {
|
|
2061
|
-
throw new ValidationError(
|
|
2062
|
-
"You already have a nest with this name. Pick a different one."
|
|
2063
|
-
);
|
|
2064
|
-
}
|
|
2065
2128
|
const visibility = userId === ANON_USER_ID ? "public" : "private";
|
|
2066
2129
|
await db.run(
|
|
2067
2130
|
"INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
|
|
@@ -2079,6 +2142,11 @@ async function importNest(userId, name) {
|
|
|
2079
2142
|
const db = getDb();
|
|
2080
2143
|
const id = uuid3();
|
|
2081
2144
|
const slug = toSlug(nm);
|
|
2145
|
+
await assertSlugFree(
|
|
2146
|
+
userId,
|
|
2147
|
+
slug,
|
|
2148
|
+
"A nest with this name already exists. Pick a different name."
|
|
2149
|
+
);
|
|
2082
2150
|
const visibility = userId === ANON_USER_ID ? "public" : "private";
|
|
2083
2151
|
mkdirSync(resolveNestPath(id), { recursive: true });
|
|
2084
2152
|
await db.run(
|
|
@@ -2088,26 +2156,79 @@ async function importNest(userId, name) {
|
|
|
2088
2156
|
trackEvent("nest.import", { nestId: id, userId });
|
|
2089
2157
|
return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
|
|
2090
2158
|
}
|
|
2091
|
-
async function
|
|
2159
|
+
async function pinNest(userId, nestId) {
|
|
2160
|
+
const db = getDb();
|
|
2161
|
+
const existing = await db.get(
|
|
2162
|
+
"SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
|
|
2163
|
+
[userId, nestId]
|
|
2164
|
+
);
|
|
2165
|
+
if (existing) return;
|
|
2166
|
+
try {
|
|
2167
|
+
await db.run(
|
|
2168
|
+
"INSERT INTO nest_pins (user_id, nest_id) VALUES (?, ?)",
|
|
2169
|
+
[userId, nestId]
|
|
2170
|
+
);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
const now = await db.get(
|
|
2173
|
+
"SELECT 1 FROM nest_pins WHERE user_id = ? AND nest_id = ?",
|
|
2174
|
+
[userId, nestId]
|
|
2175
|
+
);
|
|
2176
|
+
if (!now) throw err;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
async function unpinNest(userId, nestId) {
|
|
2180
|
+
await getDb().run(
|
|
2181
|
+
"DELETE FROM nest_pins WHERE user_id = ? AND nest_id = ?",
|
|
2182
|
+
[userId, nestId]
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
async function pinnedNestIds(userId) {
|
|
2186
|
+
const rows = await getDb().all(
|
|
2187
|
+
"SELECT nest_id FROM nest_pins WHERE user_id = ?",
|
|
2188
|
+
[userId]
|
|
2189
|
+
);
|
|
2190
|
+
return new Set(rows.map((r) => r.nest_id));
|
|
2191
|
+
}
|
|
2192
|
+
var LIVE_ONLY = "archived_at IS NULL";
|
|
2193
|
+
var LIVE_ONLY_N = "n.archived_at IS NULL";
|
|
2194
|
+
async function listNests(userId, opts = {}) {
|
|
2092
2195
|
const db = getDb();
|
|
2196
|
+
const scope = opts.archived ? "archived_at IS NOT NULL" : LIVE_ONLY;
|
|
2093
2197
|
if (userId === ANON_USER_ID) {
|
|
2094
2198
|
return await db.all(
|
|
2095
|
-
|
|
2199
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
|
|
2096
2200
|
[ANON_USER_ID]
|
|
2097
2201
|
);
|
|
2098
2202
|
}
|
|
2099
2203
|
const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
|
|
2100
2204
|
if (!includeAnon) {
|
|
2101
2205
|
return await db.all(
|
|
2102
|
-
|
|
2206
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${scope} ORDER BY created_at DESC`,
|
|
2103
2207
|
[userId]
|
|
2104
2208
|
);
|
|
2105
2209
|
}
|
|
2106
2210
|
return await db.all(
|
|
2107
|
-
|
|
2211
|
+
`SELECT * FROM nests WHERE (user_id = ? OR user_id = ?) AND ${scope}
|
|
2212
|
+
ORDER BY created_at DESC`,
|
|
2108
2213
|
[userId, ANON_USER_ID]
|
|
2109
2214
|
);
|
|
2110
2215
|
}
|
|
2216
|
+
async function archiveNest(nestId) {
|
|
2217
|
+
await getDb().transaction(async (tx) => {
|
|
2218
|
+
await tx.run(
|
|
2219
|
+
"UPDATE nests SET archived_at = ? WHERE id = ? AND archived_at IS NULL",
|
|
2220
|
+
[(/* @__PURE__ */ new Date()).toISOString(), nestId]
|
|
2221
|
+
);
|
|
2222
|
+
await tx.run(
|
|
2223
|
+
`DELETE FROM deletion_requests
|
|
2224
|
+
WHERE nest_id = ? AND target_type = 'nest_archive' AND status = 'pending'`,
|
|
2225
|
+
[nestId]
|
|
2226
|
+
);
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
async function restoreNest(nestId) {
|
|
2230
|
+
await getDb().run("UPDATE nests SET archived_at = NULL WHERE id = ?", [nestId]);
|
|
2231
|
+
}
|
|
2111
2232
|
async function nestsSharedViaTeams(userId) {
|
|
2112
2233
|
const db = getDb();
|
|
2113
2234
|
const teams = await db.all(
|
|
@@ -2129,6 +2250,7 @@ async function nestsSharedViaTeams(userId) {
|
|
|
2129
2250
|
const candidates = await db.all(
|
|
2130
2251
|
`SELECT * FROM nests
|
|
2131
2252
|
WHERE user_id != ?
|
|
2253
|
+
AND ${LIVE_ONLY}
|
|
2132
2254
|
AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
|
|
2133
2255
|
[userId, ...ids.map((id) => `%${id}%`)]
|
|
2134
2256
|
);
|
|
@@ -2154,6 +2276,7 @@ async function listSharedNests(userId) {
|
|
|
2154
2276
|
LEFT JOIN grants g
|
|
2155
2277
|
ON g.nest_id = n.id AND g.user_id = ?
|
|
2156
2278
|
WHERE n.user_id != ?
|
|
2279
|
+
AND ${LIVE_ONLY_N}
|
|
2157
2280
|
AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL OR g.id IS NOT NULL)
|
|
2158
2281
|
ORDER BY n.created_at DESC`,
|
|
2159
2282
|
[userId, userId, userId, userId]
|
|
@@ -2167,7 +2290,8 @@ async function listSharedNests(userId) {
|
|
|
2167
2290
|
}
|
|
2168
2291
|
if (await isServerAdminUserId(userId)) {
|
|
2169
2292
|
const anon = await db.all(
|
|
2170
|
-
|
|
2293
|
+
`SELECT * FROM nests WHERE user_id = ? AND ${LIVE_ONLY}
|
|
2294
|
+
ORDER BY created_at DESC`,
|
|
2171
2295
|
[ANON_USER_ID]
|
|
2172
2296
|
);
|
|
2173
2297
|
for (const n of anon) {
|
|
@@ -2201,11 +2325,12 @@ async function listVisibleNests(userId, opts = {}) {
|
|
|
2201
2325
|
ON s.nest_id = n.id AND s.is_active = 1
|
|
2202
2326
|
AND LOWER(s.user_email) = LOWER(u.email)
|
|
2203
2327
|
${opts.readable ? "LEFT JOIN grants g ON g.nest_id = n.id AND g.user_id = ?" : ""}
|
|
2204
|
-
WHERE
|
|
2328
|
+
WHERE ${LIVE_ONLY_N}
|
|
2329
|
+
AND (n.user_id = ?
|
|
2205
2330
|
OR nc.user_id IS NOT NULL
|
|
2206
2331
|
OR s.id IS NOT NULL
|
|
2207
2332
|
${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
|
|
2208
|
-
${includeAnon ? "OR n.user_id = ?" : ""}
|
|
2333
|
+
${includeAnon ? "OR n.user_id = ?" : ""})
|
|
2209
2334
|
ORDER BY n.created_at DESC`,
|
|
2210
2335
|
params
|
|
2211
2336
|
);
|
|
@@ -2223,6 +2348,7 @@ async function listPublicNests(userId) {
|
|
|
2223
2348
|
return await db.all(
|
|
2224
2349
|
`SELECT n.* FROM nests n
|
|
2225
2350
|
WHERE n.visibility = 'public'
|
|
2351
|
+
AND ${LIVE_ONLY_N}
|
|
2226
2352
|
AND n.user_id != ?
|
|
2227
2353
|
AND NOT EXISTS (
|
|
2228
2354
|
SELECT 1 FROM nest_collaborators nc
|
|
@@ -2250,6 +2376,10 @@ async function deleteNest(nestId) {
|
|
|
2250
2376
|
} catch {
|
|
2251
2377
|
}
|
|
2252
2378
|
await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
|
|
2379
|
+
try {
|
|
2380
|
+
await tx.run("DELETE FROM nest_pins WHERE nest_id = ?", [id]);
|
|
2381
|
+
} catch {
|
|
2382
|
+
}
|
|
2253
2383
|
await tx.run("DELETE FROM nests WHERE id = ?", [id]);
|
|
2254
2384
|
});
|
|
2255
2385
|
const path = resolveNestPath(nestId);
|
|
@@ -2283,7 +2413,7 @@ var META_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
2283
2413
|
]);
|
|
2284
2414
|
function isMetaFile(idOrPath) {
|
|
2285
2415
|
const base = idOrPath.split(/[/\\]/).pop() ?? "";
|
|
2286
|
-
return META_BASENAMES.has(base.replace(/\.md$/i, ""));
|
|
2416
|
+
return META_BASENAMES.has(base.replace(/\.md$/i, "").toUpperCase());
|
|
2287
2417
|
}
|
|
2288
2418
|
|
|
2289
2419
|
// src/shared/batch.ts
|
|
@@ -2457,8 +2587,8 @@ async function ensureNodeIndex(nestId) {
|
|
|
2457
2587
|
return run;
|
|
2458
2588
|
}
|
|
2459
2589
|
async function rebuildNodeIndex(nestId) {
|
|
2460
|
-
const { engineCache: engineCache2 } = await import("./engine-
|
|
2461
|
-
const { documentsWithSuggestions } = await import("./external-edit-service-
|
|
2590
|
+
const { engineCache: engineCache2 } = await import("./engine-IKZQ46P7.js");
|
|
2591
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-LTON57HO.js");
|
|
2462
2592
|
const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
|
|
2463
2593
|
dropDiscoveryCache();
|
|
2464
2594
|
const startedAt = writeGeneration.get(nestId) ?? 0;
|
|
@@ -2739,7 +2869,12 @@ export {
|
|
|
2739
2869
|
renameNest,
|
|
2740
2870
|
createNest,
|
|
2741
2871
|
importNest,
|
|
2872
|
+
pinNest,
|
|
2873
|
+
unpinNest,
|
|
2874
|
+
pinnedNestIds,
|
|
2742
2875
|
listNests,
|
|
2876
|
+
archiveNest,
|
|
2877
|
+
restoreNest,
|
|
2743
2878
|
listSharedNests,
|
|
2744
2879
|
listVisibleNests,
|
|
2745
2880
|
listPublicNests,
|
|
@@ -2786,12 +2921,16 @@ export {
|
|
|
2786
2921
|
replaceTeamRoster,
|
|
2787
2922
|
updateMemberRole,
|
|
2788
2923
|
removeMember,
|
|
2924
|
+
parseSharedTeams,
|
|
2925
|
+
listSharedTeamMembers,
|
|
2789
2926
|
listNestTeams,
|
|
2790
2927
|
shareTeamWithNest,
|
|
2791
2928
|
unshareTeamFromNest,
|
|
2792
2929
|
resolveTeamRolesForUser,
|
|
2793
2930
|
isServerAdminUserId,
|
|
2794
2931
|
resolveNestAccess,
|
|
2932
|
+
isNestArchived,
|
|
2933
|
+
capForArchive,
|
|
2795
2934
|
resolveNestPermission,
|
|
2796
2935
|
permissionLevel,
|
|
2797
2936
|
isPublicReader
|