@promptowl/contextnest-community 1.14.0 → 1.15.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.
@@ -0,0 +1,624 @@
1
+ import {
2
+ buildTitleMap,
3
+ canEditWith,
4
+ canViewWith,
5
+ collabPermToRole,
6
+ docLink,
7
+ isSuperAdmin,
8
+ nestAllowsSelfApprove,
9
+ nestName,
10
+ primaryRole,
11
+ resolveNestPermission,
12
+ resolveTeamRolesForUser,
13
+ sendEmailToRecipient
14
+ } from "./chunk-7BOG3S5H.js";
15
+ import {
16
+ ConflictError,
17
+ ValidationError
18
+ } from "./chunk-GUNJTORH.js";
19
+ import {
20
+ config,
21
+ getDb,
22
+ isEmailish
23
+ } from "./chunk-BCFKLY4H.js";
24
+
25
+ // src/governance/stewardship-service.ts
26
+ import { v4 as uuid } from "uuid";
27
+ async function assignSteward(data) {
28
+ const db = getDb();
29
+ const id = uuid();
30
+ await db.run(
31
+ `INSERT INTO stewards
32
+ (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
33
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
34
+ [
35
+ id,
36
+ data.nestId,
37
+ data.scope,
38
+ data.nodePattern || null,
39
+ data.tagName || null,
40
+ data.userEmail,
41
+ data.userId || null,
42
+ data.role,
43
+ data.assignedBy,
44
+ data.isActive ? 1 : 0
45
+ ]
46
+ );
47
+ return { ...data, id };
48
+ }
49
+ async function removeSteward(id) {
50
+ const db = getDb();
51
+ await db.run("DELETE FROM stewards WHERE id = ?", [id]);
52
+ }
53
+ var VALID_STEWARD_ROLES = ["editor", "reviewer", "viewer"];
54
+ async function updateSteward(id, update) {
55
+ const db = getDb();
56
+ const current = await db.get(
57
+ "SELECT * FROM stewards WHERE id = ? AND is_active = 1",
58
+ [id]
59
+ );
60
+ if (!current) {
61
+ throw new ValidationError("Steward not found");
62
+ }
63
+ const role = update.role ?? current.role;
64
+ if (!VALID_STEWARD_ROLES.includes(role)) {
65
+ throw new ValidationError(
66
+ `Invalid role "${role}" \u2014 must be editor, reviewer, or viewer.`
67
+ );
68
+ }
69
+ let scope = current.scope;
70
+ let nodePattern = current.node_pattern ?? null;
71
+ let tagName = current.tag_name ?? null;
72
+ if (update.scope) {
73
+ scope = update.scope;
74
+ nodePattern = null;
75
+ tagName = null;
76
+ switch (scope) {
77
+ case "document":
78
+ if (!update.documentId)
79
+ throw new ValidationError("documentId required for document scope");
80
+ nodePattern = update.documentId;
81
+ break;
82
+ case "tag":
83
+ if (!update.tagName)
84
+ throw new ValidationError("tagName required for tag scope");
85
+ tagName = update.tagName.trim().replace(/^#+/, "").toLowerCase();
86
+ break;
87
+ case "nest":
88
+ break;
89
+ }
90
+ const dup = await db.get(
91
+ `SELECT role FROM stewards
92
+ WHERE nest_id = ? AND is_active = 1 AND id != ? AND scope = ?
93
+ AND user_email = ?
94
+ AND COALESCE(node_pattern, '') = COALESCE(?, '')
95
+ AND COALESCE(tag_name, '') = COALESCE(?, '')`,
96
+ [current.nest_id, id, scope, current.user_email, nodePattern, tagName]
97
+ );
98
+ if (dup) {
99
+ const scopeLabel = scope === "document" ? `document "${nodePattern}"` : scope === "tag" ? `tag "#${tagName}"` : "this nest";
100
+ throw new ConflictError(
101
+ `"${current.user_email}" is already a steward of ${scopeLabel} with the "${dup.role}" role.`
102
+ );
103
+ }
104
+ }
105
+ await db.run(
106
+ "UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?",
107
+ [role, scope, nodePattern, tagName, id]
108
+ );
109
+ const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
110
+ return rowToSteward(row);
111
+ }
112
+ async function updateStewardRole(id, role) {
113
+ return updateSteward(id, { role });
114
+ }
115
+ async function getSteward(id) {
116
+ const db = getDb();
117
+ const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
118
+ return row ? rowToSteward(row) : null;
119
+ }
120
+ async function getStewardsForNest(nestId) {
121
+ const db = getDb();
122
+ const rows = await db.all(
123
+ "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1",
124
+ [nestId]
125
+ );
126
+ return enrichWithTitles(nestId, rows.map(rowToSteward));
127
+ }
128
+ async function getStewardsForScope(params) {
129
+ const db = getDb();
130
+ let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
131
+ const args = [params.nestId];
132
+ if (params.scope) {
133
+ sql += " AND scope = ?";
134
+ args.push(params.scope);
135
+ }
136
+ if (params.scopeTarget) {
137
+ sql += " AND (node_pattern = ? OR tag_name = ?)";
138
+ args.push(params.scopeTarget, params.scopeTarget);
139
+ }
140
+ return (await db.all(sql, args)).map(rowToSteward);
141
+ }
142
+ async function listStewards(params) {
143
+ const db = getDb();
144
+ let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
145
+ const args = [params.nestId];
146
+ if (params.scope) {
147
+ sql += " AND scope = ?";
148
+ args.push(params.scope);
149
+ }
150
+ if (params.search) {
151
+ sql += " AND (user_email LIKE ? OR tag_name LIKE ? OR node_pattern LIKE ?)";
152
+ const like = `%${params.search.toLowerCase()}%`;
153
+ args.push(like, like, like);
154
+ }
155
+ sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
156
+ const stewards = (await db.all(sql, args)).map(rowToSteward);
157
+ return enrichWithTitles(params.nestId, stewards);
158
+ }
159
+ async function enrichWithTitles(nestId, stewards) {
160
+ if (!stewards.some((s) => s.scope === "document")) return stewards;
161
+ const titleByNodeId = await buildTitleMap(nestId);
162
+ for (const s of stewards) {
163
+ if (s.scope === "document" && s.nodePattern) {
164
+ s.nodeTitle = titleByNodeId.get(s.nodePattern);
165
+ }
166
+ }
167
+ return stewards;
168
+ }
169
+ async function createStewardRecord(params) {
170
+ if (params.users.length === 0) {
171
+ throw new Error("At least one user is required");
172
+ }
173
+ let nodePattern;
174
+ let tagName;
175
+ switch (params.scope) {
176
+ case "document":
177
+ if (!params.documentId) throw new Error("documentId required for document scope");
178
+ nodePattern = params.documentId;
179
+ break;
180
+ case "tag":
181
+ if (!params.tagName) throw new Error("tagName required for tag scope");
182
+ tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
183
+ break;
184
+ case "nest":
185
+ break;
186
+ }
187
+ const db = getDb();
188
+ const results = [];
189
+ const actor = (params.assignedBy || "").trim().toLowerCase();
190
+ const ownerEmail = (await getNestOwnerEmail(params.nestId) || "").toLowerCase();
191
+ for (const user of params.users) {
192
+ const email = user.email.trim().toLowerCase();
193
+ if (!email) continue;
194
+ if (!isEmailish(email)) {
195
+ throw new ValidationError(`"${user.email}" is not a valid email address.`);
196
+ }
197
+ if (email === actor) {
198
+ throw new ValidationError(
199
+ "You already manage this nest, so you can't add yourself as a steward."
200
+ );
201
+ }
202
+ if (ownerEmail && email === ownerEmail) {
203
+ throw new ValidationError(
204
+ "The nest owner already has full access and doesn't need a steward role."
205
+ );
206
+ }
207
+ const existing = await db.get(
208
+ `SELECT * FROM stewards
209
+ WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
210
+ AND COALESCE(node_pattern, '') = COALESCE(?, '')
211
+ AND COALESCE(tag_name, '') = COALESCE(?, '')`,
212
+ [
213
+ params.nestId,
214
+ params.scope,
215
+ email,
216
+ nodePattern ?? null,
217
+ tagName ?? null
218
+ ]
219
+ );
220
+ if (existing) {
221
+ const scopeLabel = params.scope === "document" ? `document "${nodePattern}"` : params.scope === "tag" ? `tag "#${tagName}"` : "this nest";
222
+ throw new ConflictError(
223
+ `"${email}" is already a steward of ${scopeLabel} with the "${existing.role}" role. Remove the existing assignment first to change the role.`
224
+ );
225
+ }
226
+ const userRow = await db.get(
227
+ "SELECT id FROM users WHERE email = ?",
228
+ [email]
229
+ );
230
+ let userId = userRow?.id;
231
+ if (!userId) {
232
+ const { hashPassword } = await import("./keys-73STFJJB.js");
233
+ userId = uuid();
234
+ await db.run(
235
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
236
+ [userId, email, null, await hashPassword(uuid())]
237
+ );
238
+ }
239
+ const created = await assignSteward({
240
+ nestId: params.nestId,
241
+ scope: params.scope,
242
+ nodePattern,
243
+ tagName,
244
+ userEmail: email,
245
+ userId,
246
+ role: user.role ?? "reviewer",
247
+ assignedBy: params.assignedBy,
248
+ assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
249
+ isActive: true
250
+ });
251
+ results.push(created);
252
+ }
253
+ await db.run(
254
+ "UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
255
+ [params.nestId]
256
+ );
257
+ if (results.length > 0) {
258
+ let scopeLabel;
259
+ if (params.scope === "document") {
260
+ const titles = await buildTitleMap(params.nestId);
261
+ const docTitle = titles.get(nodePattern) || nodePattern;
262
+ const capitalized = docTitle.charAt(0).toUpperCase() + docTitle.slice(1);
263
+ scopeLabel = `"${capitalized}" document`;
264
+ } else if (params.scope === "tag") {
265
+ scopeLabel = `#${tagName}`;
266
+ } else {
267
+ scopeLabel = "the whole nest";
268
+ }
269
+ const shareNestName = await nestName(params.nestId);
270
+ const link = docLink(params.nestId, void 0, params.baseUrl);
271
+ for (const s of results) {
272
+ void sendEmailToRecipient(s.userEmail, {
273
+ status: "steward",
274
+ docTitle: shareNestName,
275
+ path: shareNestName,
276
+ permission: s.role,
277
+ scopeLabel,
278
+ by: params.assignedBy || void 0,
279
+ link
280
+ });
281
+ }
282
+ }
283
+ return results;
284
+ }
285
+ async function resolveStewardsForNode(nestId, nodeId) {
286
+ return (await resolve(nestId, nodeId)).stewards;
287
+ }
288
+ async function resolveStewardsWithFallback(nestId, nodeId) {
289
+ return resolve(nestId, nodeId);
290
+ }
291
+ async function resolve(nestId, nodeId) {
292
+ const db = getDb();
293
+ const rows = await db.all(
294
+ `
295
+ SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
296
+ FROM stewards s
297
+ WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
298
+ AND s.node_pattern = ?
299
+ UNION ALL
300
+ SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
301
+ FROM stewards s
302
+ JOIN node_tag_index nt
303
+ ON nt.nest_id = s.nest_id
304
+ AND nt.tag_name = s.tag_name
305
+ WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
306
+ AND nt.node_id = ?
307
+ UNION ALL
308
+ SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
309
+ FROM stewards s
310
+ WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
311
+ ORDER BY priority ASC, user_email ASC
312
+ `,
313
+ [
314
+ nestId,
315
+ nodeId,
316
+ // document branch
317
+ nestId,
318
+ nodeId,
319
+ // tag branch
320
+ nestId
321
+ // nest branch
322
+ ]
323
+ );
324
+ const resolved = rows.map((row) => ({
325
+ steward: rowToSteward(row),
326
+ priority: row.priority,
327
+ source: row.match_source
328
+ }));
329
+ if (resolved.length > 0) {
330
+ return { stewards: resolved, fallbackToOwner: false };
331
+ }
332
+ const owner = await db.get(
333
+ `SELECT u.email FROM nests n
334
+ JOIN users u ON u.id = n.user_id
335
+ WHERE n.id = ?`,
336
+ [nestId]
337
+ );
338
+ return {
339
+ stewards: [],
340
+ fallbackToOwner: true,
341
+ ownerEmail: owner?.email
342
+ };
343
+ }
344
+ async function userIdForEmail(email) {
345
+ const db = getDb();
346
+ const row = await db.get(
347
+ "SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
348
+ [email]
349
+ );
350
+ return row?.id;
351
+ }
352
+ async function getStewardRolesForUser(nestId, userEmail) {
353
+ const db = getDb();
354
+ const rows = await db.all(
355
+ "SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
356
+ [nestId, userEmail]
357
+ );
358
+ return rows.map((r) => r.role);
359
+ }
360
+ async function getStewardsForUser(nestId, userEmail) {
361
+ const db = getDb();
362
+ const rows = await db.all(
363
+ "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
364
+ [nestId, userEmail]
365
+ );
366
+ return rows.map(rowToSteward);
367
+ }
368
+ async function getCollaboratorRole(nestId, userEmail) {
369
+ const userId = await userIdForEmail(userEmail);
370
+ if (!userId) return null;
371
+ const db = getDb();
372
+ const row = await db.get(
373
+ "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
374
+ [nestId, userId]
375
+ );
376
+ return row?.permission ?? null;
377
+ }
378
+ async function resolveGrantRoles(nestId, userEmail, userId) {
379
+ const [grantRaw, stewardRoles, teamRoles] = await Promise.all([
380
+ getCollaboratorRole(nestId, userEmail),
381
+ getStewardRolesForUser(nestId, userEmail),
382
+ resolveTeamRolesForUser(nestId, userId)
383
+ ]);
384
+ return [
385
+ ...new Set(
386
+ [collabPermToRole(grantRaw), ...stewardRoles, ...teamRoles].filter(
387
+ Boolean
388
+ )
389
+ )
390
+ ];
391
+ }
392
+ async function resolveUserRoles(nestId, userEmail, opts) {
393
+ const roles = /* @__PURE__ */ new Set();
394
+ if (isSuperAdmin(userEmail)) roles.add("admin");
395
+ const owner = await getNestOwnerEmail(nestId);
396
+ if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
397
+ roles.add("owner");
398
+ }
399
+ const userId = await userIdForEmail(userEmail);
400
+ if (userId) {
401
+ const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
402
+ if (collabRole) roles.add(collabRole);
403
+ for (const role of await resolveTeamRolesForUser(nestId, userId)) {
404
+ roles.add(role);
405
+ }
406
+ }
407
+ const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
408
+ (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
409
+ ).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
410
+ for (const role of stewardRoles) roles.add(role);
411
+ return [...roles];
412
+ }
413
+ async function canManageStewards(nestId, userId) {
414
+ if (config.AUTH_MODE === "open") return true;
415
+ const perm = await resolveNestPermission(nestId, userId);
416
+ return perm === "owner" || perm === "admin";
417
+ }
418
+ async function canCreateInNest(nestId, userEmail) {
419
+ if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
420
+ const userId = await userIdForEmail(userEmail);
421
+ if (userId) {
422
+ const perm = await resolveNestPermission(nestId, userId);
423
+ if (perm === "owner" || perm === "admin" || perm === "write") return true;
424
+ }
425
+ return (await getStewardsForUser(nestId, userEmail)).some(
426
+ (s) => s.role === "editor" && s.scope === "nest"
427
+ );
428
+ }
429
+ async function getNestOwnerEmail(nestId) {
430
+ const db = getDb();
431
+ const row = await db.get(
432
+ `SELECT u.email FROM nests n
433
+ JOIN users u ON u.id = n.user_id
434
+ WHERE n.id = ?`,
435
+ [nestId]
436
+ );
437
+ return row?.email ?? null;
438
+ }
439
+ async function canUserEdit(nestId, nodeId, userEmail) {
440
+ const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
441
+ if (roles.includes("owner")) {
442
+ return { allowed: true, reason: "nest owner", role: "owner" };
443
+ }
444
+ if (isSuperAdmin(userEmail)) {
445
+ return { allowed: true, reason: "super admin", role: "super_admin" };
446
+ }
447
+ if (canEditWith(roles)) {
448
+ return {
449
+ allowed: true,
450
+ reason: "editor access (collaborator or steward)",
451
+ role: roles.includes("admin") ? "admin" : "editor"
452
+ };
453
+ }
454
+ return { allowed: false, reason: "no editor role on this node", role: null };
455
+ }
456
+ async function getCurrentVersionAuthor(nestId, nodeId) {
457
+ const db = getDb();
458
+ const row = await db.get(
459
+ `SELECT author FROM node_versions
460
+ WHERE nest_id = ? AND node_id = ?
461
+ ORDER BY version DESC LIMIT 1`,
462
+ [nestId, nodeId]
463
+ );
464
+ return row?.author ?? null;
465
+ }
466
+ async function getPendingReviewRequester(nestId, nodeId) {
467
+ const db = getDb();
468
+ const row = await db.get(
469
+ `SELECT requested_by FROM review_requests
470
+ WHERE nest_id = ? AND node_id = ? AND status = 'pending'
471
+ ORDER BY requested_at DESC LIMIT 1`,
472
+ [nestId, nodeId]
473
+ );
474
+ return row?.requested_by ?? null;
475
+ }
476
+ async function canUserApprove(nestId, nodeId, userEmail) {
477
+ const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
478
+ const isOwner = roles.includes("owner");
479
+ const isSuper = isSuperAdmin(userEmail);
480
+ const allowSelf = await nestAllowsSelfApprove(nestId);
481
+ const hasStewardApprove = roles.includes("admin") || roles.includes("reviewer");
482
+ const actor = await getPendingReviewRequester(nestId, nodeId) ?? await getCurrentVersionAuthor(nestId, nodeId);
483
+ const isOwnSubmission = !!actor && actor.toLowerCase() === userEmail.toLowerCase();
484
+ if (hasStewardApprove) {
485
+ if (isOwnSubmission && !((isOwner || isSuper) && allowSelf)) {
486
+ return {
487
+ allowed: false,
488
+ reason: "You submitted this version for review, so you can't approve it yourself. Ask another reviewer to approve it (separation of duties).",
489
+ role: roles.includes("admin") ? "admin" : "reviewer"
490
+ };
491
+ }
492
+ return {
493
+ allowed: true,
494
+ reason: "reviewer access (collaborator or steward)",
495
+ role: roles.includes("admin") ? "admin" : "reviewer"
496
+ };
497
+ }
498
+ if (isOwner || isSuper) {
499
+ if (allowSelf) {
500
+ return {
501
+ allowed: true,
502
+ reason: isOwner ? "nest owner (self-approve enabled)" : "super admin",
503
+ role: isOwner ? "owner" : "super_admin"
504
+ };
505
+ }
506
+ return {
507
+ allowed: false,
508
+ reason: "Approvals go to assigned reviewers while self-approve is off. Enable self-approve in nest settings to approve directly (e.g. while seeding), or add a reviewer steward.",
509
+ role: isOwner ? "owner" : "super_admin"
510
+ };
511
+ }
512
+ const held = primaryRole(roles);
513
+ return {
514
+ allowed: false,
515
+ reason: held ? `You're a "${held}" on this document \u2014 only reviewers (or admins) can approve. Ask the nest owner to grant you the reviewer steward role.` : "You're not a steward on this document, so you can't approve it. Ask the nest owner to add you as a reviewer.",
516
+ role: held
517
+ };
518
+ }
519
+ async function canUserAccess(nestId, nodeId, userEmail) {
520
+ const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
521
+ if (roles.includes("owner")) {
522
+ return { allowed: true, reason: "nest owner", role: "owner" };
523
+ }
524
+ if (isSuperAdmin(userEmail)) {
525
+ return { allowed: true, reason: "super admin", role: "super_admin" };
526
+ }
527
+ if (canViewWith(roles)) {
528
+ return {
529
+ allowed: true,
530
+ reason: "collaborator or steward access",
531
+ role: primaryRole(roles)
532
+ };
533
+ }
534
+ return { allowed: false, reason: "no collaborator or steward access", role: null };
535
+ }
536
+ async function syncFromConfig(nestId, config2) {
537
+ const db = getDb();
538
+ let count = 0;
539
+ await db.run(
540
+ "UPDATE stewards SET is_active = 0 WHERE nest_id = ?",
541
+ [nestId]
542
+ );
543
+ await db.run(
544
+ "UPDATE nests SET stewardship_enabled = 1 WHERE id = ?",
545
+ [nestId]
546
+ );
547
+ const addEntries = async (scope, entries, target) => {
548
+ for (const entry of entries) {
549
+ const user = await db.get(
550
+ "SELECT id FROM users WHERE email = ?",
551
+ [entry.email]
552
+ );
553
+ const rawRole = entry.role || "reviewer";
554
+ const role = rawRole === "admin" ? "reviewer" : rawRole;
555
+ await assignSteward({
556
+ nestId,
557
+ scope,
558
+ nodePattern: target?.nodePattern,
559
+ tagName: target?.tagName ? target.tagName.trim().replace(/^#+/, "").toLowerCase() : void 0,
560
+ userEmail: entry.email.toLowerCase(),
561
+ userId: user?.id,
562
+ role,
563
+ assignedBy: "config",
564
+ assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
565
+ isActive: true
566
+ });
567
+ count++;
568
+ }
569
+ };
570
+ if (config2.nest) {
571
+ await addEntries("nest", config2.nest);
572
+ }
573
+ if (config2.tags) {
574
+ for (const [tagName, entries] of Object.entries(config2.tags)) {
575
+ await addEntries("tag", entries, { tagName });
576
+ }
577
+ }
578
+ if (config2.documents) {
579
+ for (const [docPattern, entries] of Object.entries(config2.documents)) {
580
+ await addEntries("document", entries, { nodePattern: docPattern });
581
+ }
582
+ }
583
+ return count;
584
+ }
585
+ function rowToSteward(row) {
586
+ return {
587
+ id: row.id,
588
+ nestId: row.nest_id,
589
+ scope: row.scope,
590
+ nodePattern: row.node_pattern || void 0,
591
+ tagName: row.tag_name || void 0,
592
+ userEmail: row.user_email,
593
+ userId: row.user_id || void 0,
594
+ role: row.role,
595
+ assignedBy: row.assigned_by,
596
+ assignedAt: row.assigned_at,
597
+ isActive: !!row.is_active
598
+ };
599
+ }
600
+
601
+ export {
602
+ assignSteward,
603
+ removeSteward,
604
+ updateSteward,
605
+ updateStewardRole,
606
+ getSteward,
607
+ getStewardsForNest,
608
+ getStewardsForScope,
609
+ listStewards,
610
+ createStewardRecord,
611
+ resolveStewardsForNode,
612
+ resolveStewardsWithFallback,
613
+ getStewardRolesForUser,
614
+ getStewardsForUser,
615
+ getCollaboratorRole,
616
+ resolveGrantRoles,
617
+ resolveUserRoles,
618
+ canManageStewards,
619
+ canCreateInNest,
620
+ canUserEdit,
621
+ canUserApprove,
622
+ canUserAccess,
623
+ syncFromConfig
624
+ };
@@ -1,10 +1,11 @@
1
1
  import {
2
+ buildTitleMap,
2
3
  insertOrReplace,
3
4
  nowExpr
4
- } from "./chunk-XQ46F76G.js";
5
+ } from "./chunk-7BOG3S5H.js";
5
6
  import {
6
7
  getDb
7
- } from "./chunk-I5KYGMIT.js";
8
+ } from "./chunk-BCFKLY4H.js";
8
9
 
