@promptowl/contextnest-community 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONFIGURATION.md CHANGED
@@ -76,6 +76,8 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
76
76
  | `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
77
77
  | `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
78
78
  | `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
79
+ | `POSTHOG_KEY` | `""` | PostHog project API key for product analytics in the UI. Empty = analytics off (the default — a self-hosted install brings its own project). Served to the browser via `/health`, but only while `TELEMETRY_ENABLED` is on, so that switch turns off everything this server sends outward. Also editable from Settings → Advanced. |
80
+ | `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Settings → Advanced. |
79
81
  | `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
80
82
  | `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
81
83
  | `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
@@ -5,7 +5,17 @@ function translatePlaceholders(sql) {
5
5
  let n = 0;
6
6
  for (let i = 0; i < sql.length; i++) {
7
7
  const ch = sql[i];
8
- if (ch === "'") {
8
+ if (!inStr && ch === "-" && sql[i + 1] === "-") {
9
+ const end = sql.indexOf("\n", i);
10
+ const stop = end === -1 ? sql.length : end;
11
+ out += sql.slice(i, stop);
12
+ i = stop - 1;
13
+ } else if (!inStr && ch === "/" && sql[i + 1] === "*") {
14
+ const end = sql.indexOf("*/", i + 2);
15
+ const stop = end === -1 ? sql.length : end + 2;
16
+ out += sql.slice(i, stop);
17
+ i = stop - 1;
18
+ } else if (ch === "'") {
9
19
  inStr = !inStr;
10
20
  out += ch;
11
21
  } else if (ch === "?" && !inStr) {
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-6JGQX4GA.js";
5
+ } from "./chunk-W5ILNGPD.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-LPPKPEYI.js";
8
+ } from "./chunk-J2OQ3MEB.js";
9
9
 
10
10
  // src/governance/version-service.ts
11
11
  import { createHash } from "crypto";
@@ -351,6 +351,19 @@ var config = {
351
351
  get TELEMETRY_INTERVAL_MS() {
352
352
  return parseInt(process.env.TELEMETRY_INTERVAL_MS || "3600000", 10);
353
353
  },
354
+ /**
355
+ * PostHog product analytics. Empty key = off, which is the default: a
356
+ * self-hosted operator brings their own project. The key is a public client
357
+ * key by design (the browser sends it), so /health may hand it out — but
358
+ * only while TELEMETRY_ENABLED is on, so one switch stops all outbound
359
+ * usage data. Editable at runtime from the admin Settings page.
360
+ */
361
+ get POSTHOG_KEY() {
362
+ return (process.env.POSTHOG_KEY || "").trim();
363
+ },
364
+ get POSTHOG_HOST() {
365
+ return (process.env.POSTHOG_HOST || "").trim() || "https://us.i.posthog.com";
366
+ },
354
367
  /**
355
368
  * Activity-trace retention window in days (the api_events table behind
356
369
  * GET /admin/trace and GET /nests/:id/trace). 0 = keep forever (pruning is
@@ -626,7 +639,7 @@ function runMigrations(db) {
626
639
  slug TEXT NOT NULL,
627
640
  description TEXT,
628
641
  visibility TEXT NOT NULL DEFAULT 'private'
629
- CHECK(visibility IN ('private', 'public')),
642
+ CHECK(visibility IN ('private', 'org', 'public')),
630
643
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
631
644
  UNIQUE(user_id, slug)
632
645
  );
@@ -1702,6 +1715,67 @@ function runMigrations(db) {
1702
1715
  })();
1703
1716
  recordMigration("039_nest_pins");
1704
1717
  }
1718
+ if (!hasMigration("041_nest_visibility_org")) {
1719
+ widenNestVisibilityWithGuard(db);
1720
+ recordMigration("041_nest_visibility_org");
1721
+ }
1722
+ }
1723
+ function fkViolationCounts(db) {
1724
+ const rows = db.pragma("foreign_key_check");
1725
+ const counts = /* @__PURE__ */ new Map();
1726
+ for (const r of rows) {
1727
+ const key = `${r.table}|${r.parent}|${r.fkid}`;
1728
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1729
+ }
1730
+ return counts;
1731
+ }
1732
+ function widenNestVisibilityWithGuard(db) {
1733
+ const before = fkViolationCounts(db);
1734
+ const fkWasOn = db.pragma("foreign_keys", { simple: true }) === 1;
1735
+ if (fkWasOn) db.pragma("foreign_keys = OFF");
1736
+ try {
1737
+ db.transaction(() => {
1738
+ widenNestVisibilityCheck(db);
1739
+ })();
1740
+ const after = fkViolationCounts(db);
1741
+ const introduced = [...after].filter(
1742
+ ([key, n]) => n > (before.get(key) ?? 0)
1743
+ );
1744
+ if (introduced.length > 0) {
1745
+ throw new Error(
1746
+ `041_nest_visibility_org introduced foreign-key violations: ${introduced.map(([key]) => key).join(", ")}`
1747
+ );
1748
+ }
1749
+ } finally {
1750
+ if (fkWasOn) db.pragma("foreign_keys = ON");
1751
+ }
1752
+ }
1753
+ function widenNestVisibilityCheck(db) {
1754
+ const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'nests'").get();
1755
+ const ddl = row?.sql;
1756
+ if (!ddl) return;
1757
+ const checkRe = /CHECK\s*\(\s*visibility\s+IN\s*\([^)]*\)\s*\)/i;
1758
+ const current = ddl.match(checkRe);
1759
+ if (!current) return;
1760
+ if (/'org'/i.test(current[0])) return;
1761
+ const newDdl = ddl.replace(
1762
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`\[]?nests["'`\]]?/i,
1763
+ "CREATE TABLE nests_new"
1764
+ ).replace(checkRe, "CHECK(visibility IN ('private', 'org', 'public'))");
1765
+ if (!newDdl.startsWith("CREATE TABLE nests_new")) {
1766
+ throw new Error("041: could not rewrite the nests CREATE TABLE statement");
1767
+ }
1768
+ const cols = db.prepare("PRAGMA table_info(nests)").all().map((c) => `"${c.name}"`).join(", ");
1769
+ const indexes = db.prepare(
1770
+ "SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = 'nests' AND sql IS NOT NULL"
1771
+ ).all().map((r) => `${r.sql};`);
1772
+ db.exec(`
1773
+ ${newDdl};
1774
+ INSERT INTO nests_new (${cols}) SELECT ${cols} FROM nests;
1775
+ DROP TABLE nests;
1776
+ ALTER TABLE nests_new RENAME TO nests;
1777
+ ${indexes.join("\n ")}
1778
+ `);
1705
1779
  }
