@promptowl/contextnest-community 1.22.0 → 1.24.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/API.md +2092 -0
- package/CONFIGURATION.md +5 -2
- package/README.md +62 -10
- package/STEWARDSHIP.md +229 -0
- package/dist/{chunk-PX3P4FTU.js → chunk-2TPQTN4Y.js} +5 -4
- package/dist/{chunk-OQXZ43HG.js → chunk-5EZOPA47.js} +40 -10
- package/dist/{chunk-MY4JIWQD.js → chunk-I3CSD6CK.js} +1 -1
- package/dist/{chunk-W5ILNGPD.js → chunk-KIAAEHWL.js} +80 -28
- package/dist/{chunk-J2OQ3MEB.js → chunk-LA3VTQ22.js} +63 -1
- package/dist/{chunk-ZAV4QUZA.js → chunk-XUIWAWDO.js} +28 -33
- package/dist/{chunk-6WESEA75.js → chunk-ZTT4U4NE.js} +2 -2
- package/dist/{client-CSCHXUNX.js → client-KEY4PYJH.js} +1 -1
- package/dist/{engine-XKRP7GOQ.js → engine-S3QBQ7LH.js} +2 -2
- package/dist/{external-edit-service-TXW7SXSF.js → external-edit-service-6FNFPOJJ.js} +3 -3
- package/dist/{grants-service-UZ2MGDO5.js → grants-service-UT3EQ3R7.js} +2 -2
- package/dist/index.js +1261 -305
- package/dist/{migrations.postgres-KKICIH2C.js → migrations.postgres-AIQ7WSU7.js} +60 -4
- package/dist/{review-service-XFWTENTN.js → review-service-HHGOTO6P.js} +6 -6
- package/dist/{stewardship-service-5HBQGDMK.js → stewardship-service-4DHNRULG.js} +3 -3
- package/dist/{version-service-TKNYJMCM.js → version-service-KIOQME6U.js} +3 -3
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
- package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
- package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
- package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
- package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
- package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
- package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
- package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
- package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
- package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
- package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
- package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
- package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
- package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
- package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
- package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
- package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
- package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
- package/dist/web3/assets/index-BM-h3DwI.css +1 -0
- package/dist/web3/assets/index-C1wTSjfP.js +29 -0
- package/dist/web3/assets/index-EaX2yql0.js +389 -0
- package/dist/web3/assets/page-B3yvQvHy.js +1 -0
- package/dist/web3/assets/page-BExMvttJ.js +1 -0
- package/dist/web3/assets/page-BUOADv2X.js +1 -0
- package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
- package/dist/web3/assets/page-Bicf02Cz.js +1 -0
- package/dist/web3/assets/page-BsPMDm5d.js +45 -0
- package/dist/web3/assets/page-ByyVLDVo.js +16 -0
- package/dist/web3/assets/page-CC0NJi8v.js +1 -0
- package/dist/web3/assets/page-CHb0F5hV.js +24 -0
- package/dist/web3/assets/page-CPjWQNKx.js +11 -0
- package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
- package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
- package/dist/web3/assets/page-CtaW65El.js +1 -0
- package/dist/web3/assets/page-DGOL9l8i.js +6 -0
- package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
- package/dist/web3/assets/play-C5YNKpAH.js +6 -0
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
- package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
- package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
- package/dist/web3/assets/select-DBFTrUmI.js +6 -0
- package/dist/web3/assets/send-0CHvmluT.js +6 -0
- package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
- package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
- package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
- package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
- package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
- package/dist/web3/assets/x-By7piikG.js +6 -0
- package/dist/web3/assets/zap-C2T8riBp.js +11 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -2
- package/dist/web3/assets/index-C2hW_9j9.js +0 -1382
- package/dist/web3/assets/index-T4pilFNL.css +0 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getPendingReview,
|
|
19
19
|
getReviewHistory,
|
|
20
20
|
getReviewQueue,
|
|
21
|
+
hasAnyConnector,
|
|
21
22
|
isNestTarget,
|
|
22
23
|
isSafeConnectorUrl,
|
|
23
24
|
listDeletionRequests,
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
submitForReview,
|
|
38
39
|
withNodeWriteLock,
|
|
39
40
|
withdrawDeletionRequest
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-5EZOPA47.js";
|
|
41
42
|
import {
|
|
42
43
|
canCreateInNest,
|
|
43
44
|
canManageStewards,
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
resolveUserRoles,
|
|
58
59
|
syncFromConfig,
|
|
59
60
|
updateSteward
|
|
60
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-2TPQTN4Y.js";
|
|
61
62
|
import {
|
|
62
63
|
createGrant,
|
|
63
64
|
deleteGrant,
|
|
@@ -66,7 +67,7 @@ import {
|
|
|
66
67
|
listGrants,
|
|
67
68
|
listUserGrants,
|
|
68
69
|
resolveNodeGrant
|
|
69
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-I3CSD6CK.js";
|
|
70
71
|
import {
|
|
71
72
|
generateApiKey,
|
|
72
73
|
getKeyPrefix,
|
|
@@ -92,7 +93,7 @@ import {
|
|
|
92
93
|
listMyDrafts,
|
|
93
94
|
setApprovedVersion,
|
|
94
95
|
upsertVersion
|
|
95
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-ZTT4U4NE.js";
|
|
96
97
|
import {
|
|
97
98
|
approveExternalEdit,
|
|
98
99
|
getExternalEditDetail,
|
|
@@ -104,12 +105,13 @@ import {
|
|
|
104
105
|
scanDocumentForDrift,
|
|
105
106
|
scanNestForDrift,
|
|
106
107
|
startDriftScanner
|
|
107
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-XUIWAWDO.js";
|
|
108
109
|
import {
|
|
109
110
|
addMember,
|
|
110
111
|
addWatcher,
|
|
111
112
|
archiveNest,
|
|
112
113
|
assertSafeNodeId,
|
|
114
|
+
buildDocContext,
|
|
113
115
|
buildTitleMap,
|
|
114
116
|
canApproveWith,
|
|
115
117
|
canEditWith,
|
|
@@ -119,7 +121,6 @@ import {
|
|
|
119
121
|
createTeam,
|
|
120
122
|
deleteNest,
|
|
121
123
|
deleteTeam,
|
|
122
|
-
describeNode,
|
|
123
124
|
disableStewardshipAndWipeGovernance,
|
|
124
125
|
docLink,
|
|
125
126
|
engineApi,
|
|
@@ -169,6 +170,7 @@ import {
|
|
|
169
170
|
normalizeTag,
|
|
170
171
|
notifyNestInvite,
|
|
171
172
|
nowExpr,
|
|
173
|
+
nowMsExpr,
|
|
172
174
|
opContext,
|
|
173
175
|
parseSharedTeams,
|
|
174
176
|
permissionLevel,
|
|
@@ -200,6 +202,7 @@ import {
|
|
|
200
202
|
shareTeamWithNest,
|
|
201
203
|
startLicenseSafetyPoll,
|
|
202
204
|
startTelemetryLoop,
|
|
205
|
+
suffixOf,
|
|
203
206
|
syncNodeTags,
|
|
204
207
|
trackEvent,
|
|
205
208
|
uniqueNestName,
|
|
@@ -208,7 +211,7 @@ import {
|
|
|
208
211
|
updateMemberRole,
|
|
209
212
|
validateLicense,
|
|
210
213
|
verifySmtp
|
|
211
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-KIAAEHWL.js";
|
|
212
215
|
import {
|
|
213
216
|
AppError,
|
|
214
217
|
ConflictError,
|
|
@@ -226,7 +229,7 @@ import {
|
|
|
226
229
|
initDb,
|
|
227
230
|
isEmailListish,
|
|
228
231
|
isEmailish
|
|
229
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-LA3VTQ22.js";
|
|
230
233
|
import {
|
|
231
234
|
ANON_EMAIL,
|
|
232
235
|
ANON_USER_ID,
|
|
@@ -240,7 +243,7 @@ import {
|
|
|
240
243
|
import { serve } from "@hono/node-server";
|
|
241
244
|
|
|
242
245
|
// src/app.ts
|
|
243
|
-
import { Hono as
|
|
246
|
+
import { Hono as Hono25 } from "hono";
|
|
244
247
|
import { LinearRouter } from "hono/router/linear-router";
|
|
245
248
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
246
249
|
import { cors } from "hono/cors";
|
|
@@ -270,7 +273,7 @@ function applyArtifactSandboxHeaders(c) {
|
|
|
270
273
|
|
|
271
274
|
// src/auth/routes.ts
|
|
272
275
|
import { Hono as Hono2 } from "hono";
|
|
273
|
-
import { v4 as
|
|
276
|
+
import { v4 as uuid4 } from "uuid";
|
|
274
277
|
|
|
275
278
|
// src/auth/middleware.ts
|
|
276
279
|
import { createMiddleware } from "hono/factory";
|
|
@@ -565,10 +568,89 @@ function verifySsoTicket(token, secret, nowSec = Math.floor(Date.now() / 1e3)) {
|
|
|
565
568
|
return claims;
|
|
566
569
|
}
|
|
567
570
|
|
|
571
|
+
// src/auth/community-sites.ts
|
|
572
|
+
import { v4 as uuid } from "uuid";
|
|
573
|
+
function rowToSite(row) {
|
|
574
|
+
return { ...row, is_active: !!row.is_active };
|
|
575
|
+
}
|
|
576
|
+
var SELECT_COLUMNS = "id, name, url, secret, is_active, created_at, updated_at";
|
|
577
|
+
var activeSitesCache = [];
|
|
578
|
+
async function loadActiveCommunitySites() {
|
|
579
|
+
const rows = await getDb().all(
|
|
580
|
+
`SELECT ${SELECT_COLUMNS} FROM community_sites WHERE is_active = 1 ORDER BY name`
|
|
581
|
+
);
|
|
582
|
+
activeSitesCache = rows.map(rowToSite);
|
|
583
|
+
}
|
|
584
|
+
function getActiveCommunitySites() {
|
|
585
|
+
return activeSitesCache;
|
|
586
|
+
}
|
|
587
|
+
async function listCommunitySites() {
|
|
588
|
+
const rows = await getDb().all(
|
|
589
|
+
`SELECT ${SELECT_COLUMNS} FROM community_sites ORDER BY name`
|
|
590
|
+
);
|
|
591
|
+
return rows.map(rowToSite);
|
|
592
|
+
}
|
|
593
|
+
async function getCommunitySite(id) {
|
|
594
|
+
const row = await getDb().get(
|
|
595
|
+
`SELECT ${SELECT_COLUMNS} FROM community_sites WHERE id = ?`,
|
|
596
|
+
[id]
|
|
597
|
+
);
|
|
598
|
+
return row ? rowToSite(row) : null;
|
|
599
|
+
}
|
|
600
|
+
async function createCommunitySite(input) {
|
|
601
|
+
const id = uuid();
|
|
602
|
+
await getDb().run(
|
|
603
|
+
`INSERT INTO community_sites (id, name, url, secret, is_active) VALUES (?, ?, ?, ?, ?)`,
|
|
604
|
+
[id, input.name, input.url, input.secret, input.is_active ? 1 : 0]
|
|
605
|
+
);
|
|
606
|
+
const site = await getCommunitySite(id);
|
|
607
|
+
if (!site) throw new Error("Failed to create community site");
|
|
608
|
+
return site;
|
|
609
|
+
}
|
|
610
|
+
async function updateCommunitySite(id, patch) {
|
|
611
|
+
const existing = await getCommunitySite(id);
|
|
612
|
+
if (!existing) return null;
|
|
613
|
+
const next = {
|
|
614
|
+
name: patch.name ?? existing.name,
|
|
615
|
+
url: patch.url ?? existing.url,
|
|
616
|
+
secret: patch.secret ?? existing.secret,
|
|
617
|
+
is_active: patch.is_active ?? existing.is_active
|
|
618
|
+
};
|
|
619
|
+
const db = getDb();
|
|
620
|
+
await db.run(
|
|
621
|
+
`UPDATE community_sites
|
|
622
|
+
SET name = ?, url = ?, secret = ?, is_active = ?, updated_at = ${nowExpr(db)}
|
|
623
|
+
WHERE id = ?`,
|
|
624
|
+
[next.name, next.url, next.secret, next.is_active ? 1 : 0, id]
|
|
625
|
+
);
|
|
626
|
+
return getCommunitySite(id);
|
|
627
|
+
}
|
|
628
|
+
async function deleteCommunitySite(id) {
|
|
629
|
+
const result = await getDb().run("DELETE FROM community_sites WHERE id = ?", [
|
|
630
|
+
id
|
|
631
|
+
]);
|
|
632
|
+
return result.changes > 0;
|
|
633
|
+
}
|
|
634
|
+
function legacyCandidate() {
|
|
635
|
+
const secret = config.OFFICIAL_COMMUNITY_SSO_SECRET;
|
|
636
|
+
if (!secret) return null;
|
|
637
|
+
return { id: "legacy", label: "legacy", secret, url: config.PUBLIC_BASE_URL };
|
|
638
|
+
}
|
|
639
|
+
function ssoCandidates() {
|
|
640
|
+
const fromDb = getActiveCommunitySites().map((s) => ({
|
|
641
|
+
id: s.id,
|
|
642
|
+
label: s.name,
|
|
643
|
+
secret: s.secret,
|
|
644
|
+
url: s.url.replace(/\/$/, "")
|
|
645
|
+
}));
|
|
646
|
+
const legacy = legacyCandidate();
|
|
647
|
+
return legacy ? [...fromDb, legacy] : fromDb;
|
|
648
|
+
}
|
|
649
|
+
|
|
568
650
|
// src/auth/oidc.ts
|
|
569
651
|
import { Hono } from "hono";
|
|
570
652
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
571
|
-
import { v4 as
|
|
653
|
+
import { v4 as uuid2 } from "uuid";
|
|
572
654
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
573
655
|
|
|
574
656
|
// src/shared/base-url.ts
|
|
@@ -824,8 +906,8 @@ oidcRoutes.get("/callback", async (c) => {
|
|
|
824
906
|
);
|
|
825
907
|
if (!user) {
|
|
826
908
|
if (!config.OIDC_AUTO_PROVISION) return bounce("not_invited");
|
|
827
|
-
const userId =
|
|
828
|
-
const placeholderHash = await hashPassword(
|
|
909
|
+
const userId = uuid2();
|
|
910
|
+
const placeholderHash = await hashPassword(uuid2());
|
|
829
911
|
try {
|
|
830
912
|
await db.run(
|
|
831
913
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
@@ -897,7 +979,7 @@ oidcRoutes.get("/logout", async (c) => {
|
|
|
897
979
|
});
|
|
898
980
|
|
|
899
981
|
// src/auth/token-exchange.ts
|
|
900
|
-
import { v4 as
|
|
982
|
+
import { v4 as uuid3 } from "uuid";
|
|
901
983
|
import { jwtVerify as jwtVerify2 } from "jose";
|
|
902
984
|
var TX_LIMIT = { max: 60, windowMs: 15 * 6e4 };
|
|
903
985
|
var ExchangeError = class extends Error {
|
|
@@ -984,8 +1066,8 @@ async function resolveUser(email, name, department) {
|
|
|
984
1066
|
if (!config.OIDC_AUTO_PROVISION || config.OIDC_ALLOWED_DOMAINS.length === 0) {
|
|
985
1067
|
throw new ExchangeError("no_account", 403);
|
|
986
1068
|
}
|
|
987
|
-
const userId =
|
|
988
|
-
const placeholderHash = await hashPassword(
|
|
1069
|
+
const userId = uuid3();
|
|
1070
|
+
const placeholderHash = await hashPassword(uuid3());
|
|
989
1071
|
try {
|
|
990
1072
|
await db.run(
|
|
991
1073
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
@@ -1161,8 +1243,8 @@ async function provisionPromptowlUser(c, rawEmail, rawName) {
|
|
|
1161
1243
|
[meEmail]
|
|
1162
1244
|
);
|
|
1163
1245
|
if (!user) {
|
|
1164
|
-
const userId =
|
|
1165
|
-
const placeholderHash = await hashPassword(
|
|
1246
|
+
const userId = uuid4();
|
|
1247
|
+
const placeholderHash = await hashPassword(uuid4());
|
|
1166
1248
|
await db.run(
|
|
1167
1249
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
1168
1250
|
[userId, meEmail, rawName || null, placeholderHash]
|
|
@@ -1235,7 +1317,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1235
1317
|
}
|
|
1236
1318
|
throw new ValidationError("Email already registered");
|
|
1237
1319
|
}
|
|
1238
|
-
const userId =
|
|
1320
|
+
const userId = uuid4();
|
|
1239
1321
|
const passwordHash = await hashPassword(body.password);
|
|
1240
1322
|
await db.run(
|
|
1241
1323
|
"INSERT INTO users (id, email, name, password_hash) VALUES (?, ?, ?, ?)",
|
|
@@ -1337,7 +1419,7 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1337
1419
|
});
|
|
1338
1420
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1339
1421
|
const apiKey = generateApiKey();
|
|
1340
|
-
const keyId =
|
|
1422
|
+
const keyId = uuid4();
|
|
1341
1423
|
await db.run(
|
|
1342
1424
|
"INSERT INTO api_keys (id, user_id, key_hash, key_prefix, nest_id, label) VALUES (?, ?, ?, ?, ?, ?)",
|
|
1343
1425
|
[
|
|
@@ -1366,7 +1448,7 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1366
1448
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1367
1449
|
const prior = check.key;
|
|
1368
1450
|
const apiKey = generateApiKey();
|
|
1369
|
-
const keyId =
|
|
1451
|
+
const keyId = uuid4();
|
|
1370
1452
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
1371
1453
|
const replaced = await db.transaction(async (tx) => {
|
|
1372
1454
|
const removed = await tx.run(
|
|
@@ -1520,25 +1602,30 @@ authRoutes.post("/promptowl", async (c) => {
|
|
|
1520
1602
|
});
|
|
1521
1603
|
authRoutes.get("/sso", async (c) => {
|
|
1522
1604
|
const ssoError = (code) => c.redirect(`/?sso_error=${code}`, 302);
|
|
1523
|
-
const
|
|
1524
|
-
if (
|
|
1605
|
+
const candidates = ssoCandidates();
|
|
1606
|
+
if (candidates.length === 0) return ssoError("not_supported");
|
|
1525
1607
|
if (!tryConsume(`sso:ip:${clientIp(c)}`, DEVICE_LIMIT)) {
|
|
1526
1608
|
return ssoError("rate_limited");
|
|
1527
1609
|
}
|
|
1528
1610
|
const ticket = c.req.query("ticket");
|
|
1529
1611
|
if (!ticket) return ssoError("missing_ticket");
|
|
1530
1612
|
let claims;
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1613
|
+
let matched;
|
|
1614
|
+
for (const candidate of candidates) {
|
|
1615
|
+
try {
|
|
1616
|
+
claims = verifySsoTicket(ticket, candidate.secret);
|
|
1617
|
+
matched = candidate;
|
|
1618
|
+
break;
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1535
1621
|
}
|
|
1622
|
+
if (!claims || !matched) return ssoError("invalid_ticket");
|
|
1536
1623
|
if ((claims.scope || "") === "mcp") return ssoError("invalid_ticket");
|
|
1537
1624
|
const expectedAud = config.PUBLIC_BASE_URL;
|
|
1538
1625
|
if (!expectedAud && !warnedMissingSsoAudience) {
|
|
1539
1626
|
warnedMissingSsoAudience = true;
|
|
1540
1627
|
console.warn(
|
|
1541
|
-
|
|
1628
|
+
`[sso] site "${matched.label}" has a secret but PUBLIC_BASE_URL is unset \u2014 ticket audience binding is DISABLED. Set PUBLIC_BASE_URL to this deployment's own URL so a ticket minted for another deployment cannot be replayed against this one.`
|
|
1542
1629
|
);
|
|
1543
1630
|
}
|
|
1544
1631
|
if (expectedAud) {
|
|
@@ -1609,6 +1696,15 @@ authRoutes.get("/admin-status", async (c) => {
|
|
|
1609
1696
|
me
|
|
1610
1697
|
});
|
|
1611
1698
|
});
|
|
1699
|
+
authRoutes.patch("/profile", authMiddleware, async (c) => {
|
|
1700
|
+
const userId = c.get("userId");
|
|
1701
|
+
const body = await c.req.json();
|
|
1702
|
+
if (typeof body.name !== "string") throw new ValidationError("name is required");
|
|
1703
|
+
const name = body.name.trim();
|
|
1704
|
+
if (name.length > 80) throw new ValidationError("name must be 80 characters or fewer");
|
|
1705
|
+
await getDb().run("UPDATE users SET name = ? WHERE id = ?", [name || null, userId]);
|
|
1706
|
+
return c.json({ ok: true, name: name || null });
|
|
1707
|
+
});
|
|
1612
1708
|
authRoutes.post("/password", authMiddleware, async (c) => {
|
|
1613
1709
|
const body = await c.req.json();
|
|
1614
1710
|
if (!body.current || !body.next) {
|
|
@@ -1674,7 +1770,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
1674
1770
|
if (supplied !== void 0) {
|
|
1675
1771
|
assertValidPassword(supplied);
|
|
1676
1772
|
}
|
|
1677
|
-
const generated = supplied ? null :
|
|
1773
|
+
const generated = supplied ? null : uuid4().replace(/-/g, "").slice(0, 16);
|
|
1678
1774
|
const newPassword = supplied ?? generated;
|
|
1679
1775
|
const newHash = await hashPassword(newPassword);
|
|
1680
1776
|
await db.transaction(async (tx) => {
|
|
@@ -1789,7 +1885,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1789
1885
|
409
|
|
1790
1886
|
);
|
|
1791
1887
|
}
|
|
1792
|
-
const tempPassword =
|
|
1888
|
+
const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
|
|
1793
1889
|
const passwordHash = await hashPassword(tempPassword);
|
|
1794
1890
|
let userId;
|
|
1795
1891
|
if (existing) {
|
|
@@ -1799,7 +1895,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1799
1895
|
userId
|
|
1800
1896
|
]);
|
|
1801
1897
|
} else {
|
|
1802
|
-
userId =
|
|
1898
|
+
userId = uuid4();
|
|
1803
1899
|
await db.run(
|
|
1804
1900
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1805
1901
|
[userId, email, null, passwordHash]
|
|
@@ -1939,7 +2035,43 @@ function documentPrimeFlag(metadata) {
|
|
|
1939
2035
|
}
|
|
1940
2036
|
|
|
1941
2037
|
// src/governance/mention-notify.ts
|
|
1942
|
-
import { v4 as
|
|
2038
|
+
import { v4 as uuid5 } from "uuid";
|
|
2039
|
+
|
|
2040
|
+
// src/shared/mentions.ts
|
|
2041
|
+
var MENTION = /(?:^|\s)@(?:\(([^)]+)\)|([a-zA-Z0-9._@-]+))/g;
|
|
2042
|
+
function stripCode(markdown2) {
|
|
2043
|
+
return markdown2.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
2044
|
+
}
|
|
2045
|
+
function extractMentions(markdown2) {
|
|
2046
|
+
const names = [];
|
|
2047
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2048
|
+
const text = stripCode(markdown2 || "");
|
|
2049
|
+
let match;
|
|
2050
|
+
MENTION.lastIndex = 0;
|
|
2051
|
+
while ((match = MENTION.exec(text)) !== null) {
|
|
2052
|
+
const name = (match[1] || match[2] || "").trim();
|
|
2053
|
+
const key = name.toLowerCase();
|
|
2054
|
+
if (!name || seen.has(key)) continue;
|
|
2055
|
+
seen.add(key);
|
|
2056
|
+
names.push(name);
|
|
2057
|
+
}
|
|
2058
|
+
return names;
|
|
2059
|
+
}
|
|
2060
|
+
function newMentions(previous, next) {
|
|
2061
|
+
const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
|
|
2062
|
+
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
2063
|
+
}
|
|
2064
|
+
var MAX_MENTION_RECIPIENTS = 25;
|
|
2065
|
+
function resolveMention(people, name) {
|
|
2066
|
+
const n = name.trim().toLowerCase();
|
|
2067
|
+
if (!n) return void 0;
|
|
2068
|
+
const exact = people.find((p) => p.email.toLowerCase() === n);
|
|
2069
|
+
if (exact) return exact;
|
|
2070
|
+
const matches = people.filter(
|
|
2071
|
+
(p) => p.email.toLowerCase().split("@")[0] === n || (p.name || "").toLowerCase() === n
|
|
2072
|
+
);
|
|
2073
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2074
|
+
}
|
|
1943
2075
|
|
|
1944
2076
|
// src/nests/people-service.ts
|
|
1945
2077
|
var PERSON_SOURCES = [
|
|
@@ -2003,40 +2135,129 @@ async function listNestPeople(nestId) {
|
|
|
2003
2135
|
}
|
|
2004
2136
|
return people;
|
|
2005
2137
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
const
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
while ((match = MENTION.exec(text)) !== null) {
|
|
2019
|
-
const name = (match[1] || match[2] || "").trim();
|
|
2020
|
-
const key = name.toLowerCase();
|
|
2021
|
-
if (!name || seen.has(key)) continue;
|
|
2022
|
-
seen.add(key);
|
|
2023
|
-
names.push(name);
|
|
2138
|
+
async function withDirectory(roster) {
|
|
2139
|
+
if (!config.PEOPLE_SUGGEST_ALL_USERS) return roster;
|
|
2140
|
+
const seen = new Set(roster.map((p) => p.email.toLowerCase()));
|
|
2141
|
+
const rows = await getDb().all(
|
|
2142
|
+
"SELECT id, email, name FROM users WHERE id != ? ORDER BY email LIMIT ?",
|
|
2143
|
+
[ANON_USER_ID, SUGGEST_FETCH]
|
|
2144
|
+
);
|
|
2145
|
+
const extra = [];
|
|
2146
|
+
for (const r of rows) {
|
|
2147
|
+
const email = (r.email || "").trim();
|
|
2148
|
+
if (!email || seen.has(email.toLowerCase())) continue;
|
|
2149
|
+
extra.push({ email, name: r.name ?? null, userId: r.id, source: "directory" });
|
|
2024
2150
|
}
|
|
2025
|
-
return
|
|
2151
|
+
return [...roster, ...extra];
|
|
2026
2152
|
}
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
2030
|
-
}
|
|
2031
|
-
function resolveMention(people, name) {
|
|
2153
|
+
async function findDirectoryPerson(name) {
|
|
2154
|
+
if (!config.PEOPLE_SUGGEST_ALL_USERS) return void 0;
|
|
2032
2155
|
const n = name.trim().toLowerCase();
|
|
2033
2156
|
if (!n) return void 0;
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2157
|
+
const rows = await getDb().all(
|
|
2158
|
+
`SELECT id, email, name FROM users
|
|
2159
|
+
WHERE id != ?
|
|
2160
|
+
AND (LOWER(email) = ? OR LOWER(email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(name, '')) = ?)`,
|
|
2161
|
+
[ANON_USER_ID, n, `${escapeLike(n)}@%`, n]
|
|
2162
|
+
);
|
|
2163
|
+
return resolveMention(
|
|
2164
|
+
rows.map((r) => ({ email: r.email, name: r.name ?? null, userId: r.id, source: "directory" })),
|
|
2165
|
+
name
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
var SUGGEST_LIMIT = 20;
|
|
2169
|
+
var SUGGEST_FETCH = 200;
|
|
2170
|
+
function matchesQuery(person, q) {
|
|
2171
|
+
if (!q) return true;
|
|
2172
|
+
return person.email.toLowerCase().includes(q) || (person.name || "").toLowerCase().includes(q);
|
|
2173
|
+
}
|
|
2174
|
+
var escapeLike = (s) => s.replace(/([\\%_])/g, "\\$1");
|
|
2175
|
+
function queryFilter(q) {
|
|
2176
|
+
if (!q) return { sql: "", args: [] };
|
|
2177
|
+
const pattern = `%${escapeLike(q)}%`;
|
|
2178
|
+
return {
|
|
2179
|
+
sql: " AND (LOWER(u.email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(u.name, '')) LIKE ? ESCAPE '\\')",
|
|
2180
|
+
args: [pattern, pattern]
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
async function suggestPeopleForUser(callerUserId, query = "", limit = SUGGEST_LIMIT) {
|
|
2184
|
+
const db = getDb();
|
|
2185
|
+
const q = query.trim().toLowerCase();
|
|
2186
|
+
const cap = Math.min(Math.max(limit, 1), SUGGEST_LIMIT);
|
|
2187
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
2188
|
+
callerUserId
|
|
2189
|
+
]);
|
|
2190
|
+
const callerEmail = (caller?.email || "").toLowerCase();
|
|
2191
|
+
const filter = queryFilter(q);
|
|
2192
|
+
const wholeDirectory = config.PEOPLE_SUGGEST_ALL_USERS || await isServerAdminUserId(callerUserId);
|
|
2193
|
+
const rows = await (async () => {
|
|
2194
|
+
if (wholeDirectory) {
|
|
2195
|
+
return await db.all(
|
|
2196
|
+
`SELECT u.id AS user_id, u.email AS email, u.name AS name, 'directory' AS source
|
|
2197
|
+
FROM users u
|
|
2198
|
+
WHERE u.id != ? AND u.id != ?${filter.sql}
|
|
2199
|
+
ORDER BY u.email
|
|
2200
|
+
LIMIT ?`,
|
|
2201
|
+
[callerUserId, ANON_USER_ID, ...filter.args, SUGGEST_FETCH]
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
const args = [callerUserId, ANON_USER_ID];
|
|
2205
|
+
const myNests = () => {
|
|
2206
|
+
args.push(callerUserId, callerUserId);
|
|
2207
|
+
return `SELECT id FROM nests WHERE user_id = ?
|
|
2208
|
+
UNION SELECT nest_id FROM nest_collaborators WHERE user_id = ?`;
|
|
2209
|
+
};
|
|
2210
|
+
const ownedNests = myNests();
|
|
2211
|
+
const collabNests = myNests();
|
|
2212
|
+
const stewardNests = myNests();
|
|
2213
|
+
const sql = `SELECT u.id AS user_id, u.email AS email, u.name AS name, 'nest' AS source
|
|
2214
|
+
FROM users u
|
|
2215
|
+
WHERE u.id != ? AND u.id != ?
|
|
2216
|
+
AND (u.id IN (SELECT user_id FROM nests WHERE id IN (${ownedNests}))
|
|
2217
|
+
OR u.id IN (SELECT user_id FROM nest_collaborators
|
|
2218
|
+
WHERE nest_id IN (${collabNests}))
|
|
2219
|
+
OR LOWER(u.email) IN (SELECT LOWER(user_email) FROM stewards
|
|
2220
|
+
WHERE is_active = 1
|
|
2221
|
+
AND nest_id IN (${stewardNests})))${filter.sql}
|
|
2222
|
+
ORDER BY u.email
|
|
2223
|
+
LIMIT ?`;
|
|
2224
|
+
args.push(...filter.args, SUGGEST_FETCH);
|
|
2225
|
+
return await db.all(sql, args);
|
|
2226
|
+
})();
|
|
2227
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
2228
|
+
const add = (email, name, source) => {
|
|
2229
|
+
const addr = (email || "").trim();
|
|
2230
|
+
const key = addr.toLowerCase();
|
|
2231
|
+
if (!addr || !key || key === callerEmail || byEmail.has(key)) return;
|
|
2232
|
+
const person = { email: addr, name: name ?? null, source };
|
|
2233
|
+
if (!matchesQuery(person, q)) return;
|
|
2234
|
+
byEmail.set(key, person);
|
|
2235
|
+
};
|
|
2236
|
+
for (const r of rows) add(r.email, r.name, r.source);
|
|
2237
|
+
const teamEmails = /* @__PURE__ */ new Set();
|
|
2238
|
+
for (const team of wholeDirectory ? [] : await listTeamsForUser(callerUserId)) {
|
|
2239
|
+
for (const member of team.members ?? []) {
|
|
2240
|
+
const key = (member.email || "").trim().toLowerCase();
|
|
2241
|
+
if (key && key !== callerEmail && !byEmail.has(key)) teamEmails.add(key);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (teamEmails.size > 0) {
|
|
2245
|
+
const list = [...teamEmails].slice(0, SUGGEST_FETCH);
|
|
2246
|
+
const named = await db.all(
|
|
2247
|
+
`SELECT email, name FROM users
|
|
2248
|
+
WHERE LOWER(email) IN (${list.map(() => "?").join(",")})`,
|
|
2249
|
+
list
|
|
2250
|
+
);
|
|
2251
|
+
const nameOf = new Map(
|
|
2252
|
+
named.map((u) => [u.email.toLowerCase(), u.name ?? null])
|
|
2253
|
+
);
|
|
2254
|
+
for (const email of list) add(email, nameOf.get(email) ?? null, "team");
|
|
2255
|
+
}
|
|
2256
|
+
return [...byEmail.values()].sort(
|
|
2257
|
+
(a, b) => (a.name || a.email).localeCompare(b.name || b.email, void 0, {
|
|
2258
|
+
sensitivity: "base"
|
|
2259
|
+
})
|
|
2260
|
+
).slice(0, cap);
|
|
2040
2261
|
}
|
|
2041
2262
|
|
|
2042
2263
|
// src/governance/mention-notify.ts
|
|
@@ -2047,48 +2268,172 @@ async function canOpenNode(nestId, nodeId, userId, email) {
|
|
|
2047
2268
|
}
|
|
2048
2269
|
return canReadNode(nestId, nodeId, userId, email);
|
|
2049
2270
|
}
|
|
2271
|
+
async function resolveRecipients(params) {
|
|
2272
|
+
const { nestId, nodeId, names, actorEmail } = params;
|
|
2273
|
+
if (names.length === 0) return [];
|
|
2274
|
+
const roster = await listNestPeople(nestId);
|
|
2275
|
+
const actor = actorEmail.toLowerCase();
|
|
2276
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2277
|
+
const outsiders = [];
|
|
2278
|
+
for (const name of names) {
|
|
2279
|
+
const member = resolveMention(roster, name);
|
|
2280
|
+
const person = member ?? await findDirectoryPerson(name);
|
|
2281
|
+
if (!person?.userId) continue;
|
|
2282
|
+
const key = person.email.toLowerCase();
|
|
2283
|
+
if (key === actor || candidates.has(key)) continue;
|
|
2284
|
+
const r = { email: person.email, userId: person.userId };
|
|
2285
|
+
candidates.set(key, r);
|
|
2286
|
+
if (!member) outsiders.push(r);
|
|
2287
|
+
if (candidates.size >= MAX_MENTION_RECIPIENTS) break;
|
|
2288
|
+
}
|
|
2289
|
+
if (outsiders.length) await inviteMentioned(nestId, actorEmail, outsiders);
|
|
2290
|
+
const checked = await Promise.all(
|
|
2291
|
+
[...candidates.values()].map(async (r) => ({
|
|
2292
|
+
r,
|
|
2293
|
+
allowed: await canOpenNode(nestId, nodeId, r.userId, r.email)
|
|
2294
|
+
}))
|
|
2295
|
+
);
|
|
2296
|
+
return checked.filter((c) => c.allowed).map((c) => c.r);
|
|
2297
|
+
}
|
|
2298
|
+
async function inviteMentioned(nestId, actorEmail, outsiders) {
|
|
2299
|
+
const db = getDb();
|
|
2300
|
+
const actor = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [
|
|
2301
|
+
actorEmail
|
|
2302
|
+
]);
|
|
2303
|
+
if (!actor) return;
|
|
2304
|
+
const access = await resolveNestAccess(nestId, actor.id);
|
|
2305
|
+
if (permissionLevel(access.permission) < permissionLevel("write")) return;
|
|
2306
|
+
for (const r of outsiders) {
|
|
2307
|
+
await db.run(
|
|
2308
|
+
`INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by)
|
|
2309
|
+
SELECT ?, ?, ?, 'read', ?
|
|
2310
|
+
WHERE NOT EXISTS (SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?)`,
|
|
2311
|
+
[uuid5(), nestId, r.userId, actor.id, nestId, r.userId]
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
async function insertInboxRows(params) {
|
|
2316
|
+
const db = getDb();
|
|
2317
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2318
|
+
let created = 0;
|
|
2319
|
+
for (const r of params.recipients) {
|
|
2320
|
+
await db.run(
|
|
2321
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
2322
|
+
VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
|
|
2323
|
+
[uuid5(), params.nestId, r.email.toLowerCase(), params.nodeId, params.message, now]
|
|
2324
|
+
);
|
|
2325
|
+
created++;
|
|
2326
|
+
}
|
|
2327
|
+
return created;
|
|
2328
|
+
}
|
|
2329
|
+
async function deliverMentionMessages(params) {
|
|
2330
|
+
const { nestId, nodeId, recipients, channelMessage, actorEmail, excerpt: excerpt2, ctx } = params;
|
|
2331
|
+
let throttled = 0;
|
|
2332
|
+
const channelReachable = await hasAnyConnector(nestId, "mention");
|
|
2333
|
+
if (channelReachable && canDeliver("channel", nestId, actorEmail)) {
|
|
2334
|
+
void dispatchEvent({
|
|
2335
|
+
kind: "mention",
|
|
2336
|
+
nestId,
|
|
2337
|
+
subjectId: nodeId,
|
|
2338
|
+
actor: actorEmail,
|
|
2339
|
+
message: channelMessage
|
|
2340
|
+
});
|
|
2341
|
+
} else if (channelReachable) {
|
|
2342
|
+
throttled++;
|
|
2343
|
+
}
|
|
2344
|
+
for (const r of isEmailConfigured() ? recipients : []) {
|
|
2345
|
+
if (!canDeliver("email", nestId, actorEmail)) {
|
|
2346
|
+
throttled++;
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
void sendEmailToRecipient(r.email, {
|
|
2350
|
+
status: "mention",
|
|
2351
|
+
docTitle: ctx.docTitle,
|
|
2352
|
+
path: ctx.path,
|
|
2353
|
+
link: ctx.link,
|
|
2354
|
+
actor: actorEmail,
|
|
2355
|
+
note: excerpt2 || null
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
if (throttled > 0) {
|
|
2359
|
+
console.warn(
|
|
2360
|
+
`[notify] mention delivery budget exhausted for ${actorEmail} in ${nestId} \u2014 ${throttled} outbound message(s) skipped; inbox rows unaffected`
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
var MENTION_DELIVERY_BUDGET = { max: 50, windowMs: 5 * 6e4 };
|
|
2365
|
+
var MENTION_CHANNEL_BUDGET = { max: 25, windowMs: 5 * 6e4 };
|
|
2366
|
+
function canDeliver(kind, nestId, actorEmail) {
|
|
2367
|
+
return tryConsume(
|
|
2368
|
+
`mention-${kind}:${nestId}:${actorEmail.toLowerCase()}`,
|
|
2369
|
+
kind === "email" ? MENTION_DELIVERY_BUDGET : MENTION_CHANNEL_BUDGET
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
function channelSentence(actorEmail, recipients, where) {
|
|
2373
|
+
const names = recipients.map((r) => r.email).join(", ");
|
|
2374
|
+
return `${actorEmail} mentioned ${names} ${where}`;
|
|
2375
|
+
}
|
|
2376
|
+
function excerptOf(body, max = 140) {
|
|
2377
|
+
const s = (body || "").replace(/\s+/g, " ").trim();
|
|
2378
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
2379
|
+
}
|
|
2050
2380
|
async function notifyMentions(params) {
|
|
2051
|
-
const { nestId, nodeId, previousBody, nextBody, actorEmail } = params;
|
|
2381
|
+
const { nestId, nodeId, previousBody, nextBody, actorEmail, baseUrl } = params;
|
|
2052
2382
|
try {
|
|
2053
2383
|
const names = newMentions(previousBody, nextBody);
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
const
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
await db.run(
|
|
2073
|
-
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
2074
|
-
VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
|
|
2075
|
-
[
|
|
2076
|
-
uuid4(),
|
|
2077
|
-
nestId,
|
|
2078
|
-
email.toLowerCase(),
|
|
2079
|
-
nodeId,
|
|
2080
|
-
`${actorEmail} mentioned you in "${label}"`,
|
|
2081
|
-
now
|
|
2082
|
-
]
|
|
2083
|
-
);
|
|
2084
|
-
created++;
|
|
2085
|
-
}
|
|
2384
|
+
const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
|
|
2385
|
+
if (recipients.length === 0) return 0;
|
|
2386
|
+
const ctx = await buildDocContext(nestId, nodeId, baseUrl);
|
|
2387
|
+
const message = `${actorEmail} mentioned you in "${ctx.label}"`;
|
|
2388
|
+
const created = await insertInboxRows({ nestId, nodeId, recipients, message });
|
|
2389
|
+
await deliverMentionMessages({
|
|
2390
|
+
nestId,
|
|
2391
|
+
nodeId,
|
|
2392
|
+
recipients,
|
|
2393
|
+
channelMessage: channelSentence(
|
|
2394
|
+
actorEmail,
|
|
2395
|
+
recipients,
|
|
2396
|
+
`in *${ctx.label}*`
|
|
2397
|
+
),
|
|
2398
|
+
actorEmail,
|
|
2399
|
+
excerpt: "",
|
|
2400
|
+
ctx
|
|
2401
|
+
});
|
|
2086
2402
|
return created;
|
|
2087
2403
|
} catch (err) {
|
|
2088
2404
|
console.error("[notify] mention fan-out failed", nestId, nodeId, err);
|
|
2089
2405
|
return 0;
|
|
2090
2406
|
}
|
|
2091
2407
|
}
|
|
2408
|
+
async function notifyCommentMentions(params) {
|
|
2409
|
+
const { nestId, nodeId, body, actorEmail, baseUrl } = params;
|
|
2410
|
+
try {
|
|
2411
|
+
const names = extractMentions(body || "");
|
|
2412
|
+
const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
|
|
2413
|
+
if (recipients.length === 0) return 0;
|
|
2414
|
+
const ctx = await buildDocContext(nestId, nodeId, baseUrl);
|
|
2415
|
+
const excerpt2 = excerptOf(body);
|
|
2416
|
+
const message = `${actorEmail} mentioned you in a comment on "${ctx.label}"`;
|
|
2417
|
+
const created = await insertInboxRows({ nestId, nodeId, recipients, message });
|
|
2418
|
+
await deliverMentionMessages({
|
|
2419
|
+
nestId,
|
|
2420
|
+
nodeId,
|
|
2421
|
+
recipients,
|
|
2422
|
+
channelMessage: channelSentence(
|
|
2423
|
+
actorEmail,
|
|
2424
|
+
recipients,
|
|
2425
|
+
`in a comment on *${ctx.label}*`
|
|
2426
|
+
),
|
|
2427
|
+
actorEmail,
|
|
2428
|
+
excerpt: excerpt2,
|
|
2429
|
+
ctx
|
|
2430
|
+
});
|
|
2431
|
+
return created;
|
|
2432
|
+
} catch (err) {
|
|
2433
|
+
console.error("[notify] comment mention fan-out failed", nestId, nodeId, err);
|
|
2434
|
+
return 0;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2092
2437
|
|
|
2093
2438
|
// src/nodes/service.ts
|
|
2094
2439
|
async function userIdFromEmail(email) {
|
|
@@ -2223,6 +2568,19 @@ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2223
2568
|
"artifact",
|
|
2224
2569
|
"table"
|
|
2225
2570
|
]);
|
|
2571
|
+
function assertAcceptedType(type) {
|
|
2572
|
+
if (type === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
|
|
2573
|
+
throw new ValidationError("Artifact nodes are disabled on this server");
|
|
2574
|
+
}
|
|
2575
|
+
if (type === "table" && !config.TYPE_TABLE_ENABLED) {
|
|
2576
|
+
throw new ValidationError("Table nodes are disabled on this server");
|
|
2577
|
+
}
|
|
2578
|
+
if (!ACCEPTED_NODE_TYPES.has(type)) {
|
|
2579
|
+
throw new ValidationError(
|
|
2580
|
+
`unknown node type "${type}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2226
2584
|
function toNodeResponse(node) {
|
|
2227
2585
|
const fm = node.frontmatter;
|
|
2228
2586
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
@@ -2312,8 +2670,9 @@ function engineScopesFolders() {
|
|
|
2312
2670
|
}
|
|
2313
2671
|
return folderScopeSupported;
|
|
2314
2672
|
}
|
|
2673
|
+
var DERIVED_ANNOTATIONS_SUFFIX = "--annotations";
|
|
2315
2674
|
function isDerivedAnnotationsNode(id) {
|
|
2316
|
-
return id.endsWith(
|
|
2675
|
+
return id.endsWith(DERIVED_ANNOTATIONS_SUFFIX);
|
|
2317
2676
|
}
|
|
2318
2677
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2319
2678
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
@@ -2647,11 +3006,11 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2647
3006
|
}
|
|
2648
3007
|
return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
|
|
2649
3008
|
}
|
|
2650
|
-
async function createNode(nestId, input, userEmail) {
|
|
3009
|
+
async function createNode(nestId, input, userEmail, opts = {}) {
|
|
2651
3010
|
const generatedId = !input.id && !slugify(input.title);
|
|
2652
3011
|
for (let attempt = 0; ; attempt++) {
|
|
2653
3012
|
try {
|
|
2654
|
-
return await createNodeOnce(nestId, input, userEmail);
|
|
3013
|
+
return await createNodeOnce(nestId, input, userEmail, opts);
|
|
2655
3014
|
} catch (err) {
|
|
2656
3015
|
if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
|
|
2657
3016
|
throw err;
|
|
@@ -2659,7 +3018,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2659
3018
|
}
|
|
2660
3019
|
}
|
|
2661
3020
|
}
|
|
2662
|
-
async function createNodeOnce(nestId, input, userEmail) {
|
|
3021
|
+
async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
2663
3022
|
const { storage } = await engineCache.get(nestId);
|
|
2664
3023
|
assertTitleLength(input.title);
|
|
2665
3024
|
assertTitleReadable(input.title);
|
|
@@ -2672,17 +3031,7 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2672
3031
|
}
|
|
2673
3032
|
const tags = requireTags(input.tags);
|
|
2674
3033
|
const requestedType = input.type || "document";
|
|
2675
|
-
|
|
2676
|
-
throw new ValidationError("Artifact nodes are disabled on this server");
|
|
2677
|
-
}
|
|
2678
|
-
if (requestedType === "table" && !config.TYPE_TABLE_ENABLED) {
|
|
2679
|
-
throw new ValidationError("Table nodes are disabled on this server");
|
|
2680
|
-
}
|
|
2681
|
-
if (!ACCEPTED_NODE_TYPES.has(requestedType)) {
|
|
2682
|
-
throw new ValidationError(
|
|
2683
|
-
`unknown node type "${requestedType}"; allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")}`
|
|
2684
|
-
);
|
|
2685
|
-
}
|
|
3034
|
+
assertAcceptedType(requestedType);
|
|
2686
3035
|
if (input.schedule !== void 0 && !isRunnableType(input.type)) {
|
|
2687
3036
|
throw new ValidationError(
|
|
2688
3037
|
`schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
|
|
@@ -2792,13 +3141,15 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2792
3141
|
});
|
|
2793
3142
|
}
|
|
2794
3143
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3144
|
+
if (opts.notifyMentions !== false) {
|
|
3145
|
+
await notifyMentions({
|
|
3146
|
+
nestId,
|
|
3147
|
+
nodeId: id,
|
|
3148
|
+
previousBody: "",
|
|
3149
|
+
nextBody: node.body || "",
|
|
3150
|
+
actorEmail: userEmail
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
2802
3153
|
return { node, version: savedVersion };
|
|
2803
3154
|
});
|
|
2804
3155
|
}
|
|
@@ -2892,7 +3243,7 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
2892
3243
|
await trackEvent("nest.import.documents", { nestId, registered: docs.length });
|
|
2893
3244
|
return docs.length;
|
|
2894
3245
|
}
|
|
2895
|
-
async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
3246
|
+
async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
2896
3247
|
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
2897
3248
|
try {
|
|
2898
3249
|
const { storage, versions: versionManager } = await engineCache.get(
|
|
@@ -2943,6 +3294,20 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
2943
3294
|
};
|
|
2944
3295
|
opInput.title = patch.title;
|
|
2945
3296
|
}
|
|
3297
|
+
if (patch.type !== void 0 && patch.type !== node.frontmatter.type) {
|
|
3298
|
+
assertAcceptedType(patch.type);
|
|
3299
|
+
if (patch.type === "skill") {
|
|
3300
|
+
opInput.trigger = `when asked to ${String(node.frontmatter.title || nodeId).toLowerCase()}`;
|
|
3301
|
+
}
|
|
3302
|
+
if (isRunnableType(node.frontmatter.type) && !isRunnableType(patch.type)) {
|
|
3303
|
+
const metadata = { ...node.frontmatter.metadata };
|
|
3304
|
+
delete metadata.schedule;
|
|
3305
|
+
node = { ...node, frontmatter: { ...node.frontmatter, metadata } };
|
|
3306
|
+
opInput.metadata = { ...opInput.metadata, schedule: null };
|
|
3307
|
+
}
|
|
3308
|
+
node = { ...node, frontmatter: { ...node.frontmatter, type: patch.type } };
|
|
3309
|
+
opInput.type = patch.type;
|
|
3310
|
+
}
|
|
2946
3311
|
if (patch.schedule !== void 0) {
|
|
2947
3312
|
if (!isRunnableType(node.frontmatter.type)) {
|
|
2948
3313
|
throw new ValidationError(
|
|
@@ -3044,13 +3409,15 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
3044
3409
|
);
|
|
3045
3410
|
responseVersion = publishedVersion;
|
|
3046
3411
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3412
|
+
if (opts.notifyMentions !== false) {
|
|
3413
|
+
await notifyMentions({
|
|
3414
|
+
nestId,
|
|
3415
|
+
nodeId,
|
|
3416
|
+
previousBody,
|
|
3417
|
+
nextBody: node.body || "",
|
|
3418
|
+
actorEmail: userEmail
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3054
3421
|
return { node, version: responseVersion };
|
|
3055
3422
|
} catch (err) {
|
|
3056
3423
|
throw toSafeError(
|
|
@@ -3632,8 +3999,13 @@ nestRoutes.get("/", async (c) => {
|
|
|
3632
3999
|
}
|
|
3633
4000
|
const out = await Promise.all(deduped.map(annotate));
|
|
3634
4001
|
const docRows = await db.all(
|
|
3635
|
-
`SELECT nest_id,
|
|
3636
|
-
|
|
4002
|
+
`SELECT nest_id,
|
|
4003
|
+
COUNT(DISTINCT CASE
|
|
4004
|
+
WHEN ${suffixOf(db, "node_id", DERIVED_ANNOTATIONS_SUFFIX.length)} <> ?
|
|
4005
|
+
THEN node_id END) AS c,
|
|
4006
|
+
MAX(created_at) AS last_activity
|
|
4007
|
+
FROM node_versions GROUP BY nest_id`,
|
|
4008
|
+
[DERIVED_ANNOTATIONS_SUFFIX]
|
|
3637
4009
|
);
|
|
3638
4010
|
const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
3639
4011
|
const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
|
|
@@ -3675,6 +4047,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3675
4047
|
);
|
|
3676
4048
|
for (const r of rows) {
|
|
3677
4049
|
const targets = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
4050
|
+
if (isDerivedAnnotationsNode(r.node_id)) continue;
|
|
3678
4051
|
if (grantCoversNode(targets, r.node_id)) {
|
|
3679
4052
|
grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
|
|
3680
4053
|
}
|
|
@@ -3960,7 +4333,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
3960
4333
|
import { Hono as Hono4 } from "hono";
|
|
3961
4334
|
|
|
3962
4335
|
// src/nests/sharing-service.ts
|
|
3963
|
-
import { v4 as
|
|
4336
|
+
import { v4 as uuid6 } from "uuid";
|
|
3964
4337
|
var VALID_PERMISSIONS = ["read", "write", "admin"];
|
|
3965
4338
|
async function addCollaborator(params) {
|
|
3966
4339
|
const { nestId } = params;
|
|
@@ -3987,10 +4360,10 @@ async function addCollaborator(params) {
|
|
|
3987
4360
|
userId = existing.id;
|
|
3988
4361
|
} else {
|
|
3989
4362
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3990
|
-
userId =
|
|
4363
|
+
userId = uuid6();
|
|
3991
4364
|
await db.run(
|
|
3992
4365
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3993
|
-
[userId, email, null, await hashPassword2(
|
|
4366
|
+
[userId, email, null, await hashPassword2(uuid6())]
|
|
3994
4367
|
);
|
|
3995
4368
|
}
|
|
3996
4369
|
}
|
|
@@ -4028,7 +4401,7 @@ async function addCollaborator(params) {
|
|
|
4028
4401
|
[params.grantedByEmail]
|
|
4029
4402
|
))?.id : void 0;
|
|
4030
4403
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
4031
|
-
const collabId =
|
|
4404
|
+
const collabId = uuid6();
|
|
4032
4405
|
await db.run(
|
|
4033
4406
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
4034
4407
|
[collabId, nestId, userId, params.permission, granterId]
|
|
@@ -4101,7 +4474,7 @@ sharingRoutes.get("/collaborators", async (c) => {
|
|
|
4101
4474
|
}
|
|
4102
4475
|
return c.json({
|
|
4103
4476
|
collaborators: enriched,
|
|
4104
|
-
people: await listNestPeople(nestId)
|
|
4477
|
+
people: await withDirectory(await listNestPeople(nestId))
|
|
4105
4478
|
});
|
|
4106
4479
|
});
|
|
4107
4480
|
sharingRoutes.post("/collaborators", async (c) => {
|
|
@@ -4145,6 +4518,22 @@ sharingRoutes.delete("/collaborators/:collabId", async (c) => {
|
|
|
4145
4518
|
}
|
|
4146
4519
|
return c.json({ removed: true });
|
|
4147
4520
|
});
|
|
4521
|
+
sharingRoutes.get("/mentionable", async (c) => {
|
|
4522
|
+
const nestId = c.req.param("nestId");
|
|
4523
|
+
const userId = c.get("userId");
|
|
4524
|
+
const people = await listNestPeople(nestId);
|
|
4525
|
+
const callerEmail = (await resolveCallerEmail(userId)).toLowerCase();
|
|
4526
|
+
const onRoster = people.some((p) => p.email.toLowerCase() === callerEmail);
|
|
4527
|
+
if (!onRoster) {
|
|
4528
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
4529
|
+
if (permissionLevel(access.permission) < permissionLevel("write")) {
|
|
4530
|
+
throw new ForbiddenError("Only people on this nest can see its roster");
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
return c.json({
|
|
4534
|
+
people: (await withDirectory(people)).map((p) => ({ email: p.email, name: p.name }))
|
|
4535
|
+
});
|
|
4536
|
+
});
|
|
4148
4537
|
sharingRoutes.patch("/visibility", async (c) => {
|
|
4149
4538
|
const body = await c.req.json();
|
|
4150
4539
|
if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
|
|
@@ -4176,10 +4565,14 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
4176
4565
|
});
|
|
4177
4566
|
sharingRoutes.patch("/reader-mode", async (c) => {
|
|
4178
4567
|
const userId = c.get("userId");
|
|
4568
|
+
const nestId = c.req.param("nestId");
|
|
4179
4569
|
if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4570
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
4571
|
+
if (permissionLevel(access.permission) < permissionLevel("admin")) {
|
|
4572
|
+
throw new ForbiddenError(
|
|
4573
|
+
"Only the nest owner, a nest admin, or the server admin can configure reader mode"
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4183
4576
|
}
|
|
4184
4577
|
const body = await c.req.json();
|
|
4185
4578
|
if (body.enabled !== void 0 && typeof body.enabled !== "boolean") {
|
|
@@ -4202,7 +4595,6 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4202
4595
|
);
|
|
4203
4596
|
}
|
|
4204
4597
|
const db = getDb();
|
|
4205
|
-
const nestId = c.req.param("nestId");
|
|
4206
4598
|
const exists = await db.get("SELECT id FROM nests WHERE id = ?", [
|
|
4207
4599
|
nestId
|
|
4208
4600
|
]);
|
|
@@ -4222,14 +4614,21 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4222
4614
|
|
|
4223
4615
|
// src/nests/grant-routes.ts
|
|
4224
4616
|
import { Hono as Hono5 } from "hono";
|
|
4225
|
-
import { v4 as
|
|
4617
|
+
import { v4 as uuid7 } from "uuid";
|
|
4226
4618
|
var grantRoutes = new Hono5();
|
|
4227
|
-
async function requireManage(c, nestId) {
|
|
4228
|
-
if (
|
|
4619
|
+
async function requireManage(c, nestId, opts = {}) {
|
|
4620
|
+
if (config.AUTH_MODE === "open") return;
|
|
4621
|
+
const access = await resolveNestAccess(nestId, c.get("userId"));
|
|
4622
|
+
if (access.rawPermission !== "owner" && access.rawPermission !== "admin") {
|
|
4229
4623
|
throw new ForbiddenError(
|
|
4230
4624
|
"Only a nest admin, the nest owner, or the server admin can manage sharing."
|
|
4231
4625
|
);
|
|
4232
4626
|
}
|
|
4627
|
+
if (access.archived && !opts.readOnly) {
|
|
4628
|
+
throw new ForbiddenError(
|
|
4629
|
+
"This nest is archived, so it is read-only. Restore it to change sharing."
|
|
4630
|
+
);
|
|
4631
|
+
}
|
|
4233
4632
|
}
|
|
4234
4633
|
async function resolveGrantee(email) {
|
|
4235
4634
|
const db = getDb();
|
|
@@ -4237,10 +4636,10 @@ async function resolveGrantee(email) {
|
|
|
4237
4636
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
4238
4637
|
if (existing) return existing.id;
|
|
4239
4638
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4240
|
-
const id =
|
|
4639
|
+
const id = uuid7();
|
|
4241
4640
|
await db.run(
|
|
4242
4641
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4243
|
-
[id, e, null, await hashPassword2(
|
|
4642
|
+
[id, e, null, await hashPassword2(uuid7())]
|
|
4244
4643
|
);
|
|
4245
4644
|
return id;
|
|
4246
4645
|
}
|
|
@@ -4319,7 +4718,7 @@ grantRoutes.post("/", async (c) => {
|
|
|
4319
4718
|
});
|
|
4320
4719
|
grantRoutes.get("/", async (c) => {
|
|
4321
4720
|
const nestId = c.req.param("nestId");
|
|
4322
|
-
await requireManage(c, nestId);
|
|
4721
|
+
await requireManage(c, nestId, { readOnly: true });
|
|
4323
4722
|
const grants = await listGrants(nestId);
|
|
4324
4723
|
const db = getDb();
|
|
4325
4724
|
const withEmail = await Promise.all(
|
|
@@ -4342,7 +4741,7 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4342
4741
|
import { Hono as Hono6 } from "hono";
|
|
4343
4742
|
|
|
4344
4743
|
// src/nests/publish-service.ts
|
|
4345
|
-
import { v4 as
|
|
4744
|
+
import { v4 as uuid8 } from "uuid";
|
|
4346
4745
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
4347
4746
|
var MODES = ["public", "email", "code", "invite"];
|
|
4348
4747
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -4411,7 +4810,7 @@ async function createPublish(params) {
|
|
|
4411
4810
|
);
|
|
4412
4811
|
}
|
|
4413
4812
|
const db = getDb();
|
|
4414
|
-
const id =
|
|
4813
|
+
const id = uuid8();
|
|
4415
4814
|
const slug = newSlug();
|
|
4416
4815
|
const created = nowIso();
|
|
4417
4816
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -4442,7 +4841,7 @@ async function createPublish(params) {
|
|
|
4442
4841
|
await db.run(
|
|
4443
4842
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4444
4843
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
4445
|
-
[
|
|
4844
|
+
[uuid8(), id, email, created]
|
|
4446
4845
|
);
|
|
4447
4846
|
}
|
|
4448
4847
|
}
|
|
@@ -4497,7 +4896,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
4497
4896
|
await getDb().run(
|
|
4498
4897
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4499
4898
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
4500
|
-
[
|
|
4899
|
+
[uuid8(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
4501
4900
|
);
|
|
4502
4901
|
return token;
|
|
4503
4902
|
}
|
|
@@ -4527,7 +4926,7 @@ async function createSession2(publishId, email) {
|
|
|
4527
4926
|
await getDb().run(
|
|
4528
4927
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4529
4928
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
4530
|
-
[
|
|
4929
|
+
[uuid8(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
4531
4930
|
);
|
|
4532
4931
|
return token;
|
|
4533
4932
|
}
|
|
@@ -4544,7 +4943,7 @@ async function logView(publishId, email) {
|
|
|
4544
4943
|
await getDb().run(
|
|
4545
4944
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4546
4945
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
4547
|
-
[
|
|
4946
|
+
[uuid8(), publishId, email, nowIso()]
|
|
4548
4947
|
);
|
|
4549
4948
|
}
|
|
4550
4949
|
async function listAllowlist(publishId) {
|
|
@@ -4637,7 +5036,7 @@ import { Hono as Hono7 } from "hono";
|
|
|
4637
5036
|
import MarkdownIt from "markdown-it";
|
|
4638
5037
|
|
|
4639
5038
|
// src/annotations/service.ts
|
|
4640
|
-
import { v4 as
|
|
5039
|
+
import { v4 as uuid9 } from "uuid";
|
|
4641
5040
|
|
|
4642
5041
|
// src/annotations/projection.ts
|
|
4643
5042
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -4762,6 +5161,10 @@ async function rowToThread(row) {
|
|
|
4762
5161
|
async function getThreadRow(threadId) {
|
|
4763
5162
|
return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
|
|
4764
5163
|
}
|
|
5164
|
+
async function getThread(threadId) {
|
|
5165
|
+
const row = await getThreadRow(threadId);
|
|
5166
|
+
return row ? await rowToThread(row) : null;
|
|
5167
|
+
}
|
|
4765
5168
|
async function countThreadsByNode(nestId) {
|
|
4766
5169
|
const rows = await getDb().all(
|
|
4767
5170
|
`SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
|
|
@@ -4787,14 +5190,14 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4787
5190
|
throw new Error("comment body is required");
|
|
4788
5191
|
}
|
|
4789
5192
|
const db = getDb();
|
|
4790
|
-
const id =
|
|
5193
|
+
const id = uuid9();
|
|
4791
5194
|
const anchor = clampAnchor(input.anchor);
|
|
4792
5195
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
4793
5196
|
await db.transaction(async (tx) => {
|
|
4794
5197
|
await tx.run(
|
|
4795
5198
|
`INSERT INTO annotation_threads
|
|
4796
|
-
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
|
|
4797
|
-
VALUES (?, ?, ?, ?, ?, 'open',
|
|
5199
|
+
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by, created_at)
|
|
5200
|
+
VALUES (?, ?, ?, ?, ?, 'open', ?, ${nowMsExpr(db)})`,
|
|
4798
5201
|
[
|
|
4799
5202
|
id,
|
|
4800
5203
|
nestId,
|
|
@@ -4805,8 +5208,9 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4805
5208
|
]
|
|
4806
5209
|
);
|
|
4807
5210
|
await tx.run(
|
|
4808
|
-
|
|
4809
|
-
|
|
5211
|
+
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5212
|
+
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5213
|
+
[uuid9(), id, authorEmail, body]
|
|
4810
5214
|
);
|
|
4811
5215
|
});
|
|
4812
5216
|
return rowToThread(await getThreadRow(id));
|
|
@@ -4824,9 +5228,11 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
4824
5228
|
throw new Error("comment body is required");
|
|
4825
5229
|
}
|
|
4826
5230
|
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
5231
|
+
const db = getDb();
|
|
5232
|
+
await db.run(
|
|
5233
|
+
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5234
|
+
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5235
|
+
[uuid9(), threadId, authorEmail, trimmed]
|
|
4830
5236
|
);
|
|
4831
5237
|
return rowToThread(await getThreadRow(threadId));
|
|
4832
5238
|
}
|
|
@@ -4913,7 +5319,13 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
4913
5319
|
const markdown2 = projectThreadsToMarkdown(title, threads);
|
|
4914
5320
|
const derivedId = derivedAnnotationsId(nodeId);
|
|
4915
5321
|
try {
|
|
4916
|
-
await updateNode(
|
|
5322
|
+
await updateNode(
|
|
5323
|
+
nestId,
|
|
5324
|
+
derivedId,
|
|
5325
|
+
{ content: markdown2 },
|
|
5326
|
+
userEmail,
|
|
5327
|
+
{ notifyMentions: false }
|
|
5328
|
+
);
|
|
4917
5329
|
} catch (err) {
|
|
4918
5330
|
if (err instanceof NotFoundError) {
|
|
4919
5331
|
await createNode(
|
|
@@ -4925,7 +5337,8 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
4925
5337
|
type: "document",
|
|
4926
5338
|
tags: ["annotations"]
|
|
4927
5339
|
},
|
|
4928
|
-
userEmail
|
|
5340
|
+
userEmail,
|
|
5341
|
+
{ notifyMentions: false }
|
|
4929
5342
|
);
|
|
4930
5343
|
} else {
|
|
4931
5344
|
throw err;
|
|
@@ -5350,8 +5763,19 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
|
5350
5763
|
return c.json({ teams });
|
|
5351
5764
|
});
|
|
5352
5765
|
|
|
5353
|
-
// src/
|
|
5766
|
+
// src/nests/people-routes.ts
|
|
5354
5767
|
import { Hono as Hono9 } from "hono";
|
|
5768
|
+
var peopleRoutes = new Hono9();
|
|
5769
|
+
peopleRoutes.get("/suggest", async (c) => {
|
|
5770
|
+
const userId = c.get("userId");
|
|
5771
|
+
const q = c.req.query("q") ?? "";
|
|
5772
|
+
const rawLimit = Number(c.req.query("limit"));
|
|
5773
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : void 0;
|
|
5774
|
+
return c.json({ people: await suggestPeopleForUser(userId, q, limit) });
|
|
5775
|
+
});
|
|
5776
|
+
|
|
5777
|
+
// src/governance/team-routes.ts
|
|
5778
|
+
import { Hono as Hono10 } from "hono";
|
|
5355
5779
|
|
|
5356
5780
|
// src/governance/promptowl-teams.ts
|
|
5357
5781
|
var PROMPTOWL_SOURCE = "promptowl";
|
|
@@ -5498,7 +5922,7 @@ async function desiredRoster(poTeam, callerEmail) {
|
|
|
5498
5922
|
}
|
|
5499
5923
|
|
|
5500
5924
|
// src/governance/team-routes.ts
|
|
5501
|
-
var teamRoutes = new
|
|
5925
|
+
var teamRoutes = new Hono10();
|
|
5502
5926
|
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
5503
5927
|
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
5504
5928
|
var CONNECT_REQUIRED_STATUS = 428;
|
|
@@ -5683,7 +6107,7 @@ function toSearchHit(node, query) {
|
|
|
5683
6107
|
}
|
|
5684
6108
|
|
|
5685
6109
|
// src/nodes/routes.ts
|
|
5686
|
-
import { Hono as
|
|
6110
|
+
import { Hono as Hono11 } from "hono";
|
|
5687
6111
|
|
|
5688
6112
|
// src/nodes/markdown-export.ts
|
|
5689
6113
|
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
@@ -5735,7 +6159,7 @@ ${m[1]}
|
|
|
5735
6159
|
}
|
|
5736
6160
|
|
|
5737
6161
|
// src/nodes/routes.ts
|
|
5738
|
-
var nodeRoutes = new
|
|
6162
|
+
var nodeRoutes = new Hono11();
|
|
5739
6163
|
function nodeAsMarkdown(response, nodeId) {
|
|
5740
6164
|
return nodeToMarkdown({
|
|
5741
6165
|
id: nodeId,
|
|
@@ -5817,7 +6241,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
5817
6241
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
5818
6242
|
const nestId = c.req.param("nestId");
|
|
5819
6243
|
const nodeId = c.req.param("nodeId");
|
|
5820
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
6244
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-4DHNRULG.js");
|
|
5821
6245
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
5822
6246
|
nestId,
|
|
5823
6247
|
nodeId
|
|
@@ -5838,7 +6262,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
5838
6262
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
5839
6263
|
const nestId = c.req.param("nestId");
|
|
5840
6264
|
const nodeId = c.req.param("nodeId");
|
|
5841
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
6265
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-HHGOTO6P.js");
|
|
5842
6266
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
5843
6267
|
return c.json({ reviews: history });
|
|
5844
6268
|
});
|
|
@@ -5977,7 +6401,8 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
5977
6401
|
}
|
|
5978
6402
|
}
|
|
5979
6403
|
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
5980
|
-
|
|
6404
|
+
const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
|
|
6405
|
+
if (publicReader || approvedOnly) {
|
|
5981
6406
|
const approved = await getApprovedVersion(nestId, nodeId);
|
|
5982
6407
|
if (approved != null) {
|
|
5983
6408
|
let approvedRaw = "";
|
|
@@ -6008,6 +6433,9 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
6008
6433
|
}
|
|
6009
6434
|
return c.json({ node: response });
|
|
6010
6435
|
}
|
|
6436
|
+
if (approvedOnly) {
|
|
6437
|
+
throw new NotFoundError("This document has no approved version yet.");
|
|
6438
|
+
}
|
|
6011
6439
|
}
|
|
6012
6440
|
response.status = node.pendingChange ? "external_edit_pending" : await getDisplayStatus(nestId, nodeId);
|
|
6013
6441
|
if (response.status === "pending_review") {
|
|
@@ -6032,6 +6460,9 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
6032
6460
|
const nestId = c.req.param("nestId");
|
|
6033
6461
|
const nodeId = c.req.param("nodeId");
|
|
6034
6462
|
const body = await c.req.json();
|
|
6463
|
+
if (body.type !== void 0 && typeof body.type !== "string") {
|
|
6464
|
+
throw new ValidationError("type must be a string");
|
|
6465
|
+
}
|
|
6035
6466
|
const baseVersionHeader = c.req.header("X-Base-Version");
|
|
6036
6467
|
if (baseVersionHeader) {
|
|
6037
6468
|
const baseVersion = parseInt(baseVersionHeader, 10);
|
|
@@ -6062,7 +6493,8 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
6062
6493
|
status: body.status,
|
|
6063
6494
|
changeNote: body.changeNote,
|
|
6064
6495
|
schedule: body.schedule,
|
|
6065
|
-
prime: body.prime
|
|
6496
|
+
prime: body.prime,
|
|
6497
|
+
type: body.type
|
|
6066
6498
|
},
|
|
6067
6499
|
authorEmail
|
|
6068
6500
|
);
|
|
@@ -6082,13 +6514,13 @@ async function getUserEmail(c) {
|
|
|
6082
6514
|
}
|
|
6083
6515
|
|
|
6084
6516
|
// src/annotations/routes.ts
|
|
6085
|
-
import { Hono as
|
|
6517
|
+
import { Hono as Hono12 } from "hono";
|
|
6086
6518
|
|
|
6087
6519
|
// src/annotations/types.ts
|
|
6088
6520
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
6089
6521
|
|
|
6090
6522
|
// src/annotations/routes.ts
|
|
6091
|
-
var annotationRoutes = new
|
|
6523
|
+
var annotationRoutes = new Hono12();
|
|
6092
6524
|
function getNodeId(c) {
|
|
6093
6525
|
const raw = c.req.param("nodeId");
|
|
6094
6526
|
try {
|
|
@@ -6129,6 +6561,13 @@ annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
|
|
|
6129
6561
|
},
|
|
6130
6562
|
userEmail
|
|
6131
6563
|
);
|
|
6564
|
+
await notifyCommentMentions({
|
|
6565
|
+
nestId,
|
|
6566
|
+
nodeId,
|
|
6567
|
+
body: input.body,
|
|
6568
|
+
actorEmail: userEmail,
|
|
6569
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
6570
|
+
});
|
|
6132
6571
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
6133
6572
|
return c.json({ thread }, 201);
|
|
6134
6573
|
});
|
|
@@ -6144,6 +6583,13 @@ async function handleAnnotationReply(c, nodeId, threadId) {
|
|
|
6144
6583
|
throw new ValidationError("body is required");
|
|
6145
6584
|
}
|
|
6146
6585
|
const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
|
|
6586
|
+
await notifyCommentMentions({
|
|
6587
|
+
nestId,
|
|
6588
|
+
nodeId,
|
|
6589
|
+
body: input.body,
|
|
6590
|
+
actorEmail: userEmail,
|
|
6591
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
6592
|
+
});
|
|
6147
6593
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
6148
6594
|
return c.json({ thread });
|
|
6149
6595
|
}
|
|
@@ -6202,7 +6648,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
6202
6648
|
});
|
|
6203
6649
|
|
|
6204
6650
|
// src/nodes/query-routes.ts
|
|
6205
|
-
import { Hono as
|
|
6651
|
+
import { Hono as Hono13 } from "hono";
|
|
6206
6652
|
|
|
6207
6653
|
// src/tables/service.ts
|
|
6208
6654
|
function splitCsvLine(line) {
|
|
@@ -6552,7 +6998,18 @@ var STOPWORDS = /* @__PURE__ */ new Set([
|
|
|
6552
6998
|
"ok",
|
|
6553
6999
|
"please",
|
|
6554
7000
|
"thanks",
|
|
6555
|
-
"thank"
|
|
7001
|
+
"thank",
|
|
7002
|
+
// Greetings and fillers a chat turn opens with — never a topic.
|
|
7003
|
+
"hello",
|
|
7004
|
+
"hi",
|
|
7005
|
+
"hey",
|
|
7006
|
+
"there",
|
|
7007
|
+
"good",
|
|
7008
|
+
"morning",
|
|
7009
|
+
"afternoon",
|
|
7010
|
+
"evening",
|
|
7011
|
+
"bye",
|
|
7012
|
+
"cheers"
|
|
6556
7013
|
]);
|
|
6557
7014
|
function tokenizePrompt(prompt) {
|
|
6558
7015
|
const words = prompt.toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean).filter((w) => w.length >= 3 && !STOPWORDS.has(w));
|
|
@@ -6594,6 +7051,34 @@ async function compilePrompt(prompt, nestId, titles) {
|
|
|
6594
7051
|
unmatched
|
|
6595
7052
|
};
|
|
6596
7053
|
}
|
|
7054
|
+
function scorePrompt(doc, tokens) {
|
|
7055
|
+
if (tokens.length === 0) return { score: 0, via: null };
|
|
7056
|
+
const title = doc.frontmatter.title || "";
|
|
7057
|
+
const tags = (doc.frontmatter.tags || []).map(normalizeTag);
|
|
7058
|
+
const body = (doc.body || "").toLowerCase();
|
|
7059
|
+
let sum = 0;
|
|
7060
|
+
let via = null;
|
|
7061
|
+
for (const t of tokens) {
|
|
7062
|
+
const inTitle = titleScore(title, t);
|
|
7063
|
+
if (inTitle > 0) {
|
|
7064
|
+
sum += inTitle;
|
|
7065
|
+
via = "title";
|
|
7066
|
+
} else if (tags.some((tag) => tag.startsWith(t))) {
|
|
7067
|
+
sum += 2;
|
|
7068
|
+
if (via !== "title") via = "tag";
|
|
7069
|
+
} else if (startsWord(body, t)) {
|
|
7070
|
+
sum += 1;
|
|
7071
|
+
via ??= "fulltext";
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
return { score: sum / (4 * tokens.length), via };
|
|
7075
|
+
}
|
|
7076
|
+
function startsWord(text, term) {
|
|
7077
|
+
for (let i = text.indexOf(term); i !== -1; i = text.indexOf(term, i + 1)) {
|
|
7078
|
+
if (i === 0 || !/[a-z0-9]/.test(text[i - 1])) return true;
|
|
7079
|
+
}
|
|
7080
|
+
return false;
|
|
7081
|
+
}
|
|
6597
7082
|
|
|
6598
7083
|
// src/nodes/readable-body.ts
|
|
6599
7084
|
async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
@@ -6836,7 +7321,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
6836
7321
|
}
|
|
6837
7322
|
|
|
6838
7323
|
// src/nodes/query-routes.ts
|
|
6839
|
-
var queryRoutes = new
|
|
7324
|
+
var queryRoutes = new Hono13();
|
|
6840
7325
|
queryRoutes.get("/changes", async (c) => {
|
|
6841
7326
|
const nestId = c.req.param("nestId");
|
|
6842
7327
|
const userId = c.get("userId");
|
|
@@ -6988,7 +7473,8 @@ queryRoutes.post("/context", async (c) => {
|
|
|
6988
7473
|
const baseUrl = requestBaseUrl(c.req.url);
|
|
6989
7474
|
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
6990
7475
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
6991
|
-
const
|
|
7476
|
+
const promptPath = !body.selector?.trim() && !!body.prompt;
|
|
7477
|
+
const hops = body.hops ?? (promptPath ? 0 : 2);
|
|
6992
7478
|
const includeDrafts = body.include_drafts === true;
|
|
6993
7479
|
const isVisible = (d) => includeDrafts || d.frontmatter.status === "published";
|
|
6994
7480
|
let selector = body.selector?.trim() || null;
|
|
@@ -7022,7 +7508,7 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7022
7508
|
compileDetail.matchedTitles.map((t) => t.toLowerCase())
|
|
7023
7509
|
);
|
|
7024
7510
|
titleMatches = allDocs.filter(
|
|
7025
|
-
(d) => matchedLower.has(d.frontmatter.title.toLowerCase())
|
|
7511
|
+
(d) => matchedLower.has(d.frontmatter.title.toLowerCase()) && isVisible(d)
|
|
7026
7512
|
);
|
|
7027
7513
|
}
|
|
7028
7514
|
}
|
|
@@ -7086,6 +7572,27 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7086
7572
|
}
|
|
7087
7573
|
}
|
|
7088
7574
|
}
|
|
7575
|
+
const relevance = /* @__PURE__ */ new Map();
|
|
7576
|
+
let fallback = null;
|
|
7577
|
+
if (compileDetail) {
|
|
7578
|
+
const tokens = tokenizePrompt(body.prompt);
|
|
7579
|
+
const compiled = new Set(documents.map((d) => d.id));
|
|
7580
|
+
for (const d of allDocs) {
|
|
7581
|
+
const { score, via } = scorePrompt(d, tokens);
|
|
7582
|
+
if (compiled.has(d.id)) relevance.set(d.id, { score, via: via ?? "hop" });
|
|
7583
|
+
else if (score > 0 && isVisible(d)) {
|
|
7584
|
+
relevance.set(d.id, { score, via });
|
|
7585
|
+
documents.push(d);
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
if (compiled.size === 0 && documents.length > 0) fallback = "fulltext";
|
|
7589
|
+
const top = Math.max(0, ...documents.map((d) => relevance.get(d.id).score));
|
|
7590
|
+
documents = documents.filter((d) => {
|
|
7591
|
+
const r = relevance.get(d.id);
|
|
7592
|
+
return compiled.has(d.id) || r.via === "hop" || r.score >= top / 2;
|
|
7593
|
+
});
|
|
7594
|
+
documents.sort((a, b) => relevance.get(b.id).score - relevance.get(a.id).score);
|
|
7595
|
+
}
|
|
7089
7596
|
const userId = c.get("userId");
|
|
7090
7597
|
const userEmail = await resolveCallerEmail(userId);
|
|
7091
7598
|
const beforePermission = documents.length;
|
|
@@ -7103,10 +7610,14 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7103
7610
|
for (const doc of readable) {
|
|
7104
7611
|
const block = formatContextBlock(doc, doc.url);
|
|
7105
7612
|
const blockTokens = approxTokens(block);
|
|
7106
|
-
if (tokenCount + blockTokens > maxTokens
|
|
7613
|
+
if (tokenCount + blockTokens > maxTokens) continue;
|
|
7107
7614
|
included.push(doc);
|
|
7108
7615
|
tokenCount += blockTokens;
|
|
7109
7616
|
}
|
|
7617
|
+
if (included.length === 0 && readable.length > 0) {
|
|
7618
|
+
included.push(readable[0]);
|
|
7619
|
+
tokenCount = approxTokens(formatContextBlock(readable[0], readable[0].url));
|
|
7620
|
+
}
|
|
7110
7621
|
const truncatedByBudget = accessible.length - included.length;
|
|
7111
7622
|
const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
|
|
7112
7623
|
trackEvent("context.resolve", {
|
|
@@ -7123,7 +7634,8 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7123
7634
|
title: n.frontmatter.title,
|
|
7124
7635
|
url: n.url,
|
|
7125
7636
|
tags: n.frontmatter.tags || [],
|
|
7126
|
-
type: n.frontmatter.type || "document"
|
|
7637
|
+
type: n.frontmatter.type || "document",
|
|
7638
|
+
...relevance.get(n.id)
|
|
7127
7639
|
})),
|
|
7128
7640
|
trace: {
|
|
7129
7641
|
selector_used: selector,
|
|
@@ -7131,7 +7643,8 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7131
7643
|
compiled: compileDetail ? {
|
|
7132
7644
|
matched_tags: compileDetail.matchedTags,
|
|
7133
7645
|
matched_titles: compileDetail.matchedTitles,
|
|
7134
|
-
unmatched_tokens: compileDetail.unmatched
|
|
7646
|
+
unmatched_tokens: compileDetail.unmatched,
|
|
7647
|
+
fallback
|
|
7135
7648
|
} : null,
|
|
7136
7649
|
total_matched: documents.length,
|
|
7137
7650
|
permission_filtered: permissionFiltered,
|
|
@@ -7316,11 +7829,11 @@ queryRoutes.get("/export", async (c) => {
|
|
|
7316
7829
|
let tokens = 0;
|
|
7317
7830
|
for (const f of fields) {
|
|
7318
7831
|
const t = approxTokens(nodeToMarkdown(f));
|
|
7319
|
-
if (tokens + t > maxTokens
|
|
7832
|
+
if (tokens + t > maxTokens) continue;
|
|
7320
7833
|
kept.push(f);
|
|
7321
7834
|
tokens += t;
|
|
7322
7835
|
}
|
|
7323
|
-
included = kept;
|
|
7836
|
+
included = kept.length > 0 ? kept : fields.slice(0, 1);
|
|
7324
7837
|
}
|
|
7325
7838
|
trackEvent("nest.export", {
|
|
7326
7839
|
nestId,
|
|
@@ -7398,7 +7911,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
7398
7911
|
});
|
|
7399
7912
|
|
|
7400
7913
|
// src/mcp/routes.ts
|
|
7401
|
-
import { Hono as
|
|
7914
|
+
import { Hono as Hono15 } from "hono";
|
|
7402
7915
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7403
7916
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7404
7917
|
|
|
@@ -7478,10 +7991,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
7478
7991
|
}
|
|
7479
7992
|
|
|
7480
7993
|
// src/mcp/workflow-tools.ts
|
|
7481
|
-
import { v4 as
|
|
7994
|
+
import { v4 as uuid12 } from "uuid";
|
|
7482
7995
|
|
|
7483
7996
|
// src/workflow/run-service.ts
|
|
7484
|
-
import { v4 as
|
|
7997
|
+
import { v4 as uuid10 } from "uuid";
|
|
7485
7998
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
7486
7999
|
async function flowSubgraph(nestId, agentNode) {
|
|
7487
8000
|
const db = getDb();
|
|
@@ -7611,7 +8124,7 @@ async function triggerRun(opts) {
|
|
|
7611
8124
|
}
|
|
7612
8125
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
7613
8126
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
7614
|
-
const id = `run_${
|
|
8127
|
+
const id = `run_${uuid10()}`;
|
|
7615
8128
|
const db = getDb();
|
|
7616
8129
|
await db.transaction(async (tx) => {
|
|
7617
8130
|
if (parentRunId) {
|
|
@@ -7797,8 +8310,8 @@ async function listRuns(nestId, opts) {
|
|
|
7797
8310
|
}
|
|
7798
8311
|
|
|
7799
8312
|
// src/workflow/edge-routes.ts
|
|
7800
|
-
import { Hono as
|
|
7801
|
-
import { v4 as
|
|
8313
|
+
import { Hono as Hono14 } from "hono";
|
|
8314
|
+
import { v4 as uuid11 } from "uuid";
|
|
7802
8315
|
function toResponse(e, t) {
|
|
7803
8316
|
return {
|
|
7804
8317
|
id: e.id,
|
|
@@ -7859,7 +8372,9 @@ async function validateCondition(nestId, mode, condition) {
|
|
|
7859
8372
|
async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
7860
8373
|
if (fromNode === toNode) {
|
|
7861
8374
|
throw new ConflictError(
|
|
7862
|
-
`This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}.
|
|
8375
|
+
`This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}.
|
|
8376
|
+
|
|
8377
|
+
A flow can't return to a step it already ran.`
|
|
7863
8378
|
);
|
|
7864
8379
|
}
|
|
7865
8380
|
const rows = await db.all(
|
|
@@ -7888,7 +8403,9 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
|
7888
8403
|
}
|
|
7889
8404
|
path.reverse().push(toNode);
|
|
7890
8405
|
throw new ConflictError(
|
|
7891
|
-
`These steps loop back on each other: ${await nodeTrail(db, nestId, path)}.
|
|
8406
|
+
`These steps loop back on each other: ${await nodeTrail(db, nestId, path)}.
|
|
8407
|
+
|
|
8408
|
+
A flow can't return to a step it already ran.`
|
|
7892
8409
|
);
|
|
7893
8410
|
}
|
|
7894
8411
|
for (const nb of adj.get(cur) ?? []) {
|
|
@@ -7912,7 +8429,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
7912
8429
|
);
|
|
7913
8430
|
}
|
|
7914
8431
|
}
|
|
7915
|
-
var edgeRoutes = new
|
|
8432
|
+
var edgeRoutes = new Hono14();
|
|
7916
8433
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
7917
8434
|
const nestId = c.req.param("nestId");
|
|
7918
8435
|
const db = getDb();
|
|
@@ -7980,7 +8497,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
7980
8497
|
);
|
|
7981
8498
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
7982
8499
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7983
|
-
const id =
|
|
8500
|
+
const id = uuid11();
|
|
7984
8501
|
await db.transaction(async (tx) => {
|
|
7985
8502
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
7986
8503
|
if (type.is_flow) {
|
|
@@ -8381,7 +8898,7 @@ async function createEdge(ctx, args) {
|
|
|
8381
8898
|
);
|
|
8382
8899
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
8383
8900
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8384
|
-
const id =
|
|
8901
|
+
const id = uuid12();
|
|
8385
8902
|
await db.transaction(async (tx) => {
|
|
8386
8903
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
8387
8904
|
if (type.is_flow) {
|
|
@@ -8445,7 +8962,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
8445
8962
|
`INSERT INTO edge_types
|
|
8446
8963
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
8447
8964
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8448
|
-
[
|
|
8965
|
+
[uuid12(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
8449
8966
|
);
|
|
8450
8967
|
}
|
|
8451
8968
|
});
|
|
@@ -8654,6 +9171,43 @@ var TOOL_DEFINITIONS = [
|
|
|
8654
9171
|
}
|
|
8655
9172
|
}
|
|
8656
9173
|
},
|
|
9174
|
+
{
|
|
9175
|
+
name: "context_comment",
|
|
9176
|
+
description: "Leave a comment on a document \u2014 the same threads the web UI's comments panel shows. Omit `thread` to open a new one; pass a thread id (from context_comments) to reply to it. Use this to answer feedback you were given, or to raise something you noticed while working. Anyone who can read the document may comment; `@name` in the body notifies that person.",
|
|
9177
|
+
inputSchema: {
|
|
9178
|
+
type: "object",
|
|
9179
|
+
properties: {
|
|
9180
|
+
title: { type: "string", description: "Title of the node" },
|
|
9181
|
+
id: { type: "string", description: "ID of the node" },
|
|
9182
|
+
body: { type: "string", description: "The comment text" },
|
|
9183
|
+
thread: {
|
|
9184
|
+
type: "string",
|
|
9185
|
+
description: "Thread id to reply to (from context_comments). Omit to open a new thread on the document."
|
|
9186
|
+
}
|
|
9187
|
+
},
|
|
9188
|
+
required: ["body"]
|
|
9189
|
+
}
|
|
9190
|
+
},
|
|
9191
|
+
{
|
|
9192
|
+
name: "context_resolve_comment",
|
|
9193
|
+
description: "Mark a comment thread resolved once you have addressed it, or reopen one that was closed too early. Records you as the resolver, exactly as the web UI does. Resolve only threads you actually acted on.",
|
|
9194
|
+
inputSchema: {
|
|
9195
|
+
type: "object",
|
|
9196
|
+
properties: {
|
|
9197
|
+
title: { type: "string", description: "Title of the node" },
|
|
9198
|
+
id: { type: "string", description: "ID of the node" },
|
|
9199
|
+
thread: {
|
|
9200
|
+
type: "string",
|
|
9201
|
+
description: "Thread id, from context_comments"
|
|
9202
|
+
},
|
|
9203
|
+
reopen: {
|
|
9204
|
+
type: "boolean",
|
|
9205
|
+
description: "Reopen the thread instead of resolving it"
|
|
9206
|
+
}
|
|
9207
|
+
},
|
|
9208
|
+
required: ["thread"]
|
|
9209
|
+
}
|
|
9210
|
+
},
|
|
8657
9211
|
{
|
|
8658
9212
|
name: "context_create",
|
|
8659
9213
|
description: "Create a new knowledge node in the vault. The title must not already be in use \u2014 these tools resolve documents by title, so a duplicate is refused rather than filed as an unreachable twin. To change an existing document use context_update; to refile it use context_move.",
|
|
@@ -9359,8 +9913,8 @@ ${parts.join(
|
|
|
9359
9913
|
node = docs.find((n) => n.id === args.id);
|
|
9360
9914
|
}
|
|
9361
9915
|
if (!node) return `Node not found: ${args.title || args.id}`;
|
|
9362
|
-
if (await
|
|
9363
|
-
return `Node
|
|
9916
|
+
if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
|
|
9917
|
+
return `Node not found: ${args.title || args.id}`;
|
|
9364
9918
|
}
|
|
9365
9919
|
const threads = await listThreads(nestId, node.id);
|
|
9366
9920
|
if (!threads.length)
|
|
@@ -9379,6 +9933,89 @@ ${quote}${comments}`;
|
|
|
9379
9933
|
|
|
9380
9934
|
${sections}`;
|
|
9381
9935
|
}
|
|
9936
|
+
// ── Comments: write half of context_comments ──────────────────────────
|
|
9937
|
+
//
|
|
9938
|
+
// Both cases target the annotation store the UI's comments panel uses, so
|
|
9939
|
+
// an agent's comment and a human's are the same object, and both carry the
|
|
9940
|
+
// side effects the REST routes carry: the @mention fan-out and the derived
|
|
9941
|
+
// `— Annotations` projection an artifact's agents read back.
|
|
9942
|
+
//
|
|
9943
|
+
// PRIMARY permission gate, like context_create's: the /mcp middleware
|
|
9944
|
+
// admits read-tier callers (the transport is always POST), so the check
|
|
9945
|
+
// here is the real one. Commenting is READ-tier — the same rule REST
|
|
9946
|
+
// applies in app.ts (isAnnotationAction) — so canReadNode is the bar, not
|
|
9947
|
+
// write access. Deliberately NOT gated on an approved version: a reviewer
|
|
9948
|
+
// comments on a draft, and unlike reading a thread, writing one discloses
|
|
9949
|
+
// nothing (the caller supplies the text).
|
|
9950
|
+
case "context_comment":
|
|
9951
|
+
case "context_resolve_comment": {
|
|
9952
|
+
const body = String(args.body ?? "");
|
|
9953
|
+
const isResolve = toolName === "context_resolve_comment";
|
|
9954
|
+
if (!isResolve && !body.trim()) {
|
|
9955
|
+
return "body is required \u2014 the comment text.";
|
|
9956
|
+
}
|
|
9957
|
+
const threadId = args.thread ? String(args.thread) : "";
|
|
9958
|
+
if (isResolve && !threadId) {
|
|
9959
|
+
return "thread is required \u2014 the id from context_comments.";
|
|
9960
|
+
}
|
|
9961
|
+
const docs = await storage.discoverDocuments();
|
|
9962
|
+
let node;
|
|
9963
|
+
if (args.title) {
|
|
9964
|
+
node = docs.find(
|
|
9965
|
+
(n) => n.frontmatter.title.toLowerCase() === String(args.title).toLowerCase()
|
|
9966
|
+
);
|
|
9967
|
+
} else if (args.id) {
|
|
9968
|
+
node = docs.find((n) => n.id === String(args.id));
|
|
9969
|
+
}
|
|
9970
|
+
if (!node) {
|
|
9971
|
+
return toolError(
|
|
9972
|
+
`Node not found: ${args.title || args.id || "(no title or id given)"}`,
|
|
9973
|
+
"DOCUMENT_NOT_FOUND"
|
|
9974
|
+
);
|
|
9975
|
+
}
|
|
9976
|
+
if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
|
|
9977
|
+
return toolError(
|
|
9978
|
+
`Node not found: ${args.title || args.id}`,
|
|
9979
|
+
"DOCUMENT_NOT_FOUND"
|
|
9980
|
+
);
|
|
9981
|
+
}
|
|
9982
|
+
const label = node.frontmatter.title || node.id;
|
|
9983
|
+
if (threadId) {
|
|
9984
|
+
const existing = await getThread(threadId);
|
|
9985
|
+
if (!existing || existing.nodeId !== node.id || existing.nestId !== nestId) {
|
|
9986
|
+
return toolError(
|
|
9987
|
+
`Thread ${threadId} is not a thread on "${label}". Call context_comments to list its threads.`,
|
|
9988
|
+
"DOCUMENT_NOT_FOUND"
|
|
9989
|
+
);
|
|
9990
|
+
}
|
|
9991
|
+
}
|
|
9992
|
+
if (isResolve) {
|
|
9993
|
+
const reopen = args.reopen === true;
|
|
9994
|
+
const thread2 = await setThreadStatus(
|
|
9995
|
+
nestId,
|
|
9996
|
+
node.id,
|
|
9997
|
+
threadId,
|
|
9998
|
+
reopen ? "open" : "resolved",
|
|
9999
|
+
userEmail
|
|
10000
|
+
);
|
|
10001
|
+
await syncAnnotationsNode(nestId, node.id, userEmail);
|
|
10002
|
+
return reopen ? `Reopened the thread on "${label}". It is back in the open list for whoever picks it up.` : `Resolved the thread on "${label}", recorded against ${thread2.resolvedBy}.`;
|
|
10003
|
+
}
|
|
10004
|
+
const thread = threadId ? await addComment(nestId, node.id, threadId, userEmail, body) : await createThread(nestId, node.id, { body, anchor: null }, userEmail);
|
|
10005
|
+
await notifyCommentMentions({
|
|
10006
|
+
nestId,
|
|
10007
|
+
nodeId: node.id,
|
|
10008
|
+
body,
|
|
10009
|
+
actorEmail: userEmail,
|
|
10010
|
+
// Without this the notification still reaches the person, but its
|
|
10011
|
+
// "Open document" link falls back to PUBLIC_BASE_URL and comes out
|
|
10012
|
+
// empty when that isn't configured — every other notifying tool in
|
|
10013
|
+
// this file passes it, and the REST route passes requestBaseUrl.
|
|
10014
|
+
baseUrl: ctx.baseUrl
|
|
10015
|
+
});
|
|
10016
|
+
await syncAnnotationsNode(nestId, node.id, userEmail);
|
|
10017
|
+
return threadId ? `Replied on "${label}", in thread ${thread.id}.` : `Commented on "${label}" \u2014 opened thread ${thread.id}. Reply to it by passing that id as \`thread\`.`;
|
|
10018
|
+
}
|
|
9382
10019
|
case "context_create": {
|
|
9383
10020
|
if (!await canCreateInNest(nestId, userEmail)) {
|
|
9384
10021
|
return toolError(
|
|
@@ -9727,21 +10364,20 @@ ${list}${more}
|
|
|
9727
10364
|
Submit one with \`context_submit_review\`.`;
|
|
9728
10365
|
}
|
|
9729
10366
|
case "context_submit_review": {
|
|
9730
|
-
const
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
if (!node) return toolError(`Node not found: ${args.title}`, "DOCUMENT_NOT_FOUND");
|
|
10367
|
+
const resolved = await resolveTargetNode(storage, args);
|
|
10368
|
+
if (typeof resolved === "string") return toolError(resolved, "DOCUMENT_NOT_FOUND");
|
|
10369
|
+
const node = resolved;
|
|
10370
|
+
const nodeLabel = args.title || args.id;
|
|
9735
10371
|
const submitCheck = await canUserEdit(ctx.nestId, node.id, userEmail);
|
|
9736
10372
|
if (!submitCheck.allowed) {
|
|
9737
10373
|
return toolError(
|
|
9738
|
-
`You don't have permission to submit "${
|
|
10374
|
+
`You don't have permission to submit "${nodeLabel}" for review: ${submitCheck.reason}`,
|
|
9739
10375
|
"UNAUTHORIZED_ACTION"
|
|
9740
10376
|
);
|
|
9741
10377
|
}
|
|
9742
10378
|
const currentVersion = await getCurrentVersion(ctx.nestId, node.id);
|
|
9743
10379
|
if (currentVersion === 0)
|
|
9744
|
-
return toolError(`No versions found for "${
|
|
10380
|
+
return toolError(`No versions found for "${nodeLabel}"`, "VERSION_NOT_FOUND");
|
|
9745
10381
|
try {
|
|
9746
10382
|
const request = await submitForReview({
|
|
9747
10383
|
nestId: ctx.nestId,
|
|
@@ -9752,16 +10388,16 @@ Submit one with \`context_submit_review\`.`;
|
|
|
9752
10388
|
priority: args.priority,
|
|
9753
10389
|
baseUrl: ctx.baseUrl
|
|
9754
10390
|
});
|
|
9755
|
-
const
|
|
10391
|
+
const resolved2 = await resolveStewardsForNode(
|
|
9756
10392
|
ctx.nestId,
|
|
9757
10393
|
node.id
|
|
9758
10394
|
);
|
|
9759
|
-
const stewardList =
|
|
10395
|
+
const stewardList = resolved2.length > 0 ? `
|
|
9760
10396
|
|
|
9761
10397
|
**Stewards who can review:**
|
|
9762
|
-
${
|
|
10398
|
+
${resolved2.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "";
|
|
9763
10399
|
return {
|
|
9764
|
-
text: `Submitted "${
|
|
10400
|
+
text: `Submitted "${nodeLabel}" v${currentVersion} for review (${request.priority} priority).${stewardList}`,
|
|
9765
10401
|
// No catalog shape to conform to — `context_submit_review` is a
|
|
9766
10402
|
// governance-namespace operation and that namespace is declared but
|
|
9767
10403
|
// unimplemented in the engine (`api/index.ts`, NAMESPACES). So this
|
|
@@ -9782,7 +10418,7 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
|
|
|
9782
10418
|
requested_by: request.requestedBy,
|
|
9783
10419
|
requested_at: request.requestedAt
|
|
9784
10420
|
},
|
|
9785
|
-
reviewers:
|
|
10421
|
+
reviewers: resolved2.map((r) => r.steward.userEmail)
|
|
9786
10422
|
}
|
|
9787
10423
|
};
|
|
9788
10424
|
} catch (err) {
|
|
@@ -9927,19 +10563,19 @@ ${list}`,
|
|
|
9927
10563
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
9928
10564
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
9929
10565
|
try {
|
|
9930
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
9931
|
-
const db = (await import("./client-
|
|
10566
|
+
const { createGrant: createGrant2 } = await import("./grants-service-UT3EQ3R7.js");
|
|
10567
|
+
const db = (await import("./client-KEY4PYJH.js")).getDb();
|
|
9932
10568
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
9933
10569
|
const e = normalizeEmail2(String(args.email || ""));
|
|
9934
10570
|
if (!e) return "email is required.";
|
|
9935
10571
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
9936
10572
|
if (!row) {
|
|
9937
10573
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
9938
|
-
const { v4:
|
|
9939
|
-
const id =
|
|
10574
|
+
const { v4: uuid20 } = await import("uuid");
|
|
10575
|
+
const id = uuid20();
|
|
9940
10576
|
await db.run(
|
|
9941
10577
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
9942
|
-
[id, e, null, await hashPassword2(
|
|
10578
|
+
[id, e, null, await hashPassword2(uuid20())]
|
|
9943
10579
|
);
|
|
9944
10580
|
row = { id };
|
|
9945
10581
|
}
|
|
@@ -9994,7 +10630,7 @@ ${list}`;
|
|
|
9994
10630
|
|
|
9995
10631
|
// src/mcp/routes.ts
|
|
9996
10632
|
import { z } from "zod";
|
|
9997
|
-
var mcpRoutes = new
|
|
10633
|
+
var mcpRoutes = new Hono15();
|
|
9998
10634
|
async function getUserEmail2(userId) {
|
|
9999
10635
|
const db = getDb();
|
|
10000
10636
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -10060,8 +10696,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
10060
10696
|
});
|
|
10061
10697
|
|
|
10062
10698
|
// src/workflow/run-routes.ts
|
|
10063
|
-
import { Hono as
|
|
10064
|
-
import { v4 as
|
|
10699
|
+
import { Hono as Hono16 } from "hono";
|
|
10700
|
+
import { v4 as uuid13 } from "uuid";
|
|
10065
10701
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
10066
10702
|
var safeJson2 = (s) => {
|
|
10067
10703
|
if (!s) return null;
|
|
@@ -10179,7 +10815,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
10179
10815
|
definitions
|
|
10180
10816
|
};
|
|
10181
10817
|
}
|
|
10182
|
-
var runTriggerRoutes = new
|
|
10818
|
+
var runTriggerRoutes = new Hono16();
|
|
10183
10819
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
10184
10820
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
10185
10821
|
throw new ValidationError(
|
|
@@ -10258,7 +10894,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10258
10894
|
}
|
|
10259
10895
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
10260
10896
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
10261
|
-
const id = `run_${
|
|
10897
|
+
const id = `run_${uuid13()}`;
|
|
10262
10898
|
await getDb().run(
|
|
10263
10899
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
10264
10900
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -10287,7 +10923,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10287
10923
|
201
|
|
10288
10924
|
);
|
|
10289
10925
|
});
|
|
10290
|
-
var runListRoutes = new
|
|
10926
|
+
var runListRoutes = new Hono16();
|
|
10291
10927
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10292
10928
|
const nestId = c.req.param("nestId");
|
|
10293
10929
|
const db = getDb();
|
|
@@ -10319,7 +10955,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
10319
10955
|
}))
|
|
10320
10956
|
});
|
|
10321
10957
|
});
|
|
10322
|
-
var runDetailRoutes = new
|
|
10958
|
+
var runDetailRoutes = new Hono16();
|
|
10323
10959
|
async function loadRunWithPermission(c, runId, required) {
|
|
10324
10960
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
10325
10961
|
runId
|
|
@@ -10470,8 +11106,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10470
11106
|
});
|
|
10471
11107
|
|
|
10472
11108
|
// src/workflow/schedule-routes.ts
|
|
10473
|
-
import { Hono as
|
|
10474
|
-
import { v4 as
|
|
11109
|
+
import { Hono as Hono17 } from "hono";
|
|
11110
|
+
import { v4 as uuid14 } from "uuid";
|
|
10475
11111
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
10476
11112
|
var MIN_EVERY = 5;
|
|
10477
11113
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -10508,7 +11144,7 @@ function parseEvery(v) {
|
|
|
10508
11144
|
}
|
|
10509
11145
|
return n;
|
|
10510
11146
|
}
|
|
10511
|
-
var scheduleRoutes = new
|
|
11147
|
+
var scheduleRoutes = new Hono17();
|
|
10512
11148
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10513
11149
|
const nestId = c.req.param("nestId");
|
|
10514
11150
|
const rows = await getDb().all(
|
|
@@ -10549,7 +11185,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
10549
11185
|
);
|
|
10550
11186
|
}
|
|
10551
11187
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10552
|
-
const id = `sch_${
|
|
11188
|
+
const id = `sch_${uuid14()}`;
|
|
10553
11189
|
await db.run(
|
|
10554
11190
|
`INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
|
|
10555
11191
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -10604,13 +11240,14 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10604
11240
|
});
|
|
10605
11241
|
|
|
10606
11242
|
// src/notify/connector-routes.ts
|
|
10607
|
-
import { Hono as
|
|
10608
|
-
import { v4 as
|
|
11243
|
+
import { Hono as Hono18 } from "hono";
|
|
11244
|
+
import { v4 as uuid15 } from "uuid";
|
|
10609
11245
|
var KNOWN_EVENTS = [
|
|
10610
11246
|
"review_requested",
|
|
10611
11247
|
"review_approved",
|
|
10612
11248
|
"review_rejected",
|
|
10613
|
-
"run_failed"
|
|
11249
|
+
"run_failed",
|
|
11250
|
+
"mention"
|
|
10614
11251
|
];
|
|
10615
11252
|
var CHANNELS = ["slack", "teams", "webhook"];
|
|
10616
11253
|
function toResponse3(r) {
|
|
@@ -10651,7 +11288,7 @@ function parseUrl(v) {
|
|
|
10651
11288
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
10652
11289
|
);
|
|
10653
11290
|
}
|
|
10654
|
-
var connectorRoutes = new
|
|
11291
|
+
var connectorRoutes = new Hono18();
|
|
10655
11292
|
connectorRoutes.get("/", async (c) => {
|
|
10656
11293
|
const nestId = c.req.param("nestId");
|
|
10657
11294
|
const rows = await getDb().all(
|
|
@@ -10670,7 +11307,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
10670
11307
|
const url = parseUrl(body.url);
|
|
10671
11308
|
const events = parseEvents(body.events);
|
|
10672
11309
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10673
|
-
const id = `con_${
|
|
11310
|
+
const id = `con_${uuid15()}`;
|
|
10674
11311
|
await getDb().run(
|
|
10675
11312
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
10676
11313
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -10713,7 +11350,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
10713
11350
|
});
|
|
10714
11351
|
|
|
10715
11352
|
// src/workflow/hook-routes.ts
|
|
10716
|
-
import { Hono as
|
|
11353
|
+
import { Hono as Hono19 } from "hono";
|
|
10717
11354
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
10718
11355
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
10719
11356
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -10736,7 +11373,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
10736
11373
|
created_at: r.created_at
|
|
10737
11374
|
};
|
|
10738
11375
|
}
|
|
10739
|
-
var hookNestRoutes = new
|
|
11376
|
+
var hookNestRoutes = new Hono19();
|
|
10740
11377
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10741
11378
|
const nestId = c.req.param("nestId");
|
|
10742
11379
|
const agent = c.req.query("agent");
|
|
@@ -10810,7 +11447,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
10810
11447
|
function stripMentions(text) {
|
|
10811
11448
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
10812
11449
|
}
|
|
10813
|
-
var hookFireRoutes = new
|
|
11450
|
+
var hookFireRoutes = new Hono19();
|
|
10814
11451
|
hookFireRoutes.post("/:token", async (c) => {
|
|
10815
11452
|
const token = c.req.param("token");
|
|
10816
11453
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -10912,9 +11549,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
10912
11549
|
});
|
|
10913
11550
|
|
|
10914
11551
|
// src/definitions/routes.ts
|
|
10915
|
-
import { Hono as
|
|
10916
|
-
import { v4 as
|
|
10917
|
-
var definitionRoutes = new
|
|
11552
|
+
import { Hono as Hono20 } from "hono";
|
|
11553
|
+
import { v4 as uuid16 } from "uuid";
|
|
11554
|
+
var definitionRoutes = new Hono20();
|
|
10918
11555
|
definitionRoutes.get("/", async (c) => {
|
|
10919
11556
|
const nestId = c.req.param("nestId");
|
|
10920
11557
|
const db = getDb();
|
|
@@ -10993,7 +11630,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
10993
11630
|
await applyUpdate(tx, existing.id);
|
|
10994
11631
|
targetId = existing.id;
|
|
10995
11632
|
} else {
|
|
10996
|
-
targetId =
|
|
11633
|
+
targetId = uuid16();
|
|
10997
11634
|
wasCreated = true;
|
|
10998
11635
|
await tx.run(
|
|
10999
11636
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -11022,11 +11659,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
11022
11659
|
});
|
|
11023
11660
|
|
|
11024
11661
|
// src/nests/asset-routes.ts
|
|
11025
|
-
import { Hono as
|
|
11026
|
-
import { v4 as
|
|
11662
|
+
import { Hono as Hono21 } from "hono";
|
|
11663
|
+
import { v4 as uuid17 } from "uuid";
|
|
11027
11664
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
11028
11665
|
import { join as join4 } from "path";
|
|
11029
|
-
var assetRoutes = new
|
|
11666
|
+
var assetRoutes = new Hono21();
|
|
11030
11667
|
var CONTENT_TYPES = {
|
|
11031
11668
|
png: "image/png",
|
|
11032
11669
|
jpg: "image/jpeg",
|
|
@@ -11072,7 +11709,7 @@ assetRoutes.post("/", async (c) => {
|
|
|
11072
11709
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
11073
11710
|
);
|
|
11074
11711
|
}
|
|
11075
|
-
const name = `${
|
|
11712
|
+
const name = `${uuid17()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
11076
11713
|
const dir = join4(resolveNestPath(nestId), "assets");
|
|
11077
11714
|
await mkdir2(dir, { recursive: true });
|
|
11078
11715
|
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
@@ -11112,12 +11749,125 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
11112
11749
|
});
|
|
11113
11750
|
});
|
|
11114
11751
|
|
|
11752
|
+
// src/auth/community-sites-routes.ts
|
|
11753
|
+
import { Hono as Hono22 } from "hono";
|
|
11754
|
+
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
11755
|
+
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
11756
|
+
var serializeSite = (site) => ({
|
|
11757
|
+
id: site.id,
|
|
11758
|
+
name: site.name,
|
|
11759
|
+
url: site.url,
|
|
11760
|
+
secret_set: !!site.secret,
|
|
11761
|
+
is_active: site.is_active,
|
|
11762
|
+
created_at: site.created_at,
|
|
11763
|
+
updated_at: site.updated_at
|
|
11764
|
+
});
|
|
11765
|
+
var communitySiteRoutes = new Hono22();
|
|
11766
|
+
communitySiteRoutes.get("/", async (c) => {
|
|
11767
|
+
if (!await serverAdminAllowed(c))
|
|
11768
|
+
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
11769
|
+
const sites = await listCommunitySites();
|
|
11770
|
+
return c.json({ community_sites: sites.map(serializeSite) });
|
|
11771
|
+
});
|
|
11772
|
+
communitySiteRoutes.post("/", async (c) => {
|
|
11773
|
+
if (!await serverAdminAllowed(c))
|
|
11774
|
+
return c.json({ error: "Only the server admin can add a community site." }, 403);
|
|
11775
|
+
let body;
|
|
11776
|
+
try {
|
|
11777
|
+
body = await c.req.json();
|
|
11778
|
+
} catch {
|
|
11779
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
11780
|
+
}
|
|
11781
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
11782
|
+
const url = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
|
|
11783
|
+
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
11784
|
+
const is_active = body.is_active === void 0 ? true : !!body.is_active;
|
|
11785
|
+
if (!name) return c.json({ error: "A name is required." }, 400);
|
|
11786
|
+
if (!url || !isHttpUrl(url))
|
|
11787
|
+
return c.json({ error: "A valid http(s) site URL is required." }, 400);
|
|
11788
|
+
if (!secret || secret.length < 16)
|
|
11789
|
+
return c.json({ error: "A secret of at least 16 characters is required." }, 400);
|
|
11790
|
+
try {
|
|
11791
|
+
const site = await createCommunitySite({ name, url, secret, is_active });
|
|
11792
|
+
await loadActiveCommunitySites();
|
|
11793
|
+
return c.json(serializeSite(site), 201);
|
|
11794
|
+
} catch (err) {
|
|
11795
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11796
|
+
if (/unique|duplicate/i.test(msg))
|
|
11797
|
+
return c.json({ error: "A community site with this URL already exists." }, 409);
|
|
11798
|
+
throw err;
|
|
11799
|
+
}
|
|
11800
|
+
});
|
|
11801
|
+
communitySiteRoutes.patch("/:id", async (c) => {
|
|
11802
|
+
if (!await serverAdminAllowed(c))
|
|
11803
|
+
return c.json({ error: "Only the server admin can edit a community site." }, 403);
|
|
11804
|
+
const id = c.req.param("id");
|
|
11805
|
+
let body;
|
|
11806
|
+
try {
|
|
11807
|
+
body = await c.req.json();
|
|
11808
|
+
} catch {
|
|
11809
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
11810
|
+
}
|
|
11811
|
+
const patch = {};
|
|
11812
|
+
if ("name" in body) {
|
|
11813
|
+
const v = typeof body.name === "string" ? body.name.trim() : "";
|
|
11814
|
+
if (!v) return c.json({ error: "A name is required." }, 400);
|
|
11815
|
+
patch.name = v;
|
|
11816
|
+
}
|
|
11817
|
+
if ("url" in body) {
|
|
11818
|
+
const v = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
|
|
11819
|
+
if (!v || !isHttpUrl(v))
|
|
11820
|
+
return c.json({ error: "A valid http(s) site URL is required." }, 400);
|
|
11821
|
+
patch.url = v;
|
|
11822
|
+
}
|
|
11823
|
+
if ("secret" in body) {
|
|
11824
|
+
const v = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
11825
|
+
if (!v || v.length < 16)
|
|
11826
|
+
return c.json({ error: "A secret of at least 16 characters is required." }, 400);
|
|
11827
|
+
patch.secret = v;
|
|
11828
|
+
}
|
|
11829
|
+
if ("is_active" in body) patch.is_active = !!body.is_active;
|
|
11830
|
+
try {
|
|
11831
|
+
const site = await updateCommunitySite(id, patch);
|
|
11832
|
+
if (!site) return c.json({ error: "Community site not found." }, 404);
|
|
11833
|
+
await loadActiveCommunitySites();
|
|
11834
|
+
return c.json(serializeSite(site));
|
|
11835
|
+
} catch (err) {
|
|
11836
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11837
|
+
if (/unique|duplicate/i.test(msg))
|
|
11838
|
+
return c.json({ error: "A community site with this URL already exists." }, 409);
|
|
11839
|
+
throw err;
|
|
11840
|
+
}
|
|
11841
|
+
});
|
|
11842
|
+
communitySiteRoutes.delete("/:id", async (c) => {
|
|
11843
|
+
if (!await serverAdminAllowed(c))
|
|
11844
|
+
return c.json({ error: "Only the server admin can remove a community site." }, 403);
|
|
11845
|
+
const id = c.req.param("id");
|
|
11846
|
+
const removed = await deleteCommunitySite(id);
|
|
11847
|
+
if (!removed) return c.json({ error: "Community site not found." }, 404);
|
|
11848
|
+
await loadActiveCommunitySites();
|
|
11849
|
+
return c.json({ ok: true, id });
|
|
11850
|
+
});
|
|
11851
|
+
|
|
11115
11852
|
// src/mcp/server-routes.ts
|
|
11116
|
-
import { Hono as
|
|
11853
|
+
import { Hono as Hono23 } from "hono";
|
|
11117
11854
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11118
11855
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
11119
11856
|
import { z as z2 } from "zod";
|
|
11120
|
-
var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
11857
|
+
var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
11858
|
+
"nest_create",
|
|
11859
|
+
"context_create",
|
|
11860
|
+
"context_update",
|
|
11861
|
+
"context_delete",
|
|
11862
|
+
"context_move",
|
|
11863
|
+
"context_submit_review"
|
|
11864
|
+
]);
|
|
11865
|
+
var FANOUT_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
11866
|
+
"context_search",
|
|
11867
|
+
"context_list",
|
|
11868
|
+
"context_query",
|
|
11869
|
+
"context_overview"
|
|
11870
|
+
]);
|
|
11121
11871
|
async function getUserEmail3(userId) {
|
|
11122
11872
|
const db = getDb();
|
|
11123
11873
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
@@ -11173,7 +11923,9 @@ async function buildNestIndex(userId, nestScope) {
|
|
|
11173
11923
|
let document_count = null;
|
|
11174
11924
|
try {
|
|
11175
11925
|
const { storage } = await engineCache.get(n.id);
|
|
11176
|
-
document_count = (await storage.discoverDocuments()).
|
|
11926
|
+
document_count = (await storage.discoverDocuments()).filter(
|
|
11927
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
11928
|
+
).length;
|
|
11177
11929
|
} catch {
|
|
11178
11930
|
}
|
|
11179
11931
|
return {
|
|
@@ -11215,77 +11967,83 @@ var NEST_ARG = {
|
|
|
11215
11967
|
description: "Target nest \u2014 id, or exact name/slug (case-insensitive)"
|
|
11216
11968
|
}
|
|
11217
11969
|
};
|
|
11970
|
+
var NEST_ARG_OPTIONAL = {
|
|
11971
|
+
nest: {
|
|
11972
|
+
type: "string",
|
|
11973
|
+
description: "Target nest \u2014 id or name. Omit to search/list across ALL accessible nests."
|
|
11974
|
+
}
|
|
11975
|
+
};
|
|
11218
11976
|
var SERVER_TOOL_DEFINITIONS = [
|
|
11219
11977
|
{
|
|
11220
11978
|
name: "nest_index",
|
|
11221
|
-
description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST
|
|
11979
|
+
description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST.",
|
|
11222
11980
|
inputSchema: { type: "object", properties: {} }
|
|
11223
11981
|
},
|
|
11982
|
+
// ─── Fan-out reads (nest optional) ─────────────────────────────────────
|
|
11224
11983
|
{
|
|
11225
11984
|
name: "context_query",
|
|
11226
|
-
description: "Run a structured selector query
|
|
11985
|
+
description: "Run a structured selector query. Omit `nest` to query ALL accessible nests. Selector: #tag, type:X, [[Title]], scope:X, +AND, |OR, -NOT.",
|
|
11227
11986
|
inputSchema: {
|
|
11228
11987
|
type: "object",
|
|
11229
11988
|
properties: {
|
|
11230
|
-
...
|
|
11989
|
+
...NEST_ARG_OPTIONAL,
|
|
11231
11990
|
query: { type: "string", description: "Selector query" },
|
|
11232
11991
|
hops: {
|
|
11233
11992
|
type: "number",
|
|
11234
11993
|
description: "Graph traversal depth from the matches (default: 2)"
|
|
11235
11994
|
}
|
|
11236
11995
|
},
|
|
11237
|
-
required: ["
|
|
11996
|
+
required: ["query"]
|
|
11238
11997
|
}
|
|
11239
11998
|
},
|
|
11240
11999
|
{
|
|
11241
12000
|
name: "context_search",
|
|
11242
|
-
description: "Full-text keyword search
|
|
12001
|
+
description: "Full-text keyword search. Omit `nest` to search ALL accessible nests. Returns count and first `limit` hits.",
|
|
11243
12002
|
inputSchema: {
|
|
11244
12003
|
type: "object",
|
|
11245
12004
|
properties: {
|
|
11246
|
-
...
|
|
12005
|
+
...NEST_ARG_OPTIONAL,
|
|
11247
12006
|
query: { type: "string", description: "Search terms" },
|
|
11248
12007
|
limit: {
|
|
11249
12008
|
type: "number",
|
|
11250
12009
|
description: "Max hits to return (default 20, max 200)"
|
|
11251
12010
|
}
|
|
11252
12011
|
},
|
|
11253
|
-
required: ["
|
|
11254
|
-
}
|
|
11255
|
-
},
|
|
11256
|
-
{
|
|
11257
|
-
name: "context_get",
|
|
11258
|
-
description: "Get the FULL content of a node in a nest by title or id.",
|
|
11259
|
-
inputSchema: {
|
|
11260
|
-
type: "object",
|
|
11261
|
-
properties: {
|
|
11262
|
-
...NEST_ARG,
|
|
11263
|
-
title: { type: "string", description: "Title of the node" },
|
|
11264
|
-
id: { type: "string", description: "ID of the node" }
|
|
11265
|
-
},
|
|
11266
|
-
required: ["nest"]
|
|
12012
|
+
required: ["query"]
|
|
11267
12013
|
}
|
|
11268
12014
|
},
|
|
11269
12015
|
{
|
|
11270
12016
|
name: "context_list",
|
|
11271
|
-
description: "Browse
|
|
12017
|
+
description: "Browse contents with optional type, tag, or limit filters. Omit `nest` to list ALL accessible nests.",
|
|
11272
12018
|
inputSchema: {
|
|
11273
12019
|
type: "object",
|
|
11274
12020
|
properties: {
|
|
11275
|
-
...
|
|
12021
|
+
...NEST_ARG_OPTIONAL,
|
|
11276
12022
|
type: { type: "string", description: "Filter by node type" },
|
|
11277
12023
|
tag: { type: "string", description: "Filter by tag" },
|
|
11278
12024
|
limit: { type: "number", description: "Max nodes to return" }
|
|
11279
|
-
}
|
|
11280
|
-
required: ["nest"]
|
|
12025
|
+
}
|
|
11281
12026
|
}
|
|
11282
12027
|
},
|
|
11283
12028
|
{
|
|
11284
12029
|
name: "context_overview",
|
|
11285
|
-
description: "Map of
|
|
12030
|
+
description: "Map of nests: node count, types, tags, title+snippet per node. Omit `nest` for ALL accessible nests.",
|
|
12031
|
+
inputSchema: {
|
|
12032
|
+
type: "object",
|
|
12033
|
+
properties: { ...NEST_ARG_OPTIONAL }
|
|
12034
|
+
}
|
|
12035
|
+
},
|
|
12036
|
+
// ─── Targeted reads (nest required) ────────────────────────────────────
|
|
12037
|
+
{
|
|
12038
|
+
name: "context_get",
|
|
12039
|
+
description: "Get the FULL content of a node in a nest by title or id.",
|
|
11286
12040
|
inputSchema: {
|
|
11287
12041
|
type: "object",
|
|
11288
|
-
properties: {
|
|
12042
|
+
properties: {
|
|
12043
|
+
...NEST_ARG,
|
|
12044
|
+
title: { type: "string", description: "Title of the node" },
|
|
12045
|
+
id: { type: "string", description: "ID of the node" }
|
|
12046
|
+
},
|
|
11289
12047
|
required: ["nest"]
|
|
11290
12048
|
}
|
|
11291
12049
|
},
|
|
@@ -11309,6 +12067,22 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
11309
12067
|
required: ["nest", "selector"]
|
|
11310
12068
|
}
|
|
11311
12069
|
},
|
|
12070
|
+
{
|
|
12071
|
+
// Read-only, so it belongs on this endpoint: an agent reading a nest's
|
|
12072
|
+
// documents across nests also needs the feedback left on them. Writing a
|
|
12073
|
+
// comment stays on the per-nest endpoint with the other write tools.
|
|
12074
|
+
name: "context_comments",
|
|
12075
|
+
description: "Get the review comments/annotations on a document in a nest \u2014 anchored quotes, threaded replies, and open/resolved status. Use to pull human feedback into context before acting on a document.",
|
|
12076
|
+
inputSchema: {
|
|
12077
|
+
type: "object",
|
|
12078
|
+
properties: {
|
|
12079
|
+
nest: { type: "string", description: "Nest name or id (see nest_index)" },
|
|
12080
|
+
title: { type: "string", description: "Title of the node" },
|
|
12081
|
+
id: { type: "string", description: "ID of the node" }
|
|
12082
|
+
},
|
|
12083
|
+
required: ["nest"]
|
|
12084
|
+
}
|
|
12085
|
+
},
|
|
11312
12086
|
{
|
|
11313
12087
|
name: "nest_create",
|
|
11314
12088
|
description: "Create a new nest owned by the caller. Returns the new nest's id \u2014 target it with the other tools afterwards.",
|
|
@@ -11321,9 +12095,10 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
11321
12095
|
required: ["name"]
|
|
11322
12096
|
}
|
|
11323
12097
|
},
|
|
12098
|
+
// ─── Writes (nest required) ────────────────────────────────────────────
|
|
11324
12099
|
{
|
|
11325
12100
|
name: "context_create",
|
|
11326
|
-
description: "Create a new
|
|
12101
|
+
description: "Create a new document in a nest. Needs write permission. A `folder` path creates the folder implicitly.",
|
|
11327
12102
|
inputSchema: {
|
|
11328
12103
|
type: "object",
|
|
11329
12104
|
properties: {
|
|
@@ -11340,6 +12115,66 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
11340
12115
|
},
|
|
11341
12116
|
required: ["nest", "title", "content"]
|
|
11342
12117
|
}
|
|
12118
|
+
},
|
|
12119
|
+
{
|
|
12120
|
+
name: "context_update",
|
|
12121
|
+
description: "Update an existing node by title or id. Needs write permission. Use `content` to replace, `append` to add.",
|
|
12122
|
+
inputSchema: {
|
|
12123
|
+
type: "object",
|
|
12124
|
+
properties: {
|
|
12125
|
+
...NEST_ARG,
|
|
12126
|
+
title: { type: "string", description: "Title of node to update" },
|
|
12127
|
+
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12128
|
+
content: { type: "string", description: "New content (replaces)" },
|
|
12129
|
+
append: { type: "string", description: "Content to append" },
|
|
12130
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
|
|
12131
|
+
status: { type: "string", description: "New status: draft|pending_review|approved|published" },
|
|
12132
|
+
changeNote: { type: "string", description: "Why this changed \u2014 recorded in history" }
|
|
12133
|
+
},
|
|
12134
|
+
required: ["nest"]
|
|
12135
|
+
}
|
|
12136
|
+
},
|
|
12137
|
+
{
|
|
12138
|
+
name: "context_delete",
|
|
12139
|
+
description: "Delete a node by title or id. Needs write permission. Irreversible.",
|
|
12140
|
+
inputSchema: {
|
|
12141
|
+
type: "object",
|
|
12142
|
+
properties: {
|
|
12143
|
+
...NEST_ARG,
|
|
12144
|
+
title: { type: "string", description: "Title of node to delete" },
|
|
12145
|
+
id: { type: "string", description: "Node id (use when title is ambiguous)" }
|
|
12146
|
+
},
|
|
12147
|
+
required: ["nest"]
|
|
12148
|
+
}
|
|
12149
|
+
},
|
|
12150
|
+
{
|
|
12151
|
+
name: "context_move",
|
|
12152
|
+
description: "Move a node to a different folder. The id changes; history and links follow. Needs write permission.",
|
|
12153
|
+
inputSchema: {
|
|
12154
|
+
type: "object",
|
|
12155
|
+
properties: {
|
|
12156
|
+
...NEST_ARG,
|
|
12157
|
+
title: { type: "string", description: "Title of node to move" },
|
|
12158
|
+
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12159
|
+
folder: { type: "string", description: 'Destination folder (e.g. "gtm/deals"); "" for root' }
|
|
12160
|
+
},
|
|
12161
|
+
required: ["nest", "folder"]
|
|
12162
|
+
}
|
|
12163
|
+
},
|
|
12164
|
+
{
|
|
12165
|
+
name: "context_submit_review",
|
|
12166
|
+
description: "Submit a node for steward review. Stewards will be notified. Use on governed nests.",
|
|
12167
|
+
inputSchema: {
|
|
12168
|
+
type: "object",
|
|
12169
|
+
properties: {
|
|
12170
|
+
...NEST_ARG,
|
|
12171
|
+
title: { type: "string", description: "Title of node to submit" },
|
|
12172
|
+
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12173
|
+
note: { type: "string", description: "Note for the reviewer" },
|
|
12174
|
+
priority: { type: "string", description: "Priority: low, normal, high, urgent" }
|
|
12175
|
+
},
|
|
12176
|
+
required: ["nest"]
|
|
12177
|
+
}
|
|
11343
12178
|
}
|
|
11344
12179
|
];
|
|
11345
12180
|
async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl) {
|
|
@@ -11373,8 +12208,49 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
11373
12208
|
throw err;
|
|
11374
12209
|
}
|
|
11375
12210
|
}
|
|
11376
|
-
const
|
|
11377
|
-
|
|
12211
|
+
const nestArg = args.nest;
|
|
12212
|
+
if (!nestArg && FANOUT_READ_TOOLS.has(toolName)) {
|
|
12213
|
+
const nests = await buildNestIndex(userId, nestScope);
|
|
12214
|
+
if (!nests.length) return "No nests accessible with these credentials.";
|
|
12215
|
+
const nonEmpty = nests.filter((n) => (n.document_count ?? 0) > 0);
|
|
12216
|
+
if (!nonEmpty.length) {
|
|
12217
|
+
return "All accessible nests are empty. Use context_create to add documents first.";
|
|
12218
|
+
}
|
|
12219
|
+
const results = await Promise.allSettled(
|
|
12220
|
+
nonEmpty.map(async (n) => {
|
|
12221
|
+
const engine2 = await engineCache.get(n.id);
|
|
12222
|
+
const result = await handleToolCall(toolName, args, {
|
|
12223
|
+
storage: engine2.storage,
|
|
12224
|
+
queryEngine: engine2.query,
|
|
12225
|
+
versionManager: engine2.versions,
|
|
12226
|
+
nestId: n.id,
|
|
12227
|
+
userId,
|
|
12228
|
+
userEmail,
|
|
12229
|
+
baseUrl
|
|
12230
|
+
});
|
|
12231
|
+
return { nest: n, result };
|
|
12232
|
+
})
|
|
12233
|
+
);
|
|
12234
|
+
const parts = [];
|
|
12235
|
+
for (const r of results) {
|
|
12236
|
+
if (r.status === "fulfilled") {
|
|
12237
|
+
const { nest: n, result } = r.value;
|
|
12238
|
+
const text = typeof result === "string" ? result : result.text;
|
|
12239
|
+
parts.push(`## ${n.name} (${n.id})
|
|
12240
|
+
|
|
12241
|
+
${text}`);
|
|
12242
|
+
}
|
|
12243
|
+
}
|
|
12244
|
+
if (!parts.length) {
|
|
12245
|
+
return "No results found across any accessible nests.";
|
|
12246
|
+
}
|
|
12247
|
+
return parts.join("\n\n---\n\n");
|
|
12248
|
+
}
|
|
12249
|
+
const inaccessible = `Nest not found or not accessible: "${nestArg}". Use nest_index to see what these credentials can read.`;
|
|
12250
|
+
if (!nestArg) {
|
|
12251
|
+
return "This tool requires a `nest` argument. Use nest_index to see available nests.";
|
|
12252
|
+
}
|
|
12253
|
+
const nest = await resolveNestRef(String(nestArg), userId);
|
|
11378
12254
|
if (!nest) return inaccessible;
|
|
11379
12255
|
if (nestScope && nest.id !== nestScope) return inaccessible;
|
|
11380
12256
|
if (!await canRead(nest.id, userId)) return inaccessible;
|
|
@@ -11421,7 +12297,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
11421
12297
|
}
|
|
11422
12298
|
return server;
|
|
11423
12299
|
}
|
|
11424
|
-
var serverMcpRoutes = new
|
|
12300
|
+
var serverMcpRoutes = new Hono23();
|
|
11425
12301
|
serverMcpRoutes.all("/", async (c) => {
|
|
11426
12302
|
const userId = c.get("userId");
|
|
11427
12303
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -11443,7 +12319,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
11443
12319
|
await server.server.close();
|
|
11444
12320
|
}
|
|
11445
12321
|
});
|
|
11446
|
-
var nestIndexRoutes = new
|
|
12322
|
+
var nestIndexRoutes = new Hono23();
|
|
11447
12323
|
nestIndexRoutes.get("/", async (c) => {
|
|
11448
12324
|
const userId = c.get("userId");
|
|
11449
12325
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -11516,26 +12392,47 @@ Other read endpoints (all under \`${B}/nests/<nest-id>\`):
|
|
|
11516
12392
|
- \`GET /nodes/<node-id>?format=raw\` \u2014 direct consumption path: the node BODY as plain text (no JSON envelope; add \`&frontmatter=1\` for the stored YAML block)
|
|
11517
12393
|
- \`GET /export?format=markdown\` \u2014 whole nest as one markdown file (optional \`selector\`, \`max_tokens\`)
|
|
11518
12394
|
- \`GET /graph\` \u2014 ontology graph (nodes, links, tags, stewards)
|
|
12395
|
+
- \`GET /folders\` \u2014 folder tree; \`GET /nodes?folder=<path>\` \u2014 one folder level
|
|
12396
|
+
- \`POST /table-query\` \u2014 exact rows from a table node: \`{ "table": "[[Title]]", "query": "where <col> <op> <value> [select cols] [limit N]" }\`
|
|
12397
|
+
- \`GET /nodes/<node-id>/versions\` \u2014 history (who, when, approval state); \`/versions/<n>\` \u2014 one version
|
|
12398
|
+
- \`GET /nodes/<node-id>/comments\` \u2014 review threads (anchored quotes, replies, open/resolved)
|
|
11519
12399
|
|
|
11520
12400
|
Write endpoints (need write permission):
|
|
11521
12401
|
- \`POST /nodes\` \u2014 \`{ "title", "content", "tags"?, "type"?, "folder"? }\`
|
|
11522
12402
|
- \`PATCH /nodes/<node-id>\` \u2014 \`{ "content"? , "append"?, "tags"?, "title"? }\`
|
|
11523
|
-
- \`DELETE /nodes/<node-id
|
|
11524
|
-
-
|
|
12403
|
+
- \`DELETE /nodes/<node-id>\`; \`POST /nodes/<id>/request-deletion\` when you lack delete rights
|
|
12404
|
+
- \`POST /nodes/<id>/move\` \u2014 \`{ "folder" }\`; history and comments follow
|
|
12405
|
+
- \`POST /nodes/<id>/revert\` \u2014 \`{ "targetVersion" }\` restores a past version as a new one
|
|
12406
|
+
- \`POST /nodes/<id>/comments\` \u2014 leave or reply to a review comment
|
|
12407
|
+
- Governance: \`POST /nodes/<id>/submit-review\`, \`/approve\`, \`/reject\`, \`/cancel-review\`
|
|
12408
|
+
- \`GET /review-queue\` \u2014 what is waiting on a steward in this nest
|
|
12409
|
+
|
|
12410
|
+
## Your inbox (server-level)
|
|
12411
|
+
|
|
12412
|
+
- \`GET ${B}/me/work\` \u2014 reviews waiting on you and handoffs, across every nest
|
|
12413
|
+
- \`GET ${B}/me/drafts\` \u2014 your documents never submitted for review
|
|
12414
|
+
- \`GET ${B}/me/notifications\` \u2014 watcher and review notifications
|
|
11525
12415
|
|
|
11526
12416
|
## MCP (Model Context Protocol)
|
|
11527
12417
|
|
|
11528
12418
|
Streamable HTTP, stateless. Two endpoints:
|
|
11529
12419
|
|
|
11530
|
-
- \`${B}/mcp\` \u2014 server-level, one connection for everything
|
|
11531
|
-
Tools: \`nest_index\` (call first), then
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, full toolset
|
|
11538
|
-
|
|
12420
|
+
- \`${B}/mcp\` \u2014 server-level, one connection for everything.
|
|
12421
|
+
Tools: \`nest_index\` (call first), then:
|
|
12422
|
+
- Fan-out reads (omit \`nest\` to search/list ALL nests): \`context_search\`,
|
|
12423
|
+
\`context_list\`, \`context_query\`, \`context_overview\`
|
|
12424
|
+
- Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`
|
|
12425
|
+
- Writes (\`nest\` required): \`nest_create\`, \`context_create\`, \`context_update\`,
|
|
12426
|
+
\`context_delete\`, \`context_move\`, \`context_submit_review\`
|
|
12427
|
+
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, the full toolset. Same names as the
|
|
12428
|
+
server-level tools without the \`nest\` argument, plus:
|
|
12429
|
+
- read: \`context_init\` (operating instructions \u2014 call first), \`context_export\`, \`context_table_query\`, \`context_versions\`
|
|
12430
|
+
- write: \`context_update\`, \`context_delete\`, \`context_move\`, \`context_request_deletion\`
|
|
12431
|
+
- review: \`context_submit_review\`, \`context_approve\`, \`context_reject\`, \`context_review_queue\`, \`context_my_drafts\`
|
|
12432
|
+
- comments: \`context_comment\`, \`context_resolve_comment\`
|
|
12433
|
+
- people: \`context_stewards\`, \`context_assign_steward\`, \`context_share_nest\`, \`context_grant\`
|
|
12434
|
+
- admin: \`context_deletion_queue\`, \`context_resolve_deletion\`, \`context_unsynced_list\`, \`context_sync_folder\`
|
|
12435
|
+
- \`workflow_*\` (runs, edges, edge types) when the nest's workflow plane is on
|
|
11539
12436
|
Sharing a slice is one call: \`context_grant\` with
|
|
11540
12437
|
\`{ "email", "target_type": "document" | "folder", "target", "role": "read" | "write" }\`
|
|
11541
12438
|
\u2014 \`target\` is a node id (\`nodes/win-loss-log\`) or a folder prefix
|
|
@@ -11582,14 +12479,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
11582
12479
|
through steward review before they become AI-visible.
|
|
11583
12480
|
`;
|
|
11584
12481
|
}
|
|
11585
|
-
var llmsTxtRoutes = new
|
|
12482
|
+
var llmsTxtRoutes = new Hono23();
|
|
11586
12483
|
llmsTxtRoutes.get("/", (c) => {
|
|
11587
12484
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
11588
12485
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
11589
12486
|
});
|
|
11590
12487
|
|
|
11591
12488
|
// src/governance/routes.ts
|
|
11592
|
-
import { Hono as
|
|
12489
|
+
import { Hono as Hono24 } from "hono";
|
|
11593
12490
|
|
|
11594
12491
|
// src/governance/feed-service.ts
|
|
11595
12492
|
var SNIPPET_CHARS = 200;
|
|
@@ -11720,7 +12617,7 @@ async function getNestFeed(params) {
|
|
|
11720
12617
|
}
|
|
11721
12618
|
|
|
11722
12619
|
// src/governance/comment-service.ts
|
|
11723
|
-
import { v4 as
|
|
12620
|
+
import { v4 as uuid18 } from "uuid";
|
|
11724
12621
|
async function createComment(params) {
|
|
11725
12622
|
const db = getDb();
|
|
11726
12623
|
const body = (params.body ?? "").trim();
|
|
@@ -11736,7 +12633,7 @@ async function createComment(params) {
|
|
|
11736
12633
|
throw new Error("Parent comment not found on this node");
|
|
11737
12634
|
}
|
|
11738
12635
|
}
|
|
11739
|
-
const id =
|
|
12636
|
+
const id = uuid18();
|
|
11740
12637
|
await db.run(
|
|
11741
12638
|
`INSERT INTO comments
|
|
11742
12639
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -12021,7 +12918,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
12021
12918
|
if (Number.isNaN(n) || n < 1) return def;
|
|
12022
12919
|
return Math.min(n, max);
|
|
12023
12920
|
}
|
|
12024
|
-
var governanceRoutes = new
|
|
12921
|
+
var governanceRoutes = new Hono24();
|
|
12025
12922
|
governanceRoutes.get("/stewards", async (c) => {
|
|
12026
12923
|
const nestId = c.req.param("nestId");
|
|
12027
12924
|
const scope = c.req.query("scope");
|
|
@@ -12374,6 +13271,7 @@ governanceRoutes.get("/external-edits", async (c) => {
|
|
|
12374
13271
|
await scanNestForDrift(nestId, "user:refresh");
|
|
12375
13272
|
}
|
|
12376
13273
|
const entries = await listNestExternalEdits(nestId);
|
|
13274
|
+
if (c.req.query("count") === "1") return c.json({ total: entries.length });
|
|
12377
13275
|
return c.json({ entries, total: entries.length });
|
|
12378
13276
|
});
|
|
12379
13277
|
governanceRoutes.post("/external-edits/scan", async (c) => {
|
|
@@ -12388,7 +13286,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
12388
13286
|
const activity = await getActivity({ nestId, limit });
|
|
12389
13287
|
return c.json({ activity });
|
|
12390
13288
|
});
|
|
12391
|
-
var governanceNodeRoutes = new
|
|
13289
|
+
var governanceNodeRoutes = new Hono24();
|
|
12392
13290
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
12393
13291
|
const nestId = c.req.param("nestId");
|
|
12394
13292
|
const nodeId = c.req.param("nodeId");
|
|
@@ -12520,8 +13418,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
|
|
|
12520
13418
|
}
|
|
12521
13419
|
const body = await c.req.json();
|
|
12522
13420
|
const author = await getUserEmail4(c);
|
|
13421
|
+
let comment;
|
|
12523
13422
|
try {
|
|
12524
|
-
|
|
13423
|
+
comment = await createComment({
|
|
12525
13424
|
nestId,
|
|
12526
13425
|
nodeId,
|
|
12527
13426
|
author,
|
|
@@ -12530,12 +13429,19 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
|
|
|
12530
13429
|
anchor: body.anchor,
|
|
12531
13430
|
parentId: body.parentId
|
|
12532
13431
|
});
|
|
12533
|
-
return c.json({ comment }, 201);
|
|
12534
13432
|
} catch (err) {
|
|
12535
13433
|
throw new ValidationError(
|
|
12536
13434
|
err instanceof Error ? err.message : String(err)
|
|
12537
13435
|
);
|
|
12538
13436
|
}
|
|
13437
|
+
await notifyCommentMentions({
|
|
13438
|
+
nestId,
|
|
13439
|
+
nodeId,
|
|
13440
|
+
body: comment.body,
|
|
13441
|
+
actorEmail: author,
|
|
13442
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
13443
|
+
});
|
|
13444
|
+
return c.json({ comment }, 201);
|
|
12539
13445
|
});
|
|
12540
13446
|
governanceNodeRoutes.post(
|
|
12541
13447
|
"/:nodeId{.+}/comments/:commentId/resolve",
|
|
@@ -12832,6 +13738,21 @@ async function ensureAnonymousUser() {
|
|
|
12832
13738
|
return ANON_USER_ID;
|
|
12833
13739
|
}
|
|
12834
13740
|
|
|
13741
|
+
// src/shared/posthog-key.ts
|
|
13742
|
+
var POSTHOG_KEY_MIN_TOKEN = 20;
|
|
13743
|
+
function posthogKeyError(value) {
|
|
13744
|
+
const v = value.trim();
|
|
13745
|
+
if (!v) return null;
|
|
13746
|
+
if (!v.startsWith("phc_"))
|
|
13747
|
+
return "PostHog project key must start with phc_ \u2014 copy the project API key from PostHog settings.";
|
|
13748
|
+
const token = v.slice(4);
|
|
13749
|
+
if (!/^[A-Za-z0-9]*$/.test(token))
|
|
13750
|
+
return "PostHog project key may only contain letters and digits after phc_ \u2014 check for spaces or punctuation.";
|
|
13751
|
+
if (token.length < POSTHOG_KEY_MIN_TOKEN)
|
|
13752
|
+
return `PostHog project key looks too short \u2014 a real key is about 47 characters long (phc_ plus ${POSTHOG_KEY_MIN_TOKEN}+ letters and digits). Copy the whole key from PostHog settings.`;
|
|
13753
|
+
return null;
|
|
13754
|
+
}
|
|
13755
|
+
|
|
12835
13756
|
// src/app.ts
|
|
12836
13757
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
12837
13758
|
import { fileURLToPath } from "url";
|
|
@@ -12953,7 +13874,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
12953
13874
|
return ok;
|
|
12954
13875
|
}
|
|
12955
13876
|
function createApp() {
|
|
12956
|
-
const app = new
|
|
13877
|
+
const app = new Hono25({ router: new LinearRouter() });
|
|
12957
13878
|
const corsOrigins = config.CORS_ORIGINS;
|
|
12958
13879
|
app.use(
|
|
12959
13880
|
"*",
|
|
@@ -13134,7 +14055,7 @@ function createApp() {
|
|
|
13134
14055
|
return c.json({ error: msg }, 500);
|
|
13135
14056
|
}
|
|
13136
14057
|
});
|
|
13137
|
-
const
|
|
14058
|
+
const serverAdminAllowed2 = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
13138
14059
|
app.use("/admin/settings", flexAuthMiddleware);
|
|
13139
14060
|
app.use("/admin/settings/*", flexAuthMiddleware);
|
|
13140
14061
|
const currentServerSettings = () => ({
|
|
@@ -13163,6 +14084,7 @@ function createApp() {
|
|
|
13163
14084
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
13164
14085
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
13165
14086
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
14087
|
+
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
13166
14088
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
13167
14089
|
// presence is reported, never the value (same discipline as the OIDC secret).
|
|
13168
14090
|
sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
|
|
@@ -13187,12 +14109,12 @@ function createApp() {
|
|
|
13187
14109
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
13188
14110
|
});
|
|
13189
14111
|
app.get("/admin/settings", async (c) => {
|
|
13190
|
-
if (!await
|
|
14112
|
+
if (!await serverAdminAllowed2(c))
|
|
13191
14113
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13192
14114
|
return c.json(currentServerSettings());
|
|
13193
14115
|
});
|
|
13194
14116
|
app.post("/admin/settings/test-notification", async (c) => {
|
|
13195
|
-
if (!await
|
|
14117
|
+
if (!await serverAdminAllowed2(c))
|
|
13196
14118
|
return c.json({ error: "Only the server admin can do this." }, 403);
|
|
13197
14119
|
let body;
|
|
13198
14120
|
try {
|
|
@@ -13206,7 +14128,7 @@ function createApp() {
|
|
|
13206
14128
|
return result.ok ? c.json({ ok: true }) : c.json({ error: result.error }, 502);
|
|
13207
14129
|
});
|
|
13208
14130
|
app.patch("/admin/settings", async (c) => {
|
|
13209
|
-
if (!await
|
|
14131
|
+
if (!await serverAdminAllowed2(c))
|
|
13210
14132
|
return c.json({ error: "Only the server admin can change this." }, 403);
|
|
13211
14133
|
let body;
|
|
13212
14134
|
try {
|
|
@@ -13244,7 +14166,9 @@ function createApp() {
|
|
|
13244
14166
|
}
|
|
13245
14167
|
if ("posthog_key" in body) {
|
|
13246
14168
|
const v = String(body.posthog_key ?? "").trim();
|
|
13247
|
-
|
|
14169
|
+
const keyError = posthogKeyError(v);
|
|
14170
|
+
if (keyError) addError("posthog_key", keyError);
|
|
14171
|
+
else pending.push({ name: "POSTHOG_KEY", value: v || null });
|
|
13248
14172
|
}
|
|
13249
14173
|
if ("posthog_host" in body) {
|
|
13250
14174
|
const v = String(body.posthog_host ?? "").trim().replace(/\/$/, "");
|
|
@@ -13383,6 +14307,12 @@ function createApp() {
|
|
|
13383
14307
|
value: body.oidc_department_tagging ? "true" : null
|
|
13384
14308
|
});
|
|
13385
14309
|
}
|
|
14310
|
+
if ("people_suggest_all_users" in body) {
|
|
14311
|
+
pending.push({
|
|
14312
|
+
name: "PEOPLE_SUGGEST_ALL_USERS",
|
|
14313
|
+
value: body.people_suggest_all_users ? "true" : null
|
|
14314
|
+
});
|
|
14315
|
+
}
|
|
13386
14316
|
if ("sso_token_exchange_enabled" in body) {
|
|
13387
14317
|
pending.push({
|
|
13388
14318
|
name: "SSO_TOKEN_EXCHANGE_ENABLED",
|
|
@@ -13551,7 +14481,7 @@ function createApp() {
|
|
|
13551
14481
|
});
|
|
13552
14482
|
app.use("/admin/trace", flexAuthMiddleware);
|
|
13553
14483
|
app.get("/admin/trace", async (c) => {
|
|
13554
|
-
if (!await
|
|
14484
|
+
if (!await serverAdminAllowed2(c))
|
|
13555
14485
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13556
14486
|
const limitRaw = Number(c.req.query("limit"));
|
|
13557
14487
|
const offsetRaw = Number(c.req.query("offset"));
|
|
@@ -13569,7 +14499,7 @@ function createApp() {
|
|
|
13569
14499
|
app.use("/admin/super-admins", flexAuthMiddleware);
|
|
13570
14500
|
app.use("/admin/super-admins/*", flexAuthMiddleware);
|
|
13571
14501
|
app.get("/admin/super-admins", async (c) => {
|
|
13572
|
-
if (!await
|
|
14502
|
+
if (!await serverAdminAllowed2(c))
|
|
13573
14503
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13574
14504
|
const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
|
|
13575
14505
|
const byEmail = /* @__PURE__ */ new Map();
|
|
@@ -13595,7 +14525,7 @@ function createApp() {
|
|
|
13595
14525
|
return c.json({ super_admins: [...byEmail.values()] });
|
|
13596
14526
|
});
|
|
13597
14527
|
app.post("/admin/super-admins", async (c) => {
|
|
13598
|
-
if (!await
|
|
14528
|
+
if (!await serverAdminAllowed2(c))
|
|
13599
14529
|
return c.json({ error: "Only the server admin can grant superadmin." }, 403);
|
|
13600
14530
|
let body;
|
|
13601
14531
|
try {
|
|
@@ -13640,7 +14570,7 @@ function createApp() {
|
|
|
13640
14570
|
return c.json({ email, source: "granted", granted_by: grantedBy }, 201);
|
|
13641
14571
|
});
|
|
13642
14572
|
app.delete("/admin/super-admins/:email", async (c) => {
|
|
13643
|
-
if (!await
|
|
14573
|
+
if (!await serverAdminAllowed2(c))
|
|
13644
14574
|
return c.json({ error: "Only the server admin can revoke superadmin." }, 403);
|
|
13645
14575
|
const email = c.req.param("email").trim().toLowerCase();
|
|
13646
14576
|
const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
|
|
@@ -13681,9 +14611,14 @@ function createApp() {
|
|
|
13681
14611
|
await loadGrantedSuperAdmins();
|
|
13682
14612
|
return c.json({ ok: true, email });
|
|
13683
14613
|
});
|
|
14614
|
+
app.use("/admin/community-sites", flexAuthMiddleware);
|
|
14615
|
+
app.use("/admin/community-sites/*", flexAuthMiddleware);
|
|
14616
|
+
app.route("/admin/community-sites", communitySiteRoutes);
|
|
13684
14617
|
app.use("/teams/*", flexAuthMiddleware);
|
|
13685
14618
|
app.use("/teams", flexAuthMiddleware);
|
|
13686
14619
|
app.route("/teams", teamRoutes);
|
|
14620
|
+
app.use("/people/*", flexAuthMiddleware);
|
|
14621
|
+
app.route("/people", peopleRoutes);
|
|
13687
14622
|
app.use("/me/*", flexAuthMiddleware);
|
|
13688
14623
|
app.get("/me/work", async (c) => {
|
|
13689
14624
|
const userId = c.get("userId");
|
|
@@ -13827,7 +14762,9 @@ function createApp() {
|
|
|
13827
14762
|
for (const nest of visibleNests) {
|
|
13828
14763
|
try {
|
|
13829
14764
|
const { storage } = await engineCache.get(nest.id);
|
|
13830
|
-
const docs = await storage.discoverDocuments()
|
|
14765
|
+
const docs = (await storage.discoverDocuments()).filter(
|
|
14766
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
14767
|
+
);
|
|
13831
14768
|
documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
|
|
13832
14769
|
} catch {
|
|
13833
14770
|
}
|
|
@@ -13882,7 +14819,7 @@ function createApp() {
|
|
|
13882
14819
|
app.route("/index", nestIndexRoutes);
|
|
13883
14820
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
13884
14821
|
app.route("/mcp", serverMcpRoutes);
|
|
13885
|
-
const nestsApp = new
|
|
14822
|
+
const nestsApp = new Hono25();
|
|
13886
14823
|
nestsApp.use("*", async (c, next) => {
|
|
13887
14824
|
const parts = nestPathParts(c.req.path);
|
|
13888
14825
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -13996,7 +14933,7 @@ function createApp() {
|
|
|
13996
14933
|
const isCommentAction = /\/comments$/.test(path) || /\/comments\/[^/]+\/resolve$/.test(path);
|
|
13997
14934
|
const isDeletionRequestAction = /\/request-deletion$/.test(path) || /\/deletion-requests\/[^/]+\/withdraw$/.test(path);
|
|
13998
14935
|
const isStewardRoster = path.includes("/stewards") && !path.includes("/nodes/");
|
|
13999
|
-
if (isStewardRoster &&
|
|
14936
|
+
if (isStewardRoster && access.rawPermission !== "owner" && access.rawPermission !== "admin") {
|
|
14000
14937
|
return c.json(
|
|
14001
14938
|
{
|
|
14002
14939
|
error: "You don't have permission to manage stewards. Only a nest admin, the nest owner, or the server admin can do this."
|
|
@@ -14004,6 +14941,13 @@ function createApp() {
|
|
|
14004
14941
|
403
|
|
14005
14942
|
);
|
|
14006
14943
|
}
|
|
14944
|
+
if (isStewardRoster && access.archived && c.req.method !== "GET") {
|
|
14945
|
+
return deny(
|
|
14946
|
+
403,
|
|
14947
|
+
"This nest is archived, so it is read-only. Restore it to change stewards.",
|
|
14948
|
+
{ archived: true }
|
|
14949
|
+
);
|
|
14950
|
+
}
|
|
14007
14951
|
const resource = parts[1];
|
|
14008
14952
|
if (resource === "visibility" || resource === "reader-mode") {
|
|
14009
14953
|
required = "admin";
|
|
@@ -14042,6 +14986,13 @@ function createApp() {
|
|
|
14042
14986
|
const grantLevel = grantRole ? permissionLevel(grantRole) : grantNestVisible ? permissionLevel("read") : 0;
|
|
14043
14987
|
const effectiveLevel = Math.max(permissionLevel(permission), grantLevel);
|
|
14044
14988
|
if (!stewardEditorBypass && effectiveLevel < permissionLevel(required)) {
|
|
14989
|
+
if (access.archived && permissionLevel(access.rawPermission) >= permissionLevel(required)) {
|
|
14990
|
+
return deny(
|
|
14991
|
+
403,
|
|
14992
|
+
"This nest is archived, so it is read-only. Restore it to make changes.",
|
|
14993
|
+
{ archived: true }
|
|
14994
|
+
);
|
|
14995
|
+
}
|
|
14045
14996
|
return deny(
|
|
14046
14997
|
403,
|
|
14047
14998
|
`You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,
|
|
@@ -14414,7 +15365,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
14414
15365
|
}
|
|
14415
15366
|
|
|
14416
15367
|
// src/workflow/scheduler.ts
|
|
14417
|
-
import { v4 as
|
|
15368
|
+
import { v4 as uuid19 } from "uuid";
|
|
14418
15369
|
function isDue(s, now) {
|
|
14419
15370
|
if (!s.last_run_at) return true;
|
|
14420
15371
|
const last = Date.parse(s.last_run_at);
|
|
@@ -14442,7 +15393,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
14442
15393
|
);
|
|
14443
15394
|
continue;
|
|
14444
15395
|
}
|
|
14445
|
-
const id = `run_${
|
|
15396
|
+
const id = `run_${uuid19()}`;
|
|
14446
15397
|
const nowIso2 = new Date(now).toISOString();
|
|
14447
15398
|
await db.run(
|
|
14448
15399
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
@@ -14512,6 +15463,11 @@ async function main() {
|
|
|
14512
15463
|
} catch (err) {
|
|
14513
15464
|
console.error("[access] failed to load DB-granted superadmins:", err);
|
|
14514
15465
|
}
|
|
15466
|
+
try {
|
|
15467
|
+
await loadActiveCommunitySites();
|
|
15468
|
+
} catch (err) {
|
|
15469
|
+
console.error("[sso] failed to load community sites:", err);
|
|
15470
|
+
}
|
|
14515
15471
|
const license = await validateLicense();
|
|
14516
15472
|
if (!license.valid) {
|
|
14517
15473
|
console.warn(`
|