@snagset/server 0.1.15 → 0.1.18

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.
Files changed (197) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +9 -10
  3. package/dist/app.js +52 -48
  4. package/dist/app.js.map +1 -1
  5. package/dist/bin.js +31 -32
  6. package/dist/bin.js.map +1 -1
  7. package/dist/boot.js +21 -21
  8. package/dist/boot.js.map +1 -1
  9. package/dist/config.generated.js +6 -8
  10. package/dist/config.generated.js.map +1 -1
  11. package/dist/config.js +14 -14
  12. package/dist/config.js.map +1 -1
  13. package/dist/crypto.js +1 -1
  14. package/dist/crypto.js.map +1 -1
  15. package/dist/db/connect.js +1 -1
  16. package/dist/db/migrate.js +11 -11
  17. package/dist/db/rls.js +3 -3
  18. package/dist/db/rls.js.map +1 -1
  19. package/dist/domain.js +73 -5
  20. package/dist/domain.js.map +1 -1
  21. package/dist/explain.js +1 -1
  22. package/dist/instance/address.js +3 -3
  23. package/dist/instance/address.js.map +1 -1
  24. package/dist/instance/settings.js +1 -1
  25. package/dist/integrations/github.js +3 -3
  26. package/dist/integrations/github.js.map +1 -1
  27. package/dist/integrations/inbound.js +3 -3
  28. package/dist/integrations/inbound.js.map +1 -1
  29. package/dist/integrations/linear.js +1 -1
  30. package/dist/integrations/linear.js.map +1 -1
  31. package/dist/integrations/outbox.js +4 -4
  32. package/dist/integrations/outbox.js.map +1 -1
  33. package/dist/integrations/slack.js +2 -2
  34. package/dist/integrations/slack.js.map +1 -1
  35. package/dist/jobs/index.js +3 -3
  36. package/dist/jobs/reaper.js +11 -11
  37. package/dist/jobs/reaper.js.map +1 -1
  38. package/dist/jobs/set.js +3 -3
  39. package/dist/jobs/set.js.map +1 -1
  40. package/dist/jobs/soon.js +3 -3
  41. package/dist/keys.js +3 -3
  42. package/dist/keys.js.map +1 -1
  43. package/dist/listen.js +3 -3
  44. package/dist/listen.js.map +1 -1
  45. package/dist/mail/assigned.js +328 -0
  46. package/dist/mail/assigned.js.map +1 -0
  47. package/dist/mail/digest.js +3 -3
  48. package/dist/mail/digest.js.map +1 -1
  49. package/dist/mail/index.js +2 -2
  50. package/dist/mail/links.js +5 -5
  51. package/dist/mail/mailer.js +2 -2
  52. package/dist/mail/notify.js +85 -20
  53. package/dist/mail/notify.js.map +1 -1
  54. package/dist/mail/resend.js +1 -1
  55. package/dist/mail/resolve.js +1 -1
  56. package/dist/mail/resolve.js.map +1 -1
  57. package/dist/mail/smtp.js +1 -1
  58. package/dist/mail/unsubscribe.js +1 -1
  59. package/dist/net/publicFetch.js +1 -1
  60. package/dist/orgs.js +5 -5
  61. package/dist/orgs.js.map +1 -1
  62. package/dist/plan.js +1 -1
  63. package/dist/projects.js +4 -4
  64. package/dist/projects.js.map +1 -1
  65. package/dist/routes/account.js +1 -1
  66. package/dist/routes/account.js.map +1 -1
  67. package/dist/routes/activity.js +1 -1
  68. package/dist/routes/anchorReports.js +9 -9
  69. package/dist/routes/anchorReports.js.map +1 -1
  70. package/dist/routes/assignees.js +4 -4
  71. package/dist/routes/assignees.js.map +1 -1
  72. package/dist/routes/attachments.js +6 -6
  73. package/dist/routes/attachments.js.map +1 -1
  74. package/dist/routes/auth.js +4 -4
  75. package/dist/routes/bodyLimit.js +5 -5
  76. package/dist/routes/bodyLimit.js.map +1 -1
  77. package/dist/routes/commentEdit.js +5 -5
  78. package/dist/routes/commentEdit.js.map +1 -1
  79. package/dist/routes/comments.js +24 -24
  80. package/dist/routes/comments.js.map +1 -1
  81. package/dist/routes/cors.js +33 -15
  82. package/dist/routes/cors.js.map +1 -1
  83. package/dist/routes/instance.js +3 -3
  84. package/dist/routes/instance.js.map +1 -1
  85. package/dist/routes/integrations.js +14 -11
  86. package/dist/routes/integrations.js.map +1 -1
  87. package/dist/routes/invites.js +7 -7
  88. package/dist/routes/invites.js.map +1 -1
  89. package/dist/routes/mcpAuth.js +13 -13
  90. package/dist/routes/mcpAuth.js.map +1 -1
  91. package/dist/routes/mcpToken.js +4 -4
  92. package/dist/routes/mcpToken.js.map +1 -1
  93. package/dist/routes/mcpTokens.js +27 -14
  94. package/dist/routes/mcpTokens.js.map +1 -1
  95. package/dist/routes/members.js +9 -5
  96. package/dist/routes/members.js.map +1 -1
  97. package/dist/routes/owner.js +13 -13
  98. package/dist/routes/owner.js.map +1 -1
  99. package/dist/routes/reviewLinks.js +4 -4
  100. package/dist/routes/reviewLinks.js.map +1 -1
  101. package/dist/routes/rounds.js +3 -3
  102. package/dist/routes/rounds.js.map +1 -1
  103. package/dist/routes/sessions.js +186 -71
  104. package/dist/routes/sessions.js.map +1 -1
  105. package/dist/routes/sites.js +9 -9
  106. package/dist/routes/sites.js.map +1 -1
  107. package/dist/routes/threads.js +144 -90
  108. package/dist/routes/threads.js.map +1 -1
  109. package/dist/routes/widget.js +5 -5
  110. package/dist/routes/widget.js.map +1 -1
  111. package/dist/serverless.js +4 -4
  112. package/dist/serverless.js.map +1 -1
  113. package/dist/session.js +8 -8
  114. package/dist/session.js.map +1 -1
  115. package/dist/storage/bucket.js +4 -4
  116. package/dist/storage/bucket.js.map +1 -1
  117. package/dist/storage/settings.js +4 -4
  118. package/dist/storage/settings.js.map +1 -1
  119. package/dist/studioReviewers.js +4 -4
  120. package/dist/vendor/adapters/outbound/fence.js +2 -2
  121. package/dist/vendor/adapters/outbound/sanitise.js +14 -14
  122. package/dist/vendor/mcp/bin.js +4 -4
  123. package/dist/vendor/mcp/client.js +3 -3
  124. package/dist/vendor/mcp/server.js +3 -3
  125. package/dist/vendor/mcp/source.js +13 -13
  126. package/dist/vendor/mcp/view.js +15 -15
  127. package/dist/vendor/types/brief.js +2 -2
  128. package/dist/vendor/types/index.js +1 -1
  129. package/dist/vendor/types/names.js +20 -18
  130. package/dist/vendor/widget/object.js +3 -3
  131. package/dist/vendor/widget/predicates.js +38 -39
  132. package/package.json +1 -5
  133. package/static/dashboard/assets/{CompositeItem-DcTtfhMj.js → CompositeItem-D1SCvKo_.js} +1 -1
  134. package/static/dashboard/assets/{ConfirmDialog-C7IpllNH.js → ConfirmDialog-DdUOWt96.js} +1 -1
  135. package/static/dashboard/assets/{CopyButton-C0gZH7mu.js → CopyButton-DX7vax3M.js} +1 -1
  136. package/static/dashboard/assets/{FieldItemContext-D0qd3DQv.js → FieldItemContext-CMpCHNX8.js} +1 -1
  137. package/static/dashboard/assets/{InstallTag-DTw-EtuD.js → InstallTag-DkYPsEI0.js} +1 -1
  138. package/static/dashboard/assets/{SaveState-Bk30DUJ3.js → SaveState-D0Hl8cny.js} +1 -1
  139. package/static/dashboard/assets/{_app-CAmVh6lR.js → _app-CcUZ9WT2.js} +1 -1
  140. package/static/dashboard/assets/{_app-gSqPuozu.js → _app-CnG-wKIX.js} +1 -1
  141. package/static/dashboard/assets/{activity-zmYVgnQm.js → activity-DnvsedIb.js} +1 -1
  142. package/static/dashboard/assets/{arrow-left-CqsX1hoI.js → arrow-left-4jbTF1bA.js} +1 -1
  143. package/static/dashboard/assets/{avatar-08tu7BM9.js → avatar-BKSc7tnv.js} +1 -1
  144. package/static/dashboard/assets/{badge-CgsMqJa4.js → badge-aWVtPffe.js} +1 -1
  145. package/static/dashboard/assets/{card-CPsX9FTO.js → card-Bz0VdsxC.js} +1 -1
  146. package/static/dashboard/assets/{check-CJRIq1X0.js → check-Ti2IOlgc.js} +1 -1
  147. package/static/dashboard/assets/{chevron-right-BEmxTSY1.js → chevron-right-g-zbIopD.js} +1 -1
  148. package/static/dashboard/assets/{copy-xpnXaoOA.js → copy-BwBuD0FO.js} +1 -1
  149. package/static/dashboard/assets/{dialog-CxlCupuA.js → dialog-DdpHZobU.js} +2 -2
  150. package/static/dashboard/assets/{dropdown-menu-CGMdCM3V.js → dropdown-menu-CB2L3nig.js} +1 -1
  151. package/static/dashboard/assets/{image-off-XJf-5RS9.js → image-off-w_ej9pIJ.js} +1 -1
  152. package/static/dashboard/assets/index-DAgnfv_E.css +1 -0
  153. package/static/dashboard/assets/{index-CR63M02l.js → index-DtF8YHqn.js} +3 -3
  154. package/static/dashboard/assets/{invite-CcYboRQr.js → invite-Dy3iQjzf.js} +1 -1
  155. package/static/dashboard/assets/{label-Ds2N_0qv.js → label-Dj8L94iS.js} +1 -1
  156. package/static/dashboard/assets/{layout-grid-Dw9tiVXr.js → layout-grid-DuweH0Vl.js} +1 -1
  157. package/static/dashboard/assets/me-7-r9OeMR.js +1 -0
  158. package/static/dashboard/assets/{parts-BYKOSeUb.js → parts-CA4cbfy3.js} +1 -1
  159. package/static/dashboard/assets/{plus-DvfThULG.js → plus-CEWgXDLj.js} +1 -1
  160. package/static/dashboard/assets/s._site-D69M50OI.js +1 -0
  161. package/static/dashboard/assets/{s._site.comments-DTMz620W.js → s._site.comments-DNJ2SE5c.js} +1 -1
  162. package/static/dashboard/assets/{s._site.reviewers-CDhFQ_du.js → s._site.reviewers-B07NzSWj.js} +1 -1
  163. package/static/dashboard/assets/{s._site.settings-BWE3eC3w.js → s._site.settings-B577G5iG.js} +1 -1
  164. package/static/dashboard/assets/{setup-BP-kGzjw.js → setup-DcjjtB3g.js} +1 -1
  165. package/static/dashboard/assets/{sign-in-Brn6kGrC.js → sign-in-gfvOs6Jw.js} +1 -1
  166. package/static/dashboard/assets/{sign-up-DxeJ0RND.js → sign-up-CFJt7ibH.js} +1 -1
  167. package/static/dashboard/assets/{sites-BSP6CMHL.js → sites-DbB5cjz5.js} +1 -1
  168. package/static/dashboard/assets/studio-BO-MHFqc.js +1 -0
  169. package/static/dashboard/assets/{studio-BqA_XZwz.js → studio-D9GxJaSF.js} +1 -1
  170. package/static/dashboard/assets/{studio.index-CjkyAcKF.js → studio.index-DktAxIPT.js} +1 -1
  171. package/static/dashboard/assets/studio.integrations-CbLyqPLX.js +1 -0
  172. package/static/dashboard/assets/{studio.members-CECLI69a.js → studio.members-DFtIL3JS.js} +1 -1
  173. package/static/dashboard/assets/{sun-B5BhapBo.js → sun-Bg7U-6Y0.js} +1 -1
  174. package/static/dashboard/assets/{switch-C4RnvllL.js → switch-CTN6hIca.js} +1 -1
  175. package/static/dashboard/assets/{table-DOCWCO9W.js → table-zQ3pj50_.js} +1 -1
  176. package/static/dashboard/assets/{useAriaLabelledBy-C6VUMzW5.js → useAriaLabelledBy-BsNncGbx.js} +1 -1
  177. package/static/dashboard/assets/{useBaseQuery-ldF9uyzz.js → useBaseQuery-DSqwWD_O.js} +1 -1
  178. package/static/dashboard/assets/useQuery-OgrKuHbc.js +1 -0
  179. package/static/dashboard/assets/useRender-DOFVvGfR.js +1 -0
  180. package/static/dashboard/assets/{useValueChanged-BVjcYor6.js → useValueChanged-B9k7l-mp.js} +1 -1
  181. package/static/dashboard/assets/{users-Ji1BLjAt.js → users-CRiOyD0f.js} +1 -1
  182. package/static/dashboard/assets/{welcome-DzAUdMme.js → welcome-DT2DB4jN.js} +1 -1
  183. package/static/dashboard/assets/{widget-signin-CKUvfq3K.js → widget-signin-CC9owVvs.js} +1 -1
  184. package/static/dashboard/assets/{you-2QsQK86v.js → you-CtcXklc_.js} +1 -1
  185. package/static/dashboard/assets/{zod-D-eI47yt.js → zod-CLxHj89d.js} +1 -1
  186. package/static/dashboard/index.html +2 -2
  187. package/static/widget/snag.capture.js +1 -1
  188. package/static/widget/snag.dock.js +5 -5
  189. package/static/widget/snag.runtime.js +2 -2
  190. package/static/widget/snag.screenshot.js +1 -1
  191. package/static/dashboard/assets/index-Ddi7bl87.css +0 -1
  192. package/static/dashboard/assets/me-BbIUR0d4.js +0 -1
  193. package/static/dashboard/assets/s._site-D7IqF1qB.js +0 -1
  194. package/static/dashboard/assets/studio-BW7OdRP1.js +0 -1
  195. package/static/dashboard/assets/studio.integrations-NDAXAPKG.js +0 -1
  196. package/static/dashboard/assets/useQuery-VzaemGSO.js +0 -1
  197. package/static/dashboard/assets/useRender-CRLtPZ-q.js +0 -1