9
10
  // src/governance/version-service.ts
10
11
  import { createHash } from "crypto";
@@ -187,6 +188,59 @@ async function getDisplayStatus(nestId, nodeId) {
187
188
  if (current.status === "rejected") return "rejected";
188
189
  return "draft";
189
190
  }
191
+ async function listMyDrafts(params) {
192
+ if (params.nestIds.length === 0) return { drafts: [], total: 0 };
193
+ const db = getDb();
194
+ const nestPlaceholders = params.nestIds.map(() => "?").join(",");
195
+ const where = `
196
+ WHERE v.author = ?
197
+ AND v.status IN ('draft', 'rejected')
198
+ AND v.nest_id IN (${nestPlaceholders})
199
+ AND v.version = (
200
+ SELECT MAX(v2.version) FROM node_versions v2
201
+ WHERE v2.nest_id = v.nest_id AND v2.node_id = v.node_id)
202
+ AND NOT EXISTS (
203
+ SELECT 1 FROM review_requests r
204
+ WHERE r.nest_id = v.nest_id AND r.node_id = v.node_id
205
+ AND r.status = 'pending')`;
206
+ const args = [params.authorEmail.toLowerCase(), ...params.nestIds];
207
+ const total = Number(
208
+ (await db.get(
209
+ `SELECT COUNT(*) as c FROM node_versions v ${where}`,
210
+ args
211
+ )).c
212
+ );
213
+ const limit = Math.min(Math.max(params.limit ?? 25, 1), 100);
214
+ const offset = Math.max(params.offset ?? 0, 0);
215
+ const rows = await db.all(
216
+ `SELECT v.nest_id, v.node_id, v.status, v.version, v.created_at
217
+ FROM node_versions v ${where}
218
+ ORDER BY v.created_at DESC
219
+ LIMIT ? OFFSET ?`,
220
+ [...args, limit, offset]
221
+ );
222
+ const titlesByNest = new Map(
223
+ await Promise.all(
224
+ [...new Set(rows.map((r) => r.nest_id))].map(
225
+ async (nestId) => [
226
+ nestId,
227
+ await buildTitleMap(nestId)
228
+ ]
229
+ )
230
+ )
231
+ );
232
+ const drafts = rows.filter((r) => titlesByNest.get(r.nest_id)?.has(r.node_id)).map((r) => ({
233
+ nestId: r.nest_id,
234
+ nodeId: r.node_id,
235
+ title: titlesByNest.get(r.nest_id).get(r.node_id) || r.node_id,
236
+ status: r.status,
237
+ // Number(): Postgres returns INTEGER columns fine, but stay explicit —
238
+ // the modal renders this straight into copy.
239
+ version: Number(r.version),
240
+ updatedAt: r.created_at
241
+ }));
242
+ return { drafts, total };
243
+ }
190
244
  function rowToVersion(row) {
191
245
  return {
192
246
  version: row.version,
@@ -212,5 +266,6 @@ export {
212
266
  setApprovedVersion,
213
267
  checkConflict,
214
268
  getNodeTags,
215
- getDisplayStatus
269
+ getDisplayStatus,
270
+ listMyDrafts
216
271
  };
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-I5KYGMIT.js";
5
+ } from "./chunk-BCFKLY4H.js";
6
6
  import "./chunk-SLTQACJW.js";
7
7
  export {
8
8
  getDb,