@promptowl/contextnest-community 1.13.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.
@@ -4,208 +4,27 @@ import {
4
4
  NotFoundError,
5
5
  ValidationError
6
6
  } from "./chunk-GUNJTORH.js";
7
- import {
8
- normalizeEmail
9
- } from "./chunk-FRQJWGN3.js";
10
- import {
11
- insertOrReplace,
12
- nowExpr
13
- } from "./chunk-XQ46F76G.js";
14
7
  import {
15
8
  config,
16
9
  getDb,
17
10
  isEmailish
18
- } from "./chunk-5QQ7WKAI.js";
11
+ } from "./chunk-BCFKLY4H.js";
19
12
  import {
20
13
  ANON_USER_ID
21
14
  } from "./chunk-SLTQACJW.js";
22
-
23
- // src/governance/stewardship-service.ts
24
- import { v4 as uuid3 } from "uuid";
25
-
26
- // src/governance/access-service.ts
27
- import { readFileSync, existsSync } from "fs";
28
- import { join } from "path";
29
- var accessConfig = null;
30
- var grantedSuperAdmins = /* @__PURE__ */ new Set();
31
- async function loadGrantedSuperAdmins() {
32
- const rows = await getDb().all(
33
- "SELECT email FROM super_admins"
34
- );
35
- grantedSuperAdmins = new Set(rows.map((r) => r.email.toLowerCase()));
36
- }
37
- function loadAccessConfig() {
38
- const candidates = [
39
- join(config.DATA_ROOT, "access.yaml"),
40
- join(config.DATA_ROOT, "access.yml")
41
- ];
42
- for (const path of candidates) {
43
- if (existsSync(path)) {
44
- const content = readFileSync(path, "utf-8");
45
- accessConfig = parseAccessYaml(content);
46
- return accessConfig;
47
- }
48
- }
49
- accessConfig = null;
50
- return null;
51
- }
52
- function getAccessConfig() {
53
- return accessConfig;
54
- }
55
- function isSuperAdmin(email) {
56
- const lower = email.toLowerCase();
57
- if (grantedSuperAdmins.has(lower)) return true;
58
- return isConfigSuperAdmin(lower);
59
- }
60
- function isConfigSuperAdmin(email) {
61
- if (!accessConfig?.super_admins) return false;
62
- return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
63
- }
64
- function parseAccessYaml(content) {
65
- const result = {};
66
- const lines = content.split("\n");
67
- let currentSection = null;
68
- let currentGroup = null;
69
- let inMembers = false;
70
- for (const rawLine of lines) {
71
- const line = rawLine.trimEnd();
72
- if (!line || line.startsWith("#")) continue;
73
- if (!line.startsWith(" ") && !line.startsWith(" ")) {
74
- const match = line.match(/^(\w+):\s*(.*)?$/);
75
- if (!match) continue;
76
- const key = match[1];
77
- const value = match[2]?.trim();
78
- if (key === "mode") {
79
- result.mode = value;
80
- currentSection = null;
81
- } else if (key === "allowed_users") {
82
- currentSection = "allowed_users";
83
- result.allowed_users = [];
84
- } else if (key === "groups") {
85
- currentSection = "groups";
86
- result.groups = {};
87
- } else if (key === "super_admins") {
88
- currentSection = "super_admins";
89
- result.super_admins = [];
90
- }
91
- currentGroup = null;
92
- inMembers = false;
93
- continue;
94
- }
95
- const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
96
- if (currentSection === "allowed_users" && listMatch) {
97
- result.allowed_users.push(listMatch[1].trim());
98
- continue;
99
- }
100
- if (currentSection === "super_admins" && listMatch) {
101
- result.super_admins.push(listMatch[1].trim());
102
- continue;
103
- }
104
- if (currentSection === "groups") {
105
- const groupMatch = line.match(/^ (\w+):$/);
106
- if (groupMatch) {
107
- currentGroup = groupMatch[1];
108
- result.groups[currentGroup] = { members: [], default_permission: "read" };
109
- inMembers = false;
110
- continue;
111
- }
112
- if (currentGroup) {
113
- const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
114
- if (propMatch) {
115
- const prop = propMatch[1];
116
- const val = propMatch[2]?.trim();
117
- if (prop === "default_permission" && val) {
118
- result.groups[currentGroup].default_permission = val;
119
- }
120
- if (prop === "members") {
121
- inMembers = true;
122
- }
123
- continue;
124
- }
125
- if (inMembers && listMatch) {
126
- result.groups[currentGroup].members.push(listMatch[1].trim());
127
- }
128
- }
129
- }
130
- }
131
- return result;
132
- }
133
-
134
- // src/nodes/engine.ts
135
15
  import {
136
- NestStorage as NestStorage3,
137
- GraphQueryEngine,
138
- VersionManager
139
- } from "@promptowl/contextnest-engine";
140
-
141
- // src/nests/service.ts
142
- import { rmSync as rmSync2, mkdirSync } from "fs";
143
- import { v4 as uuid2 } from "uuid";
144
- import { NestStorage } from "@promptowl/contextnest-engine";
16
+ normalizeEmail
17
+ } from "./chunk-FRQJWGN3.js";
145
18
 