1706
1780
  function mergeCaseCollidingUsers(db) {
1707
1781
  const groups = db.prepare(
@@ -1918,8 +1992,8 @@ async function initDb() {
1918
1992
  if (adapter) return adapter;
1919
1993
  if (config.DB_DRIVER === "postgres") {
1920
1994
  const { Pool } = await import("pg");
1921
- const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
1922
- const { runPostgresMigrations } = await import("./migrations.postgres-JA2YGLS3.js");
1995
+ const { PostgresAdapter } = await import("./adapter.postgres-6VZCMPQL.js");
1996
+ const { runPostgresMigrations } = await import("./migrations.postgres-KKICIH2C.js");
1923
1997
  const pool = new Pool(buildPgConfig());
1924
1998
  adapter = new PostgresAdapter(pool);
1925
1999
  await runPostgresMigrations(adapter);
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-LPPKPEYI.js";
6
+ } from "./chunk-J2OQ3MEB.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -4,19 +4,19 @@ import {
4
4
  resolveNestWideRoles,
5
5
  resolveStewardsForNode,
6
6
  stewardCoverageForUser
7
- } from "./chunk-N2DTJFCD.js";
7
+ } from "./chunk-PX3P4FTU.js";
8
8
  import {
9
9
  grantCoversNode,
10
10
  listUserGrants,
11
11
  resolveNodeGrant
12
- } from "./chunk-XWGEXQU3.js";
12
+ } from "./chunk-MY4JIWQD.js";
13
13
  import {
14
14
  createVersion,
15
15
  getApprovedVersion,
16
16
  getApprovedVersions,
17
17
  getCurrentVersion,
18
18
  setApprovedVersion
19
- } from "./chunk-IGPJ74O4.js";
19
+ } from "./chunk-6WESEA75.js";
20
20
  import {
21
21
  buildDocContext,
22
22
  buildTitleMap,
@@ -36,7 +36,7 @@ import {
36
36
  resolveNestPermission,
37
37
  sendEmailToRecipient,
38
38
  titleForNode
39
- } from "./chunk-6JGQX4GA.js";
39
+ } from "./chunk-W5ILNGPD.js";
40
40
  import {
41
41
  ConflictError,
42
42
  NotFoundError,
@@ -45,7 +45,7 @@ import {
45
45
  import {
46
46
  config,
47
47
  getDb
48
- } from "./chunk-LPPKPEYI.js";
48
+ } from "./chunk-J2OQ3MEB.js";
49
49
 
50
50
  // src/governance/review-service.ts
51
51
  import { v4 as uuid2 } from "uuid";
@@ -131,7 +131,17 @@ async function nodeTrail(db, nestId, ids) {
131
131
  for (const r of rows) if (r.title) titles.set(r.node_id, r.title);
132
132
  } catch {
133
133
  }
134
- return ids.map((id) => `"${titles.get(id) ?? id}"`).join(" \u2192 ");
134
+ const label = (id) => titles.get(id) ?? id;
135
+ const idsPerLabel = /* @__PURE__ */ new Map();
136
+ for (const id of unique) {
137
+ const l = label(id);
138
+ (idsPerLabel.get(l) ?? idsPerLabel.set(l, /* @__PURE__ */ new Set()).get(l)).add(id);
139
+ }
140
+ return ids.map((id) => {
141
+ const l = label(id);
142
+ const ambiguous = (idsPerLabel.get(l)?.size ?? 0) > 1;
143
+ return ambiguous && l !== id ? `"${l}" (${id})` : `"${l}"`;
144
+ }).join(" \u2192 ");
135
145
  }
