@promptowl/contextnest-community 1.14.0 → 1.15.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 +14 -0
- package/dist/{chunk-IHGPZ6NY.js → chunk-5IAKUUZF.js} +1 -1
- package/dist/{chunk-KPK656BH.js → chunk-7BOG3S5H.js} +563 -1006
- package/dist/{chunk-I5KYGMIT.js → chunk-BCFKLY4H.js} +54 -1
- package/dist/{chunk-MZ4IR5KH.js → chunk-C4U75IUR.js} +349 -32
- package/dist/chunk-GWDOLYFJ.js +624 -0
- package/dist/{chunk-JP67WXLA.js → chunk-JWVZ3T35.js} +58 -3
- package/dist/{client-3MGIP57D.js → client-NFUHPLLM.js} +1 -1
- package/dist/{grants-service-5ZFISCBH.js → grants-service-O4AO326A.js} +2 -2
- package/dist/index.js +1547 -657
- package/dist/{migrations.postgres-BRXZY2GE.js → migrations.postgres-CYKO5CFV.js} +41 -1
- package/dist/review-service-JUAFYB73.js +47 -0
- package/dist/{stewardship-service-G7IF7ZNP.js → stewardship-service-FHJ2RBKN.js} +4 -4
- package/dist/{version-service-FGL47APL.js → version-service-ECYCBVSE.js} +7 -3
- package/dist/web3/assets/index-BaMJ4tEz.css +1 -0
- package/dist/web3/assets/index-D950G4hR.js +1052 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -2
- package/dist/chunk-XQ46F76G.js +0 -16
- package/dist/review-service-6G7MVTY6.js +0 -31
- package/dist/web3/assets/index-BYmOkm4p.js +0 -1052
- package/dist/web3/assets/index-p6uQyhml.css +0 -1
|
@@ -711,6 +711,12 @@ function runMigrations(db) {
|
|
|
711
711
|
if (!nestCols.includes("allow_self_approve")) {
|
|
712
712
|
db.exec("ALTER TABLE nests ADD COLUMN allow_self_approve INTEGER NOT NULL DEFAULT 0");
|
|
713
713
|
}
|
|
714
|
+
if (!nestCols.includes("prime_only_review")) {
|
|
715
|
+
db.exec("ALTER TABLE nests ADD COLUMN prime_only_review INTEGER NOT NULL DEFAULT 0");
|
|
716
|
+
}
|
|
717
|
+
if (!nestCols.includes("prime_tags")) {
|
|
718
|
+
db.exec("ALTER TABLE nests ADD COLUMN prime_tags TEXT NOT NULL DEFAULT 'prime-document'");
|
|
719
|
+
}
|
|
714
720
|
const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
|
|
715
721
|
if (!userCols.includes("is_admin")) {
|
|
716
722
|
db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
|
|
@@ -1454,6 +1460,53 @@ function runMigrations(db) {
|
|
|
1454
1460
|
})();
|
|
1455
1461
|
recordMigration("031_team_source");
|
|
1456
1462
|
}
|
|
1463
|
+
if (!hasMigration("032_deletion_requests")) {
|
|
1464
|
+
db.transaction(() => {
|
|
1465
|
+
db.exec(`
|
|
1466
|
+
CREATE TABLE IF NOT EXISTS deletion_requests (
|
|
1467
|
+
id TEXT PRIMARY KEY,
|
|
1468
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
1469
|
+
node_id TEXT NOT NULL,
|
|
1470
|
+
requested_by TEXT NOT NULL, -- email
|
|
1471
|
+
reason TEXT NOT NULL,
|
|
1472
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
1473
|
+
CHECK(status IN ('pending', 'declined')),
|
|
1474
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1475
|
+
resolved_by TEXT, -- email
|
|
1476
|
+
resolved_at TEXT,
|
|
1477
|
+
resolution_note TEXT
|
|
1478
|
+
);
|
|
1479
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_deletion_requests_pending
|
|
1480
|
+
ON deletion_requests(nest_id, node_id) WHERE status = 'pending';
|
|
1481
|
+
CREATE INDEX IF NOT EXISTS idx_deletion_requests_nest
|
|
1482
|
+
ON deletion_requests(nest_id, status);
|
|
1483
|
+
`);
|
|
1484
|
+
})();
|
|
1485
|
+
recordMigration("032_deletion_requests");
|
|
1486
|
+
}
|
|
1487
|
+
if (!hasMigration("033_deletion_request_targets")) {
|
|
1488
|
+
db.transaction(() => {
|
|
1489
|
+
const cols = db.prepare("PRAGMA table_info(deletion_requests)").all().map((c) => c.name);
|
|
1490
|
+
if (!cols.includes("target_type")) {
|
|
1491
|
+
db.exec(
|
|
1492
|
+
"ALTER TABLE deletion_requests ADD COLUMN target_type TEXT NOT NULL DEFAULT 'document'"
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
db.exec(`
|
|
1496
|
+
DROP INDEX IF EXISTS idx_deletion_requests_pending;
|
|
1497
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_deletion_requests_pending
|
|
1498
|
+
ON deletion_requests(nest_id, target_type, node_id) WHERE status = 'pending';
|
|
1499
|
+
`);
|
|
1500
|
+
})();
|
|
1501
|
+
recordMigration("033_deletion_request_targets");
|
|
1502
|
+
}
|
|
1503
|
+
if (!hasMigration("034_versions_author_status_index")) {
|
|
1504
|
+
db.exec(`
|
|
1505
|
+
CREATE INDEX IF NOT EXISTS idx_versions_author_status
|
|
1506
|
+
ON node_versions(author, status);
|
|
1507
|
+
`);
|
|
1508
|
+
recordMigration("034_versions_author_status_index");
|
|
1509
|
+
}
|
|
1457
1510
|
}
|
|
1458
1511
|
function mergeCaseCollidingUsers(db) {
|
|
1459
1512
|
const groups = db.prepare(
|
|
@@ -1671,7 +1724,7 @@ async function initDb() {
|
|
|
1671
1724
|
if (config.DB_DRIVER === "postgres") {
|
|
1672
1725
|
const { Pool } = await import("pg");
|
|
1673
1726
|
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1674
|
-
const { runPostgresMigrations } = await import("./migrations.postgres-
|
|
1727
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-CYKO5CFV.js");
|
|
1675
1728
|
const pool = new Pool(buildPgConfig());
|
|
1676
1729
|
adapter = new PostgresAdapter(pool);
|
|
1677
1730
|
await runPostgresMigrations(adapter);
|
|
@@ -2,39 +2,42 @@ import {
|
|
|
2
2
|
grantCoversNode,
|
|
3
3
|
listUserGrants,
|
|
4
4
|
resolveNodeGrant
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5IAKUUZF.js";
|
|
6
6
|
import {
|
|
7
7
|
createVersion,
|
|
8
8
|
getApprovedVersion,
|
|
9
9
|
getCurrentVersion,
|
|
10
10
|
setApprovedVersion
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JWVZ3T35.js";
|
|
12
12
|
import {
|
|
13
|
-
buildDocContext,
|
|
14
|
-
buildTitleMap,
|
|
15
13
|
canUserAccess,
|
|
16
14
|
canUserApprove,
|
|
15
|
+
resolveStewardsForNode
|
|
16
|
+
} from "./chunk-GWDOLYFJ.js";
|
|
17
|
+
import {
|
|
18
|
+
buildDocContext,
|
|
19
|
+
buildTitleMap,
|
|
17
20
|
describeNode,
|
|
18
21
|
engineCache,
|
|
22
|
+
insertOrReplace,
|
|
19
23
|
isPublicReader,
|
|
20
24
|
isStewardshipEnabled,
|
|
25
|
+
nestName,
|
|
21
26
|
notifyEmailForNest,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
nowExpr,
|
|
28
|
+
prettyFolderPath,
|
|
29
|
+
sendEmailToRecipient,
|
|
30
|
+
titleForNode
|
|
31
|
+
} from "./chunk-7BOG3S5H.js";
|
|
25
32
|
import {
|
|
26
33
|
ConflictError,
|
|
27
34
|
NotFoundError,
|
|
28
35
|
ValidationError
|
|
29
36
|
} from "./chunk-GUNJTORH.js";
|
|
30
|
-
import {
|
|
31
|
-
insertOrReplace,
|
|
32
|
-
nowExpr
|
|
33
|
-
} from "./chunk-XQ46F76G.js";
|
|
34
37
|
import {
|
|
35
38
|
config,
|
|
36
39
|
getDb
|
|
37
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-BCFKLY4H.js";
|
|
38
41
|
|
|
39
42
|
// src/governance/review-service.ts
|
|
40
43
|
import { v4 as uuid3 } from "uuid";
|
|
@@ -83,6 +86,7 @@ async function notifyReviewRequested(params) {
|
|
|
83
86
|
[nestId, nodeId, nestId, nodeId]
|
|
84
87
|
);
|
|
85
88
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
89
|
+
const label = await describeNode(nestId, nodeId);
|
|
86
90
|
let created = 0;
|
|
87
91
|
for (const r of rows) {
|
|
88
92
|
if (r.user_email.toLowerCase() === requestedBy.toLowerCase()) continue;
|
|
@@ -94,7 +98,7 @@ async function notifyReviewRequested(params) {
|
|
|
94
98
|
nestId,
|
|
95
99
|
r.user_email.toLowerCase(),
|
|
96
100
|
reviewId,
|
|
97
|
-
r.via === nodeId ? `"${
|
|
101
|
+
r.via === nodeId ? `"${label}" has a new version awaiting review (submitted by ${requestedBy})` : `Agent "${await describeNode(nestId, r.via)}" produced output on "${label}" \u2014 awaiting review`,
|
|
98
102
|
now
|
|
99
103
|
]
|
|
100
104
|
);
|
|
@@ -126,6 +130,7 @@ async function notifyReviewResolved(params) {
|
|
|
126
130
|
recipients.add(requestedBy.toLowerCase());
|
|
127
131
|
recipients.delete(resolvedBy.toLowerCase());
|
|
128
132
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
133
|
+
const label = await describeNode(nestId, nodeId);
|
|
129
134
|
let created = 0;
|
|
130
135
|
for (const email of recipients) {
|
|
131
136
|
await db.run(
|
|
@@ -137,7 +142,7 @@ async function notifyReviewResolved(params) {
|
|
|
137
142
|
email,
|
|
138
143
|
`review_${status}`,
|
|
139
144
|
reviewId,
|
|
140
|
-
`"${
|
|
145
|
+
`"${label}" was ${status} by ${resolvedBy}`,
|
|
141
146
|
now
|
|
142
147
|
]
|
|
143
148
|
);
|
|
@@ -150,7 +155,7 @@ async function notifyReviewResolved(params) {
|
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
async function notifyNestInvite(params) {
|
|
153
|
-
const { nestId, inviteeEmail, permission, nestName } = params;
|
|
158
|
+
const { nestId, inviteeEmail, permission, nestName: nestName2 } = params;
|
|
154
159
|
try {
|
|
155
160
|
await getDb().run(
|
|
156
161
|
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
@@ -160,7 +165,7 @@ async function notifyNestInvite(params) {
|
|
|
160
165
|
nestId,
|
|
161
166
|
inviteeEmail.toLowerCase(),
|
|
162
167
|
nestId,
|
|
163
|
-
`You were added to "${
|
|
168
|
+
`You were added to "${nestName2}" as ${permission}`,
|
|
164
169
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
165
170
|
]
|
|
166
171
|
);
|
|
@@ -628,8 +633,8 @@ function msTeamsPayload(title, message) {
|
|
|
628
633
|
}
|
|
629
634
|
async function notifyMsTeamsForNest(nestId, message) {
|
|
630
635
|
if (!config.MSTEAMS_WEBHOOK_URL) return;
|
|
631
|
-
const
|
|
632
|
-
return notifyMsTeams(
|
|
636
|
+
const nestName2 = await resolveNestName(nestId);
|
|
637
|
+
return notifyMsTeams(nestName2, message);
|
|
633
638
|
}
|
|
634
639
|
async function notifyMsTeams(title, message) {
|
|
635
640
|
const url = config.MSTEAMS_WEBHOOK_URL;
|
|
@@ -640,8 +645,8 @@ async function notifyMsTeams(title, message) {
|
|
|
640
645
|
// src/notify/slack.ts
|
|
641
646
|
async function notifySlackForNest(nestId, message) {
|
|
642
647
|
if (!config.SLACK_WEBHOOK_URL) return;
|
|
643
|
-
const
|
|
644
|
-
return notifySlack(`*[${
|
|
648
|
+
const nestName2 = await resolveNestName(nestId);
|
|
649
|
+
return notifySlack(`*[${nestName2}]* ${message}`);
|
|
645
650
|
}
|
|
646
651
|
function escapeMrkdwn(s) {
|
|
647
652
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -657,25 +662,31 @@ var EVENT_EMOJI = {
|
|
|
657
662
|
review_requested: ":memo:",
|
|
658
663
|
review_approved: ":white_check_mark:",
|
|
659
664
|
review_rejected: ":x:",
|
|
660
|
-
run_failed: ":rotating_light:"
|
|
665
|
+
run_failed: ":rotating_light:",
|
|
666
|
+
deletion_requested: ":wastebasket:",
|
|
667
|
+
deletion_deleted: ":wastebasket:",
|
|
668
|
+
deletion_declined: ":no_entry_sign:"
|
|
661
669
|
};
|
|
662
670
|
var DIGEST_DELIVERED_KINDS = /* @__PURE__ */ new Set([
|
|
663
671
|
"review_requested",
|
|
664
672
|
"review_approved",
|
|
665
|
-
"review_rejected"
|
|
673
|
+
"review_rejected",
|
|
674
|
+
"deletion_requested",
|
|
675
|
+
"deletion_deleted",
|
|
676
|
+
"deletion_declined"
|
|
666
677
|
]);
|
|
667
|
-
function formatBody(channel,
|
|
668
|
-
const line = `[${
|
|
678
|
+
function formatBody(channel, nestName2, ev) {
|
|
679
|
+
const line = `[${nestName2}] ${ev.message}`;
|
|
669
680
|
if (channel === "slack") {
|
|
670
|
-
return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${
|
|
681
|
+
return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${nestName2}]* ${ev.message}`) };
|
|
671
682
|
}
|
|
672
683
|
if (channel === "teams") {
|
|
673
|
-
return msTeamsPayload(
|
|
684
|
+
return msTeamsPayload(nestName2, `${EVENT_EMOJI[ev.kind] ?? ""} ${ev.message}`.trim());
|
|
674
685
|
}
|
|
675
686
|
return {
|
|
676
687
|
kind: ev.kind,
|
|
677
688
|
nest_id: ev.nestId,
|
|
678
|
-
nest_name:
|
|
689
|
+
nest_name: nestName2,
|
|
679
690
|
message: ev.message,
|
|
680
691
|
subject_id: ev.subjectId ?? null,
|
|
681
692
|
actor: ev.actor ?? null,
|
|
@@ -716,18 +727,18 @@ async function resolveUrl(nestId, raw) {
|
|
|
716
727
|
}
|
|
717
728
|
async function dispatchEvent(ev) {
|
|
718
729
|
const db = getDb();
|
|
719
|
-
const
|
|
730
|
+
const nestName2 = await resolveNestName(ev.nestId);
|
|
720
731
|
if (config.SLACK_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
|
|
721
732
|
void postWebhook(
|
|
722
733
|
config.SLACK_WEBHOOK_URL,
|
|
723
|
-
formatBody("slack",
|
|
734
|
+
formatBody("slack", nestName2, ev),
|
|
724
735
|
"server-slack"
|
|
725
736
|
);
|
|
726
737
|
}
|
|
727
738
|
if (config.MSTEAMS_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
|
|
728
739
|
void postWebhook(
|
|
729
740
|
config.MSTEAMS_WEBHOOK_URL,
|
|
730
|
-
formatBody("teams",
|
|
741
|
+
formatBody("teams", nestName2, ev),
|
|
731
742
|
"server-msteams"
|
|
732
743
|
);
|
|
733
744
|
}
|
|
@@ -749,7 +760,7 @@ async function dispatchEvent(ev) {
|
|
|
749
760
|
if (!kinds.includes("*") && !kinds.includes(ev.kind)) continue;
|
|
750
761
|
const url = await resolveUrl(ev.nestId, r.url);
|
|
751
762
|
if (!url) continue;
|
|
752
|
-
void postWebhook(url, formatBody(r.channel,
|
|
763
|
+
void postWebhook(url, formatBody(r.channel, nestName2, ev), `connector:${r.id}`);
|
|
753
764
|
}
|
|
754
765
|
}
|
|
755
766
|
async function sendTestNotification(channel) {
|
|
@@ -835,6 +846,12 @@ async function safePublishDocument(storage, docId, options) {
|
|
|
835
846
|
await storage.writeDocument(docId, serializeDocument(cleanedNode));
|
|
836
847
|
return publishDocument(storage, docId, options);
|
|
837
848
|
}
|
|
849
|
+
function serializeNodeSafe(node) {
|
|
850
|
+
return serializeDocument({
|
|
851
|
+
...node,
|
|
852
|
+
frontmatter: stripUndefinedDeep(node.frontmatter)
|
|
853
|
+
});
|
|
854
|
+
}
|
|
838
855
|
function stripUndefinedDeep(value) {
|
|
839
856
|
if (Array.isArray(value)) {
|
|
840
857
|
return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
|
|
@@ -1248,6 +1265,297 @@ async function getPendingReview(nestId, nodeId) {
|
|
|
1248
1265
|
);
|
|
1249
1266
|
return row ? rowToReviewRequest(row) : null;
|
|
1250
1267
|
}
|
|
1268
|
+
async function deletionTargetTitle(request, titles) {
|
|
1269
|
+
if (request.targetType === "folder") return prettyFolderPath(request.nodeId);
|
|
1270
|
+
if (request.targetType === "nest") return await nestName(request.nestId);
|
|
1271
|
+
return titles?.get(request.nodeId) ?? await titleForNode(request.nestId, request.nodeId);
|
|
1272
|
+
}
|
|
1273
|
+
async function nestAdminEmails(nestId) {
|
|
1274
|
+
const db = getDb();
|
|
1275
|
+
const rows = await db.all(
|
|
1276
|
+
`SELECT u.email FROM nests n JOIN users u ON u.id = n.user_id WHERE n.id = ?
|
|
1277
|
+
UNION
|
|
1278
|
+
SELECT u.email FROM nest_collaborators nc JOIN users u ON u.id = nc.user_id
|
|
1279
|
+
WHERE nc.nest_id = ? AND nc.permission IN ('owner', 'admin')`,
|
|
1280
|
+
[nestId, nestId]
|
|
1281
|
+
);
|
|
1282
|
+
return [...new Set(rows.map((r) => r.email.toLowerCase()))];
|
|
1283
|
+
}
|
|
1284
|
+
async function inboxInsert(nestId, recipients, kind, subjectId, message) {
|
|
1285
|
+
const db = getDb();
|
|
1286
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1287
|
+
for (const email of recipients) {
|
|
1288
|
+
try {
|
|
1289
|
+
await db.run(
|
|
1290
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
1291
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1292
|
+
[uuid3(), nestId, email.toLowerCase(), kind, subjectId, message, now]
|
|
1293
|
+
);
|
|
1294
|
+
} catch (err) {
|
|
1295
|
+
console.error("[notify] deletion inbox insert failed", nestId, email, err);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async function deletionTargetContext(request, baseUrl) {
|
|
1300
|
+
if (request.targetType === "document") {
|
|
1301
|
+
const ctx = await buildDocContext(request.nestId, request.nodeId, baseUrl);
|
|
1302
|
+
return { noun: "document", ...ctx };
|
|
1303
|
+
}
|
|
1304
|
+
const name = await nestName(request.nestId);
|
|
1305
|
+
const label = request.targetType === "folder" ? prettyFolderPath(request.nodeId) : name;
|
|
1306
|
+
return {
|
|
1307
|
+
noun: request.targetType,
|
|
1308
|
+
label,
|
|
1309
|
+
docTitle: label,
|
|
1310
|
+
path: name,
|
|
1311
|
+
link: baseUrl ? `${baseUrl}/?nest=${encodeURIComponent(request.nestId)}` : ""
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
async function notifyDeletionRequested(request, baseUrl) {
|
|
1315
|
+
try {
|
|
1316
|
+
const ctx = await deletionTargetContext(request, baseUrl);
|
|
1317
|
+
const line = `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
|
|
1318
|
+
const admins = (await nestAdminEmails(request.nestId)).filter(
|
|
1319
|
+
(e) => e !== request.requestedBy.toLowerCase()
|
|
1320
|
+
);
|
|
1321
|
+
await inboxInsert(
|
|
1322
|
+
request.nestId,
|
|
1323
|
+
admins,
|
|
1324
|
+
"deletion_requested",
|
|
1325
|
+
request.id,
|
|
1326
|
+
`${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1327
|
+
);
|
|
1328
|
+
void dispatchEvent({
|
|
1329
|
+
kind: "deletion_requested",
|
|
1330
|
+
nestId: request.nestId,
|
|
1331
|
+
subjectId: request.nodeId,
|
|
1332
|
+
actor: request.requestedBy,
|
|
1333
|
+
message: line
|
|
1334
|
+
});
|
|
1335
|
+
void notifyNestEvent(
|
|
1336
|
+
request.nestId,
|
|
1337
|
+
`:wastebasket: ${line}`,
|
|
1338
|
+
{
|
|
1339
|
+
status: "deletion_requested",
|
|
1340
|
+
docTitle: ctx.docTitle,
|
|
1341
|
+
path: ctx.path,
|
|
1342
|
+
link: ctx.link,
|
|
1343
|
+
actor: request.requestedBy,
|
|
1344
|
+
note: request.reason
|
|
1345
|
+
},
|
|
1346
|
+
// The admins are emailed directly below; don't also hit the shared
|
|
1347
|
+
// NOTIFY_EMAIL_TO team inbox with the same event.
|
|
1348
|
+
{ skipEmail: true }
|
|
1349
|
+
);
|
|
1350
|
+
if (admins.length > 0) {
|
|
1351
|
+
void sendEmailToRecipient(admins, {
|
|
1352
|
+
status: "deletion_requested",
|
|
1353
|
+
docTitle: ctx.docTitle,
|
|
1354
|
+
path: ctx.path,
|
|
1355
|
+
link: ctx.link,
|
|
1356
|
+
actor: request.requestedBy,
|
|
1357
|
+
note: request.reason
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
console.error(
|
|
1362
|
+
"[notify] deletion request fan-out failed",
|
|
1363
|
+
request.nestId,
|
|
1364
|
+
request.nodeId,
|
|
1365
|
+
err
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
async function notifyDeletionResolved(params) {
|
|
1370
|
+
try {
|
|
1371
|
+
const { outcome, resolvedBy, note } = params;
|
|
1372
|
+
const verb = outcome === "deleted" ? "deleted" : "declined the deletion of";
|
|
1373
|
+
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1374
|
+
const recipient = params.requestedBy.toLowerCase();
|
|
1375
|
+
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
1376
|
+
if (canInbox && recipient !== resolvedBy.toLowerCase()) {
|
|
1377
|
+
await inboxInsert(
|
|
1378
|
+
params.nestId,
|
|
1379
|
+
[recipient],
|
|
1380
|
+
`deletion_${outcome}`,
|
|
1381
|
+
params.requestId,
|
|
1382
|
+
outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was declined by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
void dispatchEvent({
|
|
1386
|
+
kind: `deletion_${outcome}`,
|
|
1387
|
+
nestId: params.nestId,
|
|
1388
|
+
subjectId: params.nodeId,
|
|
1389
|
+
actor: resolvedBy,
|
|
1390
|
+
message: line
|
|
1391
|
+
});
|
|
1392
|
+
void notifyNestEvent(
|
|
1393
|
+
params.nestId,
|
|
1394
|
+
`${outcome === "deleted" ? ":wastebasket:" : ":no_entry_sign:"} ${line}`,
|
|
1395
|
+
void 0,
|
|
1396
|
+
// Verdict emails the requester directly (below), not the team inbox.
|
|
1397
|
+
{ skipEmail: true }
|
|
1398
|
+
);
|
|
1399
|
+
if (recipient !== resolvedBy.toLowerCase()) {
|
|
1400
|
+
const link = params.baseUrl ? `${params.baseUrl}/?nest=${encodeURIComponent(params.nestId)}` : "";
|
|
1401
|
+
void sendEmailToRecipient(recipient, {
|
|
1402
|
+
status: outcome === "deleted" ? "deletion_deleted" : "deletion_declined",
|
|
1403
|
+
docTitle: params.docTitle,
|
|
1404
|
+
path: params.nestId,
|
|
1405
|
+
link: outcome === "deleted" ? link : void 0,
|
|
1406
|
+
actor: resolvedBy,
|
|
1407
|
+
note: note || null
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
console.error(
|
|
1412
|
+
"[notify] deletion verdict fan-out failed",
|
|
1413
|
+
params.nestId,
|
|
1414
|
+
params.nodeId,
|
|
1415
|
+
err
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async function requestDeletion(params) {
|
|
1420
|
+
const db = getDb();
|
|
1421
|
+
const targetType = params.targetType || "document";
|
|
1422
|
+
const noun = targetType === "document" ? "node" : targetType === "folder" ? "folder" : "nest";
|
|
1423
|
+
const existing = await db.get(
|
|
1424
|
+
"SELECT id FROM deletion_requests WHERE nest_id = ? AND node_id = ? AND target_type = ? AND status = 'pending'",
|
|
1425
|
+
[params.nestId, params.nodeId, targetType]
|
|
1426
|
+
);
|
|
1427
|
+
if (existing) {
|
|
1428
|
+
throw new ConflictError(`A deletion request is already pending for this ${noun}`);
|
|
1429
|
+
}
|
|
1430
|
+
const id = uuid3();
|
|
1431
|
+
try {
|
|
1432
|
+
await db.run(
|
|
1433
|
+
`INSERT INTO deletion_requests (id, nest_id, node_id, target_type, requested_by, reason)
|
|
1434
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1435
|
+
[
|
|
1436
|
+
id,
|
|
1437
|
+
params.nestId,
|
|
1438
|
+
params.nodeId,
|
|
1439
|
+
targetType,
|
|
1440
|
+
params.requestedBy,
|
|
1441
|
+
params.reason
|
|
1442
|
+
]
|
|
1443
|
+
);
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
console.error(
|
|
1446
|
+
"[deletion] request insert failed",
|
|
1447
|
+
params.nestId,
|
|
1448
|
+
params.nodeId,
|
|
1449
|
+
err
|
|
1450
|
+
);
|
|
1451
|
+
throw new ConflictError(`A deletion request is already pending for this ${noun}`);
|
|
1452
|
+
}
|
|
1453
|
+
const request = await getDeletionRequest(id);
|
|
1454
|
+
await notifyDeletionRequested(request, params.baseUrl);
|
|
1455
|
+
return request;
|
|
1456
|
+
}
|
|
1457
|
+
async function getLatestDeletionRequest(nestId, nodeId) {
|
|
1458
|
+
const db = getDb();
|
|
1459
|
+
const row = await db.get(
|
|
1460
|
+
`SELECT * FROM deletion_requests WHERE nest_id = ? AND node_id = ?
|
|
1461
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
1462
|
+
[nestId, nodeId]
|
|
1463
|
+
);
|
|
1464
|
+
return row ? rowToDeletionRequest(row) : null;
|
|
1465
|
+
}
|
|
1466
|
+
async function getDeletionRequest(id) {
|
|
1467
|
+
const db = getDb();
|
|
1468
|
+
const row = await db.get(
|
|
1469
|
+
"SELECT * FROM deletion_requests WHERE id = ?",
|
|
1470
|
+
[id]
|
|
1471
|
+
);
|
|
1472
|
+
return row ? rowToDeletionRequest(row) : null;
|
|
1473
|
+
}
|
|
1474
|
+
async function listDeletionRequests(params) {
|
|
1475
|
+
const db = getDb();
|
|
1476
|
+
const status = params.status || "pending";
|
|
1477
|
+
const where = [];
|
|
1478
|
+
const args = [];
|
|
1479
|
+
if (params.nestId) {
|
|
1480
|
+
where.push("nest_id = ?");
|
|
1481
|
+
args.push(params.nestId);
|
|
1482
|
+
}
|
|
1483
|
+
if (params.nestIds) {
|
|
1484
|
+
if (params.nestIds.length === 0) return [];
|
|
1485
|
+
where.push(`nest_id IN (${params.nestIds.map(() => "?").join(",")})`);
|
|
1486
|
+
args.push(...params.nestIds);
|
|
1487
|
+
}
|
|
1488
|
+
if (status !== "all") {
|
|
1489
|
+
where.push("status = ?");
|
|
1490
|
+
args.push(status);
|
|
1491
|
+
}
|
|
1492
|
+
const rows = await db.all(
|
|
1493
|
+
`SELECT * FROM deletion_requests${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`,
|
|
1494
|
+
args
|
|
1495
|
+
);
|
|
1496
|
+
if (rows.length === 0) return [];
|
|
1497
|
+
const titlesByNest = /* @__PURE__ */ new Map();
|
|
1498
|
+
for (const nid of new Set(rows.map((r) => r.nest_id))) {
|
|
1499
|
+
titlesByNest.set(nid, await buildTitleMap(nid));
|
|
1500
|
+
}
|
|
1501
|
+
const out = [];
|
|
1502
|
+
for (const r of rows) {
|
|
1503
|
+
const request = rowToDeletionRequest(r);
|
|
1504
|
+
out.push({
|
|
1505
|
+
...request,
|
|
1506
|
+
title: await deletionTargetTitle(request, titlesByNest.get(r.nest_id))
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
return out;
|
|
1510
|
+
}
|
|
1511
|
+
async function dropDeletionRequest(id) {
|
|
1512
|
+
await getDb().run("DELETE FROM deletion_requests WHERE id = ?", [id]);
|
|
1513
|
+
}
|
|
1514
|
+
async function declineDeletion(params) {
|
|
1515
|
+
const db = getDb();
|
|
1516
|
+
const pending = await getDeletionRequest(params.id);
|
|
1517
|
+
if (!pending || pending.nestId !== params.nestId || pending.status !== "pending") {
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
const { changes } = await db.run(
|
|
1521
|
+
`UPDATE deletion_requests
|
|
1522
|
+
SET status = 'declined', resolved_by = ?, resolved_at = ${nowExpr(db)},
|
|
1523
|
+
resolution_note = ?
|
|
1524
|
+
WHERE id = ? AND status = 'pending'`,
|
|
1525
|
+
[params.declinedBy, params.note, params.id]
|
|
1526
|
+
);
|
|
1527
|
+
if (changes === 0) return null;
|
|
1528
|
+
const declined = await getDeletionRequest(params.id);
|
|
1529
|
+
if (declined) {
|
|
1530
|
+
await notifyDeletionResolved({
|
|
1531
|
+
nestId: params.nestId,
|
|
1532
|
+
nodeId: declined.nodeId,
|
|
1533
|
+
docTitle: await deletionTargetTitle(declined),
|
|
1534
|
+
outcome: "declined",
|
|
1535
|
+
requestedBy: declined.requestedBy,
|
|
1536
|
+
resolvedBy: params.declinedBy,
|
|
1537
|
+
requestId: declined.id,
|
|
1538
|
+
note: params.note,
|
|
1539
|
+
baseUrl: params.baseUrl
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return declined;
|
|
1543
|
+
}
|
|
1544
|
+
function rowToDeletionRequest(row) {
|
|
1545
|
+
return {
|
|
1546
|
+
id: row.id,
|
|
1547
|
+
nestId: row.nest_id,
|
|
1548
|
+
nodeId: row.node_id,
|
|
1549
|
+
targetType: row.target_type || "document",
|
|
1550
|
+
requestedBy: row.requested_by,
|
|
1551
|
+
reason: row.reason,
|
|
1552
|
+
status: row.status,
|
|
1553
|
+
createdAt: row.created_at,
|
|
1554
|
+
resolvedBy: row.resolved_by || void 0,
|
|
1555
|
+
resolvedAt: row.resolved_at || void 0,
|
|
1556
|
+
resolutionNote: row.resolution_note || void 0
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1251
1559
|
async function getReviewRequest(id) {
|
|
1252
1560
|
const db = getDb();
|
|
1253
1561
|
const row = await db.get(
|
|
@@ -1275,6 +1583,7 @@ function rowToReviewRequest(row) {
|
|
|
1275
1583
|
|
|
1276
1584
|
export {
|
|
1277
1585
|
safePublishDocument,
|
|
1586
|
+
serializeNodeSafe,
|
|
1278
1587
|
stripUndefinedDeep,
|
|
1279
1588
|
listWatchers,
|
|
1280
1589
|
addWatcher,
|
|
@@ -1306,5 +1615,13 @@ export {
|
|
|
1306
1615
|
cancelReview,
|
|
1307
1616
|
getReviewQueue,
|
|
1308
1617
|
getReviewHistory,
|
|
1309
|
-
getPendingReview
|
|
1618
|
+
getPendingReview,
|
|
1619
|
+
deletionTargetTitle,
|
|
1620
|
+
notifyDeletionResolved,
|
|
1621
|
+
requestDeletion,
|
|
1622
|
+
getLatestDeletionRequest,
|
|
1623
|
+
getDeletionRequest,
|
|
1624
|
+
listDeletionRequests,
|
|
1625
|
+
dropDeletionRequest,
|
|
1626
|
+
declineDeletion
|
|
1310
1627
|
};
|