@promptowl/contextnest-community 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CONFIGURATION.md +415 -415
  2. package/LICENSE.md +142 -142
  3. package/README.md +168 -169
  4. package/dist/{chunk-D6HICFVW.js → chunk-34UXRZXD.js} +59 -14
  5. package/dist/chunk-F66VCBLA.js +136 -0
  6. package/dist/{chunk-L7UCMGWZ.js → chunk-FUUJG6XP.js} +641 -285
  7. package/dist/{chunk-QJVRZT6M.js → chunk-HGN45I5P.js} +59 -21
  8. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  9. package/dist/chunk-OOBZG6BW.js +379 -0
  10. package/dist/{chunk-OTGZQZKK.js → chunk-QUZXRLUA.js} +11 -10
  11. package/dist/{chunk-CLKZZZS2.js → chunk-TQACHVVA.js} +35 -4
  12. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  13. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  14. package/dist/engine-34E3YNX7.js +14 -0
  15. package/dist/external-edit-service-6Q6SHEOF.js +33 -0
  16. package/dist/{grants-service-I535QVTW.js → grants-service-ME7HIVHU.js} +5 -5
  17. package/dist/index.js +700 -526
  18. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  19. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  20. package/dist/{review-service-V2GR6VM3.js → review-service-H2M5ACPQ.js} +11 -9
  21. package/dist/{stewardship-service-LTREGVSJ.js → stewardship-service-U4CKL7Y5.js} +8 -4
  22. package/dist/{version-service-V6K5SZ3P.js → version-service-JWEN5UUW.js} +6 -4
  23. package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
  24. package/dist/web3/assets/index-DONv5_Ni.css +1 -0
  25. package/dist/web3/assets/index-jSGrnboh.js +1065 -0
  26. package/dist/web3/index.html +14 -14
  27. package/package.json +165 -165
  28. package/dist/chunk-XRK6SQSC.js +0 -58
  29. package/dist/web3/assets/index-DnQARUL3.css +0 -1
  30. package/dist/web3/assets/index-_m7WyozM.js +0 -1051
