@promptowl/contextnest-community 0.1.0-alpha.2 → 1.0.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 -118
- package/dist/{chunk-DJFEV4ET.js → chunk-5VHKEIAW.js} +100 -4
- package/dist/{chunk-P6NG56CO.js → chunk-JMZ75ZCD.js} +44 -6
- package/dist/{chunk-Q2DCOS7V.js → chunk-K22GWPT4.js} +65 -58
- package/dist/{chunk-USIDOGVJ.js → chunk-KQCWNHDM.js} +218 -21
- package/dist/index.js +1738 -462
- package/dist/{review-service-5CLVZKAR.js → review-service-4WS3XL6K.js} +4 -3
- package/dist/{stewardship-service-NC67XBYO.js → stewardship-service-C5D2O7ZE.js} +2 -2
- package/dist/{version-service-Z6FYJRAG.js → version-service-TFEYNPH7.js} +10 -4
- package/dist/web3/assets/index-DkLevP7k.js +624 -0
- package/dist/web3/assets/index-DpoBdKrd.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +134 -108
- package/dist/web3/assets/index-CemroDXg.css +0 -1
- package/dist/web3/assets/index-xLLf4lHJ.js +0 -332
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
config,
|
|
3
3
|
getDb
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KQCWNHDM.js";
|
|
5
5
|
|
|
6
6
|
// src/governance/stewardship-service.ts
|
|
7
7
|
import { v4 as uuid } from "uuid";
|
|
@@ -108,8 +108,8 @@ function assignSteward(data) {
|
|
|
108
108
|
const id = uuid();
|
|
109
109
|
db.prepare(
|
|
110
110
|
`INSERT INTO stewards
|
|
111
|
-
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role,
|
|
112
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
111
|
+
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
113
113
|
).run(
|
|
114
114
|
id,
|
|
115
115
|
data.nestId,
|
|
@@ -119,8 +119,6 @@ function assignSteward(data) {
|
|
|
119
119
|
data.userEmail,
|
|
120
120
|
data.userId || null,
|
|
121
121
|
data.role,
|
|
122
|
-
data.canApprove ? 1 : 0,
|
|
123
|
-
data.canReject ? 1 : 0,
|
|
124
122
|
data.assignedBy,
|
|
125
123
|
data.isActive ? 1 : 0
|
|
126
124
|
);
|
|
@@ -128,7 +126,21 @@ function assignSteward(data) {
|
|
|
128
126
|
}
|
|
129
127
|
function removeSteward(id) {
|
|
130
128
|
const db = getDb();
|
|
131
|
-
|
|
129
|
+
const remove = db.transaction((stewardId) => {
|
|
130
|
+
const row = db.prepare("SELECT nest_id, user_email FROM stewards WHERE id = ?").get(stewardId);
|
|
131
|
+
db.prepare("DELETE FROM stewards WHERE id = ?").run(stewardId);
|
|
132
|
+
if (!row) return;
|
|
133
|
+
const remaining = db.prepare(
|
|
134
|
+
"SELECT 1 FROM stewards WHERE nest_id = ? AND LOWER(user_email) = LOWER(?) AND is_active = 1 LIMIT 1"
|
|
135
|
+
).get(row.nest_id, row.user_email);
|
|
136
|
+
if (remaining) return;
|
|
137
|
+
const user = db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(row.user_email);
|
|
138
|
+
if (!user) return;
|
|
139
|
+
db.prepare(
|
|
140
|
+
"DELETE FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
141
|
+
).run(row.nest_id, user.id);
|
|
142
|
+
});
|
|
143
|
+
remove(id);
|
|
132
144
|
}
|
|
133
145
|
function getSteward(id) {
|
|
134
146
|
const db = getDb();
|
|
@@ -170,7 +182,34 @@ function listStewards(params) {
|
|
|
170
182
|
sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
|
|
171
183
|
return db.prepare(sql).all(...args).map(rowToSteward);
|
|
172
184
|
}
|
|
173
|
-
function
|
|
185
|
+
function rolePermission(role) {
|
|
186
|
+
return role === "editor" ? "write" : "read";
|
|
187
|
+
}
|
|
188
|
+
async function ensureCollaborator(nestId, email, permission, grantedBy) {
|
|
189
|
+
const db = getDb();
|
|
190
|
+
let userRow = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
|
|
191
|
+
if (!userRow) {
|
|
192
|
+
const { hashPassword } = await import("./keys-YV33AJK3.js");
|
|
193
|
+
const newId = uuid();
|
|
194
|
+
db.prepare(
|
|
195
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)"
|
|
196
|
+
).run(newId, email, null, await hashPassword(uuid()));
|
|
197
|
+
userRow = { id: newId };
|
|
198
|
+
}
|
|
199
|
+
const nestRow = db.prepare("SELECT user_id FROM nests WHERE id = ?").get(nestId);
|
|
200
|
+
if (nestRow && nestRow.user_id === userRow.id) return;
|
|
201
|
+
const existing = db.prepare(
|
|
202
|
+
"SELECT id FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
203
|
+
).get(nestId, userRow.id);
|
|
204
|
+
if (existing) return;
|
|
205
|
+
const granterRow = db.prepare("SELECT id FROM users WHERE email = ?").get(grantedBy);
|
|
206
|
+
const granterId = granterRow?.id ?? nestRow?.user_id;
|
|
207
|
+
if (!granterId) return;
|
|
208
|
+
db.prepare(
|
|
209
|
+
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)"
|
|
210
|
+
).run(uuid(), nestId, userRow.id, permission, granterId);
|
|
211
|
+
}
|
|
212
|
+
async function createStewardRecord(params) {
|
|
174
213
|
if (params.users.length === 0) {
|
|
175
214
|
throw new Error("At least one user is required");
|
|
176
215
|
}
|
|
@@ -181,10 +220,6 @@ function createStewardRecord(params) {
|
|
|
181
220
|
if (!params.documentId) throw new Error("documentId required for document scope");
|
|
182
221
|
nodePattern = params.documentId;
|
|
183
222
|
break;
|
|
184
|
-
case "folder":
|
|
185
|
-
if (!params.folderPath) throw new Error("folderPath required for folder scope");
|
|
186
|
-
nodePattern = params.folderPath;
|
|
187
|
-
break;
|
|
188
223
|
case "tag":
|
|
189
224
|
if (!params.tagName) throw new Error("tagName required for tag scope");
|
|
190
225
|
tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
|
|
@@ -222,13 +257,17 @@ function createStewardRecord(params) {
|
|
|
222
257
|
userEmail: email,
|
|
223
258
|
userId: userRow?.id,
|
|
224
259
|
role: user.role ?? "reviewer",
|
|
225
|
-
canApprove: user.canApprove !== false,
|
|
226
|
-
canReject: user.canReject !== false,
|
|
227
260
|
assignedBy: params.assignedBy,
|
|
228
261
|
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
229
262
|
isActive: true
|
|
230
263
|
});
|
|
231
264
|
results.push(created);
|
|
265
|
+
await ensureCollaborator(
|
|
266
|
+
params.nestId,
|
|
267
|
+
email,
|
|
268
|
+
rolePermission(user.role),
|
|
269
|
+
params.assignedBy
|
|
270
|
+
);
|
|
232
271
|
}
|
|
233
272
|
db.prepare(
|
|
234
273
|
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0"
|
|
@@ -250,14 +289,7 @@ function resolve(nestId, nodeId) {
|
|
|
250
289
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
|
|
251
290
|
AND s.node_pattern = ?
|
|
252
291
|
UNION ALL
|
|
253
|
-
SELECT s.*, 2 AS priority, ('
|
|
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
|
|
292
|
+
SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
|
|
261
293
|
FROM stewards s
|
|
262
294
|
JOIN node_tag_index nt
|
|
263
295
|
ON nt.nest_id = s.nest_id
|
|
@@ -265,34 +297,26 @@ function resolve(nestId, nodeId) {
|
|
|
265
297
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
|
|
266
298
|
AND nt.node_id = ?
|
|
267
299
|
UNION ALL
|
|
268
|
-
SELECT s.*,
|
|
300
|
+
SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
|
|
269
301
|
FROM stewards s
|
|
270
302
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
|
|
271
303
|
ORDER BY priority ASC, user_email ASC
|
|
272
304
|
`
|
|
273
|
-
).all(
|
|
305
|
+
).all(
|
|
306
|
+
nestId,
|
|
307
|
+
nodeId,
|
|
308
|
+
// document branch
|
|
309
|
+
nestId,
|
|
310
|
+
nodeId,
|
|
311
|
+
// tag branch
|
|
312
|
+
nestId
|
|
313
|
+
// nest branch
|
|
314
|
+
);
|
|
274
315
|
const resolved = rows.map((row) => ({
|
|
275
316
|
steward: rowToSteward(row),
|
|
276
317
|
priority: row.priority,
|
|
277
318
|
source: row.match_source
|
|
278
319
|
}));
|
|
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
320
|
if (resolved.length > 0) {
|
|
297
321
|
return { stewards: resolved, fallbackToOwner: false };
|
|
298
322
|
}
|
|
@@ -359,7 +383,7 @@ function canUserApprove(nestId, nodeId, userEmail) {
|
|
|
359
383
|
};
|
|
360
384
|
}
|
|
361
385
|
const match = resolved.find(
|
|
362
|
-
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer"
|
|
386
|
+
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer"
|
|
363
387
|
);
|
|
364
388
|
if (!match) {
|
|
365
389
|
return {
|
|
@@ -395,14 +419,6 @@ function canUserAccess(nestId, nodeId, userEmail) {
|
|
|
395
419
|
}
|
|
396
420
|
return { allowed: false, reason: "no steward assignment", role: null };
|
|
397
421
|
}
|
|
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
422
|
function syncFromConfig(nestId, config2) {
|
|
407
423
|
const db = getDb();
|
|
408
424
|
let count = 0;
|
|
@@ -425,8 +441,6 @@ function syncFromConfig(nestId, config2) {
|
|
|
425
441
|
userEmail: entry.email.toLowerCase(),
|
|
426
442
|
userId: user?.id,
|
|
427
443
|
role,
|
|
428
|
-
canApprove: entry.can_approve !== false,
|
|
429
|
-
canReject: entry.can_reject !== false,
|
|
430
444
|
assignedBy: "config",
|
|
431
445
|
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
432
446
|
isActive: true
|
|
@@ -437,11 +451,6 @@ function syncFromConfig(nestId, config2) {
|
|
|
437
451
|
if (config2.nest) {
|
|
438
452
|
addEntries("nest", config2.nest);
|
|
439
453
|
}
|
|
440
|
-
if (config2.folders) {
|
|
441
|
-
for (const [pattern, entries] of Object.entries(config2.folders)) {
|
|
442
|
-
addEntries("folder", entries, { nodePattern: pattern });
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
454
|
if (config2.tags) {
|
|
446
455
|
for (const [tagName, entries] of Object.entries(config2.tags)) {
|
|
447
456
|
addEntries("tag", entries, { tagName });
|
|
@@ -464,8 +473,6 @@ function rowToSteward(row) {
|
|
|
464
473
|
userEmail: row.user_email,
|
|
465
474
|
userId: row.user_id || void 0,
|
|
466
475
|
role: row.role,
|
|
467
|
-
canApprove: !!row.can_approve,
|
|
468
|
-
canReject: !!row.can_reject,
|
|
469
476
|
assignedBy: row.assigned_by,
|
|
470
477
|
assignedAt: row.assigned_at,
|
|
471
478
|
isActive: !!row.is_active
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
import { join } from "path";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
var __dirname = dirname(__filename);
|
|
8
|
+
var envCandidates = [
|
|
9
|
+
join(process.cwd(), ".env"),
|
|
10
|
+
join(__dirname, "..", ".env")
|
|
11
|
+
];
|
|
12
|
+
var envFileLoaded = envCandidates.find((p) => existsSync(p)) || null;
|
|
13
|
+
var isTestRun = !!process.env.VITEST;
|
|
14
|
+
if (envFileLoaded && !isTestRun) {
|
|
15
|
+
dotenv.config({ path: envFileLoaded, override: true });
|
|
16
|
+
}
|
|
17
|
+
if (!isTestRun) {
|
|
18
|
+
console.log(
|
|
19
|
+
`[config] dotenv: ${envFileLoaded ? `loaded ${envFileLoaded}` : "no .env file found"}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
3
22
|
function dataRoot() {
|
|
4
23
|
return process.env.DATA_ROOT || join(process.cwd(), "data");
|
|
5
24
|
}
|
|
@@ -19,6 +38,14 @@ var config = {
|
|
|
19
38
|
get PROMPTOWL_KEY() {
|
|
20
39
|
return process.env.PROMPTOWL_KEY || "";
|
|
21
40
|
},
|
|
41
|
+
/**
|
|
42
|
+
* Path to the .env file the server reads its config from. Used by
|
|
43
|
+
* the license install flow to persist PROMPTOWL_KEY alongside any
|
|
44
|
+
* existing env vars, instead of a separate sidecar file.
|
|
45
|
+
*/
|
|
46
|
+
get ENV_FILE_PATH() {
|
|
47
|
+
return process.env.ENV_FILE_PATH || join(process.cwd(), ".env");
|
|
48
|
+
},
|
|
22
49
|
get TELEMETRY_ENABLED() {
|
|
23
50
|
return process.env.TELEMETRY_ENABLED !== "false";
|
|
24
51
|
},
|
|
@@ -52,7 +79,11 @@ var config = {
|
|
|
52
79
|
// src/db/client.ts
|
|
53
80
|
import Database from "better-sqlite3";
|
|
54
81
|
import { mkdirSync } from "fs";
|
|
55
|
-
import { dirname } from "path";
|
|
82
|
+
import { dirname as dirname2 } from "path";
|
|
83
|
+
|
|
84
|
+
// src/shared/constants.ts
|
|
85
|
+
var ANON_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
86
|
+
var ANON_EMAIL = "admin@localhost";
|
|
56
87
|
|
|
57
88
|
// src/db/migrations.ts
|
|
58
89
|
function runMigrations(db2) {
|
|
@@ -124,19 +155,17 @@ function runMigrations(db2) {
|
|
|
124
155
|
db2.exec(`
|
|
125
156
|
-- Steward assignments (mirrors PromptOwl ContextSteward model)
|
|
126
157
|
-- scope+target combination determines what the steward governs
|
|
127
|
-
-- Resolution priority: document(1) >
|
|
158
|
+
-- Resolution priority: document(1) > tag(2) > nest(3)
|
|
128
159
|
CREATE TABLE IF NOT EXISTS stewards (
|
|
129
160
|
id TEXT PRIMARY KEY,
|
|
130
161
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
131
|
-
scope TEXT NOT NULL CHECK(scope IN ('document', '
|
|
132
|
-
node_pattern TEXT, --
|
|
162
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
163
|
+
node_pattern TEXT, -- exact node id for document scope
|
|
133
164
|
tag_name TEXT, -- for tag scope
|
|
134
165
|
user_email TEXT NOT NULL,
|
|
135
166
|
user_id TEXT REFERENCES users(id),
|
|
136
167
|
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
137
168
|
CHECK(role IN ('editor', 'reviewer', 'admin')),
|
|
138
|
-
can_approve INTEGER NOT NULL DEFAULT 1,
|
|
139
|
-
can_reject INTEGER NOT NULL DEFAULT 1,
|
|
140
169
|
assigned_by TEXT NOT NULL,
|
|
141
170
|
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
142
171
|
is_active INTEGER NOT NULL DEFAULT 1
|
|
@@ -204,6 +233,22 @@ function runMigrations(db2) {
|
|
|
204
233
|
if (!userCols.includes("is_admin")) {
|
|
205
234
|
db2.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
|
|
206
235
|
}
|
|
236
|
+
if (!userCols.includes("is_invited")) {
|
|
237
|
+
db2.exec("ALTER TABLE users ADD COLUMN is_invited INTEGER NOT NULL DEFAULT 0");
|
|
238
|
+
}
|
|
239
|
+
const stewardCols = db2.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
240
|
+
if (stewardCols.length > 0) {
|
|
241
|
+
if (!stewardCols.includes("can_approve")) {
|
|
242
|
+
db2.exec(
|
|
243
|
+
"ALTER TABLE stewards ADD COLUMN can_approve INTEGER NOT NULL DEFAULT 1"
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!stewardCols.includes("can_reject")) {
|
|
247
|
+
db2.exec(
|
|
248
|
+
"ALTER TABLE stewards ADD COLUMN can_reject INTEGER NOT NULL DEFAULT 1"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
207
252
|
db2.exec(`
|
|
208
253
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
209
254
|
id TEXT PRIMARY KEY,
|
|
@@ -218,24 +263,23 @@ function runMigrations(db2) {
|
|
|
218
263
|
CREATE TABLE stewards_new (
|
|
219
264
|
id TEXT PRIMARY KEY,
|
|
220
265
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
221
|
-
scope TEXT NOT NULL CHECK(scope IN ('document', '
|
|
266
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
222
267
|
node_pattern TEXT,
|
|
223
268
|
tag_name TEXT,
|
|
224
269
|
user_email TEXT NOT NULL,
|
|
225
270
|
user_id TEXT REFERENCES users(id),
|
|
226
271
|
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
227
272
|
CHECK(role IN ('editor', 'reviewer', 'viewer')),
|
|
228
|
-
can_approve INTEGER NOT NULL DEFAULT 1,
|
|
229
|
-
can_reject INTEGER NOT NULL DEFAULT 1,
|
|
230
273
|
assigned_by TEXT NOT NULL,
|
|
231
274
|
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
232
275
|
is_active INTEGER NOT NULL DEFAULT 1
|
|
233
276
|
);
|
|
234
277
|
|
|
235
|
-
-- Copy rows; map legacy 'admin' role to 'reviewer', normalize tag_name + email
|
|
278
|
+
-- Copy rows; map legacy 'admin' role to 'reviewer', normalize tag_name + email.
|
|
279
|
+
-- Legacy can_approve/can_reject columns are dropped here (role is now the sole signal).
|
|
236
280
|
INSERT INTO stewards_new
|
|
237
281
|
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
238
|
-
role,
|
|
282
|
+
role, assigned_by, assigned_at, is_active)
|
|
239
283
|
SELECT
|
|
240
284
|
id, nest_id, scope, node_pattern,
|
|
241
285
|
CASE
|
|
@@ -245,7 +289,7 @@ function runMigrations(db2) {
|
|
|
245
289
|
lower(user_email),
|
|
246
290
|
user_id,
|
|
247
291
|
CASE role WHEN 'admin' THEN 'reviewer' ELSE role END,
|
|
248
|
-
|
|
292
|
+
assigned_by, assigned_at, is_active
|
|
249
293
|
FROM stewards;
|
|
250
294
|
|
|
251
295
|
DROP TABLE stewards;
|
|
@@ -264,10 +308,6 @@ function runMigrations(db2) {
|
|
|
264
308
|
ON stewards(nest_id, node_pattern, user_email)
|
|
265
309
|
WHERE scope = 'document' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
266
310
|
|
|
267
|
-
CREATE UNIQUE INDEX idx_stewards_uniq_folder
|
|
268
|
-
ON stewards(nest_id, node_pattern, user_email)
|
|
269
|
-
WHERE scope = 'folder' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
270
|
-
|
|
271
311
|
CREATE UNIQUE INDEX idx_stewards_uniq_tag
|
|
272
312
|
ON stewards(nest_id, tag_name, user_email)
|
|
273
313
|
WHERE scope = 'tag' AND tag_name IS NOT NULL AND is_active = 1;
|
|
@@ -276,9 +316,6 @@ function runMigrations(db2) {
|
|
|
276
316
|
CREATE INDEX idx_stewards_tag_lookup
|
|
277
317
|
ON stewards(nest_id, tag_name)
|
|
278
318
|
WHERE scope = 'tag' AND is_active = 1;
|
|
279
|
-
CREATE INDEX idx_stewards_folder_lookup
|
|
280
|
-
ON stewards(nest_id, node_pattern)
|
|
281
|
-
WHERE scope = 'folder' AND is_active = 1;
|
|
282
319
|
CREATE INDEX idx_stewards_doc_lookup
|
|
283
320
|
ON stewards(nest_id, node_pattern)
|
|
284
321
|
WHERE scope = 'document' AND is_active = 1;
|
|
@@ -326,16 +363,174 @@ function runMigrations(db2) {
|
|
|
326
363
|
recordMigration("002_steward_parity");
|
|
327
364
|
})();
|
|
328
365
|
}
|
|
366
|
+
if (!hasMigration("003_sessions_and_single_api_key")) {
|
|
367
|
+
db2.transaction(() => {
|
|
368
|
+
db2.exec(`
|
|
369
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
370
|
+
id TEXT PRIMARY KEY,
|
|
371
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
372
|
+
expires_at TEXT NOT NULL,
|
|
373
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
374
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
375
|
+
user_agent TEXT
|
|
376
|
+
);
|
|
377
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
379
|
+
`);
|
|
380
|
+
db2.exec(`
|
|
381
|
+
DELETE FROM api_keys
|
|
382
|
+
WHERE id IN (
|
|
383
|
+
SELECT id FROM (
|
|
384
|
+
SELECT
|
|
385
|
+
id,
|
|
386
|
+
ROW_NUMBER() OVER (
|
|
387
|
+
PARTITION BY user_id
|
|
388
|
+
ORDER BY
|
|
389
|
+
COALESCE(last_used_at, '') DESC,
|
|
390
|
+
created_at DESC,
|
|
391
|
+
id DESC
|
|
392
|
+
) AS rn
|
|
393
|
+
FROM api_keys
|
|
394
|
+
)
|
|
395
|
+
WHERE rn > 1
|
|
396
|
+
);
|
|
397
|
+
`);
|
|
398
|
+
db2.exec(`
|
|
399
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_user_unique
|
|
400
|
+
ON api_keys(user_id);
|
|
401
|
+
`);
|
|
402
|
+
recordMigration("003_sessions_and_single_api_key");
|
|
403
|
+
})();
|
|
404
|
+
}
|
|
405
|
+
if (!hasMigration("004_license_cache_owner_email")) {
|
|
406
|
+
db2.transaction(() => {
|
|
407
|
+
const cols = db2.prepare("PRAGMA table_info(license_cache)").all().map((c) => c.name);
|
|
408
|
+
if (!cols.includes("owner_email")) {
|
|
409
|
+
db2.exec("ALTER TABLE license_cache ADD COLUMN owner_email TEXT");
|
|
410
|
+
}
|
|
411
|
+
recordMigration("004_license_cache_owner_email");
|
|
412
|
+
})();
|
|
413
|
+
}
|
|
414
|
+
if (!hasMigration("005_anon_nest_public_default")) {
|
|
415
|
+
db2.transaction(() => {
|
|
416
|
+
db2.prepare(
|
|
417
|
+
"UPDATE nests SET visibility = 'public' WHERE user_id = ? AND visibility = 'private'"
|
|
418
|
+
).run(ANON_USER_ID);
|
|
419
|
+
recordMigration("005_anon_nest_public_default");
|
|
420
|
+
})();
|
|
421
|
+
}
|
|
422
|
+
if (!hasMigration("006_node_versions_published_status")) {
|
|
423
|
+
db2.transaction(() => {
|
|
424
|
+
db2.exec(`
|
|
425
|
+
CREATE TABLE node_versions_new (
|
|
426
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
427
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
428
|
+
node_id TEXT NOT NULL,
|
|
429
|
+
version INTEGER NOT NULL,
|
|
430
|
+
content_hash TEXT NOT NULL,
|
|
431
|
+
author TEXT NOT NULL,
|
|
432
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
433
|
+
CHECK(status IN ('draft', 'pending_review', 'approved', 'published', 'rejected')),
|
|
434
|
+
change_note TEXT,
|
|
435
|
+
tags_json TEXT,
|
|
436
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
437
|
+
UNIQUE(nest_id, node_id, version)
|
|
438
|
+
);
|
|
439
|
+
INSERT INTO node_versions_new
|
|
440
|
+
(id, nest_id, node_id, version, content_hash, author, status, change_note, tags_json, created_at)
|
|
441
|
+
SELECT
|
|
442
|
+
id, nest_id, node_id, version, content_hash, author, status, change_note, tags_json, created_at
|
|
443
|
+
FROM node_versions;
|
|
444
|
+
DROP TABLE node_versions;
|
|
445
|
+
ALTER TABLE node_versions_new RENAME TO node_versions;
|
|
446
|
+
CREATE INDEX idx_versions_node ON node_versions(nest_id, node_id);
|
|
447
|
+
`);
|
|
448
|
+
recordMigration("006_node_versions_published_status");
|
|
449
|
+
})();
|
|
450
|
+
}
|
|
451
|
+
if (!hasMigration("007_drop_steward_capability_flags")) {
|
|
452
|
+
const stewardCols2 = db2.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
453
|
+
const hasLegacyCols = stewardCols2.includes("can_approve") || stewardCols2.includes("can_reject");
|
|
454
|
+
if (hasLegacyCols) {
|
|
455
|
+
db2.transaction(() => {
|
|
456
|
+
if (stewardCols2.includes("can_approve")) {
|
|
457
|
+
db2.exec("ALTER TABLE stewards DROP COLUMN can_approve");
|
|
458
|
+
}
|
|
459
|
+
if (stewardCols2.includes("can_reject")) {
|
|
460
|
+
db2.exec("ALTER TABLE stewards DROP COLUMN can_reject");
|
|
461
|
+
}
|
|
462
|
+
recordMigration("007_drop_steward_capability_flags");
|
|
463
|
+
})();
|
|
464
|
+
} else {
|
|
465
|
+
recordMigration("007_drop_steward_capability_flags");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!hasMigration("008_drop_steward_folder_scope")) {
|
|
469
|
+
db2.transaction(() => {
|
|
470
|
+
db2.exec("DELETE FROM stewards WHERE scope = 'folder'");
|
|
471
|
+
db2.exec("DROP INDEX IF EXISTS idx_stewards_uniq_folder");
|
|
472
|
+
db2.exec("DROP INDEX IF EXISTS idx_stewards_folder_lookup");
|
|
473
|
+
const tbl = db2.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='stewards'").get();
|
|
474
|
+
if (tbl?.sql && tbl.sql.includes("'folder'")) {
|
|
475
|
+
db2.exec(`
|
|
476
|
+
CREATE TABLE stewards_new (
|
|
477
|
+
id TEXT PRIMARY KEY,
|
|
478
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
479
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
480
|
+
node_pattern TEXT,
|
|
481
|
+
tag_name TEXT,
|
|
482
|
+
user_email TEXT NOT NULL,
|
|
483
|
+
user_id TEXT REFERENCES users(id),
|
|
484
|
+
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
485
|
+
CHECK(role IN ('editor', 'reviewer', 'viewer')),
|
|
486
|
+
assigned_by TEXT NOT NULL,
|
|
487
|
+
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
488
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
489
|
+
);
|
|
490
|
+
INSERT INTO stewards_new
|
|
491
|
+
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
492
|
+
role, assigned_by, assigned_at, is_active)
|
|
493
|
+
SELECT
|
|
494
|
+
id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
495
|
+
role, assigned_by, assigned_at, is_active
|
|
496
|
+
FROM stewards;
|
|
497
|
+
DROP TABLE stewards;
|
|
498
|
+
ALTER TABLE stewards_new RENAME TO stewards;
|
|
499
|
+
|
|
500
|
+
CREATE INDEX idx_stewards_nest ON stewards(nest_id);
|
|
501
|
+
CREATE INDEX idx_stewards_email ON stewards(user_email);
|
|
502
|
+
CREATE INDEX idx_stewards_scope ON stewards(nest_id, scope);
|
|
503
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_nest
|
|
504
|
+
ON stewards(nest_id, user_email)
|
|
505
|
+
WHERE scope = 'nest' AND is_active = 1;
|
|
506
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_document
|
|
507
|
+
ON stewards(nest_id, node_pattern, user_email)
|
|
508
|
+
WHERE scope = 'document' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
509
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_tag
|
|
510
|
+
ON stewards(nest_id, tag_name, user_email)
|
|
511
|
+
WHERE scope = 'tag' AND tag_name IS NOT NULL AND is_active = 1;
|
|
512
|
+
CREATE INDEX idx_stewards_tag_lookup
|
|
513
|
+
ON stewards(nest_id, tag_name)
|
|
514
|
+
WHERE scope = 'tag' AND is_active = 1;
|
|
515
|
+
CREATE INDEX idx_stewards_doc_lookup
|
|
516
|
+
ON stewards(nest_id, node_pattern)
|
|
517
|
+
WHERE scope = 'document' AND is_active = 1;
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
recordMigration("008_drop_steward_folder_scope");
|
|
521
|
+
})();
|
|
522
|
+
}
|
|
329
523
|
}
|
|
330
524
|
|
|
331
525
|
// src/db/client.ts
|
|
332
526
|
var db = null;
|
|
333
527
|
function getDb() {
|
|
334
528
|
if (!db) {
|
|
335
|
-
mkdirSync(
|
|
529
|
+
mkdirSync(dirname2(config.DATABASE_PATH), { recursive: true });
|
|
336
530
|
db = new Database(config.DATABASE_PATH);
|
|
337
531
|
db.pragma("journal_mode = WAL");
|
|
338
532
|
db.pragma("foreign_keys = ON");
|
|
533
|
+
db.pragma("busy_timeout = 5000");
|
|
339
534
|
runMigrations(db);
|
|
340
535
|
}
|
|
341
536
|
return db;
|
|
@@ -343,5 +538,7 @@ function getDb() {
|
|
|
343
538
|
|
|
344
539
|
export {
|
|
345
540
|
config,
|
|
541
|
+
ANON_USER_ID,
|
|
542
|
+
ANON_EMAIL,
|
|
346
543
|
getDb
|
|
347
544
|
};
|