@@ -0,0 +1,328 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { sanitise, sanitiseWithRedactor } from "../vendor/adapters/index.js";
3
+ import { redactText } from "../vendor/widget/predicates.js";
4
+ import { noteQueued } from "../jobs/soon.js";
5
+ import { threadUrl } from "./links.js";
6
+ /**
7
+ * "This snag is yours now", and deliberately NOT sent at the click.
8
+ *
9
+ * Pastel mails the assignee the moment they are picked, and the maintainer's brief
10
+ * was the opposite: assignment is something a studio does in bursts — triage
11
+ * a round, hand out twelve snags, change its mind about three of them — and
12
+ * twelve mails in a minute, three of them about work that has since moved to
13
+ * somebody else, teach the recipient to filter the sender before the one that
14
+ * matters arrives. So the mail WAITS, and when it goes it goes once.
15
+ *
16
+ * ── THREE RULES, ONE ROW EACH ──
17
+ *
18
+ * 1. **Late.** The row is queued in the PATCH's transaction (as
19
+ * every outbox row is) with `next_attempt_at = now() + ASSIGN_DELAY`.
20
+ * An hour, as a constant rather than a variable for the reason
21
+ * `PER_ADDRESS_HOURLY` gives in `notify.ts`: a number nobody has asked to
22
+ * change is not worth a line an operator has to read past.
23
+ * 2. **Still true.** At send time the row is checked again and DROPPED —
24
+ * with a reason in `last_error`, the status the design has for "nothing to
25
+ * send" — when the snag is no longer theirs, is resolved or deleted, or
26
+ * they have clearly seen it: commented on it, or changed its status,
27
+ * after the assignment. The status half is decided at the PATCH that
28
+ * changes it (`dropAssignedBy`), because nothing records WHO moved a
29
+ * status and the moment of the move is the one place that knows.
30
+ * 3. **Together.** The delivery job claims one row as it always has; when
31
+ * that row is an assignment, every other DUE assignment for the same
32
+ * address in the same studio is claimed with it and the lot goes as one
33
+ * mail — "3 snags assigned to you on Acme" — and all of them are
34
+ * marked `done` together. Across sites, too: a studio member has one
35
+ * reviewer row per site, and the address is what makes them one person.
36
+ *
37
+ * ── WHO IS NEVER QUEUED ──
38
+ *
39
+ * The person who assigned it to themselves (by reviewer row, by dashboard
40
+ * user, or by address — `samePerson` below, the rule); anyone with no
41
+ * address; anyone who unsubscribed, was blocked or erased; a studio member
42
+ * whose user was deleted. All decided at enqueue AND again at send, because
43
+ * an hour is long enough for any of them to change.
44
+ *
45
+ * ── THE DEDUPE KEY ──
46
+ *
47
+ * `thread.assigned:<threadId>:<threads.version>:<reviewerId>` — the
48
+ * `<topic>:<entityId>:<version>` composition with the recipient appended, as
49
+ * the fan-out does. The version is what lets a snag assigned, taken away and
50
+ * given back NEXT WEEK be a new mail; it is not what stops the same person
51
+ * being queued twice inside the hour. That is the push-back: a PENDING row
52
+ * for this thread and this person is moved to an hour from now instead of a
53
+ * second row being written. The thread's row lock, held by the PATCH's own
54
+ * UPDATE, serialises two assigners racing on one thread, so the look-then-
55
+ * insert cannot double.
56
+ */
57
+ export const ASSIGNED_TOPIC = "thread.assigned";
58
+ /** How long an assignment waits before it is worth a mail. */
59
+ /** The default wait; `SNAGSET_ASSIGN_MAIL_DELAY_S` overrides it. */
60
+ export const ASSIGN_DELAY_MS = 60 * 60_000;
61
+ /** How many snags one mail LISTS — the digest's reason (event size, attention). */
62
+ export const ASSIGNED_LISTED = 50;
63
+ const NAME_MAX = 60;
64
+ const EXCERPT_MAX = 140;
65
+ /**
66
+ * The SQL test "is reviewer `r` this actor?" — by row, by dashboard user, or
67
+ * by address, case-insensitively. The rule, in the form the queries below
68
+ * need. A session's row is expanded to its user and address first
69
+ * (`expand`), so a member signed in through the widget is the same person as
70
+ * their dashboard self.
71
+ */
72
+ function matches(tx, alias, a) {
73
+ const r = tx(alias);
74
+ return tx `(
75
+ ${a.reviewerId ? tx `${r}.id = ${a.reviewerId}::uuid` : tx `false`}
76
+ OR ${a.userId ? tx `${r}.user_id = ${a.userId}::uuid` : tx `false`}
77
+ OR ${a.email ? tx `lower(${r}.email) = lower(${a.email})` : tx `false`}
78
+ )`;
79
+ }
80
+ /** A session's reviewer row → the user and address behind it, when it has them. */
81
+ async function expand(tx, a) {
82
+ if (!a.reviewerId || (a.userId && a.email))
83
+ return a;
84
+ const [r] = await tx `
85
+ SELECT user_id, email FROM reviewers WHERE id = ${a.reviewerId}::uuid`;
86
+ return {
87
+ reviewerId: a.reviewerId,
88
+ userId: a.userId ?? r?.user_id ?? undefined,
89
+ email: a.email ?? r?.email ?? undefined,
90
+ };
91
+ }
92
+ /**
93
+ * Queue (or push back) one assignment mail, INSIDE the PATCH's transaction.
94
+ * Returns whether a row now waits, for the tests and nothing else.
95
+ */
96
+ export async function queueAssignedNotice(tx, n) {
97
+ const actor = await expand(tx, n.actor);
98
+ /**
99
+ * The assignee, deliverable or not. The studio member's address is the
100
+ * USER's, read now: the reviewer row copied it when it was materialised and
101
+ * a user who has since changed theirs should be mailed where they read.
102
+ */
103
+ const [to] = await tx `
104
+ SELECT r.id, ${matches(tx, "r", actor)} AS is_actor
105
+ FROM reviewers r
106
+ LEFT JOIN users u ON u.id = r.user_id
107
+ WHERE r.id = ${n.assigneeId}::uuid
108
+ AND r.notifications_off_at IS NULL
109
+ AND r.blocked_at IS NULL
110
+ AND r.erased_at IS NULL
111
+ AND (r.user_id IS NULL OR u.deleted_at IS NULL)
112
+ AND COALESCE(u.email, r.email) IS NOT NULL`;
113
+ // Nobody to write to, or the assigner picked themselves: no row at all —
114
+ // the row never written is the one that cannot be sent.
115
+ if (!to || to.is_actor)
116
+ return false;
117
+ const event = {
118
+ threadKey: n.threadKey,
119
+ actorName: n.actorName,
120
+ toReviewer: n.assigneeId,
121
+ };
122
+ // Already waiting for this person on this thread: wait an hour from NOW
123
+ // instead, and remember the newer assignment time — "has since commented"
124
+ // is measured from the assignment the mail is about.
125
+ const [pushed] = await tx `
126
+ UPDATE outbox
127
+ SET next_attempt_at = now() + ${`${n.delayMs ?? ASSIGN_DELAY_MS} milliseconds`}::interval,
128
+ event = event || jsonb_build_object('assignedAt', now(), 'actorName', ${n.actorName}::text)
129
+ WHERE provider = 'email'
130
+ AND topic = ${ASSIGNED_TOPIC}
131
+ AND thread_id = ${n.threadId}::uuid
132
+ AND status = 'pending'
133
+ AND event->>'toReviewer' = ${n.assigneeId}
134
+ RETURNING id`;
135
+ if (pushed)
136
+ return true;
137
+ await tx `
138
+ INSERT INTO outbox (id, org_id, project_id, thread_id, provider, topic, dedupe_key, event, next_attempt_at)
139
+ VALUES (
140
+ ${randomUUID()}, ${n.orgId}, ${n.projectId}, ${n.threadId},
141
+ 'email', ${ASSIGNED_TOPIC},
142
+ ${`${ASSIGNED_TOPIC}:${n.threadId}:${n.version}:${n.assigneeId}`},
143
+ ${tx.json(event)}::jsonb || jsonb_build_object('assignedAt', now()),
144
+ now() + ${`${n.delayMs ?? ASSIGN_DELAY_MS} milliseconds`}::interval
145
+ )
146
+ ON CONFLICT (provider, dedupe_key) DO NOTHING`;
147
+ // A drain after the response finds nothing due for an hour — and
148
+ // then whatever else this write queued goes, which is the point of it.
149
+ noteQueued();
150
+ return true;
151
+ }
152
+ /**
153
+ * The assignee moved the status themselves: they have seen it, and the mail
154
+ * that would tell them is dropped now. Called by the PATCH that changes a
155
+ * status, with whoever is pressing the button.
156
+ */
157
+ export async function dropAssignedBy(tx, threadId, actor) {
158
+ const who = await expand(tx, actor);
159
+ await tx `
160
+ UPDATE outbox o
161
+ SET status = 'dropped', last_error = 'the assignee changed its status', completed_at = now()
162
+ FROM reviewers r
163
+ WHERE o.provider = 'email'
164
+ AND o.topic = ${ASSIGNED_TOPIC}
165
+ AND o.thread_id = ${threadId}::uuid
166
+ AND o.status = 'pending'
167
+ AND r.id = CASE WHEN o.event->>'toReviewer' ~ '^[0-9a-fA-F-]{36}$'
168
+ THEN (o.event->>'toReviewer')::uuid END
169
+ AND ${matches(tx, "r", who)}`;
170
+ }
171
+ /** The reviewer an assignment row is for, if they can still be mailed. */
172
+ export async function assignedRecipient(sql, row) {
173
+ const id = String(row.event["toReviewer"] ?? "");
174
+ if (!/^[0-9a-fA-F-]{36}$/.test(id))
175
+ return null;
176
+ const [r] = await sql `
177
+ SELECT r.id, r.user_id, COALESCE(u.email, r.email) AS email
178
+ FROM reviewers r
179
+ LEFT JOIN users u ON u.id = r.user_id
180
+ WHERE r.id = ${id}::uuid
181
+ AND r.notifications_off_at IS NULL
182
+ AND r.blocked_at IS NULL
183
+ AND r.erased_at IS NULL
184
+ AND (r.user_id IS NULL OR u.deleted_at IS NULL)`;
185
+ return r?.email ? { reviewerId: r.id, userId: r.user_id, email: r.email } : null;
186
+ }
187
+ /**
188
+ * Every OTHER due assignment for the same address in the same studio,
189
+ * claimed (`inflight`, attempts + 1) exactly as the delivery job claims its
190
+ * one row — `SKIP LOCKED`, so a second instance draining at once takes what
191
+ * this one did not. Each candidate's own recipient is re-derived here, so a
192
+ * row whose reviewer unsubscribed since is simply not part of the batch and
193
+ * is dropped by its own claim later.
194
+ */
195
+ export async function claimSiblings(sql, first, email, now) {
196
+ return sql `
197
+ UPDATE outbox SET status = 'inflight', attempts = attempts + 1
198
+ WHERE id IN (
199
+ SELECT o.id
200
+ FROM outbox o
201
+ JOIN reviewers r ON r.id = CASE WHEN o.event->>'toReviewer' ~ '^[0-9a-fA-F-]{36}$'
202
+ THEN (o.event->>'toReviewer')::uuid END
203
+ LEFT JOIN users u ON u.id = r.user_id
204
+ WHERE o.provider = 'email'
205
+ AND o.topic = ${ASSIGNED_TOPIC}
206
+ AND o.status = 'pending'
207
+ AND o.org_id = ${first.org_id}
208
+ AND o.id <> ${first.id}
209
+ AND o.next_attempt_at <= ${now}
210
+ AND r.notifications_off_at IS NULL
211
+ AND r.blocked_at IS NULL
212
+ AND r.erased_at IS NULL
213
+ AND lower(COALESCE(u.email, r.email)) = lower(${email})
214
+ ORDER BY o.next_attempt_at
215
+ LIMIT ${ASSIGNED_LISTED - 1}
216
+ FOR UPDATE OF o SKIP LOCKED
217
+ )
218
+ RETURNING id, org_id, attempts, event`;
219
+ }
220
+ /**
221
+ * Split claimed rows into what is still news and what is not, with the reason
222
+ * for each drop. Read at SEND time, which is the point of waiting an hour.
223
+ */
224
+ export async function liveAssignments(sql, rows) {
225
+ const live = [];
226
+ const dropped = [];
227
+ for (const row of rows) {
228
+ const to = String(row.event["toReviewer"] ?? "");
229
+ const at = String(row.event["assignedAt"] ?? "") || "-infinity";
230
+ const [t] = await sql `
231
+ SELECT t.key, t.status::text AS status,
232
+ (t.deleted_at IS NOT NULL OR p.deleted_at IS NOT NULL) AS gone,
233
+ t.assignee_id, p.name AS site, p.uid, rd.number AS round,
234
+ (SELECT c.body FROM comments c
235
+ WHERE c.thread_id = t.id AND c.deleted_at IS NULL
236
+ ORDER BY c.seq LIMIT 1) AS first,
237
+ -- "They have clearly seen it": a comment by the same person —
238
+ -- this row, their dashboard self, or their address — written
239
+ -- after the assignment the mail is about.
240
+ EXISTS (
241
+ SELECT 1 FROM comments c
242
+ JOIN reviewers cr ON cr.id = c.author_id
243
+ JOIN reviewers me ON me.id = ${to}::uuid
244
+ WHERE c.thread_id = t.id
245
+ AND c.deleted_at IS NULL
246
+ AND c.created_at > ${at}::timestamptz
247
+ AND (cr.id = me.id
248
+ OR (me.user_id IS NOT NULL AND cr.user_id = me.user_id)
249
+ OR (me.email IS NOT NULL AND lower(cr.email) = lower(me.email)))
250
+ ) AS seen
251
+ FROM outbox o
252
+ JOIN threads t ON t.id = o.thread_id
253
+ JOIN projects p ON p.id = t.project_id
254
+ LEFT JOIN rounds rd ON rd.id = t.round_id
255
+ WHERE o.id = ${row.id}::uuid`;
256
+ const reason = !t
257
+ ? "the thread was deleted"
258
+ : t.gone
259
+ ? "the thread was deleted"
260
+ : t.assignee_id !== to
261
+ ? "the thread is no longer assigned to them"
262
+ : t.status === "resolved"
263
+ ? "the thread was resolved"
264
+ : t.seen
265
+ ? "the assignee has replied since"
266
+ : null;
267
+ if (reason || !t) {
268
+ dropped.push({ id: row.id, reason: reason ?? "the thread was deleted" });
269
+ continue;
270
+ }
271
+ live.push({
272
+ rowId: row.id,
273
+ threadKey: t.key,
274
+ site: t.site,
275
+ siteUid: t.uid,
276
+ round: t.round,
277
+ actorName: String(row.event["actorName"] ?? "Someone"),
278
+ // Redacted BEFORE clamping, as the digest does: half a token is one the
279
+ // redactor no longer recognises.
280
+ excerpt: sanitiseWithRedactor((t.first ?? "").split("\n")[0] ?? "", "plain", redactText, {
281
+ singleLine: true,
282
+ max: EXCERPT_MAX,
283
+ }),
284
+ });
285
+ }
286
+ return { live, dropped };
287
+ }
288
+ /**
289
+ * The mail. Text only, as every mail here is. One snag reads as a sentence;
290
+ * several read as a list, grouped by site, each with its key, the first line
291
+ * of what was said, and the link that opens it.
292
+ */
293
+ export function renderAssigned(items, to, baseUrl) {
294
+ const plain = (s, max) => sanitise(s, "plain", { singleLine: true, max });
295
+ const sites = [...new Set(items.map((i) => i.siteUid))];
296
+ const siteName = plain(items[0].site, NAME_MAX);
297
+ const n = items.length;
298
+ if (n === 1) {
299
+ const it = items[0];
300
+ const key = plain(it.threadKey, 24);
301
+ return {
302
+ to,
303
+ subject: `${key} was assigned to you on ${siteName}`,
304
+ text: [
305
+ `${plain(it.actorName, NAME_MAX)} assigned ${key} to you on ${siteName}.`,
306
+ "",
307
+ ...(it.excerpt ? [`“${plain(it.excerpt, EXCERPT_MAX + 1)}”`, ""] : []),
308
+ `Open it: ${threadUrl(baseUrl, it.siteUid, it.threadKey, it.round)}`,
309
+ "",
310
+ "— Snagset",
311
+ "Sent an hour after the assignment, unless it moved on or you had already answered it.",
312
+ ].join("\n"),
313
+ };
314
+ }
315
+ const subject = sites.length === 1 ? `${n} snags assigned to you on ${siteName}` : `${n} snags assigned to you across ${sites.length} sites`;
316
+ const lines = [`${n} snags were assigned to you.`, ""];
317
+ for (const uid of sites) {
318
+ const mine = items.filter((i) => i.siteUid === uid);
319
+ lines.push(`${plain(mine[0].site, NAME_MAX)} (${mine.length})`);
320
+ for (const it of mine) {
321
+ lines.push(` ${plain(it.threadKey, 24)} · from ${plain(it.actorName, NAME_MAX)}${it.excerpt ? `: ${plain(it.excerpt, EXCERPT_MAX + 1)}` : ""}`, ` ${threadUrl(baseUrl, it.siteUid, it.threadKey, it.round)}`);
322
+ }
323
+ lines.push("");
324
+ }
325
+ lines.push("— Snagset", "Assignments are gathered for an hour and sent together, leaving out any that moved on or that you had already answered.");
326
+ return { to, subject, text: lines.join("\n") };
327
+ }
328
+ //# sourceMappingURL=assigned.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assigned.js","sourceRoot":"","sources":["../../src/mail/assigned.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ import { redactText } from "../vendor/widget/predicates.js";
4
4
  import { isConfigured } from "./mailer.js";
