@promptowl/contextnest-community 1.9.0 → 1.11.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-DPHV6Q26.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) {
@@ -235,7 +265,7 @@ async function flushTelemetry() {
235
265
  try {
236
266
  const res = await fetch(url, {
237
267
  method: "POST",
238
- headers: { "Content-Type": "application/json" },
268
+ headers: config.PROMPTOWL_FETCH_HEADERS,
239
269
  body: JSON.stringify(payload)
240
270
  });
241
271
  console.log(`[telemetry] response: ${res.status} ${res.statusText}`);
@@ -261,8 +291,53 @@ function startTelemetryLoop() {
261
291
  );
262
292
  }
263
293
 
294
+ // src/db/settings.ts
295
+ var deployBaseline = null;
296
+ function norm(v) {
297
+ return v === void 0 || v === null ? null : v;
298
+ }
299
+ function baseline() {
300
+ if (!deployBaseline) deployBaseline = { ...process.env };
301
+ return deployBaseline;
302
+ }
303
+ async function persistSetting(name, value) {
304
+ const db = getDb();
305
+ const envAtWrite = norm(baseline()[name]);
306
+ await db.run(
307
+ insertOrReplace(
308
+ db,
309
+ `INSERT INTO server_settings (key, value, env_at_write, updated_at)
310
+ VALUES (?, ?, ?, ${nowExpr(db)})`,
311
+ ["key"],
312
+ `value = excluded.value, env_at_write = excluded.env_at_write, updated_at = ${nowExpr(db)}`
313
+ ),
314
+ [name, value, envAtWrite]
315
+ );
316
+ }
317
+ async function loadServerSettings() {
318
+ deployBaseline = { ...process.env };
319
+ const base = deployBaseline;
320
+ const db = getDb();
321
+ const rows = await db.all(
322
+ "SELECT key, value, env_at_write FROM server_settings"
323
+ );
324
+ let applied = 0;
325
+ for (const row of rows) {
326
+ const envVal = norm(base[row.key]);
327
+ if (envVal !== norm(row.env_at_write)) {
328
+ await persistSetting(row.key, envVal);
329
+ if (envVal === null) delete process.env[row.key];
330
+ else process.env[row.key] = envVal;
331
+ } else {
332
+ if (row.value === null) delete process.env[row.key];
333
+ else process.env[row.key] = row.value;
334
+ }
335
+ applied++;
336
+ }
337
+ return applied;
338
+ }
339
+
264
340
  // src/auth/license.ts
265
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
266
341
  var currentLicense = null;
267
342
  function getCurrentLicense() {
268
343
  return currentLicense;
@@ -284,21 +359,6 @@ async function isLicenseAdminUserId(userId) {
284
359
  return false;
285
360
  }
286
361
  }
287
- function upsertEnvVar(filePath, varName, value) {
288
- const prefix = `${varName}=`;
289
- let lines = [];
290
- if (existsSync2(filePath)) {
291
- lines = readFileSync2(filePath, "utf8").split(/\r?\n/);
292
- }
293
- const filtered = lines.filter((line) => !line.trimStart().startsWith(prefix));
294
- if (value !== null) {
295
- filtered.push(`${prefix}${value}`);
296
- }
297
- while (filtered.length && filtered[filtered.length - 1] === "") {
298
- filtered.pop();
299
- }
300
- writeFileSync2(filePath, filtered.join("\n") + "\n", "utf8");
301
- }
302
362
  async function installLicenseKey(key) {
303
363
  const trimmed = key.trim();
304
364
  if (!trimmed.startsWith("pk_")) {
@@ -315,12 +375,12 @@ async function installLicenseKey(key) {
315
375
  return { ...info, persisted: false };
316
376
  }
317
377
  try {
318
- upsertEnvVar(config.ENV_FILE_PATH, "PROMPTOWL_KEY", trimmed);
378
+ await persistSetting("PROMPTOWL_KEY", trimmed);
319
379
  return { ...info, persisted: true };
320
380
  } catch (err) {
321
381
  const persistError = err instanceof Error ? err.message : String(err);
322
382
  console.warn(
323
- `[license] validated but FAILED to persist to ${config.ENV_FILE_PATH}: ${persistError}`
383
+ `[license] validated but FAILED to persist to the database: ${persistError}`
324
384
  );
325
385
  return { ...info, persisted: false, persistError };
326
386
  }
@@ -360,12 +420,12 @@ async function handleLicenseRevoked() {
360
420
  console.warn("[license] failed to wipe sessions:", err);
361
421
  }
362
422
  try {
363
- upsertEnvVar(config.ENV_FILE_PATH, "PROMPTOWL_KEY", null);
423
+ await persistSetting("PROMPTOWL_KEY", null);
364
424
  console.warn(
365
- `[license] revoked \u2014 removed PROMPTOWL_KEY from ${config.ENV_FILE_PATH}`
425
+ "[license] revoked \u2014 cleared persisted PROMPTOWL_KEY from the database"
366
426
  );
367
427
  } catch (err) {
368
- console.warn("[license] failed to strip key from .env:", err);
428
+ console.warn("[license] failed to clear persisted key:", err);
369
429
  }
370
430
  process.env.PROMPTOWL_KEY = "";
371
431
  currentLicense = {
@@ -430,7 +490,7 @@ async function _validateLicenseImpl(forceFresh) {
430
490
  const promptowlUrl = config.PROMPTOWL_API_URL.replace(/\/$/, "");
431
491
  const res = await fetch(`${promptowlUrl}/api/license/validate`, {
432
492
  method: "POST",
433
- headers: { "Content-Type": "application/json" },
493
+ headers: config.PROMPTOWL_FETCH_HEADERS,
434
494
  body: JSON.stringify({ key })
435
495
  });
436
496
  if (!res.ok) {
@@ -742,6 +802,39 @@ async function listNests(userId) {
742
802
  [userId, ANON_USER_ID]
743
803
  );
744
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
+ }
745
838
  async function listSharedNests(userId) {
746
839
  const db = getDb();
747
840
  const caller = await db.get("SELECT email FROM users WHERE id = ?", [
@@ -754,7 +847,7 @@ async function listSharedNests(userId) {
754
847
  [userId]
755
848
  );
756
849
  }
757
- return await db.all(
850
+ const direct = await db.all(
758
851
  `SELECT DISTINCT n.* FROM nests n
759
852
  LEFT JOIN nest_collaborators nc
760
853
  ON nc.nest_id = n.id AND nc.user_id = ?
@@ -767,6 +860,17 @@ async function listSharedNests(userId) {
767
860
  ORDER BY n.created_at DESC`,
768
861
  [userId, userId, userId]
769
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;
770
874
  }
771
875
  async function listPublicNests(userId) {
772
876
  const db = getDb();
@@ -839,9 +943,7 @@ var FlatNestStorage = class extends NestStorage2 {
839
943
  if (META_FILES.has(e.name)) continue;
840
944
  const dir = e.parentPath ?? e.path ?? root;
841
945
  const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
842
- if (rel.split("/").some(isSkippedSegment)) {
843
- continue;
844
- }
946
+ if (rel.split("/").some(isSkippedSegment)) continue;
845
947
  ids.push(rel.replace(/\.md$/, ""));
846
948
  }
847
949
  const nodes = [];
@@ -890,6 +992,12 @@ async function buildTitleMap(nestId) {
890
992
  function folderForNode(nodeId) {
891
993
  return nodeId.replace(/^nodes\//, "").split("/").slice(0, -1).join("/");
892
994
  }
995
+ async function describeNode(nestId, nodeId) {
996
+ const titles = await buildTitleMap(nestId);
997
+ const title = titles.get(nodeId) || nodeId;
998
+ const folder = folderForNode(nodeId);
999
+ return folder ? `${title} (in ${folder})` : title;
1000
+ }
893
1001
  async function nestName(nestId) {
894
1002
  try {
895
1003
  const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
@@ -921,7 +1029,452 @@ async function buildDocContext(nestId, nodeId, baseUrl) {
921
1029
  };
922
1030
  }
923
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
+
924
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
+ }
925
1478
  var PERMISSION_LEVELS = {
926
1479
  none: 0,
927
1480
  read: 1,
@@ -942,11 +1495,18 @@ async function resolveNestPermission(nestId, userId) {
942
1495
  if (caller?.email && isSuperAdmin(caller.email)) {
943
1496
  return "admin";
944
1497
  }
1498
+ const higher = (a, b) => b && PERMISSION_LEVELS[b] > PERMISSION_LEVELS[a] ? b : a;
1499
+ let best = "none";
945
1500
  const directGrant = await db.get(
946
1501
  "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
947
1502
  [nestId, userId]
948
1503
  );
949
- 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;
950
1510
  const stewardGrant = await db.get(
951
1511
  `SELECT 1 FROM stewards s
952
1512
  JOIN users u ON u.id = ?
@@ -955,9 +1515,9 @@ async function resolveNestPermission(nestId, userId) {
955
1515
  LIMIT 1`,
956
1516
  [userId, nestId]
957
1517
  );
958
- if (stewardGrant) return "read";
959
- if (nest.visibility === "public") return "read";
960
- return "none";
1518
+ if (stewardGrant) best = higher(best, "read");
1519
+ if (nest.visibility === "public") best = higher(best, "read");
1520
+ return best;
961
1521
  }
962
1522
  function permissionLevel(p) {
963
1523
  return PERMISSION_LEVELS[p] ?? 0;
@@ -985,43 +1545,14 @@ async function isPublicReader(nestId, userId) {
985
1545
  [userId, nestId]
986
1546
  );
987
1547
  if (stewardGrant) return false;
1548
+ if ((await resolveTeamRolesForUser(nestId, userId)).length > 0) return false;
988
1549
  return true;
989
1550
  }
990
1551
 
991
- // src/governance/roles.ts
992
- function collabPermToRole(permission) {
993
- switch (permission) {
994
- case "owner":
995
- return "owner";
996
- case "admin":
997
- return "admin";
998
- case "write":
999
- return "editor";
1000
- case "read":
1001
- return "viewer";
1002
- default:
1003
- return null;
1004
- }
1005
- }
1006
- var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1007
- var canViewWith = (roles) => roles.length > 0;
1008
- var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1009
- var PRECEDENCE = [
1010
- "owner",
1011
- "admin",
1012
- "editor",
1013
- "reviewer",
1014
- "viewer"
1015
- ];
1016
- function primaryRole(roles) {
1017
- for (const r of PRECEDENCE) if (roles.includes(r)) return r;
1018
- return null;
1019
- }
1020
-
1021
1552
  // src/governance/stewardship-service.ts
1022
1553
  async function assignSteward(data) {
1023
1554
  const db = getDb();
1024
- const id = uuid2();
1555
+ const id = uuid3();
1025
1556
  await db.run(
1026
1557
  `INSERT INTO stewards
1027
1558
  (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
@@ -1219,13 +1750,22 @@ async function createStewardRecord(params) {
1219
1750
  "SELECT id FROM users WHERE email = ?",
1220
1751
  [email]
1221
1752
  );
1753
+ let userId = userRow?.id;
1754
+ if (!userId) {
1755
+ const { hashPassword } = await import("./keys-73STFJJB.js");
1756
+ userId = uuid3();
1757
+ await db.run(
1758
+ "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
1759
+ [userId, email, null, await hashPassword(uuid3())]
1760
+ );
1761
+ }
1222
1762
  const created = await assignSteward({
1223
1763
  nestId: params.nestId,
1224
1764
  scope: params.scope,
1225
1765
  nodePattern,
1226
1766
  tagName,
1227
1767
  userEmail: email,
1228
- userId: userRow?.id,
1768
+ userId,
1229
1769
  role: user.role ?? "reviewer",
1230
1770
  assignedBy: params.assignedBy,
1231
1771
  assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1343,6 +1883,9 @@ async function resolveUserRoles(nestId, userEmail, opts) {
1343
1883
  if (userId) {
1344
1884
  const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
1345
1885
  if (collabRole) roles.add(collabRole);
1886
+ for (const role of await resolveTeamRolesForUser(nestId, userId)) {
1887
+ roles.add(role);
1888
+ }
1346
1889
  }
1347
1890
  const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
1348
1891
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
@@ -1541,17 +2084,38 @@ function rowToSteward(row) {
1541
2084
  export {
1542
2085
  trackEvent,
1543
2086
  startTelemetryLoop,
2087
+ persistSetting,
2088
+ loadServerSettings,
1544
2089
  getCurrentLicense,
1545
2090
  isLicenseAdminEmail,
1546
2091
  isLicenseAdminUserId,
1547
- upsertEnvVar,
1548
2092
  installLicenseKey,
1549
2093
  startLicenseSafetyPoll,
1550
2094
  isSuspended,
1551
2095
  getSuspensionReason,
1552
2096
  validateLicense,
2097
+ loadGrantedSuperAdmins,
1553
2098
  loadAccessConfig,
2099
+ getAccessConfig,
1554
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,
1555
2119
  resolveNestPermission,
1556
2120
  permissionLevel,
1557
2121
  isPublicReader,
@@ -1573,10 +2137,10 @@ export {
1573
2137
  deleteNest,
1574
2138
  engineCache,
1575
2139
  buildTitleMap,
2140
+ describeNode,
1576
2141
  nestName,
1577
2142
  docLink,
1578
2143
  buildDocContext,
1579
- collabPermToRole,
1580
2144
  assignSteward,
1581
2145
  removeSteward,
1582
2146
  updateSteward,