@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.
- package/CONFIGURATION.md +127 -2
- package/README.md +4 -61
- package/dist/{chunk-BC6KFUZH.js → chunk-3M7677XW.js} +159 -1
- package/dist/{chunk-KFGZIECB.js → chunk-F7EKYNXG.js} +564 -47
- package/dist/{chunk-3JTODC3Y.js → chunk-GUNJTORH.js} +7 -0
- package/dist/{chunk-UHUU3VAK.js → chunk-JHIU6RZU.js} +1 -1
- package/dist/{chunk-ZM4F7MY7.js → chunk-XB7T3THE.js} +5 -5
- package/dist/{chunk-7PQREKSM.js → chunk-ZMSU7BCC.js} +2 -2
- package/dist/{client-VZLX4THW.js → client-YUOEVUFU.js} +1 -1
- package/dist/{grants-service-6H4WUA2F.js → grants-service-PS5RMMEA.js} +3 -3
- package/dist/index.js +1745 -307
- package/dist/{migrations.postgres-4XYY3CTF.js → migrations.postgres-BRXZY2GE.js} +50 -2
- package/dist/{review-service-VLSUT6KK.js → review-service-DPN26C47.js} +7 -6
- package/dist/{stewardship-service-D5PDWXBR.js → stewardship-service-7BHI2YXB.js} +4 -3
- package/dist/{version-service-A2YAKYQW.js → version-service-YQIIQ7FN.js} +2 -2
- package/dist/web3/assets/index-BiGiarBx.css +1 -0
- package/dist/web3/assets/index-CUVRdYvH.js +1026 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -1
- package/dist/web3/assets/index-B2PkvzmY.js +0 -1007
- package/dist/web3/assets/index-C-tX933G.css +0 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConflictError,
|
|
3
|
+
ForbiddenError,
|
|
4
|
+
NotFoundError,
|
|
3
5
|
ValidationError
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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)
|
|
995
|
-
if (nest.visibility === "public")
|
|
996
|
-
return
|
|
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 =
|
|
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 =
|
|
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(
|
|
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,
|