package/README.md CHANGED
@@ -1,169 +1,168 @@
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 editortables, 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"), filed in folders you can reorganize as the vault grows
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 — connect one by pasting a generated setup prompt or downloading a ready `.env`
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
+ | Folder organization nested folders, move documents, lazy folder tree | ✅ | ✅ |
103
+ | Scales to large vaults — nest listings served from a document index, not a disk crawl | ✅ | ✅ |
104
+ | Steward version revert | ✅ | ✅ |
105
+ | MCP server for AI agents | ✅ | ✅ |
106
+ | One-shot agent setup generated connect prompt + `.env` download | ✅ | ✅ |
107
+ | Several API keys per account — one credential per client, rotate one at a time | ✅ | ✅ |
108
+ | Centralized multi-tenant admin console | | ✅ |
109
+ | Single sign-on (OIDC Entra ID, Google, Okta, Keycloak) | | ✅ |
110
+ | SAML / SCIM provisioning | | ✅ |
111
+ | Audit log streaming | — | ✅ |
112
+ | Policy transforms (redaction, summarization) | — | ✅ |
113
+ | Priority support and SLA | — | ✅ |
114
+
115
+ For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <https://promptowl.ai/contextnest/>.
116
+
117
+ ## What's new
118
+
119
+ Release notes live in [CHANGELOG.md](./CHANGELOG.md).
120
+
121
+ ## Licensing
122
+
123
+ ContextNest Community Edition is **commercial software**. It is **not open source**.
124
+
125
+ **You may:**
126
+ - Install and run the Software on devices You own or control
127
+ - Use the Software for internal business purposes, tied to a valid PromptOwl account
128
+ - Make backup and archival copies
129
+
130
+ **You may not:**
131
+ - Redistribute, resell, rent, lease, or sublicense the Software
132
+ - Offer the Software as a hosted, managed, or software-as-a-service product to third parties
133
+ - Reverse engineer, decompile, or create derivative works
134
+ - Use the Software to build a competing product or service
135
+ - Remove copyright, trademark, or license notices
136
+
137
+ Full license text: [LICENSE.md](./LICENSE.md)
138
+
139
+ **For redistribution, hosted-service, OEM, or regulated-industry use,** contact **hoot@promptowl.ai** for a commercial license agreement.
140
+
141
+ ## Platform terms
142
+
143
+ Because the Software requires a PromptOwl account, the following terms also apply to Your use:
144
+
145
+ - **End User License Agreement** — <https://promptowl.ai/eula/>
146
+ - **Terms of Service** — <https://promptowl.ai/terms-of-service/>
147
+ - **Privacy Policy** — <https://promptowl.ai/privacy-policy/>
148
+ - **Acceptable Use Policy** — <https://promptowl.ai/acceptable-use/>
149
+ - **Disclaimer** — <https://promptowl.ai/disclaimer/>
150
+ - **Cookie Policy** — <https://promptowl.ai/cookies/>
151
+
152
+ ## Support
153
+
154
+ - **Documentation:** <https://promptowl.ai/contextnest/>
155
+ - **Product questions:** <https://promptowl.ai/contact-us/>
156
+ - **Support & bugs:** `hoot@promptowl.ai`
157
+ - **Commercial licensing:** `hoot@promptowl.ai` (subject: *ContextNest Commercial License*)
158
+
159
+ ## AI output disclaimer
160
+
161
+ 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.
162
+
163
+ ---
164
+
165
+ **Copyright © 2026 Promptowl LLC.** All rights reserved.
166
+ "ContextNest" and "PromptOwl" are trademarks of Promptowl LLC.
167
+
168
+ Promptowl LLC · 3060 Mercer University Dr Ste 110 · Atlanta, GA 30341 · USA
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-L7UCMGWZ.js";
5
+ } from "./chunk-FUUJG6XP.js";
6
6
  import {
7
7
  getDb
8
- } from "./chunk-JMJLNEXE.js";
8
+ } from "./chunk-HYSTFMFG.js";
9
9
 
10
10
  // src/governance/version-service.ts
11
11
  import { createHash } from "crypto";
@@ -178,32 +178,76 @@ async function getNodeTags(nestId, nodeId) {
178
178
  return [];
179
179
  }
180
180
  }
