@promptowl/contextnest-community 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIGURATION.md +4 -2
- package/README.md +1 -1
- package/dist/{chunk-PX3P4FTU.js → chunk-2YPY2HGZ.js} +2 -2
- package/dist/{chunk-6WESEA75.js → chunk-5CVZMHHB.js} +2 -2
- package/dist/{chunk-OQXZ43HG.js → chunk-MLQU4I5J.js} +40 -10
- package/dist/{chunk-W5ILNGPD.js → chunk-QLXC6542.js} +34 -6
- package/dist/{chunk-J2OQ3MEB.js → chunk-QWNXWRWO.js} +39 -1
- package/dist/{chunk-MY4JIWQD.js → chunk-UOMW7TT6.js} +1 -1
- package/dist/{chunk-ZAV4QUZA.js → chunk-XYD6V2LH.js} +4 -4
- package/dist/{client-CSCHXUNX.js → client-LWDMNKX3.js} +1 -1
- package/dist/{engine-XKRP7GOQ.js → engine-QFF2IA3G.js} +2 -2
- package/dist/{external-edit-service-TXW7SXSF.js → external-edit-service-F7D3VSA2.js} +3 -3
- package/dist/{grants-service-UZ2MGDO5.js → grants-service-TBGTCO5K.js} +2 -2
- package/dist/index.js +901 -226
- package/dist/{migrations.postgres-KKICIH2C.js → migrations.postgres-APCVSYUE.js} +16 -1
- package/dist/{review-service-XFWTENTN.js → review-service-A7PNLWG5.js} +6 -6
- package/dist/{stewardship-service-5HBQGDMK.js → stewardship-service-32UHIRQ5.js} +3 -3
- package/dist/{version-service-TKNYJMCM.js → version-service-RJ6CWZ3O.js} +3 -3
- package/dist/web3/assets/index-BJ-LRNis.js +1382 -0
- package/dist/web3/assets/index-DPMEt-A_.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +1 -1
- 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-MLQU4I5J.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-2YPY2HGZ.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-UOMW7TT6.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-5CVZMHHB.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-XYD6V2LH.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-QLXC6542.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-QWNXWRWO.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) {
|
|
@@ -1674,7 +1761,7 @@ authRoutes.post("/admin/reset-password/:userId", async (c) => {
|
|
|
1674
1761
|
if (supplied !== void 0) {
|
|
1675
1762
|
assertValidPassword(supplied);
|
|
1676
1763
|
}
|
|
1677
|
-
const generated = supplied ? null :
|
|
1764
|
+
const generated = supplied ? null : uuid4().replace(/-/g, "").slice(0, 16);
|
|
1678
1765
|
const newPassword = supplied ?? generated;
|
|
1679
1766
|
const newHash = await hashPassword(newPassword);
|
|
1680
1767
|
await db.transaction(async (tx) => {
|
|
@@ -1789,7 +1876,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1789
1876
|
409
|
|
1790
1877
|
);
|
|
1791
1878
|
}
|
|
1792
|
-
const tempPassword =
|
|
1879
|
+
const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
|
|
1793
1880
|
const passwordHash = await hashPassword(tempPassword);
|
|
1794
1881
|
let userId;
|
|
1795
1882
|
if (existing) {
|
|
@@ -1799,7 +1886,7 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1799
1886
|
userId
|
|
1800
1887
|
]);
|
|
1801
1888
|
} else {
|
|
1802
|
-
userId =
|
|
1889
|
+
userId = uuid4();
|
|
1803
1890
|
await db.run(
|
|
1804
1891
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1805
1892
|
[userId, email, null, passwordHash]
|
|
@@ -1939,7 +2026,43 @@ function documentPrimeFlag(metadata) {
|
|
|
1939
2026
|
}
|
|
1940
2027
|
|
|
1941
2028
|
// src/governance/mention-notify.ts
|
|
1942
|
-
import { v4 as
|
|
2029
|
+
import { v4 as uuid5 } from "uuid";
|
|
2030
|
+
|
|
2031
|
+
// src/shared/mentions.ts
|
|
2032
|
+
var MENTION = /(?:^|\s)@(?:\(([^)]+)\)|([a-zA-Z0-9._@-]+))/g;
|
|
2033
|
+
function stripCode(markdown2) {
|
|
2034
|
+
return markdown2.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
2035
|
+
}
|
|
2036
|
+
function extractMentions(markdown2) {
|
|
2037
|
+
const names = [];
|
|
2038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2039
|
+
const text = stripCode(markdown2 || "");
|
|
2040
|
+
let match;
|
|
2041
|
+
MENTION.lastIndex = 0;
|
|
2042
|
+
while ((match = MENTION.exec(text)) !== null) {
|
|
2043
|
+
const name = (match[1] || match[2] || "").trim();
|
|
2044
|
+
const key = name.toLowerCase();
|
|
2045
|
+
if (!name || seen.has(key)) continue;
|
|
2046
|
+
seen.add(key);
|
|
2047
|
+
names.push(name);
|
|
2048
|
+
}
|
|
2049
|
+
return names;
|
|
2050
|
+
}
|
|
2051
|
+
function newMentions(previous, next) {
|
|
2052
|
+
const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
|
|
2053
|
+
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
2054
|
+
}
|
|
2055
|
+
var MAX_MENTION_RECIPIENTS = 25;
|
|
2056
|
+
function resolveMention(people, name) {
|
|
2057
|
+
const n = name.trim().toLowerCase();
|
|
2058
|
+
if (!n) return void 0;
|
|
2059
|
+
const exact = people.find((p) => p.email.toLowerCase() === n);
|
|
2060
|
+
if (exact) return exact;
|
|
2061
|
+
const matches = people.filter(
|
|
2062
|
+
(p) => p.email.toLowerCase().split("@")[0] === n || (p.name || "").toLowerCase() === n
|
|
2063
|
+
);
|
|
2064
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2065
|
+
}
|
|
1943
2066
|
|
|
1944
2067
|
// src/nests/people-service.ts
|
|
1945
2068
|
var PERSON_SOURCES = [
|
|
@@ -2003,40 +2126,129 @@ async function listNestPeople(nestId) {
|
|
|
2003
2126
|
}
|
|
2004
2127
|
return people;
|
|
2005
2128
|
}
|
|
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);
|
|
2129
|
+
async function withDirectory(roster) {
|
|
2130
|
+
if (!config.PEOPLE_SUGGEST_ALL_USERS) return roster;
|
|
2131
|
+
const seen = new Set(roster.map((p) => p.email.toLowerCase()));
|
|
2132
|
+
const rows = await getDb().all(
|
|
2133
|
+
"SELECT id, email, name FROM users WHERE id != ? ORDER BY email LIMIT ?",
|
|
2134
|
+
[ANON_USER_ID, SUGGEST_FETCH]
|
|
2135
|
+
);
|
|
2136
|
+
const extra = [];
|
|
2137
|
+
for (const r of rows) {
|
|
2138
|
+
const email = (r.email || "").trim();
|
|
2139
|
+
if (!email || seen.has(email.toLowerCase())) continue;
|
|
2140
|
+
extra.push({ email, name: r.name ?? null, userId: r.id, source: "directory" });
|
|
2024
2141
|
}
|
|
2025
|
-
return
|
|
2142
|
+
return [...roster, ...extra];
|
|
2026
2143
|
}
|
|
2027
|
-
function
|
|
2028
|
-
|
|
2029
|
-
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
2030
|
-
}
|
|
2031
|
-
function resolveMention(people, name) {
|
|
2144
|
+
async function findDirectoryPerson(name) {
|
|
2145
|
+
if (!config.PEOPLE_SUGGEST_ALL_USERS) return void 0;
|
|
2032
2146
|
const n = name.trim().toLowerCase();
|
|
2033
2147
|
if (!n) return void 0;
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2148
|
+
const rows = await getDb().all(
|
|
2149
|
+
`SELECT id, email, name FROM users
|
|
2150
|
+
WHERE id != ?
|
|
2151
|
+
AND (LOWER(email) = ? OR LOWER(email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(name, '')) = ?)`,
|
|
2152
|
+
[ANON_USER_ID, n, `${escapeLike(n)}@%`, n]
|
|
2153
|
+
);
|
|
2154
|
+
return resolveMention(
|
|
2155
|
+
rows.map((r) => ({ email: r.email, name: r.name ?? null, userId: r.id, source: "directory" })),
|
|
2156
|
+
name
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
var SUGGEST_LIMIT = 20;
|
|
2160
|
+
var SUGGEST_FETCH = 200;
|
|
2161
|
+
function matchesQuery(person, q) {
|
|
2162
|
+
if (!q) return true;
|
|
2163
|
+
return person.email.toLowerCase().includes(q) || (person.name || "").toLowerCase().includes(q);
|
|
2164
|
+
}
|
|
2165
|
+
var escapeLike = (s) => s.replace(/([\\%_])/g, "\\$1");
|
|
2166
|
+
function queryFilter(q) {
|
|
2167
|
+
if (!q) return { sql: "", args: [] };
|
|
2168
|
+
const pattern = `%${escapeLike(q)}%`;
|
|
2169
|
+
return {
|
|
2170
|
+
sql: " AND (LOWER(u.email) LIKE ? ESCAPE '\\' OR LOWER(COALESCE(u.name, '')) LIKE ? ESCAPE '\\')",
|
|
2171
|
+
args: [pattern, pattern]
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
async function suggestPeopleForUser(callerUserId, query = "", limit = SUGGEST_LIMIT) {
|
|
2175
|
+
const db = getDb();
|
|
2176
|
+
const q = query.trim().toLowerCase();
|
|
2177
|
+
const cap = Math.min(Math.max(limit, 1), SUGGEST_LIMIT);
|
|
2178
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
2179
|
+
callerUserId
|
|
2180
|
+
]);
|
|
2181
|
+
const callerEmail = (caller?.email || "").toLowerCase();
|
|
2182
|
+
const filter = queryFilter(q);
|
|
2183
|
+
const wholeDirectory = config.PEOPLE_SUGGEST_ALL_USERS || await isServerAdminUserId(callerUserId);
|
|
2184
|
+
const rows = await (async () => {
|
|
2185
|
+
if (wholeDirectory) {
|
|
2186
|
+
return await db.all(
|
|
2187
|
+
`SELECT u.id AS user_id, u.email AS email, u.name AS name, 'directory' AS source
|
|
2188
|
+
FROM users u
|
|
2189
|
+
WHERE u.id != ? AND u.id != ?${filter.sql}
|
|
2190
|
+
ORDER BY u.email
|
|
2191
|
+
LIMIT ?`,
|
|
2192
|
+
[callerUserId, ANON_USER_ID, ...filter.args, SUGGEST_FETCH]
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
const args = [callerUserId, ANON_USER_ID];
|
|
2196
|
+
const myNests = () => {
|
|
2197
|
+
args.push(callerUserId, callerUserId);
|
|
2198
|
+
return `SELECT id FROM nests WHERE user_id = ?
|
|
2199
|
+
UNION SELECT nest_id FROM nest_collaborators WHERE user_id = ?`;
|
|
2200
|
+
};
|
|
2201
|
+
const ownedNests = myNests();
|
|
2202
|
+
const collabNests = myNests();
|
|
2203
|
+
const stewardNests = myNests();
|
|
2204
|
+
const sql = `SELECT u.id AS user_id, u.email AS email, u.name AS name, 'nest' AS source
|
|
2205
|
+
FROM users u
|
|
2206
|
+
WHERE u.id != ? AND u.id != ?
|
|
2207
|
+
AND (u.id IN (SELECT user_id FROM nests WHERE id IN (${ownedNests}))
|
|
2208
|
+
OR u.id IN (SELECT user_id FROM nest_collaborators
|
|
2209
|
+
WHERE nest_id IN (${collabNests}))
|
|
2210
|
+
OR LOWER(u.email) IN (SELECT LOWER(user_email) FROM stewards
|
|
2211
|
+
WHERE is_active = 1
|
|
2212
|
+
AND nest_id IN (${stewardNests})))${filter.sql}
|
|
2213
|
+
ORDER BY u.email
|
|
2214
|
+
LIMIT ?`;
|
|
2215
|
+
args.push(...filter.args, SUGGEST_FETCH);
|
|
2216
|
+
return await db.all(sql, args);
|
|
2217
|
+
})();
|
|
2218
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
2219
|
+
const add = (email, name, source) => {
|
|
2220
|
+
const addr = (email || "").trim();
|
|
2221
|
+
const key = addr.toLowerCase();
|
|
2222
|
+
if (!addr || !key || key === callerEmail || byEmail.has(key)) return;
|
|
2223
|
+
const person = { email: addr, name: name ?? null, source };
|
|
2224
|
+
if (!matchesQuery(person, q)) return;
|
|
2225
|
+
byEmail.set(key, person);
|
|
2226
|
+
};
|
|
2227
|
+
for (const r of rows) add(r.email, r.name, r.source);
|
|
2228
|
+
const teamEmails = /* @__PURE__ */ new Set();
|
|
2229
|
+
for (const team of wholeDirectory ? [] : await listTeamsForUser(callerUserId)) {
|
|
2230
|
+
for (const member of team.members ?? []) {
|
|
2231
|
+
const key = (member.email || "").trim().toLowerCase();
|
|
2232
|
+
if (key && key !== callerEmail && !byEmail.has(key)) teamEmails.add(key);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (teamEmails.size > 0) {
|
|
2236
|
+
const list = [...teamEmails].slice(0, SUGGEST_FETCH);
|
|
2237
|
+
const named = await db.all(
|
|
2238
|
+
`SELECT email, name FROM users
|
|
2239
|
+
WHERE LOWER(email) IN (${list.map(() => "?").join(",")})`,
|
|
2240
|
+
list
|
|
2241
|
+
);
|
|
2242
|
+
const nameOf = new Map(
|
|
2243
|
+
named.map((u) => [u.email.toLowerCase(), u.name ?? null])
|
|
2244
|
+
);
|
|
2245
|
+
for (const email of list) add(email, nameOf.get(email) ?? null, "team");
|
|
2246
|
+
}
|
|
2247
|
+
return [...byEmail.values()].sort(
|
|
2248
|
+
(a, b) => (a.name || a.email).localeCompare(b.name || b.email, void 0, {
|
|
2249
|
+
sensitivity: "base"
|
|
2250
|
+
})
|
|
2251
|
+
).slice(0, cap);
|
|
2040
2252
|
}
|
|
2041
2253
|
|
|
2042
2254
|
// src/governance/mention-notify.ts
|
|
@@ -2047,48 +2259,172 @@ async function canOpenNode(nestId, nodeId, userId, email) {
|
|
|
2047
2259
|
}
|
|
2048
2260
|
return canReadNode(nestId, nodeId, userId, email);
|
|
2049
2261
|
}
|
|
2262
|
+
async function resolveRecipients(params) {
|
|
2263
|
+
const { nestId, nodeId, names, actorEmail } = params;
|
|
2264
|
+
if (names.length === 0) return [];
|
|
2265
|
+
const roster = await listNestPeople(nestId);
|
|
2266
|
+
const actor = actorEmail.toLowerCase();
|
|
2267
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
2268
|
+
const outsiders = [];
|
|
2269
|
+
for (const name of names) {
|
|
2270
|
+
const member = resolveMention(roster, name);
|
|
2271
|
+
const person = member ?? await findDirectoryPerson(name);
|
|
2272
|
+
if (!person?.userId) continue;
|
|
2273
|
+
const key = person.email.toLowerCase();
|
|
2274
|
+
if (key === actor || candidates.has(key)) continue;
|
|
2275
|
+
const r = { email: person.email, userId: person.userId };
|
|
2276
|
+
candidates.set(key, r);
|
|
2277
|
+
if (!member) outsiders.push(r);
|
|
2278
|
+
if (candidates.size >= MAX_MENTION_RECIPIENTS) break;
|
|
2279
|
+
}
|
|
2280
|
+
if (outsiders.length) await inviteMentioned(nestId, actorEmail, outsiders);
|
|
2281
|
+
const checked = await Promise.all(
|
|
2282
|
+
[...candidates.values()].map(async (r) => ({
|
|
2283
|
+
r,
|
|
2284
|
+
allowed: await canOpenNode(nestId, nodeId, r.userId, r.email)
|
|
2285
|
+
}))
|
|
2286
|
+
);
|
|
2287
|
+
return checked.filter((c) => c.allowed).map((c) => c.r);
|
|
2288
|
+
}
|
|
2289
|
+
async function inviteMentioned(nestId, actorEmail, outsiders) {
|
|
2290
|
+
const db = getDb();
|
|
2291
|
+
const actor = await db.get("SELECT id FROM users WHERE LOWER(email) = LOWER(?)", [
|
|
2292
|
+
actorEmail
|
|
2293
|
+
]);
|
|
2294
|
+
if (!actor) return;
|
|
2295
|
+
const access = await resolveNestAccess(nestId, actor.id);
|
|
2296
|
+
if (permissionLevel(access.permission) < permissionLevel("write")) return;
|
|
2297
|
+
for (const r of outsiders) {
|
|
2298
|
+
await db.run(
|
|
2299
|
+
`INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by)
|
|
2300
|
+
SELECT ?, ?, ?, 'read', ?
|
|
2301
|
+
WHERE NOT EXISTS (SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?)`,
|
|
2302
|
+
[uuid5(), nestId, r.userId, actor.id, nestId, r.userId]
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
async function insertInboxRows(params) {
|
|
2307
|
+
const db = getDb();
|
|
2308
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2309
|
+
let created = 0;
|
|
2310
|
+
for (const r of params.recipients) {
|
|
2311
|
+
await db.run(
|
|
2312
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
2313
|
+
VALUES (?, ?, ?, 'mention', ?, ?, ?)`,
|
|
2314
|
+
[uuid5(), params.nestId, r.email.toLowerCase(), params.nodeId, params.message, now]
|
|
2315
|
+
);
|
|
2316
|
+
created++;
|
|
2317
|
+
}
|
|
2318
|
+
return created;
|
|
2319
|
+
}
|
|
2320
|
+
async function deliverMentionMessages(params) {
|
|
2321
|
+
const { nestId, nodeId, recipients, channelMessage, actorEmail, excerpt: excerpt2, ctx } = params;
|
|
2322
|
+
let throttled = 0;
|
|
2323
|
+
const channelReachable = await hasAnyConnector(nestId, "mention");
|
|
2324
|
+
if (channelReachable && canDeliver("channel", nestId, actorEmail)) {
|
|
2325
|
+
void dispatchEvent({
|
|
2326
|
+
kind: "mention",
|
|
2327
|
+
nestId,
|
|
2328
|
+
subjectId: nodeId,
|
|
2329
|
+
actor: actorEmail,
|
|
2330
|
+
message: channelMessage
|
|
2331
|
+
});
|
|
2332
|
+
} else if (channelReachable) {
|
|
2333
|
+
throttled++;
|
|
2334
|
+
}
|
|
2335
|
+
for (const r of isEmailConfigured() ? recipients : []) {
|
|
2336
|
+
if (!canDeliver("email", nestId, actorEmail)) {
|
|
2337
|
+
throttled++;
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
void sendEmailToRecipient(r.email, {
|
|
2341
|
+
status: "mention",
|
|
2342
|
+
docTitle: ctx.docTitle,
|
|
2343
|
+
path: ctx.path,
|
|
2344
|
+
link: ctx.link,
|
|
2345
|
+
actor: actorEmail,
|
|
2346
|
+
note: excerpt2 || null
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
if (throttled > 0) {
|
|
2350
|
+
console.warn(
|
|
2351
|
+
`[notify] mention delivery budget exhausted for ${actorEmail} in ${nestId} \u2014 ${throttled} outbound message(s) skipped; inbox rows unaffected`
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
var MENTION_DELIVERY_BUDGET = { max: 50, windowMs: 5 * 6e4 };
|
|
2356
|
+
var MENTION_CHANNEL_BUDGET = { max: 25, windowMs: 5 * 6e4 };
|
|
2357
|
+
function canDeliver(kind, nestId, actorEmail) {
|
|
2358
|
+
return tryConsume(
|
|
2359
|
+
`mention-${kind}:${nestId}:${actorEmail.toLowerCase()}`,
|
|
2360
|
+
kind === "email" ? MENTION_DELIVERY_BUDGET : MENTION_CHANNEL_BUDGET
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
function channelSentence(actorEmail, recipients, where) {
|
|
2364
|
+
const names = recipients.map((r) => r.email).join(", ");
|
|
2365
|
+
return `${actorEmail} mentioned ${names} ${where}`;
|
|
2366
|
+
}
|
|
2367
|
+
function excerptOf(body, max = 140) {
|
|
2368
|
+
const s = (body || "").replace(/\s+/g, " ").trim();
|
|
2369
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
2370
|
+
}
|
|
2050
2371
|
async function notifyMentions(params) {
|
|
2051
|
-
const { nestId, nodeId, previousBody, nextBody, actorEmail } = params;
|
|
2372
|
+
const { nestId, nodeId, previousBody, nextBody, actorEmail, baseUrl } = params;
|
|
2052
2373
|
try {
|
|
2053
2374
|
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
|
-
}
|
|
2375
|
+
const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
|
|
2376
|
+
if (recipients.length === 0) return 0;
|
|
2377
|
+
const ctx = await buildDocContext(nestId, nodeId, baseUrl);
|
|
2378
|
+
const message = `${actorEmail} mentioned you in "${ctx.label}"`;
|
|
2379
|
+
const created = await insertInboxRows({ nestId, nodeId, recipients, message });
|
|
2380
|
+
await deliverMentionMessages({
|
|
2381
|
+
nestId,
|
|
2382
|
+
nodeId,
|
|
2383
|
+
recipients,
|
|
2384
|
+
channelMessage: channelSentence(
|
|
2385
|
+
actorEmail,
|
|
2386
|
+
recipients,
|
|
2387
|
+
`in *${ctx.label}*`
|
|
2388
|
+
),
|
|
2389
|
+
actorEmail,
|
|
2390
|
+
excerpt: "",
|
|
2391
|
+
ctx
|
|
2392
|
+
});
|
|
2086
2393
|
return created;
|
|
2087
2394
|
} catch (err) {
|
|
2088
2395
|
console.error("[notify] mention fan-out failed", nestId, nodeId, err);
|
|
2089
2396
|
return 0;
|
|
2090
2397
|
}
|
|
2091
2398
|
}
|
|
2399
|
+
async function notifyCommentMentions(params) {
|
|
2400
|
+
const { nestId, nodeId, body, actorEmail, baseUrl } = params;
|
|
2401
|
+
try {
|
|
2402
|
+
const names = extractMentions(body || "");
|
|
2403
|
+
const recipients = await resolveRecipients({ nestId, nodeId, names, actorEmail });
|
|
2404
|
+
if (recipients.length === 0) return 0;
|
|
2405
|
+
const ctx = await buildDocContext(nestId, nodeId, baseUrl);
|
|
2406
|
+
const excerpt2 = excerptOf(body);
|
|
2407
|
+
const message = `${actorEmail} mentioned you in a comment on "${ctx.label}"`;
|
|
2408
|
+
const created = await insertInboxRows({ nestId, nodeId, recipients, message });
|
|
2409
|
+
await deliverMentionMessages({
|
|
2410
|
+
nestId,
|
|
2411
|
+
nodeId,
|
|
2412
|
+
recipients,
|
|
2413
|
+
channelMessage: channelSentence(
|
|
2414
|
+
actorEmail,
|
|
2415
|
+
recipients,
|
|
2416
|
+
`in a comment on *${ctx.label}*`
|
|
2417
|
+
),
|
|
2418
|
+
actorEmail,
|
|
2419
|
+
excerpt: excerpt2,
|
|
2420
|
+
ctx
|
|
2421
|
+
});
|
|
2422
|
+
return created;
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
console.error("[notify] comment mention fan-out failed", nestId, nodeId, err);
|
|
2425
|
+
return 0;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2092
2428
|
|
|
2093
2429
|
// src/nodes/service.ts
|
|
2094
2430
|
async function userIdFromEmail(email) {
|
|
@@ -2312,8 +2648,9 @@ function engineScopesFolders() {
|
|
|
2312
2648
|
}
|
|
2313
2649
|
return folderScopeSupported;
|
|
2314
2650
|
}
|
|
2651
|
+
var DERIVED_ANNOTATIONS_SUFFIX = "--annotations";
|
|
2315
2652
|
function isDerivedAnnotationsNode(id) {
|
|
2316
|
-
return id.endsWith(
|
|
2653
|
+
return id.endsWith(DERIVED_ANNOTATIONS_SUFFIX);
|
|
2317
2654
|
}
|
|
2318
2655
|
async function getNodeForCaller(nestId, selector, userId, userEmail, opts = {}) {
|
|
2319
2656
|
if (selector.id && !await canReadNode(nestId, selector.id, userId, userEmail)) {
|
|
@@ -2647,11 +2984,11 @@ async function autoPublishReason(nestId, userEmail, doc) {
|
|
|
2647
2984
|
}
|
|
2648
2985
|
return await actorAutoPublishes(nestId, userEmail) ? "owner/admin self-approve" : null;
|
|
2649
2986
|
}
|
|
2650
|
-
async function createNode(nestId, input, userEmail) {
|
|
2987
|
+
async function createNode(nestId, input, userEmail, opts = {}) {
|
|
2651
2988
|
const generatedId = !input.id && !slugify(input.title);
|
|
2652
2989
|
for (let attempt = 0; ; attempt++) {
|
|
2653
2990
|
try {
|
|
2654
|
-
return await createNodeOnce(nestId, input, userEmail);
|
|
2991
|
+
return await createNodeOnce(nestId, input, userEmail, opts);
|
|
2655
2992
|
} catch (err) {
|
|
2656
2993
|
if (!generatedId || attempt >= 3 || !(err instanceof ConflictError)) {
|
|
2657
2994
|
throw err;
|
|
@@ -2659,7 +2996,7 @@ async function createNode(nestId, input, userEmail) {
|
|
|
2659
2996
|
}
|
|
2660
2997
|
}
|
|
2661
2998
|
}
|
|
2662
|
-
async function createNodeOnce(nestId, input, userEmail) {
|
|
2999
|
+
async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
2663
3000
|
const { storage } = await engineCache.get(nestId);
|
|
2664
3001
|
assertTitleLength(input.title);
|
|
2665
3002
|
assertTitleReadable(input.title);
|
|
@@ -2792,13 +3129,15 @@ async function createNodeOnce(nestId, input, userEmail) {
|
|
|
2792
3129
|
});
|
|
2793
3130
|
}
|
|
2794
3131
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
3132
|
+
if (opts.notifyMentions !== false) {
|
|
3133
|
+
await notifyMentions({
|
|
3134
|
+
nestId,
|
|
3135
|
+
nodeId: id,
|
|
3136
|
+
previousBody: "",
|
|
3137
|
+
nextBody: node.body || "",
|
|
3138
|
+
actorEmail: userEmail
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
2802
3141
|
return { node, version: savedVersion };
|
|
2803
3142
|
});
|
|
2804
3143
|
}
|
|
@@ -2892,7 +3231,7 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
2892
3231
|
await trackEvent("nest.import.documents", { nestId, registered: docs.length });
|
|
2893
3232
|
return docs.length;
|
|
2894
3233
|
}
|
|
2895
|
-
async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
3234
|
+
async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
2896
3235
|
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
2897
3236
|
try {
|
|
2898
3237
|
const { storage, versions: versionManager } = await engineCache.get(
|
|
@@ -3044,13 +3383,15 @@ async function updateNode(nestId, nodeId, patch, userEmail) {
|
|
|
3044
3383
|
);
|
|
3045
3384
|
responseVersion = publishedVersion;
|
|
3046
3385
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3386
|
+
if (opts.notifyMentions !== false) {
|
|
3387
|
+
await notifyMentions({
|
|
3388
|
+
nestId,
|
|
3389
|
+
nodeId,
|
|
3390
|
+
previousBody,
|
|
3391
|
+
nextBody: node.body || "",
|
|
3392
|
+
actorEmail: userEmail
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3054
3395
|
return { node, version: responseVersion };
|
|
3055
3396
|
} catch (err) {
|
|
3056
3397
|
throw toSafeError(
|
|
@@ -3632,8 +3973,13 @@ nestRoutes.get("/", async (c) => {
|
|
|
3632
3973
|
}
|
|
3633
3974
|
const out = await Promise.all(deduped.map(annotate));
|
|
3634
3975
|
const docRows = await db.all(
|
|
3635
|
-
`SELECT nest_id,
|
|
3636
|
-
|
|
3976
|
+
`SELECT nest_id,
|
|
3977
|
+
COUNT(DISTINCT CASE
|
|
3978
|
+
WHEN ${suffixOf(db, "node_id", DERIVED_ANNOTATIONS_SUFFIX.length)} <> ?
|
|
3979
|
+
THEN node_id END) AS c,
|
|
3980
|
+
MAX(created_at) AS last_activity
|
|
3981
|
+
FROM node_versions GROUP BY nest_id`,
|
|
3982
|
+
[DERIVED_ANNOTATIONS_SUFFIX]
|
|
3637
3983
|
);
|
|
3638
3984
|
const docByNest = new Map(docRows.map((r) => [r.nest_id, Number(r.c)]));
|
|
3639
3985
|
const activityByNest = new Map(docRows.map((r) => [r.nest_id, r.last_activity]));
|
|
@@ -3675,6 +4021,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
3675
4021
|
);
|
|
3676
4022
|
for (const r of rows) {
|
|
3677
4023
|
const targets = grantTargetsByNest.get(r.nest_id) ?? [];
|
|
4024
|
+
if (isDerivedAnnotationsNode(r.node_id)) continue;
|
|
3678
4025
|
if (grantCoversNode(targets, r.node_id)) {
|
|
3679
4026
|
grantedDocsByNest.set(r.nest_id, (grantedDocsByNest.get(r.nest_id) ?? 0) + 1);
|
|
3680
4027
|
}
|
|
@@ -3960,7 +4307,7 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
3960
4307
|
import { Hono as Hono4 } from "hono";
|
|
3961
4308
|
|
|
3962
4309
|
// src/nests/sharing-service.ts
|
|
3963
|
-
import { v4 as
|
|
4310
|
+
import { v4 as uuid6 } from "uuid";
|
|
3964
4311
|
var VALID_PERMISSIONS = ["read", "write", "admin"];
|
|
3965
4312
|
async function addCollaborator(params) {
|
|
3966
4313
|
const { nestId } = params;
|
|
@@ -3987,10 +4334,10 @@ async function addCollaborator(params) {
|
|
|
3987
4334
|
userId = existing.id;
|
|
3988
4335
|
} else {
|
|
3989
4336
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
3990
|
-
userId =
|
|
4337
|
+
userId = uuid6();
|
|
3991
4338
|
await db.run(
|
|
3992
4339
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
3993
|
-
[userId, email, null, await hashPassword2(
|
|
4340
|
+
[userId, email, null, await hashPassword2(uuid6())]
|
|
3994
4341
|
);
|
|
3995
4342
|
}
|
|
3996
4343
|
}
|
|
@@ -4028,7 +4375,7 @@ async function addCollaborator(params) {
|
|
|
4028
4375
|
[params.grantedByEmail]
|
|
4029
4376
|
))?.id : void 0;
|
|
4030
4377
|
const granterId = params.grantedByUserId || granterByEmail || ownerRow.user_id;
|
|
4031
|
-
const collabId =
|
|
4378
|
+
const collabId = uuid6();
|
|
4032
4379
|
await db.run(
|
|
4033
4380
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
4034
4381
|
[collabId, nestId, userId, params.permission, granterId]
|
|
@@ -4101,7 +4448,7 @@ sharingRoutes.get("/collaborators", async (c) => {
|
|
|
4101
4448
|
}
|
|
4102
4449
|
return c.json({
|
|
4103
4450
|
collaborators: enriched,
|
|
4104
|
-
people: await listNestPeople(nestId)
|
|
4451
|
+
people: await withDirectory(await listNestPeople(nestId))
|
|
4105
4452
|
});
|
|
4106
4453
|
});
|
|
4107
4454
|
sharingRoutes.post("/collaborators", async (c) => {
|
|
@@ -4145,6 +4492,22 @@ sharingRoutes.delete("/collaborators/:collabId", async (c) => {
|
|
|
4145
4492
|
}
|
|
4146
4493
|
return c.json({ removed: true });
|
|
4147
4494
|
});
|
|
4495
|
+
sharingRoutes.get("/mentionable", async (c) => {
|
|
4496
|
+
const nestId = c.req.param("nestId");
|
|
4497
|
+
const userId = c.get("userId");
|
|
4498
|
+
const people = await listNestPeople(nestId);
|
|
4499
|
+
const callerEmail = (await resolveCallerEmail(userId)).toLowerCase();
|
|
4500
|
+
const onRoster = people.some((p) => p.email.toLowerCase() === callerEmail);
|
|
4501
|
+
if (!onRoster) {
|
|
4502
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
4503
|
+
if (permissionLevel(access.permission) < permissionLevel("write")) {
|
|
4504
|
+
throw new ForbiddenError("Only people on this nest can see its roster");
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
return c.json({
|
|
4508
|
+
people: (await withDirectory(people)).map((p) => ({ email: p.email, name: p.name }))
|
|
4509
|
+
});
|
|
4510
|
+
});
|
|
4148
4511
|
sharingRoutes.patch("/visibility", async (c) => {
|
|
4149
4512
|
const body = await c.req.json();
|
|
4150
4513
|
if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
|
|
@@ -4176,10 +4539,14 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
4176
4539
|
});
|
|
4177
4540
|
sharingRoutes.patch("/reader-mode", async (c) => {
|
|
4178
4541
|
const userId = c.get("userId");
|
|
4542
|
+
const nestId = c.req.param("nestId");
|
|
4179
4543
|
if (config.AUTH_MODE !== "open" && !await isServerAdminUserId(userId)) {
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4544
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
4545
|
+
if (permissionLevel(access.permission) < permissionLevel("admin")) {
|
|
4546
|
+
throw new ForbiddenError(
|
|
4547
|
+
"Only the nest owner, a nest admin, or the server admin can configure reader mode"
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4183
4550
|
}
|
|
4184
4551
|
const body = await c.req.json();
|
|
4185
4552
|
if (body.enabled !== void 0 && typeof body.enabled !== "boolean") {
|
|
@@ -4202,7 +4569,6 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4202
4569
|
);
|
|
4203
4570
|
}
|
|
4204
4571
|
const db = getDb();
|
|
4205
|
-
const nestId = c.req.param("nestId");
|
|
4206
4572
|
const exists = await db.get("SELECT id FROM nests WHERE id = ?", [
|
|
4207
4573
|
nestId
|
|
4208
4574
|
]);
|
|
@@ -4222,7 +4588,7 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4222
4588
|
|
|
4223
4589
|
// src/nests/grant-routes.ts
|
|
4224
4590
|
import { Hono as Hono5 } from "hono";
|
|
4225
|
-
import { v4 as
|
|
4591
|
+
import { v4 as uuid7 } from "uuid";
|
|
4226
4592
|
var grantRoutes = new Hono5();
|
|
4227
4593
|
async function requireManage(c, nestId) {
|
|
4228
4594
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
@@ -4237,10 +4603,10 @@ async function resolveGrantee(email) {
|
|
|
4237
4603
|
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
4238
4604
|
if (existing) return existing.id;
|
|
4239
4605
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4240
|
-
const id =
|
|
4606
|
+
const id = uuid7();
|
|
4241
4607
|
await db.run(
|
|
4242
4608
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4243
|
-
[id, e, null, await hashPassword2(
|
|
4609
|
+
[id, e, null, await hashPassword2(uuid7())]
|
|
4244
4610
|
);
|
|
4245
4611
|
return id;
|
|
4246
4612
|
}
|
|
@@ -4342,7 +4708,7 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4342
4708
|
import { Hono as Hono6 } from "hono";
|
|
4343
4709
|
|
|
4344
4710
|
// src/nests/publish-service.ts
|
|
4345
|
-
import { v4 as
|
|
4711
|
+
import { v4 as uuid8 } from "uuid";
|
|
4346
4712
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
4347
4713
|
var MODES = ["public", "email", "code", "invite"];
|
|
4348
4714
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -4411,7 +4777,7 @@ async function createPublish(params) {
|
|
|
4411
4777
|
);
|
|
4412
4778
|
}
|
|
4413
4779
|
const db = getDb();
|
|
4414
|
-
const id =
|
|
4780
|
+
const id = uuid8();
|
|
4415
4781
|
const slug = newSlug();
|
|
4416
4782
|
const created = nowIso();
|
|
4417
4783
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -4442,7 +4808,7 @@ async function createPublish(params) {
|
|
|
4442
4808
|
await db.run(
|
|
4443
4809
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4444
4810
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
4445
|
-
[
|
|
4811
|
+
[uuid8(), id, email, created]
|
|
4446
4812
|
);
|
|
4447
4813
|
}
|
|
4448
4814
|
}
|
|
@@ -4497,7 +4863,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
4497
4863
|
await getDb().run(
|
|
4498
4864
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4499
4865
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
4500
|
-
[
|
|
4866
|
+
[uuid8(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
4501
4867
|
);
|
|
4502
4868
|
return token;
|
|
4503
4869
|
}
|
|
@@ -4527,7 +4893,7 @@ async function createSession2(publishId, email) {
|
|
|
4527
4893
|
await getDb().run(
|
|
4528
4894
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4529
4895
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
4530
|
-
[
|
|
4896
|
+
[uuid8(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
4531
4897
|
);
|
|
4532
4898
|
return token;
|
|
4533
4899
|
}
|
|
@@ -4544,7 +4910,7 @@ async function logView(publishId, email) {
|
|
|
4544
4910
|
await getDb().run(
|
|
4545
4911
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4546
4912
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
4547
|
-
[
|
|
4913
|
+
[uuid8(), publishId, email, nowIso()]
|
|
4548
4914
|
);
|
|
4549
4915
|
}
|
|
4550
4916
|
async function listAllowlist(publishId) {
|
|
@@ -4637,7 +5003,7 @@ import { Hono as Hono7 } from "hono";
|
|
|
4637
5003
|
import MarkdownIt from "markdown-it";
|
|
4638
5004
|
|
|
4639
5005
|
// src/annotations/service.ts
|
|
4640
|
-
import { v4 as
|
|
5006
|
+
import { v4 as uuid9 } from "uuid";
|
|
4641
5007
|
|
|
4642
5008
|
// src/annotations/projection.ts
|
|
4643
5009
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -4762,6 +5128,10 @@ async function rowToThread(row) {
|
|
|
4762
5128
|
async function getThreadRow(threadId) {
|
|
4763
5129
|
return await getDb().get("SELECT * FROM annotation_threads WHERE id = ?", [threadId]);
|
|
4764
5130
|
}
|
|
5131
|
+
async function getThread(threadId) {
|
|
5132
|
+
const row = await getThreadRow(threadId);
|
|
5133
|
+
return row ? await rowToThread(row) : null;
|
|
5134
|
+
}
|
|
4765
5135
|
async function countThreadsByNode(nestId) {
|
|
4766
5136
|
const rows = await getDb().all(
|
|
4767
5137
|
`SELECT node_id, CAST(COUNT(*) AS INTEGER) AS count, MAX(created_at) AS last_at
|
|
@@ -4787,14 +5157,14 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4787
5157
|
throw new Error("comment body is required");
|
|
4788
5158
|
}
|
|
4789
5159
|
const db = getDb();
|
|
4790
|
-
const id =
|
|
5160
|
+
const id = uuid9();
|
|
4791
5161
|
const anchor = clampAnchor(input.anchor);
|
|
4792
5162
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
4793
5163
|
await db.transaction(async (tx) => {
|
|
4794
5164
|
await tx.run(
|
|
4795
5165
|
`INSERT INTO annotation_threads
|
|
4796
|
-
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by)
|
|
4797
|
-
VALUES (?, ?, ?, ?, ?, 'open',
|
|
5166
|
+
(id, nest_id, node_id, snapshot_version, anchor_json, status, created_by, created_at)
|
|
5167
|
+
VALUES (?, ?, ?, ?, ?, 'open', ?, ${nowMsExpr(db)})`,
|
|
4798
5168
|
[
|
|
4799
5169
|
id,
|
|
4800
5170
|
nestId,
|
|
@@ -4805,8 +5175,9 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
4805
5175
|
]
|
|
4806
5176
|
);
|
|
4807
5177
|
await tx.run(
|
|
4808
|
-
|
|
4809
|
-
|
|
5178
|
+
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5179
|
+
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5180
|
+
[uuid9(), id, authorEmail, body]
|
|
4810
5181
|
);
|
|
4811
5182
|
});
|
|
4812
5183
|
return rowToThread(await getThreadRow(id));
|
|
@@ -4824,9 +5195,11 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
4824
5195
|
throw new Error("comment body is required");
|
|
4825
5196
|
}
|
|
4826
5197
|
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
5198
|
+
const db = getDb();
|
|
5199
|
+
await db.run(
|
|
5200
|
+
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5201
|
+
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5202
|
+
[uuid9(), threadId, authorEmail, trimmed]
|
|
4830
5203
|
);
|
|
4831
5204
|
return rowToThread(await getThreadRow(threadId));
|
|
4832
5205
|
}
|
|
@@ -4913,7 +5286,13 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
4913
5286
|
const markdown2 = projectThreadsToMarkdown(title, threads);
|
|
4914
5287
|
const derivedId = derivedAnnotationsId(nodeId);
|
|
4915
5288
|
try {
|
|
4916
|
-
await updateNode(
|
|
5289
|
+
await updateNode(
|
|
5290
|
+
nestId,
|
|
5291
|
+
derivedId,
|
|
5292
|
+
{ content: markdown2 },
|
|
5293
|
+
userEmail,
|
|
5294
|
+
{ notifyMentions: false }
|
|
5295
|
+
);
|
|
4917
5296
|
} catch (err) {
|
|
4918
5297
|
if (err instanceof NotFoundError) {
|
|
4919
5298
|
await createNode(
|
|
@@ -4925,7 +5304,8 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
4925
5304
|
type: "document",
|
|
4926
5305
|
tags: ["annotations"]
|
|
4927
5306
|
},
|
|
4928
|
-
userEmail
|
|
5307
|
+
userEmail,
|
|
5308
|
+
{ notifyMentions: false }
|
|
4929
5309
|
);
|
|
4930
5310
|
} else {
|
|
4931
5311
|
throw err;
|
|
@@ -5350,8 +5730,19 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
|
5350
5730
|
return c.json({ teams });
|
|
5351
5731
|
});
|
|
5352
5732
|
|
|
5353
|
-
// src/
|
|
5733
|
+
// src/nests/people-routes.ts
|
|
5354
5734
|
import { Hono as Hono9 } from "hono";
|
|
5735
|
+
var peopleRoutes = new Hono9();
|
|
5736
|
+
peopleRoutes.get("/suggest", async (c) => {
|
|
5737
|
+
const userId = c.get("userId");
|
|
5738
|
+
const q = c.req.query("q") ?? "";
|
|
5739
|
+
const rawLimit = Number(c.req.query("limit"));
|
|
5740
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : void 0;
|
|
5741
|
+
return c.json({ people: await suggestPeopleForUser(userId, q, limit) });
|
|
5742
|
+
});
|
|
5743
|
+
|
|
5744
|
+
// src/governance/team-routes.ts
|
|
5745
|
+
import { Hono as Hono10 } from "hono";
|
|
5355
5746
|
|
|
5356
5747
|
// src/governance/promptowl-teams.ts
|
|
5357
5748
|
var PROMPTOWL_SOURCE = "promptowl";
|
|
@@ -5498,7 +5889,7 @@ async function desiredRoster(poTeam, callerEmail) {
|
|
|
5498
5889
|
}
|
|
5499
5890
|
|
|
5500
5891
|
// src/governance/team-routes.ts
|
|
5501
|
-
var teamRoutes = new
|
|
5892
|
+
var teamRoutes = new Hono10();
|
|
5502
5893
|
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
5503
5894
|
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
5504
5895
|
var CONNECT_REQUIRED_STATUS = 428;
|
|
@@ -5683,7 +6074,7 @@ function toSearchHit(node, query) {
|
|
|
5683
6074
|
}
|
|
5684
6075
|
|
|
5685
6076
|
// src/nodes/routes.ts
|
|
5686
|
-
import { Hono as
|
|
6077
|
+
import { Hono as Hono11 } from "hono";
|
|
5687
6078
|
|
|
5688
6079
|
// src/nodes/markdown-export.ts
|
|
5689
6080
|
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
@@ -5735,7 +6126,7 @@ ${m[1]}
|
|
|
5735
6126
|
}
|
|
5736
6127
|
|
|
5737
6128
|
// src/nodes/routes.ts
|
|
5738
|
-
var nodeRoutes = new
|
|
6129
|
+
var nodeRoutes = new Hono11();
|
|
5739
6130
|
function nodeAsMarkdown(response, nodeId) {
|
|
5740
6131
|
return nodeToMarkdown({
|
|
5741
6132
|
id: nodeId,
|
|
@@ -5817,7 +6208,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
5817
6208
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
5818
6209
|
const nestId = c.req.param("nestId");
|
|
5819
6210
|
const nodeId = c.req.param("nodeId");
|
|
5820
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
6211
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-32UHIRQ5.js");
|
|
5821
6212
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
5822
6213
|
nestId,
|
|
5823
6214
|
nodeId
|
|
@@ -5838,7 +6229,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
5838
6229
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
5839
6230
|
const nestId = c.req.param("nestId");
|
|
5840
6231
|
const nodeId = c.req.param("nodeId");
|
|
5841
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
6232
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-A7PNLWG5.js");
|
|
5842
6233
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
5843
6234
|
return c.json({ reviews: history });
|
|
5844
6235
|
});
|
|
@@ -6082,13 +6473,13 @@ async function getUserEmail(c) {
|
|
|
6082
6473
|
}
|
|
6083
6474
|
|
|
6084
6475
|
// src/annotations/routes.ts
|
|
6085
|
-
import { Hono as
|
|
6476
|
+
import { Hono as Hono12 } from "hono";
|
|
6086
6477
|
|
|
6087
6478
|
// src/annotations/types.ts
|
|
6088
6479
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
6089
6480
|
|
|
6090
6481
|
// src/annotations/routes.ts
|
|
6091
|
-
var annotationRoutes = new
|
|
6482
|
+
var annotationRoutes = new Hono12();
|
|
6092
6483
|
function getNodeId(c) {
|
|
6093
6484
|
const raw = c.req.param("nodeId");
|
|
6094
6485
|
try {
|
|
@@ -6129,6 +6520,13 @@ annotationRoutes.post("/:nodeId{.+}/annotations", async (c) => {
|
|
|
6129
6520
|
},
|
|
6130
6521
|
userEmail
|
|
6131
6522
|
);
|
|
6523
|
+
await notifyCommentMentions({
|
|
6524
|
+
nestId,
|
|
6525
|
+
nodeId,
|
|
6526
|
+
body: input.body,
|
|
6527
|
+
actorEmail: userEmail,
|
|
6528
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
6529
|
+
});
|
|
6132
6530
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
6133
6531
|
return c.json({ thread }, 201);
|
|
6134
6532
|
});
|
|
@@ -6144,6 +6542,13 @@ async function handleAnnotationReply(c, nodeId, threadId) {
|
|
|
6144
6542
|
throw new ValidationError("body is required");
|
|
6145
6543
|
}
|
|
6146
6544
|
const thread = await addComment(nestId, nodeId, threadId, userEmail, input.body);
|
|
6545
|
+
await notifyCommentMentions({
|
|
6546
|
+
nestId,
|
|
6547
|
+
nodeId,
|
|
6548
|
+
body: input.body,
|
|
6549
|
+
actorEmail: userEmail,
|
|
6550
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
6551
|
+
});
|
|
6147
6552
|
await syncAnnotationsNode(nestId, nodeId, userEmail);
|
|
6148
6553
|
return c.json({ thread });
|
|
6149
6554
|
}
|
|
@@ -6202,7 +6607,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
6202
6607
|
});
|
|
6203
6608
|
|
|
6204
6609
|
// src/nodes/query-routes.ts
|
|
6205
|
-
import { Hono as
|
|
6610
|
+
import { Hono as Hono13 } from "hono";
|
|
6206
6611
|
|
|
6207
6612
|
// src/tables/service.ts
|
|
6208
6613
|
function splitCsvLine(line) {
|
|
@@ -6836,7 +7241,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
6836
7241
|
}
|
|
6837
7242
|
|
|
6838
7243
|
// src/nodes/query-routes.ts
|
|
6839
|
-
var queryRoutes = new
|
|
7244
|
+
var queryRoutes = new Hono13();
|
|
6840
7245
|
queryRoutes.get("/changes", async (c) => {
|
|
6841
7246
|
const nestId = c.req.param("nestId");
|
|
6842
7247
|
const userId = c.get("userId");
|
|
@@ -7398,7 +7803,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
7398
7803
|
});
|
|
7399
7804
|
|
|
7400
7805
|
// src/mcp/routes.ts
|
|
7401
|
-
import { Hono as
|
|
7806
|
+
import { Hono as Hono15 } from "hono";
|
|
7402
7807
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7403
7808
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7404
7809
|
|
|
@@ -7478,10 +7883,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
7478
7883
|
}
|
|
7479
7884
|
|
|
7480
7885
|
// src/mcp/workflow-tools.ts
|
|
7481
|
-
import { v4 as
|
|
7886
|
+
import { v4 as uuid12 } from "uuid";
|
|
7482
7887
|
|
|
7483
7888
|
// src/workflow/run-service.ts
|
|
7484
|
-
import { v4 as
|
|
7889
|
+
import { v4 as uuid10 } from "uuid";
|
|
7485
7890
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
7486
7891
|
async function flowSubgraph(nestId, agentNode) {
|
|
7487
7892
|
const db = getDb();
|
|
@@ -7611,7 +8016,7 @@ async function triggerRun(opts) {
|
|
|
7611
8016
|
}
|
|
7612
8017
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
7613
8018
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
7614
|
-
const id = `run_${
|
|
8019
|
+
const id = `run_${uuid10()}`;
|
|
7615
8020
|
const db = getDb();
|
|
7616
8021
|
await db.transaction(async (tx) => {
|
|
7617
8022
|
if (parentRunId) {
|
|
@@ -7797,8 +8202,8 @@ async function listRuns(nestId, opts) {
|
|
|
7797
8202
|
}
|
|
7798
8203
|
|
|
7799
8204
|
// src/workflow/edge-routes.ts
|
|
7800
|
-
import { Hono as
|
|
7801
|
-
import { v4 as
|
|
8205
|
+
import { Hono as Hono14 } from "hono";
|
|
8206
|
+
import { v4 as uuid11 } from "uuid";
|
|
7802
8207
|
function toResponse(e, t) {
|
|
7803
8208
|
return {
|
|
7804
8209
|
id: e.id,
|
|
@@ -7859,7 +8264,9 @@ async function validateCondition(nestId, mode, condition) {
|
|
|
7859
8264
|
async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
7860
8265
|
if (fromNode === toNode) {
|
|
7861
8266
|
throw new ConflictError(
|
|
7862
|
-
`This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}.
|
|
8267
|
+
`This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}.
|
|
8268
|
+
|
|
8269
|
+
A flow can't return to a step it already ran.`
|
|
7863
8270
|
);
|
|
7864
8271
|
}
|
|
7865
8272
|
const rows = await db.all(
|
|
@@ -7888,7 +8295,9 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
|
7888
8295
|
}
|
|
7889
8296
|
path.reverse().push(toNode);
|
|
7890
8297
|
throw new ConflictError(
|
|
7891
|
-
`These steps loop back on each other: ${await nodeTrail(db, nestId, path)}.
|
|
8298
|
+
`These steps loop back on each other: ${await nodeTrail(db, nestId, path)}.
|
|
8299
|
+
|
|
8300
|
+
A flow can't return to a step it already ran.`
|
|
7892
8301
|
);
|
|
7893
8302
|
}
|
|
7894
8303
|
for (const nb of adj.get(cur) ?? []) {
|
|
@@ -7912,7 +8321,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
7912
8321
|
);
|
|
7913
8322
|
}
|
|
7914
8323
|
}
|
|
7915
|
-
var edgeRoutes = new
|
|
8324
|
+
var edgeRoutes = new Hono14();
|
|
7916
8325
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
7917
8326
|
const nestId = c.req.param("nestId");
|
|
7918
8327
|
const db = getDb();
|
|
@@ -7980,7 +8389,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
7980
8389
|
);
|
|
7981
8390
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
7982
8391
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7983
|
-
const id =
|
|
8392
|
+
const id = uuid11();
|
|
7984
8393
|
await db.transaction(async (tx) => {
|
|
7985
8394
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
7986
8395
|
if (type.is_flow) {
|
|
@@ -8381,7 +8790,7 @@ async function createEdge(ctx, args) {
|
|
|
8381
8790
|
);
|
|
8382
8791
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
8383
8792
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8384
|
-
const id =
|
|
8793
|
+
const id = uuid12();
|
|
8385
8794
|
await db.transaction(async (tx) => {
|
|
8386
8795
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
8387
8796
|
if (type.is_flow) {
|
|
@@ -8445,7 +8854,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
8445
8854
|
`INSERT INTO edge_types
|
|
8446
8855
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
8447
8856
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8448
|
-
[
|
|
8857
|
+
[uuid12(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
8449
8858
|
);
|
|
8450
8859
|
}
|
|
8451
8860
|
});
|
|
@@ -8654,6 +9063,43 @@ var TOOL_DEFINITIONS = [
|
|
|
8654
9063
|
}
|
|
8655
9064
|
}
|
|
8656
9065
|
},
|
|
9066
|
+
{
|
|
9067
|
+
name: "context_comment",
|
|
9068
|
+
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.",
|
|
9069
|
+
inputSchema: {
|
|
9070
|
+
type: "object",
|
|
9071
|
+
properties: {
|
|
9072
|
+
title: { type: "string", description: "Title of the node" },
|
|
9073
|
+
id: { type: "string", description: "ID of the node" },
|
|
9074
|
+
body: { type: "string", description: "The comment text" },
|
|
9075
|
+
thread: {
|
|
9076
|
+
type: "string",
|
|
9077
|
+
description: "Thread id to reply to (from context_comments). Omit to open a new thread on the document."
|
|
9078
|
+
}
|
|
9079
|
+
},
|
|
9080
|
+
required: ["body"]
|
|
9081
|
+
}
|
|
9082
|
+
},
|
|
9083
|
+
{
|
|
9084
|
+
name: "context_resolve_comment",
|
|
9085
|
+
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.",
|
|
9086
|
+
inputSchema: {
|
|
9087
|
+
type: "object",
|
|
9088
|
+
properties: {
|
|
9089
|
+
title: { type: "string", description: "Title of the node" },
|
|
9090
|
+
id: { type: "string", description: "ID of the node" },
|
|
9091
|
+
thread: {
|
|
9092
|
+
type: "string",
|
|
9093
|
+
description: "Thread id, from context_comments"
|
|
9094
|
+
},
|
|
9095
|
+
reopen: {
|
|
9096
|
+
type: "boolean",
|
|
9097
|
+
description: "Reopen the thread instead of resolving it"
|
|
9098
|
+
}
|
|
9099
|
+
},
|
|
9100
|
+
required: ["thread"]
|
|
9101
|
+
}
|
|
9102
|
+
},
|
|
8657
9103
|
{
|
|
8658
9104
|
name: "context_create",
|
|
8659
9105
|
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 +9805,8 @@ ${parts.join(
|
|
|
9359
9805
|
node = docs.find((n) => n.id === args.id);
|
|
9360
9806
|
}
|
|
9361
9807
|
if (!node) return `Node not found: ${args.title || args.id}`;
|
|
9362
|
-
if (await
|
|
9363
|
-
return `Node
|
|
9808
|
+
if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
|
|
9809
|
+
return `Node not found: ${args.title || args.id}`;
|
|
9364
9810
|
}
|
|
9365
9811
|
const threads = await listThreads(nestId, node.id);
|
|
9366
9812
|
if (!threads.length)
|
|
@@ -9379,6 +9825,89 @@ ${quote}${comments}`;
|
|
|
9379
9825
|
|
|
9380
9826
|
${sections}`;
|
|
9381
9827
|
}
|
|
9828
|
+
// ── Comments: write half of context_comments ──────────────────────────
|
|
9829
|
+
//
|
|
9830
|
+
// Both cases target the annotation store the UI's comments panel uses, so
|
|
9831
|
+
// an agent's comment and a human's are the same object, and both carry the
|
|
9832
|
+
// side effects the REST routes carry: the @mention fan-out and the derived
|
|
9833
|
+
// `— Annotations` projection an artifact's agents read back.
|
|
9834
|
+
//
|
|
9835
|
+
// PRIMARY permission gate, like context_create's: the /mcp middleware
|
|
9836
|
+
// admits read-tier callers (the transport is always POST), so the check
|
|
9837
|
+
// here is the real one. Commenting is READ-tier — the same rule REST
|
|
9838
|
+
// applies in app.ts (isAnnotationAction) — so canReadNode is the bar, not
|
|
9839
|
+
// write access. Deliberately NOT gated on an approved version: a reviewer
|
|
9840
|
+
// comments on a draft, and unlike reading a thread, writing one discloses
|
|
9841
|
+
// nothing (the caller supplies the text).
|
|
9842
|
+
case "context_comment":
|
|
9843
|
+
case "context_resolve_comment": {
|
|
9844
|
+
const body = String(args.body ?? "");
|
|
9845
|
+
const isResolve = toolName === "context_resolve_comment";
|
|
9846
|
+
if (!isResolve && !body.trim()) {
|
|
9847
|
+
return "body is required \u2014 the comment text.";
|
|
9848
|
+
}
|
|
9849
|
+
const threadId = args.thread ? String(args.thread) : "";
|
|
9850
|
+
if (isResolve && !threadId) {
|
|
9851
|
+
return "thread is required \u2014 the id from context_comments.";
|
|
9852
|
+
}
|
|
9853
|
+
const docs = await storage.discoverDocuments();
|
|
9854
|
+
let node;
|
|
9855
|
+
if (args.title) {
|
|
9856
|
+
node = docs.find(
|
|
9857
|
+
(n) => n.frontmatter.title.toLowerCase() === String(args.title).toLowerCase()
|
|
9858
|
+
);
|
|
9859
|
+
} else if (args.id) {
|
|
9860
|
+
node = docs.find((n) => n.id === String(args.id));
|
|
9861
|
+
}
|
|
9862
|
+
if (!node) {
|
|
9863
|
+
return toolError(
|
|
9864
|
+
`Node not found: ${args.title || args.id || "(no title or id given)"}`,
|
|
9865
|
+
"DOCUMENT_NOT_FOUND"
|
|
9866
|
+
);
|
|
9867
|
+
}
|
|
9868
|
+
if (!await canReadNode(nestId, node.id, ctx.userId, userEmail)) {
|
|
9869
|
+
return toolError(
|
|
9870
|
+
`Node not found: ${args.title || args.id}`,
|
|
9871
|
+
"DOCUMENT_NOT_FOUND"
|
|
9872
|
+
);
|
|
9873
|
+
}
|
|
9874
|
+
const label = node.frontmatter.title || node.id;
|
|
9875
|
+
if (threadId) {
|
|
9876
|
+
const existing = await getThread(threadId);
|
|
9877
|
+
if (!existing || existing.nodeId !== node.id || existing.nestId !== nestId) {
|
|
9878
|
+
return toolError(
|
|
9879
|
+
`Thread ${threadId} is not a thread on "${label}". Call context_comments to list its threads.`,
|
|
9880
|
+
"DOCUMENT_NOT_FOUND"
|
|
9881
|
+
);
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
if (isResolve) {
|
|
9885
|
+
const reopen = args.reopen === true;
|
|
9886
|
+
const thread2 = await setThreadStatus(
|
|
9887
|
+
nestId,
|
|
9888
|
+
node.id,
|
|
9889
|
+
threadId,
|
|
9890
|
+
reopen ? "open" : "resolved",
|
|
9891
|
+
userEmail
|
|
9892
|
+
);
|
|
9893
|
+
await syncAnnotationsNode(nestId, node.id, userEmail);
|
|
9894
|
+
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}.`;
|
|
9895
|
+
}
|
|
9896
|
+
const thread = threadId ? await addComment(nestId, node.id, threadId, userEmail, body) : await createThread(nestId, node.id, { body, anchor: null }, userEmail);
|
|
9897
|
+
await notifyCommentMentions({
|
|
9898
|
+
nestId,
|
|
9899
|
+
nodeId: node.id,
|
|
9900
|
+
body,
|
|
9901
|
+
actorEmail: userEmail,
|
|
9902
|
+
// Without this the notification still reaches the person, but its
|
|
9903
|
+
// "Open document" link falls back to PUBLIC_BASE_URL and comes out
|
|
9904
|
+
// empty when that isn't configured — every other notifying tool in
|
|
9905
|
+
// this file passes it, and the REST route passes requestBaseUrl.
|
|
9906
|
+
baseUrl: ctx.baseUrl
|
|
9907
|
+
});
|
|
9908
|
+
await syncAnnotationsNode(nestId, node.id, userEmail);
|
|
9909
|
+
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\`.`;
|
|
9910
|
+
}
|
|
9382
9911
|
case "context_create": {
|
|
9383
9912
|
if (!await canCreateInNest(nestId, userEmail)) {
|
|
9384
9913
|
return toolError(
|
|
@@ -9927,19 +10456,19 @@ ${list}`,
|
|
|
9927
10456
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
9928
10457
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
9929
10458
|
try {
|
|
9930
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
9931
|
-
const db = (await import("./client-
|
|
10459
|
+
const { createGrant: createGrant2 } = await import("./grants-service-TBGTCO5K.js");
|
|
10460
|
+
const db = (await import("./client-LWDMNKX3.js")).getDb();
|
|
9932
10461
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
9933
10462
|
const e = normalizeEmail2(String(args.email || ""));
|
|
9934
10463
|
if (!e) return "email is required.";
|
|
9935
10464
|
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
9936
10465
|
if (!row) {
|
|
9937
10466
|
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
9938
|
-
const { v4:
|
|
9939
|
-
const id =
|
|
10467
|
+
const { v4: uuid20 } = await import("uuid");
|
|
10468
|
+
const id = uuid20();
|
|
9940
10469
|
await db.run(
|
|
9941
10470
|
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
9942
|
-
[id, e, null, await hashPassword2(
|
|
10471
|
+
[id, e, null, await hashPassword2(uuid20())]
|
|
9943
10472
|
);
|
|
9944
10473
|
row = { id };
|
|
9945
10474
|
}
|
|
@@ -9994,7 +10523,7 @@ ${list}`;
|
|
|
9994
10523
|
|
|
9995
10524
|
// src/mcp/routes.ts
|
|
9996
10525
|
import { z } from "zod";
|
|
9997
|
-
var mcpRoutes = new
|
|
10526
|
+
var mcpRoutes = new Hono15();
|
|
9998
10527
|
async function getUserEmail2(userId) {
|
|
9999
10528
|
const db = getDb();
|
|
10000
10529
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -10060,8 +10589,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
10060
10589
|
});
|
|
10061
10590
|
|
|
10062
10591
|
// src/workflow/run-routes.ts
|
|
10063
|
-
import { Hono as
|
|
10064
|
-
import { v4 as
|
|
10592
|
+
import { Hono as Hono16 } from "hono";
|
|
10593
|
+
import { v4 as uuid13 } from "uuid";
|
|
10065
10594
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
10066
10595
|
var safeJson2 = (s) => {
|
|
10067
10596
|
if (!s) return null;
|
|
@@ -10179,7 +10708,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
10179
10708
|
definitions
|
|
10180
10709
|
};
|
|
10181
10710
|
}
|
|
10182
|
-
var runTriggerRoutes = new
|
|
10711
|
+
var runTriggerRoutes = new Hono16();
|
|
10183
10712
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
10184
10713
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
10185
10714
|
throw new ValidationError(
|
|
@@ -10258,7 +10787,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10258
10787
|
}
|
|
10259
10788
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
10260
10789
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
10261
|
-
const id = `run_${
|
|
10790
|
+
const id = `run_${uuid13()}`;
|
|
10262
10791
|
await getDb().run(
|
|
10263
10792
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
10264
10793
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -10287,7 +10816,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10287
10816
|
201
|
|
10288
10817
|
);
|
|
10289
10818
|
});
|
|
10290
|
-
var runListRoutes = new
|
|
10819
|
+
var runListRoutes = new Hono16();
|
|
10291
10820
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10292
10821
|
const nestId = c.req.param("nestId");
|
|
10293
10822
|
const db = getDb();
|
|
@@ -10319,7 +10848,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
10319
10848
|
}))
|
|
10320
10849
|
});
|
|
10321
10850
|
});
|
|
10322
|
-
var runDetailRoutes = new
|
|
10851
|
+
var runDetailRoutes = new Hono16();
|
|
10323
10852
|
async function loadRunWithPermission(c, runId, required) {
|
|
10324
10853
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
10325
10854
|
runId
|
|
@@ -10470,8 +10999,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10470
10999
|
});
|
|
10471
11000
|
|
|
10472
11001
|
// src/workflow/schedule-routes.ts
|
|
10473
|
-
import { Hono as
|
|
10474
|
-
import { v4 as
|
|
11002
|
+
import { Hono as Hono17 } from "hono";
|
|
11003
|
+
import { v4 as uuid14 } from "uuid";
|
|
10475
11004
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
10476
11005
|
var MIN_EVERY = 5;
|
|
10477
11006
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -10508,7 +11037,7 @@ function parseEvery(v) {
|
|
|
10508
11037
|
}
|
|
10509
11038
|
return n;
|
|
10510
11039
|
}
|
|
10511
|
-
var scheduleRoutes = new
|
|
11040
|
+
var scheduleRoutes = new Hono17();
|
|
10512
11041
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10513
11042
|
const nestId = c.req.param("nestId");
|
|
10514
11043
|
const rows = await getDb().all(
|
|
@@ -10549,7 +11078,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
10549
11078
|
);
|
|
10550
11079
|
}
|
|
10551
11080
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10552
|
-
const id = `sch_${
|
|
11081
|
+
const id = `sch_${uuid14()}`;
|
|
10553
11082
|
await db.run(
|
|
10554
11083
|
`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
11084
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -10604,13 +11133,14 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
10604
11133
|
});
|
|
10605
11134
|
|
|
10606
11135
|
// src/notify/connector-routes.ts
|
|
10607
|
-
import { Hono as
|
|
10608
|
-
import { v4 as
|
|
11136
|
+
import { Hono as Hono18 } from "hono";
|
|
11137
|
+
import { v4 as uuid15 } from "uuid";
|
|
10609
11138
|
var KNOWN_EVENTS = [
|
|
10610
11139
|
"review_requested",
|
|
10611
11140
|
"review_approved",
|
|
10612
11141
|
"review_rejected",
|
|
10613
|
-
"run_failed"
|
|
11142
|
+
"run_failed",
|
|
11143
|
+
"mention"
|
|
10614
11144
|
];
|
|
10615
11145
|
var CHANNELS = ["slack", "teams", "webhook"];
|
|
10616
11146
|
function toResponse3(r) {
|
|
@@ -10651,7 +11181,7 @@ function parseUrl(v) {
|
|
|
10651
11181
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
10652
11182
|
);
|
|
10653
11183
|
}
|
|
10654
|
-
var connectorRoutes = new
|
|
11184
|
+
var connectorRoutes = new Hono18();
|
|
10655
11185
|
connectorRoutes.get("/", async (c) => {
|
|
10656
11186
|
const nestId = c.req.param("nestId");
|
|
10657
11187
|
const rows = await getDb().all(
|
|
@@ -10670,7 +11200,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
10670
11200
|
const url = parseUrl(body.url);
|
|
10671
11201
|
const events = parseEvents(body.events);
|
|
10672
11202
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10673
|
-
const id = `con_${
|
|
11203
|
+
const id = `con_${uuid15()}`;
|
|
10674
11204
|
await getDb().run(
|
|
10675
11205
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
10676
11206
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -10713,7 +11243,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
10713
11243
|
});
|
|
10714
11244
|
|
|
10715
11245
|
// src/workflow/hook-routes.ts
|
|
10716
|
-
import { Hono as
|
|
11246
|
+
import { Hono as Hono19 } from "hono";
|
|
10717
11247
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
10718
11248
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
10719
11249
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -10736,7 +11266,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
10736
11266
|
created_at: r.created_at
|
|
10737
11267
|
};
|
|
10738
11268
|
}
|
|
10739
|
-
var hookNestRoutes = new
|
|
11269
|
+
var hookNestRoutes = new Hono19();
|
|
10740
11270
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10741
11271
|
const nestId = c.req.param("nestId");
|
|
10742
11272
|
const agent = c.req.query("agent");
|
|
@@ -10810,7 +11340,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
10810
11340
|
function stripMentions(text) {
|
|
10811
11341
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
10812
11342
|
}
|
|
10813
|
-
var hookFireRoutes = new
|
|
11343
|
+
var hookFireRoutes = new Hono19();
|
|
10814
11344
|
hookFireRoutes.post("/:token", async (c) => {
|
|
10815
11345
|
const token = c.req.param("token");
|
|
10816
11346
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -10912,9 +11442,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
10912
11442
|
});
|
|
10913
11443
|
|
|
10914
11444
|
// src/definitions/routes.ts
|
|
10915
|
-
import { Hono as
|
|
10916
|
-
import { v4 as
|
|
10917
|
-
var definitionRoutes = new
|
|
11445
|
+
import { Hono as Hono20 } from "hono";
|
|
11446
|
+
import { v4 as uuid16 } from "uuid";
|
|
11447
|
+
var definitionRoutes = new Hono20();
|
|
10918
11448
|
definitionRoutes.get("/", async (c) => {
|
|
10919
11449
|
const nestId = c.req.param("nestId");
|
|
10920
11450
|
const db = getDb();
|
|
@@ -10993,7 +11523,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
10993
11523
|
await applyUpdate(tx, existing.id);
|
|
10994
11524
|
targetId = existing.id;
|
|
10995
11525
|
} else {
|
|
10996
|
-
targetId =
|
|
11526
|
+
targetId = uuid16();
|
|
10997
11527
|
wasCreated = true;
|
|
10998
11528
|
await tx.run(
|
|
10999
11529
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -11022,11 +11552,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
11022
11552
|
});
|
|
11023
11553
|
|
|
11024
11554
|
// src/nests/asset-routes.ts
|
|
11025
|
-
import { Hono as
|
|
11026
|
-
import { v4 as
|
|
11555
|
+
import { Hono as Hono21 } from "hono";
|
|
11556
|
+
import { v4 as uuid17 } from "uuid";
|
|
11027
11557
|
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
11028
11558
|
import { join as join4 } from "path";
|
|
11029
|
-
var assetRoutes = new
|
|
11559
|
+
var assetRoutes = new Hono21();
|
|
11030
11560
|
var CONTENT_TYPES = {
|
|
11031
11561
|
png: "image/png",
|
|
11032
11562
|
jpg: "image/jpeg",
|
|
@@ -11072,7 +11602,7 @@ assetRoutes.post("/", async (c) => {
|
|
|
11072
11602
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
11073
11603
|
);
|
|
11074
11604
|
}
|
|
11075
|
-
const name = `${
|
|
11605
|
+
const name = `${uuid17()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
11076
11606
|
const dir = join4(resolveNestPath(nestId), "assets");
|
|
11077
11607
|
await mkdir2(dir, { recursive: true });
|
|
11078
11608
|
await writeFile(join4(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
@@ -11112,8 +11642,108 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
11112
11642
|
});
|
|
11113
11643
|
});
|
|
11114
11644
|
|
|
11645
|
+
// src/auth/community-sites-routes.ts
|
|
11646
|
+
import { Hono as Hono22 } from "hono";
|
|
11647
|
+
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
11648
|
+
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
11649
|
+
var serializeSite = (site) => ({
|
|
11650
|
+
id: site.id,
|
|
11651
|
+
name: site.name,
|
|
11652
|
+
url: site.url,
|
|
11653
|
+
secret_set: !!site.secret,
|
|
11654
|
+
is_active: site.is_active,
|
|
11655
|
+
created_at: site.created_at,
|
|
11656
|
+
updated_at: site.updated_at
|
|
11657
|
+
});
|
|
11658
|
+
var communitySiteRoutes = new Hono22();
|
|
11659
|
+
communitySiteRoutes.get("/", async (c) => {
|
|
11660
|
+
if (!await serverAdminAllowed(c))
|
|
11661
|
+
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
11662
|
+
const sites = await listCommunitySites();
|
|
11663
|
+
return c.json({ community_sites: sites.map(serializeSite) });
|
|
11664
|
+
});
|
|
11665
|
+
communitySiteRoutes.post("/", async (c) => {
|
|
11666
|
+
if (!await serverAdminAllowed(c))
|
|
11667
|
+
return c.json({ error: "Only the server admin can add a community site." }, 403);
|
|
11668
|
+
let body;
|
|
11669
|
+
try {
|
|
11670
|
+
body = await c.req.json();
|
|
11671
|
+
} catch {
|
|
11672
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
11673
|
+
}
|
|
11674
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
11675
|
+
const url = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
|
|
11676
|
+
const secret = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
11677
|
+
const is_active = body.is_active === void 0 ? true : !!body.is_active;
|
|
11678
|
+
if (!name) return c.json({ error: "A name is required." }, 400);
|
|
11679
|
+
if (!url || !isHttpUrl(url))
|
|
11680
|
+
return c.json({ error: "A valid http(s) site URL is required." }, 400);
|
|
11681
|
+
if (!secret || secret.length < 16)
|
|
11682
|
+
return c.json({ error: "A secret of at least 16 characters is required." }, 400);
|
|
11683
|
+
try {
|
|
11684
|
+
const site = await createCommunitySite({ name, url, secret, is_active });
|
|
11685
|
+
await loadActiveCommunitySites();
|
|
11686
|
+
return c.json(serializeSite(site), 201);
|
|
11687
|
+
} catch (err) {
|
|
11688
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11689
|
+
if (/unique|duplicate/i.test(msg))
|
|
11690
|
+
return c.json({ error: "A community site with this URL already exists." }, 409);
|
|
11691
|
+
throw err;
|
|
11692
|
+
}
|
|
11693
|
+
});
|
|
11694
|
+
communitySiteRoutes.patch("/:id", async (c) => {
|
|
11695
|
+
if (!await serverAdminAllowed(c))
|
|
11696
|
+
return c.json({ error: "Only the server admin can edit a community site." }, 403);
|
|
11697
|
+
const id = c.req.param("id");
|
|
11698
|
+
let body;
|
|
11699
|
+
try {
|
|
11700
|
+
body = await c.req.json();
|
|
11701
|
+
} catch {
|
|
11702
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
11703
|
+
}
|
|
11704
|
+
const patch = {};
|
|
11705
|
+
if ("name" in body) {
|
|
11706
|
+
const v = typeof body.name === "string" ? body.name.trim() : "";
|
|
11707
|
+
if (!v) return c.json({ error: "A name is required." }, 400);
|
|
11708
|
+
patch.name = v;
|
|
11709
|
+
}
|
|
11710
|
+
if ("url" in body) {
|
|
11711
|
+
const v = typeof body.url === "string" ? body.url.trim().replace(/\/$/, "") : "";
|
|
11712
|
+
if (!v || !isHttpUrl(v))
|
|
11713
|
+
return c.json({ error: "A valid http(s) site URL is required." }, 400);
|
|
11714
|
+
patch.url = v;
|
|
11715
|
+
}
|
|
11716
|
+
if ("secret" in body) {
|
|
11717
|
+
const v = typeof body.secret === "string" ? body.secret.trim() : "";
|
|
11718
|
+
if (!v || v.length < 16)
|
|
11719
|
+
return c.json({ error: "A secret of at least 16 characters is required." }, 400);
|
|
11720
|
+
patch.secret = v;
|
|
11721
|
+
}
|
|
11722
|
+
if ("is_active" in body) patch.is_active = !!body.is_active;
|
|
11723
|
+
try {
|
|
11724
|
+
const site = await updateCommunitySite(id, patch);
|
|
11725
|
+
if (!site) return c.json({ error: "Community site not found." }, 404);
|
|
11726
|
+
await loadActiveCommunitySites();
|
|
11727
|
+
return c.json(serializeSite(site));
|
|
11728
|
+
} catch (err) {
|
|
11729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11730
|
+
if (/unique|duplicate/i.test(msg))
|
|
11731
|
+
return c.json({ error: "A community site with this URL already exists." }, 409);
|
|
11732
|
+
throw err;
|
|
11733
|
+
}
|
|
11734
|
+
});
|
|
11735
|
+
communitySiteRoutes.delete("/:id", async (c) => {
|
|
11736
|
+
if (!await serverAdminAllowed(c))
|
|
11737
|
+
return c.json({ error: "Only the server admin can remove a community site." }, 403);
|
|
11738
|
+
const id = c.req.param("id");
|
|
11739
|
+
const removed = await deleteCommunitySite(id);
|
|
11740
|
+
if (!removed) return c.json({ error: "Community site not found." }, 404);
|
|
11741
|
+
await loadActiveCommunitySites();
|
|
11742
|
+
return c.json({ ok: true, id });
|
|
11743
|
+
});
|
|
11744
|
+
|
|
11115
11745
|
// src/mcp/server-routes.ts
|
|
11116
|
-
import { Hono as
|
|
11746
|
+
import { Hono as Hono23 } from "hono";
|
|
11117
11747
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11118
11748
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
11119
11749
|
import { z as z2 } from "zod";
|
|
@@ -11173,7 +11803,9 @@ async function buildNestIndex(userId, nestScope) {
|
|
|
11173
11803
|
let document_count = null;
|
|
11174
11804
|
try {
|
|
11175
11805
|
const { storage } = await engineCache.get(n.id);
|
|
11176
|
-
document_count = (await storage.discoverDocuments()).
|
|
11806
|
+
document_count = (await storage.discoverDocuments()).filter(
|
|
11807
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
11808
|
+
).length;
|
|
11177
11809
|
} catch {
|
|
11178
11810
|
}
|
|
11179
11811
|
return {
|
|
@@ -11309,6 +11941,22 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
11309
11941
|
required: ["nest", "selector"]
|
|
11310
11942
|
}
|
|
11311
11943
|
},
|
|
11944
|
+
{
|
|
11945
|
+
// Read-only, so it belongs on this endpoint: an agent reading a nest's
|
|
11946
|
+
// documents across nests also needs the feedback left on them. Writing a
|
|
11947
|
+
// comment stays on the per-nest endpoint with the other write tools.
|
|
11948
|
+
name: "context_comments",
|
|
11949
|
+
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.",
|
|
11950
|
+
inputSchema: {
|
|
11951
|
+
type: "object",
|
|
11952
|
+
properties: {
|
|
11953
|
+
nest: { type: "string", description: "Nest name or id (see nest_index)" },
|
|
11954
|
+
title: { type: "string", description: "Title of the node" },
|
|
11955
|
+
id: { type: "string", description: "ID of the node" }
|
|
11956
|
+
},
|
|
11957
|
+
required: ["nest"]
|
|
11958
|
+
}
|
|
11959
|
+
},
|
|
11312
11960
|
{
|
|
11313
11961
|
name: "nest_create",
|
|
11314
11962
|
description: "Create a new nest owned by the caller. Returns the new nest's id \u2014 target it with the other tools afterwards.",
|
|
@@ -11421,7 +12069,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
11421
12069
|
}
|
|
11422
12070
|
return server;
|
|
11423
12071
|
}
|
|
11424
|
-
var serverMcpRoutes = new
|
|
12072
|
+
var serverMcpRoutes = new Hono23();
|
|
11425
12073
|
serverMcpRoutes.all("/", async (c) => {
|
|
11426
12074
|
const userId = c.get("userId");
|
|
11427
12075
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -11443,7 +12091,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
11443
12091
|
await server.server.close();
|
|
11444
12092
|
}
|
|
11445
12093
|
});
|
|
11446
|
-
var nestIndexRoutes = new
|
|
12094
|
+
var nestIndexRoutes = new Hono23();
|
|
11447
12095
|
nestIndexRoutes.get("/", async (c) => {
|
|
11448
12096
|
const userId = c.get("userId");
|
|
11449
12097
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -11582,14 +12230,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
11582
12230
|
through steward review before they become AI-visible.
|
|
11583
12231
|
`;
|
|
11584
12232
|
}
|
|
11585
|
-
var llmsTxtRoutes = new
|
|
12233
|
+
var llmsTxtRoutes = new Hono23();
|
|
11586
12234
|
llmsTxtRoutes.get("/", (c) => {
|
|
11587
12235
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
11588
12236
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
11589
12237
|
});
|
|
11590
12238
|
|
|
11591
12239
|
// src/governance/routes.ts
|
|
11592
|
-
import { Hono as
|
|
12240
|
+
import { Hono as Hono24 } from "hono";
|
|
11593
12241
|
|
|
11594
12242
|
// src/governance/feed-service.ts
|
|
11595
12243
|
var SNIPPET_CHARS = 200;
|
|
@@ -11720,7 +12368,7 @@ async function getNestFeed(params) {
|
|
|
11720
12368
|
}
|
|
11721
12369
|
|
|
11722
12370
|
// src/governance/comment-service.ts
|
|
11723
|
-
import { v4 as
|
|
12371
|
+
import { v4 as uuid18 } from "uuid";
|
|
11724
12372
|
async function createComment(params) {
|
|
11725
12373
|
const db = getDb();
|
|
11726
12374
|
const body = (params.body ?? "").trim();
|
|
@@ -11736,7 +12384,7 @@ async function createComment(params) {
|
|
|
11736
12384
|
throw new Error("Parent comment not found on this node");
|
|
11737
12385
|
}
|
|
11738
12386
|
}
|
|
11739
|
-
const id =
|
|
12387
|
+
const id = uuid18();
|
|
11740
12388
|
await db.run(
|
|
11741
12389
|
`INSERT INTO comments
|
|
11742
12390
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -12021,7 +12669,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
12021
12669
|
if (Number.isNaN(n) || n < 1) return def;
|
|
12022
12670
|
return Math.min(n, max);
|
|
12023
12671
|
}
|
|
12024
|
-
var governanceRoutes = new
|
|
12672
|
+
var governanceRoutes = new Hono24();
|
|
12025
12673
|
governanceRoutes.get("/stewards", async (c) => {
|
|
12026
12674
|
const nestId = c.req.param("nestId");
|
|
12027
12675
|
const scope = c.req.query("scope");
|
|
@@ -12388,7 +13036,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
12388
13036
|
const activity = await getActivity({ nestId, limit });
|
|
12389
13037
|
return c.json({ activity });
|
|
12390
13038
|
});
|
|
12391
|
-
var governanceNodeRoutes = new
|
|
13039
|
+
var governanceNodeRoutes = new Hono24();
|
|
12392
13040
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
12393
13041
|
const nestId = c.req.param("nestId");
|
|
12394
13042
|
const nodeId = c.req.param("nodeId");
|
|
@@ -12520,8 +13168,9 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
|
|
|
12520
13168
|
}
|
|
12521
13169
|
const body = await c.req.json();
|
|
12522
13170
|
const author = await getUserEmail4(c);
|
|
13171
|
+
let comment;
|
|
12523
13172
|
try {
|
|
12524
|
-
|
|
13173
|
+
comment = await createComment({
|
|
12525
13174
|
nestId,
|
|
12526
13175
|
nodeId,
|
|
12527
13176
|
author,
|
|
@@ -12530,12 +13179,19 @@ governanceNodeRoutes.post("/:nodeId{.+}/comments", async (c) => {
|
|
|
12530
13179
|
anchor: body.anchor,
|
|
12531
13180
|
parentId: body.parentId
|
|
12532
13181
|
});
|
|
12533
|
-
return c.json({ comment }, 201);
|
|
12534
13182
|
} catch (err) {
|
|
12535
13183
|
throw new ValidationError(
|
|
12536
13184
|
err instanceof Error ? err.message : String(err)
|
|
12537
13185
|
);
|
|
12538
13186
|
}
|
|
13187
|
+
await notifyCommentMentions({
|
|
13188
|
+
nestId,
|
|
13189
|
+
nodeId,
|
|
13190
|
+
body: comment.body,
|
|
13191
|
+
actorEmail: author,
|
|
13192
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
13193
|
+
});
|
|
13194
|
+
return c.json({ comment }, 201);
|
|
12539
13195
|
});
|
|
12540
13196
|
governanceNodeRoutes.post(
|
|
12541
13197
|
"/:nodeId{.+}/comments/:commentId/resolve",
|
|
@@ -12953,7 +13609,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
12953
13609
|
return ok;
|
|
12954
13610
|
}
|
|
12955
13611
|
function createApp() {
|
|
12956
|
-
const app = new
|
|
13612
|
+
const app = new Hono25({ router: new LinearRouter() });
|
|
12957
13613
|
const corsOrigins = config.CORS_ORIGINS;
|
|
12958
13614
|
app.use(
|
|
12959
13615
|
"*",
|
|
@@ -13134,7 +13790,7 @@ function createApp() {
|
|
|
13134
13790
|
return c.json({ error: msg }, 500);
|
|
13135
13791
|
}
|
|
13136
13792
|
});
|
|
13137
|
-
const
|
|
13793
|
+
const serverAdminAllowed2 = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
13138
13794
|
app.use("/admin/settings", flexAuthMiddleware);
|
|
13139
13795
|
app.use("/admin/settings/*", flexAuthMiddleware);
|
|
13140
13796
|
const currentServerSettings = () => ({
|
|
@@ -13163,6 +13819,7 @@ function createApp() {
|
|
|
13163
13819
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
13164
13820
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
13165
13821
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
13822
|
+
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
13166
13823
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
13167
13824
|
// presence is reported, never the value (same discipline as the OIDC secret).
|
|
13168
13825
|
sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
|
|
@@ -13187,12 +13844,12 @@ function createApp() {
|
|
|
13187
13844
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
13188
13845
|
});
|
|
13189
13846
|
app.get("/admin/settings", async (c) => {
|
|
13190
|
-
if (!await
|
|
13847
|
+
if (!await serverAdminAllowed2(c))
|
|
13191
13848
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13192
13849
|
return c.json(currentServerSettings());
|
|
13193
13850
|
});
|
|
13194
13851
|
app.post("/admin/settings/test-notification", async (c) => {
|
|
13195
|
-
if (!await
|
|
13852
|
+
if (!await serverAdminAllowed2(c))
|
|
13196
13853
|
return c.json({ error: "Only the server admin can do this." }, 403);
|
|
13197
13854
|
let body;
|
|
13198
13855
|
try {
|
|
@@ -13206,7 +13863,7 @@ function createApp() {
|
|
|
13206
13863
|
return result.ok ? c.json({ ok: true }) : c.json({ error: result.error }, 502);
|
|
13207
13864
|
});
|
|
13208
13865
|
app.patch("/admin/settings", async (c) => {
|
|
13209
|
-
if (!await
|
|
13866
|
+
if (!await serverAdminAllowed2(c))
|
|
13210
13867
|
return c.json({ error: "Only the server admin can change this." }, 403);
|
|
13211
13868
|
let body;
|
|
13212
13869
|
try {
|
|
@@ -13383,6 +14040,12 @@ function createApp() {
|
|
|
13383
14040
|
value: body.oidc_department_tagging ? "true" : null
|
|
13384
14041
|
});
|
|
13385
14042
|
}
|
|
14043
|
+
if ("people_suggest_all_users" in body) {
|
|
14044
|
+
pending.push({
|
|
14045
|
+
name: "PEOPLE_SUGGEST_ALL_USERS",
|
|
14046
|
+
value: body.people_suggest_all_users ? "true" : null
|
|
14047
|
+
});
|
|
14048
|
+
}
|
|
13386
14049
|
if ("sso_token_exchange_enabled" in body) {
|
|
13387
14050
|
pending.push({
|
|
13388
14051
|
name: "SSO_TOKEN_EXCHANGE_ENABLED",
|
|
@@ -13551,7 +14214,7 @@ function createApp() {
|
|
|
13551
14214
|
});
|
|
13552
14215
|
app.use("/admin/trace", flexAuthMiddleware);
|
|
13553
14216
|
app.get("/admin/trace", async (c) => {
|
|
13554
|
-
if (!await
|
|
14217
|
+
if (!await serverAdminAllowed2(c))
|
|
13555
14218
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13556
14219
|
const limitRaw = Number(c.req.query("limit"));
|
|
13557
14220
|
const offsetRaw = Number(c.req.query("offset"));
|
|
@@ -13569,7 +14232,7 @@ function createApp() {
|
|
|
13569
14232
|
app.use("/admin/super-admins", flexAuthMiddleware);
|
|
13570
14233
|
app.use("/admin/super-admins/*", flexAuthMiddleware);
|
|
13571
14234
|
app.get("/admin/super-admins", async (c) => {
|
|
13572
|
-
if (!await
|
|
14235
|
+
if (!await serverAdminAllowed2(c))
|
|
13573
14236
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
13574
14237
|
const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
|
|
13575
14238
|
const byEmail = /* @__PURE__ */ new Map();
|
|
@@ -13595,7 +14258,7 @@ function createApp() {
|
|
|
13595
14258
|
return c.json({ super_admins: [...byEmail.values()] });
|
|
13596
14259
|
});
|
|
13597
14260
|
app.post("/admin/super-admins", async (c) => {
|
|
13598
|
-
if (!await
|
|
14261
|
+
if (!await serverAdminAllowed2(c))
|
|
13599
14262
|
return c.json({ error: "Only the server admin can grant superadmin." }, 403);
|
|
13600
14263
|
let body;
|
|
13601
14264
|
try {
|
|
@@ -13640,7 +14303,7 @@ function createApp() {
|
|
|
13640
14303
|
return c.json({ email, source: "granted", granted_by: grantedBy }, 201);
|
|
13641
14304
|
});
|
|
13642
14305
|
app.delete("/admin/super-admins/:email", async (c) => {
|
|
13643
|
-
if (!await
|
|
14306
|
+
if (!await serverAdminAllowed2(c))
|
|
13644
14307
|
return c.json({ error: "Only the server admin can revoke superadmin." }, 403);
|
|
13645
14308
|
const email = c.req.param("email").trim().toLowerCase();
|
|
13646
14309
|
const callerEmail = (await resolveCallerEmail(c.get("userId"))).toLowerCase();
|
|
@@ -13681,9 +14344,14 @@ function createApp() {
|
|
|
13681
14344
|
await loadGrantedSuperAdmins();
|
|
13682
14345
|
return c.json({ ok: true, email });
|
|
13683
14346
|
});
|
|
14347
|
+
app.use("/admin/community-sites", flexAuthMiddleware);
|
|
14348
|
+
app.use("/admin/community-sites/*", flexAuthMiddleware);
|
|
14349
|
+
app.route("/admin/community-sites", communitySiteRoutes);
|
|
13684
14350
|
app.use("/teams/*", flexAuthMiddleware);
|
|
13685
14351
|
app.use("/teams", flexAuthMiddleware);
|
|
13686
14352
|
app.route("/teams", teamRoutes);
|
|
14353
|
+
app.use("/people/*", flexAuthMiddleware);
|
|
14354
|
+
app.route("/people", peopleRoutes);
|
|
13687
14355
|
app.use("/me/*", flexAuthMiddleware);
|
|
13688
14356
|
app.get("/me/work", async (c) => {
|
|
13689
14357
|
const userId = c.get("userId");
|
|
@@ -13827,7 +14495,9 @@ function createApp() {
|
|
|
13827
14495
|
for (const nest of visibleNests) {
|
|
13828
14496
|
try {
|
|
13829
14497
|
const { storage } = await engineCache.get(nest.id);
|
|
13830
|
-
const docs = await storage.discoverDocuments()
|
|
14498
|
+
const docs = (await storage.discoverDocuments()).filter(
|
|
14499
|
+
(d) => !isDerivedAnnotationsNode(d.id)
|
|
14500
|
+
);
|
|
13831
14501
|
documents += (await filterAccessible(nest.id, userId, userEmail, docs)).length;
|
|
13832
14502
|
} catch {
|
|
13833
14503
|
}
|
|
@@ -13882,7 +14552,7 @@ function createApp() {
|
|
|
13882
14552
|
app.route("/index", nestIndexRoutes);
|
|
13883
14553
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
13884
14554
|
app.route("/mcp", serverMcpRoutes);
|
|
13885
|
-
const nestsApp = new
|
|
14555
|
+
const nestsApp = new Hono25();
|
|
13886
14556
|
nestsApp.use("*", async (c, next) => {
|
|
13887
14557
|
const parts = nestPathParts(c.req.path);
|
|
13888
14558
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -14414,7 +15084,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
14414
15084
|
}
|
|
14415
15085
|
|
|
14416
15086
|
// src/workflow/scheduler.ts
|
|
14417
|
-
import { v4 as
|
|
15087
|
+
import { v4 as uuid19 } from "uuid";
|
|
14418
15088
|
function isDue(s, now) {
|
|
14419
15089
|
if (!s.last_run_at) return true;
|
|
14420
15090
|
const last = Date.parse(s.last_run_at);
|
|
@@ -14442,7 +15112,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
14442
15112
|
);
|
|
14443
15113
|
continue;
|
|
14444
15114
|
}
|
|
14445
|
-
const id = `run_${
|
|
15115
|
+
const id = `run_${uuid19()}`;
|
|
14446
15116
|
const nowIso2 = new Date(now).toISOString();
|
|
14447
15117
|
await db.run(
|
|
14448
15118
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
@@ -14512,6 +15182,11 @@ async function main() {
|
|
|
14512
15182
|
} catch (err) {
|
|
14513
15183
|
console.error("[access] failed to load DB-granted superadmins:", err);
|
|
14514
15184
|
}
|
|
15185
|
+
try {
|
|
15186
|
+
await loadActiveCommunitySites();
|
|
15187
|
+
} catch (err) {
|
|
15188
|
+
console.error("[sso] failed to load community sites:", err);
|
|
15189
|
+
}
|
|
14515
15190
|
const license = await validateLicense();
|
|
14516
15191
|
if (!license.valid) {
|
|
14517
15192
|
console.warn(`
|