@aglyn/shared-util-email 1.0.0-beta.143

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 (72) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/package.json +37 -0
  4. package/src/index.d.ts +37 -0
  5. package/src/index.js +46 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/email-delivery-events.d.ts +198 -0
  8. package/src/lib/email-delivery-events.js +310 -0
  9. package/src/lib/email-delivery-events.js.map +1 -0
  10. package/src/lib/email-health.d.ts +154 -0
  11. package/src/lib/email-health.js +264 -0
  12. package/src/lib/email-health.js.map +1 -0
  13. package/src/lib/email-media-src.d.ts +33 -0
  14. package/src/lib/email-media-src.js +113 -0
  15. package/src/lib/email-media-src.js.map +1 -0
  16. package/src/lib/email-merge.d.ts +31 -0
  17. package/src/lib/email-merge.js +36 -0
  18. package/src/lib/email-merge.js.map +1 -0
  19. package/src/lib/email-render.d.ts +114 -0
  20. package/src/lib/email-render.js +293 -0
  21. package/src/lib/email-render.js.map +1 -0
  22. package/src/lib/email-revenue-window.d.ts +55 -0
  23. package/src/lib/email-revenue-window.js +58 -0
  24. package/src/lib/email-revenue-window.js.map +1 -0
  25. package/src/lib/host-email-render.d.ts +99 -0
  26. package/src/lib/host-email-render.js +126 -0
  27. package/src/lib/host-email-render.js.map +1 -0
  28. package/src/lib/host-sender.d.ts +86 -0
  29. package/src/lib/host-sender.js +131 -0
  30. package/src/lib/host-sender.js.map +1 -0
  31. package/src/lib/marketing-send.d.ts +466 -0
  32. package/src/lib/marketing-send.js +459 -0
  33. package/src/lib/marketing-send.js.map +1 -0
  34. package/src/lib/platform-sending-domain.d.ts +362 -0
  35. package/src/lib/platform-sending-domain.js +697 -0
  36. package/src/lib/platform-sending-domain.js.map +1 -0
  37. package/src/lib/received-email.d.ts +86 -0
  38. package/src/lib/received-email.js +124 -0
  39. package/src/lib/received-email.js.map +1 -0
  40. package/src/lib/send-ceilings.d.ts +394 -0
  41. package/src/lib/send-ceilings.js +341 -0
  42. package/src/lib/send-ceilings.js.map +1 -0
  43. package/src/lib/send-email.d.ts +385 -0
  44. package/src/lib/send-email.js +586 -0
  45. package/src/lib/send-email.js.map +1 -0
  46. package/src/lib/send-rate.d.ts +298 -0
  47. package/src/lib/send-rate.js +310 -0
  48. package/src/lib/send-rate.js.map +1 -0
  49. package/src/lib/sender-reputation.d.ts +357 -0
  50. package/src/lib/sender-reputation.js +392 -0
  51. package/src/lib/sender-reputation.js.map +1 -0
  52. package/src/lib/sending-domain.d.ts +744 -0
  53. package/src/lib/sending-domain.js +798 -0
  54. package/src/lib/sending-domain.js.map +1 -0
  55. package/src/lib/sending-mailbox.d.ts +132 -0
  56. package/src/lib/sending-mailbox.js +192 -0
  57. package/src/lib/sending-mailbox.js.map +1 -0
  58. package/src/lib/stored-email-nodes.d.ts +38 -0
  59. package/src/lib/stored-email-nodes.js +78 -0
  60. package/src/lib/stored-email-nodes.js.map +1 -0
  61. package/src/lib/svix-signature.d.ts +38 -0
  62. package/src/lib/svix-signature.js +55 -0
  63. package/src/lib/svix-signature.js.map +1 -0
  64. package/src/lib/system-email-catalog.d.ts +170 -0
  65. package/src/lib/system-email-catalog.js +828 -0
  66. package/src/lib/system-email-catalog.js.map +1 -0
  67. package/src/lib/tenant-email-catalog.d.ts +78 -0
  68. package/src/lib/tenant-email-catalog.js +452 -0
  69. package/src/lib/tenant-email-catalog.js.map +1 -0
  70. package/src/lib/text-email-html.d.ts +32 -0
  71. package/src/lib/text-email-html.js +119 -0
  72. package/src/lib/text-email-html.js.map +1 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * THE PLATFORM SEND-RATE GOVERNOR — policy half (AGL-2409).
