@promptowl/contextnest-community 1.25.0 → 1.27.0

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