@promptowl/contextnest-community 1.24.0 → 1.26.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 +177 -14
- package/CONFIGURATION.md +577 -116
- package/README.md +4 -3
- package/STEWARDSHIP.md +24 -0
- package/dist/{chunk-XUIWAWDO.js → chunk-2QOJE7A5.js} +4 -4
- package/dist/{chunk-2TPQTN4Y.js → chunk-DGPTJXIV.js} +47 -15
- package/dist/chunk-I5EDEZBQ.js +523 -0
- package/dist/{chunk-ZTT4U4NE.js → chunk-IEAWRHMV.js} +99 -12
- package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
- package/dist/{chunk-5EZOPA47.js → chunk-M6PGYMBD.js} +20 -11
- package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
- package/dist/{chunk-I3CSD6CK.js → chunk-RPGFHESR.js} +2 -2
- package/dist/{chunk-KIAAEHWL.js → chunk-SWA5M5XX.js} +88 -546
- package/dist/{chunk-LA3VTQ22.js → chunk-TRSGPS27.js} +81 -14
- package/dist/{engine-S3QBQ7LH.js → engine-QNMFDIBV.js} +4 -3
- package/dist/{external-edit-service-6FNFPOJJ.js → external-edit-service-4MEGPT4D.js} +5 -4
- package/dist/{grants-service-UT3EQ3R7.js → grants-service-24GLUBY4.js} +3 -3
- package/dist/index.js +1739 -583
- package/dist/invited-user-AM6A3WGO.js +12 -0
- package/dist/{migrations.postgres-AIQ7WSU7.js → migrations.postgres-HGSQ7IA5.js} +16 -2
- package/dist/{review-service-HHGOTO6P.js → review-service-2EWN7WWO.js} +8 -7
- package/dist/{stewardship-service-4DHNRULG.js → stewardship-service-TDZUC2Q6.js} +7 -4
- package/dist/{version-service-KIOQME6U.js → version-service-C3JTZLJH.js} +5 -4
- package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
- package/dist/web3/assets/{AgentDocsPage-CIiaBqMy.js → AgentDocsPage-ixP2UdAE.js} +1 -1
- package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +1 -0
- package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +36 -0
- package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +1 -0
- package/dist/web3/assets/MarkdownEditor-CKh4WSn-.js +653 -0
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Cizo8nUi.js +1 -0
- package/dist/web3/assets/NestView-EW-sraEb.js +73 -0
- package/dist/web3/assets/OverviewTab-CjidsMX_.js +6 -0
- package/dist/web3/assets/PersonCombobox-BKI7dff4.js +1 -0
- package/dist/web3/assets/{ReviewActions-OfOdqzCn.js → ReviewActions-CS8JXgXF.js} +2 -2
- package/dist/web3/assets/ReviewTab-CwVrXzw5.js +1 -0
- package/dist/web3/assets/StewardsTab-C-iP6gnh.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-Biq3m79i.js +1 -0
- package/dist/web3/assets/{alert-dialog-DTozKlmV.js → alert-dialog-BvF3NFr7.js} +2 -2
- package/dist/web3/assets/{arrow-left-BCKt4CwQ.js → arrow-left-CWIsv-sG.js} +1 -1
- package/dist/web3/assets/backlinks-Cqcn7n1J.js +19 -0
- package/dist/web3/assets/boxes-ClbgQqG2.js +6 -0
- package/dist/web3/assets/{chevron-left-BPwUT9DS.js → chevron-left-CK_ZPFm9.js} +1 -1
- package/dist/web3/assets/{circle-check-DI8IBGeo.js → circle-check-Bw_aTXiH.js} +1 -1
- package/dist/web3/assets/{circle-x-VKVhC88W.js → circle-x-DkIG68H7.js} +1 -1
- package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
- package/dist/web3/assets/{code-xml-CMabYZvH.js → code-xml-BlR7EN6l.js} +1 -1
- package/dist/web3/assets/{corner-down-right-BGxwK6wH.js → corner-down-right-DSVxK3tf.js} +1 -1
- package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
- package/dist/web3/assets/{earth-BEp1EKTo.js → earth-BXhVPvCR.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-r8qZGIjP.js → file-exclamation-point-BevGZgtj.js} +1 -1
- package/dist/web3/assets/{folder-input-Cmtx1Jhk.js → folder-input-m8UBV4fA.js} +1 -1
- package/dist/web3/assets/{index-C1wTSjfP.js → index-BOVYgMvj.js} +1 -1
- package/dist/web3/assets/index-DVnaYqj9.css +1 -0
- package/dist/web3/assets/index-WYDmEOfO.js +404 -0
- package/dist/web3/assets/page-B6pkc9cd.js +2 -0
- package/dist/web3/assets/page-BJrFCKXB.js +1 -0
- package/dist/web3/assets/page-Bgy9Ng9p.js +1 -0
- package/dist/web3/assets/page-C2zoxumw.js +1 -0
- package/dist/web3/assets/page-CJWO06dN.js +1 -0
- package/dist/web3/assets/page-CptuAPJc.js +6 -0
- package/dist/web3/assets/page-DPY01SOj.js +1 -0
- package/dist/web3/assets/page-Dea2SblF.js +9 -0
- package/dist/web3/assets/page-Deq6IKiN.js +6 -0
- package/dist/web3/assets/page-Dfjg8p7Z.js +1 -0
- package/dist/web3/assets/{page-ByyVLDVo.js → page-Dh57L0RG.js} +5 -5
- package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
- package/dist/web3/assets/{page-CHb0F5hV.js → page-DzKwP5Ds.js} +3 -3
- package/dist/web3/assets/page-YxjrE5b5.js +45 -0
- package/dist/web3/assets/{page-title-ClvvBsIo.js → page-title-DVq4qvMR.js} +1 -1
- package/dist/web3/assets/{play-C5YNKpAH.js → play-CXhS6Pgq.js} +1 -1
- package/dist/web3/assets/{send-0CHvmluT.js → send-DdfwnIrj.js} +1 -1
- package/dist/web3/assets/{settings-CeeF4LEb.js → settings-DKX0Q4gO.js} +1 -1
- package/dist/web3/assets/{share-2-CMnOlOt-.js → share-2-CxmXRt58.js} +1 -1
- package/dist/web3/assets/{tag-DQ_6J5Gv.js → tag-DeYC_mRv.js} +1 -1
- package/dist/web3/assets/{trash-2-HBi-Pslz.js → trash-2-B0HyPTnI.js} +1 -1
- package/dist/web3/assets/{user-plus-CvBNcpn4.js → user-plus-Ckm_Isyc.js} +1 -1
- package/dist/web3/assets/{x-By7piikG.js → x-mNtADYHc.js} +1 -1
- package/dist/web3/assets/zap-D2ifcnYb.js +16 -0
- package/dist/web3/index.html +2 -2
- package/package.json +3 -2
- package/dist/client-KEY4PYJH.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +0 -1
- package/dist/web3/assets/CollaboratorManager-By91wrIr.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +0 -1
- package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +0 -36
- package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +0 -1
- package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +0 -1
- package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +0 -643
- package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +0 -1
- package/dist/web3/assets/NestView-CTZ55tXD.js +0 -63
- package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +0 -1
- package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +0 -1
- package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +0 -1
- package/dist/web3/assets/ReviewTab-CyX8yxd3.js +0 -1
- package/dist/web3/assets/StewardsTab-DBEEfAQI.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +0 -1
- package/dist/web3/assets/backlinks-CYd4xENL.js +0 -24
- package/dist/web3/assets/card-XG2dP5Xf.js +0 -1
- package/dist/web3/assets/count-skeleton-DMbdpscX.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-BM-h3DwI.css +0 -1
- package/dist/web3/assets/index-EaX2yql0.js +0 -389
- package/dist/web3/assets/page-B3yvQvHy.js +0 -1
- package/dist/web3/assets/page-BExMvttJ.js +0 -1
- package/dist/web3/assets/page-BUOADv2X.js +0 -1
- package/dist/web3/assets/page-BYUFhYWZ.js +0 -1
- package/dist/web3/assets/page-Bicf02Cz.js +0 -1
- package/dist/web3/assets/page-BsPMDm5d.js +0 -45
- package/dist/web3/assets/page-CC0NJi8v.js +0 -1
- package/dist/web3/assets/page-CPjWQNKx.js +0 -11
- package/dist/web3/assets/page-CQt3ZGbf.js +0 -2
- package/dist/web3/assets/page-Cj1NetQ-.js +0 -1
- package/dist/web3/assets/page-CtaW65El.js +0 -1
- package/dist/web3/assets/page-DGOL9l8i.js +0 -6
- package/dist/web3/assets/refresh-cw-BhHzSH9m.js +0 -6
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/scroll-area-hz-xtayP.js +0 -1
- package/dist/web3/assets/select-DBFTrUmI.js +0 -6
- package/dist/web3/assets/triangle-alert-CDy8-7sv.js +0 -6
- package/dist/web3/assets/zap-C2T8riBp.js +0 -11
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-M6PGYMBD.js";
|
|
42
42
|
import {
|
|
43
43
|
canCreateInNest,
|
|
44
44
|
canManageStewards,
|
|
@@ -56,9 +56,10 @@ import {
|
|
|
56
56
|
resolveStewardsForNode,
|
|
57
57
|
resolveStewardsWithFallback,
|
|
58
58
|
resolveUserRoles,
|
|
59
|
+
stewardDocumentCreator,
|
|
59
60
|
syncFromConfig,
|
|
60
61
|
updateSteward
|
|
61
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-DGPTJXIV.js";
|
|
62
63
|
import {
|
|
63
64
|
createGrant,
|
|
64
65
|
deleteGrant,
|
|
@@ -67,21 +68,16 @@ import {
|
|
|
67
68
|
listGrants,
|
|
68
69
|
listUserGrants,
|
|
69
70
|
resolveNodeGrant
|
|
70
|
-
} from "./chunk-
|
|
71
|
-
import {
|
|
72
|
-
generateApiKey,
|
|
73
|
-
getKeyPrefix,
|
|
74
|
-
hashApiKey,
|
|
75
|
-
hashPassword,
|
|
76
|
-
parseBearerToken,
|
|
77
|
-
verifyKeyRequest,
|
|
78
|
-
verifyPassword
|
|
79
|
-
} from "./chunk-F66VCBLA.js";
|
|
71
|
+
} from "./chunk-RPGFHESR.js";
|
|
80
72
|
import {
|
|
73
|
+
attributeRequest,
|
|
81
74
|
checkConflict,
|
|
75
|
+
clientToolField,
|
|
76
|
+
connectionClient,
|
|
82
77
|
createVersion,
|
|
83
78
|
deleteSupersededDrafts,
|
|
84
79
|
deleteUnsealedVersions,
|
|
80
|
+
deserializeClient,
|
|
85
81
|
getApprovedVersion,
|
|
86
82
|
getApprovedVersions,
|
|
87
83
|
getCurrentVersion,
|
|
@@ -91,9 +87,12 @@ import {
|
|
|
91
87
|
getTrackedNodeIds,
|
|
92
88
|
getVersions,
|
|
93
89
|
listMyDrafts,
|
|
90
|
+
mergeClient,
|
|
91
|
+
parseClientMetadata,
|
|
92
|
+
serializeClient,
|
|
94
93
|
setApprovedVersion,
|
|
95
94
|
upsertVersion
|
|
96
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-IEAWRHMV.js";
|
|
97
96
|
import {
|
|
98
97
|
approveExternalEdit,
|
|
99
98
|
getExternalEditDetail,
|
|
@@ -105,7 +104,7 @@ import {
|
|
|
105
104
|
scanDocumentForDrift,
|
|
106
105
|
scanNestForDrift,
|
|
107
106
|
startDriftScanner
|
|
108
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-2QOJE7A5.js";
|
|
109
108
|
import {
|
|
110
109
|
addMember,
|
|
111
110
|
addWatcher,
|
|
@@ -113,10 +112,7 @@ import {
|
|
|
113
112
|
assertSafeNodeId,
|
|
114
113
|
buildDocContext,
|
|
115
114
|
buildTitleMap,
|
|
116
|
-
canApproveWith,
|
|
117
|
-
canEditWith,
|
|
118
115
|
capForArchive,
|
|
119
|
-
collabPermToRole,
|
|
120
116
|
createNest,
|
|
121
117
|
createTeam,
|
|
122
118
|
deleteNest,
|
|
@@ -139,7 +135,6 @@ import {
|
|
|
139
135
|
insertOrIgnore,
|
|
140
136
|
installLicenseKey,
|
|
141
137
|
isConfigSuperAdmin,
|
|
142
|
-
isEmailConfigured,
|
|
143
138
|
isLicenseAdminEmail,
|
|
144
139
|
isLicenseAdminUserId,
|
|
145
140
|
isNestArchived,
|
|
@@ -162,6 +157,7 @@ import {
|
|
|
162
157
|
loadServerSettings,
|
|
163
158
|
markNotificationsRead,
|
|
164
159
|
nestAllowsSelfApprove,
|
|
160
|
+
nestCreatorIsReviewer,
|
|
165
161
|
nestName,
|
|
166
162
|
nestPrimeTags,
|
|
167
163
|
nestReviewsPrimeOnly,
|
|
@@ -177,7 +173,6 @@ import {
|
|
|
177
173
|
persistSetting,
|
|
178
174
|
pinNest,
|
|
179
175
|
pinnedNestIds,
|
|
180
|
-
primaryRole,
|
|
181
176
|
readIndexedNodes,
|
|
182
177
|
rebuildNodeIndex,
|
|
183
178
|
removeMember,
|
|
@@ -193,8 +188,8 @@ import {
|
|
|
193
188
|
resolveNodeUrl,
|
|
194
189
|
resolveTeamRolesForUser,
|
|
195
190
|
restoreNest,
|
|
196
|
-
sendEmailToRecipient,
|
|
197
191
|
setAllowSelfApprove,
|
|
192
|
+
setCreatorIsReviewer,
|
|
198
193
|
setPrimeOnlyReview,
|
|
199
194
|
setPrimeTags,
|
|
200
195
|
setReaderMode,
|
|
@@ -209,27 +204,48 @@ import {
|
|
|
209
204
|
unpinNest,
|
|
210
205
|
unshareTeamFromNest,
|
|
211
206
|
updateMemberRole,
|
|
212
|
-
validateLicense
|
|
213
|
-
|
|
214
|
-
} from "./chunk-KIAAEHWL.js";
|
|
207
|
+
validateLicense
|
|
208
|
+
} from "./chunk-SWA5M5XX.js";
|
|
215
209
|
import {
|
|
216
210
|
AppError,
|
|
217
211
|
ConflictError,
|
|
218
212
|
ForbiddenError,
|
|
219
213
|
LockedError,
|
|
220
214
|
NotFoundError,
|
|
215
|
+
PayloadTooLargeError,
|
|
221
216
|
UnauthorizedError,
|
|
222
217
|
ValidationError,
|
|
223
218
|
VaultBusyError,
|
|
224
219
|
isVaultLockTimeout
|
|
225
|
-
} from "./chunk-
|
|
220
|
+
} from "./chunk-LHWOJPFE.js";
|
|
221
|
+
import {
|
|
222
|
+
createInvitedUser,
|
|
223
|
+
findOrCreateInvitedUser,
|
|
224
|
+
generateApiKey,
|
|
225
|
+
getKeyPrefix,
|
|
226
|
+
hashApiKey,
|
|
227
|
+
hashPassword,
|
|
228
|
+
parseBearerToken,
|
|
229
|
+
verifyKeyRequest,
|
|
230
|
+
verifyPassword
|
|
231
|
+
} from "./chunk-PICUHQEM.js";
|
|
232
|
+
import {
|
|
233
|
+
canApproveWith,
|
|
234
|
+
canEditWith,
|
|
235
|
+
collabPermToRole,
|
|
236
|
+
isEmailConfigured,
|
|
237
|
+
primaryRole,
|
|
238
|
+
sendEmailToRecipient,
|
|
239
|
+
transportSettings,
|
|
240
|
+
verifyEmailTransport
|
|
241
|
+
} from "./chunk-I5EDEZBQ.js";
|
|
226
242
|
import {
|
|
227
243
|
config,
|
|
228
244
|
getDb,
|
|
229
245
|
initDb,
|
|
230
246
|
isEmailListish,
|
|
231
247
|
isEmailish
|
|
232
|
-
} from "./chunk-
|
|
248
|
+
} from "./chunk-TRSGPS27.js";
|
|
233
249
|
import {
|
|
234
250
|
ANON_EMAIL,
|
|
235
251
|
ANON_USER_ID,
|
|
@@ -243,9 +259,9 @@ import {
|
|
|
243
259
|
import { serve } from "@hono/node-server";
|
|
244
260
|
|
|
245
261
|
// src/app.ts
|
|
246
|
-
import { Hono as
|
|
262
|
+
import { Hono as Hono27 } from "hono";
|
|
247
263
|
import { LinearRouter } from "hono/router/linear-router";
|
|
248
|
-
import { createMiddleware as
|
|
264
|
+
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
249
265
|
import { cors } from "hono/cors";
|
|
250
266
|
|
|
251
267
|
// src/shared/artifact-sandbox.ts
|
|
@@ -430,7 +446,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
430
446
|
if (key) {
|
|
431
447
|
const keyHash = hashApiKey(key);
|
|
432
448
|
const record = await db.get(
|
|
433
|
-
"SELECT user_id, nest_id FROM api_keys WHERE key_hash = ?",
|
|
449
|
+
"SELECT user_id, nest_id, label FROM api_keys WHERE key_hash = ?",
|
|
434
450
|
[keyHash]
|
|
435
451
|
);
|
|
436
452
|
if (record) {
|
|
@@ -440,10 +456,104 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
440
456
|
);
|
|
441
457
|
c.set("userId", record.user_id);
|
|
442
458
|
c.set("nestScope", record.nest_id);
|
|
459
|
+
if (record.label) c.set("apiKeyLabel", record.label);
|
|
443
460
|
return next();
|
|
444
461
|
}
|
|
445
462
|
}
|
|
446
|
-
|
|
463
|
+
if (sessionId) {
|
|
464
|
+
return c.json(
|
|
465
|
+
{ error: "Your session has expired \u2014 sign in again.", code: "session_expired" },
|
|
466
|
+
401
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (key) {
|
|
470
|
+
return c.json(
|
|
471
|
+
{ error: "This API key is invalid or has been revoked \u2014 generate a new one.", code: "invalid_key" },
|
|
472
|
+
401
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return c.json(
|
|
476
|
+
{ error: "Authentication required \u2014 sign in or provide an API key.", code: "missing_credentials" },
|
|
477
|
+
401
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// src/auth/nest-scope-guard.ts
|
|
482
|
+
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
483
|
+
var NEST_SCOPED_KEY_CODE = "nest_scoped_key";
|
|
484
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set([
|
|
485
|
+
"/health",
|
|
486
|
+
"/license/status",
|
|
487
|
+
"/llms.txt",
|
|
488
|
+
"/auth/admin-status"
|
|
489
|
+
]);
|
|
490
|
+
function segmentMatches(seg, nestId) {
|
|
491
|
+
if (seg === nestId) return true;
|
|
492
|
+
try {
|
|
493
|
+
return decodeURIComponent(seg) === nestId;
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function nestKeyVerdict(method, rawPath, scope) {
|
|
499
|
+
const path = rawPath.length > 1 ? rawPath.replace(/\/+$/, "") : rawPath;
|
|
500
|
+
const isGet = method === "GET" || method === "HEAD";
|
|
501
|
+
if (PUBLIC_PATHS.has(path) && isGet) return { allowed: true };
|
|
502
|
+
if (path === "/mcp") return { allowed: true };
|
|
503
|
+
if (path === "/index" && isGet) return { allowed: true };
|
|
504
|
+
if ((path === "/search" || path === "/stats" || path === "/nests") && isGet) {
|
|
505
|
+
return { allowed: true };
|
|
506
|
+
}
|
|
507
|
+
if (path.startsWith("/runs/")) return { allowed: true };
|
|
508
|
+
const m = /^\/nests\/([^/]+)(\/.*)?$/.exec(path);
|
|
509
|
+
if (m) {
|
|
510
|
+
if (segmentMatches(m[1], scope)) return { allowed: true };
|
|
511
|
+
return {
|
|
512
|
+
allowed: false,
|
|
513
|
+
reason: "other_nest",
|
|
514
|
+
mcpTransport: m[2] === "/mcp"
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return { allowed: false, reason: "outside_nest" };
|
|
518
|
+
}
|
|
519
|
+
async function bearerKeyScope(authorization) {
|
|
520
|
+
const key = parseBearerToken(authorization);
|
|
521
|
+
if (!key) return null;
|
|
522
|
+
const row = await getDb().get(
|
|
523
|
+
"SELECT nest_id FROM api_keys WHERE key_hash = ?",
|
|
524
|
+
[hashApiKey(key)]
|
|
525
|
+
);
|
|
526
|
+
return row?.nest_id ?? null;
|
|
527
|
+
}
|
|
528
|
+
var nestScopedKeyGuard = createMiddleware2(async (c, next) => {
|
|
529
|
+
const scope = await bearerKeyScope(c.req.header("Authorization"));
|
|
530
|
+
if (!scope) return next();
|
|
531
|
+
const verdict = nestKeyVerdict(c.req.method, c.req.path, scope);
|
|
532
|
+
if (verdict.allowed) return next();
|
|
533
|
+
const sessionId = getSessionIdFromRequest(c);
|
|
534
|
+
if (sessionId && await resolveSession(sessionId)) return next();
|
|
535
|
+
if (verdict.reason === "other_nest") {
|
|
536
|
+
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.`;
|
|
537
|
+
if (verdict.mcpTransport) {
|
|
538
|
+
let id = null;
|
|
539
|
+
try {
|
|
540
|
+
id = (await c.req.json())?.id ?? null;
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return c.json(
|
|
544
|
+
{ jsonrpc: "2.0", id, error: { code: -32e3, message: error } },
|
|
545
|
+
403
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return c.json({ error, code: NEST_SCOPED_KEY_CODE }, 403);
|
|
549
|
+
}
|
|
550
|
+
return c.json(
|
|
551
|
+
{
|
|
552
|
+
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.",
|
|
553
|
+
code: NEST_SCOPED_KEY_CODE
|
|
554
|
+
},
|
|
555
|
+
403
|
|
556
|
+
);
|
|
447
557
|
});
|
|
448
558
|
|
|
449
559
|
// src/shared/rate-limit.ts
|
|
@@ -1283,7 +1393,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1283
1393
|
if (config.MANUAL_SIGN_IN !== "open") {
|
|
1284
1394
|
return c.json(
|
|
1285
1395
|
{
|
|
1286
|
-
error: "Self-registration is disabled on this server. If you were invited,
|
|
1396
|
+
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."
|
|
1287
1397
|
},
|
|
1288
1398
|
403
|
|
1289
1399
|
);
|
|
@@ -1310,7 +1420,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1310
1420
|
if (existing.is_invited === 1) {
|
|
1311
1421
|
return c.json(
|
|
1312
1422
|
{
|
|
1313
|
-
error: "You've been invited to this server.
|
|
1423
|
+
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."
|
|
1314
1424
|
},
|
|
1315
1425
|
409
|
|
1316
1426
|
);
|
|
@@ -1407,10 +1517,36 @@ authRoutes.post("/logout", async (c) => {
|
|
|
1407
1517
|
);
|
|
1408
1518
|
return c.json({ ok: true });
|
|
1409
1519
|
});
|
|
1520
|
+
function refuseNestScopedCaller(c) {
|
|
1521
|
+
if (!c.get("nestScope")) return null;
|
|
1522
|
+
return c.json(
|
|
1523
|
+
{
|
|
1524
|
+
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.",
|
|
1525
|
+
code: NEST_SCOPED_KEY_CODE
|
|
1526
|
+
},
|
|
1527
|
+
403
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
async function checkKeyNest(userId, nestId) {
|
|
1531
|
+
if (typeof nestId !== "string" || !nestId.trim()) {
|
|
1532
|
+
return { error: "nest_id must be a nest id.", status: 400 };
|
|
1533
|
+
}
|
|
1534
|
+
const row = await getDb().get("SELECT id FROM nests WHERE id = ?", [nestId]);
|
|
1535
|
+
const reachable = !!row && (config.AUTH_MODE === "open" || await resolveNestPermission(nestId, userId) !== "none");
|
|
1536
|
+
if (!reachable) {
|
|
1537
|
+
return {
|
|
1538
|
+
error: "Nest not found \u2014 you can only scope a key to a nest you have access to.",
|
|
1539
|
+
status: 404
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1410
1544
|
authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
1411
1545
|
const body = await c.req.json().catch(
|
|
1412
1546
|
() => ({})
|
|
1413
1547
|
);
|
|
1548
|
+
const refused = refuseNestScopedCaller(c);
|
|
1549
|
+
if (refused) return refused;
|
|
1414
1550
|
const db = getDb();
|
|
1415
1551
|
const userId = c.get("userId");
|
|
1416
1552
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1418,6 +1554,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1418
1554
|
label: body.label
|
|
1419
1555
|
});
|
|
1420
1556
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1557
|
+
if (body.nest_id) {
|
|
1558
|
+
const bad = await checkKeyNest(userId, body.nest_id);
|
|
1559
|
+
if (bad) return c.json({ error: bad.error }, bad.status);
|
|
1560
|
+
}
|
|
1421
1561
|
const apiKey = generateApiKey();
|
|
1422
1562
|
const keyId = uuid4();
|
|
1423
1563
|
await db.run(
|
|
@@ -1432,12 +1572,20 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1432
1572
|
]
|
|
1433
1573
|
);
|
|
1434
1574
|
return c.json(
|
|
1435
|
-
{
|
|
1575
|
+
{
|
|
1576
|
+
id: keyId,
|
|
1577
|
+
api_key: apiKey,
|
|
1578
|
+
key_prefix: getKeyPrefix(apiKey),
|
|
1579
|
+
label: check.label,
|
|
1580
|
+
nest_id: body.nest_id || null
|
|
1581
|
+
},
|
|
1436
1582
|
201
|
|
1437
1583
|
);
|
|
1438
1584
|
});
|
|
1439
1585
|
authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
1440
1586
|
const body = await c.req.json().catch(() => ({}));
|
|
1587
|
+
const refused = refuseNestScopedCaller(c);
|
|
1588
|
+
if (refused) return refused;
|
|
1441
1589
|
const db = getDb();
|
|
1442
1590
|
const userId = c.get("userId");
|
|
1443
1591
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1447,6 +1595,10 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1447
1595
|
});
|
|
1448
1596
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1449
1597
|
const prior = check.key;
|
|
1598
|
+
if ("nest_id" in body && body.nest_id) {
|
|
1599
|
+
const bad = await checkKeyNest(userId, body.nest_id);
|
|
1600
|
+
if (bad) return c.json({ error: bad.error }, bad.status);
|
|
1601
|
+
}
|
|
1450
1602
|
const apiKey = generateApiKey();
|
|
1451
1603
|
const keyId = uuid4();
|
|
1452
1604
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
@@ -1479,10 +1631,13 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1479
1631
|
id: keyId,
|
|
1480
1632
|
api_key: apiKey,
|
|
1481
1633
|
key_prefix: getKeyPrefix(apiKey),
|
|
1482
|
-
label: check.label ?? prior.label
|
|
1634
|
+
label: check.label ?? prior.label,
|
|
1635
|
+
nest_id: nestScope
|
|
1483
1636
|
});
|
|
1484
1637
|
});
|
|
1485
1638
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1639
|
+
const refused = refuseNestScopedCaller(c);
|
|
1640
|
+
if (refused) return refused;
|
|
1486
1641
|
const db = getDb();
|
|
1487
1642
|
const userId = c.get("userId");
|
|
1488
1643
|
const check = await verifyKeyRequest(db, userId, { action: "fetch" });
|
|
@@ -1494,6 +1649,8 @@ authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
|
1494
1649
|
return c.json({ keys });
|
|
1495
1650
|
});
|
|
1496
1651
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1652
|
+
const refused = refuseNestScopedCaller(c);
|
|
1653
|
+
if (refused) return refused;
|
|
1497
1654
|
const db = getDb();
|
|
1498
1655
|
const userId = c.get("userId");
|
|
1499
1656
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1877,47 +2034,30 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1877
2034
|
"SELECT id, email, is_invited FROM users WHERE LOWER(email) = ?",
|
|
1878
2035
|
[email]
|
|
1879
2036
|
);
|
|
1880
|
-
if (existing
|
|
2037
|
+
if (existing) {
|
|
1881
2038
|
return c.json(
|
|
1882
2039
|
{
|
|
1883
|
-
error:
|
|
2040
|
+
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.`
|
|
1884
2041
|
},
|
|
1885
2042
|
409
|
|
1886
2043
|
);
|
|
1887
2044
|
}
|
|
1888
|
-
const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
|
|
1889
|
-
const passwordHash = await hashPassword(tempPassword);
|
|
1890
|
-
let userId;
|
|
1891
|
-
if (existing) {
|
|
1892
|
-
userId = existing.id;
|
|
1893
|
-
await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
1894
|
-
passwordHash,
|
|
1895
|
-
userId
|
|
1896
|
-
]);
|
|
1897
|
-
} else {
|
|
1898
|
-
userId = uuid4();
|
|
1899
|
-
await db.run(
|
|
1900
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1901
|
-
[userId, email, null, passwordHash]
|
|
1902
|
-
);
|
|
1903
|
-
}
|
|
1904
|
-
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1905
2045
|
const inviter = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
1906
2046
|
callerId
|
|
1907
2047
|
]);
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
path: "",
|
|
1912
|
-
actor: email,
|
|
1913
|
-
by: inviter?.email || void 0,
|
|
1914
|
-
tempPassword,
|
|
1915
|
-
link: requestBaseUrl(c.req.url)
|
|
2048
|
+
const { id: userId, tempPassword } = await createInvitedUser(email, {
|
|
2049
|
+
invitedBy: inviter?.email,
|
|
2050
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
1916
2051
|
});
|
|
2052
|
+
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1917
2053
|
return c.json(
|
|
1918
2054
|
{
|
|
1919
2055
|
temporary_password: tempPassword,
|
|
1920
2056
|
user: { id: userId, email },
|
|
2057
|
+
// PromptOwl sign-in only counts as an option for them when it's open to all.
|
|
2058
|
+
promptowl_sign_in: config.PROMPTOWL_SIGN_IN_GATE === "open",
|
|
2059
|
+
// Email is configured, so the invite email (with this password) went out.
|
|
2060
|
+
emailed: isEmailConfigured(),
|
|
1921
2061
|
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."
|
|
1922
2062
|
},
|
|
1923
2063
|
201
|
|
@@ -1945,7 +2085,12 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
1945
2085
|
const teammates = await db.all(
|
|
1946
2086
|
`SELECT u.id, u.email, u.name, u.is_invited,
|
|
1947
2087
|
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
|
|
1948
|
-
|
|
2088
|
+
-- Latest of API-key use and web-session activity (same text format).
|
|
2089
|
+
(SELECT MAX(t) FROM (
|
|
2090
|
+
SELECT last_used_at AS t FROM api_keys WHERE user_id = u.id
|
|
2091
|
+
UNION ALL
|
|
2092
|
+
SELECT last_seen_at FROM sessions WHERE user_id = u.id
|
|
2093
|
+
) activity) as last_active
|
|
1949
2094
|
FROM users u
|
|
1950
2095
|
WHERE u.id != ?
|
|
1951
2096
|
ORDER BY u.created_at DESC`,
|
|
@@ -1986,7 +2131,9 @@ import {
|
|
|
1986
2131
|
DocumentNotFoundError,
|
|
1987
2132
|
NODE_TYPES,
|
|
1988
2133
|
TAG_PATTERN,
|
|
1989
|
-
generateIndexMd
|
|
2134
|
+
generateIndexMd,
|
|
2135
|
+
readPdfBinary,
|
|
2136
|
+
pdfSidecarPath
|
|
1990
2137
|
} from "@promptowl/contextnest-engine";
|
|
1991
2138
|
import { rename, mkdir, rm, stat } from "fs/promises";
|
|
1992
2139
|
import { dirname, join } from "path";
|
|
@@ -2566,8 +2713,20 @@ var ACCEPTED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2566
2713
|
...NODE_TYPES,
|
|
2567
2714
|
...RUNNABLE_NODE_TYPES,
|
|
2568
2715
|
"artifact",
|
|
2569
|
-
"table"
|
|
2716
|
+
"table",
|
|
2717
|
+
"pdf"
|
|
2570
2718
|
]);
|
|
2719
|
+
var PDF_NODE_TYPE = "pdf";
|
|
2720
|
+
var PDF_UPLOAD_HINT = "upload the file to POST /nests/:nestId/nodes/pdf (the Upload PDF action)";
|
|
2721
|
+
function pdfEngineError(err) {
|
|
2722
|
+
const code = err?.code;
|
|
2723
|
+
const message = err?.message || "The PDF could not be imported.";
|
|
2724
|
+
if (code === "VALIDATION_FAILED" || code === "INVALID_DOCUMENT_ID") {
|
|
2725
|
+
return new ValidationError(message);
|
|
2726
|
+
}
|
|
2727
|
+
if (code === "DOCUMENT_ALREADY_EXISTS") return new ConflictError(message);
|
|
2728
|
+
return err;
|
|
2729
|
+
}
|
|
2571
2730
|
function assertAcceptedType(type) {
|
|
2572
2731
|
if (type === "artifact" && !config.TYPE_ARTIFACT_ENABLED) {
|
|
2573
2732
|
throw new ValidationError("Artifact nodes are disabled on this server");
|
|
@@ -2581,6 +2740,13 @@ function assertAcceptedType(type) {
|
|
|
2581
2740
|
);
|
|
2582
2741
|
}
|
|
2583
2742
|
}
|
|
2743
|
+
function assertUpdatableType(storedType) {
|
|
2744
|
+
const type = storedType || "document";
|
|
2745
|
+
if (ACCEPTED_NODE_TYPES.has(type)) return;
|
|
2746
|
+
throw new ValidationError(
|
|
2747
|
+
`node has legacy type "${type}" which this server no longer accepts; include "type": "<accepted type>" in the same PATCH to migrate it (allowed: ${[...ACCEPTED_NODE_TYPES].sort().join(", ")})`
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2584
2750
|
function toNodeResponse(node) {
|
|
2585
2751
|
const fm = node.frontmatter;
|
|
2586
2752
|
const title = fm.title === void 0 || fm.title === null ? "" : String(fm.title);
|
|
@@ -2605,7 +2771,8 @@ function toNodeResponse(node) {
|
|
|
2605
2771
|
// Same YAML-coercion rule as title above: `schedule: 123` parses as a
|
|
2606
2772
|
// number — normalize to string at the API boundary.
|
|
2607
2773
|
schedule: fm.metadata?.schedule != null ? String(fm.metadata.schedule) : void 0,
|
|
2608
|
-
prime: documentPrimeFlag(fm.metadata)
|
|
2774
|
+
prime: documentPrimeFlag(fm.metadata),
|
|
2775
|
+
...fm.type === "pdf" && fm.pdf ? { pdf: fm.pdf } : {}
|
|
2609
2776
|
};
|
|
2610
2777
|
}
|
|
2611
2778
|
async function resolveAuthorship(nestId, nodes) {
|
|
@@ -2881,7 +3048,7 @@ async function vaultFolderPaths(nestId, opts = {}) {
|
|
|
2881
3048
|
});
|
|
2882
3049
|
return entries.map(
|
|
2883
3050
|
(e) => prefix && e.path.startsWith(`${prefix}/`) ? e.path.slice(prefix.length + 1) : e.path
|
|
2884
|
-
).filter(
|
|
3051
|
+
).filter((p) => p && !(!prefix && (p === "assets" || p.startsWith("assets/"))));
|
|
2885
3052
|
}
|
|
2886
3053
|
async function listNestFolders(nestId, opts = {}) {
|
|
2887
3054
|
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
@@ -2998,9 +3165,11 @@ async function assertMayFlagPrime(nestId, userEmail) {
|
|
|
2998
3165
|
);
|
|
2999
3166
|
}
|
|
3000
3167
|
}
|
|
3001
|
-
async function autoPublishReason(nestId, userEmail, doc) {
|
|
3002
|
-
|
|
3168
|
+
async function autoPublishReason(nestId, userEmail, doc, opts = {}) {
|
|
3169
|
+
const governed = opts.governed ?? await isStewardshipEnabled(nestId);
|
|
3170
|
+
if (!governed) return "no stewards configured";
|
|
3003
3171
|
if (await isPrimeDocument(nestId, doc.tags, doc.metadata)) return null;
|
|
3172
|
+
if (opts.creatorReviews) return "creator is the document's reviewer";
|
|
3004
3173
|
if (await nestReviewsPrimeOnly(nestId)) {
|
|
3005
3174
|
return "prime-only review, document is not prime";
|
|
3006
3175
|
}
|
|
@@ -3030,8 +3199,14 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3030
3199
|
id = `${prefix}${slug || await generatedSlug(storage, prefix)}`;
|
|
3031
3200
|
}
|
|
3032
3201
|
const tags = requireTags(input.tags);
|
|
3033
|
-
const requestedType = input.type || "document";
|
|
3202
|
+
const requestedType = input.pdf ? input.type || PDF_NODE_TYPE : input.type || "document";
|
|
3034
3203
|
assertAcceptedType(requestedType);
|
|
3204
|
+
if (requestedType === PDF_NODE_TYPE && !input.pdf) {
|
|
3205
|
+
throw new ValidationError(`PDF documents can't be created from text \u2014 ${PDF_UPLOAD_HINT}.`);
|
|
3206
|
+
}
|
|
3207
|
+
if (input.pdf && requestedType !== PDF_NODE_TYPE) {
|
|
3208
|
+
throw new ValidationError(`a PDF upload creates a "pdf" node, not "${requestedType}"`);
|
|
3209
|
+
}
|
|
3035
3210
|
if (input.schedule !== void 0 && !isRunnableType(input.type)) {
|
|
3036
3211
|
throw new ValidationError(
|
|
3037
3212
|
`schedule is only valid for runnable node types (${RUNNABLE_NODE_TYPES.join(
|
|
@@ -3076,42 +3251,85 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3076
3251
|
...input.schedule ? { schedule: input.schedule } : {},
|
|
3077
3252
|
...input.prime === void 0 ? {} : { prime: input.prime }
|
|
3078
3253
|
};
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
let published = publishReason !== null;
|
|
3254
|
+
const governed = await isStewardshipEnabled(nestId);
|
|
3255
|
+
const creatorReviews = opts.creatorReviews !== false && governed && await nestCreatorIsReviewer(nestId);
|
|
3256
|
+
const creatorSteward = creatorReviews ? await stewardDocumentCreator(nestId, id, userEmail) : null;
|
|
3257
|
+
let published = false;
|
|
3258
|
+
let publishNote;
|
|
3085
3259
|
try {
|
|
3086
|
-
await
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
content: input.content,
|
|
3092
|
-
type: requestedType,
|
|
3093
|
-
// A skill node is invalid without a skill block, and this surface has
|
|
3094
|
-
// no field for one — default the trigger from the title, same as the
|
|
3095
|
-
// CLI and mcp-server do.
|
|
3096
|
-
...requestedType === "skill" ? { trigger: `when asked to ${input.title.toLowerCase()}` } : {},
|
|
3097
|
-
...tags.length ? { tags } : {},
|
|
3098
|
-
...input.status ? { status: input.status } : {},
|
|
3099
|
-
metadata,
|
|
3100
|
-
publish: published,
|
|
3101
|
-
...publishNote ? { note: publishNote } : {}
|
|
3102
|
-
},
|
|
3103
|
-
await opContext(nestId, userEmail)
|
|
3260
|
+
const publishReason = await autoPublishReason(
|
|
3261
|
+
nestId,
|
|
3262
|
+
userEmail,
|
|
3263
|
+
{ tags, metadata },
|
|
3264
|
+
{ creatorReviews: creatorSteward?.canApprove === true, governed }
|
|
3104
3265
|
);
|
|
3105
|
-
|
|
3106
|
-
|
|
3266
|
+
publishNote = publishReason === null ? void 0 : `Auto-published on create (${publishReason})`;
|
|
3267
|
+
published = publishReason !== null;
|
|
3107
3268
|
try {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3269
|
+
if (input.pdf) {
|
|
3270
|
+
try {
|
|
3271
|
+
await engineApi.run(
|
|
3272
|
+
"context_import_pdf",
|
|
3273
|
+
{
|
|
3274
|
+
id,
|
|
3275
|
+
title: input.title,
|
|
3276
|
+
bytes_base64: Buffer.from(input.pdf.bytes).toString("base64"),
|
|
3277
|
+
...input.pdf.filename ? { filename: input.pdf.filename } : {},
|
|
3278
|
+
...tags.length ? { tags } : {},
|
|
3279
|
+
publish: published,
|
|
3280
|
+
...publishNote ? { note: publishNote } : {},
|
|
3281
|
+
...input.client ? { client: input.client } : {}
|
|
3282
|
+
},
|
|
3283
|
+
await opContext(nestId, userEmail)
|
|
3284
|
+
);
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
throw pdfEngineError(err);
|
|
3287
|
+
}
|
|
3288
|
+
} else {
|
|
3289
|
+
await engineApi.run(
|
|
3290
|
+
"context_create",
|
|
3291
|
+
{
|
|
3292
|
+
id,
|
|
3293
|
+
title: input.title,
|
|
3294
|
+
content: input.content,
|
|
3295
|
+
type: requestedType,
|
|
3296
|
+
// A skill node is invalid without a skill block, and this surface
|
|
3297
|
+
// has no field for one — default the trigger from the title, same
|
|
3298
|
+
// as the CLI and mcp-server do.
|
|
3299
|
+
...requestedType === "skill" ? { trigger: `when asked to ${input.title.toLowerCase()}` } : {},
|
|
3300
|
+
...tags.length ? { tags } : {},
|
|
3301
|
+
...input.status ? { status: input.status } : {},
|
|
3302
|
+
metadata,
|
|
3303
|
+
publish: published,
|
|
3304
|
+
...publishNote ? { note: publishNote } : {}
|
|
3305
|
+
},
|
|
3306
|
+
await opContext(nestId, userEmail)
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
} catch (err) {
|
|
3310
|
+
let onDisk = true;
|
|
3311
|
+
try {
|
|
3312
|
+
await storage.readDocument(id);
|
|
3313
|
+
} catch {
|
|
3314
|
+
onDisk = false;
|
|
3315
|
+
}
|
|
3316
|
+
if (!onDisk) throw err;
|
|
3317
|
+
console.error("publishDocument failed (node create auto-publish)", err);
|
|
3318
|
+
published = false;
|
|
3319
|
+
}
|
|
3320
|
+
} catch (err) {
|
|
3321
|
+
if (creatorSteward?.insertedId) {
|
|
3322
|
+
try {
|
|
3323
|
+
await removeSteward(creatorSteward.insertedId);
|
|
3324
|
+
} catch (cleanupErr) {
|
|
3325
|
+
console.error(
|
|
3326
|
+
"removeSteward failed while compensating a failed create",
|
|
3327
|
+
id,
|
|
3328
|
+
cleanupErr
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3111
3331
|
}
|
|
3112
|
-
|
|
3113
|
-
console.error("publishDocument failed (node create auto-publish)", err);
|
|
3114
|
-
published = false;
|
|
3332
|
+
throw err;
|
|
3115
3333
|
}
|
|
3116
3334
|
const node = await storage.readDocument(id);
|
|
3117
3335
|
await syncNodeTags(nestId, id, tags);
|
|
@@ -3126,7 +3344,8 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3126
3344
|
author: userEmail,
|
|
3127
3345
|
status: "published",
|
|
3128
3346
|
tags,
|
|
3129
|
-
changeNote: publishNote
|
|
3347
|
+
changeNote: publishNote,
|
|
3348
|
+
client: input.client
|
|
3130
3349
|
});
|
|
3131
3350
|
await setApprovedVersion(nestId, id, savedVersion, userEmail);
|
|
3132
3351
|
} else {
|
|
@@ -3134,10 +3353,12 @@ async function createNodeOnce(nestId, input, userEmail, opts = {}) {
|
|
|
3134
3353
|
nestId,
|
|
3135
3354
|
nodeId: id,
|
|
3136
3355
|
version: 1,
|
|
3137
|
-
|
|
3356
|
+
// A pdf node's body is what the engine extracted, not anything sent.
|
|
3357
|
+
content: input.pdf ? node.body || "" : input.content,
|
|
3138
3358
|
author: userEmail,
|
|
3139
3359
|
status: "draft",
|
|
3140
|
-
tags
|
|
3360
|
+
tags,
|
|
3361
|
+
client: input.client
|
|
3141
3362
|
});
|
|
3142
3363
|
}
|
|
3143
3364
|
await trackEvent("node.create", { nestId, nodeId: id });
|
|
@@ -3191,7 +3412,7 @@ async function alignOnDiskStatus(nestId, documentId, status) {
|
|
|
3191
3412
|
);
|
|
3192
3413
|
}
|
|
3193
3414
|
}
|
|
3194
|
-
async function registerImportedDocuments(nestId, userEmail) {
|
|
3415
|
+
async function registerImportedDocuments(nestId, userEmail, client) {
|
|
3195
3416
|
const startedAt = Date.now();
|
|
3196
3417
|
const tracked = await getTrackedNodeIds(nestId);
|
|
3197
3418
|
(await engineCache.get(nestId)).invalidateDiscovery();
|
|
@@ -3221,7 +3442,8 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
3221
3442
|
author: userEmail,
|
|
3222
3443
|
status: d.status,
|
|
3223
3444
|
changeNote: d.status === "published" ? "Imported from existing folder" : "Imported from existing folder \u2014 could not be published, kept as a draft",
|
|
3224
|
-
tags
|
|
3445
|
+
tags,
|
|
3446
|
+
client
|
|
3225
3447
|
});
|
|
3226
3448
|
if (d.status === "published") {
|
|
3227
3449
|
await setApprovedVersion(nestId, d.id, d.version, userEmail);
|
|
@@ -3243,6 +3465,44 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
3243
3465
|
await trackEvent("nest.import.documents", { nestId, registered: docs.length });
|
|
3244
3466
|
return docs.length;
|
|
3245
3467
|
}
|
|
3468
|
+
async function stagePdfImport(nestId, nodeId, pdf, userEmail, client, priorRaw) {
|
|
3469
|
+
const { storage } = await engineCache.get(nestId);
|
|
3470
|
+
const sidecar = pdfSidecarPath(nodeId);
|
|
3471
|
+
let priorSidecar = null;
|
|
3472
|
+
try {
|
|
3473
|
+
priorSidecar = await storage.readVaultBinary(sidecar);
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
const sealedBefore = await storage.maxRecordedVersion(nodeId);
|
|
3477
|
+
const rollback = async () => {
|
|
3478
|
+
if (await storage.maxRecordedVersion(nodeId).catch(() => sealedBefore) > sealedBefore) {
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
try {
|
|
3482
|
+
await storage.writeDocument(nodeId, priorRaw);
|
|
3483
|
+
if (priorSidecar) await storage.writeVaultBinary(sidecar, priorSidecar);
|
|
3484
|
+
await storage.regenerateIndex();
|
|
3485
|
+
} catch (err) {
|
|
3486
|
+
console.error("[pdf] could not roll back a failed PDF replace", nestId, nodeId, err);
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
try {
|
|
3490
|
+
const result = await engineApi.run(
|
|
3491
|
+
"context_import_pdf",
|
|
3492
|
+
{
|
|
3493
|
+
id: nodeId,
|
|
3494
|
+
bytes_base64: Buffer.from(pdf.bytes).toString("base64"),
|
|
3495
|
+
...pdf.filename ? { filename: pdf.filename } : {},
|
|
3496
|
+
publish: false,
|
|
3497
|
+
...client ? { client } : {}
|
|
3498
|
+
},
|
|
3499
|
+
await opContext(nestId, userEmail)
|
|
3500
|
+
);
|
|
3501
|
+
return { unchanged: result.unchanged === true, rollback };
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
throw pdfEngineError(err);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3246
3506
|
async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
3247
3507
|
return withNodeWriteLock(nodeWriteKey(nestId, nodeId), async () => {
|
|
3248
3508
|
try {
|
|
@@ -3255,6 +3515,35 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3255
3515
|
} catch {
|
|
3256
3516
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
3257
3517
|
}
|
|
3518
|
+
const priorRaw = node.rawContent;
|
|
3519
|
+
let pdfRollback = null;
|
|
3520
|
+
if (patch.type === void 0 || patch.type === node.frontmatter.type) {
|
|
3521
|
+
assertUpdatableType(node.frontmatter.type);
|
|
3522
|
+
}
|
|
3523
|
+
const storedType = node.frontmatter.type || "document";
|
|
3524
|
+
const isPdfNode = storedType === PDF_NODE_TYPE;
|
|
3525
|
+
if (patch.pdf && !isPdfNode) {
|
|
3526
|
+
throw new ValidationError(
|
|
3527
|
+
`${nodeId} is a "${storedType}" node, not a PDF \u2014 a PDF can only replace the PDF of a pdf node.`
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
if (patch.type !== void 0 && patch.type !== storedType) {
|
|
3531
|
+
if (isPdfNode || patch.type === PDF_NODE_TYPE) {
|
|
3532
|
+
throw new ValidationError(
|
|
3533
|
+
isPdfNode ? "A PDF document can't be converted to another type \u2014 its body is the text of its PDF." : `A document can't be converted to a PDF \u2014 ${PDF_UPLOAD_HINT}.`
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
if (isPdfNode) {
|
|
3538
|
+
const readOnly = "This is a PDF document: its text is extracted from the PDF and can't be edited directly. Upload a new version of the PDF (Replace PDF) to change it.";
|
|
3539
|
+
if (patch.append) throw new ConflictError(readOnly);
|
|
3540
|
+
if (patch.content !== void 0) {
|
|
3541
|
+
if (patch.content.trim() !== (node.body || "").trim()) {
|
|
3542
|
+
throw new ConflictError(readOnly);
|
|
3543
|
+
}
|
|
3544
|
+
patch = { ...patch, content: void 0 };
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3258
3547
|
const previousBody = node.body || "";
|
|
3259
3548
|
const opInput = {};
|
|
3260
3549
|
if (patch.content !== void 0) {
|
|
@@ -3350,11 +3639,30 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3350
3639
|
if (publishReason === null || isAuthorEditingOwnReview) {
|
|
3351
3640
|
const currentVersion = await getCurrentVersion(nestId, nodeId);
|
|
3352
3641
|
const newVersion = await versionManager.nextVersion(nodeId, currentVersion);
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3642
|
+
if (patch.pdf) {
|
|
3643
|
+
const staged = await stagePdfImport(
|
|
3644
|
+
nestId,
|
|
3645
|
+
nodeId,
|
|
3646
|
+
patch.pdf,
|
|
3647
|
+
userEmail,
|
|
3648
|
+
patch.client,
|
|
3649
|
+
priorRaw
|
|
3650
|
+
);
|
|
3651
|
+
if (staged.unchanged && Object.keys(opInput).length === 0) {
|
|
3652
|
+
return { node, version: node.frontmatter.version || currentVersion || 1 };
|
|
3653
|
+
}
|
|
3654
|
+
if (!staged.unchanged) pdfRollback = staged.rollback;
|
|
3655
|
+
}
|
|
3656
|
+
try {
|
|
3657
|
+
await engineApi.run(
|
|
3658
|
+
"context_update",
|
|
3659
|
+
{ id: nodeId, ...opInput, publish: false, version: newVersion },
|
|
3660
|
+
await opContext(nestId, userEmail)
|
|
3661
|
+
);
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
if (pdfRollback) await pdfRollback();
|
|
3664
|
+
throw err;
|
|
3665
|
+
}
|
|
3358
3666
|
node = await storage.readDocument(nodeId);
|
|
3359
3667
|
await syncNodeTags(nestId, nodeId, currentTags);
|
|
3360
3668
|
await deleteSupersededDrafts(nestId, nodeId, newVersion);
|
|
@@ -3366,7 +3674,8 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3366
3674
|
author: userEmail,
|
|
3367
3675
|
status: "draft",
|
|
3368
3676
|
tags: currentTags,
|
|
3369
|
-
changeNote: patch.changeNote
|
|
3677
|
+
changeNote: patch.changeNote,
|
|
3678
|
+
client: patch.client
|
|
3370
3679
|
});
|
|
3371
3680
|
if (isAuthorEditingOwnReview) {
|
|
3372
3681
|
await repointPendingReview({ nestId, nodeId, newVersion });
|
|
@@ -3375,6 +3684,20 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3375
3684
|
} else {
|
|
3376
3685
|
const publishNote = patch.changeNote || `Auto-published on edit (${publishReason})`;
|
|
3377
3686
|
let publishedVersion;
|
|
3687
|
+
if (patch.pdf) {
|
|
3688
|
+
const staged = await stagePdfImport(
|
|
3689
|
+
nestId,
|
|
3690
|
+
nodeId,
|
|
3691
|
+
patch.pdf,
|
|
3692
|
+
userEmail,
|
|
3693
|
+
patch.client,
|
|
3694
|
+
priorRaw
|
|
3695
|
+
);
|
|
3696
|
+
if (staged.unchanged && Object.keys(opInput).length === 0) {
|
|
3697
|
+
return { node, version: node.frontmatter.version || 1 };
|
|
3698
|
+
}
|
|
3699
|
+
if (!staged.unchanged) pdfRollback = staged.rollback;
|
|
3700
|
+
}
|
|
3378
3701
|
try {
|
|
3379
3702
|
const result = await engineApi.run(
|
|
3380
3703
|
"context_update",
|
|
@@ -3387,6 +3710,7 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3387
3710
|
"publishDocument failed (node patch auto-publish)",
|
|
3388
3711
|
err
|
|
3389
3712
|
);
|
|
3713
|
+
if (pdfRollback) await pdfRollback();
|
|
3390
3714
|
throw err;
|
|
3391
3715
|
}
|
|
3392
3716
|
node = await storage.readDocument(nodeId);
|
|
@@ -3399,7 +3723,8 @@ async function updateNode(nestId, nodeId, patch, userEmail, opts = {}) {
|
|
|
3399
3723
|
author: userEmail,
|
|
3400
3724
|
status: "published",
|
|
3401
3725
|
tags: currentTags,
|
|
3402
|
-
changeNote: publishNote
|
|
3726
|
+
changeNote: publishNote,
|
|
3727
|
+
client: patch.client
|
|
3403
3728
|
});
|
|
3404
3729
|
await setApprovedVersion(
|
|
3405
3730
|
nestId,
|
|
@@ -3505,6 +3830,11 @@ async function moveNode(nestId, nodeId, folder, userEmail) {
|
|
|
3505
3830
|
} catch {
|
|
3506
3831
|
throw new NotFoundError(`Node not found: ${nodeId}`);
|
|
3507
3832
|
}
|
|
3833
|
+
if (node.frontmatter.type === PDF_NODE_TYPE) {
|
|
3834
|
+
throw new ValidationError(
|
|
3835
|
+
"PDF documents can't be moved to another folder yet. Upload the PDF again in the destination folder instead."
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3508
3838
|
await assertNotReviewLocked(nestId, nodeId, userEmail, "moving");
|
|
3509
3839
|
const leaf = nodeId.split("/").pop();
|
|
3510
3840
|
const segments = folderToSegments(folder);
|
|
@@ -3601,6 +3931,16 @@ async function discardDraft(nestId, nodeId, userEmail) {
|
|
|
3601
3931
|
await storage.writeDocument(nodeId, raw);
|
|
3602
3932
|
await storage.regenerateIndex();
|
|
3603
3933
|
const restored = parseDocument(`${nodeId}.md`, raw, nodeId);
|
|
3934
|
+
let warning;
|
|
3935
|
+
if (restored.frontmatter.type === PDF_NODE_TYPE) {
|
|
3936
|
+
try {
|
|
3937
|
+
const bytes = await readPdfBinary(storage, nodeId);
|
|
3938
|
+
await storage.writeVaultBinary(pdfSidecarPath(nodeId), bytes);
|
|
3939
|
+
} catch (err) {
|
|
3940
|
+
console.error("[pdf] could not restore the sealed binary on discard", nestId, nodeId, err);
|
|
3941
|
+
warning = `The draft was discarded, but the PDF of version ${sealed} could not be restored (its archived copy is missing or no longer matches). Upload the PDF again, or ask an admin to verify the nest.`;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3604
3944
|
await syncNodeTags(
|
|
3605
3945
|
nestId,
|
|
3606
3946
|
nodeId,
|
|
@@ -3608,7 +3948,7 @@ async function discardDraft(nestId, nodeId, userEmail) {
|
|
|
3608
3948
|
);
|
|
3609
3949
|
await deleteUnsealedVersions(nestId, nodeId, sealed);
|
|
3610
3950
|
await trackEvent("node.discard_draft", { nestId, nodeId, version: sealed });
|
|
3611
|
-
return { deleted: false, version: sealed };
|
|
3951
|
+
return { deleted: false, version: sealed, ...warning ? { warning } : {} };
|
|
3612
3952
|
});
|
|
3613
3953
|
}
|
|
3614
3954
|
async function deleteNode(nestId, nodeId, userEmail) {
|
|
@@ -3686,6 +4026,7 @@ async function vaultFolderDir(nestId, rel) {
|
|
|
3686
4026
|
}
|
|
3687
4027
|
async function vaultFolderExists(nestId, rel) {
|
|
3688
4028
|
if (!rel) return false;
|
|
4029
|
+
assertSafeFolder(rel);
|
|
3689
4030
|
try {
|
|
3690
4031
|
return (await stat(await vaultFolderDir(nestId, rel))).isDirectory();
|
|
3691
4032
|
} catch {
|
|
@@ -3835,7 +4176,7 @@ function collectMarkdownFiles(dir, root) {
|
|
|
3835
4176
|
}
|
|
3836
4177
|
return out;
|
|
3837
4178
|
}
|
|
3838
|
-
async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
4179
|
+
async function syncUnsyncedFolder(userId, folderName, callerEmail, client) {
|
|
3839
4180
|
console.log(
|
|
3840
4181
|
`[unsynced] sync requested folder="${folderName}" user="${callerEmail}"`
|
|
3841
4182
|
);
|
|
@@ -3844,14 +4185,14 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3844
4185
|
if (isNestStorageRoot(src)) {
|
|
3845
4186
|
throw new ValidationError("Folder is not eligible for sync");
|
|
3846
4187
|
}
|
|
3847
|
-
let
|
|
4188
|
+
let stat3;
|
|
3848
4189
|
try {
|
|
3849
|
-
|
|
4190
|
+
stat3 = statSync(src);
|
|
3850
4191
|
} catch {
|
|
3851
4192
|
console.warn(`[unsynced] source missing: ${src}`);
|
|
3852
4193
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
3853
4194
|
}
|
|
3854
|
-
if (!
|
|
4195
|
+
if (!stat3.isDirectory()) {
|
|
3855
4196
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
3856
4197
|
}
|
|
3857
4198
|
const files = collectMarkdownFiles(src, src);
|
|
@@ -3876,7 +4217,7 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
3876
4217
|
let documents;
|
|
3877
4218
|
try {
|
|
3878
4219
|
await stageImportedFiles(nest.id, files, callerEmail);
|
|
3879
|
-
documents = await registerImportedDocuments(nest.id, callerEmail);
|
|
4220
|
+
documents = await registerImportedDocuments(nest.id, callerEmail, client);
|
|
3880
4221
|
} catch (err) {
|
|
3881
4222
|
await deleteNest(nest.id).catch(() => {
|
|
3882
4223
|
});
|
|
@@ -3900,19 +4241,226 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
3900
4241
|
if (isNestStorageRoot(src)) {
|
|
3901
4242
|
throw new ValidationError("Folder is not eligible for delete");
|
|
3902
4243
|
}
|
|
3903
|
-
let
|
|
4244
|
+
let stat3;
|
|
3904
4245
|
try {
|
|
3905
|
-
|
|
4246
|
+
stat3 = statSync(src);
|
|
3906
4247
|
} catch {
|
|
3907
4248
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
3908
4249
|
}
|
|
3909
|
-
if (!
|
|
4250
|
+
if (!stat3.isDirectory()) {
|
|
3910
4251
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
3911
4252
|
}
|
|
3912
4253
|
await rm2(src, { recursive: true, force: true });
|
|
3913
4254
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
3914
4255
|
}
|
|
3915
4256
|
|
|
4257
|
+
// src/shared/branding.ts
|
|
4258
|
+
import { z } from "zod";
|
|
4259
|
+
var HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
4260
|
+
var IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
4261
|
+
var ASSET_NAME_RE = /^[0-9a-f-]{36}\.(?:png|jpe?g|gif|webp)$/;
|
|
4262
|
+
var BRANDING_LIMITS = {
|
|
4263
|
+
site_title: 120,
|
|
4264
|
+
banner_headline: 120,
|
|
4265
|
+
banner_subtext: 280,
|
|
4266
|
+
footer_text: 280,
|
|
4267
|
+
footer_link_label: 80,
|
|
4268
|
+
url: 2048
|
|
4269
|
+
};
|
|
4270
|
+
var IMAGE_FIELDS = ["logo_url", "banner_url", "favicon_url"];
|
|
4271
|
+
var COLOR_FIELDS = ["accent_color", "header_bg_color", "page_tint_color"];
|
|
4272
|
+
var CONTROL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/;
|
|
4273
|
+
var optText = (max) => z.string().max(max).refine((s) => !CONTROL_RE.test(s), "must not contain control characters").nullable().optional();
|
|
4274
|
+
var optColor = z.string().regex(HEX_RE, "must be a #rgb or #rrggbb hex color").nullable().optional();
|
|
4275
|
+
var optUrl = z.string().max(BRANDING_LIMITS.url).nullable().optional();
|
|
4276
|
+
var BrandingSchema = z.strictObject({
|
|
4277
|
+
logo_url: optUrl,
|
|
4278
|
+
banner_url: optUrl,
|
|
4279
|
+
banner_headline: optText(BRANDING_LIMITS.banner_headline),
|
|
4280
|
+
banner_subtext: optText(BRANDING_LIMITS.banner_subtext),
|
|
4281
|
+
accent_color: optColor,
|
|
4282
|
+
header_bg_color: optColor,
|
|
4283
|
+
page_tint_color: optColor,
|
|
4284
|
+
site_title: optText(BRANDING_LIMITS.site_title),
|
|
4285
|
+
footer_text: optText(BRANDING_LIMITS.footer_text),
|
|
4286
|
+
footer_link_url: optUrl,
|
|
4287
|
+
footer_link_label: optText(BRANDING_LIMITS.footer_link_label),
|
|
4288
|
+
favicon_url: optUrl,
|
|
4289
|
+
hide_powered_by: z.boolean().optional()
|
|
4290
|
+
});
|
|
4291
|
+
function nestAssetName(url, nestId) {
|
|
4292
|
+
const prefix = `/nests/${nestId}/assets/`;
|
|
4293
|
+
if (!url.startsWith(prefix)) return null;
|
|
4294
|
+
const name = url.slice(prefix.length);
|
|
4295
|
+
return ASSET_NAME_RE.test(name) ? name : null;
|
|
4296
|
+
}
|
|
4297
|
+
function isHttpsImageUrl(url) {
|
|
4298
|
+
if (!/^https:\/\//i.test(url)) return false;
|
|
4299
|
+
let u;
|
|
4300
|
+
try {
|
|
4301
|
+
u = new URL(url);
|
|
4302
|
+
} catch {
|
|
4303
|
+
return false;
|
|
4304
|
+
}
|
|
4305
|
+
if (u.protocol !== "https:" || u.username || u.password || !u.hostname) return false;
|
|
4306
|
+
return IMAGE_EXT_RE.test(u.pathname);
|
|
4307
|
+
}
|
|
4308
|
+
function isHttpsLink(url) {
|
|
4309
|
+
if (!/^https:\/\//i.test(url)) return false;
|
|
4310
|
+
try {
|
|
4311
|
+
const u = new URL(url);
|
|
4312
|
+
return u.protocol === "https:" && !u.username && !u.password && !!u.hostname;
|
|
4313
|
+
} catch {
|
|
4314
|
+
return false;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
function normalizeHex(v) {
|
|
4318
|
+
if (typeof v !== "string" || !HEX_RE.test(v)) return null;
|
|
4319
|
+
let h = v.slice(1).toLowerCase();
|
|
4320
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
4321
|
+
return "#" + h;
|
|
4322
|
+
}
|
|
4323
|
+
function compact(b) {
|
|
4324
|
+
const out = {};
|
|
4325
|
+
for (const [k, v] of Object.entries(b)) {
|
|
4326
|
+
if (v === null || v === void 0) continue;
|
|
4327
|
+
if (typeof v === "string") {
|
|
4328
|
+
const t = v.trim();
|
|
4329
|
+
if (t === "") continue;
|
|
4330
|
+
out[k] = t;
|
|
4331
|
+
} else if (v === true) {
|
|
4332
|
+
out[k] = true;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (Object.keys(out).length === 0) return null;
|
|
4336
|
+
return out;
|
|
4337
|
+
}
|
|
4338
|
+
var BrandingValidationError = class extends Error {
|
|
4339
|
+
};
|
|
4340
|
+
async function validateBrandingInput(body, nestId, assetExists) {
|
|
4341
|
+
if (body === null || body === void 0) return null;
|
|
4342
|
+
const parsed = BrandingSchema.safeParse(body);
|
|
4343
|
+
if (!parsed.success) {
|
|
4344
|
+
const issue = parsed.error.issues[0];
|
|
4345
|
+
const field2 = issue?.path?.join(".") || "branding";
|
|
4346
|
+
const msg = issue?.code === "unrecognized_keys" ? `unknown branding field(s): ${issue.keys.join(", ")}` : `${field2}: ${issue?.message ?? "invalid"}`;
|
|
4347
|
+
throw new BrandingValidationError(msg);
|
|
4348
|
+
}
|
|
4349
|
+
const b = compact(parsed.data);
|
|
4350
|
+
if (!b) return null;
|
|
4351
|
+
for (const f of COLOR_FIELDS) {
|
|
4352
|
+
if (b[f]) b[f] = normalizeHex(b[f]);
|
|
4353
|
+
}
|
|
4354
|
+
for (const f of IMAGE_FIELDS) {
|
|
4355
|
+
const url = b[f];
|
|
4356
|
+
if (!url) continue;
|
|
4357
|
+
const asset = nestAssetName(url, nestId);
|
|
4358
|
+
if (asset) {
|
|
4359
|
+
if (!await assetExists(asset)) {
|
|
4360
|
+
throw new BrandingValidationError(`${f}: no such asset in this nest`);
|
|
4361
|
+
}
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
4364
|
+
if (!isHttpsImageUrl(url)) {
|
|
4365
|
+
throw new BrandingValidationError(
|
|
4366
|
+
`${f}: must be an image uploaded to this nest or an https:// URL ending in .png, .jpg, .gif or .webp`
|
|
4367
|
+
);
|
|
4368
|
+
}
|
|
4369
|
+
b[f] = new URL(url).href;
|
|
4370
|
+
}
|
|
4371
|
+
if (b.footer_link_url) {
|
|
4372
|
+
if (!isHttpsLink(b.footer_link_url)) {
|
|
4373
|
+
throw new BrandingValidationError("footer_link_url: must be an https:// URL");
|
|
4374
|
+
}
|
|
4375
|
+
b.footer_link_url = new URL(b.footer_link_url).href;
|
|
4376
|
+
}
|
|
4377
|
+
return b;
|
|
4378
|
+
}
|
|
4379
|
+
function parseStoredBranding(raw, nestId) {
|
|
4380
|
+
if (!raw) return null;
|
|
4381
|
+
let data;
|
|
4382
|
+
try {
|
|
4383
|
+
data = JSON.parse(raw);
|
|
4384
|
+
} catch {
|
|
4385
|
+
return null;
|
|
4386
|
+
}
|
|
4387
|
+
const parsed = BrandingSchema.safeParse(data);
|
|
4388
|
+
if (!parsed.success) return null;
|
|
4389
|
+
const b = compact(parsed.data);
|
|
4390
|
+
if (!b) return null;
|
|
4391
|
+
for (const f of IMAGE_FIELDS) {
|
|
4392
|
+
const url = b[f];
|
|
4393
|
+
if (url && !nestAssetName(url, nestId) && !isHttpsImageUrl(url)) delete b[f];
|
|
4394
|
+
}
|
|
4395
|
+
if (b.footer_link_url && !isHttpsLink(b.footer_link_url)) delete b.footer_link_url;
|
|
4396
|
+
const kept = compact(b);
|
|
4397
|
+
if (!kept) return null;
|
|
4398
|
+
return { ...kept, hide_powered_by: kept.hide_powered_by === true };
|
|
4399
|
+
}
|
|
4400
|
+
function rgb(hex) {
|
|
4401
|
+
const h = normalizeHex(hex).slice(1);
|
|
4402
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
4403
|
+
}
|
|
4404
|
+
function toHex([r, g, b]) {
|
|
4405
|
+
return "#" + [r, g, b].map((n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0")).join("");
|
|
4406
|
+
}
|
|
4407
|
+
function relativeLuminance(hex) {
|
|
4408
|
+
const [r, g, b] = rgb(hex).map((v) => {
|
|
4409
|
+
const c = v / 255;
|
|
4410
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
4411
|
+
});
|
|
4412
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
4413
|
+
}
|
|
4414
|
+
function contrastRatio(a, b) {
|
|
4415
|
+
const la = relativeLuminance(a);
|
|
4416
|
+
const lb = relativeLuminance(b);
|
|
4417
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
4418
|
+
}
|
|
4419
|
+
function readableForeground(bg) {
|
|
4420
|
+
return contrastRatio("#000000", bg) >= contrastRatio("#ffffff", bg) ? "#000000" : "#ffffff";
|
|
4421
|
+
}
|
|
4422
|
+
function mixHex(a, b, weight) {
|
|
4423
|
+
const ca = rgb(a);
|
|
4424
|
+
const cb = rgb(b);
|
|
4425
|
+
return toHex([0, 1, 2].map((i) => ca[i] * weight + cb[i] * (1 - weight)));
|
|
4426
|
+
}
|
|
4427
|
+
function ensureContrast(fg, bg, min = 4.5) {
|
|
4428
|
+
const start = normalizeHex(fg);
|
|
4429
|
+
if (contrastRatio(start, bg) >= min) return start;
|
|
4430
|
+
const target = readableForeground(bg);
|
|
4431
|
+
for (let w = 0.05; w <= 1.0001; w += 0.05) {
|
|
4432
|
+
const c = mixHex(target, start, w);
|
|
4433
|
+
if (contrastRatio(c, bg) >= min) return c;
|
|
4434
|
+
}
|
|
4435
|
+
return target;
|
|
4436
|
+
}
|
|
4437
|
+
function publishBrandCssVars(b) {
|
|
4438
|
+
if (!b) return "";
|
|
4439
|
+
const decls = [];
|
|
4440
|
+
const tint = normalizeHex(b.page_tint_color);
|
|
4441
|
+
const page = tint ? mixHex(tint, "#ffffff", 0.1) : "#ffffff";
|
|
4442
|
+
const accent = normalizeHex(b.accent_color);
|
|
4443
|
+
if (accent) {
|
|
4444
|
+
const text = ensureContrast(ensureContrast(accent, page), "#ffffff");
|
|
4445
|
+
decls.push(
|
|
4446
|
+
`--cn-accent:${accent}`,
|
|
4447
|
+
`--cn-accent-fg:${readableForeground(accent)}`,
|
|
4448
|
+
`--cn-accent-text:${text}`
|
|
4449
|
+
);
|
|
4450
|
+
}
|
|
4451
|
+
const header = normalizeHex(b.header_bg_color);
|
|
4452
|
+
if (header) decls.push(`--cn-header-bg:${header}`, `--cn-header-fg:${readableForeground(header)}`);
|
|
4453
|
+
if (tint) decls.push(`--cn-page-bg:${page}`);
|
|
4454
|
+
return decls.length ? `:root{${decls.join(";")}}` : "";
|
|
4455
|
+
}
|
|
4456
|
+
function cssUrl(url) {
|
|
4457
|
+
const safe = url.replace(
|
|
4458
|
+
/["'()\\\s\u0000-\u001f\u007f]/g,
|
|
4459
|
+
(ch) => "%" + ch.charCodeAt(0).toString(16).padStart(2, "0").toUpperCase()
|
|
4460
|
+
);
|
|
4461
|
+
return `url("${safe}")`;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
3916
4464
|
// src/nests/routes.ts
|
|
3917
4465
|
async function effectiveAccess(nestId, userId) {
|
|
3918
4466
|
const access = await resolveNestAccess(nestId, userId);
|
|
@@ -3935,6 +4483,11 @@ function isGrantOnly(access) {
|
|
|
3935
4483
|
return access.permission === "none" && access.hasGrant;
|
|
3936
4484
|
}
|
|
3937
4485
|
var nestRoutes = new Hono3();
|
|
4486
|
+
function withParsedBranding(nest) {
|
|
4487
|
+
if (!nest) return nest;
|
|
4488
|
+
const { branding_json, ...rest } = nest;
|
|
4489
|
+
return { ...rest, branding: parseStoredBranding(branding_json, nest.id) };
|
|
4490
|
+
}
|
|
3938
4491
|
nestRoutes.get("/", async (c) => {
|
|
3939
4492
|
const userId = c.get("userId");
|
|
3940
4493
|
const archivedOnly = c.req.query("archived") === "1";
|
|
@@ -3990,14 +4543,20 @@ nestRoutes.get("/", async (c) => {
|
|
|
3990
4543
|
pinned: pinned.has(n.id)
|
|
3991
4544
|
};
|
|
3992
4545
|
};
|
|
4546
|
+
const nestScope = c.get("nestScope");
|
|
3993
4547
|
const seen = /* @__PURE__ */ new Set();
|
|
3994
4548
|
const deduped = [];
|
|
3995
4549
|
for (const n of [...owned, ...shared, ...publicExtras]) {
|
|
4550
|
+
if (nestScope && n.id !== nestScope) continue;
|
|
3996
4551
|
if (seen.has(n.id)) continue;
|
|
3997
4552
|
seen.add(n.id);
|
|
3998
4553
|
deduped.push(n);
|
|
3999
4554
|
}
|
|
4000
|
-
const
|
|
4555
|
+
const memberIds = new Set([...owned, ...shared].map((n) => n.id));
|
|
4556
|
+
const out = (await Promise.all(deduped.map(annotate))).map((row) => ({
|
|
4557
|
+
...row,
|
|
4558
|
+
discovered: !memberIds.has(row.id)
|
|
4559
|
+
}));
|
|
4001
4560
|
const docRows = await db.all(
|
|
4002
4561
|
`SELECT nest_id,
|
|
4003
4562
|
COUNT(DISTINCT CASE
|
|
@@ -4053,7 +4612,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
4053
4612
|
}
|
|
4054
4613
|
}
|
|
4055
4614
|
}
|
|
4056
|
-
const withMeta = out.map(
|
|
4615
|
+
const withMeta = out.map((raw) => withParsedBranding(raw)).map(
|
|
4057
4616
|
(n) => n.grant_only ? {
|
|
4058
4617
|
...n,
|
|
4059
4618
|
document_count: grantedDocsByNest.get(n.id) ?? 0,
|
|
@@ -4079,7 +4638,7 @@ nestRoutes.post("/", async (c) => {
|
|
|
4079
4638
|
throw new ValidationError("name is required");
|
|
4080
4639
|
}
|
|
4081
4640
|
const nest = await createNest(c.get("userId"), body.name, body.description);
|
|
4082
|
-
return c.json({ nest }, 201);
|
|
4641
|
+
return c.json({ nest: withParsedBranding(nest) }, 201);
|
|
4083
4642
|
});
|
|
4084
4643
|
nestRoutes.post("/:nestId", async (c) => {
|
|
4085
4644
|
if (c.req.param("nestId") !== "import") {
|
|
@@ -4096,7 +4655,14 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4096
4655
|
}
|
|
4097
4656
|
const email2 = await resolveCallerEmail(userId);
|
|
4098
4657
|
if (body.finalize) {
|
|
4099
|
-
return c.json({
|
|
4658
|
+
return c.json({
|
|
4659
|
+
nest: withParsedBranding(nest2),
|
|
4660
|
+
documents: await registerImportedDocuments(
|
|
4661
|
+
target,
|
|
4662
|
+
email2,
|
|
4663
|
+
attributeRequest(c, body.client)
|
|
4664
|
+
)
|
|
4665
|
+
});
|
|
4100
4666
|
}
|
|
4101
4667
|
const registered = await getDb().get(
|
|
4102
4668
|
"SELECT COUNT(*) AS c FROM node_versions WHERE nest_id = ?",
|
|
@@ -4121,10 +4687,14 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4121
4687
|
try {
|
|
4122
4688
|
const { rejected } = await stageImportedFiles(nest.id, files, email);
|
|
4123
4689
|
if (body.partial) {
|
|
4124
|
-
return c.json({ nest, rejected }, 201);
|
|
4690
|
+
return c.json({ nest: withParsedBranding(nest), rejected }, 201);
|
|
4125
4691
|
}
|
|
4126
|
-
const documents = await registerImportedDocuments(
|
|
4127
|
-
|
|
4692
|
+
const documents = await registerImportedDocuments(
|
|
4693
|
+
nest.id,
|
|
4694
|
+
email,
|
|
4695
|
+
attributeRequest(c, body.client)
|
|
4696
|
+
);
|
|
4697
|
+
return c.json({ nest: withParsedBranding(nest), documents, rejected }, 201);
|
|
4128
4698
|
} catch (err) {
|
|
4129
4699
|
await deleteNest(nest.id).catch(() => {
|
|
4130
4700
|
});
|
|
@@ -4153,7 +4723,8 @@ nestRoutes.post("/:nestId/sync", async (c) => {
|
|
|
4153
4723
|
const result = await syncUnsyncedFolder(
|
|
4154
4724
|
userId,
|
|
4155
4725
|
body.name,
|
|
4156
|
-
await resolveCallerEmail(userId)
|
|
4726
|
+
await resolveCallerEmail(userId),
|
|
4727
|
+
attributeRequest(c, body.client)
|
|
4157
4728
|
);
|
|
4158
4729
|
return c.json(result, 201);
|
|
4159
4730
|
});
|
|
@@ -4189,7 +4760,7 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
4189
4760
|
const grant_only = roles.length === 0 && isGrantOnly(access);
|
|
4190
4761
|
const nest = await getNest(nestId);
|
|
4191
4762
|
return c.json({
|
|
4192
|
-
nest,
|
|
4763
|
+
nest: withParsedBranding(nest),
|
|
4193
4764
|
permission,
|
|
4194
4765
|
roles: grant_only ? ["viewer"] : roles,
|
|
4195
4766
|
myStewards,
|
|
@@ -4213,7 +4784,7 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
4213
4784
|
name: body.name,
|
|
4214
4785
|
description: body.description
|
|
4215
4786
|
});
|
|
4216
|
-
return c.json({ nest });
|
|
4787
|
+
return c.json({ nest: withParsedBranding(nest) });
|
|
4217
4788
|
});
|
|
4218
4789
|
nestRoutes.post("/:nestId/pin", async (c) => {
|
|
4219
4790
|
const nestId = c.req.param("nestId");
|
|
@@ -4275,7 +4846,8 @@ nestRoutes.get("/:nestId/settings", async (c) => {
|
|
|
4275
4846
|
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
4276
4847
|
allow_self_approve: await nestAllowsSelfApprove(nestId),
|
|
4277
4848
|
prime_only_review: await nestReviewsPrimeOnly(nestId),
|
|
4278
|
-
prime_tags: await nestPrimeTags(nestId)
|
|
4849
|
+
prime_tags: await nestPrimeTags(nestId),
|
|
4850
|
+
creator_is_reviewer: await nestCreatorIsReviewer(nestId)
|
|
4279
4851
|
});
|
|
4280
4852
|
});
|
|
4281
4853
|
nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
@@ -4320,11 +4892,15 @@ nestRoutes.patch("/:nestId/settings", async (c) => {
|
|
|
4320
4892
|
Array.isArray(body.prime_tags) ? body.prime_tags : [body.prime_tags]
|
|
4321
4893
|
);
|
|
4322
4894
|
}
|
|
4895
|
+
if (typeof body.creator_is_reviewer === "boolean") {
|
|
4896
|
+
await setCreatorIsReviewer(nestId, body.creator_is_reviewer);
|
|
4897
|
+
}
|
|
4323
4898
|
return c.json({
|
|
4324
4899
|
stewardship_enabled: await isStewardshipEnabled(nestId),
|
|
4325
4900
|
allow_self_approve: await nestAllowsSelfApprove(nestId),
|
|
4326
4901
|
prime_only_review: await nestReviewsPrimeOnly(nestId),
|
|
4327
4902
|
prime_tags: await nestPrimeTags(nestId),
|
|
4903
|
+
creator_is_reviewer: await nestCreatorIsReviewer(nestId),
|
|
4328
4904
|
wiped
|
|
4329
4905
|
});
|
|
4330
4906
|
});
|
|
@@ -4351,21 +4927,10 @@ async function addCollaborator(params) {
|
|
|
4351
4927
|
if (!isEmailish(params.email)) {
|
|
4352
4928
|
throw new ValidationError("Enter a valid email address.");
|
|
4353
4929
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
);
|
|
4359
|
-
if (existing) {
|
|
4360
|
-
userId = existing.id;
|
|
4361
|
-
} else {
|
|
4362
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4363
|
-
userId = uuid6();
|
|
4364
|
-
await db.run(
|
|
4365
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4366
|
-
[userId, email, null, await hashPassword2(uuid6())]
|
|
4367
|
-
);
|
|
4368
|
-
}
|
|
4930
|
+
userId = (await findOrCreateInvitedUser(params.email, {
|
|
4931
|
+
invitedBy: params.grantedByEmail,
|
|
4932
|
+
baseUrl: params.baseUrl
|
|
4933
|
+
})).id;
|
|
4369
4934
|
}
|
|
4370
4935
|
if (!userId) {
|
|
4371
4936
|
throw new ValidationError("user_id or email is required");
|
|
@@ -4424,13 +4989,18 @@ async function addCollaborator(params) {
|
|
|
4424
4989
|
// event. Slack/Teams still fire.
|
|
4425
4990
|
{ skipEmail: true }
|
|
4426
4991
|
);
|
|
4427
|
-
const
|
|
4992
|
+
const invitee = await db.get(
|
|
4993
|
+
"SELECT email, is_invited FROM users WHERE id = ?",
|
|
4994
|
+
[userId]
|
|
4995
|
+
);
|
|
4996
|
+
const inviteeEmail = params.email || invitee?.email;
|
|
4428
4997
|
if (inviteeEmail) {
|
|
4998
|
+
const nestLink = docLink(nestId, void 0, params.baseUrl);
|
|
4429
4999
|
void sendEmailToRecipient(inviteeEmail, {
|
|
4430
5000
|
status: "shared",
|
|
4431
5001
|
docTitle: shareNestName,
|
|
4432
5002
|
path: shareNestName,
|
|
4433
|
-
link:
|
|
5003
|
+
link: nestLink && invitee?.is_invited === 1 ? `${nestLink}&invite=${encodeURIComponent(inviteeEmail)}` : nestLink,
|
|
4434
5004
|
actor: inviteeEmail,
|
|
4435
5005
|
permission: params.permission,
|
|
4436
5006
|
by: params.grantedByEmail || void 0
|
|
@@ -4539,12 +5109,24 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
4539
5109
|
if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
|
|
4540
5110
|
throw new ValidationError("visibility must be private, org or public");
|
|
4541
5111
|
}
|
|
4542
|
-
|
|
5112
|
+
const ack = body.acknowledge_public === true;
|
|
5113
|
+
const shared = config.SHARED_DEPLOYMENT;
|
|
5114
|
+
if (body.visibility === "public" && !ack) {
|
|
4543
5115
|
return c.json(
|
|
4544
5116
|
{
|
|
4545
5117
|
error: "Publishing this nest requires acknowledgement.",
|
|
4546
5118
|
requires_acknowledgement: true,
|
|
4547
|
-
warning: "This makes the nest readable by anyone on the internet, outside your organization."
|
|
5119
|
+
warning: shared ? "This makes the nest readable by anyone on the internet \u2014 including every account on this server, which belongs to other organizations too." : "This makes the nest readable by anyone on the internet, outside your organization."
|
|
5120
|
+
},
|
|
5121
|
+
409
|
|
5122
|
+
);
|
|
5123
|
+
}
|
|
5124
|
+
if (body.visibility === "org" && shared && !ack) {
|
|
5125
|
+
return c.json(
|
|
5126
|
+
{
|
|
5127
|
+
error: "Opening this nest to the organization requires acknowledgement.",
|
|
5128
|
+
requires_acknowledgement: true,
|
|
5129
|
+
warning: "This server is shared: 'Organization' makes the nest readable by every account on this server, including other organizations \u2014 not just yours."
|
|
4548
5130
|
},
|
|
4549
5131
|
409
|
|
4550
5132
|
);
|
|
@@ -4612,10 +5194,78 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4612
5194
|
});
|
|
4613
5195
|
});
|
|
4614
5196
|
|
|
4615
|
-
// src/nests/
|
|
5197
|
+
// src/nests/branding-routes.ts
|
|
4616
5198
|
import { Hono as Hono5 } from "hono";
|
|
4617
|
-
import {
|
|
4618
|
-
|
|
5199
|
+
import { access as fsAccess } from "fs/promises";
|
|
5200
|
+
import { join as join3 } from "path";
|
|
5201
|
+
var brandingRoutes = new Hono5();
|
|
5202
|
+
brandingRoutes.patch("/branding", async (c) => {
|
|
5203
|
+
const userId = c.get("userId");
|
|
5204
|
+
const nestId = c.req.param("nestId");
|
|
5205
|
+
const isServerAdmin = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
|
|
5206
|
+
if (!isServerAdmin) {
|
|
5207
|
+
const access = await resolveNestAccess(nestId, userId);
|
|
5208
|
+
if (permissionLevel(access.permission) < permissionLevel("admin")) {
|
|
5209
|
+
throw new ForbiddenError(
|
|
5210
|
+
"Only the nest owner, a nest admin, or the server admin can change branding"
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
const db = getDb();
|
|
5215
|
+
if (!await db.get("SELECT id FROM nests WHERE id = ?", [nestId])) {
|
|
5216
|
+
throw new NotFoundError("Nest not found");
|
|
5217
|
+
}
|
|
5218
|
+
const text = await c.req.text();
|
|
5219
|
+
let body = null;
|
|
5220
|
+
if (text.trim() !== "") {
|
|
5221
|
+
try {
|
|
5222
|
+
body = JSON.parse(text);
|
|
5223
|
+
} catch {
|
|
5224
|
+
throw new ValidationError("body must be JSON");
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
if (body !== null && (typeof body !== "object" || Array.isArray(body))) {
|
|
5228
|
+
throw new ValidationError("branding must be an object or null");
|
|
5229
|
+
}
|
|
5230
|
+
const assetsDir = join3(resolveNestPath(nestId), "assets");
|
|
5231
|
+
let next;
|
|
5232
|
+
try {
|
|
5233
|
+
next = await validateBrandingInput(body, nestId, async (name) => {
|
|
5234
|
+
try {
|
|
5235
|
+
await fsAccess(join3(assetsDir, name));
|
|
5236
|
+
return true;
|
|
5237
|
+
} catch {
|
|
5238
|
+
return false;
|
|
5239
|
+
}
|
|
5240
|
+
});
|
|
5241
|
+
} catch (err) {
|
|
5242
|
+
if (err instanceof BrandingValidationError) throw new ValidationError(err.message);
|
|
5243
|
+
throw err;
|
|
5244
|
+
}
|
|
5245
|
+
const mentionsHide = body !== null && Object.prototype.hasOwnProperty.call(body, "hide_powered_by");
|
|
5246
|
+
const stored = await db.transaction(async (tx) => {
|
|
5247
|
+
const lock = db.dialect === "postgres" ? " FOR UPDATE" : "";
|
|
5248
|
+
const row = await tx.get(`SELECT branding_json FROM nests WHERE id = ?${lock}`, [nestId]);
|
|
5249
|
+
if (!row) throw new NotFoundError("Nest not found");
|
|
5250
|
+
const wasHidden = parseStoredBranding(row.branding_json, nestId)?.hide_powered_by === true;
|
|
5251
|
+
if (!isServerAdmin) {
|
|
5252
|
+
if (next?.hide_powered_by === true && !wasHidden) {
|
|
5253
|
+
throw new ForbiddenError(
|
|
5254
|
+
'Only the server admin can hide the "Powered by ContextNest" line'
|
|
5255
|
+
);
|
|
5256
|
+
}
|
|
5257
|
+
if (!mentionsHide && wasHidden) next = { ...next ?? {}, hide_powered_by: true };
|
|
5258
|
+
}
|
|
5259
|
+
const value = next ? JSON.stringify(next) : null;
|
|
5260
|
+
await tx.run("UPDATE nests SET branding_json = ? WHERE id = ?", [value, nestId]);
|
|
5261
|
+
return value;
|
|
5262
|
+
});
|
|
5263
|
+
return c.json({ branding: parseStoredBranding(stored, nestId) });
|
|
5264
|
+
});
|
|
5265
|
+
|
|
5266
|
+
// src/nests/grant-routes.ts
|
|
5267
|
+
import { Hono as Hono6 } from "hono";
|
|
5268
|
+
var grantRoutes = new Hono6();
|
|
4619
5269
|
async function requireManage(c, nestId, opts = {}) {
|
|
4620
5270
|
if (config.AUTH_MODE === "open") return;
|
|
4621
5271
|
const access = await resolveNestAccess(nestId, c.get("userId"));
|
|
@@ -4631,17 +5281,8 @@ async function requireManage(c, nestId, opts = {}) {
|
|
|
4631
5281
|
}
|
|
4632
5282
|
}
|
|
4633
5283
|
async function resolveGrantee(email) {
|
|
4634
|
-
const
|
|
4635
|
-
|
|
4636
|
-
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
4637
|
-
if (existing) return existing.id;
|
|
4638
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4639
|
-
const id = uuid7();
|
|
4640
|
-
await db.run(
|
|
4641
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4642
|
-
[id, e, null, await hashPassword2(uuid7())]
|
|
4643
|
-
);
|
|
4644
|
-
return id;
|
|
5284
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
|
|
5285
|
+
return (await findOrCreateInvitedUser2(email)).id;
|
|
4645
5286
|
}
|
|
4646
5287
|
async function assertGrantAddsAccess(params) {
|
|
4647
5288
|
const { nestId, email, userId, targetType, target, role } = params;
|
|
@@ -4738,10 +5379,10 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4738
5379
|
});
|
|
4739
5380
|
|
|
4740
5381
|
// src/nests/publish-routes.ts
|
|
4741
|
-
import { Hono as
|
|
5382
|
+
import { Hono as Hono7 } from "hono";
|
|
4742
5383
|
|
|
4743
5384
|
// src/nests/publish-service.ts
|
|
4744
|
-
import { v4 as
|
|
5385
|
+
import { v4 as uuid7 } from "uuid";
|
|
4745
5386
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
4746
5387
|
var MODES = ["public", "email", "code", "invite"];
|
|
4747
5388
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -4810,7 +5451,7 @@ async function createPublish(params) {
|
|
|
4810
5451
|
);
|
|
4811
5452
|
}
|
|
4812
5453
|
const db = getDb();
|
|
4813
|
-
const id =
|
|
5454
|
+
const id = uuid7();
|
|
4814
5455
|
const slug = newSlug();
|
|
4815
5456
|
const created = nowIso();
|
|
4816
5457
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -4841,7 +5482,7 @@ async function createPublish(params) {
|
|
|
4841
5482
|
await db.run(
|
|
4842
5483
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4843
5484
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
4844
|
-
[
|
|
5485
|
+
[uuid7(), id, email, created]
|
|
4845
5486
|
);
|
|
4846
5487
|
}
|
|
4847
5488
|
}
|
|
@@ -4896,7 +5537,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
4896
5537
|
await getDb().run(
|
|
4897
5538
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4898
5539
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
4899
|
-
[
|
|
5540
|
+
[uuid7(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
4900
5541
|
);
|
|
4901
5542
|
return token;
|
|
4902
5543
|
}
|
|
@@ -4926,7 +5567,7 @@ async function createSession2(publishId, email) {
|
|
|
4926
5567
|
await getDb().run(
|
|
4927
5568
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4928
5569
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
4929
|
-
[
|
|
5570
|
+
[uuid7(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
4930
5571
|
);
|
|
4931
5572
|
return token;
|
|
4932
5573
|
}
|
|
@@ -4943,7 +5584,7 @@ async function logView(publishId, email) {
|
|
|
4943
5584
|
await getDb().run(
|
|
4944
5585
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
4945
5586
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
4946
|
-
[
|
|
5587
|
+
[uuid7(), publishId, email, nowIso()]
|
|
4947
5588
|
);
|
|
4948
5589
|
}
|
|
4949
5590
|
async function listAllowlist(publishId) {
|
|
@@ -4961,7 +5602,7 @@ async function listViews(publishId) {
|
|
|
4961
5602
|
}
|
|
4962
5603
|
|
|
4963
5604
|
// src/nests/publish-routes.ts
|
|
4964
|
-
var publishRoutes = new
|
|
5605
|
+
var publishRoutes = new Hono7();
|
|
4965
5606
|
async function requireManage2(c, nestId) {
|
|
4966
5607
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
4967
5608
|
throw new ForbiddenError(
|
|
@@ -4976,7 +5617,7 @@ publishRoutes.post("/", async (c) => {
|
|
|
4976
5617
|
const mode = body.access_mode ?? "public";
|
|
4977
5618
|
if ((mode === "email" || mode === "invite") && !isEmailConfigured()) {
|
|
4978
5619
|
throw new ValidationError(
|
|
4979
|
-
"Email and invite links need email set up on this server (
|
|
5620
|
+
"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."
|
|
4980
5621
|
);
|
|
4981
5622
|
}
|
|
4982
5623
|
const row = await createPublish({
|
|
@@ -5032,11 +5673,11 @@ publishRoutes.delete("/:id", async (c) => {
|
|
|
5032
5673
|
});
|
|
5033
5674
|
|
|
5034
5675
|
// src/nests/publish-render-routes.ts
|
|
5035
|
-
import { Hono as
|
|
5676
|
+
import { Hono as Hono8 } from "hono";
|
|
5036
5677
|
import MarkdownIt from "markdown-it";
|
|
5037
5678
|
|
|
5038
5679
|
// src/annotations/service.ts
|
|
5039
|
-
import { v4 as
|
|
5680
|
+
import { v4 as uuid8 } from "uuid";
|
|
5040
5681
|
|
|
5041
5682
|
// src/annotations/projection.ts
|
|
5042
5683
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -5190,7 +5831,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5190
5831
|
throw new Error("comment body is required");
|
|
5191
5832
|
}
|
|
5192
5833
|
const db = getDb();
|
|
5193
|
-
const id =
|
|
5834
|
+
const id = uuid8();
|
|
5194
5835
|
const anchor = clampAnchor(input.anchor);
|
|
5195
5836
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
5196
5837
|
await db.transaction(async (tx) => {
|
|
@@ -5210,7 +5851,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5210
5851
|
await tx.run(
|
|
5211
5852
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5212
5853
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5213
|
-
[
|
|
5854
|
+
[uuid8(), id, authorEmail, body]
|
|
5214
5855
|
);
|
|
5215
5856
|
});
|
|
5216
5857
|
return rowToThread(await getThreadRow(id));
|
|
@@ -5232,7 +5873,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
5232
5873
|
await db.run(
|
|
5233
5874
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5234
5875
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5235
|
-
[
|
|
5876
|
+
[uuid8(), threadId, authorEmail, trimmed]
|
|
5236
5877
|
);
|
|
5237
5878
|
return rowToThread(await getThreadRow(threadId));
|
|
5238
5879
|
}
|
|
@@ -5338,7 +5979,7 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
5338
5979
|
tags: ["annotations"]
|
|
5339
5980
|
},
|
|
5340
5981
|
userEmail,
|
|
5341
|
-
{ notifyMentions: false }
|
|
5982
|
+
{ notifyMentions: false, creatorReviews: false }
|
|
5342
5983
|
);
|
|
5343
5984
|
} else {
|
|
5344
5985
|
throw err;
|
|
@@ -5353,7 +5994,9 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
5353
5994
|
}
|
|
5354
5995
|
|
|
5355
5996
|
// src/nests/publish-render-routes.ts
|
|
5356
|
-
|
|
5997
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
5998
|
+
import { join as join4 } from "path";
|
|
5999
|
+
var publishRenderRoutes = new Hono8();
|
|
5357
6000
|
var SESSION_TTL_SECONDS2 = 24 * 60 * 60;
|
|
5358
6001
|
var GATE_LIMIT = { max: 10, windowMs: 10 * 6e4 };
|
|
5359
6002
|
var COMMENT_LIMIT = { max: 5, windowMs: 10 * 6e4 };
|
|
@@ -5410,6 +6053,114 @@ function setSessionCookie2(c, slug, token) {
|
|
|
5410
6053
|
function esc(s) {
|
|
5411
6054
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5412
6055
|
}
|
|
6056
|
+
var IMAGE_MIME = {
|
|
6057
|
+
png: "image/png",
|
|
6058
|
+
jpg: "image/jpeg",
|
|
6059
|
+
jpeg: "image/jpeg",
|
|
6060
|
+
gif: "image/gif",
|
|
6061
|
+
webp: "image/webp"
|
|
6062
|
+
};
|
|
6063
|
+
var INLINE_CAP = { logo: 512 * 1024, favicon: 256 * 1024, banner: 2 * 1024 * 1024 };
|
|
6064
|
+
var INLINE_CACHE_MAX_CHARS = 24 * 1024 * 1024;
|
|
6065
|
+
var inlineCache = /* @__PURE__ */ new Map();
|
|
6066
|
+
var inlineCacheChars = 0;
|
|
6067
|
+
function cacheGet(key) {
|
|
6068
|
+
if (!inlineCache.has(key)) return void 0;
|
|
6069
|
+
const v = inlineCache.get(key);
|
|
6070
|
+
inlineCache.delete(key);
|
|
6071
|
+
inlineCache.set(key, v);
|
|
6072
|
+
return v;
|
|
6073
|
+
}
|
|
6074
|
+
function cachePut(key, v) {
|
|
6075
|
+
const size = v?.length ?? 0;
|
|
6076
|
+
if (size > INLINE_CACHE_MAX_CHARS) return;
|
|
6077
|
+
inlineCacheChars -= inlineCache.get(key)?.length ?? 0;
|
|
6078
|
+
inlineCache.set(key, v);
|
|
6079
|
+
inlineCacheChars += size;
|
|
6080
|
+
for (const [k, old] of inlineCache) {
|
|
6081
|
+
if (inlineCacheChars <= INLINE_CACHE_MAX_CHARS && inlineCache.size <= 512) break;
|
|
6082
|
+
inlineCache.delete(k);
|
|
6083
|
+
inlineCacheChars -= old?.length ?? 0;
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
async function inlineAsset(nestId, url, cap) {
|
|
6087
|
+
if (!url) return null;
|
|
6088
|
+
const name = nestAssetName(url, nestId);
|
|
6089
|
+
if (!name) return null;
|
|
6090
|
+
const key = `${nestId}/${name}/${cap}`;
|
|
6091
|
+
const hit = cacheGet(key);
|
|
6092
|
+
if (hit !== void 0) return hit;
|
|
6093
|
+
let out = null;
|
|
6094
|
+
try {
|
|
6095
|
+
const path = join4(resolveNestPath(nestId), "assets", name);
|
|
6096
|
+
if ((await stat2(path)).size <= cap) {
|
|
6097
|
+
const mime = IMAGE_MIME[name.split(".").pop().toLowerCase()];
|
|
6098
|
+
if (mime) out = `data:${mime};base64,${(await readFile(path)).toString("base64")}`;
|
|
6099
|
+
}
|
|
6100
|
+
} catch {
|
|
6101
|
+
return null;
|
|
6102
|
+
}
|
|
6103
|
+
cachePut(key, out);
|
|
6104
|
+
return out;
|
|
6105
|
+
}
|
|
6106
|
+
async function loadBrand(row) {
|
|
6107
|
+
let b = null;
|
|
6108
|
+
try {
|
|
6109
|
+
const r = await getDb().get("SELECT branding_json FROM nests WHERE id = ?", [row.nest_id]);
|
|
6110
|
+
b = parseStoredBranding(r?.branding_json, row.nest_id);
|
|
6111
|
+
} catch {
|
|
6112
|
+
b = null;
|
|
6113
|
+
}
|
|
6114
|
+
if (!b) return null;
|
|
6115
|
+
const [logo, banner, favicon] = await Promise.all([
|
|
6116
|
+
inlineAsset(row.nest_id, b.logo_url, INLINE_CAP.logo),
|
|
6117
|
+
inlineAsset(row.nest_id, b.banner_url, INLINE_CAP.banner),
|
|
6118
|
+
inlineAsset(row.nest_id, b.favicon_url, INLINE_CAP.favicon)
|
|
6119
|
+
]);
|
|
6120
|
+
return { b, logo, banner, favicon };
|
|
6121
|
+
}
|
|
6122
|
+
var BRAND_CSS = `
|
|
6123
|
+
.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}
|
|
6124
|
+
.cn-brand img{height:32px;width:auto;max-width:160px;object-fit:contain}
|
|
6125
|
+
.cn-brand span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6126
|
+
.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}
|
|
6127
|
+
.cn-hero.has-img{color:#fff}
|
|
6128
|
+
.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))}
|
|
6129
|
+
.cn-hero *{position:relative;margin:0;max-width:820px}
|
|
6130
|
+
.cn-hero strong{display:block;font:700 clamp(20px,4vw,30px)/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
6131
|
+
.cn-hero p{margin-top:6px;font-size:15px;opacity:.92}
|
|
6132
|
+
.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}
|
|
6133
|
+
.cn-foot a{color:var(--cn-accent-text,#142a80)}
|
|
6134
|
+
`;
|
|
6135
|
+
function brandHead(brand, extraCss = "") {
|
|
6136
|
+
if (!brand) return "";
|
|
6137
|
+
const icon = brand.favicon ? `<link rel="icon" href="${esc(brand.favicon)}">` : "";
|
|
6138
|
+
return `<style>${publishBrandCssVars(brand.b)}${BRAND_CSS}${extraCss}</style>${icon}`;
|
|
6139
|
+
}
|
|
6140
|
+
function brandBar(brand) {
|
|
6141
|
+
if (!brand) return "";
|
|
6142
|
+
const title = brand.b.site_title ? `<span>${esc(brand.b.site_title)}</span>` : "";
|
|
6143
|
+
const logo = brand.logo ? `<img src="${esc(brand.logo)}" alt="">` : "";
|
|
6144
|
+
if (!title && !logo) return "";
|
|
6145
|
+
return `<header class="cn-brand">${logo}${title}</header>`;
|
|
6146
|
+
}
|
|
6147
|
+
function brandHero(brand) {
|
|
6148
|
+
if (!brand) return "";
|
|
6149
|
+
const { banner_headline: h, banner_subtext: sub } = brand.b;
|
|
6150
|
+
if (!brand.banner && !h) return "";
|
|
6151
|
+
const style = brand.banner ? ` style="background-image:${esc(cssUrl(brand.banner))}"` : "";
|
|
6152
|
+
const cls = brand.banner ? "cn-hero has-img" : "cn-hero";
|
|
6153
|
+
return `<div class="${cls}"${style}>${h ? `<strong>${esc(h)}</strong>` : ""}${sub ? `<p>${esc(sub)}</p>` : ""}</div>`;
|
|
6154
|
+
}
|
|
6155
|
+
function brandFooter(brand) {
|
|
6156
|
+
if (!brand) return "";
|
|
6157
|
+
const { footer_text: text, footer_link_url: href, footer_link_label: label } = brand.b;
|
|
6158
|
+
const parts = [];
|
|
6159
|
+
if (text) parts.push(`<span>${esc(text)}</span>`);
|
|
6160
|
+
if (href) parts.push(`<a href="${esc(href)}" rel="noopener noreferrer" target="_blank">${esc(label || href)}</a>`);
|
|
6161
|
+
if (!brand.b.hide_powered_by) parts.push(`<span>Secured by PromptOwl \xB7 ContextNest</span>`);
|
|
6162
|
+
return parts.length ? `<footer class="cn-foot">${parts.join("")}</footer>` : "";
|
|
6163
|
+
}
|
|
5413
6164
|
var NO_DOWNLOAD_CSS = `<style>
|
|
5414
6165
|
@media print{html,body{display:none !important}}
|
|
5415
6166
|
html,body{-webkit-user-select:none;-ms-user-select:none;user-select:none}
|
|
@@ -5481,7 +6232,10 @@ async function renderPublished(c, row, viewerEmail) {
|
|
|
5481
6232
|
}
|
|
5482
6233
|
const title = esc(artifact.title || row.node_id);
|
|
5483
6234
|
const heading = /^\s*#\s/.test(body) ? "" : `<h1>${title}</h1>`;
|
|
5484
|
-
const
|
|
6235
|
+
const brand = await loadBrand(row);
|
|
6236
|
+
const docTitle = brand?.b.site_title ? `${title} \xB7 ${esc(brand.b.site_title)}` : title;
|
|
6237
|
+
const brandCss = brand ? `body{background:var(--cn-page-bg,#fff)}h1,h2,h3,h4,a{color:var(--cn-accent-text,#142a80)}` : "";
|
|
6238
|
+
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>`;
|
|
5485
6239
|
return c.body(shell, 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
5486
6240
|
}
|
|
5487
6241
|
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>`;
|
|
@@ -5502,9 +6256,29 @@ var GATE_EYE_SCRIPT = `<script>
|
|
|
5502
6256
|
});
|
|
5503
6257
|
})();
|
|
5504
6258
|
</script>`;
|
|
5505
|
-
|
|
6259
|
+
var GATE_BRAND_CSS = `
|
|
6260
|
+
body{background:var(--cn-page-bg,#f4f4f5)}
|
|
6261
|
+
.card{box-sizing:border-box;overflow:hidden}
|
|
6262
|
+
h1{color:var(--cn-accent-text,#142a80)}
|
|
6263
|
+
button[type=submit]{background:var(--cn-accent,#142a80);color:var(--cn-accent-fg,#fff)}
|
|
6264
|
+
.cn-brand{margin:-32px -34px 20px;border-radius:14px 14px 0 0;padding:14px 20px}
|
|
6265
|
+
.cn-gate-foot{font-size:12px;color:#a8a29e;margin:10px 0 0;text-align:center}
|
|
6266
|
+
.cn-gate-foot a{color:var(--cn-accent-text,#142a80)}
|
|
6267
|
+
`;
|
|
6268
|
+
function gateBrandTop(brand) {
|
|
6269
|
+
return brandBar(brand);
|
|
6270
|
+
}
|
|
6271
|
+
function gateBrandFoot(brand) {
|
|
6272
|
+
if (!brand) return "";
|
|
6273
|
+
const { footer_text: text, footer_link_url: href, footer_link_label: label } = brand.b;
|
|
6274
|
+
const parts = [];
|
|
6275
|
+
if (text) parts.push(esc(text));
|
|
6276
|
+
if (href) parts.push(`<a href="${esc(href)}" rel="noopener noreferrer" target="_blank">${esc(label || href)}</a>`);
|
|
6277
|
+
return parts.length ? `<p class="cn-gate-foot">${parts.join(" \xB7 ")}</p>` : "";
|
|
6278
|
+
}
|
|
6279
|
+
function gatePage(row, message, brand = null) {
|
|
5506
6280
|
const wantsCode = row.access_mode === "code";
|
|
5507
|
-
const
|
|
6281
|
+
const field2 = wantsCode ? `<label>Access code<span class="wrap">
|
|
5508
6282
|
<input id="cn-code" name="code" type="password" autocomplete="off" required autofocus>
|
|
5509
6283
|
<button type="button" id="cn-eye" class="eye" aria-label="Show code" aria-pressed="false">${EYE_SVG}</button>
|
|
5510
6284
|
</span></label>` : `<label>Your email<input name="email" type="email" autocomplete="email" required autofocus></label>`;
|
|
@@ -5527,10 +6301,10 @@ function gatePage(row, message) {
|
|
|
5527
6301
|
button.eye svg{width:18px;height:18px;display:block}
|
|
5528
6302
|
.err{color:#be123c;font-weight:600}
|
|
5529
6303
|
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0;text-align:center}
|
|
5530
|
-
</style
|
|
5531
|
-
<h1>Protected document</h1><p>${esc(note)}</p>${err}${
|
|
6304
|
+
</style>${brandHead(brand, GATE_BRAND_CSS)}</head><body><form class="card" method="POST" action="/p/${esc(row.slug)}/gate">
|
|
6305
|
+
${gateBrandTop(brand)}<h1>Protected document</h1><p>${esc(note)}</p>${err}${field2}
|
|
5532
6306
|
<button type="submit">${cta}</button>
|
|
5533
|
-
|
|
6307
|
+
${brand?.b.hide_powered_by ? "" : `<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>`}${gateBrandFoot(brand)}
|
|
5534
6308
|
</form>${wantsCode ? GATE_EYE_SCRIPT : ""}</body></html>`;
|
|
5535
6309
|
}
|
|
5536
6310
|
function unavailablePage() {
|
|
@@ -5549,7 +6323,7 @@ function unavailablePage() {
|
|
|
5549
6323
|
function sentPage() {
|
|
5550
6324
|
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>`;
|
|
5551
6325
|
}
|
|
5552
|
-
function readerShell(row, title, viewerEmail) {
|
|
6326
|
+
function readerShell(row, title, viewerEmail, brand = null) {
|
|
5553
6327
|
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>`;
|
|
5554
6328
|
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${esc(title)}</title><style>
|
|
5555
6329
|
html,body{margin:0;height:100%}
|
|
@@ -5570,7 +6344,7 @@ function readerShell(row, title, viewerEmail) {
|
|
|
5570
6344
|
button[disabled]{opacity:.6;cursor:default}
|
|
5571
6345
|
.msg{font-weight:600;margin:0 0 14px;font-size:14px}
|
|
5572
6346
|
.msg.err{color:#be123c}.msg.ok{color:#15803d}
|
|
5573
|
-
</style
|
|
6347
|
+
</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>
|
|
5574
6348
|
<iframe id="cn-frame" src="/p/${esc(row.slug)}/view" title="${esc(title)}"></iframe>
|
|
5575
6349
|
<button id="cn-open" type="button">Leave a comment</button>
|
|
5576
6350
|
<div id="cn-back"><div class="card" role="dialog" aria-modal="true" aria-label="Leave a comment">
|
|
@@ -5641,12 +6415,12 @@ publishRenderRoutes.get("/:slug", async (c) => {
|
|
|
5641
6415
|
const row = await getPublishBySlug(slug);
|
|
5642
6416
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5643
6417
|
const viewer = await resolveViewer(c, row);
|
|
5644
|
-
if (!viewer) return c.html(gatePage(row));
|
|
6418
|
+
if (!viewer) return c.html(gatePage(row, void 0, await loadBrand(row)));
|
|
5645
6419
|
if (row.comments_enabled !== 1) return renderPublished(c, row, viewer.email);
|
|
5646
6420
|
const title = await publishedTitle(row);
|
|
5647
6421
|
c.header("Content-Security-Policy", SHELL_CSP);
|
|
5648
6422
|
c.header("Referrer-Policy", "no-referrer");
|
|
5649
|
-
return c.html(readerShell(row, title, viewer.email));
|
|
6423
|
+
return c.html(readerShell(row, title, viewer.email, await loadBrand(row)));
|
|
5650
6424
|
});
|
|
5651
6425
|
publishRenderRoutes.get("/:slug/view", async (c) => {
|
|
5652
6426
|
const slug = c.req.param("slug");
|
|
@@ -5684,20 +6458,20 @@ publishRenderRoutes.post("/:slug/gate", async (c) => {
|
|
|
5684
6458
|
const row = await getPublishBySlug(slug);
|
|
5685
6459
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5686
6460
|
if (!tryConsume(`publish-gate:${slug}:${clientIp(c)}`, GATE_LIMIT)) {
|
|
5687
|
-
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes."), 429);
|
|
6461
|
+
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes.", await loadBrand(row)), 429);
|
|
5688
6462
|
}
|
|
5689
6463
|
if (row.access_mode === "public") return c.redirect(`/p/${slug}`);
|
|
5690
6464
|
const form = await c.req.parseBody();
|
|
5691
6465
|
if (row.access_mode === "code") {
|
|
5692
6466
|
if (!await verifyCode(row, String(form.code ?? ""))) {
|
|
5693
|
-
return c.html(gatePage(row, "That code is not correct."), 401);
|
|
6467
|
+
return c.html(gatePage(row, "That code is not correct.", await loadBrand(row)), 401);
|
|
5694
6468
|
}
|
|
5695
6469
|
setSessionCookie2(c, slug, await createSession2(row.id, null));
|
|
5696
6470
|
return c.redirect(`/p/${slug}`);
|
|
5697
6471
|
}
|
|
5698
6472
|
const email = normalizeEmail(String(form.email ?? ""));
|
|
5699
6473
|
if (!isEmailish(email)) {
|
|
5700
|
-
return c.html(gatePage(row, "Enter a valid email address."), 400);
|
|
6474
|
+
return c.html(gatePage(row, "Enter a valid email address.", await loadBrand(row)), 400);
|
|
5701
6475
|
}
|
|
5702
6476
|
const allowed = row.access_mode !== "invite" || await isAllowlisted(row.id, email);
|
|
5703
6477
|
const linkToken = await mintMagicLink(row.id, email);
|
|
@@ -5717,15 +6491,15 @@ publishRenderRoutes.get("/:slug/verify", async (c) => {
|
|
|
5717
6491
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5718
6492
|
const email = await consumeMagicLink(row.id, c.req.query("token") ?? "");
|
|
5719
6493
|
if (!email) {
|
|
5720
|
-
return c.html(gatePage(row, "That link is invalid or has expired. Request a new one."), 401);
|
|
6494
|
+
return c.html(gatePage(row, "That link is invalid or has expired. Request a new one.", await loadBrand(row)), 401);
|
|
5721
6495
|
}
|
|
5722
6496
|
setSessionCookie2(c, slug, await createSession2(row.id, email));
|
|
5723
6497
|
return c.redirect(`/p/${slug}`);
|
|
5724
6498
|
});
|
|
5725
6499
|
|
|
5726
6500
|
// src/nests/nest-team-routes.ts
|
|
5727
|
-
import { Hono as
|
|
5728
|
-
var nestTeamRoutes = new
|
|
6501
|
+
import { Hono as Hono9 } from "hono";
|
|
6502
|
+
var nestTeamRoutes = new Hono9();
|
|
5729
6503
|
nestTeamRoutes.get("/", async (c) => {
|
|
5730
6504
|
const nestId = c.req.param("nestId");
|
|
5731
6505
|
const refs = await listNestTeams(nestId);
|
|
@@ -5764,8 +6538,8 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
|
5764
6538
|
});
|
|
5765
6539
|
|
|
5766
6540
|
// src/nests/people-routes.ts
|
|
5767
|
-
import { Hono as
|
|
5768
|
-
var peopleRoutes = new
|
|
6541
|
+
import { Hono as Hono10 } from "hono";
|
|
6542
|
+
var peopleRoutes = new Hono10();
|
|
5769
6543
|
peopleRoutes.get("/suggest", async (c) => {
|
|
5770
6544
|
const userId = c.get("userId");
|
|
5771
6545
|
const q = c.req.query("q") ?? "";
|
|
@@ -5775,7 +6549,7 @@ peopleRoutes.get("/suggest", async (c) => {
|
|
|
5775
6549
|
});
|
|
5776
6550
|
|
|
5777
6551
|
// src/governance/team-routes.ts
|
|
5778
|
-
import { Hono as
|
|
6552
|
+
import { Hono as Hono11 } from "hono";
|
|
5779
6553
|
|
|
5780
6554
|
// src/governance/promptowl-teams.ts
|
|
5781
6555
|
var PROMPTOWL_SOURCE = "promptowl";
|
|
@@ -5922,7 +6696,7 @@ async function desiredRoster(poTeam, callerEmail) {
|
|
|
5922
6696
|
}
|
|
5923
6697
|
|
|
5924
6698
|
// src/governance/team-routes.ts
|
|
5925
|
-
var teamRoutes = new
|
|
6699
|
+
var teamRoutes = new Hono11();
|
|
5926
6700
|
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
5927
6701
|
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
5928
6702
|
var CONNECT_REQUIRED_STATUS = 428;
|
|
@@ -6107,7 +6881,8 @@ function toSearchHit(node, query) {
|
|
|
6107
6881
|
}
|
|
6108
6882
|
|
|
6109
6883
|
// src/nodes/routes.ts
|
|
6110
|
-
import { Hono as
|
|
6884
|
+
import { Hono as Hono12 } from "hono";
|
|
6885
|
+
import { parseDocument as parseDocument3, readPdfBinary as readPdfBinary2 } from "@promptowl/contextnest-engine";
|
|
6111
6886
|
|
|
6112
6887
|
// src/nodes/markdown-export.ts
|
|
6113
6888
|
import { parseDocument as parseDocument2 } from "@promptowl/contextnest-engine";
|
|
@@ -6159,7 +6934,7 @@ ${m[1]}
|
|
|
6159
6934
|
}
|
|
6160
6935
|
|
|
6161
6936
|
// src/nodes/routes.ts
|
|
6162
|
-
var nodeRoutes = new
|
|
6937
|
+
var nodeRoutes = new Hono12();
|
|
6163
6938
|
function nodeAsMarkdown(response, nodeId) {
|
|
6164
6939
|
return nodeToMarkdown({
|
|
6165
6940
|
id: nodeId,
|
|
@@ -6224,7 +6999,8 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6224
6999
|
status: body.status,
|
|
6225
7000
|
schedule: body.schedule,
|
|
6226
7001
|
folder: body.folder,
|
|
6227
|
-
prime: body.prime
|
|
7002
|
+
prime: body.prime,
|
|
7003
|
+
client: attributeRequest(c, body.client)
|
|
6228
7004
|
},
|
|
6229
7005
|
authorEmail
|
|
6230
7006
|
);
|
|
@@ -6241,7 +7017,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6241
7017
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
6242
7018
|
const nestId = c.req.param("nestId");
|
|
6243
7019
|
const nodeId = c.req.param("nodeId");
|
|
6244
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
7020
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-TDZUC2Q6.js");
|
|
6245
7021
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
6246
7022
|
nestId,
|
|
6247
7023
|
nodeId
|
|
@@ -6262,7 +7038,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
6262
7038
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
6263
7039
|
const nestId = c.req.param("nestId");
|
|
6264
7040
|
const nodeId = c.req.param("nodeId");
|
|
6265
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
7041
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2EWN7WWO.js");
|
|
6266
7042
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
6267
7043
|
return c.json({ reviews: history });
|
|
6268
7044
|
});
|
|
@@ -6283,7 +7059,7 @@ nodeRoutes.post("/:nodeId{.+}/discard", async (c) => {
|
|
|
6283
7059
|
nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
6284
7060
|
const nestId = c.req.param("nestId");
|
|
6285
7061
|
const nodeId = c.req.param("nodeId");
|
|
6286
|
-
const { versions: versionManager } = await engineCache.get(nestId);
|
|
7062
|
+
const { storage, versions: versionManager } = await engineCache.get(nestId);
|
|
6287
7063
|
const userId = c.get("userId");
|
|
6288
7064
|
const userEmail = await resolveCallerEmail(userId);
|
|
6289
7065
|
if (!await canReadNode(nestId, nodeId, userId, userEmail)) {
|
|
@@ -6305,11 +7081,26 @@ nodeRoutes.post("/:nodeId{.+}/revert", async (c) => {
|
|
|
6305
7081
|
`Version ${targetVersion} not found for ${nodeId}`
|
|
6306
7082
|
);
|
|
6307
7083
|
}
|
|
7084
|
+
let restoredPdf = null;
|
|
7085
|
+
if (pdfMetaOf(nodeId, raw)) {
|
|
7086
|
+
try {
|
|
7087
|
+
restoredPdf = await readPdfBinary2(storage, nodeId, {
|
|
7088
|
+
version: targetVersion
|
|
7089
|
+
});
|
|
7090
|
+
} catch (err) {
|
|
7091
|
+
console.error("[pdf] revert could not read the version's binary", nestId, nodeId, targetVersion, err);
|
|
7092
|
+
throw new NotFoundError(`The PDF of version ${targetVersion} is not available to restore.`);
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
6308
7095
|
const content = bodyOnly(nodeId, raw);
|
|
6309
7096
|
const { node, version } = await updateNode(
|
|
6310
7097
|
nestId,
|
|
6311
7098
|
nodeId,
|
|
6312
|
-
{
|
|
7099
|
+
{
|
|
7100
|
+
...restoredPdf ? { pdf: { bytes: new Uint8Array(restoredPdf) } } : { content },
|
|
7101
|
+
changeNote: `Restored from version ${targetVersion}`,
|
|
7102
|
+
client: attributeRequest(c, body.client)
|
|
7103
|
+
},
|
|
6313
7104
|
userEmail
|
|
6314
7105
|
);
|
|
6315
7106
|
await trackEvent("node.revert", { nestId, nodeId, targetVersion });
|
|
@@ -6412,6 +7203,10 @@ nodeRoutes.get("/:nodeId{.+}", async (c) => {
|
|
|
6412
7203
|
approved
|
|
6413
7204
|
);
|
|
6414
7205
|
response.content = bodyOnly(nodeId, approvedRaw);
|
|
7206
|
+
if (response.type === "pdf") {
|
|
7207
|
+
const approvedPdf = pdfMetaOf(nodeId, approvedRaw);
|
|
7208
|
+
if (approvedPdf) response.pdf = approvedPdf;
|
|
7209
|
+
}
|
|
6415
7210
|
} catch (err) {
|
|
6416
7211
|
console.error(
|
|
6417
7212
|
"reconstructVersion failed (public single)",
|
|
@@ -6494,6 +7289,7 @@ nodeRoutes.patch("/:nodeId{.+}", async (c) => {
|
|
|
6494
7289
|
changeNote: body.changeNote,
|
|
6495
7290
|
schedule: body.schedule,
|
|
6496
7291
|
prime: body.prime,
|
|
7292
|
+
client: attributeRequest(c, body.client),
|
|
6497
7293
|
type: body.type
|
|
6498
7294
|
},
|
|
6499
7295
|
authorEmail
|
|
@@ -6506,6 +7302,13 @@ nodeRoutes.delete("/:nodeId{.+}", async (c) => {
|
|
|
6506
7302
|
await deleteNode(nestId, nodeId, await getUserEmail(c));
|
|
6507
7303
|
return c.json({ deleted: true });
|
|
6508
7304
|
});
|
|
7305
|
+
function pdfMetaOf(nodeId, raw) {
|
|
7306
|
+
try {
|
|
7307
|
+
return parseDocument3(`${nodeId}.md`, raw, nodeId).frontmatter.pdf;
|
|
7308
|
+
} catch {
|
|
7309
|
+
return void 0;
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
6509
7312
|
async function getUserEmail(c) {
|
|
6510
7313
|
const userId = c.get("userId");
|
|
6511
7314
|
const db = getDb();
|
|
@@ -6514,13 +7317,13 @@ async function getUserEmail(c) {
|
|
|
6514
7317
|
}
|
|
6515
7318
|
|
|
6516
7319
|
// src/annotations/routes.ts
|
|
6517
|
-
import { Hono as
|
|
7320
|
+
import { Hono as Hono13 } from "hono";
|
|
6518
7321
|
|
|
6519
7322
|
// src/annotations/types.ts
|
|
6520
7323
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
6521
7324
|
|
|
6522
7325
|
// src/annotations/routes.ts
|
|
6523
|
-
var annotationRoutes = new
|
|
7326
|
+
var annotationRoutes = new Hono13();
|
|
6524
7327
|
function getNodeId(c) {
|
|
6525
7328
|
const raw = c.req.param("nodeId");
|
|
6526
7329
|
try {
|
|
@@ -6648,7 +7451,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
6648
7451
|
});
|
|
6649
7452
|
|
|
6650
7453
|
// src/nodes/query-routes.ts
|
|
6651
|
-
import { Hono as
|
|
7454
|
+
import { Hono as Hono14 } from "hono";
|
|
6652
7455
|
|
|
6653
7456
|
// src/tables/service.ts
|
|
6654
7457
|
function splitCsvLine(line) {
|
|
@@ -7108,7 +7911,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
7108
7911
|
|
|
7109
7912
|
// src/nests/bundle-service.ts
|
|
7110
7913
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
7111
|
-
import { join as
|
|
7914
|
+
import { join as join5, sep } from "path";
|
|
7112
7915
|
import JSZip from "jszip";
|
|
7113
7916
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
7114
7917
|
function walkFiles(root) {
|
|
@@ -7121,7 +7924,7 @@ function walkFiles(root) {
|
|
|
7121
7924
|
}
|
|
7122
7925
|
for (const rel of entries) {
|
|
7123
7926
|
try {
|
|
7124
|
-
if (statSync2(
|
|
7927
|
+
if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
7125
7928
|
} catch {
|
|
7126
7929
|
}
|
|
7127
7930
|
}
|
|
@@ -7162,13 +7965,17 @@ async function exportNestBundle(nestId, includeDrafts) {
|
|
|
7162
7965
|
const segs = rel.split("/");
|
|
7163
7966
|
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
7164
7967
|
if (!keep) continue;
|
|
7165
|
-
zip.file(rel, readFileSync2(
|
|
7968
|
+
zip.file(rel, readFileSync2(join5(nestPath, rel)));
|
|
7166
7969
|
}
|
|
7167
7970
|
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
7168
7971
|
return zip.generateAsync({ type: "arraybuffer" });
|
|
7169
7972
|
}
|
|
7170
7973
|
|
|
7171
7974
|
// src/nodes/graph-service.ts
|
|
7975
|
+
import {
|
|
7976
|
+
contextLinkTarget,
|
|
7977
|
+
extractContextLinks
|
|
7978
|
+
} from "@promptowl/contextnest-engine";
|
|
7172
7979
|
var MAX_GRAPH_NODES = 150;
|
|
7173
7980
|
var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
7174
7981
|
function bareSlug(idOrTarget) {
|
|
@@ -7186,6 +7993,15 @@ function extractWikiTargets(body) {
|
|
|
7186
7993
|
}
|
|
7187
7994
|
return out;
|
|
7188
7995
|
}
|
|
7996
|
+
function extractContextTargets(body) {
|
|
7997
|
+
const out = /* @__PURE__ */ new Set();
|
|
7998
|
+
if (!body) return [];
|
|
7999
|
+
for (const uri of extractContextLinks(body)) {
|
|
8000
|
+
const t = contextLinkTarget(uri);
|
|
8001
|
+
if (t) out.add(t);
|
|
8002
|
+
}
|
|
8003
|
+
return [...out];
|
|
8004
|
+
}
|
|
7189
8005
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
7190
8006
|
const { storage } = await engineCache.get(nestId);
|
|
7191
8007
|
const docs = (await storage.discoverDocuments()).filter(
|
|
@@ -7220,6 +8036,15 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7220
8036
|
allLinks.push({ source: d.id, target: targetId });
|
|
7221
8037
|
}
|
|
7222
8038
|
}
|
|
8039
|
+
for (const d of accessible) {
|
|
8040
|
+
for (const targetId of extractContextTargets(d.body || "")) {
|
|
8041
|
+
if (!idSet.has(targetId) || targetId === d.id) continue;
|
|
8042
|
+
const key = `${d.id}->${targetId}`;
|
|
8043
|
+
if (seen.has(key)) continue;
|
|
8044
|
+
seen.add(key);
|
|
8045
|
+
allLinks.push({ source: d.id, target: targetId });
|
|
8046
|
+
}
|
|
8047
|
+
}
|
|
7223
8048
|
const tagCounts = /* @__PURE__ */ new Map();
|
|
7224
8049
|
const tagToDocs = /* @__PURE__ */ new Map();
|
|
7225
8050
|
for (const n of allNodes) {
|
|
@@ -7231,6 +8056,17 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7231
8056
|
}
|
|
7232
8057
|
}
|
|
7233
8058
|
const tags = [...tagCounts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
|
|
8059
|
+
for (const docIds of tagToDocs.values()) {
|
|
8060
|
+
const ids = [...docIds].sort();
|
|
8061
|
+
for (let i = 0; i < ids.length; i++) {
|
|
8062
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
8063
|
+
const key = `${ids[i]}->${ids[j]}`;
|
|
8064
|
+
if (seen.has(key) || seen.has(`${ids[j]}->${ids[i]}`)) continue;
|
|
8065
|
+
seen.add(key);
|
|
8066
|
+
allLinks.push({ source: ids[i], target: ids[j] });
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
7234
8070
|
const exposeStewards = await isStewardshipEnabled(nestId) && !await isPublicReader(nestId, userId);
|
|
7235
8071
|
const stewardRows = exposeStewards ? (await listStewards({ nestId })).filter(
|
|
7236
8072
|
// Drop document-scoped rows whose node the caller can't see.
|
|
@@ -7321,7 +8157,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7321
8157
|
}
|
|
7322
8158
|
|
|
7323
8159
|
// src/nodes/query-routes.ts
|
|
7324
|
-
var queryRoutes = new
|
|
8160
|
+
var queryRoutes = new Hono14();
|
|
7325
8161
|
queryRoutes.get("/changes", async (c) => {
|
|
7326
8162
|
const nestId = c.req.param("nestId");
|
|
7327
8163
|
const userId = c.get("userId");
|
|
@@ -7525,7 +8361,10 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7525
8361
|
hopsUsed = result.hopsUsed;
|
|
7526
8362
|
nodesTraversed = result.nodesTraversed;
|
|
7527
8363
|
}
|
|
7528
|
-
|
|
8364
|
+
const hopSeeds = [
|
|
8365
|
+
...new Map([...documents, ...titleMatches].map((d) => [d.id, d])).values()
|
|
8366
|
+
];
|
|
8367
|
+
if (hopSeeds.length > 0 && hops > 0) {
|
|
7529
8368
|
const byTitle = new Map(
|
|
7530
8369
|
allDocs.map((d) => [
|
|
7531
8370
|
String(d.frontmatter.title || "").toLowerCase(),
|
|
@@ -7543,9 +8382,13 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7543
8382
|
const t = byId.get(target) ?? byTitle.get(target.toLowerCase());
|
|
7544
8383
|
if (t && t.id !== d.id) link(d.id, t.id);
|
|
7545
8384
|
}
|
|
8385
|
+
for (const target of extractContextTargets(d.body || "")) {
|
|
8386
|
+
const t = byId.get(target);
|
|
8387
|
+
if (t && t.id !== d.id) link(d.id, t.id);
|
|
8388
|
+
}
|
|
7546
8389
|
}
|
|
7547
|
-
const seen = new Set(
|
|
7548
|
-
let frontier =
|
|
8390
|
+
const seen = new Set(hopSeeds.map((d) => d.id));
|
|
8391
|
+
let frontier = hopSeeds.map((d) => d.id);
|
|
7549
8392
|
for (let depth = 0; depth < hops && frontier.length; depth++) {
|
|
7550
8393
|
const next = [];
|
|
7551
8394
|
for (const id of frontier) {
|
|
@@ -7879,7 +8722,8 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
7879
8722
|
content: doc.content,
|
|
7880
8723
|
type: doc.type,
|
|
7881
8724
|
tags: doc.tags,
|
|
7882
|
-
scope: doc.scope
|
|
8725
|
+
scope: doc.scope,
|
|
8726
|
+
folder: doc.folder
|
|
7883
8727
|
},
|
|
7884
8728
|
userEmail
|
|
7885
8729
|
);
|
|
@@ -7911,10 +8755,13 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
7911
8755
|
});
|
|
7912
8756
|
|
|
7913
8757
|
// src/mcp/routes.ts
|
|
7914
|
-
import { Hono as
|
|
8758
|
+
import { Hono as Hono16 } from "hono";
|
|
7915
8759
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7916
8760
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7917
8761
|
|
|
8762
|
+
// src/mcp/tools.ts
|
|
8763
|
+
import { parseDocument as parseDocument4 } from "@promptowl/contextnest-engine";
|
|
8764
|
+
|
|
7918
8765
|
// src/telemetry/trace-log.ts
|
|
7919
8766
|
var PRUNE_EVERY = 500;
|
|
7920
8767
|
var insertsSincePrune = 0;
|
|
@@ -7923,8 +8770,8 @@ async function logTraceEvent(e) {
|
|
|
7923
8770
|
const db = getDb();
|
|
7924
8771
|
await db.run(
|
|
7925
8772
|
`INSERT INTO api_events
|
|
7926
|
-
(ts, kind, method, path, tool, nest_id, user_id, user_email, status, duration_ms)
|
|
7927
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8773
|
+
(ts, kind, method, path, tool, nest_id, user_id, user_email, status, duration_ms, client_json)
|
|
8774
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
7928
8775
|
[
|
|
7929
8776
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
7930
8777
|
e.kind,
|
|
@@ -7935,7 +8782,8 @@ async function logTraceEvent(e) {
|
|
|
7935
8782
|
e.userId ?? null,
|
|
7936
8783
|
e.userEmail ?? null,
|
|
7937
8784
|
e.status ?? null,
|
|
7938
|
-
e.durationMs ?? null
|
|
8785
|
+
e.durationMs ?? null,
|
|
8786
|
+
serializeClient(e.client)
|
|
7939
8787
|
]
|
|
7940
8788
|
);
|
|
7941
8789
|
if (++insertsSincePrune >= PRUNE_EVERY) {
|
|
@@ -7984,17 +8832,23 @@ async function listTraceEvents(filters = {}) {
|
|
|
7984
8832
|
LIMIT ? OFFSET ?`,
|
|
7985
8833
|
[...args, limit, offset]
|
|
7986
8834
|
);
|
|
7987
|
-
return {
|
|
8835
|
+
return {
|
|
8836
|
+
events: events.map(({ client_json, ...e }) => {
|
|
8837
|
+
const client = deserializeClient(client_json);
|
|
8838
|
+
return client ? { ...e, client } : e;
|
|
8839
|
+
}),
|
|
8840
|
+
total
|
|
8841
|
+
};
|
|
7988
8842
|
} catch {
|
|
7989
8843
|
return { events: [], total: 0 };
|
|
7990
8844
|
}
|
|
7991
8845
|
}
|
|
7992
8846
|
|
|
7993
8847
|
// src/mcp/workflow-tools.ts
|
|
7994
|
-
import { v4 as
|
|
8848
|
+
import { v4 as uuid11 } from "uuid";
|
|
7995
8849
|
|
|
7996
8850
|
// src/workflow/run-service.ts
|
|
7997
|
-
import { v4 as
|
|
8851
|
+
import { v4 as uuid9 } from "uuid";
|
|
7998
8852
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
7999
8853
|
async function flowSubgraph(nestId, agentNode) {
|
|
8000
8854
|
const db = getDb();
|
|
@@ -8124,7 +8978,7 @@ async function triggerRun(opts) {
|
|
|
8124
8978
|
}
|
|
8125
8979
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
8126
8980
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
8127
|
-
const id = `run_${
|
|
8981
|
+
const id = `run_${uuid9()}`;
|
|
8128
8982
|
const db = getDb();
|
|
8129
8983
|
await db.transaction(async (tx) => {
|
|
8130
8984
|
if (parentRunId) {
|
|
@@ -8310,8 +9164,8 @@ async function listRuns(nestId, opts) {
|
|
|
8310
9164
|
}
|
|
8311
9165
|
|
|
8312
9166
|
// src/workflow/edge-routes.ts
|
|
8313
|
-
import { Hono as
|
|
8314
|
-
import { v4 as
|
|
9167
|
+
import { Hono as Hono15 } from "hono";
|
|
9168
|
+
import { v4 as uuid10 } from "uuid";
|
|
8315
9169
|
function toResponse(e, t) {
|
|
8316
9170
|
return {
|
|
8317
9171
|
id: e.id,
|
|
@@ -8429,7 +9283,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
8429
9283
|
);
|
|
8430
9284
|
}
|
|
8431
9285
|
}
|
|
8432
|
-
var edgeRoutes = new
|
|
9286
|
+
var edgeRoutes = new Hono15();
|
|
8433
9287
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
8434
9288
|
const nestId = c.req.param("nestId");
|
|
8435
9289
|
const db = getDb();
|
|
@@ -8497,7 +9351,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
8497
9351
|
);
|
|
8498
9352
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
8499
9353
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8500
|
-
const id =
|
|
9354
|
+
const id = uuid10();
|
|
8501
9355
|
await db.transaction(async (tx) => {
|
|
8502
9356
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
8503
9357
|
if (type.is_flow) {
|
|
@@ -8898,7 +9752,7 @@ async function createEdge(ctx, args) {
|
|
|
8898
9752
|
);
|
|
8899
9753
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
8900
9754
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8901
|
-
const id =
|
|
9755
|
+
const id = uuid11();
|
|
8902
9756
|
await db.transaction(async (tx) => {
|
|
8903
9757
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
8904
9758
|
if (type.is_flow) {
|
|
@@ -8962,7 +9816,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
8962
9816
|
`INSERT INTO edge_types
|
|
8963
9817
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
8964
9818
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
8965
|
-
[
|
|
9819
|
+
[uuid11(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
8966
9820
|
);
|
|
8967
9821
|
}
|
|
8968
9822
|
});
|
|
@@ -9032,6 +9886,19 @@ function summarizeNode(node, body) {
|
|
|
9032
9886
|
body: body ?? void 0
|
|
9033
9887
|
};
|
|
9034
9888
|
}
|
|
9889
|
+
function pdfSourceLine(node, body) {
|
|
9890
|
+
if ((node.frontmatter.type || "document") !== "pdf") return null;
|
|
9891
|
+
let pdf = node.frontmatter.pdf;
|
|
9892
|
+
if (body && body.startsWith("---")) {
|
|
9893
|
+
try {
|
|
9894
|
+
pdf = parseDocument4(`${node.id}.md`, body, node.id).frontmatter.pdf ?? pdf;
|
|
9895
|
+
} catch {
|
|
9896
|
+
}
|
|
9897
|
+
}
|
|
9898
|
+
if (!pdf) return "**Source file:** PDF";
|
|
9899
|
+
const pages = `${pdf.pages} page${pdf.pages === 1 ? "" : "s"}`;
|
|
9900
|
+
return `**Source file:** PDF, ${pages}${pdf.text_layer ? "" : ", no text layer"}`;
|
|
9901
|
+
}
|
|
9035
9902
|
var TOOL_DEFINITIONS = [
|
|
9036
9903
|
{
|
|
9037
9904
|
name: "context_init",
|
|
@@ -9262,7 +10129,7 @@ var TOOL_DEFINITIONS = [
|
|
|
9262
10129
|
tags: {
|
|
9263
10130
|
type: "array",
|
|
9264
10131
|
items: { type: "string" },
|
|
9265
|
-
description: "
|
|
10132
|
+
description: "The node's full tag set \u2014 REPLACES the existing tags (same as the engine catalog). To add one, send the current tags plus the new one; to remove one, send the set without it."
|
|
9266
10133
|
},
|
|
9267
10134
|
status: {
|
|
9268
10135
|
type: "string",
|
|
@@ -9431,10 +10298,14 @@ var TOOL_DEFINITIONS = [
|
|
|
9431
10298
|
type: "object",
|
|
9432
10299
|
properties: {
|
|
9433
10300
|
title: { type: "string", description: "Title of the node to submit" },
|
|
10301
|
+
id: {
|
|
10302
|
+
type: "string",
|
|
10303
|
+
description: 'Node id / path (e.g. "nodes/gtm/deals/win-loss-log"). Use instead of title when two nodes share a title or the title is unknown.'
|
|
10304
|
+
},
|
|
9434
10305
|
note: { type: "string", description: "Optional note for the reviewer" },
|
|
9435
10306
|
priority: { type: "string", description: "Priority: low, normal (default), high, urgent" }
|
|
9436
|
-
}
|
|
9437
|
-
|
|
10307
|
+
}
|
|
10308
|
+
// One of id or title — see context_update.
|
|
9438
10309
|
}
|
|
9439
10310
|
},
|
|
9440
10311
|
{
|
|
@@ -9572,8 +10443,30 @@ async function resolveLlmBody(ctx, node, gate) {
|
|
|
9572
10443
|
async function handleToolCall(toolName, args, ctx) {
|
|
9573
10444
|
const started = Date.now();
|
|
9574
10445
|
let status = 200;
|
|
10446
|
+
let client;
|
|
10447
|
+
try {
|
|
10448
|
+
client = mergeClient(ctx.client, parseClientMetadata(args.client));
|
|
10449
|
+
} catch (err) {
|
|
10450
|
+
void logTraceEvent({
|
|
10451
|
+
kind: "mcp",
|
|
10452
|
+
tool: toolName,
|
|
10453
|
+
nestId: ctx.nestId,
|
|
10454
|
+
userId: ctx.userId,
|
|
10455
|
+
userEmail: ctx.userEmail,
|
|
10456
|
+
status: 400,
|
|
10457
|
+
durationMs: Date.now() - started
|
|
10458
|
+
});
|
|
10459
|
+
return toolError(err.message, "VALIDATION_FAILED");
|
|
10460
|
+
}
|
|
10461
|
+
const attributed = { ...ctx, client };
|
|
10462
|
+
if ((toolName === "context_create" || toolName === "context_update") && typeof args.content === "string" && /^\s*---\r?\n[\w-]+:/.test(args.content)) {
|
|
10463
|
+
return toolError(
|
|
10464
|
+
"`content` is the markdown body only \u2014 it starts with a YAML frontmatter block, which would be stored as body text. Drop the `---` block and pass tags/status as parameters; the server owns the frontmatter.",
|
|
10465
|
+
"VALIDATION_FAILED"
|
|
10466
|
+
);
|
|
10467
|
+
}
|
|
9575
10468
|
try {
|
|
9576
|
-
return await runTool(toolName, args,
|
|
10469
|
+
return await runTool(toolName, args, attributed);
|
|
9577
10470
|
} catch (err) {
|
|
9578
10471
|
if (isVaultLockTimeout(err) || err instanceof VaultBusyError) {
|
|
9579
10472
|
status = 503;
|
|
@@ -9592,7 +10485,10 @@ async function handleToolCall(toolName, args, ctx) {
|
|
|
9592
10485
|
userId: ctx.userId,
|
|
9593
10486
|
userEmail: ctx.userEmail,
|
|
9594
10487
|
status,
|
|
9595
|
-
durationMs: Date.now() - started
|
|
10488
|
+
durationMs: Date.now() - started,
|
|
10489
|
+
// Reads record attribution HERE — the activity trace is this edition's
|
|
10490
|
+
// §9.2 access trace, and a read seals no version to carry it.
|
|
10491
|
+
client
|
|
9596
10492
|
});
|
|
9597
10493
|
}
|
|
9598
10494
|
}
|
|
@@ -9787,7 +10683,8 @@ ${list}`,
|
|
|
9787
10683
|
const meta = [
|
|
9788
10684
|
`**Title:** ${node.frontmatter.title}`,
|
|
9789
10685
|
`**Type:** ${node.frontmatter.type || "document"}`,
|
|
9790
|
-
node.frontmatter.tags?.length ? `**Tags:** ${node.frontmatter.tags.join(" ")}` : null
|
|
10686
|
+
node.frontmatter.tags?.length ? `**Tags:** ${node.frontmatter.tags.join(" ")}` : null,
|
|
10687
|
+
pdfSourceLine(node, body)
|
|
9791
10688
|
].filter(Boolean).join("\n");
|
|
9792
10689
|
return {
|
|
9793
10690
|
text: `${meta}
|
|
@@ -10050,7 +10947,8 @@ ${sections}`;
|
|
|
10050
10947
|
type: args.type,
|
|
10051
10948
|
tags: args.tags,
|
|
10052
10949
|
scope: args.scope,
|
|
10053
|
-
folder: args.folder
|
|
10950
|
+
folder: args.folder,
|
|
10951
|
+
client: ctx.client
|
|
10054
10952
|
},
|
|
10055
10953
|
userEmail
|
|
10056
10954
|
);
|
|
@@ -10078,16 +10976,16 @@ ${sections}`;
|
|
|
10078
10976
|
"UNAUTHORIZED_ACTION"
|
|
10079
10977
|
);
|
|
10080
10978
|
}
|
|
10081
|
-
const mergedTags = args.tags ? [.../* @__PURE__ */ new Set([...node.frontmatter.tags || [], ...args.tags])] : void 0;
|
|
10082
10979
|
const { node: updated, version } = await updateNode(
|
|
10083
10980
|
nestId,
|
|
10084
10981
|
node.id,
|
|
10085
10982
|
{
|
|
10086
10983
|
content: args.content,
|
|
10087
10984
|
append: args.append,
|
|
10088
|
-
tags:
|
|
10985
|
+
tags: args.tags,
|
|
10089
10986
|
status: args.status,
|
|
10090
|
-
changeNote: args.changeNote
|
|
10987
|
+
changeNote: args.changeNote,
|
|
10988
|
+
client: ctx.client
|
|
10091
10989
|
},
|
|
10092
10990
|
userEmail
|
|
10093
10991
|
);
|
|
@@ -10140,10 +11038,11 @@ Use context_request_deletion to ask a nest owner/admin to delete it, with a reas
|
|
|
10140
11038
|
args.folder,
|
|
10141
11039
|
userEmail
|
|
10142
11040
|
);
|
|
11041
|
+
const data = { id: moved.id, previous_id: previousId, title: moved.frontmatter.title };
|
|
10143
11042
|
if (previousId === moved.id) {
|
|
10144
|
-
return `**${moved.frontmatter.title}** is already at \`${moved.id}\` \u2014 nothing to move
|
|
11043
|
+
return { text: `**${moved.frontmatter.title}** is already at \`${moved.id}\` \u2014 nothing to move.`, data };
|
|
10145
11044
|
}
|
|
10146
|
-
return `Moved **${moved.frontmatter.title}**: \`${previousId}\` \u2192 \`${moved.id}
|
|
11045
|
+
return { text: `Moved **${moved.frontmatter.title}**: \`${previousId}\` \u2192 \`${moved.id}\``, data };
|
|
10147
11046
|
}
|
|
10148
11047
|
// ─── Governance Tool Handlers ──────────────────────────────────────
|
|
10149
11048
|
case "context_stewards": {
|
|
@@ -10386,7 +11285,8 @@ Submit one with \`context_submit_review\`.`;
|
|
|
10386
11285
|
requestedBy: ctx.userEmail,
|
|
10387
11286
|
note: args.note,
|
|
10388
11287
|
priority: args.priority,
|
|
10389
|
-
baseUrl: ctx.baseUrl
|
|
11288
|
+
baseUrl: ctx.baseUrl,
|
|
11289
|
+
client: ctx.client
|
|
10390
11290
|
});
|
|
10391
11291
|
const resolved2 = await resolveStewardsForNode(
|
|
10392
11292
|
ctx.nestId,
|
|
@@ -10437,7 +11337,8 @@ ${resolved2.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` :
|
|
|
10437
11337
|
nodeId: node.id,
|
|
10438
11338
|
approvedBy: ctx.userEmail,
|
|
10439
11339
|
note: args.note,
|
|
10440
|
-
baseUrl: ctx.baseUrl
|
|
11340
|
+
baseUrl: ctx.baseUrl,
|
|
11341
|
+
client: ctx.client
|
|
10441
11342
|
});
|
|
10442
11343
|
return `Approved "${args.title}" v${request.version}. This version is now live for AI queries.${args.note ? `
|
|
10443
11344
|
Note: ${args.note}` : ""}`;
|
|
@@ -10478,7 +11379,9 @@ Reason: ${args.note}`;
|
|
|
10478
11379
|
};
|
|
10479
11380
|
}
|
|
10480
11381
|
const list = allVersions.map(
|
|
10481
|
-
(v) => `- **v${v.version}** ${v.status}${v.version === approved ? " (AI-active)" : ""} \u2014 by ${v.editedBy} at ${v.editedAt}${v.changeNote ? ` "${v.changeNote}"` : ""}
|
|
11382
|
+
(v) => `- **v${v.version}** ${v.status}${v.version === approved ? " (AI-active)" : ""} \u2014 by ${v.editedBy} at ${v.editedAt}${v.changeNote ? ` "${v.changeNote}"` : ""}${// §9.4: which agent wrote it. In the prose as well as the
|
|
11383
|
+
// structured payload — a chat client reads only this half.
|
|
11384
|
+
v.client?.agent ? ` [via ${v.client.agent}]` : ""}`
|
|
10482
11385
|
).join("\n");
|
|
10483
11386
|
return {
|
|
10484
11387
|
text: `# Version History: ${label}
|
|
@@ -10502,7 +11405,11 @@ ${list}`,
|
|
|
10502
11405
|
status: v.status,
|
|
10503
11406
|
edited_by: v.editedBy,
|
|
10504
11407
|
edited_at: v.editedAt,
|
|
10505
|
-
note: v.changeNote
|
|
11408
|
+
note: v.changeNote,
|
|
11409
|
+
// §9.4: which agent wrote this version, in which session. Omitted
|
|
11410
|
+
// (not `{}`) when the write carried no attribution — "not
|
|
11411
|
+
// attributed" and "attributed to nobody" are different claims.
|
|
11412
|
+
...v.client ? { client: v.client } : {}
|
|
10506
11413
|
}))
|
|
10507
11414
|
}
|
|
10508
11415
|
};
|
|
@@ -10563,22 +11470,12 @@ ${list}`,
|
|
|
10563
11470
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
10564
11471
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
10565
11472
|
try {
|
|
10566
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
10567
|
-
const db = (await import("./client-KEY4PYJH.js")).getDb();
|
|
11473
|
+
const { createGrant: createGrant2 } = await import("./grants-service-24GLUBY4.js");
|
|
10568
11474
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
10569
11475
|
const e = normalizeEmail2(String(args.email || ""));
|
|
10570
11476
|
if (!e) return "email is required.";
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
10574
|
-
const { v4: uuid20 } = await import("uuid");
|
|
10575
|
-
const id = uuid20();
|
|
10576
|
-
await db.run(
|
|
10577
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
10578
|
-
[id, e, null, await hashPassword2(uuid20())]
|
|
10579
|
-
);
|
|
10580
|
-
row = { id };
|
|
10581
|
-
}
|
|
11477
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
|
|
11478
|
+
const row = await findOrCreateInvitedUser2(e, { invitedBy: ctx.userEmail });
|
|
10582
11479
|
await createGrant2({
|
|
10583
11480
|
nestId: ctx.nestId,
|
|
10584
11481
|
targetType,
|
|
@@ -10613,7 +11510,8 @@ ${list}`;
|
|
|
10613
11510
|
const { nest, documents } = await syncUnsyncedFolder(
|
|
10614
11511
|
userId,
|
|
10615
11512
|
args.name,
|
|
10616
|
-
userEmail
|
|
11513
|
+
userEmail,
|
|
11514
|
+
ctx.client
|
|
10617
11515
|
);
|
|
10618
11516
|
return `Synced folder "${args.name}" \u2192 nest **${nest.name}** (${nest.id}) with ${documents} document(s).`;
|
|
10619
11517
|
} catch (err) {
|
|
@@ -10629,14 +11527,14 @@ ${list}`;
|
|
|
10629
11527
|
}
|
|
10630
11528
|
|
|
10631
11529
|
// src/mcp/routes.ts
|
|
10632
|
-
import { z } from "zod";
|
|
10633
|
-
var mcpRoutes = new
|
|
11530
|
+
import { z as z2 } from "zod";
|
|
11531
|
+
var mcpRoutes = new Hono16();
|
|
10634
11532
|
async function getUserEmail2(userId) {
|
|
10635
11533
|
const db = getDb();
|
|
10636
11534
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
10637
11535
|
return user?.email || "anonymous@localhost";
|
|
10638
11536
|
}
|
|
10639
|
-
function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
11537
|
+
function createMcpServerForNest(nestId, userId, userEmail, baseUrl, c) {
|
|
10640
11538
|
const server = new McpServer(
|
|
10641
11539
|
{ name: `contextnest-${nestId}`, version: "1.0.0" },
|
|
10642
11540
|
{ capabilities: { tools: {} } }
|
|
@@ -10647,16 +11545,18 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
10647
11545
|
const required = tool.inputSchema.required || [];
|
|
10648
11546
|
const shape = {};
|
|
10649
11547
|
for (const [key, def] of Object.entries(props)) {
|
|
10650
|
-
let
|
|
10651
|
-
if (def.type === "string")
|
|
10652
|
-
else if (def.type === "number")
|
|
10653
|
-
else if (def.type === "array")
|
|
10654
|
-
else
|
|
10655
|
-
if (!required.includes(key))
|
|
10656
|
-
shape[key] =
|
|
10657
|
-
}
|
|
11548
|
+
let field2;
|
|
11549
|
+
if (def.type === "string") field2 = z2.string();
|
|
11550
|
+
else if (def.type === "number") field2 = z2.number();
|
|
11551
|
+
else if (def.type === "array") field2 = z2.array(z2.string());
|
|
11552
|
+
else field2 = z2.any();
|
|
11553
|
+
if (!required.includes(key)) field2 = field2.optional();
|
|
11554
|
+
shape[key] = field2;
|
|
11555
|
+
}
|
|
11556
|
+
Object.assign(shape, clientToolField);
|
|
10658
11557
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
10659
11558
|
const engine = await engineCache.get(nestId);
|
|
11559
|
+
const client = attributeRequest(c, void 0, connectionClient(server));
|
|
10660
11560
|
const out = await handleToolCall(tool.name, args, {
|
|
10661
11561
|
storage: engine.storage,
|
|
10662
11562
|
queryEngine: engine.query,
|
|
@@ -10664,7 +11564,8 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
|
10664
11564
|
nestId,
|
|
10665
11565
|
userId,
|
|
10666
11566
|
userEmail,
|
|
10667
|
-
baseUrl
|
|
11567
|
+
baseUrl,
|
|
11568
|
+
client
|
|
10668
11569
|
});
|
|
10669
11570
|
return toCallToolResult(out);
|
|
10670
11571
|
});
|
|
@@ -10679,7 +11580,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
10679
11580
|
nestId,
|
|
10680
11581
|
userId,
|
|
10681
11582
|
userEmail,
|
|
10682
|
-
requestBaseUrl(c.req.url)
|
|
11583
|
+
requestBaseUrl(c.req.url),
|
|
11584
|
+
c
|
|
10683
11585
|
);
|
|
10684
11586
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
10685
11587
|
sessionIdGenerator: void 0,
|
|
@@ -10696,8 +11598,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
10696
11598
|
});
|
|
10697
11599
|
|
|
10698
11600
|
// src/workflow/run-routes.ts
|
|
10699
|
-
import { Hono as
|
|
10700
|
-
import { v4 as
|
|
11601
|
+
import { Hono as Hono17 } from "hono";
|
|
11602
|
+
import { v4 as uuid12 } from "uuid";
|
|
10701
11603
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
10702
11604
|
var safeJson2 = (s) => {
|
|
10703
11605
|
if (!s) return null;
|
|
@@ -10815,7 +11717,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
10815
11717
|
definitions
|
|
10816
11718
|
};
|
|
10817
11719
|
}
|
|
10818
|
-
var runTriggerRoutes = new
|
|
11720
|
+
var runTriggerRoutes = new Hono17();
|
|
10819
11721
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
10820
11722
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
10821
11723
|
throw new ValidationError(
|
|
@@ -10894,7 +11796,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10894
11796
|
}
|
|
10895
11797
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
10896
11798
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
10897
|
-
const id = `run_${
|
|
11799
|
+
const id = `run_${uuid12()}`;
|
|
10898
11800
|
await getDb().run(
|
|
10899
11801
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
10900
11802
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -10923,7 +11825,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
10923
11825
|
201
|
|
10924
11826
|
);
|
|
10925
11827
|
});
|
|
10926
|
-
var runListRoutes = new
|
|
11828
|
+
var runListRoutes = new Hono17();
|
|
10927
11829
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
10928
11830
|
const nestId = c.req.param("nestId");
|
|
10929
11831
|
const db = getDb();
|
|
@@ -10955,7 +11857,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
10955
11857
|
}))
|
|
10956
11858
|
});
|
|
10957
11859
|
});
|
|
10958
|
-
var runDetailRoutes = new
|
|
11860
|
+
var runDetailRoutes = new Hono17();
|
|
10959
11861
|
async function loadRunWithPermission(c, runId, required) {
|
|
10960
11862
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
10961
11863
|
runId
|
|
@@ -11106,8 +12008,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11106
12008
|
});
|
|
11107
12009
|
|
|
11108
12010
|
// src/workflow/schedule-routes.ts
|
|
11109
|
-
import { Hono as
|
|
11110
|
-
import { v4 as
|
|
12011
|
+
import { Hono as Hono18 } from "hono";
|
|
12012
|
+
import { v4 as uuid13 } from "uuid";
|
|
11111
12013
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
11112
12014
|
var MIN_EVERY = 5;
|
|
11113
12015
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -11144,7 +12046,7 @@ function parseEvery(v) {
|
|
|
11144
12046
|
}
|
|
11145
12047
|
return n;
|
|
11146
12048
|
}
|
|
11147
|
-
var scheduleRoutes = new
|
|
12049
|
+
var scheduleRoutes = new Hono18();
|
|
11148
12050
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11149
12051
|
const nestId = c.req.param("nestId");
|
|
11150
12052
|
const rows = await getDb().all(
|
|
@@ -11185,7 +12087,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
11185
12087
|
);
|
|
11186
12088
|
}
|
|
11187
12089
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11188
|
-
const id = `sch_${
|
|
12090
|
+
const id = `sch_${uuid13()}`;
|
|
11189
12091
|
await db.run(
|
|
11190
12092
|
`INSERT INTO schedules (id, nest_id, agent_node, every_minutes, enabled, last_run_at, max_runs, runs_created, created_by, created_at, updated_at)
|
|
11191
12093
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -11240,8 +12142,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11240
12142
|
});
|
|
11241
12143
|
|
|
11242
12144
|
// src/notify/connector-routes.ts
|
|
11243
|
-
import { Hono as
|
|
11244
|
-
import { v4 as
|
|
12145
|
+
import { Hono as Hono19 } from "hono";
|
|
12146
|
+
import { v4 as uuid14 } from "uuid";
|
|
11245
12147
|
var KNOWN_EVENTS = [
|
|
11246
12148
|
"review_requested",
|
|
11247
12149
|
"review_approved",
|
|
@@ -11288,7 +12190,7 @@ function parseUrl(v) {
|
|
|
11288
12190
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
11289
12191
|
);
|
|
11290
12192
|
}
|
|
11291
|
-
var connectorRoutes = new
|
|
12193
|
+
var connectorRoutes = new Hono19();
|
|
11292
12194
|
connectorRoutes.get("/", async (c) => {
|
|
11293
12195
|
const nestId = c.req.param("nestId");
|
|
11294
12196
|
const rows = await getDb().all(
|
|
@@ -11307,7 +12209,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
11307
12209
|
const url = parseUrl(body.url);
|
|
11308
12210
|
const events = parseEvents(body.events);
|
|
11309
12211
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11310
|
-
const id = `con_${
|
|
12212
|
+
const id = `con_${uuid14()}`;
|
|
11311
12213
|
await getDb().run(
|
|
11312
12214
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
11313
12215
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -11350,7 +12252,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
11350
12252
|
});
|
|
11351
12253
|
|
|
11352
12254
|
// src/workflow/hook-routes.ts
|
|
11353
|
-
import { Hono as
|
|
12255
|
+
import { Hono as Hono20 } from "hono";
|
|
11354
12256
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
11355
12257
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
11356
12258
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -11373,7 +12275,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
11373
12275
|
created_at: r.created_at
|
|
11374
12276
|
};
|
|
11375
12277
|
}
|
|
11376
|
-
var hookNestRoutes = new
|
|
12278
|
+
var hookNestRoutes = new Hono20();
|
|
11377
12279
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11378
12280
|
const nestId = c.req.param("nestId");
|
|
11379
12281
|
const agent = c.req.query("agent");
|
|
@@ -11447,7 +12349,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
11447
12349
|
function stripMentions(text) {
|
|
11448
12350
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
11449
12351
|
}
|
|
11450
|
-
var hookFireRoutes = new
|
|
12352
|
+
var hookFireRoutes = new Hono20();
|
|
11451
12353
|
hookFireRoutes.post("/:token", async (c) => {
|
|
11452
12354
|
const token = c.req.param("token");
|
|
11453
12355
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -11549,9 +12451,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
11549
12451
|
});
|
|
11550
12452
|
|
|
11551
12453
|
// src/definitions/routes.ts
|
|
11552
|
-
import { Hono as
|
|
11553
|
-
import { v4 as
|
|
11554
|
-
var definitionRoutes = new
|
|
12454
|
+
import { Hono as Hono21 } from "hono";
|
|
12455
|
+
import { v4 as uuid15 } from "uuid";
|
|
12456
|
+
var definitionRoutes = new Hono21();
|
|
11555
12457
|
definitionRoutes.get("/", async (c) => {
|
|
11556
12458
|
const nestId = c.req.param("nestId");
|
|
11557
12459
|
const db = getDb();
|
|
@@ -11630,7 +12532,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
11630
12532
|
await applyUpdate(tx, existing.id);
|
|
11631
12533
|
targetId = existing.id;
|
|
11632
12534
|
} else {
|
|
11633
|
-
targetId =
|
|
12535
|
+
targetId = uuid15();
|
|
11634
12536
|
wasCreated = true;
|
|
11635
12537
|
await tx.run(
|
|
11636
12538
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -11659,11 +12561,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
11659
12561
|
});
|
|
11660
12562
|
|
|
11661
12563
|
// src/nests/asset-routes.ts
|
|
11662
|
-
import { Hono as
|
|
11663
|
-
import { v4 as
|
|
11664
|
-
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
11665
|
-
import { join as
|
|
11666
|
-
var assetRoutes = new
|
|
12564
|
+
import { Hono as Hono22 } from "hono";
|
|
12565
|
+
import { v4 as uuid16 } from "uuid";
|
|
12566
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
12567
|
+
import { join as join6 } from "path";
|
|
12568
|
+
var assetRoutes = new Hono22();
|
|
11667
12569
|
var CONTENT_TYPES = {
|
|
11668
12570
|
png: "image/png",
|
|
11669
12571
|
jpg: "image/jpeg",
|
|
@@ -11709,10 +12611,10 @@ assetRoutes.post("/", async (c) => {
|
|
|
11709
12611
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
11710
12612
|
);
|
|
11711
12613
|
}
|
|
11712
|
-
const name = `${
|
|
11713
|
-
const dir =
|
|
12614
|
+
const name = `${uuid16()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
12615
|
+
const dir = join6(resolveNestPath(nestId), "assets");
|
|
11714
12616
|
await mkdir2(dir, { recursive: true });
|
|
11715
|
-
await writeFile(
|
|
12617
|
+
await writeFile(join6(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
11716
12618
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
11717
12619
|
return c.json(
|
|
11718
12620
|
{
|
|
@@ -11734,7 +12636,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
11734
12636
|
}
|
|
11735
12637
|
let bytes;
|
|
11736
12638
|
try {
|
|
11737
|
-
bytes = await
|
|
12639
|
+
bytes = await readFile2(join6(resolveNestPath(nestId), "assets", name));
|
|
11738
12640
|
} catch {
|
|
11739
12641
|
throw new NotFoundError("Asset not found");
|
|
11740
12642
|
}
|
|
@@ -11749,8 +12651,185 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
11749
12651
|
});
|
|
11750
12652
|
});
|
|
11751
12653
|
|
|
12654
|
+
// src/nodes/pdf-routes.ts
|
|
12655
|
+
import { Hono as Hono23 } from "hono";
|
|
12656
|
+
import { bodyLimit } from "hono/body-limit";
|
|
12657
|
+
import { createHash as createHash3 } from "crypto";
|
|
12658
|
+
import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
|
|
12659
|
+
var pdfNodeRoutes = new Hono23();
|
|
12660
|
+
var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
|
|
12661
|
+
function pdfMaxRequestBytes() {
|
|
12662
|
+
return config.PDF_MAX_BYTES + 1024 * 1024;
|
|
12663
|
+
}
|
|
12664
|
+
function tooLarge(bytes) {
|
|
12665
|
+
return new PayloadTooLargeError(
|
|
12666
|
+
`PDF too large (${(bytes / (1024 * 1024)).toFixed(1)} MB, max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12667
|
+
);
|
|
12668
|
+
}
|
|
12669
|
+
function field(body, name) {
|
|
12670
|
+
const v = body[name];
|
|
12671
|
+
const one = Array.isArray(v) ? v[0] : v;
|
|
12672
|
+
return typeof one === "string" ? one.trim() : "";
|
|
12673
|
+
}
|
|
12674
|
+
function tagsField(body) {
|
|
12675
|
+
const v = body["tags"];
|
|
12676
|
+
if (v === void 0) return void 0;
|
|
12677
|
+
const raw = (Array.isArray(v) ? v : [v]).filter((x) => typeof x === "string");
|
|
12678
|
+
const tags = raw.flatMap((s) => s.split(",")).map((t) => t.trim().replace(/^#/, "")).filter(Boolean);
|
|
12679
|
+
return tags;
|
|
12680
|
+
}
|
|
12681
|
+
function titleFromFilename(name) {
|
|
12682
|
+
return name.split(/[/\\]/).pop().replace(/\.pdf$/i, "").replace(/[_]+/g, " ").trim().slice(0, 200);
|
|
12683
|
+
}
|
|
12684
|
+
pdfNodeRoutes.post(
|
|
12685
|
+
"/pdf",
|
|
12686
|
+
(c, next) => bodyLimit({
|
|
12687
|
+
maxSize: pdfMaxRequestBytes(),
|
|
12688
|
+
onError: (c2) => c2.json(
|
|
12689
|
+
{
|
|
12690
|
+
error: `PDF too large (max ${config.PDF_MAX_BYTES / (1024 * 1024)} MB)`
|
|
12691
|
+
},
|
|
12692
|
+
413
|
|
12693
|
+
)
|
|
12694
|
+
})(c, next),
|
|
12695
|
+
async (c) => {
|
|
12696
|
+
const nestId = c.req.param("nestId");
|
|
12697
|
+
const lenStr = c.req.header("Content-Length");
|
|
12698
|
+
if (lenStr) {
|
|
12699
|
+
const len = parseInt(lenStr, 10);
|
|
12700
|
+
if (Number.isFinite(len) && len > pdfMaxRequestBytes()) throw tooLarge(len);
|
|
12701
|
+
}
|
|
12702
|
+
const body = await c.req.parseBody({ all: true });
|
|
12703
|
+
const fileField = Array.isArray(body["file"]) ? body["file"][0] : body["file"];
|
|
12704
|
+
if (!(fileField instanceof File)) {
|
|
12705
|
+
throw new ValidationError('multipart field "file" (a PDF) is required');
|
|
12706
|
+
}
|
|
12707
|
+
const file = fileField;
|
|
12708
|
+
if (file.size > config.PDF_MAX_BYTES) throw tooLarge(file.size);
|
|
12709
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
12710
|
+
if (!isPdf(bytes)) {
|
|
12711
|
+
throw new ValidationError("That file is not a PDF (it does not start with %PDF-).");
|
|
12712
|
+
}
|
|
12713
|
+
const userEmail = await resolveCallerEmail(c.get("userId"));
|
|
12714
|
+
const title = field(body, "title");
|
|
12715
|
+
const tags = tagsField(body);
|
|
12716
|
+
const client = attributeRequest(c, void 0);
|
|
12717
|
+
const pdf = { bytes, filename: file.name || void 0 };
|
|
12718
|
+
const targetId = field(body, "nodeId");
|
|
12719
|
+
if (targetId) {
|
|
12720
|
+
assertSafeNodeId(targetId);
|
|
12721
|
+
const changeNote = field(body, "changeNote") || "Replaced the PDF";
|
|
12722
|
+
const { node: node2, version: version2 } = await updateNode(
|
|
12723
|
+
nestId,
|
|
12724
|
+
targetId,
|
|
12725
|
+
{
|
|
12726
|
+
pdf,
|
|
12727
|
+
...title ? { title } : {},
|
|
12728
|
+
...tags ? { tags } : {},
|
|
12729
|
+
changeNote,
|
|
12730
|
+
client
|
|
12731
|
+
},
|
|
12732
|
+
userEmail
|
|
12733
|
+
);
|
|
12734
|
+
return c.json({ node: toNodeResponse(node2), version: version2 });
|
|
12735
|
+
}
|
|
12736
|
+
const folder = field(body, "folder");
|
|
12737
|
+
const { node, version } = await createNode(
|
|
12738
|
+
nestId,
|
|
12739
|
+
{
|
|
12740
|
+
title: title || titleFromFilename(file.name || "") || "Untitled PDF",
|
|
12741
|
+
content: "",
|
|
12742
|
+
type: PDF_NODE_TYPE,
|
|
12743
|
+
...tags ? { tags } : {},
|
|
12744
|
+
...folder ? { folder } : {},
|
|
12745
|
+
client,
|
|
12746
|
+
pdf
|
|
12747
|
+
},
|
|
12748
|
+
userEmail
|
|
12749
|
+
);
|
|
12750
|
+
const resolved = await resolveStewardsForNode(nestId, node.id);
|
|
12751
|
+
return c.json(
|
|
12752
|
+
{
|
|
12753
|
+
node: toNodeResponse(node),
|
|
12754
|
+
version,
|
|
12755
|
+
stewards: resolved.length > 0 ? resolved.map((r) => ({
|
|
12756
|
+
email: r.steward.userEmail,
|
|
12757
|
+
role: r.steward.role,
|
|
12758
|
+
source: r.source
|
|
12759
|
+
})) : void 0
|
|
12760
|
+
},
|
|
12761
|
+
201
|
|
12762
|
+
);
|
|
12763
|
+
}
|
|
12764
|
+
);
|
|
12765
|
+
pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
|
|
12766
|
+
const nestId = c.req.param("nestId");
|
|
12767
|
+
const nodeId = c.req.param("nodeId");
|
|
12768
|
+
const userId = c.get("userId");
|
|
12769
|
+
const userEmail = await resolveCallerEmail(userId);
|
|
12770
|
+
let node;
|
|
12771
|
+
try {
|
|
12772
|
+
node = await getNodeForCaller(nestId, { id: nodeId }, userId, userEmail);
|
|
12773
|
+
} catch {
|
|
12774
|
+
return next();
|
|
12775
|
+
}
|
|
12776
|
+
if (node.frontmatter.type !== PDF_NODE_TYPE) return next();
|
|
12777
|
+
const requested = c.req.query("version");
|
|
12778
|
+
if (requested !== void 0 && !/^[1-9]\d*$/.test(requested)) {
|
|
12779
|
+
throw new ValidationError("version must be a positive integer");
|
|
12780
|
+
}
|
|
12781
|
+
const approvedOnly = c.req.query("approved_only") === "1" || c.req.query("approved_only") === "true";
|
|
12782
|
+
let version;
|
|
12783
|
+
if (approvedOnly || await isPublicReader(nestId, userId)) {
|
|
12784
|
+
const approved = await getApprovedVersion(nestId, nodeId);
|
|
12785
|
+
if (approved == null) {
|
|
12786
|
+
throw new NotFoundError("This document has no approved version yet.");
|
|
12787
|
+
}
|
|
12788
|
+
version = approved;
|
|
12789
|
+
} else if (requested !== void 0) {
|
|
12790
|
+
version = Number(requested);
|
|
12791
|
+
}
|
|
12792
|
+
const { storage } = await engineCache.get(nestId);
|
|
12793
|
+
let bytes;
|
|
12794
|
+
try {
|
|
12795
|
+
bytes = await readPdfBinary3(
|
|
12796
|
+
storage,
|
|
12797
|
+
nodeId,
|
|
12798
|
+
version !== void 0 ? { version } : {}
|
|
12799
|
+
);
|
|
12800
|
+
} catch (err) {
|
|
12801
|
+
const code = err?.code;
|
|
12802
|
+
if (code === "INTEGRITY_ERROR") {
|
|
12803
|
+
console.error("[pdf] integrity failure serving", nestId, nodeId, version, err);
|
|
12804
|
+
throw new AppError(
|
|
12805
|
+
500,
|
|
12806
|
+
"This PDF no longer matches the version history's record of it, so it is not being served. Ask an admin to verify the nest."
|
|
12807
|
+
);
|
|
12808
|
+
}
|
|
12809
|
+
throw new NotFoundError(
|
|
12810
|
+
version !== void 0 ? `Version ${version} of this PDF was not found.` : "This document's PDF was not found."
|
|
12811
|
+
);
|
|
12812
|
+
}
|
|
12813
|
+
const etag = `"sha256:${createHash3("sha256").update(bytes).digest("hex")}"`;
|
|
12814
|
+
const leaf = (nodeId.split("/").pop() || "document").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
12815
|
+
const disposition = c.req.query("download") === "1" ? "attachment" : "inline";
|
|
12816
|
+
const headers = {
|
|
12817
|
+
"Content-Type": "application/pdf",
|
|
12818
|
+
"Content-Disposition": `${disposition}; filename="${leaf}.pdf"`,
|
|
12819
|
+
"Content-Security-Policy": "sandbox",
|
|
12820
|
+
"X-Content-Type-Options": "nosniff",
|
|
12821
|
+
"Cache-Control": "private, no-cache",
|
|
12822
|
+
ETag: etag
|
|
12823
|
+
};
|
|
12824
|
+
const ifNoneMatch = c.req.header("If-None-Match");
|
|
12825
|
+
if (ifNoneMatch && ifNoneMatch.split(",").map((t) => t.trim()).includes(etag)) {
|
|
12826
|
+
return c.body(null, 304, headers);
|
|
12827
|
+
}
|
|
12828
|
+
return c.body(new Uint8Array(bytes), 200, headers);
|
|
12829
|
+
});
|
|
12830
|
+
|
|
11752
12831
|
// src/auth/community-sites-routes.ts
|
|
11753
|
-
import { Hono as
|
|
12832
|
+
import { Hono as Hono24 } from "hono";
|
|
11754
12833
|
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
11755
12834
|
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
11756
12835
|
var serializeSite = (site) => ({
|
|
@@ -11762,7 +12841,7 @@ var serializeSite = (site) => ({
|
|
|
11762
12841
|
created_at: site.created_at,
|
|
11763
12842
|
updated_at: site.updated_at
|
|
11764
12843
|
});
|
|
11765
|
-
var communitySiteRoutes = new
|
|
12844
|
+
var communitySiteRoutes = new Hono24();
|
|
11766
12845
|
communitySiteRoutes.get("/", async (c) => {
|
|
11767
12846
|
if (!await serverAdminAllowed(c))
|
|
11768
12847
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
@@ -11850,10 +12929,10 @@ communitySiteRoutes.delete("/:id", async (c) => {
|
|
|
11850
12929
|
});
|
|
11851
12930
|
|
|
11852
12931
|
// src/mcp/server-routes.ts
|
|
11853
|
-
import { Hono as
|
|
12932
|
+
import { Hono as Hono25 } from "hono";
|
|
11854
12933
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11855
12934
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
11856
|
-
import { z as
|
|
12935
|
+
import { z as z3 } from "zod";
|
|
11857
12936
|
var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
11858
12937
|
"nest_create",
|
|
11859
12938
|
"context_create",
|
|
@@ -11973,116 +13052,39 @@ var NEST_ARG_OPTIONAL = {
|
|
|
11973
13052
|
description: "Target nest \u2014 id or name. Omit to search/list across ALL accessible nests."
|
|
11974
13053
|
}
|
|
11975
13054
|
};
|
|
13055
|
+
var SERVER_NEST_TOOLS = [
|
|
13056
|
+
// Fan-out reads — `nest` optional: omit it to span every accessible nest.
|
|
13057
|
+
...FANOUT_READ_TOOLS,
|
|
13058
|
+
// Targeted reads and writes — `nest` required.
|
|
13059
|
+
"context_get",
|
|
13060
|
+
"context_resolve",
|
|
13061
|
+
"context_comments",
|
|
13062
|
+
"context_versions",
|
|
13063
|
+
"context_create",
|
|
13064
|
+
"context_update",
|
|
13065
|
+
"context_delete",
|
|
13066
|
+
"context_move",
|
|
13067
|
+
"context_submit_review"
|
|
13068
|
+
];
|
|
13069
|
+
function withNestArg(tool) {
|
|
13070
|
+
const fanout = FANOUT_READ_TOOLS.has(tool.name);
|
|
13071
|
+
const schema = tool.inputSchema;
|
|
13072
|
+
return {
|
|
13073
|
+
name: tool.name,
|
|
13074
|
+
description: fanout ? `${tool.description} On this server-level endpoint, omit \`nest\` to run it across ALL accessible nests (each hit names its nest).` : `${tool.description} On this server-level endpoint, \`nest\` (id, name or slug) is required.`,
|
|
13075
|
+
inputSchema: {
|
|
13076
|
+
...schema,
|
|
13077
|
+
properties: { ...fanout ? NEST_ARG_OPTIONAL : NEST_ARG, ...schema.properties },
|
|
13078
|
+
required: [...fanout ? [] : ["nest"], ...schema.required ?? []]
|
|
13079
|
+
}
|
|
13080
|
+
};
|
|
13081
|
+
}
|
|
11976
13082
|
var SERVER_TOOL_DEFINITIONS = [
|
|
11977
13083
|
{
|
|
11978
13084
|
name: "nest_index",
|
|
11979
13085
|
description: "Grounded index of every nest these credentials can read: name, id, permission, document count. Call this FIRST.",
|
|
11980
13086
|
inputSchema: { type: "object", properties: {} }
|
|
11981
13087
|
},
|
|
11982
|
-
// ─── Fan-out reads (nest optional) ─────────────────────────────────────
|
|
11983
|
-
{
|
|
11984
|
-
name: "context_query",
|
|
11985
|
-
description: "Run a structured selector query. Omit `nest` to query ALL accessible nests. Selector: #tag, type:X, [[Title]], scope:X, +AND, |OR, -NOT.",
|
|
11986
|
-
inputSchema: {
|
|
11987
|
-
type: "object",
|
|
11988
|
-
properties: {
|
|
11989
|
-
...NEST_ARG_OPTIONAL,
|
|
11990
|
-
query: { type: "string", description: "Selector query" },
|
|
11991
|
-
hops: {
|
|
11992
|
-
type: "number",
|
|
11993
|
-
description: "Graph traversal depth from the matches (default: 2)"
|
|
11994
|
-
}
|
|
11995
|
-
},
|
|
11996
|
-
required: ["query"]
|
|
11997
|
-
}
|
|
11998
|
-
},
|
|
11999
|
-
{
|
|
12000
|
-
name: "context_search",
|
|
12001
|
-
description: "Full-text keyword search. Omit `nest` to search ALL accessible nests. Returns count and first `limit` hits.",
|
|
12002
|
-
inputSchema: {
|
|
12003
|
-
type: "object",
|
|
12004
|
-
properties: {
|
|
12005
|
-
...NEST_ARG_OPTIONAL,
|
|
12006
|
-
query: { type: "string", description: "Search terms" },
|
|
12007
|
-
limit: {
|
|
12008
|
-
type: "number",
|
|
12009
|
-
description: "Max hits to return (default 20, max 200)"
|
|
12010
|
-
}
|
|
12011
|
-
},
|
|
12012
|
-
required: ["query"]
|
|
12013
|
-
}
|
|
12014
|
-
},
|
|
12015
|
-
{
|
|
12016
|
-
name: "context_list",
|
|
12017
|
-
description: "Browse contents with optional type, tag, or limit filters. Omit `nest` to list ALL accessible nests.",
|
|
12018
|
-
inputSchema: {
|
|
12019
|
-
type: "object",
|
|
12020
|
-
properties: {
|
|
12021
|
-
...NEST_ARG_OPTIONAL,
|
|
12022
|
-
type: { type: "string", description: "Filter by node type" },
|
|
12023
|
-
tag: { type: "string", description: "Filter by tag" },
|
|
12024
|
-
limit: { type: "number", description: "Max nodes to return" }
|
|
12025
|
-
}
|
|
12026
|
-
}
|
|
12027
|
-
},
|
|
12028
|
-
{
|
|
12029
|
-
name: "context_overview",
|
|
12030
|
-
description: "Map of nests: node count, types, tags, title+snippet per node. Omit `nest` for ALL accessible nests.",
|
|
12031
|
-
inputSchema: {
|
|
12032
|
-
type: "object",
|
|
12033
|
-
properties: { ...NEST_ARG_OPTIONAL }
|
|
12034
|
-
}
|
|
12035
|
-
},
|
|
12036
|
-
// ─── Targeted reads (nest required) ────────────────────────────────────
|
|
12037
|
-
{
|
|
12038
|
-
name: "context_get",
|
|
12039
|
-
description: "Get the FULL content of a node in a nest by title or id.",
|
|
12040
|
-
inputSchema: {
|
|
12041
|
-
type: "object",
|
|
12042
|
-
properties: {
|
|
12043
|
-
...NEST_ARG,
|
|
12044
|
-
title: { type: "string", description: "Title of the node" },
|
|
12045
|
-
id: { type: "string", description: "ID of the node" }
|
|
12046
|
-
},
|
|
12047
|
-
required: ["nest"]
|
|
12048
|
-
}
|
|
12049
|
-
},
|
|
12050
|
-
{
|
|
12051
|
-
name: "context_resolve",
|
|
12052
|
-
description: "Full deterministic context resolution against one nest \u2014 selector + hops + token budget, complete node bodies.",
|
|
12053
|
-
inputSchema: {
|
|
12054
|
-
type: "object",
|
|
12055
|
-
properties: {
|
|
12056
|
-
...NEST_ARG,
|
|
12057
|
-
selector: { type: "string", description: "Selector query string" },
|
|
12058
|
-
max_tokens: {
|
|
12059
|
-
type: "number",
|
|
12060
|
-
description: "Approximate token budget (default: 8000)"
|
|
12061
|
-
},
|
|
12062
|
-
hops: {
|
|
12063
|
-
type: "number",
|
|
12064
|
-
description: "Graph traversal depth from the matches (default: 2)"
|
|
12065
|
-
}
|
|
12066
|
-
},
|
|
12067
|
-
required: ["nest", "selector"]
|
|
12068
|
-
}
|
|
12069
|
-
},
|
|
12070
|
-
{
|
|
12071
|
-
// Read-only, so it belongs on this endpoint: an agent reading a nest's
|
|
12072
|
-
// documents across nests also needs the feedback left on them. Writing a
|
|
12073
|
-
// comment stays on the per-nest endpoint with the other write tools.
|
|
12074
|
-
name: "context_comments",
|
|
12075
|
-
description: "Get the review comments/annotations on a document in a nest \u2014 anchored quotes, threaded replies, and open/resolved status. Use to pull human feedback into context before acting on a document.",
|
|
12076
|
-
inputSchema: {
|
|
12077
|
-
type: "object",
|
|
12078
|
-
properties: {
|
|
12079
|
-
nest: { type: "string", description: "Nest name or id (see nest_index)" },
|
|
12080
|
-
title: { type: "string", description: "Title of the node" },
|
|
12081
|
-
id: { type: "string", description: "ID of the node" }
|
|
12082
|
-
},
|
|
12083
|
-
required: ["nest"]
|
|
12084
|
-
}
|
|
12085
|
-
},
|
|
12086
13088
|
{
|
|
12087
13089
|
name: "nest_create",
|
|
12088
13090
|
description: "Create a new nest owned by the caller. Returns the new nest's id \u2014 target it with the other tools afterwards.",
|
|
@@ -12095,93 +13097,18 @@ var SERVER_TOOL_DEFINITIONS = [
|
|
|
12095
13097
|
required: ["name"]
|
|
12096
13098
|
}
|
|
12097
13099
|
},
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
name
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
type: "object",
|
|
12104
|
-
properties: {
|
|
12105
|
-
...NEST_ARG,
|
|
12106
|
-
title: { type: "string", description: "Descriptive title" },
|
|
12107
|
-
content: { type: "string", description: "Markdown content body" },
|
|
12108
|
-
type: { type: "string", description: "Node type (default: document)" },
|
|
12109
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
12110
|
-
scope: { type: "string", description: "Visibility scope" },
|
|
12111
|
-
folder: {
|
|
12112
|
-
type: "string",
|
|
12113
|
-
description: 'Folder path under nodes/ (e.g. "gtm/deals"); segments are slugified'
|
|
12114
|
-
}
|
|
12115
|
-
},
|
|
12116
|
-
required: ["nest", "title", "content"]
|
|
12117
|
-
}
|
|
12118
|
-
},
|
|
12119
|
-
{
|
|
12120
|
-
name: "context_update",
|
|
12121
|
-
description: "Update an existing node by title or id. Needs write permission. Use `content` to replace, `append` to add.",
|
|
12122
|
-
inputSchema: {
|
|
12123
|
-
type: "object",
|
|
12124
|
-
properties: {
|
|
12125
|
-
...NEST_ARG,
|
|
12126
|
-
title: { type: "string", description: "Title of node to update" },
|
|
12127
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12128
|
-
content: { type: "string", description: "New content (replaces)" },
|
|
12129
|
-
append: { type: "string", description: "Content to append" },
|
|
12130
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags to add" },
|
|
12131
|
-
status: { type: "string", description: "New status: draft|pending_review|approved|published" },
|
|
12132
|
-
changeNote: { type: "string", description: "Why this changed \u2014 recorded in history" }
|
|
12133
|
-
},
|
|
12134
|
-
required: ["nest"]
|
|
12135
|
-
}
|
|
12136
|
-
},
|
|
12137
|
-
{
|
|
12138
|
-
name: "context_delete",
|
|
12139
|
-
description: "Delete a node by title or id. Needs write permission. Irreversible.",
|
|
12140
|
-
inputSchema: {
|
|
12141
|
-
type: "object",
|
|
12142
|
-
properties: {
|
|
12143
|
-
...NEST_ARG,
|
|
12144
|
-
title: { type: "string", description: "Title of node to delete" },
|
|
12145
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" }
|
|
12146
|
-
},
|
|
12147
|
-
required: ["nest"]
|
|
12148
|
-
}
|
|
12149
|
-
},
|
|
12150
|
-
{
|
|
12151
|
-
name: "context_move",
|
|
12152
|
-
description: "Move a node to a different folder. The id changes; history and links follow. Needs write permission.",
|
|
12153
|
-
inputSchema: {
|
|
12154
|
-
type: "object",
|
|
12155
|
-
properties: {
|
|
12156
|
-
...NEST_ARG,
|
|
12157
|
-
title: { type: "string", description: "Title of node to move" },
|
|
12158
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12159
|
-
folder: { type: "string", description: 'Destination folder (e.g. "gtm/deals"); "" for root' }
|
|
12160
|
-
},
|
|
12161
|
-
required: ["nest", "folder"]
|
|
12162
|
-
}
|
|
12163
|
-
},
|
|
12164
|
-
{
|
|
12165
|
-
name: "context_submit_review",
|
|
12166
|
-
description: "Submit a node for steward review. Stewards will be notified. Use on governed nests.",
|
|
12167
|
-
inputSchema: {
|
|
12168
|
-
type: "object",
|
|
12169
|
-
properties: {
|
|
12170
|
-
...NEST_ARG,
|
|
12171
|
-
title: { type: "string", description: "Title of node to submit" },
|
|
12172
|
-
id: { type: "string", description: "Node id (use when title is ambiguous)" },
|
|
12173
|
-
note: { type: "string", description: "Note for the reviewer" },
|
|
12174
|
-
priority: { type: "string", description: "Priority: low, normal, high, urgent" }
|
|
12175
|
-
},
|
|
12176
|
-
required: ["nest"]
|
|
12177
|
-
}
|
|
12178
|
-
}
|
|
13100
|
+
...SERVER_NEST_TOOLS.map((name) => {
|
|
13101
|
+
const tool = TOOL_DEFINITIONS.find((t) => t.name === name);
|
|
13102
|
+
if (!tool) throw new Error(`server-level tool ${name} has no per-nest definition`);
|
|
13103
|
+
return withNestArg(tool);
|
|
13104
|
+
})
|
|
12179
13105
|
];
|
|
12180
|
-
async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl) {
|
|
13106
|
+
async function handleServerToolCall(toolName, args, userId, userEmail, nestScope, baseUrl, client) {
|
|
12181
13107
|
if (toolName === "nest_index") {
|
|
12182
13108
|
const nests = await buildNestIndex(userId, nestScope);
|
|
12183
|
-
|
|
12184
|
-
|
|
13109
|
+
const data = { nests };
|
|
13110
|
+
if (!nests.length) return { text: "No nests accessible with these credentials.", data };
|
|
13111
|
+
return { text: renderNestIndexMarkdown(nests, baseUrl), data };
|
|
12185
13112
|
}
|
|
12186
13113
|
if (SERVER_WRITE_TOOLS.has(toolName)) {
|
|
12187
13114
|
if (isSuspended()) {
|
|
@@ -12204,7 +13131,7 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12204
13131
|
);
|
|
12205
13132
|
return `Created nest "${nest2.name}" (id: ${nest2.id}). Add documents with context_create, retrieve with the other tools.`;
|
|
12206
13133
|
} catch (err) {
|
|
12207
|
-
if (err instanceof ValidationError) return err.message;
|
|
13134
|
+
if (err instanceof ValidationError || err instanceof ForbiddenError) return err.message;
|
|
12208
13135
|
throw err;
|
|
12209
13136
|
}
|
|
12210
13137
|
}
|
|
@@ -12213,9 +13140,6 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12213
13140
|
const nests = await buildNestIndex(userId, nestScope);
|
|
12214
13141
|
if (!nests.length) return "No nests accessible with these credentials.";
|
|
12215
13142
|
const nonEmpty = nests.filter((n) => (n.document_count ?? 0) > 0);
|
|
12216
|
-
if (!nonEmpty.length) {
|
|
12217
|
-
return "All accessible nests are empty. Use context_create to add documents first.";
|
|
12218
|
-
}
|
|
12219
13143
|
const results = await Promise.allSettled(
|
|
12220
13144
|
nonEmpty.map(async (n) => {
|
|
12221
13145
|
const engine2 = await engineCache.get(n.id);
|
|
@@ -12226,25 +13150,66 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12226
13150
|
nestId: n.id,
|
|
12227
13151
|
userId,
|
|
12228
13152
|
userEmail,
|
|
12229
|
-
baseUrl
|
|
13153
|
+
baseUrl,
|
|
13154
|
+
// Same attribution the single-nest branch below passes (§9.4). A
|
|
13155
|
+
// fan-out read is still one call per nest on the trace, and each row
|
|
13156
|
+
// has to name the caller — without this the derived agent/session is
|
|
13157
|
+
// dropped and only an explicit `client` in the args survives.
|
|
13158
|
+
client
|
|
12230
13159
|
});
|
|
12231
13160
|
return { nest: n, result };
|
|
12232
13161
|
})
|
|
12233
13162
|
);
|
|
12234
13163
|
const parts = [];
|
|
13164
|
+
let firstError;
|
|
13165
|
+
let firstRejection;
|
|
13166
|
+
const tag = (n, items) => Array.isArray(items) ? items.map((it) => ({ ...it, nest: { id: n.id, name: n.name } })) : [];
|
|
13167
|
+
const perNestDocs = [];
|
|
13168
|
+
const perNestHits = [];
|
|
13169
|
+
let count = 0;
|
|
13170
|
+
let nodesTraversed = 0;
|
|
13171
|
+
let hopsUsed = 0;
|
|
13172
|
+
let mode;
|
|
12235
13173
|
for (const r of results) {
|
|
12236
|
-
if (r.status
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
13174
|
+
if (r.status !== "fulfilled") {
|
|
13175
|
+
firstRejection ??= r;
|
|
13176
|
+
continue;
|
|
13177
|
+
}
|
|
13178
|
+
const { nest: n, result } = r.value;
|
|
13179
|
+
if (typeof result !== "string" && result.isError) {
|
|
13180
|
+
firstError ??= result;
|
|
13181
|
+
continue;
|
|
13182
|
+
}
|
|
13183
|
+
parts.push(`## ${n.name} (${n.id})
|
|
12240
13184
|
|
|
12241
|
-
${text}`);
|
|
13185
|
+
${typeof result === "string" ? result : result.text}`);
|
|
13186
|
+
const d = typeof result === "string" ? void 0 : result.data;
|
|
13187
|
+
if (!d) continue;
|
|
13188
|
+
perNestDocs.push(tag(n, d.documents));
|
|
13189
|
+
perNestHits.push(tag(n, d.results));
|
|
13190
|
+
count += d.count ?? 0;
|
|
13191
|
+
nodesTraversed += d.traversal?.nodes_traversed ?? 0;
|
|
13192
|
+
hopsUsed = Math.max(hopsUsed, d.traversal?.hops_used ?? 0);
|
|
13193
|
+
mode ??= d.traversal?.mode;
|
|
13194
|
+
}
|
|
13195
|
+
const interleave = (lists) => {
|
|
13196
|
+
const out = [];
|
|
13197
|
+
for (let i = 0; lists.some((l) => i < l.length); i++) {
|
|
13198
|
+
for (const l of lists) if (i < l.length) out.push(l[i]);
|
|
12242
13199
|
}
|
|
13200
|
+
return out;
|
|
13201
|
+
};
|
|
13202
|
+
if (!parts.length && firstError) return firstError;
|
|
13203
|
+
if (!parts.length && firstRejection) throw firstRejection.reason;
|
|
13204
|
+
const documents = interleave(perNestDocs);
|
|
13205
|
+
const listLimit = typeof args.limit === "number" && args.limit >= 1 ? Math.floor(args.limit) : 50;
|
|
13206
|
+
const data = toolName === "context_search" ? { results: interleave(perNestHits).slice(0, normalizeSearchLimit(args.limit)), count } : toolName === "context_query" ? { documents, traversal: { mode: mode ?? "selector", hops_used: hopsUsed, nodes_traversed: nodesTraversed } } : { documents: documents.slice(0, listLimit) };
|
|
13207
|
+
const reply = (text) => toolName === "context_overview" ? text : { text, data };
|
|
13208
|
+
if (!nonEmpty.length) {
|
|
13209
|
+
return reply("All accessible nests are empty. Use context_create to add documents first.");
|
|
12243
13210
|
}
|
|
12244
|
-
if (!parts.length)
|
|
12245
|
-
|
|
12246
|
-
}
|
|
12247
|
-
return parts.join("\n\n---\n\n");
|
|
13211
|
+
if (!parts.length) return reply("No results found across any accessible nests.");
|
|
13212
|
+
return reply(parts.join("\n\n---\n\n"));
|
|
12248
13213
|
}
|
|
12249
13214
|
const inaccessible = `Nest not found or not accessible: "${nestArg}". Use nest_index to see what these credentials can read.`;
|
|
12250
13215
|
if (!nestArg) {
|
|
@@ -12262,10 +13227,11 @@ ${text}`);
|
|
|
12262
13227
|
nestId: nest.id,
|
|
12263
13228
|
userId,
|
|
12264
13229
|
userEmail,
|
|
12265
|
-
baseUrl
|
|
13230
|
+
baseUrl,
|
|
13231
|
+
client
|
|
12266
13232
|
});
|
|
12267
13233
|
}
|
|
12268
|
-
function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
13234
|
+
function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
12269
13235
|
const server = new McpServer2(
|
|
12270
13236
|
{ name: "contextnest-server", version: "1.0.0" },
|
|
12271
13237
|
{ capabilities: { tools: {} } }
|
|
@@ -12275,14 +13241,15 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
12275
13241
|
const required = tool.inputSchema.required || [];
|
|
12276
13242
|
const shape = {};
|
|
12277
13243
|
for (const [key, def] of Object.entries(props)) {
|
|
12278
|
-
let
|
|
12279
|
-
if (def.type === "string")
|
|
12280
|
-
else if (def.type === "number")
|
|
12281
|
-
else if (def.type === "array")
|
|
12282
|
-
else
|
|
12283
|
-
if (!required.includes(key))
|
|
12284
|
-
shape[key] =
|
|
12285
|
-
}
|
|
13244
|
+
let field2;
|
|
13245
|
+
if (def.type === "string") field2 = z3.string();
|
|
13246
|
+
else if (def.type === "number") field2 = z3.number();
|
|
13247
|
+
else if (def.type === "array") field2 = z3.array(z3.string());
|
|
13248
|
+
else field2 = z3.any();
|
|
13249
|
+
if (!required.includes(key)) field2 = field2.optional();
|
|
13250
|
+
shape[key] = field2;
|
|
13251
|
+
}
|
|
13252
|
+
Object.assign(shape, clientToolField);
|
|
12286
13253
|
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
12287
13254
|
const out = await handleServerToolCall(
|
|
12288
13255
|
tool.name,
|
|
@@ -12290,14 +13257,15 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
12290
13257
|
userId,
|
|
12291
13258
|
userEmail,
|
|
12292
13259
|
nestScope,
|
|
12293
|
-
baseUrl
|
|
13260
|
+
baseUrl,
|
|
13261
|
+
attributeRequest(c, void 0, connectionClient(server))
|
|
12294
13262
|
);
|
|
12295
13263
|
return toCallToolResult(out);
|
|
12296
13264
|
});
|
|
12297
13265
|
}
|
|
12298
13266
|
return server;
|
|
12299
13267
|
}
|
|
12300
|
-
var serverMcpRoutes = new
|
|
13268
|
+
var serverMcpRoutes = new Hono25();
|
|
12301
13269
|
serverMcpRoutes.all("/", async (c) => {
|
|
12302
13270
|
const userId = c.get("userId");
|
|
12303
13271
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -12305,7 +13273,8 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12305
13273
|
userId,
|
|
12306
13274
|
userEmail,
|
|
12307
13275
|
c.get("nestScope"),
|
|
12308
|
-
requestBaseUrl(c.req.url)
|
|
13276
|
+
requestBaseUrl(c.req.url),
|
|
13277
|
+
c
|
|
12309
13278
|
);
|
|
12310
13279
|
const transport = new WebStandardStreamableHTTPServerTransport2({
|
|
12311
13280
|
sessionIdGenerator: void 0,
|
|
@@ -12319,7 +13288,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12319
13288
|
await server.server.close();
|
|
12320
13289
|
}
|
|
12321
13290
|
});
|
|
12322
|
-
var nestIndexRoutes = new
|
|
13291
|
+
var nestIndexRoutes = new Hono25();
|
|
12323
13292
|
nestIndexRoutes.get("/", async (c) => {
|
|
12324
13293
|
const userId = c.get("userId");
|
|
12325
13294
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -12413,6 +13382,20 @@ Write endpoints (need write permission):
|
|
|
12413
13382
|
- \`GET ${B}/me/drafts\` \u2014 your documents never submitted for review
|
|
12414
13383
|
- \`GET ${B}/me/notifications\` \u2014 watcher and review notifications
|
|
12415
13384
|
|
|
13385
|
+
## Say who you are: \`client\`
|
|
13386
|
+
|
|
13387
|
+
Every write body and every MCP tool call takes an optional
|
|
13388
|
+
\`client\` object \u2014 \`{ "agent": "claude-code", "session_id": "s-9f2" }\`, plus any
|
|
13389
|
+
custom scalar keys. A write that seals a version records it on that version
|
|
13390
|
+
(\`GET /nodes/<id>/versions\` and \`context_versions\` serve it back); every call
|
|
13391
|
+
records it on this server's activity trace, which is how a READ gets attributed.
|
|
13392
|
+
|
|
13393
|
+
Send it. Nothing requires it and no call is refused for omitting it, but a vault
|
|
13394
|
+
serving several agents cannot otherwise tell which of you wrote what. It is a
|
|
13395
|
+
label, never an identity claim: it is not authenticated, grants nothing, and is
|
|
13396
|
+
never part of a hash. Bounded \u2014 scalar values up to 512 chars, at most 16 custom
|
|
13397
|
+
keys beyond \`agent\` and \`session_id\`.
|
|
13398
|
+
|
|
12416
13399
|
## MCP (Model Context Protocol)
|
|
12417
13400
|
|
|
12418
13401
|
Streamable HTTP, stateless. Two endpoints:
|
|
@@ -12421,7 +13404,7 @@ Streamable HTTP, stateless. Two endpoints:
|
|
|
12421
13404
|
Tools: \`nest_index\` (call first), then:
|
|
12422
13405
|
- Fan-out reads (omit \`nest\` to search/list ALL nests): \`context_search\`,
|
|
12423
13406
|
\`context_list\`, \`context_query\`, \`context_overview\`
|
|
12424
|
-
- Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`
|
|
13407
|
+
- Targeted reads (\`nest\` required): \`context_get\`, \`context_resolve\`, \`context_comments\`, \`context_versions\`
|
|
12425
13408
|
- Writes (\`nest\` required): \`nest_create\`, \`context_create\`, \`context_update\`,
|
|
12426
13409
|
\`context_delete\`, \`context_move\`, \`context_submit_review\`
|
|
12427
13410
|
- \`${B}/nests/<nest-id>/mcp\` \u2014 per-nest, the full toolset. Same names as the
|
|
@@ -12458,9 +13441,11 @@ on the command line.
|
|
|
12458
13441
|
export CONTEXTNEST_API_KEY=<api-key>
|
|
12459
13442
|
# every nest these credentials can read, through the server-level endpoint
|
|
12460
13443
|
ctx vault add contextnest --url ${B}/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
12461
|
-
#
|
|
13444
|
+
ctx vault list # lists contextnest/<nest> for each nest
|
|
13445
|
+
ctx search "pricing" --vault contextnest # reads span every nest
|
|
13446
|
+
ctx update nodes/x --tags a,b --vault contextnest/<nest> # a write names one nest
|
|
13447
|
+
# or pin one nest by its own endpoint
|
|
12462
13448
|
ctx vault add mynest --url ${B}/nests/<nest-id>/mcp --bearer-env CONTEXTNEST_API_KEY
|
|
12463
|
-
ctx query "#gtm" --vault mynest
|
|
12464
13449
|
\`\`\`
|
|
12465
13450
|
|
|
12466
13451
|
Servers in "open" auth mode: drop \`--bearer-env\`. Pushing a local folder up
|
|
@@ -12479,14 +13464,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
12479
13464
|
through steward review before they become AI-visible.
|
|
12480
13465
|
`;
|
|
12481
13466
|
}
|
|
12482
|
-
var llmsTxtRoutes = new
|
|
13467
|
+
var llmsTxtRoutes = new Hono25();
|
|
12483
13468
|
llmsTxtRoutes.get("/", (c) => {
|
|
12484
13469
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
12485
13470
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
12486
13471
|
});
|
|
12487
13472
|
|
|
12488
13473
|
// src/governance/routes.ts
|
|
12489
|
-
import { Hono as
|
|
13474
|
+
import { Hono as Hono26 } from "hono";
|
|
12490
13475
|
|
|
12491
13476
|
// src/governance/feed-service.ts
|
|
12492
13477
|
var SNIPPET_CHARS = 200;
|
|
@@ -12617,7 +13602,7 @@ async function getNestFeed(params) {
|
|
|
12617
13602
|
}
|
|
12618
13603
|
|
|
12619
13604
|
// src/governance/comment-service.ts
|
|
12620
|
-
import { v4 as
|
|
13605
|
+
import { v4 as uuid17 } from "uuid";
|
|
12621
13606
|
async function createComment(params) {
|
|
12622
13607
|
const db = getDb();
|
|
12623
13608
|
const body = (params.body ?? "").trim();
|
|
@@ -12633,7 +13618,7 @@ async function createComment(params) {
|
|
|
12633
13618
|
throw new Error("Parent comment not found on this node");
|
|
12634
13619
|
}
|
|
12635
13620
|
}
|
|
12636
|
-
const id =
|
|
13621
|
+
const id = uuid17();
|
|
12637
13622
|
await db.run(
|
|
12638
13623
|
`INSERT INTO comments
|
|
12639
13624
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -12833,7 +13818,7 @@ function rowToComment(row) {
|
|
|
12833
13818
|
|
|
12834
13819
|
// src/governance/stewards-parser.ts
|
|
12835
13820
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
12836
|
-
import { join as
|
|
13821
|
+
import { join as join7 } from "path";
|
|
12837
13822
|
function parseStewardsYaml(content) {
|
|
12838
13823
|
const result = { version: 1 };
|
|
12839
13824
|
const lines = content.split("\n");
|
|
@@ -12899,9 +13884,9 @@ function parseEntry(str) {
|
|
|
12899
13884
|
function loadStewardsConfig(nestId) {
|
|
12900
13885
|
const nestPath = resolveNestPath(nestId);
|
|
12901
13886
|
const candidates = [
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
13887
|
+
join7(nestPath, "stewards.yaml"),
|
|
13888
|
+
join7(nestPath, "stewards.yml"),
|
|
13889
|
+
join7(nestPath, ".context", "stewards.yaml")
|
|
12905
13890
|
];
|
|
12906
13891
|
for (const candidatePath of candidates) {
|
|
12907
13892
|
if (existsSync(candidatePath)) {
|
|
@@ -12918,7 +13903,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
12918
13903
|
if (Number.isNaN(n) || n < 1) return def;
|
|
12919
13904
|
return Math.min(n, max);
|
|
12920
13905
|
}
|
|
12921
|
-
var governanceRoutes = new
|
|
13906
|
+
var governanceRoutes = new Hono26();
|
|
12922
13907
|
governanceRoutes.get("/stewards", async (c) => {
|
|
12923
13908
|
const nestId = c.req.param("nestId");
|
|
12924
13909
|
const scope = c.req.query("scope");
|
|
@@ -13058,6 +14043,7 @@ governanceRoutes.post("/review-queue/bulk", async (c) => {
|
|
|
13058
14043
|
}
|
|
13059
14044
|
const userEmail = await getUserEmail4(c);
|
|
13060
14045
|
const baseUrl = requestBaseUrl(c.req.url);
|
|
14046
|
+
const client = attributeRequest(c, body.client);
|
|
13061
14047
|
const results = [];
|
|
13062
14048
|
for (const nodeId of nodeIds) {
|
|
13063
14049
|
try {
|
|
@@ -13068,7 +14054,8 @@ governanceRoutes.post("/review-queue/bulk", async (c) => {
|
|
|
13068
14054
|
approvedBy: userEmail,
|
|
13069
14055
|
note: body.note,
|
|
13070
14056
|
override: false,
|
|
13071
|
-
baseUrl
|
|
14057
|
+
baseUrl,
|
|
14058
|
+
client
|
|
13072
14059
|
});
|
|
13073
14060
|
} else {
|
|
13074
14061
|
await reject({
|
|
@@ -13160,7 +14147,7 @@ governanceRoutes.post("/request-deletion", async (c) => {
|
|
|
13160
14147
|
if (body.targetType === "folder") {
|
|
13161
14148
|
const { storage } = await engineCache.get(nestId);
|
|
13162
14149
|
const docs = await storage.discoverDocuments();
|
|
13163
|
-
if (!docs.some((d) => isIdUnderFolder(d.id, target))) {
|
|
14150
|
+
if (!docs.some((d) => isIdUnderFolder(d.id, target)) && !await vaultFolderExists(nestId, target)) {
|
|
13164
14151
|
return c.json({ error: `Folder not found: ${target}` }, 404);
|
|
13165
14152
|
}
|
|
13166
14153
|
}
|
|
@@ -13286,7 +14273,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
13286
14273
|
const activity = await getActivity({ nestId, limit });
|
|
13287
14274
|
return c.json({ activity });
|
|
13288
14275
|
});
|
|
13289
|
-
var governanceNodeRoutes = new
|
|
14276
|
+
var governanceNodeRoutes = new Hono26();
|
|
13290
14277
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
13291
14278
|
const nestId = c.req.param("nestId");
|
|
13292
14279
|
const nodeId = c.req.param("nodeId");
|
|
@@ -13492,7 +14479,10 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
13492
14479
|
requestedBy: userEmail,
|
|
13493
14480
|
note: body.note,
|
|
13494
14481
|
priority: body.priority,
|
|
13495
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
14482
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
14483
|
+
// Submitting seals the draft into the engine's append-only chain, so the
|
|
14484
|
+
// block lands on that entry as well as on this request's trace row.
|
|
14485
|
+
client: attributeRequest(c, body.client)
|
|
13496
14486
|
});
|
|
13497
14487
|
} catch (err) {
|
|
13498
14488
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -13626,7 +14616,8 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
|
|
|
13626
14616
|
approvedBy: userEmail,
|
|
13627
14617
|
note: body.note,
|
|
13628
14618
|
override: body.override && isAdmin,
|
|
13629
|
-
baseUrl: requestBaseUrl(c.req.url)
|
|
14619
|
+
baseUrl: requestBaseUrl(c.req.url),
|
|
14620
|
+
client: attributeRequest(c, body.client)
|
|
13630
14621
|
});
|
|
13631
14622
|
return c.json({ review: request });
|
|
13632
14623
|
} catch (err) {
|
|
@@ -13641,6 +14632,7 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
13641
14632
|
throw new ValidationError("Rejection note is required");
|
|
13642
14633
|
}
|
|
13643
14634
|
const userEmail = await getUserEmail4(c);
|
|
14635
|
+
attributeRequest(c, body.client);
|
|
13644
14636
|
try {
|
|
13645
14637
|
const request = await reject({
|
|
13646
14638
|
nestId,
|
|
@@ -13753,26 +14745,52 @@ function posthogKeyError(value) {
|
|
|
13753
14745
|
return null;
|
|
13754
14746
|
}
|
|
13755
14747
|
|
|
14748
|
+
// src/shared/public-base-url.ts
|
|
14749
|
+
var UNSAFE = /["'`\\$;|&<>(){}\s]/;
|
|
14750
|
+
function publicBaseUrlError(value) {
|
|
14751
|
+
const v = value.trim();
|
|
14752
|
+
if (!v) return null;
|
|
14753
|
+
let parsed;
|
|
14754
|
+
try {
|
|
14755
|
+
parsed = new URL(v);
|
|
14756
|
+
} catch {
|
|
14757
|
+
return "Public base URL must be a full http(s) URL, e.g. https://nest.example.com";
|
|
14758
|
+
}
|
|
14759
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
14760
|
+
return "Public base URL must use http or https, e.g. https://nest.example.com";
|
|
14761
|
+
if (!parsed.hostname)
|
|
14762
|
+
return "Public base URL must include a hostname, e.g. https://nest.example.com";
|
|
14763
|
+
if (UNSAFE.test(v))
|
|
14764
|
+
return "Public base URL must not contain spaces, quotes or shell characters (\" ' ` \\ $ ; | & < > ( ) { }) \u2014 it is pasted into terminal commands and config files.";
|
|
14765
|
+
return null;
|
|
14766
|
+
}
|
|
14767
|
+
|
|
13756
14768
|
// src/app.ts
|
|
13757
14769
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
13758
14770
|
import { fileURLToPath } from "url";
|
|
13759
|
-
import { dirname as dirname2, join as
|
|
14771
|
+
import { dirname as dirname2, join as join8, relative as relative2 } from "path";
|
|
13760
14772
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
13761
14773
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
13762
14774
|
var SERVICE_VERSION = (() => {
|
|
13763
14775
|
try {
|
|
13764
|
-
return JSON.parse(readFileSync4(
|
|
14776
|
+
return JSON.parse(readFileSync4(join8(HERE, "..", "package.json"), "utf8")).version;
|
|
13765
14777
|
} catch {
|
|
13766
14778
|
return "unknown";
|
|
13767
14779
|
}
|
|
13768
14780
|
})();
|
|
13769
14781
|
var UI_DIR_CANDIDATES = [
|
|
13770
|
-
|
|
13771
|
-
|
|
14782
|
+
join8(HERE, "web3"),
|
|
14783
|
+
join8(process.cwd(), "dist", "web3")
|
|
13772
14784
|
];
|
|
13773
14785
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
13774
14786
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
13775
|
-
var
|
|
14787
|
+
var isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
14788
|
+
var serveIndexShell = serveStatic({
|
|
14789
|
+
root: UI_DIR_REL,
|
|
14790
|
+
path: "index.html",
|
|
14791
|
+
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
14792
|
+
});
|
|
14793
|
+
var openModeMiddleware = createMiddleware3(async (c, next) => {
|
|
13776
14794
|
const anonId = await ensureAnonymousUser();
|
|
13777
14795
|
c.set("userId", anonId);
|
|
13778
14796
|
c.set("nestScope", null);
|
|
@@ -13784,7 +14802,7 @@ function isPublicReadEligiblePath(method, path) {
|
|
|
13784
14802
|
if (/^\/nests\/[^/]+\/stewards/.test(path)) return false;
|
|
13785
14803
|
return !/\/(collaborators|visibility|settings|mcp)/.test(path);
|
|
13786
14804
|
}
|
|
13787
|
-
var flexAuthMiddleware =
|
|
14805
|
+
var flexAuthMiddleware = createMiddleware3(async (c, next) => {
|
|
13788
14806
|
const hasBearer = c.req.header("Authorization")?.startsWith("Bearer cnst_");
|
|
13789
14807
|
const hasCookie = !!c.req.header("Cookie")?.includes("cnst_session=");
|
|
13790
14808
|
if (hasBearer || hasCookie) {
|
|
@@ -13802,12 +14820,15 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
13802
14820
|
c.set("nestScope", null);
|
|
13803
14821
|
return next();
|
|
13804
14822
|
}
|
|
13805
|
-
return c.json(
|
|
14823
|
+
return c.json(
|
|
14824
|
+
{ error: "Authentication required \u2014 sign in or provide an API key.", code: "missing_credentials" },
|
|
14825
|
+
401
|
|
14826
|
+
);
|
|
13806
14827
|
});
|
|
13807
14828
|
function nestPathParts(path) {
|
|
13808
14829
|
return path.replace(/^\/nests\//, "").split("/").filter(Boolean);
|
|
13809
14830
|
}
|
|
13810
|
-
var flexAuthWithPromptowlTicket =
|
|
14831
|
+
var flexAuthWithPromptowlTicket = createMiddleware3(
|
|
13811
14832
|
async (c, next) => {
|
|
13812
14833
|
const header = c.req.header("Authorization") || "";
|
|
13813
14834
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
@@ -13820,14 +14841,24 @@ var flexAuthWithPromptowlTicket = createMiddleware2(
|
|
|
13820
14841
|
return flexAuthMiddleware(c, next);
|
|
13821
14842
|
}
|
|
13822
14843
|
let claims = null;
|
|
14844
|
+
let expired = false;
|
|
13823
14845
|
for (const secret of secrets) {
|
|
13824
14846
|
try {
|
|
13825
14847
|
claims = verifySsoTicket(token, secret);
|
|
13826
14848
|
break;
|
|
13827
|
-
} catch {
|
|
14849
|
+
} catch (err) {
|
|
14850
|
+
if (err instanceof SsoTokenError && err.message === "Ticket expired") {
|
|
14851
|
+
expired = true;
|
|
14852
|
+
}
|
|
13828
14853
|
}
|
|
13829
14854
|
}
|
|
13830
14855
|
if (!claims) {
|
|
14856
|
+
if (expired) {
|
|
14857
|
+
return c.json(
|
|
14858
|
+
{ error: "Service ticket expired \u2014 reconnect to refresh.", code: "ticket_expired" },
|
|
14859
|
+
401
|
|
14860
|
+
);
|
|
14861
|
+
}
|
|
13831
14862
|
return c.json({ error: "Invalid service ticket" }, 401);
|
|
13832
14863
|
}
|
|
13833
14864
|
if (claims.scope !== "mcp" || claims.jti) {
|
|
@@ -13874,7 +14905,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
13874
14905
|
return ok;
|
|
13875
14906
|
}
|
|
13876
14907
|
function createApp() {
|
|
13877
|
-
const app = new
|
|
14908
|
+
const app = new Hono27({ router: new LinearRouter() });
|
|
13878
14909
|
const corsOrigins = config.CORS_ORIGINS;
|
|
13879
14910
|
app.use(
|
|
13880
14911
|
"*",
|
|
@@ -13892,7 +14923,8 @@ function createApp() {
|
|
|
13892
14923
|
const lenStr = c.req.header("Content-Length");
|
|
13893
14924
|
if (lenStr) {
|
|
13894
14925
|
const len = parseInt(lenStr, 10);
|
|
13895
|
-
const
|
|
14926
|
+
const isPost = c.req.method === "POST";
|
|
14927
|
+
const cap = isPost && ASSET_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, ASSET_MAX_REQUEST_BYTES) : isPost && PDF_UPLOAD_PATH.test(c.req.path) ? Math.max(config.MAX_BODY_BYTES, pdfMaxRequestBytes()) : config.MAX_BODY_BYTES;
|
|
13896
14928
|
if (Number.isFinite(len) && len > cap) {
|
|
13897
14929
|
return c.json(
|
|
13898
14930
|
{
|
|
@@ -13917,9 +14949,13 @@ function createApp() {
|
|
|
13917
14949
|
nestId: /^\/nests\/([^/]+)/.exec(path)?.[1] ?? null,
|
|
13918
14950
|
userId: c.get("userId") ?? null,
|
|
13919
14951
|
status: c.res.status,
|
|
13920
|
-
durationMs: Date.now() - started
|
|
14952
|
+
durationMs: Date.now() - started,
|
|
14953
|
+
// Set by whichever handler validated the request's `client` block
|
|
14954
|
+
// (§9.4). Absent on a request that sent none.
|
|
14955
|
+
client: c.get("clientMetadata")
|
|
13921
14956
|
});
|
|
13922
14957
|
});
|
|
14958
|
+
app.use("*", nestScopedKeyGuard);
|
|
13923
14959
|
app.get("/health", (c) => {
|
|
13924
14960
|
const oidcReady = isOidcReady();
|
|
13925
14961
|
return c.json({
|
|
@@ -13934,6 +14970,9 @@ function createApp() {
|
|
|
13934
14970
|
// Presence only: the publish dialog hides the email/invite gates when
|
|
13935
14971
|
// this server cannot send mail, because their only way in is a mailed link.
|
|
13936
14972
|
email_configured: isEmailConfigured(),
|
|
14973
|
+
// Who may create nests (admins | everyone) — the Nests page hides the
|
|
14974
|
+
// create buttons from non-admins when it's admins-only.
|
|
14975
|
+
nest_creation: config.NEST_CREATION,
|
|
13937
14976
|
// Product analytics for the SPA. The PostHog project key is a public
|
|
13938
14977
|
// client key, but it only ships while usage telemetry is on — one switch
|
|
13939
14978
|
// covers everything this server sends outward.
|
|
@@ -13962,6 +15001,11 @@ function createApp() {
|
|
|
13962
15001
|
// API would refuse it — existing nodes of the type stay readable.
|
|
13963
15002
|
type_artifact_enabled: config.TYPE_ARTIFACT_ENABLED,
|
|
13964
15003
|
type_table_enabled: config.TYPE_TABLE_ENABLED,
|
|
15004
|
+
// This server hosts several organizations (PromptOwl-hosted) rather than
|
|
15005
|
+
// one. The visibility picker words 'Organization' and 'Public' by it and
|
|
15006
|
+
// confirms before 'org'; the server enforces the same in
|
|
15007
|
+
// PATCH /nests/:id/visibility.
|
|
15008
|
+
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
13965
15009
|
// Sub-agent runs gate its own recursion surface separately (env-only,
|
|
13966
15010
|
// must clear a security review — NOT a UI toggle). Exposed read-only so
|
|
13967
15011
|
// the Runs page shows the "spawn sub-agent" option only when it's live.
|
|
@@ -14084,7 +15128,9 @@ function createApp() {
|
|
|
14084
15128
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
14085
15129
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
14086
15130
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
15131
|
+
nest_creation: config.NEST_CREATION,
|
|
14087
15132
|
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
15133
|
+
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
14088
15134
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
14089
15135
|
// presence is reported, never the value (same discipline as the OIDC secret).
|
|
14090
15136
|
sso_token_exchange_enabled: config.SSO_TOKEN_EXCHANGE_ENABLED,
|
|
@@ -14104,7 +15150,12 @@ function createApp() {
|
|
|
14104
15150
|
anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
|
|
14105
15151
|
slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
|
|
14106
15152
|
msteams_webhook_url: config.MSTEAMS_WEBHOOK_URL ?? "",
|
|
15153
|
+
email_provider: config.EMAIL_PROVIDER,
|
|
14107
15154
|
smtp_url: config.SMTP_URL ?? "",
|
|
15155
|
+
ses_region: config.SES_REGION ?? "",
|
|
15156
|
+
ses_access_key_id: config.SES_ACCESS_KEY_ID ?? "",
|
|
15157
|
+
// Write-only, like the OIDC secret — presence only, never the value.
|
|
15158
|
+
ses_secret_access_key_set: !!config.SES_SECRET_ACCESS_KEY,
|
|
14108
15159
|
notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
|
|
14109
15160
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
14110
15161
|
});
|
|
@@ -14137,8 +15188,8 @@ function createApp() {
|
|
|
14137
15188
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
14138
15189
|
}
|
|
14139
15190
|
const fieldErrors = {};
|
|
14140
|
-
const addError = (
|
|
14141
|
-
fieldErrors[
|
|
15191
|
+
const addError = (field2, message) => {
|
|
15192
|
+
fieldErrors[field2] = message;
|
|
14142
15193
|
};
|
|
14143
15194
|
const pending = [];
|
|
14144
15195
|
if ("promptowl_sign_in_gate" in body) {
|
|
@@ -14159,7 +15210,9 @@ function createApp() {
|
|
|
14159
15210
|
}
|
|
14160
15211
|
if ("public_base_url" in body) {
|
|
14161
15212
|
const v = String(body.public_base_url ?? "").trim().replace(/\/$/, "");
|
|
14162
|
-
|
|
15213
|
+
const err = v ? publicBaseUrlError(v) : null;
|
|
15214
|
+
if (err) addError("public_base_url", err);
|
|
15215
|
+
else pending.push({ name: "PUBLIC_BASE_URL", value: v || null });
|
|
14163
15216
|
}
|
|
14164
15217
|
if ("telemetry_enabled" in body) {
|
|
14165
15218
|
pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
|
|
@@ -14307,12 +15360,24 @@ function createApp() {
|
|
|
14307
15360
|
value: body.oidc_department_tagging ? "true" : null
|
|
14308
15361
|
});
|
|
14309
15362
|
}
|
|
15363
|
+
if ("nest_creation" in body) {
|
|
15364
|
+
const v = String(body.nest_creation ?? "").trim().toLowerCase();
|
|
15365
|
+
if (v !== "admins" && v !== "everyone")
|
|
15366
|
+
addError("nest_creation", "Nest creation must be admins or everyone.");
|
|
15367
|
+
else pending.push({ name: "NEST_CREATION", value: v });
|
|
15368
|
+
}
|
|
14310
15369
|
if ("people_suggest_all_users" in body) {
|
|
14311
15370
|
pending.push({
|
|
14312
15371
|
name: "PEOPLE_SUGGEST_ALL_USERS",
|
|
14313
15372
|
value: body.people_suggest_all_users ? "true" : null
|
|
14314
15373
|
});
|
|
14315
15374
|
}
|
|
15375
|
+
if ("shared_deployment" in body) {
|
|
15376
|
+
pending.push({
|
|
15377
|
+
name: "SHARED_DEPLOYMENT",
|
|
15378
|
+
value: body.shared_deployment ? "true" : null
|
|
15379
|
+
});
|
|
15380
|
+
}
|
|
14316
15381
|
if ("sso_token_exchange_enabled" in body) {
|
|
14317
15382
|
pending.push({
|
|
14318
15383
|
name: "SSO_TOKEN_EXCHANGE_ENABLED",
|
|
@@ -14363,13 +15428,13 @@ function createApp() {
|
|
|
14363
15428
|
const effAllowedDomains = "oidc_allowed_domains" in body ? String(body.oidc_allowed_domains ?? "").split(",").map((d) => d.trim()).filter(Boolean) : config.OIDC_ALLOWED_DOMAINS;
|
|
14364
15429
|
const effAutoProvision = "oidc_auto_provision" in body ? !!body.oidc_auto_provision : config.OIDC_AUTO_PROVISION;
|
|
14365
15430
|
if (effAutoProvision && effAllowedDomains.length === 0) {
|
|
14366
|
-
for (const
|
|
15431
|
+
for (const field2 of [
|
|
14367
15432
|
"ms_token_exchange_enabled",
|
|
14368
15433
|
"google_token_exchange_enabled"
|
|
14369
15434
|
]) {
|
|
14370
|
-
if (body[
|
|
15435
|
+
if (body[field2])
|
|
14371
15436
|
addError(
|
|
14372
|
-
|
|
15437
|
+
field2,
|
|
14373
15438
|
"Set allowed email domains (or turn auto-provision off) before enabling token exchange \u2014 otherwise anyone with an account at that provider can sign themselves up and mint a token."
|
|
14374
15439
|
);
|
|
14375
15440
|
}
|
|
@@ -14410,6 +15475,50 @@ function createApp() {
|
|
|
14410
15475
|
addError("smtp_url", "SMTP connection URL must start with smtp:// or smtps:// (or leave it empty to turn email notifications off).");
|
|
14411
15476
|
else pending.push({ name: "SMTP_URL", value: v || null });
|
|
14412
15477
|
}
|
|
15478
|
+
if ("email_provider" in body) {
|
|
15479
|
+
const v = String(body.email_provider ?? "").trim().toLowerCase();
|
|
15480
|
+
if (v !== "smtp" && v !== "ses")
|
|
15481
|
+
addError("email_provider", "Email provider must be SMTP or AWS SES.");
|
|
15482
|
+
else pending.push({ name: "EMAIL_PROVIDER", value: v });
|
|
15483
|
+
}
|
|
15484
|
+
if ("ses_region" in body) {
|
|
15485
|
+
const v = String(body.ses_region ?? "").trim();
|
|
15486
|
+
if (v && !/^[a-z]{2}(-[a-z]+)+-\d+$/.test(v))
|
|
15487
|
+
addError("ses_region", "AWS region must look like us-east-1.");
|
|
15488
|
+
else pending.push({ name: "SES_REGION", value: v || null });
|
|
15489
|
+
}
|
|
15490
|
+
if ("ses_access_key_id" in body) {
|
|
15491
|
+
const v = String(body.ses_access_key_id ?? "").trim();
|
|
15492
|
+
pending.push({ name: "SES_ACCESS_KEY_ID", value: v || null });
|
|
15493
|
+
}
|
|
15494
|
+
if ("ses_secret_access_key" in body) {
|
|
15495
|
+
const v = body.ses_secret_access_key;
|
|
15496
|
+
if (v !== null && typeof v !== "string")
|
|
15497
|
+
addError("ses_secret_access_key", "AWS secret access key must be a string (or empty to clear it).");
|
|
15498
|
+
else pending.push({ name: "SES_SECRET_ACCESS_KEY", value: v?.trim() || null });
|
|
15499
|
+
}
|
|
15500
|
+
const provider = String(body.email_provider ?? "").trim().toLowerCase();
|
|
15501
|
+
if (provider === "smtp" || provider === "ses") {
|
|
15502
|
+
const stale = provider === "ses" ? ["SMTP_URL"] : ["SES_REGION", "SES_ACCESS_KEY_ID", "SES_SECRET_ACCESS_KEY"];
|
|
15503
|
+
for (const name of stale) {
|
|
15504
|
+
const i = pending.findIndex((p) => p.name === name);
|
|
15505
|
+
if (i >= 0) pending.splice(i, 1);
|
|
15506
|
+
pending.push({ name, value: null });
|
|
15507
|
+
}
|
|
15508
|
+
}
|
|
15509
|
+
if ("ses_access_key_id" in body || "ses_secret_access_key" in body) {
|
|
15510
|
+
const next = (name, current) => {
|
|
15511
|
+
const p = pending.find((x) => x.name === name);
|
|
15512
|
+
return p ? p.value : current;
|
|
15513
|
+
};
|
|
15514
|
+
const hasId = !!next("SES_ACCESS_KEY_ID", config.SES_ACCESS_KEY_ID);
|
|
15515
|
+
const hasSecret = !!next("SES_SECRET_ACCESS_KEY", config.SES_SECRET_ACCESS_KEY);
|
|
15516
|
+
if (hasId !== hasSecret)
|
|
15517
|
+
addError(
|
|
15518
|
+
"ses_access_key_id",
|
|
15519
|
+
"Set both the AWS access key ID and secret access key, or leave both empty to use the server's IAM role."
|
|
15520
|
+
);
|
|
15521
|
+
}
|
|
14413
15522
|
if ("notify_email_from" in body) {
|
|
14414
15523
|
const v = String(body.notify_email_from ?? "").trim();
|
|
14415
15524
|
if (v && v.includes(","))
|
|
@@ -14447,15 +15556,18 @@ function createApp() {
|
|
|
14447
15556
|
{ error: errorMessages.join("; "), fieldErrors, settings: currentServerSettings() },
|
|
14448
15557
|
400
|
|
14449
15558
|
);
|
|
14450
|
-
const
|
|
14451
|
-
|
|
14452
|
-
|
|
15559
|
+
const over = Object.fromEntries(pending.map((p) => [p.name, p.value]));
|
|
15560
|
+
const nextEmail = transportSettings(over);
|
|
15561
|
+
const emailFields = nextEmail?.provider === "ses" ? ["EMAIL_PROVIDER", "SES_REGION", "SES_ACCESS_KEY_ID", "SES_SECRET_ACCESS_KEY"] : ["EMAIL_PROVIDER", "SMTP_URL"];
|
|
15562
|
+
if (nextEmail && emailFields.some((k) => k in over)) {
|
|
15563
|
+
const result = await verifyEmailTransport(nextEmail);
|
|
14453
15564
|
if (!result.ok) {
|
|
14454
|
-
const
|
|
15565
|
+
const ses = nextEmail.provider === "ses";
|
|
15566
|
+
const message = `${ses ? "AWS SES" : "SMTP"} connection failed: ${result.error}`;
|
|
14455
15567
|
return c.json(
|
|
14456
15568
|
{
|
|
14457
15569
|
error: message,
|
|
14458
|
-
fieldErrors: { smtp_url: message },
|
|
15570
|
+
fieldErrors: { [ses ? "ses_region" : "smtp_url"]: message },
|
|
14459
15571
|
settings: currentServerSettings()
|
|
14460
15572
|
},
|
|
14461
15573
|
400
|
|
@@ -14614,6 +15726,10 @@ function createApp() {
|
|
|
14614
15726
|
app.use("/admin/community-sites", flexAuthMiddleware);
|
|
14615
15727
|
app.use("/admin/community-sites/*", flexAuthMiddleware);
|
|
14616
15728
|
app.route("/admin/community-sites", communitySiteRoutes);
|
|
15729
|
+
app.get("/teams", async (c, next) => {
|
|
15730
|
+
if (isDocumentNavigation(c)) return serveIndexShell(c, next);
|
|
15731
|
+
return next();
|
|
15732
|
+
});
|
|
14617
15733
|
app.use("/teams/*", flexAuthMiddleware);
|
|
14618
15734
|
app.use("/teams", flexAuthMiddleware);
|
|
14619
15735
|
app.route("/teams", teamRoutes);
|
|
@@ -14667,7 +15783,7 @@ function createApp() {
|
|
|
14667
15783
|
const others = requests.filter((r) => r.requestedBy !== me);
|
|
14668
15784
|
const nestApprove = new Map(
|
|
14669
15785
|
await Promise.all(
|
|
14670
|
-
[...
|
|
15786
|
+
[...nestName2.keys()].map(
|
|
14671
15787
|
async (nestId) => [
|
|
14672
15788
|
nestId,
|
|
14673
15789
|
await canApproveInNest(nestId, me)
|
|
@@ -14697,13 +15813,33 @@ function createApp() {
|
|
|
14697
15813
|
);
|
|
14698
15814
|
waiting.sort(byNewest);
|
|
14699
15815
|
const notifications = await listNotifications(me, { unreadOnly: true, limit: 50 });
|
|
15816
|
+
const approvableNestIds = [...nestName2.keys()].filter(
|
|
15817
|
+
(id) => nestApprove.get(id)
|
|
15818
|
+
);
|
|
15819
|
+
const restNestIds = [...nestName2.keys()].filter(
|
|
15820
|
+
(id) => !nestApprove.get(id)
|
|
15821
|
+
);
|
|
15822
|
+
const [{ total: approvableReviewTotal }, { total: myReviewsElsewhereTotal }] = await Promise.all([
|
|
15823
|
+
getReviewQueue({ status: "pending", nestIds: approvableNestIds, limit: 1 }),
|
|
15824
|
+
getReviewQueue({
|
|
15825
|
+
status: "pending",
|
|
15826
|
+
nestIds: restNestIds,
|
|
15827
|
+
requestedByEmail: me,
|
|
15828
|
+
limit: 1
|
|
15829
|
+
})
|
|
15830
|
+
]);
|
|
15831
|
+
const reviewTotal = approvableReviewTotal + myReviewsElsewhereTotal;
|
|
15832
|
+
const deletionsShown = items.filter((i) => i.type === "deletion").length + waiting.filter((i) => i.type === "deletion").length;
|
|
14700
15833
|
return c.json({
|
|
14701
15834
|
items,
|
|
14702
15835
|
waiting,
|
|
14703
15836
|
notifications,
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
15837
|
+
pagination: {
|
|
15838
|
+
total: reviewTotal + deletionsShown,
|
|
15839
|
+
limit,
|
|
15840
|
+
offset,
|
|
15841
|
+
has_more: offset + requests.length < total
|
|
15842
|
+
}
|
|
14707
15843
|
});
|
|
14708
15844
|
});
|
|
14709
15845
|
app.get("/me/drafts", async (c) => {
|
|
@@ -14752,12 +15888,29 @@ function createApp() {
|
|
|
14752
15888
|
await markNotificationsRead(me, body.ids === "all" ? "all" : body.ids ?? []);
|
|
14753
15889
|
return c.json({ ok: true });
|
|
14754
15890
|
});
|
|
15891
|
+
app.post("/me/nests/:nestId/leave", async (c) => {
|
|
15892
|
+
const nestId = c.req.param("nestId");
|
|
15893
|
+
const me = await resolveCallerEmail(c.get("userId"));
|
|
15894
|
+
const db = getDb();
|
|
15895
|
+
const info = await db.run(
|
|
15896
|
+
"DELETE FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
|
|
15897
|
+
[nestId, c.get("userId")]
|
|
15898
|
+
);
|
|
15899
|
+
await db.run(
|
|
15900
|
+
`UPDATE notifications SET read_at = ? WHERE LOWER(user_email) = LOWER(?) AND nest_id = ? AND kind = 'nest_invite' AND read_at IS NULL`,
|
|
15901
|
+
[(/* @__PURE__ */ new Date()).toISOString(), me, nestId]
|
|
15902
|
+
);
|
|
15903
|
+
return c.json({ left: info.changes > 0 });
|
|
15904
|
+
});
|
|
14755
15905
|
app.use("/stats", flexAuthMiddleware);
|
|
14756
15906
|
app.get("/stats", async (c) => {
|
|
14757
15907
|
const db = getDb();
|
|
14758
15908
|
const userId = c.get("userId");
|
|
14759
15909
|
const userEmail = await resolveCallerEmail(userId);
|
|
14760
|
-
const
|
|
15910
|
+
const nestScope = c.get("nestScope");
|
|
15911
|
+
const visibleNests = (await listVisibleNests(userId)).filter(
|
|
15912
|
+
(n) => !nestScope || n.id === nestScope
|
|
15913
|
+
);
|
|
14761
15914
|
let documents = 0;
|
|
14762
15915
|
for (const nest of visibleNests) {
|
|
14763
15916
|
try {
|
|
@@ -14785,8 +15938,12 @@ function createApp() {
|
|
|
14785
15938
|
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 20, 1), 50);
|
|
14786
15939
|
const userId = c.get("userId");
|
|
14787
15940
|
const userEmail = await resolveCallerEmail(userId);
|
|
15941
|
+
const nestScope = c.get("nestScope");
|
|
15942
|
+
const searchable = (await listVisibleNests(userId, { readable: true })).filter(
|
|
15943
|
+
(n) => !nestScope || n.id === nestScope
|
|
15944
|
+
);
|
|
14788
15945
|
const perNest = await Promise.all(
|
|
14789
|
-
|
|
15946
|
+
searchable.map(async (nest) => {
|
|
14790
15947
|
try {
|
|
14791
15948
|
const { storage } = await engineCache.get(nest.id);
|
|
14792
15949
|
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
@@ -14819,7 +15976,7 @@ function createApp() {
|
|
|
14819
15976
|
app.route("/index", nestIndexRoutes);
|
|
14820
15977
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
14821
15978
|
app.route("/mcp", serverMcpRoutes);
|
|
14822
|
-
const nestsApp = new
|
|
15979
|
+
const nestsApp = new Hono27();
|
|
14823
15980
|
nestsApp.use("*", async (c, next) => {
|
|
14824
15981
|
const parts = nestPathParts(c.req.path);
|
|
14825
15982
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -14949,7 +16106,7 @@ function createApp() {
|
|
|
14949
16106
|
);
|
|
14950
16107
|
}
|
|
14951
16108
|
const resource = parts[1];
|
|
14952
|
-
if (resource === "visibility" || resource === "reader-mode") {
|
|
16109
|
+
if (resource === "visibility" || resource === "reader-mode" || resource === "branding") {
|
|
14953
16110
|
required = "admin";
|
|
14954
16111
|
} else if (resource === "collaborators") {
|
|
14955
16112
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
@@ -14976,7 +16133,9 @@ function createApp() {
|
|
|
14976
16133
|
stewardEditorBypass = resolved.some(
|
|
14977
16134
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor"
|
|
14978
16135
|
);
|
|
14979
|
-
} else if (
|
|
16136
|
+
} else if (c.req.method === "POST" && // POST /nodes (create) and POST /nodes/pdf (PDF upload — a create, or
|
|
16137
|
+
// a new version of a pdf node) share the nest-scope editor rule.
|
|
16138
|
+
(parts.length === 2 || parts.length === 3 && parts[2] === "pdf")) {
|
|
14980
16139
|
const resolved = await resolveStewardsForNode(nestId, "");
|
|
14981
16140
|
stewardEditorBypass = resolved.some(
|
|
14982
16141
|
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && r.steward.scope === "nest"
|
|
@@ -15038,11 +16197,13 @@ function createApp() {
|
|
|
15038
16197
|
});
|
|
15039
16198
|
nestsApp.route("/", nestRoutes);
|
|
15040
16199
|
nestsApp.route("/:nestId", governanceRoutes);
|
|
16200
|
+
nestsApp.route("/:nestId/nodes", pdfNodeRoutes);
|
|
15041
16201
|
nestsApp.route("/:nestId/nodes", annotationRoutes);
|
|
15042
16202
|
nestsApp.route("/:nestId/nodes", governanceNodeRoutes);
|
|
15043
16203
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|
|
15044
16204
|
nestsApp.route("/:nestId", queryRoutes);
|
|
15045
16205
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
16206
|
+
nestsApp.route("/:nestId", brandingRoutes);
|
|
15046
16207
|
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
15047
16208
|
nestsApp.route("/:nestId/publications", publishRoutes);
|
|
15048
16209
|
nestsApp.route("/:nestId/teams", nestTeamRoutes);
|
|
@@ -15066,18 +16227,13 @@ function createApp() {
|
|
|
15066
16227
|
onFound: (_p, c) => c.header("Cache-Control", "public, max-age=31536000, immutable")
|
|
15067
16228
|
})
|
|
15068
16229
|
);
|
|
15069
|
-
const isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
15070
16230
|
app.get(
|
|
15071
16231
|
"*",
|
|
15072
16232
|
async (c, next) => {
|
|
15073
16233
|
if (isDocumentNavigation(c)) return next();
|
|
15074
16234
|
return c.json({ error: "Not found" }, 404);
|
|
15075
16235
|
},
|
|
15076
|
-
|
|
15077
|
-
root: UI_DIR_REL,
|
|
15078
|
-
path: "index.html",
|
|
15079
|
-
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
15080
|
-
})
|
|
16236
|
+
serveIndexShell
|
|
15081
16237
|
);
|
|
15082
16238
|
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
15083
16239
|
app.onError((err, c) => {
|
|
@@ -15365,7 +16521,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
15365
16521
|
}
|
|
15366
16522
|
|
|
15367
16523
|
// src/workflow/scheduler.ts
|
|
15368
|
-
import { v4 as
|
|
16524
|
+
import { v4 as uuid18 } from "uuid";
|
|
15369
16525
|
function isDue(s, now) {
|
|
15370
16526
|
if (!s.last_run_at) return true;
|
|
15371
16527
|
const last = Date.parse(s.last_run_at);
|
|
@@ -15393,7 +16549,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
15393
16549
|
);
|
|
15394
16550
|
continue;
|
|
15395
16551
|
}
|
|
15396
|
-
const id = `run_${
|
|
16552
|
+
const id = `run_${uuid18()}`;
|
|
15397
16553
|
const nowIso2 = new Date(now).toISOString();
|
|
15398
16554
|
await db.run(
|
|
15399
16555
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|