@promptowl/contextnest-community 1.16.1 → 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/CONFIGURATION.md +415 -415
- package/LICENSE.md +142 -142
- package/README.md +168 -169
- package/dist/{chunk-D6HICFVW.js → chunk-34UXRZXD.js} +59 -14
- package/dist/chunk-F66VCBLA.js +136 -0
- package/dist/{chunk-L7UCMGWZ.js → chunk-FUUJG6XP.js} +641 -285
- package/dist/{chunk-QJVRZT6M.js → chunk-HGN45I5P.js} +59 -21
- package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
- package/dist/chunk-OOBZG6BW.js +379 -0
- package/dist/{chunk-OTGZQZKK.js → chunk-QUZXRLUA.js} +11 -10
- package/dist/{chunk-CLKZZZS2.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-I535QVTW.js → grants-service-ME7HIVHU.js} +5 -5
- package/dist/index.js +700 -526
- 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-V2GR6VM3.js → review-service-H2M5ACPQ.js} +11 -9
- package/dist/{stewardship-service-LTREGVSJ.js → stewardship-service-U4CKL7Y5.js} +8 -4
- package/dist/{version-service-V6K5SZ3P.js → version-service-JWEN5UUW.js} +6 -4
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-DONv5_Ni.css +1 -0
- package/dist/web3/assets/index-jSGrnboh.js +1065 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -165
- package/dist/chunk-XRK6SQSC.js +0 -58
- package/dist/web3/assets/index-DnQARUL3.css +0 -1
- package/dist/web3/assets/index-_m7WyozM.js +0 -1051
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
addWatcher,
|
|
4
4
|
approve,
|
|
5
5
|
canReadNode,
|
|
6
|
+
canRequestDeletion,
|
|
6
7
|
cancelReview,
|
|
7
8
|
declineDeletion,
|
|
8
9
|
deletionTargetTitle,
|
|
@@ -38,25 +39,47 @@ import {
|
|
|
38
39
|
seedDefaultEdgeTypes,
|
|
39
40
|
sendTestNotification,
|
|
40
41
|
submitForReview,
|
|
41
|
-
withNodeWriteLock
|
|
42
|
-
|
|
42
|
+
withNodeWriteLock,
|
|
43
|
+
withdrawDeletionRequest
|
|
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";
|
|
43
65
|
import {
|
|
44
66
|
createGrant,
|
|
45
67
|
deleteGrant,
|
|
46
68
|
grantCoversNode,
|
|
47
|
-
|
|
69
|
+
grantRoleForNode,
|
|
48
70
|
listGrants,
|
|
49
71
|
listUserGrants,
|
|
50
72
|
resolveNodeGrant
|
|
51
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-QUZXRLUA.js";
|
|
52
74
|
import {
|
|
53
75
|
generateApiKey,
|
|
54
76
|
getKeyPrefix,
|
|
55
77
|
hashApiKey,
|
|
56
78
|
hashPassword,
|
|
57
79
|
parseBearerToken,
|
|
80
|
+
verifyKeyRequest,
|
|
58
81
|
verifyPassword
|
|
59
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-F66VCBLA.js";
|
|
60
83
|
import {
|
|
61
84
|
checkConflict,
|
|
62
85
|
createVersion,
|
|
@@ -64,36 +87,32 @@ import {
|
|
|
64
87
|
getApprovedVersions,
|
|
65
88
|
getCurrentVersion,
|
|
66
89
|
getDisplayStatus,
|
|
90
|
+
getDisplayStatuses,
|
|
67
91
|
getTrackedNodeIds,
|
|
68
92
|
getVersions,
|
|
69
93
|
listMyDrafts,
|
|
70
94
|
setApprovedVersion,
|
|
71
95
|
upsertVersion
|
|
72
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-34UXRZXD.js";
|
|
73
97
|
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
removeSteward,
|
|
86
|
-
resolveGrantRoles,
|
|
87
|
-
resolveStewardsForNode,
|
|
88
|
-
resolveStewardsWithFallback,
|
|
89
|
-
resolveUserRoles,
|
|
90
|
-
syncFromConfig,
|
|
91
|
-
updateSteward
|
|
92
|
-
} from "./chunk-CLKZZZS2.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";
|
|
93
109
|
import {
|
|
94
110
|
addMember,
|
|
111
|
+
assertSafeNodeId,
|
|
95
112
|
buildTitleMap,
|
|
96
113
|
canApproveWith,
|
|
114
|
+
canEditWith,
|
|
115
|
+
collabPermToRole,
|
|
97
116
|
createNest,
|
|
98
117
|
createTeam,
|
|
99
118
|
deleteNest,
|
|
@@ -102,14 +121,17 @@ import {
|
|
|
102
121
|
docLink,
|
|
103
122
|
engineApi,
|
|
104
123
|
engineCache,
|
|
124
|
+
ensureNodeIndex,
|
|
105
125
|
findTeamByExternalId,
|
|
106
126
|
folderForNode,
|
|
127
|
+
folderOfId,
|
|
107
128
|
getAccessConfig,
|
|
108
129
|
getCurrentLicense,
|
|
109
130
|
getNest,
|
|
110
131
|
getSuspensionReason,
|
|
111
132
|
getTeam,
|
|
112
133
|
importNest,
|
|
134
|
+
indexedFolderCounts,
|
|
113
135
|
insertOrIgnore,
|
|
114
136
|
installLicenseKey,
|
|
115
137
|
isConfigSuperAdmin,
|
|
@@ -134,19 +156,25 @@ import {
|
|
|
134
156
|
nestPrimeTags,
|
|
135
157
|
nestReviewsPrimeOnly,
|
|
136
158
|
nestStorageRoot,
|
|
159
|
+
nodesWithSuggestions,
|
|
137
160
|
normalizeTag,
|
|
138
161
|
nowExpr,
|
|
139
162
|
opContext,
|
|
140
163
|
permissionLevel,
|
|
141
164
|
persistSetting,
|
|
165
|
+
primaryRole,
|
|
166
|
+
readIndexedNodes,
|
|
167
|
+
rebuildNodeIndex,
|
|
142
168
|
removeMember,
|
|
143
169
|
removeNodeFromTagIndex,
|
|
144
170
|
renameNest,
|
|
145
171
|
renameTeam,
|
|
146
172
|
replaceTeamRoster,
|
|
147
173
|
resolveMemberIdentity,
|
|
174
|
+
resolveNestAccess,
|
|
148
175
|
resolveNestPath,
|
|
149
176
|
resolveNestPermission,
|
|
177
|
+
resolveTeamRolesForUser,
|
|
150
178
|
sendEmailToRecipient,
|
|
151
179
|
setAllowSelfApprove,
|
|
152
180
|
setPrimeOnlyReview,
|
|
@@ -162,7 +190,7 @@ import {
|
|
|
162
190
|
updateMemberRole,
|
|
163
191
|
validateLicense,
|
|
164
192
|
verifySmtp
|
|
165
|
-
} from "./chunk-
|
|
193
|
+
} from "./chunk-FUUJG6XP.js";
|
|
166
194
|
import {
|
|
167
195
|
AppError,
|
|
168
196
|
ConflictError,
|
|
@@ -178,11 +206,12 @@ import {
|
|
|
178
206
|
initDb,
|
|
179
207
|
isEmailListish,
|
|
180
208
|
isEmailish
|
|
181
|
-
} from "./chunk-
|
|
209
|
+
} from "./chunk-HYSTFMFG.js";
|
|
182
210
|
import {
|
|
183
211
|
ANON_EMAIL,
|
|
184
|
-
ANON_USER_ID
|
|
185
|
-
|
|
212
|
+
ANON_USER_ID,
|
|
213
|
+
MAX_FOLDER_DEPTH
|
|
214
|
+
} from "./chunk-YB3LKF7U.js";
|
|
186
215
|
import {
|
|
187
216
|
normalizeEmail
|
|
188
217
|
} from "./chunk-FRQJWGN3.js";
|
|
@@ -1076,19 +1105,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1076
1105
|
);
|
|
1077
1106
|
const db = getDb();
|
|
1078
1107
|
const userId = c.get("userId");
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
);
|
|
1083
|
-
if (
|
|
1084
|
-
return c.json(
|
|
1085
|
-
{
|
|
1086
|
-
error: "An API key already exists for this user. Rotate it instead.",
|
|
1087
|
-
key_prefix: existing.key_prefix
|
|
1088
|
-
},
|
|
1089
|
-
409
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
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);
|
|
1092
1113
|
const apiKey = generateApiKey();
|
|
1093
1114
|
const keyId = uuid2();
|
|
1094
1115
|
await db.run(
|
|
@@ -1099,11 +1120,11 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1099
1120
|
hashApiKey(apiKey),
|
|
1100
1121
|
getKeyPrefix(apiKey),
|
|
1101
1122
|
body.nest_id || null,
|
|
1102
|
-
|
|
1123
|
+
check.label
|
|
1103
1124
|
]
|
|
1104
1125
|
);
|
|
1105
1126
|
return c.json(
|
|
1106
|
-
{ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) },
|
|
1127
|
+
{ id: keyId, api_key: apiKey, key_prefix: getKeyPrefix(apiKey), label: check.label },
|
|
1107
1128
|
201
|
|
1108
1129
|
);
|
|
1109
1130
|
});
|
|
@@ -1111,15 +1132,22 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1111
1132
|
const body = await c.req.json().catch(() => ({}));
|
|
1112
1133
|
const db = getDb();
|
|
1113
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;
|
|
1114
1142
|
const apiKey = generateApiKey();
|
|
1115
1143
|
const keyId = uuid2();
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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;
|
|
1123
1151
|
await tx.run(
|
|
1124
1152
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1125
1153
|
[
|
|
@@ -1128,29 +1156,47 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1128
1156
|
hashApiKey(apiKey),
|
|
1129
1157
|
getKeyPrefix(apiKey),
|
|
1130
1158
|
nestScope,
|
|
1131
|
-
|
|
1159
|
+
check.label ?? prior.label
|
|
1132
1160
|
]
|
|
1133
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
|
|
1134
1175
|
});
|
|
1135
|
-
return c.json({ api_key: apiKey, key_prefix: getKeyPrefix(apiKey) });
|
|
1136
1176
|
});
|
|
1137
1177
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1138
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);
|
|
1139
1182
|
const keys = await db.all(
|
|
1140
|
-
"SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ?",
|
|
1141
|
-
[
|
|
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]
|
|
1142
1185
|
);
|
|
1143
1186
|
return c.json({ keys });
|
|
1144
1187
|
});
|
|
1145
1188
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1146
1189
|
const db = getDb();
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
+
]);
|
|
1154
1200
|
return c.json({ deleted: true });
|
|
1155
1201
|
});
|
|
1156
1202
|
authRoutes.post("/device", async (c) => {
|
|
@@ -1607,13 +1653,14 @@ import { Hono as Hono3 } from "hono";
|
|
|
1607
1653
|
|
|
1608
1654
|
// src/nodes/service.ts
|
|
1609
1655
|
import {
|
|
1610
|
-
parseDocument
|
|
1656
|
+
parseDocument,
|
|
1611
1657
|
normalizeStatus,
|
|
1612
1658
|
isRejected,
|
|
1659
|
+
filterDocuments,
|
|
1613
1660
|
DocumentNotFoundError
|
|
1614
1661
|
} from "@promptowl/contextnest-engine";
|
|
1615
1662
|
import { rename, mkdir } from "fs/promises";
|
|
1616
|
-
import { dirname, join
|
|
1663
|
+
import { dirname, join } from "path";
|
|
1617
1664
|
|
|
1618
1665
|
// src/governance/safe-publish.ts
|
|
1619
1666
|
import {
|
|
@@ -1641,6 +1688,9 @@ function stripUndefinedDeep(value) {
|
|
|
1641
1688
|
return value;
|
|
1642
1689
|
}
|
|
1643
1690
|
|
|
1691
|
+
// src/nodes/service.ts
|
|
1692
|
+
import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
|
|
1693
|
+
|
|
1644
1694
|
// src/governance/prime-service.ts
|
|
1645
1695
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
1646
1696
|
const flag = documentPrimeFlag(metadata);
|
|
@@ -1658,313 +1708,6 @@ function documentPrimeFlag(metadata) {
|
|
|
1658
1708
|
return void 0;
|
|
1659
1709
|
}
|
|
1660
1710
|
|
|
1661
|
-
// src/governance/external-edit-service.ts
|
|
1662
|
-
import { readFile } from "fs/promises";
|
|
1663
|
-
import { join } from "path";
|
|
1664
|
-
import {
|
|
1665
|
-
detectDrift,
|
|
1666
|
-
stageSuggestion,
|
|
1667
|
-
ChainEventLog,
|
|
1668
|
-
approveSuggestion,
|
|
1669
|
-
rejectSuggestion,
|
|
1670
|
-
listSuggestions,
|
|
1671
|
-
readSuggestion,
|
|
1672
|
-
parseDocument,
|
|
1673
|
-
VersionManager,
|
|
1674
|
-
computeContentHash,
|
|
1675
|
-
getChecksumContent
|
|
1676
|
-
} from "@promptowl/contextnest-engine";
|
|
1677
|
-
var communityRbac = {
|
|
1678
|
-
isCzar: () => false,
|
|
1679
|
-
canIngest: () => true,
|
|
1680
|
-
isDocOwner: () => true
|
|
1681
|
-
};
|
|
1682
|
-
function docPath(nestId, documentId) {
|
|
1683
|
-
return join(resolveNestPath(nestId), `${documentId}.md`);
|
|
1684
|
-
}
|
|
1685
|
-
async function readRaw(nestId, documentId) {
|
|
1686
|
-
try {
|
|
1687
|
-
return await readFile(docPath(nestId, documentId), "utf-8");
|
|
1688
|
-
} catch {
|
|
1689
|
-
return null;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
async function loadChainHead(storage, documentId) {
|
|
1693
|
-
const history = await storage.readHistory(documentId);
|
|
1694
|
-
if (!history || history.versions.length === 0) return null;
|
|
1695
|
-
const latest = history.versions[history.versions.length - 1];
|
|
1696
|
-
try {
|
|
1697
|
-
const content = await new VersionManager(storage).reconstructVersion(
|
|
1698
|
-
documentId,
|
|
1699
|
-
latest.version
|
|
1700
|
-
);
|
|
1701
|
-
return { version: latest.version, content };
|
|
1702
|
-
} catch {
|
|
1703
|
-
return null;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
async function loadLatestApprovedNode(nestId, documentId) {
|
|
1707
|
-
const { storage } = await engineCache.get(nestId);
|
|
1708
|
-
const head = await loadChainHead(storage, documentId);
|
|
1709
|
-
if (!head) return null;
|
|
1710
|
-
return parseDocument(docPath(nestId, documentId), head.content, documentId);
|
|
1711
|
-
}
|
|
1712
|
-
async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
|
|
1713
|
-
const head = await loadChainHead(storage, documentId);
|
|
1714
|
-
if (!head) return false;
|
|
1715
|
-
const headBodyHash = computeContentHash(getChecksumContent(head.content));
|
|
1716
|
-
return headBodyHash === liveBodyHash;
|
|
1717
|
-
}
|
|
1718
|
-
async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
|
|
1719
|
-
const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
|
|
1720
|
-
return res?.meta ?? null;
|
|
1721
|
-
}
|
|
1722
|
-
async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
1723
|
-
const { storage } = await engineCache.get(nestId);
|
|
1724
|
-
const node = await storage.readDocument(documentId).catch(() => null);
|
|
1725
|
-
if (!node) return null;
|
|
1726
|
-
const raw = await readRaw(nestId, documentId);
|
|
1727
|
-
if (raw == null) return null;
|
|
1728
|
-
const drift = detectDrift(raw, node.frontmatter.checksum);
|
|
1729
|
-
if (!drift.drifted) return null;
|
|
1730
|
-
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
1731
|
-
return null;
|
|
1732
|
-
}
|
|
1733
|
-
const approved = await loadChainHead(storage, documentId);
|
|
1734
|
-
if (!approved) return null;
|
|
1735
|
-
const existing = await listSuggestions(storage, documentId);
|
|
1736
|
-
const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
|
|
1737
|
-
if (dup) return { meta: dup, created: false };
|
|
1738
|
-
const result = await stageSuggestion({
|
|
1739
|
-
storage,
|
|
1740
|
-
documentId,
|
|
1741
|
-
approvedRawContent: approved.content,
|
|
1742
|
-
proposedRawContent: raw,
|
|
1743
|
-
source: "out-of-band-edit",
|
|
1744
|
-
actor,
|
|
1745
|
-
docTier: "standard"
|
|
1746
|
-
});
|
|
1747
|
-
return { meta: result.meta, created: true };
|
|
1748
|
-
}
|
|
1749
|
-
async function scanNestForDrift(nestId, actor = "system:scanner") {
|
|
1750
|
-
const { storage, invalidateDiscovery } = await engineCache.get(nestId);
|
|
1751
|
-
invalidateDiscovery();
|
|
1752
|
-
const docs = await storage.discoverDocuments();
|
|
1753
|
-
const results = await Promise.all(
|
|
1754
|
-
docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
|
|
1755
|
-
);
|
|
1756
|
-
const staged = results.filter((r) => r?.created).length;
|
|
1757
|
-
return { scanned: docs.length, staged };
|
|
1758
|
-
}
|
|
1759
|
-
async function getPendingChange(nestId, documentId) {
|
|
1760
|
-
const { storage } = await engineCache.get(nestId);
|
|
1761
|
-
const list = await listSuggestions(storage, documentId);
|
|
1762
|
-
for (let i = list.length - 1; i >= 0; i--) {
|
|
1763
|
-
const meta = list[i];
|
|
1764
|
-
if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
|
|
1765
|
-
continue;
|
|
1766
|
-
}
|
|
1767
|
-
return {
|
|
1768
|
-
suggestion_id: meta.suggestion_id,
|
|
1769
|
-
detected_at: meta.detected_at,
|
|
1770
|
-
source: meta.source,
|
|
1771
|
-
proposed_hash: meta.proposed_hash
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
return null;
|
|
1775
|
-
}
|
|
1776
|
-
async function listNestExternalEdits(nestId) {
|
|
1777
|
-
const { storage } = await engineCache.get(nestId);
|
|
1778
|
-
const docs = await storage.discoverDocuments();
|
|
1779
|
-
const lists = await Promise.all(
|
|
1780
|
-
docs.map(async (doc) => {
|
|
1781
|
-
const metas = await listSuggestions(storage, doc.id);
|
|
1782
|
-
const fresh = [];
|
|
1783
|
-
for (const meta of metas) {
|
|
1784
|
-
if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
fresh.push(meta);
|
|
1788
|
-
}
|
|
1789
|
-
return fresh;
|
|
1790
|
-
})
|
|
1791
|
-
);
|
|
1792
|
-
const entries = lists.flat().map((meta) => ({
|
|
1793
|
-
suggestion_id: meta.suggestion_id,
|
|
1794
|
-
nest_id: nestId,
|
|
1795
|
-
document_id: meta.document_id,
|
|
1796
|
-
source: meta.source,
|
|
1797
|
-
detected_at: meta.detected_at,
|
|
1798
|
-
actor: meta.actor,
|
|
1799
|
-
target_hash: meta.target_hash,
|
|
1800
|
-
proposed_hash: meta.proposed_hash,
|
|
1801
|
-
note: meta.note
|
|
1802
|
-
}));
|
|
1803
|
-
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
1804
|
-
}
|
|
1805
|
-
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
1806
|
-
const { storage } = await engineCache.get(nestId);
|
|
1807
|
-
const found = await readSuggestion(storage, documentId, suggestionId);
|
|
1808
|
-
if (!found) return null;
|
|
1809
|
-
return {
|
|
1810
|
-
suggestion_id: found.meta.suggestion_id,
|
|
1811
|
-
nest_id: nestId,
|
|
1812
|
-
document_id: found.meta.document_id,
|
|
1813
|
-
source: found.meta.source,
|
|
1814
|
-
detected_at: found.meta.detected_at,
|
|
1815
|
-
actor: found.meta.actor,
|
|
1816
|
-
target_hash: found.meta.target_hash,
|
|
1817
|
-
proposed_hash: found.meta.proposed_hash,
|
|
1818
|
-
note: found.meta.note,
|
|
1819
|
-
patch: found.patch
|
|
1820
|
-
};
|
|
1821
|
-
}
|
|
1822
|
-
async function recordVerdict(nestId, event) {
|
|
1823
|
-
try {
|
|
1824
|
-
const { storage } = await engineCache.get(nestId);
|
|
1825
|
-
await new ChainEventLog(storage).append(event);
|
|
1826
|
-
return true;
|
|
1827
|
-
} catch (err) {
|
|
1828
|
-
console.error(
|
|
1829
|
-
`[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
|
|
1830
|
-
err
|
|
1831
|
-
);
|
|
1832
|
-
return false;
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
async function listExternalEditVerdicts(nestId, documentId) {
|
|
1836
|
-
try {
|
|
1837
|
-
const { storage } = await engineCache.get(nestId);
|
|
1838
|
-
const events = await new ChainEventLog(storage).readByDocument(documentId);
|
|
1839
|
-
return events.flatMap((e) => {
|
|
1840
|
-
const meta = e.action_metadata || {};
|
|
1841
|
-
if (typeof meta.suggestion_id !== "string") return [];
|
|
1842
|
-
const reason = meta.rejection_reason;
|
|
1843
|
-
const comment = meta.approval_comment;
|
|
1844
|
-
return [
|
|
1845
|
-
{
|
|
1846
|
-
suggestion_id: meta.suggestion_id,
|
|
1847
|
-
status: typeof reason === "string" ? "rejected" : "approved",
|
|
1848
|
-
actor: e.actor,
|
|
1849
|
-
at: e.timestamp,
|
|
1850
|
-
note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
|
|
1851
|
-
source: typeof meta.source === "string" ? meta.source : void 0
|
|
1852
|
-
}
|
|
1853
|
-
];
|
|
1854
|
-
});
|
|
1855
|
-
} catch (err) {
|
|
1856
|
-
console.error(
|
|
1857
|
-
`[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
|
|
1858
|
-
err
|
|
1859
|
-
);
|
|
1860
|
-
return [];
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
async function mirrorVersion(input) {
|
|
1864
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1865
|
-
const node = await storage.readDocument(input.documentId);
|
|
1866
|
-
const { upsertVersion: upsertVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-V6K5SZ3P.js");
|
|
1867
|
-
await upsertVersion2({
|
|
1868
|
-
nestId: input.nestId,
|
|
1869
|
-
nodeId: input.documentId,
|
|
1870
|
-
version: input.version,
|
|
1871
|
-
content: node.body || "",
|
|
1872
|
-
author: input.actor,
|
|
1873
|
-
status: "published",
|
|
1874
|
-
tags: node.frontmatter.tags || [],
|
|
1875
|
-
changeNote: input.note
|
|
1876
|
-
});
|
|
1877
|
-
await setApprovedVersion2(
|
|
1878
|
-
input.nestId,
|
|
1879
|
-
input.documentId,
|
|
1880
|
-
input.version,
|
|
1881
|
-
input.actor
|
|
1882
|
-
);
|
|
1883
|
-
}
|
|
1884
|
-
async function approveExternalEdit(input) {
|
|
1885
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1886
|
-
let result;
|
|
1887
|
-
try {
|
|
1888
|
-
result = await approveSuggestion({
|
|
1889
|
-
storage,
|
|
1890
|
-
rbac: communityRbac,
|
|
1891
|
-
documentId: input.documentId,
|
|
1892
|
-
suggestionId: input.suggestionId,
|
|
1893
|
-
actor: input.actor,
|
|
1894
|
-
zone: "default",
|
|
1895
|
-
comment: input.comment
|
|
1896
|
-
});
|
|
1897
|
-
} catch (err) {
|
|
1898
|
-
console.error(
|
|
1899
|
-
`[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
|
|
1900
|
-
err
|
|
1901
|
-
);
|
|
1902
|
-
throw err;
|
|
1903
|
-
}
|
|
1904
|
-
try {
|
|
1905
|
-
await mirrorVersion({
|
|
1906
|
-
nestId: input.nestId,
|
|
1907
|
-
documentId: input.documentId,
|
|
1908
|
-
version: result.versionEntry.version,
|
|
1909
|
-
actor: input.actor,
|
|
1910
|
-
note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
|
|
1911
|
-
});
|
|
1912
|
-
} catch (err) {
|
|
1913
|
-
console.error(
|
|
1914
|
-
`[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
|
|
1915
|
-
err
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1919
|
-
return { ...result, historyRecorded };
|
|
1920
|
-
}
|
|
1921
|
-
async function rejectExternalEdit(input) {
|
|
1922
|
-
const { storage } = await engineCache.get(input.nestId);
|
|
1923
|
-
const result = await rejectSuggestion({
|
|
1924
|
-
storage,
|
|
1925
|
-
rbac: communityRbac,
|
|
1926
|
-
documentId: input.documentId,
|
|
1927
|
-
suggestionId: input.suggestionId,
|
|
1928
|
-
actor: input.actor,
|
|
1929
|
-
zone: "default",
|
|
1930
|
-
reason: input.reason
|
|
1931
|
-
});
|
|
1932
|
-
const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
|
|
1933
|
-
try {
|
|
1934
|
-
const approved = await loadChainHead(storage, input.documentId);
|
|
1935
|
-
if (approved) {
|
|
1936
|
-
await storage.writeDocument(input.documentId, approved.content);
|
|
1937
|
-
}
|
|
1938
|
-
} catch (err) {
|
|
1939
|
-
console.error(
|
|
1940
|
-
`[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
|
|
1941
|
-
err
|
|
1942
|
-
);
|
|
1943
|
-
}
|
|
1944
|
-
return { ...result, historyRecorded };
|
|
1945
|
-
}
|
|
1946
|
-
var scannerTimer = null;
|
|
1947
|
-
async function scanAllNests() {
|
|
1948
|
-
const db = getDb();
|
|
1949
|
-
const rows = await db.all("SELECT id FROM nests");
|
|
1950
|
-
await Promise.all(
|
|
1951
|
-
rows.map(
|
|
1952
|
-
({ id }) => scanNestForDrift(id).catch(
|
|
1953
|
-
(err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
|
|
1954
|
-
)
|
|
1955
|
-
)
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
function startDriftScanner(intervalMs = 3e4) {
|
|
1959
|
-
if (scannerTimer) return;
|
|
1960
|
-
scannerTimer = setInterval(() => {
|
|
1961
|
-
scanAllNests().catch(
|
|
1962
|
-
(err) => console.error("[external-edit] scanner tick failed:", err)
|
|
1963
|
-
);
|
|
1964
|
-
}, intervalMs);
|
|
1965
|
-
scannerTimer.unref?.();
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
1711
|
// src/nodes/service.ts
|
|
1969
1712
|
async function userIdFromEmail(email) {
|
|
1970
1713
|
const db = getDb();
|
|
@@ -1983,17 +1726,42 @@ function assertTitleLength(title) {
|
|
|
1983
1726
|
);
|
|
1984
1727
|
}
|
|
1985
1728
|
}
|
|
1986
|
-
function
|
|
1987
|
-
if (
|
|
1729
|
+
function assertTitleReadable(title) {
|
|
1730
|
+
if (!/[\p{L}\p{N}]/u.test(title)) {
|
|
1988
1731
|
throw new ValidationError(
|
|
1989
|
-
"the
|
|
1732
|
+
"the title needs at least one letter or number \u2014 it is what search and wiki links read back"
|
|
1990
1733
|
);
|
|
1991
1734
|
}
|
|
1992
1735
|
}
|
|
1993
|
-
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
|
+
}
|
|
1994
1751
|
function isIdUnderFolder(id, folder) {
|
|
1995
1752
|
return id.startsWith(`nodes/${folder}/`) || id.startsWith(`${folder}/`);
|
|
1996
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
|
+
}
|
|
1997
1765
|
function folderToSegments(folder) {
|
|
1998
1766
|
const segments = folder.split("/").map(slugify).filter(Boolean);
|
|
1999
1767
|
if (folder.trim() && segments.length === 0) {
|
|
@@ -2002,8 +1770,10 @@ function folderToSegments(folder) {
|
|
|
2002
1770
|
);
|
|
2003
1771
|
}
|
|
2004
1772
|
if (segments[0] === "nodes") segments.shift();
|
|
2005
|
-
if (segments.length >
|
|
2006
|
-
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
|
+
);
|
|
2007
1777
|
}
|
|
2008
1778
|
if (segments.some((seg) => seg.length > 100)) {
|
|
2009
1779
|
throw new ValidationError(
|
|
@@ -2039,7 +1809,7 @@ function toSafeError(err, context, message) {
|
|
|
2039
1809
|
}
|
|
2040
1810
|
function bodyOnly(nodeId, raw) {
|
|
2041
1811
|
try {
|
|
2042
|
-
return
|
|
1812
|
+
return parseDocument(`${nodeId}.md`, raw, nodeId).body ?? "";
|
|
2043
1813
|
} catch {
|
|
2044
1814
|
return raw;
|
|
2045
1815
|
}
|
|
@@ -2124,6 +1894,15 @@ async function resolveAuthorship(nestId, nodes) {
|
|
|
2124
1894
|
}
|
|
2125
1895
|
return result;
|
|
2126
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
|
+
}
|
|
2127
1906
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2128
1907
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
2129
1908
|
throw new ForbiddenError(
|
|
@@ -2165,29 +1944,49 @@ async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {})
|
|
|
2165
1944
|
};
|
|
2166
1945
|
}
|
|
2167
1946
|
async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
1947
|
+
if (filters.folder !== void 0) assertSafeFolder(filters.folder);
|
|
2168
1948
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
2169
1949
|
const userEmail = await resolveCallerEmail(userId);
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
{
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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) => ({
|
|
2186
1983
|
id: d.id,
|
|
2187
1984
|
filePath: "",
|
|
2188
1985
|
rawContent: "",
|
|
2189
1986
|
frontmatter: d.frontmatter,
|
|
2190
|
-
|
|
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 ?? ""
|
|
2191
1990
|
}));
|
|
2192
1991
|
const publicReader = await isPublicReader(nestId, userId);
|
|
2193
1992
|
const approvedOnly = publicReader || filters.approvedOnly === true;
|
|
@@ -2199,14 +1998,18 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2199
1998
|
nodes,
|
|
2200
1999
|
approvedVersions ?? void 0
|
|
2201
2000
|
);
|
|
2202
|
-
|
|
2203
|
-
const grants = await listUserGrants(nestId, userId);
|
|
2204
|
-
accessible = grants.length ? nodes.filter((d) => grantCoversNode(grants, d.id)) : accessible;
|
|
2205
|
-
}
|
|
2001
|
+
const myGrants = await listUserGrants(nestId, userId);
|
|
2206
2002
|
if (approvedVersions && !publicReader) {
|
|
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) => ({
|
|
@@ -2222,6 +2025,9 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2222
2025
|
r.author = people.author;
|
|
2223
2026
|
r.collaborators = people.collaborators;
|
|
2224
2027
|
}
|
|
2028
|
+
if (myGrants.length) {
|
|
2029
|
+
r.myGrant = grantRoleForNode(myGrants, doc.id);
|
|
2030
|
+
}
|
|
2225
2031
|
if (approvedVersions) {
|
|
2226
2032
|
const approved = approvedVersions.get(doc.id);
|
|
2227
2033
|
if (approved != null) {
|
|
@@ -2246,22 +2052,125 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2246
2052
|
return r;
|
|
2247
2053
|
}
|
|
2248
2054
|
let pending = null;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2055
|
+
if (driftedDocs?.has(doc.id)) {
|
|
2056
|
+
try {
|
|
2057
|
+
pending = await getPendingChange(nestId, doc.id);
|
|
2058
|
+
} catch {
|
|
2059
|
+
pending = null;
|
|
2060
|
+
}
|
|
2253
2061
|
}
|
|
2254
2062
|
if (pending) {
|
|
2255
2063
|
r.pendingChange = pending;
|
|
2256
2064
|
r.status = "external_edit_pending";
|
|
2257
2065
|
} else {
|
|
2258
|
-
r.status =
|
|
2066
|
+
r.status = displayStatuses?.get(r.id) ?? "draft";
|
|
2259
2067
|
}
|
|
2260
2068
|
return r;
|
|
2261
2069
|
})
|
|
2262
2070
|
);
|
|
2263
2071
|
return enriched;
|
|
2264
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
|
+
}
|
|
2265
2174
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2266
2175
|
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
2267
2176
|
}
|
|
@@ -2287,14 +2196,27 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2287
2196
|
return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
|
|
2288
2197
|
}
|
|
2289
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) {
|
|
2290
2211
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2291
2212
|
assertTitleLength(input.title);
|
|
2292
|
-
|
|
2213
|
+
assertTitleReadable(input.title);
|
|
2293
2214
|
const slug = slugify(input.title).slice(0, 100).replace(/-+$/, "");
|
|
2294
2215
|
let id = input.id;
|
|
2295
2216
|
if (!id) {
|
|
2296
2217
|
const folderSegments = folderToSegments(input.folder ?? "");
|
|
2297
|
-
|
|
2218
|
+
const prefix = folderSegments.length > 0 ? `nodes/${folderSegments.join("/")}/` : "nodes/";
|
|
2219
|
+
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
2298
2220
|
}
|
|
2299
2221
|
const tags = (input.tags || []).map(normalizeTag2);
|
|
2300
2222
|
const requestedType = input.type || "document";
|
|
@@ -2555,7 +2477,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2555
2477
|
if (patch.title) {
|
|
2556
2478
|
if (patch.title !== node.frontmatter.title) {
|
|
2557
2479
|
assertTitleLength(patch.title);
|
|
2558
|
-
|
|
2480
|
+
assertTitleReadable(patch.title);
|
|
2559
2481
|
}
|
|
2560
2482
|
node = {
|
|
2561
2483
|
...node,
|
|
@@ -2703,6 +2625,8 @@ var NODE_KEYED_TABLES = [
|
|
|
2703
2625
|
["annotation_threads", "node_id"],
|
|
2704
2626
|
["comments", "node_id"],
|
|
2705
2627
|
["node_tag_index", "node_id"],
|
|
2628
|
+
["node_index", "node_id"],
|
|
2629
|
+
["node_suggestions", "node_id"],
|
|
2706
2630
|
["watchers", "node_id"],
|
|
2707
2631
|
["schedules", "agent_node"],
|
|
2708
2632
|
// A live capability token bound to one agent node — deleting the agent must
|
|
@@ -2781,8 +2705,8 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2781
2705
|
await storage.writeDocument(newId, serializeNodeSafe(moved));
|
|
2782
2706
|
try {
|
|
2783
2707
|
const nestPath = resolveNestPath(nestId);
|
|
2784
|
-
const oldChain =
|
|
2785
|
-
const newChain =
|
|
2708
|
+
const oldChain = join(nestPath, dirname(nodeId), ".versions", leaf);
|
|
2709
|
+
const newChain = join(nestPath, dirname(newId), ".versions", leaf);
|
|
2786
2710
|
if (oldChain !== newChain) {
|
|
2787
2711
|
await mkdir(dirname(newChain), { recursive: true });
|
|
2788
2712
|
await rename(oldChain, newChain);
|
|
@@ -2815,6 +2739,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
2815
2739
|
await storage.deleteDocument(oldDerived);
|
|
2816
2740
|
} catch {
|
|
2817
2741
|
}
|
|
2742
|
+
try {
|
|
2743
|
+
await storage.regenerateIndex();
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
console.error("[move] regenerateIndex failed", nestId, nodeId, newId, err);
|
|
2746
|
+
}
|
|
2818
2747
|
await trackEvent("node.move", { nestId, nodeId, newId });
|
|
2819
2748
|
return { node: moved, previousId: nodeId };
|
|
2820
2749
|
});
|
|
@@ -2885,7 +2814,7 @@ async function deleteFolder(nestId, folder, userEmail) {
|
|
|
2885
2814
|
|
|
2886
2815
|
// src/nests/unsynced-service.ts
|
|
2887
2816
|
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2888
|
-
import { join as
|
|
2817
|
+
import { join as join2, relative, resolve } from "path";
|
|
2889
2818
|
import { rm } from "fs/promises";
|
|
2890
2819
|
var RESERVED = /* @__PURE__ */ new Set(["nests"]);
|
|
2891
2820
|
function isNestStorageRoot(absPath) {
|
|
@@ -2902,7 +2831,7 @@ function scanMarkdown(dir) {
|
|
|
2902
2831
|
}
|
|
2903
2832
|
for (const e of entries) {
|
|
2904
2833
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2905
|
-
const full =
|
|
2834
|
+
const full = join2(dir, e.name);
|
|
2906
2835
|
if (e.isDirectory()) {
|
|
2907
2836
|
const sub = scanMarkdown(full);
|
|
2908
2837
|
count += sub.count;
|
|
@@ -2952,7 +2881,7 @@ function collectLeafFolders(rel, abs) {
|
|
|
2952
2881
|
if (!e.isDirectory()) continue;
|
|
2953
2882
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2954
2883
|
out.push(
|
|
2955
|
-
...collectLeafFolders(`${rel}/${e.name}`,
|
|
2884
|
+
...collectLeafFolders(`${rel}/${e.name}`, join2(abs, e.name))
|
|
2956
2885
|
);
|
|
2957
2886
|
}
|
|
2958
2887
|
return out;
|
|
@@ -2970,7 +2899,7 @@ function listUnsyncedFolders() {
|
|
|
2970
2899
|
if (!e.isDirectory()) continue;
|
|
2971
2900
|
if (e.name.startsWith(".")) continue;
|
|
2972
2901
|
if (RESERVED.has(e.name)) continue;
|
|
2973
|
-
const abs =
|
|
2902
|
+
const abs = join2(root, e.name);
|
|
2974
2903
|
if (isNestStorageRoot(abs)) continue;
|
|
2975
2904
|
out.push(...collectLeafFolders(e.name, abs));
|
|
2976
2905
|
}
|
|
@@ -3007,7 +2936,7 @@ function collectMarkdownFiles(dir, root) {
|
|
|
3007
2936
|
}
|
|
3008
2937
|
for (const e of entries) {
|
|
3009
2938
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
3010
|
-
const full =
|
|
2939
|
+
const full = join2(dir, e.name);
|
|
3011
2940
|
if (e.isDirectory()) {
|
|
3012
2941
|
out.push(...collectMarkdownFiles(full, root));
|
|
3013
2942
|
} else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) {
|
|
@@ -3025,7 +2954,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3025
2954
|
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
3026
2955
|
);
|
|
3027
2956
|
assertSafeFolderName(folderName);
|
|
3028
|
-
const src =
|
|
2957
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3029
2958
|
if (isNestStorageRoot(src)) {
|
|
3030
2959
|
throw new ValidationError("Folder is not eligible for sync");
|
|
3031
2960
|
}
|
|
@@ -3081,7 +3010,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3081
3010
|
async function deleteUnsyncedFolder(folderName) {
|
|
3082
3011
|
console.log(`[unsynced] delete requested folder="${folderName}"`);
|
|
3083
3012
|
assertSafeFolderName(folderName);
|
|
3084
|
-
const src =
|
|
3013
|
+
const src = join2(config.DATA_ROOT, folderName);
|
|
3085
3014
|
if (isNestStorageRoot(src)) {
|
|
3086
3015
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3087
3016
|
}
|
|
@@ -3099,16 +3028,22 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3099
3028
|
}
|
|
3100
3029
|
|
|
3101
3030
|
// src/nests/routes.ts
|
|
3102
|
-
async function
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3031
|
+
async function effectiveAccess(nestId, userId) {
|
|
3032
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
3033
|
+
if (config.AUTH_MODE === "open" && access.ownerId === ANON_USER_ID) {
|
|
3034
|
+
return { permission: "owner", access };
|
|
3035
|
+
}
|
|
3036
|
+
const permission = access.permission;
|
|
3037
|
+
if (permission === "none" && access.hasGrant) {
|
|
3038
|
+
return { permission: "read", access };
|
|
3110
3039
|
}
|
|
3111
|
-
return
|
|
3040
|
+
return { permission, access };
|
|
3041
|
+
}
|
|
3042
|
+
async function effectivePermission(nestId, userId) {
|
|
3043
|
+
return (await effectiveAccess(nestId, userId)).permission;
|
|
3044
|
+
}
|
|
3045
|
+
function isGrantOnly(access) {
|
|
3046
|
+
return access.permission === "none" && access.hasGrant;
|
|
3112
3047
|
}
|
|
3113
3048
|
var nestRoutes = new Hono3();
|
|
3114
3049
|
nestRoutes.get("/", async (c) => {
|
|
@@ -3119,8 +3054,31 @@ nestRoutes.get("/", async (c) => {
|
|
|
3119
3054
|
const db = getDb();
|
|
3120
3055
|
const ownerEmailSql = "SELECT email FROM users WHERE id = ?";
|
|
3121
3056
|
const callerEmail = await resolveCallerEmail(userId);
|
|
3057
|
+
const myGrantRows = await db.all(
|
|
3058
|
+
"SELECT nest_id, target FROM grants WHERE user_id = ?",
|
|
3059
|
+
[userId]
|
|
3060
|
+
);
|
|
3061
|
+
const grantTargetsByNest = /* @__PURE__ */ new Map();
|
|
3062
|
+
for (const r of myGrantRows) {
|
|
3063
|
+
const arr = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
3064
|
+
arr.push({ target: r.target });
|
|
3065
|
+
grantTargetsByNest.set(r.nest_id, arr);
|
|
3066
|
+
}
|
|
3122
3067
|
const annotate = async (n) => {
|
|
3123
|
-
const permission = await
|
|
3068
|
+
const { permission, access } = await effectiveAccess(n.id, userId);
|
|
3069
|
+
if (permission !== "owner" && // open-mode commons promotion outranks a grant
|
|
3070
|
+
isGrantOnly(access)) {
|
|
3071
|
+
const owner_email2 = n.user_id === ANON_USER_ID ? null : (await db.get(ownerEmailSql, [n.user_id]))?.email ?? null;
|
|
3072
|
+
return {
|
|
3073
|
+
...n,
|
|
3074
|
+
permission: "read",
|
|
3075
|
+
is_owner: false,
|
|
3076
|
+
owner_email: owner_email2,
|
|
3077
|
+
roles: ["viewer"],
|
|
3078
|
+
grant_only: true,
|
|
3079
|
+
stewardship_enabled: !!n.stewardship_enabled
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3124
3082
|
const is_owner = permission === "owner";
|
|
3125
3083
|
const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
|
|
3126
3084
|
const [roles, owner_email] = await Promise.all([
|
|
@@ -3135,6 +3093,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3135
3093
|
is_owner,
|
|
3136
3094
|
owner_email,
|
|
3137
3095
|
roles,
|
|
3096
|
+
grant_only: false,
|
|
3138
3097
|
stewardship_enabled: !!n.stewardship_enabled
|
|
3139
3098
|
};
|
|
3140
3099
|
};
|
|
@@ -3147,9 +3106,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
3147
3106
|
}
|
|
3148
3107
|
const out = await Promise.all(deduped.map(annotate));
|
|
3149
3108
|
const docRows = await db.all(
|
|
3150
|
-
|
|
3109
|
+
`SELECT nest_id, COUNT(DISTINCT node_id) AS c, MAX(created_at) AS last_activity
|
|
3110
|
+
FROM node_versions GROUP BY nest_id`
|
|
3151
3111
|
);
|
|
3152
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]));
|
|
3153
3114
|
const collabRows = await db.all(
|
|
3154
3115
|
"SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
|
|
3155
3116
|
);
|
|
@@ -3178,14 +3139,39 @@ nestRoutes.get("/", async (c) => {
|
|
|
3178
3139
|
if (arr.length < 8) arr.push(r.email);
|
|
3179
3140
|
collabEmailsByNest.set(r.nest_id, arr);
|
|
3180
3141
|
}
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3142
|
+
const grantOnlyIds = out.filter((n) => n.grant_only).map((n) => n.id);
|
|
3143
|
+
const grantedDocsByNest = /* @__PURE__ */ new Map();
|
|
3144
|
+
if (grantOnlyIds.length) {
|
|
3145
|
+
const rows = await db.all(
|
|
3146
|
+
`SELECT DISTINCT nest_id, node_id FROM node_versions
|
|
3147
|
+
WHERE nest_id IN (${grantOnlyIds.map(() => "?").join(",")})`,
|
|
3148
|
+
grantOnlyIds
|
|
3149
|
+
);
|
|
3150
|
+
for (const r of rows) {
|
|
3151
|
+
const targets = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
3152
|
+
if (grantCoversNode(targets, r.node_id)) {
|
|
3153
|
+
grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
const withMeta = out.map(
|
|
3158
|
+
(n) => n.grant_only ? {
|
|
3159
|
+
...n,
|
|
3160
|
+
document_count: grantedDocsByNest.get(n.id) ?? 0,
|
|
3161
|
+
collaborator_count: 0,
|
|
3162
|
+
steward_count: 0,
|
|
3163
|
+
steward_emails: [],
|
|
3164
|
+
collaborator_emails: []
|
|
3165
|
+
} : {
|
|
3166
|
+
...n,
|
|
3167
|
+
last_activity: activityByNest.get(n.id) ?? null,
|
|
3168
|
+
document_count: docByNest.get(n.id) ?? 0,
|
|
3169
|
+
collaborator_count: collabByNest.get(n.id) ?? 0,
|
|
3170
|
+
steward_count: stewardByNest.get(n.id) ?? 0,
|
|
3171
|
+
steward_emails: stewardEmailsByNest.get(n.id) ?? [],
|
|
3172
|
+
collaborator_emails: collabEmailsByNest.get(n.id) ?? []
|
|
3173
|
+
}
|
|
3174
|
+
);
|
|
3189
3175
|
return c.json({ nests: withMeta });
|
|
3190
3176
|
});
|
|
3191
3177
|
nestRoutes.post("/", async (c) => {
|
|
@@ -3291,7 +3277,7 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
3291
3277
|
const nestId = c.req.param("nestId");
|
|
3292
3278
|
if (nestId === "unsynced") return handleUnsyncedList(c);
|
|
3293
3279
|
const userId = c.get("userId");
|
|
3294
|
-
const permission = await
|
|
3280
|
+
const { permission, access } = await effectiveAccess(nestId, userId);
|
|
3295
3281
|
if (permission === "none") {
|
|
3296
3282
|
throw new NotFoundError("Nest not found");
|
|
3297
3283
|
}
|
|
@@ -3301,8 +3287,15 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
3301
3287
|
roles = ["owner", ...roles];
|
|
3302
3288
|
}
|
|
3303
3289
|
const myStewards = await getStewardsForUser(nestId, email);
|
|
3290
|
+
const grant_only = roles.length === 0 && isGrantOnly(access);
|
|
3304
3291
|
const nest = await getNest(nestId);
|
|
3305
|
-
return c.json({
|
|
3292
|
+
return c.json({
|
|
3293
|
+
nest,
|
|
3294
|
+
permission,
|
|
3295
|
+
roles: grant_only ? ["viewer"] : roles,
|
|
3296
|
+
myStewards,
|
|
3297
|
+
grant_only
|
|
3298
|
+
});
|
|
3306
3299
|
});
|
|
3307
3300
|
nestRoutes.patch("/:nestId", async (c) => {
|
|
3308
3301
|
const nestId = c.req.param("nestId");
|
|
@@ -3435,7 +3428,7 @@ async function addCollaborator(params) {
|
|
|
3435
3428
|
if (existing) {
|
|
3436
3429
|
userId = existing.id;
|
|
3437
3430
|
} else {
|
|
3438
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3431
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3439
3432
|
userId = uuid3();
|
|
3440
3433
|
await db.run(
|
|
3441
3434
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3627,7 +3620,7 @@ async function resolveGrantee(email) {
|
|
|
3627
3620
|
const e = normalizeEmail(email);
|
|
3628
3621
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
3629
3622
|
if (existing) return existing.id;
|
|
3630
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
3623
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3631
3624
|
const id = uuid4();
|
|
3632
3625
|
await db.run(
|
|
3633
3626
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
@@ -3635,6 +3628,41 @@ async function resolveGrantee(email) {
|
|
|
3635
3628
|
);
|
|
3636
3629
|
return id;
|
|
3637
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
|
+
}
|
|
3638
3666
|
grantRoutes.post("/", async (c) => {
|
|
3639
3667
|
const nestId = c.req.param("nestId");
|
|
3640
3668
|
await requireManage(c, nestId);
|
|
@@ -3650,7 +3678,19 @@ grantRoutes.post("/", async (c) => {
|
|
|
3650
3678
|
if (!["read", "write"].includes(role)) {
|
|
3651
3679
|
throw new ValidationError("role must be read | write");
|
|
3652
3680
|
}
|
|
3653
|
-
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);
|
|
3654
3694
|
const grant = await createGrant({
|
|
3655
3695
|
nestId,
|
|
3656
3696
|
targetType,
|
|
@@ -4002,6 +4042,25 @@ teamRoutes.delete("/:teamId/members/:userId", async (c) => {
|
|
|
4002
4042
|
});
|
|
4003
4043
|
|
|
4004
4044
|
// src/nodes/search.ts
|
|
4045
|
+
async function discoverForSearch(storage) {
|
|
4046
|
+
return storage.discoverDocuments({ includeRetired: true });
|
|
4047
|
+
}
|
|
4048
|
+
function titleScore(title, query) {
|
|
4049
|
+
const t = (title || "").toLowerCase();
|
|
4050
|
+
const q = query.trim().toLowerCase();
|
|
4051
|
+
if (!q) return 0;
|
|
4052
|
+
if (t === q) return 4;
|
|
4053
|
+
let midWord = false;
|
|
4054
|
+
for (let i = t.indexOf(q); i !== -1; i = t.indexOf(q, i + 1)) {
|
|
4055
|
+
if (i === 0) return 3;
|
|
4056
|
+
if (!/[a-z0-9]/.test(t[i - 1])) return 2;
|
|
4057
|
+
midWord = true;
|
|
4058
|
+
}
|
|
4059
|
+
if (midWord) return 1;
|
|
4060
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
4061
|
+
if (terms.length > 1 && terms.every((term) => t.includes(term))) return 1;
|
|
4062
|
+
return 0;
|
|
4063
|
+
}
|
|
4005
4064
|
function matchDocuments(documents, query) {
|
|
4006
4065
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
4007
4066
|
if (terms.length === 0) return [];
|
|
@@ -4013,15 +4072,25 @@ function matchDocuments(documents, query) {
|
|
|
4013
4072
|
...node.frontmatter.tags || []
|
|
4014
4073
|
].join(" ").toLowerCase();
|
|
4015
4074
|
return terms.every((term) => haystack.includes(term));
|
|
4016
|
-
})
|
|
4075
|
+
}).sort(
|
|
4076
|
+
(a, b) => titleScore(b.frontmatter.title, query) - titleScore(a.frontmatter.title, query)
|
|
4077
|
+
);
|
|
4017
4078
|
}
|
|
4018
|
-
function
|
|
4079
|
+
function bodySnippet(body, query) {
|
|
4080
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
4081
|
+
const terms = (query || "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
4082
|
+
const at = terms.map((t) => flat.toLowerCase().indexOf(t)).filter((i) => i >= 0).sort((a, b) => a - b)[0];
|
|
4083
|
+
if (at === void 0 || at < 80) return flat.slice(0, 200);
|
|
4084
|
+
const start = Math.max(0, at - 60);
|
|
4085
|
+
return `\u2026${flat.slice(start, start + 200)}`;
|
|
4086
|
+
}
|
|
4087
|
+
function toSearchHit(node, query) {
|
|
4019
4088
|
return {
|
|
4020
4089
|
id: node.id,
|
|
4021
4090
|
title: node.frontmatter.title,
|
|
4022
4091
|
type: node.frontmatter.type || "document",
|
|
4023
4092
|
tags: node.frontmatter.tags || [],
|
|
4024
|
-
snippet: (node.body || ""
|
|
4093
|
+
snippet: bodySnippet(node.body || "", query)
|
|
4025
4094
|
};
|
|
4026
4095
|
}
|
|
4027
4096
|
|
|
@@ -4029,7 +4098,7 @@ function toSearchHit(node) {
|
|
|
4029
4098
|
import { Hono as Hono8 } from "hono";
|
|
4030
4099
|
|
|
4031
4100
|
// src/nodes/markdown-export.ts
|
|
4032
|
-
import { parseDocument as
|
|
4101
|
+
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
4033
4102
|
function nodeToMarkdown(node) {
|
|
4034
4103
|
const tags = node.tags ?? [];
|
|
4035
4104
|
return [
|
|
@@ -4065,7 +4134,7 @@ function rawContentType(nodeType) {
|
|
|
4065
4134
|
function frontmatterBlockOf(raw) {
|
|
4066
4135
|
if (!raw || !raw.startsWith("---")) return "";
|
|
4067
4136
|
try {
|
|
4068
|
-
const body =
|
|
4137
|
+
const body = parseDocument2("frontmatter-split.md", raw, "frontmatter-split").body ?? "";
|
|
4069
4138
|
if (raw.endsWith(body)) return raw.slice(0, raw.length - body.length);
|
|
4070
4139
|
} catch {
|
|
4071
4140
|
}
|
|
@@ -4111,7 +4180,14 @@ nodeRoutes.get("/", async (c) => {
|
|
|
4111
4180
|
// Lets an owner preview what making the nest public would expose.
|
|
4112
4181
|
...q.approved_only === "1" || q.approved_only === "true" ? { approvedOnly: true } : {},
|
|
4113
4182
|
// Ignore junk rather than 400 — a bad ?limit should not fail a listing.
|
|
4114
|
-
...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 } : {}
|
|
4115
4191
|
});
|
|
4116
4192
|
return c.json({ count: nodes.length, nodes });
|
|
4117
4193
|
});
|
|
@@ -4153,7 +4229,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
4153
4229
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4154
4230
|
const nestId = c.req.param("nestId");
|
|
4155
4231
|
const nodeId = c.req.param("nodeId");
|
|
4156
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4232
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-U4CKL7Y5.js");
|
|
4157
4233
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
4158
4234
|
nestId,
|
|
4159
4235
|
nodeId
|
|
@@ -4174,7 +4250,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4174
4250
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4175
4251
|
const nestId = c.req.param("nestId");
|
|
4176
4252
|
const nodeId = c.req.param("nodeId");
|
|
4177
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4253
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-H2M5ACPQ.js");
|
|
4178
4254
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4179
4255
|
return c.json({ reviews: history });
|
|
4180
4256
|
});
|
|
@@ -4298,6 +4374,7 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
4298
4374
|
response.collaborators = people.collaborators;
|
|
4299
4375
|
}
|
|
4300
4376
|
}
|
|
4377
|
+
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
4301
4378
|
if (publicReader) {
|
|
4302
4379
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
4303
4380
|
if (approved != null) {
|
|
@@ -5233,7 +5310,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
5233
5310
|
|
|
5234
5311
|
// src/nests/bundle-service.ts
|
|
5235
5312
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
5236
|
-
import { join as
|
|
5313
|
+
import { join as join3, sep } from "path";
|
|
5237
5314
|
import JSZip from "jszip";
|
|
5238
5315
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
5239
5316
|
function walkFiles(root) {
|
|
@@ -5246,7 +5323,7 @@ function walkFiles(root) {
|
|
|
5246
5323
|
}
|
|
5247
5324
|
for (const rel of entries) {
|
|
5248
5325
|
try {
|
|
5249
|
-
if (statSync2(
|
|
5326
|
+
if (statSync2(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
5250
5327
|
} catch {
|
|
5251
5328
|
}
|
|
5252
5329
|
}
|
|
@@ -5287,7 +5364,7 @@ async function exportNestBundle(nestId, includeDrafts) {
|
|
|
5287
5364
|
const segs = rel.split("/");
|
|
5288
5365
|
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
5289
5366
|
if (!keep) continue;
|
|
5290
|
-
zip.file(rel, readFileSync2(
|
|
5367
|
+
zip.file(rel, readFileSync2(join3(nestPath, rel)));
|
|
5291
5368
|
}
|
|
5292
5369
|
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
5293
5370
|
return zip.generateAsync({ type: "arraybuffer" });
|
|
@@ -5458,7 +5535,9 @@ queryRoutes.get("/changes", async (c) => {
|
|
|
5458
5535
|
const includeContent = ["1", "true"].includes(
|
|
5459
5536
|
c.req.query("include_content") || ""
|
|
5460
5537
|
);
|
|
5461
|
-
const all = await listNodesForCaller(nestId, userId
|
|
5538
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
5539
|
+
...includeContent ? { withContent: true } : {}
|
|
5540
|
+
});
|
|
5462
5541
|
const db = getDb();
|
|
5463
5542
|
const publicReader = await isPublicReader(nestId, userId);
|
|
5464
5543
|
const approvedRows = await db.all(
|
|
@@ -5525,7 +5604,10 @@ queryRoutes.get("/runnables", async (c) => {
|
|
|
5525
5604
|
const nestId = c.req.param("nestId");
|
|
5526
5605
|
const userId = c.get("userId");
|
|
5527
5606
|
const all = await listNodesForCaller(nestId, userId, {
|
|
5528
|
-
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
|
|
5529
5611
|
});
|
|
5530
5612
|
const runnables = all.map((n) => ({
|
|
5531
5613
|
id: n.id,
|
|
@@ -5785,15 +5867,14 @@ queryRoutes.get("/search", async (c) => {
|
|
|
5785
5867
|
}
|
|
5786
5868
|
const nestId = c.req.param("nestId");
|
|
5787
5869
|
const { storage } = await engineCache.get(nestId);
|
|
5788
|
-
const
|
|
5789
|
-
const matches = matchDocuments(documents, q);
|
|
5870
|
+
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
5790
5871
|
const userId = c.get("userId");
|
|
5791
5872
|
const userEmail = await resolveCallerEmail(userId);
|
|
5792
5873
|
const accessible = await filterAccessible(nestId, userId, userEmail, matches);
|
|
5793
5874
|
return c.json({
|
|
5794
5875
|
query: q,
|
|
5795
5876
|
count: accessible.length,
|
|
5796
|
-
nodes: accessible.map(toSearchHit)
|
|
5877
|
+
nodes: accessible.map((n) => toSearchHit(n, q))
|
|
5797
5878
|
});
|
|
5798
5879
|
});
|
|
5799
5880
|
queryRoutes.get("/overview", async (c) => {
|
|
@@ -5821,6 +5902,25 @@ queryRoutes.get("/overview", async (c) => {
|
|
|
5821
5902
|
}))
|
|
5822
5903
|
});
|
|
5823
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
|
+
});
|
|
5824
5924
|
queryRoutes.get("/comment-counts", async (c) => {
|
|
5825
5925
|
const counts = await countThreadsByNode(c.req.param("nestId"));
|
|
5826
5926
|
return c.json({ counts });
|
|
@@ -5833,10 +5933,6 @@ queryRoutes.get("/context", async (c) => {
|
|
|
5833
5933
|
queryRoutes.get("/export", async (c) => {
|
|
5834
5934
|
const nestId = c.req.param("nestId");
|
|
5835
5935
|
if (c.req.query("format") === "bundle") {
|
|
5836
|
-
const perm = await resolveNestPermission(nestId, c.get("userId"));
|
|
5837
|
-
if (perm !== "owner" && perm !== "admin") {
|
|
5838
|
-
throw new ForbiddenError("Only the nest owner or an admin can export a bundle");
|
|
5839
|
-
}
|
|
5840
5936
|
const draftsParam = c.req.query("includeDrafts");
|
|
5841
5937
|
const includeDrafts = draftsParam === "1" || draftsParam === "true";
|
|
5842
5938
|
const zip = await exportNestBundle(nestId, includeDrafts);
|
|
@@ -5969,30 +6065,6 @@ import { Hono as Hono12 } from "hono";
|
|
|
5969
6065
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5970
6066
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
5971
6067
|
|
|
5972
|
-
// src/shared/node-id.ts
|
|
5973
|
-
function assertSafeNodeId(rawId) {
|
|
5974
|
-
let id = rawId;
|
|
5975
|
-
try {
|
|
5976
|
-
id = decodeURIComponent(rawId);
|
|
5977
|
-
} catch {
|
|
5978
|
-
}
|
|
5979
|
-
if (!id || id.length > 512) {
|
|
5980
|
-
throw new ValidationError("invalid node id");
|
|
5981
|
-
}
|
|
5982
|
-
if (id.includes("\0") || id.includes("\\")) {
|
|
5983
|
-
throw new ValidationError("invalid node id");
|
|
5984
|
-
}
|
|
5985
|
-
if (id.startsWith("/")) {
|
|
5986
|
-
throw new ValidationError("invalid node id (absolute path)");
|
|
5987
|
-
}
|
|
5988
|
-
for (const seg of id.split("/")) {
|
|
5989
|
-
if (seg === "" || seg === "." || seg === "..") {
|
|
5990
|
-
throw new ValidationError("invalid node id (path traversal)");
|
|
5991
|
-
}
|
|
5992
|
-
}
|
|
5993
|
-
return id;
|
|
5994
|
-
}
|
|
5995
|
-
|
|
5996
6068
|
// src/telemetry/trace-log.ts
|
|
5997
6069
|
var PRUNE_EVERY = 500;
|
|
5998
6070
|
var insertsSincePrune = 0;
|
|
@@ -6296,6 +6368,30 @@ async function appendRunStep(runId, input) {
|
|
|
6296
6368
|
return { seq };
|
|
6297
6369
|
}
|
|
6298
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
|
+
}
|
|
6299
6395
|
async function closeRun(runId, input) {
|
|
6300
6396
|
const status = input.status;
|
|
6301
6397
|
if (typeof status !== "string" || !RUN_STATUSES.includes(status)) {
|
|
@@ -6317,6 +6413,7 @@ async function closeRun(runId, input) {
|
|
|
6317
6413
|
if (!run) throw new NotFoundError("Run not found");
|
|
6318
6414
|
throw new ConflictError("Run already terminal \u2014 status can't be changed");
|
|
6319
6415
|
}
|
|
6416
|
+
if (status === "cancelled") await cancelDescendants(runId);
|
|
6320
6417
|
return { status };
|
|
6321
6418
|
}
|
|
6322
6419
|
async function getRunTrace(run, opts) {
|
|
@@ -7620,7 +7717,9 @@ ${context_md}`;
|
|
|
7620
7717
|
{},
|
|
7621
7718
|
await opContext(nestId, userEmail)
|
|
7622
7719
|
);
|
|
7623
|
-
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7720
|
+
const visible = await listNodesForCallerByEmail(nestId, userEmail, {
|
|
7721
|
+
withContent: true
|
|
7722
|
+
});
|
|
7624
7723
|
const types = {};
|
|
7625
7724
|
const tags = {};
|
|
7626
7725
|
for (const n of visible) {
|
|
@@ -7923,6 +8022,13 @@ ${sections}`;
|
|
|
7923
8022
|
(n) => String(n.frontmatter.title ?? "").toLowerCase() === wanted
|
|
7924
8023
|
);
|
|
7925
8024
|
if (twin) {
|
|
8025
|
+
const twinVersions = await getVersions(nestId, twin.id);
|
|
8026
|
+
if (twinVersions.length === 0) {
|
|
8027
|
+
return toolError(
|
|
8028
|
+
`A node titled "${twin.frontmatter.title}" already exists (${twin.id}) but has no version history, which is why it doesn't show up in a read or a draft list. It may be an interrupted create or a document whose import never finished. Write into it with context_update "${twin.id}" to keep whatever is there, or remove it with context_delete "${twin.id}" and create again \u2014 check its content first.`,
|
|
8029
|
+
"DOCUMENT_ALREADY_EXISTS"
|
|
8030
|
+
);
|
|
8031
|
+
}
|
|
7926
8032
|
return toolError(
|
|
7927
8033
|
`A node titled "${twin.frontmatter.title}" already exists (${twin.id}). Titles must be unique \u2014 MCP tools resolve documents by title, so a second one would be unreachable. Use context_update to change it, context_move to refile it, or create this one under a different title.`,
|
|
7928
8034
|
"DOCUMENT_ALREADY_EXISTS"
|
|
@@ -8076,6 +8182,9 @@ ${sections}`;
|
|
|
8076
8182
|
return "reason is required \u2014 say why it should be deleted; that is what the admin judges.";
|
|
8077
8183
|
}
|
|
8078
8184
|
if (args.nest === true) {
|
|
8185
|
+
if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
|
|
8186
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a nest.";
|
|
8187
|
+
}
|
|
8079
8188
|
const request2 = await requestDeletion({
|
|
8080
8189
|
nestId,
|
|
8081
8190
|
nodeId: nestId,
|
|
@@ -8088,6 +8197,9 @@ ${sections}`;
|
|
|
8088
8197
|
Request id: \`${request2.id}\``;
|
|
8089
8198
|
}
|
|
8090
8199
|
if (typeof args.folder === "string" && args.folder.trim()) {
|
|
8200
|
+
if (!await canRequestDeletion({ nestId, userId, userEmail, nodeId: "" })) {
|
|
8201
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a folder.";
|
|
8202
|
+
}
|
|
8091
8203
|
const folder = args.folder.trim().replace(/^\/+|\/+$/g, "").replace(/^nodes\//, "");
|
|
8092
8204
|
const docs = await storage.discoverDocuments();
|
|
8093
8205
|
const inside = docs.filter((d) => isIdUnderFolder(d.id, folder));
|
|
@@ -8110,6 +8222,14 @@ Request id: \`${request2.id}\``;
|
|
|
8110
8222
|
if (!await canReadNode(nestId, node.id, userId, userEmail)) {
|
|
8111
8223
|
return `Node not found: ${node.id}`;
|
|
8112
8224
|
}
|
|
8225
|
+
if (!await canRequestDeletion({
|
|
8226
|
+
nestId,
|
|
8227
|
+
userId,
|
|
8228
|
+
userEmail,
|
|
8229
|
+
nodeId: node.id
|
|
8230
|
+
})) {
|
|
8231
|
+
return "Only an editor, a nest admin, or the nest owner can request the deletion of a document.";
|
|
8232
|
+
}
|
|
8113
8233
|
const request = await requestDeletion({
|
|
8114
8234
|
nestId,
|
|
8115
8235
|
nodeId: node.id,
|
|
@@ -8434,14 +8554,14 @@ ${list}`,
|
|
|
8434
8554
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
8435
8555
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
8436
8556
|
try {
|
|
8437
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
8438
|
-
const db = (await import("./client-
|
|
8557
|
+
const { createGrant: createGrant2 } = await import("./grants-service-ME7HIVHU.js");
|
|
8558
|
+
const db = (await import("./client-SW5I6ZBK.js")).getDb();
|
|
8439
8559
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
8440
8560
|
const e = normalizeEmail2(String(args.email || ""));
|
|
8441
8561
|
if (!e) return "email is required.";
|
|
8442
8562
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
8443
8563
|
if (!row) {
|
|
8444
|
-
const { hashPassword: hashPassword2 } = await import("./keys-
|
|
8564
|
+
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
8445
8565
|
const { v4: uuid16 } = await import("uuid");
|
|
8446
8566
|
const id = uuid16();
|
|
8447
8567
|
await db.run(
|
|
@@ -8904,8 +9024,9 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
8904
9024
|
throw new ValidationError("status must be running | succeeded | failed | cancelled");
|
|
8905
9025
|
}
|
|
8906
9026
|
const terminal = status !== "running";
|
|
8907
|
-
await getDb().run(
|
|
8908
|
-
`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'`,
|
|
8909
9030
|
[
|
|
8910
9031
|
status,
|
|
8911
9032
|
body.trace === void 0 ? null : JSON.stringify(body.trace),
|
|
@@ -8913,6 +9034,13 @@ runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
|
8913
9034
|
run.id
|
|
8914
9035
|
]
|
|
8915
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);
|
|
8916
9044
|
if (status === "failed") {
|
|
8917
9045
|
const err = body.trace && typeof body.trace === "object" ? String(body.trace.error ?? "").slice(0, 200) : "";
|
|
8918
9046
|
void dispatchEvent({
|
|
@@ -9521,8 +9649,8 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
9521
9649
|
// src/nests/asset-routes.ts
|
|
9522
9650
|
import { Hono as Hono18 } from "hono";
|
|
9523
9651
|
import { v4 as uuid13 } from "uuid";
|
|
9524
|
-
import { mkdir as mkdir2, readFile
|
|
9525
|
-
import { join as
|
|
9652
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
9653
|
+
import { join as join4 } from "path";
|
|
9526
9654
|
var assetRoutes = new Hono18();
|
|
9527
9655
|
var CONTENT_TYPES = {
|
|
9528
9656
|
png: "image/png",
|
|
@@ -9570,9 +9698,9 @@ assetRoutes.post("/", async (c) => {
|
|
|
9570
9698
|
);
|
|
9571
9699
|
}
|
|
9572
9700
|
const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
9573
|
-
const dir =
|
|
9701
|
+
const dir = join4(resolveNestPath(nestId), "assets");
|
|
9574
9702
|
await mkdir2(dir, { recursive: true });
|
|
9575
|
-
await writeFile(
|
|
9703
|
+
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
9576
9704
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
9577
9705
|
return c.json(
|
|
9578
9706
|
{
|
|
@@ -9594,7 +9722,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
9594
9722
|
}
|
|
9595
9723
|
let bytes;
|
|
9596
9724
|
try {
|
|
9597
|
-
bytes = await
|
|
9725
|
+
bytes = await readFile(join4(resolveNestPath(nestId), "assets", name));
|
|
9598
9726
|
} catch {
|
|
9599
9727
|
throw new NotFoundError("Asset not found");
|
|
9600
9728
|
}
|
|
@@ -10024,6 +10152,11 @@ Streamable HTTP, stateless. Two endpoints:
|
|
|
10024
10152
|
(new document in a writable nest; a \`folder\` path creates the folder).
|
|
10025
10153
|
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset including writes and
|
|
10026
10154
|
governance (create/update, submit/approve/reject, stewards, sharing).
|
|
10155
|
+
Sharing a slice is one call: \`context_grant\` with
|
|
10156
|
+
\`{ "email", "target_type": "document" | "folder", "target", "role": "read" | "write" }\`
|
|
10157
|
+
\u2014 \`target\` is a node id (\`nodes/win-loss-log\`) or a folder prefix
|
|
10158
|
+
(\`nodes/gtm/deals\`). The recipient reaches exactly that document, or
|
|
10159
|
+
everything under that folder, and nothing else in the nest. Owner/admin only.
|
|
10027
10160
|
|
|
10028
10161
|
Claude Code / Cursor / VS Code connect natively via HTTP with the
|
|
10029
10162
|
\`Authorization: Bearer\` header. Claude Desktop needs the mcp-remote bridge:
|
|
@@ -10032,20 +10165,9 @@ Claude Code / Cursor / VS Code connect natively via HTTP with the
|
|
|
10032
10165
|
npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
10033
10166
|
\`\`\`
|
|
10034
10167
|
|
|
10035
|
-
## ctx CLI (npm: @promptowl/contextnest-cli)
|
|
10036
|
-
|
|
10037
|
-
\`npm i -g @promptowl/contextnest-cli\`
|
|
10038
|
-
|
|
10039
|
-
- \`ctx push --server ${B} --nest <nest-id> --key <api-key>\` \u2014 upload a local
|
|
10040
|
-
vault into a nest (add \`--include-drafts\` for unpublished nodes).
|
|
10041
|
-
- All other ctx commands (\`query\`, \`add\`, \`search\`, \`checkpoint\`) operate on
|
|
10042
|
-
a LOCAL vault directory only. For remote reads use the API or MCP above \u2014
|
|
10043
|
-
there is currently no remote \`ctx query\`.
|
|
10044
|
-
|
|
10045
10168
|
## npm packages
|
|
10046
10169
|
|
|
10047
10170
|
- \`@promptowl/contextnest-community\` \u2014 this server
|
|
10048
|
-
- \`@promptowl/contextnest-cli\` \u2014 the ctx CLI
|
|
10049
10171
|
- \`@promptowl/contextnest-engine\` \u2014 vault storage + selector/graph engine
|
|
10050
10172
|
|
|
10051
10173
|
## Conventions
|
|
@@ -10410,7 +10532,7 @@ function rowToComment(row) {
|
|
|
10410
10532
|
|
|
10411
10533
|
// src/governance/stewards-parser.ts
|
|
10412
10534
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
10413
|
-
import { join as
|
|
10535
|
+
import { join as join5 } from "path";
|
|
10414
10536
|
function parseStewardsYaml(content) {
|
|
10415
10537
|
const result = { version: 1 };
|
|
10416
10538
|
const lines = content.split("\n");
|
|
@@ -10476,9 +10598,9 @@ function parseEntry(str) {
|
|
|
10476
10598
|
function loadStewardsConfig(nestId) {
|
|
10477
10599
|
const nestPath = resolveNestPath(nestId);
|
|
10478
10600
|
const candidates = [
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10601
|
+
join5(nestPath, "stewards.yaml"),
|
|
10602
|
+
join5(nestPath, "stewards.yml"),
|
|
10603
|
+
join5(nestPath, ".context", "stewards.yaml")
|
|
10482
10604
|
];
|
|
10483
10605
|
for (const candidatePath of candidates) {
|
|
10484
10606
|
if (existsSync(candidatePath)) {
|
|
@@ -10624,6 +10746,25 @@ function denyUnlessNestManager(c, what) {
|
|
|
10624
10746
|
403
|
|
10625
10747
|
);
|
|
10626
10748
|
}
|
|
10749
|
+
async function mayRequestDeletion(c, nestId, nodeId) {
|
|
10750
|
+
return canRequestDeletion({
|
|
10751
|
+
nestId,
|
|
10752
|
+
userId: c.get("userId"),
|
|
10753
|
+
userEmail: await getUserEmail4(c),
|
|
10754
|
+
nodeId,
|
|
10755
|
+
// The middleware promotes open-mode callers; resolving fresh would miss it.
|
|
10756
|
+
permission: c.get("nestPermission")
|
|
10757
|
+
});
|
|
10758
|
+
}
|
|
10759
|
+
function denyUnlessEditor(allowed, c, what) {
|
|
10760
|
+
if (allowed) return null;
|
|
10761
|
+
return c.json(
|
|
10762
|
+
{
|
|
10763
|
+
error: `Only an editor, a nest admin, or the nest owner can request the deletion of ${what}.`
|
|
10764
|
+
},
|
|
10765
|
+
403
|
|
10766
|
+
);
|
|
10767
|
+
}
|
|
10627
10768
|
governanceRoutes.post("/delete-folder", async (c) => {
|
|
10628
10769
|
const nestId = c.req.param("nestId");
|
|
10629
10770
|
const denied = denyUnlessNestManager(c, "a folder");
|
|
@@ -10641,6 +10782,12 @@ governanceRoutes.post("/delete-folder", async (c) => {
|
|
|
10641
10782
|
});
|
|
10642
10783
|
governanceRoutes.post("/request-deletion", async (c) => {
|
|
10643
10784
|
const nestId = c.req.param("nestId");
|
|
10785
|
+
const denied = denyUnlessEditor(
|
|
10786
|
+
await mayRequestDeletion(c, nestId, ""),
|
|
10787
|
+
c,
|
|
10788
|
+
"a folder or a nest"
|
|
10789
|
+
);
|
|
10790
|
+
if (denied) return denied;
|
|
10644
10791
|
const body = await c.req.json().catch(() => ({}));
|
|
10645
10792
|
const reason = body.reason?.trim();
|
|
10646
10793
|
if (!reason) {
|
|
@@ -10731,6 +10878,16 @@ governanceRoutes.post("/deletion-requests/:id/decline", async (c) => {
|
|
|
10731
10878
|
if (!request) return c.json({ error: "Deletion request not found" }, 404);
|
|
10732
10879
|
return c.json({ request });
|
|
10733
10880
|
});
|
|
10881
|
+
governanceRoutes.post("/deletion-requests/:id/withdraw", async (c) => {
|
|
10882
|
+
const nestId = c.req.param("nestId");
|
|
10883
|
+
const request = await withdrawDeletionRequest({
|
|
10884
|
+
nestId,
|
|
10885
|
+
id: c.req.param("id"),
|
|
10886
|
+
withdrawnBy: await getUserEmail4(c)
|
|
10887
|
+
});
|
|
10888
|
+
if (!request) return c.json({ error: "Deletion request not found or not yours" }, 404);
|
|
10889
|
+
return c.json({ withdrawn: true, request });
|
|
10890
|
+
});
|
|
10734
10891
|
governanceRoutes.get("/feed", async (c) => {
|
|
10735
10892
|
const nestId = c.req.param("nestId");
|
|
10736
10893
|
const filterRaw = c.req.query("filter");
|
|
@@ -10804,7 +10961,7 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10804
10961
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
10805
10962
|
const { versions: versionManager } = await engineCache.get(nestId);
|
|
10806
10963
|
const resolutions = await getDb().all(
|
|
10807
|
-
`SELECT version, status, resolved_by
|
|
10964
|
+
`SELECT version, status, resolved_by, resolution_note
|
|
10808
10965
|
FROM review_requests
|
|
10809
10966
|
WHERE nest_id = ? AND node_id = ?
|
|
10810
10967
|
AND status IN ('approved', 'rejected')
|
|
@@ -10815,13 +10972,21 @@ governanceNodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
10815
10972
|
const byVersion = /* @__PURE__ */ new Map();
|
|
10816
10973
|
for (const r of resolutions) {
|
|
10817
10974
|
if (!byVersion.has(r.version)) {
|
|
10818
|
-
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
|
+
});
|
|
10819
10980
|
}
|
|
10820
10981
|
}
|
|
10821
10982
|
const enriched = await Promise.all(
|
|
10822
10983
|
allVersions.map(async (v) => {
|
|
10823
10984
|
const r = byVersion.get(v.version);
|
|
10824
|
-
const resolution = r ? {
|
|
10985
|
+
const resolution = r ? {
|
|
10986
|
+
resolvedBy: r.resolvedBy,
|
|
10987
|
+
resolutionStatus: r.status,
|
|
10988
|
+
resolutionNote: r.resolutionNote
|
|
10989
|
+
} : {};
|
|
10825
10990
|
try {
|
|
10826
10991
|
const diff = await versionManager.getDiff(nodeId, v.version);
|
|
10827
10992
|
return { ...v, ...resolution, diff: diff ?? void 0 };
|
|
@@ -10988,6 +11153,12 @@ governanceNodeRoutes.post("/:nodeId{.+}/request-deletion", async (c) => {
|
|
|
10988
11153
|
if (!await canReadNode(nestId, nodeId, c.get("userId"), userEmail)) {
|
|
10989
11154
|
return c.json({ error: `Node not found: ${nodeId}` }, 404);
|
|
10990
11155
|
}
|
|
11156
|
+
const denied = denyUnlessEditor(
|
|
11157
|
+
await mayRequestDeletion(c, nestId, nodeId),
|
|
11158
|
+
c,
|
|
11159
|
+
"a document"
|
|
11160
|
+
);
|
|
11161
|
+
if (denied) return denied;
|
|
10991
11162
|
const { storage } = await engineCache.get(nestId);
|
|
10992
11163
|
try {
|
|
10993
11164
|
await storage.readDocument(nodeId);
|
|
@@ -11193,19 +11364,19 @@ async function ensureAnonymousUser() {
|
|
|
11193
11364
|
// src/app.ts
|
|
11194
11365
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
11195
11366
|
import { fileURLToPath } from "url";
|
|
11196
|
-
import { dirname as dirname2, join as
|
|
11367
|
+
import { dirname as dirname2, join as join6, relative as relative2 } from "path";
|
|
11197
11368
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
11198
11369
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
11199
11370
|
var SERVICE_VERSION = (() => {
|
|
11200
11371
|
try {
|
|
11201
|
-
return JSON.parse(readFileSync4(
|
|
11372
|
+
return JSON.parse(readFileSync4(join6(HERE, "..", "package.json"), "utf8")).version;
|
|
11202
11373
|
} catch {
|
|
11203
11374
|
return "unknown";
|
|
11204
11375
|
}
|
|
11205
11376
|
})();
|
|
11206
11377
|
var UI_DIR_CANDIDATES = [
|
|
11207
|
-
|
|
11208
|
-
|
|
11378
|
+
join6(HERE, "web3"),
|
|
11379
|
+
join6(process.cwd(), "dist", "web3")
|
|
11209
11380
|
];
|
|
11210
11381
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
11211
11382
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
@@ -12102,14 +12273,14 @@ function createApp() {
|
|
|
12102
12273
|
const userId = c.get("userId");
|
|
12103
12274
|
const userEmail = await resolveCallerEmail(userId);
|
|
12104
12275
|
const perNest = await Promise.all(
|
|
12105
|
-
(await listVisibleNests(userId)).map(async (nest) => {
|
|
12276
|
+
(await listVisibleNests(userId, { readable: true })).map(async (nest) => {
|
|
12106
12277
|
try {
|
|
12107
12278
|
const { storage } = await engineCache.get(nest.id);
|
|
12108
|
-
const matches = matchDocuments(await storage
|
|
12279
|
+
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
12109
12280
|
if (matches.length === 0) return [];
|
|
12110
12281
|
const accessible = await filterAccessible(nest.id, userId, userEmail, matches);
|
|
12111
12282
|
return accessible.map((n) => ({
|
|
12112
|
-
...toSearchHit(n),
|
|
12283
|
+
...toSearchHit(n, q),
|
|
12113
12284
|
nest_id: nest.id,
|
|
12114
12285
|
nest_name: nest.name ?? nest.id
|
|
12115
12286
|
}));
|
|
@@ -12119,7 +12290,7 @@ function createApp() {
|
|
|
12119
12290
|
}
|
|
12120
12291
|
})
|
|
12121
12292
|
);
|
|
12122
|
-
const hits = perNest.flat();
|
|
12293
|
+
const hits = perNest.flat().sort((a, b) => titleScore(b.title, q) - titleScore(a.title, q));
|
|
12123
12294
|
return c.json({
|
|
12124
12295
|
query: q,
|
|
12125
12296
|
count: hits.length,
|
|
@@ -12195,7 +12366,8 @@ function createApp() {
|
|
|
12195
12366
|
c.set("nestPermission", "owner");
|
|
12196
12367
|
return next();
|
|
12197
12368
|
}
|
|
12198
|
-
const
|
|
12369
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
12370
|
+
const permission = access.permission;
|
|
12199
12371
|
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
12200
12372
|
let grantRole = null;
|
|
12201
12373
|
let grantNestVisible = false;
|
|
@@ -12208,11 +12380,13 @@ function createApp() {
|
|
|
12208
12380
|
}
|
|
12209
12381
|
grantRole = await resolveNodeGrant(nestId, userId, targetTail);
|
|
12210
12382
|
} else if (parts.length === 1 && c.req.method === "GET") {
|
|
12211
|
-
grantNestVisible =
|
|
12383
|
+
grantNestVisible = access.hasGrant;
|
|
12212
12384
|
} else if (parts[1] === "nodes" && parts.length === 2 && c.req.method === "GET") {
|
|
12213
|
-
grantNestVisible =
|
|
12385
|
+
grantNestVisible = access.hasGrant;
|
|
12214
12386
|
} else if (isReadQuery) {
|
|
12215
|
-
grantNestVisible =
|
|
12387
|
+
grantNestVisible = access.hasGrant;
|
|
12388
|
+
} else if (parts[1] === "definitions" && parts.length === 2 && c.req.method === "GET") {
|
|
12389
|
+
grantNestVisible = access.hasGrant;
|
|
12216
12390
|
}
|
|
12217
12391
|
if (!grantRole && !grantNestVisible) {
|
|
12218
12392
|
return deny(404, "Nest not found", {
|
|
@@ -12225,7 +12399,7 @@ function createApp() {
|
|
|
12225
12399
|
const isStewardActionPath = path.includes("/approve") || path.includes("/reject") || path.includes("/submit-review") || path.includes("/cancel-review");
|
|
12226
12400
|
const isAnnotationAction = /\/annotations$/.test(path) || /\/annotations\/[^/]+\/(comments|resolve|reopen)$/.test(path);
|
|
12227
12401
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
12228
|
-
const isDeletionRequestAction = /\/request-deletion$/.test(path);
|
|
12402
|
+
const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
|
|
12229
12403
|
const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
|
|
12230
12404
|
if (isStewardRoster && permission !== "owner" && permission !== "admin") {
|
|
12231
12405
|
return c.json(
|