@promptowl/contextnest-community 1.17.0 → 1.18.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 +6 -2
- package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
- package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-OOBZG6BW.js +379 -0
- package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
- package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
- package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
- package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
- package/dist/engine-34E3YNX7.js +14 -0
- package/dist/external-edit-service-6Q6SHEOF.js +33 -0
- package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
- package/dist/index.js +496 -470
- package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
- package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
- package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
- package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
- package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
- package/dist/web3/assets/index-DONv5_Ni.css +1 -0
- package/dist/web3/assets/index-jSGrnboh.js +1065 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -3
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-D1299AIf.css +0 -1
- package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
package/dist/index.js
CHANGED
|
@@ -41,7 +41,27 @@ import {
|
|
|
41
41
|
submitForReview,
|
|
42
42
|
withNodeWriteLock,
|
|
43
43
|
withdrawDeletionRequest
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-HGN45I5P.js";
|
|
45
|
+
import {
|
|
46
|
+
canCreateInNest,
|
|
47
|
+
canManageStewards,
|
|
48
|
+
canUserAccess,
|
|
49
|
+
canUserApprove,
|
|
50
|
+
canUserEdit,
|
|
51
|
+
createStewardRecord,
|
|
52
|
+
getCollaboratorRole,
|
|
53
|
+
getStewardRolesForUser,
|
|
54
|
+
getStewardsForNest,
|
|
55
|
+
getStewardsForUser,
|
|
56
|
+
listStewards,
|
|
57
|
+
removeSteward,
|
|
58
|
+
resolveGrantRoles,
|
|
59
|
+
resolveStewardsForNode,
|
|
60
|
+
resolveStewardsWithFallback,
|
|
61
|
+
resolveUserRoles,
|
|
62
|
+
syncFromConfig,
|
|
63
|
+
updateSteward
|
|
64
|
+
} from "./chunk-TQACHVVA.js";
|
|
45
65
|
import {
|
|
46
66
|
createGrant,
|
|
47
67
|
deleteGrant,
|
|
@@ -50,15 +70,16 @@ import {
|
|
|
50
70
|
listGrants,
|
|
51
71
|
listUserGrants,
|
|
52
72
|
resolveNodeGrant
|
|
53
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-QUZXRLUA.js";
|
|
54
74
|
import {
|
|
55
75
|
generateApiKey,
|
|
56
76
|
getKeyPrefix,
|
|
57
77
|
hashApiKey,
|
|
58
78
|
hashPassword,
|
|
59
79
|
parseBearerToken,
|
|
80
|
+
verifyKeyRequest,
|
|
60
81
|
verifyPassword
|
|
61
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-F66VCBLA.js";
|
|
62
83
|
import {
|
|
63
84
|
checkConflict,
|
|
64
85
|
createVersion,
|
|
@@ -66,36 +87,32 @@ import {
|
|
|
66
87
|
getApprovedVersions,
|
|
67
88
|
getCurrentVersion,
|
|
68
89
|
getDisplayStatus,
|
|
90
|
+
getDisplayStatuses,
|
|
69
91
|
getTrackedNodeIds,
|
|
70
92
|
getVersions,
|
|
71
93
|
listMyDrafts,
|
|
72
94
|
setApprovedVersion,
|
|
73
95
|
upsertVersion
|
|
74
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-34UXRZXD.js";
|
|
75
97
|
import {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
removeSteward,
|
|
88
|
-
resolveGrantRoles,
|
|
89
|
-
resolveStewardsForNode,
|
|
90
|
-
resolveStewardsWithFallback,
|
|
91
|
-
resolveUserRoles,
|
|
92
|
-
syncFromConfig,
|
|
93
|
-
updateSteward
|
|
94
|
-
} from "./chunk-YF4OFT2V.js";
|
|
98
|
+
approveExternalEdit,
|
|
99
|
+
getExternalEditDetail,
|
|
100
|
+
getPendingChange,
|
|
101
|
+
listExternalEditVerdicts,
|
|
102
|
+
listNestExternalEdits,
|
|
103
|
+
loadLatestApprovedNode,
|
|
104
|
+
rejectExternalEdit,
|
|
105
|
+
scanDocumentForDrift,
|
|
106
|
+
scanNestForDrift,
|
|
107
|
+
startDriftScanner
|
|
108
|
+
} from "./chunk-OOBZG6BW.js";
|
|
95
109
|
import {
|
|
96
110
|
addMember,
|
|
111
|
+
assertSafeNodeId,
|
|
97
112
|
buildTitleMap,
|
|
98
113
|
canApproveWith,
|
|
114
|
+
canEditWith,
|
|
115
|
+
collabPermToRole,
|
|
99
116
|
createNest,
|
|
100
117
|
createTeam,
|
|
101
118
|
deleteNest,
|
|
@@ -104,14 +121,17 @@ import {
|
|
|
104
121
|
docLink,
|
|
105
122
|
engineApi,
|
|
106
123
|
engineCache,
|
|
124
|
+
ensureNodeIndex,
|
|
107
125
|
findTeamByExternalId,
|
|
108
126
|
folderForNode,
|
|
127
|
+
folderOfId,
|
|
109
128
|
getAccessConfig,
|
|
110
129
|
getCurrentLicense,
|
|
111
130
|
getNest,
|
|
112
131
|
getSuspensionReason,
|
|
113
132
|
getTeam,
|
|
114
133
|
importNest,
|
|
134
|
+
indexedFolderCounts,
|
|
115
135
|
insertOrIgnore,
|
|
116
136
|
installLicenseKey,
|
|
117
137
|
isConfigSuperAdmin,
|
|
@@ -136,11 +156,15 @@ import {
|
|
|
136
156
|
nestPrimeTags,
|
|
137
157
|
nestReviewsPrimeOnly,
|
|
138
158
|
nestStorageRoot,
|
|
159
|
+
nodesWithSuggestions,
|
|
139
160
|
normalizeTag,
|
|
140
161
|
nowExpr,
|
|
141
162
|
opContext,
|
|
142
163
|
permissionLevel,
|
|
143
164
|
persistSetting,
|
|
165
|
+
primaryRole,
|
|
166
|
+
readIndexedNodes,
|
|
167
|
+
rebuildNodeIndex,
|
|
144
168
|
removeMember,
|
|
145
169
|
removeNodeFromTagIndex,
|
|
146
170
|
renameNest,
|
|
@@ -150,6 +174,7 @@ import {
|
|
|
150
174
|
resolveNestAccess,
|
|
151
175
|
resolveNestPath,
|
|
152
176
|
resolveNestPermission,
|
|
177
|
+
resolveTeamRolesForUser,
|
|
153
178
|
sendEmailToRecipient,
|
|
154
179
|
setAllowSelfApprove,
|
|
155
180
|
setPrimeOnlyReview,
|
|
@@ -165,7 +190,7 @@ import {
|
|
|
165
190
|
updateMemberRole,
|
|
166
191
|
validateLicense,
|
|
167
192
|
verifySmtp
|
|
168
|
-
} from "./chunk-
|
|
193
|
+
} from "./chunk-FUUJG6XP.js";
|
|
169
194
|
import {
|
|
170
195
|
AppError,
|
|
171
196
|
ConflictError,
|
|
@@ -181,11 +206,12 @@ import {
|
|
|
181
206
|
initDb,
|
|
182
207
|
isEmailListish,
|
|
183
208
|
isEmailish
|
|
184
|
-
} from "./chunk-
|
|
209
|
+
} from "./chunk-HYSTFMFG.js";
|
|
185
210
|
import {
|
|
186
211
|
ANON_EMAIL,
|
|
187
|
-
ANON_USER_ID
|
|
188
|
-
|
|
212
|
+
ANON_USER_ID,
|
|
213
|
+
MAX_FOLDER_DEPTH
|
|
214
|
+
} from "./chunk-YB3LKF7U.js";
|
|
189
215
|
import {
|
|
190
216
|
normalizeEmail
|
|
191
217
|
} from "./chunk-FRQJWGN3.js";
|
|
@@ -1079,19 +1105,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1079
1105
|
);
|
|
1080
1106
|
const db = getDb();
|
|
1081
1107
|
const userId = c.get("userId");
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
);
|
|
1086
|
-
if (
|
|
1087
|
-
return c.json(
|
|
1088
|
-
{
|
|
1089
|
-
error: "An API key already exists for this user. Rotate it instead.",
|
|
1090
|
-
key_prefix: existing.key_prefix
|
|
1091
|
-
},
|
|
1092
|
-
409
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1108
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1109
|
+
action: "generate",
|
|
1110
|
+
label: body.label
|
|
1111
|
+
});
|
|
1112
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1095
1113
|
const apiKey = generateApiKey();
|
|
1096
1114
|
const keyId = uuid2();
|
|
1097
1115
|
await db.run(
|
|
@@ -1102,11 +1120,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1102
1120
|
hashApiKey(apiKey),
|
|
1103
1121
|
getKeyPrefix(apiKey),
|
|
1104
1122
|
body.nest_id || null,
|
|
1105
|
-
|
|
1123
|
+
check.label
|
|
1106
1124
|
]
|
|
1107
1125
|
);
|
|
1108
1126
|
return c.json(
|
|
1109
|
-
{ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
|
|
1127
|
+
{ id: keyId, api_key: apiKey, key_prefix: getKeyPrefix(apiKey), label: check.label },
|
|
1110
1128
|
201
|
|
1111
1129
|
);
|
|
1112
1130
|
});
|
|
@@ -1114,15 +1132,22 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1114
1132
|
const body = await c.req.json().catch(() => ({}));
|
|
1115
1133
|
const db = getDb();
|
|
1116
1134
|
const userId = c.get("userId");
|
|
1135
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1136
|
+
action: "update",
|
|
1137
|
+
label: body.label,
|
|
1138
|
+
keyId: body.key_id
|
|
1139
|
+
});
|
|
1140
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1141
|
+
const prior = check.key;
|
|
1117
1142
|
const apiKey = generateApiKey();
|
|
1118
1143
|
const keyId = uuid2();
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1144
|
+
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
1145
|
+
const replaced = await db.transaction(async (tx) => {
|
|
1146
|
+
const removed = await tx.run(
|
|
1147
|
+
"DELETE FROM api_keys WHERE id = ? AND user_id = ?",
|
|
1148
|
+
[prior.id, userId]
|
|
1149
|
+
);
|
|
1150
|
+
if (removed.changes === 0) return false;
|
|
1126
1151
|
await tx.run(
|
|
1127
1152
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1128
1153
|
[
|
|
@@ -1131,29 +1156,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1131
1156
|
hashApiKey(apiKey),
|
|
1132
1157
|
getKeyPrefix(apiKey),
|
|
1133
1158
|
nestScope,
|
|
1134
|
-
|
|
1159
|
+
check.label ?? prior.label
|
|
1135
1160
|
]
|
|
1136
1161
|
);
|
|
1162
|
+
return true;
|
|
1163
|
+
});
|
|
1164
|
+
if (!replaced) {
|
|
1165
|
+
return c.json(
|
|
1166
|
+
{ error: "That key no longer exists \u2014 reload and try again." },
|
|
1167
|
+
404
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
return c.json({
|
|
1171
|
+
id: keyId,
|
|
1172
|
+
api_key: apiKey,
|
|
1173
|
+
key_prefix: getKeyPrefix(apiKey),
|
|
1174
|
+
label: check.label ?? prior.label
|
|
1137
1175
|
});
|
|
1138
|
-
return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
|
|
1139
1176
|
});
|
|
1140
1177
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1141
1178
|
const db = getDb();
|
|
1179
|
+
const userId = c.get("userId");
|
|
1180
|
+
const check = await verifyKeyRequest(db, userId, { action: "fetch" });
|
|
1181
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1142
1182
|
const keys = await db.all(
|
|
1143
|
-
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
|
|
1144
|
-
[
|
|
1183
|
+
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ? ORDER BY created_at ASC, id ASC",
|
|
1184
|
+
[userId]
|
|
1145
1185
|
);
|
|
1146
1186
|
return c.json({ keys });
|
|
1147
1187
|
});
|
|
1148
1188
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1149
1189
|
const db = getDb();
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1190
|
+
const userId = c.get("userId");
|
|
1191
|
+
const check = await verifyKeyRequest(db, userId, {
|
|
1192
|
+
action: "update",
|
|
1193
|
+
keyId: c.req.param("keyId")
|
|
1194
|
+
});
|
|
1195
|
+
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1196
|
+
await db.run("DELETE FROM api_keys WHERE id = ? AND user_id = ?", [
|
|
1197
|
+
check.key.id,
|
|
1198
|
+
userId
|
|
1199
|
+
]);
|
|
1157
1200
|
return c.json({ deleted: true });
|
|
1158
1201
|
});
|
|
1159
1202
|
authRoutes.post("/device", async (c) => {
|
|
@@ -1610,13 +1653,14 @@ import { Hono as Hono3 } from "hono";
|
|
|
1610
1653
|
|
|
1611
1654
|
// src/nodes/service.ts
|
|
1612
1655
|
import {
|
|
1613
|
-
parseDocument
|
|
1656
|
+
parseDocument,
|
|
1614
1657
|
normalizeStatus,
|
|
1615
1658
|
isRejected,
|
|
1659
|
+
filterDocuments,
|
|
1616
1660
|
DocumentNotFoundError
|
|
1617
1661
|
} from "@promptowl/contextnest-engine";
|
|
1618
1662
|
import { rename, mkdir } from "fs/promises";
|
|
1619
|
-
import { dirname, join
|
|
1663
|
+
import { dirname, join } from "path";
|
|
1620
1664
|
|
|
1621
1665
|
// src/governance/safe-publish.ts
|
|
1622
1666
|
import {
|
|
@@ -1644,6 +1688,9 @@ function stripUndefinedDeep(value) {
|
|
|
1644
1688
|
return value;
|
|
1645
1689
|
}
|
|
1646
1690
|
|
|
1691
|
+
// src/nodes/service.ts
|
|
1692
|
+
import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
|
|
1693
|
+
|
|
1647
1694
|
// src/governance/prime-service.ts
|
|
1648
1695
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
1649
1696
|
const flag = documentPrimeFlag(metadata);
|
|
@@ -1661,313 +1708,6 @@ function documentPrimeFlag(metadata) {
|
|
|
1661
1708
|
return void 0;
|
|
1662
1709
|
}
|
|
1663
1710
|
|
|
1664
|
-
// src/governance/external-edit-service.ts
|
|
1665
|
-
import { readFile } from "fs/promises";
|
|
1666
|
-
import { join } from "path";
|
|
1667
|
-
import {
|
|
1668
|
-
detectDrift,
|
|
1669
|
-
stageSuggestion,
|
|
1670
|
-
ChainEventLog,
|
|
1671
|
-
approveSuggestion,
|
|
1672
|
-
rejectSuggestion,
|
|
1673
|
-
listSuggestions,
|
|
1674
|
-
readSuggestion,
|
|
1675
|
-
parseDocument,
|
|
1676
|
-
VersionManager,
|
|
1677
|
-
computeContentHash,
|
|
1678
|
-
getChecksumContent
|
|
1679
|
-
} from "@promptowl/contextnest-engine";
|
|
1680
|
-
var communityRbac = {
|
|
1681
|
-
isCzar: () => false,
|
|
1682
|
-
canIngest: () => true,
|
|
1683
|
-
isDocOwner: () => true
|
|
1684
|
-
};
|
|
1685
|
-
function docPath(nestId, documentId) {
|
|
1686
|
-
return join(resolveNestPath(nestId), `${documentId}.md`);
|
|
1687
|
-
}
|
|
1688
|
-
async function readRaw(nestId, documentId) {
|
|
1689
|
-
try {
|
|
1690
|
-
return await readFile(docPath(nestId, documentId), "utf-8");
|
|
1691
|
-
} catch {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
async function loadChainHead(storage, documentId) {
|
|
1696
|
-
const history = await storage.readHistory(documentId);
|
|
1697
|
-
if (!history || history.versions.length === 0) return null;
|
|
1698
|
-
const latest = history.versions[history.versions.length - 1];
|
|
1699
|
-
try {
|
|
1700
|
-
const content = await new VersionManager(storage).reconstructVersion(
|
|
1701
|
-
documentId,
|
|
1702
|
-
latest.version
|
|
1703
|
-
);
|
|
1704
|
-
return { version: latest.version, content };
|
|
1705
|
-
} catch {
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
async function loadLatestApprovedNode(nestId, documentId) {
|
|
1710
|
-
const { storage } = await engineCache.get(nestId);
|
|
1711
|
-
const head = await loadChainHead(storage, documentId);
|
|
1712
|
-
if (!head) return null;
|
|
1713
|
-
return parseDocument(docPath(nestId, documentId), head.content, documentId);
|
|
1714
|
-
}
|
|
1715
|
-
async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
|
|
1716
|
-
const head = await loadChainHead(storage, documentId);
|
|
1717
|
-
if (!head) return false;
|
|
1718
|
-
const headBodyHash = computeContentHash(getChecksumContent(head.content));
|
|
1719
|
-
return headBodyHash === liveBodyHash;
|
|
1720
|
-
}
|
|
1721
|
-
async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
|
|
1722
|
-
const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
|
|
1723
|
-
return res?.meta ?? null;
|
|
1724
|
-
}
|
|
1725
|
-
async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
1726
|
-
const { storage } = await engineCache.get(nestId);
|
|
1727
|
-
const node = await storage.readDocument(documentId).catch(() => null);
|
|
1728
|
-
if (!node) return null;
|
|
1729
|
-
const raw = await readRaw(nestId, documentId);
|
|
1730
|
-
if (raw == null) return null;
|
|
1731
|
-
const drift = detectDrift(raw, node.frontmatter.checksum);
|
|
1732
|
-
if (!drift.drifted) return null;
|
|
1733
|
-
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
1734
|
-
return null;
|
|
1735
|
-
}
|
|
1736
|
-
const approved = await loadChainHead(storage, documentId);
|
|
1737
|
-
if (!approved) return null;
|
|
1738
|
-
const existing = await listSuggestions(storage, documentId);
|
|
1739
|
-
const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
|
|
1740
|
-
if (dup) return { meta: dup, created: false };
|
|
1741
|
-
const result = await stageSuggestion({
|
|
1742
|
-
storage,
|
|
1743
|
-
documentId,
|
|
1744
|
-
approvedRawContent: approved.content,
|
|
1745
|
-
proposedRawContent: raw,
|
|
1746
|
-
source: "out-of-band-edit",
|
|
1747
|
-
actor,
|
|
1748
|
-
docTier: "standard"
|
|
1749
|
-
});
|
|
1750
|
-
return { meta: result.meta, created: true };
|
|
1751
|
-
}
|
|
1752
|
-
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
1753
|
-
const { storage, invalidateDiscovery } = await engineCache.get(nestId);
|
|
1754
|
-
invalidateDiscovery();
|
|
1755
|
-
const docs = await storage.discoverDocuments();
|
|
1756
|
-
const results = await Promise.all(
|
|
1757
|
-
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
1758
|
-
);
|
|
1759
|
-
const staged = results.filter((r) => r?.created).length;
|
|
1760
|
-
return { scanned: docs.length, staged };
|
|
1761
|
-
}
|
|
1762
|
-
async function getPendingChange(nestId, documentId) {
|
|
1763
|
-
const { storage } = await engineCache.get(nestId);
|
|
1764
|
-
const list = await listSuggestions(storage, documentId);
|
|
1765
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
1766
|
-
const meta = list[i];
|
|
1767
|
-
if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
return {
|
|
1771
|
-
suggestion_id: meta.suggestion_id,
|
|
1772
|
-
detected_at: meta.detected_at,
|
|
1773
|
-
source: meta.source,
|
|
1774
|
-
proposed_hash: meta.proposed_hash
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
return null;
|
|
1778
|
-
}
|
|
1779
|
-
async function listNestExternalEdits(nestId) {
|
|
1780
|
-
const { storage } = await engineCache.get(nestId);
|
|
1781
|
-
const docs = await storage.discoverDocuments();
|
|
1782
|
-
const lists = await Promise.all(
|
|
1783
|
-
docs.map(async (doc) => {
|
|
1784
|
-
const metas = await listSuggestions(storage, doc.id);
|
|
1785
|
-
const fresh = [];
|
|
1786
|
-
for (const meta of metas) {
|
|
1787
|
-
if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
fresh.push(meta);
|
|
1791
|
-
}
|
|
1792
|
-
return fresh;
|
|
1793
|
-
})
|
|
1794
|
-
);
|
|
1795
|
-
const entries = lists.flat().map((meta) => ({
|
|
1796
|
-
suggestion_id: meta.suggestion_id,
|
|
1797
|
-
nest_id: nestId,
|
|
1798
|
-
document_id: meta.document_id,
|
|
1799
|
-
source: meta.source,
|
|
1800
|
-
detected_at: meta.detected_at,
|
|
1801
|
-
actor: meta.actor,
|
|
1802
|
-
target_hash: meta.target_hash,
|
|
1803
|
-
proposed_hash: meta.proposed_hash,
|
|
1804
|
-
note: meta.note
|
|
1805
|
-
}));
|
|
1806
|
-
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
1807
|
-
}
|
|
1808
|
-
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
1809
|
-
const { storage } = await engineCache.get(nestId);
|
|
1810
|
-
const found = await readSuggestion(storage, documentId, suggestionId);
|
|
1811
|
-
if (!found) return null;
|
|
1812
|
-
return {
|
|
1813
|
-
suggestion_id: found.meta.suggestion_id,
|
|
1814
|
-
nest_id: nestId,
|
|
1815
|
-
document_id: found.meta.document_id,
|
|
1816
|
-
source: found.meta.source,
|
|
1817
|
-
detected_at: found.meta.detected_at,
|
|
1818
|
-
actor: found.meta.actor,
|
|
1819
|
-
target_hash: found.meta.target_hash,
|
|
1820
|
-
proposed_hash: found.meta.proposed_hash,
|
|
1821
|
-
note: found.meta.note,
|
|
1822
|
-
patch: found.patch
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
async function recordVerdict(nestId, event) {
|
|
1826
|
-
try {
|
|
1827
|
-
const { storage } = await engineCache.get(nestId);
|
|
1828
|
-
await new ChainEventLog(storage).append(event);
|
|
1829
|
-
return true;
|
|
1830
|
-
} catch (err) {
|
|
1831
|
-
console.error(
|
|
1832
|
-
`[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
|
|
1833
|
-
err
|
|
1834
|
-
);
|
|
1835
|
-
return false;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
async function listExternalEditVerdicts(nestId, documentId) {
|
|
1839
|
-
try {
|
|
1840
|
-
const { storage } = await engineCache.get(nestId);
|
|
1841
|
-
const events = await new ChainEventLog(storage).readByDocument(documentId);
|
|
1842
|
-
return events.flatMap((e) => {
|
|
1843
|
-
const meta = e.action_metadata || {};
|
|
1844
|
-
if (typeof meta.suggestion_id !== "string") return [];
|
|
1845
|
-
const reason = meta.rejection_reason;
|
|
1846
|
-
const comment = meta.approval_comment;
|
|
1847
|
-
return [
|
|
1848
|
-
{
|
|
1849
|
-
suggestion_id: meta.suggestion_id,
|
|
1850
|
-
status: typeof reason === "string" ? "rejected" : "approved",
|
|
1851
|
-
actor: e.actor,
|
|
1852
|
-
at: e.timestamp,
|
|
1853
|
-
note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
|
|
1854
|
-
source: typeof meta.source === "string" ? meta.source : void 0
|
|
1855
|
-
}
|
|
1856
|
-
];
|
|
1857
|
-
});
|
|
1858
|
-
} catch (err) {
|
|
1859
|
-
console.error(
|
|
1860
|
-
`[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
|
|
1861
|
-
err
|
|
1862
|
-
);
|
|
1863
|
-
return [];
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
async function mirrorVersion(input) {
|
|
1867
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1868
|
-
const node = await storage.readDocument(input.documentId);
|
|
1869
|
-
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-EFZGGL3Y.js");
|
|
1870
|
-
await upsertVersion2({
|
|
1871
|
-
nestId: input.nestId,
|
|
1872
|
-
nodeId: input.documentId,
|
|
1873
|
-
version: input.version,
|
|
1874
|
-
content: node.body || "",
|
|
1875
|
-
author: input.actor,
|
|
1876
|
-
status: "published",
|
|
1877
|
-
tags: node.frontmatter.tags || [],
|
|
1878
|
-
changeNote: input.note
|
|
1879
|
-
});
|
|
1880
|
-
await setApprovedVersion2(
|
|
1881
|
-
input.nestId,
|
|
1882
|
-
input.documentId,
|
|
1883
|
-
input.version,
|
|
1884
|
-
input.actor
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
async function approveExternalEdit(input) {
|
|
1888
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1889
|
-
let result;
|
|
1890
|
-
try {
|
|
1891
|
-
result = await approveSuggestion({
|
|
1892
|
-
storage,
|
|
1893
|
-
rbac: communityRbac,
|
|
1894
|
-
documentId: input.documentId,
|
|
1895
|
-
suggestionId: input.suggestionId,
|
|
1896
|
-
actor: input.actor,
|
|
1897
|
-
zone: "default",
|
|
1898
|
-
comment: input.comment
|
|
1899
|
-
});
|
|
1900
|
-
} catch (err) {
|
|
1901
|
-
console.error(
|
|
1902
|
-
`[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
|
|
1903
|
-
err
|
|
1904
|
-
);
|
|
1905
|
-
throw err;
|
|
1906
|
-
}
|
|
1907
|
-
try {
|
|
1908
|
-
await mirrorVersion({
|
|
1909
|
-
nestId: input.nestId,
|
|
1910
|
-
documentId: input.documentId,
|
|
1911
|
-
version: result.versionEntry.version,
|
|
1912
|
-
actor: input.actor,
|
|
1913
|
-
note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
|
|
1914
|
-
});
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
console.error(
|
|
1917
|
-
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
1918
|
-
err
|
|
1919
|
-
);
|
|
1920
|
-
}
|
|
1921
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1922
|
-
return { ...result, historyRecorded };
|
|
1923
|
-
}
|
|
1924
|
-
async function rejectExternalEdit(input) {
|
|
1925
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1926
|
-
const result = await rejectSuggestion({
|
|
1927
|
-
storage,
|
|
1928
|
-
rbac: communityRbac,
|
|
1929
|
-
documentId: input.documentId,
|
|
1930
|
-
suggestionId: input.suggestionId,
|
|
1931
|
-
actor: input.actor,
|
|
1932
|
-
zone: "default",
|
|
1933
|
-
reason: input.reason
|
|
1934
|
-
});
|
|
1935
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1936
|
-
try {
|
|
1937
|
-
const approved = await loadChainHead(storage, input.documentId);
|
|
1938
|
-
if (approved) {
|
|
1939
|
-
await storage.writeDocument(input.documentId, approved.content);
|
|
1940
|
-
}
|
|
1941
|
-
} catch (err) {
|
|
1942
|
-
console.error(
|
|
1943
|
-
`[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
|
|
1944
|
-
err
|
|
1945
|
-
);
|
|
1946
|
-
}
|
|
1947
|
-
return { ...result, historyRecorded };
|
|
1948
|
-
}
|
|
1949
|
-
var scannerTimer = null;
|
|
1950
|
-
async function scanAllNests() {
|
|
1951
|
-
const db = getDb();
|
|
1952
|
-
const rows = await db.all("SELECT id FROM nests");
|
|
1953
|
-
await Promise.all(
|
|
1954
|
-
rows.map(
|
|
1955
|
-
({ id }) => scanNestForDrift(id).catch(
|
|
1956
|
-
(err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
|
|
1957
|
-
)
|
|
1958
|
-
)
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
function startDriftScanner(intervalMs = 3e4) {
|
|
1962
|
-
if (scannerTimer) return;
|
|
1963
|
-
scannerTimer = setInterval(() => {
|
|
1964
|
-
scanAllNests().catch(
|
|
1965
|
-
(err) => console.error("[external-edit] scanner tick failed:", err)
|
|
1966
|
-
);
|
|
1967
|
-
}, intervalMs);
|
|
1968
|
-
scannerTimer.unref?.();
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
1711
|
// src/nodes/service.ts
|
|
1972
1712
|
async function userIdFromEmail(email) {
|
|
1973
1713
|
const db = getDb();
|
|
@@ -1986,17 +1726,42 @@ function assertTitleLength(title) {
|
|
|
1986
1726
|
);
|
|
1987
1727
|
}
|
|
1988
1728
|
}
|
|
1989
|
-
function
|
|
1990
|
-
if (
|
|
1729
|
+
function assertTitleReadable(title) {
|
|
1730
|
+
if (!/[\p{L}\p{N}]/u.test(title)) {
|
|
1991
1731
|
throw new ValidationError(
|
|
1992
|
-
"the
|
|
1732
|
+
"the title needs at least one letter or number \u2014 it is what search and wiki links read back"
|
|
1993
1733
|
);
|
|
1994
1734
|
}
|
|
1995
1735
|
}
|
|
1996
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1736
|
+
var slugify = (s) => s.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1737
|
+
async function generatedSlug(storage, prefix) {
|
|
1738
|
+
for (let n = 1; n <= 1e3; n++) {
|
|
1739
|
+
const slug = `document-${n}`;
|
|
1740
|
+
try {
|
|
1741
|
+
await storage.readDocument(`${prefix}${slug}`);
|
|
1742
|
+
} catch (err) {
|
|
1743
|
+
if (err instanceof DocumentNotFoundError) return slug;
|
|
1744
|
+
throw err;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
throw new ValidationError(
|
|
1748
|
+
"too many generated document ids in this folder \u2014 give the document a title with Latin letters or digits, or file it elsewhere"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1997
1751
|
function isIdUnderFolder(id, folder) {
|
|
1998
1752
|
return id.startsWith(`nodes/${folder}/`) || id.startsWith(`${folder}/`);
|
|
1999
1753
|
}
|
|
1754
|
+
function assertSafeFolder(folder) {
|
|
1755
|
+
if (folder.split(/[/\\]/).some((seg) => seg === "..")) {
|
|
1756
|
+
throw new ValidationError(
|
|
1757
|
+
`Invalid folder "${folder}": path traversal ("..") is not allowed.`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async function storagePrefix(nestId) {
|
|
1762
|
+
const { storage } = await engineCache.get(nestId);
|
|
1763
|
+
return await storage.detectLayout() === "structured" ? "nodes" : "";
|
|
1764
|
+
}
|
|
2000
1765
|
function folderToSegments(folder) {
|
|
2001
1766
|
const segments = folder.split("/").map(slugify).filter(Boolean);
|
|
2002
1767
|
if (folder.trim() && segments.length === 0) {
|
|
@@ -2005,8 +1770,10 @@ function folderToSegments(folder) {
|
|
|
2005
1770
|
);
|
|
2006
1771
|
}
|
|
2007
1772
|
if (segments[0] === "nodes") segments.shift();
|
|
2008
|
-
if (segments.length >
|
|
2009
|
-
throw new ValidationError(
|
|
1773
|
+
if (segments.length > MAX_FOLDER_DEPTH) {
|
|
1774
|
+
throw new ValidationError(
|
|
1775
|
+
`folder may nest at most ${MAX_FOLDER_DEPTH} levels deep`
|
|
1776
|
+
);
|
|
2010
1777
|
}
|
|
2011
1778
|
if (segments.some((seg) => seg.length > 100)) {
|
|
2012
1779
|
throw new ValidationError(
|
|
@@ -2042,7 +1809,7 @@ function toSafeError(err, context, message) {
|
|
|
2042
1809
|
}
|
|
2043
1810
|
function bodyOnly(nodeId, raw) {
|
|
2044
1811
|
try {
|
|
2045
|
-
return
|
|
1812
|
+
return parseDocument(`${nodeId}.md`, raw, nodeId).body ?? "";
|
|
2046
1813
|
} catch {
|
|
2047
1814
|
return raw;
|
|
2048
1815
|
}
|
|
@@ -2127,6 +1894,15 @@ async function resolveAuthorship(nestId, nodes) {
|
|
|
2127
1894
|
}
|
|
2128
1895
|
return result;
|
|
2129
1896
|
}
|
|
1897
|
+
var folderScopeSupported = null;
|
|
1898
|
+
function engineScopesFolders() {
|
|
1899
|
+
if (folderScopeSupported === null) {
|
|
1900
|
+
const op = engineApi.getOperation("context_list");
|
|
1901
|
+
const properties = op ? inputJsonSchema(op).properties ?? {} : {};
|
|
1902
|
+
folderScopeSupported = "folder" in properties;
|
|
1903
|
+
}
|
|
1904
|
+
return folderScopeSupported;
|
|
1905
|
+
}
|
|
2130
1906
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2131
1907
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
2132
1908
|
throw new ForbiddenError(
|
|
@@ -2168,29 +1944,49 @@ async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {})
|
|
|
2168
1944
|
};
|
|
2169
1945
|
}
|
|
2170
1946
|
async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
1947
|
+
if (filters.folder !== void 0) assertSafeFolder(filters.folder);
|
|
2171
1948
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
2172
1949
|
const userEmail = await resolveCallerEmail(userId);
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
{
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
1950
|
+
const frontmatterFilters = {
|
|
1951
|
+
...filters.type ? { type: filters.type } : {},
|
|
1952
|
+
...filters.tag ? { tag: filters.tag } : {},
|
|
1953
|
+
...filters.status ? { status: filters.status } : {},
|
|
1954
|
+
includeRetired: true
|
|
1955
|
+
};
|
|
1956
|
+
let documents;
|
|
1957
|
+
if (filters.withContent) {
|
|
1958
|
+
const scopeInEngine = filters.folder !== void 0 && engineScopesFolders();
|
|
1959
|
+
const scopePrefix = scopeInEngine ? await storagePrefix(nestId) : "";
|
|
1960
|
+
({ documents } = await engineApi.run(
|
|
1961
|
+
"context_list",
|
|
1962
|
+
{
|
|
1963
|
+
...frontmatterFilters,
|
|
1964
|
+
include_retired: true,
|
|
1965
|
+
...scopeInEngine ? {
|
|
1966
|
+
folder: [scopePrefix, filters.folder].filter(Boolean).join("/"),
|
|
1967
|
+
recursive: false
|
|
1968
|
+
} : {},
|
|
1969
|
+
full: true
|
|
1970
|
+
},
|
|
1971
|
+
await opContext(nestId, userEmail)
|
|
1972
|
+
));
|
|
1973
|
+
} else {
|
|
1974
|
+
await ensureNodeIndex(nestId);
|
|
1975
|
+
const indexed = await readIndexedNodes(
|
|
1976
|
+
nestId,
|
|
1977
|
+
filters.folder !== void 0 ? { folder: filters.folder } : {}
|
|
1978
|
+
);
|
|
1979
|
+
documents = filterDocuments(indexed, frontmatterFilters);
|
|
1980
|
+
}
|
|
1981
|
+
const scoped = filters.folder === void 0 ? documents : documents.filter((d) => folderOfId(d.id) === filters.folder);
|
|
1982
|
+
const nodes = scoped.map((d) => ({
|
|
2189
1983
|
id: d.id,
|
|
2190
1984
|
filePath: "",
|
|
2191
1985
|
rawContent: "",
|
|
2192
1986
|
frontmatter: d.frontmatter,
|
|
2193
|
-
|
|
1987
|
+
// Empty on the indexed path: no listing renders a body, and the
|
|
1988
|
+
// approved-only branch below serves its own from the version store.
|
|
1989
|
+
body: d.body ?? ""
|
|
2194
1990
|
}));
|
|
2195
1991
|
const publicReader = await isPublicReader(nestId, userId);
|
|
2196
1992
|
const approvedOnly = publicReader || filters.approvedOnly === true;
|
|
@@ -2207,6 +2003,13 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2207
2003
|
accessible = accessible.filter((d) => approvedVersions.has(d.id));
|
|
2208
2004
|
}
|
|
2209
2005
|
if (filters.limit) accessible = accessible.slice(0, filters.limit);
|
|
2006
|
+
const [displayStatuses, driftedDocs] = approvedVersions ? [null, null] : await Promise.all([
|
|
2007
|
+
getDisplayStatuses(nestId),
|
|
2008
|
+
// From the DB, not a vault walk. The disk truth behind it is
|
|
2009
|
+
// reconciled by the index rebuild and kept current by the stage /
|
|
2010
|
+
// approve / reject paths.
|
|
2011
|
+
nodesWithSuggestions(nestId)
|
|
2012
|
+
]);
|
|
2210
2013
|
const authorship = publicReader ? null : await resolveAuthorship(
|
|
2211
2014
|
nestId,
|
|
2212
2015
|
accessible.map((d) => ({
|
|
@@ -2249,22 +2052,125 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2249
2052
|
return r;
|
|
2250
2053
|
}
|
|
2251
2054
|
let pending = null;
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2055
|
+
if (driftedDocs?.has(doc.id)) {
|
|
2056
|
+
try {
|
|
2057
|
+
pending = await getPendingChange(nestId, doc.id);
|
|
2058
|
+
} catch {
|
|
2059
|
+
pending = null;
|
|
2060
|
+
}
|
|
2256
2061
|
}
|
|
2257
2062
|
if (pending) {
|
|
2258
2063
|
r.pendingChange = pending;
|
|
2259
2064
|
r.status = "external_edit_pending";
|
|
2260
2065
|
} else {
|
|
2261
|
-
r.status =
|
|
2066
|
+
r.status = displayStatuses?.get(r.id) ?? "draft";
|
|
2262
2067
|
}
|
|
2263
2068
|
return r;
|
|
2264
2069
|
})
|
|
2265
2070
|
);
|
|
2266
2071
|
return enriched;
|
|
2267
2072
|
}
|
|
2073
|
+
function withSubtreeTotals(direct) {
|
|
2074
|
+
return direct.map(({ path, count }) => ({
|
|
2075
|
+
path,
|
|
2076
|
+
count,
|
|
2077
|
+
total: direct.reduce(
|
|
2078
|
+
(sum, f) => f.path === path || f.path.startsWith(`${path}/`) ? sum + f.count : sum,
|
|
2079
|
+
0
|
|
2080
|
+
)
|
|
2081
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
2082
|
+
}
|
|
2083
|
+
async function listNestFolders(nestId, opts = {}) {
|
|
2084
|
+
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
2085
|
+
await ensureNodeIndex(nestId);
|
|
2086
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2087
|
+
for (const { path, count } of await indexedFolderCounts(nestId)) {
|
|
2088
|
+
counts.set(path, count);
|
|
2089
|
+
let parent = path;
|
|
2090
|
+
while (parent.includes("/")) {
|
|
2091
|
+
parent = parent.slice(0, parent.lastIndexOf("/"));
|
|
2092
|
+
if (!counts.has(parent)) counts.set(parent, 0);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
const direct = [...counts].map(([path, count]) => ({ path, count }));
|
|
2096
|
+
const scope = opts.folder ?? "";
|
|
2097
|
+
const inScope = direct.filter((f) => {
|
|
2098
|
+
if (scope && !f.path.startsWith(`${scope}/`)) return false;
|
|
2099
|
+
if (opts.recursive === false) {
|
|
2100
|
+
const rel = scope ? f.path.slice(scope.length + 1) : f.path;
|
|
2101
|
+
return !rel.includes("/");
|
|
2102
|
+
}
|
|
2103
|
+
return true;
|
|
2104
|
+
});
|
|
2105
|
+
return withSubtreeTotals(inScope);
|
|
2106
|
+
}
|
|
2107
|
+
var RECENT_LIMIT = 3;
|
|
2108
|
+
async function nestCensus(nestId, userId) {
|
|
2109
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
2110
|
+
await ensureNodeIndex(nestId);
|
|
2111
|
+
const documents = await readIndexedNodes(nestId);
|
|
2112
|
+
const visible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
2113
|
+
const tags = {};
|
|
2114
|
+
const types = {};
|
|
2115
|
+
const direct = /* @__PURE__ */ new Map();
|
|
2116
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
2117
|
+
let editedLast7d = 0;
|
|
2118
|
+
for (const d of visible) {
|
|
2119
|
+
const fm = d.frontmatter;
|
|
2120
|
+
types[fm.type || "document"] = (types[fm.type || "document"] || 0) + 1;
|
|
2121
|
+
for (const t of fm.tags || []) tags[String(t)] = (tags[String(t)] || 0) + 1;
|
|
2122
|
+
const folder = folderOfId(d.id);
|
|
2123
|
+
if (folder) direct.set(folder, (direct.get(folder) || 0) + 1);
|
|
2124
|
+
const edited = Date.parse(String(fm.updated_at || ""));
|
|
2125
|
+
if (!Number.isNaN(edited) && edited >= cutoff) editedLast7d++;
|
|
2126
|
+
}
|
|
2127
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2128
|
+
for (const folder of direct.keys()) {
|
|
2129
|
+
const segs = folder.split("/");
|
|
2130
|
+
for (let i = 1; i <= segs.length; i++) paths.add(segs.slice(0, i).join("/"));
|
|
2131
|
+
}
|
|
2132
|
+
const folders = withSubtreeTotals(
|
|
2133
|
+
[...paths].map((path) => ({ path, count: direct.get(path) ?? 0 }))
|
|
2134
|
+
);
|
|
2135
|
+
const statusMap = await getDisplayStatuses(nestId);
|
|
2136
|
+
const statuses = {};
|
|
2137
|
+
for (const d of visible) {
|
|
2138
|
+
const st = statusMap.get(d.id) ?? "draft";
|
|
2139
|
+
statuses[st] = (statuses[st] || 0) + 1;
|
|
2140
|
+
}
|
|
2141
|
+
const authorship = await resolveAuthorship(
|
|
2142
|
+
nestId,
|
|
2143
|
+
visible.map((d) => ({
|
|
2144
|
+
id: d.id,
|
|
2145
|
+
frontmatterAuthor: d.frontmatter.author ?? null
|
|
2146
|
+
}))
|
|
2147
|
+
);
|
|
2148
|
+
const authors = [
|
|
2149
|
+
...new Set(
|
|
2150
|
+
[...authorship.values()].map((a) => a.author).filter((a) => !!a)
|
|
2151
|
+
)
|
|
2152
|
+
].sort((a, b) => a.localeCompare(b));
|
|
2153
|
+
const recent = visible.filter((d) => d.frontmatter.updated_at).sort(
|
|
2154
|
+
(a, b) => String(b.frontmatter.updated_at).localeCompare(
|
|
2155
|
+
String(a.frontmatter.updated_at)
|
|
2156
|
+
)
|
|
2157
|
+
).slice(0, RECENT_LIMIT).map((d) => ({
|
|
2158
|
+
id: d.id,
|
|
2159
|
+
title: String(d.frontmatter.title ?? ""),
|
|
2160
|
+
type: d.frontmatter.type || "document",
|
|
2161
|
+
updated_at: d.frontmatter.updated_at
|
|
2162
|
+
}));
|
|
2163
|
+
return {
|
|
2164
|
+
total: visible.length,
|
|
2165
|
+
folders,
|
|
2166
|
+
tags,
|
|
2167
|
+
types,
|
|
2168
|
+
statuses,
|
|
2169
|
+
authors,
|
|
2170
|
+
editedLast7d,
|
|
2171
|
+
recent
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2268
2174
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2269
2175
|
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
2270
2176
|
}
|
|
@@ -2290,14 +2196,27 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2290
2196
|
return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
|
|
2291
2197
|
}
|
|
2292
2198
|
async function createNode(nestId, input, userEmail) {
|
|
2199
|
+
const generatedId = !input.id && !slugify(input.title);
|
|
2200
|
+
for (let attempt = 0; ; attempt++) {
|
|
2201
|
+
try {
|
|
2202
|
+
return await createNodeOnce(nestId, input, userEmail);
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
|
|
2205
|
+
throw err;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
async function createNodeOnce(nestId, input, userEmail) {
|
|
2293
2211
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2294
2212
|
assertTitleLength(input.title);
|
|
2295
|
-
|
|
2213
|
+
assertTitleReadable(input.title);
|
|
2296
2214
|
const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
|
|
2297
2215
|
let id = input.id;
|
|
2298
2216
|
if (!id) {
|
|
2299
2217
|
const folderSegments = folderToSegments(input.folder ?? "");
|
|
2300
|
-
|
|
2218
|
+
const prefix = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/` : "nodes/";
|
|
2219
|
+
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
2301
2220
|
}
|
|
2302
2221
|
const tags = (input.tags || []).map(normalizeTag2);
|
|
2303
2222
|
const requestedType = input.type || "document";
|
|
@@ -2558,7 +2477,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2558
2477
|
if (patch.title) {
|
|
2559
2478
|
if (patch.title !== node.frontmatter.title) {
|
|
2560
2479
|
assertTitleLength(patch.title);
|
|
2561
|
-
|
|
2480
|
+
assertTitleReadable(patch.title);
|
|
2562
2481
|
}
|
|
2563
2482
|
node = {
|
|
2564
2483
|
...node,
|
|
@@ -2706,6 +2625,8 @@ var NODE_KEYED_TABLES = [
|
|
|
2706
2625
|
["annotation_threads", "node_id"],
|
|
2707
2626
|
["comments", "node_id"],
|
|
2708
2627
|
["node_tag_index", "node_id"],
|
|
2628
|
+
["node_index", "node_id"],
|
|
2629
|
+
["node_suggestions", "node_id"],
|
|
2709
2630
|
["watchers", "node_id"],
|
|
2710
2631
|
["schedules", "agent_node"],
|
|
2711
2632
|
// A live capability token bound to one agent node — deleting the agent must
|
|
@@ -2784,8 +2705,8 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2784
2705
|
await storage.writeDocument(newId, serializeNodeSafe(moved));
|
|
2785
2706
|
try {
|
|
2786
2707
|
const nestPath = resolveNestPath(nestId);
|
|
2787
|
-
const oldChain =
|
|
2788
|
-
const newChain =
|
|
2708
|
+
const oldChain = join(nestPath, dirname(nodeId), ".versions", leaf);
|
|
2709
|
+
const newChain = join(nestPath, dirname(newId), ".versions", leaf);
|
|
2789
2710
|
if (oldChain !== newChain) {
|
|
2790
2711
|
await mkdir(dirname(newChain), { recursive: true });
|
|
2791
2712
|
await rename(oldChain, newChain);
|
|
@@ -2818,6 +2739,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2818
2739
|
await storage.deleteDocument(oldDerived);
|
|
2819
2740
|
} catch {
|
|
2820
2741
|
}
|
|
2742
|
+
try {
|
|
2743
|
+
await storage.regenerateIndex();
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
console.error("[move] regenerateIndex failed", nestId, nodeId, newId, err);
|
|
2746
|
+
}
|
|
2821
2747
|
await trackEvent("node.move", { nestId, nodeId, newId });
|
|
2822
2748
|
return { node: moved, previousId: nodeId };
|
|
2823
2749
|
});
|
|
@@ -2888,7 +2814,7 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2888
2814
|
|
|
2889
2815
|
// src/nests/unsynced-service.ts
|
|
2890
2816
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2891
|
-
import { join as
|
|
2817
|
+
import { join as join2, relative, resolve } from "path";
|
|
2892
2818
|
import { rm } from "fs/promises";
|
|
2893
2819
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2894
2820
|
function isNestStorageRoot(absPath) {
|
|
@@ -2905,7 +2831,7 @@ function scanMarkdown(dir) {
|
|
|
2905
2831
|
}
|
|
2906
2832
|
for (const e of entries) {
|
|
2907
2833
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2908
|
-
const full =
|
|
2834
|
+
const full = join2(dir, e.name);
|
|
2909
2835
|
if (e.isDirectory()) {
|
|
2910
2836
|
const sub = scanMarkdown(full);
|
|
2911
2837
|
count += sub.count;
|
|
@@ -2955,7 +2881,7 @@ function collectLeafFolders(rel, abs) {
|
|
|
2955
2881
|
if (!e.isDirectory()) continue;
|
|
2956
2882
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2957
2883
|
out.push(
|
|
2958
|
-
...collectLeafFolders(`${rel}/${e.name}`,
|
|
2884
|
+
...collectLeafFolders(`${rel}/${e.name}`, join2(abs, e.name))
|
|
2959
2885
|
);
|
|
2960
2886
|
}
|
|
2961
2887
|
return out;
|
|
@@ -2973,7 +2899,7 @@ function listUnsyncedFolders() {
|
|
|
2973
2899
|
if (!e.isDirectory()) continue;
|
|
2974
2900
|
if (e.name.startsWith(".")) continue;
|
|
2975
2901
|
if (RESERVED.has(e.name)) continue;
|
|
2976
|
-
const abs =
|
|
2902
|
+
const abs = join2(root, e.name);
|
|
2977
2903
|
if (isNestStorageRoot(abs)) continue;
|
|
2978
2904
|
out.push(...collectLeafFolders(e.name, abs));
|
|
2979
2905
|
}
|
|
@@ -3010,7 +2936,7 @@ function collectMarkdownFiles(dir, root) {
|
|
|
3010
2936
|
}
|
|
3011
2937
|
for (const e of entries) {
|
|
3012
2938
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
3013
|
-
const full =
|
|
2939
|
+
const full = join2(dir, e.name);
|
|
3014
2940
|
if (e.isDirectory()) {
|
|
3015
2941
|
out.push(...collectMarkdownFiles(full, root));
|
|
3016
2942
|
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
@@ -3028,7 +2954,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3028
2954
|
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
3029
2955
|
);
|
|
3030
2956
|
assertSafeFolderName(folderName);
|
|
3031
|
-
const src =
|
|
2957
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3032
2958
|
if (isNestStorageRoot(src)) {
|
|
3033
2959
|
throw new ValidationError("Folder is not eligible for sync");
|
|
3034
2960
|
}
|
|
@@ -3084,7 +3010,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3084
3010
|
async function deleteUnsyncedFolder(folderName) {
|
|
3085
3011
|
console.log(`[unsynced] delete requested folder="${folderName}"`);
|
|
3086
3012
|
assertSafeFolderName(folderName);
|
|
3087
|
-
const src =
|
|
3013
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3088
3014
|
if (isNestStorageRoot(src)) {
|
|
3089
3015
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3090
3016
|
}
|
|
@@ -3180,9 +3106,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
3180
3106
|
}
|
|
3181
3107
|
const out = await Promise.all(deduped.map(annotate));
|
|
3182
3108
|
const docRows = await db.all(
|
|
3183
|
-
|
|
3109
|
+
`SELECT nest_id, COUNT(DISTINCT node_id) AS c, MAX(created_at) AS last_activity
|
|
3110
|
+
FROM node_versions GROUP BY nest_id`
|
|
3184
3111
|
);
|
|
3185
3112
|
const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
3113
|
+
const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
|
|
3186
3114
|
const collabRows = await db.all(
|
|
3187
3115
|
"SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
|
|
3188
3116
|
);
|
|
@@ -3236,6 +3164,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3236
3164
|
collaborator_emails: []
|
|
3237
3165
|
} : {
|
|
3238
3166
|
...n,
|
|
3167
|
+
last_activity: activityByNest.get(n.id) ?? null,
|
|
3239
3168
|
document_count: docByNest.get(n.id) ?? 0,
|
|
3240
3169
|
collaborator_count: collabByNest.get(n.id) ?? 0,
|
|
3241
3170
|
steward_count: stewardByNest.get(n.id) ?? 0,
|
|
@@ -3499,7 +3428,7 @@ async function addCollaborator(params) {
|
|
|
3499
3428
|
if (existing) {
|
|
3500
3429
|
userId = existing.id;
|
|
3501
3430
|
} else {
|
|
3502
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3431
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3503
3432
|
userId = uuid3();
|
|
3504
3433
|
await db.run(
|
|
3505
3434
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3691,7 +3620,7 @@ async function resolveGrantee(email) {
|
|
|
3691
3620
|
const e = normalizeEmail(email);
|
|
3692
3621
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3693
3622
|
if (existing) return existing.id;
|
|
3694
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3623
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3695
3624
|
const id = uuid4();
|
|
3696
3625
|
await db.run(
|
|
3697
3626
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3699,6 +3628,41 @@ async function resolveGrantee(email) {
|
|
|
3699
3628
|
);
|
|
3700
3629
|
return id;
|
|
3701
3630
|
}
|
|
3631
|
+
async function assertGrantAddsAccess(params) {
|
|
3632
|
+
const { nestId, email, userId, targetType, target, role } = params;
|
|
3633
|
+
const roles = [];
|
|
3634
|
+
if (userId) {
|
|
3635
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
3636
|
+
if (access.ownerId === userId) roles.push("owner");
|
|
3637
|
+
const collab = collabPermToRole(access.collaboratorPermission);
|
|
3638
|
+
if (collab) roles.push(collab);
|
|
3639
|
+
for (const teamRole of await resolveTeamRolesForUser(nestId, userId)) {
|
|
3640
|
+
roles.push(teamRole);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
if (targetType === "document") {
|
|
3644
|
+
const stewards = await resolveStewardsForNode(nestId, target);
|
|
3645
|
+
for (const s of stewards) {
|
|
3646
|
+
if (s.steward.userEmail.toLowerCase() === email.toLowerCase()) {
|
|
3647
|
+
roles.push(s.steward.role);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
const covered = role === "write" ? canEditWith(roles) : roles.length > 0;
|
|
3652
|
+
if (covered) {
|
|
3653
|
+
const held = primaryRole(roles) ?? "existing";
|
|
3654
|
+
throw new ValidationError(
|
|
3655
|
+
`${email} already has ${held} access here, so this ${targetType} is already ${role === "write" ? "editable by" : "visible to"} them.`
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
if (!userId) return;
|
|
3659
|
+
const existing = await resolveNodeGrant(nestId, userId, target);
|
|
3660
|
+
if (existing && (role === "read" || existing === "write")) {
|
|
3661
|
+
throw new ValidationError(
|
|
3662
|
+
`${email} has already been shared this ${targetType} (${existing === "write" ? "can edit" : "can view"}).`
|
|
3663
|
+
);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3702
3666
|
grantRoutes.post("/", async (c) => {
|
|
3703
3667
|
const nestId = c.req.param("nestId");
|
|
3704
3668
|
await requireManage(c, nestId);
|
|
@@ -3714,7 +3678,19 @@ grantRoutes.post("/", async (c) => {
|
|
|
3714
3678
|
if (!["read", "write"].includes(role)) {
|
|
3715
3679
|
throw new ValidationError("role must be read | write");
|
|
3716
3680
|
}
|
|
3717
|
-
const
|
|
3681
|
+
const email = normalizeEmail(body.email);
|
|
3682
|
+
const known = await getDb().get("SELECT id FROM users WHERE LOWER(email) = ?", [
|
|
3683
|
+
email
|
|
3684
|
+
]);
|
|
3685
|
+
await assertGrantAddsAccess({
|
|
3686
|
+
nestId,
|
|
3687
|
+
email,
|
|
3688
|
+
userId: known?.id ?? null,
|
|
3689
|
+
targetType,
|
|
3690
|
+
target,
|
|
3691
|
+
role
|
|
3692
|
+
});
|
|
3693
|
+
const userId = known?.id ?? await resolveGrantee(body.email);
|
|
3718
3694
|
const grant = await createGrant({
|
|
3719
3695
|
nestId,
|
|
3720
3696
|
targetType,
|
|
@@ -4122,7 +4098,7 @@ function toSearchHit(node, query) {
|
|
|
4122
4098
|
import { Hono as Hono8 } from "hono";
|
|
4123
4099
|
|
|
4124
4100
|
// src/nodes/markdown-export.ts
|
|
4125
|
-
import { parseDocument as
|
|
4101
|
+
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
4126
4102
|
function nodeToMarkdown(node) {
|
|
4127
4103
|
const tags = node.tags ?? [];
|
|
4128
4104
|
return [
|
|
@@ -4158,7 +4134,7 @@ function rawContentType(nodeType) {
|
|
|
4158
4134
|
function frontmatterBlockOf(raw) {
|
|
4159
4135
|
if (!raw || !raw.startsWith("---")) return "";
|
|
4160
4136
|
try {
|
|
4161
|
-
const body =
|
|
4137
|
+
const body = parseDocument2("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
|
|
4162
4138
|
if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
|
|
4163
4139
|
} catch {
|
|
4164
4140
|
}
|
|
@@ -4204,7 +4180,14 @@ nodeRoutes.get("/", async (c) => {
|
|
|
4204
4180
|
// Lets an owner preview what making the nest public would expose.
|
|
4205
4181
|
...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
|
|
4206
4182
|
// Ignore junk rather than 400 — a bad ?limit should not fail a listing.
|
|
4207
|
-
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {}
|
|
4183
|
+
...limit !== void 0 && Number.isFinite(limit) && limit > 0 ? { limit } : {},
|
|
4184
|
+
// Present (even as "") = one folder level only, for the lazily-expanded
|
|
4185
|
+
// document tree. Absent = the whole nest, as every existing caller expects.
|
|
4186
|
+
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
4187
|
+
// Bodies are off by default — the list is served from the DB index, which
|
|
4188
|
+
// holds metadata only. Opt back in and the listing crawls the vault for
|
|
4189
|
+
// them; kept for any client that was reading `content` off this route.
|
|
4190
|
+
...q.content === "1" || q.content === "true" ? { withContent: true } : {}
|
|
4208
4191
|
});
|
|
4209
4192
|
return c.json({ count: nodes.length, nodes });
|
|
4210
4193
|
});
|
|
@@ -4246,7 +4229,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
4246
4229
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4247
4230
|
const nestId = c.req.param("nestId");
|
|
4248
4231
|
const nodeId = c.req.param("nodeId");
|
|
4249
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4232
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-U4CKL7Y5.js");
|
|
4250
4233
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
4251
4234
|
nestId,
|
|
4252
4235
|
nodeId
|
|
@@ -4267,7 +4250,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4267
4250
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4268
4251
|
const nestId = c.req.param("nestId");
|
|
4269
4252
|
const nodeId = c.req.param("nodeId");
|
|
4270
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4253
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-H2M5ACPQ.js");
|
|
4271
4254
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4272
4255
|
return c.json({ reviews: history });
|
|
4273
4256
|
});
|
|
@@ -5327,7 +5310,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
5327
5310
|
|
|
5328
5311
|
// src/nests/bundle-service.ts
|
|
5329
5312
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
5330
|
-
import { join as
|
|
5313
|
+
import { join as join3, sep } from "path";
|
|
5331
5314
|
import JSZip from "jszip";
|
|
5332
5315
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
5333
5316
|
function walkFiles(root) {
|
|
@@ -5340,7 +5323,7 @@ function walkFiles(root) {
|
|
|
5340
5323
|
}
|
|
5341
5324
|
for (const rel of entries) {
|
|
5342
5325
|
try {
|
|
5343
|
-
if (statSync2(
|
|
5326
|
+
if (statSync2(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
5344
5327
|
} catch {
|
|
5345
5328
|
}
|
|
5346
5329
|
}
|
|
@@ -5381,7 +5364,7 @@ async function exportNestBundle(nestId, includeDrafts) {
|
|
|
5381
5364
|
const segs = rel.split("/");
|
|
5382
5365
|
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
5383
5366
|
if (!keep) continue;
|
|
5384
|
-
zip.file(rel, readFileSync2(
|
|
5367
|
+
zip.file(rel, readFileSync2(join3(nestPath, rel)));
|
|
5385
5368
|
}
|
|
5386
5369
|
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
5387
5370
|
return zip.generateAsync({ type: "arraybuffer" });
|
|
@@ -5552,7 +5535,9 @@ queryRoutes.get("/changes", async (c) => {
|
|
|
5552
5535
|
const includeContent = ["1", "true"].includes(
|
|
5553
5536
|
c.req.query("include_content") || ""
|
|
5554
5537
|
);
|
|
5555
|
-
const all = await listNodesForCaller(nestId, userId
|
|
5538
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
5539
|
+
...includeContent ? { withContent: true } : {}
|
|
5540
|
+
});
|
|
5556
5541
|
const db = getDb();
|
|
5557
5542
|
const publicReader = await isPublicReader(nestId, userId);
|
|
5558
5543
|
const approvedRows = await db.all(
|
|
@@ -5619,7 +5604,10 @@ queryRoutes.get("/runnables", async (c) => {
|
|
|
5619
5604
|
const nestId = c.req.param("nestId");
|
|
5620
5605
|
const userId = c.get("userId");
|
|
5621
5606
|
const all = await listNodesForCaller(nestId, userId, {
|
|
5622
|
-
type: RUNNABLE_NODE_TYPES
|
|
5607
|
+
type: RUNNABLE_NODE_TYPES,
|
|
5608
|
+
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
5609
|
+
// run the thing, so this surface pays for the file reads.
|
|
5610
|
+
withContent: true
|
|
5623
5611
|
});
|
|
5624
5612
|
const runnables = all.map((n) => ({
|
|
5625
5613
|
id: n.id,
|
|
@@ -5914,6 +5902,25 @@ queryRoutes.get("/overview", async (c) => {
|
|
|
5914
5902
|
}))
|
|
5915
5903
|
});
|
|
5916
5904
|
});
|
|
5905
|
+
queryRoutes.get("/census", async (c) => {
|
|
5906
|
+
const census = await nestCensus(c.req.param("nestId"), c.get("userId"));
|
|
5907
|
+
return c.json({ census });
|
|
5908
|
+
});
|
|
5909
|
+
queryRoutes.get("/folders", async (c) => {
|
|
5910
|
+
const q = c.req.query();
|
|
5911
|
+
const folders = await listNestFolders(c.req.param("nestId"), {
|
|
5912
|
+
...q.folder !== void 0 ? { folder: q.folder } : {},
|
|
5913
|
+
...q.recursive === "0" || q.recursive === "false" ? { recursive: false } : {}
|
|
5914
|
+
});
|
|
5915
|
+
return c.json({ folders });
|
|
5916
|
+
});
|
|
5917
|
+
queryRoutes.post("/reindex", async (c) => {
|
|
5918
|
+
if (permissionLevel(c.get("nestPermission")) < permissionLevel("write")) {
|
|
5919
|
+
throw new ForbiddenError("Reindexing a nest requires write access");
|
|
5920
|
+
}
|
|
5921
|
+
const indexed = await rebuildNodeIndex(c.req.param("nestId"));
|
|
5922
|
+
return c.json({ indexed });
|
|
5923
|
+
});
|
|
5917
5924
|
queryRoutes.get("/comment-counts", async (c) => {
|
|
5918
5925
|
const counts = await countThreadsByNode(c.req.param("nestId"));
|
|
5919
5926
|
return c.json({ counts });
|
|
@@ -6058,30 +6065,6 @@ import { Hono as Hono12 } from "hono";
|
|
|
6058
6065
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6059
6066
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
6060
6067
|
|
|
6061
|
-
// src/shared/node-id.ts
|
|
6062
|
-
function assertSafeNodeId(rawId) {
|
|
6063
|
-
let id = rawId;
|
|
6064
|
-
try {
|
|
6065
|
-
id = decodeURIComponent(rawId);
|
|
6066
|
-
} catch {
|
|
6067
|
-
}
|
|
6068
|
-
if (!id || id.length > 512) {
|
|
6069
|
-
throw new ValidationError("invalid node id");
|
|
6070
|
-
}
|
|
6071
|
-
if (id.includes("\0") || id.includes("\\")) {
|
|
6072
|
-
throw new ValidationError("invalid node id");
|
|
6073
|
-
}
|
|
6074
|
-
if (id.startsWith("/")) {
|
|
6075
|
-
throw new ValidationError("invalid node id (absolute path)");
|
|
6076
|
-
}
|
|
6077
|
-
for (const seg of id.split("/")) {
|
|
6078
|
-
if (seg === "" || seg === "." || seg === "..") {
|
|
6079
|
-
throw new ValidationError("invalid node id (path traversal)");
|
|
6080
|
-
}
|
|
6081
|
-
}
|
|
6082
|
-
return id;
|
|
6083
|
-
}
|
|
6084
|
-
|
|
6085
6068
|
// src/telemetry/trace-log.ts
|
|
6086
6069
|
var PRUNE_EVERY = 500;
|
|
6087
6070
|
var insertsSincePrune = 0;
|
|
@@ -6385,6 +6368,30 @@ async function appendRunStep(runId, input) {
|
|
|
6385
6368
|
return { seq };
|
|
6386
6369
|
}
|
|
6387
6370
|
var RUN_STATUSES = ["running", "succeeded", "failed", "cancelled"];
|
|
6371
|
+
async function cancelDescendants(runId) {
|
|
6372
|
+
const db = getDb();
|
|
6373
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
6374
|
+
const trace = JSON.stringify({ error: "Parent run cancelled" });
|
|
6375
|
+
let frontier = [runId];
|
|
6376
|
+
let cancelled = 0;
|
|
6377
|
+
for (let depth = 0; frontier.length && depth < 64; depth++) {
|
|
6378
|
+
const ph = frontier.map(() => "?").join(",");
|
|
6379
|
+
const kids = await db.all(
|
|
6380
|
+
`SELECT id FROM runs WHERE parent_run_id IN (${ph}) AND status = 'running'`,
|
|
6381
|
+
frontier
|
|
6382
|
+
);
|
|
6383
|
+
if (!kids.length) break;
|
|
6384
|
+
frontier = kids.map((k) => k.id);
|
|
6385
|
+
const kph = frontier.map(() => "?").join(",");
|
|
6386
|
+
await db.run(
|
|
6387
|
+
`UPDATE runs SET status = 'cancelled', trace = COALESCE(trace, ?), finished_at = ?
|
|
6388
|
+
WHERE id IN (${kph}) AND status = 'running'`,
|
|
6389
|
+
[trace, at, ...frontier]
|
|
6390
|
+
);
|
|
6391
|
+
cancelled += frontier.length;
|
|
6392
|
+
}
|
|
6393
|
+
return cancelled;
|
|
6394
|
+
}
|
|
6388
6395
|
async function closeRun(runId, input) {
|
|
6389
6396
|
const status = input.status;
|
|
6390
6397
|
if (typeof status !== "string" || !RUN_STATUSES.includes(status)) {
|
|
@@ -6406,6 +6413,7 @@ async function closeRun(runId, input) {
|
|
|
6406
6413
|
if (!run) throw new NotFoundError("Run not found");
|
|
6407
6414
|
throw new ConflictError("Run already terminal \u2014 status can't be changed");
|
|
6408
6415
|
}
|
|
6416
|
+
if (status === "cancelled") await cancelDescendants(runId);
|
|
6409
6417
|
return { status };
|
|
6410
6418
|
}
|
|
6411
6419
|
async function getRunTrace(run, opts) {
|
|
@@ -7709,7 +7717,9 @@ ${context_md}`;
|
|
|
7709
7717
|
{},
|
|
7710
7718
|
await opContext(nestId, userEmail)
|
|
7711
7719
|
);
|
|
7712
|
-
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7720
|
+
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7721
|
+
withContent: true
|
|
7722
|
+
});
|
|
7713
7723
|
const types = {};
|
|
7714
7724
|
const tags = {};
|
|
7715
7725
|
for (const n of visible) {
|
|
@@ -8544,14 +8554,14 @@ ${list}`,
|
|
|
8544
8554
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8545
8555
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8546
8556
|
try {
|
|
8547
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8548
|
-
const db = (await import("./client-
|
|
8557
|
+
const { createGrant: createGrant2 } = await import("./grants-service-ME7HIVHU.js");
|
|
8558
|
+
const db = (await import("./client-SW5I6ZBK.js")).getDb();
|
|
8549
8559
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8550
8560
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8551
8561
|
if (!e) return "email is required.";
|
|
8552
8562
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
8553
8563
|
if (!row) {
|
|
8554
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
8564
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
8555
8565
|
const { v4: uuid16 } = await import("uuid");
|
|
8556
8566
|
const id = uuid16();
|
|
8557
8567
|
await db.run(
|
|
@@ -9014,8 +9024,9 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9014
9024
|
throw new ValidationError("status must be running | succeeded | failed | cancelled");
|
|
9015
9025
|
}
|
|
9016
9026
|
const terminal = status !== "running";
|
|
9017
|
-
await getDb().run(
|
|
9018
|
-
`UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
|
|
9027
|
+
const res = await getDb().run(
|
|
9028
|
+
`UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
|
|
9029
|
+
WHERE id = ? AND status = 'running'`,
|
|
9019
9030
|
[
|
|
9020
9031
|
status,
|
|
9021
9032
|
body.trace === void 0 ? null : JSON.stringify(body.trace),
|
|
@@ -9023,6 +9034,13 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
9023
9034
|
run.id
|
|
9024
9035
|
]
|
|
9025
9036
|
);
|
|
9037
|
+
if (!res.changes) {
|
|
9038
|
+
const now = await getDb().get("SELECT status FROM runs WHERE id = ?", [
|
|
9039
|
+
run.id
|
|
9040
|
+
]);
|
|
9041
|
+
throw new ConflictError(`Run already terminal (${now?.status ?? "closed"})`);
|
|
9042
|
+
}
|
|
9043
|
+
if (status === "cancelled") await cancelDescendants(run.id);
|
|
9026
9044
|
if (status === "failed") {
|
|
9027
9045
|
const err = body.trace && typeof body.trace === "object" ? String(body.trace.error ?? "").slice(0, 200) : "";
|
|
9028
9046
|
void dispatchEvent({
|
|
@@ -9631,8 +9649,8 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
9631
9649
|
// src/nests/asset-routes.ts
|
|
9632
9650
|
import { Hono as Hono18 } from "hono";
|
|
9633
9651
|
import { v4 as uuid13 } from "uuid";
|
|
9634
|
-
import { mkdir as mkdir2, readFile
|
|
9635
|
-
import { join as
|
|
9652
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
9653
|
+
import { join as join4 } from "path";
|
|
9636
9654
|
var assetRoutes = new Hono18();
|
|
9637
9655
|
var CONTENT_TYPES = {
|
|
9638
9656
|
png: "image/png",
|
|
@@ -9680,9 +9698,9 @@ assetRoutes.post("/", async (c) => {
|
|
|
9680
9698
|
);
|
|
9681
9699
|
}
|
|
9682
9700
|
const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
9683
|
-
const dir =
|
|
9701
|
+
const dir = join4(resolveNestPath(nestId), "assets");
|
|
9684
9702
|
await mkdir2(dir, { recursive: true });
|
|
9685
|
-
await writeFile(
|
|
9703
|
+
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
9686
9704
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
9687
9705
|
return c.json(
|
|
9688
9706
|
{
|
|
@@ -9704,7 +9722,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
9704
9722
|
}
|
|
9705
9723
|
let bytes;
|
|
9706
9724
|
try {
|
|
9707
|
-
bytes = await
|
|
9725
|
+
bytes = await readFile(join4(resolveNestPath(nestId), "assets", name));
|
|
9708
9726
|
} catch {
|
|
9709
9727
|
throw new NotFoundError("Asset not found");
|
|
9710
9728
|
}
|
|
@@ -10514,7 +10532,7 @@ function rowToComment(row) {
|
|
|
10514
10532
|
|
|
10515
10533
|
// src/governance/stewards-parser.ts
|
|
10516
10534
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
10517
|
-
import { join as
|
|
10535
|
+
import { join as join5 } from "path";
|
|
10518
10536
|
function parseStewardsYaml(content) {
|
|
10519
10537
|
const result = { version: 1 };
|
|
10520
10538
|
const lines = content.split("\n");
|
|
@@ -10580,9 +10598,9 @@ function parseEntry(str) {
|
|
|
10580
10598
|
function loadStewardsConfig(nestId) {
|
|
10581
10599
|
const nestPath = resolveNestPath(nestId);
|
|
10582
10600
|
const candidates = [
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10601
|
+
join5(nestPath, "stewards.yaml"),
|
|
10602
|
+
join5(nestPath, "stewards.yml"),
|
|
10603
|
+
join5(nestPath, ".context", "stewards.yaml")
|
|
10586
10604
|
];
|
|
10587
10605
|
for (const candidatePath of candidates) {
|
|
10588
10606
|
if (existsSync(candidatePath)) {
|
|
@@ -10943,7 +10961,7 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10943
10961
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
10944
10962
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
10945
10963
|
const resolutions = await getDb().all(
|
|
10946
|
-
`SELECT version, status, resolved_by
|
|
10964
|
+
`SELECT version, status, resolved_by, resolution_note
|
|
10947
10965
|
FROM review_requests
|
|
10948
10966
|
WHERE nest_id = ? AND node_id = ?
|
|
10949
10967
|
AND status IN ('approved', 'rejected')
|
|
@@ -10954,13 +10972,21 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10954
10972
|
const byVersion = /* @__PURE__ */ new Map();
|
|
10955
10973
|
for (const r of resolutions) {
|
|
10956
10974
|
if (!byVersion.has(r.version)) {
|
|
10957
|
-
byVersion.set(r.version, {
|
|
10975
|
+
byVersion.set(r.version, {
|
|
10976
|
+
status: r.status,
|
|
10977
|
+
resolvedBy: r.resolved_by,
|
|
10978
|
+
resolutionNote: r.resolution_note?.trim() || void 0
|
|
10979
|
+
});
|
|
10958
10980
|
}
|
|
10959
10981
|
}
|
|
10960
10982
|
const enriched = await Promise.all(
|
|
10961
10983
|
allVersions.map(async (v) => {
|
|
10962
10984
|
const r = byVersion.get(v.version);
|
|
10963
|
-
const resolution = r ? {
|
|
10985
|
+
const resolution = r ? {
|
|
10986
|
+
resolvedBy: r.resolvedBy,
|
|
10987
|
+
resolutionStatus: r.status,
|
|
10988
|
+
resolutionNote: r.resolutionNote
|
|
10989
|
+
} : {};
|
|
10964
10990
|
try {
|
|
10965
10991
|
const diff = await versionManager.getDiff(nodeId, v.version);
|
|
10966
10992
|
return { ...v, ...resolution, diff: diff ?? void 0 };
|
|
@@ -11338,19 +11364,19 @@ async function ensureAnonymousUser() {
|
|
|
11338
11364
|
// src/app.ts
|
|
11339
11365
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
11340
11366
|
import { fileURLToPath } from "url";
|
|
11341
|
-
import { dirname as dirname2, join as
|
|
11367
|
+
import { dirname as dirname2, join as join6, relative as relative2 } from "path";
|
|
11342
11368
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
11343
11369
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
11344
11370
|
var SERVICE_VERSION = (() => {
|
|
11345
11371
|
try {
|
|
11346
|
-
return JSON.parse(readFileSync4(
|
|
11372
|
+
return JSON.parse(readFileSync4(join6(HERE, "..", "package.json"), "utf8")).version;
|
|
11347
11373
|
} catch {
|
|
11348
11374
|
return "unknown";
|
|
11349
11375
|
}
|
|
11350
11376
|
})();
|
|
11351
11377
|
var UI_DIR_CANDIDATES = [
|
|
11352
|
-
|
|
11353
|
-
|
|
11378
|
+
join6(HERE, "web3"),
|
|
11379
|
+
join6(process.cwd(), "dist", "web3")
|
|
11354
11380
|
];
|
|
11355
11381
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
11356
11382
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|