5
5
  import { dashboardUrl, threadUrl } from "./links.js";
6
6
  /**
7
- * The daily digest — D120, on Eurico's "we don't need to send the email every
7
+ * The daily digest, on the maintainer's "we don't need to send the email every
8
8
  * time there is a comment, maybe once in a while".
9
9
  *
10
10
  * `SNAGSET_MAIL_NOTIFY=digest` (the default) changes ONE thing: how the
@@ -54,7 +54,7 @@ import { dashboardUrl, threadUrl } from "./links.js";
54
54
  * Whatever runs the sweep runs this: the platform's daily cron (03:00 UTC in
55
55
  * the scaffold, so the digest arrives then), the bin's hourly ticker (so the
56
56
  * first tick after midnight), or `/api/jobs/run` by hand. NOT the drain a
57
- * write starts (D119): a comment is never the reason to send a digest, and the
57
+ * write starts: a comment is never the reason to send a digest, and the
58
58
  * drain running this would only find the day's window already closed.
59
59
  */
60
60
  export const DIGEST_TOPIC = "comment.digest";
@@ -153,7 +153,7 @@ export function buildDigests(mailer) {
153
153
  if (rows.length === 0)
154
154
  return { did: 0 };
155
155
  // A comment deleted before its digest is not news. Dropped, with the
156
- // status 07 §2.5 has for "nothing to send", rather than listed.
156
+ // status the design has for "nothing to send", rather than listed.
157
157
  const gone = rows.filter((r) => r.gone).map((r) => r.id);
158
158
  const live = rows.filter((r) => !r.gone);
159
159
  if (gone.length) {
@@ -1 +1 @@
1
- {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/mail/digest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/mail/digest.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,7 @@ export const MAIL_SCHEMES = ["smtp", "smtps", "resend"];
22
22
  * case in one function and no new configuration at all. An operator moving
23
23
  * from their own mail server to a provider edits one line.
24
24
  *
25
- * 08 §4.8's other rules, implemented rather than restated:
25
+ * The other rules, implemented rather than restated:
26
26
  *
27
27
  * · Unset ⇒ notifications are **off and say so**. Not queued. A feature that
28
28
  * is off and visible is an operator's decision; one that queues rows
@@ -55,7 +55,7 @@ export function selectMailer(cfg) {
55
55
  /**
56
56
  * A transport URL and a From address → a mailer, or a sentence. The ONE parse
57
57
  * — the environment (`selectMailer`) and the dashboard's stored transport
58
- * (`resolve.ts`, D89) both come through here, so an unsupported scheme is
58
+ * (`resolve.ts`) both come through here, so an unsupported scheme is
59
59
  * refused with the same words wherever it was typed.
60
60
  *
61
61
  * Parsed by hand rather than with `new URL`: a Resend key is the AUTHORITY
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Where a notification's link goes: the thread, open, in the dashboard (D120).
2
+ * Where a notification's link goes: the thread, open, in the dashboard.
3
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
4
+ * Until every mail linked `<address>/dashboard`, and the dashboard is
5
+ * served at the ROOT of the address (`app.ts`'s fallback), so that link
6
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
7
+ * the thread as a search parameter — every view there is a link, — and the
8
8
  * round with it, because the list shows the latest round unless told
9
9
  * otherwise, and a thread from an earlier round would open to nothing.
10
10
  *
@@ -23,7 +23,7 @@ export function dashboardUrl(baseUrl) {
23
23
  }
24
24
  /**
25
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
26
+ * the site's uid are not in the event, and a row queued before the decision has
27
27
  * neither. A thread that has gone since falls back to the dashboard rather
28
28
  * than to a link that opens nothing.
29
29
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The `Mailer` seam — LOCK L7, and built BEFORE the first notification call
2
+ * The `Mailer` seam, and built BEFORE the first notification call
3
3
  * site exists, which is the entire point of naming it in the lock.
4
4
  *
5
5
  * *"otherwise the transport gets imported in eight places, and the import lint
@@ -18,7 +18,7 @@
18
18
  * have to add a vendor to a product whose pitch is that it self-hosts.
19
19
  *
20
20
  * Neither is required. `DATABASE_URL` remains the only mandatory variable
21
- * (LOCK L5), and with no transport configured notifications are OFF AND SAY SO
21
+ * and with no transport configured notifications are OFF AND SAY SO
22
22
  * rather than queueing rows nobody will deliver.
23
23
  */
24
24
  export function isConfigured(m) {