@promptowl/contextnest-community 1.13.0 → 1.14.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 +57 -1
- package/README.md +2 -0
- package/dist/{chunk-5QQ7WKAI.js → chunk-I5KYGMIT.js} +22 -0
- package/dist/{chunk-ZQS5W45U.js → chunk-IHGPZ6NY.js} +1 -1
- package/dist/{chunk-EG77SQHO.js → chunk-JP67WXLA.js} +1 -1
- package/dist/{chunk-Q5NRCDD4.js → chunk-KPK656BH.js} +456 -27
- package/dist/{chunk-PAIP5KHB.js → chunk-MZ4IR5KH.js} +458 -500
- package/dist/{client-AAMF22YJ.js → client-3MGIP57D.js} +1 -1
- package/dist/{grants-service-FZD6QM6V.js → grants-service-5ZFISCBH.js} +2 -2
- package/dist/index.js +189 -60
- package/dist/{review-service-Q3NDK5TX.js → review-service-6G7MVTY6.js} +7 -5
- package/dist/{stewardship-service-B6L7UACX.js → stewardship-service-G7IF7ZNP.js} +2 -2
- package/dist/{version-service-L52F4WVZ.js → version-service-FGL47APL.js} +2 -2
- package/dist/web3/assets/index-BYmOkm4p.js +1052 -0
- package/dist/web3/assets/index-p6uQyhml.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +2 -1
- package/dist/web3/assets/index-DcmDj50q.css +0 -1
- package/dist/web3/assets/index-EGPZ6MAy.js +0 -1023
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
config,
|
|
16
16
|
getDb,
|
|
17
17
|
isEmailish
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-I5KYGMIT.js";
|
|
19
19
|
import {
|
|
20
20
|
ANON_USER_ID
|
|
21
21
|
} from "./chunk-SLTQACJW.js";
|
|
@@ -150,13 +150,14 @@ import { join as join2, dirname, isAbsolute } from "path";
|
|
|
150
150
|
function isSkippedSegment(segment) {
|
|
151
151
|
return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
|
|
152
152
|
}
|
|
153
|
+
var ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".versions", ".context"]);
|
|
153
154
|
function safeSegments(rel) {
|
|
154
155
|
if (!rel || isAbsolute(rel)) return null;
|
|
155
156
|
const segs = rel.split(/[\\/]/).filter(Boolean);
|
|
156
157
|
if (!segs.length) return null;
|
|
157
158
|
for (const s of segs) {
|
|
158
159
|
if (s === "..") return null;
|
|
159
|
-
if (s
|
|
160
|
+
if (!ALLOWED_DOT_DIRS.has(s) && isSkippedSegment(s)) return null;
|
|
160
161
|
}
|
|
161
162
|
return segs;
|
|
162
163
|
}
|
|
@@ -641,6 +642,373 @@ async function _validateLicenseImpl(forceFresh) {
|
|
|
641
642
|
|
|
642
643
|
// src/governance/teams-service.ts
|
|
643
644
|
import { v4 as uuid } from "uuid";
|
|
645
|
+
|
|
646
|
+
// src/notify/email-render.ts
|
|
647
|
+
function escapeHtml(s) {
|
|
648
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
649
|
+
}
|
|
650
|
+
var EVENT = {
|
|
651
|
+
":white_check_mark:": { icon: "\u2705", color: "#16a34a" },
|
|
652
|
+
":x:": { icon: "\u274C", color: "#dc2626" },
|
|
653
|
+
":memo:": { icon: "\u{1F4DD}", color: "#d97706" },
|
|
654
|
+
":key:": { icon: "\u{1F511}", color: "#2563eb" }
|
|
655
|
+
};
|
|
656
|
+
var ALL_EMOJI = Object.fromEntries(
|
|
657
|
+
Object.entries(EVENT).map(([code, m]) => [code, m.icon])
|
|
658
|
+
);
|
|
659
|
+
function renderText(message) {
|
|
660
|
+
return message.replace(/:[a-z_]+:/g, (m) => ALL_EMOJI[m] ?? "").replace(/\*/g, "").replace(/[ \t]{2,}/g, " ").trim();
|
|
661
|
+
}
|
|
662
|
+
var STATUS_META = {
|
|
663
|
+
approved: { subjectWord: "approved", label: "Approved", color: "#16a34a" },
|
|
664
|
+
rejected: { subjectWord: "rejected", label: "Rejected", color: "#dc2626" },
|
|
665
|
+
pending_review: {
|
|
666
|
+
subjectWord: "awaiting review",
|
|
667
|
+
label: "Pending review",
|
|
668
|
+
color: "#d97706"
|
|
669
|
+
},
|
|
670
|
+
shared: { subjectWord: "shared with you", label: "Shared", color: "#2563eb" },
|
|
671
|
+
invited: { subjectWord: "invited you", label: "Invitation", color: "#2563eb" },
|
|
672
|
+
steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" }
|
|
673
|
+
};
|
|
674
|
+
var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
675
|
+
var detailRow = (label, valueHtml) => `<tr>
|
|
676
|
+
<td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
|
|
677
|
+
<td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
|
|
678
|
+
</tr>`;
|
|
679
|
+
var plainRow = (label, value) => ({
|
|
680
|
+
label,
|
|
681
|
+
text: value,
|
|
682
|
+
html: escapeHtml(value)
|
|
683
|
+
});
|
|
684
|
+
var governanceRows = (x) => [
|
|
685
|
+
plainRow("Path", x.d.path),
|
|
686
|
+
{ label: "Status", text: x.meta.label, html: x.statusBadge },
|
|
687
|
+
...x.d.version != null ? [plainRow("Version", `v${x.d.version}`)] : []
|
|
688
|
+
];
|
|
689
|
+
var TEMPLATES = {
|
|
690
|
+
approved: {
|
|
691
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
692
|
+
detailsHeader: "Document details",
|
|
693
|
+
linkLabel: "Open document",
|
|
694
|
+
lineT: (x) => `${x.title}${x.vT} has been approved${x.byT}${x.noteT}.`,
|
|
695
|
+
lineH: (x) => `${x.doc}${x.vH} has been approved${x.byH}${x.noteH}.`,
|
|
696
|
+
rows: governanceRows
|
|
697
|
+
},
|
|
698
|
+
rejected: {
|
|
699
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
700
|
+
detailsHeader: "Document details",
|
|
701
|
+
linkLabel: "Open document",
|
|
702
|
+
lineT: (x) => `${x.title}${x.vT} has been rejected${x.byT}${x.noteT}.`,
|
|
703
|
+
lineH: (x) => `${x.doc}${x.vH} has been rejected${x.byH}${x.noteH}.`,
|
|
704
|
+
rows: governanceRows
|
|
705
|
+
},
|
|
706
|
+
pending_review: {
|
|
707
|
+
heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
|
|
708
|
+
detailsHeader: "Document details",
|
|
709
|
+
linkLabel: "Open document",
|
|
710
|
+
lineT: (x) => `A review has been requested for ${x.title}${x.vT}${x.byT}${x.noteT}.`,
|
|
711
|
+
lineH: (x) => `A review has been requested for ${x.doc}${x.vH}${x.byH}${x.noteH}.`,
|
|
712
|
+
rows: governanceRows
|
|
713
|
+
},
|
|
714
|
+
shared: {
|
|
715
|
+
heading: (x) => `You've been added to ${x.title}`,
|
|
716
|
+
detailsHeader: "Details",
|
|
717
|
+
linkLabel: "Open nest",
|
|
718
|
+
lineT: (x) => `You've been given ${x.d.permission || "collaborator"} access to ${x.title}${x.d.by ? ` by ${x.d.by}` : ""}. Open it below to start collaborating.`,
|
|
719
|
+
lineH: (x) => `You've been given <strong>${escapeHtml(x.d.permission || "collaborator")}</strong> access to ${x.doc}${x.d.by ? ` by ${escapeHtml(x.d.by)}` : ""}. Open it below to start collaborating.`,
|
|
720
|
+
rows: (x) => [
|
|
721
|
+
plainRow("Nest", x.d.path),
|
|
722
|
+
...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : []
|
|
723
|
+
]
|
|
724
|
+
},
|
|
725
|
+
invited: {
|
|
726
|
+
heading: (x) => `You've been invited to ${x.title}`,
|
|
727
|
+
detailsHeader: "Sign-in details",
|
|
728
|
+
linkLabel: "Sign in",
|
|
729
|
+
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.`,
|
|
730
|
+
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.`,
|
|
731
|
+
rows: (x) => [
|
|
732
|
+
plainRow("Email", x.d.actor || ""),
|
|
733
|
+
...x.d.tempPassword ? [
|
|
734
|
+
{
|
|
735
|
+
label: "Temporary password",
|
|
736
|
+
text: x.d.tempPassword,
|
|
737
|
+
html: x.codeValue(x.d.tempPassword)
|
|
738
|
+
}
|
|
739
|
+
] : []
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
steward: {
|
|
743
|
+
heading: (x) => `You're now a steward of ${x.title}`,
|
|
744
|
+
detailsHeader: "Details",
|
|
745
|
+
linkLabel: "Open nest",
|
|
746
|
+
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.`,
|
|
747
|
+
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.`,
|
|
748
|
+
rows: (x) => [
|
|
749
|
+
plainRow("Nest", x.d.path),
|
|
750
|
+
...x.d.permission ? [plainRow("Role", cap(x.d.permission))] : [],
|
|
751
|
+
...x.d.scopeLabel ? [plainRow("Scope", cap(x.d.scopeLabel))] : []
|
|
752
|
+
]
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
function renderEmail(d) {
|
|
756
|
+
const meta = STATUS_META[d.status];
|
|
757
|
+
const t = TEMPLATES[d.status];
|
|
758
|
+
const title = d.docTitle.replace(/[\r\n]+/g, " ").trim();
|
|
759
|
+
const vLabel = d.version != null ? `v${d.version}` : "";
|
|
760
|
+
const ctx = {
|
|
761
|
+
d,
|
|
762
|
+
title,
|
|
763
|
+
doc: `<strong>${escapeHtml(title)}</strong>`,
|
|
764
|
+
byT: d.actor ? ` by ${d.actor}` : "",
|
|
765
|
+
byH: d.actor ? ` by ${escapeHtml(d.actor)}` : "",
|
|
766
|
+
noteT: d.note ? ` with the note: "${d.note}"` : "",
|
|
767
|
+
noteH: d.note ? ` with the note: “${escapeHtml(d.note)}”` : "",
|
|
768
|
+
vT: vLabel ? ` ${vLabel}` : "",
|
|
769
|
+
vH: vLabel ? ` ${vLabel}` : "",
|
|
770
|
+
meta,
|
|
771
|
+
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>`,
|
|
772
|
+
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>`
|
|
773
|
+
};
|
|
774
|
+
const heading = t.heading(ctx).replace(/[\r\n]+/g, " ").trim();
|
|
775
|
+
const subject = heading;
|
|
776
|
+
const detailsHeader = t.detailsHeader;
|
|
777
|
+
const linkLabel = t.linkLabel;
|
|
778
|
+
const lineH = t.lineH(ctx);
|
|
779
|
+
const rows = t.rows(ctx);
|
|
780
|
+
const detailLines = [
|
|
781
|
+
`${detailsHeader}:`,
|
|
782
|
+
...rows.map((r) => ` ${r.label}: ${r.text}`),
|
|
783
|
+
...d.link ? [` ${linkLabel}: ${d.link}`] : []
|
|
784
|
+
];
|
|
785
|
+
const text = ["Hi,", "", t.lineT(ctx), "", ...detailLines, "", "Regards,", "Community Nest"].join("\n");
|
|
786
|
+
const detailRows = rows.map((r) => detailRow(r.label, r.html)).join("");
|
|
787
|
+
const buttonRow = d.link ? `<tr><td style="padding:20px 24px 0;">
|
|
788
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
|
|
789
|
+
<td bgcolor="${meta.color}" style="border-radius:6px;">
|
|
790
|
+
<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>
|
|
791
|
+
</td>
|
|
792
|
+
</tr></table>
|
|
793
|
+
</td></tr>` : "";
|
|
794
|
+
const html = `<div style="margin:0;padding:24px;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#111827;">
|
|
795
|
+
<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;">
|
|
796
|
+
<tr><td style="padding:20px 24px 0;">
|
|
797
|
+
<div style="font-size:12px;font-weight:700;color:#6b7280;letter-spacing:0.06em;text-transform:uppercase;">ContextNest</div>
|
|
798
|
+
</td></tr>
|
|
799
|
+
<tr><td style="padding:10px 24px 0;">
|
|
800
|
+
<h1 style="margin:0;font-size:19px;line-height:1.35;font-weight:700;color:#111827;">${escapeHtml(heading)}</h1>
|
|
801
|
+
</td></tr>
|
|
802
|
+
<tr><td style="padding:16px 24px 0;font-size:14px;line-height:1.6;color:#374151;">
|
|
803
|
+
Hi,<br><br>${lineH}
|
|
804
|
+
</td></tr>
|
|
805
|
+
<tr><td style="padding:20px 24px 0;">
|
|
806
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #eef0f2;border-radius:8px;">
|
|
807
|
+
<tr><td style="padding:12px 16px;">
|
|
808
|
+
<div style="font-size:11px;font-weight:600;color:#9ca3af;letter-spacing:0.04em;text-transform:uppercase;padding-bottom:4px;">${detailsHeader}</div>
|
|
809
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">${detailRows}</table>
|
|
810
|
+
</td></tr>
|
|
811
|
+
</table>
|
|
812
|
+
</td></tr>
|
|
813
|
+
${buttonRow}
|
|
814
|
+
<tr><td style="padding:22px 24px 24px;font-size:14px;line-height:1.6;color:#374151;">
|
|
815
|
+
Regards,<br>Community Nest
|
|
816
|
+
</td></tr>
|
|
817
|
+
</table>
|
|
818
|
+
</div>`;
|
|
819
|
+
return { subject, text, html };
|
|
820
|
+
}
|
|
821
|
+
function renderHtml(message, nestName2) {
|
|
822
|
+
const lead = message.match(/^(:[a-z_]+:)\s*/);
|
|
823
|
+
const meta = lead && EVENT[lead[1]] || { icon: "\u{1F514}", color: "#475569" };
|
|
824
|
+
const rest = lead ? message.slice(lead[0].length) : message;
|
|
825
|
+
const body = escapeHtml(rest).replace(/\*([^*]+)\*/g, "<strong>$1</strong>").replace(/\n/g, "<br>");
|
|
826
|
+
const safeNest = escapeHtml(nestName2);
|
|
827
|
+
return `<div style="background:#f6f8fa;padding:24px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
828
|
+
<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;">
|
|
829
|
+
<tr><td style="padding:20px 24px 16px;border-bottom:1px solid #f0f0f0;">
|
|
830
|
+
<div style="font-size:15px;font-weight:700;color:#111827;letter-spacing:-0.01em;">ContextNest</div>
|
|
831
|
+
<div style="font-size:13px;color:#6b7280;margin-top:2px;">${safeNest}</div>
|
|
832
|
+
</td></tr>
|
|
833
|
+
<tr><td style="padding:24px;">
|
|
834
|
+
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
|
|
835
|
+
<td valign="top" style="font-size:22px;line-height:1;padding-right:14px;">${meta.icon}</td>
|
|
836
|
+
<td valign="top" style="font-size:15px;line-height:1.55;color:#111827;border-left:3px solid ${meta.color};padding-left:14px;">${body}</td>
|
|
837
|
+
</tr></table>
|
|
838
|
+
</td></tr>
|
|
839
|
+
</table>
|
|
840
|
+
</div>`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/notify/email.ts
|
|
844
|
+
var transporterPromise = null;
|
|
845
|
+
var configuredUrl = null;
|
|
846
|
+
var warnedOnce = false;
|
|
847
|
+
var SMTP_TIMEOUT_DEFAULTS = [
|
|
848
|
+
["connectionTimeout", 5e3],
|
|
849
|
+
["greetingTimeout", 5e3],
|
|
850
|
+
["socketTimeout", 1e4]
|
|
851
|
+
];
|
|
852
|
+
function withTimeouts(url) {
|
|
853
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
854
|
+
const missing = SMTP_TIMEOUT_DEFAULTS.filter(
|
|
855
|
+
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
856
|
+
).map(([name, ms]) => `${name}=${ms}`);
|
|
857
|
+
if (missing.length === 0) return url;
|
|
858
|
+
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
859
|
+
}
|
|
860
|
+
var POOL_DEFAULTS = [
|
|
861
|
+
["pool", "true"],
|
|
862
|
+
["maxConnections", "1"],
|
|
863
|
+
["rateDelta", "1000"],
|
|
864
|
+
["rateLimit", "8"]
|
|
865
|
+
// messages per second
|
|
866
|
+
];
|
|
867
|
+
function withPool(url) {
|
|
868
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
|
|
869
|
+
const missing = POOL_DEFAULTS.filter(
|
|
870
|
+
([name]) => !new RegExp(`(^|&)${name}=`, "i").test(query)
|
|
871
|
+
).map(([name, val]) => `${name}=${val}`);
|
|
872
|
+
if (missing.length === 0) return url;
|
|
873
|
+
return url + (url.includes("?") ? "&" : "?") + missing.join("&");
|
|
874
|
+
}
|
|
875
|
+
async function verifySmtp(url) {
|
|
876
|
+
let transporter = null;
|
|
877
|
+
try {
|
|
878
|
+
const nodemailer = await import("nodemailer");
|
|
879
|
+
transporter = nodemailer.default.createTransport(withTimeouts(url), {
|
|
880
|
+
disableFileAccess: true,
|
|
881
|
+
disableUrlAccess: true
|
|
882
|
+
});
|
|
883
|
+
await transporter.verify();
|
|
884
|
+
return { ok: true };
|
|
885
|
+
} catch (err) {
|
|
886
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
887
|
+
} finally {
|
|
888
|
+
try {
|
|
889
|
+
transporter?.close?.();
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function getTransporter() {
|
|
895
|
+
const url = config.SMTP_URL;
|
|
896
|
+
if (!url) return null;
|
|
897
|
+
if (!transporterPromise || configuredUrl !== url) {
|
|
898
|
+
configuredUrl = url;
|
|
899
|
+
transporterPromise = import("nodemailer").then(
|
|
900
|
+
(nodemailer) => (
|
|
901
|
+
// withPool → connection pool + per-second rate limit, so a burst queues
|
|
902
|
+
// instead of tripping the provider's "too many emails" 550.
|
|
903
|
+
nodemailer.default.createTransport(withPool(withTimeouts(url)), {
|
|
904
|
+
// Per-message defaults (belt-and-braces; also set on each sendMail).
|
|
905
|
+
disableFileAccess: true,
|
|
906
|
+
disableUrlAccess: true
|
|
907
|
+
})
|
|
908
|
+
)
|
|
909
|
+
);
|
|
910
|
+
transporterPromise.catch(() => {
|
|
911
|
+
transporterPromise = null;
|
|
912
|
+
configuredUrl = null;
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return transporterPromise;
|
|
916
|
+
}
|
|
917
|
+
async function notifyEmailForNest(nestId, message, details) {
|
|
918
|
+
const from = config.NOTIFY_EMAIL_FROM;
|
|
919
|
+
const to = config.NOTIFY_EMAIL_TO;
|
|
920
|
+
if (!config.SMTP_URL || !from || !to) return;
|
|
921
|
+
try {
|
|
922
|
+
const transporter = await getTransporter();
|
|
923
|
+
if (!transporter) return;
|
|
924
|
+
let subject;
|
|
925
|
+
let text;
|
|
926
|
+
let html;
|
|
927
|
+
if (details) {
|
|
928
|
+
({ subject, text, html } = renderEmail(details));
|
|
929
|
+
} else {
|
|
930
|
+
let name = nestId;
|
|
931
|
+
try {
|
|
932
|
+
const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
|
|
933
|
+
nestId
|
|
934
|
+
]);
|
|
935
|
+
if (row?.name) name = row.name;
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
const safeName = name.replace(/[\r\n]+/g, " ").trim();
|
|
939
|
+
subject = `ContextNest \xB7 ${safeName}`;
|
|
940
|
+
text = renderText(message);
|
|
941
|
+
html = renderHtml(message, safeName);
|
|
942
|
+
}
|
|
943
|
+
await transporter.sendMail({
|
|
944
|
+
from,
|
|
945
|
+
to,
|
|
946
|
+
subject,
|
|
947
|
+
text,
|
|
948
|
+
html,
|
|
949
|
+
disableFileAccess: true,
|
|
950
|
+
disableUrlAccess: true
|
|
951
|
+
});
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (!warnedOnce) {
|
|
954
|
+
warnedOnce = true;
|
|
955
|
+
console.warn(
|
|
956
|
+
"[email] notification failed (suppressing further warnings):",
|
|
957
|
+
err instanceof Error ? err.message : err
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
963
|
+
function recipientEnvelope(to, from) {
|
|
964
|
+
const recipients = Array.isArray(to) ? to : [to];
|
|
965
|
+
return recipients.length > 1 ? { to: from, bcc: recipients } : { to: recipients[0] };
|
|
966
|
+
}
|
|
967
|
+
function isRateLimited(err) {
|
|
968
|
+
const msg = String(err?.message ?? err);
|
|
969
|
+
const code = Number(err?.responseCode);
|
|
970
|
+
return /too many|rate limit|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);
|
|
971
|
+
}
|
|
972
|
+
async function sendMailWithRetry(transporter, msg, attempts = 5) {
|
|
973
|
+
for (let i = 0; ; i++) {
|
|
974
|
+
try {
|
|
975
|
+
return await transporter.sendMail(msg);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
if (i >= attempts - 1 || !isRateLimited(err)) throw err;
|
|
978
|
+
const wait = 1e3 * 2 ** i;
|
|
979
|
+
await sleep(wait);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function sendEmailToRecipient(to, details) {
|
|
984
|
+
const from = config.NOTIFY_EMAIL_FROM;
|
|
985
|
+
const hasRecipient = Array.isArray(to) ? to.length > 0 : !!to;
|
|
986
|
+
if (!config.SMTP_URL || !from || !hasRecipient) return;
|
|
987
|
+
try {
|
|
988
|
+
const transporter = await getTransporter();
|
|
989
|
+
if (!transporter) return;
|
|
990
|
+
const { subject, text, html } = renderEmail(details);
|
|
991
|
+
await sendMailWithRetry(transporter, {
|
|
992
|
+
from,
|
|
993
|
+
...recipientEnvelope(to, from),
|
|
994
|
+
subject,
|
|
995
|
+
text,
|
|
996
|
+
html,
|
|
997
|
+
disableFileAccess: true,
|
|
998
|
+
disableUrlAccess: true
|
|
999
|
+
});
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
if (!warnedOnce) {
|
|
1002
|
+
warnedOnce = true;
|
|
1003
|
+
console.warn(
|
|
1004
|
+
"[email] notification failed (suppressing further warnings):",
|
|
1005
|
+
err instanceof Error ? err.message : err
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/governance/teams-service.ts
|
|
644
1012
|
var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
|
|
645
1013
|
function parseMembers(json) {
|
|
646
1014
|
if (!json) return [];
|
|
@@ -929,7 +1297,8 @@ async function listNestTeams(nestId) {
|
|
|
929
1297
|
}
|
|
930
1298
|
async function shareTeamWithNest(params) {
|
|
931
1299
|
const db = getDb();
|
|
932
|
-
|
|
1300
|
+
let sharedMembers = [];
|
|
1301
|
+
const refs = await db.transaction(async (tx) => {
|
|
933
1302
|
const team = await tx.get(
|
|
934
1303
|
"SELECT id, name, owner_id, members FROM teams WHERE id = ?",
|
|
935
1304
|
[params.teamId]
|
|
@@ -944,16 +1313,18 @@ async function shareTeamWithNest(params) {
|
|
|
944
1313
|
params.nestId
|
|
945
1314
|
]);
|
|
946
1315
|
if (!nest) throw new NotFoundError("Nest not found");
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
1316
|
+
const refs2 = parseSharedTeams(nest.shared_teams);
|
|
1317
|
+
if (refs2.some((r) => r.teamId === params.teamId)) {
|
|
949
1318
|
throw new ConflictError("This team is already shared with the nest.");
|
|
950
1319
|
}
|
|
951
|
-
|
|
1320
|
+
refs2.push({ teamId: team.id, teamName: team.name });
|
|
952
1321
|
await tx.run("UPDATE nests SET shared_teams = ? WHERE id = ?", [
|
|
953
|
-
JSON.stringify(
|
|
1322
|
+
JSON.stringify(refs2),
|
|
954
1323
|
params.nestId
|
|
955
1324
|
]);
|
|
956
|
-
const
|
|
1325
|
+
const members = parseMembers(team.members);
|
|
1326
|
+
sharedMembers = members;
|
|
1327
|
+
const hasGovernanceRole = members.some(
|
|
957
1328
|
(m) => m.role === "viewer" || m.role === "editor"
|
|
958
1329
|
);
|
|
959
1330
|
if (hasGovernanceRole) {
|
|
@@ -961,8 +1332,37 @@ async function shareTeamWithNest(params) {
|
|
|
961
1332
|
params.nestId
|
|
962
1333
|
]);
|
|
963
1334
|
}
|
|
964
|
-
return
|
|
1335
|
+
return refs2;
|
|
965
1336
|
});
|
|
1337
|
+
try {
|
|
1338
|
+
const caller = await db.get("SELECT email FROM users WHERE id = ?", [
|
|
1339
|
+
params.callerUserId
|
|
1340
|
+
]);
|
|
1341
|
+
const callerEmail = caller?.email?.toLowerCase();
|
|
1342
|
+
const shareNestName = await nestName(params.nestId);
|
|
1343
|
+
const link = docLink(params.nestId, void 0, params.baseUrl);
|
|
1344
|
+
const byRole = /* @__PURE__ */ new Map();
|
|
1345
|
+
for (const m of sharedMembers) {
|
|
1346
|
+
if (!m.email || m.email.toLowerCase() === callerEmail) continue;
|
|
1347
|
+
const arr = byRole.get(m.role) ?? [];
|
|
1348
|
+
arr.push(m.email);
|
|
1349
|
+
byRole.set(m.role, arr);
|
|
1350
|
+
}
|
|
1351
|
+
for (const [role, emails] of byRole) {
|
|
1352
|
+
if (emails.length === 0) continue;
|
|
1353
|
+
void sendEmailToRecipient(emails, {
|
|
1354
|
+
status: "shared",
|
|
1355
|
+
docTitle: shareNestName,
|
|
1356
|
+
path: shareNestName,
|
|
1357
|
+
link,
|
|
1358
|
+
permission: role,
|
|
1359
|
+
by: caller?.email || void 0
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
console.error("[notify] team-share email fan-out failed", params.nestId, err);
|
|
1364
|
+
}
|
|
1365
|
+
return refs;
|
|
966
1366
|
}
|
|
967
1367
|
async function unshareTeamFromNest(params) {
|
|
968
1368
|
const db = getDb();
|
|
@@ -1841,6 +2241,32 @@ async function createStewardRecord(params) {
|
|
|
1841
2241
|
"UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
|
|
1842
2242
|
[params.nestId]
|
|
1843
2243
|
);
|
|
2244
|
+
if (results.length > 0) {
|
|
2245
|
+
let scopeLabel;
|
|
2246
|
+
if (params.scope === "document") {
|
|
2247
|
+
const titles = await buildTitleMap(params.nestId);
|
|
2248
|
+
const docTitle = titles.get(nodePattern) || nodePattern;
|
|
2249
|
+
const capitalized = docTitle.charAt(0).toUpperCase() + docTitle.slice(1);
|
|
2250
|
+
scopeLabel = `"${capitalized}" document`;
|
|
2251
|
+
} else if (params.scope === "tag") {
|
|
2252
|
+
scopeLabel = `#${tagName}`;
|
|
2253
|
+
} else {
|
|
2254
|
+
scopeLabel = "the whole nest";
|
|
2255
|
+
}
|
|
2256
|
+
const shareNestName = await nestName(params.nestId);
|
|
2257
|
+
const link = docLink(params.nestId, void 0, params.baseUrl);
|
|
2258
|
+
for (const s of results) {
|
|
2259
|
+
void sendEmailToRecipient(s.userEmail, {
|
|
2260
|
+
status: "steward",
|
|
2261
|
+
docTitle: shareNestName,
|
|
2262
|
+
path: shareNestName,
|
|
2263
|
+
permission: s.role,
|
|
2264
|
+
scopeLabel,
|
|
2265
|
+
by: params.assignedBy || void 0,
|
|
2266
|
+
link
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
1844
2270
|
return results;
|
|
1845
2271
|
}
|
|
1846
2272
|
async function resolveStewardsForNode(nestId, nodeId) {
|
|
@@ -2177,24 +2603,9 @@ export {
|
|
|
2177
2603
|
getAccessConfig,
|
|
2178
2604
|
isSuperAdmin,
|
|
2179
2605
|
isConfigSuperAdmin,
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
getTeam,
|
|
2184
|
-
renameTeam,
|
|
2185
|
-
deleteTeam,
|
|
2186
|
-
addMember,
|
|
2187
|
-
resolveMemberIdentity,
|
|
2188
|
-
replaceTeamRoster,
|
|
2189
|
-
updateMemberRole,
|
|
2190
|
-
removeMember,
|
|
2191
|
-
listNestTeams,
|
|
2192
|
-
shareTeamWithNest,
|
|
2193
|
-
unshareTeamFromNest,
|
|
2194
|
-
isServerAdminUserId,
|
|
2195
|
-
resolveNestPermission,
|
|
2196
|
-
permissionLevel,
|
|
2197
|
-
isPublicReader,
|
|
2606
|
+
verifySmtp,
|
|
2607
|
+
notifyEmailForNest,
|
|
2608
|
+
sendEmailToRecipient,
|
|
2198
2609
|
nestStorageRoot,
|
|
2199
2610
|
resolveNestPath,
|
|
2200
2611
|
uniqueNestName,
|
|
@@ -2218,6 +2629,24 @@ export {
|
|
|
2218
2629
|
nestName,
|
|
2219
2630
|
docLink,
|
|
2220
2631
|
buildDocContext,
|
|
2632
|
+
createTeam,
|
|
2633
|
+
findTeamByExternalId,
|
|
2634
|
+
listTeamsForUser,
|
|
2635
|
+
getTeam,
|
|
2636
|
+
renameTeam,
|
|
2637
|
+
deleteTeam,
|
|
2638
|
+
addMember,
|
|
2639
|
+
resolveMemberIdentity,
|
|
2640
|
+
replaceTeamRoster,
|
|
2641
|
+
updateMemberRole,
|
|
2642
|
+
removeMember,
|
|
2643
|
+
listNestTeams,
|
|
2644
|
+
shareTeamWithNest,
|
|
2645
|
+
unshareTeamFromNest,
|
|
2646
|
+
isServerAdminUserId,
|
|
2647
|
+
resolveNestPermission,
|
|
2648
|
+
permissionLevel,
|
|
2649
|
+
isPublicReader,
|
|
2221
2650
|
assignSteward,
|
|
2222
2651
|
removeSteward,
|
|
2223
2652
|
updateSteward,
|