@promptowl/contextnest-community 1.19.0 → 1.21.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 (83) hide show
  1. package/CONFIGURATION.md +83 -1
  2. package/README.md +1 -1
  3. package/dist/{chunk-FBDEESNJ.js → chunk-4KJGQHWB.js} +113 -218
  4. package/dist/{chunk-ENDHMQPD.js → chunk-6JGQX4GA.js} +415 -64
  5. package/dist/{chunk-TNAZJYL5.js → chunk-6PJQSKKV.js} +12 -4
  6. package/dist/{chunk-BXUVIAMP.js → chunk-IGPJ74O4.js} +60 -12
  7. package/dist/{chunk-HYSTFMFG.js → chunk-LPPKPEYI.js} +131 -1
  8. package/dist/{chunk-3GMGLYTZ.js → chunk-N2DTJFCD.js} +2 -2
  9. package/dist/{chunk-GSYMJ3A4.js → chunk-XWGEXQU3.js} +6 -2
  10. package/dist/{client-SW5I6ZBK.js → client-BD6PAUCZ.js} +1 -1
  11. package/dist/{engine-V4DY4PYN.js → engine-IKZQ46P7.js} +2 -2
  12. package/dist/{external-edit-service-3NK5DGLA.js → external-edit-service-LTON57HO.js} +3 -3
  13. package/dist/{grants-service-CADI6LIB.js → grants-service-LSNZN6NG.js} +2 -2
  14. package/dist/index.js +2704 -1156
  15. package/dist/{migrations.postgres-5VI4RDTS.js → migrations.postgres-JA2YGLS3.js} +66 -1
  16. package/dist/{review-service-LK4M2C43.js → review-service-6JK64IRT.js} +8 -6
  17. package/dist/{stewardship-service-JAFSU5GN.js → stewardship-service-2YKGBP63.js} +3 -3
  18. package/dist/{version-service-G7SFFKFO.js → version-service-PJV74PVQ.js} +11 -3
  19. package/dist/web3/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  20. package/dist/web3/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  21. package/dist/web3/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  22. package/dist/web3/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  23. package/dist/web3/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  24. package/dist/web3/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  25. package/dist/web3/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  26. package/dist/web3/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  27. package/dist/web3/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  28. package/dist/web3/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  29. package/dist/web3/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  30. package/dist/web3/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  31. package/dist/web3/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  32. package/dist/web3/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  33. package/dist/web3/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  34. package/dist/web3/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  35. package/dist/web3/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  36. package/dist/web3/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  37. package/dist/web3/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  38. package/dist/web3/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  39. package/dist/web3/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  40. package/dist/web3/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  41. package/dist/web3/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  42. package/dist/web3/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  43. package/dist/web3/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  44. package/dist/web3/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  45. package/dist/web3/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  46. package/dist/web3/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  47. package/dist/web3/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  48. package/dist/web3/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  49. package/dist/web3/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  50. package/dist/web3/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  51. package/dist/web3/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  52. package/dist/web3/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  53. package/dist/web3/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  54. package/dist/web3/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  55. package/dist/web3/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  56. package/dist/web3/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  57. package/dist/web3/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  58. package/dist/web3/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  59. package/dist/web3/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  60. package/dist/web3/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  61. package/dist/web3/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  62. package/dist/web3/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  63. package/dist/web3/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  64. package/dist/web3/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  65. package/dist/web3/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  66. package/dist/web3/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  67. package/dist/web3/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  68. package/dist/web3/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  69. package/dist/web3/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  70. package/dist/web3/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  71. package/dist/web3/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  72. package/dist/web3/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  73. package/dist/web3/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  74. package/dist/web3/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  75. package/dist/web3/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  76. package/dist/web3/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  77. package/dist/web3/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  78. package/dist/web3/assets/index-T4pilFNL.css +1 -0
  79. package/dist/web3/assets/index-deyhV93k.js +1382 -0
  80. package/dist/web3/index.html +2 -2
  81. package/package.json +6 -1
  82. package/dist/web3/assets/index-C26nM5Kn.css +0 -1
  83. package/dist/web3/assets/index-C4oalc2K.js +0 -1065
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  engineCache,
3
3
  indexedNodeIds,
