@promptowl/contextnest-community 1.14.0 → 1.16.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 +15 -0
- package/dist/chunk-CLKZZZS2.js +624 -0
- package/dist/{chunk-JP67WXLA.js → chunk-D6HICFVW.js} +77 -3
- package/dist/{chunk-I5KYGMIT.js → chunk-JMJLNEXE.js} +78 -1
- package/dist/{chunk-KPK656BH.js → chunk-L7UCMGWZ.js} +478 -924
- package/dist/{chunk-IHGPZ6NY.js → chunk-OTGZQZKK.js} +1 -1
- package/dist/{chunk-MZ4IR5KH.js → chunk-QJVRZT6M.js} +356 -76
- package/dist/{client-3MGIP57D.js → client-OBS5HOTA.js} +1 -1
- package/dist/{grants-service-5ZFISCBH.js → grants-service-I535QVTW.js} +2 -2
- package/dist/index.js +2419 -1061
- package/dist/{migrations.postgres-BRXZY2GE.js → migrations.postgres-CYKO5CFV.js} +41 -1
- package/dist/review-service-V2GR6VM3.js +47 -0
- package/dist/{stewardship-service-G7IF7ZNP.js → stewardship-service-LTREGVSJ.js} +4 -4
- package/dist/{version-service-FGL47APL.js → version-service-V6K5SZ3P.js} +11 -3
- package/dist/web3/assets/index-DnQARUL3.css +1 -0
- package/dist/web3/assets/index-_m7WyozM.js +1051 -0
- package/dist/web3/index.html +2 -2
- package/package.json +4 -3
- 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
|
@@ -2,39 +2,45 @@ import {
|
|
|
2
2
|
grantCoversNode,
|
|
3
3
|
listUserGrants,
|
|
4
4
|
resolveNodeGrant
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OTGZQZKK.js";
|
|
6
6
|
import {
|
|
7
7
|
createVersion,
|
|
8
8
|
getApprovedVersion,
|
|
9
|
+
getApprovedVersions,
|
|
9
10
|
getCurrentVersion,
|
|
10
11
|
setApprovedVersion
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-D6HICFVW.js";
|
|
12
13
|
import {
|
|
13
|
-
buildDocContext,
|
|
14
|
-
buildTitleMap,
|
|
15
14
|
canUserAccess,
|
|
16
15
|
canUserApprove,
|
|
16
|
+
resolveStewardsForNode
|
|
17
|
+
} from "./chunk-CLKZZZS2.js";
|
|
18
|
+
import {
|
|
19
|
+
buildDocContext,
|
|
20
|
+
buildTitleMap,
|
|
17
21
|
describeNode,
|
|
22
|
+
engineApi,
|
|
18
23
|
engineCache,
|
|
24
|
+
insertOrReplace,
|
|
19
25
|
isPublicReader,
|
|
20
26
|
isStewardshipEnabled,
|
|
27
|
+
nestName,
|
|
21
28
|
notifyEmailForNest,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
nowExpr,
|
|
30
|
+
opContext,
|
|
31
|
+
prettyFolderPath,
|
|
32
|
+
sendEmailToRecipient,
|
|
33
|
+
titleForNode
|
|
34
|
+
} from "./chunk-L7UCMGWZ.js";
|
|
25
35
|
import {
|
|
26
36
|
ConflictError,
|
|
27
37
|
NotFoundError,
|
|
28
38
|
ValidationError
|
|
29
39
|
} from "./chunk-GUNJTORH.js";
|
|
30
|
-
import {
|
|
31
|
-
insertOrReplace,
|
|
32
|
-
nowExpr
|
|
33
|
-
} from "./chunk-XQ46F76G.js";
|
|
34
40
|
import {
|
|
35
41
|
config,
|
|
36
42
|
getDb
|
|
37
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-JMJLNEXE.js";
|
|
38
44
|
|
|
39
45
|
// src/governance/review-service.ts
|
|
40
46
|
import { v4 as uuid3 } from "uuid";
|
|
@@ -83,6 +89,7 @@ async function notifyReviewRequested(params) {
|
|
|
83
89
|
[nestId, nodeId, nestId, nodeId]
|
|
84
90
|
);
|
|
85
91
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
92
|
+
const label = await describeNode(nestId, nodeId);
|
|
86
93
|
let created = 0;
|
|
87
94
|
for (const r of rows) {
|
|
88
95
|
if (r.user_email.toLowerCase() === requestedBy.toLowerCase()) continue;
|
|
@@ -94,7 +101,7 @@ async function notifyReviewRequested(params) {
|
|
|
94
101
|
nestId,
|
|
95
102
|
r.user_email.toLowerCase(),
|
|
96
103
|
reviewId,
|
|
97
|
-
r.via === nodeId ? `"${
|
|
104
|
+
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
105
|
now
|
|
99
106
|
]
|
|
100
107
|
);
|
|
@@ -126,6 +133,7 @@ async function notifyReviewResolved(params) {
|
|
|
126
133
|
recipients.add(requestedBy.toLowerCase());
|
|
127
134
|
recipients.delete(resolvedBy.toLowerCase());
|
|
128
135
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
136
|
+
const label = await describeNode(nestId, nodeId);
|
|
129
137
|
let created = 0;
|
|
130
138
|
for (const email of recipients) {
|
|
131
139
|
await db.run(
|
|
@@ -137,7 +145,7 @@ async function notifyReviewResolved(params) {
|
|
|
137
145
|
email,
|
|
138
146
|
`review_${status}`,
|
|
139
147
|
reviewId,
|
|
140
|
-
`"${
|
|
148
|
+
`"${label}" was ${status} by ${resolvedBy}`,
|
|
141
149
|
now
|
|
142
150
|
]
|
|
143
151
|
);
|
|
@@ -150,7 +158,7 @@ async function notifyReviewResolved(params) {
|
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
async function notifyNestInvite(params) {
|
|
153
|
-
const { nestId, inviteeEmail, permission, nestName } = params;
|
|
161
|
+
const { nestId, inviteeEmail, permission, nestName: nestName2 } = params;
|
|
154
162
|
try {
|
|
155
163
|
await getDb().run(
|
|
156
164
|
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
@@ -160,7 +168,7 @@ async function notifyNestInvite(params) {
|
|
|
160
168
|
nestId,
|
|
161
169
|
inviteeEmail.toLowerCase(),
|
|
162
170
|
nestId,
|
|
163
|
-
`You were added to "${
|
|
171
|
+
`You were added to "${nestName2}" as ${permission}`,
|
|
164
172
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
165
173
|
]
|
|
166
174
|
);
|
|
@@ -217,15 +225,10 @@ async function canReadNode(nestId, nodeId, userId, userEmail) {
|
|
|
217
225
|
if ((await canUserAccess(nestId, nodeId, userEmail)).allowed) return true;
|
|
218
226
|
return await resolveNodeGrant(nestId, userId, nodeId) !== null;
|
|
219
227
|
}
|
|
220
|
-
async function filterAccessible(nestId, userId, userEmail, nodes) {
|
|
228
|
+
async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersions) {
|
|
221
229
|
if (await isPublicReader(nestId, userId)) {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
if (await getApprovedVersion(nestId, n.id) !== null) {
|
|
225
|
-
filtered.push(n);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return filtered;
|
|
230
|
+
const approved = approvedVersions ?? await getApprovedVersions(nestId);
|
|
231
|
+
return nodes.filter((n) => approved.has(n.id));
|
|
229
232
|
}
|
|
230
233
|
if (!await isStewardshipEnabled(nestId)) return nodes;
|
|
231
234
|
const grants = await listUserGrants(nestId, userId);
|
|
@@ -628,8 +631,8 @@ function msTeamsPayload(title, message) {
|
|
|
628
631
|
}
|
|
629
632
|
async function notifyMsTeamsForNest(nestId, message) {
|
|
630
633
|
if (!config.MSTEAMS_WEBHOOK_URL) return;
|
|
631
|
-
const
|
|
632
|
-
return notifyMsTeams(
|
|
634
|
+
const nestName2 = await resolveNestName(nestId);
|
|
635
|
+
return notifyMsTeams(nestName2, message);
|
|
633
636
|
}
|
|
634
637
|
async function notifyMsTeams(title, message) {
|
|
635
638
|
const url = config.MSTEAMS_WEBHOOK_URL;
|
|
@@ -640,8 +643,8 @@ async function notifyMsTeams(title, message) {
|
|
|
640
643
|
// src/notify/slack.ts
|
|
641
644
|
async function notifySlackForNest(nestId, message) {
|
|
642
645
|
if (!config.SLACK_WEBHOOK_URL) return;
|
|
643
|
-
const
|
|
644
|
-
return notifySlack(`*[${
|
|
646
|
+
const nestName2 = await resolveNestName(nestId);
|
|
647
|
+
return notifySlack(`*[${nestName2}]* ${message}`);
|
|
645
648
|
}
|
|
646
649
|
function escapeMrkdwn(s) {
|
|
647
650
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -657,25 +660,31 @@ var EVENT_EMOJI = {
|
|
|
657
660
|
review_requested: ":memo:",
|
|
658
661
|
review_approved: ":white_check_mark:",
|
|
659
662
|
review_rejected: ":x:",
|
|
660
|
-
run_failed: ":rotating_light:"
|
|
663
|
+
run_failed: ":rotating_light:",
|
|
664
|
+
deletion_requested: ":wastebasket:",
|
|
665
|
+
deletion_deleted: ":wastebasket:",
|
|
666
|
+
deletion_declined: ":no_entry_sign:"
|
|
661
667
|
};
|
|
662
668
|
var DIGEST_DELIVERED_KINDS = /* @__PURE__ */ new Set([
|
|
663
669
|
"review_requested",
|
|
664
670
|
"review_approved",
|
|
665
|
-
"review_rejected"
|
|
671
|
+
"review_rejected",
|
|
672
|
+
"deletion_requested",
|
|
673
|
+
"deletion_deleted",
|
|
674
|
+
"deletion_declined"
|
|
666
675
|
]);
|
|
667
|
-
function formatBody(channel,
|
|
668
|
-
const line = `[${
|
|
676
|
+
function formatBody(channel, nestName2, ev) {
|
|
677
|
+
const line = `[${nestName2}] ${ev.message}`;
|
|
669
678
|
if (channel === "slack") {
|
|
670
|
-
return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${
|
|
679
|
+
return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${nestName2}]* ${ev.message}`) };
|
|
671
680
|
}
|
|
672
681
|
if (channel === "teams") {
|
|
673
|
-
return msTeamsPayload(
|
|
682
|
+
return msTeamsPayload(nestName2, `${EVENT_EMOJI[ev.kind] ?? ""} ${ev.message}`.trim());
|
|
674
683
|
}
|
|
675
684
|
return {
|
|
676
685
|
kind: ev.kind,
|
|
677
686
|
nest_id: ev.nestId,
|
|
678
|
-
nest_name:
|
|
687
|
+
nest_name: nestName2,
|
|
679
688
|
message: ev.message,
|
|
680
689
|
subject_id: ev.subjectId ?? null,
|
|
681
690
|
actor: ev.actor ?? null,
|
|
@@ -716,18 +725,18 @@ async function resolveUrl(nestId, raw) {
|
|
|
716
725
|
}
|
|
717
726
|
async function dispatchEvent(ev) {
|
|
718
727
|
const db = getDb();
|
|
719
|
-
const
|
|
728
|
+
const nestName2 = await resolveNestName(ev.nestId);
|
|
720
729
|
if (config.SLACK_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
|
|
721
730
|
void postWebhook(
|
|
722
731
|
config.SLACK_WEBHOOK_URL,
|
|
723
|
-
formatBody("slack",
|
|
732
|
+
formatBody("slack", nestName2, ev),
|
|
724
733
|
"server-slack"
|
|
725
734
|
);
|
|
726
735
|
}
|
|
727
736
|
if (config.MSTEAMS_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
|
|
728
737
|
void postWebhook(
|
|
729
738
|
config.MSTEAMS_WEBHOOK_URL,
|
|
730
|
-
formatBody("teams",
|
|
739
|
+
formatBody("teams", nestName2, ev),
|
|
731
740
|
"server-msteams"
|
|
732
741
|
);
|
|
733
742
|
}
|
|
@@ -749,7 +758,7 @@ async function dispatchEvent(ev) {
|
|
|
749
758
|
if (!kinds.includes("*") && !kinds.includes(ev.kind)) continue;
|
|
750
759
|
const url = await resolveUrl(ev.nestId, r.url);
|
|
751
760
|
if (!url) continue;
|
|
752
|
-
void postWebhook(url, formatBody(r.channel,
|
|
761
|
+
void postWebhook(url, formatBody(r.channel, nestName2, ev), `connector:${r.id}`);
|
|
753
762
|
}
|
|
754
763
|
}
|
|
755
764
|
async function sendTestNotification(channel) {
|
|
@@ -823,33 +832,6 @@ function withNodeWriteLock(key, fn) {
|
|
|
823
832
|
return run;
|
|
824
833
|
}
|
|
825
834
|
|
|
826
|
-
// src/governance/safe-publish.ts
|
|
827
|
-
import {
|
|
828
|
-
publishDocument,
|
|
829
|
-
serializeDocument
|
|
830
|
-
} from "@promptowl/contextnest-engine";
|
|
831
|
-
async function safePublishDocument(storage, docId, options) {
|
|
832
|
-
const node = await storage.readDocument(docId);
|
|
833
|
-
const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
|
|
834
|
-
const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
|
|
835
|
-
await storage.writeDocument(docId, serializeDocument(cleanedNode));
|
|
836
|
-
return publishDocument(storage, docId, options);
|
|
837
|
-
}
|
|
838
|
-
function stripUndefinedDeep(value) {
|
|
839
|
-
if (Array.isArray(value)) {
|
|
840
|
-
return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
|
|
841
|
-
}
|
|
842
|
-
if (value && typeof value === "object") {
|
|
843
|
-
const out = {};
|
|
844
|
-
for (const [k, v] of Object.entries(value)) {
|
|
845
|
-
if (v === void 0) continue;
|
|
846
|
-
out[k] = stripUndefinedDeep(v);
|
|
847
|
-
}
|
|
848
|
-
return out;
|
|
849
|
-
}
|
|
850
|
-
return value;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
835
|
// src/governance/review-service.ts
|
|
854
836
|
async function submitForReview(params) {
|
|
855
837
|
const db = getDb();
|
|
@@ -1015,13 +997,14 @@ async function approve(params) {
|
|
|
1015
997
|
);
|
|
1016
998
|
try {
|
|
1017
999
|
const { storage } = await engineCache.get(params.nestId);
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1000
|
+
const note = params.note || `Approved review request ${pending.id}`;
|
|
1001
|
+
const { version: engineVersion } = await engineApi.run(
|
|
1002
|
+
"context_publish",
|
|
1003
|
+
{ id: params.nodeId, note },
|
|
1004
|
+
await opContext(params.nestId, params.approvedBy)
|
|
1005
|
+
);
|
|
1023
1006
|
if (engineVersion !== version) {
|
|
1024
|
-
const node =
|
|
1007
|
+
const node = await storage.readDocument(params.nodeId);
|
|
1025
1008
|
const tags = node.frontmatter.tags || [];
|
|
1026
1009
|
await createVersion({
|
|
1027
1010
|
nestId: params.nestId,
|
|
@@ -1031,7 +1014,7 @@ async function approve(params) {
|
|
|
1031
1014
|
author: params.approvedBy,
|
|
1032
1015
|
status: "published",
|
|
1033
1016
|
tags,
|
|
1034
|
-
changeNote:
|
|
1017
|
+
changeNote: note
|
|
1035
1018
|
});
|
|
1036
1019
|
await setApprovedVersion(
|
|
1037
1020
|
params.nestId,
|
|
@@ -1248,6 +1231,297 @@ async function getPendingReview(nestId, nodeId) {
|
|
|
1248
1231
|
);
|
|
1249
1232
|
return row ? rowToReviewRequest(row) : null;
|
|
1250
1233
|
}
|
|
1234
|
+
async function deletionTargetTitle(request, titles) {
|
|
1235
|
+
if (request.targetType === "folder") return prettyFolderPath(request.nodeId);
|
|
1236
|
+
if (request.targetType === "nest") return await nestName(request.nestId);
|
|
1237
|
+
return titles?.get(request.nodeId) ?? await titleForNode(request.nestId, request.nodeId);
|
|
1238
|
+
}
|
|
1239
|
+
async function nestAdminEmails(nestId) {
|
|
1240
|
+
const db = getDb();
|
|
1241
|
+
const rows = await db.all(
|
|
1242
|
+
`SELECT u.email FROM nests n JOIN users u ON u.id = n.user_id WHERE n.id = ?
|
|
1243
|
+
UNION
|
|
1244
|
+
SELECT u.email FROM nest_collaborators nc JOIN users u ON u.id = nc.user_id
|
|
1245
|
+
WHERE nc.nest_id = ? AND nc.permission IN ('owner', 'admin')`,
|
|
1246
|
+
[nestId, nestId]
|
|
1247
|
+
);
|
|
1248
|
+
return [...new Set(rows.map((r) => r.email.toLowerCase()))];
|
|
1249
|
+
}
|
|
1250
|
+
async function inboxInsert(nestId, recipients, kind, subjectId, message) {
|
|
1251
|
+
const db = getDb();
|
|
1252
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1253
|
+
for (const email of recipients) {
|
|
1254
|
+
try {
|
|
1255
|
+
await db.run(
|
|
1256
|
+
`INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
|
|
1257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1258
|
+
[uuid3(), nestId, email.toLowerCase(), kind, subjectId, message, now]
|
|
1259
|
+
);
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
console.error("[notify] deletion inbox insert failed", nestId, email, err);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
async function deletionTargetContext(request, baseUrl) {
|
|
1266
|
+
if (request.targetType === "document") {
|
|
1267
|
+
const ctx = await buildDocContext(request.nestId, request.nodeId, baseUrl);
|
|
1268
|
+
return { noun: "document", ...ctx };
|
|
1269
|
+
}
|
|
1270
|
+
const name = await nestName(request.nestId);
|
|
1271
|
+
const label = request.targetType === "folder" ? prettyFolderPath(request.nodeId) : name;
|
|
1272
|
+
return {
|
|
1273
|
+
noun: request.targetType,
|
|
1274
|
+
label,
|
|
1275
|
+
docTitle: label,
|
|
1276
|
+
path: name,
|
|
1277
|
+
link: baseUrl ? `${baseUrl}/?nest=${encodeURIComponent(request.nestId)}` : ""
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
async function notifyDeletionRequested(request, baseUrl) {
|
|
1281
|
+
try {
|
|
1282
|
+
const ctx = await deletionTargetContext(request, baseUrl);
|
|
1283
|
+
const line = `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
|
|
1284
|
+
const admins = (await nestAdminEmails(request.nestId)).filter(
|
|
1285
|
+
(e) => e !== request.requestedBy.toLowerCase()
|
|
1286
|
+
);
|
|
1287
|
+
await inboxInsert(
|
|
1288
|
+
request.nestId,
|
|
1289
|
+
admins,
|
|
1290
|
+
"deletion_requested",
|
|
1291
|
+
request.id,
|
|
1292
|
+
`${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
|
|
1293
|
+
);
|
|
1294
|
+
void dispatchEvent({
|
|
1295
|
+
kind: "deletion_requested",
|
|
1296
|
+
nestId: request.nestId,
|
|
1297
|
+
subjectId: request.nodeId,
|
|
1298
|
+
actor: request.requestedBy,
|
|
1299
|
+
message: line
|
|
1300
|
+
});
|
|
1301
|
+
void notifyNestEvent(
|
|
1302
|
+
request.nestId,
|
|
1303
|
+
`:wastebasket: ${line}`,
|
|
1304
|
+
{
|
|
1305
|
+
status: "deletion_requested",
|
|
1306
|
+
docTitle: ctx.docTitle,
|
|
1307
|
+
path: ctx.path,
|
|
1308
|
+
link: ctx.link,
|
|
1309
|
+
actor: request.requestedBy,
|
|
1310
|
+
note: request.reason
|
|
1311
|
+
},
|
|
1312
|
+
// The admins are emailed directly below; don't also hit the shared
|
|
1313
|
+
// NOTIFY_EMAIL_TO team inbox with the same event.
|
|
1314
|
+
{ skipEmail: true }
|
|
1315
|
+
);
|
|
1316
|
+
if (admins.length > 0) {
|
|
1317
|
+
void sendEmailToRecipient(admins, {
|
|
1318
|
+
status: "deletion_requested",
|
|
1319
|
+
docTitle: ctx.docTitle,
|
|
1320
|
+
path: ctx.path,
|
|
1321
|
+
link: ctx.link,
|
|
1322
|
+
actor: request.requestedBy,
|
|
1323
|
+
note: request.reason
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
console.error(
|
|
1328
|
+
"[notify] deletion request fan-out failed",
|
|
1329
|
+
request.nestId,
|
|
1330
|
+
request.nodeId,
|
|
1331
|
+
err
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function notifyDeletionResolved(params) {
|
|
1336
|
+
try {
|
|
1337
|
+
const { outcome, resolvedBy, note } = params;
|
|
1338
|
+
const verb = outcome === "deleted" ? "deleted" : "rejected the deletion of";
|
|
1339
|
+
const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
|
|
1340
|
+
const recipient = params.requestedBy.toLowerCase();
|
|
1341
|
+
const canInbox = !(params.targetType === "nest" && outcome === "deleted");
|
|
1342
|
+
if (canInbox && recipient !== resolvedBy.toLowerCase()) {
|
|
1343
|
+
await inboxInsert(
|
|
1344
|
+
params.nestId,
|
|
1345
|
+
[recipient],
|
|
1346
|
+
`deletion_${outcome}`,
|
|
1347
|
+
params.requestId,
|
|
1348
|
+
outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
void dispatchEvent({
|
|
1352
|
+
kind: `deletion_${outcome}`,
|
|
1353
|
+
nestId: params.nestId,
|
|
1354
|
+
subjectId: params.nodeId,
|
|
1355
|
+
actor: resolvedBy,
|
|
1356
|
+
message: line
|
|
1357
|
+
});
|
|
1358
|
+
void notifyNestEvent(
|
|
1359
|
+
params.nestId,
|
|
1360
|
+
`${outcome === "deleted" ? ":wastebasket:" : ":no_entry_sign:"} ${line}`,
|
|
1361
|
+
void 0,
|
|
1362
|
+
// Verdict emails the requester directly (below), not the team inbox.
|
|
1363
|
+
{ skipEmail: true }
|
|
1364
|
+
);
|
|
1365
|
+
if (recipient !== resolvedBy.toLowerCase()) {
|
|
1366
|
+
const link = params.baseUrl ? `${params.baseUrl}/?nest=${encodeURIComponent(params.nestId)}` : "";
|
|
1367
|
+
void sendEmailToRecipient(recipient, {
|
|
1368
|
+
status: outcome === "deleted" ? "deletion_deleted" : "deletion_declined",
|
|
1369
|
+
docTitle: params.docTitle,
|
|
1370
|
+
path: params.nestId,
|
|
1371
|
+
link: outcome === "deleted" ? link : void 0,
|
|
1372
|
+
actor: resolvedBy,
|
|
1373
|
+
note: note || null
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
console.error(
|
|
1378
|
+
"[notify] deletion verdict fan-out failed",
|
|
1379
|
+
params.nestId,
|
|
1380
|
+
params.nodeId,
|
|
1381
|
+
err
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function requestDeletion(params) {
|
|
1386
|
+
const db = getDb();
|
|
1387
|
+
const targetType = params.targetType || "document";
|
|
1388
|
+
const noun = targetType === "document" ? "node" : targetType === "folder" ? "folder" : "nest";
|
|
1389
|
+
const existing = await db.get(
|
|
1390
|
+
"SELECT id FROM deletion_requests WHERE nest_id = ? AND node_id = ? AND target_type = ? AND status = 'pending'",
|
|
1391
|
+
[params.nestId, params.nodeId, targetType]
|
|
1392
|
+
);
|
|
1393
|
+
if (existing) {
|
|
1394
|
+
throw new ConflictError(`A deletion request is already pending for this ${noun}`);
|
|
1395
|
+
}
|
|
1396
|
+
const id = uuid3();
|
|
1397
|
+
try {
|
|
1398
|
+
await db.run(
|
|
1399
|
+
`INSERT INTO deletion_requests (id, nest_id, node_id, target_type, requested_by, reason)
|
|
1400
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
1401
|
+
[
|
|
1402
|
+
id,
|
|
1403
|
+
params.nestId,
|
|
1404
|
+
params.nodeId,
|
|
1405
|
+
targetType,
|
|
1406
|
+
params.requestedBy,
|
|
1407
|
+
params.reason
|
|
1408
|
+
]
|
|
1409
|
+
);
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
console.error(
|
|
1412
|
+
"[deletion] request insert failed",
|
|
1413
|
+
params.nestId,
|
|
1414
|
+
params.nodeId,
|
|
1415
|
+
err
|
|
1416
|
+
);
|
|
1417
|
+
throw new ConflictError(`A deletion request is already pending for this ${noun}`);
|
|
1418
|
+
}
|
|
1419
|
+
const request = await getDeletionRequest(id);
|
|
1420
|
+
await notifyDeletionRequested(request, params.baseUrl);
|
|
1421
|
+
return request;
|
|
1422
|
+
}
|
|
1423
|
+
async function getLatestDeletionRequest(nestId, nodeId) {
|
|
1424
|
+
const db = getDb();
|
|
1425
|
+
const row = await db.get(
|
|
1426
|
+
`SELECT * FROM deletion_requests WHERE nest_id = ? AND node_id = ?
|
|
1427
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
1428
|
+
[nestId, nodeId]
|
|
1429
|
+
);
|
|
1430
|
+
return row ? rowToDeletionRequest(row) : null;
|
|
1431
|
+
}
|
|
1432
|
+
async function getDeletionRequest(id) {
|
|
1433
|
+
const db = getDb();
|
|
1434
|
+
const row = await db.get(
|
|
1435
|
+
"SELECT * FROM deletion_requests WHERE id = ?",
|
|
1436
|
+
[id]
|
|
1437
|
+
);
|
|
1438
|
+
return row ? rowToDeletionRequest(row) : null;
|
|
1439
|
+
}
|
|
1440
|
+
async function listDeletionRequests(params) {
|
|
1441
|
+
const db = getDb();
|
|
1442
|
+
const status = params.status || "pending";
|
|
1443
|
+
const where = [];
|
|
1444
|
+
const args = [];
|
|
1445
|
+
if (params.nestId) {
|
|
1446
|
+
where.push("nest_id = ?");
|
|
1447
|
+
args.push(params.nestId);
|
|
1448
|
+
}
|
|
1449
|
+
if (params.nestIds) {
|
|
1450
|
+
if (params.nestIds.length === 0) return [];
|
|
1451
|
+
where.push(`nest_id IN (${params.nestIds.map(() => "?").join(",")})`);
|
|
1452
|
+
args.push(...params.nestIds);
|
|
1453
|
+
}
|
|
1454
|
+
if (status !== "all") {
|
|
1455
|
+
where.push("status = ?");
|
|
1456
|
+
args.push(status);
|
|
1457
|
+
}
|
|
1458
|
+
const rows = await db.all(
|
|
1459
|
+
`SELECT * FROM deletion_requests${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`,
|
|
1460
|
+
args
|
|
1461
|
+
);
|
|
1462
|
+
if (rows.length === 0) return [];
|
|
1463
|
+
const titlesByNest = /* @__PURE__ */ new Map();
|
|
1464
|
+
for (const nid of new Set(rows.map((r) => r.nest_id))) {
|
|
1465
|
+
titlesByNest.set(nid, await buildTitleMap(nid));
|
|
1466
|
+
}
|
|
1467
|
+
const out = [];
|
|
1468
|
+
for (const r of rows) {
|
|
1469
|
+
const request = rowToDeletionRequest(r);
|
|
1470
|
+
out.push({
|
|
1471
|
+
...request,
|
|
1472
|
+
title: await deletionTargetTitle(request, titlesByNest.get(r.nest_id))
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
return out;
|
|
1476
|
+
}
|
|
1477
|
+
async function dropDeletionRequest(id) {
|
|
1478
|
+
await getDb().run("DELETE FROM deletion_requests WHERE id = ?", [id]);
|
|
1479
|
+
}
|
|
1480
|
+
async function declineDeletion(params) {
|
|
1481
|
+
const db = getDb();
|
|
1482
|
+
const pending = await getDeletionRequest(params.id);
|
|
1483
|
+
if (!pending || pending.nestId !== params.nestId || pending.status !== "pending") {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
const { changes } = await db.run(
|
|
1487
|
+
`UPDATE deletion_requests
|
|
1488
|
+
SET status = 'declined', resolved_by = ?, resolved_at = ${nowExpr(db)},
|
|
1489
|
+
resolution_note = ?
|
|
1490
|
+
WHERE id = ? AND status = 'pending'`,
|
|
1491
|
+
[params.declinedBy, params.note, params.id]
|
|
1492
|
+
);
|
|
1493
|
+
if (changes === 0) return null;
|
|
1494
|
+
const declined = await getDeletionRequest(params.id);
|
|
1495
|
+
if (declined) {
|
|
1496
|
+
await notifyDeletionResolved({
|
|
1497
|
+
nestId: params.nestId,
|
|
1498
|
+
nodeId: declined.nodeId,
|
|
1499
|
+
docTitle: await deletionTargetTitle(declined),
|
|
1500
|
+
outcome: "declined",
|
|
1501
|
+
requestedBy: declined.requestedBy,
|
|
1502
|
+
resolvedBy: params.declinedBy,
|
|
1503
|
+
requestId: declined.id,
|
|
1504
|
+
note: params.note,
|
|
1505
|
+
baseUrl: params.baseUrl
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
return declined;
|
|
1509
|
+
}
|
|
1510
|
+
function rowToDeletionRequest(row) {
|
|
1511
|
+
return {
|
|
1512
|
+
id: row.id,
|
|
1513
|
+
nestId: row.nest_id,
|
|
1514
|
+
nodeId: row.node_id,
|
|
1515
|
+
targetType: row.target_type || "document",
|
|
1516
|
+
requestedBy: row.requested_by,
|
|
1517
|
+
reason: row.reason,
|
|
1518
|
+
status: row.status,
|
|
1519
|
+
createdAt: row.created_at,
|
|
1520
|
+
resolvedBy: row.resolved_by || void 0,
|
|
1521
|
+
resolvedAt: row.resolved_at || void 0,
|
|
1522
|
+
resolutionNote: row.resolution_note || void 0
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1251
1525
|
async function getReviewRequest(id) {
|
|
1252
1526
|
const db = getDb();
|
|
1253
1527
|
const row = await db.get(
|
|
@@ -1274,8 +1548,6 @@ function rowToReviewRequest(row) {
|
|
|
1274
1548
|
}
|
|
1275
1549
|
|
|
1276
1550
|
export {
|
|
1277
|
-
safePublishDocument,
|
|
1278
|
-
stripUndefinedDeep,
|
|
1279
1551
|
listWatchers,
|
|
1280
1552
|
addWatcher,
|
|
1281
1553
|
removeWatcher,
|
|
@@ -1306,5 +1578,13 @@ export {
|
|
|
1306
1578
|
cancelReview,
|
|
1307
1579
|
getReviewQueue,
|
|
1308
1580
|
getReviewHistory,
|
|
1309
|
-
getPendingReview
|
|
1581
|
+
getPendingReview,
|
|
1582
|
+
deletionTargetTitle,
|
|
1583
|
+
notifyDeletionResolved,
|
|
1584
|
+
requestDeletion,
|
|
1585
|
+
getLatestDeletionRequest,
|
|
1586
|
+
getDeletionRequest,
|
|
1587
|
+
listDeletionRequests,
|
|
1588
|
+
dropDeletionRequest,
|
|
1589
|
+
declineDeletion
|
|
1310
1590
|
};
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
listGrants,
|
|
7
7
|
listUserGrants,
|
|
8
8
|
resolveNodeGrant
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-OTGZQZKK.js";
|
|
10
10
|
import "./chunk-GUNJTORH.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-JMJLNEXE.js";
|
|
12
12
|
import "./chunk-SLTQACJW.js";
|
|
13
13
|
export {
|
|
14
14
|
createGrant,
|