@snagset/server 0.1.10 → 0.1.11
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/dist/app.js +5 -0
- package/dist/app.js.map +1 -1
- package/dist/config.generated.js +2 -1
- package/dist/config.generated.js.map +1 -1
- package/dist/db/migrations/0008_outbox_org_rows.sql +29 -0
- package/dist/jobs/set.js +15 -4
- package/dist/jobs/set.js.map +1 -1
- package/dist/mail/digest.js +291 -0
- package/dist/mail/digest.js.map +1 -0
- package/dist/mail/links.js +41 -0
- package/dist/mail/links.js.map +1 -0
- package/dist/mail/notify.js +126 -25
- package/dist/mail/notify.js.map +1 -1
- package/dist/net/publicFetch.js +202 -0
- package/dist/net/publicFetch.js.map +1 -0
- package/dist/routes/commentEdit.js +20 -5
- package/dist/routes/commentEdit.js.map +1 -1
- package/dist/routes/imageProxy.js +77 -0
- package/dist/routes/imageProxy.js.map +1 -0
- package/dist/routes/threads.js +182 -42
- package/dist/routes/threads.js.map +1 -1
- package/package.json +3 -3
- package/static/dashboard/assets/CompositeItem-Bg_GF93m.js +1 -0
- package/static/dashboard/assets/{ConfirmDialog-eCzw7vjK.js → ConfirmDialog-WnZ92dk2.js} +1 -1
- package/static/dashboard/assets/{CopyButton-lcjux1TA.js → CopyButton-rL26RLOJ.js} +1 -1
- package/static/dashboard/assets/{FieldItemContext-i5zdyQYl.js → FieldItemContext-BFJSEdB-.js} +1 -1
- package/static/dashboard/assets/{InstallTag-Eqs3RM9e.js → InstallTag-BKIArjPk.js} +1 -1
- package/static/dashboard/assets/{SaveState-Cf_bnJmg.js → SaveState-DVHi0zeB.js} +1 -1
- package/static/dashboard/assets/{_app-CuKLSKZ2.js → _app-D2r-zfbF.js} +1 -1
- package/static/dashboard/assets/_app-iK-weT5A.js +56 -0
- package/static/dashboard/assets/activity-CCqTW3Th.js +1 -0
- package/static/dashboard/assets/{arrow-left-CQUHEFUP.js → arrow-left-BEHXEKLy.js} +1 -1
- package/static/dashboard/assets/{avatar-CWT37Ivf.js → avatar-BnPBt2UW.js} +1 -1
- package/static/dashboard/assets/{badge-thtKAJ0h.js → badge-B2rHdH8b.js} +1 -1
- package/static/dashboard/assets/{card-CsXE7vKD.js → card-BNGTf8dq.js} +1 -1
- package/static/dashboard/assets/{check-Bc_1fmPx.js → check-C3gaSIDM.js} +1 -1
- package/static/dashboard/assets/{chevron-right-neQQkaOt.js → chevron-right-CLLS6JcX.js} +1 -1
- package/static/dashboard/assets/{copy-O3USBatC.js → copy-Dy7nBeu2.js} +1 -1
- package/static/dashboard/assets/{dialog-MC-TOvQV.js → dialog-q6J2VL2H.js} +2 -2
- package/static/dashboard/assets/{dropdown-menu-C2ZhmONu.js → dropdown-menu-DFZrgFpI.js} +1 -1
- package/static/dashboard/assets/{image-off-Bu6l8gP5.js → image-off-Bug-3iC-.js} +1 -1
- package/static/dashboard/assets/index-ClhJtAFR.css +1 -0
- package/static/dashboard/assets/index-CtyJ8ep7.js +169 -0
- package/static/dashboard/assets/{label-BTdAT39L.js → label-DoQ07Q_y.js} +1 -1
- package/static/dashboard/assets/{layout-grid-rYWIGaIJ.js → layout-grid-B3BkFR60.js} +1 -1
- package/static/dashboard/assets/me-NFjosbdz.js +1 -0
- package/static/dashboard/assets/{parts-kPNZVO9H.js → parts-BTWw4Uv-.js} +1 -1
- package/static/dashboard/assets/{plus-CcJ1Zwa7.js → plus-DqdyxL8q.js} +1 -1
- package/static/dashboard/assets/s._site-kVvU6ucw.js +1 -0
- package/static/dashboard/assets/s._site.comments-kEGEpFR2.js +54 -0
- package/static/dashboard/assets/{s._site.reviewers-Djq2t6Dx.js → s._site.reviewers-C1LLoSS5.js} +1 -1
- package/static/dashboard/assets/s._site.settings-CeKMuuRo.js +1 -0
- package/static/dashboard/assets/{setup-D748SEIm.js → setup-CdZYVZmd.js} +1 -1
- package/static/dashboard/assets/{sign-in-CKSiZWCo.js → sign-in-CvstpETe.js} +1 -1
- package/static/dashboard/assets/{sign-up-BX9j_Ya2.js → sign-up-Cq7YvdYN.js} +1 -1
- package/static/dashboard/assets/sites-CUWMKMCM.js +1 -0
- package/static/dashboard/assets/{studio-HH_J6PPv.js → studio-BfA8kzFc.js} +1 -1
- package/static/dashboard/assets/studio-uAl1i7Nd.js +1 -0
- package/static/dashboard/assets/studio.index-BTF7YIX-.js +1 -0
- package/static/dashboard/assets/studio.integrations-Nr_jBcXh.js +1 -0
- package/static/dashboard/assets/{studio.members-DTbKcrEI.js → studio.members-BtbPt4iX.js} +1 -1
- package/static/dashboard/assets/{sun-CWPmS6_O.js → sun-s_t6Zx3r.js} +1 -1
- package/static/dashboard/assets/switch-DbqX1mWE.js +1 -0
- package/static/dashboard/assets/{table-CiZO2uuW.js → table-CrWNqpP8.js} +1 -1
- package/static/dashboard/assets/{useAriaLabelledBy-oZFqC809.js → useAriaLabelledBy-CgL3SaJd.js} +1 -1
- package/static/dashboard/assets/{useBaseQuery-CHgLQD-M.js → useBaseQuery-CBTINzcX.js} +1 -1
- package/static/dashboard/assets/useQuery-BPuxCfhJ.js +1 -0
- package/static/dashboard/assets/useRender-BCpbp3WG.js +1 -0
- package/static/dashboard/assets/{useValueChanged-Dvr7PvAV.js → useValueChanged-B20GM2vK.js} +1 -1
- package/static/dashboard/assets/{users-Ci-ogubz.js → users-CQwkq_Tk.js} +1 -1
- package/static/dashboard/assets/{welcome-BDWw5Qlv.js → welcome-W1YxYlGW.js} +1 -1
- package/static/dashboard/assets/{you-DAUNHAFI.js → you-DXZRdbGS.js} +1 -1
- package/static/dashboard/assets/{zod-CtjFmGX5.js → zod-DWEgoEmm.js} +1 -1
- package/static/dashboard/index.html +2 -2
- package/static/widget/snag.capture.js +1 -1
- package/static/widget/snag.dock.js +1 -1
- package/static/widget/snag.runtime.js +2 -2
- package/static/widget/snag.screenshot.js +1 -1
- package/static/dashboard/assets/CompositeItem-Db_8iHSG.js +0 -1
- package/static/dashboard/assets/_app-qNVG5lsX.js +0 -56
- package/static/dashboard/assets/activity-CQLBFIV4.js +0 -1
- package/static/dashboard/assets/index-BR4Mnw-o.css +0 -1
- package/static/dashboard/assets/index-C6gyyLd5.js +0 -169
- package/static/dashboard/assets/me-CI9giaT9.js +0 -1
- package/static/dashboard/assets/s._site-D0HRtia-.js +0 -1
- package/static/dashboard/assets/s._site.comments-D3ACsr3a.js +0 -39
- package/static/dashboard/assets/s._site.settings-D0b6vJci.js +0 -1
- package/static/dashboard/assets/sites-QwJy87Y8.js +0 -1
- package/static/dashboard/assets/studio-B_ma5JGA.js +0 -1
- package/static/dashboard/assets/studio.index-DrtJF7DT.js +0 -1
- package/static/dashboard/assets/studio.integrations-Cv34ukzT.js +0 -1
- package/static/dashboard/assets/switch-CzijGUed.js +0 -1
- package/static/dashboard/assets/useQuery-C6FyjMS-.js +0 -1
- package/static/dashboard/assets/useRender-D5jZ1wns.js +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a notification's link goes: the thread, open, in the dashboard (D120).
|
|
3
|
+
*
|
|
4
|
+
* Until D120 every mail linked `<address>/dashboard`, and the dashboard is
|
|
5
|
+
* served at the ROOT of the address (`app.ts`'s fallback, 08 §3), so that link
|
|
6
|
+
* landed on a route the SPA does not have. The comments page has always taken
|
|
7
|
+
* the thread as a search parameter — every view there is a link, D87 — and the
|
|
8
|
+
* round with it, because the list shows the latest round unless told
|
|
9
|
+
* otherwise, and a thread from an earlier round would open to nothing.
|
|
10
|
+
*
|
|
11
|
+
* One function for both mails, the per-comment one and the digest, so the two
|
|
12
|
+
* cannot drift into linking different places for the same thread.
|
|
13
|
+
*/
|
|
14
|
+
export function threadUrl(baseUrl, site, key, round) {
|
|
15
|
+
const q = new URLSearchParams({ thread: key });
|
|
16
|
+
if (round !== null)
|
|
17
|
+
q.set("round", String(round));
|
|
18
|
+
return `${baseUrl}/s/${encodeURIComponent(site)}/comments?${q.toString()}`;
|
|
19
|
+
}
|
|
20
|
+
/** The dashboard itself — what a mail links to when it is about no one thread. */
|
|
21
|
+
export function dashboardUrl(baseUrl) {
|
|
22
|
+
return `${baseUrl}/`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The link for a row that names a thread, read at SEND time: the round and
|
|
26
|
+
* the site's uid are not in the event, and a row queued before D120 has
|
|
27
|
+
* neither. A thread that has gone since falls back to the dashboard rather
|
|
28
|
+
* than to a link that opens nothing.
|
|
29
|
+
*/
|
|
30
|
+
export async function threadLink(sql, threadId, baseUrl) {
|
|
31
|
+
if (!threadId)
|
|
32
|
+
return dashboardUrl(baseUrl);
|
|
33
|
+
const [t] = await sql `
|
|
34
|
+
SELECT p.uid, t.key, r.number AS round
|
|
35
|
+
FROM threads t
|
|
36
|
+
JOIN projects p ON p.id = t.project_id
|
|
37
|
+
LEFT JOIN rounds r ON r.id = t.round_id
|
|
38
|
+
WHERE t.id = ${threadId}::uuid AND t.deleted_at IS NULL`;
|
|
39
|
+
return t ? threadUrl(baseUrl, t.uid, t.key, t.round) : dashboardUrl(baseUrl);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/mail/links.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,IAAY,EAAE,GAAW,EAAE,KAAoB;IACxF,MAAM,CAAC,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/C,IAAI,KAAK,KAAK,IAAI;QAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAClD,OAAO,GAAG,OAAO,MAAM,kBAAkB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC7E,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,GAAG,OAAO,GAAG,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAA2C,EAC3C,QAAuB,EACvB,OAAe;IAEf,IAAI,CAAC,QAAQ;QAAE,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAsD;;;;;oBAKzD,QAAQ,iCAAiC,CAAC;IAC5D,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;AAC/E,CAAC"}
|
package/dist/mail/notify.js
CHANGED
|
@@ -4,6 +4,8 @@ import { redactText } from "../vendor/widget/predicates.js";
|
|
|
4
4
|
import { noteQueued } from "../jobs/soon.js";
|
|
5
5
|
import { isConfigured } from "./mailer.js";
|
|
6
6
|
import { unsubscribeHeaders, unsubscribeToken } from "./unsubscribe.js";
|
|
7
|
+
import { DIGEST_TOPIC, pruneDigest, renderDigest } from "./digest.js";
|
|
8
|
+
import { threadLink } from "./links.js";
|
|
7
9
|
/**
|
|
8
10
|
* Notification email — L1.4, and deliberately ONE event.
|
|
9
11
|
*
|
|
@@ -16,6 +18,13 @@ import { unsubscribeHeaders, unsubscribeToken } from "./unsubscribe.js";
|
|
|
16
18
|
* does not exist yet. The rest lands with full M2-32, behind subscription and
|
|
17
19
|
* unsubscribe.
|
|
18
20
|
*
|
|
21
|
+
* Since D120 the owner's half arrives as a DAILY DIGEST by default
|
|
22
|
+
* (`SNAGSET_MAIL_NOTIFY=digest`, 08 §4.8): the rows below are queued exactly
|
|
23
|
+
* as before, the delivery job leaves them alone, and `digest.ts` folds a day's
|
|
24
|
+
* worth into one outbox row of its own. `each` restores one mail per comment.
|
|
25
|
+
* In both modes nobody is mailed about a comment they wrote — see
|
|
26
|
+
* `queueCommentNotice`.
|
|
27
|
+
*
|
|
19
28
|
* ── HOW "A RETRY NEVER DOUBLE-SENDS" IS ACTUALLY ACHIEVED ──
|
|
20
29
|
*
|
|
21
30
|
* Two mechanisms, and both are needed:
|
|
@@ -58,18 +67,37 @@ export async function queueCommentNotice(tx, notice, id) {
|
|
|
58
67
|
pageUrl: notice.pageUrl,
|
|
59
68
|
commentId: notice.commentId,
|
|
60
69
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const owner = await ownerOf(tx, notice.orgId);
|
|
71
|
+
const author = await reviewerIdentity(tx, notice.authorId);
|
|
72
|
+
/**
|
|
73
|
+
* THE OWNER's row — unless the owner wrote this comment (D120).
|
|
74
|
+
*
|
|
75
|
+
* "If there is a comment we made, we don't need to receive the email." The
|
|
76
|
+
* owner writes in two ways and both are theirs: from the dashboard, where
|
|
77
|
+
* the author is their studio reviewer row (`user_id` set), and through the
|
|
78
|
+
* widget on their own site, where the author is whatever reviewer identity
|
|
79
|
+
* they typed — "eurico-test" — with their own address in it. The first is
|
|
80
|
+
* matched by user id, the second by address, case-insensitively, because an
|
|
81
|
+
* address typed into a widget has whatever capitals the hand gave it.
|
|
82
|
+
*
|
|
83
|
+
* Decided HERE, at enqueue, rather than at send: the row that is never
|
|
84
|
+
* written is the one that cannot be sent in `each` mode and cannot be folded
|
|
85
|
+
* into a digest in `digest` mode, so one check covers both.
|
|
86
|
+
*
|
|
87
|
+
* No recipient in the event: the delivery job reads that absence as "the
|
|
88
|
+
* project owner", which is what every row queued before the fan-out existed
|
|
89
|
+
* also means. Backward compatibility by shape rather than by version flag.
|
|
90
|
+
*/
|
|
91
|
+
if (!isSamePerson(author, owner)) {
|
|
92
|
+
await tx `
|
|
93
|
+
INSERT INTO outbox (id, org_id, project_id, thread_id, provider, topic, dedupe_key, event)
|
|
94
|
+
VALUES (
|
|
95
|
+
${id}, ${notice.orgId}, ${notice.projectId}, ${notice.threadId},
|
|
96
|
+
'email', ${TOPIC}, ${`${TOPIC}:${notice.commentId}`}, ${tx.json(payload)}
|
|
97
|
+
)
|
|
98
|
+
ON CONFLICT (provider, dedupe_key) DO NOTHING
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
73
101
|
/**
|
|
74
102
|
* …and ONE ROW PER PARTICIPANT — B2's "created or replied".
|
|
75
103
|
*
|
|
@@ -87,6 +115,12 @@ export async function queueCommentNotice(tx, notice, id) {
|
|
|
87
115
|
* Excluded: the author of this very comment (nobody needs mail about their
|
|
88
116
|
* own typing), anyone with no address, anyone who unsubscribed, and anyone
|
|
89
117
|
* blocked or erased.
|
|
118
|
+
*
|
|
119
|
+
* …and THE OWNER, under any identity (D120). The owner already has the row
|
|
120
|
+
* above; if they have also replied on this thread from the dashboard, their
|
|
121
|
+
* studio reviewer row is a participant too, and without this they got the
|
|
122
|
+
* same comment twice — and in `digest` mode the participant copy would have
|
|
123
|
+
* arrived at once, around the digest that exists to stop exactly that.
|
|
90
124
|
*/
|
|
91
125
|
await fanOut(tx, {
|
|
92
126
|
orgId: notice.orgId,
|
|
@@ -95,6 +129,8 @@ export async function queueCommentNotice(tx, notice, id) {
|
|
|
95
129
|
topic: TOPIC,
|
|
96
130
|
keyPrefix: `${TOPIC}:${notice.commentId}`,
|
|
97
131
|
exclude: notice.authorId,
|
|
132
|
+
excludeEmail: author?.email ?? null,
|
|
133
|
+
excludeOwner: owner,
|
|
98
134
|
payload,
|
|
99
135
|
});
|
|
100
136
|
// D119: the request drains this once its response is ready.
|
|
@@ -109,6 +145,9 @@ export async function queueCommentNotice(tx, notice, id) {
|
|
|
109
145
|
* somebody who did not ask.
|
|
110
146
|
*/
|
|
111
147
|
async function fanOut(tx, o) {
|
|
148
|
+
const email = o.excludeEmail ?? null;
|
|
149
|
+
const ownerUser = o.excludeOwner?.userId ?? null;
|
|
150
|
+
const ownerEmail = o.excludeOwner?.email ?? null;
|
|
112
151
|
const participants = await tx `
|
|
113
152
|
SELECT DISTINCT r.id
|
|
114
153
|
FROM comments c
|
|
@@ -120,6 +159,9 @@ async function fanOut(tx, o) {
|
|
|
120
159
|
AND r.notifications_off_at IS NULL
|
|
121
160
|
AND r.blocked_at IS NULL
|
|
122
161
|
AND r.erased_at IS NULL
|
|
162
|
+
AND (${email}::text IS NULL OR lower(r.email) <> lower(${email}::text))
|
|
163
|
+
AND (${ownerUser}::uuid IS NULL OR r.user_id IS DISTINCT FROM ${ownerUser}::uuid)
|
|
164
|
+
AND (${ownerEmail}::text IS NULL OR lower(r.email) <> lower(${ownerEmail}::text))
|
|
123
165
|
`;
|
|
124
166
|
for (const p of participants) {
|
|
125
167
|
await tx `
|
|
@@ -133,6 +175,40 @@ async function fanOut(tx, o) {
|
|
|
133
175
|
`;
|
|
134
176
|
}
|
|
135
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* THE owner of an org — the first `owner` membership, as it has always been
|
|
180
|
+
* chosen for owner mail. One query in one place, because "who is the owner"
|
|
181
|
+
* answered differently at enqueue (am I mailing myself?) and at send (whom do
|
|
182
|
+
* I mail?) is how the self-check passes and the mail still arrives.
|
|
183
|
+
*/
|
|
184
|
+
export async function ownerOf(sql, orgId) {
|
|
185
|
+
const [owner] = await sql `
|
|
186
|
+
SELECT u.id AS user_id, u.email
|
|
187
|
+
FROM memberships m
|
|
188
|
+
JOIN users u ON u.id = m.user_id
|
|
189
|
+
WHERE m.org_id = ${orgId} AND m.role = 'owner' AND u.deleted_at IS NULL
|
|
190
|
+
ORDER BY m.created_at
|
|
191
|
+
LIMIT 1`;
|
|
192
|
+
return owner ? { userId: owner.user_id, email: owner.email } : null;
|
|
193
|
+
}
|
|
194
|
+
/** A reviewer row's link to a person: the dashboard user behind it, and its address. */
|
|
195
|
+
async function reviewerIdentity(sql, reviewerId) {
|
|
196
|
+
const [r] = await sql `
|
|
197
|
+
SELECT user_id, email FROM reviewers WHERE id = ${reviewerId}::uuid`;
|
|
198
|
+
return r ? { userId: r.user_id, email: r.email } : null;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Is this reviewer identity the owner? By user id first — the studio row the
|
|
202
|
+
* dashboard writes with — and then by address, case-insensitively, which is
|
|
203
|
+
* what catches the owner reviewing their own site through the widget.
|
|
204
|
+
*/
|
|
205
|
+
export function isSamePerson(who, owner) {
|
|
206
|
+
if (!who || !owner)
|
|
207
|
+
return false;
|
|
208
|
+
if (who.userId && who.userId === owner.userId)
|
|
209
|
+
return true;
|
|
210
|
+
return !!who.email && who.email.trim().toLowerCase() === owner.email.trim().toLowerCase();
|
|
211
|
+
}
|
|
136
212
|
const RESOLVED = "thread.resolved";
|
|
137
213
|
/**
|
|
138
214
|
* "Somebody marked your snag resolved" — B2's second topic.
|
|
@@ -157,6 +233,8 @@ export async function queueResolvedNotice(tx, notice) {
|
|
|
157
233
|
topic: RESOLVED,
|
|
158
234
|
keyPrefix: `${RESOLVED}:${notice.threadId}:${notice.version}`,
|
|
159
235
|
exclude: notice.actorId,
|
|
236
|
+
// The person who clicked, under any identity — the same rule as a comment.
|
|
237
|
+
excludeEmail: (await reviewerIdentity(tx, notice.actorId))?.email ?? null,
|
|
160
238
|
payload: {
|
|
161
239
|
threadKey: notice.threadKey,
|
|
162
240
|
actorName: notice.actorName,
|
|
@@ -173,8 +251,14 @@ const BACKOFF_MIN = [1, 5, 15, 30, 60];
|
|
|
173
251
|
/**
|
|
174
252
|
* The delivery job. Registered only when a mailer is configured — with none,
|
|
175
253
|
* there is nothing to drain and the absence is reported at boot instead.
|
|
254
|
+
*
|
|
255
|
+
* `notify` defaults to `each` HERE — the job's behaviour before D120, for a
|
|
256
|
+
* caller that does not say — while the PRODUCT's default is `digest`, which
|
|
257
|
+
* `jobs/set.ts` reads from the registry. The registry is where a default an
|
|
258
|
+
* operator can see belongs; a function's default is only what it does when
|
|
259
|
+
* nobody asked.
|
|
176
260
|
*/
|
|
177
|
-
export function deliverEmail(mailer, address, keys) {
|
|
261
|
+
export function deliverEmail(mailer, address, keys, notify = "each") {
|
|
178
262
|
return {
|
|
179
263
|
name: "deliver-email",
|
|
180
264
|
everyMs: 30_000,
|
|
@@ -187,6 +271,11 @@ export function deliverEmail(mailer, address, keys) {
|
|
|
187
271
|
// that one provider timeout takes the whole batch's rows to `inflight`
|
|
188
272
|
// with nothing sent. One row per pass, `more: true` to drain — the
|
|
189
273
|
// engine's batching, reused rather than reinvented.
|
|
274
|
+
//
|
|
275
|
+
// In `digest` mode the owner's comment rows are NOT claimable: they wait
|
|
276
|
+
// for `digest.ts` to fold them. Excluded in the claim itself rather than
|
|
277
|
+
// claimed and put back, because a row moved to `inflight` and back is a
|
|
278
|
+
// row the "never twice" machinery has to reason about for nothing.
|
|
190
279
|
const [row] = await sql `
|
|
191
280
|
UPDATE outbox SET status = 'inflight', attempts = attempts + 1
|
|
192
281
|
WHERE id = (
|
|
@@ -194,11 +283,12 @@ export function deliverEmail(mailer, address, keys) {
|
|
|
194
283
|
WHERE provider = 'email'
|
|
195
284
|
AND status = 'pending'
|
|
196
285
|
AND next_attempt_at <= ${now}
|
|
286
|
+
${notify === "digest" ? sql `AND NOT (topic = ${TOPIC} AND event->>'toReviewer' IS NULL)` : sql ``}
|
|
197
287
|
ORDER BY next_attempt_at
|
|
198
288
|
FOR UPDATE SKIP LOCKED
|
|
199
289
|
LIMIT 1
|
|
200
290
|
)
|
|
201
|
-
RETURNING id, org_id, project_id, thread_id, attempts, event
|
|
291
|
+
RETURNING id, org_id, project_id, thread_id, topic, attempts, event
|
|
202
292
|
`;
|
|
203
293
|
if (!row)
|
|
204
294
|
return { did: 0 };
|
|
@@ -217,6 +307,17 @@ export function deliverEmail(mailer, address, keys) {
|
|
|
217
307
|
await sql `UPDATE outbox SET status = 'dropped', last_error = 'no deliverable recipient', completed_at = ${now} WHERE id = ${row.id}`;
|
|
218
308
|
return { did: 0, more: true, note: "one notification had no recipient — dropped" };
|
|
219
309
|
}
|
|
310
|
+
// A digest without the sites deleted since it was folded; with none
|
|
311
|
+
// left there is nothing to say, and it is dropped before it can count
|
|
312
|
+
// against the address's allowance.
|
|
313
|
+
let digest = null;
|
|
314
|
+
if (row.topic === DIGEST_TOPIC) {
|
|
315
|
+
digest = await pruneDigest(sql, row.org_id, row.event);
|
|
316
|
+
if (!digest) {
|
|
317
|
+
await sql `UPDATE outbox SET status = 'dropped', last_error = 'every site in the digest was deleted', completed_at = ${now} WHERE id = ${row.id}`;
|
|
318
|
+
return { did: 0, more: true, note: "one digest listed only deleted sites — dropped" };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
220
321
|
/**
|
|
221
322
|
* PER-ADDRESS RATE LIMIT — B2's last piece.
|
|
222
323
|
*
|
|
@@ -241,8 +342,14 @@ export function deliverEmail(mailer, address, keys) {
|
|
|
241
342
|
WHERE id = ${row.id}`;
|
|
242
343
|
return { did: 0, more: true, note: "one notification deferred — address over its hourly rate" };
|
|
243
344
|
}
|
|
345
|
+
// The digest is its own message (`digest.ts`); every other row links
|
|
346
|
+
// to its thread, read now — outside the `try`, because a failing read
|
|
347
|
+
// is the database's problem, not a send to back off from.
|
|
348
|
+
const mail = digest
|
|
349
|
+
? renderDigest(digest, to.email, baseUrl)
|
|
350
|
+
: render(row, to.email, await threadLink(sql, row.thread_id, baseUrl));
|
|
244
351
|
try {
|
|
245
|
-
await mailer.send({ ...
|
|
352
|
+
await mailer.send({ ...mail, ...(to.headers ? { headers: to.headers } : {}) });
|
|
246
353
|
await sql `
|
|
247
354
|
UPDATE outbox SET status = 'done', completed_at = ${now}, last_error = NULL
|
|
248
355
|
WHERE id = ${row.id}`;
|
|
@@ -341,13 +448,7 @@ async function recipient(sql, row, keys, baseUrl) {
|
|
|
341
448
|
? { email: r.email, headers: unsubscribeHeaders(baseUrl, unsubscribeToken(keys, reviewerId)) }
|
|
342
449
|
: { email: r.email };
|
|
343
450
|
}
|
|
344
|
-
const
|
|
345
|
-
SELECT u.email
|
|
346
|
-
FROM memberships m
|
|
347
|
-
JOIN users u ON u.id = m.user_id
|
|
348
|
-
WHERE m.org_id = ${row.org_id} AND m.role = 'owner' AND u.deleted_at IS NULL
|
|
349
|
-
ORDER BY m.created_at
|
|
350
|
-
LIMIT 1`;
|
|
451
|
+
const owner = await ownerOf(sql, row.org_id);
|
|
351
452
|
return owner ? { email: owner.email } : null;
|
|
352
453
|
}
|
|
353
454
|
/**
|
|
@@ -363,7 +464,7 @@ async function recipient(sql, row, keys, baseUrl) {
|
|
|
363
464
|
* the ones above, and the message is four lines: who, where, what, and a link.
|
|
364
465
|
* There is nothing here that a `<div>` would make clearer.
|
|
365
466
|
*/
|
|
366
|
-
function render(row, to,
|
|
467
|
+
function render(row, to, link) {
|
|
367
468
|
const key = sanitise(row.event.threadKey ?? "a snag", "plain", { singleLine: true, max: 24 });
|
|
368
469
|
const author = sanitise(row.event.authorName ?? "Someone", "plain", { singleLine: true, max: 60 });
|
|
369
470
|
const body = sanitiseWithRedactor(row.event.body ?? "", "plain", redactText, { max: 2000 });
|
|
@@ -383,7 +484,7 @@ function render(row, to, baseUrl) {
|
|
|
383
484
|
`${actor} marked ${key} resolved.`,
|
|
384
485
|
"",
|
|
385
486
|
page ? `On the page: ${page}` : "",
|
|
386
|
-
`See the
|
|
487
|
+
`See the thread: ${link}`,
|
|
387
488
|
"",
|
|
388
489
|
"— Snagset",
|
|
389
490
|
]
|
|
@@ -400,7 +501,7 @@ function render(row, to, baseUrl) {
|
|
|
400
501
|
body,
|
|
401
502
|
"",
|
|
402
503
|
page ? `On the page: ${page}` : "",
|
|
403
|
-
`Open the
|
|
504
|
+
`Open the thread: ${link}`,
|
|
404
505
|
"",
|
|
405
506
|
"— Snagset",
|
|
406
507
|
]
|
package/dist/mail/notify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/mail/notify.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAqB,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAgBhC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,EAA0C,EAC1C,MAAqB,EACrB,EAAU;IAEV,MAAM,OAAO,GAAG;QACd,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC;IAEF,0EAA0E;IAC1E,wEAAwE;IACxE,0EAA0E;IAC1E,wBAAwB;IACxB,MAAM,EAAE,CAAA;;;QAGF,EAAE,KAAK,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,QAAQ;iBACnD,KAAK,KAAK,GAAG,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;;;GAG3E,CAAC;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,MAAM,CAAC,EAAE,EAAE;QACf,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,KAAK;QACZ,SAAS,EAAE,GAAG,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE;QACzC,OAAO,EAAE,MAAM,CAAC,QAAQ;QACxB,OAAO;KACR,CAAC,CAAC;IACH,4DAA4D;IAC5D,UAAU,EAAE,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,MAAM,CACnB,EAA0C,EAC1C,CAUC;IAED,MAAM,YAAY,GAAG,MAAM,EAAE,CAAkB;;;;2BAItB,CAAC,CAAC,QAAQ;;qBAEhB,CAAC,CAAC,OAAO;;;;;GAK3B,CAAC;IAEF,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,EAAE,CAAA;;;UAGF,UAAU,EAAE,KAAK,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,QAAQ;mBAC9C,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,EAAE;UAC7C,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;;;KAGhD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAenC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAA0C,EAC1C,MAAsB;IAEtB,MAAM,MAAM,CAAC,EAAE,EAAE;QACf,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,OAAO,EAAE;QAC7D,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE;YACP,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,QAAQ,EAAE,IAAI;SACf;KACF,CAAC,CAAC;IACH,UAAU,EAAE,CAAC,CAAC,OAAO;AACvB,CAAC;AAqBD;;wEAEwE;AACxE,MAAM,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAEvC;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB,EAAE,OAAsB,EAAE,IAAc;IACvF,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,MAAM;QACf,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAc;YACrC,6EAA6E;YAC7E,MAAM,OAAO,GAAG,MAAM,OAAO,EAAE,CAAC;YAChC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;YAE7C,uEAAuE;YACvE,uEAAuE;YACvE,mEAAmE;YACnE,oDAAoD;YACpD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAa;;;;;;uCAMH,GAAG;;;;;;OAMnC,CAAC;YACF,IAAI,CAAC,GAAG;gBAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;YAE5B;;;;eAIG;YACH,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YACpD,IAAI,CAAC,EAAE,EAAE,CAAC;gBACR,gEAAgE;gBAChE,qEAAqE;gBACrE,mEAAmE;gBACnE,mEAAmE;gBACnE,kDAAkD;gBAClD,MAAM,GAAG,CAAA,iGAAiG,GAAG,eAAe,GAAG,CAAC,EAAE,EAAE,CAAC;gBACrI,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,6CAA6C,EAAE,CAAC;YACrF,CAAC;YAED;;;;;;;;;;;;;;eAcG;YACH,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YACjE,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,GAAG,CAAA;;yDAEwC,IAAI;;wBAErC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACzB,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,0DAA0D,EAAE,CAAC;YAClG,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACzG,MAAM,GAAG,CAAA;8DAC6C,GAAG;wBACzC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACzB,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChC,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAE,CAAC;gBAC9E,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,IAAI,WAAW,CAAC,MAAM,CAAC;gBACrD,MAAM,GAAG,CAAA;;4BAEW,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;gCAC5B,OAAO;qCACF,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC;wBACpD,GAAG,CAAC,EAAE,EAAE,CAAC;gBACzB,GAAG,CAAC,gCAAgC,GAAG,CAAC,QAAQ,YAAY,OAAO,EAAE,CAAC,CAAC;gBACvE,gEAAgE;gBAChE,gDAAgD;gBAChD,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChC,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEtC;;;;;;;GAOG;AACH,KAAK,UAAU,aAAa,CAC1B,GAA2C,EAC3C,KAAa,EACb,KAAa,EACb,GAAS;IAET,MAAM,GAAG,GAAG,QAAQ,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC;IAC3F,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC,CAAC;IAE7D,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAyC;;cAEpD,KAAK,KAAK,GAAG,KAAK,GAAG,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC;;2DAExB,WAAW;;kEAEJ,WAAW,SAAS,GAAG;;gEAEzB,WAAW;mCACxC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC;;;GAGxE,CAAC;IACF,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,kBAAkB;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,GAAG,cAAc,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,KAAK,UAAU,SAAS,CACtB,GAA2C,EAC3C,GAAc,EACd,IAAyB,EACzB,OAAe;IAEf,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAA4B;;oBAEjC,UAAU;;;+BAGC,CAAC;QAC5B,IAAI,CAAC,CAAC,EAAE,KAAK;YAAE,OAAO,IAAI,CAAC;QAC3B,yEAAyE;QACzE,yEAAyE;QACzE,iBAAiB;QACjB,OAAO,IAAI;YACT,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,EAAE;YAC9F,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,CAAqB;;;;wBAIxB,GAAG,CAAC,MAAM;;aAErB,CAAC;IACZ,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,MAAM,CAAC,GAAc,EAAE,EAAU,EAAE,OAAe;IAKzD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,QAAQ,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IAC9F,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,SAAS,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IACnG,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5F,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IAErC;;;;;OAKG;IACH,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,SAAS,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;QACjG,OAAO;YACL,EAAE;YACF,OAAO,EAAE,GAAG,GAAG,sBAAsB;YACrC,IAAI,EAAE;gBACJ,GAAG,KAAK,WAAW,GAAG,YAAY;gBAClC,EAAE;gBACF,IAAI,CAAC,CAAC,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;gBAClC,kBAAkB,OAAO,YAAY;gBACrC,EAAE;gBACF,WAAW;aACZ;iBACE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;iBAC7D,IAAI,CAAC,IAAI,CAAC;SACd,CAAC;IACJ,CAAC;IAED,OAAO;QACL,EAAE;QACF,OAAO,EAAE,GAAG,GAAG,sBAAsB,MAAM,EAAE;QAC7C,IAAI,EAAE;YACJ,GAAG,MAAM,iBAAiB,GAAG,GAAG;YAChC,EAAE;YACF,IAAI;YACJ,EAAE;YACF,IAAI,CAAC,CAAC,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;YAClC,oBAAoB,OAAO,YAAY;YACvC,EAAE;YACF,WAAW;SACZ;aACE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;aAC7D,IAAI,CAAC,IAAI,CAAC;KACd,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/mail/notify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
2
|
+
import { BlockList, isIP } from "node:net";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import https from "node:https";
|
|
5
|
+
/**
|
|
6
|
+
* Fetch an image from the public internet, and from nowhere else — the image
|
|
7
|
+
* proxy's half on the server (D-image-proxy, found reviewing Bringg).
|
|
8
|
+
*
|
|
9
|
+
* A server that fetches a URL a browser hands it is a server that can be
|
|
10
|
+
* pointed at itself: `http://localhost:5432`, the cloud metadata address
|
|
11
|
+
* `169.254.169.254`, a database on the private network beside it. So the
|
|
12
|
+
* check is on the ADDRESS, not the name, and it happens at connect time,
|
|
13
|
+
* inside the socket's own DNS lookup: checking the name first and connecting
|
|
14
|
+
* afterwards is exactly the gap DNS rebinding drives through, because the
|
|
15
|
+
* second resolution can answer differently from the first. Redirects are
|
|
16
|
+
* followed by hand for the same reason — each hop is a new connection and
|
|
17
|
+
* gets the same check.
|
|
18
|
+
*
|
|
19
|
+
* Node's built-in BlockList, not a dependency: the ranges are short, fixed
|
|
20
|
+
* and RFC-numbered, and a parser library is one more thing on the path of
|
|
21
|
+
* every request this route serves.
|
|
22
|
+
*/
|
|
23
|
+
const BLOCKED = new BlockList();
|
|
24
|
+
for (const [net, bits] of [
|
|
25
|
+
["0.0.0.0", 8], // "this network"
|
|
26
|
+
["10.0.0.0", 8], // RFC 1918
|
|
27
|
+
["100.64.0.0", 10], // carrier-grade NAT
|
|
28
|
+
["127.0.0.0", 8], // loopback
|
|
29
|
+
["169.254.0.0", 16], // link-local, and the cloud metadata address in it
|
|
30
|
+
["172.16.0.0", 12], // RFC 1918
|
|
31
|
+
["192.0.0.0", 24], // IETF protocol assignments
|
|
32
|
+
["192.0.2.0", 24], // documentation
|
|
33
|
+
["192.88.99.0", 24], // 6to4 relay
|
|
34
|
+
["192.168.0.0", 16], // RFC 1918
|
|
35
|
+
["198.18.0.0", 15], // benchmarking
|
|
36
|
+
["198.51.100.0", 24], // documentation
|
|
37
|
+
["203.0.113.0", 24], // documentation
|
|
38
|
+
["224.0.0.0", 4], // multicast
|
|
39
|
+
["240.0.0.0", 4], // reserved, and broadcast
|
|
40
|
+
]) {
|
|
41
|
+
BLOCKED.addSubnet(net, bits, "ipv4");
|
|
42
|
+
}
|
|
43
|
+
for (const [net, bits] of [
|
|
44
|
+
["::", 128], // unspecified
|
|
45
|
+
["::1", 128], // loopback
|
|
46
|
+
["64:ff9b::", 96], // NAT64, which can reach an IPv4 private address
|
|
47
|
+
["64:ff9b:1::", 48], // NAT64 local use
|
|
48
|
+
["100::", 64], // discard
|
|
49
|
+
["2001:db8::", 32], // documentation
|
|
50
|
+
["fc00::", 7], // unique local
|
|
51
|
+
["fe80::", 10], // link-local
|
|
52
|
+
["ff00::", 8], // multicast
|
|
53
|
+
]) {
|
|
54
|
+
BLOCKED.addSubnet(net, bits, "ipv6");
|
|
55
|
+
}
|
|
56
|
+
// NOT `::ffff:0:0/96`. Node's BlockList matches an IPv4 address against an
|
|
57
|
+
// IPv4-mapped IPv6 subnet, so that one line blocked the whole IPv4 internet —
|
|
58
|
+
// found by the test that expects 8.8.8.8 to be public. Mapped addresses are
|
|
59
|
+
// unwrapped and judged as IPv4 in isPublicAddress instead.
|
|
60
|
+
/** True when an address is one the proxy may connect to. */
|
|
61
|
+
export function isPublicAddress(address) {
|
|
62
|
+
const family = isIP(address);
|
|
63
|
+
if (family === 4)
|
|
64
|
+
return !BLOCKED.check(address, "ipv4");
|
|
65
|
+
if (family === 6) {
|
|
66
|
+
// `::ffff:10.0.0.1` is 10.0.0.1. Judged as what it reaches, not as what
|
|
67
|
+
// it is written as — the IPv6 range alone would say only "mapped".
|
|
68
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address)?.[1];
|
|
69
|
+
if (mapped)
|
|
70
|
+
return !BLOCKED.check(mapped, "ipv4");
|
|
71
|
+
return !BLOCKED.check(address, "ipv6");
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
export class PublicFetchError extends Error {
|
|
76
|
+
// Assigned in the body: the server runs under --experimental-strip-types,
|
|
77
|
+
// which has no parameter properties.
|
|
78
|
+
code;
|
|
79
|
+
constructor(code, message) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.code = code;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const PORTS = new Set(["", "80", "443"]);
|
|
85
|
+
export async function fetchPublicImage(raw, opts) {
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new PublicFetchError("bad_url", "That is not a URL.");
|
|
92
|
+
}
|
|
93
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
94
|
+
for (let hop = 0; hop <= (opts.maxRedirects ?? 3); hop++) {
|
|
95
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
96
|
+
throw new PublicFetchError("bad_url", "Only http and https images can be fetched.");
|
|
97
|
+
}
|
|
98
|
+
// Credentials in a URL are someone's, and nothing an image needs. A port
|
|
99
|
+
// beyond the web's two is how "fetch this image" becomes "probe this
|
|
100
|
+
// service" — the allowed origin's images are served on 80 and 443.
|
|
101
|
+
if (url.username || url.password || (!opts.allowPrivate && !PORTS.has(url.port))) {
|
|
102
|
+
throw new PublicFetchError("bad_url", "Only images on the standard web ports can be fetched.");
|
|
103
|
+
}
|
|
104
|
+
const res = await request(url, deadline, opts);
|
|
105
|
+
if (res.status >= 300 && res.status < 400 && res.location) {
|
|
106
|
+
url = new URL(res.location, url);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (res.status !== 200)
|
|
110
|
+
throw new PublicFetchError("upstream", `The image's server answered ${res.status}.`);
|
|
111
|
+
return { bytes: res.bytes, contentType: res.contentType };
|
|
112
|
+
}
|
|
113
|
+
throw new PublicFetchError("too_many_redirects", "The image redirected too many times.");
|
|
114
|
+
}
|
|
115
|
+
function guardedLookup(allowPrivate) {
|
|
116
|
+
return (hostname, options, callback) => {
|
|
117
|
+
dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
118
|
+
if (err)
|
|
119
|
+
return callback(err, []);
|
|
120
|
+
const list = addresses;
|
|
121
|
+
// EVERY answer, not the first: a name that resolves to one public and
|
|
122
|
+
// one private address would otherwise be a coin toss on which the
|
|
123
|
+
// socket picks.
|
|
124
|
+
if (!allowPrivate && (list.length === 0 || !list.every((a) => isPublicAddress(a.address)))) {
|
|
125
|
+
return callback(Object.assign(new Error("blocked_address"), { code: "EBLOCKED" }), []);
|
|
126
|
+
}
|
|
127
|
+
const all = options.all;
|
|
128
|
+
if (all)
|
|
129
|
+
return callback(null, list);
|
|
130
|
+
callback(null, list[0].address, list[0].family);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function request(url, deadline, opts) {
|
|
135
|
+
const lib = url.protocol === "https:" ? https : http;
|
|
136
|
+
// A literal IP skips DNS, so it is checked here instead.
|
|
137
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
138
|
+
if (!opts.allowPrivate && isIP(host) && !isPublicAddress(host)) {
|
|
139
|
+
return Promise.reject(new PublicFetchError("blocked_address", "That address is not on the public internet."));
|
|
140
|
+
}
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const remaining = deadline - Date.now();
|
|
143
|
+
if (remaining <= 0)
|
|
144
|
+
return reject(new PublicFetchError("timeout", "The image took too long."));
|
|
145
|
+
const req = lib.get(url, {
|
|
146
|
+
lookup: guardedLookup(!!opts.allowPrivate),
|
|
147
|
+
timeout: remaining,
|
|
148
|
+
headers: { accept: "image/*", "user-agent": "Snagset image proxy (+https://snagset.com)" },
|
|
149
|
+
}, (res) => {
|
|
150
|
+
const status = res.statusCode ?? 0;
|
|
151
|
+
if (status >= 300 && status < 400) {
|
|
152
|
+
res.resume();
|
|
153
|
+
return resolve({ status, ...(res.headers.location ? { location: res.headers.location } : {}) });
|
|
154
|
+
}
|
|
155
|
+
if (status !== 200) {
|
|
156
|
+
res.resume();
|
|
157
|
+
return resolve({ status });
|
|
158
|
+
}
|
|
159
|
+
const contentType = String(res.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
|
|
160
|
+
// An image or nothing. The proxy exists to put pixels in a canvas; a
|
|
161
|
+
// page, a script or a JSON document has no business coming back.
|
|
162
|
+
if (!contentType.startsWith("image/")) {
|
|
163
|
+
res.destroy();
|
|
164
|
+
return reject(new PublicFetchError("not_an_image", `Expected an image; got "${contentType || "nothing"}".`));
|
|
165
|
+
}
|
|
166
|
+
const declared = Number(res.headers["content-length"] ?? 0);
|
|
167
|
+
if (declared > opts.maxBytes) {
|
|
168
|
+
res.destroy();
|
|
169
|
+
return reject(new PublicFetchError("too_large", "The image is larger than the proxy carries."));
|
|
170
|
+
}
|
|
171
|
+
const chunks = [];
|
|
172
|
+
let size = 0;
|
|
173
|
+
res.on("data", (chunk) => {
|
|
174
|
+
size += chunk.length;
|
|
175
|
+
// Counted as it arrives: a Content-Length can be absent or a lie.
|
|
176
|
+
if (size > opts.maxBytes) {
|
|
177
|
+
res.destroy();
|
|
178
|
+
reject(new PublicFetchError("too_large", "The image is larger than the proxy carries."));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
chunks.push(chunk);
|
|
182
|
+
});
|
|
183
|
+
res.on("end", () => resolve({ status, bytes: new Uint8Array(Buffer.concat(chunks)), contentType }));
|
|
184
|
+
res.on("error", (e) => reject(new PublicFetchError("upstream", e.message)));
|
|
185
|
+
});
|
|
186
|
+
req.on("timeout", () => {
|
|
187
|
+
req.destroy();
|
|
188
|
+
reject(new PublicFetchError("timeout", "The image took too long."));
|
|
189
|
+
});
|
|
190
|
+
req.on("error", (e) => {
|
|
191
|
+
if (e.code === "EBLOCKED" || e.message === "blocked_address") {
|
|
192
|
+
reject(new PublicFetchError("blocked_address", "That address is not on the public internet."));
|
|
193
|
+
}
|
|
194
|
+
else if (!(e instanceof PublicFetchError)) {
|
|
195
|
+
reject(new PublicFetchError("upstream", e.message));
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
reject(e);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=publicFetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicFetch.js","sourceRoot":"","sources":["../../src/net/publicFetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,8 +26,8 @@ export function commentEditRoutes(deps) {
|
|
|
26
26
|
const [row] = await maintenance(sql, (tx) => tx `
|
|
27
27
|
SELECT c.id, c.org_id, c.project_id, c.thread_id, c.author_id, c.body, c.private,
|
|
28
28
|
c.created_at, c.deleted_at,
|
|
29
|
-
t.private AS thread_private,
|
|
30
|
-
r.name AS author_name, r.kind AS author_kind
|
|
29
|
+
t.private AS thread_private, t.deleted_at AS thread_deleted_at,
|
|
30
|
+
r.name AS author_name, r.kind AS author_kind, r.user_id AS author_user_id
|
|
31
31
|
FROM comments c
|
|
32
32
|
JOIN threads t ON t.id = c.thread_id
|
|
33
33
|
JOIN reviewers r ON r.id = c.author_id
|
|
@@ -46,7 +46,18 @@ export function commentEditRoutes(deps) {
|
|
|
46
46
|
const owner = c.get("owner");
|
|
47
47
|
const session = c.get("session");
|
|
48
48
|
const asOwner = !!owner && owner.orgId === row.org_id;
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* TWO ways to be the author, because there are two identity systems.
|
|
51
|
+
*
|
|
52
|
+
* A reviewer session's `sub` IS a `reviewers.id`. An owner has no `sub`:
|
|
53
|
+
* a dashboard reply is written under the studio member's own reviewer row
|
|
54
|
+
* (`studioReviewerId`, `user_id` set), so the owner is the author when THAT
|
|
55
|
+
* row's `user_id` is theirs. This used to read only the session half, so
|
|
56
|
+
* an owner could never edit their own reply — the "edit only their own"
|
|
57
|
+
* rule below was "edit nothing" for every dashboard comment ever written.
|
|
58
|
+
*/
|
|
59
|
+
const isAuthor = (!!session && session.sub === row.author_id) ||
|
|
60
|
+
(asOwner && !!row.author_user_id && row.author_user_id === owner.userId);
|
|
50
61
|
if (!asOwner && !session) {
|
|
51
62
|
return fail(c, 401, "unauthorized", "Sign in, or open your review link, and try again.");
|
|
52
63
|
}
|
|
@@ -105,7 +116,10 @@ export function commentEditRoutes(deps) {
|
|
|
105
116
|
/** Endpoint 12 — `PATCH /api/v1/comments/{id}`. */
|
|
106
117
|
const patch = async (c) => {
|
|
107
118
|
const row = await load(c.req.param("id"));
|
|
108
|
-
|
|
119
|
+
// A comment in a DELETED thread is gone with it. Without this a stale tab
|
|
120
|
+
// could edit words nobody can read any more, and the edit would bump the
|
|
121
|
+
// dead thread's `updated_at` for nothing.
|
|
122
|
+
if (!row || row.deleted_at || row.thread_deleted_at)
|
|
109
123
|
return fail(c, 404, "comment_not_found", "No such comment.");
|
|
110
124
|
const refused = refuse(c, row, "edit");
|
|
111
125
|
if (refused)
|
|
@@ -148,7 +162,8 @@ export function commentEditRoutes(deps) {
|
|
|
148
162
|
/** Endpoint 13 — `DELETE /api/v1/comments/{id}`. */
|
|
149
163
|
const remove = async (c) => {
|
|
150
164
|
const row = await load(c.req.param("id"));
|
|
151
|
-
|
|
165
|
+
// Same as the edit: the thread's deletion took its comments with it.
|
|
166
|
+
if (!row || row.thread_deleted_at)
|
|
152
167
|
return fail(c, 404, "comment_not_found", "No such comment.");
|
|
153
168
|
// Deleting twice is the same outcome as deleting once, and a reviewer who
|
|
154
169
|
// taps twice on a slow connection should not see an error for getting what
|