@promptowl/contextnest-community 1.25.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 +68 -8
- package/CONFIGURATION.md +572 -116
- package/README.md +3 -2
- package/dist/{chunk-32SZCH36.js → chunk-2QOJE7A5.js} +4 -4
- package/dist/{chunk-2KHM5C7V.js → chunk-DGPTJXIV.js} +13 -15
- package/dist/chunk-I5EDEZBQ.js +523 -0
- package/dist/{chunk-TI7HMP64.js → chunk-IEAWRHMV.js} +2 -2
- package/dist/{chunk-DTLMBF4W.js → chunk-M6PGYMBD.js} +15 -8
- package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
- package/dist/{chunk-KPGNZLZL.js → chunk-RPGFHESR.js} +1 -1
- package/dist/{chunk-7FROMOZM.js → chunk-SWA5M5XX.js} +68 -544
- package/dist/{chunk-JDAOA5KP.js → chunk-TRSGPS27.js} +39 -14
- package/dist/{engine-TER6FDBP.js → engine-QNMFDIBV.js} +3 -2
- package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-4MEGPT4D.js} +4 -3
- package/dist/{grants-service-2OTFS7EH.js → grants-service-24GLUBY4.js} +2 -2
- package/dist/index.js +911 -263
- package/dist/invited-user-AM6A3WGO.js +12 -0
- package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
- package/dist/{review-service-4G2XUFS6.js → review-service-2EWN7WWO.js} +7 -6
- package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-TDZUC2Q6.js} +4 -3
- package/dist/{version-service-X4FAG2OZ.js → version-service-C3JTZLJH.js} +4 -3
- package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
- package/dist/web3/assets/{AgentDocsPage-Dkhgb1hk.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-Dfk5uIE5.js → MarkdownEditor-CKh4WSn-.js} +136 -131
- 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-BXDTDPp1.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-4SiLKlz8.js → alert-dialog-BvF3NFr7.js} +2 -2
- package/dist/web3/assets/{arrow-left-BAgIocLn.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-XR4ReJ9Z.js → chevron-left-CK_ZPFm9.js} +1 -1
- package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-Bw_aTXiH.js} +1 -1
- package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-DkIG68H7.js} +1 -1
- package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BlR7EN6l.js} +1 -1
- package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DSVxK3tf.js} +1 -1
- package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
- package/dist/web3/assets/{earth-CXOcThqn.js → earth-BXhVPvCR.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-BevGZgtj.js} +1 -1
- package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-m8UBV4fA.js} +1 -1
- package/dist/web3/assets/{index-A0_ymcqL.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-xzYqkBUP.js → page-Dh57L0RG.js} +5 -5
- package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
- package/dist/web3/assets/{page-CUjuSs5Q.js → page-DzKwP5Ds.js} +3 -3
- package/dist/web3/assets/page-YxjrE5b5.js +45 -0
- package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-DVq4qvMR.js} +1 -1
- package/dist/web3/assets/{play-B8xE9j5f.js → play-CXhS6Pgq.js} +1 -1
- package/dist/web3/assets/{send-CvAgOUPG.js → send-DdfwnIrj.js} +1 -1
- package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DKX0Q4gO.js} +1 -1
- package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-CxmXRt58.js} +1 -1
- package/dist/web3/assets/{tag-Br_y1f00.js → tag-DeYC_mRv.js} +1 -1
- package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-B0HyPTnI.js} +1 -1
- package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-Ckm_Isyc.js} +1 -1
- package/dist/web3/assets/{x-qxt1WrUi.js → x-mNtADYHc.js} +1 -1
- package/dist/web3/assets/{zap-BcsKR0ef.js → zap-D2ifcnYb.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +3 -2
- package/dist/client-D44ZHDTJ.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
- package/dist/web3/assets/card-UDyMRcps.js +0 -1
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-B5hLclEq.js +0 -389
- package/dist/web3/assets/index-DrUAtoQM.css +0 -1
- package/dist/web3/assets/page-BHD4beun.js +0 -1
- package/dist/web3/assets/page-BSYUXYmO.js +0 -11
- package/dist/web3/assets/page-BYc2DIux.js +0 -45
- package/dist/web3/assets/page-BfibLg8e.js +0 -1
- package/dist/web3/assets/page-CGe8MtUu.js +0 -9
- package/dist/web3/assets/page-CIgPJmG6.js +0 -2
- package/dist/web3/assets/page-Ck31nx9b.js +0 -1
- package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
- package/dist/web3/assets/page-D0sN3GO6.js +0 -1
- package/dist/web3/assets/page-DEpH91X2.js +0 -1
- package/dist/web3/assets/page-l8-B7CGt.js +0 -1
- package/dist/web3/assets/page-mnSHmk6A.js +0 -6
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/select-DrL0mpRN.js +0 -6
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +0 -6
package/dist/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,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
stewardDocumentCreator,
|
|
60
60
|
syncFromConfig,
|
|
61
61
|
updateSteward
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-DGPTJXIV.js";
|
|
63
63
|
import {
|
|
64
64
|
createGrant,
|
|
65
65
|
deleteGrant,
|
|
@@ -68,16 +68,7 @@ import {
|
|
|
68
68
|
listGrants,
|
|
69
69
|
listUserGrants,
|
|
70
70
|
resolveNodeGrant
|
|
71
|
-
} from "./chunk-
|
|
72
|
-
import {
|
|
73
|
-
generateApiKey,
|
|
74
|
-
getKeyPrefix,
|
|
75
|
-
hashApiKey,
|
|
76
|
-
hashPassword,
|
|
77
|
-
parseBearerToken,
|
|
78
|
-
verifyKeyRequest,
|
|
79
|
-
verifyPassword
|
|
80
|
-
} from "./chunk-F66VCBLA.js";
|
|
71
|
+
} from "./chunk-RPGFHESR.js";
|
|
81
72
|
import {
|
|
82
73
|
attributeRequest,
|
|
83
74
|
checkConflict,
|
|
@@ -101,7 +92,7 @@ import {
|
|
|
101
92
|
serializeClient,
|
|
102
93
|
setApprovedVersion,
|
|
103
94
|
upsertVersion
|
|
104
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-IEAWRHMV.js";
|
|
105
96
|
import {
|
|
106
97
|
approveExternalEdit,
|
|
107
98
|
getExternalEditDetail,
|
|
@@ -113,7 +104,7 @@ import {
|
|
|
113
104
|
scanDocumentForDrift,
|
|
114
105
|
scanNestForDrift,
|
|
115
106
|
startDriftScanner
|
|
116
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-2QOJE7A5.js";
|
|
117
108
|
import {
|
|
118
109
|
addMember,
|
|
119
110
|
addWatcher,
|
|
@@ -121,10 +112,7 @@ import {
|
|
|
121
112
|
assertSafeNodeId,
|
|
122
113
|
buildDocContext,
|
|
123
114
|
buildTitleMap,
|
|
124
|
-
canApproveWith,
|
|
125
|
-
canEditWith,
|
|
126
115
|
capForArchive,
|
|
127
|
-
collabPermToRole,
|
|
128
116
|
createNest,
|
|
129
117
|
createTeam,
|
|
130
118
|
deleteNest,
|
|
@@ -147,7 +135,6 @@ import {
|
|
|
147
135
|
insertOrIgnore,
|
|
148
136
|
installLicenseKey,
|
|
149
137
|
isConfigSuperAdmin,
|
|
150
|
-
isEmailConfigured,
|
|
151
138
|
isLicenseAdminEmail,
|
|
152
139
|
isLicenseAdminUserId,
|
|
153
140
|
isNestArchived,
|
|
@@ -186,7 +173,6 @@ import {
|
|
|
186
173
|
persistSetting,
|
|
187
174
|
pinNest,
|
|
188
175
|
pinnedNestIds,
|
|
189
|
-
primaryRole,
|
|
190
176
|
readIndexedNodes,
|
|
191
177
|
rebuildNodeIndex,
|
|
192
178
|
removeMember,
|
|
@@ -202,7 +188,6 @@ import {
|
|
|
202
188
|
resolveNodeUrl,
|
|
203
189
|
resolveTeamRolesForUser,
|
|
204
190
|
restoreNest,
|
|
205
|
-
sendEmailToRecipient,
|
|
206
191
|
setAllowSelfApprove,
|
|
207
192
|
setCreatorIsReviewer,
|
|
208
193
|
setPrimeOnlyReview,
|
|
@@ -219,9 +204,8 @@ import {
|
|
|
219
204
|
unpinNest,
|
|
220
205
|
unshareTeamFromNest,
|
|
221
206
|
updateMemberRole,
|
|
222
|
-
validateLicense
|
|
223
|
-
|
|
224
|
-
} from "./chunk-7FROMOZM.js";
|
|
207
|
+
validateLicense
|
|
208
|
+
} from "./chunk-SWA5M5XX.js";
|
|
225
209
|
import {
|
|
226
210
|
AppError,
|
|
227
211
|
ConflictError,
|
|
@@ -234,13 +218,34 @@ import {
|
|
|
234
218
|
VaultBusyError,
|
|
235
219
|
isVaultLockTimeout
|
|
236
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";
|
|
237
242
|
import {
|
|
238
243
|
config,
|
|
239
244
|
getDb,
|
|
240
245
|
initDb,
|
|
241
246
|
isEmailListish,
|
|
242
247
|
isEmailish
|
|
243
|
-
} from "./chunk-
|
|
248
|
+
} from "./chunk-TRSGPS27.js";
|
|
244
249
|
import {
|
|
245
250
|
ANON_EMAIL,
|
|
246
251
|
ANON_USER_ID,
|
|
@@ -254,9 +259,9 @@ import {
|
|
|
254
259
|
import { serve } from "@hono/node-server";
|
|
255
260
|
|
|
256
261
|
// src/app.ts
|
|
257
|
-
import { Hono as
|
|
262
|
+
import { Hono as Hono27 } from "hono";
|
|
258
263
|
import { LinearRouter } from "hono/router/linear-router";
|
|
259
|
-
import { createMiddleware as
|
|
264
|
+
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
260
265
|
import { cors } from "hono/cors";
|
|
261
266
|
|
|
262
267
|
// src/shared/artifact-sandbox.ts
|
|
@@ -473,6 +478,84 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
473
478
|
);
|
|
474
479
|
});
|
|
475
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
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
476
559
|
// src/shared/rate-limit.ts
|
|
477
560
|
var buckets = /* @__PURE__ */ new Map();
|
|
478
561
|
function liveBucket(key, cutoff) {
|
|
@@ -1310,7 +1393,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1310
1393
|
if (config.MANUAL_SIGN_IN !== "open") {
|
|
1311
1394
|
return c.json(
|
|
1312
1395
|
{
|
|
1313
|
-
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."
|
|
1314
1397
|
},
|
|
1315
1398
|
403
|
|
1316
1399
|
);
|
|
@@ -1337,7 +1420,7 @@ authRoutes.post("/register", async (c) => {
|
|
|
1337
1420
|
if (existing.is_invited === 1) {
|
|
1338
1421
|
return c.json(
|
|
1339
1422
|
{
|
|
1340
|
-
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."
|
|
1341
1424
|
},
|
|
1342
1425
|
409
|
|
1343
1426
|
);
|
|
@@ -1434,10 +1517,36 @@ authRoutes.post("/logout", async (c) => {
|
|
|
1434
1517
|
);
|
|
1435
1518
|
return c.json({ ok: true });
|
|
1436
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
|
+
}
|
|
1437
1544
|
authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
1438
1545
|
const body = await c.req.json().catch(
|
|
1439
1546
|
() => ({})
|
|
1440
1547
|
);
|
|
1548
|
+
const refused = refuseNestScopedCaller(c);
|
|
1549
|
+
if (refused) return refused;
|
|
1441
1550
|
const db = getDb();
|
|
1442
1551
|
const userId = c.get("userId");
|
|
1443
1552
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1445,6 +1554,10 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1445
1554
|
label: body.label
|
|
1446
1555
|
});
|
|
1447
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
|
+
}
|
|
1448
1561
|
const apiKey = generateApiKey();
|
|
1449
1562
|
const keyId = uuid4();
|
|
1450
1563
|
await db.run(
|
|
@@ -1459,12 +1572,20 @@ authRoutes.post("/keys", authMiddleware, async (c) => {
|
|
|
1459
1572
|
]
|
|
1460
1573
|
);
|
|
1461
1574
|
return c.json(
|
|
1462
|
-
{
|
|
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
|
+
},
|
|
1463
1582
|
201
|
|
1464
1583
|
);
|
|
1465
1584
|
});
|
|
1466
1585
|
authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
1467
1586
|
const body = await c.req.json().catch(() => ({}));
|
|
1587
|
+
const refused = refuseNestScopedCaller(c);
|
|
1588
|
+
if (refused) return refused;
|
|
1468
1589
|
const db = getDb();
|
|
1469
1590
|
const userId = c.get("userId");
|
|
1470
1591
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1474,6 +1595,10 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1474
1595
|
});
|
|
1475
1596
|
if (!check.ok) return c.json({ error: check.error }, check.status);
|
|
1476
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
|
+
}
|
|
1477
1602
|
const apiKey = generateApiKey();
|
|
1478
1603
|
const keyId = uuid4();
|
|
1479
1604
|
const nestScope = "nest_id" in body ? body.nest_id || null : prior.nest_id;
|
|
@@ -1506,10 +1631,13 @@ authRoutes.post("/keys/rotate", authMiddleware, async (c) => {
|
|
|
1506
1631
|
id: keyId,
|
|
1507
1632
|
api_key: apiKey,
|
|
1508
1633
|
key_prefix: getKeyPrefix(apiKey),
|
|
1509
|
-
label: check.label ?? prior.label
|
|
1634
|
+
label: check.label ?? prior.label,
|
|
1635
|
+
nest_id: nestScope
|
|
1510
1636
|
});
|
|
1511
1637
|
});
|
|
1512
1638
|
authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
1639
|
+
const refused = refuseNestScopedCaller(c);
|
|
1640
|
+
if (refused) return refused;
|
|
1513
1641
|
const db = getDb();
|
|
1514
1642
|
const userId = c.get("userId");
|
|
1515
1643
|
const check = await verifyKeyRequest(db, userId, { action: "fetch" });
|
|
@@ -1521,6 +1649,8 @@ authRoutes.get("/keys", authMiddleware, async (c) => {
|
|
|
1521
1649
|
return c.json({ keys });
|
|
1522
1650
|
});
|
|
1523
1651
|
authRoutes.delete("/keys/:keyId", authMiddleware, async (c) => {
|
|
1652
|
+
const refused = refuseNestScopedCaller(c);
|
|
1653
|
+
if (refused) return refused;
|
|
1524
1654
|
const db = getDb();
|
|
1525
1655
|
const userId = c.get("userId");
|
|
1526
1656
|
const check = await verifyKeyRequest(db, userId, {
|
|
@@ -1904,47 +2034,30 @@ authRoutes.post("/invite", async (c) => {
|
|
|
1904
2034
|
"SELECT id, email, is_invited FROM users WHERE LOWER(email) = ?",
|
|
1905
2035
|
[email]
|
|
1906
2036
|
);
|
|
1907
|
-
if (existing
|
|
2037
|
+
if (existing) {
|
|
1908
2038
|
return c.json(
|
|
1909
2039
|
{
|
|
1910
|
-
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.`
|
|
1911
2041
|
},
|
|
1912
2042
|
409
|
|
1913
2043
|
);
|
|
1914
2044
|
}
|
|
1915
|
-
const tempPassword = uuid4().replace(/-/g, "").slice(0, 16);
|
|
1916
|
-
const passwordHash = await hashPassword(tempPassword);
|
|
1917
|
-
let userId;
|
|
1918
|
-
if (existing) {
|
|
1919
|
-
userId = existing.id;
|
|
1920
|
-
await db.run("UPDATE users SET password_hash = ? WHERE id = ?", [
|
|
1921
|
-
passwordHash,
|
|
1922
|
-
userId
|
|
1923
|
-
]);
|
|
1924
|
-
} else {
|
|
1925
|
-
userId = uuid4();
|
|
1926
|
-
await db.run(
|
|
1927
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
1928
|
-
[userId, email, null, passwordHash]
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1932
2045
|
const inviter = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
1933
2046
|
callerId
|
|
1934
2047
|
]);
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
path: "",
|
|
1939
|
-
actor: email,
|
|
1940
|
-
by: inviter?.email || void 0,
|
|
1941
|
-
tempPassword,
|
|
1942
|
-
link: requestBaseUrl(c.req.url)
|
|
2048
|
+
const { id: userId, tempPassword } = await createInvitedUser(email, {
|
|
2049
|
+
invitedBy: inviter?.email,
|
|
2050
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
1943
2051
|
});
|
|
2052
|
+
trackEvent("admin.invite", { adminId: callerId, email });
|
|
1944
2053
|
return c.json(
|
|
1945
2054
|
{
|
|
1946
2055
|
temporary_password: tempPassword,
|
|
1947
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(),
|
|
1948
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."
|
|
1949
2062
|
},
|
|
1950
2063
|
201
|
|
@@ -1972,7 +2085,12 @@ authRoutes.get("/teammates", async (c) => {
|
|
|
1972
2085
|
const teammates = await db.all(
|
|
1973
2086
|
`SELECT u.id, u.email, u.name, u.is_invited,
|
|
1974
2087
|
(SELECT COUNT(*) FROM api_keys WHERE user_id = u.id) as key_count,
|
|
1975
|
-
|
|
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
|
|
1976
2094
|
FROM users u
|
|
1977
2095
|
WHERE u.id != ?
|
|
1978
2096
|
ORDER BY u.created_at DESC`,
|
|
@@ -2930,7 +3048,7 @@ async function vaultFolderPaths(nestId, opts = {}) {
|
|
|
2930
3048
|
});
|
|
2931
3049
|
return entries.map(
|
|
2932
3050
|
(e) => prefix && e.path.startsWith(`${prefix}/`) ? e.path.slice(prefix.length + 1) : e.path
|
|
2933
|
-
).filter(
|
|
3051
|
+
).filter((p) => p && !(!prefix && (p === "assets" || p.startsWith("assets/"))));
|
|
2934
3052
|
}
|
|
2935
3053
|
async function listNestFolders(nestId, opts = {}) {
|
|
2936
3054
|
if (opts.folder !== void 0) assertSafeFolder(opts.folder);
|
|
@@ -3908,6 +4026,7 @@ async function vaultFolderDir(nestId, rel) {
|
|
|
3908
4026
|
}
|
|
3909
4027
|
async function vaultFolderExists(nestId, rel) {
|
|
3910
4028
|
if (!rel) return false;
|
|
4029
|
+
assertSafeFolder(rel);
|
|
3911
4030
|
try {
|
|
3912
4031
|
return (await stat(await vaultFolderDir(nestId, rel))).isDirectory();
|
|
3913
4032
|
} catch {
|
|
@@ -4066,14 +4185,14 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail, client) {
|
|
|
4066
4185
|
if (isNestStorageRoot(src)) {
|
|
4067
4186
|
throw new ValidationError("Folder is not eligible for sync");
|
|
4068
4187
|
}
|
|
4069
|
-
let
|
|
4188
|
+
let stat3;
|
|
4070
4189
|
try {
|
|
4071
|
-
|
|
4190
|
+
stat3 = statSync(src);
|
|
4072
4191
|
} catch {
|
|
4073
4192
|
console.warn(`[unsynced] source missing: ${src}`);
|
|
4074
4193
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
4075
4194
|
}
|
|
4076
|
-
if (!
|
|
4195
|
+
if (!stat3.isDirectory()) {
|
|
4077
4196
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
4078
4197
|
}
|
|
4079
4198
|
const files = collectMarkdownFiles(src, src);
|
|
@@ -4122,19 +4241,226 @@ async function deleteUnsyncedFolder(folderName) {
|
|
|
4122
4241
|
if (isNestStorageRoot(src)) {
|
|
4123
4242
|
throw new ValidationError("Folder is not eligible for delete");
|
|
4124
4243
|
}
|
|
4125
|
-
let
|
|
4244
|
+
let stat3;
|
|
4126
4245
|
try {
|
|
4127
|
-
|
|
4246
|
+
stat3 = statSync(src);
|
|
4128
4247
|
} catch {
|
|
4129
4248
|
throw new NotFoundError(`Folder not found: ${folderName}`);
|
|
4130
4249
|
}
|
|
4131
|
-
if (!
|
|
4250
|
+
if (!stat3.isDirectory()) {
|
|
4132
4251
|
throw new ValidationError(`Not a directory: ${folderName}`);
|
|
4133
4252
|
}
|
|
4134
4253
|
await rm2(src, { recursive: true, force: true });
|
|
4135
4254
|
console.log(`[unsynced] deleted folder ${src}`);
|
|
4136
4255
|
}
|
|
4137
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
|
+
|
|
4138
4464
|
// src/nests/routes.ts
|
|
4139
4465
|
async function effectiveAccess(nestId, userId) {
|
|
4140
4466
|
const access = await resolveNestAccess(nestId, userId);
|
|
@@ -4157,6 +4483,11 @@ function isGrantOnly(access) {
|
|
|
4157
4483
|
return access.permission === "none" && access.hasGrant;
|
|
4158
4484
|
}
|
|
4159
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
|
+
}
|
|
4160
4491
|
nestRoutes.get("/", async (c) => {
|
|
4161
4492
|
const userId = c.get("userId");
|
|
4162
4493
|
const archivedOnly = c.req.query("archived") === "1";
|
|
@@ -4212,9 +4543,11 @@ nestRoutes.get("/", async (c) => {
|
|
|
4212
4543
|
pinned: pinned.has(n.id)
|
|
4213
4544
|
};
|
|
4214
4545
|
};
|
|
4546
|
+
const nestScope = c.get("nestScope");
|
|
4215
4547
|
const seen = /* @__PURE__ */ new Set();
|
|
4216
4548
|
const deduped = [];
|
|
4217
4549
|
for (const n of [...owned, ...shared, ...publicExtras]) {
|
|
4550
|
+
if (nestScope && n.id !== nestScope) continue;
|
|
4218
4551
|
if (seen.has(n.id)) continue;
|
|
4219
4552
|
seen.add(n.id);
|
|
4220
4553
|
deduped.push(n);
|
|
@@ -4279,7 +4612,7 @@ nestRoutes.get("/", async (c) => {
|
|
|
4279
4612
|
}
|
|
4280
4613
|
}
|
|
4281
4614
|
}
|
|
4282
|
-
const withMeta = out.map(
|
|
4615
|
+
const withMeta = out.map((raw) => withParsedBranding(raw)).map(
|
|
4283
4616
|
(n) => n.grant_only ? {
|
|
4284
4617
|
...n,
|
|
4285
4618
|
document_count: grantedDocsByNest.get(n.id) ?? 0,
|
|
@@ -4305,7 +4638,7 @@ nestRoutes.post("/", async (c) => {
|
|
|
4305
4638
|
throw new ValidationError("name is required");
|
|
4306
4639
|
}
|
|
4307
4640
|
const nest = await createNest(c.get("userId"), body.name, body.description);
|
|
4308
|
-
return c.json({ nest }, 201);
|
|
4641
|
+
return c.json({ nest: withParsedBranding(nest) }, 201);
|
|
4309
4642
|
});
|
|
4310
4643
|
nestRoutes.post("/:nestId", async (c) => {
|
|
4311
4644
|
if (c.req.param("nestId") !== "import") {
|
|
@@ -4323,7 +4656,7 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4323
4656
|
const email2 = await resolveCallerEmail(userId);
|
|
4324
4657
|
if (body.finalize) {
|
|
4325
4658
|
return c.json({
|
|
4326
|
-
nest: nest2,
|
|
4659
|
+
nest: withParsedBranding(nest2),
|
|
4327
4660
|
documents: await registerImportedDocuments(
|
|
4328
4661
|
target,
|
|
4329
4662
|
email2,
|
|
@@ -4354,14 +4687,14 @@ nestRoutes.post("/:nestId", async (c) => {
|
|
|
4354
4687
|
try {
|
|
4355
4688
|
const { rejected } = await stageImportedFiles(nest.id, files, email);
|
|
4356
4689
|
if (body.partial) {
|
|
4357
|
-
return c.json({ nest, rejected }, 201);
|
|
4690
|
+
return c.json({ nest: withParsedBranding(nest), rejected }, 201);
|
|
4358
4691
|
}
|
|
4359
4692
|
const documents = await registerImportedDocuments(
|
|
4360
4693
|
nest.id,
|
|
4361
4694
|
email,
|
|
4362
4695
|
attributeRequest(c, body.client)
|
|
4363
4696
|
);
|
|
4364
|
-
return c.json({ nest, documents, rejected }, 201);
|
|
4697
|
+
return c.json({ nest: withParsedBranding(nest), documents, rejected }, 201);
|
|
4365
4698
|
} catch (err) {
|
|
4366
4699
|
await deleteNest(nest.id).catch(() => {
|
|
4367
4700
|
});
|
|
@@ -4427,7 +4760,7 @@ nestRoutes.get("/:nestId", async (c) => {
|
|
|
4427
4760
|
const grant_only = roles.length === 0 && isGrantOnly(access);
|
|
4428
4761
|
const nest = await getNest(nestId);
|
|
4429
4762
|
return c.json({
|
|
4430
|
-
nest,
|
|
4763
|
+
nest: withParsedBranding(nest),
|
|
4431
4764
|
permission,
|
|
4432
4765
|
roles: grant_only ? ["viewer"] : roles,
|
|
4433
4766
|
myStewards,
|
|
@@ -4451,7 +4784,7 @@ nestRoutes.patch("/:nestId", async (c) => {
|
|
|
4451
4784
|
name: body.name,
|
|
4452
4785
|
description: body.description
|
|
4453
4786
|
});
|
|
4454
|
-
return c.json({ nest });
|
|
4787
|
+
return c.json({ nest: withParsedBranding(nest) });
|
|
4455
4788
|
});
|
|
4456
4789
|
nestRoutes.post("/:nestId/pin", async (c) => {
|
|
4457
4790
|
const nestId = c.req.param("nestId");
|
|
@@ -4594,21 +4927,10 @@ async function addCollaborator(params) {
|
|
|
4594
4927
|
if (!isEmailish(params.email)) {
|
|
4595
4928
|
throw new ValidationError("Enter a valid email address.");
|
|
4596
4929
|
}
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
);
|
|
4602
|
-
if (existing) {
|
|
4603
|
-
userId = existing.id;
|
|
4604
|
-
} else {
|
|
4605
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4606
|
-
userId = uuid6();
|
|
4607
|
-
await db.run(
|
|
4608
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4609
|
-
[userId, email, null, await hashPassword2(uuid6())]
|
|
4610
|
-
);
|
|
4611
|
-
}
|
|
4930
|
+
userId = (await findOrCreateInvitedUser(params.email, {
|
|
4931
|
+
invitedBy: params.grantedByEmail,
|
|
4932
|
+
baseUrl: params.baseUrl
|
|
4933
|
+
})).id;
|
|
4612
4934
|
}
|
|
4613
4935
|
if (!userId) {
|
|
4614
4936
|
throw new ValidationError("user_id or email is required");
|
|
@@ -4667,13 +4989,18 @@ async function addCollaborator(params) {
|
|
|
4667
4989
|
// event. Slack/Teams still fire.
|
|
4668
4990
|
{ skipEmail: true }
|
|
4669
4991
|
);
|
|
4670
|
-
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;
|
|
4671
4997
|
if (inviteeEmail) {
|
|
4998
|
+
const nestLink = docLink(nestId, void 0, params.baseUrl);
|
|
4672
4999
|
void sendEmailToRecipient(inviteeEmail, {
|
|
4673
5000
|
status: "shared",
|
|
4674
5001
|
docTitle: shareNestName,
|
|
4675
5002
|
path: shareNestName,
|
|
4676
|
-
link:
|
|
5003
|
+
link: nestLink && invitee?.is_invited === 1 ? `${nestLink}&invite=${encodeURIComponent(inviteeEmail)}` : nestLink,
|
|
4677
5004
|
actor: inviteeEmail,
|
|
4678
5005
|
permission: params.permission,
|
|
4679
5006
|
by: params.grantedByEmail || void 0
|
|
@@ -4867,10 +5194,78 @@ sharingRoutes.patch("/reader-mode", async (c) => {
|
|
|
4867
5194
|
});
|
|
4868
5195
|
});
|
|
4869
5196
|
|
|
4870
|
-
// src/nests/
|
|
5197
|
+
// src/nests/branding-routes.ts
|
|
4871
5198
|
import { Hono as Hono5 } from "hono";
|
|
4872
|
-
import {
|
|
4873
|
-
|
|
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();
|
|
4874
5269
|
async function requireManage(c, nestId, opts = {}) {
|
|
4875
5270
|
if (config.AUTH_MODE === "open") return;
|
|
4876
5271
|
const access = await resolveNestAccess(nestId, c.get("userId"));
|
|
@@ -4886,17 +5281,8 @@ async function requireManage(c, nestId, opts = {}) {
|
|
|
4886
5281
|
}
|
|
4887
5282
|
}
|
|
4888
5283
|
async function resolveGrantee(email) {
|
|
4889
|
-
const
|
|
4890
|
-
|
|
4891
|
-
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
4892
|
-
if (existing) return existing.id;
|
|
4893
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
4894
|
-
const id = uuid7();
|
|
4895
|
-
await db.run(
|
|
4896
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
4897
|
-
[id, e, null, await hashPassword2(uuid7())]
|
|
4898
|
-
);
|
|
4899
|
-
return id;
|
|
5284
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
|
|
5285
|
+
return (await findOrCreateInvitedUser2(email)).id;
|
|
4900
5286
|
}
|
|
4901
5287
|
async function assertGrantAddsAccess(params) {
|
|
4902
5288
|
const { nestId, email, userId, targetType, target, role } = params;
|
|
@@ -4993,10 +5379,10 @@ grantRoutes.delete("/:id", async (c) => {
|
|
|
4993
5379
|
});
|
|
4994
5380
|
|
|
4995
5381
|
// src/nests/publish-routes.ts
|
|
4996
|
-
import { Hono as
|
|
5382
|
+
import { Hono as Hono7 } from "hono";
|
|
4997
5383
|
|
|
4998
5384
|
// src/nests/publish-service.ts
|
|
4999
|
-
import { v4 as
|
|
5385
|
+
import { v4 as uuid7 } from "uuid";
|
|
5000
5386
|
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
5001
5387
|
var MODES = ["public", "email", "code", "invite"];
|
|
5002
5388
|
var LINK_TTL_SECONDS = 60 * 60;
|
|
@@ -5065,7 +5451,7 @@ async function createPublish(params) {
|
|
|
5065
5451
|
);
|
|
5066
5452
|
}
|
|
5067
5453
|
const db = getDb();
|
|
5068
|
-
const id =
|
|
5454
|
+
const id = uuid7();
|
|
5069
5455
|
const slug = newSlug();
|
|
5070
5456
|
const created = nowIso();
|
|
5071
5457
|
const codeHash = mode === "code" && params.code ? await hashPassword(params.code.trim()) : null;
|
|
@@ -5096,7 +5482,7 @@ async function createPublish(params) {
|
|
|
5096
5482
|
await db.run(
|
|
5097
5483
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5098
5484
|
VALUES (?, ?, 'invite', ?, NULL, NULL, NULL, ?)`,
|
|
5099
|
-
[
|
|
5485
|
+
[uuid7(), id, email, created]
|
|
5100
5486
|
);
|
|
5101
5487
|
}
|
|
5102
5488
|
}
|
|
@@ -5151,7 +5537,7 @@ async function mintMagicLink(publishId, email) {
|
|
|
5151
5537
|
await getDb().run(
|
|
5152
5538
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5153
5539
|
VALUES (?, ?, 'link', ?, ?, ?, NULL, ?)`,
|
|
5154
|
-
[
|
|
5540
|
+
[uuid7(), publishId, e, sha256Hex(token), expires, nowIso()]
|
|
5155
5541
|
);
|
|
5156
5542
|
return token;
|
|
5157
5543
|
}
|
|
@@ -5181,7 +5567,7 @@ async function createSession2(publishId, email) {
|
|
|
5181
5567
|
await getDb().run(
|
|
5182
5568
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5183
5569
|
VALUES (?, ?, 'session', ?, ?, ?, NULL, ?)`,
|
|
5184
|
-
[
|
|
5570
|
+
[uuid7(), publishId, email, sha256Hex(token), expires, nowIso()]
|
|
5185
5571
|
);
|
|
5186
5572
|
return token;
|
|
5187
5573
|
}
|
|
@@ -5198,7 +5584,7 @@ async function logView(publishId, email) {
|
|
|
5198
5584
|
await getDb().run(
|
|
5199
5585
|
`INSERT INTO access_grant (id, publish_id, kind, email, token_hash, expires_at, used_at, created_at)
|
|
5200
5586
|
VALUES (?, ?, 'view', ?, NULL, NULL, NULL, ?)`,
|
|
5201
|
-
[
|
|
5587
|
+
[uuid7(), publishId, email, nowIso()]
|
|
5202
5588
|
);
|
|
5203
5589
|
}
|
|
5204
5590
|
async function listAllowlist(publishId) {
|
|
@@ -5216,7 +5602,7 @@ async function listViews(publishId) {
|
|
|
5216
5602
|
}
|
|
5217
5603
|
|
|
5218
5604
|
// src/nests/publish-routes.ts
|
|
5219
|
-
var publishRoutes = new
|
|
5605
|
+
var publishRoutes = new Hono7();
|
|
5220
5606
|
async function requireManage2(c, nestId) {
|
|
5221
5607
|
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
5222
5608
|
throw new ForbiddenError(
|
|
@@ -5231,7 +5617,7 @@ publishRoutes.post("/", async (c) => {
|
|
|
5231
5617
|
const mode = body.access_mode ?? "public";
|
|
5232
5618
|
if ((mode === "email" || mode === "invite") && !isEmailConfigured()) {
|
|
5233
5619
|
throw new ValidationError(
|
|
5234
|
-
"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."
|
|
5235
5621
|
);
|
|
5236
5622
|
}
|
|
5237
5623
|
const row = await createPublish({
|
|
@@ -5287,11 +5673,11 @@ publishRoutes.delete("/:id", async (c) => {
|
|
|
5287
5673
|
});
|
|
5288
5674
|
|
|
5289
5675
|
// src/nests/publish-render-routes.ts
|
|
5290
|
-
import { Hono as
|
|
5676
|
+
import { Hono as Hono8 } from "hono";
|
|
5291
5677
|
import MarkdownIt from "markdown-it";
|
|
5292
5678
|
|
|
5293
5679
|
// src/annotations/service.ts
|
|
5294
|
-
import { v4 as
|
|
5680
|
+
import { v4 as uuid8 } from "uuid";
|
|
5295
5681
|
|
|
5296
5682
|
// src/annotations/projection.ts
|
|
5297
5683
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -5445,7 +5831,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5445
5831
|
throw new Error("comment body is required");
|
|
5446
5832
|
}
|
|
5447
5833
|
const db = getDb();
|
|
5448
|
-
const id =
|
|
5834
|
+
const id = uuid8();
|
|
5449
5835
|
const anchor = clampAnchor(input.anchor);
|
|
5450
5836
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
5451
5837
|
await db.transaction(async (tx) => {
|
|
@@ -5465,7 +5851,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
5465
5851
|
await tx.run(
|
|
5466
5852
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5467
5853
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5468
|
-
[
|
|
5854
|
+
[uuid8(), id, authorEmail, body]
|
|
5469
5855
|
);
|
|
5470
5856
|
});
|
|
5471
5857
|
return rowToThread(await getThreadRow(id));
|
|
@@ -5487,7 +5873,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
5487
5873
|
await db.run(
|
|
5488
5874
|
`INSERT INTO annotation_comments (id, thread_id, author, body, created_at)
|
|
5489
5875
|
VALUES (?, ?, ?, ?, ${nowMsExpr(db)})`,
|
|
5490
|
-
[
|
|
5876
|
+
[uuid8(), threadId, authorEmail, trimmed]
|
|
5491
5877
|
);
|
|
5492
5878
|
return rowToThread(await getThreadRow(threadId));
|
|
5493
5879
|
}
|
|
@@ -5608,7 +5994,9 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
5608
5994
|
}
|
|
5609
5995
|
|
|
5610
5996
|
// src/nests/publish-render-routes.ts
|
|
5611
|
-
|
|
5997
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
5998
|
+
import { join as join4 } from "path";
|
|
5999
|
+
var publishRenderRoutes = new Hono8();
|
|
5612
6000
|
var SESSION_TTL_SECONDS2 = 24 * 60 * 60;
|
|
5613
6001
|
var GATE_LIMIT = { max: 10, windowMs: 10 * 6e4 };
|
|
5614
6002
|
var COMMENT_LIMIT = { max: 5, windowMs: 10 * 6e4 };
|
|
@@ -5665,6 +6053,114 @@ function setSessionCookie2(c, slug, token) {
|
|
|
5665
6053
|
function esc(s) {
|
|
5666
6054
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5667
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
|
+
}
|
|
5668
6164
|
var NO_DOWNLOAD_CSS = `<style>
|
|
5669
6165
|
@media print{html,body{display:none !important}}
|
|
5670
6166
|
html,body{-webkit-user-select:none;-ms-user-select:none;user-select:none}
|
|
@@ -5736,7 +6232,10 @@ async function renderPublished(c, row, viewerEmail) {
|
|
|
5736
6232
|
}
|
|
5737
6233
|
const title = esc(artifact.title || row.node_id);
|
|
5738
6234
|
const heading = /^\s*#\s/.test(body) ? "" : `<h1>${title}</h1>`;
|
|
5739
|
-
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>`;
|
|
5740
6239
|
return c.body(shell, 200, { "Content-Type": "text/html; charset=utf-8" });
|
|
5741
6240
|
}
|
|
5742
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>`;
|
|
@@ -5757,7 +6256,27 @@ var GATE_EYE_SCRIPT = `<script>
|
|
|
5757
6256
|
});
|
|
5758
6257
|
})();
|
|
5759
6258
|
</script>`;
|
|
5760
|
-
|
|
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) {
|
|
5761
6280
|
const wantsCode = row.access_mode === "code";
|
|
5762
6281
|
const field2 = wantsCode ? `<label>Access code<span class="wrap">
|
|
5763
6282
|
<input id="cn-code" name="code" type="password" autocomplete="off" required autofocus>
|
|
@@ -5782,10 +6301,10 @@ function gatePage(row, message) {
|
|
|
5782
6301
|
button.eye svg{width:18px;height:18px;display:block}
|
|
5783
6302
|
.err{color:#be123c;font-weight:600}
|
|
5784
6303
|
.brand{font-size:12px;color:#a8a29e;margin:18px 0 0;text-align:center}
|
|
5785
|
-
</style
|
|
5786
|
-
<h1>Protected document</h1><p>${esc(note)}</p>${err}${field2}
|
|
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}
|
|
5787
6306
|
<button type="submit">${cta}</button>
|
|
5788
|
-
|
|
6307
|
+
${brand?.b.hide_powered_by ? "" : `<p class="brand">Secured by PromptOwl \xB7 ContextNest</p>`}${gateBrandFoot(brand)}
|
|
5789
6308
|
</form>${wantsCode ? GATE_EYE_SCRIPT : ""}</body></html>`;
|
|
5790
6309
|
}
|
|
5791
6310
|
function unavailablePage() {
|
|
@@ -5804,7 +6323,7 @@ function unavailablePage() {
|
|
|
5804
6323
|
function sentPage() {
|
|
5805
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>`;
|
|
5806
6325
|
}
|
|
5807
|
-
function readerShell(row, title, viewerEmail) {
|
|
6326
|
+
function readerShell(row, title, viewerEmail, brand = null) {
|
|
5808
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>`;
|
|
5809
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>
|
|
5810
6329
|
html,body{margin:0;height:100%}
|
|
@@ -5825,7 +6344,7 @@ function readerShell(row, title, viewerEmail) {
|
|
|
5825
6344
|
button[disabled]{opacity:.6;cursor:default}
|
|
5826
6345
|
.msg{font-weight:600;margin:0 0 14px;font-size:14px}
|
|
5827
6346
|
.msg.err{color:#be123c}.msg.ok{color:#15803d}
|
|
5828
|
-
</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>
|
|
5829
6348
|
<iframe id="cn-frame" src="/p/${esc(row.slug)}/view" title="${esc(title)}"></iframe>
|
|
5830
6349
|
<button id="cn-open" type="button">Leave a comment</button>
|
|
5831
6350
|
<div id="cn-back"><div class="card" role="dialog" aria-modal="true" aria-label="Leave a comment">
|
|
@@ -5896,12 +6415,12 @@ publishRenderRoutes.get("/:slug", async (c) => {
|
|
|
5896
6415
|
const row = await getPublishBySlug(slug);
|
|
5897
6416
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5898
6417
|
const viewer = await resolveViewer(c, row);
|
|
5899
|
-
if (!viewer) return c.html(gatePage(row));
|
|
6418
|
+
if (!viewer) return c.html(gatePage(row, void 0, await loadBrand(row)));
|
|
5900
6419
|
if (row.comments_enabled !== 1) return renderPublished(c, row, viewer.email);
|
|
5901
6420
|
const title = await publishedTitle(row);
|
|
5902
6421
|
c.header("Content-Security-Policy", SHELL_CSP);
|
|
5903
6422
|
c.header("Referrer-Policy", "no-referrer");
|
|
5904
|
-
return c.html(readerShell(row, title, viewer.email));
|
|
6423
|
+
return c.html(readerShell(row, title, viewer.email, await loadBrand(row)));
|
|
5905
6424
|
});
|
|
5906
6425
|
publishRenderRoutes.get("/:slug/view", async (c) => {
|
|
5907
6426
|
const slug = c.req.param("slug");
|
|
@@ -5939,20 +6458,20 @@ publishRenderRoutes.post("/:slug/gate", async (c) => {
|
|
|
5939
6458
|
const row = await getPublishBySlug(slug);
|
|
5940
6459
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5941
6460
|
if (!tryConsume(`publish-gate:${slug}:${clientIp(c)}`, GATE_LIMIT)) {
|
|
5942
|
-
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes."), 429);
|
|
6461
|
+
return c.html(gatePage(row, "Too many attempts. Try again in a few minutes.", await loadBrand(row)), 429);
|
|
5943
6462
|
}
|
|
5944
6463
|
if (row.access_mode === "public") return c.redirect(`/p/${slug}`);
|
|
5945
6464
|
const form = await c.req.parseBody();
|
|
5946
6465
|
if (row.access_mode === "code") {
|
|
5947
6466
|
if (!await verifyCode(row, String(form.code ?? ""))) {
|
|
5948
|
-
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);
|
|
5949
6468
|
}
|
|
5950
6469
|
setSessionCookie2(c, slug, await createSession2(row.id, null));
|
|
5951
6470
|
return c.redirect(`/p/${slug}`);
|
|
5952
6471
|
}
|
|
5953
6472
|
const email = normalizeEmail(String(form.email ?? ""));
|
|
5954
6473
|
if (!isEmailish(email)) {
|
|
5955
|
-
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);
|
|
5956
6475
|
}
|
|
5957
6476
|
const allowed = row.access_mode !== "invite" || await isAllowlisted(row.id, email);
|
|
5958
6477
|
const linkToken = await mintMagicLink(row.id, email);
|
|
@@ -5972,15 +6491,15 @@ publishRenderRoutes.get("/:slug/verify", async (c) => {
|
|
|
5972
6491
|
if (!isLivePublish(row)) return c.html(unavailablePage(), 404);
|
|
5973
6492
|
const email = await consumeMagicLink(row.id, c.req.query("token") ?? "");
|
|
5974
6493
|
if (!email) {
|
|
5975
|
-
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);
|
|
5976
6495
|
}
|
|
5977
6496
|
setSessionCookie2(c, slug, await createSession2(row.id, email));
|
|
5978
6497
|
return c.redirect(`/p/${slug}`);
|
|
5979
6498
|
});
|
|
5980
6499
|
|
|
5981
6500
|
// src/nests/nest-team-routes.ts
|
|
5982
|
-
import { Hono as
|
|
5983
|
-
var nestTeamRoutes = new
|
|
6501
|
+
import { Hono as Hono9 } from "hono";
|
|
6502
|
+
var nestTeamRoutes = new Hono9();
|
|
5984
6503
|
nestTeamRoutes.get("/", async (c) => {
|
|
5985
6504
|
const nestId = c.req.param("nestId");
|
|
5986
6505
|
const refs = await listNestTeams(nestId);
|
|
@@ -6019,8 +6538,8 @@ nestTeamRoutes.delete("/:teamId", async (c) => {
|
|
|
6019
6538
|
});
|
|
6020
6539
|
|
|
6021
6540
|
// src/nests/people-routes.ts
|
|
6022
|
-
import { Hono as
|
|
6023
|
-
var peopleRoutes = new
|
|
6541
|
+
import { Hono as Hono10 } from "hono";
|
|
6542
|
+
var peopleRoutes = new Hono10();
|
|
6024
6543
|
peopleRoutes.get("/suggest", async (c) => {
|
|
6025
6544
|
const userId = c.get("userId");
|
|
6026
6545
|
const q = c.req.query("q") ?? "";
|
|
@@ -6030,7 +6549,7 @@ peopleRoutes.get("/suggest", async (c) => {
|
|
|
6030
6549
|
});
|
|
6031
6550
|
|
|
6032
6551
|
// src/governance/team-routes.ts
|
|
6033
|
-
import { Hono as
|
|
6552
|
+
import { Hono as Hono11 } from "hono";
|
|
6034
6553
|
|
|
6035
6554
|
// src/governance/promptowl-teams.ts
|
|
6036
6555
|
var PROMPTOWL_SOURCE = "promptowl";
|
|
@@ -6177,7 +6696,7 @@ async function desiredRoster(poTeam, callerEmail) {
|
|
|
6177
6696
|
}
|
|
6178
6697
|
|
|
6179
6698
|
// src/governance/team-routes.ts
|
|
6180
|
-
var teamRoutes = new
|
|
6699
|
+
var teamRoutes = new Hono11();
|
|
6181
6700
|
var PROMPTOWL_LIMIT = { max: 10, windowMs: 15 * 6e4 };
|
|
6182
6701
|
var CONNECT_REQUIRED = "promptowl_connect_required";
|
|
6183
6702
|
var CONNECT_REQUIRED_STATUS = 428;
|
|
@@ -6362,7 +6881,7 @@ function toSearchHit(node, query) {
|
|
|
6362
6881
|
}
|
|
6363
6882
|
|
|
6364
6883
|
// src/nodes/routes.ts
|
|
6365
|
-
import { Hono as
|
|
6884
|
+
import { Hono as Hono12 } from "hono";
|
|
6366
6885
|
import { parseDocument as parseDocument3, readPdfBinary as readPdfBinary2 } from "@promptowl/contextnest-engine";
|
|
6367
6886
|
|
|
6368
6887
|
// src/nodes/markdown-export.ts
|
|
@@ -6415,7 +6934,7 @@ ${m[1]}
|
|
|
6415
6934
|
}
|
|
6416
6935
|
|
|
6417
6936
|
// src/nodes/routes.ts
|
|
6418
|
-
var nodeRoutes = new
|
|
6937
|
+
var nodeRoutes = new Hono12();
|
|
6419
6938
|
function nodeAsMarkdown(response, nodeId) {
|
|
6420
6939
|
return nodeToMarkdown({
|
|
6421
6940
|
id: nodeId,
|
|
@@ -6498,7 +7017,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
6498
7017
|
nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
6499
7018
|
const nestId = c.req.param("nestId");
|
|
6500
7019
|
const nodeId = c.req.param("nodeId");
|
|
6501
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
7020
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-TDZUC2Q6.js");
|
|
6502
7021
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
6503
7022
|
nestId,
|
|
6504
7023
|
nodeId
|
|
@@ -6519,7 +7038,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
|
6519
7038
|
nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
|
|
6520
7039
|
const nestId = c.req.param("nestId");
|
|
6521
7040
|
const nodeId = c.req.param("nodeId");
|
|
6522
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
7041
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-2EWN7WWO.js");
|
|
6523
7042
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
6524
7043
|
return c.json({ reviews: history });
|
|
6525
7044
|
});
|
|
@@ -6798,13 +7317,13 @@ async function getUserEmail(c) {
|
|
|
6798
7317
|
}
|
|
6799
7318
|
|
|
6800
7319
|
// src/annotations/routes.ts
|
|
6801
|
-
import { Hono as
|
|
7320
|
+
import { Hono as Hono13 } from "hono";
|
|
6802
7321
|
|
|
6803
7322
|
// src/annotations/types.ts
|
|
6804
7323
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
6805
7324
|
|
|
6806
7325
|
// src/annotations/routes.ts
|
|
6807
|
-
var annotationRoutes = new
|
|
7326
|
+
var annotationRoutes = new Hono13();
|
|
6808
7327
|
function getNodeId(c) {
|
|
6809
7328
|
const raw = c.req.param("nodeId");
|
|
6810
7329
|
try {
|
|
@@ -6932,7 +7451,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
6932
7451
|
});
|
|
6933
7452
|
|
|
6934
7453
|
// src/nodes/query-routes.ts
|
|
6935
|
-
import { Hono as
|
|
7454
|
+
import { Hono as Hono14 } from "hono";
|
|
6936
7455
|
|
|
6937
7456
|
// src/tables/service.ts
|
|
6938
7457
|
function splitCsvLine(line) {
|
|
@@ -7392,7 +7911,7 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
7392
7911
|
|
|
7393
7912
|
// src/nests/bundle-service.ts
|
|
7394
7913
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
7395
|
-
import { join as
|
|
7914
|
+
import { join as join5, sep } from "path";
|
|
7396
7915
|
import JSZip from "jszip";
|
|
7397
7916
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
7398
7917
|
function walkFiles(root) {
|
|
@@ -7405,7 +7924,7 @@ function walkFiles(root) {
|
|
|
7405
7924
|
}
|
|
7406
7925
|
for (const rel of entries) {
|
|
7407
7926
|
try {
|
|
7408
|
-
if (statSync2(
|
|
7927
|
+
if (statSync2(join5(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
|
|
7409
7928
|
} catch {
|
|
7410
7929
|
}
|
|
7411
7930
|
}
|
|
@@ -7446,13 +7965,17 @@ async function exportNestBundle(nestId, includeDrafts) {
|
|
|
7446
7965
|
const segs = rel.split("/");
|
|
7447
7966
|
const keep = segs.includes(".versions") || rel === ".context/config.yaml" || rel === "CONTEXT.md";
|
|
7448
7967
|
if (!keep) continue;
|
|
7449
|
-
zip.file(rel, readFileSync2(
|
|
7968
|
+
zip.file(rel, readFileSync2(join5(nestPath, rel)));
|
|
7450
7969
|
}
|
|
7451
7970
|
trackEvent("nest.export.bundle", { nestId, documents: count, includeDrafts });
|
|
7452
7971
|
return zip.generateAsync({ type: "arraybuffer" });
|
|
7453
7972
|
}
|
|
7454
7973
|
|
|
7455
7974
|
// src/nodes/graph-service.ts
|
|
7975
|
+
import {
|
|
7976
|
+
contextLinkTarget,
|
|
7977
|
+
extractContextLinks
|
|
7978
|
+
} from "@promptowl/contextnest-engine";
|
|
7456
7979
|
var MAX_GRAPH_NODES = 150;
|
|
7457
7980
|
var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
7458
7981
|
function bareSlug(idOrTarget) {
|
|
@@ -7470,6 +7993,15 @@ function extractWikiTargets(body) {
|
|
|
7470
7993
|
}
|
|
7471
7994
|
return out;
|
|
7472
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
|
+
}
|
|
7473
8005
|
async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
7474
8006
|
const { storage } = await engineCache.get(nestId);
|
|
7475
8007
|
const docs = (await storage.discoverDocuments()).filter(
|
|
@@ -7504,6 +8036,15 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7504
8036
|
allLinks.push({ source: d.id, target: targetId });
|
|
7505
8037
|
}
|
|
7506
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
|
+
}
|
|
7507
8048
|
const tagCounts = /* @__PURE__ */ new Map();
|
|
7508
8049
|
const tagToDocs = /* @__PURE__ */ new Map();
|
|
7509
8050
|
for (const n of allNodes) {
|
|
@@ -7616,7 +8157,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
7616
8157
|
}
|
|
7617
8158
|
|
|
7618
8159
|
// src/nodes/query-routes.ts
|
|
7619
|
-
var queryRoutes = new
|
|
8160
|
+
var queryRoutes = new Hono14();
|
|
7620
8161
|
queryRoutes.get("/changes", async (c) => {
|
|
7621
8162
|
const nestId = c.req.param("nestId");
|
|
7622
8163
|
const userId = c.get("userId");
|
|
@@ -7820,7 +8361,10 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7820
8361
|
hopsUsed = result.hopsUsed;
|
|
7821
8362
|
nodesTraversed = result.nodesTraversed;
|
|
7822
8363
|
}
|
|
7823
|
-
|
|
8364
|
+
const hopSeeds = [
|
|
8365
|
+
...new Map([...documents, ...titleMatches].map((d) => [d.id, d])).values()
|
|
8366
|
+
];
|
|
8367
|
+
if (hopSeeds.length > 0 && hops > 0) {
|
|
7824
8368
|
const byTitle = new Map(
|
|
7825
8369
|
allDocs.map((d) => [
|
|
7826
8370
|
String(d.frontmatter.title || "").toLowerCase(),
|
|
@@ -7838,9 +8382,13 @@ queryRoutes.post("/context", async (c) => {
|
|
|
7838
8382
|
const t = byId.get(target) ?? byTitle.get(target.toLowerCase());
|
|
7839
8383
|
if (t && t.id !== d.id) link(d.id, t.id);
|
|
7840
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
|
+
}
|
|
7841
8389
|
}
|
|
7842
|
-
const seen = new Set(
|
|
7843
|
-
let frontier =
|
|
8390
|
+
const seen = new Set(hopSeeds.map((d) => d.id));
|
|
8391
|
+
let frontier = hopSeeds.map((d) => d.id);
|
|
7844
8392
|
for (let depth = 0; depth < hops && frontier.length; depth++) {
|
|
7845
8393
|
const next = [];
|
|
7846
8394
|
for (const id of frontier) {
|
|
@@ -8207,7 +8755,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
8207
8755
|
});
|
|
8208
8756
|
|
|
8209
8757
|
// src/mcp/routes.ts
|
|
8210
|
-
import { Hono as
|
|
8758
|
+
import { Hono as Hono16 } from "hono";
|
|
8211
8759
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8212
8760
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
8213
8761
|
|
|
@@ -8297,10 +8845,10 @@ async function listTraceEvents(filters = {}) {
|
|
|
8297
8845
|
}
|
|
8298
8846
|
|
|
8299
8847
|
// src/mcp/workflow-tools.ts
|
|
8300
|
-
import { v4 as
|
|
8848
|
+
import { v4 as uuid11 } from "uuid";
|
|
8301
8849
|
|
|
8302
8850
|
// src/workflow/run-service.ts
|
|
8303
|
-
import { v4 as
|
|
8851
|
+
import { v4 as uuid9 } from "uuid";
|
|
8304
8852
|
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
8305
8853
|
async function flowSubgraph(nestId, agentNode) {
|
|
8306
8854
|
const db = getDb();
|
|
@@ -8430,7 +8978,7 @@ async function triggerRun(opts) {
|
|
|
8430
8978
|
}
|
|
8431
8979
|
const edges = await flowSubgraph(nestId, agentNode);
|
|
8432
8980
|
const definitions = await referencedDefinitions(nestId, edges);
|
|
8433
|
-
const id = `run_${
|
|
8981
|
+
const id = `run_${uuid9()}`;
|
|
8434
8982
|
const db = getDb();
|
|
8435
8983
|
await db.transaction(async (tx) => {
|
|
8436
8984
|
if (parentRunId) {
|
|
@@ -8616,8 +9164,8 @@ async function listRuns(nestId, opts) {
|
|
|
8616
9164
|
}
|
|
8617
9165
|
|
|
8618
9166
|
// src/workflow/edge-routes.ts
|
|
8619
|
-
import { Hono as
|
|
8620
|
-
import { v4 as
|
|
9167
|
+
import { Hono as Hono15 } from "hono";
|
|
9168
|
+
import { v4 as uuid10 } from "uuid";
|
|
8621
9169
|
function toResponse(e, t) {
|
|
8622
9170
|
return {
|
|
8623
9171
|
id: e.id,
|
|
@@ -8735,7 +9283,7 @@ async function assertNoDuplicateEdge(db, nestId, fromNode, toNode, type, exclude
|
|
|
8735
9283
|
);
|
|
8736
9284
|
}
|
|
8737
9285
|
}
|
|
8738
|
-
var edgeRoutes = new
|
|
9286
|
+
var edgeRoutes = new Hono15();
|
|
8739
9287
|
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
8740
9288
|
const nestId = c.req.param("nestId");
|
|
8741
9289
|
const db = getDb();
|
|
@@ -8803,7 +9351,7 @@ edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
8803
9351
|
);
|
|
8804
9352
|
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
8805
9353
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8806
|
-
const id =
|
|
9354
|
+
const id = uuid10();
|
|
8807
9355
|
await db.transaction(async (tx) => {
|
|
8808
9356
|
await assertNoDuplicateEdge(tx, nestId, fromNode, toNode, type);
|
|
8809
9357
|
if (type.is_flow) {
|
|
@@ -9204,7 +9752,7 @@ async function createEdge(ctx, args) {
|
|
|
9204
9752
|
);
|
|
9205
9753
|
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
9206
9754
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9207
|
-
const id =
|
|
9755
|
+
const id = uuid11();
|
|
9208
9756
|
await db.transaction(async (tx) => {
|
|
9209
9757
|
await assertNoDuplicateEdge(tx, ctx.nestId, fromNode, toNode, type);
|
|
9210
9758
|
if (type.is_flow) {
|
|
@@ -9268,7 +9816,7 @@ async function upsertEdgeType(ctx, args) {
|
|
|
9268
9816
|
`INSERT INTO edge_types
|
|
9269
9817
|
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
9270
9818
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
9271
|
-
[
|
|
9819
|
+
[uuid11(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
9272
9820
|
);
|
|
9273
9821
|
}
|
|
9274
9822
|
});
|
|
@@ -10922,22 +11470,12 @@ ${list}`,
|
|
|
10922
11470
|
if (!target) return "target is required (a node id or folder prefix).";
|
|
10923
11471
|
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
10924
11472
|
try {
|
|
10925
|
-
const { createGrant: createGrant2 } = await import("./grants-service-
|
|
10926
|
-
const db = (await import("./client-D44ZHDTJ.js")).getDb();
|
|
11473
|
+
const { createGrant: createGrant2 } = await import("./grants-service-24GLUBY4.js");
|
|
10927
11474
|
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
10928
11475
|
const e = normalizeEmail2(String(args.email || ""));
|
|
10929
11476
|
if (!e) return "email is required.";
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
const { hashPassword: hashPassword2 } = await import("./keys-CTOGAG3W.js");
|
|
10933
|
-
const { v4: uuid20 } = await import("uuid");
|
|
10934
|
-
const id = uuid20();
|
|
10935
|
-
await db.run(
|
|
10936
|
-
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
10937
|
-
[id, e, null, await hashPassword2(uuid20())]
|
|
10938
|
-
);
|
|
10939
|
-
row = { id };
|
|
10940
|
-
}
|
|
11477
|
+
const { findOrCreateInvitedUser: findOrCreateInvitedUser2 } = await import("./invited-user-AM6A3WGO.js");
|
|
11478
|
+
const row = await findOrCreateInvitedUser2(e, { invitedBy: ctx.userEmail });
|
|
10941
11479
|
await createGrant2({
|
|
10942
11480
|
nestId: ctx.nestId,
|
|
10943
11481
|
targetType,
|
|
@@ -10989,8 +11527,8 @@ ${list}`;
|
|
|
10989
11527
|
}
|
|
10990
11528
|
|
|
10991
11529
|
// src/mcp/routes.ts
|
|
10992
|
-
import { z } from "zod";
|
|
10993
|
-
var mcpRoutes = new
|
|
11530
|
+
import { z as z2 } from "zod";
|
|
11531
|
+
var mcpRoutes = new Hono16();
|
|
10994
11532
|
async function getUserEmail2(userId) {
|
|
10995
11533
|
const db = getDb();
|
|
10996
11534
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
@@ -11008,10 +11546,10 @@ function createMcpServerForNest(nestId, userId, userEmail, baseUrl, c) {
|
|
|
11008
11546
|
const shape = {};
|
|
11009
11547
|
for (const [key, def] of Object.entries(props)) {
|
|
11010
11548
|
let field2;
|
|
11011
|
-
if (def.type === "string") field2 =
|
|
11012
|
-
else if (def.type === "number") field2 =
|
|
11013
|
-
else if (def.type === "array") field2 =
|
|
11014
|
-
else field2 =
|
|
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();
|
|
11015
11553
|
if (!required.includes(key)) field2 = field2.optional();
|
|
11016
11554
|
shape[key] = field2;
|
|
11017
11555
|
}
|
|
@@ -11060,8 +11598,8 @@ mcpRoutes.all("/", async (c) => {
|
|
|
11060
11598
|
});
|
|
11061
11599
|
|
|
11062
11600
|
// src/workflow/run-routes.ts
|
|
11063
|
-
import { Hono as
|
|
11064
|
-
import { v4 as
|
|
11601
|
+
import { Hono as Hono17 } from "hono";
|
|
11602
|
+
import { v4 as uuid12 } from "uuid";
|
|
11065
11603
|
var RUNNABLE_TYPES2 = ["agent", "skill"];
|
|
11066
11604
|
var safeJson2 = (s) => {
|
|
11067
11605
|
if (!s) return null;
|
|
@@ -11179,7 +11717,7 @@ async function buildRunBundle(nestId, agentNode) {
|
|
|
11179
11717
|
definitions
|
|
11180
11718
|
};
|
|
11181
11719
|
}
|
|
11182
|
-
var runTriggerRoutes = new
|
|
11720
|
+
var runTriggerRoutes = new Hono17();
|
|
11183
11721
|
async function resolveParentRun2(nestId, parentRunId, childAgent) {
|
|
11184
11722
|
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
11185
11723
|
throw new ValidationError(
|
|
@@ -11258,7 +11796,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
11258
11796
|
}
|
|
11259
11797
|
const bundle = await buildRunBundle(nestId, agentNode);
|
|
11260
11798
|
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
11261
|
-
const id = `run_${
|
|
11799
|
+
const id = `run_${uuid12()}`;
|
|
11262
11800
|
await getDb().run(
|
|
11263
11801
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
11264
11802
|
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
@@ -11287,7 +11825,7 @@ runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
|
11287
11825
|
201
|
|
11288
11826
|
);
|
|
11289
11827
|
});
|
|
11290
|
-
var runListRoutes = new
|
|
11828
|
+
var runListRoutes = new Hono17();
|
|
11291
11829
|
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11292
11830
|
const nestId = c.req.param("nestId");
|
|
11293
11831
|
const db = getDb();
|
|
@@ -11319,7 +11857,7 @@ runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
|
11319
11857
|
}))
|
|
11320
11858
|
});
|
|
11321
11859
|
});
|
|
11322
|
-
var runDetailRoutes = new
|
|
11860
|
+
var runDetailRoutes = new Hono17();
|
|
11323
11861
|
async function loadRunWithPermission(c, runId, required) {
|
|
11324
11862
|
const run = await getDb().get("SELECT * FROM runs WHERE id = ?", [
|
|
11325
11863
|
runId
|
|
@@ -11470,8 +12008,8 @@ runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11470
12008
|
});
|
|
11471
12009
|
|
|
11472
12010
|
// src/workflow/schedule-routes.ts
|
|
11473
|
-
import { Hono as
|
|
11474
|
-
import { v4 as
|
|
12011
|
+
import { Hono as Hono18 } from "hono";
|
|
12012
|
+
import { v4 as uuid13 } from "uuid";
|
|
11475
12013
|
var RUNNABLE_TYPES3 = ["agent", "skill"];
|
|
11476
12014
|
var MIN_EVERY = 5;
|
|
11477
12015
|
var MAX_EVERY = 7 * 24 * 60;
|
|
@@ -11508,7 +12046,7 @@ function parseEvery(v) {
|
|
|
11508
12046
|
}
|
|
11509
12047
|
return n;
|
|
11510
12048
|
}
|
|
11511
|
-
var scheduleRoutes = new
|
|
12049
|
+
var scheduleRoutes = new Hono18();
|
|
11512
12050
|
scheduleRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11513
12051
|
const nestId = c.req.param("nestId");
|
|
11514
12052
|
const rows = await getDb().all(
|
|
@@ -11549,7 +12087,7 @@ scheduleRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
|
11549
12087
|
);
|
|
11550
12088
|
}
|
|
11551
12089
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11552
|
-
const id = `sch_${
|
|
12090
|
+
const id = `sch_${uuid13()}`;
|
|
11553
12091
|
await db.run(
|
|
11554
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)
|
|
11555
12093
|
VALUES (?, ?, ?, ?, ?, NULL, ?, 0, ?, ?, ?)`,
|
|
@@ -11604,8 +12142,8 @@ scheduleRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
|
11604
12142
|
});
|
|
11605
12143
|
|
|
11606
12144
|
// src/notify/connector-routes.ts
|
|
11607
|
-
import { Hono as
|
|
11608
|
-
import { v4 as
|
|
12145
|
+
import { Hono as Hono19 } from "hono";
|
|
12146
|
+
import { v4 as uuid14 } from "uuid";
|
|
11609
12147
|
var KNOWN_EVENTS = [
|
|
11610
12148
|
"review_requested",
|
|
11611
12149
|
"review_approved",
|
|
@@ -11652,7 +12190,7 @@ function parseUrl(v) {
|
|
|
11652
12190
|
'url must be a public https:// URL (loopback/private/metadata hosts are refused) or an env reference like "env:SLACK_WEBHOOK"'
|
|
11653
12191
|
);
|
|
11654
12192
|
}
|
|
11655
|
-
var connectorRoutes = new
|
|
12193
|
+
var connectorRoutes = new Hono19();
|
|
11656
12194
|
connectorRoutes.get("/", async (c) => {
|
|
11657
12195
|
const nestId = c.req.param("nestId");
|
|
11658
12196
|
const rows = await getDb().all(
|
|
@@ -11671,7 +12209,7 @@ connectorRoutes.post("/", async (c) => {
|
|
|
11671
12209
|
const url = parseUrl(body.url);
|
|
11672
12210
|
const events = parseEvents(body.events);
|
|
11673
12211
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11674
|
-
const id = `con_${
|
|
12212
|
+
const id = `con_${uuid14()}`;
|
|
11675
12213
|
await getDb().run(
|
|
11676
12214
|
`INSERT INTO connectors (id, nest_id, channel, url, events, enabled, created_by, created_at, updated_at)
|
|
11677
12215
|
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)`,
|
|
@@ -11714,7 +12252,7 @@ connectorRoutes.delete("/:id", async (c) => {
|
|
|
11714
12252
|
});
|
|
11715
12253
|
|
|
11716
12254
|
// src/workflow/hook-routes.ts
|
|
11717
|
-
import { Hono as
|
|
12255
|
+
import { Hono as Hono20 } from "hono";
|
|
11718
12256
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2, randomUUID } from "crypto";
|
|
11719
12257
|
var PRESETS = ["slack", "teams", "webhook"];
|
|
11720
12258
|
var RUNNABLE_TYPES4 = ["agent", "skill"];
|
|
@@ -11737,7 +12275,7 @@ function toResponse4(c, r, opts = {}) {
|
|
|
11737
12275
|
created_at: r.created_at
|
|
11738
12276
|
};
|
|
11739
12277
|
}
|
|
11740
|
-
var hookNestRoutes = new
|
|
12278
|
+
var hookNestRoutes = new Hono20();
|
|
11741
12279
|
hookNestRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
11742
12280
|
const nestId = c.req.param("nestId");
|
|
11743
12281
|
const agent = c.req.query("agent");
|
|
@@ -11811,7 +12349,7 @@ function teamsSignatureValid(secret, authHeader, rawBody) {
|
|
|
11811
12349
|
function stripMentions(text) {
|
|
11812
12350
|
return text.replace(/<@[^>]+>/g, "").replace(/<at>[^<]*<\/at>/gi, "").trim();
|
|
11813
12351
|
}
|
|
11814
|
-
var hookFireRoutes = new
|
|
12352
|
+
var hookFireRoutes = new Hono20();
|
|
11815
12353
|
hookFireRoutes.post("/:token", async (c) => {
|
|
11816
12354
|
const token = c.req.param("token");
|
|
11817
12355
|
if (!tryConsume("hook:*", { max: 120, windowMs: 6e4 })) {
|
|
@@ -11913,9 +12451,9 @@ hookFireRoutes.post("/:token", async (c) => {
|
|
|
11913
12451
|
});
|
|
11914
12452
|
|
|
11915
12453
|
// src/definitions/routes.ts
|
|
11916
|
-
import { Hono as
|
|
11917
|
-
import { v4 as
|
|
11918
|
-
var definitionRoutes = new
|
|
12454
|
+
import { Hono as Hono21 } from "hono";
|
|
12455
|
+
import { v4 as uuid15 } from "uuid";
|
|
12456
|
+
var definitionRoutes = new Hono21();
|
|
11919
12457
|
definitionRoutes.get("/", async (c) => {
|
|
11920
12458
|
const nestId = c.req.param("nestId");
|
|
11921
12459
|
const db = getDb();
|
|
@@ -11994,7 +12532,7 @@ definitionRoutes.post("/", async (c) => {
|
|
|
11994
12532
|
await applyUpdate(tx, existing.id);
|
|
11995
12533
|
targetId = existing.id;
|
|
11996
12534
|
} else {
|
|
11997
|
-
targetId =
|
|
12535
|
+
targetId = uuid15();
|
|
11998
12536
|
wasCreated = true;
|
|
11999
12537
|
await tx.run(
|
|
12000
12538
|
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
@@ -12023,11 +12561,11 @@ definitionRoutes.delete("/:id", async (c) => {
|
|
|
12023
12561
|
});
|
|
12024
12562
|
|
|
12025
12563
|
// src/nests/asset-routes.ts
|
|
12026
|
-
import { Hono as
|
|
12027
|
-
import { v4 as
|
|
12028
|
-
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
12029
|
-
import { join as
|
|
12030
|
-
var assetRoutes = new
|
|
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();
|
|
12031
12569
|
var CONTENT_TYPES = {
|
|
12032
12570
|
png: "image/png",
|
|
12033
12571
|
jpg: "image/jpeg",
|
|
@@ -12073,10 +12611,10 @@ assetRoutes.post("/", async (c) => {
|
|
|
12073
12611
|
`${isVideo ? "video" : "image"} too large (${(file.size / (1024 * 1024)).toFixed(1)} MB, max ${maxBytes / (1024 * 1024)} MB for ${isVideo ? "video" : "images"})`
|
|
12074
12612
|
);
|
|
12075
12613
|
}
|
|
12076
|
-
const name = `${
|
|
12077
|
-
const dir =
|
|
12614
|
+
const name = `${uuid16()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
12615
|
+
const dir = join6(resolveNestPath(nestId), "assets");
|
|
12078
12616
|
await mkdir2(dir, { recursive: true });
|
|
12079
|
-
await writeFile(
|
|
12617
|
+
await writeFile(join6(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
12080
12618
|
const url = `/nests/${nestId}/assets/${name}`;
|
|
12081
12619
|
return c.json(
|
|
12082
12620
|
{
|
|
@@ -12098,7 +12636,7 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
12098
12636
|
}
|
|
12099
12637
|
let bytes;
|
|
12100
12638
|
try {
|
|
12101
|
-
bytes = await
|
|
12639
|
+
bytes = await readFile2(join6(resolveNestPath(nestId), "assets", name));
|
|
12102
12640
|
} catch {
|
|
12103
12641
|
throw new NotFoundError("Asset not found");
|
|
12104
12642
|
}
|
|
@@ -12114,11 +12652,11 @@ assetRoutes.get("/:file", async (c) => {
|
|
|
12114
12652
|
});
|
|
12115
12653
|
|
|
12116
12654
|
// src/nodes/pdf-routes.ts
|
|
12117
|
-
import { Hono as
|
|
12655
|
+
import { Hono as Hono23 } from "hono";
|
|
12118
12656
|
import { bodyLimit } from "hono/body-limit";
|
|
12119
12657
|
import { createHash as createHash3 } from "crypto";
|
|
12120
12658
|
import { isPdf, readPdfBinary as readPdfBinary3 } from "@promptowl/contextnest-engine";
|
|
12121
|
-
var pdfNodeRoutes = new
|
|
12659
|
+
var pdfNodeRoutes = new Hono23();
|
|
12122
12660
|
var PDF_UPLOAD_PATH = /^\/nests\/[^/]+\/nodes\/pdf\/?$/;
|
|
12123
12661
|
function pdfMaxRequestBytes() {
|
|
12124
12662
|
return config.PDF_MAX_BYTES + 1024 * 1024;
|
|
@@ -12291,7 +12829,7 @@ pdfNodeRoutes.get("/:nodeId{.+}/pdf", async (c, next) => {
|
|
|
12291
12829
|
});
|
|
12292
12830
|
|
|
12293
12831
|
// src/auth/community-sites-routes.ts
|
|
12294
|
-
import { Hono as
|
|
12832
|
+
import { Hono as Hono24 } from "hono";
|
|
12295
12833
|
var serverAdminAllowed = async (c) => config.AUTH_MODE === "open" || await isServerAdminUserId(c.get("userId"));
|
|
12296
12834
|
var isHttpUrl = (v) => /^https?:\/\/\S+$/i.test(v);
|
|
12297
12835
|
var serializeSite = (site) => ({
|
|
@@ -12303,7 +12841,7 @@ var serializeSite = (site) => ({
|
|
|
12303
12841
|
created_at: site.created_at,
|
|
12304
12842
|
updated_at: site.updated_at
|
|
12305
12843
|
});
|
|
12306
|
-
var communitySiteRoutes = new
|
|
12844
|
+
var communitySiteRoutes = new Hono24();
|
|
12307
12845
|
communitySiteRoutes.get("/", async (c) => {
|
|
12308
12846
|
if (!await serverAdminAllowed(c))
|
|
12309
12847
|
return c.json({ error: "Only the server admin can view this." }, 403);
|
|
@@ -12391,10 +12929,10 @@ communitySiteRoutes.delete("/:id", async (c) => {
|
|
|
12391
12929
|
});
|
|
12392
12930
|
|
|
12393
12931
|
// src/mcp/server-routes.ts
|
|
12394
|
-
import { Hono as
|
|
12932
|
+
import { Hono as Hono25 } from "hono";
|
|
12395
12933
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12396
12934
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
12397
|
-
import { z as
|
|
12935
|
+
import { z as z3 } from "zod";
|
|
12398
12936
|
var SERVER_WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
12399
12937
|
"nest_create",
|
|
12400
12938
|
"context_create",
|
|
@@ -12593,7 +13131,7 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
12593
13131
|
);
|
|
12594
13132
|
return `Created nest "${nest2.name}" (id: ${nest2.id}). Add documents with context_create, retrieve with the other tools.`;
|
|
12595
13133
|
} catch (err) {
|
|
12596
|
-
if (err instanceof ValidationError) return err.message;
|
|
13134
|
+
if (err instanceof ValidationError || err instanceof ForbiddenError) return err.message;
|
|
12597
13135
|
throw err;
|
|
12598
13136
|
}
|
|
12599
13137
|
}
|
|
@@ -12704,10 +13242,10 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
|
12704
13242
|
const shape = {};
|
|
12705
13243
|
for (const [key, def] of Object.entries(props)) {
|
|
12706
13244
|
let field2;
|
|
12707
|
-
if (def.type === "string") field2 =
|
|
12708
|
-
else if (def.type === "number") field2 =
|
|
12709
|
-
else if (def.type === "array") field2 =
|
|
12710
|
-
else field2 =
|
|
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();
|
|
12711
13249
|
if (!required.includes(key)) field2 = field2.optional();
|
|
12712
13250
|
shape[key] = field2;
|
|
12713
13251
|
}
|
|
@@ -12727,7 +13265,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl, c) {
|
|
|
12727
13265
|
}
|
|
12728
13266
|
return server;
|
|
12729
13267
|
}
|
|
12730
|
-
var serverMcpRoutes = new
|
|
13268
|
+
var serverMcpRoutes = new Hono25();
|
|
12731
13269
|
serverMcpRoutes.all("/", async (c) => {
|
|
12732
13270
|
const userId = c.get("userId");
|
|
12733
13271
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -12750,7 +13288,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
12750
13288
|
await server.server.close();
|
|
12751
13289
|
}
|
|
12752
13290
|
});
|
|
12753
|
-
var nestIndexRoutes = new
|
|
13291
|
+
var nestIndexRoutes = new Hono25();
|
|
12754
13292
|
nestIndexRoutes.get("/", async (c) => {
|
|
12755
13293
|
const userId = c.get("userId");
|
|
12756
13294
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -12926,14 +13464,14 @@ is \`ctx push --server ${B} --nest <nest-id>\` with the same env var set.
|
|
|
12926
13464
|
through steward review before they become AI-visible.
|
|
12927
13465
|
`;
|
|
12928
13466
|
}
|
|
12929
|
-
var llmsTxtRoutes = new
|
|
13467
|
+
var llmsTxtRoutes = new Hono25();
|
|
12930
13468
|
llmsTxtRoutes.get("/", (c) => {
|
|
12931
13469
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
12932
13470
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
12933
13471
|
});
|
|
12934
13472
|
|
|
12935
13473
|
// src/governance/routes.ts
|
|
12936
|
-
import { Hono as
|
|
13474
|
+
import { Hono as Hono26 } from "hono";
|
|
12937
13475
|
|
|
12938
13476
|
// src/governance/feed-service.ts
|
|
12939
13477
|
var SNIPPET_CHARS = 200;
|
|
@@ -13064,7 +13602,7 @@ async function getNestFeed(params) {
|
|
|
13064
13602
|
}
|
|
13065
13603
|
|
|
13066
13604
|
// src/governance/comment-service.ts
|
|
13067
|
-
import { v4 as
|
|
13605
|
+
import { v4 as uuid17 } from "uuid";
|
|
13068
13606
|
async function createComment(params) {
|
|
13069
13607
|
const db = getDb();
|
|
13070
13608
|
const body = (params.body ?? "").trim();
|
|
@@ -13080,7 +13618,7 @@ async function createComment(params) {
|
|
|
13080
13618
|
throw new Error("Parent comment not found on this node");
|
|
13081
13619
|
}
|
|
13082
13620
|
}
|
|
13083
|
-
const id =
|
|
13621
|
+
const id = uuid17();
|
|
13084
13622
|
await db.run(
|
|
13085
13623
|
`INSERT INTO comments
|
|
13086
13624
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -13280,7 +13818,7 @@ function rowToComment(row) {
|
|
|
13280
13818
|
|
|
13281
13819
|
// src/governance/stewards-parser.ts
|
|
13282
13820
|
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
13283
|
-
import { join as
|
|
13821
|
+
import { join as join7 } from "path";
|
|
13284
13822
|
function parseStewardsYaml(content) {
|
|
13285
13823
|
const result = { version: 1 };
|
|
13286
13824
|
const lines = content.split("\n");
|
|
@@ -13346,9 +13884,9 @@ function parseEntry(str) {
|
|
|
13346
13884
|
function loadStewardsConfig(nestId) {
|
|
13347
13885
|
const nestPath = resolveNestPath(nestId);
|
|
13348
13886
|
const candidates = [
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13887
|
+
join7(nestPath, "stewards.yaml"),
|
|
13888
|
+
join7(nestPath, "stewards.yml"),
|
|
13889
|
+
join7(nestPath, ".context", "stewards.yaml")
|
|
13352
13890
|
];
|
|
13353
13891
|
for (const candidatePath of candidates) {
|
|
13354
13892
|
if (existsSync(candidatePath)) {
|
|
@@ -13365,7 +13903,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
13365
13903
|
if (Number.isNaN(n) || n < 1) return def;
|
|
13366
13904
|
return Math.min(n, max);
|
|
13367
13905
|
}
|
|
13368
|
-
var governanceRoutes = new
|
|
13906
|
+
var governanceRoutes = new Hono26();
|
|
13369
13907
|
governanceRoutes.get("/stewards", async (c) => {
|
|
13370
13908
|
const nestId = c.req.param("nestId");
|
|
13371
13909
|
const scope = c.req.query("scope");
|
|
@@ -13609,7 +14147,7 @@ governanceRoutes.post("/request-deletion", async (c) => {
|
|
|
13609
14147
|
if (body.targetType === "folder") {
|
|
13610
14148
|
const { storage } = await engineCache.get(nestId);
|
|
13611
14149
|
const docs = await storage.discoverDocuments();
|
|
13612
|
-
if (!docs.some((d) => isIdUnderFolder(d.id, target))) {
|
|
14150
|
+
if (!docs.some((d) => isIdUnderFolder(d.id, target)) && !await vaultFolderExists(nestId, target)) {
|
|
13613
14151
|
return c.json({ error: `Folder not found: ${target}` }, 404);
|
|
13614
14152
|
}
|
|
13615
14153
|
}
|
|
@@ -13735,7 +14273,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
13735
14273
|
const activity = await getActivity({ nestId, limit });
|
|
13736
14274
|
return c.json({ activity });
|
|
13737
14275
|
});
|
|
13738
|
-
var governanceNodeRoutes = new
|
|
14276
|
+
var governanceNodeRoutes = new Hono26();
|
|
13739
14277
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
13740
14278
|
const nestId = c.req.param("nestId");
|
|
13741
14279
|
const nodeId = c.req.param("nodeId");
|
|
@@ -14230,23 +14768,29 @@ function publicBaseUrlError(value) {
|
|
|
14230
14768
|
// src/app.ts
|
|
14231
14769
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
14232
14770
|
import { fileURLToPath } from "url";
|
|
14233
|
-
import { dirname as dirname2, join as
|
|
14771
|
+
import { dirname as dirname2, join as join8, relative as relative2 } from "path";
|
|
14234
14772
|
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
14235
14773
|
var HERE = dirname2(fileURLToPath(import.meta.url));
|
|
14236
14774
|
var SERVICE_VERSION = (() => {
|
|
14237
14775
|
try {
|
|
14238
|
-
return JSON.parse(readFileSync4(
|
|
14776
|
+
return JSON.parse(readFileSync4(join8(HERE, "..", "package.json"), "utf8")).version;
|
|
14239
14777
|
} catch {
|
|
14240
14778
|
return "unknown";
|
|
14241
14779
|
}
|
|
14242
14780
|
})();
|
|
14243
14781
|
var UI_DIR_CANDIDATES = [
|
|
14244
|
-
|
|
14245
|
-
|
|
14782
|
+
join8(HERE, "web3"),
|
|
14783
|
+
join8(process.cwd(), "dist", "web3")
|
|
14246
14784
|
];
|
|
14247
14785
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
14248
14786
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
14249
|
-
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) => {
|
|
14250
14794
|
const anonId = await ensureAnonymousUser();
|
|
14251
14795
|
c.set("userId", anonId);
|
|
14252
14796
|
c.set("nestScope", null);
|
|
@@ -14258,7 +14802,7 @@ function isPublicReadEligiblePath(method, path) {
|
|
|
14258
14802
|
if (/^\/nests\/[^/]+\/stewards/.test(path)) return false;
|
|
14259
14803
|
return !/\/(collaborators|visibility|settings|mcp)/.test(path);
|
|
14260
14804
|
}
|
|
14261
|
-
var flexAuthMiddleware =
|
|
14805
|
+
var flexAuthMiddleware = createMiddleware3(async (c, next) => {
|
|
14262
14806
|
const hasBearer = c.req.header("Authorization")?.startsWith("Bearer cnst_");
|
|
14263
14807
|
const hasCookie = !!c.req.header("Cookie")?.includes("cnst_session=");
|
|
14264
14808
|
if (hasBearer || hasCookie) {
|
|
@@ -14284,7 +14828,7 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
14284
14828
|
function nestPathParts(path) {
|
|
14285
14829
|
return path.replace(/^\/nests\//, "").split("/").filter(Boolean);
|
|
14286
14830
|
}
|
|
14287
|
-
var flexAuthWithPromptowlTicket =
|
|
14831
|
+
var flexAuthWithPromptowlTicket = createMiddleware3(
|
|
14288
14832
|
async (c, next) => {
|
|
14289
14833
|
const header = c.req.header("Authorization") || "";
|
|
14290
14834
|
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
|
|
@@ -14361,7 +14905,7 @@ async function canApproveInNest(nestId, email) {
|
|
|
14361
14905
|
return ok;
|
|
14362
14906
|
}
|
|
14363
14907
|
function createApp() {
|
|
14364
|
-
const app = new
|
|
14908
|
+
const app = new Hono27({ router: new LinearRouter() });
|
|
14365
14909
|
const corsOrigins = config.CORS_ORIGINS;
|
|
14366
14910
|
app.use(
|
|
14367
14911
|
"*",
|
|
@@ -14411,6 +14955,7 @@ function createApp() {
|
|
|
14411
14955
|
client: c.get("clientMetadata")
|
|
14412
14956
|
});
|
|
14413
14957
|
});
|
|
14958
|
+
app.use("*", nestScopedKeyGuard);
|
|
14414
14959
|
app.get("/health", (c) => {
|
|
14415
14960
|
const oidcReady = isOidcReady();
|
|
14416
14961
|
return c.json({
|
|
@@ -14425,6 +14970,9 @@ function createApp() {
|
|
|
14425
14970
|
// Presence only: the publish dialog hides the email/invite gates when
|
|
14426
14971
|
// this server cannot send mail, because their only way in is a mailed link.
|
|
14427
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,
|
|
14428
14976
|
// Product analytics for the SPA. The PostHog project key is a public
|
|
14429
14977
|
// client key, but it only ships while usage telemetry is on — one switch
|
|
14430
14978
|
// covers everything this server sends outward.
|
|
@@ -14580,6 +15128,7 @@ function createApp() {
|
|
|
14580
15128
|
oidc_allowed_domains: config.OIDC_ALLOWED_DOMAINS.join(", "),
|
|
14581
15129
|
oidc_auto_provision: config.OIDC_AUTO_PROVISION,
|
|
14582
15130
|
oidc_department_tagging: config.OIDC_DEPARTMENT_TAGGING,
|
|
15131
|
+
nest_creation: config.NEST_CREATION,
|
|
14583
15132
|
people_suggest_all_users: config.PEOPLE_SUGGEST_ALL_USERS,
|
|
14584
15133
|
shared_deployment: config.SHARED_DEPLOYMENT,
|
|
14585
15134
|
// Agent SSO / token-exchange. MCP_SIGNING_SECRET is WRITE-ONLY — only its
|
|
@@ -14601,7 +15150,12 @@ function createApp() {
|
|
|
14601
15150
|
anthropic_api_key_masked: process.env.ANTHROPIC_API_KEY ? `\u2022\u2022\u2022\u2022${process.env.ANTHROPIC_API_KEY.slice(-4)}` : null,
|
|
14602
15151
|
slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
|
|
14603
15152
|
msteams_webhook_url: config.MSTEAMS_WEBHOOK_URL ?? "",
|
|
15153
|
+
email_provider: config.EMAIL_PROVIDER,
|
|
14604
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,
|
|
14605
15159
|
notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
|
|
14606
15160
|
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
14607
15161
|
});
|
|
@@ -14806,6 +15360,12 @@ function createApp() {
|
|
|
14806
15360
|
value: body.oidc_department_tagging ? "true" : null
|
|
14807
15361
|
});
|
|
14808
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
|
+
}
|
|
14809
15369
|
if ("people_suggest_all_users" in body) {
|
|
14810
15370
|
pending.push({
|
|
14811
15371
|
name: "PEOPLE_SUGGEST_ALL_USERS",
|
|
@@ -14915,6 +15475,50 @@ function createApp() {
|
|
|
14915
15475
|
addError("smtp_url", "SMTP connection URL must start with smtp:// or smtps:// (or leave it empty to turn email notifications off).");
|
|
14916
15476
|
else pending.push({ name: "SMTP_URL", value: v || null });
|
|
14917
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
|
+
}
|
|
14918
15522
|
if ("notify_email_from" in body) {
|
|
14919
15523
|
const v = String(body.notify_email_from ?? "").trim();
|
|
14920
15524
|
if (v && v.includes(","))
|
|
@@ -14952,15 +15556,18 @@ function createApp() {
|
|
|
14952
15556
|
{ error: errorMessages.join("; "), fieldErrors, settings: currentServerSettings() },
|
|
14953
15557
|
400
|
|
14954
15558
|
);
|
|
14955
|
-
const
|
|
14956
|
-
|
|
14957
|
-
|
|
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);
|
|
14958
15564
|
if (!result.ok) {
|
|
14959
|
-
const
|
|
15565
|
+
const ses = nextEmail.provider === "ses";
|
|
15566
|
+
const message = `${ses ? "AWS SES" : "SMTP"} connection failed: ${result.error}`;
|
|
14960
15567
|
return c.json(
|
|
14961
15568
|
{
|
|
14962
15569
|
error: message,
|
|
14963
|
-
fieldErrors: { smtp_url: message },
|
|
15570
|
+
fieldErrors: { [ses ? "ses_region" : "smtp_url"]: message },
|
|
14964
15571
|
settings: currentServerSettings()
|
|
14965
15572
|
},
|
|
14966
15573
|
400
|
|
@@ -15119,6 +15726,10 @@ function createApp() {
|
|
|
15119
15726
|
app.use("/admin/community-sites", flexAuthMiddleware);
|
|
15120
15727
|
app.use("/admin/community-sites/*", flexAuthMiddleware);
|
|
15121
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
|
+
});
|
|
15122
15733
|
app.use("/teams/*", flexAuthMiddleware);
|
|
15123
15734
|
app.use("/teams", flexAuthMiddleware);
|
|
15124
15735
|
app.route("/teams", teamRoutes);
|
|
@@ -15172,7 +15783,7 @@ function createApp() {
|
|
|
15172
15783
|
const others = requests.filter((r) => r.requestedBy !== me);
|
|
15173
15784
|
const nestApprove = new Map(
|
|
15174
15785
|
await Promise.all(
|
|
15175
|
-
[...
|
|
15786
|
+
[...nestName2.keys()].map(
|
|
15176
15787
|
async (nestId) => [
|
|
15177
15788
|
nestId,
|
|
15178
15789
|
await canApproveInNest(nestId, me)
|
|
@@ -15202,13 +15813,33 @@ function createApp() {
|
|
|
15202
15813
|
);
|
|
15203
15814
|
waiting.sort(byNewest);
|
|
15204
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;
|
|
15205
15833
|
return c.json({
|
|
15206
15834
|
items,
|
|
15207
15835
|
waiting,
|
|
15208
15836
|
notifications,
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15837
|
+
pagination: {
|
|
15838
|
+
total: reviewTotal + deletionsShown,
|
|
15839
|
+
limit,
|
|
15840
|
+
offset,
|
|
15841
|
+
has_more: offset + requests.length < total
|
|
15842
|
+
}
|
|
15212
15843
|
});
|
|
15213
15844
|
});
|
|
15214
15845
|
app.get("/me/drafts", async (c) => {
|
|
@@ -15257,12 +15888,29 @@ function createApp() {
|
|
|
15257
15888
|
await markNotificationsRead(me, body.ids === "all" ? "all" : body.ids ?? []);
|
|
15258
15889
|
return c.json({ ok: true });
|
|
15259
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
|
+
});
|
|
15260
15905
|
app.use("/stats", flexAuthMiddleware);
|
|
15261
15906
|
app.get("/stats", async (c) => {
|
|
15262
15907
|
const db = getDb();
|
|
15263
15908
|
const userId = c.get("userId");
|
|
15264
15909
|
const userEmail = await resolveCallerEmail(userId);
|
|
15265
|
-
const
|
|
15910
|
+
const nestScope = c.get("nestScope");
|
|
15911
|
+
const visibleNests = (await listVisibleNests(userId)).filter(
|
|
15912
|
+
(n) => !nestScope || n.id === nestScope
|
|
15913
|
+
);
|
|
15266
15914
|
let documents = 0;
|
|
15267
15915
|
for (const nest of visibleNests) {
|
|
15268
15916
|
try {
|
|
@@ -15290,8 +15938,12 @@ function createApp() {
|
|
|
15290
15938
|
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 20, 1), 50);
|
|
15291
15939
|
const userId = c.get("userId");
|
|
15292
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
|
+
);
|
|
15293
15945
|
const perNest = await Promise.all(
|
|
15294
|
-
|
|
15946
|
+
searchable.map(async (nest) => {
|
|
15295
15947
|
try {
|
|
15296
15948
|
const { storage } = await engineCache.get(nest.id);
|
|
15297
15949
|
const matches = matchDocuments(await discoverForSearch(storage), q);
|
|
@@ -15324,7 +15976,7 @@ function createApp() {
|
|
|
15324
15976
|
app.route("/index", nestIndexRoutes);
|
|
15325
15977
|
app.use("/mcp", flexAuthWithPromptowlTicket);
|
|
15326
15978
|
app.route("/mcp", serverMcpRoutes);
|
|
15327
|
-
const nestsApp = new
|
|
15979
|
+
const nestsApp = new Hono27();
|
|
15328
15980
|
nestsApp.use("*", async (c, next) => {
|
|
15329
15981
|
const parts = nestPathParts(c.req.path);
|
|
15330
15982
|
const ticketEligible = parts.length === 2 && (c.req.method === "POST" && parts[1] === "context" || c.req.method === "GET" && parts[1] === "nodes" || parts[1] === "mcp");
|
|
@@ -15454,7 +16106,7 @@ function createApp() {
|
|
|
15454
16106
|
);
|
|
15455
16107
|
}
|
|
15456
16108
|
const resource = parts[1];
|
|
15457
|
-
if (resource === "visibility" || resource === "reader-mode") {
|
|
16109
|
+
if (resource === "visibility" || resource === "reader-mode" || resource === "branding") {
|
|
15458
16110
|
required = "admin";
|
|
15459
16111
|
} else if (resource === "collaborators") {
|
|
15460
16112
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
@@ -15551,6 +16203,7 @@ function createApp() {
|
|
|
15551
16203
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|
|
15552
16204
|
nestsApp.route("/:nestId", queryRoutes);
|
|
15553
16205
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
16206
|
+
nestsApp.route("/:nestId", brandingRoutes);
|
|
15554
16207
|
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
15555
16208
|
nestsApp.route("/:nestId/publications", publishRoutes);
|
|
15556
16209
|
nestsApp.route("/:nestId/teams", nestTeamRoutes);
|
|
@@ -15574,18 +16227,13 @@ function createApp() {
|
|
|
15574
16227
|
onFound: (_p, c) => c.header("Cache-Control", "public, max-age=31536000, immutable")
|
|
15575
16228
|
})
|
|
15576
16229
|
);
|
|
15577
|
-
const isDocumentNavigation = (c) => c.req.header("Sec-Fetch-Mode") === "navigate" || (c.req.header("Accept") || "").includes("text/html");
|
|
15578
16230
|
app.get(
|
|
15579
16231
|
"*",
|
|
15580
16232
|
async (c, next) => {
|
|
15581
16233
|
if (isDocumentNavigation(c)) return next();
|
|
15582
16234
|
return c.json({ error: "Not found" }, 404);
|
|
15583
16235
|
},
|
|
15584
|
-
|
|
15585
|
-
root: UI_DIR_REL,
|
|
15586
|
-
path: "index.html",
|
|
15587
|
-
onFound: (_p, c) => c.header("Cache-Control", "no-cache")
|
|
15588
|
-
})
|
|
16236
|
+
serveIndexShell
|
|
15589
16237
|
);
|
|
15590
16238
|
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
15591
16239
|
app.onError((err, c) => {
|
|
@@ -15873,7 +16521,7 @@ async function repairGraftedVersionChains(db) {
|
|
|
15873
16521
|
}
|
|
15874
16522
|
|
|
15875
16523
|
// src/workflow/scheduler.ts
|
|
15876
|
-
import { v4 as
|
|
16524
|
+
import { v4 as uuid18 } from "uuid";
|
|
15877
16525
|
function isDue(s, now) {
|
|
15878
16526
|
if (!s.last_run_at) return true;
|
|
15879
16527
|
const last = Date.parse(s.last_run_at);
|
|
@@ -15901,7 +16549,7 @@ async function schedulerTick(now = Date.now()) {
|
|
|
15901
16549
|
);
|
|
15902
16550
|
continue;
|
|
15903
16551
|
}
|
|
15904
|
-
const id = `run_${
|
|
16552
|
+
const id = `run_${uuid18()}`;
|
|
15905
16553
|
const nowIso2 = new Date(now).toISOString();
|
|
15906
16554
|
await db.run(
|
|
15907
16555
|
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|