@promptowl/contextnest-community 1.25.0 → 1.27.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/API.md +102 -9
- package/CONFIGURATION.md +604 -117
- package/README.md +3 -2
- package/dist/{chunk-JDAOA5KP.js → chunk-H3W72HN3.js} +67 -14
- package/dist/{chunk-2KHM5C7V.js → chunk-MB52DWWP.js} +13 -15
- package/dist/{chunk-TI7HMP64.js → chunk-NFFUZF2B.js} +23 -2
- package/dist/{chunk-F66VCBLA.js → chunk-O3JEI33N.js} +51 -2
- package/dist/{chunk-7FROMOZM.js → chunk-QMQDWRHN.js} +180 -550
- package/dist/chunk-RXCVXG34.js +523 -0
- package/dist/{chunk-32SZCH36.js → chunk-SE5U4J4T.js} +4 -4
- package/dist/{chunk-KPGNZLZL.js → chunk-VHEMGQ3I.js} +1 -1
- package/dist/{chunk-DTLMBF4W.js → chunk-VRLAP4HB.js} +42 -17
- package/dist/{engine-TER6FDBP.js → engine-HLYQLP5T.js} +3 -2
- package/dist/{external-edit-service-CAGENHDJ.js → external-edit-service-3DFQ4EZZ.js} +4 -3
- package/dist/{grants-service-2OTFS7EH.js → grants-service-WICV4Y2J.js} +2 -2
- package/dist/index.js +1669 -521
- package/dist/invited-user-XEBX6BM4.js +12 -0
- package/dist/{migrations.postgres-23VJJJJX.js → migrations.postgres-HGSQ7IA5.js} +4 -0
- package/dist/{review-service-4G2XUFS6.js → review-service-GB43UQVH.js} +7 -6
- package/dist/{stewardship-service-S22V2JKG.js → stewardship-service-KDBPWRZI.js} +4 -3
- package/dist/{version-service-X4FAG2OZ.js → version-service-JYHFPSYP.js} +8 -3
- package/dist/web3/assets/ActivityTracePage-CkRoyIc-.js +1 -0
- package/dist/web3/assets/AgentDocsPage-BTRXmliv.js +1 -0
- package/dist/web3/assets/CollaboratorManager-CqJIkvs-.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-JcO1P-qm.js +1 -0
- package/dist/web3/assets/DocumentEditor-D1GLeeEP.js +36 -0
- package/dist/web3/assets/DocumentsTab-BAgSxTX1.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-DuuTXZap.js +1 -0
- package/dist/web3/assets/{MarkdownEditor-Dfk5uIE5.js → MarkdownEditor-BELFww9q.js} +130 -135
- package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
- package/dist/web3/assets/NestPageHeader-Bbje2ncY.js +1 -0
- package/dist/web3/assets/NestView-D4OMXPQ7.js +73 -0
- package/dist/web3/assets/OverviewTab-FIDGjR3Q.js +6 -0
- package/dist/web3/assets/PersonCombobox-D52m_jhc.js +1 -0
- package/dist/web3/assets/{ReviewActions-BXDTDPp1.js → ReviewActions-DYqBcWlv.js} +2 -2
- package/dist/web3/assets/ReviewTab-DoxfygOb.js +1 -0
- package/dist/web3/assets/StewardsTab-CFsYjbEn.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-AW2OLNAE.js +1 -0
- package/dist/web3/assets/{alert-dialog-4SiLKlz8.js → alert-dialog-p-fBLSWx.js} +2 -2
- package/dist/web3/assets/{arrow-left-BAgIocLn.js → arrow-left-CiErCCTG.js} +1 -1
- package/dist/web3/assets/backlinks-DaVSYvhV.js +19 -0
- package/dist/web3/assets/boxes-VWrCCMzW.js +6 -0
- package/dist/web3/assets/{chevron-left-XR4ReJ9Z.js → chevron-left-CsD8gZwL.js} +1 -1
- package/dist/web3/assets/{circle-check-CSkZUEFK.js → circle-check-BBCXd9LI.js} +1 -1
- package/dist/web3/assets/{circle-x-in2o3aHU.js → circle-x-C9LsnORQ.js} +1 -1
- package/dist/web3/assets/{code-xml-pPQgdctI.js → code-xml-BaoV7HiQ.js} +1 -1
- package/dist/web3/assets/{corner-down-right-CuxZWscX.js → corner-down-right-DgiTXrtw.js} +1 -1
- package/dist/web3/assets/count-skeleton-CnEE4uF-.js +1 -0
- package/dist/web3/assets/{earth-CXOcThqn.js → earth-BmMbVt9k.js} +1 -1
- package/dist/web3/assets/{file-exclamation-point-CSphs1VD.js → file-exclamation-point-qJeca9-A.js} +1 -1
- package/dist/web3/assets/{folder-input-BAwK9tFL.js → folder-input-D7R93LJ6.js} +1 -1
- package/dist/web3/assets/index-DNmWTxN7.css +1 -0
- package/dist/web3/assets/index-DvuUIUP6.js +451 -0
- package/dist/web3/assets/{page-CUjuSs5Q.js → page-BJQuaTRW.js} +3 -3
- package/dist/web3/assets/page-Bb8xGOPQ.js +2 -0
- package/dist/web3/assets/page-BtM-qDzr.js +6 -0
- package/dist/web3/assets/page-Bxd0EG25.js +1 -0
- package/dist/web3/assets/page-C3QCO6IR.js +1 -0
- package/dist/web3/assets/page-CfoR0khy.js +6 -0
- package/dist/web3/assets/page-D7Zr5tAI.js +1 -0
- package/dist/web3/assets/page-DATSlHCW.js +6 -0
- package/dist/web3/assets/page-DTjPQizZ.js +45 -0
- package/dist/web3/assets/page-DspptlWr.js +1 -0
- package/dist/web3/assets/{page-xzYqkBUP.js → page-I6vAFJOj.js} +5 -5
- package/dist/web3/assets/page-QxI4Ud2s.js +9 -0
- package/dist/web3/assets/page-YcNNg-dX.js +1 -0
- package/dist/web3/assets/page-kM7eO5Kj.js +1 -0
- package/dist/web3/assets/{page-title-BkqlUA0j.js → page-title-Dn5jANiU.js} +1 -1
- package/dist/web3/assets/{play-B8xE9j5f.js → play-B2k_Ix-L.js} +1 -1
- package/dist/web3/assets/{send-CvAgOUPG.js → send-DPNxv3As.js} +1 -1
- package/dist/web3/assets/{settings-EY4A_DYv.js → settings-DZjq-OQA.js} +1 -1
- package/dist/web3/assets/{share-2-vGmZUl90.js → share-2-C-OnE2ru.js} +1 -1
- package/dist/web3/assets/{tag-Br_y1f00.js → tag-CwJl7FdI.js} +1 -1
- package/dist/web3/assets/{trash-2-B4hRXo-p.js → trash-2-aljmIff4.js} +1 -1
- package/dist/web3/assets/{user-plus-4CTYEkd3.js → user-plus-DPerZexX.js} +1 -1
- package/dist/web3/assets/{zap-BcsKR0ef.js → zap-CHZtiN7P.js} +1 -1
- package/dist/web3/index.html +2 -2
- package/package.json +8 -3
- package/dist/client-D44ZHDTJ.js +0 -11
- package/dist/keys-CTOGAG3W.js +0 -20
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +0 -1
- package/dist/web3/assets/AgentDocsPage-Dkhgb1hk.js +0 -1
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +0 -1
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +0 -1
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +0 -36
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +0 -6
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +0 -1
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +0 -1
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +0 -1
- package/dist/web3/assets/NestView-DwIagxwJ.js +0 -68
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +0 -1
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +0 -1
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +0 -1
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +0 -1
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +0 -1
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +0 -1
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +0 -24
- package/dist/web3/assets/card-UDyMRcps.js +0 -1
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +0 -1
- package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
- package/dist/web3/assets/index-A0_ymcqL.js +0 -29
- package/dist/web3/assets/index-B5hLclEq.js +0 -389
- package/dist/web3/assets/index-DrUAtoQM.css +0 -1
- package/dist/web3/assets/page-BHD4beun.js +0 -1
- package/dist/web3/assets/page-BSYUXYmO.js +0 -11
- package/dist/web3/assets/page-BYc2DIux.js +0 -45
- package/dist/web3/assets/page-BfibLg8e.js +0 -1
- package/dist/web3/assets/page-CGe8MtUu.js +0 -9
- package/dist/web3/assets/page-CIgPJmG6.js +0 -2
- package/dist/web3/assets/page-Ck31nx9b.js +0 -1
- package/dist/web3/assets/page-Cpdj_11Y.js +0 -1
- package/dist/web3/assets/page-D0sN3GO6.js +0 -1
- package/dist/web3/assets/page-DEpH91X2.js +0 -1
- package/dist/web3/assets/page-l8-B7CGt.js +0 -1
- package/dist/web3/assets/page-mnSHmk6A.js +0 -6
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +0 -6
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +0 -1
- package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
- package/dist/web3/assets/select-DrL0mpRN.js +0 -6
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +0 -6
- package/dist/web3/assets/x-qxt1WrUi.js +0 -6
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import {
|
|
2
|
+
config,
|
|
3
|
+
getDb
|
|
4
|
+
} from "./chunk-H3W72HN3.js";
|
|
5
|
+
|
|
6
|
+
// src/governance/roles.ts
|
|
7
|
+
function collabPermToRole(permission) {
|
|
8
|
+
switch (permission) {
|
|
9
|
+
case "owner":
|
|
10
|
+
return "owner";
|
|
11
|
+
case "admin":
|
|
12
|
+
return "admin";
|
|
13
|
+
case "write":
|
|
14
|
+
return "editor";
|
|
15
|
+
case "read":
|
|
16
|
+
return "viewer";
|
|
17
|
+
default:
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function teamRoleToPermission(role) {
|
|
22
|
+
switch (role) {
|
|
23
|
+
case "admin":
|
|
24
|
+
return "admin";
|
|
25
|
+
case "editor":
|
|
26
|
+
return "write";
|
|
27
|
+
case "viewer":
|
|
28
|
+
return "read";
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
|
|
34
|
+
var canViewWith = (roles) => roles.length > 0;
|
|
35
|
+
var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
|
|
36
|
+
var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
|
|
37
|
+
var PRECEDENCE = [
|
|
38
|
+
"owner",
|
|
39
|
+
"admin",
|
|
40
|
+
"editor",
|
|
41
|
+
"reviewer",
|
|
42
|
+
"viewer"
|
|
43
|
+
];
|
|
44
|
+
function primaryRole(roles) {
|
|
45
|
+
for (const r of PRECEDENCE) if (roles.includes(r)) return r;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function permissionLabel(permission) {
|
|
49
|
+
const role = collabPermToRole(permission);
|
|
50
|
+
return role ? role[0].toUpperCase() + role.slice(1) : "Collaborator";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/notify/email-render.ts
|
|
54
|
+
function escapeHtml(s) {
|
|
55
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
56
|
+
}
|
|
57
|
+
var EVENT = {
|
|
58
|
+
":white_check_mark:": { icon: "\u2705", color: "#16a34a" },
|
|
59
|
+
":x:": { icon: "\u274C", color: "#dc2626" },
|
|
60
|
+
":memo:": { icon: "\u{1F4DD}", color: "#d97706" },
|
|
61
|
+
":key:": { icon: "\u{1F511}", color: "#2563eb" }
|
|
62
|
+
};
|
|
63
|
+
var ALL_EMOJI = Object.fromEntries(
|
|
64
|
+
Object.entries(EVENT).map(([code, m]) => [code, m.icon])
|
|
65
|
+
);
|
|
66
|
+
function renderText(message) {
|
|
67
|
+
return message.replace(/:[a-z_]+:/g, (m) => ALL_EMOJI[m] ?? "").replace(/\*/g, "").replace(/[ \t]{2,}/g, " ").trim();
|
|
68
|
+
}
|
|
69
|
+
var STATUS_META = {
|
|
70
|
+
approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
|
|
71
|
+
mention: { subjectWord: "mentioned you", label: "Mention", color: "#7c3aed" },
|
|
72
|
+
rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
|
|
73
|
+
pending_review: {
|
|
74
|
+
subjectWord: "awaiting review",
|
|
75
|
+
label: "Pending review",
|
|
76
|
+
color: "#d97706"
|
|
77
|
+
},
|
|
78
|
+
shared: { subjectWord: "shared with you", label: "Shared", color: "#2563eb" },
|
|
79
|
+
invited: { subjectWord: "invited you", label: "Invitation", color: "#2563eb" },
|
|
80
|
+
steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" },
|
|
81
|
+
deletion_requested: {
|
|
82
|
+
subjectWord: "flagged for deletion",
|
|
83
|
+
label: "Deletion requested",
|
|
84
|
+
color: "#d97706"
|
|
85
|
+
},
|
|
86
|
+
deletion_declined: {
|
|
87
|
+
subjectWord: "kept",
|
|
88
|
+
label: "Deletion rejected",
|
|
89
|
+
color: "#d97706"
|
|
90
|
+
},
|
|
91
|
+
deletion_deleted: {
|
|
92
|
+
subjectWord: "deleted",
|
|
93
|
+
label: "Deleted",
|
|
94
|
+
color: "#dc2626"
|
|
95
|
+
},
|
|
96
|
+
// An honoured ARCHIVE request. Deliberately not the deletion palette: the
|
|
97
|
+
// nest is intact and restorable, and red "Deleted" would tell the requester
|
|
98
|
+
// the opposite of what happened.
|
|
99
|
+
deletion_archived: {
|
|
100
|
+
subjectWord: "archived",
|
|
101
|
+
label: "Archived",
|
|
102
|
+
color: "#0891b2"
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
106
|
+
var shareRole = (permission) => permission ? permissionLabel(permission) : "collaborator";
|
|
107
|
+
var detailRow = (label, valueHtml) => `<tr>
|
|
108
|
+
<td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
|
|
109
|
+
<td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
|
|
110
|
+
</tr>`;
|
|
111
|
+
var plainRow = (label, value) => ({
|
|
112
|
+
label,
|
|
113
|
+
text: value,
|
|
114
|
+
html: escapeHtml(value)
|
|
115
|
+
});
|
|
116
|
+
var governanceRows = (x) => [
|
|
117
|
+
plainRow("Path", x.d.path),
|
|
118
|
+
{ label: "Status", text: x.meta.label, html: x.statusBadge },
|
|
119
|
+
...x.d.version != null ? [plainRow("Version", `v${x.d.version}`)] : []
|
|
120
|
+
];
|
|
121
|
+
var PROMPTOWL_LINE = " Or choose Sign in with PromptOwl, using a PromptOwl account under this same email.";
|
|
122
|
+
var TEMPLATES = {
|
|
123
|
+
approved: {
|
|
124
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
125
|
+
detailsHeader: "Document details",
|
|
126
|
+
linkLabel: "Open document",
|
|
127
|
+
lineT: (x) => `${x.title}${x.vT} has been approved${x.byT}${x.noteT}.`,
|
|
128
|
+
lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
|
|
129
|
+
rows: governanceRows
|
|
130
|
+
},
|
|
131
|
+
// A mention names one person, so this is the one template addressed to
|
|
132
|
+
// "you". `note` carries the comment's own words when there are any — a
|
|
133
|
+
// mention with no context reads as a summons.
|
|
134
|
+
mention: {
|
|
135
|
+
heading: (x) => `${x.d.actor || "Someone"} mentioned you in ${x.title}`,
|
|
136
|
+
detailsHeader: "Document details",
|
|
137
|
+
linkLabel: "Open document",
|
|
138
|
+
lineT: (x) => `You were mentioned in ${x.title}${x.byT}${x.noteT}.`,
|
|
139
|
+
lineH: (x) => `You were mentioned in ${x.doc}${x.byH}${x.noteH}.`,
|
|
140
|
+
rows: (x) => [plainRow("Nest", x.d.path)]
|
|
141
|
+
},
|
|
142
|
+
rejected: {
|
|
143
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
144
|
+
detailsHeader: "Document details",
|
|
145
|
+
linkLabel: "Open document",
|
|
146
|
+
lineT: (x) => `${x.title}${x.vT} has been rejected${x.byT}${x.noteT}.`,
|
|
147
|
+
lineH: (x) => `${x.doc}${x.vH} has been rejected${x.byH}${x.noteH}.`,
|
|
148
|
+
rows: governanceRows
|
|
149
|
+
},
|
|
150
|
+
pending_review: {
|
|
151
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
152
|
+
detailsHeader: "Document details",
|
|
153
|
+
linkLabel: "Open document",
|
|
154
|
+
lineT: (x) => `A review has been requested for ${x.title}${x.vT}${x.byT}${x.noteT}.`,
|
|
155
|
+
lineH: (x) => `A review has been requested for ${x.doc}${x.vH}${x.byH}${x.noteH}.`,
|
|
156
|
+
rows: governanceRows
|
|
157
|
+
},
|
|
158
|
+
shared: {
|
|
159
|
+
heading: (x) => `You've been added to ${x.title}`,
|
|
160
|
+
detailsHeader: "Details",
|
|
161
|
+
linkLabel: "Open nest",
|
|
162
|
+
lineT: (x) => `You've been given ${shareRole(x.d.permission)} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
|
|
163
|
+
lineH: (x) => `You've been given <strong>${escapeHtml(shareRole(x.d.permission))}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
|
|
164
|
+
rows: (x) => [
|
|
165
|
+
plainRow("Nest", x.d.path),
|
|
166
|
+
...x.d.permission ? [plainRow("Role", shareRole(x.d.permission))] : []
|
|
167
|
+
]
|
|
168
|
+
},
|
|
169
|
+
invited: {
|
|
170
|
+
heading: (x) => `You've been invited to ${x.title}`,
|
|
171
|
+
detailsHeader: "Sign-in details",
|
|
172
|
+
linkLabel: "Sign in",
|
|
173
|
+
lineT: (x) => `You've been invited to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Sign in with your email and the temporary password below, then choose your own password.${x.d.promptowlSignIn ? PROMPTOWL_LINE : ""}`,
|
|
174
|
+
lineH: (x) => `You've been invited to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Sign in with your email and the temporary password below, then choose your own password.${x.d.promptowlSignIn ? PROMPTOWL_LINE : ""}`,
|
|
175
|
+
rows: (x) => [
|
|
176
|
+
plainRow("Email", x.d.actor || ""),
|
|
177
|
+
...x.d.tempPassword ? [
|
|
178
|
+
{
|
|
179
|
+
label: "Temporary password",
|
|
180
|
+
text: x.d.tempPassword,
|
|
181
|
+
html: x.codeValue(x.d.tempPassword)
|
|
182
|
+
}
|
|
183
|
+
] : []
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
// Deletion requests reuse the governance details block — same "what/where/
|
|
187
|
+
// status" shape, and the reason/note rides in `note` like a review note.
|
|
188
|
+
deletion_requested: {
|
|
189
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
190
|
+
detailsHeader: "Document details",
|
|
191
|
+
linkLabel: "Review the request",
|
|
192
|
+
lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or reject the request from the nest.`,
|
|
193
|
+
lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or reject the request from the nest.`,
|
|
194
|
+
rows: governanceRows
|
|
195
|
+
},
|
|
196
|
+
deletion_declined: {
|
|
197
|
+
heading: (x) => `Your deletion request for ${x.title} was rejected`,
|
|
198
|
+
detailsHeader: "Document details",
|
|
199
|
+
linkLabel: "Open document",
|
|
200
|
+
lineT: (x) => `Your request to delete ${x.title} was rejected${x.byT}${x.noteT}. The document stays.`,
|
|
201
|
+
lineH: (x) => `Your request to delete ${x.doc} was rejected${x.byH}${x.noteH}. The document stays.`,
|
|
202
|
+
rows: governanceRows
|
|
203
|
+
},
|
|
204
|
+
deletion_deleted: {
|
|
205
|
+
heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
|
|
206
|
+
detailsHeader: "Document details",
|
|
207
|
+
linkLabel: "Open nest",
|
|
208
|
+
lineT: (x) => `${x.title} has been deleted${x.byT}${x.noteT} \u2014 your deletion request was accepted.`,
|
|
209
|
+
lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
|
|
210
|
+
rows: governanceRows
|
|
211
|
+
},
|
|
212
|
+
deletion_archived: {
|
|
213
|
+
heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
|
|
214
|
+
detailsHeader: "Nest details",
|
|
215
|
+
linkLabel: "Open nest",
|
|
216
|
+
lineT: (x) => `${x.title} has been archived${x.byT}${x.noteT} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
217
|
+
lineH: (x) => `${x.doc} has been archived${x.byH}${x.noteH} \u2014 your request was accepted. Nothing was deleted: every document and version is intact, and an owner can restore it.`,
|
|
218
|
+
rows: governanceRows
|
|
219
|
+
},
|
|
220
|
+
steward: {
|
|
221
|
+
heading: (x) => `You're now a steward of ${x.title}`,
|
|
222
|
+
detailsHeader: "Details",
|
|
223
|
+
linkLabel: "Open nest",
|
|
224
|
+
lineT: (x) => `You've been assigned as a ${x.d.permission || "reviewer"} steward of ${x.d.scopeLabel || x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open the nest below to start reviewing.`,
|
|
225
|
+
lineH: (x) => `You've been assigned as a <strong>${escapeHtml(x.d.permission || "reviewer")}</strong> steward of ${x.d.scopeLabel ? escapeHtml(x.d.scopeLabel) : x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open the nest below to start reviewing.`,
|
|
226
|
+
rows: (x) => [
|
|
227
|
+
plainRow("Nest", x.d.path),
|
|
228
|
+
...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : [],
|
|
229
|
+
...x.d.scopeLabel ? [plainRow("Scope", cap(x.d.scopeLabel))] : []
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function renderEmail(d) {
|
|
234
|
+
const meta = STATUS_META[d.status];
|
|
235
|
+
const t = TEMPLATES[d.status];
|
|
236
|
+
const title = d.docTitle.replace(/[\r\n]+/g, " ").trim();
|
|
237
|
+
const vLabel = d.version != null ? `v${d.version}` : "";
|
|
238
|
+
const ctx = {
|
|
239
|
+
d,
|
|
240
|
+
title,
|
|
241
|
+
doc: `<strong>${escapeHtml(title)}</strong>`,
|
|
242
|
+
byT: d.actor ? ` by ${d.actor}` : "",
|
|
243
|
+
byH: d.actor ? ` by ${escapeHtml(d.actor)}` : "",
|
|
244
|
+
noteT: d.note ? ` with the note: "${d.note}"` : "",
|
|
245
|
+
noteH: d.note ? ` with the note: “${escapeHtml(d.note)}”` : "",
|
|
246
|
+
vT: vLabel ? ` ${vLabel}` : "",
|
|
247
|
+
vH: vLabel ? ` ${vLabel}` : "",
|
|
248
|
+
meta,
|
|
249
|
+
statusBadge: `<span style="display:inline-block;padding:2px 9px;border-radius:9999px;background:#f3f4f6;color:${meta.color};font-size:12px;font-weight:600;">${meta.label}</span>`,
|
|
250
|
+
codeValue: (v) => `<span style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:600;color:#111827;background:#f3f4f6;padding:2px 8px;border-radius:6px;">${escapeHtml(v)}</span>`
|
|
251
|
+
};
|
|
252
|
+
const heading = t.heading(ctx).replace(/[\r\n]+/g, " ").trim();
|
|
253
|
+
const subject = heading;
|
|
254
|
+
const detailsHeader = t.detailsHeader;
|
|
255
|
+
const linkLabel = t.linkLabel;
|
|
256
|
+
const lineH = t.lineH(ctx);
|
|
257
|
+
const rows = t.rows(ctx);
|
|
258
|
+
const detailLines = [
|
|
259
|
+
`${detailsHeader}:`,
|
|
260
|
+
...rows.map((r) => ` ${r.label}: ${r.text}`),
|
|
261
|
+
...d.link ? [` ${linkLabel}: ${d.link}`] : []
|
|
262
|
+
];
|
|
263
|
+
const text = ["Hi,", "", t.lineT(ctx), "", ...detailLines, "", "Regards,", "Community Nest"].join("\n");
|
|
264
|
+
const detailRows = rows.map((r) => detailRow(r.label, r.html)).join("");
|
|
265
|
+
const buttonRow = d.link ? `<tr><td style="padding:20px 24px 0;">
|
|
266
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
|
|
267
|
+
<td bgcolor="${meta.color}" style="border-radius:6px;">
|
|
268
|
+
<a href="${escapeHtml(d.link)}" target="_blank" style="display:inline-block;padding:10px 20px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;">${linkLabel}</a>
|
|
269
|
+
</td>
|
|
270
|
+
</tr></table>
|
|
271
|
+
</td></tr>` : "";
|
|
272
|
+
const html = `<div style="margin:0;padding:24px;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#111827;">
|
|
273
|
+
<table role="presentation" width="480" cellpadding="0" cellspacing="0" border="0" align="center" style="width:100%;max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;">
|
|
274
|
+
<tr><td style="padding:20px 24px 0;">
|
|
275
|
+
<div style="font-size:12px;font-weight:700;color:#6b7280;letter-spacing:0.06em;text-transform:uppercase;">ContextNest</div>
|
|
276
|
+
</td></tr>
|
|
277
|
+
<tr><td style="padding:10px 24px 0;">
|
|
278
|
+
<h1 style="margin:0;font-size:19px;line-height:1.35;font-weight:700;color:#111827;">${escapeHtml(heading)}</h1>
|
|
279
|
+
</td></tr>
|
|
280
|
+
<tr><td style="padding:16px 24px 0;font-size:14px;line-height:1.6;color:#374151;">
|
|
281
|
+
Hi,<br><br>${lineH}
|
|
282
|
+
</td></tr>
|
|
283
|
+
<tr><td style="padding:20px 24px 0;">
|
|
284
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #eef0f2;border-radius:8px;">
|
|
285
|
+
<tr><td style="padding:12px 16px;">
|
|
286
|
+
<div style="font-size:11px;font-weight:600;color:#9ca3af;letter-spacing:0.04em;text-transform:uppercase;padding-bottom:4px;">${detailsHeader}</div>
|
|
287
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${detailRows}</table>
|
|
288
|
+
</td></tr>
|
|
289
|
+
</table>
|
|
290
|
+
</td></tr>
|
|
291
|
+
${buttonRow}
|
|
292
|
+
<tr><td style="padding:22px 24px 24px;font-size:14px;line-height:1.6;color:#374151;">
|
|
293
|
+
Regards,<br>Community Nest
|
|
294
|
+
</td></tr>
|
|
295
|
+
</table>
|
|
296
|
+
</div>`;
|
|
297
|
+
return { subject, text, html };
|
|
298
|
+
}
|
|
299
|
+
function renderHtml(message, nestName) {
|
|
300
|
+
const lead = message.match(/^(:[a-z_]+:)\s*/);
|
|
301
|
+
const meta = lead && EVENT[lead[1]] || { icon: "\u{1F514}", color: "#475569" };
|
|
302
|
+
const rest = lead ? message.slice(lead[0].length) : message;
|
|
303
|
+
const body = escapeHtml(rest).replace(/\*([^*]+)\*/g, "<strong>$1</strong>").replace(/\n/g, "<br>");
|
|
304
|
+
const safeNest = escapeHtml(nestName);
|
|
305
|
+
return `<div style="background:#f6f8fa;padding:24px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
306
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;">
|
|
307
|
+
<tr><td style="padding:20px 24px 16px;border-bottom:1px solid #f0f0f0;">
|
|
308
|
+
<div style="font-size:15px;font-weight:700;color:#111827;letter-spacing:-0.01em;">ContextNest</div>
|
|
309
|
+
<div style="font-size:13px;color:#6b7280;margin-top:2px;">${safeNest}</div>
|
|
310
|
+
</td></tr>
|
|
311
|
+
<tr><td style="padding:24px;">
|
|
312
|
+
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
|
|
313
|
+
<td valign="top" style="font-size:22px;line-height:1;padding-right:14px;">${meta.icon}</td>
|
|
314
|
+
<td valign="top" style="font-size:15px;line-height:1.55;color:#111827;border-left:3px solid ${meta.color};padding-left:14px;">${body}</td>
|
|
315
|
+
</tr></table>
|
|
316
|
+
</td></tr>
|
|
317
|
+
</table>
|
|
318
|
+
</div>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/notify/email.ts
|
|
322
|
+
var transporterPromise = null;
|
|
323
|
+
var configuredKey = null;
|
|
324
|
+
var warnedOnce = false;
|
|
325
|
+
var SMTP_TIMEOUT_DEFAULTS = [
|
|
326
|
+
["connectionTimeout", 5e3],
|
|
327
|
+
["greetingTimeout", 5e3],
|
|
328
|
+
["socketTimeout", 1e4]
|
|
329
|
+
];
|
|
330
|
+
function withTimeouts(url) {
|
|
331
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
332
|
+
const missing = SMTP_TIMEOUT_DEFAULTS.filter(
|
|
333
|
+
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
334
|
+
).map(([name, ms]) => `${name}=${ms}`);
|
|
335
|
+
if (missing.length === 0) return url;
|
|
336
|
+
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
337
|
+
}
|
|
338
|
+
var POOL_DEFAULTS = [
|
|
339
|
+
["pool", "true"],
|
|
340
|
+
["maxConnections", "1"],
|
|
341
|
+
["rateDelta", "1000"],
|
|
342
|
+
["rateLimit", "8"]
|
|
343
|
+
// messages per second
|
|
344
|
+
];
|
|
345
|
+
function withPool(url) {
|
|
346
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
347
|
+
const missing = POOL_DEFAULTS.filter(
|
|
348
|
+
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
349
|
+
).map(([name, val]) => `${name}=${val}`);
|
|
350
|
+
if (missing.length === 0) return url;
|
|
351
|
+
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
352
|
+
}
|
|
353
|
+
function transportSettings(over = {}) {
|
|
354
|
+
const v = (name, current) => name in over ? over[name] : current;
|
|
355
|
+
if (v("EMAIL_PROVIDER", config.EMAIL_PROVIDER) === "ses") {
|
|
356
|
+
const region = v("SES_REGION", config.SES_REGION);
|
|
357
|
+
return region ? {
|
|
358
|
+
provider: "ses",
|
|
359
|
+
region,
|
|
360
|
+
accessKeyId: v("SES_ACCESS_KEY_ID", config.SES_ACCESS_KEY_ID),
|
|
361
|
+
secretAccessKey: v("SES_SECRET_ACCESS_KEY", config.SES_SECRET_ACCESS_KEY)
|
|
362
|
+
} : null;
|
|
363
|
+
}
|
|
364
|
+
const url = v("SMTP_URL", config.SMTP_URL);
|
|
365
|
+
return url ? { provider: "smtp", url } : null;
|
|
366
|
+
}
|
|
367
|
+
async function createTransport(s, pooled) {
|
|
368
|
+
const nodemailer = (await import("nodemailer")).default;
|
|
369
|
+
const hardening = { disableFileAccess: true, disableUrlAccess: true };
|
|
370
|
+
if (s.provider === "smtp") {
|
|
371
|
+
const url = withTimeouts(s.url);
|
|
372
|
+
return nodemailer.createTransport(pooled ? withPool(url) : url, hardening);
|
|
373
|
+
}
|
|
374
|
+
const ses = await import("@aws-sdk/client-sesv2");
|
|
375
|
+
const sesClient = new ses.SESv2Client({
|
|
376
|
+
region: s.region,
|
|
377
|
+
...s.accessKeyId && s.secretAccessKey ? { credentials: { accessKeyId: s.accessKeyId, secretAccessKey: s.secretAccessKey } } : {}
|
|
378
|
+
});
|
|
379
|
+
return nodemailer.createTransport({
|
|
380
|
+
SES: { sesClient, SendEmailCommand: ses.SendEmailCommand },
|
|
381
|
+
...hardening
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async function verifyEmailTransport(s) {
|
|
385
|
+
let transporter = null;
|
|
386
|
+
try {
|
|
387
|
+
transporter = await createTransport(s, false);
|
|
388
|
+
await transporter.verify();
|
|
389
|
+
return { ok: true };
|
|
390
|
+
} catch (err) {
|
|
391
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
392
|
+
} finally {
|
|
393
|
+
try {
|
|
394
|
+
transporter?.close?.();
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function getTransporter() {
|
|
400
|
+
const s = transportSettings();
|
|
401
|
+
if (!s) return null;
|
|
402
|
+
const key = JSON.stringify(s);
|
|
403
|
+
if (!transporterPromise || configuredKey !== key) {
|
|
404
|
+
configuredKey = key;
|
|
405
|
+
transporterPromise = createTransport(s, true);
|
|
406
|
+
transporterPromise.catch(() => {
|
|
407
|
+
transporterPromise = null;
|
|
408
|
+
configuredKey = null;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return transporterPromise;
|
|
412
|
+
}
|
|
413
|
+
async function notifyEmailForNest(nestId, message, details) {
|
|
414
|
+
const from = config.NOTIFY_EMAIL_FROM;
|
|
415
|
+
const to = config.NOTIFY_EMAIL_TO;
|
|
416
|
+
if (!transportSettings() || !from || !to) return;
|
|
417
|
+
try {
|
|
418
|
+
const transporter = await getTransporter();
|
|
419
|
+
if (!transporter) return;
|
|
420
|
+
let subject;
|
|
421
|
+
let text;
|
|
422
|
+
let html;
|
|
423
|
+
if (details) {
|
|
424
|
+
({ subject, text, html } = renderEmail(details));
|
|
425
|
+
} else {
|
|
426
|
+
let name = nestId;
|
|
427
|
+
try {
|
|
428
|
+
const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
|
|
429
|
+
nestId
|
|
430
|
+
]);
|
|
431
|
+
if (row?.name) name = row.name;
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
const safeName = name.replace(/[\r\n]+/g, " ").trim();
|
|
435
|
+
subject = `ContextNest \xB7 ${safeName}`;
|
|
436
|
+
text = renderText(message);
|
|
437
|
+
html = renderHtml(message, safeName);
|
|
438
|
+
}
|
|
439
|
+
await transporter.sendMail({
|
|
440
|
+
from,
|
|
441
|
+
to,
|
|
442
|
+
subject,
|
|
443
|
+
text,
|
|
444
|
+
html,
|
|
445
|
+
disableFileAccess: true,
|
|
446
|
+
disableUrlAccess: true
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
if (!warnedOnce) {
|
|
450
|
+
warnedOnce = true;
|
|
451
|
+
console.warn(
|
|
452
|
+
"[email] notification failed (suppressing further warnings):",
|
|
453
|
+
err instanceof Error ? err.message : err
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function isEmailConfigured() {
|
|
459
|
+
return !!transportSettings() && !!config.NOTIFY_EMAIL_FROM;
|
|
460
|
+
}
|
|
461
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
462
|
+
function recipientEnvelope(to, from) {
|
|
463
|
+
const recipients = Array.isArray(to) ? to : [to];
|
|
464
|
+
return recipients.length > 1 ? { to: from, bcc: recipients } : { to: recipients[0] };
|
|
465
|
+
}
|
|
466
|
+
function isRateLimited(err) {
|
|
467
|
+
const msg = String(err?.message ?? err);
|
|
468
|
+
const code = Number(err?.responseCode);
|
|
469
|
+
return /too many|rate limit|rate exceeded|throttl|try again|4\.7\.\d|too many emails/i.test(msg) || code === 421 || code === 429 || code === 450 || code === 451 || code === 550 && /too many|rate/i.test(msg);
|
|
470
|
+
}
|
|
471
|
+
async function sendMailWithRetry(transporter, msg, attempts = 5) {
|
|
472
|
+
for (let i = 0; ; i++) {
|
|
473
|
+
try {
|
|
474
|
+
return await transporter.sendMail(msg);
|
|
475
|
+
} catch (err) {
|
|
476
|
+
if (i >= attempts - 1 || !isRateLimited(err)) throw err;
|
|
477
|
+
const wait = 1e3 * 2 ** i;
|
|
478
|
+
await sleep(wait);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function sendEmailToRecipient(to, details) {
|
|
483
|
+
const from = config.NOTIFY_EMAIL_FROM;
|
|
484
|
+
const hasRecipient = Array.isArray(to) ? to.length > 0 : !!to;
|
|
485
|
+
if (!transportSettings() || !from || !hasRecipient) return;
|
|
486
|
+
try {
|
|
487
|
+
const transporter = await getTransporter();
|
|
488
|
+
if (!transporter) return;
|
|
489
|
+
const { subject, text, html } = renderEmail(details);
|
|
490
|
+
await sendMailWithRetry(transporter, {
|
|
491
|
+
from,
|
|
492
|
+
...recipientEnvelope(to, from),
|
|
493
|
+
subject,
|
|
494
|
+
text,
|
|
495
|
+
html,
|
|
496
|
+
disableFileAccess: true,
|
|
497
|
+
disableUrlAccess: true
|
|
498
|
+
});
|
|
499
|
+
} catch (err) {
|
|
500
|
+
if (!warnedOnce) {
|
|
501
|
+
warnedOnce = true;
|
|
502
|
+
console.warn(
|
|
503
|
+
"[email] notification failed (suppressing further warnings):",
|
|
504
|
+
err instanceof Error ? err.message : err
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export {
|
|
511
|
+
collabPermToRole,
|
|
512
|
+
teamRoleToPermission,
|
|
513
|
+
canViewWith,
|
|
514
|
+
canEditWith,
|
|
515
|
+
canApproveWith,
|
|
516
|
+
primaryRole,
|
|
517
|
+
permissionLabel,
|
|
518
|
+
transportSettings,
|
|
519
|
+
verifyEmailTransport,
|
|
520
|
+
notifyEmailForNest,
|
|
521
|
+
isEmailConfigured,
|
|
522
|
+
sendEmailToRecipient
|
|
523
|
+
};
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
markIndexStale,
|
|
7
7
|
resolveNestPath,
|
|
8
8
|
syncSuggestionRows
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QMQDWRHN.js";
|
|
10
10
|
import {
|
|
11
11
|
getDb
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-H3W72HN3.js";
|
|
13
13
|
|
|
14
14
|
// src/governance/external-edit-service.ts
|
|
15
15
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -83,7 +83,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
83
83
|
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
-
const { hasUnsealedDraft } = await import("./version-service-
|
|
86
|
+
const { hasUnsealedDraft } = await import("./version-service-JYHFPSYP.js");
|
|
87
87
|
if (await hasUnsealedDraft(nestId, documentId)) {
|
|
88
88
|
return null;
|
|
89
89
|
}
|
|
@@ -254,7 +254,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
254
254
|
async function mirrorVersion(input) {
|
|
255
255
|
const { storage } = await engineCache.get(input.nestId);
|
|
256
256
|
const node = await storage.readDocument(input.documentId);
|
|
257
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
257
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-JYHFPSYP.js");
|
|
258
258
|
await upsertVersion({
|
|
259
259
|
nestId: input.nestId,
|
|
260
260
|
nodeId: input.documentId,
|