@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/CONFIGURATION.md +415 -414
- package/LICENSE.md +142 -142
- package/README.md +164 -169
- package/dist/{chunk-C4U75IUR.js → chunk-4YGJSUJC.js} +52 -59
- package/dist/{chunk-5IAKUUZF.js → chunk-HWDPCPYT.js} +11 -10
- package/dist/{chunk-BCFKLY4H.js → chunk-JMJLNEXE.js} +24 -0
- package/dist/{chunk-7BOG3S5H.js → chunk-SNFGU5Q7.js} +244 -162
- package/dist/{chunk-JWVZ3T35.js → chunk-UXJSO54R.js} +21 -2
- package/dist/{chunk-GWDOLYFJ.js → chunk-YF4OFT2V.js} +2 -2
- package/dist/{client-NFUHPLLM.js → client-OBS5HOTA.js} +1 -1
- package/dist/{grants-service-O4AO326A.js → grants-service-4NXPBYWD.js} +4 -4
- package/dist/index.js +1160 -544
- package/dist/{review-service-JUAFYB73.js → review-service-3CCKDO5N.js} +10 -8
- package/dist/{stewardship-service-FHJ2RBKN.js → stewardship-service-OHTTZFND.js} +3 -3
- package/dist/{version-service-ECYCBVSE.js → version-service-EFZGGL3Y.js} +7 -3
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-D1299AIf.css +1 -0
- package/dist/web3/assets/index-W6tEA3RK.js +1046 -0
- package/dist/web3/index.html +14 -14
- package/package.json +165 -164
- package/dist/web3/assets/index-BaMJ4tEz.css +0 -1
- package/dist/web3/assets/index-D950G4hR.js +0 -1052
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
**You may:**
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
- **
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- **
|
|
151
|
-
- **
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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-
|
|
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-
|
|
12
|
+
} from "./chunk-UXJSO54R.js";
|
|
12
13
|
import {
|
|
13
14
|
canUserAccess,
|
|
14
15
|
canUserApprove,
|
|
15
16
|
resolveStewardsForNode
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
228
|
-
|
|
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
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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 =
|
|
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:
|
|
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" : "
|
|
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
|
|
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-
|
|
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.
|