18
+ *
19
+ * Everything Aglyn sends leaves on ONE Resend key, from ONE verified sending
20
+ * domain, under `p=reject`. A throttle or a reputation hit there is not a spam
21
+ * folder: it is a rejection, and it lands on every customer's password resets
22
+ * and receipts at the same time. Before this there was no per-hour or per-day
23
+ * throttle, no batching and no concurrency limiter anywhere in the email path
24
+ * — so the two bursts the product can already produce (the monthly usage
25
+ * summary fanning out to 1,000 orgs in one invocation, and scheduled campaigns
26
+ * at a ceiling of 10 × 500 × 4 = 20,000 messages an hour) had nothing to ramp
27
+ * against, and the day a ramp was needed there would have been nothing to
28
+ * turn.
29
+ *
30
+ * ## Why the policy is pure and dependency-free
31
+ *
32
+ * Same split as `plugin-api-rate-limit.ts` and `api-http.ts`: the decision is
33
+ * a pure function of (priority, ceiling, used, count) and is unit-testable
34
+ * without a Firestore harness or a route. The durable counter lives in
35
+ * `@aglyn/tenant-data-admin` (`email-send-rate.ts`), which is the only layer
36
+ * that may hold the Admin SDK — `@aglyn/shared-util-email` is `scope:shared`
37
+ * and may not import it, and the existing edge already runs the other way
38
+ * (`tenant-data-admin` imports THIS library). So the governor reaches
39
+ * `sendEmail` by injection, {@link setEmailSendGovernor}, rather than by an
40
+ * import that would be a dependency cycle.
41
+ *
42
+ * ## THE BOUNDARY THAT MATTERS MOST
43
+ *
44
+ * **A rate control may only ever refuse a CAMPAIGN or a BULK sweep. It may
45
+ * never refuse a transactional message.** A password reset or an order
46
+ * receipt refused by a throttle is a strictly worse outcome than the burst
47
+ * being throttled: it converts a reputation risk into an outage on somebody
48
+ * else's business, and the mail explaining why is itself mail that will not
49
+ * send. That is the same rule `email-metering.ts` states for the monthly
50
+ * quota, applied to time distribution instead of monthly totals.
51
+ *
52
+ * The rule is enforced in two independent places on purpose — here, in
53
+ * {@link emailSendRateVerdict}, which cannot return `allowed: false` for a
54
+ * transactional send at all; and again in `sendEmail`, which ignores a
55
+ * refusal for a transactional priority whatever the installed governor says.
56
+ * A governor is injectable, so a wrong one is reachable; the send path must
57
+ * still be unable to drop a password reset.
58
+ */ /** The three send priorities, ordered by what a control may do to them. */ /** Fixed one-hour window, matching the unit the issue and the ramp speak in. */ export const EMAIL_SEND_RATE_WINDOW_MS = 3600000;
59
+ /**
60
+ * The default ceiling, in messages per hour, when nothing has been
61
+ * configured.
62
+ *
63
+ * **This number is a default, not the policy.** The whole point of AGL-2409
64
+ * is that a ramp must be a value change and not a deploy, so the live ceiling
65
+ * is read from `rateLimits/sendRateConfig` and set from the staff console.
66
+ * This is only what an unconfigured deployment (a fork, a self-host, a
67
+ * preview) gets.
68
+ *
69
+ * 2,000/hour is chosen against measured shape rather than taste. Steady-state
70
+ * volume on the domain is a few hundred a day, so this is roughly two orders
71
+ * of magnitude of headroom over normal traffic — it cannot trip on real use,
72
+ * which is the property that keeps a limit from being raised until it means
73
+ * nothing. What it does remove is the two bursts named in the issue: 20,000
74
+ * campaign messages an hour becomes 2,000, and a 1,000-org usage fan-out in
75
+ * one invocation becomes a fan-out that has to ask.
76
+ */ export const EMAIL_SEND_RATE_DEFAULT_PER_HOUR = 2000;
77
+ /**
78
+ * Hard bound on a configured ceiling. Not a business rule — a typo guard, so
79
+ * a slipped digit in the console cannot set a ceiling that is effectively no
80
+ * ceiling. Raising it past this is a deploy, which is the correct friction
81
+ * for a change of that size.
82
+ */ export const EMAIL_SEND_RATE_MAX_PER_HOUR = 100000;
83
+ /** A configured ceiling of 0 would refuse every campaign; 1 is the floor. */ export const EMAIL_SEND_RATE_MIN_PER_HOUR = 1;
84
+ /*==========================================
85
+ * THE PROVIDER'S OWN RATE, WHICH IS A DIFFERENT UNIT.
86
+ *
87
+ * Everything above this point counts MESSAGES OVER AN HOUR. Resend counts
88
+ * REQUESTS OVER A SECOND, and the two are independent: an hour's worth of
89
+ * granted budget spent in four seconds satisfies the governor completely and
90
+ * trips the provider on the fifth request.
91
+ *
92
+ * That gap is what a loop with a sequential `await` and nothing else does not
93
+ * close. Sequential is a pace, but its rate is whatever the round trip
94
+ * happens to be — so the same code that is comfortably inside the limit
95
+ * against a slow network is over it against a fast one, and the only symptom
96
+ * is a 429 the loop then has to be right about.
97
+ *=========================================*/ /**
98
+ * Requests per second the provider accepts, from its published default.
99
+ *
100
+ * Resend documents 10 requests per second PER TEAM — across every API key on
101
+ * the account, not per key and not per sending domain. Past it the API
102
+ * answers HTTP 429 `rate_limit_exceeded`, carrying `retry-after` and
103
+ * `ratelimit-reset` in whole seconds
104
+ * (https://resend.com/changelog/api-rate-limit).
105
+ *
106
+ * A vendor's number, so it is stated once here rather than divided into an
107
+ * interval at the one loop that has to respect it. It can be raised by
108
+ * arrangement with the provider; raising it here without that arrangement
109
+ * buys 429s.
110
+ */ export const EMAIL_PROVIDER_REQUESTS_PER_SECOND = 10;
111
+ /**
112
+ * Requests per second a BATCH may take of that rate.
113
+ *
114
+ * One less than the provider allows, and the missing request is not a safety
115
+ * margin — it is the transactional mail. The limit is counted per TEAM, so a
116
+ * campaign loop running flat out at the full rate is a campaign that answers
117
+ * a password reset arriving in the same second with a 429. That is the
118
+ * outcome this file's opening rule forbids a rate control from producing, and
119
+ * a batch is the only sender in the tree fast enough to produce it.
120
+ *
121
+ * One request per second is not much headroom, and it is not meant to be a
122
+ * budget for concurrent campaigns: it is enough for the one-off sends that
123
+ * make up all of this deployment's other traffic. It also keeps a batch off
124
+ * the exact boundary, where clock jitter alone would earn a 429 on a send
125
+ * that is nominally inside the limit.
126
+ */ export const EMAIL_BATCH_REQUESTS_PER_SECOND = EMAIL_PROVIDER_REQUESTS_PER_SECOND - 1;
127
+ /** The shortest gap between two batch requests that stays inside that rate. */ export const EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS = Math.ceil(1000 / EMAIL_BATCH_REQUESTS_PER_SECOND);
128
+ /**
129
+ * Environment name for a provider rate that is not the published default.
130
+ *
131
+ * Deployment-level rather than stored config, because it is a property of the
132
+ * Resend ACCOUNT the deployment holds a key for — the same layer
133
+ * `RESEND_API_KEY` lives at, and a different question from the hourly ceiling
134
+ * an operator ramps. Resend raises the limit for trusted senders on request,
135
+ * and a self-host runs its own account entirely, so the number cannot be a
136
+ * constant in a shipped build.
137
+ */ export const EMAIL_PROVIDER_RATE_ENV = 'EMAIL_PROVIDER_REQUESTS_PER_SECOND';
138
+ /**
139
+ * The interval a batch paces itself by on this deployment.
140
+ *
141
+ * Unset or unreadable resolves to the published default, never to zero: a
142
+ * typo in an env var must not silently remove a rate control. Zero is
143
+ * accepted only when it is written explicitly, and it disables pacing — which
144
+ * is what a harness with no provider behind it passes, and what an operator
145
+ * who has moved rate control somewhere else in front of this process sets.
146
+ */ export function batchRequestIntervalMs(raw = process.env[EMAIL_PROVIDER_RATE_ENV]) {
147
+ if (raw === null || raw === undefined || String(raw).trim() === '') {
148
+ return EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS;
149
+ }
150
+ const perSecond = Number(raw);
151
+ if (!Number.isFinite(perSecond) || perSecond < 0) {
152
+ return EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS;
153
+ }
154
+ if (perSecond === 0) return 0;
155
+ // The same one-request reservation the default makes, and floored at one so
156
+ // a configured rate of 1 paces at a second rather than dividing by zero.
157
+ return Math.ceil(1000 / Math.max(1, Math.floor(perSecond) - 1));
158
+ }
159
+ /**
160
+ * A pace for a loop that issues one provider request per iteration.
161
+ *
162
+ * Returns a function to await once per iteration. It waits only for whatever
163
+ * is LEFT of the interval since the previous request rather than sleeping a
164
+ * fixed amount, which is the property that makes it free where it is not
165
+ * needed: a loop whose own work already takes longer than the interval —
166
+ * every send in production, which pays a network round trip and a Firestore
167
+ * read per recipient — never waits at all, and a loop that would otherwise
168
+ * fire five hundred requests as fast as the socket allows is spread out to
169
+ * the documented rate.
170
+ *
171
+ * Per-caller state rather than a module-level clock, so two concurrent sends
172
+ * do not share one pace. That is deliberately NOT a claim that the process as
173
+ * a whole stays under the rate: the provider counts the whole team, and this
174
+ * bounds one loop. It removes the burst a single batch produces, which is the
175
+ * only place in this codebase that issues requests in a tight loop.
176
+ *
177
+ * `intervalMs` of 0 (or anything unreadable) disables the wait entirely,
178
+ * which is what a caller with nothing to pace passes. Read per call rather
179
+ * than captured at module load, for the reason `getEmailConfig` is: these run
180
+ * in serverless handlers where the module may be evaluated during a build,
181
+ * long before the runtime env exists.
182
+ */ export function createProviderRequestPacer(intervalMs = batchRequestIntervalMs()) {
183
+ const gap = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 0;
184
+ /** The earliest instant the next request may go. Zero lets the first fly. */ let nextAtMs = 0;
185
+ return async ()=>{
186
+ if (!gap) return;
187
+ const waitMs = nextAtMs - Date.now();
188
+ if (waitMs > 0) {
189
+ await new Promise((resolve)=>setTimeout(resolve, waitMs));
190
+ }
191
+ // Measured from the moment this call RELEASES rather than from the slot
192
+ // it was owed. A schedule of fixed instants lets a loop that stalled for
193
+ // a minute bank a minute's worth of slots and spend them at once, which
194
+ // is the burst this exists to remove; measuring forward from here means
195
+ // the gap can only ever be longer than the interval, never shorter.
196
+ nextAtMs = Date.now() + gap;
197
+ };
198
+ }
199
+ /** Bound on the stored note, so a config read stays a small document. */ export const EMAIL_SEND_RATE_NOTE_MAX = 500;
200
+ /**
201
+ * Reads a stored config document into a usable shape.
202
+ *
203
+ * Every field is clamped rather than trusted. This document decides whether a
204
+ * campaign goes out, and a `perHour` of `NaN` (or `-1`, or a string) reaching
205
+ * the comparison would silently refuse everything refusable on the platform —
206
+ * an outage produced by a bad write. An unreadable value falls back to the
207
+ * default ceiling, never to zero and never to unlimited.
208
+ */ export function normalizeEmailSendRateConfig(raw) {
209
+ var _ref;
210
+ const rawPerHour = Number(raw == null ? void 0 : raw.perHour);
211
+ const perHour = Number.isFinite(rawPerHour) ? Math.min(EMAIL_SEND_RATE_MAX_PER_HOUR, Math.max(EMAIL_SEND_RATE_MIN_PER_HOUR, Math.floor(rawPerHour))) : EMAIL_SEND_RATE_DEFAULT_PER_HOUR;
212
+ const updatedAtMs = Number(raw == null ? void 0 : raw.updatedAtMs);
213
+ return {
214
+ perHour,
215
+ // Absent means ON. A governor that a missing field turns off is not a
216
+ // governor; the operator must have written `false` for it to be off.
217
+ enabled: (raw == null ? void 0 : raw.enabled) !== false,
218
+ updatedAtMs: Number.isFinite(updatedAtMs) && updatedAtMs > 0 ? updatedAtMs : null,
219
+ updatedByEmail: (raw == null ? void 0 : raw.updatedByEmail) ? String(raw.updatedByEmail) : null,
220
+ note: String((_ref = raw == null ? void 0 : raw.note) != null ? _ref : '').slice(0, EMAIL_SEND_RATE_NOTE_MAX)
221
+ };
222
+ }
223
+ /**
224
+ * Contexts that are campaign sends, derived rather than threaded.
225
+ *
226
+ * The same move `contextTag` makes one file over, and for the same reason: a
227
+ * `priority` argument added to 37 call sites is 37 places to remember and one
228
+ * place that does not. `campaign-send.ts` already passes `context:
229
+ * 'campaign'`, and that is already the value `email-metering.ts` keys its
230
+ * enforceable meter on, so nothing new has to be remembered for the one class
231
+ * that must be refusable.
232
+ *
233
+ * ### Polarity
234
+ *
235
+ * Note which way an omission fails. This set enumerates what is REFUSABLE, so
236
+ * a sender that is not listed and does not pass `priority` explicitly is
237
+ * treated as transactional and is never refused — the status quo before this
238
+ * change, not a regression. The opposite polarity (enumerate what is
239
+ * protected) would make a forgotten entry a dropped password reset, which is
240
+ * the failure you find from a support ticket.
241
+ */ const CAMPAIGN_CONTEXTS = new Set([
242
+ 'campaign'
243
+ ]);
244
+ /**
245
+ * The priority for a send. An explicit `priority` always wins; otherwise a
246
+ * campaign context is recognised, and everything else is transactional.
247
+ */ export function resolveSendPriority(context, explicit) {
248
+ if (explicit === 'campaign' || explicit === 'bulk') return explicit;
249
+ if (explicit === 'transactional') return 'transactional';
250
+ return CAMPAIGN_CONTEXTS.has(String(context != null ? context : '').trim()) ? 'campaign' : 'transactional';
251
+ }
252
+ /** True when a control is permitted to refuse this priority at all. */ export function isRefusablePriority(priority) {
253
+ return priority === 'campaign' || priority === 'bulk';
254
+ }
255
+ /** Start of the fixed window containing `nowMs`. */ export function emailSendRateWindowStartMs(nowMs, windowMs = EMAIL_SEND_RATE_WINDOW_MS) {
256
+ return Math.floor(nowMs / windowMs) * windowMs;
257
+ }
258
+ /**
259
+ * The decision. Pure.
260
+ *
261
+ * A transactional send is ALWAYS `allowed`, whatever the numbers say — there
262
+ * is no branch in this function that can refuse one. That is deliberate and
263
+ * is the first of the two enforcement points described at the top of this
264
+ * file.
265
+ */ export function emailSendRateVerdict(input) {
266
+ var _input_windowMs;
267
+ const windowMs = (_input_windowMs = input.windowMs) != null ? _input_windowMs : EMAIL_SEND_RATE_WINDOW_MS;
268
+ const retryAtMs = input.windowStartMs + windowMs;
269
+ // A corrupt or negative counter must not read as headroom a cap honours,
270
+ // the same clamp `campaignEmailSendsForMonth` applies.
271
+ const usedRaw = Number(input.used);
272
+ const used = Number.isFinite(usedRaw) && usedRaw > 0 ? Math.floor(usedRaw) : 0;
273
+ const countRaw = Number(input.count);
274
+ const count = Number.isFinite(countRaw) && countRaw > 0 ? Math.floor(countRaw) : 0;
275
+ const ceilingRaw = Number(input.ceiling);
276
+ const ceiling = Number.isFinite(ceilingRaw) && ceilingRaw > 0 ? Math.floor(ceilingRaw) : EMAIL_SEND_RATE_DEFAULT_PER_HOUR;
277
+ const enabled = input.enabled !== false;
278
+ const wouldExceed = used + count > ceiling;
279
+ // Transactional mail is never refused, and neither is anything at all while
280
+ // the governor is parked. Both still count.
281
+ const allowed = !wouldExceed || !enabled || !isRefusablePriority(input.priority);
282
+ return {
283
+ allowed,
284
+ priority: input.priority,
285
+ ceiling,
286
+ used,
287
+ remaining: Math.max(0, ceiling - (used + (allowed ? count : 0))),
288
+ retryAtMs,
289
+ overCeiling: allowed && wouldExceed
290
+ };
291
+ }
292
+ /**
293
+ * The installed governor, or null.
294
+ *
295
+ * Module-scoped and null by default, so `sendEmail` in a unit test, a preview
296
+ * build or a self-host deployment that never installs one behaves exactly as
297
+ * it did before this change. Null is UNGOVERNED, not refused: a send path
298
+ * that cannot reach the counter must still send.
299
+ */ let installedGovernor = null;
300
+ /** Installs the durable governor. Called once, from `@aglyn/tenant-data-admin`. */ export function setEmailSendGovernor(governor) {
301
+ installedGovernor = governor;
302
+ }
303
+ /** The installed governor, or null when nothing has been installed. */ export function getEmailSendGovernor() {
304
+ return installedGovernor;
305
+ }
306
+ /** Test seam: forget any installed governor. */ export function resetEmailSendGovernorForTests() {
307
+ installedGovernor = null;
308
+ }
309
+
310
+ //# sourceMappingURL=send-rate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/send-rate.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE PLATFORM SEND-RATE GOVERNOR — policy half (AGL-2409).\n *\n * Everything Aglyn sends leaves on ONE Resend key, from ONE verified sending\n * domain, under `p=reject`. A throttle or a reputation hit there is not a spam\n * folder: it is a rejection, and it lands on every customer's password resets\n * and receipts at the same time. Before this there was no per-hour or per-day\n * throttle, no batching and no concurrency limiter anywhere in the email path\n * — so the two bursts the product can already produce (the monthly usage\n * summary fanning out to 1,000 orgs in one invocation, and scheduled campaigns\n * at a ceiling of 10 × 500 × 4 = 20,000 messages an hour) had nothing to ramp\n * against, and the day a ramp was needed there would have been nothing to\n * turn.\n *\n * ## Why the policy is pure and dependency-free\n *\n * Same split as `plugin-api-rate-limit.ts` and `api-http.ts`: the decision is\n * a pure function of (priority, ceiling, used, count) and is unit-testable\n * without a Firestore harness or a route. The durable counter lives in\n * `@aglyn/tenant-data-admin` (`email-send-rate.ts`), which is the only layer\n * that may hold the Admin SDK — `@aglyn/shared-util-email` is `scope:shared`\n * and may not import it, and the existing edge already runs the other way\n * (`tenant-data-admin` imports THIS library). So the governor reaches\n * `sendEmail` by injection, {@link setEmailSendGovernor}, rather than by an\n * import that would be a dependency cycle.\n *\n * ## THE BOUNDARY THAT MATTERS MOST\n *\n * **A rate control may only ever refuse a CAMPAIGN or a BULK sweep. It may\n * never refuse a transactional message.** A password reset or an order\n * receipt refused by a throttle is a strictly worse outcome than the burst\n * being throttled: it converts a reputation risk into an outage on somebody\n * else's business, and the mail explaining why is itself mail that will not\n * send. That is the same rule `email-metering.ts` states for the monthly\n * quota, applied to time distribution instead of monthly totals.\n *\n * The rule is enforced in two independent places on purpose — here, in\n * {@link emailSendRateVerdict}, which cannot return `allowed: false` for a\n * transactional send at all; and again in `sendEmail`, which ignores a\n * refusal for a transactional priority whatever the installed governor says.\n * A governor is injectable, so a wrong one is reachable; the send path must\n * still be unable to drop a password reset.\n */\n\n/** The three send priorities, ordered by what a control may do to them. */\nexport type EmailSendPriority =\n /**\n * Answers something a human just did, or a machine event a customer's\n * business depends on: password reset, invite, order confirmation, booking\n * reminder, workflow notification, membership recovery. **Never refused,\n * never delayed.** Counted, because the ceiling is about total domain\n * volume and this volume is real.\n */\n | 'transactional'\n /**\n * A scheduled fan-out that answers no immediate human action: the monthly\n * usage summary, abandoned-cart and restock sweeps, booking reminder\n * batches. Refusable, because the caller is a resumable cron — a refusal\n * means \"not this hour\", and the next run picks the subject up. A caller\n * that is NOT resumable must not use this priority.\n */\n | 'bulk'\n /**\n * Merchant marketing. Refusable outright, with a message the merchant\n * sees. The one discretionary class, exactly as in `email-metering.ts`.\n */\n | 'campaign'\n\n/** Fixed one-hour window, matching the unit the issue and the ramp speak in. */\nexport const EMAIL_SEND_RATE_WINDOW_MS = 3_600_000\n\n/**\n * The default ceiling, in messages per hour, when nothing has been\n * configured.\n *\n * **This number is a default, not the policy.** The whole point of AGL-2409\n * is that a ramp must be a value change and not a deploy, so the live ceiling\n * is read from `rateLimits/sendRateConfig` and set from the staff console.\n * This is only what an unconfigured deployment (a fork, a self-host, a\n * preview) gets.\n *\n * 2,000/hour is chosen against measured shape rather than taste. Steady-state\n * volume on the domain is a few hundred a day, so this is roughly two orders\n * of magnitude of headroom over normal traffic — it cannot trip on real use,\n * which is the property that keeps a limit from being raised until it means\n * nothing. What it does remove is the two bursts named in the issue: 20,000\n * campaign messages an hour becomes 2,000, and a 1,000-org usage fan-out in\n * one invocation becomes a fan-out that has to ask.\n */\nexport const EMAIL_SEND_RATE_DEFAULT_PER_HOUR = 2_000\n\n/**\n * Hard bound on a configured ceiling. Not a business rule — a typo guard, so\n * a slipped digit in the console cannot set a ceiling that is effectively no\n * ceiling. Raising it past this is a deploy, which is the correct friction\n * for a change of that size.\n */\nexport const EMAIL_SEND_RATE_MAX_PER_HOUR = 100_000\n\n/** A configured ceiling of 0 would refuse every campaign; 1 is the floor. */\nexport const EMAIL_SEND_RATE_MIN_PER_HOUR = 1\n\n/*==========================================\n * THE PROVIDER'S OWN RATE, WHICH IS A DIFFERENT UNIT.\n *\n * Everything above this point counts MESSAGES OVER AN HOUR. Resend counts\n * REQUESTS OVER A SECOND, and the two are independent: an hour's worth of\n * granted budget spent in four seconds satisfies the governor completely and\n * trips the provider on the fifth request.\n *\n * That gap is what a loop with a sequential `await` and nothing else does not\n * close. Sequential is a pace, but its rate is whatever the round trip\n * happens to be — so the same code that is comfortably inside the limit\n * against a slow network is over it against a fast one, and the only symptom\n * is a 429 the loop then has to be right about.\n *=========================================*/\n\n/**\n * Requests per second the provider accepts, from its published default.\n *\n * Resend documents 10 requests per second PER TEAM — across every API key on\n * the account, not per key and not per sending domain. Past it the API\n * answers HTTP 429 `rate_limit_exceeded`, carrying `retry-after` and\n * `ratelimit-reset` in whole seconds\n * (https://resend.com/changelog/api-rate-limit).\n *\n * A vendor's number, so it is stated once here rather than divided into an\n * interval at the one loop that has to respect it. It can be raised by\n * arrangement with the provider; raising it here without that arrangement\n * buys 429s.\n */\nexport const EMAIL_PROVIDER_REQUESTS_PER_SECOND = 10\n\n/**\n * Requests per second a BATCH may take of that rate.\n *\n * One less than the provider allows, and the missing request is not a safety\n * margin — it is the transactional mail. The limit is counted per TEAM, so a\n * campaign loop running flat out at the full rate is a campaign that answers\n * a password reset arriving in the same second with a 429. That is the\n * outcome this file's opening rule forbids a rate control from producing, and\n * a batch is the only sender in the tree fast enough to produce it.\n *\n * One request per second is not much headroom, and it is not meant to be a\n * budget for concurrent campaigns: it is enough for the one-off sends that\n * make up all of this deployment's other traffic. It also keeps a batch off\n * the exact boundary, where clock jitter alone would earn a 429 on a send\n * that is nominally inside the limit.\n */\nexport const EMAIL_BATCH_REQUESTS_PER_SECOND =\n EMAIL_PROVIDER_REQUESTS_PER_SECOND - 1\n\n/** The shortest gap between two batch requests that stays inside that rate. */\nexport const EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS = Math.ceil(\n 1_000 / EMAIL_BATCH_REQUESTS_PER_SECOND,\n)\n\n/**\n * Environment name for a provider rate that is not the published default.\n *\n * Deployment-level rather than stored config, because it is a property of the\n * Resend ACCOUNT the deployment holds a key for — the same layer\n * `RESEND_API_KEY` lives at, and a different question from the hourly ceiling\n * an operator ramps. Resend raises the limit for trusted senders on request,\n * and a self-host runs its own account entirely, so the number cannot be a\n * constant in a shipped build.\n */\nexport const EMAIL_PROVIDER_RATE_ENV = 'EMAIL_PROVIDER_REQUESTS_PER_SECOND'\n\n/**\n * The interval a batch paces itself by on this deployment.\n *\n * Unset or unreadable resolves to the published default, never to zero: a\n * typo in an env var must not silently remove a rate control. Zero is\n * accepted only when it is written explicitly, and it disables pacing — which\n * is what a harness with no provider behind it passes, and what an operator\n * who has moved rate control somewhere else in front of this process sets.\n */\nexport function batchRequestIntervalMs(\n raw: unknown = process.env[EMAIL_PROVIDER_RATE_ENV],\n): number {\n if (raw === null || raw === undefined || String(raw).trim() === '') {\n return EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS\n }\n const perSecond = Number(raw)\n if (!Number.isFinite(perSecond) || perSecond < 0) {\n return EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS\n }\n if (perSecond === 0) return 0\n // The same one-request reservation the default makes, and floored at one so\n // a configured rate of 1 paces at a second rather than dividing by zero.\n return Math.ceil(1_000 / Math.max(1, Math.floor(perSecond) - 1))\n}\n\n/**\n * A pace for a loop that issues one provider request per iteration.\n *\n * Returns a function to await once per iteration. It waits only for whatever\n * is LEFT of the interval since the previous request rather than sleeping a\n * fixed amount, which is the property that makes it free where it is not\n * needed: a loop whose own work already takes longer than the interval —\n * every send in production, which pays a network round trip and a Firestore\n * read per recipient — never waits at all, and a loop that would otherwise\n * fire five hundred requests as fast as the socket allows is spread out to\n * the documented rate.\n *\n * Per-caller state rather than a module-level clock, so two concurrent sends\n * do not share one pace. That is deliberately NOT a claim that the process as\n * a whole stays under the rate: the provider counts the whole team, and this\n * bounds one loop. It removes the burst a single batch produces, which is the\n * only place in this codebase that issues requests in a tight loop.\n *\n * `intervalMs` of 0 (or anything unreadable) disables the wait entirely,\n * which is what a caller with nothing to pace passes. Read per call rather\n * than captured at module load, for the reason `getEmailConfig` is: these run\n * in serverless handlers where the module may be evaluated during a build,\n * long before the runtime env exists.\n */\nexport function createProviderRequestPacer(\n intervalMs: number = batchRequestIntervalMs(),\n): () => Promise<void> {\n const gap = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 0\n /** The earliest instant the next request may go. Zero lets the first fly. */\n let nextAtMs = 0\n return async () => {\n if (!gap) return\n const waitMs = nextAtMs - Date.now()\n if (waitMs > 0) {\n await new Promise((resolve) => setTimeout(resolve, waitMs))\n }\n // Measured from the moment this call RELEASES rather than from the slot\n // it was owed. A schedule of fixed instants lets a loop that stalled for\n // a minute bank a minute's worth of slots and spend them at once, which\n // is the burst this exists to remove; measuring forward from here means\n // the gap can only ever be longer than the interval, never shorter.\n nextAtMs = Date.now() + gap\n }\n}\n\n/** The live ceiling, as stored and as the console edits it. */\nexport interface EmailSendRateConfig {\n /** Messages per hour across the whole platform. */\n perHour: number\n /**\n * False parks the governor: every priority is granted and still counted,\n * so the console keeps showing real volume. The off switch exists because\n * an operator who suspects the governor is refusing legitimate mail must be\n * able to stop it in one click rather than by raising a number they then\n * have to remember to lower.\n */\n enabled: boolean\n /** When staff last changed it, ms. Null when never configured. */\n updatedAtMs: number | null\n /** Who changed it; shown beside the value so a ramp has an author. */\n updatedByEmail: string | null\n /** Why — the ramp step, the incident. Free text, bounded. */\n note: string\n}\n\n/** Bound on the stored note, so a config read stays a small document. */\nexport const EMAIL_SEND_RATE_NOTE_MAX = 500\n\n/**\n * Reads a stored config document into a usable shape.\n *\n * Every field is clamped rather than trusted. This document decides whether a\n * campaign goes out, and a `perHour` of `NaN` (or `-1`, or a string) reaching\n * the comparison would silently refuse everything refusable on the platform —\n * an outage produced by a bad write. An unreadable value falls back to the\n * default ceiling, never to zero and never to unlimited.\n */\nexport function normalizeEmailSendRateConfig(\n raw: Partial<EmailSendRateConfig> | null | undefined,\n): EmailSendRateConfig {\n const rawPerHour = Number(raw?.perHour)\n const perHour = Number.isFinite(rawPerHour)\n ? Math.min(\n EMAIL_SEND_RATE_MAX_PER_HOUR,\n Math.max(EMAIL_SEND_RATE_MIN_PER_HOUR, Math.floor(rawPerHour)),\n )\n : EMAIL_SEND_RATE_DEFAULT_PER_HOUR\n const updatedAtMs = Number(raw?.updatedAtMs)\n return {\n perHour,\n // Absent means ON. A governor that a missing field turns off is not a\n // governor; the operator must have written `false` for it to be off.\n enabled: raw?.enabled !== false,\n updatedAtMs: Number.isFinite(updatedAtMs) && updatedAtMs > 0 ? updatedAtMs : null,\n updatedByEmail: raw?.updatedByEmail ? String(raw.updatedByEmail) : null,\n note: String(raw?.note ?? '').slice(0, EMAIL_SEND_RATE_NOTE_MAX),\n }\n}\n\n/**\n * Contexts that are campaign sends, derived rather than threaded.\n *\n * The same move `contextTag` makes one file over, and for the same reason: a\n * `priority` argument added to 37 call sites is 37 places to remember and one\n * place that does not. `campaign-send.ts` already passes `context:\n * 'campaign'`, and that is already the value `email-metering.ts` keys its\n * enforceable meter on, so nothing new has to be remembered for the one class\n * that must be refusable.\n *\n * ### Polarity\n *\n * Note which way an omission fails. This set enumerates what is REFUSABLE, so\n * a sender that is not listed and does not pass `priority` explicitly is\n * treated as transactional and is never refused — the status quo before this\n * change, not a regression. The opposite polarity (enumerate what is\n * protected) would make a forgotten entry a dropped password reset, which is\n * the failure you find from a support ticket.\n */\nconst CAMPAIGN_CONTEXTS: ReadonlySet<string> = new Set(['campaign'])\n\n/**\n * The priority for a send. An explicit `priority` always wins; otherwise a\n * campaign context is recognised, and everything else is transactional.\n */\nexport function resolveSendPriority(\n context: string | undefined,\n explicit?: EmailSendPriority,\n): EmailSendPriority {\n if (explicit === 'campaign' || explicit === 'bulk') return explicit\n if (explicit === 'transactional') return 'transactional'\n return CAMPAIGN_CONTEXTS.has(String(context ?? '').trim())\n ? 'campaign'\n : 'transactional'\n}\n\n/** True when a control is permitted to refuse this priority at all. */\nexport function isRefusablePriority(priority: EmailSendPriority): boolean {\n return priority === 'campaign' || priority === 'bulk'\n}\n\n/** Start of the fixed window containing `nowMs`. */\nexport function emailSendRateWindowStartMs(\n nowMs: number,\n windowMs: number = EMAIL_SEND_RATE_WINDOW_MS,\n): number {\n return Math.floor(nowMs / windowMs) * windowMs\n}\n\nexport interface EmailSendRateInput {\n priority: EmailSendPriority\n /** Messages already counted in this window. */\n used: number\n /** Messages this send would add. */\n count: number\n /** The live ceiling for the window. */\n ceiling: number\n /** False parks the governor — grant everything, still count it. */\n enabled?: boolean\n /** Start of the window, for `retryAtMs`. */\n windowStartMs: number\n windowMs?: number\n}\n\nexport interface EmailSendRateVerdict {\n allowed: boolean\n priority: EmailSendPriority\n ceiling: number\n /** Count in the window BEFORE this send. */\n used: number\n /** Headroom left after this send, floored at 0. */\n remaining: number\n /** When the window rolls and a refused caller may try again. */\n retryAtMs: number\n /**\n * True when the send was granted despite there being no headroom, because\n * refusing it was not permitted. This is how the console shows that a\n * window went over its ceiling on transactional volume — which is not an\n * error, it is the ceiling declining to enforce, exactly like the monthly\n * overage `emailSendsOverage` records.\n */\n overCeiling: boolean\n}\n\n/**\n * The decision. Pure.\n *\n * A transactional send is ALWAYS `allowed`, whatever the numbers say — there\n * is no branch in this function that can refuse one. That is deliberate and\n * is the first of the two enforcement points described at the top of this\n * file.\n */\nexport function emailSendRateVerdict(\n input: EmailSendRateInput,\n): EmailSendRateVerdict {\n const windowMs = input.windowMs ?? EMAIL_SEND_RATE_WINDOW_MS\n const retryAtMs = input.windowStartMs + windowMs\n // A corrupt or negative counter must not read as headroom a cap honours,\n // the same clamp `campaignEmailSendsForMonth` applies.\n const usedRaw = Number(input.used)\n const used = Number.isFinite(usedRaw) && usedRaw > 0 ? Math.floor(usedRaw) : 0\n const countRaw = Number(input.count)\n const count = Number.isFinite(countRaw) && countRaw > 0 ? Math.floor(countRaw) : 0\n const ceilingRaw = Number(input.ceiling)\n const ceiling = Number.isFinite(ceilingRaw) && ceilingRaw > 0\n ? Math.floor(ceilingRaw)\n : EMAIL_SEND_RATE_DEFAULT_PER_HOUR\n const enabled = input.enabled !== false\n\n const wouldExceed = used + count > ceiling\n // Transactional mail is never refused, and neither is anything at all while\n // the governor is parked. Both still count.\n const allowed = !wouldExceed || !enabled || !isRefusablePriority(input.priority)\n return {\n allowed,\n priority: input.priority,\n ceiling,\n used,\n remaining: Math.max(0, ceiling - (used + (allowed ? count : 0))),\n retryAtMs,\n overCeiling: allowed && wouldExceed,\n }\n}\n\n/** What `sendEmail` asks the installed governor. */\nexport interface EmailSendGovernorRequest {\n priority: EmailSendPriority\n /** Recipient addresses on this one send. */\n count: number\n context?: string\n}\n\n/** What the governor answers. A subset of the verdict `sendEmail` needs. */\nexport interface EmailSendGovernorVerdict {\n allowed: boolean\n ceiling?: number\n used?: number\n remaining?: number\n retryAtMs?: number\n /** True when the durable counter was unreachable and this failed open. */\n degraded?: boolean\n}\n\nexport type EmailSendGovernor = (\n request: EmailSendGovernorRequest,\n) => Promise<EmailSendGovernorVerdict>\n\n/**\n * The installed governor, or null.\n *\n * Module-scoped and null by default, so `sendEmail` in a unit test, a preview\n * build or a self-host deployment that never installs one behaves exactly as\n * it did before this change. Null is UNGOVERNED, not refused: a send path\n * that cannot reach the counter must still send.\n */\nlet installedGovernor: EmailSendGovernor | null = null\n\n/** Installs the durable governor. Called once, from `@aglyn/tenant-data-admin`. */\nexport function setEmailSendGovernor(governor: EmailSendGovernor | null): void {\n installedGovernor = governor\n}\n\n/** The installed governor, or null when nothing has been installed. */\nexport function getEmailSendGovernor(): EmailSendGovernor | null {\n return installedGovernor\n}\n\n/** Test seam: forget any installed governor. */\nexport function resetEmailSendGovernorForTests(): void {\n installedGovernor = null\n}\n"],"names":["EMAIL_SEND_RATE_WINDOW_MS","EMAIL_SEND_RATE_DEFAULT_PER_HOUR","EMAIL_SEND_RATE_MAX_PER_HOUR","EMAIL_SEND_RATE_MIN_PER_HOUR","EMAIL_PROVIDER_REQUESTS_PER_SECOND","EMAIL_BATCH_REQUESTS_PER_SECOND","EMAIL_BATCH_MIN_REQUEST_INTERVAL_MS","Math","ceil","EMAIL_PROVIDER_RATE_ENV","batchRequestIntervalMs","raw","process","env","undefined","String","trim","perSecond","Number","isFinite","max","floor","createProviderRequestPacer","intervalMs","gap","nextAtMs","waitMs","Date","now","Promise","resolve","setTimeout","EMAIL_SEND_RATE_NOTE_MAX","normalizeEmailSendRateConfig","rawPerHour","perHour","min","updatedAtMs","enabled","updatedByEmail","note","slice","CAMPAIGN_CONTEXTS","Set","resolveSendPriority","context","explicit","has","isRefusablePriority","priority","emailSendRateWindowStartMs","nowMs","windowMs","emailSendRateVerdict","input","retryAtMs","windowStartMs","usedRaw","used","countRaw","count","ceilingRaw","ceiling","wouldExceed","allowed","remaining","overCeiling","installedGovernor","setEmailSendGovernor","governor","getEmailSendGovernor","resetEmailSendGovernorForTests"],"mappings":"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"}