@promptowl/contextnest-community 1.17.0 → 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 (27) hide show
  1. package/README.md +6 -2
  2. package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
  3. package/dist/chunk-F66VCBLA.js +136 -0
  4. package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
  5. package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
  6. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  7. package/dist/chunk-OOBZG6BW.js +379 -0
  8. package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
  9. package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
  10. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  11. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  12. package/dist/engine-34E3YNX7.js +14 -0
  13. package/dist/external-edit-service-6Q6SHEOF.js +33 -0
  14. package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
  15. package/dist/index.js +496 -470
  16. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  17. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  18. package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
  19. package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
  20. package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
  21. package/dist/web3/assets/index-DONv5_Ni.css +1 -0
  22. package/dist/web3/assets/index-jSGrnboh.js +1065 -0
  23. package/dist/web3/index.html +2 -2
  24. package/package.json +3 -3
  25. package/dist/chunk-XRK6SQSC.js +0 -58
  26. package/dist/web3/assets/index-D1299AIf.css +0 -1
  27. package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
package/README.md CHANGED
@@ -10,12 +10,12 @@
10
10
 
11
11
  ContextNest Community Edition is a self-hosted server that lets you:
12
12
 
13
- - Store, version, and govern markdown-based context documents ("nests")
13
+ - Store, version, and govern markdown-based context documents ("nests"), filed in folders you can reorganize as the vault grows
14
14
  - Import an existing folder or vault of markdown files in one step
15
15
  - Export a nest as a portable bundle and re-import it on another self-hosted host
16
16
  - Apply stewardship workflows — draft, pending review, approved
17
17
  - Share nests with collaborators or publish them read-only to the public
18
- - Serve approved context to AI agents via MCP or HTTP
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
19
  - Sync with the PromptOwl hosted platform for multi-user collaboration
20
20
 
21
21
  The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
@@ -99,8 +99,12 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
99
99
  | Admin password reset + user removal (in-platform) | ✅ | ✅ |
100
100
  | Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
101
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 | ✅ | ✅ |
102
104
  | Steward version revert | ✅ | ✅ |
103
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 | ✅ | ✅ |
104
108
  | Centralized multi-tenant admin console | — | ✅ |
105
109
  | Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
106
110
  | SAML / SCIM provisioning | — | ✅ |
@@ -2,10 +2,10 @@ import {
2
2
  buildTitleMap,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-SNFGU5Q7.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
+ };