181
+ function decideDisplayStatus(hasPendingReview, current, approvedVersion) {
182
+ if (hasPendingReview) return "pending_review";
183
+ if (!current) return "draft";
184
+ if (current.status === "published" || current.status === "approved") {
185
+ if (approvedVersion === current.version) {
186
+ return current.status === "published" ? "published" : "approved";
187
+ }
188
+ return "draft";
189
+ }
190
+ if (current.status === "rejected") return "rejected";
191
+ return "draft";
192
+ }
181
193
  async function getDisplayStatus(nestId, nodeId) {
182
194
  const db = getDb();
183
195
  const pending = await db.get(
184
196
  "SELECT 1 FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending' LIMIT 1",
185
197
  [nestId, nodeId]
186
198
  );
187
- if (pending) return "pending_review";
188
199
  const current = await db.get(
189
200
  `SELECT version, status FROM node_versions
190
201
  WHERE nest_id = ? AND node_id = ?
191
202
  ORDER BY version DESC LIMIT 1`,
192
203
  [nestId, nodeId]
193
204
  );
194
- if (!current) return "draft";
195
- if (current.status === "published" || current.status === "approved") {
196
- const approved = await db.get(
197
- "SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?",
198
- [nestId, nodeId]
205
+ const approved = current && (current.status === "published" || current.status === "approved") ? await db.get(
206
+ "SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?",
207
+ [nestId, nodeId]
208
+ ) : void 0;
209
+ return decideDisplayStatus(!!pending, current, approved?.approved_version);
210
+ }
211
+ async function getDisplayStatuses(nestId) {
212
+ const db = getDb();
213
+ const [pendingRows, currentRows, approvedRows] = await Promise.all([
214
+ db.all(
215
+ "SELECT DISTINCT node_id FROM review_requests WHERE nest_id = ? AND status = 'pending'",
216
+ [nestId]
217
+ ),
218
+ // The latest version per node, via a grouped self-join — portable across
219
+ // the SQLite and Postgres adapters (no window functions, no row values).
220
+ db.all(
221
+ `SELECT nv.node_id, nv.version, nv.status
222
+ FROM node_versions nv
223
+ JOIN (SELECT node_id, MAX(version) AS max_version
224
+ FROM node_versions WHERE nest_id = ? GROUP BY node_id) latest
225
+ ON latest.node_id = nv.node_id AND latest.max_version = nv.version
226
+ WHERE nv.nest_id = ?`,
227
+ [nestId, nestId]
228
+ ),
229
+ db.all(
230
+ "SELECT node_id, approved_version FROM approved_versions WHERE nest_id = ?",
231
+ [nestId]
232
+ )
233
+ ]);
234
+ const pending = new Set(pendingRows.map((r) => r.node_id));
235
+ const approved = new Map(
236
+ approvedRows.map((r) => [r.node_id, r.approved_version])
237
+ );
238
+ const current = new Map(currentRows.map((r) => [r.node_id, r]));
239
+ const statuses = /* @__PURE__ */ new Map();
240
+ for (const nodeId of /* @__PURE__ */ new Set([...pending, ...current.keys()])) {
241
+ statuses.set(
242
+ nodeId,
243
+ decideDisplayStatus(
244
+ pending.has(nodeId),
245
+ current.get(nodeId),
246
+ approved.get(nodeId)
247
+ )
199
248
  );
200
- if (approved?.approved_version === current.version) {
201
- return current.status === "published" ? "published" : "approved";
202
- }
203
- return "draft";
204
249
  }
205
- if (current.status === "rejected") return "rejected";
206
- return "draft";
250
+ return statuses;
207
251
  }
208
252
  async function listMyDrafts(params) {
209
253
  if (params.nestIds.length === 0) return { drafts: [], total: 0 };
@@ -286,5 +330,6 @@ export {
286
330
  checkConflict,
287
331
  getNodeTags,
288
332
  getDisplayStatus,
333
+ getDisplayStatuses,
289
334
  listMyDrafts
290
335
  };
@@ -0,0 +1,136 @@
1
+ // src/auth/keys.ts
2
+ import { randomBytes, createHash, timingSafeEqual } from "crypto";
3
+ import bcrypt from "bcryptjs";
4
+ var KEY_PREFIX = "cnst_";
5
+ var BCRYPT_ROUNDS = process.env.VITEST ? 4 : 12;
6
+ function generateApiKey() {
7
+ return KEY_PREFIX + randomBytes(32).toString("hex");
8
+ }
9
+ function hashApiKey(key) {
10
+ return createHash("sha256").update(key).digest("hex");
11
+ }
12
+ function getKeyPrefix(key) {
13
+ return key.slice(0, 12) + "...";
14
+ }
15
+ function parseBearerToken(header) {
16
+ if (!header) return null;
17
+ const BEARER = "Bearer ";
18
+ if (!header.startsWith(BEARER)) return null;
19
+ const token = header.slice(BEARER.length);
20
+ if (!token.startsWith(KEY_PREFIX)) return null;
21
+ if (token.length < KEY_PREFIX.length + 32) return null;
22
+ return token;
23
+ }
24
+ async function hashPassword(password) {
25
+ return bcrypt.hash(password, BCRYPT_ROUNDS);
26
+ }
27
+ async function verifyPassword(password, stored) {
28
+ if (isBcrypt(stored)) {
29
+ return { ok: await bcrypt.compare(password, stored), needsRehash: false };
30
+ }
31
+ const [salt, hash] = stored.split(":");
32
+ if (!salt || !hash) return { ok: false, needsRehash: false };
33
+ const attempt = createHash("sha256").update(salt + password).digest("hex");
34
+ return {
35
+ ok: timingSafeEqualHex(hash, attempt),
36
+ needsRehash: true
37
+ };
38
+ }
39
+ function isBcrypt(stored) {
40
+ return stored.startsWith("$2a$") || stored.startsWith("$2b$") || stored.startsWith("$2y$");
41
+ }
42
+ function timingSafeEqualHex(a, b) {
43
+ if (a.length !== b.length) return false;
44
+ try {
45
+ return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ var MAX_API_KEYS_PER_USER = 20;
51
+ var MAX_LABEL_LENGTH = 60;
52
+ async function verifyKeyRequest(db, userId, opts) {
53
+ const label = normalizeKeyLabel(opts.label);
54
+ if (label === INVALID_LABEL) {
55
+ return {
56
+ ok: false,
57
+ status: 400,
58
+ error: `Label must be a string of at most ${MAX_LABEL_LENGTH} characters.`
59
+ };
60
+ }
61
+ if (opts.action === "fetch") return { ok: true, label, key: null };
62
+ const rows = await db.all(
63
+ "SELECT id, key_prefix, nest_id, label, created_at, last_used_at FROM api_keys WHERE user_id = ? ORDER BY created_at ASC, id ASC",
64
+ [userId]
65
+ );
66
+ if (opts.action === "generate") {
67
+ if (rows.length >= MAX_API_KEYS_PER_USER) {
68
+ return {
69
+ ok: false,
70
+ status: 409,
71
+ error: `You already have ${MAX_API_KEYS_PER_USER} API keys. Revoke one before creating another.`
72
+ };
73
+ }
74
+ if (!label) {
75
+ return {
76
+ ok: false,
77
+ status: 400,
78
+ error: "Name this key so you can tell it apart later."
79
+ };
80
+ }
81
+ if (rows.some((r) => r.label?.trim().toLowerCase() === label.toLowerCase())) {
82
+ return {
83
+ ok: false,
84
+ status: 409,
85
+ error: `You already have a key named "${label}". Pick a different name.`
86
+ };
87
+ }
88
+ return { ok: true, label, key: null };
89
+ }
90
+ const renameCollides = (targetId) => !!label && rows.some(
91
+ (r) => r.id !== targetId && r.label?.trim().toLowerCase() === label.toLowerCase()
92
+ );
93
+ const collision = {
94
+ ok: false,
95
+ status: 409,
96
+ error: `You already have a key named "${label}". Pick a different name.`
97
+ };
98
+ if (opts.keyId) {
99
+ const match = rows.find((r) => r.id === opts.keyId);
100
+ if (!match) return { ok: false, status: 404, error: "Key not found" };
101
+ if (renameCollides(match.id)) return collision;
102
+ return { ok: true, label, key: match };
103
+ }
104
+ if (rows.length === 1) {
105
+ if (renameCollides(rows[0].id)) return collision;
106
+ return { ok: true, label, key: rows[0] };
107
+ }
108
+ if (rows.length === 0) {
109
+ return { ok: false, status: 404, error: "No API key to rotate." };
110
+ }
111
+ return {
112
+ ok: false,
113
+ status: 400,
114
+ error: "You have several API keys \u2014 say which one to rotate (key_id)."
115
+ };
116
+ }
117
+ var INVALID_LABEL = /* @__PURE__ */ Symbol("invalid-label");
118
+ function normalizeKeyLabel(raw) {
119
+ if (raw === void 0 || raw === null || raw === "") return null;
120
+ if (typeof raw !== "string") return INVALID_LABEL;
121
+ const trimmed = raw.trim();
122
+ if (!trimmed) return null;
123
+ if (trimmed.length > MAX_LABEL_LENGTH) return INVALID_LABEL;
124
+ return trimmed;
125
+ }
126
+
127
+ export {
128
+ generateApiKey,
129
+ hashApiKey,
130
+ getKeyPrefix,
131
+ parseBearerToken,
132
+ hashPassword,
133
+ verifyPassword,
134
+ MAX_API_KEYS_PER_USER,
135
+ verifyKeyRequest
136
+ };