@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/dist/index.js
CHANGED
|
@@ -18,8 +18,10 @@ import {
|
|
|
18
18
|
getPendingReview,
|
|
19
19
|
getReviewHistory,
|
|
20
20
|
getReviewQueue,
|
|
21
|
+
isNestTarget,
|
|
21
22
|
isSafeConnectorUrl,
|
|
22
23
|
listDeletionRequests,
|
|
24
|
+
nodeTrail,
|
|
23
25
|
nodeWriteKey,
|
|
24
26
|
notifyDeletionResolved,
|
|
25
27
|
notifyNestEvent,
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
submitForReview,
|
|
36
38
|
withNodeWriteLock,
|
|
37
39
|
withdrawDeletionRequest
|
|
38
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-4KJGQHWB.js";
|
|
39
41
|
import {
|
|
40
42
|
canCreateInNest,
|
|
41
43
|
canManageStewards,
|
|
@@ -55,7 +57,7 @@ import {
|
|
|
55
57
|
resolveUserRoles,
|
|
56
58
|
syncFromConfig,
|
|
57
59
|
updateSteward
|
|
58
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-N2DTJFCD.js";
|
|
59
61
|
import {
|
|
60
62
|
createGrant,
|
|
61
63
|
deleteGrant,
|
|
@@ -64,7 +66,7 @@ import {
|
|
|
64
66
|
listGrants,
|
|
65
67
|
listUserGrants,
|
|
66
68
|
resolveNodeGrant
|
|
67
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-XWGEXQU3.js";
|
|
68
70
|
import {
|
|
69
71
|
generateApiKey,
|
|
70
72
|
getKeyPrefix,
|
|
@@ -77,17 +79,20 @@ import {
|
|
|
77
79
|
import {
|
|
78
80
|
checkConflict,
|
|
79
81
|
createVersion,
|
|
82
|
+
deleteSupersededDrafts,
|
|
83
|
+
deleteUnsealedVersions,
|
|
80
84
|
getApprovedVersion,
|
|
81
85
|
getApprovedVersions,
|
|
82
86
|
getCurrentVersion,
|
|
83
87
|
getDisplayStatus,
|
|
84
88
|
getDisplayStatuses,
|
|
89
|
+
getSealedVersion,
|
|
85
90
|
getTrackedNodeIds,
|
|
86
91
|
getVersions,
|
|
87
92
|
listMyDrafts,
|
|
88
93
|
setApprovedVersion,
|
|
89
94
|
upsertVersion
|
|
90
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-IGPJ74O4.js";
|
|
91
96
|
import {
|
|
92
97
|
approveExternalEdit,
|
|
93
98
|
getExternalEditDetail,
|
|
@@ -99,19 +104,22 @@ import {
|
|
|
99
104
|
scanDocumentForDrift,
|
|
100
105
|
scanNestForDrift,
|
|
101
106
|
startDriftScanner
|
|
102
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-6PJQSKKV.js";
|
|
103
108
|
import {
|
|
104
109
|
addMember,
|
|
105
110
|
addWatcher,
|
|
111
|
+
archiveNest,
|
|
106
112
|
assertSafeNodeId,
|
|
107
113
|
buildTitleMap,
|
|
108
114
|
canApproveWith,
|
|
109
115
|
canEditWith,
|
|
116
|
+
capForArchive,
|
|
110
117
|
collabPermToRole,
|
|
111
118
|
createNest,
|
|
112
119
|
createTeam,
|
|
113
120
|
deleteNest,
|
|
114
121
|
deleteTeam,
|
|
122
|
+
describeNode,
|
|
115
123
|
disableStewardshipAndWipeGovernance,
|
|
116
124
|
docLink,
|
|
117
125
|
engineApi,
|
|
@@ -133,6 +141,7 @@ import {
|
|
|
133
141
|
isEmailConfigured,
|
|
134
142
|
isLicenseAdminEmail,
|
|
135
143
|
isLicenseAdminUserId,
|
|
144
|
+
isNestArchived,
|
|
136
145
|
isPublicReader,
|
|
137
146
|
isServerAdminUserId,
|
|
138
147
|
isStewardshipEnabled,
|
|
@@ -143,6 +152,7 @@ import {
|
|
|
143
152
|
listNotifications,
|
|
144
153
|
listPublicNests,
|
|
145
154
|
listSharedNests,
|
|
155
|
+
listSharedTeamMembers,
|
|
146
156
|
listTeamsForUser,
|
|
147
157
|
listVisibleNests,
|
|
148
158
|
listWatchers,
|
|
@@ -160,8 +170,11 @@ import {
|
|
|
160
170
|
notifyNestInvite,
|
|
161
171
|
nowExpr,
|
|
162
172
|
opContext,
|
|
173
|
+
parseSharedTeams,
|
|
163
174
|
permissionLevel,
|
|
164
175
|
persistSetting,
|
|
176
|
+
pinNest,
|
|
177
|
+
pinnedNestIds,
|
|
165
178
|
primaryRole,
|
|
166
179
|
readIndexedNodes,
|
|
167
180
|
rebuildNodeIndex,
|
|
@@ -177,6 +190,7 @@ import {
|
|
|
177
190
|
resolveNestPermission,
|
|
178
191
|
resolveNodeUrl,
|
|
179
192
|
resolveTeamRolesForUser,
|
|
193
|
+
restoreNest,
|
|
180
194
|
sendEmailToRecipient,
|
|
181
195
|
setAllowSelfApprove,
|
|
182
196
|
setPrimeOnlyReview,
|
|
@@ -189,11 +203,12 @@ import {
|
|
|
189
203
|
syncNodeTags,
|
|
190
204
|
trackEvent,
|
|
191
205
|
uniqueNestName,
|
|
206
|
+
unpinNest,
|
|
192
207
|
unshareTeamFromNest,
|
|
193
208
|
updateMemberRole,
|
|
194
209
|
validateLicense,
|
|
195
210
|
verifySmtp
|
|
196
|
-
} from "./chunk-
|
|
211
|
+
} from "./chunk-6JGQX4GA.js";
|
|
197
212
|
import {
|
|
198
213
|
AppError,
|
|
199
214
|
ConflictError,
|
|
@@ -211,7 +226,7 @@ import {
|
|
|
211
226
|
initDb,
|
|
212
227
|
isEmailListish,
|
|
213
228
|
isEmailish
|
|
214
|
-
} from "./chunk-
|
|
229
|
+
} from "./chunk-LPPKPEYI.js";
|
|
215
230
|
import {
|
|
216
231
|
ANON_EMAIL,
|
|
217
232
|
ANON_USER_ID,
|
|
@@ -1727,6 +1742,10 @@ authRoutes.delete("/users/:userId", async (c) => {
|
|
|
1727
1742
|
target.id
|
|
1728
1743
|
]);
|
|
1729
1744
|
await tx.run("DELETE FROM api_keys WHERE user_id = ?", [target.id]);
|
|
1745
|
+
try {
|
|
1746
|
+
await tx.run("DELETE FROM nest_pins WHERE user_id = ?", [target.id]);
|
|
1747
|
+
} catch {
|
|
1748
|
+
}
|
|
1730
1749
|
await tx.run("DELETE FROM users WHERE id = ?", [target.id]);
|
|
1731
1750
|
});
|
|
1732
1751
|
await deleteAllSessionsForUser(target.id);
|
|
@@ -1870,6 +1889,7 @@ import {
|
|
|
1870
1889
|
filterDocuments,
|
|
1871
1890
|
DocumentNotFoundError,
|
|
1872
1891
|
NODE_TYPES,
|
|
1892
|
+
TAG_PATTERN,
|
|
1873
1893
|
generateIndexMd
|
|
1874
1894
|
} from "@promptowl/contextnest-engine";
|
|
1875
1895
|
import { rename, mkdir, rm, stat } from "fs/promises";
|
|
@@ -1918,6 +1938,158 @@ function documentPrimeFlag(metadata) {
|
|
|
1918
1938
|
return void 0;
|
|
1919
1939
|
}
|
|
1920
1940
|
|
|
1941
|
+
// src/governance/mention-notify.ts
|
|
1942
|
+
import { v4 as uuid4 } from "uuid";
|
|
1943
|
+
|
|
1944
|
+
// src/nests/people-service.ts
|
|
1945
|
+
var PERSON_SOURCES = [
|
|
1946
|
+
"owner",
|
|
1947
|
+
// 0
|
|
1948
|
+
"collaborator",
|
|
1949
|
+
// 1
|
|
1950
|
+
"team",
|
|
1951
|
+
// 2 — not in the union; team rosters are JSON on the team row
|
|
1952
|
+
"steward",
|
|
1953
|
+
// 3
|
|
1954
|
+
"grant"
|
|
1955
|
+
// 4
|
|
1956
|
+
];
|
|
1957
|
+
async function listNestPeople(nestId) {
|
|
1958
|
+
const db = getDb();
|
|
1959
|
+
const [nest, rows] = await Promise.all([
|
|
1960
|
+
db.get("SELECT shared_teams FROM nests WHERE id = ?", [nestId]),
|
|
1961
|
+
db.all(
|
|
1962
|
+
`SELECT 0 AS rank, u.id AS user_id, u.email AS email, u.name AS name
|
|
1963
|
+
FROM nests n JOIN users u ON u.id = n.user_id
|
|
1964
|
+
WHERE n.id = ?
|
|
1965
|
+
UNION ALL
|
|
1966
|
+
SELECT 1, u.id, u.email, u.name
|
|
1967
|
+
FROM nest_collaborators nc JOIN users u ON u.id = nc.user_id
|
|
1968
|
+
WHERE nc.nest_id = ?
|
|
1969
|
+
UNION ALL
|
|
1970
|
+
SELECT 3, u.id, s.user_email, u.name
|
|
1971
|
+
FROM stewards s
|
|
1972
|
+
LEFT JOIN users u ON LOWER(u.email) = LOWER(s.user_email)
|
|
1973
|
+
WHERE s.nest_id = ? AND s.is_active = 1
|
|
1974
|
+
UNION ALL
|
|
1975
|
+
SELECT 4, u.id, u.email, u.name
|
|
1976
|
+
FROM grants g JOIN users u ON u.id = g.user_id
|
|
1977
|
+
WHERE g.nest_id = ?
|
|
1978
|
+
ORDER BY rank`,
|
|
1979
|
+
[nestId, nestId, nestId, nestId]
|
|
1980
|
+
)
|
|
1981
|
+
]);
|
|
1982
|
+
if (!nest) return [];
|
|
1983
|
+
const teamRefs = parseSharedTeams(nest.shared_teams);
|
|
1984
|
+
const teamRows = teamRefs.length ? (await listSharedTeamMembers(teamRefs)).map((m) => ({
|
|
1985
|
+
rank: 2,
|
|
1986
|
+
user_id: m.userId,
|
|
1987
|
+
email: m.email,
|
|
1988
|
+
name: m.name
|
|
1989
|
+
})) : [];
|
|
1990
|
+
const people = [];
|
|
1991
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1992
|
+
for (const row of [...rows, ...teamRows].sort((a, b) => a.rank - b.rank)) {
|
|
1993
|
+
const email = (row.email || "").trim();
|
|
1994
|
+
const key = email.toLowerCase();
|
|
1995
|
+
if (!email || seen.has(key)) continue;
|
|
1996
|
+
seen.add(key);
|
|
1997
|
+
people.push({
|
|
1998
|
+
email,
|
|
1999
|
+
name: row.name ?? null,
|
|
2000
|
+
userId: row.user_id ?? null,
|
|
2001
|
+
source: PERSON_SOURCES[row.rank]
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
return people;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// src/shared/mentions.ts
|
|
2008
|
+
var MENTION = /(?:^|\s)@(?:\(([^)]+)\)|([a-zA-Z0-9._@-]+))/g;
|
|
2009
|
+
function stripCode(markdown2) {
|
|
2010
|
+
return markdown2.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
2011
|
+
}
|
|
2012
|
+
function extractMentions(markdown2) {
|
|
2013
|
+
const names = [];
|
|
2014
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2015
|
+
const text = stripCode(markdown2 || "");
|
|
2016
|
+
let match;
|
|
2017
|
+
MENTION.lastIndex = 0;
|
|
2018
|
+
while ((match = MENTION.exec(text)) !== null) {
|
|
2019
|
+
const name = (match[1] || match[2] || "").trim();
|
|
2020
|
+
const key = name.toLowerCase();
|
|
2021
|
+
if (!name || seen.has(key)) continue;
|
|
2022
|
+
seen.add(key);
|
|
2023
|
+
names.push(name);
|
|
2024
|
+
}
|
|
2025
|
+
return names;
|
|
2026
|
+
}
|
|
2027
|
+
function newMentions(previous, next) {
|
|
2028
|
+
const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
|
|
2029
|
+
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
2030
|
+
}
|
|
2031
|
+
function resolveMention(people, name) {
|
|
2032
|
+
const n = name.trim().toLowerCase();
|
|
2033
|
+
if (!n) return void 0;
|
|
2034
|
+
const exact = people.find((p) => p.email.toLowerCase() === n);
|
|
2035
|
+
if (exact) return exact;
|
|
2036
|
+
const matches = people.filter(
|
|
2037
|
+
(p) => p.email.toLowerCase().split("@")[0] === n || (p.name || "").toLowerCase() === n
|
|
2038
|
+
);
|
|
2039
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/governance/mention-notify.ts
|
|
2043
|
+
async function canOpenNode(nestId, nodeId, userId, email) {
|
|
2044
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
2045
|
+
if (access.permission === "none") {
|
|
2046
|
+
return await resolveNodeGrant(nestId, userId, nodeId) !== null;
|
|
2047
|
+
}
|
|
2048
|
+
return canReadNode(nestId, nodeId, userId, email);
|
|
2049
|
+
}
|
|
2050
|
+
async function notifyMentions(params) {
|
|
2051
|
+
const { nestId, nodeId, previousBody, nextBody, actorEmail } = params;
|
|
2052
|
+
try {
|
|
2053
|
+
const names = newMentions(previousBody, nextBody);
|
|
2054
|
+
if (names.length === 0) return 0;
|
|
2055
|
+
const people = await listNestPeople(nestId);
|
|
2056
|
+
const actor = actorEmail.toLowerCase();
|
|
2057
|
+
const recipients = /* @__PURE__ */ new Map();
|
|
2058
|
+
for (const name of names) {
|
|
2059
|
+
const person = resolveMention(people, name);
|
|
2060
|
+
if (!person?.userId) continue;
|
|
2061
|
+
const key = person.email.toLowerCase();
|
|
2062
|
+
if (key === actor || recipients.has(key)) continue;
|
|
2063
|
+
recipients.set(key, { email: person.email, userId: person.userId });
|
|
2064
|
+
}
|
|
2065
|
+
if (recipients.size === 0) return 0;
|
|
2066
|
+
const label = await describeNode(nestId, nodeId);
|
|
2067
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2068
|
+
const db = getDb();
|
|
2069
|
+
let created = 0;
|
|
2070
|
+
for (const { email, userId } of recipients.values()) {
|
|
2071
|
+
if (!await canOpenNode(nestId, nodeId, userId, email)) continue;
|
|
2072
|
+
await db.run(
|
|
2073
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
2074
|
+
VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
|
|
2075
|
+
[
|
|
2076
|
+
uuid4(),
|
|
2077
|
+
nestId,
|
|
2078
|
+
email.toLowerCase(),
|
|
2079
|
+
nodeId,
|
|
2080
|
+
`${actorEmail} mentioned you in "${label}"`,
|
|
2081
|
+
now
|
|
2082
|
+
]
|
|
2083
|
+
);
|
|
2084
|
+
created++;
|
|
2085
|
+
}
|
|
2086
|
+
return created;
|
|
2087
|
+
} catch (err) {
|
|
2088
|
+
console.error("[notify] mention fan-out failed", nestId, nodeId, err);
|
|
2089
|
+
return 0;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1921
2093
|
// src/nodes/service.ts
|
|
1922
2094
|
async function userIdFromEmail(email) {
|
|
1923
2095
|
const db = getDb();
|
|
@@ -1927,7 +2099,20 @@ async function userIdFromEmail(email) {
|
|
|
1927
2099
|
);
|
|
1928
2100
|
return row?.id ?? ANON_USER_ID;
|
|
1929
2101
|
}
|
|
1930
|
-
var
|
|
2102
|
+
var storedTag = (t) => `#${String(t).trim().replace(/^#+/, "").toLowerCase()}`;
|
|
2103
|
+
function requireTag(raw) {
|
|
2104
|
+
const tag = storedTag(raw);
|
|
2105
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
2106
|
+
throw new ValidationError(
|
|
2107
|
+
`"${String(raw).trim()}" isn't a usable tag \u2014 a tag starts with a letter and is built from letters a-z, digits, "_", ":" and "-".`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
return tag;
|
|
2111
|
+
}
|
|
2112
|
+
function requireTags(raw) {
|
|
2113
|
+
const tags = (raw || []).map((t) => String(t).trim()).filter(Boolean).map(requireTag);
|
|
2114
|
+
return [...new Set(tags)];
|
|
2115
|
+
}
|
|
1931
2116
|
var MAX_TITLE_LENGTH = 120;
|
|
1932
2117
|
function assertTitleLength(title) {
|
|
1933
2118
|
if (title.length > MAX_TITLE_LENGTH) {
|
|
@@ -2000,7 +2185,7 @@ async function departmentTagFor(userEmail) {
|
|
|
2000
2185
|
[userEmail]
|
|
2001
2186
|
);
|
|
2002
2187
|
const slug = slugify(String(row?.department ?? ""));
|
|
2003
|
-
return slug ?
|
|
2188
|
+
return slug ? storedTag(`dept:${slug}`) : null;
|
|
2004
2189
|
} catch (err) {
|
|
2005
2190
|
console.error("[dept-tag] department lookup failed (tag skipped):", err);
|
|
2006
2191
|
return null;
|
|
@@ -2041,7 +2226,11 @@ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2041
2226
|
function toNodeResponse(node) {
|
|
2042
2227
|
const fm = node.frontmatter;
|
|
2043
2228
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
2044
|
-
const tags = Array.isArray(fm.tags) ?
|
|
2229
|
+
const tags = Array.isArray(fm.tags) ? [
|
|
2230
|
+
...new Set(
|
|
2231
|
+
fm.tags.map((t) => String(t).trim()).filter(Boolean).map(storedTag)
|
|
2232
|
+
)
|
|
2233
|
+
] : [];
|
|
2045
2234
|
return {
|
|
2046
2235
|
id: node.id,
|
|
2047
2236
|
title,
|
|
@@ -2376,7 +2565,8 @@ async function nestCensus(nestId, userId) {
|
|
|
2376
2565
|
for (const d of visible) {
|
|
2377
2566
|
const fm = d.frontmatter;
|
|
2378
2567
|
types[fm.type || "document"] = (types[fm.type || "document"] || 0) + 1;
|
|
2379
|
-
for (const t of fm.tags || [])
|
|
2568
|
+
for (const t of new Set((fm.tags || []).map((t2) => storedTag(String(t2)))))
|
|
2569
|
+
tags[t] = (tags[t] || 0) + 1;
|
|
2380
2570
|
const folder = folderOfId(d.id);
|
|
2381
2571
|
if (folder) direct.set(folder, (direct.get(folder) || 0) + 1);
|
|
2382
2572
|
const edited = Date.parse(String(fm.updated_at || ""));
|
|
@@ -2470,7 +2660,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2470
2660
|
}
|
|
2471
2661
|
}
|
|
2472
2662
|
async function createNodeOnce(nestId, input, userEmail) {
|
|
2473
|
-
const { storage
|
|
2663
|
+
const { storage } = await engineCache.get(nestId);
|
|
2474
2664
|
assertTitleLength(input.title);
|
|
2475
2665
|
assertTitleReadable(input.title);
|
|
2476
2666
|
const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
|
|
@@ -2480,7 +2670,7 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2480
2670
|
const prefix = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/` : "nodes/";
|
|
2481
2671
|
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
2482
2672
|
}
|
|
2483
|
-
const tags = (input.tags
|
|
2673
|
+
const tags = requireTags(input.tags);
|
|
2484
2674
|
const requestedType = input.type || "document";
|
|
2485
2675
|
if (requestedType === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
|
|
2486
2676
|
throw new ValidationError("Artifact nodes are disabled on this server");
|
|
@@ -2527,7 +2717,7 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2527
2717
|
});
|
|
2528
2718
|
}
|
|
2529
2719
|
const deptTag = await departmentTagFor(userEmail);
|
|
2530
|
-
if (deptTag && !tags.
|
|
2720
|
+
if (deptTag && !tags.includes(deptTag)) {
|
|
2531
2721
|
tags.push(deptTag);
|
|
2532
2722
|
}
|
|
2533
2723
|
if (input.prime !== void 0) await assertMayFlagPrime(nestId, userEmail);
|
|
@@ -2591,13 +2781,6 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2591
2781
|
});
|
|
2592
2782
|
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
2593
2783
|
} else {
|
|
2594
|
-
if (publishReason === null) {
|
|
2595
|
-
try {
|
|
2596
|
-
await versionManager.createVersion(node, userEmail);
|
|
2597
|
-
} catch (err) {
|
|
2598
|
-
console.error("VersionManager.createVersion failed (node create)", err);
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
2784
|
await createVersion({
|
|
2602
2785
|
nestId,
|
|
2603
2786
|
nodeId: id,
|
|
@@ -2609,6 +2792,13 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2609
2792
|
});
|
|
2610
2793
|
}
|
|
2611
2794
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
2795
|
+
await notifyMentions({
|
|
2796
|
+
nestId,
|
|
2797
|
+
nodeId: id,
|
|
2798
|
+
previousBody: "",
|
|
2799
|
+
nextBody: node.body || "",
|
|
2800
|
+
actorEmail: userEmail
|
|
2801
|
+
});
|
|
2612
2802
|
return { node, version: savedVersion };
|
|
2613
2803
|
});
|
|
2614
2804
|
}
|
|
@@ -2671,7 +2861,7 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
2671
2861
|
const engineMs = Date.now() - startedAt;
|
|
2672
2862
|
const dbStartedAt = Date.now();
|
|
2673
2863
|
for (const d of docs) {
|
|
2674
|
-
const tags = d.tags.map((t) =>
|
|
2864
|
+
const tags = d.tags.map((t) => storedTag(String(t)));
|
|
2675
2865
|
await createVersion({
|
|
2676
2866
|
nestId,
|
|
2677
2867
|
nodeId: d.id,
|
|
@@ -2714,6 +2904,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2714
2904
|
} catch {
|
|
2715
2905
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
2716
2906
|
}
|
|
2907
|
+
const previousBody = node.body || "";
|
|
2717
2908
|
const opInput = {};
|
|
2718
2909
|
if (patch.content !== void 0) {
|
|
2719
2910
|
node = { ...node, body: patch.content };
|
|
@@ -2724,7 +2915,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2724
2915
|
opInput.append = "\n" + patch.append;
|
|
2725
2916
|
}
|
|
2726
2917
|
if (patch.tags) {
|
|
2727
|
-
const nextTags =
|
|
2918
|
+
const nextTags = requireTags(patch.tags);
|
|
2728
2919
|
node = { ...node, frontmatter: { ...node.frontmatter, tags: nextTags } };
|
|
2729
2920
|
opInput.tags = nextTags;
|
|
2730
2921
|
}
|
|
@@ -2801,17 +2992,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2801
2992
|
);
|
|
2802
2993
|
node = await storage.readDocument(nodeId);
|
|
2803
2994
|
await syncNodeTags(nestId, nodeId, currentTags);
|
|
2804
|
-
|
|
2805
|
-
await versionManager.createVersion(node, userEmail, {
|
|
2806
|
-
note: patch.changeNote
|
|
2807
|
-
});
|
|
2808
|
-
} catch (err) {
|
|
2809
|
-
console.error(
|
|
2810
|
-
"VersionManager.createVersion failed (node patch)",
|
|
2811
|
-
err
|
|
2812
|
-
);
|
|
2813
|
-
throw err;
|
|
2814
|
-
}
|
|
2995
|
+
await deleteSupersededDrafts(nestId, nodeId, newVersion);
|
|
2815
2996
|
await createVersion({
|
|
2816
2997
|
nestId,
|
|
2817
2998
|
nodeId,
|
|
@@ -2863,6 +3044,13 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2863
3044
|
);
|
|
2864
3045
|
responseVersion = publishedVersion;
|
|
2865
3046
|
}
|
|
3047
|
+
await notifyMentions({
|
|
3048
|
+
nestId,
|
|
3049
|
+
nodeId,
|
|
3050
|
+
previousBody,
|
|
3051
|
+
nextBody: node.body || "",
|
|
3052
|
+
actorEmail: userEmail
|
|
3053
|
+
});
|
|
2866
3054
|
return { node, version: responseVersion };
|
|
2867
3055
|
} catch (err) {
|
|
2868
3056
|
throw toSafeError(
|
|
@@ -3019,6 +3207,43 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
3019
3207
|
return { node: moved, previousId: nodeId };
|
|
3020
3208
|
});
|
|
3021
3209
|
}
|
|
3210
|
+
async function discardDraft(nestId, nodeId, userEmail) {
|
|
3211
|
+
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
3212
|
+
await assertNotReviewLocked(nestId, nodeId, userEmail, "discarding");
|
|
3213
|
+
const sealed = await getSealedVersion(nestId, nodeId);
|
|
3214
|
+
if (sealed == null) {
|
|
3215
|
+
await deleteNode(nestId, nodeId, userEmail);
|
|
3216
|
+
return { deleted: true, version: null };
|
|
3217
|
+
}
|
|
3218
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
3219
|
+
let raw;
|
|
3220
|
+
try {
|
|
3221
|
+
raw = await versionManager.reconstructVersion(nodeId, sealed);
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
console.error(
|
|
3224
|
+
"reconstructVersion failed (discard draft)",
|
|
3225
|
+
nestId,
|
|
3226
|
+
nodeId,
|
|
3227
|
+
sealed,
|
|
3228
|
+
err
|
|
3229
|
+
);
|
|
3230
|
+
throw new ValidationError(
|
|
3231
|
+
`Can't restore version ${sealed} of this document \u2014 its history no longer replays. Delete the document instead.`
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
await storage.writeDocument(nodeId, raw);
|
|
3235
|
+
await storage.regenerateIndex();
|
|
3236
|
+
const restored = parseDocument(`${nodeId}.md`, raw, nodeId);
|
|
3237
|
+
await syncNodeTags(
|
|
3238
|
+
nestId,
|
|
3239
|
+
nodeId,
|
|
3240
|
+
(restored.frontmatter.tags || []).map(storedTag)
|
|
3241
|
+
);
|
|
3242
|
+
await deleteUnsealedVersions(nestId, nodeId, sealed);
|
|
3243
|
+
await trackEvent("node.discard_draft", { nestId, nodeId, version: sealed });
|
|
3244
|
+
return { deleted: false, version: sealed };
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3022
3247
|
async function deleteNode(nestId, nodeId, userEmail) {
|
|
3023
3248
|
const { storage } = await engineCache.get(nestId);
|
|
3024
3249
|
await assertNotReviewLocked(nestId, nodeId, userEmail, "deleting");
|
|
@@ -3325,7 +3550,10 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3325
3550
|
async function effectiveAccess(nestId, userId) {
|
|
3326
3551
|
const access = await resolveNestAccess(nestId, userId);
|
|
3327
3552
|
if (config.AUTH_MODE === "open" && access.ownerId === ANON_USER_ID) {
|
|
3328
|
-
return {
|
|
3553
|
+
return {
|
|
3554
|
+
permission: capForArchive("owner", access.archived),
|
|
3555
|
+
access: { ...access, rawPermission: "owner" }
|
|
3556
|
+
};
|
|
3329
3557
|
}
|
|
3330
3558
|
const permission = access.permission;
|
|
3331
3559
|
if (permission === "none" && access.hasGrant) {
|
|
@@ -3342,12 +3570,14 @@ function isGrantOnly(access) {
|
|
|
3342
3570
|
var nestRoutes = new Hono3();
|
|
3343
3571
|
nestRoutes.get("/", async (c) => {
|
|
3344
3572
|
const userId = c.get("userId");
|
|
3345
|
-
const
|
|
3346
|
-
const
|
|
3347
|
-
const
|
|
3573
|
+
const archivedOnly = c.req.query("archived") === "1";
|
|
3574
|
+
const owned = await listNests(userId, { archived: archivedOnly });
|
|
3575
|
+
const shared = archivedOnly ? [] : await listSharedNests(userId);
|
|
3576
|
+
const publicExtras = archivedOnly ? [] : await listPublicNests(userId);
|
|
3348
3577
|
const db = getDb();
|
|
3349
3578
|
const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
|
|
3350
3579
|
const callerEmail = await resolveCallerEmail(userId);
|
|
3580
|
+
const pinned = await pinnedNestIds(userId);
|
|
3351
3581
|
const myGrantRows = await db.all(
|
|
3352
3582
|
"SELECT nest_id, target FROM grants WHERE user_id = ?",
|
|
3353
3583
|
[userId]
|
|
@@ -3370,10 +3600,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
3370
3600
|
owner_email: owner_email2,
|
|
3371
3601
|
roles: ["viewer"],
|
|
3372
3602
|
grant_only: true,
|
|
3373
|
-
stewardship_enabled: !!n.stewardship_enabled
|
|
3603
|
+
stewardship_enabled: !!n.stewardship_enabled,
|
|
3604
|
+
pinned: pinned.has(n.id)
|
|
3374
3605
|
};
|
|
3375
3606
|
}
|
|
3376
|
-
const is_owner =
|
|
3607
|
+
const is_owner = access.rawPermission === "owner";
|
|
3377
3608
|
const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
|
|
3378
3609
|
const [roles, owner_email] = await Promise.all([
|
|
3379
3610
|
is_owner ? Promise.resolve(["owner"]) : resolveGrantRoles(n.id, callerEmail, userId),
|
|
@@ -3388,7 +3619,8 @@ nestRoutes.get("/", async (c) => {
|
|
|
3388
3619
|
owner_email,
|
|
3389
3620
|
roles,
|
|
3390
3621
|
grant_only: false,
|
|
3391
|
-
stewardship_enabled: !!n.stewardship_enabled
|
|
3622
|
+
stewardship_enabled: !!n.stewardship_enabled,
|
|
3623
|
+
pinned: pinned.has(n.id)
|
|
3392
3624
|
};
|
|
3393
3625
|
};
|
|
3394
3626
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3610,24 +3842,56 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
3610
3842
|
});
|
|
3611
3843
|
return c.json({ nest });
|
|
3612
3844
|
});
|
|
3613
|
-
nestRoutes.
|
|
3845
|
+
nestRoutes.post("/:nestId/pin", async (c) => {
|
|
3846
|
+
const nestId = c.req.param("nestId");
|
|
3847
|
+
const userId = c.get("userId");
|
|
3848
|
+
if (await effectivePermission(nestId, userId) === "none") {
|
|
3849
|
+
throw new NotFoundError("Nest not found");
|
|
3850
|
+
}
|
|
3851
|
+
await pinNest(userId, nestId);
|
|
3852
|
+
return c.json({ pinned: true });
|
|
3853
|
+
});
|
|
3854
|
+
nestRoutes.delete("/:nestId/pin", async (c) => {
|
|
3614
3855
|
const nestId = c.req.param("nestId");
|
|
3615
3856
|
const userId = c.get("userId");
|
|
3857
|
+
if (await effectivePermission(nestId, userId) === "none") {
|
|
3858
|
+
throw new NotFoundError("Nest not found");
|
|
3859
|
+
}
|
|
3860
|
+
await unpinNest(userId, nestId);
|
|
3861
|
+
return c.json({ pinned: false });
|
|
3862
|
+
});
|
|
3863
|
+
async function requireNestCaretaker(nestId, userId, verb) {
|
|
3616
3864
|
const nest = await getNest(nestId);
|
|
3617
3865
|
if (!nest) {
|
|
3618
3866
|
throw new NotFoundError("Nest not found");
|
|
3619
3867
|
}
|
|
3620
|
-
const
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
if (permission !== "owner" && !adminCaretaker) {
|
|
3868
|
+
const { access } = await effectiveAccess(nestId, userId);
|
|
3869
|
+
const adminCaretaker = config.AUTH_MODE !== "open" && nest.user_id === ANON_USER_ID && await isLicenseAdminUserId(userId);
|
|
3870
|
+
if (access.rawPermission !== "owner" && !adminCaretaker) {
|
|
3624
3871
|
throw new ForbiddenError(
|
|
3625
|
-
|
|
3872
|
+
`You don't have permission to ${verb} this nest. Only the nest owner can ${verb} it.`
|
|
3626
3873
|
);
|
|
3627
3874
|
}
|
|
3875
|
+
return nest;
|
|
3876
|
+
}
|
|
3877
|
+
nestRoutes.delete("/:nestId", async (c) => {
|
|
3878
|
+
const nestId = c.req.param("nestId");
|
|
3879
|
+
await requireNestCaretaker(nestId, c.get("userId"), "delete");
|
|
3628
3880
|
await deleteNest(nestId);
|
|
3629
3881
|
return c.json({ deleted: true });
|
|
3630
3882
|
});
|
|
3883
|
+
nestRoutes.post("/:nestId/archive", async (c) => {
|
|
3884
|
+
const nestId = c.req.param("nestId");
|
|
3885
|
+
await requireNestCaretaker(nestId, c.get("userId"), "archive");
|
|
3886
|
+
await archiveNest(nestId);
|
|
3887
|
+
return c.json({ archived: true });
|
|
3888
|
+
});
|
|
3889
|
+
nestRoutes.post("/:nestId/restore", async (c) => {
|
|
3890
|
+
const nestId = c.req.param("nestId");
|
|
3891
|
+
await requireNestCaretaker(nestId, c.get("userId"), "restore");
|
|
3892
|
+
await restoreNest(nestId);
|
|
3893
|
+
return c.json({ restored: true });
|
|
3894
|
+
});
|
|
3631
3895
|
nestRoutes.get("/:nestId/settings", async (c) => {
|
|
3632
3896
|
const nestId = c.req.param("nestId");
|
|
3633
3897
|
const permission = await effectivePermission(nestId, c.get("userId"));
|
|
@@ -3696,7 +3960,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
3696
3960
|
import { Hono as Hono4 } from "hono";
|
|
3697
3961
|
|
|
3698
3962
|
// src/nests/sharing-service.ts
|
|
3699
|
-
import { v4 as
|
|
3963
|
+
import { v4 as uuid5 } from "uuid";
|
|
3700
3964
|
var VALID_PERMISSIONS = ["read", "write", "admin"];
|
|
3701
3965
|
async function addCollaborator(params) {
|
|
3702
3966
|
const { nestId } = params;
|
|
@@ -3723,10 +3987,10 @@ async function addCollaborator(params) {
|
|
|
3723
3987
|
userId = existing.id;
|
|
3724
3988
|
} else {
|
|
3725
3989
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3726
|
-
userId =
|
|
3990
|
+
userId = uuid5();
|
|
3727
3991
|
await db.run(
|
|
3728
3992
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3729
|
-
[userId, email, null, await hashPassword2(
|
|
3993
|
+
[userId, email, null, await hashPassword2(uuid5())]
|
|
3730
3994
|
);
|
|
3731
3995
|
}
|
|
3732
3996
|
}
|
|
@@ -3764,7 +4028,7 @@ async function addCollaborator(params) {
|
|
|
3764
4028
|
[params.grantedByEmail]
|
|
3765
4029
|
))?.id : void 0;
|
|
3766
4030
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
3767
|
-
const collabId =
|
|
4031
|
+
const collabId = uuid5();
|
|
3768
4032
|
await db.run(
|
|
3769
4033
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
3770
4034
|
[collabId, nestId, userId, params.permission, granterId]
|
|
@@ -3835,7 +4099,10 @@ sharingRoutes.get("/collaborators", async (c) => {
|
|
|
3835
4099
|
roles: await resolveUserRoles(nestId, collab.email)
|
|
3836
4100
|
});
|
|
3837
4101
|
}
|
|
3838
|
-
return c.json({
|
|
4102
|
+
return c.json({
|
|
4103
|
+
collaborators: enriched,
|
|
4104
|
+
people: await listNestPeople(nestId)
|
|
4105
|
+
});
|
|
3839
4106
|
});
|
|
3840
4107
|
sharingRoutes.post("/collaborators", async (c) => {
|
|
3841
4108
|
const body = await c.req.json();
|
|
@@ -3955,7 +4222,7 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
3955
4222
|
|
|
3956
4223
|
// src/nests/grant-routes.ts
|
|
3957
4224
|
import { Hono as Hono5 } from "hono";
|
|
3958
|
-
import { v4 as
|
|
4225
|
+
import { v4 as uuid6 } from "uuid";
|
|
3959
4226
|
var grantRoutes = new Hono5();
|
|
3960
4227
|
async function requireManage(c, nestId) {
|
|
3961
4228
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
@@ -3970,10 +4237,10 @@ async function resolveGrantee(email) {
|
|
|
3970
4237
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3971
4238
|
if (existing) return existing.id;
|
|
3972
4239
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3973
|
-
const id =
|
|
4240
|
+
const id = uuid6();
|
|
3974
4241
|
await db.run(
|
|
3975
4242
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3976
|
-
[id, e, null, await hashPassword2(
|
|
4243
|
+
[id, e, null, await hashPassword2(uuid6())]
|
|
3977
4244
|
);
|
|
3978
4245
|
return id;
|
|
3979
4246
|
}
|
|
@@ -4075,7 +4342,7 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4075
4342
|
import { Hono as Hono6 } from "hono";
|
|
4076
4343
|
|
|
4077
4344
|
// src/nests/publish-service.ts
|
|
4078
|
-
import { v4 as
|
|
4345
|
+
import { v4 as uuid7 } from "uuid";
|
|
4079
4346
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
4080
4347
|
var MODES = ["public", "email", "code", "invite"];
|
|
4081
4348
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -4138,8 +4405,13 @@ async function createPublish(params) {
|
|
|
4138
4405
|
if (!await nodeExists(nestId, nodeId)) {
|
|
4139
4406
|
throw new ValidationError(`No such document in this nest: ${nodeId}`);
|
|
4140
4407
|
}
|
|
4408
|
+
if (await getApprovedVersion(nestId, nodeId) == null) {
|
|
4409
|
+
throw new ValidationError(
|
|
4410
|
+
"This document has no approved version, so a link to it would show nothing. Get it approved first, then publish."
|
|
4411
|
+
);
|
|
4412
|
+
}
|
|
4141
4413
|
const db = getDb();
|
|
4142
|
-
const id =
|
|
4414
|
+
const id = uuid7();
|
|
4143
4415
|
const slug = newSlug();
|
|
4144
4416
|
const created = nowIso();
|
|
4145
4417
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -4170,7 +4442,7 @@ async function createPublish(params) {
|
|
|
4170
4442
|
await db.run(
|
|
4171
4443
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4172
4444
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
4173
|
-
[
|
|
4445
|
+
[uuid7(), id, email, created]
|
|
4174
4446
|
);
|
|
4175
4447
|
}
|
|
4176
4448
|
}
|
|
@@ -4179,7 +4451,12 @@ async function createPublish(params) {
|
|
|
4179
4451
|
return row;
|
|
4180
4452
|
}
|
|
4181
4453
|
async function getPublishBySlug(slug) {
|
|
4182
|
-
return await getDb().get(
|
|
4454
|
+
return await getDb().get(
|
|
4455
|
+
`SELECT p.* FROM publish p
|
|
4456
|
+
JOIN nests n ON n.id = p.nest_id
|
|
4457
|
+
WHERE p.slug = ? AND n.archived_at IS NULL`,
|
|
4458
|
+
[slug]
|
|
4459
|
+
);
|
|
4183
4460
|
}
|
|
4184
4461
|
async function getPublishById(nestId, id) {
|
|
4185
4462
|
return await getDb().get("SELECT * FROM publish WHERE id = ? AND nest_id = ?", [
|
|
@@ -4220,7 +4497,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
4220
4497
|
await getDb().run(
|
|
4221
4498
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4222
4499
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
4223
|
-
[
|
|
4500
|
+
[uuid7(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
4224
4501
|
);
|
|
4225
4502
|
return token;
|
|
4226
4503
|
}
|
|
@@ -4250,7 +4527,7 @@ async function createSession2(publishId, email) {
|
|
|
4250
4527
|
await getDb().run(
|
|
4251
4528
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4252
4529
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
4253
|
-
[
|
|
4530
|
+
[uuid7(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
4254
4531
|
);
|
|
4255
4532
|
return token;
|
|
4256
4533
|
}
|
|
@@ -4267,7 +4544,7 @@ async function logView(publishId, email) {
|
|
|
4267
4544
|
await getDb().run(
|
|
4268
4545
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4269
4546
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
4270
|
-
[
|
|
4547
|
+
[uuid7(), publishId, email, nowIso()]
|
|
4271
4548
|
);
|
|
4272
4549
|
}
|
|
4273
4550
|
async function listAllowlist(publishId) {
|
|
@@ -4360,7 +4637,7 @@ import { Hono as Hono7 } from "hono";
|
|
|
4360
4637
|
import MarkdownIt from "markdown-it";
|
|
4361
4638
|
|
|
4362
4639
|
// src/annotations/service.ts
|
|
4363
|
-
import { v4 as
|
|
4640
|
+
import { v4 as uuid8 } from "uuid";
|
|
4364
4641
|
|
|
4365
4642
|
// src/annotations/projection.ts
|
|
4366
4643
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -4510,7 +4787,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4510
4787
|
throw new Error("comment body is required");
|
|
4511
4788
|
}
|
|
4512
4789
|
const db = getDb();
|
|
4513
|
-
const id =
|
|
4790
|
+
const id = uuid8();
|
|
4514
4791
|
const anchor = clampAnchor(input.anchor);
|
|
4515
4792
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
4516
4793
|
await db.transaction(async (tx) => {
|
|
@@ -4529,7 +4806,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4529
4806
|
);
|
|
4530
4807
|
await tx.run(
|
|
4531
4808
|
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4532
|
-
[
|
|
4809
|
+
[uuid8(), id, authorEmail, body]
|
|
4533
4810
|
);
|
|
4534
4811
|
});
|
|
4535
4812
|
return rowToThread(await getThreadRow(id));
|
|
@@ -4549,7 +4826,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
4549
4826
|
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
4550
4827
|
await getDb().run(
|
|
4551
4828
|
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
4552
|
-
[
|
|
4829
|
+
[uuid8(), threadId, authorEmail, trimmed]
|
|
4553
4830
|
);
|
|
4554
4831
|
return rowToThread(await getThreadRow(threadId));
|
|
4555
4832
|
}
|
|
@@ -4569,6 +4846,21 @@ async function setThreadStatus(nestId, nodeId, threadId, status, byEmail) {
|
|
|
4569
4846
|
}
|
|
4570
4847
|
return rowToThread(await getThreadRow(threadId));
|
|
4571
4848
|
}
|
|
4849
|
+
async function resolveHostedNodeId(nestId, nodeId) {
|
|
4850
|
+
if (nodeId.startsWith("nodes/")) return nodeId;
|
|
4851
|
+
const row = await getDb().get(
|
|
4852
|
+
"SELECT is_imported FROM nests WHERE id = ?",
|
|
4853
|
+
[nestId]
|
|
4854
|
+
);
|
|
4855
|
+
if (!row?.is_imported) return `nodes/${nodeId}`;
|
|
4856
|
+
const { storage } = await engineCache.get(nestId);
|
|
4857
|
+
try {
|
|
4858
|
+
await storage.readDocument(nodeId);
|
|
4859
|
+
return nodeId;
|
|
4860
|
+
} catch {
|
|
4861
|
+
return `nodes/${nodeId}`;
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4572
4864
|
async function readArtifact(nestId, nodeId, version) {
|
|
4573
4865
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
4574
4866
|
const target = version ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
@@ -5525,7 +5817,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
5525
5817
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
5526
5818
|
const nestId = c.req.param("nestId");
|
|
5527
5819
|
const nodeId = c.req.param("nodeId");
|
|
5528
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
5820
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-2YKGBP63.js");
|
|
5529
5821
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
5530
5822
|
nestId,
|
|
5531
5823
|
nodeId
|
|
@@ -5546,10 +5838,24 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
5546
5838
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
5547
5839
|
const nestId = c.req.param("nestId");
|
|
5548
5840
|
const nodeId = c.req.param("nodeId");
|
|
5549
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
5841
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-6JK64IRT.js");
|
|
5550
5842
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
5551
5843
|
return c.json({ reviews: history });
|
|
5552
5844
|
});
|
|
5845
|
+
nodeRoutes.post("/:nodeId{.+}/discard", async (c) => {
|
|
5846
|
+
const nestId = c.req.param("nestId");
|
|
5847
|
+
const nodeId = c.req.param("nodeId");
|
|
5848
|
+
const userId = c.get("userId");
|
|
5849
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
5850
|
+
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
5851
|
+
return c.json(
|
|
5852
|
+
{ error: "Access denied \u2014 no steward assignment for this node" },
|
|
5853
|
+
403
|
|
5854
|
+
);
|
|
5855
|
+
}
|
|
5856
|
+
const result = await discardDraft(nestId, nodeId, userEmail);
|
|
5857
|
+
return c.json({ ok: true, ...result });
|
|
5858
|
+
});
|
|
5553
5859
|
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
5554
5860
|
const nestId = c.req.param("nestId");
|
|
5555
5861
|
const nodeId = c.req.param("nodeId");
|
|
@@ -5873,7 +6179,7 @@ annotationRoutes.post("/:nodeId{.+}/annotations/:threadId/reopen", async (c) =>
|
|
|
5873
6179
|
});
|
|
5874
6180
|
annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
5875
6181
|
const nestId = c.req.param("nestId");
|
|
5876
|
-
const nodeId = getNodeId(c);
|
|
6182
|
+
const nodeId = await resolveHostedNodeId(nestId, getNodeId(c));
|
|
5877
6183
|
const userId = c.get("userId");
|
|
5878
6184
|
const userEmail = await resolveCallerEmail(userId);
|
|
5879
6185
|
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
@@ -7172,10 +7478,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
7172
7478
|
}
|
|
7173
7479
|
|
|
7174
7480
|
// src/mcp/workflow-tools.ts
|
|
7175
|
-
import { v4 as
|
|
7481
|
+
import { v4 as uuid11 } from "uuid";
|
|
7176
7482
|
|
|
7177
7483
|
// src/workflow/run-service.ts
|
|
7178
|
-
import { v4 as
|
|
7484
|
+
import { v4 as uuid9 } from "uuid";
|
|
7179
7485
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
7180
7486
|
async function flowSubgraph(nestId, agentNode) {
|
|
7181
7487
|
const db = getDb();
|
|
@@ -7305,7 +7611,7 @@ async function triggerRun(opts) {
|
|
|
7305
7611
|
}
|
|
7306
7612
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
7307
7613
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
7308
|
-
const id = `run_${
|
|
7614
|
+
const id = `run_${uuid9()}`;
|
|
7309
7615
|
const db = getDb();
|
|
7310
7616
|
await db.transaction(async (tx) => {
|
|
7311
7617
|
if (parentRunId) {
|
|
@@ -7492,7 +7798,7 @@ async function listRuns(nestId, opts) {
|
|
|
7492
7798
|
|
|
7493
7799
|
// src/workflow/edge-routes.ts
|
|
7494
7800
|
import { Hono as Hono13 } from "hono";
|
|
7495
|
-
import { v4 as
|
|
7801
|
+
import { v4 as uuid10 } from "uuid";
|
|
7496
7802
|
function toResponse(e, t) {
|
|
7497
7803
|
return {
|
|
7498
7804
|
id: e.id,
|
|
@@ -7553,7 +7859,7 @@ async function validateCondition(nestId, mode, condition) {
|
|
|
7553
7859
|
async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
7554
7860
|
if (fromNode === toNode) {
|
|
7555
7861
|
throw new ConflictError(
|
|
7556
|
-
`cycle:
|
|
7862
|
+
`cycle: ${await nodeTrail(db, nestId, [fromNode])} cannot flow into itself`
|
|
7557
7863
|
);
|
|
7558
7864
|
}
|
|
7559
7865
|
const rows = await db.all(
|
|
@@ -7582,7 +7888,7 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
|
7582
7888
|
}
|
|
7583
7889
|
path.reverse().push(toNode);
|
|
7584
7890
|
throw new ConflictError(
|
|
7585
|
-
`cycle: ${
|
|
7891
|
+
`cycle: ${await nodeTrail(db, nestId, path)} \u2014 flow edges must form a DAG`
|
|
7586
7892
|
);
|
|
7587
7893
|
}
|
|
7588
7894
|
for (const nb of adj.get(cur) ?? []) {
|
|
@@ -7674,7 +7980,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
7674
7980
|
);
|
|
7675
7981
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
7676
7982
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7677
|
-
const id =
|
|
7983
|
+
const id = uuid10();
|
|
7678
7984
|
await db.transaction(async (tx) => {
|
|
7679
7985
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
7680
7986
|
if (type.is_flow) {
|
|
@@ -8075,7 +8381,7 @@ async function createEdge(ctx, args) {
|
|
|
8075
8381
|
);
|
|
8076
8382
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
8077
8383
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8078
|
-
const id =
|
|
8384
|
+
const id = uuid11();
|
|
8079
8385
|
await db.transaction(async (tx) => {
|
|
8080
8386
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
8081
8387
|
if (type.is_flow) {
|
|
@@ -8139,7 +8445,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
8139
8445
|
`INSERT INTO edge_types
|
|
8140
8446
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
8141
8447
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8142
|
-
[
|
|
8448
|
+
[uuid11(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
8143
8449
|
);
|
|
8144
8450
|
}
|
|
8145
8451
|
});
|
|
@@ -8172,6 +8478,13 @@ async function deleteEdgeType(nestId, id) {
|
|
|
8172
8478
|
|
|
8173
8479
|
// src/mcp/tools.ts
|
|
8174
8480
|
var MAX_HOPS = 10;
|
|
8481
|
+
var SEARCH_DEFAULT_LIMIT = 20;
|
|
8482
|
+
var SEARCH_MAX_LIMIT = 200;
|
|
8483
|
+
function normalizeSearchLimit(raw) {
|
|
8484
|
+
const n = Math.floor(Number(raw));
|
|
8485
|
+
if (!Number.isFinite(n) || n < 1) return SEARCH_DEFAULT_LIMIT;
|
|
8486
|
+
return Math.min(n, SEARCH_MAX_LIMIT);
|
|
8487
|
+
}
|
|
8175
8488
|
function normalizeHops(raw) {
|
|
8176
8489
|
if (raw == null) return 2;
|
|
8177
8490
|
const n = Number(raw);
|
|
@@ -8215,11 +8528,15 @@ var TOOL_DEFINITIONS = [
|
|
|
8215
8528
|
},
|
|
8216
8529
|
{
|
|
8217
8530
|
name: "context_search",
|
|
8218
|
-
description: "Full-text keyword search across all node content, titles, tags, and metadata.",
|
|
8531
|
+
description: "Full-text keyword search across all node content, titles, tags, and metadata \u2014 the same index-backed match as REST GET /search. Returns `count` (everything found) and the first `limit` hits, title matches first.",
|
|
8219
8532
|
inputSchema: {
|
|
8220
8533
|
type: "object",
|
|
8221
8534
|
properties: {
|
|
8222
|
-
query: { type: "string", description: "Search terms" }
|
|
8535
|
+
query: { type: "string", description: "Search terms" },
|
|
8536
|
+
limit: {
|
|
8537
|
+
type: "number",
|
|
8538
|
+
description: "Max hits to return (default 20, max 200)"
|
|
8539
|
+
}
|
|
8223
8540
|
},
|
|
8224
8541
|
required: ["query"]
|
|
8225
8542
|
}
|
|
@@ -8675,12 +8992,21 @@ var TOOL_DEFINITIONS = [
|
|
|
8675
8992
|
}
|
|
8676
8993
|
}
|
|
8677
8994
|
];
|
|
8678
|
-
async function
|
|
8679
|
-
|
|
8995
|
+
async function batchedBodyGate(nestId) {
|
|
8996
|
+
const stewardshipEnabled = await isStewardshipEnabled(nestId);
|
|
8997
|
+
return {
|
|
8998
|
+
accessChecked: true,
|
|
8999
|
+
stewardshipEnabled,
|
|
9000
|
+
approvedVersions: stewardshipEnabled ? await getApprovedVersions(nestId) : null
|
|
9001
|
+
};
|
|
9002
|
+
}
|
|
9003
|
+
async function resolveLlmBody(ctx, node, gate) {
|
|
9004
|
+
if (!gate?.accessChecked && !await canReadNode(ctx.nestId, node.id, ctx.userId, ctx.userEmail)) {
|
|
8680
9005
|
return null;
|
|
8681
9006
|
}
|
|
8682
|
-
|
|
8683
|
-
|
|
9007
|
+
const stewardship = gate ? gate.stewardshipEnabled : await isStewardshipEnabled(ctx.nestId);
|
|
9008
|
+
if (!stewardship) return node.body || "";
|
|
9009
|
+
const approved = gate ? gate.approvedVersions?.get(node.id) ?? null : await getApprovedVersion(ctx.nestId, node.id);
|
|
8684
9010
|
if (approved == null) return null;
|
|
8685
9011
|
try {
|
|
8686
9012
|
return await ctx.versionManager.reconstructVersion(node.id, approved);
|
|
@@ -8787,39 +9113,46 @@ ${fmt(tags)}
|
|
|
8787
9113
|
${nodeList}`;
|
|
8788
9114
|
}
|
|
8789
9115
|
case "context_search": {
|
|
8790
|
-
const
|
|
8791
|
-
const
|
|
8792
|
-
const
|
|
8793
|
-
|
|
9116
|
+
const query = String(args.query ?? "");
|
|
9117
|
+
const limit = normalizeSearchLimit(args.limit);
|
|
9118
|
+
const matches = matchDocuments(await discoverForSearch(storage), query).filter((n) => !isDerivedAnnotationsNode(n.id));
|
|
9119
|
+
const accessible = await filterAccessible(
|
|
9120
|
+
nestId,
|
|
9121
|
+
userId,
|
|
9122
|
+
userEmail,
|
|
9123
|
+
matches
|
|
8794
9124
|
);
|
|
8795
|
-
const
|
|
8796
|
-
|
|
8797
|
-
const hay = [
|
|
8798
|
-
n.frontmatter.title,
|
|
8799
|
-
body || "",
|
|
8800
|
-
n.frontmatter.type || "",
|
|
8801
|
-
...n.frontmatter.tags || []
|
|
8802
|
-
].join(" ").toLowerCase();
|
|
8803
|
-
return terms.every((t) => hay.includes(t));
|
|
8804
|
-
});
|
|
8805
|
-
if (!matches.length)
|
|
9125
|
+
const count = accessible.length;
|
|
9126
|
+
if (count === 0)
|
|
8806
9127
|
return {
|
|
8807
|
-
text: `No nodes matched search: "${
|
|
8808
|
-
data: { results: [] }
|
|
9128
|
+
text: `No nodes matched search: "${query}"`,
|
|
9129
|
+
data: { results: [], count }
|
|
8809
9130
|
};
|
|
8810
|
-
const
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
9131
|
+
const gate = await batchedBodyGate(nestId);
|
|
9132
|
+
const page = await Promise.all(
|
|
9133
|
+
accessible.slice(0, limit).map(async (node) => ({
|
|
9134
|
+
node,
|
|
9135
|
+
body: await resolveLlmBody(ctx, node, gate)
|
|
9136
|
+
}))
|
|
9137
|
+
);
|
|
9138
|
+
const results = page.map(({ node: n, body }, i) => {
|
|
9139
|
+
const head = `${i + 1}. **${n.frontmatter.title}** [${n.frontmatter.type || "document"}] ${(n.frontmatter.tags || []).join(" ")}`;
|
|
9140
|
+
const preview = body === null ? "_(no approved version yet \u2014 body withheld)_" : body.slice(0, 200).replace(/\n/g, " ");
|
|
9141
|
+
return `${head}
|
|
9142
|
+
${preview}`;
|
|
9143
|
+
}).join("\n\n");
|
|
9144
|
+
const more = count > page.length ? ` (showing the first ${page.length}; pass \`limit\` for more)` : "";
|
|
8814
9145
|
return {
|
|
8815
|
-
text: `Found ${
|
|
9146
|
+
text: `Found ${count} node(s) matching "${query}"${more}:
|
|
8816
9147
|
|
|
8817
9148
|
${results}`,
|
|
8818
|
-
//
|
|
8819
|
-
//
|
|
8820
|
-
// this is a term-containment filter
|
|
9149
|
+
// `count` is everything found (like REST); `results` is the page.
|
|
9150
|
+
// Bodies are the gated ones the prose is built from. No `score`:
|
|
9151
|
+
// this is a term-containment filter with title-first ordering, not
|
|
9152
|
+
// a ranked search.
|
|
8821
9153
|
data: {
|
|
8822
|
-
results:
|
|
9154
|
+
results: page.map(({ node: n, body }) => summarizeNode(n, body)),
|
|
9155
|
+
count
|
|
8823
9156
|
}
|
|
8824
9157
|
};
|
|
8825
9158
|
}
|
|
@@ -9327,7 +9660,9 @@ ${list}`;
|
|
|
9327
9660
|
});
|
|
9328
9661
|
return `Rejected the deletion request for **${label}**. It stays, and ${request.requestedBy} has been told why.`;
|
|
9329
9662
|
}
|
|
9330
|
-
if (request.targetType === "
|
|
9663
|
+
if (request.targetType === "nest_archive") {
|
|
9664
|
+
await archiveNest(nestId);
|
|
9665
|
+
} else if (request.targetType === "nest") {
|
|
9331
9666
|
await deleteNest(nestId);
|
|
9332
9667
|
} else if (request.targetType === "folder") {
|
|
9333
9668
|
await deleteFolder(nestId, request.nodeId, userEmail);
|
|
@@ -9346,7 +9681,7 @@ ${list}`;
|
|
|
9346
9681
|
targetType: request.targetType,
|
|
9347
9682
|
baseUrl: ctx.baseUrl
|
|
9348
9683
|
});
|
|
9349
|
-
return `Deleted **${label}** \u2014 ${request.requestedBy}'s request was honoured. Irreversible.`;
|
|
9684
|
+
return request.targetType === "nest_archive" ? `Archived **${label}** \u2014 ${request.requestedBy}'s request was honoured. Nothing was deleted: the nest is read-only and hidden until someone restores it.` : `Deleted **${label}** \u2014 ${request.requestedBy}'s request was honoured. Irreversible.`;
|
|
9350
9685
|
}
|
|
9351
9686
|
case "context_review_queue": {
|
|
9352
9687
|
const status = args.status || "pending";
|
|
@@ -9592,19 +9927,19 @@ ${list}`,
|
|
|
9592
9927
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
9593
9928
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
9594
9929
|
try {
|
|
9595
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
9596
|
-
const db = (await import("./client-
|
|
9930
|
+
const { createGrant: createGrant2 } = await import("./grants-service-LSNZN6NG.js");
|
|
9931
|
+
const db = (await import("./client-BD6PAUCZ.js")).getDb();
|
|
9597
9932
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
9598
9933
|
const e = normalizeEmail2(String(args.email || ""));
|
|
9599
9934
|
if (!e) return "email is required.";
|
|
9600
9935
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
9601
9936
|
if (!row) {
|
|
9602
9937
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
9603
|
-
const { v4:
|
|
9604
|
-
const id =
|
|
9938
|
+
const { v4: uuid19 } = await import("uuid");
|
|
9939
|
+
const id = uuid19();
|
|
9605
9940
|
await db.run(
|
|
9606
9941
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
9607
|
-
[id, e, null, await hashPassword2(
|
|
9942
|
+
[id, e, null, await hashPassword2(uuid19())]
|
|
9608
9943
|
);
|
|
9609
9944
|
row = { id };
|
|
9610
9945
|
}
|
|
@@ -9726,7 +10061,7 @@ mcpRoutes.all("/", async (c) => {
|
|
|
9726
10061
|
|
|
9727
10062
|
// src/workflow/run-routes.ts
|
|
9728
10063
|
import { Hono as Hono15 } from "hono";
|
|
9729
|
-
import { v4 as
|
|
10064
|
+
import { v4 as uuid12 } from "uuid";
|
|
9730
10065
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
9731
10066
|
var safeJson2 = (s) => {
|
|
9732
10067
|
if (!s) return null;
|
|
@@ -9923,7 +10258,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
9923
10258
|
}
|
|
9924
10259
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
9925
10260
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
9926
|
-
const id = `run_${
|
|
10261
|
+
const id = `run_${uuid12()}`;
|
|
9927
10262
|
await getDb().run(
|
|
9928
10263
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
9929
10264
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -10136,7 +10471,7 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10136
10471
|
|
|
10137
10472
|
// src/workflow/schedule-routes.ts
|
|
10138
10473
|
import { Hono as Hono16 } from "hono";
|
|
10139
|
-
import { v4 as
|
|
10474
|
+
import { v4 as uuid13 } from "uuid";
|
|
10140
10475
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
10141
10476
|
var MIN_EVERY = 5;
|
|
10142
10477
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -10214,7 +10549,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
10214
10549
|
);
|
|
10215
10550
|
}
|
|
10216
10551
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10217
|
-
const id = `sch_${
|
|
10552
|
+
const id = `sch_${uuid13()}`;
|
|
10218
10553
|
await db.run(
|
|
10219
10554
|
`INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
|
|
10220
10555
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -10270,7 +10605,7 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10270
10605
|
|
|
10271
10606
|
// src/notify/connector-routes.ts
|
|
10272
10607
|
import { Hono as Hono17 } from "hono";
|
|
10273
|
-
import { v4 as
|
|
10608
|
+
import { v4 as uuid14 } from "uuid";
|
|
10274
10609
|
var KNOWN_EVENTS = [
|
|
10275
10610
|
"review_requested",
|
|
10276
10611
|
"review_approved",
|
|
@@ -10335,7 +10670,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
10335
10670
|
const url = parseUrl(body.url);
|
|
10336
10671
|
const events = parseEvents(body.events);
|
|
10337
10672
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10338
|
-
const id = `con_${
|
|
10673
|
+
const id = `con_${uuid14()}`;
|
|
10339
10674
|
await getDb().run(
|
|
10340
10675
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
10341
10676
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -10483,7 +10818,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
10483
10818
|
}
|
|
10484
10819
|
const db = getDb();
|
|
10485
10820
|
const hook = await db.get(
|
|
10486
|
-
|
|
10821
|
+
`SELECT h.* FROM trigger_hooks h
|
|
10822
|
+
JOIN nests n ON n.id = h.nest_id
|
|
10823
|
+
WHERE h.id = ? AND h.enabled = 1 AND n.archived_at IS NULL`,
|
|
10487
10824
|
[token]
|
|
10488
10825
|
);
|
|
10489
10826
|
if (!hook) throw new NotFoundError("Not found");
|
|
@@ -10576,7 +10913,7 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
10576
10913
|
|
|
10577
10914
|
// src/definitions/routes.ts
|
|
10578
10915
|
import { Hono as Hono19 } from "hono";
|
|
10579
|
-
import { v4 as
|
|
10916
|
+
import { v4 as uuid15 } from "uuid";
|
|
10580
10917
|
var definitionRoutes = new Hono19();
|
|
10581
10918
|
definitionRoutes.get("/", async (c) => {
|
|
10582
10919
|
const nestId = c.req.param("nestId");
|
|
@@ -10656,7 +10993,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
10656
10993
|
await applyUpdate(tx, existing.id);
|
|
10657
10994
|
targetId = existing.id;
|
|
10658
10995
|
} else {
|
|
10659
|
-
targetId =
|
|
10996
|
+
targetId = uuid15();
|
|
10660
10997
|
wasCreated = true;
|
|
10661
10998
|
await tx.run(
|
|
10662
10999
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -10686,7 +11023,7 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
10686
11023
|
|
|
10687
11024
|
// src/nests/asset-routes.ts
|
|
10688
11025
|
import { Hono as Hono20 } from "hono";
|
|
10689
|
-
import { v4 as
|
|
11026
|
+
import { v4 as uuid16 } from "uuid";
|
|
10690
11027
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
10691
11028
|
import { join as join4 } from "path";
|
|
10692
11029
|
var assetRoutes = new Hono20();
|
|
@@ -10735,7 +11072,7 @@ assetRoutes.post("/", async (c) => {
|
|
|
10735
11072
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
10736
11073
|
);
|
|
10737
11074
|
}
|
|
10738
|
-
const name = `${
|
|
11075
|
+
const name = `${uuid16()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
10739
11076
|
const dir = join4(resolveNestPath(nestId), "assets");
|
|
10740
11077
|
await mkdir2(dir, { recursive: true });
|
|
10741
11078
|
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
@@ -10795,10 +11132,15 @@ async function canRead(nestId, userId) {
|
|
|
10795
11132
|
}
|
|
10796
11133
|
async function resolveNestRef(ref, userId) {
|
|
10797
11134
|
const db = getDb();
|
|
10798
|
-
const byId = await db.get(
|
|
11135
|
+
const byId = await db.get(
|
|
11136
|
+
"SELECT * FROM nests WHERE id = ? AND archived_at IS NULL",
|
|
11137
|
+
[ref]
|
|
11138
|
+
);
|
|
10799
11139
|
if (byId) return byId;
|
|
10800
11140
|
const byName = await db.all(
|
|
10801
|
-
|
|
11141
|
+
`SELECT * FROM nests
|
|
11142
|
+
WHERE archived_at IS NULL
|
|
11143
|
+
AND (LOWER(name) = LOWER(?) OR LOWER(slug) = LOWER(?))`,
|
|
10802
11144
|
[ref, ref]
|
|
10803
11145
|
);
|
|
10804
11146
|
if (byName.length <= 1) return byName[0] ?? null;
|
|
@@ -10897,12 +11239,16 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
10897
11239
|
},
|
|
10898
11240
|
{
|
|
10899
11241
|
name: "context_search",
|
|
10900
|
-
description: "Full-text keyword search across one nest.",
|
|
11242
|
+
description: "Full-text keyword search across one nest \u2014 the same index-backed match as REST GET /search. Returns `count` (everything found) and the first `limit` hits.",
|
|
10901
11243
|
inputSchema: {
|
|
10902
11244
|
type: "object",
|
|
10903
11245
|
properties: {
|
|
10904
11246
|
...NEST_ARG,
|
|
10905
|
-
query: { type: "string", description: "Search terms" }
|
|
11247
|
+
query: { type: "string", description: "Search terms" },
|
|
11248
|
+
limit: {
|
|
11249
|
+
type: "number",
|
|
11250
|
+
description: "Max hits to return (default 20, max 200)"
|
|
11251
|
+
}
|
|
10906
11252
|
},
|
|
10907
11253
|
required: ["nest", "query"]
|
|
10908
11254
|
}
|
|
@@ -11203,6 +11549,26 @@ Claude Code / Cursor / VS Code connect natively via HTTP with the
|
|
|
11203
11549
|
npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
11204
11550
|
\`\`\`
|
|
11205
11551
|
|
|
11552
|
+
## From the ctx CLI
|
|
11553
|
+
|
|
11554
|
+
The \`ctx\` CLI (\`@promptowl/contextnest-cli\` 2.2+) reads a hosted nest the
|
|
11555
|
+
same way it reads a local vault: register the MCP endpoint as a vault alias,
|
|
11556
|
+
then pass \`--vault <alias>\` to \`query\`, \`read\`, \`list\`, \`search\`. The key
|
|
11557
|
+
is read from the environment variable named by \`--bearer-env\` \u2014 never put it
|
|
11558
|
+
on the command line.
|
|
11559
|
+
|
|
11560
|
+
\`\`\`
|
|
11561
|
+
export CONTEXTNEST_API_KEY=<api-key>
|
|
11562
|
+
# every nest these credentials can read, through the server-level endpoint
|
|
11563
|
+
ctx vault add contextnest --url ${B}/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
11564
|
+
# or one nest
|
|
11565
|
+
ctx vault add mynest --url ${B}/nests/<nest-id>/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
11566
|
+
ctx query "#gtm" --vault mynest
|
|
11567
|
+
\`\`\`
|
|
11568
|
+
|
|
11569
|
+
Servers in "open" auth mode: drop \`--bearer-env\`. Pushing a local folder up
|
|
11570
|
+
is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
11571
|
+
|
|
11206
11572
|
## npm packages
|
|
11207
11573
|
|
|
11208
11574
|
- \`@promptowl/contextnest-community\` \u2014 this server
|
|
@@ -11354,7 +11720,7 @@ async function getNestFeed(params) {
|
|
|
11354
11720
|
}
|
|
11355
11721
|
|
|
11356
11722
|
// src/governance/comment-service.ts
|
|
11357
|
-
import { v4 as
|
|
11723
|
+
import { v4 as uuid17 } from "uuid";
|
|
11358
11724
|
async function createComment(params) {
|
|
11359
11725
|
const db = getDb();
|
|
11360
11726
|
const body = (params.body ?? "").trim();
|
|
@@ -11370,7 +11736,7 @@ async function createComment(params) {
|
|
|
11370
11736
|
throw new Error("Parent comment not found on this node");
|
|
11371
11737
|
}
|
|
11372
11738
|
}
|
|
11373
|
-
const id =
|
|
11739
|
+
const id = uuid17();
|
|
11374
11740
|
await db.run(
|
|
11375
11741
|
`INSERT INTO comments
|
|
11376
11742
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -11885,12 +12251,12 @@ governanceRoutes.post("/request-deletion", async (c) => {
|
|
|
11885
12251
|
if (!reason) {
|
|
11886
12252
|
throw new ValidationError("A reason for the deletion request is required");
|
|
11887
12253
|
}
|
|
11888
|
-
if (body.targetType !== "folder" && body.targetType !== "nest") {
|
|
12254
|
+
if (body.targetType !== "folder" && body.targetType !== "nest" && body.targetType !== "nest_archive") {
|
|
11889
12255
|
throw new ValidationError(
|
|
11890
|
-
'targetType must be "folder" or "
|
|
12256
|
+
'targetType must be "folder", "nest" or "nest_archive" \u2014 flag a single document via POST /nests/:nestId/nodes/:nodeId/request-deletion'
|
|
11891
12257
|
);
|
|
11892
12258
|
}
|
|
11893
|
-
const target = body.targetType
|
|
12259
|
+
const target = isNestTarget(body.targetType) ? nestId : (body.target || "").trim().replace(/^\/+|\/+$/g, "").replace(/^nodes\//, "");
|
|
11894
12260
|
if (body.targetType === "folder" && !target) {
|
|
11895
12261
|
throw new ValidationError("target (the folder path) is required");
|
|
11896
12262
|
}
|
|
@@ -11926,14 +12292,16 @@ governanceRoutes.post("/deletion-requests/:id/delete", async (c) => {
|
|
|
11926
12292
|
}
|
|
11927
12293
|
const actor = await getUserEmail4(c);
|
|
11928
12294
|
const docTitle = await deletionTargetTitle(request);
|
|
11929
|
-
if (request.targetType
|
|
12295
|
+
if (isNestTarget(request.targetType) || request.targetType === "folder") {
|
|
11930
12296
|
const denied = denyUnlessNestManager(
|
|
11931
12297
|
c,
|
|
11932
|
-
request.targetType === "
|
|
12298
|
+
request.targetType === "folder" ? "a folder" : "a nest"
|
|
11933
12299
|
);
|
|
11934
12300
|
if (denied) return denied;
|
|
11935
12301
|
}
|
|
11936
|
-
if (request.targetType === "
|
|
12302
|
+
if (request.targetType === "nest_archive") {
|
|
12303
|
+
await archiveNest(nestId);
|
|
12304
|
+
} else if (request.targetType === "nest") {
|
|
11937
12305
|
await deleteNest(nestId);
|
|
11938
12306
|
} else if (request.targetType === "folder") {
|
|
11939
12307
|
await deleteFolder(nestId, request.nodeId, actor);
|
|
@@ -12112,11 +12480,18 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions/:version{[0-9]+}", async (c) =>
|
|
|
12112
12480
|
403
|
|
12113
12481
|
);
|
|
12114
12482
|
}
|
|
12115
|
-
const { versions: versionManager } = await engineCache.get(nestId);
|
|
12483
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
12116
12484
|
try {
|
|
12117
12485
|
const raw = await versionManager.reconstructVersion(nodeId, version);
|
|
12118
12486
|
return c.json({ version, content: bodyOnly(nodeId, raw) });
|
|
12119
12487
|
} catch (err) {
|
|
12488
|
+
try {
|
|
12489
|
+
const node = await storage.readDocument(nodeId);
|
|
12490
|
+
if ((node.frontmatter.version ?? 1) === version) {
|
|
12491
|
+
return c.json({ version, content: node.body ?? "" });
|
|
12492
|
+
}
|
|
12493
|
+
} catch {
|
|
12494
|
+
}
|
|
12120
12495
|
console.error("reconstructVersion failed", nestId, nodeId, version, err);
|
|
12121
12496
|
return c.json({ version, content: null }, 200);
|
|
12122
12497
|
}
|
|
@@ -13387,11 +13762,15 @@ function createApp() {
|
|
|
13387
13762
|
nestName2.set(n.id, n.name ?? n.id);
|
|
13388
13763
|
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 25, 1), 100);
|
|
13389
13764
|
const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
|
|
13765
|
+
const sort = c.req.query("sort") === "title" ? "title" : "updated";
|
|
13390
13766
|
const { drafts, total } = await listMyDrafts({
|
|
13391
13767
|
authorEmail: me,
|
|
13392
13768
|
nestIds: [...nestName2.keys()],
|
|
13393
13769
|
limit,
|
|
13394
|
-
offset
|
|
13770
|
+
offset,
|
|
13771
|
+
q: c.req.query("q") ?? void 0,
|
|
13772
|
+
sort,
|
|
13773
|
+
nestNames: nestName2
|
|
13395
13774
|
});
|
|
13396
13775
|
return c.json({
|
|
13397
13776
|
drafts: drafts.map((d) => ({
|
|
@@ -13405,8 +13784,8 @@ function createApp() {
|
|
|
13405
13784
|
version: d.version,
|
|
13406
13785
|
updated_at: d.updatedAt
|
|
13407
13786
|
})),
|
|
13408
|
-
//
|
|
13409
|
-
//
|
|
13787
|
+
// `total` counts drafts whose document still exists — the same set the
|
|
13788
|
+
// page is sliced from — so this is exact in every sort and search mode.
|
|
13410
13789
|
pagination: { total, limit, offset, has_more: offset + limit < total }
|
|
13411
13790
|
});
|
|
13412
13791
|
});
|
|
@@ -13500,6 +13879,8 @@ function createApp() {
|
|
|
13500
13879
|
const userId = c.get("userId");
|
|
13501
13880
|
const nestScope = c.get("nestScope");
|
|
13502
13881
|
const isMcpTransport = parts.length === 2 && parts[1] === "mcp";
|
|
13882
|
+
const isNestLifecycleAction = parts.length === 2 && c.req.method === "POST" && (parts[1] === "archive" || parts[1] === "restore");
|
|
13883
|
+
const isPinAction = parts.length === 2 && parts[1] === "pin";
|
|
13503
13884
|
const deny = async (status, error, extra) => {
|
|
13504
13885
|
if (!isMcpTransport) return c.json({ error, ...extra }, status);
|
|
13505
13886
|
let id = null;
|
|
@@ -13544,11 +13925,25 @@ function createApp() {
|
|
|
13544
13925
|
}
|
|
13545
13926
|
}
|
|
13546
13927
|
if (config.AUTH_MODE === "open") {
|
|
13547
|
-
|
|
13928
|
+
const archived = await isNestArchived(nestId);
|
|
13929
|
+
if (archived) {
|
|
13930
|
+
if (isMcpTransport) return deny(404, "Nest not found");
|
|
13931
|
+
if (c.req.method !== "GET" && !isNestLifecycleAction) {
|
|
13932
|
+
return deny(
|
|
13933
|
+
403,
|
|
13934
|
+
"This nest is archived, so it is read-only. Restore it to make changes.",
|
|
13935
|
+
{ archived: true }
|
|
13936
|
+
);
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
c.set("nestPermission", archived ? "read" : "owner");
|
|
13548
13940
|
return next();
|
|
13549
13941
|
}
|
|
13550
13942
|
const access = await resolveNestAccess(nestId, userId);
|
|
13551
13943
|
const permission = access.permission;
|
|
13944
|
+
if (access.archived && isMcpTransport) {
|
|
13945
|
+
return deny(404, "Nest not found");
|
|
13946
|
+
}
|
|
13552
13947
|
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
13553
13948
|
let grantRole = null;
|
|
13554
13949
|
let grantNestVisible = false;
|
|
@@ -13566,6 +13961,8 @@ function createApp() {
|
|
|
13566
13961
|
grantNestVisible = access.hasGrant;
|
|
13567
13962
|
} else if (isReadQuery) {
|
|
13568
13963
|
grantNestVisible = access.hasGrant;
|
|
13964
|
+
} else if (isPinAction) {
|
|
13965
|
+
grantNestVisible = access.hasGrant;
|
|
13569
13966
|
} else if (parts[1] === "definitions" && parts.length === 2 && c.req.method === "GET") {
|
|
13570
13967
|
grantNestVisible = access.hasGrant;
|
|
13571
13968
|
}
|
|
@@ -13599,12 +13996,12 @@ function createApp() {
|
|
|
13599
13996
|
required = c.req.method === "GET" ? "write" : "admin";
|
|
13600
13997
|
} else if (parts[1] === "hooks") {
|
|
13601
13998
|
required = "write";
|
|
13602
|
-
} else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isDeletionRequestAction && !isAnnotationAction && !isReadQuery && !isMcpTransport) {
|
|
13999
|
+
} else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isDeletionRequestAction && !isNestLifecycleAction && !isPinAction && !isAnnotationAction && !isReadQuery && !isMcpTransport) {
|
|
13603
14000
|
required = "write";
|
|
13604
14001
|
}
|
|
13605
14002
|
const isNodeAction = c.req.method === "POST" && parts.length >= 4 && (parts[parts.length - 1] === "revert" || parts[parts.length - 1] === "move");
|
|
13606
14003
|
let stewardEditorBypass = false;
|
|
13607
|
-
if (required === "write" && permission === "read" && parts[1] === "nodes") {
|
|
14004
|
+
if (required === "write" && permission === "read" && !access.archived && parts[1] === "nodes") {
|
|
13608
14005
|
const userEmail = await resolveCallerEmail(userId);
|
|
13609
14006
|
if (parts.length >= 3 && (c.req.method === "PATCH" || c.req.method === "DELETE" || isNodeAction)) {
|
|
13610
14007
|
const idParts = isNodeAction ? parts.slice(2, -1) : parts.slice(2);
|
|
@@ -14000,7 +14397,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
14000
14397
|
}
|
|
14001
14398
|
|
|
14002
14399
|
// src/workflow/scheduler.ts
|
|
14003
|
-
import { v4 as
|
|
14400
|
+
import { v4 as uuid18 } from "uuid";
|
|
14004
14401
|
function isDue(s, now) {
|
|
14005
14402
|
if (!s.last_run_at) return true;
|
|
14006
14403
|
const last = Date.parse(s.last_run_at);
|
|
@@ -14009,7 +14406,11 @@ function isDue(s, now) {
|
|
|
14009
14406
|
}
|
|
14010
14407
|
async function schedulerTick(now = Date.now()) {
|
|
14011
14408
|
const db = getDb();
|
|
14012
|
-
const due = (await db.all(
|
|
14409
|
+
const due = (await db.all(
|
|
14410
|
+
`SELECT s.* FROM schedules s
|
|
14411
|
+
JOIN nests n ON n.id = s.nest_id
|
|
14412
|
+
WHERE s.enabled = 1 AND n.archived_at IS NULL`
|
|
14413
|
+
)).filter((s) => isDue(s, now));
|
|
14013
14414
|
const created = [];
|
|
14014
14415
|
for (const s of due) {
|
|
14015
14416
|
try {
|
|
@@ -14024,7 +14425,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
14024
14425
|
);
|
|
14025
14426
|
continue;
|
|
14026
14427
|
}
|
|
14027
|
-
const id = `run_${
|
|
14428
|
+
const id = `run_${uuid18()}`;
|
|
14028
14429
|
const nowIso2 = new Date(now).toISOString();
|
|
14029
14430
|
await db.run(
|
|
14030
14431
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|