146
- // src/fs/vault-import.ts
147
- import { rmSync } from "fs";
148
- import { writeFile, mkdir } from "fs/promises";
149
- import { join as join2, dirname, isAbsolute } from "path";
150
- function isSkippedSegment(segment) {
151
- return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
152
- }
153
- function safeSegments(rel) {
154
- if (!rel || isAbsolute(rel)) return null;
155
- const segs = rel.split(/[\\/]/).filter(Boolean);
156
- if (!segs.length) return null;
157
- for (const s of segs) {
158
- if (s === "..") return null;
159
- if (s !== ".versions" && isSkippedSegment(s)) return null;
160
- }
161
- return segs;
162
- }
163
- var WRITE_CONCURRENCY = 32;
164
- async function writeImportedFiles(dest, files) {
165
- const targets = [];
166
- const skipped = [];
167
- for (const f of files) {
168
- const segs = safeSegments(f?.path || "");
169
- if (!segs) {
170
- skipped.push(f?.path || "(empty)");
171
- continue;
172
- }
173
- targets.push({ full: join2(dest, ...segs), content: f.content ?? "" });
174
- }
175
- const dirs = new Set(targets.map((t) => dirname(t.full)));
176
- await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
177
- let written = 0;
178
- for (let i = 0; i < targets.length; i += WRITE_CONCURRENCY) {
179
- const batch = targets.slice(i, i + WRITE_CONCURRENCY);
180
- await Promise.all(
181
- batch.map(async (t) => {
182
- await writeFile(t.full, t.content, "utf-8");
183
- written++;
184
- })
185
- );
186
- }
187
- console.log(
188
- `[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
189
- );
190
- if (skipped.length) {
191
- console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
192
- }
193
- return written;
194
- }
195
- function discardImportDir(dest) {
196
- try {
197
- rmSync(dest, { recursive: true, force: true });
198
- } catch {
199
- }
19
+ // src/db/sql.ts
20
+ function nowExpr(db) {
21
+ return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
200
22
  }
201
-
202
- // src/shared/paths.ts
203
- import { join as join3 } from "path";
204
- function nestStorageRoot() {
205
- return config.NEST_STORAGE_ROOT;
23
+ function insertOrIgnore(db, insertSql) {
24
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
206
25
  }
207
- function resolveNestPath(nestId) {
208
- return join3(nestStorageRoot(), nestId);
26
+ function insertOrReplace(db, insertSql, conflictCols, set) {
27
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
209
28
  }
210
29
 
211
30
  // src/telemetry/tracker.ts
@@ -639,1527 +458,1697 @@ async function _validateLicenseImpl(forceFresh) {
639
458
  }
640
459
  }
641
460
 
642
- // src/governance/teams-service.ts
643
- import { v4 as uuid } from "uuid";
644
- var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
645
- function parseMembers(json) {
646
- if (!json) return [];
647
- try {
648
- const parsed = JSON.parse(json);
649
- return Array.isArray(parsed) ? parsed : [];
650
- } catch {
651
- return [];
652
- }
653
- }
654
- function rowToTeam(row) {
655
- return {
656
- id: row.id,
657
- name: row.name,
658
- owner_id: row.owner_id,
659
- members: parseMembers(row.members),
660
- source: row.source ?? null,
661
- external_id: row.external_id ?? null,
662
- created_at: row.created_at,
663
- updated_at: row.updated_at
664
- };
461
+ // src/governance/access-service.ts
462
+ import { readFileSync, existsSync } from "fs";
463
+ import { join } from "path";
464
+ var accessConfig = null;
465
+ var grantedSuperAdmins = /* @__PURE__ */ new Set();
466
+ async function loadGrantedSuperAdmins() {
467
+ const rows = await getDb().all(
468
+ "SELECT email FROM super_admins"
469
+ );
470
+ grantedSuperAdmins = new Set(rows.map((r) => r.email.toLowerCase()));
665
471
  }
666
- async function enrichTeam(team) {
667
- const db = getDb();
668
- const ids = team.members.map((m) => m.userId);
669
- const invited = /* @__PURE__ */ new Map();
670
- if (ids.length) {
671
- const rows = await db.all(
672
- `SELECT id, is_invited FROM users WHERE id IN (${ids.map(() => "?").join(",")})`,
673
- ids
674
- );
675
- for (const r of rows) invited.set(r.id, r.is_invited);
472
+ function loadAccessConfig() {
473
+ const candidates = [
474
+ join(config.DATA_ROOT, "access.yaml"),
475
+ join(config.DATA_ROOT, "access.yml")
476
+ ];
477
+ for (const path of candidates) {
478
+ if (existsSync(path)) {
479
+ const content = readFileSync(path, "utf-8");
480
+ accessConfig = parseAccessYaml(content);
481
+ return accessConfig;
482
+ }
676
483
  }
677
- const owner = await db.get("SELECT email FROM users WHERE id = ?", [
678
- team.owner_id
679
- ]);
680
- return {
681
- ...team,
682
- owner_email: owner?.email,
683
- // Unknown id (deleted user) → treat as not registered.
684
- members: team.members.map((m) => ({
685
- ...m,
686
- registered: (invited.get(m.userId) ?? 1) === 0
687
- }))
688
- };
484
+ accessConfig = null;
485
+ return null;
689
486
  }
690
- function assertValidRole(role) {
691
- if (!VALID_TEAM_ROLES.includes(role)) {
692
- throw new ValidationError(
693
- "role must be admin, editor, or viewer"
694
- );
695
- }
696
- }
697
- async function loadTeamRow(teamId) {
698
- const db = getDb();
699
- const row = await db.get("SELECT * FROM teams WHERE id = ?", [
700
- teamId
701
- ]);
702
- if (!row) throw new NotFoundError("Team not found");
703
- return row;
704
- }
705
- var ROLE_RANK = {
706
- viewer: 1,
707
- editor: 2,
708
- admin: 3
709
- };
710
- var OWNER_RANK = 4;
711
- function managementRank(row, callerUserId, allowAdmin) {
712
- if (row.owner_id === callerUserId || allowAdmin) return OWNER_RANK;
713
- const m = parseMembers(row.members).find((x) => x.userId === callerUserId);
714
- return m ? ROLE_RANK[m.role] ?? 0 : 0;
715
- }
716
- async function createTeam(params) {
717
- const name = params.name?.trim();
718
- if (!name) throw new ValidationError("Team name is required");
719
- const db = getDb();
720
- const now = (/* @__PURE__ */ new Date()).toISOString();
721
- const id = uuid();
722
- await db.run(
723
- "INSERT INTO teams (id, name, owner_id, members, source, external_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
724
- [
725
- id,
726
- name,
727
- params.ownerId,
728
- "[]",
729
- params.source ?? null,
730
- params.externalId ?? null,
731
- now,
732
- now
733
- ]
734
- );
735
- return enrichTeam(rowToTeam(await loadTeamRow(id)));
487
+ function getAccessConfig() {
488
+ return accessConfig;
736
489
  }
737
- async function findTeamByExternalId(params) {
738
- const db = getDb();
739
- const row = await db.get(
740
- "SELECT * FROM teams WHERE owner_id = ? AND source = ? AND external_id = ?",
741
- [params.ownerId, params.source, params.externalId]
742
- );
743
- return row ? enrichTeam(rowToTeam(row)) : null;
490
+ function isSuperAdmin(email) {
491
+ const lower = email.toLowerCase();
492
+ if (grantedSuperAdmins.has(lower)) return true;
493
+ return isConfigSuperAdmin(lower);
744
494
  }
745
- async function listTeamsForUser(userId) {
746
- const db = getDb();
747
- const rows = await db.all(
748
- "SELECT * FROM teams WHERE owner_id = ? OR members LIKE ? ORDER BY created_at",
749
- [userId, `%${userId}%`]
750
- );
751
- const mine = rows.filter(
752
- (r) => r.owner_id === userId || parseMembers(r.members).some((m) => m.userId === userId)
753
- );
754
- return Promise.all(mine.map((r) => enrichTeam(rowToTeam(r))));
495
+ function isConfigSuperAdmin(email) {
496
+ if (!accessConfig?.super_admins) return false;
497
+ return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
755
498
  }
756
- async function getTeam(teamId) {
757
- return enrichTeam(rowToTeam(await loadTeamRow(teamId)));
499
+ function parseAccessYaml(content) {
500
+ const result = {};
501
+ const lines = content.split("\n");
502
+ let currentSection = null;
503
+ let currentGroup = null;
504
+ let inMembers = false;
505
+ for (const rawLine of lines) {
506
+ const line = rawLine.trimEnd();
507
+ if (!line || line.startsWith("#")) continue;
508
+ if (!line.startsWith(" ") && !line.startsWith(" ")) {
509
+ const match = line.match(/^(\w+):\s*(.*)?$/);
510
+ if (!match) continue;
511
+ const key = match[1];
512
+ const value = match[2]?.trim();
513
+ if (key === "mode") {
514
+ result.mode = value;
515
+ currentSection = null;
516
+ } else if (key === "allowed_users") {
517
+ currentSection = "allowed_users";
518
+ result.allowed_users = [];
519
+ } else if (key === "groups") {
520
+ currentSection = "groups";
521
+ result.groups = {};
522
+ } else if (key === "super_admins") {
523
+ currentSection = "super_admins";
524
+ result.super_admins = [];
525
+ }
526
+ currentGroup = null;
527
+ inMembers = false;
528
+ continue;
529
+ }
530
+ const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
531
+ if (currentSection === "allowed_users" && listMatch) {
532
+ result.allowed_users.push(listMatch[1].trim());
533
+ continue;
534
+ }
535
+ if (currentSection === "super_admins" && listMatch) {
536
+ result.super_admins.push(listMatch[1].trim());
537
+ continue;
538
+ }
539
+ if (currentSection === "groups") {
540
+ const groupMatch = line.match(/^ (\w+):$/);
541
+ if (groupMatch) {
542
+ currentGroup = groupMatch[1];
543
+ result.groups[currentGroup] = { members: [], default_permission: "read" };
544
+ inMembers = false;
545
+ continue;
546
+ }
547
+ if (currentGroup) {
548
+ const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
549
+ if (propMatch) {
550
+ const prop = propMatch[1];
551
+ const val = propMatch[2]?.trim();
552
+ if (prop === "default_permission" && val) {
553
+ result.groups[currentGroup].default_permission = val;
554
+ }
555
+ if (prop === "members") {
556
+ inMembers = true;
557
+ }
558
+ continue;
559
+ }
560
+ if (inMembers && listMatch) {
561
+ result.groups[currentGroup].members.push(listMatch[1].trim());
562
+ }
563
+ }
564
+ }
565
+ }
566
+ return result;
758
567
  }
759
- async function renameTeam(params) {
760
- const name = params.name?.trim();
761
- if (!name) throw new ValidationError("Team name is required");
762
- const row = await loadTeamRow(params.teamId);
763
- if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
764
- throw new ForbiddenError("Only a team admin or the owner can rename this team.");
568
+
569
+ // src/notify/email-render.ts
570
+ function escapeHtml(s) {
571
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
572
+ }
573
+ var EVENT = {
574
+ ":white_check_mark:": { icon: "\u2705", color: "#16a34a" },
575
+ ":x:": { icon: "\u274C", color: "#dc2626" },
576
+ ":memo:": { icon: "\u{1F4DD}", color: "#d97706" },
577
+ ":key:": { icon: "\u{1F511}", color: "#2563eb" }
578
+ };
579
+ var ALL_EMOJI = Object.fromEntries(
580
+ Object.entries(EVENT).map(([code, m]) => [code, m.icon])
581
+ );
582
+ function renderText(message) {
583
+ return message.replace(/:[a-z_]+:/g, (m) => ALL_EMOJI[m] ?? "").replace(/\*/g, "").replace(/[ \t]{2,}/g, " ").trim();
584
+ }
585
+ var STATUS_META = {
586
+ approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
587
+ rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
588
+ pending_review: {
589
+ subjectWord: "awaiting review",
590
+ label: "Pending review",
591
+ color: "#d97706"
592
+ },
593
+ shared: { subjectWord: "shared with you", label: "Shared", color: "#2563eb" },
594
+ invited: { subjectWord: "invited you", label: "Invitation", color: "#2563eb" },
595
+ steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" },
596
+ deletion_requested: {
597
+ subjectWord: "flagged for deletion",
598
+ label: "Deletion requested",
599
+ color: "#d97706"
600
+ },
601
+ deletion_declined: {
602
+ subjectWord: "kept",
603
+ label: "Deletion declined",
604
+ color: "#d97706"
605
+ },
606
+ deletion_deleted: {
607
+ subjectWord: "deleted",
608
+ label: "Deleted",
609
+ color: "#dc2626"
765
610
  }
766
- const db = getDb();
767
- await db.transaction(async (tx) => {
768
- await tx.run("UPDATE teams SET name = ?, updated_at = ? WHERE id = ?", [
769
- name,
770
- (/* @__PURE__ */ new Date()).toISOString(),
771
- params.teamId
772
- ]);
773
- await refreshTeamNameRefs(tx, params.teamId, name);
774
- });
775
- return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
611
+ };
612
+ var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
613
+ var detailRow = (label, valueHtml) => `<tr>
614
+ <td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
615
+ <td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
616
+ </tr>`;
617
+ var plainRow = (label, value) => ({
618
+ label,
619
+ text: value,
620
+ html: escapeHtml(value)
621
+ });
622
+ var governanceRows = (x) => [
623
+ plainRow("Path", x.d.path),
624
+ { label: "Status", text: x.meta.label, html: x.statusBadge },
625
+ ...x.d.version != null ? [plainRow("Version", `v${x.d.version}`)] : []
626
+ ];
627
+ var TEMPLATES = {
628
+ approved: {
629
+ heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
630
+ detailsHeader: "Document details",
631
+ linkLabel: "Open document",
632
+ lineT: (x) => `${x.title}${x.vT} has been approved${x.byT}${x.noteT}.`,
633
+ lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
634
+ rows: governanceRows
635
+ },
636
+ rejected: {
637
+ heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
638
+ detailsHeader: "Document details",
639
+ linkLabel: "Open document",
640
+ lineT: (x) => `${x.title}${x.vT} has been rejected${x.byT}${x.noteT}.`,
641
+ lineH: (x) => `${x.doc}${x.vH} has been rejected${x.byH}${x.noteH}.`,
642
+ rows: governanceRows
643
+ },
644
+ pending_review: {
645
+ heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
646
+ detailsHeader: "Document details",
647
+ linkLabel: "Open document",
648
+ lineT: (x) => `A review has been requested for ${x.title}${x.vT}${x.byT}${x.noteT}.`,
649
+ lineH: (x) => `A review has been requested for ${x.doc}${x.vH}${x.byH}${x.noteH}.`,
650
+ rows: governanceRows
651
+ },
652
+ shared: {
653
+ heading: (x) => `You've been added to ${x.title}`,
654
+ detailsHeader: "Details",
655
+ linkLabel: "Open nest",
656
+ lineT: (x) => `You've been given ${x.d.permission || "collaborator"} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
657
+ lineH: (x) => `You've been given <strong>${escapeHtml(x.d.permission || "collaborator")}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
658
+ rows: (x) => [
659
+ plainRow("Nest", x.d.path),
660
+ ...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : []
661
+ ]
662
+ },
663
+ invited: {
664
+ heading: (x) => `You've been invited to ${x.title}`,
665
+ detailsHeader: "Sign-in details",
666
+ linkLabel: "Sign in",
667
+ lineT: (x) => `You've been invited to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Sign in with your email and the temporary password below, then choose your own password.`,
668
+ lineH: (x) => `You've been invited to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Sign in with your email and the temporary password below, then choose your own password.`,
669
+ rows: (x) => [
670
+ plainRow("Email", x.d.actor || ""),
671
+ ...x.d.tempPassword ? [
672
+ {
673
+ label: "Temporary password",
674
+ text: x.d.tempPassword,
675
+ html: x.codeValue(x.d.tempPassword)
676
+ }
677
+ ] : []
678
+ ]
679
+ },
680
+ // Deletion requests reuse the governance details block — same "what/where/
681
+ // status" shape, and the reason/note rides in `note` like a review note.
682
+ deletion_requested: {
683
+ heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
684
+ detailsHeader: "Document details",
685
+ linkLabel: "Review the request",
686
+ lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or decline the request from the nest.`,
687
+ lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or decline the request from the nest.`,
688
+ rows: governanceRows
689
+ },
690
+ deletion_declined: {
691
+ heading: (x) => `Your deletion request for ${x.title} was declined`,
692
+ detailsHeader: "Document details",
693
+ linkLabel: "Open document",
694
+ lineT: (x) => `Your request to delete ${x.title} was declined${x.byT}${x.noteT}. The document stays.`,
695
+ lineH: (x) => `Your request to delete ${x.doc} was declined${x.byH}${x.noteH}. The document stays.`,
696
+ rows: governanceRows
697
+ },
698
+ deletion_deleted: {
699
+ heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
700
+ detailsHeader: "Document details",
701
+ linkLabel: "Open nest",
702
+ lineT: (x) => `${x.title} has been deleted${x.byT}${x.noteT} \u2014 your deletion request was accepted.`,
703
+ lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
704
+ rows: governanceRows
705
+ },
706
+ steward: {
707
+ heading: (x) => `You're now a steward of ${x.title}`,
708
+ detailsHeader: "Details",
709
+ linkLabel: "Open nest",
710
+ lineT: (x) => `You've been assigned as a ${x.d.permission || "reviewer"} steward of ${x.d.scopeLabel || x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open the nest below to start reviewing.`,
711
+ lineH: (x) => `You've been assigned as a <strong>${escapeHtml(x.d.permission || "reviewer")}</strong> steward of ${x.d.scopeLabel ? escapeHtml(x.d.scopeLabel) : x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open the nest below to start reviewing.`,
712
+ rows: (x) => [
713
+ plainRow("Nest", x.d.path),
714
+ ...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : [],
715
+ ...x.d.scopeLabel ? [plainRow("Scope", cap(x.d.scopeLabel))] : []
716
+ ]
717
+ }
718
+ };
719
+ function renderEmail(d) {
720
+ const meta = STATUS_META[d.status];
721
+ const t = TEMPLATES[d.status];
722
+ const title = d.docTitle.replace(/[\r\n]+/g, " ").trim();
723
+ const vLabel = d.version != null ? `v${d.version}` : "";
724
+ const ctx = {
725
+ d,
726
+ title,
727
+ doc: `<strong>${escapeHtml(title)}</strong>`,
728
+ byT: d.actor ? ` by ${d.actor}` : "",
729
+ byH: d.actor ? ` by ${escapeHtml(d.actor)}` : "",
730
+ noteT: d.note ? ` with the note: "${d.note}"` : "",
731
+ noteH: d.note ? ` with the note: &ldquo;${escapeHtml(d.note)}&rdquo;` : "",
732
+ vT: vLabel ? ` ${vLabel}` : "",
733
+ vH: vLabel ? ` ${vLabel}` : "",
734
+ meta,
735
+ statusBadge: `<span style="display:inline-block;padding:2px 9px;border-radius:9999px;background:#f3f4f6;color:${meta.color};font-size:12px;font-weight:600;">${meta.label}</span>`,
736
+ codeValue: (v) => `<span style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:600;color:#111827;background:#f3f4f6;padding:2px 8px;border-radius:6px;">${escapeHtml(v)}</span>`
737
+ };
738
+ const heading = t.heading(ctx).replace(/[\r\n]+/g, " ").trim();
739
+ const subject = heading;
740
+ const detailsHeader = t.detailsHeader;
741
+ const linkLabel = t.linkLabel;
742
+ const lineH = t.lineH(ctx);
743
+ const rows = t.rows(ctx);
744
+ const detailLines = [
745
+ `${detailsHeader}:`,
746
+ ...rows.map((r) => ` ${r.label}: ${r.text}`),
747
+ ...d.link ? [` ${linkLabel}: ${d.link}`] : []
748
+ ];
749
+ const text = ["Hi,", "", t.lineT(ctx), "", ...detailLines, "", "Regards,", "Community Nest"].join("\n");
750
+ const detailRows = rows.map((r) => detailRow(r.label, r.html)).join("");
751
+ const buttonRow = d.link ? `<tr><td style="padding:20px 24px 0;">
752
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
753
+ <td bgcolor="${meta.color}" style="border-radius:6px;">
754
+ <a href="${escapeHtml(d.link)}" target="_blank" style="display:inline-block;padding:10px 20px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;">${linkLabel}</a>
755
+ </td>
756
+ </tr></table>
757
+ </td></tr>` : "";
758
+ const html = `<div style="margin:0;padding:24px;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#111827;">
759
+ <table role="presentation" width="480" cellpadding="0" cellspacing="0" border="0" align="center" style="width:100%;max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;">
760
+ <tr><td style="padding:20px 24px 0;">
761
+ <div style="font-size:12px;font-weight:700;color:#6b7280;letter-spacing:0.06em;text-transform:uppercase;">ContextNest</div>
762
+ </td></tr>
763
+ <tr><td style="padding:10px 24px 0;">
764
+ <h1 style="margin:0;font-size:19px;line-height:1.35;font-weight:700;color:#111827;">${escapeHtml(heading)}</h1>
765
+ </td></tr>
766
+ <tr><td style="padding:16px 24px 0;font-size:14px;line-height:1.6;color:#374151;">
767
+ Hi,<br><br>${lineH}
768
+ </td></tr>
769
+ <tr><td style="padding:20px 24px 0;">
770
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #eef0f2;border-radius:8px;">
771
+ <tr><td style="padding:12px 16px;">
772
+ <div style="font-size:11px;font-weight:600;color:#9ca3af;letter-spacing:0.04em;text-transform:uppercase;padding-bottom:4px;">${detailsHeader}</div>
773
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${detailRows}</table>
774
+ </td></tr>
775
+ </table>
776
+ </td></tr>
777
+ ${buttonRow}
778
+ <tr><td style="padding:22px 24px 24px;font-size:14px;line-height:1.6;color:#374151;">
779
+ Regards,<br>Community Nest
780
+ </td></tr>
781
+ </table>
782
+ </div>`;
783
+ return { subject, text, html };
784
+ }
785
+ function renderHtml(message, nestName2) {
786
+ const lead = message.match(/^(:[a-z_]+:)\s*/);
787
+ const meta = lead && EVENT[lead[1]] || { icon: "\u{1F514}", color: "#475569" };
788
+ const rest = lead ? message.slice(lead[0].length) : message;
789
+ const body = escapeHtml(rest).replace(/\*([^*]+)\*/g, "<strong>$1</strong>").replace(/\n/g, "<br>");
790
+ const safeNest = escapeHtml(nestName2);
791
+ return `<div style="background:#f6f8fa;padding:24px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
792
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;">
793
+ <tr><td style="padding:20px 24px 16px;border-bottom:1px solid #f0f0f0;">
794
+ <div style="font-size:15px;font-weight:700;color:#111827;letter-spacing:-0.01em;">ContextNest</div>
795
+ <div style="font-size:13px;color:#6b7280;margin-top:2px;">${safeNest}</div>
796
+ </td></tr>
797
+ <tr><td style="padding:24px;">
798
+ <table role="presentation" cellpadding="0" cellspacing="0"><tr>
799
+ <td valign="top" style="font-size:22px;line-height:1;padding-right:14px;">${meta.icon}</td>
800
+ <td valign="top" style="font-size:15px;line-height:1.55;color:#111827;border-left:3px solid ${meta.color};padding-left:14px;">${body}</td>
801
+ </tr></table>
802
+ </td></tr>
803
+ </table>
804
+ </div>`;
776
805
  }