136
146
  async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
137
147
  const db = getDb();
@@ -147,7 +157,7 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
147
157
  for (const r of rows) {
148
158
  if (r.from_node === r.to_node) {
149
159
  throw new ConflictError(
150
- `cycle: ${await nodeTrail(db, nestId, [r.from_node])} cannot flow into itself`
160
+ `This step loops back on itself: ${await nodeTrail(db, nestId, [r.from_node])}. A flow can't return to a step it already ran.`
151
161
  );
152
162
  }
153
163
  nodes.add(r.from_node);
@@ -170,7 +180,7 @@ async function assertFlowGraphAcyclic(nestId, extraFlowTypeId) {
170
180
  }
171
181
  if (removed < nodes.size) {
172
182
  throw new ConflictError(
173
- "cycle: enabling flow on this edge type would create a cycle in the flow graph \u2014 flow edges must form a DAG"
183
+ "Making this connection type a flow would loop these steps back on each other. A flow can't return to a step it already ran."
174
184
  );
175
185
  }
176
186
  }
@@ -11,7 +11,7 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-6JGQX4GA.js";
14
+ } from "./chunk-W5ILNGPD.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-LPPKPEYI.js";
23
+ } from "./chunk-J2OQ3MEB.js";
24
24
 
25
25
  // src/governance/stewardship-service.ts
26
26
  import { v4 as uuid } from "uuid";
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-LPPKPEYI.js";
11
+ } from "./chunk-J2OQ3MEB.js";
12
12
  import {
13
13
  ANON_USER_ID
14
14
  } from "./chunk-YB3LKF7U.js";