4
+ isNestArchived,
4
5
  markIndexStale,
5
6
  resolveNestPath,
6
7
  setSuggestionFlag
7
- } from "./chunk-ENDHMQPD.js";
8
+ } from "./chunk-6JGQX4GA.js";
8
9
  import {
9
10
  getDb
10
- } from "./chunk-HYSTFMFG.js";
11
+ } from "./chunk-LPPKPEYI.js";
11
12
 
12
13
  // src/governance/external-edit-service.ts
13
14
  import { readFile, readdir } from "fs/promises";
@@ -81,6 +82,10 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
81
82
  if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
82
83
  return null;
83
84
  }
85
+ const { hasUnsealedDraft } = await import("./version-service-PJV74PVQ.js");
86
+ if (await hasUnsealedDraft(nestId, documentId)) {
87
+ return null;
88
+ }
84
89
  const approved = await loadChainHead(storage, documentId);
85
90
  if (!approved) return null;
86
91
  const existing = await listSuggestions(storage, documentId);
@@ -110,6 +115,7 @@ async function refreshSuggestionFlag(nestId, storage, documentId) {
110
115
  }
111
116
  }
112
117
  async function scanNestForDrift(nestId, actor = "system:scanner") {
118
+ if (await isNestArchived(nestId)) return { scanned: 0, staged: 0 };
113
119
  const { storage, dropDiscoveryCache } = await engineCache.get(nestId);
114
120
  dropDiscoveryCache();
115
121
  const docs = await storage.discoverDocuments();
@@ -253,7 +259,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
253
259
  async function mirrorVersion(input) {
254
260
  const { storage } = await engineCache.get(input.nestId);
255
261
  const node = await storage.readDocument(input.documentId);
256
- const { upsertVersion, setApprovedVersion } = await import("./version-service-G7SFFKFO.js");
262
+ const { upsertVersion, setApprovedVersion } = await import("./version-service-PJV74PVQ.js");
257
263
  await upsertVersion({
258
264
  nestId: input.nestId,
259
265
  nodeId: input.documentId,
@@ -338,7 +344,9 @@ async function rejectExternalEdit(input) {
338
344
  var scannerTimer = null;
339
345
  async function scanAllNests() {
340
346
  const db = getDb();
341
- const rows = await db.all("SELECT id FROM nests");
347
+ const rows = await db.all(
348
+ "SELECT id FROM nests WHERE archived_at IS NULL"
349
+ );
342
350
  await Promise.all(
343
351
  rows.map(
344
352
  ({ id }) => scanNestForDrift(id).catch(
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-ENDHMQPD.js";
5
+ } from "./chunk-6JGQX4GA.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-HYSTFMFG.js";
8
+ } from "./chunk-LPPKPEYI.js";
9
9
 
10
10
  // src/governance/version-service.ts
11
11
  import { createHash } from "crypto";
@@ -136,6 +136,44 @@ async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
136
136
  );
137
137
  await db.run(sql, [nestId, nodeId, version, approvedBy]);
138
138
  }
139
+ async function getSealedVersion(nestId, nodeId) {
140
+ const approved = await getApprovedVersion(nestId, nodeId);
141
+ if (approved != null) return approved;
142
+ const db = getDb();
143
+ const row = await db.get(
144
+ `SELECT MAX(version) as v FROM node_versions
145
+ WHERE nest_id = ? AND node_id = ? AND status IN ('published', 'approved')`,
146
+ [nestId, nodeId]
147
+ );
148
+ return row?.v ?? null;
149
+ }
150
+ async function deleteSupersededDrafts(nestId, nodeId, belowVersion) {
151
+ const db = getDb();
152
+ await db.run(
153
+ `DELETE FROM node_versions
154
+ WHERE nest_id = ? AND node_id = ? AND status = 'draft' AND version < ?`,
155
+ [nestId, nodeId, belowVersion]
156
+ );
157
+ }
158
+ async function deleteUnsealedVersions(nestId, nodeId, sealedVersion) {
159
+ const db = getDb();
160
+ await db.run(
161
+ `DELETE FROM node_versions
162
+ WHERE nest_id = ? AND node_id = ? AND version > ?
163
+ AND status IN ('draft', 'rejected', 'pending_review')`,
164
+ [nestId, nodeId, sealedVersion]
165
+ );
166
+ }
167
+ async function hasUnsealedDraft(nestId, nodeId) {
168
+ const db = getDb();
169
+ const row = await db.get(
170
+ `SELECT status FROM node_versions
171
+ WHERE nest_id = ? AND node_id = ?
172
+ ORDER BY version DESC LIMIT 1`,
173
+ [nestId, nodeId]
174
+ );
175
+ return row?.status === "draft" || row?.status === "rejected" || row?.status === "pending_review";
176
+ }
139
177
  async function checkConflict(nestId, nodeId, baseVersion) {
140
178
  const db = getDb();
141
179
  const current = await db.get(
@@ -265,20 +303,16 @@ async function listMyDrafts(params) {
265
303
  WHERE r.nest_id = v.nest_id AND r.node_id = v.node_id
266
304
  AND r.status = 'pending')`;
267
305
  const args = [params.authorEmail.toLowerCase(), ...params.nestIds];
268
- const total = Number(
269
- (await db.get(
270
- `SELECT COUNT(*) as c FROM node_versions v ${where}`,
271
- args
272
- )).c
273
- );
274
306
  const limit = Math.min(Math.max(params.limit ?? 25, 1), 100);
275
307
  const offset = Math.max(params.offset ?? 0, 0);
308
+ const q = params.q?.trim().toLowerCase() || "";
309
+ const SCAN_CAP = 500;
276
310
  const rows = await db.all(
277
311
  `SELECT v.nest_id, v.node_id, v.status, v.version, v.created_at
278
312
  FROM node_versions v ${where}
279
313
  ORDER BY v.created_at DESC
280
- LIMIT ? OFFSET ?`,
281
- [...args, limit, offset]
314
+ LIMIT ?`,
315
+ [...args, SCAN_CAP]
282
316
  );
283
317
  const titlesByNest = new Map(
284
318
  await Promise.all(
@@ -290,7 +324,7 @@ async function listMyDrafts(params) {
290
324
  )
291
325
  )
292
326
  );
293
- const drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
327
+ let drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
294
328
  nestId: r.nest_id,
295
329
  nodeId: r.node_id,
296
330
  title: titlesByNest.get(r.nest_id).get(r.node_id) || r.node_id,
@@ -300,7 +334,17 @@ async function listMyDrafts(params) {
300
334
  version: Number(r.version),
301
335
  updatedAt: r.created_at
302
336
  }));
303
- return { drafts, total };
337
+ if (q) {
338
+ drafts = drafts.filter(
339
+ (d) => [d.title, params.nestNames?.get(d.nestId) ?? "", d.nodeId].some(
340
+ (s) => s.toLowerCase().includes(q)
341
+ )
342
+ );
343
+ }
344
+ if (params.sort === "title") {
345
+ drafts.sort((a, b) => a.title.localeCompare(b.title));
346
+ }
347
+ return { drafts: drafts.slice(offset, offset + limit), total: drafts.length };
304
348
  }
305
349
  function rowToVersion(row) {
306
350
  return {
@@ -327,6 +371,10 @@ export {
327
371
  getApprovedVersion,
328
372
  getApprovedVersions,
329
373
  setApprovedVersion,
374
+ getSealedVersion,
375
+ deleteSupersededDrafts,
376
+ deleteUnsealedVersions,
377
+ hasUnsealedDraft,
330
378
  checkConflict,
331
379
  getNodeTags,
332
380
  getDisplayStatus,
@@ -176,6 +176,58 @@ var config = {
176
176
  get PUBLIC_BASE_URL() {
177
177
  return (process.env.PUBLIC_BASE_URL || "").replace(/\/$/, "");
178
178
  },
179
+ /**
180
+ * Dedicated secret this deployment uses to SIGN the short-lived MCP tickets
181
+ * minted by the SSO token-exchange (POST /auth/token-exchange). Kept distinct
182
+ * from OFFICIAL_COMMUNITY_SSO_SECRET so a SELF-HOSTED server can issue its own
183
+ * MCP bearers without the official PromptOwl trust relationship. The MCP
184
+ * acceptor verifies a ticket signed with EITHER secret. Unset ⇒ token-exchange
185
+ * cannot mint (and the acceptor simply doesn't offer this key).
186
+ */
187
+ get MCP_SIGNING_SECRET() {
188
+ return process.env.MCP_SIGNING_SECRET || "";
189
+ },
190
+ /**
191
+ * Master switch for the SSO token-exchange endpoint. Default OFF — a
192
+ * deployment must deliberately enable it (and set MCP_SIGNING_SECRET + at
193
+ * least one provider) before /auth/token-exchange will mint anything.
194
+ */
195
+ get SSO_TOKEN_EXCHANGE_ENABLED() {
196
+ return process.env.SSO_TOKEN_EXCHANGE_ENABLED === "true";
197
+ },
198
+ get MS_TOKEN_EXCHANGE_ENABLED() {
199
+ return process.env.MS_TOKEN_EXCHANGE_ENABLED === "true";
200
+ },
201
+ get GOOGLE_TOKEN_EXCHANGE_ENABLED() {
202
+ return process.env.GOOGLE_TOKEN_EXCHANGE_ENABLED === "true";
203
+ },
204
+ /**
205
+ * Microsoft Entra: the app (client) id inbound tokens must be issued for
206
+ * (audience), and the tenant id used to build the expected issuer
207
+ * https://login.microsoftonline.com/<tenant>/v2.0.
208
+ */
209
+ get MS_CLIENT_ID() {
210
+ return (process.env.MS_CLIENT_ID || "").trim();
211
+ },
212
+ get MS_TENANT_ID() {
213
+ return (process.env.MS_TENANT_ID || "").trim();
214
+ },
215
+ /**
216
+ * Google: the OAuth client id inbound tokens must be issued for (audience).
217
+ * Issuer is fixed at https://accounts.google.com.
218
+ */
219
+ get GOOGLE_CLIENT_ID() {
220
+ return (process.env.GOOGLE_CLIENT_ID || "").trim();
221
+ },
222
+ /**
223
+ * Lifetime of a minted MCP ticket, in seconds. Clamped to [30, 3600]; short
224
+ * by design so a leaked exchange token expires quickly. Default 300.
225
+ */
226
+ get MCP_TOKEN_TTL_SECONDS() {
227
+ const raw = parseInt(process.env.MCP_TOKEN_TTL_SECONDS || "", 10);
228
+ if (!Number.isFinite(raw)) return 300;
229
+ return Math.max(30, Math.min(3600, raw));
230
+ },
179
231
  /**
180
232
  * Restrict "Sign in with PromptOwl":
181
233
  * "open" — anyone may sign in with PromptOwl (default)
@@ -744,6 +796,12 @@ function runMigrations(db) {
744
796
  if (!nestCols.includes("index_synced_at")) {
745
797
  db.exec("ALTER TABLE nests ADD COLUMN index_synced_at TEXT");
746
798
  }
799
+ if (!nestCols.includes("reader_mode")) {
800
+ db.exec("ALTER TABLE nests ADD COLUMN reader_mode INTEGER NOT NULL DEFAULT 0");
801
+ }
802
+ if (!nestCols.includes("reader_home_node")) {
803
+ db.exec("ALTER TABLE nests ADD COLUMN reader_home_node TEXT");
804
+ }
747
805
  const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
748
806
  if (!userCols.includes("is_admin")) {
749
807
  db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
@@ -1572,6 +1630,78 @@ function runMigrations(db) {
1572
1630
  })();
1573
1631
  recordMigration("036_node_index");
1574
1632
  }
1633
+ if (!hasMigration("037_publish")) {
1634
+ db.transaction(() => {
1635
+ db.exec(`
1636
+ CREATE TABLE IF NOT EXISTS publish (
1637
+ id TEXT PRIMARY KEY,
1638
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1639
+ node_id TEXT NOT NULL,
1640
+ slug TEXT NOT NULL,
1641
+ access_mode TEXT NOT NULL DEFAULT 'public'
1642
+ CHECK(access_mode IN ('public','email','code','invite')),
1643
+ code_hash TEXT,
1644
+ comments_enabled INTEGER NOT NULL DEFAULT 0,
1645
+ watermark INTEGER NOT NULL DEFAULT 0,
1646
+ no_download INTEGER NOT NULL DEFAULT 0,
1647
+ created_by TEXT NOT NULL,
1648
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1649
+ expires_at TEXT,
1650
+ revoked_at TEXT
1651
+ );
1652
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_publish_slug ON publish(slug);
1653
+ CREATE INDEX IF NOT EXISTS idx_publish_nest ON publish(nest_id, node_id);
1654
+
1655
+ CREATE TABLE IF NOT EXISTS access_grant (
1656
+ id TEXT PRIMARY KEY,
1657
+ publish_id TEXT NOT NULL REFERENCES publish(id) ON DELETE CASCADE,
1658
+ kind TEXT NOT NULL
1659
+ CHECK(kind IN ('invite','link','session','view')),
1660
+ email TEXT,
1661
+ token_hash TEXT,
1662
+ expires_at TEXT,
1663
+ used_at TEXT,
1664
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1665
+ );
1666
+ CREATE INDEX IF NOT EXISTS idx_access_grant_publish ON access_grant(publish_id, kind);
1667
+ CREATE INDEX IF NOT EXISTS idx_access_grant_token ON access_grant(token_hash);
1668
+ `);
1669
+ })();
1670
+ recordMigration("037_publish");
1671
+ }
1672
+ {
1673
+ const publishCols = db.prepare("PRAGMA table_info(publish)").all().map((c) => c.name);
1674
+ if (publishCols.length && !publishCols.includes("watermark")) {
1675
+ db.exec("ALTER TABLE publish ADD COLUMN watermark INTEGER NOT NULL DEFAULT 0");
1676
+ }
1677
+ if (publishCols.length && !publishCols.includes("no_download")) {
1678
+ db.exec("ALTER TABLE publish ADD COLUMN no_download INTEGER NOT NULL DEFAULT 0");
1679
+ }
1680
+ }
1681
+ if (!hasMigration("038_nest_archive")) {
1682
+ const cols = db.prepare("PRAGMA table_info(nests)").all().map((c) => c.name);
1683
+ if (!cols.includes("archived_at")) {
1684
+ db.exec("ALTER TABLE nests ADD COLUMN archived_at TEXT");
1685
+ }
1686
+ recordMigration("038_nest_archive");
1687
+ }
1688
+ if (!hasMigration("039_nest_pins")) {
1689
+ db.transaction(() => {
1690
+ db.exec(`
1691
+ CREATE TABLE IF NOT EXISTS nest_pins (
1692
+ -- BOTH sides cascade, as grants and nest_collaborators do. Without it
1693
+ -- on user_id, deleting an account with a single pin fails the whole
1694
+ -- transaction on a foreign-key violation (foreign_keys is ON).
1695
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
1696
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1697
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1698
+ PRIMARY KEY (user_id, nest_id)
1699
+ );
1700
+ CREATE INDEX IF NOT EXISTS idx_nest_pins_user ON nest_pins(user_id);
1701
+ `);
1702
+ })();
1703
+ recordMigration("039_nest_pins");
1704
+ }
1575
1705
  }
1576
1706
  function mergeCaseCollidingUsers(db) {
1577
1707
  const groups = db.prepare(
@@ -1789,7 +1919,7 @@ async function initDb() {
1789
1919
  if (config.DB_DRIVER === "postgres") {
1790
1920
  const { Pool } = await import("pg");
1791
1921
  const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
1792
- const { runPostgresMigrations } = await import("./migrations.postgres-5VI4RDTS.js");
1922
+ const { runPostgresMigrations } = await import("./migrations.postgres-JA2YGLS3.js");
1793
1923
  const pool = new Pool(buildPgConfig());
1794
1924
  adapter = new PostgresAdapter(pool);
1795
1925
  await runPostgresMigrations(adapter);
@@ -11,7 +11,7 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-ENDHMQPD.js";
14
+ } from "./chunk-6JGQX4GA.js";
15
15
  import {
16
16
  ConflictError,
17
17
  ValidationError
@@ -20,7 +20,7 @@ import {
20
20
  config,
21
21
  getDb,
22
22
  isEmailish
23
- } from "./chunk-HYSTFMFG.js";
23
+ } from "./chunk-LPPKPEYI.js";
24
24
 
25
25
  // src/governance/stewardship-service.ts
26
26
  import { v4 as uuid } from "uuid";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-HYSTFMFG.js";
6
+ } from "./chunk-LPPKPEYI.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -11,7 +11,10 @@ var rank = (r) => r === "write" ? 2 : 1;
11
11
  async function resolveNodeGrant(nestId, userId, nodeId) {
12
12
  if (!userId || !nodeId) return null;
13
13
  const rows = await getDb().all(
14
- "SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
14
+ `SELECT g.target, g.role, n.archived_at
15
+ FROM grants g
16
+ JOIN nests n ON n.id = g.nest_id
17
+ WHERE g.nest_id = ? AND g.user_id = ?`,
15
18
  [nestId, userId]
16
19
  );
17
20
  let best = null;
@@ -20,6 +23,7 @@ async function resolveNodeGrant(nestId, userId, nodeId) {
20
23
  if (!best || rank(r.role) > rank(best)) best = r.role;
21
24
  }
22
25
  }
26
+ if (best === "write" && rows[0]?.archived_at) return "read";
23
27
  return best;
24
28
  }
25
29
  async function listUserGrants(nestId, userId) {
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-HYSTFMFG.js";
5
+ } from "./chunk-LPPKPEYI.js";
6
6
  import "./chunk-YB3LKF7U.js";
7
7
  export {
8
8
  getDb,
@@ -2,9 +2,9 @@ import {
2
2
  engineApi,
3
3
  engineCache,
4
4
  opContext
5
- } from "./chunk-ENDHMQPD.js";
5
+ } from "./chunk-6JGQX4GA.js";
6
6
  import "./chunk-YVMSM7LS.js";
7
- import "./chunk-HYSTFMFG.js";
7
+ import "./chunk-LPPKPEYI.js";
8
8
  import "./chunk-YB3LKF7U.js";
9
9
  import "./chunk-FRQJWGN3.js";
10
10
  export {
@@ -11,10 +11,10 @@ import {
11
11
  scanNestForDrift,
12
12
  startDriftScanner,
13
13
  stopDriftScanner
14
- } from "./chunk-TNAZJYL5.js";
15
- import "./chunk-ENDHMQPD.js";
14
+ } from "./chunk-6PJQSKKV.js";
15
+ import "./chunk-6JGQX4GA.js";
16
16
  import "./chunk-YVMSM7LS.js";
17
- import "./chunk-HYSTFMFG.js";
17
+ import "./chunk-LPPKPEYI.js";
18
18
  import "./chunk-YB3LKF7U.js";
19
19
  import "./chunk-FRQJWGN3.js";
20
20
  export {
@@ -6,9 +6,9 @@ import {
6
6
  listGrants,
7
7
  listUserGrants,
8
8
  resolveNodeGrant
9
- } from "./chunk-GSYMJ3A4.js";
9
+ } from "./chunk-XWGEXQU3.js";
10
10
  import "./chunk-YVMSM7LS.js";
11
- import "./chunk-HYSTFMFG.js";
11
+ import "./chunk-LPPKPEYI.js";
12
12
  import "./chunk-YB3LKF7U.js";
13
13
  export {
14
14
  createGrant,