@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.
- package/CONFIGURATION.md +151 -3
- package/README.md +2 -1
- package/dist/{chunk-DPHV6Q26.js → chunk-3M7677XW.js} +363 -22
- package/dist/{chunk-BYS4HDME.js → chunk-F7EKYNXG.js} +634 -70
- package/dist/{chunk-3JTODC3Y.js → chunk-GUNJTORH.js} +7 -0
- package/dist/{chunk-HRQWNRVI.js → chunk-JHIU6RZU.js} +1 -1
- package/dist/{chunk-VC3QNHHB.js → chunk-XB7T3THE.js} +685 -6
- package/dist/{chunk-TIO5XOFD.js → chunk-ZMSU7BCC.js} +2 -2
- package/dist/{client-GI74NPIW.js → client-YUOEVUFU.js} +1 -1
- package/dist/{grants-service-MDLRPMZR.js → grants-service-PS5RMMEA.js} +3 -3
- package/dist/index.js +3602 -742
- package/dist/{migrations.postgres-ORSV7UFJ.js → migrations.postgres-BRXZY2GE.js} +149 -3
- package/dist/{review-service-A3XFQTJH.js → review-service-DPN26C47.js} +7 -5
- package/dist/{stewardship-service-3KCVVXCW.js → stewardship-service-7BHI2YXB.js} +4 -3
- package/dist/{version-service-6J4OWQL6.js → version-service-YQIIQ7FN.js} +2 -2
- package/dist/web3/assets/index-BiGiarBx.css +1 -0
- package/dist/web3/assets/index-DekN5OmU.js +1026 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -2
- package/dist/web3/assets/index-CHavHPIT.css +0 -1
- package/dist/web3/assets/index-DyvOBH92.js +0 -934
|
@@ -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) {
|
|
@@ -235,7 +265,7 @@ async function flushTelemetry() {
|
|
|
235
265
|
try {
|
|
236
266
|
const res = await fetch(url, {
|
|
237
267
|
method: "POST",
|
|
238
|
-
headers:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
423
|
+
await persistSetting("PROMPTOWL_KEY", null);
|
|
364
424
|
console.warn(
|
|
365
|
-
|
|
425
|
+
"[license] revoked \u2014 cleared persisted PROMPTOWL_KEY from the database"
|
|
366
426
|
);
|
|
367
427
|
} catch (err) {
|
|
368
|
-
console.warn("[license] failed to
|
|
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:
|
|
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
|
-
|
|
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)
|
|
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)
|
|
959
|
-
if (nest.visibility === "public")
|
|
960
|
-
return
|
|
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 =
|
|
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
|
|
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,
|