777
- async function deleteTeam(params) {
778
- const row = await loadTeamRow(params.teamId);
779
- if (managementRank(row, params.callerUserId, !!params.allowAdmin) < OWNER_RANK) {
780
- throw new ForbiddenError("Only the team owner can delete this team.");
806
+
807
+ // src/notify/email.ts
808
+ var transporterPromise = null;
809
+ var configuredUrl = null;
810
+ var warnedOnce = false;
811
+ var SMTP_TIMEOUT_DEFAULTS = [
812
+ ["connectionTimeout", 5e3],
813
+ ["greetingTimeout", 5e3],
814
+ ["socketTimeout", 1e4]
815
+ ];
816
+ function withTimeouts(url) {
817
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
818
+ const missing = SMTP_TIMEOUT_DEFAULTS.filter(
819
+ ([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
820
+ ).map(([name, ms]) => `${name}=${ms}`);
821
+ if (missing.length === 0) return url;
822
+ return url + (url.includes("?") ? "&" : "?") + missing.join("&");
823
+ }
824
+ var POOL_DEFAULTS = [
825
+ ["pool", "true"],
826
+ ["maxConnections", "1"],
827
+ ["rateDelta", "1000"],
828
+ ["rateLimit", "8"]
829
+ // messages per second
830
+ ];
831
+ function withPool(url) {
832
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
833
+ const missing = POOL_DEFAULTS.filter(
834
+ ([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
835
+ ).map(([name, val]) => `${name}=${val}`);
836
+ if (missing.length === 0) return url;
837
+ return url + (url.includes("?") ? "&" : "?") + missing.join("&");
838
+ }
839
+ async function verifySmtp(url) {
840
+ let transporter = null;
841
+ try {
842
+ const nodemailer = await import("nodemailer");
843
+ transporter = nodemailer.default.createTransport(withTimeouts(url), {
844
+ disableFileAccess: true,
845
+ disableUrlAccess: true
846
+ });
847
+ await transporter.verify();
848
+ return { ok: true };
849
+ } catch (err) {
850
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
851
+ } finally {
852
+ try {
853
+ transporter?.close?.();
854
+ } catch {
855
+ }
781
856
  }
782
- const db = getDb();
783
- await db.transaction(async (tx) => {
784
- await tx.run("DELETE FROM teams WHERE id = ?", [params.teamId]);
785
- await removeTeamRefs(tx, params.teamId);
786
- });
787
857
  }
788
- async function addMember(params) {
789
- assertValidRole(params.role);
790
- const row = await loadTeamRow(params.teamId);
791
- const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
792
- if (rank < ROLE_RANK.editor) {
793
- throw new ForbiddenError(
794
- "Only an editor or admin member (or the owner) can add people to this team."
858
+ async function getTransporter() {
859
+ const url = config.SMTP_URL;
860
+ if (!url) return null;
861
+ if (!transporterPromise || configuredUrl !== url) {
862
+ configuredUrl = url;
863
+ transporterPromise = import("nodemailer").then(
864
+ (nodemailer) => (
865
+ // withPool → connection pool + per-second rate limit, so a burst queues
866
+ // instead of tripping the provider's "too many emails" 550.
867
+ nodemailer.default.createTransport(withPool(withTimeouts(url)), {
868
+ // Per-message defaults (belt-and-braces; also set on each sendMail).
869
+ disableFileAccess: true,
870
+ disableUrlAccess: true
871
+ })
872
+ )
795
873
  );
874
+ transporterPromise.catch(() => {
875
+ transporterPromise = null;
876
+ configuredUrl = null;
877
+ });
796
878
  }
797
- if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
798
- throw new ForbiddenError("You can't give a member a higher role than your own.");
799
- }
800
- if (!params.userId && params.email && !isEmailish(params.email)) {
801
- throw new ValidationError("Enter a valid email address.");
802
- }
803
- const { userId: resolvedId, email: resolvedEmail } = await resolveMemberIdentity(params);
804
- await mutateMembers(params.teamId, (members) => {
805
- if (members.some((m) => m.userId === resolvedId)) {
806
- throw new ConflictError(
807
- `${resolvedEmail || "This user"} is already a member of this team. Change their role instead of adding them again.`
879
+ return transporterPromise;
880
+ }
881
+ async function notifyEmailForNest(nestId, message, details) {
882
+ const from = config.NOTIFY_EMAIL_FROM;
883
+ const to = config.NOTIFY_EMAIL_TO;
884
+ if (!config.SMTP_URL || !from || !to) return;
885
+ try {
886
+ const transporter = await getTransporter();
887
+ if (!transporter) return;
888
+ let subject;
889
+ let text;
890
+ let html;
891
+ if (details) {
892
+ ({ subject, text, html } = renderEmail(details));
893
+ } else {
894
+ let name = nestId;
895
+ try {
896
+ const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
897
+ nestId
898
+ ]);
899
+ if (row?.name) name = row.name;
900
+ } catch {
901
+ }
902
+ const safeName = name.replace(/[\r\n]+/g, " ").trim();
903
+ subject = `ContextNest \xB7 ${safeName}`;
904
+ text = renderText(message);
905
+ html = renderHtml(message, safeName);
906
+ }
907
+ await transporter.sendMail({
908
+ from,
909
+ to,
910
+ subject,
911
+ text,
912
+ html,
913
+ disableFileAccess: true,
914
+ disableUrlAccess: true
915
+ });
916
+ } catch (err) {
917
+ if (!warnedOnce) {
918
+ warnedOnce = true;
919
+ console.warn(
920
+ "[email] notification failed (suppressing further warnings):",
921
+ err instanceof Error ? err.message : err
808
922
  );
809
923
  }
810
- return [
811
- ...members,
812
- { userId: resolvedId, email: resolvedEmail, role: params.role }
813
- ];
814
- });
815
- return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
816
- }
817
- async function resolveMemberIdentity(params) {
818
- const db = getDb();
819
- if (params.userId) {
820
- const u = await db.get("SELECT email FROM users WHERE id = ?", [
821
- params.userId
822
- ]);
823
- if (!u) throw new ValidationError("User not found");
824
- return { userId: params.userId, email: normalizeEmail(u.email ?? "") };
825
924
  }
826
- const requested = params.email ? normalizeEmail(params.email) : void 0;
827
- if (!requested) {
828
- throw new ValidationError("user_id or email is required");
925
+ }
926
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
927
+ function recipientEnvelope(to, from) {
928
+ const recipients = Array.isArray(to) ? to : [to];
929
+ return recipients.length > 1 ? { to: from, bcc: recipients } : { to: recipients[0] };
930
+ }
931
+ function isRateLimited(err) {
932
+ const msg = String(err?.message ?? err);
933
+ const code = Number(err?.responseCode);
934
+ return /too many|rate limit|throttl|try again|4\.7\.\d|too many emails/i.test(msg) || code === 421 || code === 429 || code === 450 || code === 451 || code === 550 && /too many|rate/i.test(msg);
935
+ }
936
+ async function sendMailWithRetry(transporter, msg, attempts = 5) {
937
+ for (let i = 0; ; i++) {
938
+ try {
939
+ return await transporter.sendMail(msg);
940
+ } catch (err) {
941
+ if (i >= attempts - 1 || !isRateLimited(err)) throw err;
942
+ const wait = 1e3 * 2 ** i;
943
+ await sleep(wait);
944
+ }
829
945
  }
830
- const existing = await db.get(
831
- "SELECT id, email FROM users WHERE LOWER(email) = ?",
832
- [requested]
833
- );
834
- if (existing) {
835
- return {
836
- userId: existing.id,
837
- email: normalizeEmail(existing.email ?? requested)
838
- };
946
+ }
947
+ async function sendEmailToRecipient(to, details) {
948
+ const from = config.NOTIFY_EMAIL_FROM;
949
+ const hasRecipient = Array.isArray(to) ? to.length > 0 : !!to;
950
+ if (!config.SMTP_URL || !from || !hasRecipient) return;
951
+ try {
952
+ const transporter = await getTransporter();
953
+ if (!transporter) return;
954
+ const { subject, text, html } = renderEmail(details);
955
+ await sendMailWithRetry(transporter, {
956
+ from,
957
+ ...recipientEnvelope(to, from),
958
+ subject,
959
+ text,
960
+ html,
961
+ disableFileAccess: true,
962
+ disableUrlAccess: true
963
+ });
964
+ } catch (err) {
965
+ if (!warnedOnce) {
966
+ warnedOnce = true;
967
+ console.warn(
968
+ "[email] notification failed (suppressing further warnings):",
969
+ err instanceof Error ? err.message : err
970
+ );
971
+ }
839
972
  }
840
- const { hashPassword } = await import("./keys-73STFJJB.js");
841
- const userId = uuid();
842
- await db.run(
843
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
844
- [userId, requested, null, await hashPassword(uuid())]
845
- );
846
- return { userId, email: requested };
847
973
  }
848
- async function replaceTeamRoster(params) {
849
- for (const m of params.members) assertValidRole(m.role);
850
- const row = await loadTeamRow(params.teamId);
851
- if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
852
- throw new ForbiddenError(
853
- "Only a team admin or the owner can replace this team's roster."
854
- );
974
+
975
+ // src/nests/service.ts
976
+ import { rmSync as rmSync2, mkdirSync } from "fs";
977
+ import { v4 as uuid2 } from "uuid";
978
+ import { NestStorage as NestStorage3 } from "@promptowl/contextnest-engine";
979
+
980
+ // src/fs/vault-import.ts
981
+ import { rmSync } from "fs";
982
+ import { writeFile, mkdir } from "fs/promises";
983
+ import { join as join2, dirname, isAbsolute } from "path";
984
+ function isSkippedSegment(segment) {
985
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
986
+ }
987
+ var ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".versions", ".context"]);
988
+ function safeSegments(rel) {
989
+ if (!rel || isAbsolute(rel)) return null;
990
+ const segs = rel.split(/[\\/]/).filter(Boolean);
991
+ if (!segs.length) return null;
992
+ for (const s of segs) {
993
+ if (s === "..") return null;
994
+ if (!ALLOWED_DOT_DIRS.has(s) && isSkippedSegment(s)) return null;
855
995
  }
856
- await mutateMembers(
857
- params.teamId,
858
- () => params.members.map((m) => ({
859
- userId: m.userId,
860
- email: m.email,
861
- role: m.role
862
- }))
863
- );
864
- return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
996
+ return segs;
865
997
  }
866
- async function updateMemberRole(params) {
867
- assertValidRole(params.role);
868
- const row = await loadTeamRow(params.teamId);
869
- const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
870
- if (rank < ROLE_RANK.admin) {
871
- throw new ForbiddenError("Only a team admin or the owner can change roles.");
998
+ var WRITE_CONCURRENCY = 32;
999
+ async function writeImportedFiles(dest, files) {
1000
+ const targets = [];
1001
+ const skipped = [];
1002
+ for (const f of files) {
1003
+ const segs = safeSegments(f?.path || "");
1004
+ if (!segs) {
1005
+ skipped.push(f?.path || "(empty)");
1006
+ continue;
1007
+ }
1008
+ targets.push({ full: join2(dest, ...segs), content: f.content ?? "" });
872
1009
  }
873
- if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
874
- throw new ForbiddenError("You can't set a role higher than your own.");
1010
+ const dirs = new Set(targets.map((t) => dirname(t.full)));
1011
+ await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
1012
+ let written = 0;
1013
+ for (let i = 0; i < targets.length; i += WRITE_CONCURRENCY) {
1014
+ const batch = targets.slice(i, i + WRITE_CONCURRENCY);
1015
+ await Promise.all(
1016
+ batch.map(async (t) => {
1017
+ await writeFile(t.full, t.content, "utf-8");
1018
+ written++;
1019
+ })
1020
+ );
875
1021
  }
876
- await mutateMembers(params.teamId, (members) => {
877
- const member = members.find((m) => m.userId === params.userId);
878
- if (!member) throw new NotFoundError("Team member not found");
879
- member.role = params.role;
880
- return members;
881
- });
882
- return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
883
- }
884
- async function removeMember(params) {
885
- const row = await loadTeamRow(params.teamId);
886
- if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
887
- throw new ForbiddenError("Only a team admin or the owner can remove members.");
1022
+ console.log(
1023
+ `[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
1024
+ );
1025
+ if (skipped.length) {
1026
+ console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
888
1027
  }
889
- await mutateMembers(params.teamId, (members) => {
890
- const next = members.filter((m) => m.userId !== params.userId);
891
- if (next.length === members.length) {
892
- throw new NotFoundError("Team member not found");
893
- }
894
- return next;
895
- });
896
- return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
897
- }
898
- async function mutateMembers(teamId, mutate) {
899
- const db = getDb();
900
- await db.transaction(async (tx) => {
901
- const row = await tx.get("SELECT members FROM teams WHERE id = ?", [
902
- teamId
903
- ]);
904
- if (!row) throw new NotFoundError("Team not found");
905
- const next = mutate(parseMembers(row.members));
906
- await tx.run("UPDATE teams SET members = ?, updated_at = ? WHERE id = ?", [
907
- JSON.stringify(next),
908
- (/* @__PURE__ */ new Date()).toISOString(),
909
- teamId
910
- ]);
911
- });
1028
+ return written;
912
1029
  }
913
- function parseSharedTeams(json) {
914
- if (!json) return [];
1030
+ function discardImportDir(dest) {
915
1031
  try {
916
- const parsed = JSON.parse(json);
917
- return Array.isArray(parsed) ? parsed : [];
1032
+ rmSync(dest, { recursive: true, force: true });
918
1033
  } catch {
919
- return [];
920
1034
  }
921
1035
  }
922
- async function listNestTeams(nestId) {
923
- const db = getDb();
924
- const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
925
- nestId
926
- ]);
927
- if (!row) throw new NotFoundError("Nest not found");
928
- return parseSharedTeams(row.shared_teams);
1036
+
1037
+ // src/shared/paths.ts
1038
+ import { join as join3 } from "path";
1039
+ function nestStorageRoot() {
1040
+ return config.NEST_STORAGE_ROOT;
929
1041
  }
930
- async function shareTeamWithNest(params) {
931
- const db = getDb();
932
- return db.transaction(async (tx) => {
933
- const team = await tx.get(
934
- "SELECT id, name, owner_id, members FROM teams WHERE id = ?",
935
- [params.teamId]
936
- );
937
- if (!team) throw new NotFoundError("Team not found");
938
- if (managementRank(team, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.editor) {
939
- throw new ForbiddenError(
940
- "You need to be an editor, admin, or owner of a team to share it. View-only members can't."
941
- );
942
- }
943
- const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
944
- params.nestId
945
- ]);
946
- if (!nest) throw new NotFoundError("Nest not found");
947
- const refs = parseSharedTeams(nest.shared_teams);
948
- if (refs.some((r) => r.teamId === params.teamId)) {
949
- throw new ConflictError("This team is already shared with the nest.");
950
- }
951
- refs.push({ teamId: team.id, teamName: team.name });
952
- await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
953
- JSON.stringify(refs),
954
- params.nestId
955
- ]);
956
- const hasGovernanceRole = parseMembers(team.members).some(
957
- (m) => m.role === "viewer" || m.role === "editor"
958
- );
959
- if (hasGovernanceRole) {
960
- await tx.run("UPDATE nests SET stewardship_enabled = 1 WHERE id = ?", [
961
- params.nestId
962
- ]);
963
- }
964
- return refs;
965
- });
1042
+ function resolveNestPath(nestId) {
1043
+ return join3(nestStorageRoot(), nestId);
966
1044
  }
967
- async function unshareTeamFromNest(params) {
968
- const db = getDb();
969
- return db.transaction(async (tx) => {
970
- const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
971
- params.nestId
972
- ]);
973
- if (!nest) throw new NotFoundError("Nest not found");
974
- const refs = parseSharedTeams(nest.shared_teams);
975
- const next = refs.filter((r) => r.teamId !== params.teamId);
976
- if (next.length === refs.length) {
977
- throw new NotFoundError("Team is not shared with this nest.");
1045
+
1046
+ // src/governance/teams-service.ts
1047
+ import { v4 as uuid } from "uuid";
1048
+
1049
+ // src/nodes/engine.ts
1050
+ import {
1051
+ NestStorage as NestStorage2,
1052
+ GraphQueryEngine,
1053
+ VersionManager
1054
+ } from "@promptowl/contextnest-engine";
1055
+
1056
+ // src/nodes/flat-storage.ts
1057
+ import { readdirSync } from "fs";
1058
+ import { join as join4, relative, sep } from "path";
1059
+ import { NestStorage } from "@promptowl/contextnest-engine";
1060
+ var META_FILES = /* @__PURE__ */ new Set([
1061
+ "INDEX.md",
1062
+ "CONTEXT.md",
1063
+ "CLAUDE.md",
1064
+ "GEMINI.md",
1065
+ "AGENTS.md",
1066
+ "QWEN.md"
1067
+ ]);
1068
+ var FlatNestStorage = class extends NestStorage {
1069
+ async discoverDocuments() {
1070
+ const root = this.root;
1071
+ let entries;
1072
+ try {
1073
+ entries = readdirSync(root, { recursive: true, withFileTypes: true });
1074
+ } catch (err) {
1075
+ console.error("[FlatNestStorage] cannot read folder", root, err);
1076
+ return [];
978
1077
  }
979
- await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
980
- JSON.stringify(next),
981
- params.nestId
982
- ]);
983
- return next;
984
- });
985
- }
986
- async function refreshTeamNameRefs(tx, teamId, newName) {
987
- const nests = await tx.all(
988
- "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
989
- [`%${teamId}%`]
990
- );
991
- for (const n of nests) {
992
- const refs = parseSharedTeams(n.shared_teams);
993
- let changed = false;
994
- for (const r of refs) {
995
- if (r.teamId === teamId && r.teamName !== newName) {
996
- r.teamName = newName;
997
- changed = true;
998
- }
1078
+ const ids = [];
1079
+ for (const e of entries) {
1080
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
1081
+ if (META_FILES.has(e.name)) continue;
1082
+ const dir = e.parentPath ?? e.path ?? root;
1083
+ const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
1084
+ if (rel.split("/").some(isSkippedSegment)) continue;
1085
+ ids.push(rel.replace(/\.md$/, ""));
999
1086
  }
1000
- if (changed) {
1001
- await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1002
- JSON.stringify(refs),
1003
- n.id
1004
- ]);
1087
+ const nodes = [];
1088
+ for (const id of ids.sort()) {
1089
+ try {
1090
+ nodes.push(await this.readDocument(id));
1091
+ } catch (err) {
1092
+ console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1093
+ }
1005
1094
  }
1095
+ return nodes;
1006
1096
  }
1007
- }
1008
- async function removeTeamRefs(tx, teamId) {
1009
- const nests = await tx.all(
1010
- "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
1011
- [`%${teamId}%`]
1012
- );
1013
- for (const n of nests) {
1014
- const refs = parseSharedTeams(n.shared_teams);
1015
- const next = refs.filter((r) => r.teamId !== teamId);
1016
- if (next.length !== refs.length) {
1017
- await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1018
- JSON.stringify(next),
1019
- n.id
1020
- ]);
1097
+ };
1098
+
1099
+ // src/nodes/engine.ts
1100
+ var NestEngineCache = class {
1101
+ cache = /* @__PURE__ */ new Map();
1102
+ async get(nestId) {
1103
+ let engine = this.cache.get(nestId);
1104
+ if (!engine) {
1105
+ const nestPath = resolveNestPath(nestId);
1106
+ const storage = await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath);
1107
+ const query = new GraphQueryEngine(storage);
1108
+ const versions = new VersionManager(storage);
1109
+ engine = { storage, query, versions };
1110
+ this.cache.set(nestId, engine);
1021
1111
  }
1112
+ return engine;
1022
1113
  }
1023
- }
1024
- async function resolveTeamRolesForUser(nestId, userId) {
1025
- const refs = await listNestTeams(nestId).catch(() => []);
1026
- if (refs.length === 0) return [];
1027
- const db = getDb();
1028
- const roles = /* @__PURE__ */ new Set();
1029
- for (const ref of refs) {
1030
- const row = await db.get("SELECT members FROM teams WHERE id = ?", [
1031
- ref.teamId
1032
- ]);
1033
- if (!row) continue;
1034
- const member = parseMembers(row.members).find((m) => m.userId === userId);
1035
- if (member) roles.add(member.role);
1114
+ evict(nestId) {
1115
+ this.cache.delete(nestId);
1036
1116
  }
1037
- return [...roles];
1038
- }
1117
+ };
1118
+ var engineCache = new NestEngineCache();
1039
1119
 
1040
- // src/governance/roles.ts
1041
- function collabPermToRole(permission) {
1042
- switch (permission) {
1043
- case "owner":
1044
- return "owner";
1045
- case "admin":
1046
- return "admin";
1047
- case "write":
1048
- return "editor";
1049
- case "read":
1050
- return "viewer";
1051
- default:
1052
- return null;
1120
+ // src/governance/title-resolver.ts
1121
+ async function buildTitleMap(nestId) {
1122
+ try {
1123
+ const { storage } = await engineCache.get(nestId);
1124
+ const docs = await storage.discoverDocuments();
1125
+ return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1126
+ } catch {
1127
+ return /* @__PURE__ */ new Map();
1053
1128
  }
1054
1129
  }
1055
- function teamRoleToPermission(role) {
1056
- switch (role) {
1057
- case "admin":
1058
- return "admin";
1059
- case "editor":
1060
- return "write";
1061
- case "viewer":
1062
- return "read";
1063
- default:
1064
- return null;
1065
- }
1130
+ function folderForNode(nodeId) {
1131
+ const segments = nodeId.split("/");
1132
+ const root = segments.indexOf("nodes");
1133
+ return segments.slice(root + 1, -1).join("/");
1066
1134
  }
1067
- var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1068
- var canViewWith = (roles) => roles.length > 0;
1069
- var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1070
- var PRECEDENCE = [
1071
- "owner",
1072
- "admin",
1073
- "editor",
1074
- "reviewer",
1075
- "viewer"
1076
- ];
1077
- function primaryRole(roles) {
1078
- for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1079
- return null;
1135
+ async function describeNode(nestId, nodeId) {
1136
+ const title = await titleForNode(nestId, nodeId);
1137
+ const folder = folderForNode(nodeId);
1138
+ return folder ? `${title} (in ${folder})` : title;
1080
1139
  }
1081
-
1082
- // src/shared/access.ts
1083
- async function isServerAdminUserId(userId) {
1084
- if (await isLicenseAdminUserId(userId)) return true;
1085
- const row = await getDb().get(
1086
- "SELECT email FROM users WHERE id = ?",
1087
- [userId]
1088
- );
1089
- return !!row?.email && isSuperAdmin(row.email);
1140
+ async function titleForNode(nestId, nodeId) {
1141
+ const titles = await buildTitleMap(nestId);
1142
+ return titles.get(nodeId) || titleFallback(titles, nodeId);
1090
1143
  }
1091
- var PERMISSION_LEVELS = {
1092
- none: 0,
1093
- read: 1,
1094
- write: 2,
1095
- admin: 3,
1096
- owner: 4
1097
- };
1098
- async function resolveNestPermission(nestId, userId) {
1099
- const db = getDb();
1100
- const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1101
- if (!nest) return "none";
1102
- if (nest.user_id === userId) return "owner";
1103
- if (nest.user_id === ANON_USER_ID && await isLicenseAdminUserId(userId)) {
1104
- return "owner";
1105
- }
1106
- if (await isLicenseAdminUserId(userId)) return "admin";
1107
- const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1108
- if (caller?.email && isSuperAdmin(caller.email)) {
1109
- return "admin";
1144
+ function titleFallback(titles, nodeId) {
1145
+ const at = nodeId.indexOf("nodes/");
1146
+ if (at > 0) {
1147
+ const normalized = nodeId.slice(at);
1148
+ const hit = titles.get(normalized);
1149
+ if (hit) return hit;
1110
1150
  }
1111
- const higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1112
- let best = "none";
1113
- const directGrant = await db.get(
1114
- "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1115
- [nestId, userId]
1116
- );
1117
- if (directGrant) best = higher(best, directGrant.permission);
1118
- if (best === "admin") return best;
1119
- for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1120
- best = higher(best, teamRoleToPermission(role));
1151
+ return nodeId.split("/").pop() || nodeId;
1152
+ }
1153
+ function prettyFolderPath(path) {
1154
+ return path.replace(/^nodes\//, "").split("/").filter(Boolean).map(
1155
+ (seg) => seg.split(/[-_]/).map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" ")
1156
+ ).join(" / ");
1157
+ }
1158
+ async function nestName(nestId) {
1159
+ try {
1160
+ const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1161
+ nestId
1162
+ ]);
1163
+ return row?.name || nestId;
1164
+ } catch {
1165
+ return nestId;
1121
1166
  }
1122
- if (best === "admin") return best;
1123
- const stewardGrant = await db.get(
1124
- `SELECT 1 FROM stewards s
1125
- JOIN users u ON u.id = ?
1126
- WHERE s.nest_id = ? AND s.is_active = 1
1127
- AND LOWER(s.user_email) = LOWER(u.email)
1128
- LIMIT 1`,
1129
- [userId, nestId]
1130
- );
1131
- if (stewardGrant) best = higher(best, "read");
1132
- if (nest.visibility === "public") best = higher(best, "read");
1133
- return best;
1134
1167
  }
1135
- function permissionLevel(p) {
1136
- return PERMISSION_LEVELS[p] ?? 0;
1168
+ function docLink(nestId, nodeId, baseUrl) {
1169
+ const base = baseUrl || config.PUBLIC_BASE_URL;
1170
+ if (!base) return "";
1171
+ const q = new URLSearchParams({ nest: nestId });
1172
+ if (nodeId) q.set("doc", nodeId);
1173
+ return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1137
1174
  }
1138
- async function isPublicReader(nestId, userId) {
1139
- const db = getDb();
1140
- const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1141
- if (!nest) return false;
1142
- if (nest.visibility !== "public") return false;
1143
- if (nest.user_id === userId) return false;
1144
- const directGrant = await db.get(
1145
- "SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1146
- [nestId, userId]
1147
- );
1148
- if (directGrant) return false;
1149
- if (await isLicenseAdminUserId(userId)) return false;
1150
- const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1151
- if (caller?.email && isSuperAdmin(caller.email)) return false;
1152
- const stewardGrant = await db.get(
1153
- `SELECT 1 FROM stewards s
1154
- JOIN users u ON u.id = ?
1155
- WHERE s.nest_id = ? AND s.is_active = 1
1156
- AND LOWER(s.user_email) = LOWER(u.email)
1157
- LIMIT 1`,
1158
- [userId, nestId]
1159
- );
1160
- if (stewardGrant) return false;
1161
- if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
1162
- return true;
1175
+ async function buildDocContext(nestId, nodeId, baseUrl) {
1176
+ const titles = await buildTitleMap(nestId);
1177
+ const docTitle = titles.get(nodeId) || nodeId;
1178
+ const folder = folderForNode(nodeId);
1179
+ const name = await nestName(nestId);
1180
+ return {
1181
+ docTitle,
1182
+ // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1183
+ label: folder ? `${docTitle} (in ${folder})` : docTitle,
1184
+ path: folder ? `${name} / ${folder}` : name,
1185
+ link: docLink(nestId, nodeId, baseUrl)
1186
+ };
1163
1187
  }
1164
-
1165
- // src/nests/service.ts
1166
- async function isImportedNest(nestId) {
1167
- const row = await getDb().get(
1168
- "SELECT is_imported FROM nests WHERE id = ?",
1169
- [nestId]
1170
- );
1171
- return !!row?.is_imported;
1188
+
1189
+ // src/governance/teams-service.ts
1190
+ var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
1191
+ function parseMembers(json) {
1192
+ if (!json) return [];
1193
+ try {
1194
+ const parsed = JSON.parse(json);
1195
+ return Array.isArray(parsed) ? parsed : [];
1196
+ } catch {
1197
+ return [];
1198
+ }
1172
1199
  }
1173
- function toSlug(name) {
1174
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1200
+ function rowToTeam(row) {
1201
+ return {
1202
+ id: row.id,
1203
+ name: row.name,
1204
+ owner_id: row.owner_id,
1205
+ members: parseMembers(row.members),
1206
+ source: row.source ?? null,
1207
+ external_id: row.external_id ?? null,
1208
+ created_at: row.created_at,
1209
+ updated_at: row.updated_at
1210
+ };
1175
1211
  }
1176
- async function uniqueNestName(userId, baseName) {
1212
+ async function enrichTeam(team) {
1177
1213
  const db = getDb();
1178
- let candidate = baseName;
1179
- let n = 0;
1180
- while (n < 1e3) {
1181
- const slug = toSlug(candidate);
1182
- const hit = await db.get(
1183
- "SELECT 1 FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1184
- [userId, slug]
1214
+ const ids = team.members.map((m) => m.userId);
1215
+ const invited = /* @__PURE__ */ new Map();
1216
+ if (ids.length) {
1217
+ const rows = await db.all(
1218
+ `SELECT id, is_invited FROM users WHERE id IN (${ids.map(() => "?").join(",")})`,
1219
+ ids
1185
1220
  );
1186
- if (!hit) return candidate;
1187
- n++;
1188
- candidate = `${baseName} (${n})`;
1221
+ for (const r of rows) invited.set(r.id, r.is_invited);
1189
1222
  }
1190
- return candidate;
1223
+ const owner = await db.get("SELECT email FROM users WHERE id = ?", [
1224
+ team.owner_id
1225
+ ]);
1226
+ return {
1227
+ ...team,
1228
+ owner_email: owner?.email,
1229
+ // Unknown id (deleted user) → treat as not registered.
1230
+ members: team.members.map((m) => ({
1231
+ ...m,
1232
+ registered: (invited.get(m.userId) ?? 1) === 0
1233
+ }))
1234
+ };
1191
1235
  }
1192
- async function isStewardshipEnabled(nestId) {
1193
- const db = getDb();
1194
- const row = await db.get(
1195
- "SELECT stewardship_enabled FROM nests WHERE id = ?",
1196
- [nestId]
1197
- );
1198
- return !!row?.stewardship_enabled;
1236
+ function assertValidRole(role) {
1237
+ if (!VALID_TEAM_ROLES.includes(role)) {
1238
+ throw new ValidationError(
1239
+ "role must be admin, editor, or viewer"
1240
+ );
1241
+ }
1199
1242
  }
1200
- async function setStewardshipEnabled(nestId, enabled) {
1243
+ async function loadTeamRow(teamId) {
1201
1244
  const db = getDb();
1202
- await db.run("UPDATE nests SET stewardship_enabled = ? WHERE id = ?", [
1203
- enabled ? 1 : 0,
1204
- nestId
1245
+ const row = await db.get("SELECT * FROM teams WHERE id = ?", [
1246
+ teamId
1205
1247
  ]);
1248
+ if (!row) throw new NotFoundError("Team not found");
1249
+ return row;
1206
1250
  }
1207
- async function nestAllowsSelfApprove(nestId) {
1251
+ var ROLE_RANK = {
1252
+ viewer: 1,
1253
+ editor: 2,
1254
+ admin: 3
1255
+ };
1256
+ var OWNER_RANK = 4;
1257
+ function managementRank(row, callerUserId, allowAdmin) {
1258
+ if (row.owner_id === callerUserId || allowAdmin) return OWNER_RANK;
1259
+ const m = parseMembers(row.members).find((x) => x.userId === callerUserId);
1260
+ return m ? ROLE_RANK[m.role] ?? 0 : 0;
1261
+ }
1262
+ async function createTeam(params) {
1263
+ const name = params.name?.trim();
1264
+ if (!name) throw new ValidationError("Team name is required");
1265
+ const db = getDb();
1266
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1267
+ const id = uuid();
1268
+ await db.run(
1269
+ "INSERT INTO teams (id, name, owner_id, members, source, external_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1270
+ [
1271
+ id,
1272
+ name,
1273
+ params.ownerId,
1274
+ "[]",
1275
+ params.source ?? null,
1276
+ params.externalId ?? null,
1277
+ now,
1278
+ now
1279
+ ]
1280
+ );
1281
+ return enrichTeam(rowToTeam(await loadTeamRow(id)));
1282
+ }
1283
+ async function findTeamByExternalId(params) {
1208
1284
  const db = getDb();
1209
1285
  const row = await db.get(
1210
- "SELECT allow_self_approve FROM nests WHERE id = ?",
1211
- [nestId]
1286
+ "SELECT * FROM teams WHERE owner_id = ? AND source = ? AND external_id = ?",
1287
+ [params.ownerId, params.source, params.externalId]
1212
1288
  );
1213
- return !!row?.allow_self_approve;
1289
+ return row ? enrichTeam(rowToTeam(row)) : null;
1214
1290
  }
1215
- async function setAllowSelfApprove(nestId, allow) {
1291
+ async function listTeamsForUser(userId) {
1216
1292
  const db = getDb();
1217
- await db.run("UPDATE nests SET allow_self_approve = ? WHERE id = ?", [
1218
- allow ? 1 : 0,
1219
- nestId
1220
- ]);
1293
+ const rows = await db.all(
1294
+ "SELECT * FROM teams WHERE owner_id = ? OR members LIKE ? ORDER BY created_at",
1295
+ [userId, `%${userId}%`]
1296
+ );
1297
+ const mine = rows.filter(
1298
+ (r) => r.owner_id === userId || parseMembers(r.members).some((m) => m.userId === userId)
1299
+ );
1300
+ return Promise.all(mine.map((r) => enrichTeam(rowToTeam(r))));
1221
1301
  }
1222
- async function disableStewardshipAndWipeGovernance(nestId) {
1302
+ async function getTeam(teamId) {
1303
+ return enrichTeam(rowToTeam(await loadTeamRow(teamId)));
1304
+ }
1305
+ async function renameTeam(params) {
1306
+ const name = params.name?.trim();
1307
+ if (!name) throw new ValidationError("Team name is required");
1308
+ const row = await loadTeamRow(params.teamId);
1309
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1310
+ throw new ForbiddenError("Only a team admin or the owner can rename this team.");
1311
+ }
1223
1312
  const db = getDb();
1224
- return db.transaction(async (tx) => {
1225
- const id = nestId;
1226
- const stewards = (await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id])).changes;
1227
- const pendingReviews = (await tx.run(
1228
- "DELETE FROM review_requests WHERE nest_id = ? AND status = 'pending'",
1229
- [id]
1230
- )).changes;
1231
- await tx.run("UPDATE nests SET stewardship_enabled = 0 WHERE id = ?", [id]);
1232
- return { stewards, pendingReviews };
1313
+ await db.transaction(async (tx) => {
1314
+ await tx.run("UPDATE teams SET name = ?, updated_at = ? WHERE id = ?", [
1315
+ name,
1316
+ (/* @__PURE__ */ new Date()).toISOString(),
1317
+ params.teamId
1318
+ ]);
1319
+ await refreshTeamNameRefs(tx, params.teamId, name);
1233
1320
  });
1321
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1234
1322
  }
1235
- async function renameNest(nestId, patch) {
1236
- const db = getDb();
1237
- const existing = await db.get("SELECT * FROM nests WHERE id = ?", [
1238
- nestId
1239
- ]);
1240
- if (!existing) {
1241
- throw new ValidationError("Nest not found");
1242
- }
1243
- const updates = [];
1244
- const params = [];
1245
- if (patch.name !== void 0) {
1246
- const trimmed = patch.name.trim();
1247
- if (!trimmed) throw new ValidationError("Name cannot be empty");
1248
- const slug = toSlug(trimmed);
1249
- if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
1250
- const conflict = await db.get(
1251
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1",
1252
- [existing.user_id, slug, nestId]
1253
- );
1254
- if (conflict) {
1255
- throw new ValidationError(
1256
- `Another nest already uses this name. Pick a different one.`
1257
- );
1258
- }
1259
- updates.push("name = ?", "slug = ?");
1260
- params.push(trimmed, slug);
1261
- }
1262
- if (patch.description !== void 0) {
1263
- updates.push("description = ?");
1264
- params.push(patch.description || null);
1265
- }
1266
- if (updates.length === 0) {
1267
- return existing;
1323
+ async function deleteTeam(params) {
1324
+ const row = await loadTeamRow(params.teamId);
1325
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < OWNER_RANK) {
1326
+ throw new ForbiddenError("Only the team owner can delete this team.");
1268
1327
  }
1269
- params.push(nestId);
1270
- await db.run(
1271
- `UPDATE nests SET ${updates.join(", ")} WHERE id = ?`,
1272
- params
1273
- );
1274
- return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
1275
- }
1276
- async function createNest(userId, name, description) {
1277
- const id = uuid2();
1278
1328
  const db = getDb();
1279
- const trimmed = name.trim();
1280
- if (!trimmed) throw new ValidationError("Name cannot be empty");
1281
- const slug = toSlug(trimmed);
1282
- if (!slug) {
1283
- throw new ValidationError(
1284
- "Name must contain at least one alphanumeric character"
1329
+ await db.transaction(async (tx) => {
1330
+ await tx.run("DELETE FROM teams WHERE id = ?", [params.teamId]);
1331
+ await removeTeamRefs(tx, params.teamId);
1332
+ });
1333
+ }
1334
+ async function addMember(params) {
1335
+ assertValidRole(params.role);
1336
+ const row = await loadTeamRow(params.teamId);
1337
+ const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
1338
+ if (rank < ROLE_RANK.editor) {
1339
+ throw new ForbiddenError(
1340
+ "Only an editor or admin member (or the owner) can add people to this team."
1285
1341
  );
1286
1342
  }
1287
- const conflict = await db.get(
1288
- "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1289
- [userId, slug]
1290
- );
1291
- if (conflict) {
1292
- throw new ValidationError(
1293
- "You already have a nest with this name. Pick a different one."
1294
- );
1343
+ if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
1344
+ throw new ForbiddenError("You can't give a member a higher role than your own.");
1295
1345
  }
1296
- const visibility = userId === ANON_USER_ID ? "public" : "private";
1297
- await db.run(
1298
- "INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
1299
- [id, userId, trimmed, slug, description || null, visibility]
1300
- );
1301
- const path = resolveNestPath(id);
1302
- mkdirSync(path, { recursive: true });
1303
- const storage = new NestStorage(path);
1304
- await storage.init(trimmed);
1305
- trackEvent("nest.create", { nestId: id, userId });
1306
- return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1346
+ if (!params.userId && params.email && !isEmailish(params.email)) {
1347
+ throw new ValidationError("Enter a valid email address.");
1348
+ }
1349
+ const { userId: resolvedId, email: resolvedEmail } = await resolveMemberIdentity(params);
1350
+ await mutateMembers(params.teamId, (members) => {
1351
+ if (members.some((m) => m.userId === resolvedId)) {
1352
+ throw new ConflictError(
1353
+ `${resolvedEmail || "This user"} is already a member of this team. Change their role instead of adding them again.`
1354
+ );
1355
+ }
1356
+ return [
1357
+ ...members,
1358
+ { userId: resolvedId, email: resolvedEmail, role: params.role }
1359
+ ];
1360
+ });
1361
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1307
1362
  }
1308
- async function importNest(userId, name, files) {
1309
- const nm = (name || "").trim();
1310
- if (!nm) {
1311
- throw new ValidationError("name is required");
1363
+ async function resolveMemberIdentity(params) {
1364
+ const db = getDb();
1365
+ if (params.userId) {
1366
+ const u = await db.get("SELECT email FROM users WHERE id = ?", [
1367
+ params.userId
1368
+ ]);
1369
+ if (!u) throw new ValidationError("User not found");
1370
+ return { userId: params.userId, email: normalizeEmail(u.email ?? "") };
1312
1371
  }
1313
- if (!Array.isArray(files)) {
1314
- throw new ValidationError("files are required");
1372
+ const requested = params.email ? normalizeEmail(params.email) : void 0;
1373
+ if (!requested) {
1374
+ throw new ValidationError("user_id or email is required");
1315
1375
  }
1316
- const db = getDb();
1317
- const id = uuid2();
1318
- const slug = toSlug(nm);
1319
- const visibility = userId === ANON_USER_ID ? "public" : "private";
1320
- const dest = resolveNestPath(id);
1321
- mkdirSync(dest, { recursive: true });
1322
- try {
1323
- await writeImportedFiles(dest, files);
1324
- } catch (err) {
1325
- console.error("[nests] import write failed", dest, err);
1326
- discardImportDir(dest);
1327
- throw new ValidationError("Failed to import folder");
1376
+ const existing = await db.get(
1377
+ "SELECT id, email FROM users WHERE LOWER(email) = ?",
1378
+ [requested]
1379
+ );
1380
+ if (existing) {
1381
+ return {
1382
+ userId: existing.id,
1383
+ email: normalizeEmail(existing.email ?? requested)
1384
+ };
1328
1385
  }
1386
+ const { hashPassword } = await import("./keys-73STFJJB.js");
1387
+ const userId = uuid();
1329
1388
  await db.run(
1330
- "INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
1331
- [id, userId, nm, slug, null, visibility]
1389
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1390
+ [userId, requested, null, await hashPassword(uuid())]
1332
1391
  );
1333
- trackEvent("nest.import", { nestId: id, userId });
1334
- return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1392
+ return { userId, email: requested };
1335
1393
  }
1336
- async function listNests(userId) {
1337
- const db = getDb();
1338
- if (userId === ANON_USER_ID) {
1339
- return await db.all(
1340
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
1341
- [ANON_USER_ID]
1342
- );
1343
- }
1344
- const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
1345
- if (!includeAnon) {
1346
- return await db.all(
1347
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
1348
- [userId]
1394
+ async function replaceTeamRoster(params) {
1395
+ for (const m of params.members) assertValidRole(m.role);
1396
+ const row = await loadTeamRow(params.teamId);
1397
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1398
+ throw new ForbiddenError(
1399
+ "Only a team admin or the owner can replace this team's roster."
1349
1400
  );
1350
1401
  }
1351
- return await db.all(
1352
- "SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC",
1353
- [userId, ANON_USER_ID]
1402
+ await mutateMembers(
1403
+ params.teamId,
1404
+ () => params.members.map((m) => ({
1405
+ userId: m.userId,
1406
+ email: m.email,
1407
+ role: m.role
1408
+ }))
1354
1409
  );
1410
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1355
1411
  }
1356
- async function nestsSharedViaTeams(userId) {
1357
- const db = getDb();
1358
- const teams = await db.all(
1359
- "SELECT id, members FROM teams WHERE members LIKE ?",
1360
- [`%${userId}%`]
1361
- );
1362
- const myTeamIds = /* @__PURE__ */ new Set();
1363
- for (const t of teams) {
1364
- try {
1365
- const members = JSON.parse(t.members || "[]");
1366
- if (Array.isArray(members) && members.some((m) => m?.userId === userId)) {
1367
- myTeamIds.add(t.id);
1368
- }
1369
- } catch {
1370
- }
1412
+ async function updateMemberRole(params) {
1413
+ assertValidRole(params.role);
1414
+ const row = await loadTeamRow(params.teamId);
1415
+ const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
1416
+ if (rank < ROLE_RANK.admin) {
1417
+ throw new ForbiddenError("Only a team admin or the owner can change roles.");
1371
1418
  }
1372
- if (myTeamIds.size === 0) return [];
1373
- const ids = [...myTeamIds];
1374
- const candidates = await db.all(
1375
- `SELECT * FROM nests
1376
- WHERE user_id != ?
1377
- AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
1378
- [userId, ...ids.map((id) => `%${id}%`)]
1379
- );
1380
- return candidates.filter((n) => {
1381
- try {
1382
- const refs = JSON.parse(n.shared_teams || "[]");
1383
- return Array.isArray(refs) && refs.some((r) => myTeamIds.has(r?.teamId));
1384
- } catch {
1385
- return false;
1386
- }
1419
+ if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
1420
+ throw new ForbiddenError("You can't set a role higher than your own.");
1421
+ }
1422
+ await mutateMembers(params.teamId, (members) => {
1423
+ const member = members.find((m) => m.userId === params.userId);
1424
+ if (!member) throw new NotFoundError("Team member not found");
1425
+ member.role = params.role;
1426
+ return members;
1387
1427
  });
1428
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1388
1429
  }
1389
- async function listSharedNests(userId) {
1390
- const db = getDb();
1391
- const direct = await db.all(
1392
- `SELECT DISTINCT n.* FROM nests n
1393
- LEFT JOIN nest_collaborators nc
1394
- ON nc.nest_id = n.id AND nc.user_id = ?
1395
- LEFT JOIN users u ON u.id = ?
1396
- LEFT JOIN stewards s
1397
- ON s.nest_id = n.id AND s.is_active = 1
1398
- AND LOWER(s.user_email) = LOWER(u.email)
1399
- WHERE n.user_id != ?
1400
- AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
1401
- ORDER BY n.created_at DESC`,
1402
- [userId, userId, userId]
1403
- );
1404
- const seen = new Set(direct.map((n) => n.id));
1405
- for (const n of await nestsSharedViaTeams(userId)) {
1406
- if (!seen.has(n.id)) {
1407
- direct.push(n);
1408
- seen.add(n.id);
1409
- }
1430
+ async function removeMember(params) {
1431
+ const row = await loadTeamRow(params.teamId);
1432
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1433
+ throw new ForbiddenError("Only a team admin or the owner can remove members.");
1410
1434
  }
1411
- if (await isServerAdminUserId(userId)) {
1412
- const anon = await db.all(
1413
- "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
1414
- [ANON_USER_ID]
1415
- );
1416
- for (const n of anon) {
1417
- if (!seen.has(n.id)) {
1418
- direct.push(n);
1419
- seen.add(n.id);
1420
- }
1435
+ await mutateMembers(params.teamId, (members) => {
1436
+ const next = members.filter((m) => m.userId !== params.userId);
1437
+ if (next.length === members.length) {
1438
+ throw new NotFoundError("Team member not found");
1421
1439
  }
1422
- }
1423
- direct.sort(
1424
- (a, b) => a.created_at === b.created_at ? 0 : a.created_at < b.created_at ? 1 : -1
1425
- );
1426
- return direct;
1440
+ return next;
1441
+ });
1442
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1427
1443
  }
1428
- async function listVisibleNests(userId) {
1444
+ async function mutateMembers(teamId, mutate) {
1429
1445
  const db = getDb();
1430
- const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
1431
- const params = includeAnon ? [userId, userId, userId, ANON_USER_ID] : [userId, userId, userId];
1432
- const rows = await db.all(
1433
- `SELECT DISTINCT n.* FROM nests n
1434
- LEFT JOIN nest_collaborators nc
1435
- ON nc.nest_id = n.id AND nc.user_id = ?
1436
- LEFT JOIN users u ON u.id = ?
1437
- LEFT JOIN stewards s
1438
- ON s.nest_id = n.id AND s.is_active = 1
1439
- AND LOWER(s.user_email) = LOWER(u.email)
1440
- WHERE n.user_id = ?
1441
- OR nc.user_id IS NOT NULL
1442
- OR s.id IS NOT NULL
1443
- ${includeAnon ? "OR n.user_id = ?" : ""}
1444
- ORDER BY n.created_at DESC`,
1445
- params
1446
- );
1447
- const seen = new Set(rows.map((n) => n.id));
1448
- for (const n of await nestsSharedViaTeams(userId)) {
1449
- if (!seen.has(n.id)) {
1450
- rows.push(n);
1451
- seen.add(n.id);
1452
- }
1453
- }
1454
- return rows;
1446
+ await db.transaction(async (tx) => {
1447
+ const row = await tx.get("SELECT members FROM teams WHERE id = ?", [
1448
+ teamId
1449
+ ]);
1450
+ if (!row) throw new NotFoundError("Team not found");
1451
+ const next = mutate(parseMembers(row.members));
1452
+ await tx.run("UPDATE teams SET members = ?, updated_at = ? WHERE id = ?", [
1453
+ JSON.stringify(next),
1454
+ (/* @__PURE__ */ new Date()).toISOString(),
1455
+ teamId
1456
+ ]);
1457
+ });
1455
1458
  }
1456
- async function listPublicNests(userId) {
1457
- const db = getDb();
1458
- return await db.all(
1459
- `SELECT n.* FROM nests n
1460
- WHERE n.visibility = 'public'
1461
- AND n.user_id != ?
1462
- AND NOT EXISTS (
1463
- SELECT 1 FROM nest_collaborators nc
1464
- WHERE nc.nest_id = n.id AND nc.user_id = ?
1465
- )
1466
- ORDER BY n.created_at DESC`,
1467
- [userId, userId]
1468
- );
1459
+ function parseSharedTeams(json) {
1460
+ if (!json) return [];
1461
+ try {
1462
+ const parsed = JSON.parse(json);
1463
+ return Array.isArray(parsed) ? parsed : [];
1464
+ } catch {
1465
+ return [];
1466
+ }
1469
1467
  }
1470
- async function getNest(nestId) {
1468
+ async function listNestTeams(nestId) {
1471
1469
  const db = getDb();
1472
- return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]) || null;
1470
+ const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
1471
+ nestId
1472
+ ]);
1473
+ if (!row) throw new NotFoundError("Nest not found");
1474
+ return parseSharedTeams(row.shared_teams);
1473
1475
  }
1474
- async function deleteNest(nestId) {
1476
+ async function shareTeamWithNest(params) {
1475
1477
  const db = getDb();
1476
- await db.transaction(async (tx) => {
1477
- const id = nestId;
1478
- await tx.run("DELETE FROM approved_versions WHERE nest_id = ?", [id]);
1479
- await tx.run("DELETE FROM node_versions WHERE nest_id = ?", [id]);
1480
- await tx.run("DELETE FROM review_requests WHERE nest_id = ?", [id]);
1481
- await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id]);
1482
- await tx.run("DELETE FROM nest_collaborators WHERE nest_id = ?", [id]);
1483
- try {
1484
- await tx.run("DELETE FROM node_tag_index WHERE nest_id = ?", [id]);
1485
- } catch {
1478
+ let sharedMembers = [];
1479
+ const refs = await db.transaction(async (tx) => {
1480
+ const team = await tx.get(
1481
+ "SELECT id, name, owner_id, members FROM teams WHERE id = ?",
1482
+ [params.teamId]
1483
+ );
1484
+ if (!team) throw new NotFoundError("Team not found");
1485
+ if (managementRank(team, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.editor) {
1486
+ throw new ForbiddenError(
1487
+ "You need to be an editor, admin, or owner of a team to share it. View-only members can't."
1488
+ );
1486
1489
  }
1487
- await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
1488
- await tx.run("DELETE FROM nests WHERE id = ?", [id]);
1490
+ const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
1491
+ params.nestId
1492
+ ]);
1493
+ if (!nest) throw new NotFoundError("Nest not found");
1494
+ const refs2 = parseSharedTeams(nest.shared_teams);
1495
+ if (refs2.some((r) => r.teamId === params.teamId)) {
1496
+ throw new ConflictError("This team is already shared with the nest.");
1497
+ }
1498
+ refs2.push({ teamId: team.id, teamName: team.name });
1499
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1500
+ JSON.stringify(refs2),
1501
+ params.nestId
1502
+ ]);
1503
+ const members = parseMembers(team.members);
1504
+ sharedMembers = members;
1505
+ const hasGovernanceRole = members.some(
1506
+ (m) => m.role === "viewer" || m.role === "editor"
1507
+ );
1508
+ if (hasGovernanceRole) {
1509
+ await tx.run("UPDATE nests SET stewardship_enabled = 1 WHERE id = ?", [
1510
+ params.nestId
1511
+ ]);
1512
+ }
1513
+ return refs2;
1489
1514
  });
1490
- const path = resolveNestPath(nestId);
1491
1515
  try {
1492
- rmSync2(path, { recursive: true, force: true });
1516
+ const caller = await db.get("SELECT email FROM users WHERE id = ?", [
1517
+ params.callerUserId
1518
+ ]);
1519
+ const callerEmail = caller?.email?.toLowerCase();
1520
+ const shareNestName = await nestName(params.nestId);
1521
+ const link = docLink(params.nestId, void 0, params.baseUrl);
1522
+ const byRole = /* @__PURE__ */ new Map();
1523
+ for (const m of sharedMembers) {
1524
+ if (!m.email || m.email.toLowerCase() === callerEmail) continue;
1525
+ const arr = byRole.get(m.role) ?? [];
1526
+ arr.push(m.email);
1527
+ byRole.set(m.role, arr);
1528
+ }
1529
+ for (const [role, emails] of byRole) {
1530
+ if (emails.length === 0) continue;
1531
+ void sendEmailToRecipient(emails, {
1532
+ status: "shared",
1533
+ docTitle: shareNestName,
1534
+ path: shareNestName,
1535
+ link,
1536
+ permission: role,
1537
+ by: caller?.email || void 0
1538
+ });
1539
+ }
1493
1540
  } catch (err) {
1494
- console.warn(`[nests] failed to remove nest directory ${path}:`, err);
1541
+ console.error("[notify] team-share email fan-out failed", params.nestId, err);
1495
1542
  }
1496
- trackEvent("nest.delete", { nestId });
1543
+ return refs;
1497
1544
  }
1498
-
1499
- // src/nodes/flat-storage.ts
1500
- import { readdirSync } from "fs";
1501
- import { join as join4, relative, sep } from "path";
1502
- import { NestStorage as NestStorage2 } from "@promptowl/contextnest-engine";
1503
- var META_FILES = /* @__PURE__ */ new Set([
1504
- "INDEX.md",
1505
- "CONTEXT.md",
1506
- "CLAUDE.md",
1507
- "GEMINI.md",
1508
- "AGENTS.md",
1509
- "QWEN.md"
1510
- ]);
1511
- var FlatNestStorage = class extends NestStorage2 {
1512
- async discoverDocuments() {
1513
- const root = this.root;
1514
- let entries;
1515
- try {
1516
- entries = readdirSync(root, { recursive: true, withFileTypes: true });
1517
- } catch (err) {
1518
- console.error("[FlatNestStorage] cannot read folder", root, err);
1519
- return [];
1520
- }
1521
- const ids = [];
1522
- for (const e of entries) {
1523
- if (!e.isFile() || !e.name.endsWith(".md")) continue;
1524
- if (META_FILES.has(e.name)) continue;
1525
- const dir = e.parentPath ?? e.path ?? root;
1526
- const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
1527
- if (rel.split("/").some(isSkippedSegment)) continue;
1528
- ids.push(rel.replace(/\.md$/, ""));
1545
+ async function unshareTeamFromNest(params) {
1546
+ const db = getDb();
1547
+ return db.transaction(async (tx) => {
1548
+ const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
1549
+ params.nestId
1550
+ ]);
1551
+ if (!nest) throw new NotFoundError("Nest not found");
1552
+ const refs = parseSharedTeams(nest.shared_teams);
1553
+ const next = refs.filter((r) => r.teamId !== params.teamId);
1554
+ if (next.length === refs.length) {
1555
+ throw new NotFoundError("Team is not shared with this nest.");
1529
1556
  }
1530
- const nodes = [];
1531
- for (const id of ids.sort()) {
1532
- try {
1533
- nodes.push(await this.readDocument(id));
1534
- } catch (err) {
1535
- console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1557
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1558
+ JSON.stringify(next),
1559
+ params.nestId
1560
+ ]);
1561
+ return next;
1562
+ });
1563
+ }
1564
+ async function refreshTeamNameRefs(tx, teamId, newName) {
1565
+ const nests = await tx.all(
1566
+ "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
1567
+ [`%${teamId}%`]
1568
+ );
1569
+ for (const n of nests) {
1570
+ const refs = parseSharedTeams(n.shared_teams);
1571
+ let changed = false;
1572
+ for (const r of refs) {
1573
+ if (r.teamId === teamId && r.teamName !== newName) {
1574
+ r.teamName = newName;
1575
+ changed = true;
1536
1576
  }
1537
1577
  }
1538
- return nodes;
1578
+ if (changed) {
1579
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1580
+ JSON.stringify(refs),
1581
+ n.id
1582
+ ]);
1583
+ }
1539
1584
  }
1540
- };
1541
-
1542
- // src/nodes/engine.ts
1543
- var NestEngineCache = class {
1544
- cache = /* @__PURE__ */ new Map();
1545
- async get(nestId) {
1546
- let engine = this.cache.get(nestId);
1547
- if (!engine) {
1548
- const nestPath = resolveNestPath(nestId);
1549
- const storage = await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath);
1550
- const query = new GraphQueryEngine(storage);
1551
- const versions = new VersionManager(storage);
1552
- engine = { storage, query, versions };
1553
- this.cache.set(nestId, engine);
1585
+ }
1586
+ async function removeTeamRefs(tx, teamId) {
1587
+ const nests = await tx.all(
1588
+ "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
1589
+ [`%${teamId}%`]
1590
+ );
1591
+ for (const n of nests) {
1592
+ const refs = parseSharedTeams(n.shared_teams);
1593
+ const next = refs.filter((r) => r.teamId !== teamId);
1594
+ if (next.length !== refs.length) {
1595
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1596
+ JSON.stringify(next),
1597
+ n.id
1598
+ ]);
1554
1599
  }
1555
- return engine;
1556
1600
  }
1557
- evict(nestId) {
1558
- this.cache.delete(nestId);
1601
+ }
1602
+ async function resolveTeamRolesForUser(nestId, userId) {
1603
+ const refs = await listNestTeams(nestId).catch(() => []);
1604
+ if (refs.length === 0) return [];
1605
+ const db = getDb();
1606
+ const roles = /* @__PURE__ */ new Set();
1607
+ for (const ref of refs) {
1608
+ const row = await db.get("SELECT members FROM teams WHERE id = ?", [
1609
+ ref.teamId
1610
+ ]);
1611
+ if (!row) continue;
1612
+ const member = parseMembers(row.members).find((m) => m.userId === userId);
1613
+ if (member) roles.add(member.role);
1559
1614
  }
1560
- };
1561
- var engineCache = new NestEngineCache();
1615
+ return [...roles];
1616
+ }
1562
1617
 
1563
- // src/governance/title-resolver.ts
1564
- async function buildTitleMap(nestId) {
1565
- try {
1566
- const { storage } = await engineCache.get(nestId);
1567
- const docs = await storage.discoverDocuments();
1568
- return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1569
- } catch {
1570
- return /* @__PURE__ */ new Map();
1618
+ // src/governance/roles.ts
1619
+ function collabPermToRole(permission) {
1620
+ switch (permission) {
1621
+ case "owner":
1622
+ return "owner";
1623
+ case "admin":
1624
+ return "admin";
1625
+ case "write":
1626
+ return "editor";
1627
+ case "read":
1628
+ return "viewer";
1629
+ default:
1630
+ return null;
1571
1631
  }
1572
1632
  }
1573
- function folderForNode(nodeId) {
1574
- return nodeId.replace(/^nodes\//, "").split("/").slice(0, -1).join("/");
1633
+ function teamRoleToPermission(role) {
1634
+ switch (role) {
1635
+ case "admin":
1636
+ return "admin";
1637
+ case "editor":
1638
+ return "write";
1639
+ case "viewer":
1640
+ return "read";
1641
+ default:
1642
+ return null;
1643
+ }
1575
1644
  }
1576
- async function describeNode(nestId, nodeId) {
1577
- const titles = await buildTitleMap(nestId);
1578
- const title = titles.get(nodeId) || nodeId;
1579
- const folder = folderForNode(nodeId);
1580
- return folder ? `${title} (in ${folder})` : title;
1645
+ var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1646
+ var canViewWith = (roles) => roles.length > 0;
1647
+ var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1648
+ var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
1649
+ var PRECEDENCE = [
1650
+ "owner",
1651
+ "admin",
1652
+ "editor",
1653
+ "reviewer",
1654
+ "viewer"
1655
+ ];
1656
+ function primaryRole(roles) {
1657
+ for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1658
+ return null;
1581
1659
  }
1582
- async function nestName(nestId) {
1583
- try {
1584
- const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1585
- nestId
1586
- ]);
1587
- return row?.name || nestId;
1588
- } catch {
1589
- return nestId;
1590
- }
1660
+
1661
+ // src/shared/access.ts
1662
+ async function isServerAdminUserId(userId) {
1663
+ if (await isLicenseAdminUserId(userId)) return true;
1664
+ const row = await getDb().get(
1665
+ "SELECT email FROM users WHERE id = ?",
1666
+ [userId]
1667
+ );
1668
+ return !!row?.email && isSuperAdmin(row.email);
1591
1669
  }
1592
- function docLink(nestId, nodeId, baseUrl) {
1593
- const base = baseUrl || config.PUBLIC_BASE_URL;
1594
- if (!base) return "";
1595
- const q = new URLSearchParams({ nest: nestId });
1596
- if (nodeId) q.set("doc", nodeId);
1597
- return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1670
+ var PERMISSION_LEVELS = {
1671
+ none: 0,
1672
+ read: 1,
1673
+ write: 2,
1674
+ admin: 3,
1675
+ owner: 4
1676
+ };
1677
+ async function resolveNestPermission(nestId, userId) {
1678
+ const db = getDb();
1679
+ const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1680
+ if (!nest) return "none";
1681
+ if (nest.user_id === userId) return "owner";
1682
+ if (nest.user_id === ANON_USER_ID && await isLicenseAdminUserId(userId)) {
1683
+ return "owner";
1684
+ }
1685
+ if (await isLicenseAdminUserId(userId)) return "admin";
1686
+ const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1687
+ if (caller?.email && isSuperAdmin(caller.email)) {
1688
+ return "admin";
1689
+ }
1690
+ const higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1691
+ let best = "none";
1692
+ const directGrant = await db.get(
1693
+ "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1694
+ [nestId, userId]
1695
+ );
1696
+ if (directGrant) best = higher(best, directGrant.permission);
1697
+ if (best === "admin") return best;
1698
+ for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1699
+ best = higher(best, teamRoleToPermission(role));
1700
+ }
1701
+ if (best === "admin") return best;
1702
+ const stewardGrant = await db.get(
1703
+ `SELECT 1 FROM stewards s
1704
+ JOIN users u ON u.id = ?
1705
+ WHERE s.nest_id = ? AND s.is_active = 1
1706
+ AND LOWER(s.user_email) = LOWER(u.email)
1707
+ LIMIT 1`,
1708
+ [userId, nestId]
1709
+ );
1710
+ if (stewardGrant) best = higher(best, "read");
1711
+ if (nest.visibility === "public") best = higher(best, "read");
1712
+ return best;
1598
1713
  }
1599
- async function buildDocContext(nestId, nodeId, baseUrl) {
1600
- const titles = await buildTitleMap(nestId);
1601
- const docTitle = titles.get(nodeId) || nodeId;
1602
- const folder = folderForNode(nodeId);
1603
- const name = await nestName(nestId);
1604
- return {
1605
- docTitle,
1606
- // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1607
- label: folder ? `${docTitle} (in ${folder})` : docTitle,
1608
- path: folder ? `${name} / ${folder}` : name,
1609
- link: docLink(nestId, nodeId, baseUrl)
1610
- };
1714
+ function permissionLevel(p) {
1715
+ return PERMISSION_LEVELS[p] ?? 0;
1611
1716
  }
1612
-
1613
- // src/governance/stewardship-service.ts
1614
- async function assignSteward(data) {
1717
+ async function isPublicReader(nestId, userId) {
1615
1718
  const db = getDb();
1616
- const id = uuid3();
1617
- await db.run(
1618
- `INSERT INTO stewards
1619
- (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
1620
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1621
- [
1622
- id,
1623
- data.nestId,
1624
- data.scope,
1625
- data.nodePattern || null,
1626
- data.tagName || null,
1627
- data.userEmail,
1628
- data.userId || null,
1629
- data.role,
1630
- data.assignedBy,
1631
- data.isActive ? 1 : 0
1632
- ]
1719
+ const nest = await db.get("SELECT user_id, visibility FROM nests WHERE id = ?", [nestId]);
1720
+ if (!nest) return false;
1721
+ if (nest.visibility !== "public") return false;
1722
+ if (nest.user_id === userId) return false;
1723
+ const directGrant = await db.get(
1724
+ "SELECT 1 FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1725
+ [nestId, userId]
1726
+ );
1727
+ if (directGrant) return false;
1728
+ if (await isLicenseAdminUserId(userId)) return false;
1729
+ const caller = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
1730
+ if (caller?.email && isSuperAdmin(caller.email)) return false;
1731
+ const stewardGrant = await db.get(
1732
+ `SELECT 1 FROM stewards s
1733
+ JOIN users u ON u.id = ?
1734
+ WHERE s.nest_id = ? AND s.is_active = 1
1735
+ AND LOWER(s.user_email) = LOWER(u.email)
1736
+ LIMIT 1`,
1737
+ [userId, nestId]
1633
1738
  );
1634
- return { ...data, id };
1739
+ if (stewardGrant) return false;
1740
+ if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
1741
+ return true;
1635
1742
  }
1636
- async function removeSteward(id) {
1637
- const db = getDb();
1638
- await db.run("DELETE FROM stewards WHERE id = ?", [id]);
1743
+
1744
+ // src/governance/tag-index-service.ts
1745
+ function normalizeTag(raw) {
1746
+ return raw.trim().replace(/^#+/, "").toLowerCase();
1639
1747
  }
1640
- var VALID_STEWARD_ROLES = ["editor", "reviewer", "viewer"];
1641
- async function updateSteward(id, update) {
1748
+ async function syncNodeTags(nestId, nodeId, tags) {
1642
1749
  const db = getDb();
1643
- const current = await db.get(
1644
- "SELECT * FROM stewards WHERE id = ? AND is_active = 1",
1645
- [id]
1750
+ const normalized = Array.from(
1751
+ new Set(
1752
+ tags.filter((t) => typeof t === "string").map(normalizeTag).filter(Boolean)
1753
+ )
1646
1754
  );
1647
- if (!current) {
1648
- throw new ValidationError("Steward not found");
1649
- }
1650
- const role = update.role ?? current.role;
1651
- if (!VALID_STEWARD_ROLES.includes(role)) {
1652
- throw new ValidationError(
1653
- `Invalid role "${role}" \u2014 must be editor, reviewer, or viewer.`
1755
+ const insertSql = insertOrIgnore(
1756
+ db,
1757
+ "INSERT INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1758
+ );
1759
+ await db.transaction(async (tx) => {
1760
+ await tx.run(
1761
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1762
+ [nestId, nodeId]
1654
1763
  );
1655
- }
1656
- let scope = current.scope;
1657
- let nodePattern = current.node_pattern ?? null;
1658
- let tagName = current.tag_name ?? null;
1659
- if (update.scope) {
1660
- scope = update.scope;
1661
- nodePattern = null;
1662
- tagName = null;
1663
- switch (scope) {
1664
- case "document":
1665
- if (!update.documentId)
1666
- throw new ValidationError("documentId required for document scope");
1667
- nodePattern = update.documentId;
1668
- break;
1669
- case "tag":
1670
- if (!update.tagName)
1671
- throw new ValidationError("tagName required for tag scope");
1672
- tagName = update.tagName.trim().replace(/^#+/, "").toLowerCase();
1673
- break;
1674
- case "nest":
1675
- break;
1764
+ for (const tag of normalized) {
1765
+ await tx.run(insertSql, [nestId, nodeId, tag]);
1676
1766
  }
1677
- const dup = await db.get(
1678
- `SELECT role FROM stewards
1679
- WHERE nest_id = ? AND is_active = 1 AND id != ? AND scope = ?
1680
- AND user_email = ?
1681
- AND COALESCE(node_pattern, '') = COALESCE(?, '')
1682
- AND COALESCE(tag_name, '') = COALESCE(?, '')`,
1683
- [current.nest_id, id, scope, current.user_email, nodePattern, tagName]
1767
+ });
1768
+ }
1769
+ async function removeNodeFromTagIndex(nestId, nodeId) {
1770
+ const db = getDb();
1771
+ await db.run(
1772
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1773
+ [nestId, nodeId]
1774
+ );
1775
+ }
1776
+
1777
+ // src/nests/service.ts
1778
+ async function isImportedNest(nestId) {
1779
+ const row = await getDb().get(
1780
+ "SELECT is_imported FROM nests WHERE id = ?",
1781
+ [nestId]
1782
+ );
1783
+ return !!row?.is_imported;
1784
+ }
1785
+ function toSlug(name) {
1786
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1787
+ }
1788
+ async function uniqueNestName(userId, baseName) {
1789
+ const db = getDb();
1790
+ let candidate = baseName;
1791
+ let n = 0;
1792
+ while (n < 1e3) {
1793
+ const slug = toSlug(candidate);
1794
+ const hit = await db.get(
1795
+ "SELECT 1 FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1796
+ [userId, slug]
1684
1797
  );
1685
- if (dup) {
1686
- const scopeLabel = scope === "document" ? `document "${nodePattern}"` : scope === "tag" ? `tag "#${tagName}"` : "this nest";
1687
- throw new ConflictError(
1688
- `"${current.user_email}" is already a steward of ${scopeLabel} with the "${dup.role}" role.`
1689
- );
1690
- }
1798
+ if (!hit) return candidate;
1799
+ n++;
1800
+ candidate = `${baseName} (${n})`;
1691
1801
  }
1692
- await db.run(
1693
- "UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?",
1694
- [role, scope, nodePattern, tagName, id]
1802
+ return candidate;
1803
+ }
1804
+ async function isStewardshipEnabled(nestId) {
1805
+ const db = getDb();
1806
+ const row = await db.get(
1807
+ "SELECT stewardship_enabled FROM nests WHERE id = ?",
1808
+ [nestId]
1695
1809
  );
1696
- const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
1697
- return rowToSteward(row);
1810
+ return !!row?.stewardship_enabled;
1811
+ }
1812
+ async function setStewardshipEnabled(nestId, enabled) {
1813
+ const db = getDb();
1814
+ await db.run("UPDATE nests SET stewardship_enabled = ? WHERE id = ?", [
1815
+ enabled ? 1 : 0,
1816
+ nestId
1817
+ ]);
1698
1818
  }
1699
- async function updateStewardRole(id, role) {
1700
- return updateSteward(id, { role });
1819
+ async function nestAllowsSelfApprove(nestId) {
1820
+ const db = getDb();
1821
+ const row = await db.get(
1822
+ "SELECT allow_self_approve FROM nests WHERE id = ?",
1823
+ [nestId]
1824
+ );
1825
+ return !!row?.allow_self_approve;
1701
1826
  }
1702
- async function getSteward(id) {
1827
+ async function setAllowSelfApprove(nestId, allow) {
1703
1828
  const db = getDb();
1704
- const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
1705
- return row ? rowToSteward(row) : null;
1829
+ await db.run("UPDATE nests SET allow_self_approve = ? WHERE id = ?", [
1830
+ allow ? 1 : 0,
1831
+ nestId
1832
+ ]);
1706
1833
  }
1707
- async function getStewardsForNest(nestId) {
1834
+ async function nestReviewsPrimeOnly(nestId) {
1708
1835
  const db = getDb();
1709
- const rows = await db.all(
1710
- "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1",
1836
+ const row = await db.get(
1837
+ "SELECT prime_only_review FROM nests WHERE id = ?",
1711
1838
  [nestId]
1712
1839
  );
1713
- return enrichWithTitles(nestId, rows.map(rowToSteward));
1840
+ return !!row?.prime_only_review;
1714
1841
  }
1715
- async function getStewardsForScope(params) {
1842
+ async function setPrimeOnlyReview(nestId, enabled) {
1716
1843
  const db = getDb();
1717
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
1718
- const args = [params.nestId];
1719
- if (params.scope) {
1720
- sql += " AND scope = ?";
1721
- args.push(params.scope);
1722
- }
1723
- if (params.scopeTarget) {
1724
- sql += " AND (node_pattern = ? OR tag_name = ?)";
1725
- args.push(params.scopeTarget, params.scopeTarget);
1726
- }
1727
- return (await db.all(sql, args)).map(rowToSteward);
1844
+ await db.run("UPDATE nests SET prime_only_review = ? WHERE id = ?", [
1845
+ enabled ? 1 : 0,
1846
+ nestId
1847
+ ]);
1728
1848
  }
1729
- async function listStewards(params) {
1849
+ async function nestPrimeTags(nestId) {
1730
1850
  const db = getDb();
1731
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
1732
- const args = [params.nestId];
1733
- if (params.scope) {
1734
- sql += " AND scope = ?";
1735
- args.push(params.scope);
1736
- }
1737
- if (params.search) {
1738
- sql += " AND (user_email LIKE ? OR tag_name LIKE ? OR node_pattern LIKE ?)";
1739
- const like = `%${params.search.toLowerCase()}%`;
1740
- args.push(like, like, like);
1741
- }
1742
- sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
1743
- const stewards = (await db.all(sql, args)).map(rowToSteward);
1744
- return enrichWithTitles(params.nestId, stewards);
1745
- }
1746
- async function enrichWithTitles(nestId, stewards) {
1747
- if (!stewards.some((s) => s.scope === "document")) return stewards;
1748
- const titleByNodeId = await buildTitleMap(nestId);
1749
- for (const s of stewards) {
1750
- if (s.scope === "document" && s.nodePattern) {
1751
- s.nodeTitle = titleByNodeId.get(s.nodePattern);
1752
- }
1753
- }
1754
- return stewards;
1755
- }
1756
- async function createStewardRecord(params) {
1757
- if (params.users.length === 0) {
1758
- throw new Error("At least one user is required");
1759
- }
1760
- let nodePattern;
1761
- let tagName;
1762
- switch (params.scope) {
1763
- case "document":
1764
- if (!params.documentId) throw new Error("documentId required for document scope");
1765
- nodePattern = params.documentId;
1766
- break;
1767
- case "tag":
1768
- if (!params.tagName) throw new Error("tagName required for tag scope");
1769
- tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
1770
- break;
1771
- case "nest":
1772
- break;
1773
- }
1851
+ const row = await db.get("SELECT prime_tags FROM nests WHERE id = ?", [
1852
+ nestId
1853
+ ]);
1854
+ return parsePrimeTags(row?.prime_tags);
1855
+ }
1856
+ async function setPrimeTags(nestId, tags) {
1774
1857
  const db = getDb();
1775
- const results = [];
1776
- const actor = (params.assignedBy || "").trim().toLowerCase();
1777
- const ownerEmail = (await getNestOwnerEmail(params.nestId) || "").toLowerCase();
1778
- for (const user of params.users) {
1779
- const email = user.email.trim().toLowerCase();
1780
- if (!email) continue;
1781
- if (!isEmailish(email)) {
1782
- throw new ValidationError(`"${user.email}" is not a valid email address.`);
1783
- }
1784
- if (email === actor) {
1785
- throw new ValidationError(
1786
- "You already manage this nest, so you can't add yourself as a steward."
1787
- );
1788
- }
1789
- if (ownerEmail && email === ownerEmail) {
1858
+ await db.run("UPDATE nests SET prime_tags = ? WHERE id = ?", [
1859
+ parsePrimeTags(tags.join(",")).join(","),
1860
+ nestId
1861
+ ]);
1862
+ }
1863
+ function parsePrimeTags(raw) {
1864
+ if (!raw) return [];
1865
+ return [
1866
+ ...new Set(
1867
+ raw.split(",").map((t) => normalizeTag(t)).filter(Boolean)
1868
+ )
1869
+ ];
1870
+ }
1871
+ async function disableStewardshipAndWipeGovernance(nestId) {
1872
+ const db = getDb();
1873
+ return db.transaction(async (tx) => {
1874
+ const id = nestId;
1875
+ const stewards = (await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id])).changes;
1876
+ const pendingReviews = (await tx.run(
1877
+ "DELETE FROM review_requests WHERE nest_id = ? AND status = 'pending'",
1878
+ [id]
1879
+ )).changes;
1880
+ await tx.run("UPDATE nests SET stewardship_enabled = 0 WHERE id = ?", [id]);
1881
+ return { stewards, pendingReviews };
1882
+ });
1883
+ }
1884
+ async function renameNest(nestId, patch) {
1885
+ const db = getDb();
1886
+ const existing = await db.get("SELECT * FROM nests WHERE id = ?", [
1887
+ nestId
1888
+ ]);
1889
+ if (!existing) {
1890
+ throw new ValidationError("Nest not found");
1891
+ }
1892
+ const updates = [];
1893
+ const params = [];
1894
+ if (patch.name !== void 0) {
1895
+ const trimmed = patch.name.trim();
1896
+ if (!trimmed) throw new ValidationError("Name cannot be empty");
1897
+ const slug = toSlug(trimmed);
1898
+ if (!slug) throw new ValidationError("Name must contain at least one alphanumeric character");
1899
+ const conflict = await db.get(
1900
+ "SELECT id FROM nests WHERE user_id = ? AND slug = ? AND id != ? LIMIT 1",
1901
+ [existing.user_id, slug, nestId]
1902
+ );
1903
+ if (conflict) {
1790
1904
  throw new ValidationError(
1791
- "The nest owner already has full access and doesn't need a steward role."
1905
+ `Another nest already uses this name. Pick a different one.`
1792
1906
  );
1793
1907
  }
1794
- const existing = await db.get(
1795
- `SELECT * FROM stewards
1796
- WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
1797
- AND COALESCE(node_pattern, '') = COALESCE(?, '')
1798
- AND COALESCE(tag_name, '') = COALESCE(?, '')`,
1799
- [
1800
- params.nestId,
1801
- params.scope,
1802
- email,
1803
- nodePattern ?? null,
1804
- tagName ?? null
1805
- ]
1908
+ updates.push("name = ?", "slug = ?");
1909
+ params.push(trimmed, slug);
1910
+ }
1911
+ if (patch.description !== void 0) {
1912
+ updates.push("description = ?");
1913
+ params.push(patch.description || null);
1914
+ }
1915
+ if (updates.length === 0) {
1916
+ return existing;
1917
+ }
1918
+ params.push(nestId);
1919
+ await db.run(
1920
+ `UPDATE nests SET ${updates.join(", ")} WHERE id = ?`,
1921
+ params
1922
+ );
1923
+ return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]);
1924
+ }
1925
+ async function createNest(userId, name, description) {
1926
+ const id = uuid2();
1927
+ const db = getDb();
1928
+ const trimmed = name.trim();
1929
+ if (!trimmed) throw new ValidationError("Name cannot be empty");
1930
+ const slug = toSlug(trimmed);
1931
+ if (!slug) {
1932
+ throw new ValidationError(
1933
+ "Name must contain at least one alphanumeric character"
1806
1934
  );
1807
- if (existing) {
1808
- const scopeLabel = params.scope === "document" ? `document "${nodePattern}"` : params.scope === "tag" ? `tag "#${tagName}"` : "this nest";
1809
- throw new ConflictError(
1810
- `"${email}" is already a steward of ${scopeLabel} with the "${existing.role}" role. Remove the existing assignment first to change the role.`
1811
- );
1812
- }
1813
- const userRow = await db.get(
1814
- "SELECT id FROM users WHERE email = ?",
1815
- [email]
1935
+ }
1936
+ const conflict = await db.get(
1937
+ "SELECT id FROM nests WHERE user_id = ? AND slug = ? LIMIT 1",
1938
+ [userId, slug]
1939
+ );
1940
+ if (conflict) {
1941
+ throw new ValidationError(
1942
+ "You already have a nest with this name. Pick a different one."
1816
1943
  );
1817
- let userId = userRow?.id;
1818
- if (!userId) {
1819
- const { hashPassword } = await import("./keys-73STFJJB.js");
1820
- userId = uuid3();
1821
- await db.run(
1822
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1823
- [userId, email, null, await hashPassword(uuid3())]
1824
- );
1825
- }
1826
- const created = await assignSteward({
1827
- nestId: params.nestId,
1828
- scope: params.scope,
1829
- nodePattern,
1830
- tagName,
1831
- userEmail: email,
1832
- userId,
1833
- role: user.role ?? "reviewer",
1834
- assignedBy: params.assignedBy,
1835
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
1836
- isActive: true
1837
- });
1838
- results.push(created);
1944
+ }
1945
+ const visibility = userId === ANON_USER_ID ? "public" : "private";
1946
+ await db.run(
1947
+ "INSERT INTO nests (id, user_id, name, slug, description, visibility) VALUES (?, ?, ?, ?, ?, ?)",
1948
+ [id, userId, trimmed, slug, description || null, visibility]
1949
+ );
1950
+ const path = resolveNestPath(id);
1951
+ mkdirSync(path, { recursive: true });
1952
+ const storage = new NestStorage3(path);
1953
+ await storage.init(trimmed);
1954
+ trackEvent("nest.create", { nestId: id, userId });
1955
+ return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1956
+ }
1957
+ async function importNest(userId, name, files) {
1958
+ const nm = (name || "").trim();
1959
+ if (!nm) {
1960
+ throw new ValidationError("name is required");
1961
+ }
1962
+ if (!Array.isArray(files)) {
1963
+ throw new ValidationError("files are required");
1964
+ }
1965
+ const db = getDb();
1966
+ const id = uuid2();
1967
+ const slug = toSlug(nm);
1968
+ const visibility = userId === ANON_USER_ID ? "public" : "private";
1969
+ const dest = resolveNestPath(id);
1970
+ mkdirSync(dest, { recursive: true });
1971
+ try {
1972
+ await writeImportedFiles(dest, files);
1973
+ } catch (err) {
1974
+ console.error("[nests] import write failed", dest, err);
1975
+ discardImportDir(dest);
1976
+ throw new ValidationError("Failed to import folder");
1839
1977
  }
1840
1978
  await db.run(
1841
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
1842
- [params.nestId]
1843
- );
1844
- return results;
1845
- }
1846
- async function resolveStewardsForNode(nestId, nodeId) {
1847
- return (await resolve(nestId, nodeId)).stewards;
1848
- }
1849
- async function resolveStewardsWithFallback(nestId, nodeId) {
1850
- return resolve(nestId, nodeId);
1851
- }
1852
- async function resolve(nestId, nodeId) {
1853
- const db = getDb();
1854
- const rows = await db.all(
1855
- `
1856
- SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
1857
- FROM stewards s
1858
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
1859
- AND s.node_pattern = ?
1860
- UNION ALL
1861
- SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
1862
- FROM stewards s
1863
- JOIN node_tag_index nt
1864
- ON nt.nest_id = s.nest_id
1865
- AND nt.tag_name = s.tag_name
1866
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
1867
- AND nt.node_id = ?
1868
- UNION ALL
1869
- SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
1870
- FROM stewards s
1871
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
1872
- ORDER BY priority ASC, user_email ASC
1873
- `,
1874
- [
1875
- nestId,
1876
- nodeId,
1877
- // document branch
1878
- nestId,
1879
- nodeId,
1880
- // tag branch
1881
- nestId
1882
- // nest branch
1883
- ]
1884
- );
1885
- const resolved = rows.map((row) => ({
1886
- steward: rowToSteward(row),
1887
- priority: row.priority,
1888
- source: row.match_source
1889
- }));
1890
- if (resolved.length > 0) {
1891
- return { stewards: resolved, fallbackToOwner: false };
1892
- }
1893
- const owner = await db.get(
1894
- `SELECT u.email FROM nests n
1895
- JOIN users u ON u.id = n.user_id
1896
- WHERE n.id = ?`,
1897
- [nestId]
1979
+ "INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
1980
+ [id, userId, nm, slug, null, visibility]
1898
1981
  );
1899
- return {
1900
- stewards: [],
1901
- fallbackToOwner: true,
1902
- ownerEmail: owner?.email
1903
- };
1982
+ trackEvent("nest.import", { nestId: id, userId });
1983
+ return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1904
1984
  }
1905
- async function userIdForEmail(email) {
1985
+ async function listNests(userId) {
1906
1986
  const db = getDb();
1907
- const row = await db.get(
1908
- "SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
1909
- [email]
1987
+ if (userId === ANON_USER_ID) {
1988
+ return await db.all(
1989
+ "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
1990
+ [ANON_USER_ID]
1991
+ );
1992
+ }
1993
+ const includeAnon = config.AUTH_MODE === "open" || await isLicenseAdminUserId(userId);
1994
+ if (!includeAnon) {
1995
+ return await db.all(
1996
+ "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
1997
+ [userId]
1998
+ );
1999
+ }
2000
+ return await db.all(
2001
+ "SELECT * FROM nests WHERE user_id = ? OR user_id = ? ORDER BY created_at DESC",
2002
+ [userId, ANON_USER_ID]
1910
2003
  );
1911
- return row?.id;
1912
2004
  }
1913
- async function getStewardRolesForUser(nestId, userEmail) {
2005
+ async function nestsSharedViaTeams(userId) {
1914
2006
  const db = getDb();
1915
- const rows = await db.all(
1916
- "SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
1917
- [nestId, userEmail]
2007
+ const teams = await db.all(
2008
+ "SELECT id, members FROM teams WHERE members LIKE ?",
2009
+ [`%${userId}%`]
1918
2010
  );
1919
- return rows.map((r) => r.role);
1920
- }
1921
- async function getStewardsForUser(nestId, userEmail) {
1922
- const db = getDb();
1923
- const rows = await db.all(
1924
- "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
1925
- [nestId, userEmail]
2011
+ const myTeamIds = /* @__PURE__ */ new Set();
2012
+ for (const t of teams) {
2013
+ try {
2014
+ const members = JSON.parse(t.members || "[]");
2015
+ if (Array.isArray(members) && members.some((m) => m?.userId === userId)) {
2016
+ myTeamIds.add(t.id);
2017
+ }
2018
+ } catch {
2019
+ }
2020
+ }
2021
+ if (myTeamIds.size === 0) return [];
2022
+ const ids = [...myTeamIds];
2023
+ const candidates = await db.all(
2024
+ `SELECT * FROM nests
2025
+ WHERE user_id != ?
2026
+ AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
2027
+ [userId, ...ids.map((id) => `%${id}%`)]
1926
2028
  );
1927
- return rows.map(rowToSteward);
2029
+ return candidates.filter((n) => {
2030
+ try {
2031
+ const refs = JSON.parse(n.shared_teams || "[]");
2032
+ return Array.isArray(refs) && refs.some((r) => myTeamIds.has(r?.teamId));
2033
+ } catch {
2034
+ return false;
2035
+ }
2036
+ });
1928
2037
  }
1929
- async function getCollaboratorRole(nestId, userEmail) {
1930
- const userId = await userIdForEmail(userEmail);
1931
- if (!userId) return null;
2038
+ async function listSharedNests(userId) {
1932
2039
  const db = getDb();
1933
- const row = await db.get(
1934
- "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
1935
- [nestId, userId]
2040
+ const direct = await db.all(
2041
+ `SELECT DISTINCT n.* FROM nests n
2042
+ LEFT JOIN nest_collaborators nc
2043
+ ON nc.nest_id = n.id AND nc.user_id = ?
2044
+ LEFT JOIN users u ON u.id = ?
2045
+ LEFT JOIN stewards s
2046
+ ON s.nest_id = n.id AND s.is_active = 1
2047
+ AND LOWER(s.user_email) = LOWER(u.email)
2048
+ WHERE n.user_id != ?
2049
+ AND (nc.user_id IS NOT NULL OR s.id IS NOT NULL)
2050
+ ORDER BY n.created_at DESC`,
2051
+ [userId, userId, userId]
1936
2052
  );
1937
- return row?.permission ?? null;
1938
- }
1939
- async function resolveGrantRoles(nestId, userEmail, userId) {
1940
- const [grantRaw, stewardRoles, teamRoles] = await Promise.all([
1941
- getCollaboratorRole(nestId, userEmail),
1942
- getStewardRolesForUser(nestId, userEmail),
1943
- resolveTeamRolesForUser(nestId, userId)
1944
- ]);
1945
- return [
1946
- ...new Set(
1947
- [collabPermToRole(grantRaw), ...stewardRoles, ...teamRoles].filter(
1948
- Boolean
1949
- )
1950
- )
1951
- ];
1952
- }
1953
- async function resolveUserRoles(nestId, userEmail, opts) {
1954
- const roles = /* @__PURE__ */ new Set();
1955
- if (isSuperAdmin(userEmail)) roles.add("admin");
1956
- const owner = await getNestOwnerEmail(nestId);
1957
- if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
1958
- roles.add("owner");
1959
- }
1960
- const userId = await userIdForEmail(userEmail);
1961
- if (userId) {
1962
- const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
1963
- if (collabRole) roles.add(collabRole);
1964
- for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1965
- roles.add(role);
2053
+ const seen = new Set(direct.map((n) => n.id));
2054
+ for (const n of await nestsSharedViaTeams(userId)) {
2055
+ if (!seen.has(n.id)) {
2056
+ direct.push(n);
2057
+ seen.add(n.id);
1966
2058
  }
1967
2059
  }
1968
- const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
1969
- (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
1970
- ).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
1971
- for (const role of stewardRoles) roles.add(role);
1972
- return [...roles];
1973
- }
1974
- async function canManageStewards(nestId, userId) {
1975
- if (config.AUTH_MODE === "open") return true;
1976
- const perm = await resolveNestPermission(nestId, userId);
1977
- return perm === "owner" || perm === "admin";
1978
- }
1979
- async function canCreateInNest(nestId, userEmail) {
1980
- if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
1981
- const userId = await userIdForEmail(userEmail);
1982
- if (userId) {
1983
- const perm = await resolveNestPermission(nestId, userId);
1984
- if (perm === "owner" || perm === "admin" || perm === "write") return true;
2060
+ if (await isServerAdminUserId(userId)) {
2061
+ const anon = await db.all(
2062
+ "SELECT * FROM nests WHERE user_id = ? ORDER BY created_at DESC",
2063
+ [ANON_USER_ID]
2064
+ );
2065
+ for (const n of anon) {
2066
+ if (!seen.has(n.id)) {
2067
+ direct.push(n);
2068
+ seen.add(n.id);
2069
+ }
2070
+ }
1985
2071
  }
1986
- return (await getStewardsForUser(nestId, userEmail)).some(
1987
- (s) => s.role === "editor" && s.scope === "nest"
2072
+ direct.sort(
2073
+ (a, b) => a.created_at === b.created_at ? 0 : a.created_at < b.created_at ? 1 : -1
1988
2074
  );
2075
+ return direct;
1989
2076
  }
1990
- async function getNestOwnerEmail(nestId) {
2077
+ async function listVisibleNests(userId) {
1991
2078
  const db = getDb();
1992
- const row = await db.get(
1993
- `SELECT u.email FROM nests n
1994
- JOIN users u ON u.id = n.user_id
1995
- WHERE n.id = ?`,
1996
- [nestId]
2079
+ const includeAnon = config.AUTH_MODE === "open" || await isServerAdminUserId(userId);
2080
+ const params = includeAnon ? [userId, userId, userId, ANON_USER_ID] : [userId, userId, userId];
2081
+ const rows = await db.all(
2082
+ `SELECT DISTINCT n.* FROM nests n
2083
+ LEFT JOIN nest_collaborators nc
2084
+ ON nc.nest_id = n.id AND nc.user_id = ?
2085
+ LEFT JOIN users u ON u.id = ?
2086
+ LEFT JOIN stewards s
2087
+ ON s.nest_id = n.id AND s.is_active = 1
2088
+ AND LOWER(s.user_email) = LOWER(u.email)
2089
+ WHERE n.user_id = ?
2090
+ OR nc.user_id IS NOT NULL
2091
+ OR s.id IS NOT NULL
2092
+ ${includeAnon ? "OR n.user_id = ?" : ""}
2093
+ ORDER BY n.created_at DESC`,
2094
+ params
1997
2095
  );
1998
- return row?.email ?? null;
1999
- }
2000
- async function canUserEdit(nestId, nodeId, userEmail) {
2001
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2002
- if (roles.includes("owner")) {
2003
- return { allowed: true, reason: "nest owner", role: "owner" };
2004
- }
2005
- if (isSuperAdmin(userEmail)) {
2006
- return { allowed: true, reason: "super admin", role: "super_admin" };
2007
- }
2008
- if (canEditWith(roles)) {
2009
- return {
2010
- allowed: true,
2011
- reason: "editor access (collaborator or steward)",
2012
- role: roles.includes("admin") ? "admin" : "editor"
2013
- };
2096
+ const seen = new Set(rows.map((n) => n.id));
2097
+ for (const n of await nestsSharedViaTeams(userId)) {
2098
+ if (!seen.has(n.id)) {
2099
+ rows.push(n);
2100
+ seen.add(n.id);
2101
+ }
2014
2102
  }
2015
- return { allowed: false, reason: "no editor role on this node", role: null };
2103
+ return rows;
2016
2104
  }
2017
- async function getCurrentVersionAuthor(nestId, nodeId) {
2105
+ async function listPublicNests(userId) {
2018
2106
  const db = getDb();
2019
- const row = await db.get(
2020
- `SELECT author FROM node_versions
2021
- WHERE nest_id = ? AND node_id = ?
2022
- ORDER BY version DESC LIMIT 1`,
2023
- [nestId, nodeId]
2107
+ return await db.all(
2108
+ `SELECT n.* FROM nests n
2109
+ WHERE n.visibility = 'public'
2110
+ AND n.user_id != ?
2111
+ AND NOT EXISTS (
2112
+ SELECT 1 FROM nest_collaborators nc
2113
+ WHERE nc.nest_id = n.id AND nc.user_id = ?
2114
+ )
2115
+ ORDER BY n.created_at DESC`,
2116
+ [userId, userId]
2024
2117
  );
2025
- return row?.author ?? null;
2026
2118
  }
2027
- async function getPendingReviewRequester(nestId, nodeId) {
2119
+ async function getNest(nestId) {
2028
2120
  const db = getDb();
2029
- const row = await db.get(
2030
- `SELECT requested_by FROM review_requests
2031
- WHERE nest_id = ? AND node_id = ? AND status = 'pending'
2032
- ORDER BY requested_at DESC LIMIT 1`,
2033
- [nestId, nodeId]
2034
- );
2035
- return row?.requested_by ?? null;
2036
- }
2037
- async function canUserApprove(nestId, nodeId, userEmail) {
2038
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2039
- const isOwner = roles.includes("owner");
2040
- const isSuper = isSuperAdmin(userEmail);
2041
- const allowSelf = await nestAllowsSelfApprove(nestId);
2042
- const hasStewardApprove = roles.includes("admin") || roles.includes("reviewer");
2043
- const actor = await getPendingReviewRequester(nestId, nodeId) ?? await getCurrentVersionAuthor(nestId, nodeId);
2044
- const isOwnSubmission = !!actor && actor.toLowerCase() === userEmail.toLowerCase();
2045
- if (hasStewardApprove) {
2046
- if (isOwnSubmission && !((isOwner || isSuper) && allowSelf)) {
2047
- return {
2048
- allowed: false,
2049
- reason: "You submitted this version for review, so you can't approve it yourself. Ask another reviewer to approve it (separation of duties).",
2050
- role: roles.includes("admin") ? "admin" : "reviewer"
2051
- };
2052
- }
2053
- return {
2054
- allowed: true,
2055
- reason: "reviewer access (collaborator or steward)",
2056
- role: roles.includes("admin") ? "admin" : "reviewer"
2057
- };
2058
- }
2059
- if (isOwner || isSuper) {
2060
- if (allowSelf) {
2061
- return {
2062
- allowed: true,
2063
- reason: isOwner ? "nest owner (self-approve enabled)" : "super admin",
2064
- role: isOwner ? "owner" : "super_admin"
2065
- };
2066
- }
2067
- return {
2068
- allowed: false,
2069
- 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.",
2070
- role: isOwner ? "owner" : "super_admin"
2071
- };
2072
- }
2073
- const held = primaryRole(roles);
2074
- return {
2075
- allowed: false,
2076
- 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.",
2077
- role: held
2078
- };
2079
- }
2080
- async function canUserAccess(nestId, nodeId, userEmail) {
2081
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2082
- if (roles.includes("owner")) {
2083
- return { allowed: true, reason: "nest owner", role: "owner" };
2084
- }
2085
- if (isSuperAdmin(userEmail)) {
2086
- return { allowed: true, reason: "super admin", role: "super_admin" };
2087
- }
2088
- if (canViewWith(roles)) {
2089
- return {
2090
- allowed: true,
2091
- reason: "collaborator or steward access",
2092
- role: primaryRole(roles)
2093
- };
2094
- }
2095
- return { allowed: false, reason: "no collaborator or steward access", role: null };
2121
+ return await db.get("SELECT * FROM nests WHERE id = ?", [nestId]) || null;
2096
2122
  }
2097
- async function syncFromConfig(nestId, config2) {
2123
+ async function deleteNest(nestId) {
2098
2124
  const db = getDb();
2099
- let count = 0;
2100
- await db.run(
2101
- "UPDATE stewards SET is_active = 0 WHERE nest_id = ?",
2102
- [nestId]
2103
- );
2104
- await db.run(
2105
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ?",
2106
- [nestId]
2107
- );
2108
- const addEntries = async (scope, entries, target) => {
2109
- for (const entry of entries) {
2110
- const user = await db.get(
2111
- "SELECT id FROM users WHERE email = ?",
2112
- [entry.email]
2113
- );
2114
- const rawRole = entry.role || "reviewer";
2115
- const role = rawRole === "admin" ? "reviewer" : rawRole;
2116
- await assignSteward({
2117
- nestId,
2118
- scope,
2119
- nodePattern: target?.nodePattern,
2120
- tagName: target?.tagName ? target.tagName.trim().replace(/^#+/, "").toLowerCase() : void 0,
2121
- userEmail: entry.email.toLowerCase(),
2122
- userId: user?.id,
2123
- role,
2124
- assignedBy: "config",
2125
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
2126
- isActive: true
2127
- });
2128
- count++;
2129
- }
2130
- };
2131
- if (config2.nest) {
2132
- await addEntries("nest", config2.nest);
2133
- }
2134
- if (config2.tags) {
2135
- for (const [tagName, entries] of Object.entries(config2.tags)) {
2136
- await addEntries("tag", entries, { tagName });
2137
- }
2138
- }
2139
- if (config2.documents) {
2140
- for (const [docPattern, entries] of Object.entries(config2.documents)) {
2141
- await addEntries("document", entries, { nodePattern: docPattern });
2125
+ await db.transaction(async (tx) => {
2126
+ const id = nestId;
2127
+ await tx.run("DELETE FROM approved_versions WHERE nest_id = ?", [id]);
2128
+ await tx.run("DELETE FROM node_versions WHERE nest_id = ?", [id]);
2129
+ await tx.run("DELETE FROM review_requests WHERE nest_id = ?", [id]);
2130
+ await tx.run("DELETE FROM stewards WHERE nest_id = ?", [id]);
2131
+ await tx.run("DELETE FROM nest_collaborators WHERE nest_id = ?", [id]);
2132
+ try {
2133
+ await tx.run("DELETE FROM node_tag_index WHERE nest_id = ?", [id]);
2134
+ } catch {
2142
2135
  }
2136
+ await tx.run("DELETE FROM api_keys WHERE nest_id = ?", [id]);
2137
+ await tx.run("DELETE FROM nests WHERE id = ?", [id]);
2138
+ });
2139
+ const path = resolveNestPath(nestId);
2140
+ try {
2141
+ rmSync2(path, { recursive: true, force: true });
2142
+ } catch (err) {
2143
+ console.warn(`[nests] failed to remove nest directory ${path}:`, err);
2143
2144
  }
2144
- return count;
2145
- }
2146
- function rowToSteward(row) {
2147
- return {
2148
- id: row.id,
2149
- nestId: row.nest_id,
2150
- scope: row.scope,
2151
- nodePattern: row.node_pattern || void 0,
2152
- tagName: row.tag_name || void 0,
2153
- userEmail: row.user_email,
2154
- userId: row.user_id || void 0,
2155
- role: row.role,
2156
- assignedBy: row.assigned_by,
2157
- assignedAt: row.assigned_at,
2158
- isActive: !!row.is_active
2159
- };
2145
+ trackEvent("nest.delete", { nestId });
2160
2146
  }
2161
2147
 
2162
2148
  export {
2149
+ nowExpr,
2150
+ insertOrIgnore,
2151
+ insertOrReplace,
2163
2152
  trackEvent,
2164
2153
  startTelemetryLoop,
2165
2154
  persistSetting,
@@ -2177,31 +2166,23 @@ export {
2177
2166
  getAccessConfig,
2178
2167
  isSuperAdmin,
2179
2168
  isConfigSuperAdmin,
2180
- createTeam,
2181
- findTeamByExternalId,
2182
- listTeamsForUser,
2183
- getTeam,
2184
- renameTeam,
2185
- deleteTeam,
2186
- addMember,
2187
- resolveMemberIdentity,
2188
- replaceTeamRoster,
2189
- updateMemberRole,
2190
- removeMember,
2191
- listNestTeams,
2192
- shareTeamWithNest,
2193
- unshareTeamFromNest,
2194
- isServerAdminUserId,
2195
- resolveNestPermission,
2196
- permissionLevel,
2197
- isPublicReader,
2169
+ verifySmtp,
2170
+ notifyEmailForNest,
2171
+ sendEmailToRecipient,
2198
2172
  nestStorageRoot,
2199
2173
  resolveNestPath,
2174
+ normalizeTag,
2175
+ syncNodeTags,
2176
+ removeNodeFromTagIndex,
2200
2177
  uniqueNestName,
2201
2178
  isStewardshipEnabled,
2202
2179
  setStewardshipEnabled,
2203
2180
  nestAllowsSelfApprove,
2204
2181
  setAllowSelfApprove,
2182
+ nestReviewsPrimeOnly,
2183
+ setPrimeOnlyReview,
2184
+ nestPrimeTags,
2185
+ setPrimeTags,
2205
2186
  disableStewardshipAndWipeGovernance,
2206
2187
  renameNest,
2207
2188
  createNest,
@@ -2214,30 +2195,35 @@ export {
2214
2195
  deleteNest,
2215
2196
  engineCache,
2216
2197
  buildTitleMap,
2198
+ folderForNode,
2217
2199
  describeNode,
2200
+ titleForNode,
2201
+ prettyFolderPath,
2218
2202
  nestName,
2219
2203
  docLink,
2220
2204
  buildDocContext,
2221
- assignSteward,
2222
- removeSteward,
2223
- updateSteward,
2224
- updateStewardRole,
2225
- getSteward,
2226
- getStewardsForNest,
2227
- getStewardsForScope,
2228
- listStewards,
2229
- createStewardRecord,
2230
- resolveStewardsForNode,
2231
- resolveStewardsWithFallback,
2232
- getStewardRolesForUser,
2233
- getStewardsForUser,
2234
- getCollaboratorRole,
2235
- resolveGrantRoles,
2236
- resolveUserRoles,
2237
- canManageStewards,
2238
- canCreateInNest,
2239
- canUserEdit,
2240
- canUserApprove,
2241
- canUserAccess,
2242
- syncFromConfig
2205
+ createTeam,
2206
+ findTeamByExternalId,
2207
+ listTeamsForUser,
2208
+ getTeam,
2209
+ renameTeam,
2210
+ deleteTeam,
2211
+ addMember,
2212
+ resolveMemberIdentity,
2213
+ replaceTeamRoster,
2214
+ updateMemberRole,
2215
+ removeMember,
2216
+ listNestTeams,
2217
+ shareTeamWithNest,
2218
+ unshareTeamFromNest,
2219
+ resolveTeamRolesForUser,
2220
+ collabPermToRole,
2221
+ canViewWith,
2222
+ canEditWith,
2223
+ canApproveWith,
2224
+ primaryRole,
2225
+ isServerAdminUserId,
2226
+ resolveNestPermission,
2227
+ permissionLevel,
2228
+ isPublicReader
2243
2229
  };