@promptowl/contextnest-community 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +102 -9
- package/CONFIGURATION.md +604 -117
- package/README.md +3 -2
- package/dist/{chunk-JDAOA5KP.js → chunk-H3W72HN3.js} +67 -14
- package/dist/{chunk-2KHM5C7V.js → chunk-MB52DWWP.js} +13 -15
- package/dist/{chunk-TI7HMP64.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-F66VCBLA.js → chunk-O3JEI33N.js} +51 -2
- package/dist/{chunk-7FROMOZM.js → chunk-QMQDWRHN.js} +180 -550
- package/dist/chunk-RXCVXG34.js +523 -0
- package/dist/{chunk-32SZCH36.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-KPGNZLZL.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-DTLMBF4W.js → chunk-VRLAP4HB.js} +42 -17
- package/dist/{engine-TER6FDBP.js → engine-HLYQLP5T.js} +3 -2
- package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-3DFQ4EZZ.js} +4 -3
- package/dist/{grants-service-2OTFS7EH.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +1669 -521
- package/dist/invited-user-XEBX6BM4.js +12 -0
- package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
- package/dist/{review-service-4G2XUFS6.js → review-service-GB43UQVH.js} +7 -6
- package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-KDBPWRZI.js} +4 -3
- package/dist/{version-service-X4FAG2OZ.js → version-service-JYHFPSYP.js} +8 -3
- package/dist/web3/assets/ActivityTracePage-CkRoyIc-.js +1 -0
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-Dfk5uIE5.js → MarkdownEditor-BELFww9q.js} +130 -135
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Bbje2ncY.js +1 -0
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/PersonCombobox-D52m_jhc.js +1 -0
- package/dist/web3/assets/{ReviewActions-BXDTDPp1.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-AW2OLNAE.js +1 -0
- package/dist/web3/assets/{alert-dialog-4SiLKlz8.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-BAgIocLn.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/backlinks-DaVSYvhV.js +19 -0
- package/dist/web3/assets/boxes-VWrCCMzW.js +6 -0
- package/dist/web3/assets/{chevron-left-XR4ReJ9Z.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/count-skeleton-CnEE4uF-.js +1 -0
- package/dist/web3/assets/{earth-CXOcThqn.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-CUjuSs5Q.js → page-BJQuaTRW.js} +3 -3
- package/dist/web3/assets/page-Bb8xGOPQ.js +2 -0
- package/dist/web3/assets/page-BtM-qDzr.js +6 -0
- package/dist/web3/assets/page-Bxd0EG25.js +1 -0
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/page-CfoR0khy.js +6 -0
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/page-DATSlHCW.js +6 -0
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/page-DspptlWr.js +1 -0
- package/dist/web3/assets/{page-xzYqkBUP.js → page-I6vAFJOj.js} +5 -5
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/page-YcNNg-dX.js +1 -0
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-B8xE9j5f.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-CvAgOUPG.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-Br_y1f00.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-BcsKR0ef.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +8 -3
- package/dist/client-D44ZHDTJ.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
- package/dist/web3/assets/AgentDocsPage-Dkhgb1hk.js +0 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
- package/dist/web3/assets/card-UDyMRcps.js +0 -1
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-A0_ymcqL.js +0 -29
- package/dist/web3/assets/index-B5hLclEq.js +0 -389
- package/dist/web3/assets/index-DrUAtoQM.css +0 -1
- package/dist/web3/assets/page-BHD4beun.js +0 -1
- package/dist/web3/assets/page-BSYUXYmO.js +0 -11
- package/dist/web3/assets/page-BYc2DIux.js +0 -45
- package/dist/web3/assets/page-BfibLg8e.js +0 -1
- package/dist/web3/assets/page-CGe8MtUu.js +0 -9
- package/dist/web3/assets/page-CIgPJmG6.js +0 -2
- package/dist/web3/assets/page-Ck31nx9b.js +0 -1
- package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
- package/dist/web3/assets/page-D0sN3GO6.js +0 -1
- package/dist/web3/assets/page-DEpH91X2.js +0 -1
- package/dist/web3/assets/page-l8-B7CGt.js +0 -1
- package/dist/web3/assets/page-mnSHmk6A.js +0 -6
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/select-DrL0mpRN.js +0 -6
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +0 -6
- package/dist/web3/assets/x-qxt1WrUi.js +0 -6
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
submitForReview,
|
|
39
39
|
withNodeWriteLock,
|
|
40
40
|
withdrawDeletionRequest
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-VRLAP4HB.js";
|
|
42
42
|
import {
|
|
43
43
|
canCreateInNest,
|
|
44
44
|
canManageStewards,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
stewardDocumentCreator,
|
|
60
60
|
syncFromConfig,
|
|
61
61
|
updateSteward
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-MB52DWWP.js";
|
|
63
63
|
import {
|
|
64
64
|
createGrant,
|
|
65
65
|
deleteGrant,
|
|
@@ -68,16 +68,7 @@ import {
|
|
|
68
68
|
listGrants,
|
|
69
69
|
listUserGrants,
|
|
70
70
|
resolveNodeGrant
|
|
71
|
-
} from "./chunk-
|
|
72
|
-
import {
|
|
73
|
-
generateApiKey,
|
|
74
|
-
getKeyPrefix,
|
|
75
|
-
hashApiKey,
|
|
76
|
-
hashPassword,
|
|
77
|
-
parseBearerToken,
|
|
78
|
-
verifyKeyRequest,
|
|
79
|
-
verifyPassword
|
|
80
|
-
} from "./chunk-F66VCBLA.js";
|
|
71
|
+
} from "./chunk-VHEMGQ3I.js";
|
|
81
72
|
import {
|
|
82
73
|
attributeRequest,
|
|
83
74
|
checkConflict,
|
|
@@ -97,11 +88,12 @@ import {
|
|
|
97
88
|
getVersions,
|
|
98
89
|
listMyDrafts,
|
|
99
90
|
mergeClient,
|
|
91
|
+
moveApprovedVersion,
|
|
100
92
|
parseClientMetadata,
|
|
101
93
|
serializeClient,
|
|
102
94
|
setApprovedVersion,
|
|
103
95
|
upsertVersion
|
|
104
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-NFFUZF2B.js";
|
|
105
97
|
import {
|
|
106
98
|
approveExternalEdit,
|
|
107
99
|
getExternalEditDetail,
|
|
@@ -113,7 +105,7 @@ import {
|
|
|
113
105
|
scanDocumentForDrift,
|
|
114
106
|
scanNestForDrift,
|
|
115
107
|
startDriftScanner
|
|
116
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-SE5U4J4T.js";
|
|
117
109
|
import {
|
|
118
110
|
addMember,
|
|
119
111
|
addWatcher,
|
|
@@ -121,10 +113,7 @@ import {
|
|
|
121
113
|
assertSafeNodeId,
|
|
122
114
|
buildDocContext,
|
|
123
115
|
buildTitleMap,
|
|
124
|
-
canApproveWith,
|
|
125
|
-
canEditWith,
|
|
126
116
|
capForArchive,
|
|
127
|
-
collabPermToRole,
|
|
128
117
|
createNest,
|
|
129
118
|
createTeam,
|
|
130
119
|
deleteNest,
|
|
@@ -147,7 +136,6 @@ import {
|
|
|
147
136
|
insertOrIgnore,
|
|
148
137
|
installLicenseKey,
|
|
149
138
|
isConfigSuperAdmin,
|
|
150
|
-
isEmailConfigured,
|
|
151
139
|
isLicenseAdminEmail,
|
|
152
140
|
isLicenseAdminUserId,
|
|
153
141
|
isNestArchived,
|
|
@@ -186,7 +174,6 @@ import {
|
|
|
186
174
|
persistSetting,
|
|
187
175
|
pinNest,
|
|
188
176
|
pinnedNestIds,
|
|
189
|
-
primaryRole,
|
|
190
177
|
readIndexedNodes,
|
|
191
178
|
rebuildNodeIndex,
|
|
192
179
|
removeMember,
|
|
@@ -202,7 +189,6 @@ import {
|
|
|
202
189
|
resolveNodeUrl,
|
|
203
190
|
resolveTeamRolesForUser,
|
|
204
191
|
restoreNest,
|
|
205
|
-
sendEmailToRecipient,
|
|
206
192
|
setAllowSelfApprove,
|
|
207
193
|
setCreatorIsReviewer,
|
|
208
194
|
setPrimeOnlyReview,
|
|
@@ -219,9 +205,8 @@ import {
|
|
|
219
205
|
unpinNest,
|
|
220
206
|
unshareTeamFromNest,
|
|
221
207
|
updateMemberRole,
|
|
222
|
-
validateLicense
|
|
223
|
-
|
|
224
|
-
} from "./chunk-7FROMOZM.js";
|
|
208
|
+
validateLicense
|
|
209
|
+
} from "./chunk-QMQDWRHN.js";
|
|
225
210
|
import {
|
|
226
211
|
AppError,
|
|
227
212
|
ConflictError,
|
|
@@ -234,13 +219,34 @@ import {
|
|
|
234
219
|
VaultBusyError,
|
|
235
220
|
isVaultLockTimeout
|
|
236
221
|
} from "./chunk-LHWOJPFE.js";
|
|
222
|
+
import {
|
|
223
|
+
createInvitedUser,
|
|
224
|
+
findOrCreateInvitedUser,
|
|
225
|
+
generateApiKey,
|
|
226
|
+
getKeyPrefix,
|
|
227
|
+
hashApiKey,
|
|
228
|
+
hashPassword,
|
|
229
|
+
parseBearerToken,
|
|
230
|
+
verifyKeyRequest,
|
|
231
|
+
verifyPassword
|
|
232
|
+
} from "./chunk-O3JEI33N.js";
|
|
233
|
+
import {
|
|
234
|
+
canApproveWith,
|
|
235
|
+
canEditWith,
|
|
236
|
+
collabPermToRole,
|
|
237
|
+
isEmailConfigured,
|
|
238
|
+
primaryRole,
|
|
239
|
+
sendEmailToRecipient,
|
|
240
|
+
transportSettings,
|
|
241
|
+
verifyEmailTransport
|
|
242
|
+
} from "./chunk-RXCVXG34.js";
|
|
237
243
|
import {
|
|
238
244
|
config,
|
|
239
245
|
getDb,
|
|
240
246
|
initDb,
|
|
241
247
|
isEmailListish,
|
|
242
248
|
isEmailish
|
|
243
|
-
} from "./chunk-
|
|
249
|
+
} from "./chunk-H3W72HN3.js";
|
|
244
250
|
import {
|
|
245
251
|
ANON_EMAIL,
|
|
246
252
|
ANON_USER_ID,
|
|
@@ -254,9 +260,9 @@ import {
|
|
|
254
260
|
import { serve } from "@hono/node-server";
|
|
255
261
|
|
|
256
262
|
// src/app.ts
|
|
257
|
-
import { Hono as
|
|
263
|
+
import { Hono as Hono28 } from "hono";
|
|
258
264
|
import { LinearRouter } from "hono/router/linear-router";
|
|
259
|
-
import { createMiddleware as
|
|
265
|
+
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
260
266
|
import { cors } from "hono/cors";
|
|
261
267
|
|
|
262
268
|
// src/shared/artifact-sandbox.ts
|
|
@@ -473,6 +479,84 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
473
479
|
);
|
|
474
480
|
});
|
|
475
481
|
|
|
482
|
+
// src/auth/nest-scope-guard.ts
|
|
483
|
+
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
484
|
+
var NEST_SCOPED_KEY_CODE = "nest_scoped_key";
|
|
485
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
486
|
+
"/health",
|
|
487
|
+
"/license/status",
|
|
488
|
+
"/llms.txt",
|
|
489
|
+
"/auth/admin-status"
|
|
490
|
+
]);
|
|
491
|
+
function segmentMatches(seg, nestId) {
|
|
492
|
+
if (seg === nestId) return true;
|
|
493
|
+
try {
|
|
494
|
+
return decodeURIComponent(seg) === nestId;
|
|
495
|
+
} catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function nestKeyVerdict(method, rawPath, scope) {
|
|
500
|
+
const path = rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : rawPath;
|
|
501
|
+
const isGet = method === "GET" || method === "HEAD";
|
|
502
|
+
if (PUBLIC_PATHS.has(path) && isGet) return { allowed: true };
|
|
503
|
+
if (path === "/mcp") return { allowed: true };
|
|
504
|
+
if (path === "/index" && isGet) return { allowed: true };
|
|
505
|
+
if ((path === "/search" || path === "/stats" || path === "/nests") && isGet) {
|
|
506
|
+
return { allowed: true };
|
|
507
|
+
}
|
|
508
|
+
if (path.startsWith("/runs/")) return { allowed: true };
|
|
509
|
+
const m = /^\/nests\/([^/]+)(\/.*)?$/.exec(path);
|
|
510
|
+
if (m) {
|
|
511
|
+
if (segmentMatches(m[1], scope)) return { allowed: true };
|
|
512
|
+
return {
|
|
513
|
+
allowed: false,
|
|
514
|
+
reason: "other_nest",
|
|
515
|
+
mcpTransport: m[2] === "/mcp"
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return { allowed: false, reason: "outside_nest" };
|
|
519
|
+
}
|
|
520
|
+
async function bearerKeyScope(authorization) {
|
|
521
|
+
const key = parseBearerToken(authorization);
|
|
522
|
+
if (!key) return null;
|
|
523
|
+
const row = await getDb().get(
|
|
524
|
+
"SELECT nest_id FROM api_keys WHERE key_hash = ?",
|
|
525
|
+
[hashApiKey(key)]
|
|
526
|
+
);
|
|
527
|
+
return row?.nest_id ?? null;
|
|
528
|
+
}
|
|
529
|
+
var nestScopedKeyGuard = createMiddleware2(async (c, next) => {
|
|
530
|
+
const scope = await bearerKeyScope(c.req.header("Authorization"));
|
|
531
|
+
if (!scope) return next();
|
|
532
|
+
const verdict = nestKeyVerdict(c.req.method, c.req.path, scope);
|
|
533
|
+
if (verdict.allowed) return next();
|
|
534
|
+
const sessionId = getSessionIdFromRequest(c);
|
|
535
|
+
if (sessionId && await resolveSession(sessionId)) return next();
|
|
536
|
+
if (verdict.reason === "other_nest") {
|
|
537
|
+
const error = `API key not authorized for this nest \u2014 this key is scoped to nest "${scope}". Use that nest's MCP URL, or mint a user-level key in the Connect dialog.`;
|
|
538
|
+
if (verdict.mcpTransport) {
|
|
539
|
+
let id = null;
|
|
540
|
+
try {
|
|
541
|
+
id = (await c.req.json())?.id ?? null;
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
return c.json(
|
|
545
|
+
{ jsonrpc: "2.0", id, error: { code: -32e3, message: error } },
|
|
546
|
+
403
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return c.json({ error, code: NEST_SCOPED_KEY_CODE }, 403);
|
|
550
|
+
}
|
|
551
|
+
return c.json(
|
|
552
|
+
{
|
|
553
|
+
error: "This API key is scoped to one nest and can't reach account-wide or server-wide routes (API keys, admin, teams, people, your inbox). Use a key for all your nests for those.",
|
|
554
|
+
code: NEST_SCOPED_KEY_CODE
|
|
555
|
+
},
|
|
556
|
+
403
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
476
560
|
// src/shared/rate-limit.ts
|
|
477
561
|
var buckets = /* @__PURE__ */ new Map();
|
|
478
562
|
function liveBucket(key, cutoff) {
|
|
@@ -719,9 +803,9 @@ function isOidcReady() {
|
|
|
719
803
|
}
|
|
720
804
|
async function discover(issuer) {
|
|
721
805
|
const now = Date.now();
|
|
722
|
-
const
|
|
723
|
-
if (
|
|
724
|
-
return { doc:
|
|
806
|
+
const cached2 = discoveryCache.get(issuer);
|
|
807
|
+
if (cached2 && now - cached2.fetchedAt < DISCOVERY_TTL_MS) {
|
|
808
|
+
return { doc: cached2.doc, jwks: cached2.jwks };
|
|
725
809
|
}
|
|
726
810
|
const res = await fetch(`${issuer}/.well-known/openid-configuration`, {
|
|
727
811
|
headers: { Accept: "application/json" },
|
|
@@ -1310,7 +1394,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1310
1394
|
if (config.MANUAL_SIGN_IN !== "open") {
|
|
1311
1395
|
return c.json(
|
|
1312
1396
|
{
|
|
1313
|
-
error: "Self-registration is disabled on this server. If you were invited,
|
|
1397
|
+
error: "Self-registration is disabled on this server. If you were invited, sign in with the temporary password from your invite email, or continue with PromptOwl using that email. Otherwise ask your admin for an invite."
|
|
1314
1398
|
},
|
|
1315
1399
|
403
|
|
1316
1400
|
);
|
|
@@ -1337,7 +1421,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1337
1421
|
if (existing.is_invited === 1) {
|
|
1338
1422
|
return c.json(
|
|
1339
1423
|
{
|
|
1340
|
-
error: "You've been invited to this server.
|
|
1424
|
+
error: "You've been invited to this server. Sign in with the temporary password from your invite email, or continue with PromptOwl using this email. Your admin can reset the password if you don't have it."
|
|
1341
1425
|
},
|
|
1342
1426
|
409
|
|
1343
1427
|
);
|
|
@@ -1434,10 +1518,36 @@ authRoutes.post("/logout", async (c) => {
|
|
|
1434
1518
|
);
|
|
1435
1519
|
return c.json({ ok: true });
|
|
1436
1520
|
});
|
|
1521
|
+
function refuseNestScopedCaller(c) {
|
|
1522
|
+
if (!c.get("nestScope")) return null;
|
|
1523
|
+
return c.json(
|
|
1524
|
+
{
|
|
1525
|
+
error: "This API key is scoped to one nest and can't create, rotate, list or revoke API keys. Use a key for all your nests, or the web app.",
|
|
1526
|
+
code: NEST_SCOPED_KEY_CODE
|
|
1527
|
+
},
|
|
1528
|
+
403
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
async function checkKeyNest(userId, nestId) {
|
|
1532
|
+
if (typeof nestId !== "string" || !nestId.trim()) {
|
|
1533
|
+
return { error: "nest_id must be a nest id.", status: 400 };
|
|
1534
|
+
}
|
|
1535
|
+
const row = await getDb().get("SELECT id FROM nests WHERE id = ?", [nestId]);
|
|
1536
|
+
const reachable = !!row && (config.AUTH_MODE === "open" || await resolveNestPermission(nestId, userId) !== "none");
|
|
1537
|
+
if (!reachable) {
|
|
1538
|
+
return {
|
|
1539
|
+
error: "Nest not found \u2014 you can only scope a key to a nest you have access to.",
|
|
1540
|
+
status: 404
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1437
1545
|
authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
1438
1546
|
const body = await c.req.json().catch(
|
|
1439
1547
|
() => ({})
|
|
1440
1548
|
);
|
|
1549
|
+
const refused = refuseNestScopedCaller(c);
|
|
1550
|
+
if (refused) return refused;
|
|
1441
1551
|
const db = getDb();
|
|
1442
1552
|
const userId = c.get("userId");
|
|
1443
1553
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1445,6 +1555,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1445
1555
|
label: body.label
|
|
1446
1556
|
});
|
|
1447
1557
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1558
|
+
if (body.nest_id) {
|
|
1559
|
+
const bad = await checkKeyNest(userId, body.nest_id);
|
|
1560
|
+
if (bad) return c.json({ error: bad.error }, bad.status);
|
|
1561
|
+
}
|
|
1448
1562
|
const apiKey = generateApiKey();
|
|
1449
1563
|
const keyId = uuid4();
|
|
1450
1564
|
await db.run(
|
|
@@ -1459,12 +1573,20 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1459
1573
|
]
|
|
1460
1574
|
);
|
|
1461
1575
|
return c.json(
|
|
1462
|
-
{
|
|
1576
|
+
{
|
|
1577
|
+
id: keyId,
|
|
1578
|
+
api_key: apiKey,
|
|
1579
|
+
key_prefix: getKeyPrefix(apiKey),
|
|
1580
|
+
label: check.label,
|
|
1581
|
+
nest_id: body.nest_id || null
|
|
1582
|
+
},
|
|
1463
1583
|
201
|
|
1464
1584
|
);
|
|
1465
1585
|
});
|
|
1466
1586
|
authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
1467
1587
|
const body = await c.req.json().catch(() => ({}));
|
|
1588
|
+
const refused = refuseNestScopedCaller(c);
|
|
1589
|
+
if (refused) return refused;
|
|
1468
1590
|
const db = getDb();
|
|
1469
1591
|
const userId = c.get("userId");
|
|
1470
1592
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1474,6 +1596,10 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1474
1596
|
});
|
|
1475
1597
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1476
1598
|
const prior = check.key;
|
|
1599
|
+
if ("nest_id" in body && body.nest_id) {
|
|
1600
|
+
const bad = await checkKeyNest(userId, body.nest_id);
|
|
1601
|
+
if (bad) return c.json({ error: bad.error }, bad.status);
|
|
1602
|
+
}
|
|
1477
1603
|
const apiKey = generateApiKey();
|
|
1478
1604
|
const keyId = uuid4();
|
|
1479
1605
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
@@ -1506,10 +1632,13 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1506
1632
|
id: keyId,
|
|
1507
1633
|
api_key: apiKey,
|
|
1508
1634
|
key_prefix: getKeyPrefix(apiKey),
|
|
1509
|
-
label: check.label ?? prior.label
|
|
1635
|
+
label: check.label ?? prior.label,
|
|
1636
|
+
nest_id: nestScope
|
|
1510
1637
|
});
|
|
1511
1638
|
});
|
|
1512
1639
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1640
|
+
const refused = refuseNestScopedCaller(c);
|
|
1641
|
+
if (refused) return refused;
|
|
1513
1642
|
const db = getDb();
|
|
1514
1643
|
const userId = c.get("userId");
|
|
1515
1644
|
const check = await verifyKeyRequest(db, userId, { action: "fetch" });
|
|
@@ -1521,6 +1650,8 @@ authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
|
1521
1650
|
return c.json({ keys });
|
|
1522
1651
|
});
|
|
1523
1652
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1653
|
+
const refused = refuseNestScopedCaller(c);
|
|
1654
|
+
if (refused) return refused;
|
|
1524
1655
|
const db = getDb();
|
|
1525
1656
|
const userId = c.get("userId");
|
|
1526
1657
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1904,47 +2035,30 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1904
2035
|
"SELECT id, email, is_invited FROM users WHERE LOWER(email) = ?",
|
|
1905
2036
|
[email]
|
|
1906
2037
|
);
|
|
1907
|
-
if (existing
|
|
2038
|
+
if (existing) {
|
|
1908
2039
|
return c.json(
|
|
1909
2040
|
{
|
|
1910
|
-
error:
|
|
2041
|
+
error: existing.is_invited === 0 ? `${email} is already a member of this server.` : `${email} is already invited but hasn't signed in yet. Use Reset password on their row to issue a new temporary password.`
|
|
1911
2042
|
},
|
|
1912
2043
|
409
|
|
1913
2044
|
);
|
|
1914
2045
|
}
|
|
1915
|
-
const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
|
|
1916
|
-
const passwordHash = await hashPassword(tempPassword);
|
|
1917
|
-
let userId;
|
|
1918
|
-
if (existing) {
|
|
1919
|
-
userId = existing.id;
|
|
1920
|
-
await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
1921
|
-
passwordHash,
|
|
1922
|
-
userId
|
|
1923
|
-
]);
|
|
1924
|
-
} else {
|
|
1925
|
-
userId = uuid4();
|
|
1926
|
-
await db.run(
|
|
1927
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1928
|
-
[userId, email, null, passwordHash]
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1932
2046
|
const inviter = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
1933
2047
|
callerId
|
|
1934
2048
|
]);
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
path: "",
|
|
1939
|
-
actor: email,
|
|
1940
|
-
by: inviter?.email || void 0,
|
|
1941
|
-
tempPassword,
|
|
1942
|
-
link: requestBaseUrl(c.req.url)
|
|
2049
|
+
const { id: userId, tempPassword } = await createInvitedUser(email, {
|
|
2050
|
+
invitedBy: inviter?.email,
|
|
2051
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
1943
2052
|
});
|
|
2053
|
+
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1944
2054
|
return c.json(
|
|
1945
2055
|
{
|
|
1946
2056
|
temporary_password: tempPassword,
|
|
1947
2057
|
user: { id: userId, email },
|
|
2058
|
+
// PromptOwl sign-in only counts as an option for them when it's open to all.
|
|
2059
|
+
promptowl_sign_in: config.PROMPTOWL_SIGN_IN_GATE === "open",
|
|
2060
|
+
// Email is configured, so the invite email (with this password) went out.
|
|
2061
|
+
emailed: isEmailConfigured(),
|
|
1948
2062
|
message: "Share this temporary password securely. Your teammate signs in with their email + this password, then can change it and create their own API key."
|
|
1949
2063
|
},
|
|
1950
2064
|
201
|
|
@@ -1972,7 +2086,12 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
1972
2086
|
const teammates = await db.all(
|
|
1973
2087
|
`SELECT u.id, u.email, u.name, u.is_invited,
|
|
1974
2088
|
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
|
|
1975
|
-
|
|
2089
|
+
-- Latest of API-key use and web-session activity (same text format).
|
|
2090
|
+
(SELECT MAX(t) FROM (
|
|
2091
|
+
SELECT last_used_at AS t FROM api_keys WHERE user_id = u.id
|
|
2092
|
+
UNION ALL
|
|
2093
|
+
SELECT last_seen_at FROM sessions WHERE user_id = u.id
|
|
2094
|
+
) activity) as last_active
|
|
1976
2095
|
FROM users u
|
|
1977
2096
|
WHERE u.id != ?
|
|
1978
2097
|
ORDER BY u.created_at DESC`,
|
|
@@ -2046,6 +2165,35 @@ function stripUndefinedDeep(value) {
|
|
|
2046
2165
|
// src/nodes/service.ts
|
|
2047
2166
|
import { inputJsonSchema } from "@promptowl/contextnest-engine/api";
|
|
2048
2167
|
|
|
2168
|
+
// src/governance/approved-body.ts
|
|
2169
|
+
async function reconstructApproved(versionManager, nestId, nodeId, approved) {
|
|
2170
|
+
try {
|
|
2171
|
+
return { raw: await versionManager.reconstructVersion(nodeId, approved), version: approved };
|
|
2172
|
+
} catch (err) {
|
|
2173
|
+
const current = await getApprovedVersion(nestId, nodeId);
|
|
2174
|
+
if (current != null && current !== approved) {
|
|
2175
|
+
return { raw: await versionManager.reconstructVersion(nodeId, current), version: current };
|
|
2176
|
+
}
|
|
2177
|
+
const newer = (await getVersions(nestId, nodeId)).filter(
|
|
2178
|
+
(v) => v.version > approved && (v.status === "published" || v.status === "approved")
|
|
2179
|
+
);
|
|
2180
|
+
for (const { version } of newer) {
|
|
2181
|
+
let raw;
|
|
2182
|
+
try {
|
|
2183
|
+
raw = await versionManager.reconstructVersion(nodeId, version);
|
|
2184
|
+
} catch {
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
if (await moveApprovedVersion(nestId, nodeId, approved, version)) {
|
|
2188
|
+
console.warn(`[approved-heal] ${nestId}/${nodeId}: approved v${approved} \u2192 v${version} (v${approved} could not be rebuilt)`);
|
|
2189
|
+
}
|
|
2190
|
+
return { raw, version };
|
|
2191
|
+
}
|
|
2192
|
+
console.error(`[approved-heal] ${nestId}/${nodeId}: v${approved} can't be rebuilt and no newer published version can`, err);
|
|
2193
|
+
throw err;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2049
2197
|
// src/governance/prime-service.ts
|
|
2050
2198
|
async function isPrimeDocument(nestId, tags, metadata) {
|
|
2051
2199
|
const flag = documentPrimeFlag(metadata);
|
|
@@ -2086,6 +2234,21 @@ function extractMentions(markdown2) {
|
|
|
2086
2234
|
}
|
|
2087
2235
|
return names;
|
|
2088
2236
|
}
|
|
2237
|
+
function stripMentions(text) {
|
|
2238
|
+
const spans = [];
|
|
2239
|
+
const code = (text || "").replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)).replace(/`[^`\n]*`/g, (m) => " ".repeat(m.length));
|
|
2240
|
+
let match;
|
|
2241
|
+
MENTION.lastIndex = 0;
|
|
2242
|
+
while ((match = MENTION.exec(code)) !== null) {
|
|
2243
|
+
const lead = match[0].startsWith("@") ? 0 : 1;
|
|
2244
|
+
spans.push([match.index + lead, match.index + match[0].length]);
|
|
2245
|
+
}
|
|
2246
|
+
let out = text || "";
|
|
2247
|
+
for (const [start, end] of spans.reverse()) {
|
|
2248
|
+
out = out.slice(0, start) + out.slice(end);
|
|
2249
|
+
}
|
|
2250
|
+
return out.replace(/\s+/g, " ").trim();
|
|
2251
|
+
}
|
|
2089
2252
|
function newMentions(previous, next) {
|
|
2090
2253
|
const before = new Set(extractMentions(previous).map((n) => n.toLowerCase()));
|
|
2091
2254
|
return extractMentions(next).filter((n) => !before.has(n.toLowerCase()));
|
|
@@ -2855,13 +3018,16 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
2855
3018
|
r.myGrant = grantRoleForNode(myGrants, doc.id);
|
|
2856
3019
|
}
|
|
2857
3020
|
if (approvedVersions) {
|
|
2858
|
-
|
|
3021
|
+
let approved = approvedVersions.get(doc.id);
|
|
2859
3022
|
if (approved != null) {
|
|
2860
3023
|
try {
|
|
2861
|
-
|
|
3024
|
+
let raw;
|
|
3025
|
+
({ raw, version: approved } = await reconstructApproved(
|
|
3026
|
+
versionManager,
|
|
3027
|
+
nestId,
|
|
2862
3028
|
doc.id,
|
|
2863
3029
|
approved
|
|
2864
|
-
);
|
|
3030
|
+
));
|
|
2865
3031
|
r.content = bodyOnly(doc.id, raw);
|
|
2866
3032
|
} catch (err) {
|
|
2867
3033
|
console.error(
|
|
@@ -2930,7 +3096,7 @@ async function vaultFolderPaths(nestId, opts = {}) {
|
|
|
2930
3096
|
});
|
|
2931
3097
|
return entries.map(
|
|
2932
3098
|
(e) => prefix && e.path.startsWith(`${prefix}/`) ? e.path.slice(prefix.length + 1) : e.path
|
|
2933
|
-
).filter(
|
|
3099
|
+
).filter((p) => p && !(!prefix && (p === "assets" || p.startsWith("assets/"))));
|
|
2934
3100
|
}
|
|
2935
3101
|
async function listNestFolders(nestId, opts = {}) {
|
|
2936
3102
|
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
@@ -3908,6 +4074,7 @@ async function vaultFolderDir(nestId, rel) {
|
|
|
3908
4074
|
}
|
|
3909
4075
|
async function vaultFolderExists(nestId, rel) {
|
|
3910
4076
|
if (!rel) return false;
|
|
4077
|
+
assertSafeFolder(rel);
|
|
3911
4078
|
try {
|
|
3912
4079
|
return (await stat(await vaultFolderDir(nestId, rel))).isDirectory();
|
|
3913
4080
|
} catch {
|
|
@@ -4066,14 +4233,14 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail, client) {
|
|
|
4066
4233
|
if (isNestStorageRoot(src)) {
|
|
4067
4234
|
throw new ValidationError("Folder is not eligible for sync");
|
|
4068
4235
|
}
|
|
4069
|
-
let
|
|
4236
|
+
let stat3;
|
|
4070
4237
|
try {
|
|
4071
|
-
|
|
4238
|
+
stat3 = statSync(src);
|
|
4072
4239
|
} catch {
|
|
4073
4240
|
console.warn(`[unsynced] source missing: ${src}`);
|
|
4074
4241
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
4075
4242
|
}
|
|
4076
|
-
if (!
|
|
4243
|
+
if (!stat3.isDirectory()) {
|
|
4077
4244
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
4078
4245
|
}
|
|
4079
4246
|
const files = collectMarkdownFiles(src, src);
|
|
@@ -4122,19 +4289,226 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
4122
4289
|
if (isNestStorageRoot(src)) {
|
|
4123
4290
|
throw new ValidationError("Folder is not eligible for delete");
|
|
4124
4291
|
}
|
|
4125
|
-
let
|
|
4292
|
+
let stat3;
|
|
4126
4293
|
try {
|
|
4127
|
-
|
|
4294
|
+
stat3 = statSync(src);
|
|
4128
4295
|
} catch {
|
|
4129
4296
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
4130
4297
|
}
|
|
4131
|
-
if (!
|
|
4298
|
+
if (!stat3.isDirectory()) {
|
|
4132
4299
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
4133
4300
|
}
|
|
4134
4301
|
await rm2(src, { recursive: true, force: true });
|
|
4135
4302
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
4136
4303
|
}
|
|
4137
4304
|
|
|
4305
|
+
// src/shared/branding.ts
|
|
4306
|
+
import { z } from "zod";
|
|
4307
|
+
var HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
4308
|
+
var IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
4309
|
+
var ASSET_NAME_RE = /^[0-9a-f-]{36}\.(?:png|jpe?g|gif|webp)$/;
|
|
4310
|
+
var BRANDING_LIMITS = {
|
|
4311
|
+
site_title: 120,
|
|
4312
|
+
banner_headline: 120,
|
|
4313
|
+
banner_subtext: 280,
|
|
4314
|
+
footer_text: 280,
|
|
4315
|
+
footer_link_label: 80,
|
|
4316
|
+
url: 2048
|
|
4317
|
+
};
|
|
4318
|
+
var IMAGE_FIELDS = ["logo_url", "banner_url", "favicon_url"];
|
|
4319
|
+
var COLOR_FIELDS = ["accent_color", "header_bg_color", "page_tint_color"];
|
|
4320
|
+
var CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/;
|
|
4321
|
+
var optText = (max) => z.string().max(max).refine((s) => !CONTROL_RE.test(s), "must not contain control characters").nullable().optional();
|
|
4322
|
+
var optColor = z.string().regex(HEX_RE, "must be a #rgb or #rrggbb hex color").nullable().optional();
|
|
4323
|
+
var optUrl = z.string().max(BRANDING_LIMITS.url).nullable().optional();
|
|
4324
|
+
var BrandingSchema = z.strictObject({
|
|
4325
|
+
logo_url: optUrl,
|
|
4326
|
+
banner_url: optUrl,
|
|
4327
|
+
banner_headline: optText(BRANDING_LIMITS.banner_headline),
|
|
4328
|
+
banner_subtext: optText(BRANDING_LIMITS.banner_subtext),
|
|
4329
|
+
accent_color: optColor,
|
|
4330
|
+
header_bg_color: optColor,
|
|
4331
|
+
page_tint_color: optColor,
|
|
4332
|
+
site_title: optText(BRANDING_LIMITS.site_title),
|
|
4333
|
+
footer_text: optText(BRANDING_LIMITS.footer_text),
|
|
4334
|
+
footer_link_url: optUrl,
|
|
4335
|
+
footer_link_label: optText(BRANDING_LIMITS.footer_link_label),
|
|
4336
|
+
favicon_url: optUrl,
|
|
4337
|
+
hide_powered_by: z.boolean().optional()
|
|
4338
|
+
});
|
|
4339
|
+
function nestAssetName(url, nestId) {
|
|
4340
|
+
const prefix = `/nests/${nestId}/assets/`;
|
|
4341
|
+
if (!url.startsWith(prefix)) return null;
|
|
4342
|
+
const name = url.slice(prefix.length);
|
|
4343
|
+
return ASSET_NAME_RE.test(name) ? name : null;
|
|
4344
|
+
}
|
|
4345
|
+
function isHttpsImageUrl(url) {
|
|
4346
|
+
if (!/^https:\/\//i.test(url)) return false;
|
|
4347
|
+
let u;
|
|
4348
|
+
try {
|
|
4349
|
+
u = new URL(url);
|
|
4350
|
+
} catch {
|
|
4351
|
+
return false;
|
|
4352
|
+
}
|
|
4353
|
+
if (u.protocol !== "https:" || u.username || u.password || !u.hostname) return false;
|
|
4354
|
+
return IMAGE_EXT_RE.test(u.pathname);
|
|
4355
|
+
}
|
|
4356
|
+
function isHttpsLink(url) {
|
|
4357
|
+
if (!/^https:\/\//i.test(url)) return false;
|
|
4358
|
+
try {
|
|
4359
|
+
const u = new URL(url);
|
|
4360
|
+
return u.protocol === "https:" && !u.username && !u.password && !!u.hostname;
|
|
4361
|
+
} catch {
|
|
4362
|
+
return false;
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
function normalizeHex(v) {
|
|
4366
|
+
if (typeof v !== "string" || !HEX_RE.test(v)) return null;
|
|
4367
|
+
let h = v.slice(1).toLowerCase();
|
|
4368
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
4369
|
+
return "#" + h;
|
|
4370
|
+
}
|
|
4371
|
+
function compact(b) {
|
|
4372
|
+
const out = {};
|
|
4373
|
+
for (const [k, v] of Object.entries(b)) {
|
|
4374
|
+
if (v === null || v === void 0) continue;
|
|
4375
|
+
if (typeof v === "string") {
|
|
4376
|
+
const t = v.trim();
|
|
4377
|
+
if (t === "") continue;
|
|
4378
|
+
out[k] = t;
|
|
4379
|
+
} else if (v === true) {
|
|
4380
|
+
out[k] = true;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
if (Object.keys(out).length === 0) return null;
|
|
4384
|
+
return out;
|
|
4385
|
+
}
|
|
4386
|
+
var BrandingValidationError = class extends Error {
|
|
4387
|
+
};
|
|
4388
|
+
async function validateBrandingInput(body, nestId, assetExists) {
|
|
4389
|
+
if (body === null || body === void 0) return null;
|
|
4390
|
+
const parsed = BrandingSchema.safeParse(body);
|
|
4391
|
+
if (!parsed.success) {
|
|
4392
|
+
const issue = parsed.error.issues[0];
|
|
4393
|
+
const field2 = issue?.path?.join(".") || "branding";
|
|
4394
|
+
const msg = issue?.code === "unrecognized_keys" ? `unknown branding field(s): ${issue.keys.join(", ")}` : `${field2}: ${issue?.message ?? "invalid"}`;
|
|
4395
|
+
throw new BrandingValidationError(msg);
|
|
4396
|
+
}
|
|
4397
|
+
const b = compact(parsed.data);
|
|
4398
|
+
if (!b) return null;
|
|
4399
|
+
for (const f of COLOR_FIELDS) {
|
|
4400
|
+
if (b[f]) b[f] = normalizeHex(b[f]);
|
|
4401
|
+
}
|
|
4402
|
+
for (const f of IMAGE_FIELDS) {
|
|
4403
|
+
const url = b[f];
|
|
4404
|
+
if (!url) continue;
|
|
4405
|
+
const asset = nestAssetName(url, nestId);
|
|
4406
|
+
if (asset) {
|
|
4407
|
+
if (!await assetExists(asset)) {
|
|
4408
|
+
throw new BrandingValidationError(`${f}: no such asset in this nest`);
|
|
4409
|
+
}
|
|
4410
|
+
continue;
|
|
4411
|
+
}
|
|
4412
|
+
if (!isHttpsImageUrl(url)) {
|
|
4413
|
+
throw new BrandingValidationError(
|
|
4414
|
+
`${f}: must be an image uploaded to this nest or an https:// URL ending in .png, .jpg, .gif or .webp`
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
b[f] = new URL(url).href;
|
|
4418
|
+
}
|
|
4419
|
+
if (b.footer_link_url) {
|
|
4420
|
+
if (!isHttpsLink(b.footer_link_url)) {
|
|
4421
|
+
throw new BrandingValidationError("footer_link_url: must be an https:// URL");
|
|
4422
|
+
}
|
|
4423
|
+
b.footer_link_url = new URL(b.footer_link_url).href;
|
|
4424
|
+
}
|
|
4425
|
+
return b;
|
|
4426
|
+
}
|
|
4427
|
+
function parseStoredBranding(raw, nestId) {
|
|
4428
|
+
if (!raw) return null;
|
|
4429
|
+
let data;
|
|
4430
|
+
try {
|
|
4431
|
+
data = JSON.parse(raw);
|
|
4432
|
+
} catch {
|
|
4433
|
+
return null;
|
|
4434
|
+
}
|
|
4435
|
+
const parsed = BrandingSchema.safeParse(data);
|
|
4436
|
+
if (!parsed.success) return null;
|
|
4437
|
+
const b = compact(parsed.data);
|
|
4438
|
+
if (!b) return null;
|
|
4439
|
+
for (const f of IMAGE_FIELDS) {
|
|
4440
|
+
const url = b[f];
|
|
4441
|
+
if (url && !nestAssetName(url, nestId) && !isHttpsImageUrl(url)) delete b[f];
|
|
4442
|
+
}
|
|
4443
|
+
if (b.footer_link_url && !isHttpsLink(b.footer_link_url)) delete b.footer_link_url;
|
|
4444
|
+
const kept = compact(b);
|
|
4445
|
+
if (!kept) return null;
|
|
4446
|
+
return { ...kept, hide_powered_by: kept.hide_powered_by === true };
|
|
4447
|
+
}
|
|
4448
|
+
function rgb(hex) {
|
|
4449
|
+
const h = normalizeHex(hex).slice(1);
|
|
4450
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
4451
|
+
}
|
|
4452
|
+
function toHex([r, g, b]) {
|
|
4453
|
+
return "#" + [r, g, b].map((n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0")).join("");
|
|
4454
|
+
}
|
|
4455
|
+
function relativeLuminance(hex) {
|
|
4456
|
+
const [r, g, b] = rgb(hex).map((v) => {
|
|
4457
|
+
const c = v / 255;
|
|
4458
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
4459
|
+
});
|
|
4460
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
4461
|
+
}
|
|
4462
|
+
function contrastRatio(a, b) {
|
|
4463
|
+
const la = relativeLuminance(a);
|
|
4464
|
+
const lb = relativeLuminance(b);
|
|
4465
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
4466
|
+
}
|
|
4467
|
+
function readableForeground(bg) {
|
|
4468
|
+
return contrastRatio("#000000", bg) >= contrastRatio("#ffffff", bg) ? "#000000" : "#ffffff";
|
|
4469
|
+
}
|
|
4470
|
+
function mixHex(a, b, weight) {
|
|
4471
|
+
const ca = rgb(a);
|
|
4472
|
+
const cb = rgb(b);
|
|
4473
|
+
return toHex([0, 1, 2].map((i) => ca[i] * weight + cb[i] * (1 - weight)));
|
|
4474
|
+
}
|
|
4475
|
+
function ensureContrast(fg, bg, min = 4.5) {
|
|
4476
|
+
const start = normalizeHex(fg);
|
|
4477
|
+
if (contrastRatio(start, bg) >= min) return start;
|
|
4478
|
+
const target = readableForeground(bg);
|
|
4479
|
+
for (let w = 0.05; w <= 1.0001; w += 0.05) {
|
|
4480
|
+
const c = mixHex(target, start, w);
|
|
4481
|
+
if (contrastRatio(c, bg) >= min) return c;
|
|
4482
|
+
}
|
|
4483
|
+
return target;
|
|
4484
|
+
}
|
|
4485
|
+
function publishBrandCssVars(b) {
|
|
4486
|
+
if (!b) return "";
|
|
4487
|
+
const decls = [];
|
|
4488
|
+
const tint = normalizeHex(b.page_tint_color);
|
|
4489
|
+
const page = tint ? mixHex(tint, "#ffffff", 0.1) : "#ffffff";
|
|
4490
|
+
const accent = normalizeHex(b.accent_color);
|
|
4491
|
+
if (accent) {
|
|
4492
|
+
const text = ensureContrast(ensureContrast(accent, page), "#ffffff");
|
|
4493
|
+
decls.push(
|
|
4494
|
+
`--cn-accent:${accent}`,
|
|
4495
|
+
`--cn-accent-fg:${readableForeground(accent)}`,
|
|
4496
|
+
`--cn-accent-text:${text}`
|
|
4497
|
+
);
|
|
4498
|
+
}
|
|
4499
|
+
const header = normalizeHex(b.header_bg_color);
|
|
4500
|
+
if (header) decls.push(`--cn-header-bg:${header}`, `--cn-header-fg:${readableForeground(header)}`);
|
|
4501
|
+
if (tint) decls.push(`--cn-page-bg:${page}`);
|
|
4502
|
+
return decls.length ? `:root{${decls.join(";")}}` : "";
|
|
4503
|
+
}
|
|
4504
|
+
function cssUrl(url) {
|
|
4505
|
+
const safe = url.replace(
|
|
4506
|
+
/["'()\\\s\u0000-\u001f\u007f]/g,
|
|
4507
|
+
(ch) => "%" + ch.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
|
|
4508
|
+
);
|
|
4509
|
+
return `url("${safe}")`;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4138
4512
|
// src/nests/routes.ts
|
|
4139
4513
|
async function effectiveAccess(nestId, userId) {
|
|
4140
4514
|
const access = await resolveNestAccess(nestId, userId);
|
|
@@ -4157,6 +4531,11 @@ function isGrantOnly(access) {
|
|
|
4157
4531
|
return access.permission === "none" && access.hasGrant;
|
|
4158
4532
|
}
|
|
4159
4533
|
var nestRoutes = new Hono3();
|
|
4534
|
+
function withParsedBranding(nest) {
|
|
4535
|
+
if (!nest) return nest;
|
|
4536
|
+
const { branding_json, ...rest } = nest;
|
|
4537
|
+
return { ...rest, branding: parseStoredBranding(branding_json, nest.id) };
|
|
4538
|
+
}
|
|
4160
4539
|
nestRoutes.get("/", async (c) => {
|
|
4161
4540
|
const userId = c.get("userId");
|
|
4162
4541
|
const archivedOnly = c.req.query("archived") === "1";
|
|
@@ -4212,9 +4591,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
4212
4591
|
pinned: pinned.has(n.id)
|
|
4213
4592
|
};
|
|
4214
4593
|
};
|
|
4594
|
+
const nestScope = c.get("nestScope");
|
|
4215
4595
|
const seen = /* @__PURE__ */ new Set();
|
|
4216
4596
|
const deduped = [];
|
|
4217
4597
|
for (const n of [...owned, ...shared, ...publicExtras]) {
|
|
4598
|
+
if (nestScope && n.id !== nestScope) continue;
|
|
4218
4599
|
if (seen.has(n.id)) continue;
|
|
4219
4600
|
seen.add(n.id);
|
|
4220
4601
|
deduped.push(n);
|
|
@@ -4279,7 +4660,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
4279
4660
|
}
|
|
4280
4661
|
}
|
|
4281
4662
|
}
|
|
4282
|
-
const withMeta = out.map(
|
|
4663
|
+
const withMeta = out.map((raw) => withParsedBranding(raw)).map(
|
|
4283
4664
|
(n) => n.grant_only ? {
|
|
4284
4665
|
...n,
|
|
4285
4666
|
document_count: grantedDocsByNest.get(n.id) ?? 0,
|
|
@@ -4305,7 +4686,7 @@ nestRoutes.post("/", async (c) => {
|
|
|
4305
4686
|
throw new ValidationError("name is required");
|
|
4306
4687
|
}
|
|
4307
4688
|
const nest = await createNest(c.get("userId"), body.name, body.description);
|
|
4308
|
-
return c.json({ nest }, 201);
|
|
4689
|
+
return c.json({ nest: withParsedBranding(nest) }, 201);
|
|
4309
4690
|
});
|
|
4310
4691
|
nestRoutes.post("/:nestId", async (c) => {
|
|
4311
4692
|
if (c.req.param("nestId") !== "import") {
|
|
@@ -4323,7 +4704,7 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4323
4704
|
const email2 = await resolveCallerEmail(userId);
|
|
4324
4705
|
if (body.finalize) {
|
|
4325
4706
|
return c.json({
|
|
4326
|
-
nest: nest2,
|
|
4707
|
+
nest: withParsedBranding(nest2),
|
|
4327
4708
|
documents: await registerImportedDocuments(
|
|
4328
4709
|
target,
|
|
4329
4710
|
email2,
|
|
@@ -4354,14 +4735,14 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4354
4735
|
try {
|
|
4355
4736
|
const { rejected } = await stageImportedFiles(nest.id, files, email);
|
|
4356
4737
|
if (body.partial) {
|
|
4357
|
-
return c.json({ nest, rejected }, 201);
|
|
4738
|
+
return c.json({ nest: withParsedBranding(nest), rejected }, 201);
|
|
4358
4739
|
}
|
|
4359
4740
|
const documents = await registerImportedDocuments(
|
|
4360
4741
|
nest.id,
|
|
4361
4742
|
email,
|
|
4362
4743
|
attributeRequest(c, body.client)
|
|
4363
4744
|
);
|
|
4364
|
-
return c.json({ nest, documents, rejected }, 201);
|
|
4745
|
+
return c.json({ nest: withParsedBranding(nest), documents, rejected }, 201);
|
|
4365
4746
|
} catch (err) {
|
|
4366
4747
|
await deleteNest(nest.id).catch(() => {
|
|
4367
4748
|
});
|
|
@@ -4427,7 +4808,7 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
4427
4808
|
const grant_only = roles.length === 0 && isGrantOnly(access);
|
|
4428
4809
|
const nest = await getNest(nestId);
|
|
4429
4810
|
return c.json({
|
|
4430
|
-
nest,
|
|
4811
|
+
nest: withParsedBranding(nest),
|
|
4431
4812
|
permission,
|
|
4432
4813
|
roles: grant_only ? ["viewer"] : roles,
|
|
4433
4814
|
myStewards,
|
|
@@ -4451,7 +4832,7 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
4451
4832
|
name: body.name,
|
|
4452
4833
|
description: body.description
|
|
4453
4834
|
});
|
|
4454
|
-
return c.json({ nest });
|
|
4835
|
+
return c.json({ nest: withParsedBranding(nest) });
|
|
4455
4836
|
});
|
|
4456
4837
|
nestRoutes.post("/:nestId/pin", async (c) => {
|
|
4457
4838
|
const nestId = c.req.param("nestId");
|
|
@@ -4594,21 +4975,10 @@ async function addCollaborator(params) {
|
|
|
4594
4975
|
if (!isEmailish(params.email)) {
|
|
4595
4976
|
throw new ValidationError("Enter a valid email address.");
|
|
4596
4977
|
}
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
);
|
|
4602
|
-
if (existing) {
|
|
4603
|
-
userId = existing.id;
|
|
4604
|
-
} else {
|
|
4605
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4606
|
-
userId = uuid6();
|
|
4607
|
-
await db.run(
|
|
4608
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4609
|
-
[userId, email, null, await hashPassword2(uuid6())]
|
|
4610
|
-
);
|
|
4611
|
-
}
|
|
4978
|
+
userId = (await findOrCreateInvitedUser(params.email, {
|
|
4979
|
+
invitedBy: params.grantedByEmail,
|
|
4980
|
+
baseUrl: params.baseUrl
|
|
4981
|
+
})).id;
|
|
4612
4982
|
}
|
|
4613
4983
|
if (!userId) {
|
|
4614
4984
|
throw new ValidationError("user_id or email is required");
|
|
@@ -4667,13 +5037,18 @@ async function addCollaborator(params) {
|
|
|
4667
5037
|
// event. Slack/Teams still fire.
|
|
4668
5038
|
{ skipEmail: true }
|
|
4669
5039
|
);
|
|
4670
|
-
const
|
|
5040
|
+
const invitee = await db.get(
|
|
5041
|
+
"SELECT email, is_invited FROM users WHERE id = ?",
|
|
5042
|
+
[userId]
|
|
5043
|
+
);
|
|
5044
|
+
const inviteeEmail = params.email || invitee?.email;
|
|
4671
5045
|
if (inviteeEmail) {
|
|
5046
|
+
const nestLink = docLink(nestId, void 0, params.baseUrl);
|
|
4672
5047
|
void sendEmailToRecipient(inviteeEmail, {
|
|
4673
5048
|
status: "shared",
|
|
4674
5049
|
docTitle: shareNestName,
|
|
4675
5050
|
path: shareNestName,
|
|
4676
|
-
link:
|
|
5051
|
+
link: nestLink && invitee?.is_invited === 1 ? `${nestLink}&invite=${encodeURIComponent(inviteeEmail)}` : nestLink,
|
|
4677
5052
|
actor: inviteeEmail,
|
|
4678
5053
|
permission: params.permission,
|
|
4679
5054
|
by: params.grantedByEmail || void 0
|
|
@@ -4867,10 +5242,78 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4867
5242
|
});
|
|
4868
5243
|
});
|
|
4869
5244
|
|
|
4870
|
-
// src/nests/
|
|
5245
|
+
// src/nests/branding-routes.ts
|
|
4871
5246
|
import { Hono as Hono5 } from "hono";
|
|
4872
|
-
import {
|
|
4873
|
-
|
|
5247
|
+
import { access as fsAccess } from "fs/promises";
|
|
5248
|
+
import { join as join3 } from "path";
|
|
5249
|
+
var brandingRoutes = new Hono5();
|
|
5250
|
+
brandingRoutes.patch("/branding", async (c) => {
|
|
5251
|
+
const userId = c.get("userId");
|
|
5252
|
+
const nestId = c.req.param("nestId");
|
|
5253
|
+
const isServerAdmin = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
|
|
5254
|
+
if (!isServerAdmin) {
|
|
5255
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
5256
|
+
if (permissionLevel(access.permission) < permissionLevel("admin")) {
|
|
5257
|
+
throw new ForbiddenError(
|
|
5258
|
+
"Only the nest owner, a nest admin, or the server admin can change branding"
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
const db = getDb();
|
|
5263
|
+
if (!await db.get("SELECT id FROM nests WHERE id = ?", [nestId])) {
|
|
5264
|
+
throw new NotFoundError("Nest not found");
|
|
5265
|
+
}
|
|
5266
|
+
const text = await c.req.text();
|
|
5267
|
+
let body = null;
|
|
5268
|
+
if (text.trim() !== "") {
|
|
5269
|
+
try {
|
|
5270
|
+
body = JSON.parse(text);
|
|
5271
|
+
} catch {
|
|
5272
|
+
throw new ValidationError("body must be JSON");
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
if (body !== null && (typeof body !== "object" || Array.isArray(body))) {
|
|
5276
|
+
throw new ValidationError("branding must be an object or null");
|
|
5277
|
+
}
|
|
5278
|
+
const assetsDir = join3(resolveNestPath(nestId), "assets");
|
|
5279
|
+
let next;
|
|
5280
|
+
try {
|
|
5281
|
+
next = await validateBrandingInput(body, nestId, async (name) => {
|
|
5282
|
+
try {
|
|
5283
|
+
await fsAccess(join3(assetsDir, name));
|
|
5284
|
+
return true;
|
|
5285
|
+
} catch {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
});
|
|
5289
|
+
} catch (err) {
|
|
5290
|
+
if (err instanceof BrandingValidationError) throw new ValidationError(err.message);
|
|
5291
|
+
throw err;
|
|
5292
|
+
}
|
|
5293
|
+
const mentionsHide = body !== null && Object.prototype.hasOwnProperty.call(body, "hide_powered_by");
|
|
5294
|
+
const stored = await db.transaction(async (tx) => {
|
|
5295
|
+
const lock = db.dialect === "postgres" ? " FOR UPDATE" : "";
|
|
5296
|
+
const row = await tx.get(`SELECT branding_json FROM nests WHERE id = ?${lock}`, [nestId]);
|
|
5297
|
+
if (!row) throw new NotFoundError("Nest not found");
|
|
5298
|
+
const wasHidden = parseStoredBranding(row.branding_json, nestId)?.hide_powered_by === true;
|
|
5299
|
+
if (!isServerAdmin) {
|
|
5300
|
+
if (next?.hide_powered_by === true && !wasHidden) {
|
|
5301
|
+
throw new ForbiddenError(
|
|
5302
|
+
'Only the server admin can hide the "Powered by ContextNest" line'
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
if (!mentionsHide && wasHidden) next = { ...next ?? {}, hide_powered_by: true };
|
|
5306
|
+
}
|
|
5307
|
+
const value = next ? JSON.stringify(next) : null;
|
|
5308
|
+
await tx.run("UPDATE nests SET branding_json = ? WHERE id = ?", [value, nestId]);
|
|
5309
|
+
return value;
|
|
5310
|
+
});
|
|
5311
|
+
return c.json({ branding: parseStoredBranding(stored, nestId) });
|
|
5312
|
+
});
|
|
5313
|
+
|
|
5314
|
+
// src/nests/grant-routes.ts
|
|
5315
|
+
import { Hono as Hono6 } from "hono";
|
|
5316
|
+
var grantRoutes = new Hono6();
|
|
4874
5317
|
async function requireManage(c, nestId, opts = {}) {
|
|
4875
5318
|
if (config.AUTH_MODE === "open") return;
|
|
4876
5319
|
const access = await resolveNestAccess(nestId, c.get("userId"));
|
|
@@ -4886,17 +5329,8 @@ async function requireManage(c, nestId, opts = {}) {
|
|
|
4886
5329
|
}
|
|
4887
5330
|
}
|
|
4888
5331
|
async function resolveGrantee(email) {
|
|
4889
|
-
const
|
|
4890
|
-
|
|
4891
|
-
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
4892
|
-
if (existing) return existing.id;
|
|
4893
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4894
|
-
const id = uuid7();
|
|
4895
|
-
await db.run(
|
|
4896
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4897
|
-
[id, e, null, await hashPassword2(uuid7())]
|
|
4898
|
-
);
|
|
4899
|
-
return id;
|
|
5332
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
|
|
5333
|
+
return (await findOrCreateInvitedUser2(email)).id;
|
|
4900
5334
|
}
|
|
4901
5335
|
async function assertGrantAddsAccess(params) {
|
|
4902
5336
|
const { nestId, email, userId, targetType, target, role } = params;
|
|
@@ -4993,10 +5427,10 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4993
5427
|
});
|
|
4994
5428
|
|
|
4995
5429
|
// src/nests/publish-routes.ts
|
|
4996
|
-
import { Hono as
|
|
5430
|
+
import { Hono as Hono7 } from "hono";
|
|
4997
5431
|
|
|
4998
5432
|
// src/nests/publish-service.ts
|
|
4999
|
-
import { v4 as
|
|
5433
|
+
import { v4 as uuid7 } from "uuid";
|
|
5000
5434
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
5001
5435
|
var MODES = ["public", "email", "code", "invite"];
|
|
5002
5436
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -5065,7 +5499,7 @@ async function createPublish(params) {
|
|
|
5065
5499
|
);
|
|
5066
5500
|
}
|
|
5067
5501
|
const db = getDb();
|
|
5068
|
-
const id =
|
|
5502
|
+
const id = uuid7();
|
|
5069
5503
|
const slug = newSlug();
|
|
5070
5504
|
const created = nowIso();
|
|
5071
5505
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -5096,7 +5530,7 @@ async function createPublish(params) {
|
|
|
5096
5530
|
await db.run(
|
|
5097
5531
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5098
5532
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
5099
|
-
[
|
|
5533
|
+
[uuid7(), id, email, created]
|
|
5100
5534
|
);
|
|
5101
5535
|
}
|
|
5102
5536
|
}
|
|
@@ -5151,7 +5585,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
5151
5585
|
await getDb().run(
|
|
5152
5586
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5153
5587
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
5154
|
-
[
|
|
5588
|
+
[uuid7(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
5155
5589
|
);
|
|
5156
5590
|
return token;
|
|
5157
5591
|
}
|
|
@@ -5181,7 +5615,7 @@ async function createSession2(publishId, email) {
|
|
|
5181
5615
|
await getDb().run(
|
|
5182
5616
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5183
5617
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
5184
|
-
[
|
|
5618
|
+
[uuid7(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
5185
5619
|
);
|
|
5186
5620
|
return token;
|
|
5187
5621
|
}
|
|
@@ -5198,7 +5632,7 @@ async function logView(publishId, email) {
|
|
|
5198
5632
|
await getDb().run(
|
|
5199
5633
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5200
5634
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
5201
|
-
[
|
|
5635
|
+
[uuid7(), publishId, email, nowIso()]
|
|
5202
5636
|
);
|
|
5203
5637
|
}
|
|
5204
5638
|
async function listAllowlist(publishId) {
|
|
@@ -5216,7 +5650,7 @@ async function listViews(publishId) {
|
|
|
5216
5650
|
}
|
|
5217
5651
|
|
|
5218
5652
|
// src/nests/publish-routes.ts
|
|
5219
|
-
var publishRoutes = new
|
|
5653
|
+
var publishRoutes = new Hono7();
|
|
5220
5654
|
async function requireManage2(c, nestId) {
|
|
5221
5655
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
5222
5656
|
throw new ForbiddenError(
|
|
@@ -5231,7 +5665,7 @@ publishRoutes.post("/", async (c) => {
|
|
|
5231
5665
|
const mode = body.access_mode ?? "public";
|
|
5232
5666
|
if ((mode === "email" || mode === "invite") && !isEmailConfigured()) {
|
|
5233
5667
|
throw new ValidationError(
|
|
5234
|
-
"Email and invite links need email set up on this server (
|
|
5668
|
+
"Email and invite links need email set up on this server (SMTP or AWS SES, plus NOTIFY_EMAIL_FROM). Use a public or passcode link instead."
|
|
5235
5669
|
);
|
|
5236
5670
|
}
|
|
5237
5671
|
const row = await createPublish({
|
|
@@ -5287,11 +5721,11 @@ publishRoutes.delete("/:id", async (c) => {
|
|
|
5287
5721
|
});
|
|
5288
5722
|
|
|
5289
5723
|
// src/nests/publish-render-routes.ts
|
|
5290
|
-
import { Hono as
|
|
5724
|
+
import { Hono as Hono8 } from "hono";
|
|
5291
5725
|
import MarkdownIt from "markdown-it";
|
|
5292
5726
|
|
|
5293
5727
|
// src/annotations/service.ts
|
|
5294
|
-
import { v4 as
|
|
5728
|
+
import { v4 as uuid8 } from "uuid";
|
|
5295
5729
|
|
|
5296
5730
|
// src/annotations/projection.ts
|
|
5297
5731
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -5445,7 +5879,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5445
5879
|
throw new Error("comment body is required");
|
|
5446
5880
|
}
|
|
5447
5881
|
const db = getDb();
|
|
5448
|
-
const id =
|
|
5882
|
+
const id = uuid8();
|
|
5449
5883
|
const anchor = clampAnchor(input.anchor);
|
|
5450
5884
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
5451
5885
|
await db.transaction(async (tx) => {
|
|
@@ -5465,7 +5899,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5465
5899
|
await tx.run(
|
|
5466
5900
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5467
5901
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5468
|
-
[
|
|
5902
|
+
[uuid8(), id, authorEmail, body]
|
|
5469
5903
|
);
|
|
5470
5904
|
});
|
|
5471
5905
|
return rowToThread(await getThreadRow(id));
|
|
@@ -5487,7 +5921,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
5487
5921
|
await db.run(
|
|
5488
5922
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5489
5923
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5490
|
-
[
|
|
5924
|
+
[uuid8(), threadId, authorEmail, trimmed]
|
|
5491
5925
|
);
|
|
5492
5926
|
return rowToThread(await getThreadRow(threadId));
|
|
5493
5927
|
}
|
|
@@ -5608,7 +6042,9 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
5608
6042
|
}
|
|
5609
6043
|
|
|
5610
6044
|
// src/nests/publish-render-routes.ts
|
|
5611
|
-
|
|
6045
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
6046
|
+
import { join as join4 } from "path";
|
|
6047
|
+
var publishRenderRoutes = new Hono8();
|
|
5612
6048
|
var SESSION_TTL_SECONDS2 = 24 * 60 * 60;
|
|
5613
6049
|
var GATE_LIMIT = { max: 10, windowMs: 10 * 6e4 };
|
|
5614
6050
|
var COMMENT_LIMIT = { max: 5, windowMs: 10 * 6e4 };
|
|
@@ -5665,6 +6101,114 @@ function setSessionCookie2(c, slug, token) {
|
|
|
5665
6101
|
function esc(s) {
|
|
5666
6102
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5667
6103
|
}
|
|
6104
|
+
var IMAGE_MIME = {
|
|
6105
|
+
png: "image/png",
|
|
6106
|
+
jpg: "image/jpeg",
|
|
6107
|
+
jpeg: "image/jpeg",
|
|
6108
|
+
gif: "image/gif",
|
|
6109
|
+
webp: "image/webp"
|
|
6110
|
+
};
|
|
6111
|
+
var INLINE_CAP = { logo: 512 * 1024, favicon: 256 * 1024, banner: 2 * 1024 * 1024 };
|
|
6112
|
+
var INLINE_CACHE_MAX_CHARS = 24 * 1024 * 1024;
|
|
6113
|
+
var inlineCache = /* @__PURE__ */ new Map();
|
|
6114
|
+
var inlineCacheChars = 0;
|
|
6115
|
+
function cacheGet(key) {
|
|
6116
|
+
if (!inlineCache.has(key)) return void 0;
|
|
6117
|
+
const v = inlineCache.get(key);
|
|
6118
|
+
inlineCache.delete(key);
|
|
6119
|
+
inlineCache.set(key, v);
|
|
6120
|
+
return v;
|
|
6121
|
+
}
|
|
6122
|
+
function cachePut(key, v) {
|
|
6123
|
+
const size = v?.length ?? 0;
|
|
6124
|
+
if (size > INLINE_CACHE_MAX_CHARS) return;
|
|
6125
|
+
inlineCacheChars -= inlineCache.get(key)?.length ?? 0;
|
|
6126
|
+
inlineCache.set(key, v);
|
|
6127
|
+
inlineCacheChars += size;
|
|
6128
|
+
for (const [k, old] of inlineCache) {
|
|
6129
|
+
if (inlineCacheChars <= INLINE_CACHE_MAX_CHARS && inlineCache.size <= 512) break;
|
|
6130
|
+
inlineCache.delete(k);
|
|
6131
|
+
inlineCacheChars -= old?.length ?? 0;
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
async function inlineAsset(nestId, url, cap) {
|
|
6135
|
+
if (!url) return null;
|
|
6136
|
+
const name = nestAssetName(url, nestId);
|
|
6137
|
+
if (!name) return null;
|
|
6138
|
+
const key = `${nestId}/${name}/${cap}`;
|
|
6139
|
+
const hit = cacheGet(key);
|
|
6140
|
+
if (hit !== void 0) return hit;
|
|
6141
|
+
let out = null;
|
|
6142
|
+
try {
|
|
6143
|
+
const path = join4(resolveNestPath(nestId), "assets", name);
|
|
6144
|
+
if ((await stat2(path)).size <= cap) {
|
|
6145
|
+
const mime = IMAGE_MIME[name.split(".").pop().toLowerCase()];
|
|
6146
|
+
if (mime) out = `data:${mime};base64,${(await readFile(path)).toString("base64")}`;
|
|
6147
|
+
}
|
|
6148
|
+
} catch {
|
|
6149
|
+
return null;
|
|
6150
|
+
}
|
|
6151
|
+
cachePut(key, out);
|
|
6152
|
+
return out;
|
|
6153
|
+
}
|
|
6154
|
+
async function loadBrand(row) {
|
|
6155
|
+
let b = null;
|
|
6156
|
+
try {
|
|
6157
|
+
const r = await getDb().get("SELECT branding_json FROM nests WHERE id = ?", [row.nest_id]);
|
|
6158
|
+
b = parseStoredBranding(r?.branding_json, row.nest_id);
|
|
6159
|
+
} catch {
|
|
6160
|
+
b = null;
|
|
6161
|
+
}
|
|
6162
|
+
if (!b) return null;
|
|
6163
|
+
const [logo, banner, favicon] = await Promise.all([
|
|
6164
|
+
inlineAsset(row.nest_id, b.logo_url, INLINE_CAP.logo),
|
|
6165
|
+
inlineAsset(row.nest_id, b.banner_url, INLINE_CAP.banner),
|
|
6166
|
+
inlineAsset(row.nest_id, b.favicon_url, INLINE_CAP.favicon)
|
|
6167
|
+
]);
|
|
6168
|
+
return { b, logo, banner, favicon };
|
|
6169
|
+
}
|
|
6170
|
+
var BRAND_CSS = `
|
|
6171
|
+
.cn-brand{display:flex;align-items:center;gap:12px;padding:12px 24px;background:var(--cn-header-bg,#fff);color:var(--cn-header-fg,#1c1917);border-bottom:1px solid rgba(0,0,0,.08);font:600 16px/1.3 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
6172
|
+
.cn-brand img{height:32px;width:auto;max-width:160px;object-fit:contain}
|
|
6173
|
+
.cn-brand span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6174
|
+
.cn-hero{position:relative;min-height:120px;max-height:220px;height:28vw;display:flex;flex-direction:column;justify-content:flex-end;padding:20px 24px;box-sizing:border-box;background:var(--cn-accent,#142a80) center/cover no-repeat;color:var(--cn-accent-fg,#fff);overflow:hidden}
|
|
6175
|
+
.cn-hero.has-img{color:#fff}
|
|
6176
|
+
.cn-hero.has-img::before{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.62),rgba(0,0,0,.08))}
|
|
6177
|
+
.cn-hero *{position:relative;margin:0;max-width:820px}
|
|
6178
|
+
.cn-hero strong{display:block;font:700 clamp(20px,4vw,30px)/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
6179
|
+
.cn-hero p{margin-top:6px;font-size:15px;opacity:.92}
|
|
6180
|
+
.cn-foot{max-width:820px;margin:0 auto;padding:20px 24px 40px;border-top:1px solid #e7e5e4;color:#78716c;font-size:13px;display:flex;flex-wrap:wrap;gap:6px 16px}
|
|
6181
|
+
.cn-foot a{color:var(--cn-accent-text,#142a80)}
|
|
6182
|
+
`;
|
|
6183
|
+
function brandHead(brand, extraCss = "") {
|
|
6184
|
+
if (!brand) return "";
|
|
6185
|
+
const icon = brand.favicon ? `<link rel="icon" href="${esc(brand.favicon)}">` : "";
|
|
6186
|
+
return `<style>${publishBrandCssVars(brand.b)}${BRAND_CSS}${extraCss}</style>${icon}`;
|
|
6187
|
+
}
|
|
6188
|
+
function brandBar(brand) {
|
|
6189
|
+
if (!brand) return "";
|
|
6190
|
+
const title = brand.b.site_title ? `<span>${esc(brand.b.site_title)}</span>` : "";
|
|
6191
|
+
const logo = brand.logo ? `<img src="${esc(brand.logo)}" alt="">` : "";
|
|
6192
|
+
if (!title && !logo) return "";
|
|
6193
|
+
return `<header class="cn-brand">${logo}${title}</header>`;
|
|
6194
|
+
}
|
|
6195
|
+
function brandHero(brand) {
|
|
6196
|
+
if (!brand) return "";
|
|
6197
|
+
const { banner_headline: h, banner_subtext: sub } = brand.b;
|
|
6198
|
+
if (!brand.banner && !h) return "";
|
|
6199
|
+
const style = brand.banner ? ` style="background-image:${esc(cssUrl(brand.banner))}"` : "";
|
|
6200
|
+
const cls = brand.banner ? "cn-hero has-img" : "cn-hero";
|
|
6201
|
+
return `<div class="${cls}"${style}>${h ? `<strong>${esc(h)}</strong>` : ""}${sub ? `<p>${esc(sub)}</p>` : ""}</div>`;
|
|
6202
|
+
}
|
|
6203
|
+
function brandFooter(brand) {
|
|
6204
|
+
if (!brand) return "";
|
|
6205
|
+
const { footer_text: text, footer_link_url: href, footer_link_label: label } = brand.b;
|
|
6206
|
+
const parts = [];
|
|
6207
|
+
if (text) parts.push(`<span>${esc(text)}</span>`);
|
|
6208
|
+
if (href) parts.push(`<a href="${esc(href)}" rel="noopener noreferrer" target="_blank">${esc(label || href)}</a>`);
|
|
6209
|
+
if (!brand.b.hide_powered_by) parts.push(`<span>Secured by PromptOwl \xB7 ContextNest</span>`);
|
|
6210
|
+
return parts.length ? `<footer class="cn-foot">${parts.join("")}</footer>` : "";
|
|
6211
|
+
}
|
|
5668
6212
|
var NO_DOWNLOAD_CSS = `<style>
|
|
5669
6213
|
@media print{html,body{display:none !important}}
|
|
5670
6214
|
html,body{-webkit-user-select:none;-ms-user-select:none;user-select:none}
|
|
@@ -5736,7 +6280,10 @@ async function renderPublished(c, row, viewerEmail) {
|
|
|
5736
6280
|
}
|
|
5737
6281
|
const title = esc(artifact.title || row.node_id);
|
|
5738
6282
|
const heading = /^\s*#\s/.test(body) ? "" : `<h1>${title}</h1>`;
|
|
5739
|
-
const
|
|
6283
|
+
const brand = await loadBrand(row);
|
|
6284
|
+
const docTitle = brand?.b.site_title ? `${title} \xB7 ${esc(brand.b.site_title)}` : title;
|
|
6285
|
+
const brandCss = brand ? `body{background:var(--cn-page-bg,#fff)}h1,h2,h3,h4,a{color:var(--cn-accent-text,#142a80)}` : "";
|
|
6286
|
+
const shell = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${docTitle}</title><style>${PROSE_CSS}</style>${brandHead(brand, brandCss)}</head><body>${brandBar(brand)}${brandHero(brand)}<main>${heading}${markdown.render(body)}</main>${brandFooter(brand)}${overlays}</body></html>`;
|
|
5740
6287
|
return c.body(shell, 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
5741
6288
|
}
|
|
5742
6289
|
var EYE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
@@ -5757,7 +6304,27 @@ var GATE_EYE_SCRIPT = `<script>
|
|
|
5757
6304
|
});
|
|
5758
6305
|
})();
|
|
5759
6306
|
</script>`;
|
|
5760
|
-
|
|
6307
|
+
var GATE_BRAND_CSS = `
|
|
6308
|
+
body{background:var(--cn-page-bg,#f4f4f5)}
|
|
6309
|
+
.card{box-sizing:border-box;overflow:hidden}
|
|
6310
|
+
h1{color:var(--cn-accent-text,#142a80)}
|
|
6311
|
+
button[type=submit]{background:var(--cn-accent,#142a80);color:var(--cn-accent-fg,#fff)}
|
|
6312
|
+
.cn-brand{margin:-32px -34px 20px;border-radius:14px 14px 0 0;padding:14px 20px}
|
|
6313
|
+
.cn-gate-foot{font-size:12px;color:#a8a29e;margin:10px 0 0;text-align:center}
|
|
6314
|
+
.cn-gate-foot a{color:var(--cn-accent-text,#142a80)}
|
|
6315
|
+
`;
|
|
6316
|
+
function gateBrandTop(brand) {
|
|
6317
|
+
return brandBar(brand);
|
|
6318
|
+
}
|
|
6319
|
+
function gateBrandFoot(brand) {
|
|
6320
|
+
if (!brand) return "";
|
|
6321
|
+
const { footer_text: text, footer_link_url: href, footer_link_label: label } = brand.b;
|
|
6322
|
+
const parts = [];
|
|
6323
|
+
if (text) parts.push(esc(text));
|
|
6324
|
+
if (href) parts.push(`<a href="${esc(href)}" rel="noopener noreferrer" target="_blank">${esc(label || href)}</a>`);
|
|
6325
|
+
return parts.length ? `<p class="cn-gate-foot">${parts.join(" \xB7 ")}</p>` : "";
|
|
6326
|
+
}
|
|
6327
|
+
function gatePage(row, message, brand = null) {
|
|
5761
6328
|
const wantsCode = row.access_mode === "code";
|
|
5762
6329
|
const field2 = wantsCode ? `<label>Access code<span class="wrap">
|
|
5763
6330
|
<input id="cn-code" name="code" type="password" autocomplete="off" required autofocus>
|
|
@@ -5782,10 +6349,10 @@ function gatePage(row, message) {
|
|
|
5782
6349
|
button.eye svg{width:18px;height:18px;display:block}
|
|
5783
6350
|
.err{color:#be123c;font-weight:600}
|
|
5784
6351
|
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0;text-align:center}
|
|
5785
|
-
</style
|
|
5786
|
-
<h1>Protected document</h1><p>${esc(note)}</p>${err}${field2}
|
|
6352
|
+
</style>${brandHead(brand, GATE_BRAND_CSS)}</head><body><form class="card" method="POST" action="/p/${esc(row.slug)}/gate">
|
|
6353
|
+
${gateBrandTop(brand)}<h1>Protected document</h1><p>${esc(note)}</p>${err}${field2}
|
|
5787
6354
|
<button type="submit">${cta}</button>
|
|
5788
|
-
|
|
6355
|
+
${brand?.b.hide_powered_by ? "" : `<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>`}${gateBrandFoot(brand)}
|
|
5789
6356
|
</form>${wantsCode ? GATE_EYE_SCRIPT : ""}</body></html>`;
|
|
5790
6357
|
}
|
|
5791
6358
|
function unavailablePage() {
|
|
@@ -5804,7 +6371,7 @@ function unavailablePage() {
|
|
|
5804
6371
|
function sentPage() {
|
|
5805
6372
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Check your email</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#f4f4f5;color:#1c1917;font:16px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}.card{background:#fff;border:1px solid #e7e5e4;border-radius:14px;box-shadow:0 4px 24px rgba(20,42,128,.08);padding:32px 34px;max-width:380px;text-align:center}h1{font-size:20px;color:#142a80;margin:0 0 6px}p{color:#78716c;margin:0;font-size:14px}</style></head><body><div class="card"><h1>Check your email</h1><p>If your address has access, a one-time link to open this document is on its way.</p></div></body></html>`;
|
|
5806
6373
|
}
|
|
5807
|
-
function readerShell(row, title, viewerEmail) {
|
|
6374
|
+
function readerShell(row, title, viewerEmail, brand = null) {
|
|
5808
6375
|
const emailField = viewerEmail ? `<p class="who">Commenting as <strong>${esc(viewerEmail)}</strong></p>` : `<label>Your email<input id="cn-email" type="email" autocomplete="email" required></label>`;
|
|
5809
6376
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>
|
|
5810
6377
|
html,body{margin:0;height:100%}
|
|
@@ -5825,7 +6392,7 @@ function readerShell(row, title, viewerEmail) {
|
|
|
5825
6392
|
button[disabled]{opacity:.6;cursor:default}
|
|
5826
6393
|
.msg{font-weight:600;margin:0 0 14px;font-size:14px}
|
|
5827
6394
|
.msg.err{color:#be123c}.msg.ok{color:#15803d}
|
|
5828
|
-
</style
|
|
6395
|
+
</style>${brand ? `<style>${publishBrandCssVars(brand.b)}#cn-open,#cn-send{background:var(--cn-accent,#142a80);color:var(--cn-accent-fg,#fff)}.card h2{color:var(--cn-accent-text,#142a80)}</style>` : ""}</head><body>
|
|
5829
6396
|
<iframe id="cn-frame" src="/p/${esc(row.slug)}/view" title="${esc(title)}"></iframe>
|
|
5830
6397
|
<button id="cn-open" type="button">Leave a comment</button>
|
|
5831
6398
|
<div id="cn-back"><div class="card" role="dialog" aria-modal="true" aria-label="Leave a comment">
|
|
@@ -5896,12 +6463,12 @@ publishRenderRoutes.get("/:slug", async (c) => {
|
|
|
5896
6463
|
const row = await getPublishBySlug(slug);
|
|
5897
6464
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5898
6465
|
const viewer = await resolveViewer(c, row);
|
|
5899
|
-
if (!viewer) return c.html(gatePage(row));
|
|
6466
|
+
if (!viewer) return c.html(gatePage(row, void 0, await loadBrand(row)));
|
|
5900
6467
|
if (row.comments_enabled !== 1) return renderPublished(c, row, viewer.email);
|
|
5901
6468
|
const title = await publishedTitle(row);
|
|
5902
6469
|
c.header("Content-Security-Policy", SHELL_CSP);
|
|
5903
6470
|
c.header("Referrer-Policy", "no-referrer");
|
|
5904
|
-
return c.html(readerShell(row, title, viewer.email));
|
|
6471
|
+
return c.html(readerShell(row, title, viewer.email, await loadBrand(row)));
|
|
5905
6472
|
});
|
|
5906
6473
|
publishRenderRoutes.get("/:slug/view", async (c) => {
|
|
5907
6474
|
const slug = c.req.param("slug");
|
|
@@ -5939,20 +6506,20 @@ publishRenderRoutes.post("/:slug/gate", async (c) => {
|
|
|
5939
6506
|
const row = await getPublishBySlug(slug);
|
|
5940
6507
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5941
6508
|
if (!tryConsume(`publish-gate:${slug}:${clientIp(c)}`, GATE_LIMIT)) {
|
|
5942
|
-
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes."), 429);
|
|
6509
|
+
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes.", await loadBrand(row)), 429);
|
|
5943
6510
|
}
|
|
5944
6511
|
if (row.access_mode === "public") return c.redirect(`/p/${slug}`);
|
|
5945
6512
|
const form = await c.req.parseBody();
|
|
5946
6513
|
if (row.access_mode === "code") {
|
|
5947
6514
|
if (!await verifyCode(row, String(form.code ?? ""))) {
|
|
5948
|
-
return c.html(gatePage(row, "That code is not correct."), 401);
|
|
6515
|
+
return c.html(gatePage(row, "That code is not correct.", await loadBrand(row)), 401);
|
|
5949
6516
|
}
|
|
5950
6517
|
setSessionCookie2(c, slug, await createSession2(row.id, null));
|
|
5951
6518
|
return c.redirect(`/p/${slug}`);
|
|
5952
6519
|
}
|
|
5953
6520
|
const email = normalizeEmail(String(form.email ?? ""));
|
|
5954
6521
|
if (!isEmailish(email)) {
|
|
5955
|
-
return c.html(gatePage(row, "Enter a valid email address."), 400);
|
|
6522
|
+
return c.html(gatePage(row, "Enter a valid email address.", await loadBrand(row)), 400);
|
|
5956
6523
|
}
|
|
5957
6524
|
const allowed = row.access_mode !== "invite" || await isAllowlisted(row.id, email);
|
|
5958
6525
|
const linkToken = await mintMagicLink(row.id, email);
|
|
@@ -5972,15 +6539,15 @@ publishRenderRoutes.get("/:slug/verify", async (c) => {
|
|
|
5972
6539
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5973
6540
|
const email = await consumeMagicLink(row.id, c.req.query("token") ?? "");
|
|
5974
6541
|
if (!email) {
|
|
5975
|
-
return c.html(gatePage(row, "That link is invalid or has expired. Request a new one."), 401);
|
|
6542
|
+
return c.html(gatePage(row, "That link is invalid or has expired. Request a new one.", await loadBrand(row)), 401);
|
|
5976
6543
|
}
|
|
5977
6544
|
setSessionCookie2(c, slug, await createSession2(row.id, email));
|
|
5978
6545
|
return c.redirect(`/p/${slug}`);
|
|
5979
6546
|
});
|
|
5980
6547
|
|
|
5981
6548
|
// src/nests/nest-team-routes.ts
|
|
5982
|
-
import { Hono as
|
|
5983
|
-
var nestTeamRoutes = new
|
|
6549
|
+
import { Hono as Hono9 } from "hono";
|
|
6550
|
+
var nestTeamRoutes = new Hono9();
|
|
5984
6551
|
nestTeamRoutes.get("/", async (c) => {
|
|
5985
6552
|
const nestId = c.req.param("nestId");
|
|
5986
6553
|
const refs = await listNestTeams(nestId);
|
|
@@ -6019,8 +6586,8 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
|
6019
6586
|
});
|
|
6020
6587
|
|
|
6021
6588
|
// src/nests/people-routes.ts
|
|
6022
|
-
import { Hono as
|
|
6023
|
-
var peopleRoutes = new
|
|
6589
|
+
import { Hono as Hono10 } from "hono";
|
|
6590
|
+
var peopleRoutes = new Hono10();
|
|
6024
6591
|
peopleRoutes.get("/suggest", async (c) => {
|
|
6025
6592
|
const userId = c.get("userId");
|
|
6026
6593
|
const q = c.req.query("q") ?? "";
|
|
@@ -6030,7 +6597,7 @@ peopleRoutes.get("/suggest", async (c) => {
|
|
|
6030
6597
|
});
|
|
6031
6598
|
|
|
6032
6599
|
// src/governance/team-routes.ts
|
|
6033
|
-
import { Hono as
|
|
6600
|
+
import { Hono as Hono11 } from "hono";
|
|
6034
6601
|
|
|
6035
6602
|
// src/governance/promptowl-teams.ts
|
|
6036
6603
|
var PROMPTOWL_SOURCE = "promptowl";
|
|
@@ -6177,7 +6744,7 @@ async function desiredRoster(poTeam, callerEmail) {
|
|
|
6177
6744
|
}
|
|
6178
6745
|
|
|
6179
6746
|
// src/governance/team-routes.ts
|
|
6180
|
-
var teamRoutes = new
|
|
6747
|
+
var teamRoutes = new Hono11();
|
|
6181
6748
|
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
6182
6749
|
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
6183
6750
|
var CONNECT_REQUIRED_STATUS = 428;
|
|
@@ -6362,7 +6929,7 @@ function toSearchHit(node, query) {
|
|
|
6362
6929
|
}
|
|
6363
6930
|
|
|
6364
6931
|
// src/nodes/routes.ts
|
|
6365
|
-
import { Hono as
|
|
6932
|
+
import { Hono as Hono12 } from "hono";
|
|
6366
6933
|
import { parseDocument as parseDocument3, readPdfBinary as readPdfBinary2 } from "@promptowl/contextnest-engine";
|
|
6367
6934
|
|
|
6368
6935
|
// src/nodes/markdown-export.ts
|
|
@@ -6415,7 +6982,7 @@ ${m[1]}
|
|
|
6415
6982
|
}
|
|
6416
6983
|
|
|
6417
6984
|
// src/nodes/routes.ts
|
|
6418
|
-
var nodeRoutes = new
|
|
6985
|
+
var nodeRoutes = new Hono12();
|
|
6419
6986
|
function nodeAsMarkdown(response, nodeId) {
|
|
6420
6987
|
return nodeToMarkdown({
|
|
6421
6988
|
id: nodeId,
|
|
@@ -6498,7 +7065,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6498
7065
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
6499
7066
|
const nestId = c.req.param("nestId");
|
|
6500
7067
|
const nodeId = c.req.param("nodeId");
|
|
6501
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
7068
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-KDBPWRZI.js");
|
|
6502
7069
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
6503
7070
|
nestId,
|
|
6504
7071
|
nodeId
|
|
@@ -6519,7 +7086,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
6519
7086
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
6520
7087
|
const nestId = c.req.param("nestId");
|
|
6521
7088
|
const nodeId = c.req.param("nodeId");
|
|
6522
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
7089
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-GB43UQVH.js");
|
|
6523
7090
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
6524
7091
|
return c.json({ reviews: history });
|
|
6525
7092
|
});
|
|
@@ -6675,14 +7242,16 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
6675
7242
|
response.myGrant = await resolveNodeGrant(nestId, userId, nodeId);
|
|
6676
7243
|
const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
|
|
6677
7244
|
if (publicReader || approvedOnly) {
|
|
6678
|
-
|
|
7245
|
+
let approved = await getApprovedVersion(nestId, nodeId);
|
|
6679
7246
|
if (approved != null) {
|
|
6680
7247
|
let approvedRaw = "";
|
|
6681
7248
|
try {
|
|
6682
|
-
approvedRaw = await
|
|
7249
|
+
({ raw: approvedRaw, version: approved } = await reconstructApproved(
|
|
7250
|
+
versionManager,
|
|
7251
|
+
nestId,
|
|
6683
7252
|
nodeId,
|
|
6684
7253
|
approved
|
|
6685
|
-
);
|
|
7254
|
+
));
|
|
6686
7255
|
response.content = bodyOnly(nodeId, approvedRaw);
|
|
6687
7256
|
if (response.type === "pdf") {
|
|
6688
7257
|
const approvedPdf = pdfMetaOf(nodeId, approvedRaw);
|
|
@@ -6798,13 +7367,13 @@ async function getUserEmail(c) {
|
|
|
6798
7367
|
}
|
|
6799
7368
|
|
|
6800
7369
|
// src/annotations/routes.ts
|
|
6801
|
-
import { Hono as
|
|
7370
|
+
import { Hono as Hono13 } from "hono";
|
|
6802
7371
|
|
|
6803
7372
|
// src/annotations/types.ts
|
|
6804
7373
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
6805
7374
|
|
|
6806
7375
|
// src/annotations/routes.ts
|
|
6807
|
-
var annotationRoutes = new
|
|
7376
|
+
var annotationRoutes = new Hono13();
|
|
6808
7377
|
function getNodeId(c) {
|
|
6809
7378
|
const raw = c.req.param("nodeId");
|
|
6810
7379
|
try {
|
|
@@ -6932,7 +7501,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
6932
7501
|
});
|
|
6933
7502
|
|
|
6934
7503
|
// src/nodes/query-routes.ts
|
|
6935
|
-
import { Hono as
|
|
7504
|
+
import { Hono as Hono14 } from "hono";
|
|
6936
7505
|
|
|
6937
7506
|
// src/tables/service.ts
|
|
6938
7507
|
function splitCsvLine(line) {
|
|
@@ -7371,7 +7940,7 @@ async function resolveReadableBody(nestId, nodeId, userId, workingBody) {
|
|
|
7371
7940
|
if (approved == null) return "";
|
|
7372
7941
|
try {
|
|
7373
7942
|
const { versions } = await engineCache.get(nestId);
|
|
7374
|
-
const raw = await versions
|
|
7943
|
+
const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
|
|
7375
7944
|
return bodyOnly(nodeId, raw);
|
|
7376
7945
|
} catch {
|
|
7377
7946
|
return "";
|
|
@@ -7383,76 +7952,18 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
7383
7952
|
if (approved == null) return null;
|
|
7384
7953
|
try {
|
|
7385
7954
|
const { versions } = await engineCache.get(nestId);
|
|
7386
|
-
const raw = await versions
|
|
7955
|
+
const { raw } = await reconstructApproved(versions, nestId, nodeId, approved);
|
|
7387
7956
|
return bodyOnly(nodeId, raw);
|
|
7388
7957
|
} catch {
|
|
7389
7958
|
return null;
|
|
7390
7959
|
}
|
|
7391
7960
|
}
|
|
7392
7961
|
|
|
7393
|
-
// src/nests/bundle-service.ts
|
|
7394
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
7395
|
-
import { join as join3, sep } from "path";
|
|
7396
|
-
import JSZip from "jszip";
|
|
7397
|
-
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
7398
|
-
function walkFiles(root) {
|
|
7399
|
-
const out = [];
|
|
7400
|
-
let entries;
|
|
7401
|
-
try {
|
|
7402
|
-
entries = readdirSync2(root, { recursive: true });
|
|
7403
|
-
} catch {
|
|
7404
|
-
return out;
|
|
7405
|
-
}
|
|
7406
|
-
for (const rel of entries) {
|
|
7407
|
-
try {
|
|
7408
|
-
if (statSync2(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
7409
|
-
} catch {
|
|
7410
|
-
}
|
|
7411
|
-
}
|
|
7412
|
-
return out;
|
|
7413
|
-
}
|
|
7414
|
-
async function exportNestBundle(nestId, includeDrafts) {
|
|
7415
|
-
const db = getDb();
|
|
7416
|
-
const approvedRows = await db.all(
|
|
7417
|
-
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
7418
|
-
[nestId]
|
|
7419
|
-
);
|
|
7420
|
-
const approvedByNode = new Map(
|
|
7421
|
-
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
7422
|
-
);
|
|
7423
|
-
const { storage } = await engineCache.get(nestId);
|
|
7424
|
-
const docs = await storage.discoverDocuments();
|
|
7425
|
-
const zip = new JSZip();
|
|
7426
|
-
let count = 0;
|
|
7427
|
-
for (const doc of docs) {
|
|
7428
|
-
const approved = approvedByNode.get(doc.id);
|
|
7429
|
-
const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
|
|
7430
|
-
let body = doc.body || "";
|
|
7431
|
-
if (approved != null) {
|
|
7432
|
-
const approvedBody = await resolveExportBody(nestId, doc.id, body);
|
|
7433
|
-
if (approvedBody == null) continue;
|
|
7434
|
-
body = approvedBody;
|
|
7435
|
-
frontmatter.status = "published";
|
|
7436
|
-
frontmatter.version = approved;
|
|
7437
|
-
} else {
|
|
7438
|
-
if (!includeDrafts) continue;
|
|
7439
|
-
frontmatter.status = "draft";
|
|
7440
|
-
}
|
|
7441
|
-
zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
|
|
7442
|
-
count++;
|
|
7443
|
-
}
|
|
7444
|
-
const nestPath = resolveNestPath(nestId);
|
|
7445
|
-
for (const rel of walkFiles(nestPath)) {
|
|
7446
|
-
const segs = rel.split("/");
|
|
7447
|
-
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
7448
|
-
if (!keep) continue;
|
|
7449
|
-
zip.file(rel, readFileSync2(join3(nestPath, rel)));
|
|
7450
|
-
}
|
|
7451
|
-
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
7452
|
-
return zip.generateAsync({ type: "arraybuffer" });
|
|
7453
|
-
}
|
|
7454
|
-
|
|
7455
7962
|
// src/nodes/graph-service.ts
|
|
7963
|
+
import {
|
|
7964
|
+
contextLinkTarget,
|
|
7965
|
+
extractContextLinks
|
|
7966
|
+
} from "@promptowl/contextnest-engine";
|
|
7456
7967
|
var MAX_GRAPH_NODES = 150;
|
|
7457
7968
|
var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
7458
7969
|
function bareSlug(idOrTarget) {
|
|
@@ -7470,6 +7981,15 @@ function extractWikiTargets(body) {
|
|
|
7470
7981
|
}
|
|
7471
7982
|
return out;
|
|
7472
7983
|
}
|
|
7984
|
+
function extractContextTargets(body) {
|
|
7985
|
+
const out = /* @__PURE__ */ new Set();
|
|
7986
|
+
if (!body) return [];
|
|
7987
|
+
for (const uri of extractContextLinks(body)) {
|
|
7988
|
+
const t = contextLinkTarget(uri);
|
|
7989
|
+
if (t) out.add(t);
|
|
7990
|
+
}
|
|
7991
|
+
return [...out];
|
|
7992
|
+
}
|
|
7473
7993
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
7474
7994
|
const { storage } = await engineCache.get(nestId);
|
|
7475
7995
|
const docs = (await storage.discoverDocuments()).filter(
|
|
@@ -7504,6 +8024,15 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7504
8024
|
allLinks.push({ source: d.id, target: targetId });
|
|
7505
8025
|
}
|
|
7506
8026
|
}
|
|
8027
|
+
for (const d of accessible) {
|
|
8028
|
+
for (const targetId of extractContextTargets(d.body || "")) {
|
|
8029
|
+
if (!idSet.has(targetId) || targetId === d.id) continue;
|
|
8030
|
+
const key = `${d.id}->${targetId}`;
|
|
8031
|
+
if (seen.has(key)) continue;
|
|
8032
|
+
seen.add(key);
|
|
8033
|
+
allLinks.push({ source: d.id, target: targetId });
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
7507
8036
|
const tagCounts = /* @__PURE__ */ new Map();
|
|
7508
8037
|
const tagToDocs = /* @__PURE__ */ new Map();
|
|
7509
8038
|
for (const n of allNodes) {
|
|
@@ -7615,132 +8144,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7615
8144
|
};
|
|
7616
8145
|
}
|
|
7617
8146
|
|
|
7618
|
-
// src/nodes/
|
|
7619
|
-
var queryRoutes = new Hono13();
|
|
7620
|
-
queryRoutes.get("/changes", async (c) => {
|
|
7621
|
-
const nestId = c.req.param("nestId");
|
|
7622
|
-
const userId = c.get("userId");
|
|
7623
|
-
const sinceRaw = c.req.query("since");
|
|
7624
|
-
const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
|
|
7625
|
-
if (!sinceRaw || Number.isNaN(sinceMs)) {
|
|
7626
|
-
throw new ValidationError(
|
|
7627
|
-
"since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
|
|
7628
|
-
);
|
|
7629
|
-
}
|
|
7630
|
-
const includeContent = ["1", "true"].includes(
|
|
7631
|
-
c.req.query("include_content") || ""
|
|
7632
|
-
);
|
|
7633
|
-
const all = await listNodesForCaller(nestId, userId, {
|
|
7634
|
-
...includeContent ? { withContent: true } : {}
|
|
7635
|
-
});
|
|
7636
|
-
const db = getDb();
|
|
7637
|
-
const publicReader = await isPublicReader(nestId, userId);
|
|
7638
|
-
const approvedRows = await db.all(
|
|
7639
|
-
"SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
|
|
7640
|
-
[nestId]
|
|
7641
|
-
);
|
|
7642
|
-
const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
|
|
7643
|
-
const approvedAtMsByNode = new Map(
|
|
7644
|
-
approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
|
|
7645
|
-
);
|
|
7646
|
-
const publishedSince = new Set(
|
|
7647
|
-
approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
|
|
7648
|
-
);
|
|
7649
|
-
const summarize = (n, updatedOverride) => ({
|
|
7650
|
-
id: n.id,
|
|
7651
|
-
title: n.title,
|
|
7652
|
-
type: n.type,
|
|
7653
|
-
status: n.status,
|
|
7654
|
-
tags: n.tags,
|
|
7655
|
-
created_at: n.created_at,
|
|
7656
|
-
updated_at: updatedOverride ?? n.updated_at,
|
|
7657
|
-
...includeContent ? { content: n.content } : {}
|
|
7658
|
-
});
|
|
7659
|
-
const created = [];
|
|
7660
|
-
const updated = [];
|
|
7661
|
-
for (const n of all) {
|
|
7662
|
-
if (publicReader) {
|
|
7663
|
-
const approvedMs = approvedAtMsByNode.get(n.id);
|
|
7664
|
-
if (approvedMs === void 0) continue;
|
|
7665
|
-
const approvedIso = new Date(approvedMs).toISOString();
|
|
7666
|
-
const createdAt2 = Date.parse(n.created_at || "");
|
|
7667
|
-
if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
|
|
7668
|
-
created.push(summarize(n, approvedIso));
|
|
7669
|
-
} else if (approvedMs > sinceMs) {
|
|
7670
|
-
updated.push(summarize(n, approvedIso));
|
|
7671
|
-
}
|
|
7672
|
-
continue;
|
|
7673
|
-
}
|
|
7674
|
-
const createdAt = Date.parse(n.created_at || "");
|
|
7675
|
-
const updatedAt = Date.parse(n.updated_at || "");
|
|
7676
|
-
if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
|
|
7677
|
-
else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
|
|
7678
|
-
else if (publishedSince.has(n.id)) updated.push(summarize(n));
|
|
7679
|
-
}
|
|
7680
|
-
const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
7681
|
-
let deleted = null;
|
|
7682
|
-
if (canSeeDeleted) {
|
|
7683
|
-
const rows = await db.all(
|
|
7684
|
-
"SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
|
|
7685
|
-
[nestId, new Date(sinceMs).toISOString()]
|
|
7686
|
-
);
|
|
7687
|
-
const liveIds = new Set(all.map((n) => n.id));
|
|
7688
|
-
deleted = rows.filter((r) => !liveIds.has(r.node_id)).map((r) => ({ id: r.node_id, deleted_by: r.deleted_by, deleted_at: r.deleted_at }));
|
|
7689
|
-
}
|
|
7690
|
-
return c.json({
|
|
7691
|
-
since: new Date(sinceMs).toISOString(),
|
|
7692
|
-
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7693
|
-
created,
|
|
7694
|
-
updated,
|
|
7695
|
-
deleted
|
|
7696
|
-
});
|
|
7697
|
-
});
|
|
7698
|
-
queryRoutes.get("/runnables", async (c) => {
|
|
7699
|
-
const nestId = c.req.param("nestId");
|
|
7700
|
-
const userId = c.get("userId");
|
|
7701
|
-
const all = await listNodesForCaller(nestId, userId, {
|
|
7702
|
-
type: RUNNABLE_NODE_TYPES,
|
|
7703
|
-
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
7704
|
-
// run the thing, so this surface pays for the file reads.
|
|
7705
|
-
withContent: true
|
|
7706
|
-
});
|
|
7707
|
-
const runnables = all.map((n) => ({
|
|
7708
|
-
id: n.id,
|
|
7709
|
-
title: n.title,
|
|
7710
|
-
type: n.type,
|
|
7711
|
-
schedule: n.schedule ?? null,
|
|
7712
|
-
status: n.status,
|
|
7713
|
-
tags: n.tags,
|
|
7714
|
-
updated_at: n.updated_at,
|
|
7715
|
-
content: n.content
|
|
7716
|
-
}));
|
|
7717
|
-
return c.json({ count: runnables.length, runnables });
|
|
7718
|
-
});
|
|
7719
|
-
queryRoutes.get("/graph", async (c) => {
|
|
7720
|
-
const nestId = c.req.param("nestId");
|
|
7721
|
-
const userId = c.get("userId");
|
|
7722
|
-
const userEmail = await resolveCallerEmail(userId);
|
|
7723
|
-
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
7724
|
-
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
7725
|
-
if (config.FEATURE_WORKFLOW_PLANE) {
|
|
7726
|
-
const rows = await getDb().all(
|
|
7727
|
-
`SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
|
|
7728
|
-
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
7729
|
-
WHERE e.nest_id = ?`,
|
|
7730
|
-
[nestId]
|
|
7731
|
-
);
|
|
7732
|
-
const visible = new Set(graph.nodes.map((n) => n.id));
|
|
7733
|
-
graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
|
|
7734
|
-
source: r.from_node,
|
|
7735
|
-
target: r.to_node,
|
|
7736
|
-
type: r.type,
|
|
7737
|
-
color: r.color,
|
|
7738
|
-
is_flow: !!r.is_flow,
|
|
7739
|
-
condition_mode: r.condition_mode
|
|
7740
|
-
}));
|
|
7741
|
-
}
|
|
7742
|
-
return c.json(graph);
|
|
7743
|
-
});
|
|
8147
|
+
// src/nodes/context-service.ts
|
|
7744
8148
|
function approxTokens(text) {
|
|
7745
8149
|
return Math.ceil(text.length / 4);
|
|
7746
8150
|
}
|
|
@@ -7759,13 +8163,11 @@ ${meta.join("\n")}
|
|
|
7759
8163
|
`;
|
|
7760
8164
|
return header + (node.body || "").trim();
|
|
7761
8165
|
}
|
|
7762
|
-
|
|
7763
|
-
const body =
|
|
8166
|
+
async function resolveContext(params) {
|
|
8167
|
+
const { nestId, userId, baseUrl, body } = params;
|
|
7764
8168
|
if (!body.prompt && !body.selector) {
|
|
7765
8169
|
throw new ValidationError("prompt or selector is required");
|
|
7766
8170
|
}
|
|
7767
|
-
const nestId = c.req.param("nestId");
|
|
7768
|
-
const baseUrl = requestBaseUrl(c.req.url);
|
|
7769
8171
|
const { query: queryEngine, storage } = await engineCache.get(nestId);
|
|
7770
8172
|
const maxTokens = Math.max(50, body.max_tokens ?? 4e3);
|
|
7771
8173
|
const promptPath = !body.selector?.trim() && !!body.prompt;
|
|
@@ -7780,17 +8182,23 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7780
8182
|
if (selector && selector.includes("[[")) {
|
|
7781
8183
|
const parts = selector.split("|").map((p) => p.trim()).filter(Boolean);
|
|
7782
8184
|
const wantedTitles = /* @__PURE__ */ new Set();
|
|
8185
|
+
const wantedIds = /* @__PURE__ */ new Set();
|
|
7783
8186
|
const rest = [];
|
|
7784
8187
|
for (const p of parts) {
|
|
7785
8188
|
const m = /^\[\[(.+)\]\]$/.exec(p);
|
|
7786
|
-
if (m)
|
|
7787
|
-
|
|
8189
|
+
if (!m) {
|
|
8190
|
+
rest.push(p);
|
|
8191
|
+
continue;
|
|
8192
|
+
}
|
|
8193
|
+
const inner = m[1].trim();
|
|
8194
|
+
if (inner.startsWith("nodes/")) wantedIds.add(inner);
|
|
8195
|
+
else wantedTitles.add(inner.toLowerCase());
|
|
7788
8196
|
}
|
|
7789
|
-
if (wantedTitles.size > 0) {
|
|
8197
|
+
if (wantedTitles.size > 0 || wantedIds.size > 0) {
|
|
7790
8198
|
selector = rest.join("|") || null;
|
|
7791
8199
|
resolvedTitleSelector = true;
|
|
7792
8200
|
titleMatches = allDocs.filter(
|
|
7793
|
-
(d) => wantedTitles.has(String(d.frontmatter.title || "").toLowerCase()) && isVisible(d)
|
|
8201
|
+
(d) => (wantedIds.has(d.id) || wantedTitles.has(String(d.frontmatter.title || "").toLowerCase())) && isVisible(d)
|
|
7794
8202
|
);
|
|
7795
8203
|
}
|
|
7796
8204
|
}
|
|
@@ -7820,7 +8228,10 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7820
8228
|
hopsUsed = result.hopsUsed;
|
|
7821
8229
|
nodesTraversed = result.nodesTraversed;
|
|
7822
8230
|
}
|
|
7823
|
-
|
|
8231
|
+
const hopSeeds = [
|
|
8232
|
+
...new Map([...documents, ...titleMatches].map((d) => [d.id, d])).values()
|
|
8233
|
+
];
|
|
8234
|
+
if (hopSeeds.length > 0 && hops > 0) {
|
|
7824
8235
|
const byTitle = new Map(
|
|
7825
8236
|
allDocs.map((d) => [
|
|
7826
8237
|
String(d.frontmatter.title || "").toLowerCase(),
|
|
@@ -7838,9 +8249,13 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7838
8249
|
const t = byId.get(target) ?? byTitle.get(target.toLowerCase());
|
|
7839
8250
|
if (t && t.id !== d.id) link(d.id, t.id);
|
|
7840
8251
|
}
|
|
8252
|
+
for (const target of extractContextTargets(d.body || "")) {
|
|
8253
|
+
const t = byId.get(target);
|
|
8254
|
+
if (t && t.id !== d.id) link(d.id, t.id);
|
|
8255
|
+
}
|
|
7841
8256
|
}
|
|
7842
|
-
const seen = new Set(
|
|
7843
|
-
let frontier =
|
|
8257
|
+
const seen = new Set(hopSeeds.map((d) => d.id));
|
|
8258
|
+
let frontier = hopSeeds.map((d) => d.id);
|
|
7844
8259
|
for (let depth = 0; depth < hops && frontier.length; depth++) {
|
|
7845
8260
|
const next = [];
|
|
7846
8261
|
for (const id of frontier) {
|
|
@@ -7888,7 +8303,6 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7888
8303
|
});
|
|
7889
8304
|
documents.sort((a, b) => relevance.get(b.id).score - relevance.get(a.id).score);
|
|
7890
8305
|
}
|
|
7891
|
-
const userId = c.get("userId");
|
|
7892
8306
|
const userEmail = await resolveCallerEmail(userId);
|
|
7893
8307
|
const beforePermission = documents.length;
|
|
7894
8308
|
const accessible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
@@ -7913,44 +8327,248 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7913
8327
|
included.push(readable[0]);
|
|
7914
8328
|
tokenCount = approxTokens(formatContextBlock(readable[0], readable[0].url));
|
|
7915
8329
|
}
|
|
7916
|
-
const truncatedByBudget = accessible.length - included.length;
|
|
7917
|
-
const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
|
|
7918
|
-
trackEvent("context.resolve", {
|
|
7919
|
-
nestId,
|
|
7920
|
-
selector_from_prompt: !!compileDetail,
|
|
7921
|
-
included: included.length,
|
|
7922
|
-
permission_filtered: permissionFiltered,
|
|
7923
|
-
token_count: tokenCount
|
|
7924
|
-
});
|
|
7925
|
-
return
|
|
7926
|
-
context: contextMarkdown,
|
|
7927
|
-
|
|
7928
|
-
id: n.id,
|
|
7929
|
-
title: n.frontmatter.title,
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
8330
|
+
const truncatedByBudget = accessible.length - included.length;
|
|
8331
|
+
const contextMarkdown = included.map((n) => formatContextBlock(n, n.url)).join("\n\n---\n\n");
|
|
8332
|
+
trackEvent("context.resolve", {
|
|
8333
|
+
nestId,
|
|
8334
|
+
selector_from_prompt: !!compileDetail,
|
|
8335
|
+
included: included.length,
|
|
8336
|
+
permission_filtered: permissionFiltered,
|
|
8337
|
+
token_count: tokenCount
|
|
8338
|
+
});
|
|
8339
|
+
return {
|
|
8340
|
+
context: contextMarkdown,
|
|
8341
|
+
blocks: included.map((n) => ({
|
|
8342
|
+
id: n.id,
|
|
8343
|
+
title: n.frontmatter.title,
|
|
8344
|
+
markdown: formatContextBlock(n, n.url)
|
|
8345
|
+
})),
|
|
8346
|
+
nodes: included.map((n) => ({
|
|
8347
|
+
id: n.id,
|
|
8348
|
+
title: n.frontmatter.title,
|
|
8349
|
+
url: n.url,
|
|
8350
|
+
tags: n.frontmatter.tags || [],
|
|
8351
|
+
type: n.frontmatter.type || "document",
|
|
8352
|
+
...relevance.get(n.id)
|
|
8353
|
+
})),
|
|
8354
|
+
trace: {
|
|
8355
|
+
selector_used: selector,
|
|
8356
|
+
from_prompt: body.prompt ?? null,
|
|
8357
|
+
compiled: compileDetail ? {
|
|
8358
|
+
matched_tags: compileDetail.matchedTags,
|
|
8359
|
+
matched_titles: compileDetail.matchedTitles,
|
|
8360
|
+
unmatched_tokens: compileDetail.unmatched,
|
|
8361
|
+
fallback
|
|
8362
|
+
} : null,
|
|
8363
|
+
total_matched: documents.length,
|
|
8364
|
+
permission_filtered: permissionFiltered,
|
|
8365
|
+
included: included.length,
|
|
8366
|
+
truncated_by_budget: truncatedByBudget,
|
|
8367
|
+
hops_used: hopsUsed,
|
|
8368
|
+
nodes_traversed: nodesTraversed,
|
|
8369
|
+
approx_tokens: tokenCount,
|
|
8370
|
+
max_tokens: maxTokens
|
|
8371
|
+
}
|
|
8372
|
+
};
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
// src/nests/bundle-service.ts
|
|
8376
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
8377
|
+
import { join as join5, sep } from "path";
|
|
8378
|
+
import JSZip from "jszip";
|
|
8379
|
+
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
8380
|
+
function walkFiles(root) {
|
|
8381
|
+
const out = [];
|
|
8382
|
+
let entries;
|
|
8383
|
+
try {
|
|
8384
|
+
entries = readdirSync2(root, { recursive: true });
|
|
8385
|
+
} catch {
|
|
8386
|
+
return out;
|
|
8387
|
+
}
|
|
8388
|
+
for (const rel of entries) {
|
|
8389
|
+
try {
|
|
8390
|
+
if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
8391
|
+
} catch {
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
return out;
|
|
8395
|
+
}
|
|
8396
|
+
async function exportNestBundle(nestId, includeDrafts) {
|
|
8397
|
+
const db = getDb();
|
|
8398
|
+
const approvedRows = await db.all(
|
|
8399
|
+
"SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
|
|
8400
|
+
[nestId]
|
|
8401
|
+
);
|
|
8402
|
+
const approvedByNode = new Map(
|
|
8403
|
+
approvedRows.map((r) => [r.node_id, r.approved_version])
|
|
8404
|
+
);
|
|
8405
|
+
const { storage } = await engineCache.get(nestId);
|
|
8406
|
+
const docs = await storage.discoverDocuments();
|
|
8407
|
+
const zip = new JSZip();
|
|
8408
|
+
let count = 0;
|
|
8409
|
+
for (const doc of docs) {
|
|
8410
|
+
const approved = approvedByNode.get(doc.id);
|
|
8411
|
+
const frontmatter = stripUndefinedDeep({ ...doc.frontmatter });
|
|
8412
|
+
let body = doc.body || "";
|
|
8413
|
+
if (approved != null) {
|
|
8414
|
+
const approvedBody = await resolveExportBody(nestId, doc.id, body);
|
|
8415
|
+
if (approvedBody == null) continue;
|
|
8416
|
+
body = approvedBody;
|
|
8417
|
+
frontmatter.status = "published";
|
|
8418
|
+
frontmatter.version = approved;
|
|
8419
|
+
} else {
|
|
8420
|
+
if (!includeDrafts) continue;
|
|
8421
|
+
frontmatter.status = "draft";
|
|
8422
|
+
}
|
|
8423
|
+
zip.file(`${doc.id}.md`, serializeDocument2({ ...doc, frontmatter, body }));
|
|
8424
|
+
count++;
|
|
8425
|
+
}
|
|
8426
|
+
const nestPath = resolveNestPath(nestId);
|
|
8427
|
+
for (const rel of walkFiles(nestPath)) {
|
|
8428
|
+
const segs = rel.split("/");
|
|
8429
|
+
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
8430
|
+
if (!keep) continue;
|
|
8431
|
+
zip.file(rel, readFileSync2(join5(nestPath, rel)));
|
|
8432
|
+
}
|
|
8433
|
+
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
8434
|
+
return zip.generateAsync({ type: "arraybuffer" });
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
// src/nodes/query-routes.ts
|
|
8438
|
+
var queryRoutes = new Hono14();
|
|
8439
|
+
queryRoutes.get("/changes", async (c) => {
|
|
8440
|
+
const nestId = c.req.param("nestId");
|
|
8441
|
+
const userId = c.get("userId");
|
|
8442
|
+
const sinceRaw = c.req.query("since");
|
|
8443
|
+
const sinceMs = sinceRaw ? Date.parse(sinceRaw) : NaN;
|
|
8444
|
+
if (!sinceRaw || Number.isNaN(sinceMs)) {
|
|
8445
|
+
throw new ValidationError(
|
|
8446
|
+
"since must be an ISO-8601 date, e.g. ?since=2026-07-01T00:00:00Z"
|
|
8447
|
+
);
|
|
8448
|
+
}
|
|
8449
|
+
const includeContent = ["1", "true"].includes(
|
|
8450
|
+
c.req.query("include_content") || ""
|
|
8451
|
+
);
|
|
8452
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
8453
|
+
...includeContent ? { withContent: true } : {}
|
|
8454
|
+
});
|
|
8455
|
+
const db = getDb();
|
|
8456
|
+
const publicReader = await isPublicReader(nestId, userId);
|
|
8457
|
+
const approvedRows = await db.all(
|
|
8458
|
+
"SELECT node_id, approved_at FROM approved_versions WHERE nest_id = ?",
|
|
8459
|
+
[nestId]
|
|
8460
|
+
);
|
|
8461
|
+
const parseDbTs = (t) => Date.parse(t.includes("T") ? t : `${t.replace(" ", "T")}Z`);
|
|
8462
|
+
const approvedAtMsByNode = new Map(
|
|
8463
|
+
approvedRows.map((r) => [r.node_id, parseDbTs(r.approved_at)])
|
|
8464
|
+
);
|
|
8465
|
+
const publishedSince = new Set(
|
|
8466
|
+
approvedRows.filter((r) => parseDbTs(r.approved_at) > sinceMs).map((r) => r.node_id)
|
|
8467
|
+
);
|
|
8468
|
+
const summarize = (n, updatedOverride) => ({
|
|
8469
|
+
id: n.id,
|
|
8470
|
+
title: n.title,
|
|
8471
|
+
type: n.type,
|
|
8472
|
+
status: n.status,
|
|
8473
|
+
tags: n.tags,
|
|
8474
|
+
created_at: n.created_at,
|
|
8475
|
+
updated_at: updatedOverride ?? n.updated_at,
|
|
8476
|
+
...includeContent ? { content: n.content } : {}
|
|
8477
|
+
});
|
|
8478
|
+
const created = [];
|
|
8479
|
+
const updated = [];
|
|
8480
|
+
for (const n of all) {
|
|
8481
|
+
if (publicReader) {
|
|
8482
|
+
const approvedMs = approvedAtMsByNode.get(n.id);
|
|
8483
|
+
if (approvedMs === void 0) continue;
|
|
8484
|
+
const approvedIso = new Date(approvedMs).toISOString();
|
|
8485
|
+
const createdAt2 = Date.parse(n.created_at || "");
|
|
8486
|
+
if (!Number.isNaN(createdAt2) && createdAt2 > sinceMs) {
|
|
8487
|
+
created.push(summarize(n, approvedIso));
|
|
8488
|
+
} else if (approvedMs > sinceMs) {
|
|
8489
|
+
updated.push(summarize(n, approvedIso));
|
|
8490
|
+
}
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
const createdAt = Date.parse(n.created_at || "");
|
|
8494
|
+
const updatedAt = Date.parse(n.updated_at || "");
|
|
8495
|
+
if (!Number.isNaN(createdAt) && createdAt > sinceMs) created.push(summarize(n));
|
|
8496
|
+
else if (!Number.isNaN(updatedAt) && updatedAt > sinceMs) updated.push(summarize(n));
|
|
8497
|
+
else if (publishedSince.has(n.id)) updated.push(summarize(n));
|
|
8498
|
+
}
|
|
8499
|
+
const canSeeDeleted = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8500
|
+
let deleted = null;
|
|
8501
|
+
if (canSeeDeleted) {
|
|
8502
|
+
const rows = await db.all(
|
|
8503
|
+
"SELECT node_id, deleted_by, deleted_at FROM node_deletions WHERE nest_id = ? AND deleted_at > ?",
|
|
8504
|
+
[nestId, new Date(sinceMs).toISOString()]
|
|
8505
|
+
);
|
|
8506
|
+
const liveIds = new Set(all.map((n) => n.id));
|
|
8507
|
+
deleted = rows.filter((r) => !liveIds.has(r.node_id)).map((r) => ({ id: r.node_id, deleted_by: r.deleted_by, deleted_at: r.deleted_at }));
|
|
8508
|
+
}
|
|
8509
|
+
return c.json({
|
|
8510
|
+
since: new Date(sinceMs).toISOString(),
|
|
8511
|
+
now: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8512
|
+
created,
|
|
8513
|
+
updated,
|
|
8514
|
+
deleted
|
|
8515
|
+
});
|
|
8516
|
+
});
|
|
8517
|
+
queryRoutes.get("/runnables", async (c) => {
|
|
8518
|
+
const nestId = c.req.param("nestId");
|
|
8519
|
+
const userId = c.get("userId");
|
|
8520
|
+
const all = await listNodesForCaller(nestId, userId, {
|
|
8521
|
+
type: RUNNABLE_NODE_TYPES,
|
|
8522
|
+
// The prompt/skill body IS the payload here — an orchestrator pulls it to
|
|
8523
|
+
// run the thing, so this surface pays for the file reads.
|
|
8524
|
+
withContent: true
|
|
8525
|
+
});
|
|
8526
|
+
const runnables = all.map((n) => ({
|
|
8527
|
+
id: n.id,
|
|
8528
|
+
title: n.title,
|
|
8529
|
+
type: n.type,
|
|
8530
|
+
schedule: n.schedule ?? null,
|
|
8531
|
+
status: n.status,
|
|
8532
|
+
tags: n.tags,
|
|
8533
|
+
updated_at: n.updated_at,
|
|
8534
|
+
content: n.content
|
|
8535
|
+
}));
|
|
8536
|
+
return c.json({ count: runnables.length, runnables });
|
|
8537
|
+
});
|
|
8538
|
+
queryRoutes.get("/graph", async (c) => {
|
|
8539
|
+
const nestId = c.req.param("nestId");
|
|
8540
|
+
const userId = c.get("userId");
|
|
8541
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
8542
|
+
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
8543
|
+
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
8544
|
+
if (config.FEATURE_WORKFLOW_PLANE) {
|
|
8545
|
+
const rows = await getDb().all(
|
|
8546
|
+
`SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
|
|
8547
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
8548
|
+
WHERE e.nest_id = ?`,
|
|
8549
|
+
[nestId]
|
|
8550
|
+
);
|
|
8551
|
+
const visible = new Set(graph.nodes.map((n) => n.id));
|
|
8552
|
+
graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
|
|
8553
|
+
source: r.from_node,
|
|
8554
|
+
target: r.to_node,
|
|
8555
|
+
type: r.type,
|
|
8556
|
+
color: r.color,
|
|
8557
|
+
is_flow: !!r.is_flow,
|
|
8558
|
+
condition_mode: r.condition_mode
|
|
8559
|
+
}));
|
|
8560
|
+
}
|
|
8561
|
+
return c.json(graph);
|
|
8562
|
+
});
|
|
8563
|
+
queryRoutes.post("/context", async (c) => {
|
|
8564
|
+
const body = await c.req.json();
|
|
8565
|
+
const { blocks: _blocks, ...result } = await resolveContext({
|
|
8566
|
+
nestId: c.req.param("nestId"),
|
|
8567
|
+
userId: c.get("userId"),
|
|
8568
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
8569
|
+
body
|
|
7953
8570
|
});
|
|
8571
|
+
return c.json(result);
|
|
7954
8572
|
});
|
|
7955
8573
|
queryRoutes.post("/table-query", async (c) => {
|
|
7956
8574
|
const nestId = c.req.param("nestId");
|
|
@@ -8206,8 +8824,393 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
8206
8824
|
);
|
|
8207
8825
|
});
|
|
8208
8826
|
|
|
8209
|
-
// src/
|
|
8827
|
+
// src/hootie/routes.ts
|
|
8210
8828
|
import { Hono as Hono15 } from "hono";
|
|
8829
|
+
import { streamSSE } from "hono/streaming";
|
|
8830
|
+
|
|
8831
|
+
// src/hootie/model.ts
|
|
8832
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
8833
|
+
var plainModels = /* @__PURE__ */ new Set();
|
|
8834
|
+
async function* streamAnswer(params) {
|
|
8835
|
+
const client = clientFor(params.apiKey, params.baseUrl ?? null);
|
|
8836
|
+
const key = `${params.baseUrl ?? ""}|${params.model}`;
|
|
8837
|
+
if (plainModels.has(key)) {
|
|
8838
|
+
yield* answer(client, params, false);
|
|
8839
|
+
return;
|
|
8840
|
+
}
|
|
8841
|
+
const state = { started: false };
|
|
8842
|
+
try {
|
|
8843
|
+
yield* answer(client, params, true, state);
|
|
8844
|
+
} catch (err) {
|
|
8845
|
+
if (state.started || err?.status !== 400) throw err;
|
|
8846
|
+
yield* answer(client, params, false);
|
|
8847
|
+
plainModels.add(key);
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8850
|
+
async function* answer(client, params, tuned, state = { started: false }) {
|
|
8851
|
+
const stream = client.messages.stream(
|
|
8852
|
+
tuned ? {
|
|
8853
|
+
model: params.model,
|
|
8854
|
+
max_tokens: 4096,
|
|
8855
|
+
// The system prompt carries the retrieved context — the bulk of the
|
|
8856
|
+
// request, identical across a conversation's turns — so cache it.
|
|
8857
|
+
system: [
|
|
8858
|
+
{ type: "text", text: params.system, cache_control: { type: "ephemeral" } }
|
|
8859
|
+
],
|
|
8860
|
+
messages: params.turns,
|
|
8861
|
+
thinking: { type: "adaptive" },
|
|
8862
|
+
output_config: { effort: "medium" }
|
|
8863
|
+
} : (
|
|
8864
|
+
// The bare request every Messages-speaking model accepts.
|
|
8865
|
+
{
|
|
8866
|
+
model: params.model,
|
|
8867
|
+
max_tokens: 4096,
|
|
8868
|
+
system: params.system,
|
|
8869
|
+
messages: params.turns
|
|
8870
|
+
}
|
|
8871
|
+
),
|
|
8872
|
+
{ signal: params.signal }
|
|
8873
|
+
);
|
|
8874
|
+
for await (const event of stream) {
|
|
8875
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
8876
|
+
state.started = true;
|
|
8877
|
+
yield event.delta.text;
|
|
8878
|
+
}
|
|
8879
|
+
}
|
|
8880
|
+
const final = await stream.finalMessage();
|
|
8881
|
+
if (final.stop_reason === "refusal") {
|
|
8882
|
+
throw new ModelRefusedError(
|
|
8883
|
+
final.stop_details?.explanation || "The model declined to answer that."
|
|
8884
|
+
);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
function describeModelError(err) {
|
|
8888
|
+
const e = err;
|
|
8889
|
+
const detail = (e?.error?.error?.message || "").toLowerCase();
|
|
8890
|
+
const provider = config.ANTHROPIC_BASE_URL ? "The model gateway" : "Anthropic";
|
|
8891
|
+
if (detail.includes("credit balance") || detail.includes("billing")) {
|
|
8892
|
+
return "Hootie's Anthropic account is out of credit. A server admin needs to add credit in the Anthropic console (Plans & Billing).";
|
|
8893
|
+
}
|
|
8894
|
+
switch (e?.status) {
|
|
8895
|
+
case 401:
|
|
8896
|
+
case 403:
|
|
8897
|
+
return `${provider} rejected this server's API key. A server admin can replace it under Settings \u2192 General.`;
|
|
8898
|
+
case 404:
|
|
8899
|
+
return `No model named "${config.HOOTIE_MODEL}" is available on this key. A server admin can change it under Settings \u2192 General.`;
|
|
8900
|
+
case 413:
|
|
8901
|
+
return "That question plus the documents behind it are too large for one request. Try asking something narrower.";
|
|
8902
|
+
case 429:
|
|
8903
|
+
return `${provider} is rate-limiting this server right now. Try again in a moment.`;
|
|
8904
|
+
case 529:
|
|
8905
|
+
return "The model is overloaded right now. Try again in a moment.";
|
|
8906
|
+
}
|
|
8907
|
+
if (typeof e?.status === "number" && e.status >= 500) {
|
|
8908
|
+
return `${provider} is having trouble right now. Try again in a moment.`;
|
|
8909
|
+
}
|
|
8910
|
+
return "Hootie couldn't reach the model. Try again in a moment.";
|
|
8911
|
+
}
|
|
8912
|
+
async function verifyModelKey(apiKey, baseUrl) {
|
|
8913
|
+
try {
|
|
8914
|
+
await clientFor(apiKey, baseUrl).models.list(
|
|
8915
|
+
{ limit: 1 },
|
|
8916
|
+
{ signal: AbortSignal.timeout(1e4) }
|
|
8917
|
+
);
|
|
8918
|
+
return null;
|
|
8919
|
+
} catch (err) {
|
|
8920
|
+
const status = err?.status;
|
|
8921
|
+
if (status === 401 || status === 403) {
|
|
8922
|
+
return baseUrl ? `The model gateway rejected that key (${status}). Check the key and the gateway URL.` : "Anthropic rejected that key. Check it and paste it again.";
|
|
8923
|
+
}
|
|
8924
|
+
console.warn("[hootie] key check inconclusive, saving anyway", status ?? err?.message);
|
|
8925
|
+
return null;
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
var cached = null;
|
|
8929
|
+
function clientFor(apiKey, baseUrl) {
|
|
8930
|
+
if (!cached || cached.apiKey !== apiKey || cached.baseUrl !== baseUrl) {
|
|
8931
|
+
cached = {
|
|
8932
|
+
apiKey,
|
|
8933
|
+
baseUrl,
|
|
8934
|
+
client: new Anthropic({ apiKey, ...baseUrl ? { baseURL: baseUrl } : {} })
|
|
8935
|
+
};
|
|
8936
|
+
}
|
|
8937
|
+
return cached.client;
|
|
8938
|
+
}
|
|
8939
|
+
var ModelRefusedError = class extends Error {
|
|
8940
|
+
constructor(message) {
|
|
8941
|
+
super(message);
|
|
8942
|
+
this.name = "ModelRefusedError";
|
|
8943
|
+
}
|
|
8944
|
+
};
|
|
8945
|
+
|
|
8946
|
+
// src/hootie/routes.ts
|
|
8947
|
+
var hootieRoutes = new Hono15();
|
|
8948
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
8949
|
+
var MAX_HISTORY_TURNS = 20;
|
|
8950
|
+
var MAX_HISTORY_CHARS = 8e3;
|
|
8951
|
+
var MAX_HISTORY_RAW = MAX_HISTORY_TURNS * 4;
|
|
8952
|
+
var CONTEXT_BUDGET = 6e3;
|
|
8953
|
+
var CHAT_BUDGET = { max: 30, windowMs: 5 * 6e4 };
|
|
8954
|
+
hootieRoutes.post("/chat", async (c) => {
|
|
8955
|
+
if (!config.HOOTIE_ENABLED) return c.json({ error: "Not found" }, 404);
|
|
8956
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
8957
|
+
if (!apiKey) {
|
|
8958
|
+
return c.json(
|
|
8959
|
+
{
|
|
8960
|
+
error: "Hootie needs an Anthropic API key. A server admin can add one under Settings \u2192 General.",
|
|
8961
|
+
reason: "no_api_key"
|
|
8962
|
+
},
|
|
8963
|
+
503
|
|
8964
|
+
);
|
|
8965
|
+
}
|
|
8966
|
+
let body;
|
|
8967
|
+
try {
|
|
8968
|
+
body = await c.req.json();
|
|
8969
|
+
} catch {
|
|
8970
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8971
|
+
}
|
|
8972
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
8973
|
+
if (!message) return c.json({ error: "message is required" }, 400);
|
|
8974
|
+
if (message.length > MAX_MESSAGE_CHARS) {
|
|
8975
|
+
return c.json(
|
|
8976
|
+
{ error: `message is too long (max ${MAX_MESSAGE_CHARS} characters)` },
|
|
8977
|
+
400
|
|
8978
|
+
);
|
|
8979
|
+
}
|
|
8980
|
+
const nodeId = typeof body.node_id === "string" && body.node_id.trim() ? body.node_id.trim() : null;
|
|
8981
|
+
if (nodeId !== null && !isNodeIdShaped(nodeId)) {
|
|
8982
|
+
return c.json({ error: "Node not found" }, 404);
|
|
8983
|
+
}
|
|
8984
|
+
const history = normalizeHistory(body.history);
|
|
8985
|
+
if (history === null) {
|
|
8986
|
+
return c.json(
|
|
8987
|
+
{ error: "history must be a list of {role: user|assistant, content}" },
|
|
8988
|
+
400
|
|
8989
|
+
);
|
|
8990
|
+
}
|
|
8991
|
+
const nestId = c.req.param("nestId");
|
|
8992
|
+
const userId = c.get("userId");
|
|
8993
|
+
if (!tryConsume(`hootie:${userId}`, CHAT_BUDGET)) {
|
|
8994
|
+
return c.json(
|
|
8995
|
+
{ error: "Too many questions in a short time \u2014 give it a few minutes." },
|
|
8996
|
+
429
|
|
8997
|
+
);
|
|
8998
|
+
}
|
|
8999
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
9000
|
+
const baseUrl = requestBaseUrl(c.req.url);
|
|
9001
|
+
const nodes = [];
|
|
9002
|
+
const blocks = [];
|
|
9003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9004
|
+
const take = (r) => {
|
|
9005
|
+
for (let i = 0; i < r.nodes.length; i++) {
|
|
9006
|
+
const n = r.nodes[i];
|
|
9007
|
+
if (seen.has(n.id)) continue;
|
|
9008
|
+
seen.add(n.id);
|
|
9009
|
+
nodes.push(n);
|
|
9010
|
+
blocks.push(r.blocks[i].markdown);
|
|
9011
|
+
}
|
|
9012
|
+
};
|
|
9013
|
+
let anchor = null;
|
|
9014
|
+
if (nodeId) {
|
|
9015
|
+
const r = await resolveContext({
|
|
9016
|
+
nestId,
|
|
9017
|
+
userId,
|
|
9018
|
+
baseUrl,
|
|
9019
|
+
body: {
|
|
9020
|
+
selector: `[[${nodeId}]]`,
|
|
9021
|
+
hops: 1,
|
|
9022
|
+
max_tokens: CONTEXT_BUDGET,
|
|
9023
|
+
include_drafts: true
|
|
9024
|
+
}
|
|
9025
|
+
});
|
|
9026
|
+
anchor = r.nodes.find((n) => n.id === nodeId) ?? null;
|
|
9027
|
+
if (!anchor) return c.json({ error: "Node not found" }, 404);
|
|
9028
|
+
take(r);
|
|
9029
|
+
}
|
|
9030
|
+
const question = stripMentions(message);
|
|
9031
|
+
if (question) {
|
|
9032
|
+
const r = await resolveContext({
|
|
9033
|
+
nestId,
|
|
9034
|
+
userId,
|
|
9035
|
+
baseUrl,
|
|
9036
|
+
body: { prompt: question, max_tokens: CONTEXT_BUDGET }
|
|
9037
|
+
});
|
|
9038
|
+
take(r);
|
|
9039
|
+
}
|
|
9040
|
+
const nest = await getNest(nestId);
|
|
9041
|
+
const system = buildSystemPrompt({
|
|
9042
|
+
nestName: nest?.name ?? "this nest",
|
|
9043
|
+
userEmail,
|
|
9044
|
+
anchor,
|
|
9045
|
+
blocks
|
|
9046
|
+
});
|
|
9047
|
+
const turns = [...history, { role: "user", content: message }];
|
|
9048
|
+
const mentionNames = extractMentions(message);
|
|
9049
|
+
trackEvent("hootie.chat", {
|
|
9050
|
+
nestId,
|
|
9051
|
+
anchored: !!anchor,
|
|
9052
|
+
context_nodes: nodes.length,
|
|
9053
|
+
history_turns: history.length,
|
|
9054
|
+
mentions: mentionNames.length
|
|
9055
|
+
});
|
|
9056
|
+
return streamSSE(c, async (stream) => {
|
|
9057
|
+
const cancel = new AbortController();
|
|
9058
|
+
const hangUp = () => cancel.abort();
|
|
9059
|
+
stream.onAbort(hangUp);
|
|
9060
|
+
c.req.raw.signal.addEventListener("abort", hangUp, { once: true });
|
|
9061
|
+
await stream.writeSSE({
|
|
9062
|
+
event: "meta",
|
|
9063
|
+
data: JSON.stringify({ nodes, anchor })
|
|
9064
|
+
});
|
|
9065
|
+
let answer2 = "";
|
|
9066
|
+
try {
|
|
9067
|
+
for await (const chunk of streamAnswer({
|
|
9068
|
+
apiKey,
|
|
9069
|
+
model: config.HOOTIE_MODEL,
|
|
9070
|
+
baseUrl: config.ANTHROPIC_BASE_URL || null,
|
|
9071
|
+
system,
|
|
9072
|
+
turns,
|
|
9073
|
+
signal: cancel.signal
|
|
9074
|
+
})) {
|
|
9075
|
+
if (cancel.signal.aborted) return;
|
|
9076
|
+
answer2 += chunk;
|
|
9077
|
+
await stream.writeSSE({ event: "delta", data: JSON.stringify({ text: chunk }) });
|
|
9078
|
+
}
|
|
9079
|
+
} catch (err) {
|
|
9080
|
+
if (cancel.signal.aborted) return;
|
|
9081
|
+
const text = err instanceof ModelRefusedError ? err.message : describeModelError(err);
|
|
9082
|
+
console.error("[hootie] model call failed", nestId, err);
|
|
9083
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ error: text }) });
|
|
9084
|
+
return;
|
|
9085
|
+
}
|
|
9086
|
+
if (cancel.signal.aborted) return;
|
|
9087
|
+
const cited = citedNodes(answer2, nodes);
|
|
9088
|
+
let mention = null;
|
|
9089
|
+
if (mentionNames.length > 0) {
|
|
9090
|
+
mention = await deliverMentions({
|
|
9091
|
+
nestId,
|
|
9092
|
+
userEmail,
|
|
9093
|
+
baseUrl,
|
|
9094
|
+
message,
|
|
9095
|
+
answer: answer2,
|
|
9096
|
+
anchor: anchor ?? cited[0] ?? null
|
|
9097
|
+
});
|
|
9098
|
+
}
|
|
9099
|
+
await stream.writeSSE({
|
|
9100
|
+
event: "done",
|
|
9101
|
+
data: JSON.stringify({ answer: answer2, cited, mention })
|
|
9102
|
+
});
|
|
9103
|
+
});
|
|
9104
|
+
});
|
|
9105
|
+
var NODE_ID_SHAPE = /^nodes(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
|
|
9106
|
+
function isNodeIdShaped(id) {
|
|
9107
|
+
return id.length <= 512 && NODE_ID_SHAPE.test(id);
|
|
9108
|
+
}
|
|
9109
|
+
function normalizeHistory(raw) {
|
|
9110
|
+
if (raw === void 0 || raw === null) return [];
|
|
9111
|
+
if (!Array.isArray(raw)) return null;
|
|
9112
|
+
const tail = raw.length > MAX_HISTORY_RAW ? raw.slice(-MAX_HISTORY_RAW) : raw;
|
|
9113
|
+
const turns = [];
|
|
9114
|
+
for (const t of tail) {
|
|
9115
|
+
if (!t || typeof t !== "object") return null;
|
|
9116
|
+
const role = t.role;
|
|
9117
|
+
const content = t.content;
|
|
9118
|
+
if (role !== "user" && role !== "assistant" || typeof content !== "string")
|
|
9119
|
+
return null;
|
|
9120
|
+
const text = content.trim();
|
|
9121
|
+
if (!text) continue;
|
|
9122
|
+
turns.push({ role, content: text });
|
|
9123
|
+
}
|
|
9124
|
+
const merged = [];
|
|
9125
|
+
for (const t of turns) {
|
|
9126
|
+
const last = merged[merged.length - 1];
|
|
9127
|
+
if (last && last.role === t.role) last.content += "\n\n" + t.content;
|
|
9128
|
+
else merged.push({ ...t });
|
|
9129
|
+
}
|
|
9130
|
+
let kept = merged.slice(-MAX_HISTORY_TURNS);
|
|
9131
|
+
let chars = kept.reduce((n, t) => n + t.content.length, 0);
|
|
9132
|
+
while (kept.length > 0 && chars > MAX_HISTORY_CHARS) {
|
|
9133
|
+
chars -= kept[0].content.length;
|
|
9134
|
+
kept = kept.slice(1);
|
|
9135
|
+
}
|
|
9136
|
+
while (kept.length > 0 && kept[0].role !== "user") kept = kept.slice(1);
|
|
9137
|
+
while (kept.length > 0 && kept[kept.length - 1].role !== "assistant")
|
|
9138
|
+
kept = kept.slice(0, -1);
|
|
9139
|
+
return kept;
|
|
9140
|
+
}
|
|
9141
|
+
function buildSystemPrompt(params) {
|
|
9142
|
+
const { nestName: nestName2, userEmail, anchor, blocks } = params;
|
|
9143
|
+
const context = blocks.length > 0 ? blocks.join("\n\n---\n\n").replace(/<\/documents>/gi, "</ documents>") : "(nothing in the nest matched this question)";
|
|
9144
|
+
return [
|
|
9145
|
+
`You are Hootie, the assistant inside the ContextNest nest "${nestName2}". You are talking with ${userEmail}.`,
|
|
9146
|
+
"",
|
|
9147
|
+
"Answer from the documents below and nothing else. They are the governed context this person is allowed to read; do not invent facts that are not in them.",
|
|
9148
|
+
"When you use a document, cite it by its exact title as a wiki link, like [[Title]] \u2014 this is how the reader opens it. Cite every document you drew on, and only those.",
|
|
9149
|
+
"If the documents do not answer the question, say so plainly and, if you can, point to which document comes closest or what to search for. Do not guess.",
|
|
9150
|
+
"Be concise and direct. Use short paragraphs or a short list; no preamble.",
|
|
9151
|
+
"Names written as @someone in the message are colleagues the person is looping in \u2014 they are not questions for you, and you should not address them.",
|
|
9152
|
+
anchor ? `The person currently has the document [[${anchor.title}]] open; "this" in their message means that document unless they say otherwise.` : "The person is looking at the nest as a whole, not a particular document.",
|
|
9153
|
+
"",
|
|
9154
|
+
"<documents>",
|
|
9155
|
+
context,
|
|
9156
|
+
"</documents>"
|
|
9157
|
+
].join("\n");
|
|
9158
|
+
}
|
|
9159
|
+
function citedNodes(answer2, nodes) {
|
|
9160
|
+
const lower = answer2.toLowerCase();
|
|
9161
|
+
const seenTitles = /* @__PURE__ */ new Set();
|
|
9162
|
+
const hits = [];
|
|
9163
|
+
for (const n of nodes) {
|
|
9164
|
+
const key = n.title.toLowerCase();
|
|
9165
|
+
if (seenTitles.has(key)) continue;
|
|
9166
|
+
const at = lower.indexOf(`[[${key}]]`);
|
|
9167
|
+
if (at < 0) continue;
|
|
9168
|
+
seenTitles.add(key);
|
|
9169
|
+
hits.push({ n, at });
|
|
9170
|
+
}
|
|
9171
|
+
return hits.sort((a, b) => a.at - b.at).map((x) => x.n);
|
|
9172
|
+
}
|
|
9173
|
+
async function deliverMentions(params) {
|
|
9174
|
+
const { nestId, userEmail, baseUrl, message, answer: answer2, anchor } = params;
|
|
9175
|
+
if (!anchor) return { status: "undeliverable", reason: "no_anchor" };
|
|
9176
|
+
const body = [
|
|
9177
|
+
`**Asked Hootie:** ${message}`,
|
|
9178
|
+
"",
|
|
9179
|
+
"**Hootie answered:**",
|
|
9180
|
+
answer2.trim()
|
|
9181
|
+
].join("\n");
|
|
9182
|
+
let threadId;
|
|
9183
|
+
try {
|
|
9184
|
+
const thread = await createThread(
|
|
9185
|
+
nestId,
|
|
9186
|
+
anchor.id,
|
|
9187
|
+
{ body, anchor: null, snapshotVersion: null },
|
|
9188
|
+
userEmail
|
|
9189
|
+
);
|
|
9190
|
+
threadId = thread.id;
|
|
9191
|
+
} catch (err) {
|
|
9192
|
+
console.error("[hootie] mention comment failed", nestId, anchor.id, err);
|
|
9193
|
+
return { status: "undeliverable", reason: "post_failed" };
|
|
9194
|
+
}
|
|
9195
|
+
const notified = await notifyCommentMentions({
|
|
9196
|
+
nestId,
|
|
9197
|
+
nodeId: anchor.id,
|
|
9198
|
+
body: message,
|
|
9199
|
+
actorEmail: userEmail,
|
|
9200
|
+
baseUrl
|
|
9201
|
+
});
|
|
9202
|
+
await syncAnnotationsNode(nestId, anchor.id, userEmail);
|
|
9203
|
+
return {
|
|
9204
|
+
status: "posted",
|
|
9205
|
+
node_id: anchor.id,
|
|
9206
|
+
node_title: anchor.title,
|
|
9207
|
+
thread_id: threadId,
|
|
9208
|
+
notified
|
|
9209
|
+
};
|
|
9210
|
+
}
|
|
9211
|
+
|
|
9212
|
+
// src/mcp/routes.ts
|
|
9213
|
+
import { Hono as Hono17 } from "hono";
|
|
8211
9214
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8212
9215
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
8213
9216
|
|
|
@@ -8297,10 +9300,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
8297
9300
|
}
|
|
8298
9301
|
|
|
8299
9302
|
// src/mcp/workflow-tools.ts
|
|
8300
|
-
import { v4 as
|
|
9303
|
+
import { v4 as uuid11 } from "uuid";
|
|
8301
9304
|
|
|
8302
9305
|
// src/workflow/run-service.ts
|
|
8303
|
-
import { v4 as
|
|
9306
|
+
import { v4 as uuid9 } from "uuid";
|
|
8304
9307
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
8305
9308
|
async function flowSubgraph(nestId, agentNode) {
|
|
8306
9309
|
const db = getDb();
|
|
@@ -8430,7 +9433,7 @@ async function triggerRun(opts) {
|
|
|
8430
9433
|
}
|
|
8431
9434
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
8432
9435
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
8433
|
-
const id = `run_${
|
|
9436
|
+
const id = `run_${uuid9()}`;
|
|
8434
9437
|
const db = getDb();
|
|
8435
9438
|
await db.transaction(async (tx) => {
|
|
8436
9439
|
if (parentRunId) {
|
|
@@ -8616,8 +9619,8 @@ async function listRuns(nestId, opts) {
|
|
|
8616
9619
|
}
|
|
8617
9620
|
|
|
8618
9621
|
// src/workflow/edge-routes.ts
|
|
8619
|
-
import { Hono as
|
|
8620
|
-
import { v4 as
|
|
9622
|
+
import { Hono as Hono16 } from "hono";
|
|
9623
|
+
import { v4 as uuid10 } from "uuid";
|
|
8621
9624
|
function toResponse(e, t) {
|
|
8622
9625
|
return {
|
|
8623
9626
|
id: e.id,
|
|
@@ -8735,7 +9738,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
8735
9738
|
);
|
|
8736
9739
|
}
|
|
8737
9740
|
}
|
|
8738
|
-
var edgeRoutes = new
|
|
9741
|
+
var edgeRoutes = new Hono16();
|
|
8739
9742
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
8740
9743
|
const nestId = c.req.param("nestId");
|
|
8741
9744
|
const db = getDb();
|
|
@@ -8803,7 +9806,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
8803
9806
|
);
|
|
8804
9807
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
8805
9808
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8806
|
-
const id =
|
|
9809
|
+
const id = uuid10();
|
|
8807
9810
|
await db.transaction(async (tx) => {
|
|
8808
9811
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
8809
9812
|
if (type.is_flow) {
|
|
@@ -9204,7 +10207,7 @@ async function createEdge(ctx, args) {
|
|
|
9204
10207
|
);
|
|
9205
10208
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
9206
10209
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9207
|
-
const id =
|
|
10210
|
+
const id = uuid11();
|
|
9208
10211
|
await db.transaction(async (tx) => {
|
|
9209
10212
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
9210
10213
|
if (type.is_flow) {
|
|
@@ -9268,7 +10271,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
9268
10271
|
`INSERT INTO edge_types
|
|
9269
10272
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
9270
10273
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
9271
|
-
[
|
|
10274
|
+
[uuid11(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
9272
10275
|
);
|
|
9273
10276
|
}
|
|
9274
10277
|
});
|
|
@@ -9886,7 +10889,7 @@ async function resolveLlmBody(ctx, node, gate) {
|
|
|
9886
10889
|
const approved = gate ? gate.approvedVersions?.get(node.id) ?? null : await getApprovedVersion(ctx.nestId, node.id);
|
|
9887
10890
|
if (approved == null) return null;
|
|
9888
10891
|
try {
|
|
9889
|
-
return await ctx.versionManager.
|
|
10892
|
+
return (await reconstructApproved(ctx.versionManager, ctx.nestId, node.id, approved)).raw;
|
|
9890
10893
|
} catch (err) {
|
|
9891
10894
|
console.error("reconstructVersion failed", node.id, approved, err);
|
|
9892
10895
|
return null;
|
|
@@ -10922,22 +11925,12 @@ ${list}`,
|
|
|
10922
11925
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
10923
11926
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
10924
11927
|
try {
|
|
10925
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
10926
|
-
const db = (await import("./client-D44ZHDTJ.js")).getDb();
|
|
11928
|
+
const { createGrant: createGrant2 } = await import("./grants-service-WICV4Y2J.js");
|
|
10927
11929
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
10928
11930
|
const e = normalizeEmail2(String(args.email || ""));
|
|
10929
11931
|
if (!e) return "email is required.";
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
10933
|
-
const { v4: uuid20 } = await import("uuid");
|
|
10934
|
-
const id = uuid20();
|
|
10935
|
-
await db.run(
|
|
10936
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
10937
|
-
[id, e, null, await hashPassword2(uuid20())]
|
|
10938
|
-
);
|
|
10939
|
-
row = { id };
|
|
10940
|
-
}
|
|
11932
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-XEBX6BM4.js");
|
|
11933
|
+
const row = await findOrCreateInvitedUser2(e, { invitedBy: ctx.userEmail });
|
|
10941
11934
|
await createGrant2({
|
|
10942
11935
|
nestId: ctx.nestId,
|
|
10943
11936
|
targetType,
|
|
@@ -10989,8 +11982,8 @@ ${list}`;
|
|
|
10989
11982
|
}
|
|
10990
11983
|
|
|
10991
11984
|
// src/mcp/routes.ts
|
|
10992
|
-
import { z } from "zod";
|
|
10993
|
-
var mcpRoutes = new
|
|
11985
|
+
import { z as z2 } from "zod";
|
|
11986
|
+
var mcpRoutes = new Hono17();
|
|
10994
11987
|
async function getUserEmail2(userId) {
|
|
10995
11988
|
const db = getDb();
|
|
10996
11989
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -11008,10 +12001,10 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl, c) {
|
|
|
11008
12001
|
const shape = {};
|
|
11009
12002
|
for (const [key, def] of Object.entries(props)) {
|
|
11010
12003
|
let field2;
|
|
11011
|
-
if (def.type === "string") field2 =
|
|
11012
|
-
else if (def.type === "number") field2 =
|
|
11013
|
-
else if (def.type === "array") field2 =
|
|
11014
|
-
else field2 =
|
|
12004
|
+
if (def.type === "string") field2 = z2.string();
|
|
12005
|
+
else if (def.type === "number") field2 = z2.number();
|
|
12006
|
+
else if (def.type === "array") field2 = z2.array(z2.string());
|
|
12007
|
+
else field2 = z2.any();
|
|
11015
12008
|
if (!required.includes(key)) field2 = field2.optional();
|
|
11016
12009
|
shape[key] = field2;
|
|
11017
12010
|
}
|
|
@@ -11060,8 +12053,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
11060
12053
|
});
|
|
11061
12054
|
|
|
11062
12055
|
// src/workflow/run-routes.ts
|
|
11063
|
-
import { Hono as
|
|
11064
|
-
import { v4 as
|
|
12056
|
+
import { Hono as Hono18 } from "hono";
|
|
12057
|
+
import { v4 as uuid12 } from "uuid";
|
|
11065
12058
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
11066
12059
|
var safeJson2 = (s) => {
|
|
11067
12060
|
if (!s) return null;
|
|
@@ -11179,7 +12172,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
11179
12172
|
definitions
|
|
11180
12173
|
};
|
|
11181
12174
|
}
|
|
11182
|
-
var runTriggerRoutes = new
|
|
12175
|
+
var runTriggerRoutes = new Hono18();
|
|
11183
12176
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
11184
12177
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
11185
12178
|
throw new ValidationError(
|
|
@@ -11258,7 +12251,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
11258
12251
|
}
|
|
11259
12252
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
11260
12253
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
11261
|
-
const id = `run_${
|
|
12254
|
+
const id = `run_${uuid12()}`;
|
|
11262
12255
|
await getDb().run(
|
|
11263
12256
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
11264
12257
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -11287,7 +12280,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
11287
12280
|
201
|
|
11288
12281
|
);
|
|
11289
12282
|
});
|
|
11290
|
-
var runListRoutes = new
|
|
12283
|
+
var runListRoutes = new Hono18();
|
|
11291
12284
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11292
12285
|
const nestId = c.req.param("nestId");
|
|
11293
12286
|
const db = getDb();
|
|
@@ -11319,7 +12312,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
11319
12312
|
}))
|
|
11320
12313
|
});
|
|
11321
12314
|
});
|
|
11322
|
-
var runDetailRoutes = new
|
|
12315
|
+
var runDetailRoutes = new Hono18();
|
|
11323
12316
|
async function loadRunWithPermission(c, runId, required) {
|
|
11324
12317
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
11325
12318
|
runId
|
|
@@ -11470,8 +12463,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11470
12463
|
});
|
|
11471
12464
|
|
|
11472
12465
|
// src/workflow/schedule-routes.ts
|
|
11473
|
-
import { Hono as
|
|
11474
|
-
import { v4 as
|
|
12466
|
+
import { Hono as Hono19 } from "hono";
|
|
12467
|
+
import { v4 as uuid13 } from "uuid";
|
|
11475
12468
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
11476
12469
|
var MIN_EVERY = 5;
|
|
11477
12470
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -11508,7 +12501,7 @@ function parseEvery(v) {
|
|
|
11508
12501
|
}
|
|
11509
12502
|
return n;
|
|
11510
12503
|
}
|
|
11511
|
-
var scheduleRoutes = new
|
|
12504
|
+
var scheduleRoutes = new Hono19();
|
|
11512
12505
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11513
12506
|
const nestId = c.req.param("nestId");
|
|
11514
12507
|
const rows = await getDb().all(
|
|
@@ -11549,7 +12542,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
11549
12542
|
);
|
|
11550
12543
|
}
|
|
11551
12544
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11552
|
-
const id = `sch_${
|
|
12545
|
+
const id = `sch_${uuid13()}`;
|
|
11553
12546
|
await db.run(
|
|
11554
12547
|
`INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
|
|
11555
12548
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -11604,8 +12597,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11604
12597
|
});
|
|
11605
12598
|
|
|
11606
12599
|
// src/notify/connector-routes.ts
|
|
11607
|
-
import { Hono as
|
|
11608
|
-
import { v4 as
|
|
12600
|
+
import { Hono as Hono20 } from "hono";
|
|
12601
|
+
import { v4 as uuid14 } from "uuid";
|
|
11609
12602
|
var KNOWN_EVENTS = [
|
|
11610
12603
|
"review_requested",
|
|
11611
12604
|
"review_approved",
|
|
@@ -11652,7 +12645,7 @@ function parseUrl(v) {
|
|
|
11652
12645
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
11653
12646
|
);
|
|
11654
12647
|
}
|
|
11655
|
-
var connectorRoutes = new
|
|
12648
|
+
var connectorRoutes = new Hono20();
|
|
11656
12649
|
connectorRoutes.get("/", async (c) => {
|
|
11657
12650
|
const nestId = c.req.param("nestId");
|
|
11658
12651
|
const rows = await getDb().all(
|
|
@@ -11671,7 +12664,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
11671
12664
|
const url = parseUrl(body.url);
|
|
11672
12665
|
const events = parseEvents(body.events);
|
|
11673
12666
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11674
|
-
const id = `con_${
|
|
12667
|
+
const id = `con_${uuid14()}`;
|
|
11675
12668
|
await getDb().run(
|
|
11676
12669
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
11677
12670
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -11714,7 +12707,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
11714
12707
|
});
|
|
11715
12708
|
|
|
11716
12709
|
// src/workflow/hook-routes.ts
|
|
11717
|
-
import { Hono as
|
|
12710
|
+
import { Hono as Hono21 } from "hono";
|
|
11718
12711
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
11719
12712
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
11720
12713
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -11737,7 +12730,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
11737
12730
|
created_at: r.created_at
|
|
11738
12731
|
};
|
|
11739
12732
|
}
|
|
11740
|
-
var hookNestRoutes = new
|
|
12733
|
+
var hookNestRoutes = new Hono21();
|
|
11741
12734
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11742
12735
|
const nestId = c.req.param("nestId");
|
|
11743
12736
|
const agent = c.req.query("agent");
|
|
@@ -11808,10 +12801,10 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
11808
12801
|
const mine = createHmac2("sha256", Buffer.from(secret, "base64")).update(rawBody, "utf8").digest("base64");
|
|
11809
12802
|
return safeEqual(mine, authHeader.slice(5).trim());
|
|
11810
12803
|
}
|
|
11811
|
-
function
|
|
12804
|
+
function stripMentions2(text) {
|
|
11812
12805
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
11813
12806
|
}
|
|
11814
|
-
var hookFireRoutes = new
|
|
12807
|
+
var hookFireRoutes = new Hono21();
|
|
11815
12808
|
hookFireRoutes.post("/:token", async (c) => {
|
|
11816
12809
|
const token = c.req.param("token");
|
|
11817
12810
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -11859,7 +12852,7 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
11859
12852
|
if (payload.type === "event_callback") {
|
|
11860
12853
|
const ev = payload.event ?? {};
|
|
11861
12854
|
if (ev.type !== "app_mention") return c.json({ ok: true });
|
|
11862
|
-
inputs = { input:
|
|
12855
|
+
inputs = { input: stripMentions2(String(ev.text ?? "")), from: ev.user, channel: ev.channel };
|
|
11863
12856
|
ack = () => c.json({ ok: true });
|
|
11864
12857
|
} else {
|
|
11865
12858
|
inputs = {
|
|
@@ -11877,7 +12870,7 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
11877
12870
|
if (secret && !teamsSignatureValid(secret, c.req.header("authorization"), raw)) {
|
|
11878
12871
|
return c.json({ error: "bad signature" }, 401);
|
|
11879
12872
|
}
|
|
11880
|
-
inputs = { input:
|
|
12873
|
+
inputs = { input: stripMentions2(String(payload.text ?? "")), from: payload.from?.name };
|
|
11881
12874
|
ack = (runId2) => c.json({ type: "message", text: `\u23F3 Queued ${hook.agent_node} (run ${runId2.slice(0, 12)}\u2026)` });
|
|
11882
12875
|
} else {
|
|
11883
12876
|
inputs = Object.keys(payload).length ? payload : null;
|
|
@@ -11913,9 +12906,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
11913
12906
|
});
|
|
11914
12907
|
|
|
11915
12908
|
// src/definitions/routes.ts
|
|
11916
|
-
import { Hono as
|
|
11917
|
-
import { v4 as
|
|
11918
|
-
var definitionRoutes = new
|
|
12909
|
+
import { Hono as Hono22 } from "hono";
|
|
12910
|
+
import { v4 as uuid15 } from "uuid";
|
|
12911
|
+
var definitionRoutes = new Hono22();
|
|
11919
12912
|
definitionRoutes.get("/", async (c) => {
|
|
11920
12913
|
const nestId = c.req.param("nestId");
|
|
11921
12914
|
const db = getDb();
|
|
@@ -11994,7 +12987,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
11994
12987
|
await applyUpdate(tx, existing.id);
|
|
11995
12988
|
targetId = existing.id;
|
|
11996
12989
|
} else {
|
|
11997
|
-
targetId =
|
|
12990
|
+
targetId = uuid15();
|
|
11998
12991
|
wasCreated = true;
|
|
11999
12992
|
await tx.run(
|
|
12000
12993
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -12023,11 +13016,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
12023
13016
|
});
|
|
12024
13017
|
|
|
12025
13018
|
// src/nests/asset-routes.ts
|
|
12026
|
-
import { Hono as
|
|
12027
|
-
import { v4 as
|
|
12028
|
-
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
12029
|
-
import { join as
|
|
12030
|
-
var assetRoutes = new
|
|
13019
|
+
import { Hono as Hono23 } from "hono";
|
|
13020
|
+
import { v4 as uuid16 } from "uuid";
|
|
13021
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
13022
|
+
import { join as join6 } from "path";
|
|
13023
|
+
var assetRoutes = new Hono23();
|
|
12031
13024
|
var CONTENT_TYPES = {
|
|
12032
13025
|
png: "image/png",
|
|
12033
13026
|
jpg: "image/jpeg",
|
|
@@ -12073,10 +13066,10 @@ assetRoutes.post("/", async (c) => {
|
|
|
12073
13066
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
12074
13067
|
);
|
|
12075
13068
|
}
|
|
12076
|
-
const name = `${
|
|
12077
|
-
const dir =
|
|
13069
|
+
const name = `${uuid16()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
13070
|
+
const dir = join6(resolveNestPath(nestId), "assets");
|
|
12078
13071
|
await mkdir2(dir, { recursive: true });
|
|
12079
|
-
await writeFile(
|
|
13072
|
+
await writeFile(join6(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
12080
13073
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
12081
13074
|
return c.json(
|
|
12082
13075
|
{
|
|
@@ -12098,7 +13091,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
12098
13091
|
}
|
|
12099
13092
|
let bytes;
|
|
12100
13093
|
try {
|
|
12101
|
-
bytes = await
|
|
13094
|
+
bytes = await readFile2(join6(resolveNestPath(nestId), "assets", name));
|
|
12102
13095
|
} catch {
|
|
12103
13096
|
throw new NotFoundError("Asset not found");
|
|
12104
13097
|
}
|
|
@@ -12114,19 +13107,21 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
12114
13107
|
});
|
|
12115
13108
|
|
|
12116
13109
|
// src/nodes/pdf-routes.ts
|
|
12117
|
-
import { Hono as
|
|
13110
|
+
import { Hono as Hono24 } from "hono";
|
|
12118
13111
|
import { bodyLimit } from "hono/body-limit";
|
|
12119
13112
|
import { createHash as createHash3 } from "crypto";
|
|
12120
13113
|
import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
|
|
12121
|
-
var pdfNodeRoutes = new
|
|
13114
|
+
var pdfNodeRoutes = new Hono24();
|
|
12122
13115
|
var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
|
|
12123
13116
|
function pdfMaxRequestBytes() {
|
|
12124
13117
|
return config.PDF_MAX_BYTES + 1024 * 1024;
|
|
12125
13118
|
}
|
|
13119
|
+
function pdfTooLargeMessage(bytes) {
|
|
13120
|
+
const size = bytes === void 0 ? "" : `${(bytes / (1024 * 1024)).toFixed(1)} MB, `;
|
|
13121
|
+
return `PDF too large (${size}max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`;
|
|
13122
|
+
}
|
|
12126
13123
|
function tooLarge(bytes) {
|
|
12127
|
-
return new PayloadTooLargeError(
|
|
12128
|
-
`PDF too large (${(bytes / (1024 * 1024)).toFixed(1)} MB, max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12129
|
-
);
|
|
13124
|
+
return new PayloadTooLargeError(pdfTooLargeMessage(bytes));
|
|
12130
13125
|
}
|
|
12131
13126
|
function field(body, name) {
|
|
12132
13127
|
const v = body[name];
|
|
@@ -12147,12 +13142,7 @@ pdfNodeRoutes.post(
|
|
|
12147
13142
|
"/pdf",
|
|
12148
13143
|
(c, next) => bodyLimit({
|
|
12149
13144
|
maxSize: pdfMaxRequestBytes(),
|
|
12150
|
-
onError: (c2) => c2.json(
|
|
12151
|
-
{
|
|
12152
|
-
error: `PDF too large (max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12153
|
-
},
|
|
12154
|
-
413
|
|
12155
|
-
)
|
|
13145
|
+
onError: (c2) => c2.json({ error: pdfTooLargeMessage() }, 413)
|
|
12156
13146
|
})(c, next),
|
|
12157
13147
|
async (c) => {
|
|
12158
13148
|
const nestId = c.req.param("nestId");
|
|
@@ -12291,7 +13281,7 @@ pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
|
|
|
12291
13281
|
});
|
|
12292
13282
|
|
|
12293
13283
|
// src/auth/community-sites-routes.ts
|
|
12294
|
-
import { Hono as
|
|
13284
|
+
import { Hono as Hono25 } from "hono";
|
|
12295
13285
|
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
12296
13286
|
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
12297
13287
|
var serializeSite = (site) => ({
|
|
@@ -12303,7 +13293,7 @@ var serializeSite = (site) => ({
|
|
|
12303
13293
|
created_at: site.created_at,
|
|
12304
13294
|
updated_at: site.updated_at
|
|
12305
13295
|
});
|
|
12306
|
-
var communitySiteRoutes = new
|
|
13296
|
+
var communitySiteRoutes = new Hono25();
|
|
12307
13297
|
communitySiteRoutes.get("/", async (c) => {
|
|
12308
13298
|
if (!await serverAdminAllowed(c))
|
|
12309
13299
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
@@ -12391,10 +13381,10 @@ communitySiteRoutes.delete("/:id", async (c) => {
|
|
|
12391
13381
|
});
|
|
12392
13382
|
|
|
12393
13383
|
// src/mcp/server-routes.ts
|
|
12394
|
-
import { Hono as
|
|
13384
|
+
import { Hono as Hono26 } from "hono";
|
|
12395
13385
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12396
13386
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
12397
|
-
import { z as
|
|
13387
|
+
import { z as z3 } from "zod";
|
|
12398
13388
|
var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
12399
13389
|
"nest_create",
|
|
12400
13390
|
"context_create",
|
|
@@ -12593,7 +13583,7 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12593
13583
|
);
|
|
12594
13584
|
return `Created nest "${nest2.name}" (id: ${nest2.id}). Add documents with context_create, retrieve with the other tools.`;
|
|
12595
13585
|
} catch (err) {
|
|
12596
|
-
if (err instanceof ValidationError) return err.message;
|
|
13586
|
+
if (err instanceof ValidationError || err instanceof ForbiddenError) return err.message;
|
|
12597
13587
|
throw err;
|
|
12598
13588
|
}
|
|
12599
13589
|
}
|
|
@@ -12704,10 +13694,10 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
|
12704
13694
|
const shape = {};
|
|
12705
13695
|
for (const [key, def] of Object.entries(props)) {
|
|
12706
13696
|
let field2;
|
|
12707
|
-
if (def.type === "string") field2 =
|
|
12708
|
-
else if (def.type === "number") field2 =
|
|
12709
|
-
else if (def.type === "array") field2 =
|
|
12710
|
-
else field2 =
|
|
13697
|
+
if (def.type === "string") field2 = z3.string();
|
|
13698
|
+
else if (def.type === "number") field2 = z3.number();
|
|
13699
|
+
else if (def.type === "array") field2 = z3.array(z3.string());
|
|
13700
|
+
else field2 = z3.any();
|
|
12711
13701
|
if (!required.includes(key)) field2 = field2.optional();
|
|
12712
13702
|
shape[key] = field2;
|
|
12713
13703
|
}
|
|
@@ -12727,7 +13717,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
|
12727
13717
|
}
|
|
12728
13718
|
return server;
|
|
12729
13719
|
}
|
|
12730
|
-
var serverMcpRoutes = new
|
|
13720
|
+
var serverMcpRoutes = new Hono26();
|
|
12731
13721
|
serverMcpRoutes.all("/", async (c) => {
|
|
12732
13722
|
const userId = c.get("userId");
|
|
12733
13723
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -12750,7 +13740,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12750
13740
|
await server.server.close();
|
|
12751
13741
|
}
|
|
12752
13742
|
});
|
|
12753
|
-
var nestIndexRoutes = new
|
|
13743
|
+
var nestIndexRoutes = new Hono26();
|
|
12754
13744
|
nestIndexRoutes.get("/", async (c) => {
|
|
12755
13745
|
const userId = c.get("userId");
|
|
12756
13746
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -12926,14 +13916,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
12926
13916
|
through steward review before they become AI-visible.
|
|
12927
13917
|
`;
|
|
12928
13918
|
}
|
|
12929
|
-
var llmsTxtRoutes = new
|
|
13919
|
+
var llmsTxtRoutes = new Hono26();
|
|
12930
13920
|
llmsTxtRoutes.get("/", (c) => {
|
|
12931
13921
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
12932
13922
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
12933
13923
|
});
|
|
12934
13924
|
|
|
12935
13925
|
// src/governance/routes.ts
|
|
12936
|
-
import { Hono as
|
|
13926
|
+
import { Hono as Hono27 } from "hono";
|
|
12937
13927
|
|
|
12938
13928
|
// src/governance/feed-service.ts
|
|
12939
13929
|
var SNIPPET_CHARS = 200;
|
|
@@ -13064,7 +14054,7 @@ async function getNestFeed(params) {
|
|
|
13064
14054
|
}
|
|
13065
14055
|
|
|
13066
14056
|
// src/governance/comment-service.ts
|
|
13067
|
-
import { v4 as
|
|
14057
|
+
import { v4 as uuid17 } from "uuid";
|
|
13068
14058
|
async function createComment(params) {
|
|
13069
14059
|
const db = getDb();
|
|
13070
14060
|
const body = (params.body ?? "").trim();
|
|
@@ -13080,7 +14070,7 @@ async function createComment(params) {
|
|
|
13080
14070
|
throw new Error("Parent comment not found on this node");
|
|
13081
14071
|
}
|
|
13082
14072
|
}
|
|
13083
|
-
const id =
|
|
14073
|
+
const id = uuid17();
|
|
13084
14074
|
await db.run(
|
|
13085
14075
|
`INSERT INTO comments
|
|
13086
14076
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -13280,7 +14270,7 @@ function rowToComment(row) {
|
|
|
13280
14270
|
|
|
13281
14271
|
// src/governance/stewards-parser.ts
|
|
13282
14272
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
13283
|
-
import { join as
|
|
14273
|
+
import { join as join7 } from "path";
|
|
13284
14274
|
function parseStewardsYaml(content) {
|
|
13285
14275
|
const result = { version: 1 };
|
|
13286
14276
|
const lines = content.split("\n");
|
|
@@ -13346,9 +14336,9 @@ function parseEntry(str) {
|
|
|
13346
14336
|
function loadStewardsConfig(nestId) {
|
|
13347
14337
|
const nestPath = resolveNestPath(nestId);
|
|
13348
14338
|
const candidates = [
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
14339
|
+
join7(nestPath, "stewards.yaml"),
|
|
14340
|
+
join7(nestPath, "stewards.yml"),
|
|
14341
|
+
join7(nestPath, ".context", "stewards.yaml")
|
|
13352
14342
|
];
|
|
13353
14343
|
for (const candidatePath of candidates) {
|
|
13354
14344
|
if (existsSync(candidatePath)) {
|
|
@@ -13365,7 +14355,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
13365
14355
|
if (Number.isNaN(n) || n < 1) return def;
|
|
13366
14356
|
return Math.min(n, max);
|
|
13367
14357
|
}
|
|
13368
|
-
var governanceRoutes = new
|
|
14358
|
+
var governanceRoutes = new Hono27();
|
|
13369
14359
|
governanceRoutes.get("/stewards", async (c) => {
|
|
13370
14360
|
const nestId = c.req.param("nestId");
|
|
13371
14361
|
const scope = c.req.query("scope");
|
|
@@ -13609,7 +14599,7 @@ governanceRoutes.post("/request-deletion", async (c) => {
|
|
|
13609
14599
|
if (body.targetType === "folder") {
|
|
13610
14600
|
const { storage } = await engineCache.get(nestId);
|
|
13611
14601
|
const docs = await storage.discoverDocuments();
|
|
13612
|
-
if (!docs.some((d) => isIdUnderFolder(d.id, target))) {
|
|
14602
|
+
if (!docs.some((d) => isIdUnderFolder(d.id, target)) && !await vaultFolderExists(nestId, target)) {
|
|
13613
14603
|
return c.json({ error: `Folder not found: ${target}` }, 404);
|
|
13614
14604
|
}
|
|
13615
14605
|
}
|
|
@@ -13735,7 +14725,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
13735
14725
|
const activity = await getActivity({ nestId, limit });
|
|
13736
14726
|
return c.json({ activity });
|
|
13737
14727
|
});
|
|
13738
|
-
var governanceNodeRoutes = new
|
|
14728
|
+
var governanceNodeRoutes = new Hono27();
|
|
13739
14729
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
13740
14730
|
const nestId = c.req.param("nestId");
|
|
13741
14731
|
const nodeId = c.req.param("nodeId");
|
|
@@ -14230,23 +15220,29 @@ function publicBaseUrlError(value) {
|
|
|
14230
15220
|
// src/app.ts
|
|
14231
15221
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
14232
15222
|
import { fileURLToPath } from "url";
|
|
14233
|
-
import { dirname as dirname2, join as
|
|
15223
|
+
import { dirname as dirname2, join as join8, relative as relative2 } from "path";
|
|
14234
15224
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
14235
15225
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
14236
15226
|
var SERVICE_VERSION = (() => {
|
|
14237
15227
|
try {
|
|
14238
|
-
return JSON.parse(readFileSync4(
|
|
15228
|
+
return JSON.parse(readFileSync4(join8(HERE, "..", "package.json"), "utf8")).version;
|
|
14239
15229
|
} catch {
|
|
14240
15230
|
return "unknown";
|
|
14241
15231
|
}
|
|
14242
15232
|
})();
|
|
14243
15233
|
var UI_DIR_CANDIDATES = [
|
|
14244
|
-
|
|
14245
|
-
|
|
15234
|
+
join8(HERE, "web3"),
|
|
15235
|
+
join8(process.cwd(), "dist", "web3")
|
|
14246
15236
|
];
|
|
14247
15237
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
14248
15238
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
14249
|
-
var
|
|
15239
|
+
var isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
15240
|
+
var serveIndexShell = serveStatic({
|
|
15241
|
+
root: UI_DIR_REL,
|
|
15242
|
+
path: "index.html",
|
|
15243
|
+
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
15244
|
+
});
|
|
15245
|
+
var openModeMiddleware = createMiddleware3(async (c, next) => {
|
|
14250
15246
|
const anonId = await ensureAnonymousUser();
|
|
14251
15247
|
c.set("userId", anonId);
|
|
14252
15248
|
c.set("nestScope", null);
|
|
@@ -14258,7 +15254,7 @@ function isPublicReadEligiblePath(method, path) {
|
|
|
14258
15254
|
if (/^\/nests\/[^/]+\/stewards/.test(path)) return false;
|
|
14259
15255
|
return !/\/(collaborators|visibility|settings|mcp)/.test(path);
|
|
14260
15256
|
}
|
|
14261
|
-
var flexAuthMiddleware =
|
|
15257
|
+
var flexAuthMiddleware = createMiddleware3(async (c, next) => {
|
|
14262
15258
|
const hasBearer = c.req.header("Authorization")?.startsWith("Bearer cnst_");
|
|
14263
15259
|
const hasCookie = !!c.req.header("Cookie")?.includes("cnst_session=");
|
|
14264
15260
|
if (hasBearer || hasCookie) {
|
|
@@ -14284,7 +15280,7 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
14284
15280
|
function nestPathParts(path) {
|
|
14285
15281
|
return path.replace(/^\/nests\//, "").split("/").filter(Boolean);
|
|
14286
15282
|
}
|
|
14287
|
-
var flexAuthWithPromptowlTicket =
|
|
15283
|
+
var flexAuthWithPromptowlTicket = createMiddleware3(
|
|
14288
15284
|
async (c, next) => {
|
|
14289
15285
|
const header = c.req.header("Authorization") || "";
|
|
14290
15286
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
@@ -14361,7 +15357,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
14361
15357
|
return ok;
|
|
14362
15358
|
}
|
|
14363
15359
|
function createApp() {
|
|
14364
|
-
const app = new
|
|
15360
|
+
const app = new Hono28({ router: new LinearRouter() });
|
|
14365
15361
|
const corsOrigins = config.CORS_ORIGINS;
|
|
14366
15362
|
app.use(
|
|
14367
15363
|
"*",
|
|
@@ -14380,11 +15376,13 @@ function createApp() {
|
|
|
14380
15376
|
if (lenStr) {
|
|
14381
15377
|
const len = parseInt(lenStr, 10);
|
|
14382
15378
|
const isPost = c.req.method === "POST";
|
|
14383
|
-
const
|
|
15379
|
+
const isPdf2 = isPost && PDF_UPLOAD_PATH.test(c.req.path);
|
|
15380
|
+
const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPdf2 ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
|
|
14384
15381
|
if (Number.isFinite(len) && len > cap) {
|
|
14385
15382
|
return c.json(
|
|
14386
15383
|
{
|
|
14387
|
-
|
|
15384
|
+
// A PDF gets the same message as the route's own gates.
|
|
15385
|
+
error: isPdf2 ? pdfTooLargeMessage(len) : `Request body too large (${len} bytes, max ${cap})`
|
|
14388
15386
|
},
|
|
14389
15387
|
413
|
|
14390
15388
|
);
|
|
@@ -14411,6 +15409,7 @@ function createApp() {
|
|
|
14411
15409
|
client: c.get("clientMetadata")
|
|
14412
15410
|
});
|
|
14413
15411
|
});
|
|
15412
|
+
app.use("*", nestScopedKeyGuard);
|
|
14414
15413
|
app.get("/health", (c) => {
|
|
14415
15414
|
const oidcReady = isOidcReady();
|
|
14416
15415
|
return c.json({
|
|
@@ -14425,6 +15424,9 @@ function createApp() {
|
|
|
14425
15424
|
// Presence only: the publish dialog hides the email/invite gates when
|
|
14426
15425
|
// this server cannot send mail, because their only way in is a mailed link.
|
|
14427
15426
|
email_configured: isEmailConfigured(),
|
|
15427
|
+
// Who may create nests (admins | everyone) — the Nests page hides the
|
|
15428
|
+
// create buttons from non-admins when it's admins-only.
|
|
15429
|
+
nest_creation: config.NEST_CREATION,
|
|
14428
15430
|
// Product analytics for the SPA. The PostHog project key is a public
|
|
14429
15431
|
// client key, but it only ships while usage telemetry is on — one switch
|
|
14430
15432
|
// covers everything this server sends outward.
|
|
@@ -14453,6 +15455,11 @@ function createApp() {
|
|
|
14453
15455
|
// API would refuse it — existing nodes of the type stay readable.
|
|
14454
15456
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
14455
15457
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
15458
|
+
// Hootie in the nest (default ON). Off → POST /nests/:id/chat 404s and
|
|
15459
|
+
// the UI hides the launcher. Whether it can actually answer is
|
|
15460
|
+
// runner_key_configured above — the UI shows the launcher whenever the
|
|
15461
|
+
// feature is on and explains the missing key, rather than hiding it.
|
|
15462
|
+
hootie_enabled: config.HOOTIE_ENABLED,
|
|
14456
15463
|
// This server hosts several organizations (PromptOwl-hosted) rather than
|
|
14457
15464
|
// one. The visibility picker words 'Organization' and 'Public' by it and
|
|
14458
15465
|
// confirms before 'org'; the server enforces the same in
|
|
@@ -14567,6 +15574,9 @@ function createApp() {
|
|
|
14567
15574
|
promptowl_teams_enabled: config.PROMPTOWL_TEAMS_ENABLED,
|
|
14568
15575
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
14569
15576
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
15577
|
+
hootie_enabled: config.HOOTIE_ENABLED,
|
|
15578
|
+
hootie_model: config.HOOTIE_MODEL,
|
|
15579
|
+
anthropic_base_url: config.ANTHROPIC_BASE_URL,
|
|
14570
15580
|
subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
|
|
14571
15581
|
subagent_max_depth: config.SUBAGENT_MAX_DEPTH,
|
|
14572
15582
|
subagent_max_children: config.SUBAGENT_MAX_CHILDREN,
|
|
@@ -14580,6 +15590,7 @@ function createApp() {
|
|
|
14580
15590
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
14581
15591
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
14582
15592
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
15593
|
+
nest_creation: config.NEST_CREATION,
|
|
14583
15594
|
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
14584
15595
|
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
14585
15596
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
@@ -14601,7 +15612,12 @@ function createApp() {
|
|
|
14601
15612
|
anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
|
|
14602
15613
|
slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
|
|
14603
15614
|
msteams_webhook_url: config.MSTEAMS_WEBHOOK_URL ?? "",
|
|
15615
|
+
email_provider: config.EMAIL_PROVIDER,
|
|
14604
15616
|
smtp_url: config.SMTP_URL ?? "",
|
|
15617
|
+
ses_region: config.SES_REGION ?? "",
|
|
15618
|
+
ses_access_key_id: config.SES_ACCESS_KEY_ID ?? "",
|
|
15619
|
+
// Write-only, like the OIDC secret — presence only, never the value.
|
|
15620
|
+
ses_secret_access_key_set: !!config.SES_SECRET_ACCESS_KEY,
|
|
14605
15621
|
notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
|
|
14606
15622
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
14607
15623
|
});
|
|
@@ -14699,6 +15715,38 @@ function createApp() {
|
|
|
14699
15715
|
value: body.type_table_enabled ? null : "false"
|
|
14700
15716
|
});
|
|
14701
15717
|
}
|
|
15718
|
+
if ("hootie_enabled" in body) {
|
|
15719
|
+
pending.push({
|
|
15720
|
+
name: "HOOTIE_ENABLED",
|
|
15721
|
+
value: body.hootie_enabled ? null : "false"
|
|
15722
|
+
});
|
|
15723
|
+
}
|
|
15724
|
+
if ("hootie_model" in body) {
|
|
15725
|
+
const v = String(body.hootie_model ?? "").trim();
|
|
15726
|
+
if (!v) addError("hootie_model", "Model is required \u2014 e.g. claude-opus-5.");
|
|
15727
|
+
else if (v.length > 200 || /\s/.test(v))
|
|
15728
|
+
addError("hootie_model", "Model must be a single identifier with no spaces.");
|
|
15729
|
+
else pending.push({ name: "HOOTIE_MODEL", value: v === "claude-opus-5" ? null : v });
|
|
15730
|
+
}
|
|
15731
|
+
if ("anthropic_base_url" in body) {
|
|
15732
|
+
const v = String(body.anthropic_base_url ?? "").trim().replace(/\/$/, "");
|
|
15733
|
+
if (!v) pending.push({ name: "ANTHROPIC_BASE_URL", value: null });
|
|
15734
|
+
else {
|
|
15735
|
+
let ok = false;
|
|
15736
|
+
try {
|
|
15737
|
+
const u = new URL(v);
|
|
15738
|
+
ok = (u.protocol === "http:" || u.protocol === "https:") && !!u.host;
|
|
15739
|
+
} catch {
|
|
15740
|
+
ok = false;
|
|
15741
|
+
}
|
|
15742
|
+
if (!ok)
|
|
15743
|
+
addError(
|
|
15744
|
+
"anthropic_base_url",
|
|
15745
|
+
"Model gateway URL must be an http(s) URL, e.g. http://gateway:4000 \u2014 or empty to call Anthropic directly."
|
|
15746
|
+
);
|
|
15747
|
+
else pending.push({ name: "ANTHROPIC_BASE_URL", value: v });
|
|
15748
|
+
}
|
|
15749
|
+
}
|
|
14702
15750
|
if ("anthropic_api_key" in body) {
|
|
14703
15751
|
const v = body.anthropic_api_key;
|
|
14704
15752
|
if (v === null || v === "") {
|
|
@@ -14706,7 +15754,12 @@ function createApp() {
|
|
|
14706
15754
|
} else if (typeof v !== "string" || !v.trim().startsWith("sk-")) {
|
|
14707
15755
|
addError("anthropic_api_key", 'Anthropic API key must start with "sk-" (or be empty to clear it).');
|
|
14708
15756
|
} else {
|
|
14709
|
-
|
|
15757
|
+
const key = v.trim();
|
|
15758
|
+
const patchedBase = pending.find((p) => p.name === "ANTHROPIC_BASE_URL");
|
|
15759
|
+
const baseUrl = patchedBase ? patchedBase.value : config.ANTHROPIC_BASE_URL || null;
|
|
15760
|
+
const rejected = await verifyModelKey(key, baseUrl);
|
|
15761
|
+
if (rejected) addError("anthropic_api_key", rejected);
|
|
15762
|
+
else pending.push({ name: "ANTHROPIC_API_KEY", value: key });
|
|
14710
15763
|
}
|
|
14711
15764
|
}
|
|
14712
15765
|
if ("subagent_runs_enabled" in body) {
|
|
@@ -14806,6 +15859,12 @@ function createApp() {
|
|
|
14806
15859
|
value: body.oidc_department_tagging ? "true" : null
|
|
14807
15860
|
});
|
|
14808
15861
|
}
|
|
15862
|
+
if ("nest_creation" in body) {
|
|
15863
|
+
const v = String(body.nest_creation ?? "").trim().toLowerCase();
|
|
15864
|
+
if (v !== "admins" && v !== "everyone")
|
|
15865
|
+
addError("nest_creation", "Nest creation must be admins or everyone.");
|
|
15866
|
+
else pending.push({ name: "NEST_CREATION", value: v });
|
|
15867
|
+
}
|
|
14809
15868
|
if ("people_suggest_all_users" in body) {
|
|
14810
15869
|
pending.push({
|
|
14811
15870
|
name: "PEOPLE_SUGGEST_ALL_USERS",
|
|
@@ -14915,6 +15974,50 @@ function createApp() {
|
|
|
14915
15974
|
addError("smtp_url", "SMTP connection URL must start with smtp:// or smtps:// (or leave it empty to turn email notifications off).");
|
|
14916
15975
|
else pending.push({ name: "SMTP_URL", value: v || null });
|
|
14917
15976
|
}
|
|
15977
|
+
if ("email_provider" in body) {
|
|
15978
|
+
const v = String(body.email_provider ?? "").trim().toLowerCase();
|
|
15979
|
+
if (v !== "smtp" && v !== "ses")
|
|
15980
|
+
addError("email_provider", "Email provider must be SMTP or AWS SES.");
|
|
15981
|
+
else pending.push({ name: "EMAIL_PROVIDER", value: v });
|
|
15982
|
+
}
|
|
15983
|
+
if ("ses_region" in body) {
|
|
15984
|
+
const v = String(body.ses_region ?? "").trim();
|
|
15985
|
+
if (v && !/^[a-z]{2}(-[a-z]+)+-\d+$/.test(v))
|
|
15986
|
+
addError("ses_region", "AWS region must look like us-east-1.");
|
|
15987
|
+
else pending.push({ name: "SES_REGION", value: v || null });
|
|
15988
|
+
}
|
|
15989
|
+
if ("ses_access_key_id" in body) {
|
|
15990
|
+
const v = String(body.ses_access_key_id ?? "").trim();
|
|
15991
|
+
pending.push({ name: "SES_ACCESS_KEY_ID", value: v || null });
|
|
15992
|
+
}
|
|
15993
|
+
if ("ses_secret_access_key" in body) {
|
|
15994
|
+
const v = body.ses_secret_access_key;
|
|
15995
|
+
if (v !== null && typeof v !== "string")
|
|
15996
|
+
addError("ses_secret_access_key", "AWS secret access key must be a string (or empty to clear it).");
|
|
15997
|
+
else pending.push({ name: "SES_SECRET_ACCESS_KEY", value: v?.trim() || null });
|
|
15998
|
+
}
|
|
15999
|
+
const provider = String(body.email_provider ?? "").trim().toLowerCase();
|
|
16000
|
+
if (provider === "smtp" || provider === "ses") {
|
|
16001
|
+
const stale = provider === "ses" ? ["SMTP_URL"] : ["SES_REGION", "SES_ACCESS_KEY_ID", "SES_SECRET_ACCESS_KEY"];
|
|
16002
|
+
for (const name of stale) {
|
|
16003
|
+
const i = pending.findIndex((p) => p.name === name);
|
|
16004
|
+
if (i >= 0) pending.splice(i, 1);
|
|
16005
|
+
pending.push({ name, value: null });
|
|
16006
|
+
}
|
|
16007
|
+
}
|
|
16008
|
+
if ("ses_access_key_id" in body || "ses_secret_access_key" in body) {
|
|
16009
|
+
const next = (name, current) => {
|
|
16010
|
+
const p = pending.find((x) => x.name === name);
|
|
16011
|
+
return p ? p.value : current;
|
|
16012
|
+
};
|
|
16013
|
+
const hasId = !!next("SES_ACCESS_KEY_ID", config.SES_ACCESS_KEY_ID);
|
|
16014
|
+
const hasSecret = !!next("SES_SECRET_ACCESS_KEY", config.SES_SECRET_ACCESS_KEY);
|
|
16015
|
+
if (hasId !== hasSecret)
|
|
16016
|
+
addError(
|
|
16017
|
+
"ses_access_key_id",
|
|
16018
|
+
"Set both the AWS access key ID and secret access key, or leave both empty to use the server's IAM role."
|
|
16019
|
+
);
|
|
16020
|
+
}
|
|
14918
16021
|
if ("notify_email_from" in body) {
|
|
14919
16022
|
const v = String(body.notify_email_from ?? "").trim();
|
|
14920
16023
|
if (v && v.includes(","))
|
|
@@ -14952,15 +16055,18 @@ function createApp() {
|
|
|
14952
16055
|
{ error: errorMessages.join("; "), fieldErrors, settings: currentServerSettings() },
|
|
14953
16056
|
400
|
|
14954
16057
|
);
|
|
14955
|
-
const
|
|
14956
|
-
|
|
14957
|
-
|
|
16058
|
+
const over = Object.fromEntries(pending.map((p) => [p.name, p.value]));
|
|
16059
|
+
const nextEmail = transportSettings(over);
|
|
16060
|
+
const emailFields = nextEmail?.provider === "ses" ? ["EMAIL_PROVIDER", "SES_REGION", "SES_ACCESS_KEY_ID", "SES_SECRET_ACCESS_KEY"] : ["EMAIL_PROVIDER", "SMTP_URL"];
|
|
16061
|
+
if (nextEmail && emailFields.some((k) => k in over)) {
|
|
16062
|
+
const result = await verifyEmailTransport(nextEmail);
|
|
14958
16063
|
if (!result.ok) {
|
|
14959
|
-
const
|
|
16064
|
+
const ses = nextEmail.provider === "ses";
|
|
16065
|
+
const message = `${ses ? "AWS SES" : "SMTP"} connection failed: ${result.error}`;
|
|
14960
16066
|
return c.json(
|
|
14961
16067
|
{
|
|
14962
16068
|
error: message,
|
|
14963
|
-
fieldErrors: { smtp_url: message },
|
|
16069
|
+
fieldErrors: { [ses ? "ses_region" : "smtp_url"]: message },
|
|
14964
16070
|
settings: currentServerSettings()
|
|
14965
16071
|
},
|
|
14966
16072
|
400
|
|
@@ -15119,6 +16225,10 @@ function createApp() {
|
|
|
15119
16225
|
app.use("/admin/community-sites", flexAuthMiddleware);
|
|
15120
16226
|
app.use("/admin/community-sites/*", flexAuthMiddleware);
|
|
15121
16227
|
app.route("/admin/community-sites", communitySiteRoutes);
|
|
16228
|
+
app.get("/teams", async (c, next) => {
|
|
16229
|
+
if (isDocumentNavigation(c)) return serveIndexShell(c, next);
|
|
16230
|
+
return next();
|
|
16231
|
+
});
|
|
15122
16232
|
app.use("/teams/*", flexAuthMiddleware);
|
|
15123
16233
|
app.use("/teams", flexAuthMiddleware);
|
|
15124
16234
|
app.route("/teams", teamRoutes);
|
|
@@ -15172,7 +16282,7 @@ function createApp() {
|
|
|
15172
16282
|
const others = requests.filter((r) => r.requestedBy !== me);
|
|
15173
16283
|
const nestApprove = new Map(
|
|
15174
16284
|
await Promise.all(
|
|
15175
|
-
[...
|
|
16285
|
+
[...nestName2.keys()].map(
|
|
15176
16286
|
async (nestId) => [
|
|
15177
16287
|
nestId,
|
|
15178
16288
|
await canApproveInNest(nestId, me)
|
|
@@ -15202,13 +16312,33 @@ function createApp() {
|
|
|
15202
16312
|
);
|
|
15203
16313
|
waiting.sort(byNewest);
|
|
15204
16314
|
const notifications = await listNotifications(me, { unreadOnly: true, limit: 50 });
|
|
16315
|
+
const approvableNestIds = [...nestName2.keys()].filter(
|
|
16316
|
+
(id) => nestApprove.get(id)
|
|
16317
|
+
);
|
|
16318
|
+
const restNestIds = [...nestName2.keys()].filter(
|
|
16319
|
+
(id) => !nestApprove.get(id)
|
|
16320
|
+
);
|
|
16321
|
+
const [{ total: approvableReviewTotal }, { total: myReviewsElsewhereTotal }] = await Promise.all([
|
|
16322
|
+
getReviewQueue({ status: "pending", nestIds: approvableNestIds, limit: 1 }),
|
|
16323
|
+
getReviewQueue({
|
|
16324
|
+
status: "pending",
|
|
16325
|
+
nestIds: restNestIds,
|
|
16326
|
+
requestedByEmail: me,
|
|
16327
|
+
limit: 1
|
|
16328
|
+
})
|
|
16329
|
+
]);
|
|
16330
|
+
const reviewTotal = approvableReviewTotal + myReviewsElsewhereTotal;
|
|
16331
|
+
const deletionsShown = items.filter((i) => i.type === "deletion").length + waiting.filter((i) => i.type === "deletion").length;
|
|
15205
16332
|
return c.json({
|
|
15206
16333
|
items,
|
|
15207
16334
|
waiting,
|
|
15208
16335
|
notifications,
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
16336
|
+
pagination: {
|
|
16337
|
+
total: reviewTotal + deletionsShown,
|
|
16338
|
+
limit,
|
|
16339
|
+
offset,
|
|
16340
|
+
has_more: offset + requests.length < total
|
|
16341
|
+
}
|
|
15212
16342
|
});
|
|
15213
16343
|
});
|
|
15214
16344
|
app.get("/me/drafts", async (c) => {
|
|
@@ -15257,12 +16387,29 @@ function createApp() {
|
|
|
15257
16387
|
await markNotificationsRead(me, body.ids === "all" ? "all" : body.ids ?? []);
|
|
15258
16388
|
return c.json({ ok: true });
|
|
15259
16389
|
});
|
|
16390
|
+
app.post("/me/nests/:nestId/leave", async (c) => {
|
|
16391
|
+
const nestId = c.req.param("nestId");
|
|
16392
|
+
const me = await resolveCallerEmail(c.get("userId"));
|
|
16393
|
+
const db = getDb();
|
|
16394
|
+
const info = await db.run(
|
|
16395
|
+
"DELETE FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
16396
|
+
[nestId, c.get("userId")]
|
|
16397
|
+
);
|
|
16398
|
+
await db.run(
|
|
16399
|
+
`UPDATE notifications SET read_at = ? WHERE LOWER(user_email) = LOWER(?) AND nest_id = ? AND kind = 'nest_invite' AND read_at IS NULL`,
|
|
16400
|
+
[(/* @__PURE__ */ new Date()).toISOString(), me, nestId]
|
|
16401
|
+
);
|
|
16402
|
+
return c.json({ left: info.changes > 0 });
|
|
16403
|
+
});
|
|
15260
16404
|
app.use("/stats", flexAuthMiddleware);
|
|
15261
16405
|
app.get("/stats", async (c) => {
|
|
15262
16406
|
const db = getDb();
|
|
15263
16407
|
const userId = c.get("userId");
|
|
15264
16408
|
const userEmail = await resolveCallerEmail(userId);
|
|
15265
|
-
const
|
|
16409
|
+
const nestScope = c.get("nestScope");
|
|
16410
|
+
const visibleNests = (await listVisibleNests(userId)).filter(
|
|
16411
|
+
(n) => !nestScope || n.id === nestScope
|
|
16412
|
+
);
|
|
15266
16413
|
let documents = 0;
|
|
15267
16414
|
for (const nest of visibleNests) {
|
|
15268
16415
|
try {
|
|
@@ -15290,8 +16437,12 @@ function createApp() {
|
|
|
15290
16437
|
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 20, 1), 50);
|
|
15291
16438
|
const userId = c.get("userId");
|
|
15292
16439
|
const userEmail = await resolveCallerEmail(userId);
|
|
16440
|
+
const nestScope = c.get("nestScope");
|
|
16441
|
+
const searchable = (await listVisibleNests(userId, { readable: true })).filter(
|
|
16442
|
+
(n) => !nestScope || n.id === nestScope
|
|
16443
|
+
);
|
|
15293
16444
|
const perNest = await Promise.all(
|
|
15294
|
-
|
|
16445
|
+
searchable.map(async (nest) => {
|
|
15295
16446
|
try {
|
|
15296
16447
|
const { storage } = await engineCache.get(nest.id);
|
|
15297
16448
|
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
@@ -15324,7 +16475,7 @@ function createApp() {
|
|
|
15324
16475
|
app.route("/index", nestIndexRoutes);
|
|
15325
16476
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
15326
16477
|
app.route("/mcp", serverMcpRoutes);
|
|
15327
|
-
const nestsApp = new
|
|
16478
|
+
const nestsApp = new Hono28();
|
|
15328
16479
|
nestsApp.use("*", async (c, next) => {
|
|
15329
16480
|
const parts = nestPathParts(c.req.path);
|
|
15330
16481
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -15403,7 +16554,7 @@ function createApp() {
|
|
|
15403
16554
|
if (access.archived && isMcpTransport) {
|
|
15404
16555
|
return deny(404, "Nest not found");
|
|
15405
16556
|
}
|
|
15406
|
-
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
16557
|
+
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export", "chat"].includes(parts[1]);
|
|
15407
16558
|
let grantRole = null;
|
|
15408
16559
|
let grantNestVisible = false;
|
|
15409
16560
|
if (permission === "none") {
|
|
@@ -15454,7 +16605,7 @@ function createApp() {
|
|
|
15454
16605
|
);
|
|
15455
16606
|
}
|
|
15456
16607
|
const resource = parts[1];
|
|
15457
|
-
if (resource === "visibility" || resource === "reader-mode") {
|
|
16608
|
+
if (resource === "visibility" || resource === "reader-mode" || resource === "branding") {
|
|
15458
16609
|
required = "admin";
|
|
15459
16610
|
} else if (resource === "collaborators") {
|
|
15460
16611
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
@@ -15550,7 +16701,9 @@ function createApp() {
|
|
|
15550
16701
|
nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
|
|
15551
16702
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|
|
15552
16703
|
nestsApp.route("/:nestId", queryRoutes);
|
|
16704
|
+
nestsApp.route("/:nestId", hootieRoutes);
|
|
15553
16705
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
16706
|
+
nestsApp.route("/:nestId", brandingRoutes);
|
|
15554
16707
|
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
15555
16708
|
nestsApp.route("/:nestId/publications", publishRoutes);
|
|
15556
16709
|
nestsApp.route("/:nestId/teams", nestTeamRoutes);
|
|
@@ -15574,18 +16727,13 @@ function createApp() {
|
|
|
15574
16727
|
onFound: (_p, c) => c.header("Cache-Control", "public, max-age=31536000, immutable")
|
|
15575
16728
|
})
|
|
15576
16729
|
);
|
|
15577
|
-
const isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
15578
16730
|
app.get(
|
|
15579
16731
|
"*",
|
|
15580
16732
|
async (c, next) => {
|
|
15581
16733
|
if (isDocumentNavigation(c)) return next();
|
|
15582
16734
|
return c.json({ error: "Not found" }, 404);
|
|
15583
16735
|
},
|
|
15584
|
-
|
|
15585
|
-
root: UI_DIR_REL,
|
|
15586
|
-
path: "index.html",
|
|
15587
|
-
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
15588
|
-
})
|
|
16736
|
+
serveIndexShell
|
|
15589
16737
|
);
|
|
15590
16738
|
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
15591
16739
|
app.onError((err, c) => {
|
|
@@ -15873,7 +17021,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
15873
17021
|
}
|
|
15874
17022
|
|
|
15875
17023
|
// src/workflow/scheduler.ts
|
|
15876
|
-
import { v4 as
|
|
17024
|
+
import { v4 as uuid18 } from "uuid";
|
|
15877
17025
|
function isDue(s, now) {
|
|
15878
17026
|
if (!s.last_run_at) return true;
|
|
15879
17027
|
const last = Date.parse(s.last_run_at);
|
|
@@ -15901,7 +17049,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
15901
17049
|
);
|
|
15902
17050
|
continue;
|
|
15903
17051
|
}
|
|
15904
|
-
const id = `run_${
|
|
17052
|
+
const id = `run_${uuid18()}`;
|
|
15905
17053
|
const nowIso2 = new Date(now).toISOString();
|
|
15906
17054
|
await db.run(
|
|
15907
17055
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|