@promptowl/contextnest-community 1.7.0 → 1.9.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 +191 -181
- package/LICENSE.md +142 -142
- package/README.md +224 -224
- package/dist/chunk-3JTODC3Y.js +48 -0
- package/dist/{chunk-43DOX4LH.js → chunk-BYS4HDME.js} +45 -49
- package/dist/{chunk-QMLAXQES.js → chunk-DPHV6Q26.js} +311 -19
- package/dist/chunk-FRQJWGN3.js +8 -0
- package/dist/{chunk-VD5QX2ZQ.js → chunk-HRQWNRVI.js} +4 -2
- package/dist/chunk-TIO5XOFD.js +103 -0
- package/dist/chunk-VC3QNHHB.js +664 -0
- package/dist/chunk-XQ46F76G.js +16 -0
- package/dist/client-GI74NPIW.js +11 -0
- package/dist/email-R7DFS6E5.js +6 -0
- package/dist/grants-service-MDLRPMZR.js +21 -0
- package/dist/index.js +2579 -230
- package/dist/{migrations.postgres-3HMGLKOV.js → migrations.postgres-ORSV7UFJ.js} +139 -1
- package/dist/{review-service-2FSKW425.js → review-service-A3XFQTJH.js} +6 -4
- package/dist/{stewardship-service-I2JAFYJU.js → stewardship-service-3KCVVXCW.js} +4 -2
- package/dist/{version-service-FJWVTZKR.js → version-service-6J4OWQL6.js} +3 -2
- package/dist/web3/assets/{hootie-_U3ECslt.svg → hootie-C2ocYkn4.svg} +8 -8
- package/dist/web3/assets/index-CHavHPIT.css +1 -0
- package/dist/web3/assets/index-DyvOBH92.js +934 -0
- package/dist/web3/index.html +14 -14
- package/package.json +161 -152
- package/dist/chunk-MOXICJPD.js +0 -303
- package/dist/web3/assets/index-24dBhQSQ.css +0 -1
- package/dist/web3/assets/index-DpM8J4RN.js +0 -889
|
@@ -2,6 +2,11 @@ import {
|
|
|
2
2
|
ANON_USER_ID
|
|
3
3
|
} from "./chunk-SLTQACJW.js";
|
|
4
4
|
|
|
5
|
+
// src/db/client.ts
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname as dirname2 } from "path";
|
|
9
|
+
|
|
5
10
|
// src/config.ts
|
|
6
11
|
import { join, dirname } from "path";
|
|
7
12
|
import { existsSync } from "fs";
|
|
@@ -20,6 +25,15 @@ if (envFileLoaded && !isTestRun) {
|
|
|
20
25
|
}
|
|
21
26
|
var canonicalEnvFile = process.env.ENV_FILE_PATH || join(dataRoot(), ".env");
|
|
22
27
|
var canonicalEnvLoaded = null;
|
|
28
|
+
var slackUrlWarned = false;
|
|
29
|
+
var emailFromWarned = false;
|
|
30
|
+
var emailToWarned = false;
|
|
31
|
+
function isEmailish(v) {
|
|
32
|
+
return /^[^\s@]+@[^\s@]+$/.test(v) && !/[\r\n]/.test(v);
|
|
33
|
+
}
|
|
34
|
+
function isEmailListish(v) {
|
|
35
|
+
return v.split(",").every((e) => isEmailish(e.trim()));
|
|
36
|
+
}
|
|
23
37
|
if (!isTestRun && canonicalEnvFile !== envFileLoaded && existsSync(canonicalEnvFile)) {
|
|
24
38
|
dotenv.config({ path: canonicalEnvFile, override: true });
|
|
25
39
|
canonicalEnvLoaded = canonicalEnvFile;
|
|
@@ -186,9 +200,133 @@ var config = {
|
|
|
186
200
|
}
|
|
187
201
|
return raw;
|
|
188
202
|
},
|
|
203
|
+
/**
|
|
204
|
+
* Workflow plane ("turing" track): typed edges, edge-type registry, runs.
|
|
205
|
+
* OFF by default — an optional feature an admin turns on (env or Settings).
|
|
206
|
+
* This flag is also the future license-tier hook: gating a paid tier here
|
|
207
|
+
* is a one-line change because every plane route checks it per request.
|
|
208
|
+
*/
|
|
209
|
+
get FEATURE_WORKFLOW_PLANE() {
|
|
210
|
+
return process.env.FEATURE_WORKFLOW_PLANE === "true";
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* Sub-agent runs: lets a run spawn nested runs (an external runner calling
|
|
214
|
+
* back into POST /run for a child agent). Gated SEPARATELY from the base
|
|
215
|
+
* plane because recursion is the risky surface — unbounded fan-out,
|
|
216
|
+
* re-entrancy, resource exhaustion. Off by default; must clear a security
|
|
217
|
+
* review before an operator turns it on. Requires FEATURE_WORKFLOW_PLANE too.
|
|
218
|
+
*/
|
|
219
|
+
get FEATURE_SUBAGENT_RUNS() {
|
|
220
|
+
return process.env.FEATURE_SUBAGENT_RUNS === "true";
|
|
221
|
+
},
|
|
222
|
+
/** Hard ceiling on sub-agent nesting depth — keeps the call tree finite
|
|
223
|
+
* (haltable / statically bounded). Clamped to [1, 32]. */
|
|
224
|
+
get SUBAGENT_MAX_DEPTH() {
|
|
225
|
+
const n = parseInt(process.env.SUBAGENT_MAX_DEPTH || "8", 10);
|
|
226
|
+
return Number.isFinite(n) ? Math.min(32, Math.max(1, n)) : 8;
|
|
227
|
+
},
|
|
228
|
+
/** Max direct children a single run may spawn. Depth bounds the tree's
|
|
229
|
+
* height; this bounds its width — together they cap total size so a
|
|
230
|
+
* runner can't fork-bomb the DB. Clamped to [1, 128]. */
|
|
231
|
+
get SUBAGENT_MAX_CHILDREN() {
|
|
232
|
+
const n = parseInt(process.env.SUBAGENT_MAX_CHILDREN || "16", 10);
|
|
233
|
+
return Number.isFinite(n) ? Math.min(128, Math.max(1, n)) : 16;
|
|
234
|
+
},
|
|
235
|
+
/** Max steps a single run may accumulate — bounds a runaway/hostile
|
|
236
|
+
* runner's step log. Clamped to [10, 100000]. */
|
|
237
|
+
get RUN_MAX_STEPS() {
|
|
238
|
+
const n = parseInt(process.env.RUN_MAX_STEPS || "10000", 10);
|
|
239
|
+
return Number.isFinite(n) ? Math.min(1e5, Math.max(10, n)) : 1e4;
|
|
240
|
+
},
|
|
189
241
|
get AUTH_MODE() {
|
|
190
242
|
return process.env.AUTH_MODE || "key";
|
|
191
243
|
},
|
|
244
|
+
/**
|
|
245
|
+
* Optional SMTP connection URL for email notifications, e.g.
|
|
246
|
+
* smtps://user:pass@smtp.example.com:465. smtp:// or smtps:// only.
|
|
247
|
+
* Empty/unset = email connector off. Requires NOTIFY_EMAIL_FROM/_TO.
|
|
248
|
+
*/
|
|
249
|
+
get SMTP_URL() {
|
|
250
|
+
const raw = process.env.SMTP_URL?.trim();
|
|
251
|
+
if (!raw) return null;
|
|
252
|
+
if (!/^smtps?:\/\//i.test(raw)) {
|
|
253
|
+
console.warn(
|
|
254
|
+
"[config] SMTP_URL rejected: must be an smtp:// or smtps:// URL. Email notifications disabled."
|
|
255
|
+
);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return raw;
|
|
259
|
+
},
|
|
260
|
+
/**
|
|
261
|
+
* From address for notification emails. Shape-validated here (not only in
|
|
262
|
+
* the /admin/settings PATCH) so a value set directly via env/Docker can't
|
|
263
|
+
* reach nodemailer's envelope unchecked. Invalid = connector off.
|
|
264
|
+
*/
|
|
265
|
+
get NOTIFY_EMAIL_FROM() {
|
|
266
|
+
const raw = process.env.NOTIFY_EMAIL_FROM?.trim();
|
|
267
|
+
if (!raw) return null;
|
|
268
|
+
if (!isEmailish(raw)) {
|
|
269
|
+
if (!emailFromWarned) {
|
|
270
|
+
emailFromWarned = true;
|
|
271
|
+
console.warn(
|
|
272
|
+
"[config] NOTIFY_EMAIL_FROM rejected: must be a plain email address. Email notifications disabled."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return raw;
|
|
278
|
+
},
|
|
279
|
+
/**
|
|
280
|
+
* Comma-separated recipient list for notification emails. Shape-validated
|
|
281
|
+
* here for the same env-bypass reason as NOTIFY_EMAIL_FROM. Invalid (any
|
|
282
|
+
* entry malformed) = connector off.
|
|
283
|
+
*/
|
|
284
|
+
get NOTIFY_EMAIL_TO() {
|
|
285
|
+
const raw = process.env.NOTIFY_EMAIL_TO?.trim();
|
|
286
|
+
if (!raw) return null;
|
|
287
|
+
if (!isEmailListish(raw)) {
|
|
288
|
+
if (!emailToWarned) {
|
|
289
|
+
emailToWarned = true;
|
|
290
|
+
console.warn(
|
|
291
|
+
"[config] NOTIFY_EMAIL_TO rejected: must be a comma-separated list of email addresses. Email notifications disabled."
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return raw;
|
|
297
|
+
},
|
|
298
|
+
/**
|
|
299
|
+
* Optional Slack incoming-webhook URL for team notifications (review
|
|
300
|
+
* submitted/approved/rejected, nest shared). Empty/unset = connector off.
|
|
301
|
+
* https only — a webhook carries an implicit secret in its path, so it
|
|
302
|
+
* never travels plaintext.
|
|
303
|
+
*/
|
|
304
|
+
get SLACK_WEBHOOK_URL() {
|
|
305
|
+
const raw = process.env.SLACK_WEBHOOK_URL?.trim();
|
|
306
|
+
if (!raw) return null;
|
|
307
|
+
if (!/^https:\/\//i.test(raw)) {
|
|
308
|
+
if (!slackUrlWarned) {
|
|
309
|
+
slackUrlWarned = true;
|
|
310
|
+
console.warn(
|
|
311
|
+
"[config] SLACK_WEBHOOK_URL rejected: must be an https:// URL. Slack notifications disabled."
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return raw;
|
|
317
|
+
},
|
|
318
|
+
/**
|
|
319
|
+
* Per-nest notification digest window in milliseconds. A burst of governance
|
|
320
|
+
* events on one nest inside this window collapses into a single digest
|
|
321
|
+
* message instead of one post per event. Empty / non-numeric / negative /
|
|
322
|
+
* unset → 15000 (15s).
|
|
323
|
+
*/
|
|
324
|
+
get NOTIFY_DEBOUNCE_MS() {
|
|
325
|
+
const raw = process.env.NOTIFY_DEBOUNCE_MS?.trim();
|
|
326
|
+
if (!raw) return 15e3;
|
|
327
|
+
const n = Number(raw);
|
|
328
|
+
return Number.isFinite(n) && n >= 0 ? n : 15e3;
|
|
329
|
+
},
|
|
192
330
|
/**
|
|
193
331
|
* CORS origin allowlist. Comma-separated list of origins or "*".
|
|
194
332
|
* Defaults to "*" in open mode (read-heavy, no credentials to steal)
|
|
@@ -210,11 +348,6 @@ var config = {
|
|
|
210
348
|
}
|
|
211
349
|
};
|
|
212
350
|
|
|
213
|
-
// src/db/client.ts
|
|
214
|
-
import Database from "better-sqlite3";
|
|
215
|
-
import { mkdirSync, readFileSync } from "fs";
|
|
216
|
-
import { dirname as dirname2 } from "path";
|
|
217
|
-
|
|
218
351
|
// src/db/migrations.ts
|
|
219
352
|
function runMigrations(db) {
|
|
220
353
|
db.exec(`
|
|
@@ -781,6 +914,170 @@ function runMigrations(db) {
|
|
|
781
914
|
})();
|
|
782
915
|
recordMigration("012_merge_case_colliding_users");
|
|
783
916
|
}
|
|
917
|
+
if (!hasMigration("019_grants")) {
|
|
918
|
+
db.transaction(() => {
|
|
919
|
+
db.exec(`
|
|
920
|
+
CREATE TABLE IF NOT EXISTS grants (
|
|
921
|
+
id TEXT PRIMARY KEY,
|
|
922
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
923
|
+
target_type TEXT NOT NULL CHECK(target_type IN ('document', 'folder')),
|
|
924
|
+
target TEXT NOT NULL, -- node id (document) or path prefix (folder)
|
|
925
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
926
|
+
role TEXT NOT NULL CHECK(role IN ('read', 'write')),
|
|
927
|
+
granted_by TEXT NOT NULL,
|
|
928
|
+
created_at TEXT NOT NULL,
|
|
929
|
+
updated_at TEXT NOT NULL
|
|
930
|
+
);
|
|
931
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_grants_unique
|
|
932
|
+
ON grants(nest_id, target_type, target, user_id);
|
|
933
|
+
CREATE INDEX IF NOT EXISTS idx_grants_user ON grants(nest_id, user_id);
|
|
934
|
+
`);
|
|
935
|
+
})();
|
|
936
|
+
recordMigration("019_grants");
|
|
937
|
+
}
|
|
938
|
+
if (!hasMigration("013_node_deletion_tombstones")) {
|
|
939
|
+
db.transaction(() => {
|
|
940
|
+
db.exec(`
|
|
941
|
+
CREATE TABLE IF NOT EXISTS node_deletions (
|
|
942
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
943
|
+
node_id TEXT NOT NULL,
|
|
944
|
+
deleted_by TEXT,
|
|
945
|
+
deleted_at TEXT NOT NULL,
|
|
946
|
+
PRIMARY KEY (nest_id, node_id)
|
|
947
|
+
);
|
|
948
|
+
CREATE INDEX IF NOT EXISTS idx_node_deletions_time
|
|
949
|
+
ON node_deletions(nest_id, deleted_at);
|
|
950
|
+
`);
|
|
951
|
+
})();
|
|
952
|
+
recordMigration("013_node_deletion_tombstones");
|
|
953
|
+
}
|
|
954
|
+
if (!hasMigration("014_api_events")) {
|
|
955
|
+
db.transaction(() => {
|
|
956
|
+
db.exec(`
|
|
957
|
+
CREATE TABLE IF NOT EXISTS api_events (
|
|
958
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
959
|
+
ts TEXT NOT NULL,
|
|
960
|
+
kind TEXT NOT NULL CHECK(kind IN ('api', 'mcp')),
|
|
961
|
+
method TEXT, -- HTTP method (api rows)
|
|
962
|
+
path TEXT, -- request path (api rows)
|
|
963
|
+
tool TEXT, -- tool name (mcp rows)
|
|
964
|
+
nest_id TEXT,
|
|
965
|
+
user_id TEXT,
|
|
966
|
+
user_email TEXT,
|
|
967
|
+
status INTEGER, -- HTTP status (api rows)
|
|
968
|
+
duration_ms INTEGER
|
|
969
|
+
);
|
|
970
|
+
CREATE INDEX IF NOT EXISTS idx_api_events_ts ON api_events(ts);
|
|
971
|
+
`);
|
|
972
|
+
})();
|
|
973
|
+
recordMigration("014_api_events");
|
|
974
|
+
}
|
|
975
|
+
if (!hasMigration("015_definitions")) {
|
|
976
|
+
db.transaction(() => {
|
|
977
|
+
db.exec(`
|
|
978
|
+
CREATE TABLE IF NOT EXISTS definitions (
|
|
979
|
+
id TEXT PRIMARY KEY,
|
|
980
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
981
|
+
term TEXT NOT NULL,
|
|
982
|
+
definition TEXT NOT NULL,
|
|
983
|
+
linked_tag TEXT, -- optional #tag this term maps to
|
|
984
|
+
defined_by TEXT NOT NULL, -- email
|
|
985
|
+
created_at TEXT NOT NULL,
|
|
986
|
+
updated_at TEXT NOT NULL
|
|
987
|
+
);
|
|
988
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_definitions_term
|
|
989
|
+
ON definitions(nest_id, term COLLATE NOCASE);
|
|
990
|
+
`);
|
|
991
|
+
})();
|
|
992
|
+
recordMigration("015_definitions");
|
|
993
|
+
}
|
|
994
|
+
if (!hasMigration("016_workflow_edges")) {
|
|
995
|
+
db.transaction(() => {
|
|
996
|
+
db.exec(`
|
|
997
|
+
CREATE TABLE IF NOT EXISTS edge_types (
|
|
998
|
+
id TEXT PRIMARY KEY,
|
|
999
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1000
|
+
name TEXT NOT NULL,
|
|
1001
|
+
description TEXT NOT NULL,
|
|
1002
|
+
direction TEXT NOT NULL DEFAULT 'directed'
|
|
1003
|
+
CHECK(direction IN ('directed', 'undirected')),
|
|
1004
|
+
is_flow INTEGER NOT NULL DEFAULT 0, -- flow types form the DAG
|
|
1005
|
+
condition_schema TEXT, -- JSON template | NULL
|
|
1006
|
+
color TEXT,
|
|
1007
|
+
created_by TEXT NOT NULL,
|
|
1008
|
+
created_at TEXT NOT NULL,
|
|
1009
|
+
updated_at TEXT NOT NULL
|
|
1010
|
+
);
|
|
1011
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_edge_types_name
|
|
1012
|
+
ON edge_types(nest_id, LOWER(name));
|
|
1013
|
+
|
|
1014
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
1015
|
+
id TEXT PRIMARY KEY,
|
|
1016
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1017
|
+
from_node TEXT NOT NULL,
|
|
1018
|
+
to_node TEXT NOT NULL,
|
|
1019
|
+
-- RESTRICT (not CASCADE): the app refuses to delete an edge type
|
|
1020
|
+
-- while edges reference it; the DB backs that invariant instead of
|
|
1021
|
+
-- silently cascade-deleting live edges if the app check is bypassed.
|
|
1022
|
+
type_id TEXT NOT NULL REFERENCES edge_types(id) ON DELETE RESTRICT,
|
|
1023
|
+
condition_mode TEXT
|
|
1024
|
+
CHECK(condition_mode IN ('structured', 'nl', 'open')),
|
|
1025
|
+
condition TEXT,
|
|
1026
|
+
metadata TEXT,
|
|
1027
|
+
created_by TEXT NOT NULL,
|
|
1028
|
+
created_at TEXT NOT NULL,
|
|
1029
|
+
updated_at TEXT NOT NULL
|
|
1030
|
+
);
|
|
1031
|
+
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(nest_id, from_node);
|
|
1032
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(nest_id, to_node);
|
|
1033
|
+
CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(nest_id, type_id);
|
|
1034
|
+
`);
|
|
1035
|
+
})();
|
|
1036
|
+
recordMigration("016_workflow_edges");
|
|
1037
|
+
}
|
|
1038
|
+
if (!hasMigration("017_workflow_runs")) {
|
|
1039
|
+
db.transaction(() => {
|
|
1040
|
+
db.exec(`
|
|
1041
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
1042
|
+
id TEXT PRIMARY KEY,
|
|
1043
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1044
|
+
agent_node TEXT NOT NULL,
|
|
1045
|
+
triggered_by TEXT NOT NULL,
|
|
1046
|
+
status TEXT NOT NULL DEFAULT 'running'
|
|
1047
|
+
CHECK(status IN ('running', 'succeeded', 'failed', 'cancelled')),
|
|
1048
|
+
inputs TEXT,
|
|
1049
|
+
trace TEXT,
|
|
1050
|
+
started_at TEXT NOT NULL,
|
|
1051
|
+
finished_at TEXT
|
|
1052
|
+
);
|
|
1053
|
+
CREATE INDEX IF NOT EXISTS idx_runs_nest ON runs(nest_id, started_at);
|
|
1054
|
+
CREATE INDEX IF NOT EXISTS idx_runs_agent ON runs(nest_id, agent_node);
|
|
1055
|
+
|
|
1056
|
+
CREATE TABLE IF NOT EXISTS run_steps (
|
|
1057
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1058
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
1059
|
+
seq INTEGER NOT NULL,
|
|
1060
|
+
node_id TEXT,
|
|
1061
|
+
edge_id TEXT,
|
|
1062
|
+
action TEXT NOT NULL,
|
|
1063
|
+
detail TEXT,
|
|
1064
|
+
at TEXT NOT NULL
|
|
1065
|
+
);
|
|
1066
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_run_steps ON run_steps(run_id, seq);
|
|
1067
|
+
`);
|
|
1068
|
+
})();
|
|
1069
|
+
recordMigration("017_workflow_runs");
|
|
1070
|
+
}
|
|
1071
|
+
if (!hasMigration("018_subagent_runs")) {
|
|
1072
|
+
db.transaction(() => {
|
|
1073
|
+
db.exec(`
|
|
1074
|
+
ALTER TABLE runs ADD COLUMN parent_run_id TEXT;
|
|
1075
|
+
ALTER TABLE runs ADD COLUMN depth INTEGER NOT NULL DEFAULT 0;
|
|
1076
|
+
CREATE INDEX IF NOT EXISTS idx_runs_parent ON runs(parent_run_id);
|
|
1077
|
+
`);
|
|
1078
|
+
})();
|
|
1079
|
+
recordMigration("018_subagent_runs");
|
|
1080
|
+
}
|
|
784
1081
|
}
|
|
785
1082
|
function mergeCaseCollidingUsers(db) {
|
|
786
1083
|
const groups = db.prepare(
|
|
@@ -998,7 +1295,7 @@ async function initDb() {
|
|
|
998
1295
|
if (config.DB_DRIVER === "postgres") {
|
|
999
1296
|
const { Pool } = await import("pg");
|
|
1000
1297
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1001
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1298
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-ORSV7UFJ.js");
|
|
1002
1299
|
const pool = new Pool(buildPgConfig());
|
|
1003
1300
|
adapter = new PostgresAdapter(pool);
|
|
1004
1301
|
await runPostgresMigrations(adapter);
|
|
@@ -1018,23 +1315,18 @@ function getDb() {
|
|
|
1018
1315
|
}
|
|
1019
1316
|
return adapter;
|
|
1020
1317
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
function insertOrIgnore(db, insertSql) {
|
|
1027
|
-
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
|
|
1028
|
-
}
|
|
1029
|
-
function insertOrReplace(db, insertSql, conflictCols, set) {
|
|
1030
|
-
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
|
|
1318
|
+
async function resetDb() {
|
|
1319
|
+
if (adapter) {
|
|
1320
|
+
await adapter.close();
|
|
1321
|
+
adapter = null;
|
|
1322
|
+
}
|
|
1031
1323
|
}
|
|
1032
1324
|
|
|
1033
1325
|
export {
|
|
1326
|
+
isEmailish,
|
|
1327
|
+
isEmailListish,
|
|
1034
1328
|
config,
|
|
1035
1329
|
initDb,
|
|
1036
1330
|
getDb,
|
|
1037
|
-
|
|
1038
|
-
insertOrIgnore,
|
|
1039
|
-
insertOrReplace
|
|
1331
|
+
resetDb
|
|
1040
1332
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidationError
|
|
3
|
+
} from "./chunk-3JTODC3Y.js";
|
|
4
|
+
import {
|
|
5
|
+
getDb
|
|
6
|
+
} from "./chunk-DPHV6Q26.js";
|
|
7
|
+
|
|
8
|
+
// src/governance/grants-service.ts
|
|
9
|
+
import { v4 as uuid } from "uuid";
|
|
10
|
+
var rank = (r) => r === "write" ? 2 : 1;
|
|
11
|
+
async function resolveNodeGrant(nestId, userId, nodeId) {
|
|
12
|
+
if (!userId || !nodeId) return null;
|
|
13
|
+
const rows = await getDb().all(
|
|
14
|
+
"SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
|
|
15
|
+
[nestId, userId]
|
|
16
|
+
);
|
|
17
|
+
let best = null;
|
|
18
|
+
for (const r of rows) {
|
|
19
|
+
if (nodeId === r.target || nodeId.startsWith(r.target + "/")) {
|
|
20
|
+
if (!best || rank(r.role) > rank(best)) best = r.role;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return best;
|
|
24
|
+
}
|
|
25
|
+
async function hasAnyGrant(nestId, userId) {
|
|
26
|
+
if (!userId) return false;
|
|
27
|
+
const row = await getDb().get(
|
|
28
|
+
"SELECT 1 AS x FROM grants WHERE nest_id = ? AND user_id = ? LIMIT 1",
|
|
29
|
+
[nestId, userId]
|
|
30
|
+
);
|
|
31
|
+
return !!row;
|
|
32
|
+
}
|
|
33
|
+
async function listUserGrants(nestId, userId) {
|
|
34
|
+
if (!userId) return [];
|
|
35
|
+
return await getDb().all(
|
|
36
|
+
"SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
|
|
37
|
+
[nestId, userId]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function grantCoversNode(grants, nodeId) {
|
|
41
|
+
return grants.some(
|
|
42
|
+
(g) => nodeId === g.target || nodeId.startsWith(g.target + "/")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
async function createGrant(params) {
|
|
46
|
+
const { nestId, targetType, target, userId, role, grantedBy } = params;
|
|
47
|
+
if (!["document", "folder"].includes(targetType)) {
|
|
48
|
+
throw new ValidationError("target_type must be document | folder");
|
|
49
|
+
}
|
|
50
|
+
if (!["read", "write"].includes(role)) {
|
|
51
|
+
throw new ValidationError("role must be read | write");
|
|
52
|
+
}
|
|
53
|
+
if (!target.trim()) throw new ValidationError("target is required");
|
|
54
|
+
if (targetType === "folder") {
|
|
55
|
+
const t = target.trim().replace(/\/+$/, "");
|
|
56
|
+
if (t === "nodes" || t === "") {
|
|
57
|
+
throw new ValidationError("folder target must name a subfolder, not the nest root");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const db = getDb();
|
|
61
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
const existing = await db.get(
|
|
63
|
+
"SELECT id FROM grants WHERE nest_id = ? AND target_type = ? AND target = ? AND user_id = ?",
|
|
64
|
+
[nestId, targetType, target, userId]
|
|
65
|
+
);
|
|
66
|
+
if (existing) {
|
|
67
|
+
await db.run(
|
|
68
|
+
"UPDATE grants SET role = ?, granted_by = ?, updated_at = ? WHERE id = ?",
|
|
69
|
+
[role, grantedBy, now, existing.id]
|
|
70
|
+
);
|
|
71
|
+
return await db.get("SELECT * FROM grants WHERE id = ?", [existing.id]);
|
|
72
|
+
}
|
|
73
|
+
const id = uuid();
|
|
74
|
+
await db.run(
|
|
75
|
+
`INSERT INTO grants (id, nest_id, target_type, target, user_id, role, granted_by, created_at, updated_at)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
77
|
+
[id, nestId, targetType, target, userId, role, grantedBy, now, now]
|
|
78
|
+
);
|
|
79
|
+
return await db.get("SELECT * FROM grants WHERE id = ?", [id]);
|
|
80
|
+
}
|
|
81
|
+
async function listGrants(nestId) {
|
|
82
|
+
return await getDb().all(
|
|
83
|
+
"SELECT * FROM grants WHERE nest_id = ? ORDER BY target_type, target",
|
|
84
|
+
[nestId]
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
async function deleteGrant(nestId, id) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
const row = await db.get("SELECT id FROM grants WHERE id = ? AND nest_id = ?", [id, nestId]);
|
|
90
|
+
if (!row) return false;
|
|
91
|
+
await db.run("DELETE FROM grants WHERE id = ?", [id]);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
resolveNodeGrant,
|
|
97
|
+
hasAnyGrant,
|
|
98
|
+
listUserGrants,
|
|
99
|
+
grantCoversNode,
|
|
100
|
+
createGrant,
|
|
101
|
+
listGrants,
|
|
102
|
+
deleteGrant
|
|
103
|
+
};
|