@promptowl/contextnest-community 1.12.0 → 1.14.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 +59 -2
- package/README.md +3 -1
- package/dist/{chunk-3M7677XW.js → chunk-I5KYGMIT.js} +36 -1
- package/dist/{chunk-ZMSU7BCC.js → chunk-IHGPZ6NY.js} +1 -1
- package/dist/{chunk-JHIU6RZU.js → chunk-JP67WXLA.js} +34 -1
- package/dist/{chunk-F7EKYNXG.js → chunk-KPK656BH.js} +1336 -829
- package/dist/{chunk-XB7T3THE.js → chunk-MZ4IR5KH.js} +471 -504
- package/dist/{client-YUOEVUFU.js → client-3MGIP57D.js} +1 -1
- package/dist/{grants-service-PS5RMMEA.js → grants-service-5ZFISCBH.js} +2 -2
- package/dist/index.js +311 -108
- package/dist/{review-service-DPN26C47.js → review-service-6G7MVTY6.js} +7 -5
- package/dist/{stewardship-service-7BHI2YXB.js → stewardship-service-G7IF7ZNP.js} +4 -2
- package/dist/{version-service-YQIIQ7FN.js → version-service-FGL47APL.js} +6 -4
- package/dist/web3/assets/index-BYmOkm4p.js +1052 -0
- package/dist/web3/assets/index-p6uQyhml.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -1
- package/dist/web3/assets/index-BiGiarBx.css +0 -1
- package/dist/web3/assets/index-CUVRdYvH.js +0 -1026
package/dist/index.js
CHANGED
|
@@ -17,18 +17,23 @@ import {
|
|
|
17
17
|
listNotifications,
|
|
18
18
|
listWatchers,
|
|
19
19
|
markNotificationsRead,
|
|
20
|
+
nodeWriteKey,
|
|
20
21
|
notifyNestEvent,
|
|
22
|
+
notifyNestInvite,
|
|
21
23
|
parseConditionSchema,
|
|
22
24
|
reject,
|
|
23
25
|
removeWatcher,
|
|
26
|
+
repointPendingReview,
|
|
24
27
|
requireWorkflowPlane,
|
|
25
28
|
resolveCallerEmail,
|
|
26
29
|
safeJson,
|
|
27
30
|
safePublishDocument,
|
|
28
31
|
seedDefaultEdgeTypes,
|
|
32
|
+
sendTestNotification,
|
|
33
|
+
stripUndefinedDeep,
|
|
29
34
|
submitForReview,
|
|
30
|
-
|
|
31
|
-
} from "./chunk-
|
|
35
|
+
withNodeWriteLock
|
|
36
|
+
} from "./chunk-MZ4IR5KH.js";
|
|
32
37
|
import {
|
|
33
38
|
createGrant,
|
|
34
39
|
deleteGrant,
|
|
@@ -37,7 +42,7 @@ import {
|
|
|
37
42
|
listGrants,
|
|
38
43
|
listUserGrants,
|
|
39
44
|
resolveNodeGrant
|
|
40
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-IHGPZ6NY.js";
|
|
41
46
|
import {
|
|
42
47
|
generateApiKey,
|
|
43
48
|
getKeyPrefix,
|
|
@@ -53,8 +58,9 @@ import {
|
|
|
53
58
|
getCurrentVersion,
|
|
54
59
|
getDisplayStatus,
|
|
55
60
|
getVersions,
|
|
56
|
-
setApprovedVersion
|
|
57
|
-
|
|
61
|
+
setApprovedVersion,
|
|
62
|
+
upsertVersion
|
|
63
|
+
} from "./chunk-JP67WXLA.js";
|
|
58
64
|
import {
|
|
59
65
|
addMember,
|
|
60
66
|
buildTitleMap,
|
|
@@ -63,7 +69,6 @@ import {
|
|
|
63
69
|
canUserAccess,
|
|
64
70
|
canUserApprove,
|
|
65
71
|
canUserEdit,
|
|
66
|
-
collabPermToRole,
|
|
67
72
|
createNest,
|
|
68
73
|
createStewardRecord,
|
|
69
74
|
createTeam,
|
|
@@ -98,6 +103,7 @@ import {
|
|
|
98
103
|
listSharedNests,
|
|
99
104
|
listStewards,
|
|
100
105
|
listTeamsForUser,
|
|
106
|
+
listVisibleNests,
|
|
101
107
|
loadAccessConfig,
|
|
102
108
|
loadGrantedSuperAdmins,
|
|
103
109
|
loadServerSettings,
|
|
@@ -111,13 +117,14 @@ import {
|
|
|
111
117
|
renameNest,
|
|
112
118
|
renameTeam,
|
|
113
119
|
replaceTeamRoster,
|
|
120
|
+
resolveGrantRoles,
|
|
114
121
|
resolveMemberIdentity,
|
|
115
122
|
resolveNestPath,
|
|
116
123
|
resolveNestPermission,
|
|
117
124
|
resolveStewardsForNode,
|
|
118
125
|
resolveStewardsWithFallback,
|
|
119
|
-
resolveTeamRolesForUser,
|
|
120
126
|
resolveUserRoles,
|
|
127
|
+
sendEmailToRecipient,
|
|
121
128
|
setAllowSelfApprove,
|
|
122
129
|
setStewardshipEnabled,
|
|
123
130
|
shareTeamWithNest,
|
|
@@ -129,8 +136,9 @@ import {
|
|
|
129
136
|
unshareTeamFromNest,
|
|
130
137
|
updateMemberRole,
|
|
131
138
|
updateSteward,
|
|
132
|
-
validateLicense
|
|
133
|
-
|
|
139
|
+
validateLicense,
|
|
140
|
+
verifySmtp
|
|
141
|
+
} from "./chunk-KPK656BH.js";
|
|
134
142
|
import {
|
|
135
143
|
AppError,
|
|
136
144
|
ConflictError,
|
|
@@ -153,7 +161,7 @@ import {
|
|
|
153
161
|
initDb,
|
|
154
162
|
isEmailListish,
|
|
155
163
|
isEmailish
|
|
156
|
-
} from "./chunk-
|
|
164
|
+
} from "./chunk-I5KYGMIT.js";
|
|
157
165
|
import {
|
|
158
166
|
ANON_EMAIL,
|
|
159
167
|
ANON_USER_ID
|
|
@@ -901,6 +909,9 @@ authRoutes.post("/register", async (c) => {
|
|
|
901
909
|
if (!body.email || !body.password) {
|
|
902
910
|
throw new ValidationError("email and password are required");
|
|
903
911
|
}
|
|
912
|
+
if (!isEmailish(body.email)) {
|
|
913
|
+
throw new ValidationError("Enter a valid email address.");
|
|
914
|
+
}
|
|
904
915
|
assertValidPassword(body.password);
|
|
905
916
|
const email = normalizeEmail(body.email);
|
|
906
917
|
const ip = clientIp(c);
|
|
@@ -1421,6 +1432,9 @@ authRoutes.delete("/users/:userId", async (c) => {
|
|
|
1421
1432
|
authRoutes.post("/invite", async (c) => {
|
|
1422
1433
|
const body = await c.req.json();
|
|
1423
1434
|
if (!body.email) throw new ValidationError("email is required");
|
|
1435
|
+
if (!isEmailish(body.email)) {
|
|
1436
|
+
throw new ValidationError("Enter a valid email address.");
|
|
1437
|
+
}
|
|
1424
1438
|
const email = normalizeEmail(body.email);
|
|
1425
1439
|
const callerId = await resolveCallerUserId(c);
|
|
1426
1440
|
if (!callerId) {
|
|
@@ -1469,6 +1483,18 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1469
1483
|
);
|
|
1470
1484
|
}
|
|
1471
1485
|
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1486
|
+
const inviter = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
1487
|
+
callerId
|
|
1488
|
+
]);
|
|
1489
|
+
void sendEmailToRecipient(email, {
|
|
1490
|
+
status: "invited",
|
|
1491
|
+
docTitle: "ContextNest",
|
|
1492
|
+
path: "",
|
|
1493
|
+
actor: email,
|
|
1494
|
+
by: inviter?.email || void 0,
|
|
1495
|
+
tempPassword,
|
|
1496
|
+
link: requestBaseUrl(c.req.url)
|
|
1497
|
+
});
|
|
1472
1498
|
return c.json(
|
|
1473
1499
|
{
|
|
1474
1500
|
temporary_password: tempPassword,
|
|
@@ -1758,7 +1784,7 @@ async function approveExternalEdit(input) {
|
|
|
1758
1784
|
const node = await storage.readDocument(input.documentId);
|
|
1759
1785
|
const versionNum = result.versionEntry.version;
|
|
1760
1786
|
const tags = node.frontmatter.tags || [];
|
|
1761
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1787
|
+
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-FGL47APL.js");
|
|
1762
1788
|
await createVersion2({
|
|
1763
1789
|
nestId: input.nestId,
|
|
1764
1790
|
nodeId: input.documentId,
|
|
@@ -1867,18 +1893,6 @@ async function departmentTagFor(userEmail) {
|
|
|
1867
1893
|
return null;
|
|
1868
1894
|
}
|
|
1869
1895
|
}
|
|
1870
|
-
var nodeWriteChains = /* @__PURE__ */ new Map();
|
|
1871
|
-
function withNodeWriteLock(key, fn) {
|
|
1872
|
-
const prev = (nodeWriteChains.get(key) ?? Promise.resolve()).catch(() => {
|
|
1873
|
-
});
|
|
1874
|
-
const run = prev.then(fn);
|
|
1875
|
-
nodeWriteChains.set(key, run);
|
|
1876
|
-
void run.catch(() => {
|
|
1877
|
-
}).finally(() => {
|
|
1878
|
-
if (nodeWriteChains.get(key) === run) nodeWriteChains.delete(key);
|
|
1879
|
-
});
|
|
1880
|
-
return run;
|
|
1881
|
-
}
|
|
1882
1896
|
function toSafeError(err, context, message) {
|
|
1883
1897
|
if (err instanceof AppError) return err;
|
|
1884
1898
|
console.error(`${context}:`, err);
|
|
@@ -2050,6 +2064,11 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2050
2064
|
async function listNodesForCallerByEmail(nestId, userEmail, filters = {}) {
|
|
2051
2065
|
return listNodesForCaller(nestId, await userIdFromEmail(userEmail), filters);
|
|
2052
2066
|
}
|
|
2067
|
+
async function actorAutoPublishes(nestId, userEmail) {
|
|
2068
|
+
if (!await nestAllowsSelfApprove(nestId)) return false;
|
|
2069
|
+
const roles = await resolveUserRoles(nestId, userEmail);
|
|
2070
|
+
return roles.includes("owner") || roles.includes("admin");
|
|
2071
|
+
}
|
|
2053
2072
|
async function createNode(nestId, input, userEmail) {
|
|
2054
2073
|
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
2055
2074
|
const slug = slugify(input.title);
|
|
@@ -2084,8 +2103,9 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2084
2103
|
tags.push(deptTag);
|
|
2085
2104
|
}
|
|
2086
2105
|
const hasStewards = await isStewardshipEnabled(nestId);
|
|
2087
|
-
const
|
|
2088
|
-
const
|
|
2106
|
+
const autoPublish = !hasStewards || await actorAutoPublishes(nestId, userEmail);
|
|
2107
|
+
const initialStatus = autoPublish ? "published" : "draft";
|
|
2108
|
+
const initialVersion = autoPublish ? 0 : 1;
|
|
2089
2109
|
let node = {
|
|
2090
2110
|
id,
|
|
2091
2111
|
filePath: "",
|
|
@@ -2109,7 +2129,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2109
2129
|
await storage.writeDocument(id, serializeDocument(node));
|
|
2110
2130
|
await syncNodeTags(nestId, id, tags);
|
|
2111
2131
|
let savedVersion = 1;
|
|
2112
|
-
if (
|
|
2132
|
+
if (!autoPublish) {
|
|
2113
2133
|
try {
|
|
2114
2134
|
await versionManager.createVersion(node, userEmail);
|
|
2115
2135
|
} catch (err) {
|
|
@@ -2128,7 +2148,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2128
2148
|
try {
|
|
2129
2149
|
const result = await safePublishDocument(storage, id, {
|
|
2130
2150
|
editedBy: userEmail,
|
|
2131
|
-
note: "Auto-published on create (no stewards configured)"
|
|
2151
|
+
note: hasStewards ? "Auto-published on create (owner/admin self-approve)" : "Auto-published on create (no stewards configured)"
|
|
2132
2152
|
});
|
|
2133
2153
|
savedVersion = result.node.frontmatter.version || 1;
|
|
2134
2154
|
await createVersion({
|
|
@@ -2287,7 +2307,7 @@ async function registerImportedDocuments(nestId, userEmail, onProgress) {
|
|
|
2287
2307
|
return registered;
|
|
2288
2308
|
}
|
|
2289
2309
|
async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
2290
|
-
return withNodeWriteLock(
|
|
2310
|
+
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
2291
2311
|
try {
|
|
2292
2312
|
const { storage, versions: versionManager } = await engineCache.get(
|
|
2293
2313
|
nestId
|
|
@@ -2337,14 +2357,17 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2337
2357
|
node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
|
|
2338
2358
|
}
|
|
2339
2359
|
const hasStewards = await isStewardshipEnabled(nestId);
|
|
2360
|
+
const autoPublish = !hasStewards || await actorAutoPublishes(nestId, userEmail);
|
|
2340
2361
|
const currentTags = node.frontmatter.tags || [];
|
|
2341
|
-
|
|
2362
|
+
const pendingReview = hasStewards ? await getPendingReview(nestId, nodeId) : null;
|
|
2363
|
+
const isAuthorEditingOwnReview = !!pendingReview && pendingReview.requestedBy.toLowerCase() === userEmail.toLowerCase();
|
|
2364
|
+
if (pendingReview && !isAuthorEditingOwnReview) {
|
|
2342
2365
|
throw new LockedError(
|
|
2343
2366
|
"This document is awaiting steward review and is locked. Approve or reject the pending review before editing."
|
|
2344
2367
|
);
|
|
2345
2368
|
}
|
|
2346
2369
|
let responseVersion;
|
|
2347
|
-
if (
|
|
2370
|
+
if (!autoPublish || isAuthorEditingOwnReview) {
|
|
2348
2371
|
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
2349
2372
|
const newVersion = currentVersion + 1;
|
|
2350
2373
|
node = {
|
|
@@ -2382,6 +2405,9 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2382
2405
|
tags: currentTags,
|
|
2383
2406
|
changeNote: patch.changeNote
|
|
2384
2407
|
});
|
|
2408
|
+
if (isAuthorEditingOwnReview) {
|
|
2409
|
+
await repointPendingReview({ nestId, nodeId, newVersion });
|
|
2410
|
+
}
|
|
2385
2411
|
responseVersion = newVersion;
|
|
2386
2412
|
} else {
|
|
2387
2413
|
node = {
|
|
@@ -2399,7 +2425,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2399
2425
|
try {
|
|
2400
2426
|
const result = await safePublishDocument(storage, nodeId, {
|
|
2401
2427
|
editedBy: userEmail,
|
|
2402
|
-
note: patch.changeNote || "Auto-published on edit (no stewards)"
|
|
2428
|
+
note: patch.changeNote || (hasStewards ? "Auto-published on edit (owner/admin self-approve)" : "Auto-published on edit (no stewards)")
|
|
2403
2429
|
});
|
|
2404
2430
|
publishedVersion = result.node.frontmatter.version || publishedVersion;
|
|
2405
2431
|
node = result.node;
|
|
@@ -2410,7 +2436,7 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2410
2436
|
);
|
|
2411
2437
|
throw err;
|
|
2412
2438
|
}
|
|
2413
|
-
await
|
|
2439
|
+
await upsertVersion({
|
|
2414
2440
|
nestId,
|
|
2415
2441
|
nodeId,
|
|
2416
2442
|
version: publishedVersion,
|
|
@@ -2734,6 +2760,25 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
2734
2760
|
}
|
|
2735
2761
|
return { nest, documents };
|
|
2736
2762
|
}
|
|
2763
|
+
function deleteUnsyncedFolder(folderName) {
|
|
2764
|
+
console.log(`[unsynced] delete requested folder="${folderName}"`);
|
|
2765
|
+
assertSafeFolderName(folderName);
|
|
2766
|
+
const src = join3(config.DATA_ROOT, folderName);
|
|
2767
|
+
if (isNestStorageRoot(src)) {
|
|
2768
|
+
throw new ValidationError("Folder is not eligible for delete");
|
|
2769
|
+
}
|
|
2770
|
+
let stat;
|
|
2771
|
+
try {
|
|
2772
|
+
stat = statSync(src);
|
|
2773
|
+
} catch {
|
|
2774
|
+
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
2775
|
+
}
|
|
2776
|
+
if (!stat.isDirectory()) {
|
|
2777
|
+
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
2778
|
+
}
|
|
2779
|
+
rmSync(src, { recursive: true, force: true });
|
|
2780
|
+
console.log(`[unsynced] deleted folder ${src}`);
|
|
2781
|
+
}
|
|
2737
2782
|
|
|
2738
2783
|
// src/nests/routes.ts
|
|
2739
2784
|
async function effectivePermission(nestId, userId) {
|
|
@@ -2759,54 +2804,42 @@ nestRoutes.get("/", async (c) => {
|
|
|
2759
2804
|
const annotate = async (n) => {
|
|
2760
2805
|
const permission = await effectivePermission(n.id, userId);
|
|
2761
2806
|
const is_owner = permission === "owner";
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
);
|
|
2770
|
-
const stewardRoles = await getStewardRolesForUser(n.id, callerEmail);
|
|
2771
|
-
const teamRoles = await resolveTeamRolesForUser(n.id, userId);
|
|
2772
|
-
roles = [
|
|
2773
|
-
...new Set(
|
|
2774
|
-
[grantRole, ...stewardRoles, ...teamRoles].filter(Boolean)
|
|
2775
|
-
)
|
|
2776
|
-
];
|
|
2777
|
-
}
|
|
2778
|
-
if (!is_owner && n.user_id !== ANON_USER_ID) {
|
|
2779
|
-
const row = await db.get(ownerEmailSql, [n.user_id]);
|
|
2780
|
-
owner_email = row?.email ?? null;
|
|
2781
|
-
}
|
|
2807
|
+
const needOwnerEmail = !is_owner && n.user_id !== ANON_USER_ID;
|
|
2808
|
+
const [roles, owner_email] = await Promise.all([
|
|
2809
|
+
is_owner ? Promise.resolve(["owner"]) : resolveGrantRoles(n.id, callerEmail, userId),
|
|
2810
|
+
needOwnerEmail ? db.get(ownerEmailSql, [n.user_id]).then(
|
|
2811
|
+
(row) => row?.email ?? null
|
|
2812
|
+
) : Promise.resolve(null)
|
|
2813
|
+
]);
|
|
2782
2814
|
return {
|
|
2783
2815
|
...n,
|
|
2784
2816
|
permission,
|
|
2785
2817
|
is_owner,
|
|
2786
2818
|
owner_email,
|
|
2787
2819
|
roles,
|
|
2788
|
-
stewardship_enabled:
|
|
2820
|
+
stewardship_enabled: !!n.stewardship_enabled
|
|
2789
2821
|
};
|
|
2790
2822
|
};
|
|
2791
2823
|
const seen = /* @__PURE__ */ new Set();
|
|
2792
|
-
const
|
|
2824
|
+
const deduped = [];
|
|
2793
2825
|
for (const n of [...owned, ...shared, ...publicExtras]) {
|
|
2794
2826
|
if (seen.has(n.id)) continue;
|
|
2795
2827
|
seen.add(n.id);
|
|
2796
|
-
|
|
2828
|
+
deduped.push(n);
|
|
2797
2829
|
}
|
|
2830
|
+
const out = await Promise.all(deduped.map(annotate));
|
|
2798
2831
|
const docRows = await db.all(
|
|
2799
2832
|
"SELECT nest_id, COUNT(DISTINCT node_id) AS c FROM node_versions GROUP BY nest_id"
|
|
2800
2833
|
);
|
|
2801
|
-
const docByNest = new Map(docRows.map((r) => [r.nest_id, r.c]));
|
|
2834
|
+
const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
2802
2835
|
const collabRows = await db.all(
|
|
2803
2836
|
"SELECT nest_id, COUNT(*) AS c FROM nest_collaborators GROUP BY nest_id"
|
|
2804
2837
|
);
|
|
2805
|
-
const collabByNest = new Map(collabRows.map((r) => [r.nest_id, r.c]));
|
|
2838
|
+
const collabByNest = new Map(collabRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
2806
2839
|
const stewardRows = await db.all(
|
|
2807
2840
|
"SELECT nest_id, COUNT(DISTINCT user_email) AS c FROM stewards WHERE is_active = 1 GROUP BY nest_id"
|
|
2808
2841
|
);
|
|
2809
|
-
const stewardByNest = new Map(stewardRows.map((r) => [r.nest_id, r.c]));
|
|
2842
|
+
const stewardByNest = new Map(stewardRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
2810
2843
|
const stewardEmailRows = await db.all(
|
|
2811
2844
|
`SELECT nest_id, user_email FROM stewards WHERE is_active = 1
|
|
2812
2845
|
GROUP BY nest_id, user_email ORDER BY MIN(assigned_at)`
|
|
@@ -2913,6 +2946,21 @@ nestRoutes.post("/:nestId/sync", async (c) => {
|
|
|
2913
2946
|
);
|
|
2914
2947
|
return c.json(result, 201);
|
|
2915
2948
|
});
|
|
2949
|
+
nestRoutes.delete("/:nestId/sync", async (c) => {
|
|
2950
|
+
if (c.req.param("nestId") !== "unsynced") {
|
|
2951
|
+
throw new NotFoundError("Not found");
|
|
2952
|
+
}
|
|
2953
|
+
const userId = c.get("userId");
|
|
2954
|
+
if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
|
|
2955
|
+
throw new ForbiddenError("Only the server admin can delete folders");
|
|
2956
|
+
}
|
|
2957
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2958
|
+
if (!body?.name) {
|
|
2959
|
+
throw new ValidationError("name is required");
|
|
2960
|
+
}
|
|
2961
|
+
deleteUnsyncedFolder(body.name);
|
|
2962
|
+
return c.json({ ok: true });
|
|
2963
|
+
});
|
|
2916
2964
|
nestRoutes.get("/:nestId", async (c) => {
|
|
2917
2965
|
const nestId = c.req.param("nestId");
|
|
2918
2966
|
if (nestId === "unsynced") return handleUnsyncedList(c);
|
|
@@ -3037,6 +3085,9 @@ async function addCollaborator(params) {
|
|
|
3037
3085
|
const db = getDb();
|
|
3038
3086
|
let userId = params.userId;
|
|
3039
3087
|
if (!userId && params.email) {
|
|
3088
|
+
if (!isEmailish(params.email)) {
|
|
3089
|
+
throw new ValidationError("Enter a valid email address.");
|
|
3090
|
+
}
|
|
3040
3091
|
const email = normalizeEmail(params.email);
|
|
3041
3092
|
const existing = await db.get(
|
|
3042
3093
|
"SELECT id FROM users WHERE LOWER(email) = ?",
|
|
@@ -3104,8 +3155,30 @@ async function addCollaborator(params) {
|
|
|
3104
3155
|
actor: params.email || userId,
|
|
3105
3156
|
permission: params.permission,
|
|
3106
3157
|
by: params.grantedByEmail || void 0
|
|
3107
|
-
}
|
|
3108
|
-
|
|
3158
|
+
},
|
|
3159
|
+
// Invite email goes to the invitee directly (below), NOT the shared
|
|
3160
|
+
// NOTIFY_EMAIL_TO team inbox — so skip the team-channel email for this
|
|
3161
|
+
// event. Slack/Teams still fire.
|
|
3162
|
+
{ skipEmail: true }
|
|
3163
|
+
);
|
|
3164
|
+
const inviteeEmail = params.email || (await db.get("SELECT email FROM users WHERE id = ?", [userId]))?.email;
|
|
3165
|
+
if (inviteeEmail) {
|
|
3166
|
+
void sendEmailToRecipient(inviteeEmail, {
|
|
3167
|
+
status: "shared",
|
|
3168
|
+
docTitle: shareNestName,
|
|
3169
|
+
path: shareNestName,
|
|
3170
|
+
link: docLink(nestId, void 0, params.baseUrl),
|
|
3171
|
+
actor: inviteeEmail,
|
|
3172
|
+
permission: params.permission,
|
|
3173
|
+
by: params.grantedByEmail || void 0
|
|
3174
|
+
});
|
|
3175
|
+
void notifyNestInvite({
|
|
3176
|
+
nestId,
|
|
3177
|
+
inviteeEmail,
|
|
3178
|
+
permission: params.permission,
|
|
3179
|
+
nestName: shareNestName
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3109
3182
|
return await db.get(
|
|
3110
3183
|
"SELECT * FROM nest_collaborators WHERE id = ?",
|
|
3111
3184
|
[collabId]
|
|
@@ -3297,7 +3370,8 @@ nestTeamRoutes.post("/", async (c) => {
|
|
|
3297
3370
|
nestId: c.req.param("nestId"),
|
|
3298
3371
|
teamId: body.team_id,
|
|
3299
3372
|
callerUserId: userId,
|
|
3300
|
-
allowAdmin: await isLicenseAdminUserId(userId)
|
|
3373
|
+
allowAdmin: await isLicenseAdminUserId(userId),
|
|
3374
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
3301
3375
|
});
|
|
3302
3376
|
return c.json({ teams }, 201);
|
|
3303
3377
|
});
|
|
@@ -4011,7 +4085,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
4011
4085
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
4012
4086
|
const nestId = c.req.param("nestId");
|
|
4013
4087
|
const nodeId = c.req.param("nodeId");
|
|
4014
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
4088
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-G7IF7ZNP.js");
|
|
4015
4089
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
4016
4090
|
nestId,
|
|
4017
4091
|
nodeId
|
|
@@ -4032,7 +4106,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
4032
4106
|
nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
4033
4107
|
const nestId = c.req.param("nestId");
|
|
4034
4108
|
const nodeId = c.req.param("nodeId");
|
|
4035
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
4109
|
+
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-FGL47APL.js");
|
|
4036
4110
|
const allVersions = await getVersions2(nestId, nodeId);
|
|
4037
4111
|
const approved = await getApprovedVersion2(nestId, nodeId);
|
|
4038
4112
|
const db = getDb();
|
|
@@ -4064,7 +4138,7 @@ nodeRoutes.get("/:nodeId{.+}/versions", async (c) => {
|
|
|
4064
4138
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
4065
4139
|
const nestId = c.req.param("nestId");
|
|
4066
4140
|
const nodeId = c.req.param("nodeId");
|
|
4067
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
4141
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-6G7MVTY6.js");
|
|
4068
4142
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
4069
4143
|
return c.json({ reviews: history });
|
|
4070
4144
|
});
|
|
@@ -4479,7 +4553,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
4479
4553
|
|
|
4480
4554
|
// src/nodes/query-routes.ts
|
|
4481
4555
|
import { Hono as Hono10 } from "hono";
|
|
4482
|
-
import { serializeDocument as
|
|
4556
|
+
import { serializeDocument as serializeDocument3 } from "@promptowl/contextnest-engine";
|
|
4483
4557
|
|
|
4484
4558
|
// src/tables/service.ts
|
|
4485
4559
|
function splitCsvLine(line) {
|
|
@@ -4898,6 +4972,68 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
4898
4972
|
}
|
|
4899
4973
|
}
|
|
4900
4974
|
|
|
4975
|
+
// src/nests/bundle-service.ts
|
|
4976
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
4977
|
+
import { join as join4, sep } from "path";
|
|
4978
|
+
import JSZip from "jszip";
|
|
4979
|
+
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
4980
|
+
function walkFiles(root) {
|
|
4981
|
+
const out = [];
|
|
4982
|
+
let entries;
|
|
4983
|
+
try {
|
|
4984
|
+
entries = readdirSync2(root, { recursive: true });
|
|
4985
|
+
} catch {
|
|
4986
|
+
return out;
|
|
4987
|
+
}
|
|
4988
|
+
for (const rel of entries) {
|
|
4989
|
+
try {
|
|
4990
|
+
if (statSync2(join4(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
4991
|
+
} catch {
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
return out;
|
|
4995
|
+
}
|
|
4996
|
+
async function exportNestBundle(nestId, includeDrafts) {
|
|
4997
|
+
const db = getDb();
|
|
4998
|
+
const approvedRows = await db.all(
|
|
4999
|
+
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
5000
|
+
[nestId]
|
|
5001
|
+
);
|
|
5002
|
+
const approvedByNode = new Map(
|
|
5003
|
+
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
5004
|
+
);
|
|
5005
|
+
const { storage } = await engineCache.get(nestId);
|
|
5006
|
+
const docs = await storage.discoverDocuments();
|
|
5007
|
+
const zip = new JSZip();
|
|
5008
|
+
let count = 0;
|
|
5009
|
+
for (const doc of docs) {
|
|
5010
|
+
const approved = approvedByNode.get(doc.id);
|
|
5011
|
+
const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
|
|
5012
|
+
let body = doc.body || "";
|
|
5013
|
+
if (approved != null) {
|
|
5014
|
+
const approvedBody = await resolveExportBody(nestId, doc.id, body);
|
|
5015
|
+
if (approvedBody == null) continue;
|
|
5016
|
+
body = approvedBody;
|
|
5017
|
+
frontmatter.status = "published";
|
|
5018
|
+
frontmatter.version = approved;
|
|
5019
|
+
} else {
|
|
5020
|
+
if (!includeDrafts) continue;
|
|
5021
|
+
frontmatter.status = "draft";
|
|
5022
|
+
}
|
|
5023
|
+
zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
|
|
5024
|
+
count++;
|
|
5025
|
+
}
|
|
5026
|
+
const nestPath = resolveNestPath(nestId);
|
|
5027
|
+
for (const rel of walkFiles(nestPath)) {
|
|
5028
|
+
const segs = rel.split("/");
|
|
5029
|
+
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
5030
|
+
if (!keep) continue;
|
|
5031
|
+
zip.file(rel, readFileSync2(join4(nestPath, rel)));
|
|
5032
|
+
}
|
|
5033
|
+
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
5034
|
+
return zip.generateAsync({ type: "arraybuffer" });
|
|
5035
|
+
}
|
|
5036
|
+
|
|
4901
5037
|
// src/nodes/graph-service.ts
|
|
4902
5038
|
var MAX_GRAPH_NODES = 150;
|
|
4903
5039
|
var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
@@ -5451,10 +5587,24 @@ queryRoutes.get("/context", async (c) => {
|
|
|
5451
5587
|
return c.json({ content: content || "" });
|
|
5452
5588
|
});
|
|
5453
5589
|
queryRoutes.get("/export", async (c) => {
|
|
5590
|
+
const nestId = c.req.param("nestId");
|
|
5591
|
+
if (c.req.query("format") === "bundle") {
|
|
5592
|
+
const perm = await resolveNestPermission(nestId, c.get("userId"));
|
|
5593
|
+
if (perm !== "owner" && perm !== "admin") {
|
|
5594
|
+
throw new ForbiddenError("Only the nest owner or an admin can export a bundle");
|
|
5595
|
+
}
|
|
5596
|
+
const draftsParam = c.req.query("includeDrafts");
|
|
5597
|
+
const includeDrafts = draftsParam === "1" || draftsParam === "true";
|
|
5598
|
+
const zip = await exportNestBundle(nestId, includeDrafts);
|
|
5599
|
+
const filename = `nest-${nestId}.zip`;
|
|
5600
|
+
return c.body(zip, 200, {
|
|
5601
|
+
"Content-Type": "application/zip",
|
|
5602
|
+
"Content-Disposition": `attachment; filename="${filename}"`
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5454
5605
|
if (!isMarkdownFormat(c)) {
|
|
5455
|
-
throw new ValidationError("format=markdown is required");
|
|
5606
|
+
throw new ValidationError("format=markdown or format=bundle is required");
|
|
5456
5607
|
}
|
|
5457
|
-
const nestId = c.req.param("nestId");
|
|
5458
5608
|
const { storage, query: queryEngine } = await engineCache.get(nestId);
|
|
5459
5609
|
const selector = c.req.query("selector")?.trim() || null;
|
|
5460
5610
|
let documents;
|
|
@@ -5553,7 +5703,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
5553
5703
|
body: doc.content,
|
|
5554
5704
|
rawContent: ""
|
|
5555
5705
|
};
|
|
5556
|
-
const serialized =
|
|
5706
|
+
const serialized = serializeDocument3(node);
|
|
5557
5707
|
await storage.writeDocument(id, serialized);
|
|
5558
5708
|
await syncNodeTags(nestId, id, tags);
|
|
5559
5709
|
created.push(id);
|
|
@@ -7358,17 +7508,15 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
|
|
|
7358
7508
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
7359
7509
|
);
|
|
7360
7510
|
if (!node) return `Node not found: ${args.title}`;
|
|
7361
|
-
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
7362
7511
|
try {
|
|
7363
7512
|
const request = await approve({
|
|
7364
7513
|
nestId: ctx.nestId,
|
|
7365
7514
|
nodeId: node.id,
|
|
7366
|
-
version: currentVersion,
|
|
7367
7515
|
approvedBy: ctx.userEmail,
|
|
7368
7516
|
note: args.note,
|
|
7369
7517
|
baseUrl: ctx.baseUrl
|
|
7370
7518
|
});
|
|
7371
|
-
return `Approved "${args.title}" v${
|
|
7519
|
+
return `Approved "${args.title}" v${request.version}. This version is now live for AI queries.${args.note ? `
|
|
7372
7520
|
Note: ${args.note}` : ""}`;
|
|
7373
7521
|
} catch (err) {
|
|
7374
7522
|
return `Cannot approve: ${err.message}`;
|
|
@@ -7380,17 +7528,15 @@ Note: ${args.note}` : ""}`;
|
|
|
7380
7528
|
(n) => n.frontmatter.title.toLowerCase() === args.title.toLowerCase()
|
|
7381
7529
|
);
|
|
7382
7530
|
if (!node) return `Node not found: ${args.title}`;
|
|
7383
|
-
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
7384
7531
|
try {
|
|
7385
7532
|
const request = await reject({
|
|
7386
7533
|
nestId: ctx.nestId,
|
|
7387
7534
|
nodeId: node.id,
|
|
7388
|
-
version: currentVersion,
|
|
7389
7535
|
rejectedBy: ctx.userEmail,
|
|
7390
7536
|
note: args.note,
|
|
7391
7537
|
baseUrl: ctx.baseUrl
|
|
7392
7538
|
});
|
|
7393
|
-
return `Rejected "${args.title}" v${
|
|
7539
|
+
return `Rejected "${args.title}" v${request.version}.
|
|
7394
7540
|
Reason: ${args.note}`;
|
|
7395
7541
|
} catch (err) {
|
|
7396
7542
|
return `Cannot reject: ${err.message}`;
|
|
@@ -7471,8 +7617,8 @@ ${list}`;
|
|
|
7471
7617
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
7472
7618
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
7473
7619
|
try {
|
|
7474
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
7475
|
-
const db = (await import("./client-
|
|
7620
|
+
const { createGrant: createGrant2 } = await import("./grants-service-5ZFISCBH.js");
|
|
7621
|
+
const db = (await import("./client-3MGIP57D.js")).getDb();
|
|
7476
7622
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
7477
7623
|
const e = normalizeEmail2(String(args.email || ""));
|
|
7478
7624
|
if (!e) return "email is required.";
|
|
@@ -8559,7 +8705,7 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
8559
8705
|
import { Hono as Hono18 } from "hono";
|
|
8560
8706
|
import { v4 as uuid13 } from "uuid";
|
|
8561
8707
|
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
8562
|
-
import { join as
|
|
8708
|
+
import { join as join5 } from "path";
|
|
8563
8709
|
var assetRoutes = new Hono18();
|
|
8564
8710
|
var CONTENT_TYPES = {
|
|
8565
8711
|
png: "image/png",
|
|
@@ -8572,7 +8718,7 @@ var CONTENT_TYPES = {
|
|
|
8572
8718
|
};
|
|
8573
8719
|
var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm"]);
|
|
8574
8720
|
var IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
8575
|
-
var VIDEO_MAX_BYTES =
|
|
8721
|
+
var VIDEO_MAX_BYTES = config.VIDEO_MAX_BYTES;
|
|
8576
8722
|
var ASSET_MAX_REQUEST_BYTES = VIDEO_MAX_BYTES + 1024 * 1024;
|
|
8577
8723
|
var SAFE_NAME = /^[0-9a-f-]{36}\.(png|jpe?g|gif|webp|mp4|webm)$/;
|
|
8578
8724
|
assetRoutes.post("/", async (c) => {
|
|
@@ -8607,9 +8753,9 @@ assetRoutes.post("/", async (c) => {
|
|
|
8607
8753
|
);
|
|
8608
8754
|
}
|
|
8609
8755
|
const name = `${uuid13()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
8610
|
-
const dir =
|
|
8756
|
+
const dir = join5(resolveNestPath(nestId), "assets");
|
|
8611
8757
|
await mkdir2(dir, { recursive: true });
|
|
8612
|
-
await writeFile(
|
|
8758
|
+
await writeFile(join5(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
8613
8759
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
8614
8760
|
return c.json(
|
|
8615
8761
|
{
|
|
@@ -8631,7 +8777,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
8631
8777
|
}
|
|
8632
8778
|
let bytes;
|
|
8633
8779
|
try {
|
|
8634
|
-
bytes = await readFile2(
|
|
8780
|
+
bytes = await readFile2(join5(resolveNestPath(nestId), "assets", name));
|
|
8635
8781
|
} catch {
|
|
8636
8782
|
throw new NotFoundError("Asset not found");
|
|
8637
8783
|
}
|
|
@@ -9386,8 +9532,8 @@ function rowToComment(row) {
|
|
|
9386
9532
|
}
|
|
9387
9533
|
|
|
9388
9534
|
// src/governance/stewards-parser.ts
|
|
9389
|
-
import { readFileSync as
|
|
9390
|
-
import { join as
|
|
9535
|
+
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
9536
|
+
import { join as join6 } from "path";
|
|
9391
9537
|
function parseStewardsYaml(content) {
|
|
9392
9538
|
const result = { version: 1 };
|
|
9393
9539
|
const lines = content.split("\n");
|
|
@@ -9453,13 +9599,13 @@ function parseEntry(str) {
|
|
|
9453
9599
|
function loadStewardsConfig(nestId) {
|
|
9454
9600
|
const nestPath = resolveNestPath(nestId);
|
|
9455
9601
|
const candidates = [
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9602
|
+
join6(nestPath, "stewards.yaml"),
|
|
9603
|
+
join6(nestPath, "stewards.yml"),
|
|
9604
|
+
join6(nestPath, ".context", "stewards.yaml")
|
|
9459
9605
|
];
|
|
9460
9606
|
for (const candidatePath of candidates) {
|
|
9461
9607
|
if (existsSync(candidatePath)) {
|
|
9462
|
-
const content =
|
|
9608
|
+
const content = readFileSync3(candidatePath, "utf-8");
|
|
9463
9609
|
return parseStewardsYaml(content);
|
|
9464
9610
|
}
|
|
9465
9611
|
}
|
|
@@ -9515,7 +9661,8 @@ governanceRoutes.post("/stewards", async (c) => {
|
|
|
9515
9661
|
documentId: body.documentId,
|
|
9516
9662
|
tagName: body.tagName,
|
|
9517
9663
|
users: body.users,
|
|
9518
|
-
assignedBy
|
|
9664
|
+
assignedBy,
|
|
9665
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
9519
9666
|
});
|
|
9520
9667
|
return c.json({ stewards: created2 }, 201);
|
|
9521
9668
|
}
|
|
@@ -9533,7 +9680,8 @@ governanceRoutes.post("/stewards", async (c) => {
|
|
|
9533
9680
|
role: body.role
|
|
9534
9681
|
}
|
|
9535
9682
|
],
|
|
9536
|
-
assignedBy
|
|
9683
|
+
assignedBy,
|
|
9684
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
9537
9685
|
});
|
|
9538
9686
|
return c.json({ steward: created[0] }, 201);
|
|
9539
9687
|
});
|
|
@@ -9852,7 +10000,6 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
|
|
|
9852
10000
|
const request = await approve({
|
|
9853
10001
|
nestId,
|
|
9854
10002
|
nodeId,
|
|
9855
|
-
version: await getCurrentVersion(nestId, nodeId),
|
|
9856
10003
|
approvedBy: userEmail,
|
|
9857
10004
|
note: body.note,
|
|
9858
10005
|
override: body.override && isAdmin,
|
|
@@ -9875,7 +10022,6 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
9875
10022
|
const request = await reject({
|
|
9876
10023
|
nestId,
|
|
9877
10024
|
nodeId,
|
|
9878
|
-
version: await getCurrentVersion(nestId, nodeId),
|
|
9879
10025
|
rejectedBy: userEmail,
|
|
9880
10026
|
note: body.note,
|
|
9881
10027
|
baseUrl: requestBaseUrl(c.req.url)
|
|
@@ -9968,19 +10114,19 @@ async function ensureAnonymousUser() {
|
|
|
9968
10114
|
// src/app.ts
|
|
9969
10115
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
9970
10116
|
import { fileURLToPath } from "url";
|
|
9971
|
-
import { dirname as dirname2, join as
|
|
9972
|
-
import { existsSync as existsSync2, readFileSync as
|
|
10117
|
+
import { dirname as dirname2, join as join7, relative as relative2 } from "path";
|
|
10118
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
9973
10119
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
9974
10120
|
var SERVICE_VERSION = (() => {
|
|
9975
10121
|
try {
|
|
9976
|
-
return JSON.parse(
|
|
10122
|
+
return JSON.parse(readFileSync4(join7(HERE, "..", "package.json"), "utf8")).version;
|
|
9977
10123
|
} catch {
|
|
9978
10124
|
return "unknown";
|
|
9979
10125
|
}
|
|
9980
10126
|
})();
|
|
9981
10127
|
var UI_DIR_CANDIDATES = [
|
|
9982
|
-
|
|
9983
|
-
|
|
10128
|
+
join7(HERE, "web3"),
|
|
10129
|
+
join7(process.cwd(), "dist", "web3")
|
|
9984
10130
|
];
|
|
9985
10131
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
9986
10132
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
@@ -10016,6 +10162,22 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
10016
10162
|
}
|
|
10017
10163
|
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
10018
10164
|
});
|
|
10165
|
+
var APPROVE_CACHE_TTL_MS = 3e4;
|
|
10166
|
+
var approveCache = /* @__PURE__ */ new Map();
|
|
10167
|
+
async function canApproveInNest(nestId, email) {
|
|
10168
|
+
const key = `${nestId} ${email.toLowerCase()}`;
|
|
10169
|
+
const now = Date.now();
|
|
10170
|
+
const hit = approveCache.get(key);
|
|
10171
|
+
if (hit && hit.exp > now) return hit.ok;
|
|
10172
|
+
const roles = await resolveUserRoles(nestId, email);
|
|
10173
|
+
let ok = roles.includes("admin") || roles.includes("reviewer");
|
|
10174
|
+
if (!ok && roles.includes("owner")) ok = await nestAllowsSelfApprove(nestId);
|
|
10175
|
+
if (approveCache.size > 500) {
|
|
10176
|
+
for (const [k, v] of approveCache) if (v.exp <= now) approveCache.delete(k);
|
|
10177
|
+
}
|
|
10178
|
+
approveCache.set(key, { ok, exp: now + APPROVE_CACHE_TTL_MS });
|
|
10179
|
+
return ok;
|
|
10180
|
+
}
|
|
10019
10181
|
function createApp() {
|
|
10020
10182
|
const app = new Hono21({ router: new LinearRouter() });
|
|
10021
10183
|
const corsOrigins = config.CORS_ORIGINS;
|
|
@@ -10185,6 +10347,7 @@ function createApp() {
|
|
|
10185
10347
|
});
|
|
10186
10348
|
const serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
10187
10349
|
app.use("/admin/settings", flexAuthMiddleware);
|
|
10350
|
+
app.use("/admin/settings/*", flexAuthMiddleware);
|
|
10188
10351
|
const currentServerSettings = () => ({
|
|
10189
10352
|
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
10190
10353
|
manual_sign_in: config.MANUAL_SIGN_IN,
|
|
@@ -10217,6 +10380,7 @@ function createApp() {
|
|
|
10217
10380
|
// Server-wide runner default. NEVER the value — masked tail only.
|
|
10218
10381
|
anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
|
|
10219
10382
|
slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
|
|
10383
|
+
msteams_webhook_url: config.MSTEAMS_WEBHOOK_URL ?? "",
|
|
10220
10384
|
smtp_url: config.SMTP_URL ?? "",
|
|
10221
10385
|
notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
|
|
10222
10386
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
@@ -10226,6 +10390,20 @@ function createApp() {
|
|
|
10226
10390
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
10227
10391
|
return c.json(currentServerSettings());
|
|
10228
10392
|
});
|
|
10393
|
+
app.post("/admin/settings/test-notification", async (c) => {
|
|
10394
|
+
if (!await serverAdminAllowed(c))
|
|
10395
|
+
return c.json({ error: "Only the server admin can do this." }, 403);
|
|
10396
|
+
let body;
|
|
10397
|
+
try {
|
|
10398
|
+
body = await c.req.json();
|
|
10399
|
+
} catch {
|
|
10400
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
10401
|
+
}
|
|
10402
|
+
if (body.channel !== "slack" && body.channel !== "teams")
|
|
10403
|
+
return c.json({ error: "channel must be 'slack' or 'teams'" }, 400);
|
|
10404
|
+
const result = await sendTestNotification(body.channel);
|
|
10405
|
+
return result.ok ? c.json({ ok: true }) : c.json({ error: result.error }, 502);
|
|
10406
|
+
});
|
|
10229
10407
|
app.patch("/admin/settings", async (c) => {
|
|
10230
10408
|
if (!await serverAdminAllowed(c))
|
|
10231
10409
|
return c.json({ error: "Only the server admin can change this." }, 403);
|
|
@@ -10418,6 +10596,12 @@ function createApp() {
|
|
|
10418
10596
|
addError("slack_webhook_url", "Slack webhook must be an https:// URL (or leave it empty to turn Slack notifications off).");
|
|
10419
10597
|
else pending.push({ name: "SLACK_WEBHOOK_URL", value: v || null });
|
|
10420
10598
|
}
|
|
10599
|
+
if ("msteams_webhook_url" in body) {
|
|
10600
|
+
const v = String(body.msteams_webhook_url ?? "").trim();
|
|
10601
|
+
if (v && !/^https:\/\//i.test(v))
|
|
10602
|
+
addError("msteams_webhook_url", "Microsoft Teams webhook must be an https:// URL (or leave it empty to turn Teams notifications off).");
|
|
10603
|
+
else pending.push({ name: "MSTEAMS_WEBHOOK_URL", value: v || null });
|
|
10604
|
+
}
|
|
10421
10605
|
if ("smtp_url" in body) {
|
|
10422
10606
|
const v = String(body.smtp_url ?? "").trim();
|
|
10423
10607
|
if (v && !/^smtps?:\/\//i.test(v))
|
|
@@ -10633,12 +10817,16 @@ function createApp() {
|
|
|
10633
10817
|
const userId = c.get("userId");
|
|
10634
10818
|
const me = await resolveCallerEmail(userId);
|
|
10635
10819
|
const nestName2 = /* @__PURE__ */ new Map();
|
|
10636
|
-
for (const n of
|
|
10637
|
-
...await listNests(userId),
|
|
10638
|
-
...await listSharedNests(userId)
|
|
10639
|
-
])
|
|
10820
|
+
for (const n of await listVisibleNests(userId))
|
|
10640
10821
|
nestName2.set(n.id, n.name ?? n.id);
|
|
10641
|
-
const
|
|
10822
|
+
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 100);
|
|
10823
|
+
const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
|
|
10824
|
+
const { requests, total } = await getReviewQueue({
|
|
10825
|
+
status: "pending",
|
|
10826
|
+
nestIds: [...nestName2.keys()],
|
|
10827
|
+
limit,
|
|
10828
|
+
offset
|
|
10829
|
+
});
|
|
10642
10830
|
const toItem = (r) => ({
|
|
10643
10831
|
type: "review",
|
|
10644
10832
|
nest_id: r.nestId,
|
|
@@ -10649,12 +10837,29 @@ function createApp() {
|
|
|
10649
10837
|
requested_at: r.requestedAt,
|
|
10650
10838
|
priority: r.priority
|
|
10651
10839
|
});
|
|
10652
|
-
const mine = requests.filter((r) => nestName2.has(r.nestId));
|
|
10653
10840
|
const byNewest = (a, b) => a.requested_at < b.requested_at ? 1 : -1;
|
|
10654
|
-
const
|
|
10655
|
-
const
|
|
10841
|
+
const others = requests.filter((r) => r.requestedBy !== me);
|
|
10842
|
+
const nestApprove = new Map(
|
|
10843
|
+
await Promise.all(
|
|
10844
|
+
[...new Set(others.map((r) => r.nestId))].map(
|
|
10845
|
+
async (nestId) => [
|
|
10846
|
+
nestId,
|
|
10847
|
+
await canApproveInNest(nestId, me)
|
|
10848
|
+
]
|
|
10849
|
+
)
|
|
10850
|
+
)
|
|
10851
|
+
);
|
|
10852
|
+
const items = others.filter((r) => nestApprove.get(r.nestId)).map(toItem).sort(byNewest);
|
|
10853
|
+
const waiting = requests.filter((r) => r.requestedBy === me).map(toItem).sort(byNewest);
|
|
10656
10854
|
const notifications = await listNotifications(me, { unreadOnly: true, limit: 50 });
|
|
10657
|
-
return c.json({
|
|
10855
|
+
return c.json({
|
|
10856
|
+
items,
|
|
10857
|
+
waiting,
|
|
10858
|
+
notifications,
|
|
10859
|
+
// total = all pending across visible nests (both buckets); has_more drives
|
|
10860
|
+
// the "Load more" control on the client.
|
|
10861
|
+
pagination: { total, limit, offset, has_more: offset + requests.length < total }
|
|
10862
|
+
});
|
|
10658
10863
|
});
|
|
10659
10864
|
app.get("/me/notifications", async (c) => {
|
|
10660
10865
|
const me = await resolveCallerEmail(c.get("userId"));
|
|
@@ -10672,9 +10877,7 @@ function createApp() {
|
|
|
10672
10877
|
const db = getDb();
|
|
10673
10878
|
const userId = c.get("userId");
|
|
10674
10879
|
const userEmail = await resolveCallerEmail(userId);
|
|
10675
|
-
const
|
|
10676
|
-
const sharedNests = await listSharedNests(userId);
|
|
10677
|
-
const visibleNests = [...owned, ...sharedNests];
|
|
10880
|
+
const visibleNests = await listVisibleNests(userId);
|
|
10678
10881
|
let documents = 0;
|
|
10679
10882
|
for (const nest of visibleNests) {
|
|
10680
10883
|
try {
|