@promptowl/contextnest-community 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,12 @@
1
1
  import {
2
2
  ConflictError,
3
+ ForbiddenError,
4
+ NotFoundError,
3
5
  ValidationError
4
- } from "./chunk-3JTODC3Y.js";
6
+ } from "./chunk-GUNJTORH.js";
7
+ import {
8
+ normalizeEmail
9
+ } from "./chunk-FRQJWGN3.js";
5
10
  import {
6
11
  insertOrReplace,
7
12
  nowExpr
@@ -9,18 +14,25 @@ import {
9
14
  import {
10
15
  config,
11
16
  getDb
12
- } from "./chunk-BC6KFUZH.js";
17
+ } from "./chunk-3M7677XW.js";
13
18
  import {
14
19
  ANON_USER_ID
15
20
  } from "./chunk-SLTQACJW.js";
16
21
 
17
22
  // src/governance/stewardship-service.ts
18
- import { v4 as uuid2 } from "uuid";
23
+ import { v4 as uuid3 } from "uuid";
19
24
 
20
25
  // src/governance/access-service.ts
21
26
  import { readFileSync, existsSync } from "fs";
22
27
  import { join } from "path";
23
28
  var accessConfig = null;
29
+ var grantedSuperAdmins = /* @__PURE__ */ new Set();
30
+ async function loadGrantedSuperAdmins() {
31
+ const rows = await getDb().all(
32
+ "SELECT email FROM super_admins"
33
+ );
34
+ grantedSuperAdmins = new Set(rows.map((r) => r.email.toLowerCase()));
35
+ }
24
36
  function loadAccessConfig() {
25
37
  const candidates = [
26
38
  join(config.DATA_ROOT, "access.yaml"),
@@ -36,7 +48,15 @@ function loadAccessConfig() {
36
48
  accessConfig = null;
37
49
  return null;
38
50
  }
51
+ function getAccessConfig() {
52
+ return accessConfig;
53
+ }
39
54
  function isSuperAdmin(email) {
55
+ const lower = email.toLowerCase();
56
+ if (grantedSuperAdmins.has(lower)) return true;
57
+ return isConfigSuperAdmin(lower);
58
+ }
59
+ function isConfigSuperAdmin(email) {
40
60
  if (!accessConfig?.super_admins) return false;
41
61
  return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
42
62
  }
@@ -126,7 +146,7 @@ import { NestStorage } from "@promptowl/contextnest-engine";
126
146
  import { mkdirSync, writeFileSync, rmSync } from "fs";
127
147
  import { join as join2, dirname, isAbsolute } from "path";
128
148
  function isSkippedSegment(segment) {
129
- return segment.startsWith(".") || segment === "node_modules";
149
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
130
150
  }
131
151
  function safeSegments(rel) {
132
152
  if (!rel || isAbsolute(rel)) return null;
@@ -140,14 +160,24 @@ function safeSegments(rel) {
140
160
  }
141
161
  function writeImportedFiles(dest, files) {
142
162
  let written = 0;
163
+ const skipped = [];
143
164
  for (const f of files) {
144
165
  const segs = safeSegments(f?.path || "");
145
- if (!segs) continue;
166
+ if (!segs) {
167
+ skipped.push(f?.path || "(empty)");
168
+ continue;
169
+ }
146
170
  const full = join2(dest, ...segs);
147
171
  mkdirSync(dirname(full), { recursive: true });
148
172
  writeFileSync(full, f.content ?? "", "utf-8");
149
173
  written++;
150
174
  }
175
+ console.log(
176
+ `[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
177
+ );
178
+ if (skipped.length) {
179
+ console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
180
+ }
151
181
  return written;
152
182
  }
153
183
  function discardImportDir(dest) {
@@ -772,6 +802,39 @@ async function listNests(userId) {
772
802
  [userId, ANON_USER_ID]
773
803
  );
774
804
  }
805
+ async function nestsSharedViaTeams(userId) {
806
+ const db = getDb();
807
+ const teams = await db.all(
808
+ "SELECT id, members FROM teams WHERE members LIKE ?",
809
+ [`%${userId}%`]
810
+ );
811
+ const myTeamIds = /* @__PURE__ */ new Set();
812
+ for (const t of teams) {
813
+ try {
814
+ const members = JSON.parse(t.members || "[]");
815
+ if (Array.isArray(members) && members.some((m) => m?.userId === userId)) {
816
+ myTeamIds.add(t.id);
817
+ }
818
+ } catch {
819
+ }
820
+ }
821
+ if (myTeamIds.size === 0) return [];
822
+ const ids = [...myTeamIds];
823
+ const candidates = await db.all(
824
+ `SELECT * FROM nests
825
+ WHERE user_id != ?
826
+ AND (${ids.map(() => "shared_teams LIKE ?").join(" OR ")})`,
827
+ [userId, ...ids.map((id) => `%${id}%`)]
828
+ );
829
+ return candidates.filter((n) => {
830
+ try {
831
+ const refs = JSON.parse(n.shared_teams || "[]");
832
+ return Array.isArray(refs) && refs.some((r) => myTeamIds.has(r?.teamId));
833
+ } catch {
834
+ return false;
835
+ }
836
+ });
837
+ }
775
838
  async function listSharedNests(userId) {
776
839
  const db = getDb();
777
840
  const caller = await db.get("SELECT email FROM users WHERE id = ?", [
@@ -784,7 +847,7 @@ async function listSharedNests(userId) {
784
847
  [userId]
785
848
  );
786
849
  }
787
- return await db.all(
850
+ const direct = await db.all(
788
851
  `SELECT DISTINCT n.* FROM nests n
789
852
  LEFT JOIN nest_collaborators nc
790
853
  ON nc.nest_id = n.id AND nc.user_id = ?
@@ -797,6 +860,17 @@ async function listSharedNests(userId) {
797
860
  ORDER BY n.created_at DESC`,
798
861
  [userId, userId, userId]
799
862
  );
863
+ const seen = new Set(direct.map((n) => n.id));
864
+ for (const n of await nestsSharedViaTeams(userId)) {
865
+ if (!seen.has(n.id)) {
866
+ direct.push(n);
867
+ seen.add(n.id);
868
+ }
869
+ }
870
+ direct.sort(
871
+ (a, b) => a.created_at === b.created_at ? 0 : a.created_at < b.created_at ? 1 : -1
872
+ );
873
+ return direct;
800
874
  }
801
875
  async function listPublicNests(userId) {
802
876
  const db = getDb();
@@ -869,9 +943,7 @@ var FlatNestStorage = class extends NestStorage2 {
869
943
  if (META_FILES.has(e.name)) continue;
870
944
  const dir = e.parentPath ?? e.path ?? root;
871
945
  const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
872
- if (rel.split("/").some(isSkippedSegment)) {
873
- continue;
874
- }
946
+ if (rel.split("/").some(isSkippedSegment)) continue;
875
947
  ids.push(rel.replace(/\.md$/, ""));
876
948
  }
877
949
  const nodes = [];
@@ -957,7 +1029,452 @@ async function buildDocContext(nestId, nodeId, baseUrl) {
957
1029
  };
958
1030
  }
959
1031
 
1032
+ // src/governance/teams-service.ts
1033
+ import { v4 as uuid2 } from "uuid";
1034
+ var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
1035
+ function parseMembers(json) {
1036
+ if (!json) return [];
1037
+ try {
1038
+ const parsed = JSON.parse(json);
1039
+ return Array.isArray(parsed) ? parsed : [];
1040
+ } catch {
1041
+ return [];
1042
+ }
1043
+ }
1044
+ function rowToTeam(row) {
1045
+ return {
1046
+ id: row.id,
1047
+ name: row.name,
1048
+ owner_id: row.owner_id,
1049
+ members: parseMembers(row.members),
1050
+ source: row.source ?? null,
1051
+ external_id: row.external_id ?? null,
1052
+ created_at: row.created_at,
1053
+ updated_at: row.updated_at
1054
+ };
1055
+ }
1056
+ async function enrichTeam(team) {
1057
+ const db = getDb();
1058
+ const ids = team.members.map((m) => m.userId);
1059
+ const invited = /* @__PURE__ */ new Map();
1060
+ if (ids.length) {
1061
+ const rows = await db.all(
1062
+ `SELECT id, is_invited FROM users WHERE id IN (${ids.map(() => "?").join(",")})`,
1063
+ ids
1064
+ );
1065
+ for (const r of rows) invited.set(r.id, r.is_invited);
1066
+ }
1067
+ const owner = await db.get("SELECT email FROM users WHERE id = ?", [
1068
+ team.owner_id
1069
+ ]);
1070
+ return {
1071
+ ...team,
1072
+ owner_email: owner?.email,
1073
+ // Unknown id (deleted user) → treat as not registered.
1074
+ members: team.members.map((m) => ({
1075
+ ...m,
1076
+ registered: (invited.get(m.userId) ?? 1) === 0
1077
+ }))
1078
+ };
1079
+ }
1080
+ function assertValidRole(role) {
1081
+ if (!VALID_TEAM_ROLES.includes(role)) {
1082
+ throw new ValidationError(
1083
+ "role must be admin, editor, or viewer"
1084
+ );
1085
+ }
1086
+ }
1087
+ async function loadTeamRow(teamId) {
1088
+ const db = getDb();
1089
+ const row = await db.get("SELECT * FROM teams WHERE id = ?", [
1090
+ teamId
1091
+ ]);
1092
+ if (!row) throw new NotFoundError("Team not found");
1093
+ return row;
1094
+ }
1095
+ var ROLE_RANK = {
1096
+ viewer: 1,
1097
+ editor: 2,
1098
+ admin: 3
1099
+ };
1100
+ var OWNER_RANK = 4;
1101
+ function managementRank(row, callerUserId, allowAdmin) {
1102
+ if (row.owner_id === callerUserId || allowAdmin) return OWNER_RANK;
1103
+ const m = parseMembers(row.members).find((x) => x.userId === callerUserId);
1104
+ return m ? ROLE_RANK[m.role] ?? 0 : 0;
1105
+ }
1106
+ async function createTeam(params) {
1107
+ const name = params.name?.trim();
1108
+ if (!name) throw new ValidationError("Team name is required");
1109
+ const db = getDb();
1110
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1111
+ const id = uuid2();
1112
+ await db.run(
1113
+ "INSERT INTO teams (id, name, owner_id, members, source, external_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1114
+ [
1115
+ id,
1116
+ name,
1117
+ params.ownerId,
1118
+ "[]",
1119
+ params.source ?? null,
1120
+ params.externalId ?? null,
1121
+ now,
1122
+ now
1123
+ ]
1124
+ );
1125
+ return enrichTeam(rowToTeam(await loadTeamRow(id)));
1126
+ }
1127
+ async function findTeamByExternalId(params) {
1128
+ const db = getDb();
1129
+ const row = await db.get(
1130
+ "SELECT * FROM teams WHERE owner_id = ? AND source = ? AND external_id = ?",
1131
+ [params.ownerId, params.source, params.externalId]
1132
+ );
1133
+ return row ? enrichTeam(rowToTeam(row)) : null;
1134
+ }
1135
+ async function listTeamsForUser(userId) {
1136
+ const db = getDb();
1137
+ const rows = await db.all(
1138
+ "SELECT * FROM teams WHERE owner_id = ? OR members LIKE ? ORDER BY created_at",
1139
+ [userId, `%${userId}%`]
1140
+ );
1141
+ const mine = rows.filter(
1142
+ (r) => r.owner_id === userId || parseMembers(r.members).some((m) => m.userId === userId)
1143
+ );
1144
+ return Promise.all(mine.map((r) => enrichTeam(rowToTeam(r))));
1145
+ }
1146
+ async function getTeam(teamId) {
1147
+ return enrichTeam(rowToTeam(await loadTeamRow(teamId)));
1148
+ }
1149
+ async function renameTeam(params) {
1150
+ const name = params.name?.trim();
1151
+ if (!name) throw new ValidationError("Team name is required");
1152
+ const row = await loadTeamRow(params.teamId);
1153
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1154
+ throw new ForbiddenError("Only a team admin or the owner can rename this team.");
1155
+ }
1156
+ const db = getDb();
1157
+ await db.transaction(async (tx) => {
1158
+ await tx.run("UPDATE teams SET name = ?, updated_at = ? WHERE id = ?", [
1159
+ name,
1160
+ (/* @__PURE__ */ new Date()).toISOString(),
1161
+ params.teamId
1162
+ ]);
1163
+ await refreshTeamNameRefs(tx, params.teamId, name);
1164
+ });
1165
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1166
+ }
1167
+ async function deleteTeam(params) {
1168
+ const row = await loadTeamRow(params.teamId);
1169
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < OWNER_RANK) {
1170
+ throw new ForbiddenError("Only the team owner can delete this team.");
1171
+ }
1172
+ const db = getDb();
1173
+ await db.transaction(async (tx) => {
1174
+ await tx.run("DELETE FROM teams WHERE id = ?", [params.teamId]);
1175
+ await removeTeamRefs(tx, params.teamId);
1176
+ });
1177
+ }
1178
+ async function addMember(params) {
1179
+ assertValidRole(params.role);
1180
+ const row = await loadTeamRow(params.teamId);
1181
+ const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
1182
+ if (rank < ROLE_RANK.editor) {
1183
+ throw new ForbiddenError(
1184
+ "Only an editor or admin member (or the owner) can add people to this team."
1185
+ );
1186
+ }
1187
+ if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
1188
+ throw new ForbiddenError("You can't give a member a higher role than your own.");
1189
+ }
1190
+ const { userId: resolvedId, email: resolvedEmail } = await resolveMemberIdentity(params);
1191
+ await mutateMembers(params.teamId, (members) => {
1192
+ if (members.some((m) => m.userId === resolvedId)) {
1193
+ throw new ConflictError(
1194
+ `${resolvedEmail || "This user"} is already a member of this team. Change their role instead of adding them again.`
1195
+ );
1196
+ }
1197
+ return [
1198
+ ...members,
1199
+ { userId: resolvedId, email: resolvedEmail, role: params.role }
1200
+ ];
1201
+ });
1202
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1203
+ }
1204
+ async function resolveMemberIdentity(params) {
1205
+ const db = getDb();
1206
+ if (params.userId) {
1207
+ const u = await db.get("SELECT email FROM users WHERE id = ?", [
1208
+ params.userId
1209
+ ]);
1210
+ if (!u) throw new ValidationError("User not found");
1211
+ return { userId: params.userId, email: normalizeEmail(u.email ?? "") };
1212
+ }
1213
+ const requested = params.email ? normalizeEmail(params.email) : void 0;
1214
+ if (!requested) {
1215
+ throw new ValidationError("user_id or email is required");
1216
+ }
1217
+ const existing = await db.get(
1218
+ "SELECT id, email FROM users WHERE LOWER(email) = ?",
1219
+ [requested]
1220
+ );
1221
+ if (existing) {
1222
+ return {
1223
+ userId: existing.id,
1224
+ email: normalizeEmail(existing.email ?? requested)
1225
+ };
1226
+ }
1227
+ const { hashPassword } = await import("./keys-73STFJJB.js");
1228
+ const userId = uuid2();
1229
+ await db.run(
1230
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1231
+ [userId, requested, null, await hashPassword(uuid2())]
1232
+ );
1233
+ return { userId, email: requested };
1234
+ }
1235
+ async function replaceTeamRoster(params) {
1236
+ for (const m of params.members) assertValidRole(m.role);
1237
+ const row = await loadTeamRow(params.teamId);
1238
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1239
+ throw new ForbiddenError(
1240
+ "Only a team admin or the owner can replace this team's roster."
1241
+ );
1242
+ }
1243
+ await mutateMembers(
1244
+ params.teamId,
1245
+ () => params.members.map((m) => ({
1246
+ userId: m.userId,
1247
+ email: m.email,
1248
+ role: m.role
1249
+ }))
1250
+ );
1251
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1252
+ }
1253
+ async function updateMemberRole(params) {
1254
+ assertValidRole(params.role);
1255
+ const row = await loadTeamRow(params.teamId);
1256
+ const rank = managementRank(row, params.callerUserId, !!params.allowAdmin);
1257
+ if (rank < ROLE_RANK.admin) {
1258
+ throw new ForbiddenError("Only a team admin or the owner can change roles.");
1259
+ }
1260
+ if (rank < OWNER_RANK && ROLE_RANK[params.role] > rank) {
1261
+ throw new ForbiddenError("You can't set a role higher than your own.");
1262
+ }
1263
+ await mutateMembers(params.teamId, (members) => {
1264
+ const member = members.find((m) => m.userId === params.userId);
1265
+ if (!member) throw new NotFoundError("Team member not found");
1266
+ member.role = params.role;
1267
+ return members;
1268
+ });
1269
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1270
+ }
1271
+ async function removeMember(params) {
1272
+ const row = await loadTeamRow(params.teamId);
1273
+ if (managementRank(row, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.admin) {
1274
+ throw new ForbiddenError("Only a team admin or the owner can remove members.");
1275
+ }
1276
+ await mutateMembers(params.teamId, (members) => {
1277
+ const next = members.filter((m) => m.userId !== params.userId);
1278
+ if (next.length === members.length) {
1279
+ throw new NotFoundError("Team member not found");
1280
+ }
1281
+ return next;
1282
+ });
1283
+ return enrichTeam(rowToTeam(await loadTeamRow(params.teamId)));
1284
+ }
1285
+ async function mutateMembers(teamId, mutate) {
1286
+ const db = getDb();
1287
+ await db.transaction(async (tx) => {
1288
+ const row = await tx.get("SELECT members FROM teams WHERE id = ?", [
1289
+ teamId
1290
+ ]);
1291
+ if (!row) throw new NotFoundError("Team not found");
1292
+ const next = mutate(parseMembers(row.members));
1293
+ await tx.run("UPDATE teams SET members = ?, updated_at = ? WHERE id = ?", [
1294
+ JSON.stringify(next),
1295
+ (/* @__PURE__ */ new Date()).toISOString(),
1296
+ teamId
1297
+ ]);
1298
+ });
1299
+ }
1300
+ function parseSharedTeams(json) {
1301
+ if (!json) return [];
1302
+ try {
1303
+ const parsed = JSON.parse(json);
1304
+ return Array.isArray(parsed) ? parsed : [];
1305
+ } catch {
1306
+ return [];
1307
+ }
1308
+ }
1309
+ async function listNestTeams(nestId) {
1310
+ const db = getDb();
1311
+ const row = await db.get("SELECT shared_teams FROM nests WHERE id = ?", [
1312
+ nestId
1313
+ ]);
1314
+ if (!row) throw new NotFoundError("Nest not found");
1315
+ return parseSharedTeams(row.shared_teams);
1316
+ }
1317
+ async function shareTeamWithNest(params) {
1318
+ const db = getDb();
1319
+ return db.transaction(async (tx) => {
1320
+ const team = await tx.get(
1321
+ "SELECT id, name, owner_id, members FROM teams WHERE id = ?",
1322
+ [params.teamId]
1323
+ );
1324
+ if (!team) throw new NotFoundError("Team not found");
1325
+ if (managementRank(team, params.callerUserId, !!params.allowAdmin) < ROLE_RANK.editor) {
1326
+ throw new ForbiddenError(
1327
+ "You need to be an editor, admin, or owner of a team to share it. View-only members can't."
1328
+ );
1329
+ }
1330
+ const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
1331
+ params.nestId
1332
+ ]);
1333
+ if (!nest) throw new NotFoundError("Nest not found");
1334
+ const refs = parseSharedTeams(nest.shared_teams);
1335
+ if (refs.some((r) => r.teamId === params.teamId)) {
1336
+ throw new ConflictError("This team is already shared with the nest.");
1337
+ }
1338
+ refs.push({ teamId: team.id, teamName: team.name });
1339
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1340
+ JSON.stringify(refs),
1341
+ params.nestId
1342
+ ]);
1343
+ const hasGovernanceRole = parseMembers(team.members).some(
1344
+ (m) => m.role === "viewer" || m.role === "editor"
1345
+ );
1346
+ if (hasGovernanceRole) {
1347
+ await tx.run("UPDATE nests SET stewardship_enabled = 1 WHERE id = ?", [
1348
+ params.nestId
1349
+ ]);
1350
+ }
1351
+ return refs;
1352
+ });
1353
+ }
1354
+ async function unshareTeamFromNest(params) {
1355
+ const db = getDb();
1356
+ return db.transaction(async (tx) => {
1357
+ const nest = await tx.get("SELECT shared_teams FROM nests WHERE id = ?", [
1358
+ params.nestId
1359
+ ]);
1360
+ if (!nest) throw new NotFoundError("Nest not found");
1361
+ const refs = parseSharedTeams(nest.shared_teams);
1362
+ const next = refs.filter((r) => r.teamId !== params.teamId);
1363
+ if (next.length === refs.length) {
1364
+ throw new NotFoundError("Team is not shared with this nest.");
1365
+ }
1366
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1367
+ JSON.stringify(next),
1368
+ params.nestId
1369
+ ]);
1370
+ return next;
1371
+ });
1372
+ }
1373
+ async function refreshTeamNameRefs(tx, teamId, newName) {
1374
+ const nests = await tx.all(
1375
+ "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
1376
+ [`%${teamId}%`]
1377
+ );
1378
+ for (const n of nests) {
1379
+ const refs = parseSharedTeams(n.shared_teams);
1380
+ let changed = false;
1381
+ for (const r of refs) {
1382
+ if (r.teamId === teamId && r.teamName !== newName) {
1383
+ r.teamName = newName;
1384
+ changed = true;
1385
+ }
1386
+ }
1387
+ if (changed) {
1388
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1389
+ JSON.stringify(refs),
1390
+ n.id
1391
+ ]);
1392
+ }
1393
+ }
1394
+ }
1395
+ async function removeTeamRefs(tx, teamId) {
1396
+ const nests = await tx.all(
1397
+ "SELECT id, shared_teams FROM nests WHERE shared_teams LIKE ?",
1398
+ [`%${teamId}%`]
1399
+ );
1400
+ for (const n of nests) {
1401
+ const refs = parseSharedTeams(n.shared_teams);
1402
+ const next = refs.filter((r) => r.teamId !== teamId);
1403
+ if (next.length !== refs.length) {
1404
+ await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
1405
+ JSON.stringify(next),
1406
+ n.id
1407
+ ]);
1408
+ }
1409
+ }
1410
+ }
1411
+ async function resolveTeamRolesForUser(nestId, userId) {
1412
+ const refs = await listNestTeams(nestId).catch(() => []);
1413
+ if (refs.length === 0) return [];
1414
+ const db = getDb();
1415
+ const roles = /* @__PURE__ */ new Set();
1416
+ for (const ref of refs) {
1417
+ const row = await db.get("SELECT members FROM teams WHERE id = ?", [
1418
+ ref.teamId
1419
+ ]);
1420
+ if (!row) continue;
1421
+ const member = parseMembers(row.members).find((m) => m.userId === userId);
1422
+ if (member) roles.add(member.role);
1423
+ }
1424
+ return [...roles];
1425
+ }
1426
+
1427
+ // src/governance/roles.ts
1428
+ function collabPermToRole(permission) {
1429
+ switch (permission) {
1430
+ case "owner":
1431
+ return "owner";
1432
+ case "admin":
1433
+ return "admin";
1434
+ case "write":
1435
+ return "editor";
1436
+ case "read":
1437
+ return "viewer";
1438
+ default:
1439
+ return null;
1440
+ }
1441
+ }
1442
+ function teamRoleToPermission(role) {
1443
+ switch (role) {
1444
+ case "admin":
1445
+ return "admin";
1446
+ case "editor":
1447
+ return "write";
1448
+ case "viewer":
1449
+ return "read";
1450
+ default:
1451
+ return null;
1452
+ }
1453
+ }
1454
+ var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1455
+ var canViewWith = (roles) => roles.length > 0;
1456
+ var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1457
+ var PRECEDENCE = [
1458
+ "owner",
1459
+ "admin",
1460
+ "editor",
1461
+ "reviewer",
1462
+ "viewer"
1463
+ ];
1464
+ function primaryRole(roles) {
1465
+ for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1466
+ return null;
1467
+ }
1468
+
960
1469
  // src/shared/access.ts
1470
+ async function isServerAdminUserId(userId) {
1471
+ if (await isLicenseAdminUserId(userId)) return true;
1472
+ const row = await getDb().get(
1473
+ "SELECT email FROM users WHERE id = ?",
1474
+ [userId]
1475
+ );
1476
+ return !!row?.email && isSuperAdmin(row.email);
1477
+ }
961
1478
  var PERMISSION_LEVELS = {
962
1479
  none: 0,
963
1480
  read: 1,
@@ -978,11 +1495,18 @@ async function resolveNestPermission(nestId, userId) {
978
1495
  if (caller?.email && isSuperAdmin(caller.email)) {
979
1496
  return "admin";
980
1497
  }
1498
+ const higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1499
+ let best = "none";
981
1500
  const directGrant = await db.get(
982
1501
  "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
983
1502
  [nestId, userId]
984
1503
  );
985
- if (directGrant) return directGrant.permission;
1504
+ if (directGrant) best = higher(best, directGrant.permission);
1505
+ if (best === "admin") return best;
1506
+ for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1507
+ best = higher(best, teamRoleToPermission(role));
1508
+ }
1509
+ if (best === "admin") return best;
986
1510
  const stewardGrant = await db.get(
987
1511
  `SELECT 1 FROM stewards s
988
1512
  JOIN users u ON u.id = ?
@@ -991,9 +1515,9 @@ async function resolveNestPermission(nestId, userId) {
991
1515
  LIMIT 1`,
992
1516
  [userId, nestId]
993
1517
  );
994
- if (stewardGrant) return "read";
995
- if (nest.visibility === "public") return "read";
996
- return "none";
1518
+ if (stewardGrant) best = higher(best, "read");
1519
+ if (nest.visibility === "public") best = higher(best, "read");
1520
+ return best;
997
1521
  }
998
1522
  function permissionLevel(p) {
999
1523
  return PERMISSION_LEVELS[p] ?? 0;
@@ -1021,43 +1545,14 @@ async function isPublicReader(nestId, userId) {
1021
1545
  [userId, nestId]
1022
1546
  );
1023
1547
  if (stewardGrant) return false;
1548
+ if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
1024
1549
  return true;
1025
1550
  }
1026
1551
 
1027
- // src/governance/roles.ts
1028
- function collabPermToRole(permission) {
1029
- switch (permission) {
1030
- case "owner":
1031
- return "owner";
1032
- case "admin":
1033
- return "admin";
1034
- case "write":
1035
- return "editor";
1036
- case "read":
1037
- return "viewer";
1038
- default:
1039
- return null;
1040
- }
1041
- }
1042
- var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1043
- var canViewWith = (roles) => roles.length > 0;
1044
- var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1045
- var PRECEDENCE = [
1046
- "owner",
1047
- "admin",
1048
- "editor",
1049
- "reviewer",
1050
- "viewer"
1051
- ];
1052
- function primaryRole(roles) {
1053
- for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1054
- return null;
1055
- }
1056
-
1057
1552
  // src/governance/stewardship-service.ts
1058
1553
  async function assignSteward(data) {
1059
1554
  const db = getDb();
1060
- const id = uuid2();
1555
+ const id = uuid3();
1061
1556
  await db.run(
1062
1557
  `INSERT INTO stewards
1063
1558
  (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
@@ -1258,10 +1753,10 @@ async function createStewardRecord(params) {
1258
1753
  let userId = userRow?.id;
1259
1754
  if (!userId) {
1260
1755
  const { hashPassword } = await import("./keys-73STFJJB.js");
1261
- userId = uuid2();
1756
+ userId = uuid3();
1262
1757
  await db.run(
1263
1758
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1264
- [userId, email, null, await hashPassword(uuid2())]
1759
+ [userId, email, null, await hashPassword(uuid3())]
1265
1760
  );
1266
1761
  }
1267
1762
  const created = await assignSteward({
@@ -1388,6 +1883,9 @@ async function resolveUserRoles(nestId, userEmail, opts) {
1388
1883
  if (userId) {
1389
1884
  const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
1390
1885
  if (collabRole) roles.add(collabRole);
1886
+ for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1887
+ roles.add(role);
1888
+ }
1391
1889
  }
1392
1890
  const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
1393
1891
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
@@ -1596,8 +2094,28 @@ export {
1596
2094
  isSuspended,
1597
2095
  getSuspensionReason,
1598
2096
  validateLicense,
2097
+ loadGrantedSuperAdmins,
1599
2098
  loadAccessConfig,
2099
+ getAccessConfig,
1600
2100
  isSuperAdmin,
2101
+ isConfigSuperAdmin,
2102
+ createTeam,
2103
+ findTeamByExternalId,
2104
+ listTeamsForUser,
2105
+ getTeam,
2106
+ renameTeam,
2107
+ deleteTeam,
2108
+ addMember,
2109
+ resolveMemberIdentity,
2110
+ replaceTeamRoster,
2111
+ updateMemberRole,
2112
+ removeMember,
2113
+ listNestTeams,
2114
+ shareTeamWithNest,
2115
+ unshareTeamFromNest,
2116
+ resolveTeamRolesForUser,
2117
+ collabPermToRole,
2118
+ isServerAdminUserId,
1601
2119
  resolveNestPermission,
1602
2120
  permissionLevel,
1603
2121
  isPublicReader,
@@ -1623,7 +2141,6 @@ export {
1623
2141
  nestName,
1624
2142
  docLink,
1625
2143
  buildDocContext,
1626
- collabPermToRole,
1627
2144
  assignSteward,
1628
2145
  removeSteward,
1629
2146
  updateSteward,