@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.
Files changed (120) hide show
  1. package/API.md +68 -8
  2. package/CONFIGURATION.md +572 -116
  3. package/README.md +3 -2
  4. package/dist/{chunk-32SZCH36.js → chunk-2QOJE7A5.js} +4 -4
  5. package/dist/{chunk-2KHM5C7V.js → chunk-DGPTJXIV.js} +13 -15
  6. package/dist/chunk-I5EDEZBQ.js +523 -0
  7. package/dist/{chunk-TI7HMP64.js → chunk-IEAWRHMV.js} +2 -2
  8. package/dist/{chunk-DTLMBF4W.js → chunk-M6PGYMBD.js} +15 -8
  9. package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
  10. package/dist/{chunk-KPGNZLZL.js → chunk-RPGFHESR.js} +1 -1
  11. package/dist/{chunk-7FROMOZM.js → chunk-SWA5M5XX.js} +68 -544
  12. package/dist/{chunk-JDAOA5KP.js → chunk-TRSGPS27.js} +39 -14
  13. package/dist/{engine-TER6FDBP.js → engine-QNMFDIBV.js} +3 -2
  14. package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-4MEGPT4D.js} +4 -3
  15. package/dist/{grants-service-2OTFS7EH.js → grants-service-24GLUBY4.js} +2 -2
  16. package/dist/index.js +911 -263
  17. package/dist/invited-user-AM6A3WGO.js +12 -0
  18. package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
  19. package/dist/{review-service-4G2XUFS6.js → review-service-2EWN7WWO.js} +7 -6
  20. package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-TDZUC2Q6.js} +4 -3
  21. package/dist/{version-service-X4FAG2OZ.js → version-service-C3JTZLJH.js} +4 -3
  22. package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
  23. package/dist/web3/assets/{AgentDocsPage-Dkhgb1hk.js → AgentDocsPage-ixP2UdAE.js} +1 -1
  24. package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +1 -0
  25. package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +1 -0
  26. package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +36 -0
  27. package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +6 -0
  28. package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +1 -0
  29. package/dist/web3/assets/{MarkdownEditor-Dfk5uIE5.js → MarkdownEditor-CKh4WSn-.js} +136 -131
  30. package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
  31. package/dist/web3/assets/NestPageHeader-Cizo8nUi.js +1 -0
  32. package/dist/web3/assets/NestView-EW-sraEb.js +73 -0
  33. package/dist/web3/assets/OverviewTab-CjidsMX_.js +6 -0
  34. package/dist/web3/assets/PersonCombobox-BKI7dff4.js +1 -0
  35. package/dist/web3/assets/{ReviewActions-BXDTDPp1.js → ReviewActions-CS8JXgXF.js} +2 -2
  36. package/dist/web3/assets/ReviewTab-CwVrXzw5.js +1 -0
  37. package/dist/web3/assets/StewardsTab-C-iP6gnh.js +1 -0
  38. package/dist/web3/assets/SubmitForReviewModal-Biq3m79i.js +1 -0
  39. package/dist/web3/assets/{alert-dialog-4SiLKlz8.js → alert-dialog-BvF3NFr7.js} +2 -2
  40. package/dist/web3/assets/{arrow-left-BAgIocLn.js → arrow-left-CWIsv-sG.js} +1 -1
  41. package/dist/web3/assets/backlinks-Cqcn7n1J.js +19 -0
  42. package/dist/web3/assets/boxes-ClbgQqG2.js +6 -0
  43. package/dist/web3/assets/{chevron-left-XR4ReJ9Z.js → chevron-left-CK_ZPFm9.js} +1 -1
  44. package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-Bw_aTXiH.js} +1 -1
  45. package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-DkIG68H7.js} +1 -1
  46. package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BlR7EN6l.js} +1 -1
  47. package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DSVxK3tf.js} +1 -1
  48. package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
  49. package/dist/web3/assets/{earth-CXOcThqn.js → earth-BXhVPvCR.js} +1 -1
  50. package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-BevGZgtj.js} +1 -1
  51. package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-m8UBV4fA.js} +1 -1
  52. package/dist/web3/assets/{index-A0_ymcqL.js → index-BOVYgMvj.js} +1 -1
  53. package/dist/web3/assets/index-DVnaYqj9.css +1 -0
  54. package/dist/web3/assets/index-WYDmEOfO.js +404 -0
  55. package/dist/web3/assets/page-B6pkc9cd.js +2 -0
  56. package/dist/web3/assets/page-BJrFCKXB.js +1 -0
  57. package/dist/web3/assets/page-Bgy9Ng9p.js +1 -0
  58. package/dist/web3/assets/page-C2zoxumw.js +1 -0
  59. package/dist/web3/assets/page-CJWO06dN.js +1 -0
  60. package/dist/web3/assets/page-CptuAPJc.js +6 -0
  61. package/dist/web3/assets/page-DPY01SOj.js +1 -0
  62. package/dist/web3/assets/page-Dea2SblF.js +9 -0
  63. package/dist/web3/assets/page-Deq6IKiN.js +6 -0
  64. package/dist/web3/assets/page-Dfjg8p7Z.js +1 -0
  65. package/dist/web3/assets/{page-xzYqkBUP.js → page-Dh57L0RG.js} +5 -5
  66. package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
  67. package/dist/web3/assets/{page-CUjuSs5Q.js → page-DzKwP5Ds.js} +3 -3
  68. package/dist/web3/assets/page-YxjrE5b5.js +45 -0
  69. package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-DVq4qvMR.js} +1 -1
  70. package/dist/web3/assets/{play-B8xE9j5f.js → play-CXhS6Pgq.js} +1 -1
  71. package/dist/web3/assets/{send-CvAgOUPG.js → send-DdfwnIrj.js} +1 -1
  72. package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DKX0Q4gO.js} +1 -1
  73. package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-CxmXRt58.js} +1 -1
  74. package/dist/web3/assets/{tag-Br_y1f00.js → tag-DeYC_mRv.js} +1 -1
  75. package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-B0HyPTnI.js} +1 -1
  76. package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-Ckm_Isyc.js} +1 -1
  77. package/dist/web3/assets/{x-qxt1WrUi.js → x-mNtADYHc.js} +1 -1
  78. package/dist/web3/assets/{zap-BcsKR0ef.js → zap-D2ifcnYb.js} +1 -1
  79. package/dist/web3/index.html +2 -2
  80. package/package.json +3 -2
  81. package/dist/client-D44ZHDTJ.js +0 -11
  82. package/dist/keys-CTOGAG3W.js +0 -20
  83. package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
  84. package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
  85. package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
  86. package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
  87. package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
  88. package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
  89. package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
  90. package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
  91. package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
  92. package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
  93. package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
  94. package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
  95. package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
  96. package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
  97. package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
  98. package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
  99. package/dist/web3/assets/card-UDyMRcps.js +0 -1
  100. package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
  101. package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
  102. package/dist/web3/assets/index-B5hLclEq.js +0 -389
  103. package/dist/web3/assets/index-DrUAtoQM.css +0 -1
  104. package/dist/web3/assets/page-BHD4beun.js +0 -1
  105. package/dist/web3/assets/page-BSYUXYmO.js +0 -11
  106. package/dist/web3/assets/page-BYc2DIux.js +0 -45
  107. package/dist/web3/assets/page-BfibLg8e.js +0 -1
  108. package/dist/web3/assets/page-CGe8MtUu.js +0 -9
  109. package/dist/web3/assets/page-CIgPJmG6.js +0 -2
  110. package/dist/web3/assets/page-Ck31nx9b.js +0 -1
  111. package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
  112. package/dist/web3/assets/page-D0sN3GO6.js +0 -1
  113. package/dist/web3/assets/page-DEpH91X2.js +0 -1
  114. package/dist/web3/assets/page-l8-B7CGt.js +0 -1
  115. package/dist/web3/assets/page-mnSHmk6A.js +0 -6
  116. package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
  117. package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
  118. package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
  119. package/dist/web3/assets/select-DrL0mpRN.js +0 -6
  120. 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-DTLMBF4W.js";
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-2KHM5C7V.js";
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-KPGNZLZL.js";
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-TI7HMP64.js";
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-32SZCH36.js";
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
- verifySmtp
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-JDAOA5KP.js";
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 Hono26 } from "hono";
262
+ import { Hono as Hono27 } from "hono";
258
263
  import { LinearRouter } from "hono/router/linear-router";
