@promptowl/contextnest-community 0.1.0-alpha.1
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 +118 -0
- package/LICENSE.md +142 -0
- package/README.md +105 -0
- package/dist/chunk-7K2LLJXK.js +58 -0
- package/dist/chunk-DJFEV4ET.js +199 -0
- package/dist/chunk-P6NG56CO.js +127 -0
- package/dist/chunk-Q2DCOS7V.js +491 -0
- package/dist/chunk-USIDOGVJ.js +347 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2867 -0
- package/dist/keys-YV33AJK3.js +16 -0
- package/dist/review-service-5CLVZKAR.js +23 -0
- package/dist/stewardship-service-NC67XBYO.js +31 -0
- package/dist/version-service-Z6FYJRAG.js +23 -0
- package/dist/web3/assets/hootie-C2ocYkn4.svg +8 -0
- package/dist/web3/assets/index-CemroDXg.css +1 -0
- package/dist/web3/assets/index-xLLf4lHJ.js +332 -0
- package/dist/web3/index.html +14 -0
- package/package.json +108 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDb
|
|
3
|
+
} from "./chunk-USIDOGVJ.js";
|
|
4
|
+
|
|
5
|
+
// src/governance/version-service.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
function hashContent(content) {
|
|
8
|
+
return createHash("sha256").update(content).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function createVersion(params) {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const contentHash = hashContent(params.content);
|
|
13
|
+
db.prepare(
|
|
14
|
+
`INSERT INTO node_versions (nest_id, node_id, version, content_hash, author, status, change_note, tags_json)
|
|
15
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
16
|
+
).run(
|
|
17
|
+
params.nestId,
|
|
18
|
+
params.nodeId,
|
|
19
|
+
params.version,
|
|
20
|
+
contentHash,
|
|
21
|
+
params.author,
|
|
22
|
+
params.status,
|
|
23
|
+
params.changeNote || null,
|
|
24
|
+
params.tags ? JSON.stringify(params.tags) : null
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
version: params.version,
|
|
28
|
+
content: params.content,
|
|
29
|
+
editedBy: params.author,
|
|
30
|
+
editedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
changeNote: params.changeNote,
|
|
32
|
+
status: params.status
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function getVersions(nestId, nodeId) {
|
|
36
|
+
const db = getDb();
|
|
37
|
+
const rows = db.prepare(
|
|
38
|
+
"SELECT * FROM node_versions WHERE nest_id = ? AND node_id = ? ORDER BY version DESC"
|
|
39
|
+
).all(nestId, nodeId);
|
|
40
|
+
return rows.map(rowToVersion);
|
|
41
|
+
}
|
|
42
|
+
function getVersion(nestId, nodeId, version) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const row = db.prepare(
|
|
45
|
+
"SELECT * FROM node_versions WHERE nest_id = ? AND node_id = ? AND version = ?"
|
|
46
|
+
).get(nestId, nodeId, version);
|
|
47
|
+
return row ? rowToVersion(row) : null;
|
|
48
|
+
}
|
|
49
|
+
function getCurrentVersion(nestId, nodeId) {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const row = db.prepare(
|
|
52
|
+
"SELECT MAX(version) as v FROM node_versions WHERE nest_id = ? AND node_id = ?"
|
|
53
|
+
).get(nestId, nodeId);
|
|
54
|
+
return row?.v || 0;
|
|
55
|
+
}
|
|
56
|
+
function getApprovedVersion(nestId, nodeId) {
|
|
57
|
+
const db = getDb();
|
|
58
|
+
const row = db.prepare(
|
|
59
|
+
"SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?"
|
|
60
|
+
).get(nestId, nodeId);
|
|
61
|
+
return row?.approved_version ?? null;
|
|
62
|
+
}
|
|
63
|
+
function setApprovedVersion(nestId, nodeId, version, approvedBy) {
|
|
64
|
+
const db = getDb();
|
|
65
|
+
db.prepare(
|
|
66
|
+
`INSERT OR REPLACE INTO approved_versions (nest_id, node_id, approved_version, approved_by)
|
|
67
|
+
VALUES (?, ?, ?, ?)`
|
|
68
|
+
).run(nestId, nodeId, version, approvedBy);
|
|
69
|
+
}
|
|
70
|
+
function checkConflict(nestId, nodeId, baseVersion) {
|
|
71
|
+
const db = getDb();
|
|
72
|
+
const current = db.prepare(
|
|
73
|
+
"SELECT version, content_hash, author, created_at FROM node_versions WHERE nest_id = ? AND node_id = ? ORDER BY version DESC LIMIT 1"
|
|
74
|
+
).get(nestId, nodeId);
|
|
75
|
+
if (!current) {
|
|
76
|
+
return { conflict: false, currentVersion: 0, currentHash: "" };
|
|
77
|
+
}
|
|
78
|
+
if (current.version !== baseVersion) {
|
|
79
|
+
return {
|
|
80
|
+
conflict: true,
|
|
81
|
+
currentVersion: current.version,
|
|
82
|
+
currentHash: current.content_hash,
|
|
83
|
+
updatedBy: current.author,
|
|
84
|
+
updatedAt: current.created_at
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
conflict: false,
|
|
89
|
+
currentVersion: current.version,
|
|
90
|
+
currentHash: current.content_hash
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getNodeTags(nestId, nodeId) {
|
|
94
|
+
const db = getDb();
|
|
95
|
+
const row = db.prepare(
|
|
96
|
+
"SELECT tags_json FROM node_versions WHERE nest_id = ? AND node_id = ? ORDER BY version DESC LIMIT 1"
|
|
97
|
+
).get(nestId, nodeId);
|
|
98
|
+
if (!row?.tags_json) return [];
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(row.tags_json);
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function rowToVersion(row) {
|
|
106
|
+
return {
|
|
107
|
+
version: row.version,
|
|
108
|
+
content: "",
|
|
109
|
+
// Content lives on disk, not in the versions table
|
|
110
|
+
editedBy: row.author,
|
|
111
|
+
editedAt: row.created_at,
|
|
112
|
+
changeNote: row.change_note || void 0,
|
|
113
|
+
status: row.status
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
hashContent,
|
|
119
|
+
createVersion,
|
|
120
|
+
getVersions,
|
|
121
|
+
getVersion,
|
|
122
|
+
getCurrentVersion,
|
|
123
|
+
getApprovedVersion,
|
|
124
|
+
setApprovedVersion,
|
|
125
|
+
checkConflict,
|
|
126
|
+
getNodeTags
|
|
127
|
+
};
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import {
|
|
2
|
+
config,
|
|
3
|
+
getDb
|
|
4
|
+
} from "./chunk-USIDOGVJ.js";
|
|
5
|
+
|
|
6
|
+
// src/governance/stewardship-service.ts
|
|
7
|
+
import { v4 as uuid } from "uuid";
|
|
8
|
+
|
|
9
|
+
// src/governance/access-service.ts
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var accessConfig = null;
|
|
13
|
+
function loadAccessConfig() {
|
|
14
|
+
const candidates = [
|
|
15
|
+
join(config.DATA_ROOT, "access.yaml"),
|
|
16
|
+
join(config.DATA_ROOT, "access.yml")
|
|
17
|
+
];
|
|
18
|
+
for (const path of candidates) {
|
|
19
|
+
if (existsSync(path)) {
|
|
20
|
+
const content = readFileSync(path, "utf-8");
|
|
21
|
+
accessConfig = parseAccessYaml(content);
|
|
22
|
+
return accessConfig;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
accessConfig = null;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function getAccessConfig() {
|
|
29
|
+
return accessConfig;
|
|
30
|
+
}
|
|
31
|
+
function isSuperAdmin(email) {
|
|
32
|
+
if (!accessConfig?.super_admins) return false;
|
|
33
|
+
return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
function parseAccessYaml(content) {
|
|
36
|
+
const result = {};
|
|
37
|
+
const lines = content.split("\n");
|
|
38
|
+
let currentSection = null;
|
|
39
|
+
let currentGroup = null;
|
|
40
|
+
let inMembers = false;
|
|
41
|
+
for (const rawLine of lines) {
|
|
42
|
+
const line = rawLine.trimEnd();
|
|
43
|
+
if (!line || line.startsWith("#")) continue;
|
|
44
|
+
if (!line.startsWith(" ") && !line.startsWith(" ")) {
|
|
45
|
+
const match = line.match(/^(\w+):\s*(.*)?$/);
|
|
46
|
+
if (!match) continue;
|
|
47
|
+
const key = match[1];
|
|
48
|
+
const value = match[2]?.trim();
|
|
49
|
+
if (key === "mode") {
|
|
50
|
+
result.mode = value;
|
|
51
|
+
currentSection = null;
|
|
52
|
+
} else if (key === "allowed_users") {
|
|
53
|
+
currentSection = "allowed_users";
|
|
54
|
+
result.allowed_users = [];
|
|
55
|
+
} else if (key === "groups") {
|
|
56
|
+
currentSection = "groups";
|
|
57
|
+
result.groups = {};
|
|
58
|
+
} else if (key === "super_admins") {
|
|
59
|
+
currentSection = "super_admins";
|
|
60
|
+
result.super_admins = [];
|
|
61
|
+
}
|
|
62
|
+
currentGroup = null;
|
|
63
|
+
inMembers = false;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
|
|
67
|
+
if (currentSection === "allowed_users" && listMatch) {
|
|
68
|
+
result.allowed_users.push(listMatch[1].trim());
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (currentSection === "super_admins" && listMatch) {
|
|
72
|
+
result.super_admins.push(listMatch[1].trim());
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (currentSection === "groups") {
|
|
76
|
+
const groupMatch = line.match(/^ (\w+):$/);
|
|
77
|
+
if (groupMatch) {
|
|
78
|
+
currentGroup = groupMatch[1];
|
|
79
|
+
result.groups[currentGroup] = { members: [], default_permission: "read" };
|
|
80
|
+
inMembers = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (currentGroup) {
|
|
84
|
+
const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
|
|
85
|
+
if (propMatch) {
|
|
86
|
+
const prop = propMatch[1];
|
|
87
|
+
const val = propMatch[2]?.trim();
|
|
88
|
+
if (prop === "default_permission" && val) {
|
|
89
|
+
result.groups[currentGroup].default_permission = val;
|
|
90
|
+
}
|
|
91
|
+
if (prop === "members") {
|
|
92
|
+
inMembers = true;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (inMembers && listMatch) {
|
|
97
|
+
result.groups[currentGroup].members.push(listMatch[1].trim());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/governance/stewardship-service.ts
|
|
106
|
+
function assignSteward(data) {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const id = uuid();
|
|
109
|
+
db.prepare(
|
|
110
|
+
`INSERT INTO stewards
|
|
111
|
+
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, can_approve, can_reject, assigned_by, is_active)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
113
|
+
).run(
|
|
114
|
+
id,
|
|
115
|
+
data.nestId,
|
|
116
|
+
data.scope,
|
|
117
|
+
data.nodePattern || null,
|
|
118
|
+
data.tagName || null,
|
|
119
|
+
data.userEmail,
|
|
120
|
+
data.userId || null,
|
|
121
|
+
data.role,
|
|
122
|
+
data.canApprove ? 1 : 0,
|
|
123
|
+
data.canReject ? 1 : 0,
|
|
124
|
+
data.assignedBy,
|
|
125
|
+
data.isActive ? 1 : 0
|
|
126
|
+
);
|
|
127
|
+
return { ...data, id };
|
|
128
|
+
}
|
|
129
|
+
function removeSteward(id) {
|
|
130
|
+
const db = getDb();
|
|
131
|
+
db.prepare("UPDATE stewards SET is_active = 0 WHERE id = ?").run(id);
|
|
132
|
+
}
|
|
133
|
+
function getSteward(id) {
|
|
134
|
+
const db = getDb();
|
|
135
|
+
const row = db.prepare("SELECT * FROM stewards WHERE id = ?").get(id);
|
|
136
|
+
return row ? rowToSteward(row) : null;
|
|
137
|
+
}
|
|
138
|
+
function getStewardsForNest(nestId) {
|
|
139
|
+
const db = getDb();
|
|
140
|
+
const rows = db.prepare("SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1").all(nestId);
|
|
141
|
+
return rows.map(rowToSteward);
|
|
142
|
+
}
|
|
143
|
+
function getStewardsForScope(params) {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
|
|
146
|
+
const args = [params.nestId];
|
|
147
|
+
if (params.scope) {
|
|
148
|
+
sql += " AND scope = ?";
|
|
149
|
+
args.push(params.scope);
|
|
150
|
+
}
|
|
151
|
+
if (params.scopeTarget) {
|
|
152
|
+
sql += " AND (node_pattern = ? OR tag_name = ?)";
|
|
153
|
+
args.push(params.scopeTarget, params.scopeTarget);
|
|
154
|
+
}
|
|
155
|
+
return db.prepare(sql).all(...args).map(rowToSteward);
|
|
156
|
+
}
|
|
157
|
+
function listStewards(params) {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
|
|
160
|
+
const args = [params.nestId];
|
|
161
|
+
if (params.scope) {
|
|
162
|
+
sql += " AND scope = ?";
|
|
163
|
+
args.push(params.scope);
|
|
164
|
+
}
|
|
165
|
+
if (params.search) {
|
|
166
|
+
sql += " AND (user_email LIKE ? OR tag_name LIKE ? OR node_pattern LIKE ?)";
|
|
167
|
+
const like = `%${params.search.toLowerCase()}%`;
|
|
168
|
+
args.push(like, like, like);
|
|
169
|
+
}
|
|
170
|
+
sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
|
|
171
|
+
return db.prepare(sql).all(...args).map(rowToSteward);
|
|
172
|
+
}
|
|
173
|
+
function createStewardRecord(params) {
|
|
174
|
+
if (params.users.length === 0) {
|
|
175
|
+
throw new Error("At least one user is required");
|
|
176
|
+
}
|
|
177
|
+
let nodePattern;
|
|
178
|
+
let tagName;
|
|
179
|
+
switch (params.scope) {
|
|
180
|
+
case "document":
|
|
181
|
+
if (!params.documentId) throw new Error("documentId required for document scope");
|
|
182
|
+
nodePattern = params.documentId;
|
|
183
|
+
break;
|
|
184
|
+
case "folder":
|
|
185
|
+
if (!params.folderPath) throw new Error("folderPath required for folder scope");
|
|
186
|
+
nodePattern = params.folderPath;
|
|
187
|
+
break;
|
|
188
|
+
case "tag":
|
|
189
|
+
if (!params.tagName) throw new Error("tagName required for tag scope");
|
|
190
|
+
tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
|
|
191
|
+
break;
|
|
192
|
+
case "nest":
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
const db = getDb();
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const user of params.users) {
|
|
198
|
+
const email = user.email.trim().toLowerCase();
|
|
199
|
+
if (!email) continue;
|
|
200
|
+
const existing = db.prepare(
|
|
201
|
+
`SELECT * FROM stewards
|
|
202
|
+
WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
|
|
203
|
+
AND COALESCE(node_pattern, '') = COALESCE(?, '')
|
|
204
|
+
AND COALESCE(tag_name, '') = COALESCE(?, '')`
|
|
205
|
+
).get(
|
|
206
|
+
params.nestId,
|
|
207
|
+
params.scope,
|
|
208
|
+
email,
|
|
209
|
+
nodePattern ?? null,
|
|
210
|
+
tagName ?? null
|
|
211
|
+
);
|
|
212
|
+
if (existing) {
|
|
213
|
+
results.push(rowToSteward(existing));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const userRow = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
|
|
217
|
+
const created = assignSteward({
|
|
218
|
+
nestId: params.nestId,
|
|
219
|
+
scope: params.scope,
|
|
220
|
+
nodePattern,
|
|
221
|
+
tagName,
|
|
222
|
+
userEmail: email,
|
|
223
|
+
userId: userRow?.id,
|
|
224
|
+
role: user.role ?? "reviewer",
|
|
225
|
+
canApprove: user.canApprove !== false,
|
|
226
|
+
canReject: user.canReject !== false,
|
|
227
|
+
assignedBy: params.assignedBy,
|
|
228
|
+
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
229
|
+
isActive: true
|
|
230
|
+
});
|
|
231
|
+
results.push(created);
|
|
232
|
+
}
|
|
233
|
+
db.prepare(
|
|
234
|
+
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0"
|
|
235
|
+
).run(params.nestId);
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
function resolveStewardsForNode(nestId, nodeId) {
|
|
239
|
+
return resolve(nestId, nodeId).stewards;
|
|
240
|
+
}
|
|
241
|
+
function resolveStewardsWithFallback(nestId, nodeId) {
|
|
242
|
+
return resolve(nestId, nodeId);
|
|
243
|
+
}
|
|
244
|
+
function resolve(nestId, nodeId) {
|
|
245
|
+
const db = getDb();
|
|
246
|
+
const rows = db.prepare(
|
|
247
|
+
`
|
|
248
|
+
SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
|
|
249
|
+
FROM stewards s
|
|
250
|
+
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
|
|
251
|
+
AND s.node_pattern = ?
|
|
252
|
+
UNION ALL
|
|
253
|
+
SELECT s.*, 2 AS priority, ('folder: ' || s.node_pattern) AS match_source
|
|
254
|
+
FROM stewards s
|
|
255
|
+
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'folder'
|
|
256
|
+
AND s.node_pattern IS NOT NULL
|
|
257
|
+
AND instr(s.node_pattern, '*') = 0
|
|
258
|
+
AND ? LIKE s.node_pattern || '/%'
|
|
259
|
+
UNION ALL
|
|
260
|
+
SELECT s.*, 3 AS priority, ('tag: ' || s.tag_name) AS match_source
|
|
261
|
+
FROM stewards s
|
|
262
|
+
JOIN node_tag_index nt
|
|
263
|
+
ON nt.nest_id = s.nest_id
|
|
264
|
+
AND nt.tag_name = s.tag_name
|
|
265
|
+
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
|
|
266
|
+
AND nt.node_id = ?
|
|
267
|
+
UNION ALL
|
|
268
|
+
SELECT s.*, 4 AS priority, 'nest-level steward' AS match_source
|
|
269
|
+
FROM stewards s
|
|
270
|
+
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
|
|
271
|
+
ORDER BY priority ASC, user_email ASC
|
|
272
|
+
`
|
|
273
|
+
).all(nestId, nodeId, nestId, nodeId, nestId, nodeId, nestId);
|
|
274
|
+
const resolved = rows.map((row) => ({
|
|
275
|
+
steward: rowToSteward(row),
|
|
276
|
+
priority: row.priority,
|
|
277
|
+
source: row.match_source
|
|
278
|
+
}));
|
|
279
|
+
const legacyFolderGlobs = db.prepare(
|
|
280
|
+
`SELECT * FROM stewards
|
|
281
|
+
WHERE nest_id = ? AND is_active = 1 AND scope = 'folder'
|
|
282
|
+
AND node_pattern IS NOT NULL AND instr(node_pattern, '*') > 0`
|
|
283
|
+
).all(nestId);
|
|
284
|
+
for (const row of legacyFolderGlobs) {
|
|
285
|
+
if (globMatch(nodeId, row.node_pattern)) {
|
|
286
|
+
resolved.push({
|
|
287
|
+
steward: rowToSteward(row),
|
|
288
|
+
priority: 2,
|
|
289
|
+
source: `folder: ${row.node_pattern}`
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (legacyFolderGlobs.length > 0) {
|
|
294
|
+
resolved.sort((a, b) => a.priority - b.priority);
|
|
295
|
+
}
|
|
296
|
+
if (resolved.length > 0) {
|
|
297
|
+
return { stewards: resolved, fallbackToOwner: false };
|
|
298
|
+
}
|
|
299
|
+
const owner = db.prepare(
|
|
300
|
+
`SELECT u.email FROM nests n
|
|
301
|
+
JOIN users u ON u.id = n.user_id
|
|
302
|
+
WHERE n.id = ?`
|
|
303
|
+
).get(nestId);
|
|
304
|
+
return {
|
|
305
|
+
stewards: [],
|
|
306
|
+
fallbackToOwner: true,
|
|
307
|
+
ownerEmail: owner?.email
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function isSuperAdmin2(userEmail) {
|
|
311
|
+
const cfg = getAccessConfig();
|
|
312
|
+
return !!cfg?.super_admins?.includes(userEmail);
|
|
313
|
+
}
|
|
314
|
+
function getNestOwnerEmail(nestId) {
|
|
315
|
+
const db = getDb();
|
|
316
|
+
const row = db.prepare(
|
|
317
|
+
`SELECT u.email FROM nests n
|
|
318
|
+
JOIN users u ON u.id = n.user_id
|
|
319
|
+
WHERE n.id = ?`
|
|
320
|
+
).get(nestId);
|
|
321
|
+
return row?.email ?? null;
|
|
322
|
+
}
|
|
323
|
+
function canUserEdit(nestId, nodeId, userEmail) {
|
|
324
|
+
if (isSuperAdmin2(userEmail)) {
|
|
325
|
+
return { allowed: true, reason: "super admin", role: "super_admin" };
|
|
326
|
+
}
|
|
327
|
+
const owner = getNestOwnerEmail(nestId);
|
|
328
|
+
if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
|
|
329
|
+
return { allowed: true, reason: "nest owner", role: "owner" };
|
|
330
|
+
}
|
|
331
|
+
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
332
|
+
const match = resolved.find(
|
|
333
|
+
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && (r.steward.role === "editor" || r.steward.role === "reviewer")
|
|
334
|
+
);
|
|
335
|
+
if (match) {
|
|
336
|
+
return { allowed: true, reason: match.source, role: match.steward.role };
|
|
337
|
+
}
|
|
338
|
+
return { allowed: false, reason: "no editor/reviewer role on this node", role: null };
|
|
339
|
+
}
|
|
340
|
+
function getCurrentVersionAuthor(nestId, nodeId) {
|
|
341
|
+
const db = getDb();
|
|
342
|
+
const row = db.prepare(
|
|
343
|
+
`SELECT author FROM node_versions
|
|
344
|
+
WHERE nest_id = ? AND node_id = ?
|
|
345
|
+
ORDER BY version DESC LIMIT 1`
|
|
346
|
+
).get(nestId, nodeId);
|
|
347
|
+
return row?.author ?? null;
|
|
348
|
+
}
|
|
349
|
+
function canUserApprove(nestId, nodeId, userEmail) {
|
|
350
|
+
if (isSuperAdmin2(userEmail)) {
|
|
351
|
+
return { allowed: true, reason: "super admin", role: "super_admin" };
|
|
352
|
+
}
|
|
353
|
+
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
354
|
+
if (resolved.length === 0) {
|
|
355
|
+
return {
|
|
356
|
+
allowed: false,
|
|
357
|
+
reason: "no stewards configured for this node",
|
|
358
|
+
role: null
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const match = resolved.find(
|
|
362
|
+
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer" && r.steward.canApprove
|
|
363
|
+
);
|
|
364
|
+
if (!match) {
|
|
365
|
+
return {
|
|
366
|
+
allowed: false,
|
|
367
|
+
reason: "not a reviewer steward for this node",
|
|
368
|
+
role: null
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const author = getCurrentVersionAuthor(nestId, nodeId);
|
|
372
|
+
if (author && author.toLowerCase() === userEmail.toLowerCase()) {
|
|
373
|
+
return {
|
|
374
|
+
allowed: false,
|
|
375
|
+
reason: "cannot approve a version you authored (separation of duties)",
|
|
376
|
+
role: "reviewer"
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return { allowed: true, reason: match.source, role: "reviewer" };
|
|
380
|
+
}
|
|
381
|
+
function canUserAccess(nestId, nodeId, userEmail) {
|
|
382
|
+
if (isSuperAdmin2(userEmail)) {
|
|
383
|
+
return { allowed: true, reason: "super admin", role: "super_admin" };
|
|
384
|
+
}
|
|
385
|
+
const owner = getNestOwnerEmail(nestId);
|
|
386
|
+
if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
|
|
387
|
+
return { allowed: true, reason: "nest owner", role: "owner" };
|
|
388
|
+
}
|
|
389
|
+
const resolved = resolveStewardsForNode(nestId, nodeId);
|
|
390
|
+
const match = resolved.find(
|
|
391
|
+
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
|
|
392
|
+
);
|
|
393
|
+
if (match) {
|
|
394
|
+
return { allowed: true, reason: match.source, role: match.steward.role };
|
|
395
|
+
}
|
|
396
|
+
return { allowed: false, reason: "no steward assignment", role: null };
|
|
397
|
+
}
|
|
398
|
+
function globMatch(value, pattern) {
|
|
399
|
+
if (pattern === "*") return true;
|
|
400
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
401
|
+
const regex = new RegExp(
|
|
402
|
+
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
|
|
403
|
+
);
|
|
404
|
+
return regex.test(value);
|
|
405
|
+
}
|
|
406
|
+
function syncFromConfig(nestId, config2) {
|
|
407
|
+
const db = getDb();
|
|
408
|
+
let count = 0;
|
|
409
|
+
db.prepare(
|
|
410
|
+
"UPDATE stewards SET is_active = 0 WHERE nest_id = ?"
|
|
411
|
+
).run(nestId);
|
|
412
|
+
db.prepare(
|
|
413
|
+
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ?"
|
|
414
|
+
).run(nestId);
|
|
415
|
+
const addEntries = (scope, entries, target) => {
|
|
416
|
+
for (const entry of entries) {
|
|
417
|
+
const user = db.prepare("SELECT id FROM users WHERE email = ?").get(entry.email);
|
|
418
|
+
const rawRole = entry.role || "reviewer";
|
|
419
|
+
const role = rawRole === "admin" ? "reviewer" : rawRole;
|
|
420
|
+
assignSteward({
|
|
421
|
+
nestId,
|
|
422
|
+
scope,
|
|
423
|
+
nodePattern: target?.nodePattern,
|
|
424
|
+
tagName: target?.tagName ? target.tagName.trim().replace(/^#+/, "").toLowerCase() : void 0,
|
|
425
|
+
userEmail: entry.email.toLowerCase(),
|
|
426
|
+
userId: user?.id,
|
|
427
|
+
role,
|
|
428
|
+
canApprove: entry.can_approve !== false,
|
|
429
|
+
canReject: entry.can_reject !== false,
|
|
430
|
+
assignedBy: "config",
|
|
431
|
+
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
432
|
+
isActive: true
|
|
433
|
+
});
|
|
434
|
+
count++;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
if (config2.nest) {
|
|
438
|
+
addEntries("nest", config2.nest);
|
|
439
|
+
}
|
|
440
|
+
if (config2.folders) {
|
|
441
|
+
for (const [pattern, entries] of Object.entries(config2.folders)) {
|
|
442
|
+
addEntries("folder", entries, { nodePattern: pattern });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (config2.tags) {
|
|
446
|
+
for (const [tagName, entries] of Object.entries(config2.tags)) {
|
|
447
|
+
addEntries("tag", entries, { tagName });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (config2.documents) {
|
|
451
|
+
for (const [docPattern, entries] of Object.entries(config2.documents)) {
|
|
452
|
+
addEntries("document", entries, { nodePattern: docPattern });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return count;
|
|
456
|
+
}
|
|
457
|
+
function rowToSteward(row) {
|
|
458
|
+
return {
|
|
459
|
+
id: row.id,
|
|
460
|
+
nestId: row.nest_id,
|
|
461
|
+
scope: row.scope,
|
|
462
|
+
nodePattern: row.node_pattern || void 0,
|
|
463
|
+
tagName: row.tag_name || void 0,
|
|
464
|
+
userEmail: row.user_email,
|
|
465
|
+
userId: row.user_id || void 0,
|
|
466
|
+
role: row.role,
|
|
467
|
+
canApprove: !!row.can_approve,
|
|
468
|
+
canReject: !!row.can_reject,
|
|
469
|
+
assignedBy: row.assigned_by,
|
|
470
|
+
assignedAt: row.assigned_at,
|
|
471
|
+
isActive: !!row.is_active
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export {
|
|
476
|
+
loadAccessConfig,
|
|
477
|
+
isSuperAdmin,
|
|
478
|
+
assignSteward,
|
|
479
|
+
removeSteward,
|
|
480
|
+
getSteward,
|
|
481
|
+
getStewardsForNest,
|
|
482
|
+
getStewardsForScope,
|
|
483
|
+
listStewards,
|
|
484
|
+
createStewardRecord,
|
|
485
|
+
resolveStewardsForNode,
|
|
486
|
+
resolveStewardsWithFallback,
|
|
487
|
+
canUserEdit,
|
|
488
|
+
canUserApprove,
|
|
489
|
+
canUserAccess,
|
|
490
|
+
syncFromConfig
|
|
491
|
+
};
|