@promptowl/contextnest-community 1.0.1 → 1.2.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.
@@ -1,498 +0,0 @@
1
- import {
2
- config,
3
- getDb
4
- } from "./chunk-KQCWNHDM.js";
5
-
6
- // src/governance/stewardship-service.ts
7
- import { v4 as uuid } from "uuid";
8
-
9
- // src/governance/access-service.ts
10
- import { readFileSync, existsSync } from "fs";
11
- import { join } from "path";
12
- var accessConfig = null;
13
- function loadAccessConfig() {
14
- const candidates = [
15
- join(config.DATA_ROOT, "access.yaml"),
16
- join(config.DATA_ROOT, "access.yml")
17
- ];
18
- for (const path of candidates) {
19
- if (existsSync(path)) {
20
- const content = readFileSync(path, "utf-8");
21
- accessConfig = parseAccessYaml(content);
22
- return accessConfig;
23
- }
24
- }
25
- accessConfig = null;
26
- return null;
27
- }
28
- function getAccessConfig() {
29
- return accessConfig;
30
- }
31
- function isSuperAdmin(email) {
32
- if (!accessConfig?.super_admins) return false;
33
- return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
34
- }
35
- function parseAccessYaml(content) {
36
- const result = {};
37
- const lines = content.split("\n");
38
- let currentSection = null;
39
- let currentGroup = null;
40
- let inMembers = false;
41
- for (const rawLine of lines) {
42
- const line = rawLine.trimEnd();
43
- if (!line || line.startsWith("#")) continue;
44
- if (!line.startsWith(" ") && !line.startsWith(" ")) {
45
- const match = line.match(/^(\w+):\s*(.*)?$/);
46
- if (!match) continue;
47
- const key = match[1];
48
- const value = match[2]?.trim();
49
- if (key === "mode") {
50
- result.mode = value;
51
- currentSection = null;
52
- } else if (key === "allowed_users") {
53
- currentSection = "allowed_users";
54
- result.allowed_users = [];
55
- } else if (key === "groups") {
56
- currentSection = "groups";
57
- result.groups = {};
58
- } else if (key === "super_admins") {
59
- currentSection = "super_admins";
60
- result.super_admins = [];
61
- }
62
- currentGroup = null;
63
- inMembers = false;
64
- continue;
65
- }
66
- const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
67
- if (currentSection === "allowed_users" && listMatch) {
68
- result.allowed_users.push(listMatch[1].trim());
69
- continue;
70
- }
71
- if (currentSection === "super_admins" && listMatch) {
72
- result.super_admins.push(listMatch[1].trim());
73
- continue;
74
- }
75
- if (currentSection === "groups") {
76
- const groupMatch = line.match(/^ (\w+):$/);
77
- if (groupMatch) {
78
- currentGroup = groupMatch[1];
79
- result.groups[currentGroup] = { members: [], default_permission: "read" };
80
- inMembers = false;
81
- continue;
82
- }
83
- if (currentGroup) {
84
- const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
85
- if (propMatch) {
86
- const prop = propMatch[1];
87
- const val = propMatch[2]?.trim();
88
- if (prop === "default_permission" && val) {
89
- result.groups[currentGroup].default_permission = val;
90
- }
91
- if (prop === "members") {
92
- inMembers = true;
93
- }
94
- continue;
95
- }
96
- if (inMembers && listMatch) {
97
- result.groups[currentGroup].members.push(listMatch[1].trim());
98
- }
99
- }
100
- }
101
- }
102
- return result;
103
- }
104
-
105
- // src/governance/stewardship-service.ts
106
- function assignSteward(data) {
107
- const db = getDb();
108
- const id = uuid();
109
- db.prepare(
110
- `INSERT INTO stewards
111
- (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
112
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
113
- ).run(
114
- id,
115
- data.nestId,
116
- data.scope,
117
- data.nodePattern || null,
118
- data.tagName || null,
119
- data.userEmail,
120
- data.userId || null,
121
- data.role,
122
- data.assignedBy,
123
- data.isActive ? 1 : 0
124
- );
125
- return { ...data, id };
126
- }
127
- function removeSteward(id) {
128
- const db = getDb();
129
- const remove = db.transaction((stewardId) => {
130
- const row = db.prepare("SELECT nest_id, user_email FROM stewards WHERE id = ?").get(stewardId);
131
- db.prepare("DELETE FROM stewards WHERE id = ?").run(stewardId);
132
- if (!row) return;
133
- const remaining = db.prepare(
134
- "SELECT 1 FROM stewards WHERE nest_id = ? AND LOWER(user_email) = LOWER(?) AND is_active = 1 LIMIT 1"
135
- ).get(row.nest_id, row.user_email);
136
- if (remaining) return;
137
- const user = db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(row.user_email);
138
- if (!user) return;
139
- db.prepare(
140
- "DELETE FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
141
- ).run(row.nest_id, user.id);
142
- });
143
- remove(id);
144
- }
145
- function getSteward(id) {
146
- const db = getDb();
147
- const row = db.prepare("SELECT * FROM stewards WHERE id = ?").get(id);
148
- return row ? rowToSteward(row) : null;
149
- }
150
- function getStewardsForNest(nestId) {
151
- const db = getDb();
152
- const rows = db.prepare("SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1").all(nestId);
153
- return rows.map(rowToSteward);
154
- }
155
- function getStewardsForScope(params) {
156
- const db = getDb();
157
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
158
- const args = [params.nestId];
159
- if (params.scope) {
160
- sql += " AND scope = ?";
161
- args.push(params.scope);
162
- }
163
- if (params.scopeTarget) {
164
- sql += " AND (node_pattern = ? OR tag_name = ?)";
165
- args.push(params.scopeTarget, params.scopeTarget);
166
- }
167
- return db.prepare(sql).all(...args).map(rowToSteward);
168
- }
169
- function listStewards(params) {
170
- const db = getDb();
171
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
172
- const args = [params.nestId];
173
- if (params.scope) {
174
- sql += " AND scope = ?";
175
- args.push(params.scope);
176
- }
177
- if (params.search) {
178
- sql += " AND (user_email LIKE ? OR tag_name LIKE ? OR node_pattern LIKE ?)";
179
- const like = `%${params.search.toLowerCase()}%`;
180
- args.push(like, like, like);
181
- }
182
- sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
183
- return db.prepare(sql).all(...args).map(rowToSteward);
184
- }
185
- function rolePermission(role) {
186
- return role === "editor" ? "write" : "read";
187
- }
188
- async function ensureCollaborator(nestId, email, permission, grantedBy) {
189
- const db = getDb();
190
- let userRow = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
191
- if (!userRow) {
192
- const { hashPassword } = await import("./keys-YV33AJK3.js");
193
- const newId = uuid();
194
- db.prepare(
195
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
196
- ).run(newId, email, null, await hashPassword(uuid()));
197
- userRow = { id: newId };
198
- }
199
- const nestRow = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
200
- if (nestRow && nestRow.user_id === userRow.id) return;
201
- const existing = db.prepare(
202
- "SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
203
- ).get(nestId, userRow.id);
204
- if (existing) return;
205
- const granterRow = db.prepare("SELECT id FROM users WHERE email = ?").get(grantedBy);
206
- const granterId = granterRow?.id ?? nestRow?.user_id;
207
- if (!granterId) return;
208
- db.prepare(
209
- "INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)"
210
- ).run(uuid(), nestId, userRow.id, permission, granterId);
211
- }
212
- async function createStewardRecord(params) {
213
- if (params.users.length === 0) {
214
- throw new Error("At least one user is required");
215
- }
216
- let nodePattern;
217
- let tagName;
218
- switch (params.scope) {
219
- case "document":
220
- if (!params.documentId) throw new Error("documentId required for document scope");
221
- nodePattern = params.documentId;
222
- break;
223
- case "tag":
224
- if (!params.tagName) throw new Error("tagName required for tag scope");
225
- tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
226
- break;
227
- case "nest":
228
- break;
229
- }
230
- const db = getDb();
231
- const results = [];
232
- for (const user of params.users) {
233
- const email = user.email.trim().toLowerCase();
234
- if (!email) continue;
235
- const existing = db.prepare(
236
- `SELECT * FROM stewards
237
- WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
238
- AND COALESCE(node_pattern, '') = COALESCE(?, '')
239
- AND COALESCE(tag_name, '') = COALESCE(?, '')`
240
- ).get(
241
- params.nestId,
242
- params.scope,
243
- email,
244
- nodePattern ?? null,
245
- tagName ?? null
246
- );
247
- if (existing) {
248
- results.push(rowToSteward(existing));
249
- continue;
250
- }
251
- const userRow = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
252
- const created = assignSteward({
253
- nestId: params.nestId,
254
- scope: params.scope,
255
- nodePattern,
256
- tagName,
257
- userEmail: email,
258
- userId: userRow?.id,
259
- role: user.role ?? "reviewer",
260
- assignedBy: params.assignedBy,
261
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
262
- isActive: true
263
- });
264
- results.push(created);
265
- await ensureCollaborator(
266
- params.nestId,
267
- email,
268
- rolePermission(user.role),
269
- params.assignedBy
270
- );
271
- }
272
- db.prepare(
273
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0"
274
- ).run(params.nestId);
275
- return results;
276
- }
277
- function resolveStewardsForNode(nestId, nodeId) {
278
- return resolve(nestId, nodeId).stewards;
279
- }
280
- function resolveStewardsWithFallback(nestId, nodeId) {
281
- return resolve(nestId, nodeId);
282
- }
283
- function resolve(nestId, nodeId) {
284
- const db = getDb();
285
- const rows = db.prepare(
286
- `
287
- SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
288
- FROM stewards s
289
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
290
- AND s.node_pattern = ?
291
- UNION ALL
292
- SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
293
- FROM stewards s
294
- JOIN node_tag_index nt
295
- ON nt.nest_id = s.nest_id
296
- AND nt.tag_name = s.tag_name
297
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
298
- AND nt.node_id = ?
299
- UNION ALL
300
- SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
301
- FROM stewards s
302
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
303
- ORDER BY priority ASC, user_email ASC
304
- `
305
- ).all(
306
- nestId,
307
- nodeId,
308
- // document branch
309
- nestId,
310
- nodeId,
311
- // tag branch
312
- nestId
313
- // nest branch
314
- );
315
- const resolved = rows.map((row) => ({
316
- steward: rowToSteward(row),
317
- priority: row.priority,
318
- source: row.match_source
319
- }));
320
- if (resolved.length > 0) {
321
- return { stewards: resolved, fallbackToOwner: false };
322
- }
323
- const owner = db.prepare(
324
- `SELECT u.email FROM nests n
325
- JOIN users u ON u.id = n.user_id
326
- WHERE n.id = ?`
327
- ).get(nestId);
328
- return {
329
- stewards: [],
330
- fallbackToOwner: true,
331
- ownerEmail: owner?.email
332
- };
333
- }
334
- function isSuperAdmin2(userEmail) {
335
- const cfg = getAccessConfig();
336
- return !!cfg?.super_admins?.includes(userEmail);
337
- }
338
- function getNestOwnerEmail(nestId) {
339
- const db = getDb();
340
- const row = db.prepare(
341
- `SELECT u.email FROM nests n
342
- JOIN users u ON u.id = n.user_id
343
- WHERE n.id = ?`
344
- ).get(nestId);
345
- return row?.email ?? null;
346
- }
347
- function canUserEdit(nestId, nodeId, userEmail) {
348
- if (isSuperAdmin2(userEmail)) {
349
- return { allowed: true, reason: "super admin", role: "super_admin" };
350
- }
351
- const owner = getNestOwnerEmail(nestId);
352
- if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
353
- return { allowed: true, reason: "nest owner", role: "owner" };
354
- }
355
- const resolved = resolveStewardsForNode(nestId, nodeId);
356
- const match = resolved.find(
357
- (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && (r.steward.role === "editor" || r.steward.role === "reviewer")
358
- );
359
- if (match) {
360
- return { allowed: true, reason: match.source, role: match.steward.role };
361
- }
362
- return { allowed: false, reason: "no editor/reviewer role on this node", role: null };
363
- }
364
- function getCurrentVersionAuthor(nestId, nodeId) {
365
- const db = getDb();
366
- const row = db.prepare(
367
- `SELECT author FROM node_versions
368
- WHERE nest_id = ? AND node_id = ?
369
- ORDER BY version DESC LIMIT 1`
370
- ).get(nestId, nodeId);
371
- return row?.author ?? null;
372
- }
373
- function canUserApprove(nestId, nodeId, userEmail) {
374
- if (isSuperAdmin2(userEmail)) {
375
- return { allowed: true, reason: "super admin", role: "super_admin" };
376
- }
377
- const resolved = resolveStewardsForNode(nestId, nodeId);
378
- if (resolved.length === 0) {
379
- return {
380
- allowed: false,
381
- reason: "no stewards configured for this node",
382
- role: null
383
- };
384
- }
385
- const match = resolved.find(
386
- (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer"
387
- );
388
- if (!match) {
389
- return {
390
- allowed: false,
391
- reason: "not a reviewer steward for this node",
392
- role: null
393
- };
394
- }
395
- const author = getCurrentVersionAuthor(nestId, nodeId);
396
- if (author && author.toLowerCase() === userEmail.toLowerCase()) {
397
- return {
398
- allowed: false,
399
- reason: "cannot approve a version you authored (separation of duties)",
400
- role: "reviewer"
401
- };
402
- }
403
- return { allowed: true, reason: match.source, role: "reviewer" };
404
- }
405
- function canUserAccess(nestId, nodeId, userEmail) {
406
- if (isSuperAdmin2(userEmail)) {
407
- return { allowed: true, reason: "super admin", role: "super_admin" };
408
- }
409
- const owner = getNestOwnerEmail(nestId);
410
- if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
411
- return { allowed: true, reason: "nest owner", role: "owner" };
412
- }
413
- const resolved = resolveStewardsForNode(nestId, nodeId);
414
- const match = resolved.find(
415
- (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
416
- );
417
- if (match) {
418
- return { allowed: true, reason: match.source, role: match.steward.role };
419
- }
420
- return { allowed: false, reason: "no steward assignment", role: null };
421
- }
422
- function syncFromConfig(nestId, config2) {
423
- const db = getDb();
424
- let count = 0;
425
- db.prepare(
426
- "UPDATE stewards SET is_active = 0 WHERE nest_id = ?"
427
- ).run(nestId);
428
- db.prepare(
429
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ?"
430
- ).run(nestId);
431
- const addEntries = (scope, entries, target) => {
432
- for (const entry of entries) {
433
- const user = db.prepare("SELECT id FROM users WHERE email = ?").get(entry.email);
434
- const rawRole = entry.role || "reviewer";
435
- const role = rawRole === "admin" ? "reviewer" : rawRole;
436
- assignSteward({
437
- nestId,
438
- scope,
439
- nodePattern: target?.nodePattern,
440
- tagName: target?.tagName ? target.tagName.trim().replace(/^#+/, "").toLowerCase() : void 0,
441
- userEmail: entry.email.toLowerCase(),
442
- userId: user?.id,
443
- role,
444
- assignedBy: "config",
445
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
446
- isActive: true
447
- });
448
- count++;
449
- }
450
- };
451
- if (config2.nest) {
452
- addEntries("nest", config2.nest);
453
- }
454
- if (config2.tags) {
455
- for (const [tagName, entries] of Object.entries(config2.tags)) {
456
- addEntries("tag", entries, { tagName });
457
- }
458
- }
459
- if (config2.documents) {
460
- for (const [docPattern, entries] of Object.entries(config2.documents)) {
461
- addEntries("document", entries, { nodePattern: docPattern });
462
- }
463
- }
464
- return count;
465
- }
466
- function rowToSteward(row) {
467
- return {
468
- id: row.id,
469
- nestId: row.nest_id,
470
- scope: row.scope,
471
- nodePattern: row.node_pattern || void 0,
472
- tagName: row.tag_name || void 0,
473
- userEmail: row.user_email,
474
- userId: row.user_id || void 0,
475
- role: row.role,
476
- assignedBy: row.assigned_by,
477
- assignedAt: row.assigned_at,
478
- isActive: !!row.is_active
479
- };
480
- }
481
-
482
- export {
483
- loadAccessConfig,
484
- isSuperAdmin,
485
- assignSteward,
486
- removeSteward,
487
- getSteward,
488
- getStewardsForNest,
489
- getStewardsForScope,
490
- listStewards,
491
- createStewardRecord,
492
- resolveStewardsForNode,
493
- resolveStewardsWithFallback,
494
- canUserEdit,
495
- canUserApprove,
496
- canUserAccess,
497
- syncFromConfig
498
- };