@@ -1879,8 +1879,9 @@ async function resolveNestAccessRaw(nestId, userId) {
1879
1879
  return { ...base, permission: best, isPublicReader: false };
1880
1880
  }
1881
1881
  if (base.isSteward) best = higher(best, "read");
1882
- if (row.visibility === "public") best = higher(best, "read");
1883
- const isPublicReader2 = row.visibility === "public" && !row.collab_permission && !base.isSteward;
1882
+ const visibilityRead = row.visibility === "public" || row.visibility === "org" && userId !== ANON_USER_ID;
1883
+ if (visibilityRead) best = higher(best, "read");
1884
+ const isPublicReader2 = visibilityRead && !row.collab_permission && !base.isSteward;
1884
1885
  return { ...base, permission: best, isPublicReader: isPublicReader2 };
1885
1886
  }
1886
1887
  async function resolveNestPermission(nestId, userId) {
@@ -2309,11 +2310,13 @@ async function listSharedNests(userId) {
2309
2310
  async function listVisibleNests(userId, opts = {}) {
2310
2311
  const db = getDb();
2311
2312
  const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
2313
+ const visibleByVisibility = userId === ANON_USER_ID ? ["public"] : ["public", "org"];
2312
2314
  const params = [
2313
2315
  userId,
2314
2316
  userId,
2315
2317
  ...opts.readable ? [userId] : [],
2316
2318
  userId,
2319
+ ...opts.readable ? visibleByVisibility : [],
2317
2320
  ...includeAnon ? [ANON_USER_ID] : []
2318
2321
  ];
2319
2322
  const rows = await db.all(
@@ -2329,7 +2332,7 @@ async function listVisibleNests(userId, opts = {}) {
2329
2332
  AND (n.user_id = ?
2330
2333
  OR nc.user_id IS NOT NULL
2331
2334
  OR s.id IS NOT NULL
2332
- ${opts.readable ? "OR g.id IS NOT NULL OR n.visibility = 'public'" : ""}
2335
+ ${opts.readable ? `OR g.id IS NOT NULL OR n.visibility IN (${visibleByVisibility.map(() => "?").join(", ")})` : ""}
2333
2336
  ${includeAnon ? "OR n.user_id = ?" : ""})
2334
2337
  ORDER BY n.created_at DESC`,
2335
2338
  params
@@ -2345,9 +2348,10 @@ async function listVisibleNests(userId, opts = {}) {
2345
2348
  }
2346
2349
  async function listPublicNests(userId) {
2347
2350
  const db = getDb();
2351
+ const visibilities = userId === ANON_USER_ID ? ["public"] : ["public", "org"];
2348
2352
  return await db.all(
2349
2353
  `SELECT n.* FROM nests n
2350
- WHERE n.visibility = 'public'
2354
+ WHERE n.visibility IN (${visibilities.map(() => "?").join(", ")})
2351
2355
  AND ${LIVE_ONLY_N}
2352
2356
  AND n.user_id != ?
2353
2357
  AND NOT EXISTS (
@@ -2355,7 +2359,7 @@ async function listPublicNests(userId) {
2355
2359
  WHERE nc.nest_id = n.id AND nc.user_id = ?
2356
2360
  )
2357
2361
  ORDER BY n.created_at DESC`,
2358
- [userId, userId]
2362
+ [...visibilities, userId, userId]
2359
2363
  );
2360
2364
  }
2361
2365
  async function getNest(nestId) {
@@ -2587,8 +2591,8 @@ async function ensureNodeIndex(nestId) {
2587
2591
  return run;
2588
2592
  }
2589
2593
  async function rebuildNodeIndex(nestId) {
2590
- const { engineCache: engineCache2 } = await import("./engine-IKZQ46P7.js");
2591
- const { documentsWithSuggestions } = await import("./external-edit-service-LTON57HO.js");
2594
+ const { engineCache: engineCache2 } = await import("./engine-XKRP7GOQ.js");
2595
+ const { documentsWithSuggestions } = await import("./external-edit-service-TXW7SXSF.js");
2592
2596
  const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2593
2597
  dropDiscoveryCache();
2594
2598
  const startedAt = writeGeneration.get(nestId) ?? 0;
@@ -5,10 +5,10 @@ import {
5
5
  markIndexStale,
6
6
  resolveNestPath,
7
7
  setSuggestionFlag
8
- } from "./chunk-6JGQX4GA.js";
8
+ } from "./chunk-W5ILNGPD.js";
9
9
  import {
10
10
  getDb
11
- } from "./chunk-LPPKPEYI.js";
11
+ } from "./chunk-J2OQ3MEB.js";
12
12
 
13
13
  // src/governance/external-edit-service.ts
14
14
  import { readFile, readdir } from "fs/promises";
@@ -82,7 +82,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
82
82
  if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
83
83
  return null;
84
84
  }
85
- const { hasUnsealedDraft } = await import("./version-service-PJV74PVQ.js");
85
+ const { hasUnsealedDraft } = await import("./version-service-TKNYJMCM.js");
86
86
  if (await hasUnsealedDraft(nestId, documentId)) {
87
87
  return null;
88
88
  }
@@ -259,7 +259,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
259
259
  async function mirrorVersion(input) {
260
260
  const { storage } = await engineCache.get(input.nestId);
261
261
  const node = await storage.readDocument(input.documentId);
262
- const { upsertVersion, setApprovedVersion } = await import("./version-service-PJV74PVQ.js");
262
+ const { upsertVersion, setApprovedVersion } = await import("./version-service-TKNYJMCM.js");
263
263
  await upsertVersion({
264
264
  nestId: input.nestId,
265
265
  nodeId: input.documentId,
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-LPPKPEYI.js";
5
+ } from "./chunk-J2OQ3MEB.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-6JGQX4GA.js";
5
+ } from "./chunk-W5ILNGPD.js";
6
6
  import "./chunk-YVMSM7LS.js";
7
- import "./chunk-LPPKPEYI.js";
7
+ import "./chunk-J2OQ3MEB.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-6PJQSKKV.js";
15
- import "./chunk-6JGQX4GA.js";
14
+ } from "./chunk-ZAV4QUZA.js";
15
+ import "./chunk-W5ILNGPD.js";
16
16
  import "./chunk-YVMSM7LS.js";
17
- import "./chunk-LPPKPEYI.js";
17
+ import "./chunk-J2OQ3MEB.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-XWGEXQU3.js";
9
+ } from "./chunk-MY4JIWQD.js";
10
10
  import "./chunk-YVMSM7LS.js";
11
- import "./chunk-LPPKPEYI.js";
11
+ import "./chunk-J2OQ3MEB.js";
12
12
  import "./chunk-YB3LKF7U.js";
13
13
  export {
14
14
  createGrant,
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  submitForReview,
38
38
  withNodeWriteLock,
39
39
  withdrawDeletionRequest
40
- } from "./chunk-4KJGQHWB.js";
40
+ } from "./chunk-OQXZ43HG.js";
41
41
  import {
42
42
  canCreateInNest,
43
43
  canManageStewards,
@@ -57,7 +57,7 @@ import {
57
57
  resolveUserRoles,
58
58
  syncFromConfig,
59
59
  updateSteward
60
- } from "./chunk-N2DTJFCD.js";
60
+ } from "./chunk-PX3P4FTU.js";
61
61
  import {
62
62
  createGrant,
63
63
  deleteGrant,
@@ -66,7 +66,7 @@ import {
66
66
  listGrants,
67
67
  listUserGrants,
68
68
  resolveNodeGrant
69
- } from "./chunk-XWGEXQU3.js";
69
+ } from "./chunk-MY4JIWQD.js";
70
70
  import {
71
71
  generateApiKey,
72
72
  getKeyPrefix,
@@ -92,7 +92,7 @@ import {
92
92
  listMyDrafts,
93
93
  setApprovedVersion,
94
94
  upsertVersion
95
- } from "./chunk-IGPJ74O4.js";
95
+ } from "./chunk-6WESEA75.js";
96
96
  import {
97
97
  approveExternalEdit,
98
98
  getExternalEditDetail,
@@ -104,7 +104,7 @@ import {
104
104
  scanDocumentForDrift,
105
105
  scanNestForDrift,
106
106
  startDriftScanner
107
- } from "./chunk-6PJQSKKV.js";
107
+ } from "./chunk-ZAV4QUZA.js";
108
108
  import {
109
109
  addMember,
110
110
  addWatcher,
@@ -208,7 +208,7 @@ import {
208
208
  updateMemberRole,
209
209
  validateLicense,
210
210
  verifySmtp
211
- } from "./chunk-6JGQX4GA.js";
211
+ } from "./chunk-W5ILNGPD.js";
212
212
  import {
213
213
  AppError,
214
214
  ConflictError,
@@ -226,7 +226,7 @@ import {
226
226
  initDb,
227
227
  isEmailListish,
228
228
  isEmailish
229
- } from "./chunk-LPPKPEYI.js";
229
+ } from "./chunk-J2OQ3MEB.js";
230
230
  import {
231
231
  ANON_EMAIL,
232
232
  ANON_USER_ID,
@@ -4147,8 +4147,8 @@ sharingRoutes.delete("/collaborators/:collabId", async (c) => {
4147
4147
  });
4148
4148
  sharingRoutes.patch("/visibility", async (c) => {
4149
4149
  const body = await c.req.json();
4150
- if (!body.visibility || !["private", "public"].includes(body.visibility)) {
4151
- throw new ValidationError("visibility must be private or public");
4150
+ if (!body.visibility || !["private", "org", "public"].includes(body.visibility)) {
4151
+ throw new ValidationError("visibility must be private, org or public");
4152
4152
  }
4153
4153
  if (body.visibility === "public" && body.acknowledge_public !== true) {
4154
4154
  return c.json(
@@ -5817,7 +5817,7 @@ nodeRoutes.post("/", async (c) => {
5817
5817
  nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
5818
5818
  const nestId = c.req.param("nestId");
5819
5819
  const nodeId = c.req.param("nodeId");
5820
- const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-2YKGBP63.js");
5820
+ const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-5HBQGDMK.js");
5821
5821
  const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
5822
5822
  nestId,
5823
5823
  nodeId
@@ -5838,7 +5838,7 @@ nodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
5838
5838
  nodeRoutes.get("/:nodeId{.+}/reviews", async (c) => {
5839
5839
  const nestId = c.req.param("nestId");
5840
5840
  const nodeId = c.req.param("nodeId");
5841
- const { getReviewHistory: getReviewHistory2 } = await import("./review-service-6JK64IRT.js");
5841
+ const { getReviewHistory: getReviewHistory2 } = await import("./review-service-XFWTENTN.js");
5842
5842
  const history = await getReviewHistory2(nestId, nodeId);
5843
5843
  return c.json({ reviews: history });
5844
5844
  });
@@ -7859,7 +7859,7 @@ async function validateCondition(nestId, mode, condition) {
7859
7859
  async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
7860
7860
  if (fromNode === toNode) {
7861
7861
  throw new ConflictError(
7862
- `cycle: ${await nodeTrail(db, nestId, [fromNode])} cannot flow into itself`
7862
+ `This step loops back on itself: ${await nodeTrail(db, nestId, [fromNode])}. A flow can't return to a step it already ran.`
7863
7863
  );
7864
7864
  }
7865
7865
  const rows = await db.all(
@@ -7888,7 +7888,7 @@ async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
7888
7888
  }
7889
7889
  path.reverse().push(toNode);
7890
7890
  throw new ConflictError(
7891
- `cycle: ${await nodeTrail(db, nestId, path)} \u2014 flow edges must form a DAG`
7891
+ `These steps loop back on each other: ${await nodeTrail(db, nestId, path)}. A flow can't return to a step it already ran.`
7892
7892
  );
7893
7893
  }
7894
7894
  for (const nb of adj.get(cur) ?? []) {
@@ -9927,8 +9927,8 @@ ${list}`,
9927
9927
  if (!target) return "target is required (a node id or folder prefix).";
9928
9928
  if (!["read", "write"].includes(role)) return "role must be read or write.";
9929
9929
  try {
9930
- const { createGrant: createGrant2 } = await import("./grants-service-LSNZN6NG.js");
9931
- const db = (await import("./client-BD6PAUCZ.js")).getDb();
9930
+ const { createGrant: createGrant2 } = await import("./grants-service-UZ2MGDO5.js");
9931
+ const db = (await import("./client-CSCHXUNX.js")).getDb();
9932
9932
  const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
9933
9933
  const e = normalizeEmail2(String(args.email || ""));
9934
9934
  if (!e) return "email is required.";
@@ -13013,6 +13013,11 @@ function createApp() {
13013
13013
  // Presence only: the publish dialog hides the email/invite gates when
13014
13014
  // this server cannot send mail, because their only way in is a mailed link.
13015
13015
  email_configured: isEmailConfigured(),
13016
+ // Product analytics for the SPA. The PostHog project key is a public
13017
+ // client key, but it only ships while usage telemetry is on — one switch
13018
+ // covers everything this server sends outward.
13019
+ posthog_key: config.TELEMETRY_ENABLED ? config.POSTHOG_KEY || null : null,
13020
+ posthog_host: config.POSTHOG_HOST,
13016
13021
  promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
13017
13022
  // Manual (email/password) sign-in mode (open | invite-only | disabled).
13018
13023
  // The login page reads this to show/hide the credential form, and to
@@ -13137,6 +13142,8 @@ function createApp() {
13137
13142
  manual_sign_in: config.MANUAL_SIGN_IN,
13138
13143
  logo_url: config.LOGO_URL,
13139
13144
  telemetry_enabled: config.TELEMETRY_ENABLED,
13145
+ posthog_key: config.POSTHOG_KEY,
13146
+ posthog_host: config.POSTHOG_HOST,
13140
13147
  public_base_url: config.PUBLIC_BASE_URL,
13141
13148
  max_body_bytes: config.MAX_BODY_BYTES,
13142
13149
  workflow_plane_enabled: config.FEATURE_WORKFLOW_PLANE,
@@ -13235,6 +13242,16 @@ function createApp() {
13235
13242
  if ("telemetry_enabled" in body) {
13236
13243
  pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
13237
13244
  }
13245
+ if ("posthog_key" in body) {
13246
+ const v = String(body.posthog_key ?? "").trim();
13247
+ pending.push({ name: "POSTHOG_KEY", value: v || null });
13248
+ }
13249
+ if ("posthog_host" in body) {
13250
+ const v = String(body.posthog_host ?? "").trim().replace(/\/$/, "");
13251
+ if (v && !/^https?:\/\/[^\s]+$/i.test(v))
13252
+ addError("posthog_host", "PostHog host must be a http(s) URL, e.g. https://us.i.posthog.com");
13253
+ else pending.push({ name: "POSTHOG_HOST", value: v || null });
13254
+ }
13238
13255
  if ("workflow_plane_enabled" in body) {
13239
13256
  pending.push({
13240
13257
  name: "FEATURE_WORKFLOW_PLANE",
@@ -47,7 +47,8 @@ var HISTORICAL_MIGRATIONS = [
47
47
  "036_node_index",
48
48
  "037_publish",
49
49
  "038_nest_archive",
50
- "039_nest_pins"
50
+ "039_nest_pins",
51
+ "041_nest_visibility_org"
51
52
  ];
52
53
  async function runPostgresMigrations(db) {
53
54
  await db.exec(`
@@ -74,7 +75,7 @@ async function runPostgresMigrations(db) {
74
75
  slug TEXT NOT NULL,
75
76
  description TEXT,
76
77
  visibility TEXT NOT NULL DEFAULT 'private'
77
- CHECK(visibility IN ('private', 'public')),
78
+ CHECK(visibility IN ('private', 'org', 'public')),
78
79
  created_at TEXT NOT NULL DEFAULT ${NOW},
79
80
  stewardship_enabled INTEGER NOT NULL DEFAULT 0,
80
81
  is_imported INTEGER NOT NULL DEFAULT 0,
@@ -695,6 +696,20 @@ async function runPostgresMigrations(db) {
695
696
  [id]
696
697
  );
697
698
  }
699
+ const visibilityCheck = await db.get(
700
+ `SELECT pg_get_constraintdef(oid) AS def
701
+ FROM pg_constraint
702
+ WHERE conrelid = 'nests'::regclass
703
+ AND conname = 'nests_visibility_check'`
704
+ );
705
+ if (!visibilityCheck?.def?.includes("'org'")) {
706
+ await db.run(
707
+ "ALTER TABLE nests DROP CONSTRAINT IF EXISTS nests_visibility_check"
708
+ );
709
+ await db.run(
710
+ "ALTER TABLE nests ADD CONSTRAINT nests_visibility_check CHECK (visibility IN ('private', 'org', 'public'))"
711
+ );
712
+ }
698
713
  await db.run(
699
714
  "UPDATE nests SET visibility = 'public' WHERE user_id = ? AND visibility = 'private'",
700
715
  [ANON_USER_ID]
@@ -17,15 +17,15 @@ import {
17
17
  requestDeletion,
18
18
  submitForReview,
19
19
  withdrawDeletionRequest
20
- } from "./chunk-4KJGQHWB.js";
20
+ } from "./chunk-OQXZ43HG.js";
21
21
  import {
22
22
  canUserApprove
23
- } from "./chunk-N2DTJFCD.js";
24
- import "./chunk-XWGEXQU3.js";
25
- import "./chunk-IGPJ74O4.js";
26
- import "./chunk-6JGQX4GA.js";
23
+ } from "./chunk-PX3P4FTU.js";
24
+ import "./chunk-MY4JIWQD.js";
25
+ import "./chunk-6WESEA75.js";
26
+ import "./chunk-W5ILNGPD.js";
27
27
  import "./chunk-YVMSM7LS.js";
28
- import "./chunk-LPPKPEYI.js";
28
+ import "./chunk-J2OQ3MEB.js";
29
29
  import "./chunk-YB3LKF7U.js";
30
30
  import "./chunk-FRQJWGN3.js";
31
31
  export {
@@ -23,10 +23,10 @@ import {
23
23
  syncFromConfig,
24
24
  updateSteward,
25
25
  updateStewardRole
26
- } from "./chunk-N2DTJFCD.js";
27
- import "./chunk-6JGQX4GA.js";
26
+ } from "./chunk-PX3P4FTU.js";
27
+ import "./chunk-W5ILNGPD.js";
28
28
  import "./chunk-YVMSM7LS.js";
29
- import "./chunk-LPPKPEYI.js";
29
+ import "./chunk-J2OQ3MEB.js";
30
30
  import "./chunk-YB3LKF7U.js";
31
31
  import "./chunk-FRQJWGN3.js";
32
32
  export {
@@ -20,10 +20,10 @@ import {
20
20
  setApprovedVersion,
21
21
  systemAuthor,
22
22
  upsertVersion
23
- } from "./chunk-IGPJ74O4.js";
24
- import "./chunk-6JGQX4GA.js";
23
+ } from "./chunk-6WESEA75.js";
24
+ import "./chunk-W5ILNGPD.js";
25
25
  import "./chunk-YVMSM7LS.js";
26
- import "./chunk-LPPKPEYI.js";
26
+ import "./chunk-J2OQ3MEB.js";
27
27
  import "./chunk-YB3LKF7U.js";
28
28
  import "./chunk-FRQJWGN3.js";
29
29
  export {