@promptowl/contextnest-community 1.15.0 → 1.17.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/README.md CHANGED
@@ -1,169 +1,164 @@
1
- # ContextNest Community Edition
2
-
3
- **Self-hosted context governance server for AI workflows.** Part of the [PromptOwl](https://promptowl.ai) platform.
4
-
5
- > ⚠️ **Commercial Software.** ContextNest Community Edition is proprietary software licensed by Promptowl LLC. A free [PromptOwl account](https://app.promptowl.ai) is required to use it. Redistribution, hosting-as-a-service, and competitive use are prohibited. See [LICENSE.md](./LICENSE.md) for full terms.
6
-
7
- ---
8
-
9
- ## What it is
10
-
11
- ContextNest Community Edition is a self-hosted server that lets you:
12
-
13
- - Store, version, and govern markdown-based context documents ("nests")
14
- - Import an existing folder or vault of markdown files in one step
15
- - Export a nest as a portable bundle and re-import it on another self-hosted host
16
- - Apply stewardship workflows — draft, pending review, approved
17
- - Share nests with collaborators or publish them read-only to the public
18
- - Serve approved context to AI agents via MCP, HTTP, or CLI
19
- - Sync with the PromptOwl hosted platform for multi-user collaboration
20
-
21
- The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
22
-
23
- ## Quickstart
24
-
25
- ```bash
26
- # 1. Run the community server
27
- npx @promptowl/contextnest-community
28
-
29
- # 2. Open the server in your browser
30
- # http://localhost:3838
31
- # On first boot with no license, it lands on the License Setup Page.
32
-
33
- # 3. Paste your PromptOwl license key (pk_...) — see "License setup" below
34
- ```
35
-
36
- The server listens on `http://localhost:3838` by default. Without a valid license the server still runs and serves reads, but write actions return `503` until you activate. See [CONFIGURATION.md](./CONFIGURATION.md) for all environment variables (port, auth mode, storage, telemetry).
37
-
38
- > **Optional:** scaffold a local nest with the open-source CLI before connecting:
39
- > ```bash
40
- > npx @promptowl/contextnest-cli init
41
- > ```
42
-
43
- ## License setup
44
-
45
- ContextNest Community Edition requires a PromptOwl Community License key (`pk_...`). Getting and activating one:
46
-
47
- ### 1. Create the key (free)
48
-
49
- 1. Sign up or log in at <https://app.promptowl.ai>
50
- 2. Open the **Overview** menu → **Community License**
51
- 3. Click **Create a Community License key**
52
- 4. Copy the generated key — it starts with `pk_`
53
-
54
- ### 2. Activate the server
55
-
56
- Pick **one** of two ways:
57
-
58
- **A. Browser setup page (recommended for first run)**
59
-
60
- 1. Start the server: `npx @promptowl/contextnest-community`
61
- 2. Open <http://localhost:3838> — with no license installed, the server boots into **setup mode** and shows the **License Setup Page**
62
- 3. Paste your `pk_...` key and submit
63
- 4. The server validates it against PromptOwl, writes it to your `.env`, and exits setup mode — no restart needed
64
-
65
- **B. Environment variable (recommended for Docker / CI / scripted deploys)**
66
-
67
- ```bash
68
- PROMPTOWL_KEY=pk_... npx @promptowl/contextnest-community
69
- ```
70
-
71
- The key is read at boot. The server validates against PromptOwl on startup; if valid, it goes straight into licensed mode.
72
-
73
- ### 3. How licensing behaves at runtime
74
-
75
- - **Unlicensed / setup mode** — reads work; every non-GET (write) request returns `503` until a valid key is installed.
76
- - **Live revocation** — a long-poll watcher tracks license state against PromptOwl. If your key is revoked, the server blocks writes within seconds (no restart required) and returns to setup mode.
77
- - **Admin identity follows the license** — the admin user is whichever PromptOwl account owns the installed key, resolved live per request. Transferring the license to another account immediately promotes the new owner and demotes the old one.
78
-
79
- For redistribution, hosted-service, OEM, or regulated-industry licensing, contact **hoot@promptowl.ai**.
80
-
81
- ## System requirements
82
-
83
- - **Node.js** 20.x or later
84
- - **PromptOwl account** — free signup at <https://app.promptowl.ai/signup>
85
- - **OS:** Windows, macOS, or Linux
86
- - **Disk:** ~200 MB for the server, plus storage for your nests
87
-
88
- ## What you get
89
-
90
- | Feature | Community Edition | Enterprise |
91
- |---|:---:|:---:|
92
- | Self-hosted context server | ✅ | ✅ |
93
- | SQLite or PostgreSQL / Cloud SQL backend | ✅ | ✅ |
94
- | Configurable nest storage (external / GCS volume mount) | ✅ | ✅ |
95
- | Markdown + YAML frontmatter vaults | ✅ | ✅ |
96
- | Import existing folder / vault | ✅ | ✅ |
97
- | Export nest as portable bundle (.zip) | ✅ | ✅ |
98
- | Markdown rendering + wiki cross-linking | ✅ | ✅ |
99
- | External-edit detection + version diff | ✅ | ✅ |
100
- | Stewardship workflow (draft/review/approve) | ✅ | ✅ |
101
- | Per-nest sharing + collaborators | ✅ | ✅ |
102
- | Public read-only nests | ✅ | ✅ |
103
- | Custom logo / branding | ✅ | ✅ |
104
- | Admin password reset + user removal (in-platform) | ✅ | ✅ |
105
- | Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
106
- | Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload | ✅ | ✅ |
107
- | Steward version revert | ✅ | ✅ |
108
- | MCP server for AI agents | ✅ | ✅ |
109
- | Centralized multi-tenant admin console | — | ✅ |
110
- | Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
111
- | SAML / SCIM provisioning | — | ✅ |
112
- | Audit log streaming | — | ✅ |
113
- | Policy transforms (redaction, summarization) | — | ✅ |
114
- | Priority support and SLA | — | ✅ |
115
-
116
- For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <https://promptowl.ai/contextnest/>.
117
-
118
- ## What's new
119
-
120
- Release notes live in [CHANGELOG.md](./CHANGELOG.md).
121
-
122
- ## Licensing
123
-
124
- ContextNest Community Edition is **commercial software**. It is **not open source**.
125
-
126
- **You may:**
127
- - Install and run the Software on devices You own or control
128
- - Use the Software for internal business purposes, tied to a valid PromptOwl account
129
- - Make backup and archival copies
130
-
131
- **You may not:**
132
- - Redistribute, resell, rent, lease, or sublicense the Software
133
- - Offer the Software as a hosted, managed, or software-as-a-service product to third parties
134
- - Reverse engineer, decompile, or create derivative works
135
- - Use the Software to build a competing product or service
136
- - Remove copyright, trademark, or license notices
137
-
138
- Full license text: [LICENSE.md](./LICENSE.md)
139
-
140
- **For redistribution, hosted-service, OEM, or regulated-industry use,** contact **hoot@promptowl.ai** for a commercial license agreement.
141
-
142
- ## Platform terms
143
-
144
- Because the Software requires a PromptOwl account, the following terms also apply to Your use:
145
-
146
- - **End User License Agreement** — <https://promptowl.ai/eula/>
147
- - **Terms of Service** — <https://promptowl.ai/terms-of-service/>
148
- - **Privacy Policy** — <https://promptowl.ai/privacy-policy/>
149
- - **Acceptable Use Policy** — <https://promptowl.ai/acceptable-use/>
150
- - **Disclaimer** — <https://promptowl.ai/disclaimer/>
151
- - **Cookie Policy** — <https://promptowl.ai/cookies/>
152
-
153
- ## Support
154
-
155
- - **Documentation:** <https://promptowl.ai/contextnest/>
156
- - **Product questions:** <https://promptowl.ai/contact-us/>
157
- - **Support & bugs:** `hoot@promptowl.ai`
158
- - **Commercial licensing:** `hoot@promptowl.ai` (subject: *ContextNest Commercial License*)
159
-
160
- ## AI output disclaimer
161
-
162
- The Software injects content into large language models. AI output may be inaccurate, incomplete, or inappropriate for your use case. You are responsible for reviewing and validating any AI-generated content before relying on it, particularly in business-critical or regulated contexts. Do not deploy the Software in medical, legal, financial-advisory, or safety-critical environments without appropriate human oversight.
163
-
164
- ---
165
-
166
- **Copyright © 2026 Promptowl LLC.** All rights reserved.
167
- "ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
168
-
169
- Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
1
+ # ContextNest Community Edition
2
+
3
+ **Self-hosted context governance server for AI workflows.** Part of the [PromptOwl](https://promptowl.ai) platform.
4
+
5
+ > ⚠️ **Commercial Software.** ContextNest Community Edition is proprietary software licensed by Promptowl LLC. A free [PromptOwl account](https://app.promptowl.ai) is required to use it. Redistribution, hosting-as-a-service, and competitive use are prohibited. See [LICENSE.md](./LICENSE.md) for full terms.
6
+
7
+ ---
8
+
9
+ ## What it is
10
+
11
+ ContextNest Community Edition is a self-hosted server that lets you:
12
+
13
+ - Store, version, and govern markdown-based context documents ("nests")
14
+ - Import an existing folder or vault of markdown files in one step
15
+ - Export a nest as a portable bundle and re-import it on another self-hosted host
16
+ - Apply stewardship workflows — draft, pending review, approved
17
+ - Share nests with collaborators or publish them read-only to the public
18
+ - Serve approved context to AI agents via MCP or HTTP
19
+ - Sync with the PromptOwl hosted platform for multi-user collaboration
20
+
21
+ The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
22
+
23
+ ## Quickstart
24
+
25
+ ```bash
26
+ # 1. Run the community server
27
+ npx @promptowl/contextnest-community
28
+
29
+ # 2. Open the server in your browser
30
+ # http://localhost:3838
31
+ # On first boot with no license, it lands on the License Setup Page.
32
+
33
+ # 3. Paste your PromptOwl license key (pk_...) — see "License setup" below
34
+ ```
35
+
36
+ The server listens on `http://localhost:3838` by default. Without a valid license the server still runs and serves reads, but write actions return `503` until you activate. See [CONFIGURATION.md](./CONFIGURATION.md) for all environment variables (port, auth mode, storage, telemetry).
37
+
38
+ ## License setup
39
+
40
+ ContextNest Community Edition requires a PromptOwl Community License key (`pk_...`). Getting and activating one:
41
+
42
+ ### 1. Create the key (free)
43
+
44
+ 1. Sign up or log in at <https://app.promptowl.ai>
45
+ 2. Open the **Overview** menu → **Community License**
46
+ 3. Click **Create a Community License key**
47
+ 4. Copy the generated key — it starts with `pk_`
48
+
49
+ ### 2. Activate the server
50
+
51
+ Pick **one** of two ways:
52
+
53
+ **A. Browser setup page (recommended for first run)**
54
+
55
+ 1. Start the server: `npx @promptowl/contextnest-community`
56
+ 2. Open <http://localhost:3838> — with no license installed, the server boots into **setup mode** and shows the **License Setup Page**
57
+ 3. Paste your `pk_...` key and submit
58
+ 4. The server validates it against PromptOwl, writes it to your `.env`, and exits setup mode — no restart needed
59
+
60
+ **B. Environment variable (recommended for Docker / CI / scripted deploys)**
61
+
62
+ ```bash
63
+ PROMPTOWL_KEY=pk_... npx @promptowl/contextnest-community
64
+ ```
65
+
66
+ The key is read at boot. The server validates against PromptOwl on startup; if valid, it goes straight into licensed mode.
67
+
68
+ ### 3. How licensing behaves at runtime
69
+
70
+ - **Unlicensed / setup mode** — reads work; every non-GET (write) request returns `503` until a valid key is installed.
71
+ - **Live revocation** — a long-poll watcher tracks license state against PromptOwl. If your key is revoked, the server blocks writes within seconds (no restart required) and returns to setup mode.
72
+ - **Admin identity follows the license** — the admin user is whichever PromptOwl account owns the installed key, resolved live per request. Transferring the license to another account immediately promotes the new owner and demotes the old one.
73
+
74
+ For redistribution, hosted-service, OEM, or regulated-industry licensing, contact **hoot@promptowl.ai**.
75
+
76
+ ## System requirements
77
+
78
+ - **Node.js** 20.x or later
79
+ - **PromptOwl account** — free signup at <https://app.promptowl.ai/signup>
80
+ - **OS:** Windows, macOS, or Linux
81
+ - **Disk:** ~200 MB for the server, plus storage for your nests
82
+
83
+ ## What you get
84
+
85
+ | Feature | Community Edition | Enterprise |
86
+ |---|:---:|:---:|
87
+ | Self-hosted context server | ✅ | ✅ |
88
+ | SQLite or PostgreSQL / Cloud SQL backend | ✅ | ✅ |
89
+ | Configurable nest storage (external / GCS volume mount) | ✅ | ✅ |
90
+ | Markdown + YAML frontmatter vaults | ✅ | ✅ |
91
+ | Import existing folder / vault | ✅ | ✅ |
92
+ | Export nest as portable bundle (.zip) | ✅ | ✅ |
93
+ | Markdown rendering + wiki cross-linking | ✅ | ✅ |
94
+ | External-edit detection + version diff | ✅ | ✅ |
95
+ | Stewardship workflow (draft/review/approve) | ✅ | ✅ |
96
+ | Per-nest sharing + collaborators | ✅ | ✅ |
97
+ | Public read-only nests | ✅ | ✅ |
98
+ | Custom logo / branding | ✅ | ✅ |
99
+ | Admin password reset + user removal (in-platform) | ✅ | ✅ |
100
+ | Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
101
+ | Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload | ✅ | ✅ |
102
+ | Steward version revert | ✅ | ✅ |
103
+ | MCP server for AI agents | ✅ | ✅ |
104
+ | Centralized multi-tenant admin console | — | ✅ |
105
+ | Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
106
+ | SAML / SCIM provisioning | — | ✅ |
107
+ | Audit log streaming | — | ✅ |
108
+ | Policy transforms (redaction, summarization) | — | ✅ |
109
+ | Priority support and SLA | — | ✅ |
110
+
111
+ For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <https://promptowl.ai/contextnest/>.
112
+
113
+ ## What's new
114
+
115
+ Release notes live in [CHANGELOG.md](./CHANGELOG.md).
116
+
117
+ ## Licensing
118
+
119
+ ContextNest Community Edition is **commercial software**. It is **not open source**.
120
+
121
+ **You may:**
122
+ - Install and run the Software on devices You own or control
123
+ - Use the Software for internal business purposes, tied to a valid PromptOwl account
124
+ - Make backup and archival copies
125
+
126
+ **You may not:**
127
+ - Redistribute, resell, rent, lease, or sublicense the Software
128
+ - Offer the Software as a hosted, managed, or software-as-a-service product to third parties
129
+ - Reverse engineer, decompile, or create derivative works
130
+ - Use the Software to build a competing product or service
131
+ - Remove copyright, trademark, or license notices
132
+
133
+ Full license text: [LICENSE.md](./LICENSE.md)
134
+
135
+ **For redistribution, hosted-service, OEM, or regulated-industry use,** contact **hoot@promptowl.ai** for a commercial license agreement.
136
+
137
+ ## Platform terms
138
+
139
+ Because the Software requires a PromptOwl account, the following terms also apply to Your use:
140
+
141
+ - **End User License Agreement** — <https://promptowl.ai/eula/>
142
+ - **Terms of Service** — <https://promptowl.ai/terms-of-service/>
143
+ - **Privacy Policy** — <https://promptowl.ai/privacy-policy/>
144
+ - **Acceptable Use Policy** — <https://promptowl.ai/acceptable-use/>
145
+ - **Disclaimer** — <https://promptowl.ai/disclaimer/>
146
+ - **Cookie Policy** — <https://promptowl.ai/cookies/>
147
+
148
+ ## Support
149
+
150
+ - **Documentation:** <https://promptowl.ai/contextnest/>
151
+ - **Product questions:** <https://promptowl.ai/contact-us/>
152
+ - **Support & bugs:** `hoot@promptowl.ai`
153
+ - **Commercial licensing:** `hoot@promptowl.ai` (subject: *ContextNest Commercial License*)
154
+
155
+ ## AI output disclaimer
156
+
157
+ The Software injects content into large language models. AI output may be inaccurate, incomplete, or inappropriate for your use case. You are responsible for reviewing and validating any AI-generated content before relying on it, particularly in business-critical or regulated contexts. Do not deploy the Software in medical, legal, financial-advisory, or safety-critical environments without appropriate human oversight.
158
+
159
+ ---
160
+
161
+ **Copyright © 2026 Promptowl LLC.** All rights reserved.
162
+ "ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
163
+
164
+ Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
@@ -2,22 +2,24 @@ import {
2
2
  grantCoversNode,
3
3
  listUserGrants,
4
4
  resolveNodeGrant
5
- } from "./chunk-5IAKUUZF.js";
5
+ } from "./chunk-HWDPCPYT.js";
6
6
  import {
7
7
  createVersion,
8
8
  getApprovedVersion,
9
+ getApprovedVersions,
9
10
  getCurrentVersion,
10
11
  setApprovedVersion
11
- } from "./chunk-JWVZ3T35.js";
12
+ } from "./chunk-UXJSO54R.js";
12
13
  import {
13
14
  canUserAccess,
14
15
  canUserApprove,
15
16
  resolveStewardsForNode
16
- } from "./chunk-GWDOLYFJ.js";
17
+ } from "./chunk-YF4OFT2V.js";
17
18
  import {
18
19
  buildDocContext,
19
20
  buildTitleMap,
20
21
  describeNode,
22
+ engineApi,
21
23
  engineCache,
22
24
  insertOrReplace,
23
25
  isPublicReader,
@@ -25,10 +27,12 @@ import {
25
27
  nestName,
26
28
  notifyEmailForNest,
27
29
  nowExpr,
30
+ opContext,
28
31
  prettyFolderPath,
32
+ resolveNestPermission,
29
33
  sendEmailToRecipient,
30
34
  titleForNode
31
- } from "./chunk-7BOG3S5H.js";
35
+ } from "./chunk-SNFGU5Q7.js";
32
36
  import {
33
37
  ConflictError,
34
38
  NotFoundError,
@@ -37,7 +41,7 @@ import {
37
41
  import {
38
42
  config,
39
43
  getDb
40
- } from "./chunk-BCFKLY4H.js";
44
+ } from "./chunk-JMJLNEXE.js";
41
45
 
42
46
  // src/governance/review-service.ts
43
47
  import { v4 as uuid3 } from "uuid";
@@ -222,18 +226,31 @@ async function canReadNode(nestId, nodeId, userId, userEmail) {
222
226
  if ((await canUserAccess(nestId, nodeId, userEmail)).allowed) return true;
223
227
  return await resolveNodeGrant(nestId, userId, nodeId) !== null;
224
228
  }
225
- async function filterAccessible(nestId, userId, userEmail, nodes) {
229
+ async function canRequestDeletion(params) {
230
+ const { nestId, userId, userEmail, nodeId } = params;
231
+ const permission = params.permission ?? await resolveNestPermission(nestId, userId);
232
+ if (permission === "owner" || permission === "admin" || permission === "write") {
233
+ return true;
234
+ }
235
+ const resolved = await resolveStewardsForNode(nestId, nodeId);
236
+ const isEditorSteward = resolved.some(
237
+ (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "editor" && (nodeId !== "" || r.steward.scope === "nest")
238
+ );
239
+ if (isEditorSteward) return true;
240
+ if (nodeId === "") return false;
241
+ return await resolveNodeGrant(nestId, userId, nodeId) === "write";
242
+ }
243
+ async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersions) {
226
244
  if (await isPublicReader(nestId, userId)) {
227
- const filtered = [];
228
- for (const n of nodes) {
229
- if (await getApprovedVersion(nestId, n.id) !== null) {
230
- filtered.push(n);
231
- }
232
- }
233
- return filtered;
245
+ const approved = approvedVersions ?? await getApprovedVersions(nestId);
246
+ return nodes.filter((n) => approved.has(n.id));
234
247
  }
235
- if (!await isStewardshipEnabled(nestId)) return nodes;
236
248
  const grants = await listUserGrants(nestId, userId);
249
+ if (!await isStewardshipEnabled(nestId)) {
250
+ if (grants.length === 0) return nodes;
251
+ if (await resolveNestPermission(nestId, userId) !== "none") return nodes;
252
+ return nodes.filter((n) => grantCoversNode(grants, n.id));
253
+ }
237
254
  const accessible = [];
238
255
  for (const n of nodes) {
239
256
  if ((await canUserAccess(nestId, n.id, userEmail)).allowed || grantCoversNode(grants, n.id)) {
@@ -834,39 +851,6 @@ function withNodeWriteLock(key, fn) {
834
851
  return run;
835
852
  }
836
853
 
837
- // src/governance/safe-publish.ts
838
- import {
839
- publishDocument,
840
- serializeDocument
841
- } from "@promptowl/contextnest-engine";
842
- async function safePublishDocument(storage, docId, options) {
843
- const node = await storage.readDocument(docId);
844
- const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
845
- const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
846
- await storage.writeDocument(docId, serializeDocument(cleanedNode));
847
- return publishDocument(storage, docId, options);
848
- }
849
- function serializeNodeSafe(node) {
850
- return serializeDocument({
851
- ...node,
852
- frontmatter: stripUndefinedDeep(node.frontmatter)
853
- });
854
- }
855
- function stripUndefinedDeep(value) {
856
- if (Array.isArray(value)) {
857
- return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
858
- }
859
- if (value && typeof value === "object") {
860
- const out = {};
861
- for (const [k, v] of Object.entries(value)) {
862
- if (v === void 0) continue;
863
- out[k] = stripUndefinedDeep(v);
864
- }
865
- return out;
866
- }
867
- return value;
868
- }
869
-
870
854
  // src/governance/review-service.ts
871
855
  async function submitForReview(params) {
872
856
  const db = getDb();
@@ -1032,13 +1016,14 @@ async function approve(params) {
1032
1016
  );
1033
1017
  try {
1034
1018
  const { storage } = await engineCache.get(params.nestId);
1035
- const result = await safePublishDocument(storage, params.nodeId, {
1036
- editedBy: params.approvedBy,
1037
- note: params.note || `Approved review request ${pending.id}`
1038
- });
1039
- const engineVersion = result.versionEntry.version;
1019
+ const note = params.note || `Approved review request ${pending.id}`;
1020
+ const { version: engineVersion } = await engineApi.run(
1021
+ "context_publish",
1022
+ { id: params.nodeId, note },
1023
+ await opContext(params.nestId, params.approvedBy)
1024
+ );
1040
1025
  if (engineVersion !== version) {
1041
- const node = result.node;
1026
+ const node = await storage.readDocument(params.nodeId);
1042
1027
  const tags = node.frontmatter.tags || [];
1043
1028
  await createVersion({
1044
1029
  nestId: params.nestId,
@@ -1048,7 +1033,7 @@ async function approve(params) {
1048
1033
  author: params.approvedBy,
1049
1034
  status: "published",
1050
1035
  tags,
1051
- changeNote: params.note || `Approved review request ${pending.id}`
1036
+ changeNote: note
1052
1037
  });
1053
1038
  await setApprovedVersion(
1054
1039
  params.nestId,
@@ -1369,7 +1354,7 @@ async function notifyDeletionRequested(request, baseUrl) {
1369
1354
  async function notifyDeletionResolved(params) {
1370
1355
  try {
1371
1356
  const { outcome, resolvedBy, note } = params;
1372
- const verb = outcome === "deleted" ? "deleted" : "declined the deletion of";
1357
+ const verb = outcome === "deleted" ? "deleted" : "rejected the deletion of";
1373
1358
  const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
1374
1359
  const recipient = params.requestedBy.toLowerCase();
1375
1360
  const canInbox = !(params.targetType === "nest" && outcome === "deleted");
@@ -1379,7 +1364,7 @@ async function notifyDeletionResolved(params) {
1379
1364
  [recipient],
1380
1365
  `deletion_${outcome}`,
1381
1366
  params.requestId,
1382
- outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was declined by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
1367
+ outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
1383
1368
  );
1384
1369
  }
1385
1370
  void dispatchEvent({
@@ -1511,6 +1496,15 @@ async function listDeletionRequests(params) {
1511
1496
  async function dropDeletionRequest(id) {
1512
1497
  await getDb().run("DELETE FROM deletion_requests WHERE id = ?", [id]);
1513
1498
  }
1499
+ async function withdrawDeletionRequest(params) {
1500
+ const db = getDb();
1501
+ const pending = await getDeletionRequest(params.id);
1502
+ if (!pending || pending.nestId !== params.nestId || pending.status !== "pending" || pending.requestedBy.toLowerCase() !== params.withdrawnBy.toLowerCase()) {
1503
+ return null;
1504
+ }
1505
+ await db.run("DELETE FROM deletion_requests WHERE id = ?", [params.id]);
1506
+ return pending;
1507
+ }
1514
1508
  async function declineDeletion(params) {
1515
1509
  const db = getDb();
1516
1510
  const pending = await getDeletionRequest(params.id);
@@ -1582,9 +1576,6 @@ function rowToReviewRequest(row) {
1582
1576
  }
1583
1577
 
1584
1578
  export {
1585
- safePublishDocument,
1586
- serializeNodeSafe,
1587
- stripUndefinedDeep,
1588
1579
  listWatchers,
1589
1580
  addWatcher,
1590
1581
  removeWatcher,
@@ -1593,6 +1584,7 @@ export {
1593
1584
  markNotificationsRead,
1594
1585
  resolveCallerEmail,
1595
1586
  canReadNode,
1587
+ canRequestDeletion,
1596
1588
  filterAccessible,
1597
1589
  safeJson,
1598
1590
  requireWorkflowPlane,
@@ -1623,5 +1615,6 @@ export {
1623
1615
  getDeletionRequest,
1624
1616
  listDeletionRequests,
1625
1617
  dropDeletionRequest,
1618
+ withdrawDeletionRequest,
1626
1619
  declineDeletion
1627
1620
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GUNJTORH.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-BCFKLY4H.js";
6
+ } from "./chunk-JMJLNEXE.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -22,14 +22,6 @@ async function resolveNodeGrant(nestId, userId, nodeId) {
22
22
  }
23
23
  return best;
24
24
  }
25
- async function hasAnyGrant(nestId, userId) {
26
- if (!userId) return false;
27
- const row = await getDb().get(
28
- "SELECT 1 AS x FROM grants WHERE nest_id = ? AND user_id = ? LIMIT 1",
29
- [nestId, userId]
30
- );
31
- return !!row;
32
- }
33
25
  async function listUserGrants(nestId, userId) {
34
26
  if (!userId) return [];
35
27
  return await getDb().all(
@@ -42,6 +34,15 @@ function grantCoversNode(grants, nodeId) {
42
34
  (g) => nodeId === g.target || nodeId.startsWith(g.target + "/")
43
35
  );
44
36
  }
37
+ function grantRoleForNode(grants, nodeId) {
38
+ let best = null;
39
+ for (const g of grants) {
40
+ if (nodeId === g.target || nodeId.startsWith(g.target + "/")) {
41
+ if (!best || rank(g.role) > rank(best)) best = g.role;
42
+ }
43
+ }
44
+ return best;
45
+ }
45
46
  async function createGrant(params) {
46
47
  const { nestId, targetType, target, userId, role, grantedBy } = params;
47
48
  if (!["document", "folder"].includes(targetType)) {
@@ -94,9 +95,9 @@ async function deleteGrant(nestId, id) {
94
95
 
95
96
  export {
96
97
  resolveNodeGrant,
97
- hasAnyGrant,
98
98
  listUserGrants,
99
99
  grantCoversNode,
100
+ grantRoleForNode,
100
101
  createGrant,
101
102
  listGrants,
102
103
  deleteGrant
@@ -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.