@promptowl/contextnest-community 1.14.0 → 1.16.1

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.
@@ -1,10 +1,11 @@
1
1
  import {
2
+ buildTitleMap,
2
3
  insertOrReplace,
3
4
  nowExpr
4
- } from "./chunk-XQ46F76G.js";
5
+ } from "./chunk-L7UCMGWZ.js";
5
6
  import {
6
7
  getDb
7
- } from "./chunk-I5KYGMIT.js";
8
+ } from "./chunk-JMJLNEXE.js";
8
9
 
9
10
  // src/governance/version-service.ts
10
11
  import { createHash } from "crypto";
@@ -99,6 +100,15 @@ async function getCurrentVersion(nestId, nodeId) {
99
100
  );
100
101
  return row?.v || 0;
101
102
  }
103
+ async function getTrackedNodeIds(nestId) {
104
+ const db = getDb();
105
+ const rows = await db.all(
106
+ `SELECT DISTINCT node_id FROM node_versions
107
+ WHERE nest_id = ? AND author NOT LIKE ?`,
108
+ [nestId, `${SYSTEM_AUTHOR_PREFIX}%`]
109
+ );
110
+ return new Set(rows.map((r) => r.node_id));
111
+ }
102
112
  async function getApprovedVersion(nestId, nodeId) {
103
113
  const db = getDb();
104
114
  const row = await db.get(
@@ -107,6 +117,14 @@ async function getApprovedVersion(nestId, nodeId) {
107
117
  );
108
118
  return row?.approved_version ?? null;
109
119
  }
120
+ async function getApprovedVersions(nestId) {
121
+ const db = getDb();
122
+ const rows = await db.all(
123
+ "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
124
+ [nestId]
125
+ );
126
+ return new Map(rows.map((r) => [r.node_id, r.approved_version]));
127
+ }
110
128
  async function setApprovedVersion(nestId, nodeId, version, approvedBy) {
111
129
  const db = getDb();
112
130
  const sql = insertOrReplace(
@@ -187,6 +205,59 @@ async function getDisplayStatus(nestId, nodeId) {
187
205
  if (current.status === "rejected") return "rejected";
188
206
  return "draft";
189
207
  }
208
+ async function listMyDrafts(params) {
209
+ if (params.nestIds.length === 0) return { drafts: [], total: 0 };
210
+ const db = getDb();
211
+ const nestPlaceholders = params.nestIds.map(() => "?").join(",");
212
+ const where = `
213
+ WHERE v.author = ?
214
+ AND v.status IN ('draft', 'rejected')
215
+ AND v.nest_id IN (${nestPlaceholders})
216
+ AND v.version = (
217
+ SELECT MAX(v2.version) FROM node_versions v2
218
+ WHERE v2.nest_id = v.nest_id AND v2.node_id = v.node_id)
219
+ AND NOT EXISTS (
220
+ SELECT 1 FROM review_requests r
221
+ WHERE r.nest_id = v.nest_id AND r.node_id = v.node_id
222
+ AND r.status = 'pending')`;
223
+ const args = [params.authorEmail.toLowerCase(), ...params.nestIds];
224
+ const total = Number(
225
+ (await db.get(
226
+ `SELECT COUNT(*) as c FROM node_versions v ${where}`,
227
+ args
228
+ )).c
229
+ );
230
+ const limit = Math.min(Math.max(params.limit ?? 25, 1), 100);
231
+ const offset = Math.max(params.offset ?? 0, 0);
232
+ const rows = await db.all(
233
+ `SELECT v.nest_id, v.node_id, v.status, v.version, v.created_at
234
+ FROM node_versions v ${where}
235
+ ORDER BY v.created_at DESC
236
+ LIMIT ? OFFSET ?`,
237
+ [...args, limit, offset]
238
+ );
239
+ const titlesByNest = new Map(
240
+ await Promise.all(
241
+ [...new Set(rows.map((r) => r.nest_id))].map(
242
+ async (nestId) => [
243
+ nestId,
244
+ await buildTitleMap(nestId)
245
+ ]
246
+ )
247
+ )
248
+ );
249
+ const drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
250
+ nestId: r.nest_id,
251
+ nodeId: r.node_id,
252
+ title: titlesByNest.get(r.nest_id).get(r.node_id) || r.node_id,
253
+ status: r.status,
254
+ // Number(): Postgres returns INTEGER columns fine, but stay explicit —
255
+ // the modal renders this straight into copy.
256
+ version: Number(r.version),
257
+ updatedAt: r.created_at
258
+ }));
259
+ return { drafts, total };
260
+ }
190
261
  function rowToVersion(row) {
191
262
  return {
192
263
  version: row.version,
@@ -208,9 +279,12 @@ export {
208
279
  getVersions,
209
280
  getVersion,
210
281
  getCurrentVersion,
282
+ getTrackedNodeIds,
211
283
  getApprovedVersion,
284
+ getApprovedVersions,
212
285
  setApprovedVersion,
213
286
  checkConflict,
214
287
  getNodeTags,
215
- getDisplayStatus
288
+ getDisplayStatus,
289
+ listMyDrafts
216
290
  };
@@ -512,6 +512,30 @@ var config = {
512
512
  if ((process.env.AUTH_MODE || "key") === "open") return "*";
513
513
  return ["http://localhost:5173", "http://localhost:3838"];
514
514
  },
515
+ /**
516
+ * Who may frame this server, as a CSP `frame-ancestors` source list.
517
+ *
518
+ * Defaults to `'self'`: no other site can embed the UI, which is what stops
519
+ * a hostile page from framing it invisibly and stealing clicks from a
520
+ * signed-in operator. Deployments that are *meant* to be embedded name the
521
+ * embedding origin — e.g. the PromptOwl Data Room iframes this server, so
522
+ * that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`.
523
+ *
524
+ * Comma-separated; `'self'` is always included. `*` allows any site to frame
525
+ * this server and disables the protection.
526
+ *
527
+ * Note we deliberately do NOT also send `X-Frame-Options` — it only
528
+ * understands "same origin", so it would block a sibling subdomain that
529
+ * `frame-ancestors` is configured to allow. Every browser that matters
530
+ * honours `frame-ancestors`, and it wins where both are present.
531
+ */
532
+ get FRAME_ANCESTORS() {
533
+ const raw = process.env.FRAME_ANCESTORS?.trim();
534
+ if (!raw) return "'self'";
535
+ if (raw === "*") return "*";
536
+ const sources = raw.split(",").map((s) => s.trim()).filter(Boolean);
537
+ return [.../* @__PURE__ */ new Set(["'self'", ...sources])].join(" ");
538
+ },
515
539
  /**
516
540
  * Max JSON body size in bytes. Prevents DoS via giant payloads.
517
541
  * Default: 10 MB. Override via MAX_BODY_BYTES env.
@@ -711,6 +735,12 @@ function runMigrations(db) {
711
735
  if (!nestCols.includes("allow_self_approve")) {
712
736
  db.exec("ALTER TABLE nests ADD COLUMN allow_self_approve INTEGER NOT NULL DEFAULT 0");
713
737
  }
738
+ if (!nestCols.includes("prime_only_review")) {
739
+ db.exec("ALTER TABLE nests ADD COLUMN prime_only_review INTEGER NOT NULL DEFAULT 0");
740
+ }
741
+ if (!nestCols.includes("prime_tags")) {
742
+ db.exec("ALTER TABLE nests ADD COLUMN prime_tags TEXT NOT NULL DEFAULT 'prime-document'");
743
+ }
714
744
  const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
715
745
  if (!userCols.includes("is_admin")) {
716
746
  db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
@@ -1454,6 +1484,53 @@ function runMigrations(db) {
1454
1484
  })();
1455
1485
  recordMigration("031_team_source");
1456
1486
  }
1487
+ if (!hasMigration("032_deletion_requests")) {
1488
+ db.transaction(() => {
1489
+ db.exec(`
1490
+ CREATE TABLE IF NOT EXISTS deletion_requests (
1491
+ id TEXT PRIMARY KEY,
1492
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1493
+ node_id TEXT NOT NULL,
1494
+ requested_by TEXT NOT NULL, -- email
1495
+ reason TEXT NOT NULL,
1496
+ status TEXT NOT NULL DEFAULT 'pending'
1497
+ CHECK(status IN ('pending', 'declined')),
1498
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1499
+ resolved_by TEXT, -- email
1500
+ resolved_at TEXT,
1501
+ resolution_note TEXT
1502
+ );
1503
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_deletion_requests_pending
1504
+ ON deletion_requests(nest_id, node_id) WHERE status = 'pending';
1505
+ CREATE INDEX IF NOT EXISTS idx_deletion_requests_nest
1506
+ ON deletion_requests(nest_id, status);
1507
+ `);
1508
+ })();
1509
+ recordMigration("032_deletion_requests");
1510
+ }
1511
+ if (!hasMigration("033_deletion_request_targets")) {
1512
+ db.transaction(() => {
1513
+ const cols = db.prepare("PRAGMA table_info(deletion_requests)").all().map((c) => c.name);
1514
+ if (!cols.includes("target_type")) {
1515
+ db.exec(
1516
+ "ALTER TABLE deletion_requests ADD COLUMN target_type TEXT NOT NULL DEFAULT 'document'"
1517
+ );
1518
+ }
1519
+ db.exec(`
1520
+ DROP INDEX IF EXISTS idx_deletion_requests_pending;
1521
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_deletion_requests_pending
1522
+ ON deletion_requests(nest_id, target_type, node_id) WHERE status = 'pending';
1523
+ `);
1524
+ })();
1525
+ recordMigration("033_deletion_request_targets");
1526
+ }
1527
+ if (!hasMigration("034_versions_author_status_index")) {
1528
+ db.exec(`
1529
+ CREATE INDEX IF NOT EXISTS idx_versions_author_status
1530
+ ON node_versions(author, status);
1531
+ `);
1532
+ recordMigration("034_versions_author_status_index");
1533
+ }
1457
1534
  }
1458
1535
  function mergeCaseCollidingUsers(db) {
1459
1536
  const groups = db.prepare(
@@ -1671,7 +1748,7 @@ async function initDb() {
1671
1748
  if (config.DB_DRIVER === "postgres") {
1672
1749
  const { Pool } = await import("pg");
1673
1750
  const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
1674
- const { runPostgresMigrations } = await import("./migrations.postgres-BRXZY2GE.js");
1751
+ const { runPostgresMigrations } = await import("./migrations.postgres-CYKO5CFV.js");
1675
1752
  const pool = new Pool(buildPgConfig());
1676
1753
  adapter = new PostgresAdapter(pool);
1677
1754
  await runPostgresMigrations(adapter);