259
- import { createMiddleware as createMiddleware2 } from "hono/factory";
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, use \u201CSet your password\u201D to activate your account. Otherwise ask your admin for an invite."
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. Ask your admin for your temporary password, then sign in and change it \u2014 you can't set it here."
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
- { id: keyId, api_key: apiKey, key_prefix: getKeyPrefix(apiKey), label: check.label },
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 && existing.is_invited === 0) {
2037
+ if (existing) {
1908
2038
  return c.json(
1909
2039
  {
1910
- error: "A user with this email already exists. Use Reset password to issue them a new password."
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
- void sendEmailToRecipient(email, {
1936
- status: "invited",
1937
- docTitle: "ContextNest",
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
- (SELECT MAX(last_used_at) FROM api_keys WHERE user_id = u.id) as last_active
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(Boolean);
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 stat2;
4188
+ let stat3;
4070
4189
  try {
4071
- stat2 = statSync(src);
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 (!stat2.isDirectory()) {
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 stat2;
4244
+ let stat3;
4126
4245
  try {
4127
- stat2 = statSync(src);
4246
+ stat3 = statSync(src);
4128
4247
  } catch {
4129
4248
  throw new NotFoundError(`Folder not found: ${folderName}`);
4130
4249
  }
4131
- if (!stat2.isDirectory()) {
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
- const email = normalizeEmail(params.email);
4598
- const existing = await db.get(
4599
- "SELECT id FROM users WHERE LOWER(email) = ?",
4600
- [email]
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 inviteeEmail = params.email || (await db.get("SELECT email FROM users WHERE id = ?", [userId]))?.email;
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: docLink(nestId, void 0, params.baseUrl),
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/grant-routes.ts
5197
+ // src/nests/branding-routes.ts
4871
5198
  import { Hono as Hono5 } from "hono";
4872
- import { v4 as uuid7 } from "uuid";
4873
- var grantRoutes = new Hono5();
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 db = getDb();
4890
- const e = normalizeEmail(email);
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 Hono6 } from "hono";
5382
+ import { Hono as Hono7 } from "hono";
4997
5383
 
4998
5384
  // src/nests/publish-service.ts
4999
- import { v4 as uuid8 } from "uuid";
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 = uuid8();
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
- [uuid8(), id, email, created]
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
- [uuid8(), publishId, e, sha256Hex(token), expires, nowIso()]
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
- [uuid8(), publishId, email, sha256Hex(token), expires, nowIso()]
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
- [uuid8(), publishId, email, nowIso()]
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 Hono6();
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 (SMTP_URL and NOTIFY_EMAIL_FROM). Use a public or passcode link instead."
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 Hono7 } from "hono";
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 uuid9 } from "uuid";
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 = uuid9();
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
- [uuid9(), id, authorEmail, body]
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
- [uuid9(), threadId, authorEmail, trimmed]
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
- var publishRenderRoutes = new Hono7();
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 shell = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${PROSE_CSS}</style></head><body><main>${heading}${markdown.render(body)}</main>${overlays}</body></html>`;
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
- function gatePage(row, message) {
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></head><body><form class="card" method="POST" action="/p/${esc(row.slug)}/gate">
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
- <p class="brand">Secured by PromptOwl \xB7 ContextNest</p>
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></head><body>
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 Hono8 } from "hono";
5983
- var nestTeamRoutes = new Hono8();
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 Hono9 } from "hono";
6023
- var peopleRoutes = new Hono9();
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 Hono10 } from "hono";
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 Hono10();
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 Hono11 } from "hono";
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 Hono11();
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-S22V2JKG.js");
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-4G2XUFS6.js");
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 Hono12 } from "hono";
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 Hono12();
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 Hono13 } from "hono";
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 join3, sep } from "path";
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(join3(root, rel)).isFile()) out.push(rel.split(sep).join("/"));
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(join3(nestPath, rel)));
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 Hono13();
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
- if (titleMatches.length > 0 && hops > 0) {
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(titleMatches.map((d) => d.id));
7843
- let frontier = titleMatches.map((d) => d.id);
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 Hono15 } from "hono";
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 uuid12 } from "uuid";
8848
+ import { v4 as uuid11 } from "uuid";
8301
8849
 
8302
8850
  // src/workflow/run-service.ts
8303
- import { v4 as uuid10 } from "uuid";
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_${uuid10()}`;
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 Hono14 } from "hono";
8620
- import { v4 as uuid11 } from "uuid";
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 Hono14();
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 = uuid11();
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 = uuid12();
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
- [uuid12(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
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-2OTFS7EH.js");
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
- let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
10931
- if (!row) {
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 Hono15();
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 = z.string();
11012
- else if (def.type === "number") field2 = z.number();
11013
- else if (def.type === "array") field2 = z.array(z.string());
11014
- else field2 = z.any();
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 Hono16 } from "hono";
11064
- import { v4 as uuid13 } from "uuid";
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 Hono16();
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_${uuid13()}`;
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 Hono16();
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 Hono16();
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 Hono17 } from "hono";
11474
- import { v4 as uuid14 } from "uuid";
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 Hono17();
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_${uuid14()}`;
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 Hono18 } from "hono";
11608
- import { v4 as uuid15 } from "uuid";
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 Hono18();
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_${uuid15()}`;
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 Hono19 } from "hono";
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 Hono19();
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 Hono19();
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 Hono20 } from "hono";
11917
- import { v4 as uuid16 } from "uuid";
11918
- var definitionRoutes = new Hono20();
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 = uuid16();
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 Hono21 } from "hono";
12027
- import { v4 as uuid17 } from "uuid";
12028
- import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
12029
- import { join as join4 } from "path";
12030
- var assetRoutes = new Hono21();
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 = `${uuid17()}.${ext === "jpeg" ? "jpg" : ext}`;
12077
- const dir = join4(resolveNestPath(nestId), "assets");
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(join4(dir, name), Buffer.from(await file.arrayBuffer()));
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 readFile(join4(resolveNestPath(nestId), "assets", name));
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 Hono22 } from "hono";
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 Hono22();
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 Hono23 } from "hono";
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 Hono23();
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 Hono24 } from "hono";
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 z2 } from "zod";
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 = z2.string();
12708
- else if (def.type === "number") field2 = z2.number();
12709
- else if (def.type === "array") field2 = z2.array(z2.string());
12710
- else field2 = z2.any();
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 Hono24();
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 Hono24();
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 Hono24();
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 Hono25 } from "hono";
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 uuid18 } from "uuid";
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 = uuid18();
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 join5 } from "path";
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
- join5(nestPath, "stewards.yaml"),
13350
- join5(nestPath, "stewards.yml"),
13351
- join5(nestPath, ".context", "stewards.yaml")
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 Hono25();
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 Hono25();
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 join6, relative as relative2 } from "path";
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(join6(HERE, "..", "package.json"), "utf8")).version;
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
- join6(HERE, "web3"),
14245
- join6(process.cwd(), "dist", "web3")
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 openModeMiddleware = createMiddleware2(async (c, next) => {
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 = createMiddleware2(async (c, next) => {
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 = createMiddleware2(
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 Hono26({ router: new LinearRouter() });
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 smtpChange = pending.find((p) => p.name === "SMTP_URL");
14956
- if (smtpChange && smtpChange.value) {
14957
- const result = await verifySmtp(smtpChange.value);
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 message = `SMTP connection failed: ${result.error}`;
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
- [...new Set(others.map((r) => r.nestId))].map(
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
- // total = all pending across visible nests (both buckets); has_more drives
15210
- // the "Load more" control on the client.
15211
- pagination: { total, limit, offset, has_more: offset + requests.length < total }
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 visibleNests = await listVisibleNests(userId);
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
- (await listVisibleNests(userId, { readable: true })).map(async (nest) => {
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 Hono26();
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
- serveStatic({
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 uuid19 } from "uuid";
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_${uuid19()}`;
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)