@promptowl/contextnest-community 1.20.0 → 1.21.1
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/{adapter.postgres-YOODX2BI.js → adapter.postgres-6VZCMPQL.js} +11 -1
- package/dist/{chunk-KHMSDTJJ.js → chunk-FXYFLUE7.js} +60 -12
- package/dist/{chunk-7CPP6FIU.js → chunk-G4AL7ETF.js} +173 -34
- package/dist/{chunk-3LUUJUWS.js → chunk-IVXVOXYN.js} +6 -2
- package/dist/{chunk-O5DYL7ZG.js → chunk-LL2JU5AA.js} +2 -2
- package/dist/{chunk-GVUBZTI3.js → chunk-NH7L2S6S.js} +93 -40
- package/dist/{chunk-XCFKS65Y.js → chunk-SKZ7YYT4.js} +26 -2
- package/dist/{chunk-EY7WOKJR.js → chunk-UVOAYQTU.js} +12 -4
- package/dist/{client-7Z2IVCPW.js → client-KBK4VIWJ.js} +1 -1
- package/dist/{engine-VZTSDUPB.js → engine-WXWER2V7.js} +2 -2
- package/dist/{external-edit-service-3FVK3OD7.js → external-edit-service-Y757EFMS.js} +3 -3
- package/dist/{grants-service-WC27TRY3.js → grants-service-FKINSDLZ.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-PPR5BV5J.js} +8 -6
- package/dist/{stewardship-service-HRVD7SIS.js → stewardship-service-IKC7MXLP.js} +3 -3
- package/dist/{version-service-5PIFT2OY.js → version-service-GWSDSUZR.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.
|
|
@@ -5,7 +5,17 @@ function translatePlaceholders(sql) {
|
|
|
5
5
|
let n = 0;
|
|
6
6
|
for (let i = 0; i < sql.length; i++) {
|
|
7
7
|
const ch = sql[i];
|
|
8
|
-
if (ch === "
|
|
8
|
+
if (!inStr && ch === "-" && sql[i + 1] === "-") {
|
|
9
|
+
const end = sql.indexOf("\n", i);
|
|
10
|
+
const stop = end === -1 ? sql.length : end;
|
|
11
|
+
out += sql.slice(i, stop);
|
|
12
|
+
i = stop - 1;
|
|
13
|
+
} else if (!inStr && ch === "/" && sql[i + 1] === "*") {
|
|
14
|
+
const end = sql.indexOf("*/", i + 2);
|
|
15
|
+
const stop = end === -1 ? sql.length : end + 2;
|
|
16
|
+
out += sql.slice(i, stop);
|
|
17
|
+
i = stop - 1;
|
|
18
|
+
} else if (ch === "'") {
|
|
9
19
|
inStr = !inStr;
|
|
10
20
|
out += ch;
|
|
11
21
|
} else if (ch === "?" && !inStr) {
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
buildTitleMap,
|
|
3
3
|
insertOrReplace,
|
|
4
4
|
nowExpr
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-G4AL7ETF.js";
|
|
6
6
|
import {
|
|
7
7
|
getDb
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SKZ7YYT4.js";
|
|
9
9
|
|
|
10
10
|
// src/governance/version-service.ts
|
|
11
11
|
import { createHash } from "crypto";
|
|
@@ -136,6 +136,44 @@ async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
|
|
|
136
136
|
);
|
|
137
137
|
await db.run(sql, [nestId, nodeId, version, approvedBy]);
|
|
138
138
|
}
|
|
139
|
+
async function getSealedVersion(nestId, nodeId) {
|
|
140
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
141
|
+
if (approved != null) return approved;
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const row = await db.get(
|
|
144
|
+
`SELECT MAX(version) as v FROM node_versions
|
|
145
|
+
WHERE nest_id = ? AND node_id = ? AND status IN ('published', 'approved')`,
|
|
146
|
+
[nestId, nodeId]
|
|
147
|
+
);
|
|
148
|
+
return row?.v ?? null;
|
|
149
|
+
}
|
|
150
|
+
async function deleteSupersededDrafts(nestId, nodeId, belowVersion) {
|
|
151
|
+
const db = getDb();
|
|
152
|
+
await db.run(
|
|
153
|
+
`DELETE FROM node_versions
|
|
154
|
+
WHERE nest_id = ? AND node_id = ? AND status = 'draft' AND version < ?`,
|
|
155
|
+
[nestId, nodeId, belowVersion]
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
async function deleteUnsealedVersions(nestId, nodeId, sealedVersion) {
|
|
159
|
+
const db = getDb();
|
|
160
|
+
await db.run(
|
|
161
|
+
`DELETE FROM node_versions
|
|
162
|
+
WHERE nest_id = ? AND node_id = ? AND version > ?
|
|
163
|
+
AND status IN ('draft', 'rejected', 'pending_review')`,
|
|
164
|
+
[nestId, nodeId, sealedVersion]
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
async function hasUnsealedDraft(nestId, nodeId) {
|
|
168
|
+
const db = getDb();
|
|
169
|
+
const row = await db.get(
|
|
170
|
+
`SELECT status FROM node_versions
|
|
171
|
+
WHERE nest_id = ? AND node_id = ?
|
|
172
|
+
ORDER BY version DESC LIMIT 1`,
|
|
173
|
+
[nestId, nodeId]
|
|
174
|
+
);
|
|
175
|
+
return row?.status === "draft" || row?.status === "rejected" || row?.status === "pending_review";
|
|
176
|
+
}
|
|
139
177
|
async function checkConflict(nestId, nodeId, baseVersion) {
|
|
140
178
|
const db = getDb();
|
|
141
179
|
const current = await db.get(
|
|
@@ -265,20 +303,16 @@ async function listMyDrafts(params) {
|
|
|
265
303
|
WHERE r.nest_id = v.nest_id AND r.node_id = v.node_id
|
|
266
304
|
AND r.status = 'pending')`;
|
|
267
305
|
const args = [params.authorEmail.toLowerCase(), ...params.nestIds];
|
|
268
|
-
const total = Number(
|
|
269
|
-
(await db.get(
|
|
270
|
-
`SELECT COUNT(*) as c FROM node_versions v ${where}`,
|
|
271
|
-
args
|
|
272
|
-
)).c
|
|
273
|
-
);
|
|
274
306
|
const limit = Math.min(Math.max(params.limit ?? 25, 1), 100);
|
|
275
307
|
const offset = Math.max(params.offset ?? 0, 0);
|
|
308
|
+
const q = params.q?.trim().toLowerCase() || "";
|
|
309
|
+
const SCAN_CAP = 500;
|
|
276
310
|
const rows = await db.all(
|
|
277
311
|
`SELECT v.nest_id, v.node_id, v.status, v.version, v.created_at
|
|
278
312
|
FROM node_versions v ${where}
|
|
279
313
|
ORDER BY v.created_at DESC
|
|
280
|
-
LIMIT
|
|
281
|
-
[...args,
|
|
314
|
+
LIMIT ?`,
|
|
315
|
+
[...args, SCAN_CAP]
|
|
282
316
|
);
|
|
283
317
|
const titlesByNest = new Map(
|
|
284
318
|
await Promise.all(
|
|
@@ -290,7 +324,7 @@ async function listMyDrafts(params) {
|
|
|
290
324
|
)
|
|
291
325
|
)
|
|
292
326
|
);
|
|
293
|
-
|
|
327
|
+
let drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
|
|
294
328
|
nestId: r.nest_id,
|
|
295
329
|
nodeId: r.node_id,
|
|
296
330
|
title: titlesByNest.get(r.nest_id).get(r.node_id) || r.node_id,
|
|
@@ -300,7 +334,17 @@ async function listMyDrafts(params) {
|
|
|
300
334
|
version: Number(r.version),
|
|
301
335
|
updatedAt: r.created_at
|
|
302
336
|
}));
|
|
303
|
-
|
|
337
|
+
if (q) {
|
|
338
|
+
drafts = drafts.filter(
|
|
339
|
+
(d) => [d.title, params.nestNames?.get(d.nestId) ?? "", d.nodeId].some(
|
|
340
|
+
(s) => s.toLowerCase().includes(q)
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (params.sort === "title") {
|
|
345
|
+
drafts.sort((a, b) => a.title.localeCompare(b.title));
|
|
346
|
+
}
|
|
347
|
+
return { drafts: drafts.slice(offset, offset + limit), total: drafts.length };
|
|
304
348
|
}
|
|
305
349
|
function rowToVersion(row) {
|
|
306
350
|
return {
|
|
@@ -327,6 +371,10 @@ export {
|
|
|
327
371
|
getApprovedVersion,
|
|
328
372
|
getApprovedVersions,
|
|
329
373
|
setApprovedVersion,
|
|
374
|
+
getSealedVersion,
|
|
375
|
+
deleteSupersededDrafts,
|
|
376
|
+
deleteUnsealedVersions,
|
|
377
|
+
hasUnsealedDraft,
|
|
330
378
|
checkConflict,
|
|
331
379
|
getNodeTags,
|
|
332
380
|
getDisplayStatus,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
config,
|
|
9
9
|
getDb,
|
|
10
10
|
isEmailish
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-SKZ7YYT4.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-WXWER2V7.js");
|
|
2591
|
+
const { documentsWithSuggestions } = await import("./external-edit-service-Y757EFMS.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
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-YVMSM7LS.js";
|
|
4
4
|
import {
|
|
5
5
|
getDb
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SKZ7YYT4.js";
|
|
7
7
|
|
|
8
8
|
// src/governance/grants-service.ts
|
|
9
9
|
import { v4 as uuid } from "uuid";
|
|
@@ -11,7 +11,10 @@ var rank = (r) => r === "write" ? 2 : 1;
|
|
|
11
11
|
async function resolveNodeGrant(nestId, userId, nodeId) {
|
|
12
12
|
if (!userId || !nodeId) return null;
|
|
13
13
|
const rows = await getDb().all(
|
|
14
|
-
|
|
14
|
+
`SELECT g.target, g.role, n.archived_at
|
|
15
|
+
FROM grants g
|
|
16
|
+
JOIN nests n ON n.id = g.nest_id
|
|
17
|
+
WHERE g.nest_id = ? AND g.user_id = ?`,
|
|
15
18
|
[nestId, userId]
|
|
16
19
|
);
|
|
17
20
|
let best = null;
|
|
@@ -20,6 +23,7 @@ async function resolveNodeGrant(nestId, userId, nodeId) {
|
|
|
20
23
|
if (!best || rank(r.role) > rank(best)) best = r.role;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
if (best === "write" && rows[0]?.archived_at) return "read";
|
|
23
27
|
return best;
|
|
24
28
|
}
|
|
25
29
|
async function listUserGrants(nestId, userId) {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveNestPermission,
|
|
12
12
|
resolveTeamRolesForUser,
|
|
13
13
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-G4AL7ETF.js";
|
|
15
15
|
import {
|
|
16
16
|
ConflictError,
|
|
17
17
|
ValidationError
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
config,
|
|
21
21
|
getDb,
|
|
22
22
|
isEmailish
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-SKZ7YYT4.js";
|
|
24
24
|
|
|
25
25
|
// src/governance/stewardship-service.ts
|
|
26
26
|
import { v4 as uuid } from "uuid";
|