@aglyn/plugins-outreach 1.0.0-beta.160 → 1.0.0-beta.162

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 (89) hide show
  1. package/README.md +4 -0
  2. package/package.json +13 -13
  3. package/src/lib/components/compliance-section.d.ts +3 -2
  4. package/src/lib/components/compliance-section.js +7 -2
  5. package/src/lib/components/compliance-section.js.map +1 -1
  6. package/src/lib/components/do-not-contact-domains.d.ts +29 -0
  7. package/src/lib/components/do-not-contact-domains.js +208 -0
  8. package/src/lib/components/do-not-contact-domains.js.map +1 -0
  9. package/src/lib/components/use-outreach-api.d.ts +5 -1
  10. package/src/lib/components/use-outreach-api.js +12 -0
  11. package/src/lib/components/use-outreach-api.js.map +1 -1
  12. package/src/lib/components/use-outreach-data.d.ts +8 -1
  13. package/src/lib/components/use-outreach-data.js +32 -0
  14. package/src/lib/components/use-outreach-data.js.map +1 -1
  15. package/src/lib/constants/api-routes.d.ts +5 -0
  16. package/src/lib/constants/api-routes.js +4 -0
  17. package/src/lib/constants/api-routes.js.map +1 -1
  18. package/src/lib/engine/do-not-contact-domain.d.ts +39 -0
  19. package/src/lib/engine/do-not-contact-domain.js +51 -0
  20. package/src/lib/engine/do-not-contact-domain.js.map +1 -0
  21. package/src/lib/engine/gates.d.ts +6 -1
  22. package/src/lib/engine/gates.js +12 -1
  23. package/src/lib/engine/gates.js.map +1 -1
  24. package/src/lib/engine/gateway-block.d.ts +31 -0
  25. package/src/lib/engine/gateway-block.js +54 -0
  26. package/src/lib/engine/gateway-block.js.map +1 -0
  27. package/src/lib/engine/index.d.ts +3 -0
  28. package/src/lib/engine/index.js +3 -0
  29. package/src/lib/engine/index.js.map +1 -1
  30. package/src/lib/engine/mailbox-health.d.ts +5 -0
  31. package/src/lib/engine/mailbox-health.js +12 -5
  32. package/src/lib/engine/mailbox-health.js.map +1 -1
  33. package/src/lib/engine/mailbox-notice.d.ts +46 -0
  34. package/src/lib/engine/mailbox-notice.js +64 -0
  35. package/src/lib/engine/mailbox-notice.js.map +1 -0
  36. package/src/lib/enrollment/enroll-people.js +6 -3
  37. package/src/lib/enrollment/enroll-people.js.map +1 -1
  38. package/src/lib/enrollment/gate-lookups.js +7 -3
  39. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  40. package/src/lib/model/outreach-api.d.ts +23 -2
  41. package/src/lib/model/outreach-api.js.map +1 -1
  42. package/src/lib/model/outreach.types.d.ts +33 -1
  43. package/src/lib/model/outreach.types.js +11 -1
  44. package/src/lib/model/outreach.types.js.map +1 -1
  45. package/src/lib/routes/do-not-contact-routes.d.ts +35 -0
  46. package/src/lib/routes/do-not-contact-routes.js +92 -0
  47. package/src/lib/routes/do-not-contact-routes.js.map +1 -0
  48. package/src/lib/routes/enroll-routes.js +1 -0
  49. package/src/lib/routes/enroll-routes.js.map +1 -1
  50. package/src/lib/routes/enrollment-routes.js +13 -0
  51. package/src/lib/routes/enrollment-routes.js.map +1 -1
  52. package/src/lib/routes/platform-deps.d.ts +2 -1
  53. package/src/lib/routes/platform-deps.js +2 -1
  54. package/src/lib/routes/platform-deps.js.map +1 -1
  55. package/src/lib/routes/register-routes.js +8 -1
  56. package/src/lib/routes/register-routes.js.map +1 -1
  57. package/src/lib/routes/route-deps.d.ts +6 -0
  58. package/src/lib/routes/route-deps.js.map +1 -1
  59. package/src/lib/runtime/enrollment-events.d.ts +6 -2
  60. package/src/lib/runtime/enrollment-events.js +20 -3
  61. package/src/lib/runtime/enrollment-events.js.map +1 -1
  62. package/src/lib/runtime/mailbox-health-store.d.ts +1 -1
  63. package/src/lib/runtime/mailbox-health-store.js +17 -4
  64. package/src/lib/runtime/mailbox-health-store.js.map +1 -1
  65. package/src/lib/runtime/mailbox-notices.d.ts +39 -0
  66. package/src/lib/runtime/mailbox-notices.js +117 -0
  67. package/src/lib/runtime/mailbox-notices.js.map +1 -0
  68. package/src/lib/runtime/person-erasure.d.ts +4 -0
  69. package/src/lib/runtime/person-erasure.js.map +1 -1
  70. package/src/lib/runtime/platform-runtime-deps.js +32 -0
  71. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  72. package/src/lib/runtime/runtime-deps.d.ts +38 -0
  73. package/src/lib/runtime/runtime-deps.js.map +1 -1
  74. package/src/lib/runtime/send-job.js +6 -6
  75. package/src/lib/runtime/send-job.js.map +1 -1
  76. package/src/lib/runtime/sync-job.d.ts +2 -0
  77. package/src/lib/runtime/sync-job.js +84 -15
  78. package/src/lib/runtime/sync-job.js.map +1 -1
  79. package/src/lib/runtime/timeline.d.ts +10 -1
  80. package/src/lib/runtime/timeline.js +23 -1
  81. package/src/lib/runtime/timeline.js.map +1 -1
  82. package/src/lib/runtime/unsubscribe-route.d.ts +1 -1
  83. package/src/lib/runtime/unsubscribe-route.js.map +1 -1
  84. package/src/lib/server.d.ts +1 -1
  85. package/src/lib/server.js +1 -1
  86. package/src/lib/server.js.map +1 -1
  87. package/src/lib/storage/do-not-contact-store.d.ts +28 -1
  88. package/src/lib/storage/do-not-contact-store.js +117 -9
  89. package/src/lib/storage/do-not-contact-store.js.map +1 -1
@@ -1,3 +1,5 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
1
3
  /**
2
4
  * @license
3
5
  * Copyright 2026 Aglyn LLC
@@ -15,7 +17,10 @@
15
17
  * limitations under the License.
16
18
  */ /**
17
19
  * The organization's Outreach do-not-contact list (AGL-2980):
18
- * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.
20
+ * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address —
21
+ * and, beside it, `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, one
22
+ * document per DOMAIN (AGL-3244), which the same callers read: an address
23
+ * is refused when it, or the domain it is at, is listed.
19
24
  *
20
25
  * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and
21
26
  * the API is shaped for both:
@@ -44,6 +49,7 @@
44
49
  * that atomic without a transaction: two adds racing for one address cannot
45
50
  * both write.
46
51
  */ import { outreachDoNotContactKey } from "../engine/do-not-contact.js";
52
+ import { normalizeOutreachDomain, outreachEmailDomain } from "../engine/do-not-contact-domain.js";
47
53
  import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_CONTACT_SOURCES } from "../model/outreach.types.js";
48
54
  /** The longest detail an entry keeps. */ export const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500;
49
55
  /** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
@@ -121,9 +127,36 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
121
127
  * a list that could hold an entry nobody can explain is not evidence of
122
128
  * anything.
123
129
  */ export async function addOutreachDoNotContact(firestore, input) {
124
- var _input_detail;
125
130
  const key = outreachDoNotContactKey(input.email);
126
131
  if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value');
132
+ const entry = _extends({
133
+ key
134
+ }, entryFields(input));
135
+ const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key);
136
+ try {
137
+ await ref.create(entry);
138
+ return {
139
+ key,
140
+ created: true,
141
+ entry
142
+ };
143
+ } catch (error) {
144
+ var _readOutreachDoNotContactEntry;
145
+ if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
146
+ const existing = await ref.get();
147
+ return {
148
+ key,
149
+ created: false,
150
+ entry: (_readOutreachDoNotContactEntry = readOutreachDoNotContactEntry(key, existing.data())) != null ? _readOutreachDoNotContactEntry : entry
151
+ };
152
+ }
153
+ }
154
+ /**
155
+ * The fields an address entry and a domain entry share, held to the rules
156
+ * the module note gives: a reason and a source the list knows, and a member
157
+ * behind every member-sourced add.
158
+ */ function entryFields(input) {
159
+ var _input_detail;
127
160
  if (!OUTREACH_DO_NOT_CONTACT_REASONS.includes(input.reason)) {
128
161
  throw new Error(`[outreach] "${String(input.reason)}" is not a do-not-contact reason`);
129
162
  }
@@ -135,8 +168,7 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
135
168
  throw new Error('[outreach] a member-added do-not-contact entry names the member');
136
169
  }
137
170
  const detail = String((_input_detail = input.detail) != null ? _input_detail : '').replace(/\s+/g, ' ').trim();
138
- const entry = {
139
- key,
171
+ return {
140
172
  reason: input.reason,
141
173
  source: input.source,
142
174
  addedByUid,
@@ -145,24 +177,100 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
145
177
  sequenceId: input.sequenceId || null,
146
178
  detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null
147
179
  };
148
- const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key);
180
+ }
181
+ /*==========================================
182
+ * DOMAINS (AGL-3244).
183
+ *
184
+ * `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, the domain itself
185
+ * the id — it names a company, not a person, so it is stored in clear and
186
+ * the Compliance page lists it. The same three rules hold: a read that
187
+ * failed is `null`, an entry is written once, and every add is explained.
188
+ *==========================================*/ /** `orgs/{orgId}/outreachDoNotContactDomains`. */ export function outreachDoNotContactDomainCollection(firestore, orgId) {
189
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS.doNotContactDomains);
190
+ }
191
+ /** A stored domain entry held to its shape, or `null` for a document that is not one. */ export function readOutreachDoNotContactDomainEntry(domain, data) {
192
+ var _normalizeOutreachDomain;
193
+ const entry = readOutreachDoNotContactEntry(domain, data);
194
+ if (!entry) return null;
195
+ const { key } = entry, rest = _object_without_properties_loose(entry, [
196
+ "key"
197
+ ]);
198
+ return _extends({
199
+ domain: (_normalizeOutreachDomain = normalizeOutreachDomain(data == null ? void 0 : data['domain'])) != null ? _normalizeOutreachDomain : key
200
+ }, rest);
201
+ }
202
+ /**
203
+ * Whether each address's DOMAIN is on the list, in one round trip, keyed by
204
+ * the address exactly as given: `true` or `false` per address, `null` for
205
+ * an address whose domain cannot be read off it and for every address when
206
+ * the read failed.
207
+ */ export async function lookupOutreachDoNotContactDomains(firestore, orgId, emails) {
208
+ const answers = new Map();
209
+ const domainOf = new Map();
210
+ for (const email of emails){
211
+ const domain = outreachEmailDomain(email);
212
+ if (domain) domainOf.set(email, domain);
213
+ else answers.set(email, null);
214
+ }
215
+ const domains = [
216
+ ...new Set(domainOf.values())
217
+ ];
218
+ if (!domains.length) return answers;
219
+ const list = outreachDoNotContactDomainCollection(firestore, orgId);
220
+ const listed = new Map();
221
+ try {
222
+ var _listed_get;
223
+ for(let start = 0; start < domains.length; start += GET_ALL_CHUNK){
224
+ const chunk = domains.slice(start, start + GET_ALL_CHUNK);
225
+ const snapshots = await firestore.getAll(...chunk.map((domain)=>list.doc(domain)));
226
+ chunk.forEach((domain, index)=>{
227
+ var _snapshots_index;
228
+ return listed.set(domain, ((_snapshots_index = snapshots[index]) == null ? void 0 : _snapshots_index.exists) === true);
229
+ });
230
+ }
231
+ for (const [email, domain] of domainOf)answers.set(email, (_listed_get = listed.get(domain)) != null ? _listed_get : null);
232
+ } catch (error) {
233
+ console.error('[outreach] do-not-contact domain lookup failed; reading as unchecked', error);
234
+ for (const email of domainOf.keys())answers.set(email, null);
235
+ }
236
+ return answers;
237
+ }
238
+ /** Every domain on the list, alphabetically. */ export async function listOutreachDoNotContactDomains(firestore, orgId) {
239
+ const snapshot = await outreachDoNotContactDomainCollection(firestore, orgId).get();
240
+ return snapshot.docs.map((doc)=>readOutreachDoNotContactDomainEntry(doc.id, doc.data())).filter((entry)=>entry !== null).sort((a, b)=>a.domain.localeCompare(b.domain));
241
+ }
242
+ /** Puts a domain on the list; a second add changes nothing. Throws for a value that is not a domain. */ export async function addOutreachDoNotContactDomain(firestore, input) {
243
+ const domain = normalizeOutreachDomain(input.domain);
244
+ if (!domain) throw new Error('[outreach] cannot key a do-not-contact entry for a value that is not a domain');
245
+ const entry = _extends({
246
+ domain
247
+ }, entryFields(input));
248
+ const ref = outreachDoNotContactDomainCollection(firestore, input.orgId).doc(domain);
149
249
  try {
150
250
  await ref.create(entry);
151
251
  return {
152
- key,
252
+ domain,
153
253
  created: true,
154
254
  entry
155
255
  };
156
256
  } catch (error) {
157
- var _readOutreachDoNotContactEntry;
257
+ var _readOutreachDoNotContactDomainEntry;
158
258
  if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
159
259
  const existing = await ref.get();
160
260
  return {
161
- key,
261
+ domain,
162
262
  created: false,
163
- entry: (_readOutreachDoNotContactEntry = readOutreachDoNotContactEntry(key, existing.data())) != null ? _readOutreachDoNotContactEntry : entry
263
+ entry: (_readOutreachDoNotContactDomainEntry = readOutreachDoNotContactDomainEntry(domain, existing.data())) != null ? _readOutreachDoNotContactDomainEntry : entry
164
264
  };
165
265
  }
166
266
  }
267
+ /** Takes a domain off the list; answers whether it was on it. */ export async function removeOutreachDoNotContactDomain(firestore, orgId, value) {
268
+ const domain = normalizeOutreachDomain(value);
269
+ if (!domain) return false;
270
+ const ref = outreachDoNotContactDomainCollection(firestore, orgId).doc(domain);
271
+ if (!(await ref.get()).exists) return false;
272
+ await ref.delete();
273
+ return true;
274
+ }
167
275
 
168
276
  //# sourceMappingURL=do-not-contact-store.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/do-not-contact-store.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 organization's Outreach do-not-contact list (AGL-2980):\n * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.\n *\n * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and\n * the API is shaped for both:\n *\n * - The console's routes: the enrollment gates read the list for every\n * person being enrolled at once ({@link lookupOutreachDoNotContact}, one\n * `getAll`), and \"Mark do-not-contact\" on an enrollment adds an entry with\n * the member as its author.\n * - The sending runtime: it re-reads the list for each person before each\n * send ({@link isOutreachDoNotContact}), and adds an entry when a reply\n * opts out, when the unsubscribe link is used and when an address bounces\n * for good, with `source: 'runtime'` and no member.\n *\n * ## Reads answer `null` when they could not be made\n *\n * `null` is not `false`. The gates read a `null` lookup as \"we couldn't\n * check\" and refuse the person, which is the only safe reading of a list\n * whose whole job is to stop mail: a person who asked not to be emailed must\n * not be emailed because a read timed out.\n *\n * ## An entry is written once\n *\n * The first reason an address was added is the record of why it is there;\n * a later add for the same address — an unsubscribe after a member already\n * marked it — finds it on the list and changes nothing. `create()` makes\n * that atomic without a transaction: two adds racing for one address cannot\n * both write.\n */\n\nimport { outreachDoNotContactKey } from '../engine/do-not-contact'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n OUTREACH_DO_NOT_CONTACT_SOURCES,\n type OutreachDoNotContactEntry,\n type OutreachDoNotContactReason,\n type OutreachDoNotContactSource,\n} from '../model/outreach.types'\n\n/** The longest detail an entry keeps. */\nexport const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** `orgs/{orgId}/outreachDoNotContact`. */\nexport function outreachDoNotContactCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContact)\n}\n\n/** A stored entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactEntry(\n key: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactEntry | null {\n if (!data) return null\n const reason = data['reason']\n const source = data['source']\n const text = (value: unknown): string | null =>\n typeof value === 'string' && value ? value : null\n const addedAtMs = Number(data['addedAtMs'])\n return {\n key,\n // An entry some other writer produced still keeps its address off the\n // list's sends; only how it reads is defaulted.\n reason: (OUTREACH_DO_NOT_CONTACT_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachDoNotContactReason)\n : 'manual',\n source: (OUTREACH_DO_NOT_CONTACT_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachDoNotContactSource)\n : 'member',\n addedByUid: text(data['addedByUid']),\n addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,\n enrollmentId: text(data['enrollmentId']),\n sequenceId: text(data['sequenceId']),\n detail: text(data['detail']),\n }\n}\n\n/**\n * Whether each address is on the list, in one round trip: `true` or `false`\n * per address, or `null` for an address that cannot be keyed and for every\n * address when the read failed. Keyed by the address exactly as given.\n */\nexport async function lookupOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const keyed: Array<{ email: string; key: string }> = []\n for (const email of emails) {\n const key = outreachDoNotContactKey(email)\n if (key) keyed.push({ email, key })\n else answers.set(email, null)\n }\n if (!keyed.length) return answers\n const list = outreachDoNotContactCollection(firestore, orgId)\n try {\n for (let start = 0; start < keyed.length; start += GET_ALL_CHUNK) {\n const chunk = keyed.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((entry) => list.doc(entry.key)))\n chunk.forEach((entry, index) => {\n answers.set(entry.email, snapshots[index]?.exists === true)\n })\n }\n } catch (error) {\n console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error)\n for (const entry of keyed) answers.set(entry.email, null)\n }\n return answers\n}\n\n/** Whether one address is on the list; `null` when that could not be told. */\nexport async function isOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<boolean | null> {\n return (await lookupOutreachDoNotContact(firestore, orgId, [email])).get(email) ?? null\n}\n\n/** The entry an address has, or `null` when it has none. */\nexport async function getOutreachDoNotContactEntry(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<OutreachDoNotContactEntry | null> {\n const key = outreachDoNotContactKey(email)\n if (!key) return null\n const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get()\n return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null\n}\n\nexport interface AddOutreachDoNotContactInput {\n orgId: string\n email: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member adding it; omit or `null` for the runtime. */\n addedByUid?: string | null\n nowMs: number\n enrollmentId?: string | null\n sequenceId?: string | null\n detail?: string | null\n}\n\nexport interface AddOutreachDoNotContactResult {\n key: string\n /** False when the address was on the list already; its entry is unchanged. */\n created: boolean\n /** The entry as it stands: the new one, or the one that was already there. */\n entry: OutreachDoNotContactEntry\n}\n\n/**\n * Puts an address on the list — see the module note for why a second add\n * changes nothing. Throws for a value that is not an address, a reason or a\n * source the list does not know, and a member-sourced add with no member:\n * a list that could hold an entry nobody can explain is not evidence of\n * anything.\n */\nexport async function addOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactInput,\n): Promise<AddOutreachDoNotContactResult> {\n const key = outreachDoNotContactKey(input.email)\n if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value')\n if (!(OUTREACH_DO_NOT_CONTACT_REASONS as readonly string[]).includes(input.reason)) {\n throw new Error(`[outreach] \"${String(input.reason)}\" is not a do-not-contact reason`)\n }\n if (!(OUTREACH_DO_NOT_CONTACT_SOURCES as readonly string[]).includes(input.source)) {\n throw new Error(`[outreach] \"${String(input.source)}\" is not a do-not-contact source`)\n }\n const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null\n if (input.source === 'member' && !addedByUid) {\n throw new Error('[outreach] a member-added do-not-contact entry names the member')\n }\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim()\n const entry: OutreachDoNotContactEntry = {\n key,\n reason: input.reason,\n source: input.source,\n addedByUid,\n addedAtMs: input.nowMs,\n enrollmentId: input.enrollmentId || null,\n sequenceId: input.sequenceId || null,\n detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null,\n }\n const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key)\n try {\n await ref.create(entry)\n return { key, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n key,\n created: false,\n entry: readOutreachDoNotContactEntry(key, existing.data()) ?? entry,\n }\n }\n}\n"],"names":["outreachDoNotContactKey","OUTREACH_COLLECTIONS","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_DETAIL_MAX","GET_ALL_CHUNK","ALREADY_EXISTS","outreachDoNotContactCollection","firestore","orgId","collection","doc","doNotContact","readOutreachDoNotContactEntry","key","data","reason","source","text","value","addedAtMs","Number","includes","addedByUid","isFinite","enrollmentId","sequenceId","detail","lookupOutreachDoNotContact","emails","answers","Map","keyed","email","push","set","length","list","start","chunk","slice","snapshots","getAll","map","entry","forEach","index","exists","error","console","isOutreachDoNotContact","get","getOutreachDoNotContactEntry","snapshot","addOutreachDoNotContact","input","Error","String","replace","trim","nowMs","ref","create","created","code","existing"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/do-not-contact-store.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 organization's Outreach do-not-contact list (AGL-2980):\n * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address —\n * and, beside it, `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, one\n * document per DOMAIN (AGL-3244), which the same callers read: an address\n * is refused when it, or the domain it is at, is listed.\n *\n * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and\n * the API is shaped for both:\n *\n * - The console's routes: the enrollment gates read the list for every\n * person being enrolled at once ({@link lookupOutreachDoNotContact}, one\n * `getAll`), and \"Mark do-not-contact\" on an enrollment adds an entry with\n * the member as its author.\n * - The sending runtime: it re-reads the list for each person before each\n * send ({@link isOutreachDoNotContact}), and adds an entry when a reply\n * opts out, when the unsubscribe link is used and when an address bounces\n * for good, with `source: 'runtime'` and no member.\n *\n * ## Reads answer `null` when they could not be made\n *\n * `null` is not `false`. The gates read a `null` lookup as \"we couldn't\n * check\" and refuse the person, which is the only safe reading of a list\n * whose whole job is to stop mail: a person who asked not to be emailed must\n * not be emailed because a read timed out.\n *\n * ## An entry is written once\n *\n * The first reason an address was added is the record of why it is there;\n * a later add for the same address — an unsubscribe after a member already\n * marked it — finds it on the list and changes nothing. `create()` makes\n * that atomic without a transaction: two adds racing for one address cannot\n * both write.\n */\n\nimport { outreachDoNotContactKey } from '../engine/do-not-contact'\nimport { normalizeOutreachDomain, outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n OUTREACH_DO_NOT_CONTACT_SOURCES,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactEntry,\n type OutreachDoNotContactReason,\n type OutreachDoNotContactSource,\n} from '../model/outreach.types'\n\n/** The longest detail an entry keeps. */\nexport const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** `orgs/{orgId}/outreachDoNotContact`. */\nexport function outreachDoNotContactCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContact)\n}\n\n/** A stored entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactEntry(\n key: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactEntry | null {\n if (!data) return null\n const reason = data['reason']\n const source = data['source']\n const text = (value: unknown): string | null =>\n typeof value === 'string' && value ? value : null\n const addedAtMs = Number(data['addedAtMs'])\n return {\n key,\n // An entry some other writer produced still keeps its address off the\n // list's sends; only how it reads is defaulted.\n reason: (OUTREACH_DO_NOT_CONTACT_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachDoNotContactReason)\n : 'manual',\n source: (OUTREACH_DO_NOT_CONTACT_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachDoNotContactSource)\n : 'member',\n addedByUid: text(data['addedByUid']),\n addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,\n enrollmentId: text(data['enrollmentId']),\n sequenceId: text(data['sequenceId']),\n detail: text(data['detail']),\n }\n}\n\n/**\n * Whether each address is on the list, in one round trip: `true` or `false`\n * per address, or `null` for an address that cannot be keyed and for every\n * address when the read failed. Keyed by the address exactly as given.\n */\nexport async function lookupOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const keyed: Array<{ email: string; key: string }> = []\n for (const email of emails) {\n const key = outreachDoNotContactKey(email)\n if (key) keyed.push({ email, key })\n else answers.set(email, null)\n }\n if (!keyed.length) return answers\n const list = outreachDoNotContactCollection(firestore, orgId)\n try {\n for (let start = 0; start < keyed.length; start += GET_ALL_CHUNK) {\n const chunk = keyed.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((entry) => list.doc(entry.key)))\n chunk.forEach((entry, index) => {\n answers.set(entry.email, snapshots[index]?.exists === true)\n })\n }\n } catch (error) {\n console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error)\n for (const entry of keyed) answers.set(entry.email, null)\n }\n return answers\n}\n\n/** Whether one address is on the list; `null` when that could not be told. */\nexport async function isOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<boolean | null> {\n return (await lookupOutreachDoNotContact(firestore, orgId, [email])).get(email) ?? null\n}\n\n/** The entry an address has, or `null` when it has none. */\nexport async function getOutreachDoNotContactEntry(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<OutreachDoNotContactEntry | null> {\n const key = outreachDoNotContactKey(email)\n if (!key) return null\n const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get()\n return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null\n}\n\nexport interface AddOutreachDoNotContactInput {\n orgId: string\n email: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member adding it; omit or `null` for the runtime. */\n addedByUid?: string | null\n nowMs: number\n enrollmentId?: string | null\n sequenceId?: string | null\n detail?: string | null\n}\n\nexport interface AddOutreachDoNotContactResult {\n key: string\n /** False when the address was on the list already; its entry is unchanged. */\n created: boolean\n /** The entry as it stands: the new one, or the one that was already there. */\n entry: OutreachDoNotContactEntry\n}\n\n/**\n * Puts an address on the list — see the module note for why a second add\n * changes nothing. Throws for a value that is not an address, a reason or a\n * source the list does not know, and a member-sourced add with no member:\n * a list that could hold an entry nobody can explain is not evidence of\n * anything.\n */\nexport async function addOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactInput,\n): Promise<AddOutreachDoNotContactResult> {\n const key = outreachDoNotContactKey(input.email)\n if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value')\n const entry: OutreachDoNotContactEntry = { key, ...entryFields(input) }\n const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key)\n try {\n await ref.create(entry)\n return { key, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n key,\n created: false,\n entry: readOutreachDoNotContactEntry(key, existing.data()) ?? entry,\n }\n }\n}\n\n/**\n * The fields an address entry and a domain entry share, held to the rules\n * the module note gives: a reason and a source the list knows, and a member\n * behind every member-sourced add.\n */\nfunction entryFields(\n input: Omit<AddOutreachDoNotContactInput, 'orgId' | 'email'>,\n): Omit<OutreachDoNotContactEntry, 'key'> {\n if (!(OUTREACH_DO_NOT_CONTACT_REASONS as readonly string[]).includes(input.reason)) {\n throw new Error(`[outreach] \"${String(input.reason)}\" is not a do-not-contact reason`)\n }\n if (!(OUTREACH_DO_NOT_CONTACT_SOURCES as readonly string[]).includes(input.source)) {\n throw new Error(`[outreach] \"${String(input.source)}\" is not a do-not-contact source`)\n }\n const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null\n if (input.source === 'member' && !addedByUid) {\n throw new Error('[outreach] a member-added do-not-contact entry names the member')\n }\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim()\n return {\n reason: input.reason,\n source: input.source,\n addedByUid,\n addedAtMs: input.nowMs,\n enrollmentId: input.enrollmentId || null,\n sequenceId: input.sequenceId || null,\n detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null,\n }\n}\n\n/*==========================================\n * DOMAINS (AGL-3244).\n *\n * `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, the domain itself\n * the id — it names a company, not a person, so it is stored in clear and\n * the Compliance page lists it. The same three rules hold: a read that\n * failed is `null`, an entry is written once, and every add is explained.\n *==========================================*/\n\n/** `orgs/{orgId}/outreachDoNotContactDomains`. */\nexport function outreachDoNotContactDomainCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContactDomains)\n}\n\n/** A stored domain entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactDomainEntry(\n domain: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactDomainEntry | null {\n const entry = readOutreachDoNotContactEntry(domain, data)\n if (!entry) return null\n const { key, ...rest } = entry\n return { domain: normalizeOutreachDomain(data?.['domain']) ?? key, ...rest }\n}\n\n/**\n * Whether each address's DOMAIN is on the list, in one round trip, keyed by\n * the address exactly as given: `true` or `false` per address, `null` for\n * an address whose domain cannot be read off it and for every address when\n * the read failed.\n */\nexport async function lookupOutreachDoNotContactDomains(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n const domains = [...new Set(domainOf.values())]\n if (!domains.length) return answers\n const list = outreachDoNotContactDomainCollection(firestore, orgId)\n const listed = new Map<string, boolean>()\n try {\n for (let start = 0; start < domains.length; start += GET_ALL_CHUNK) {\n const chunk = domains.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((domain) => list.doc(domain)))\n chunk.forEach((domain, index) => listed.set(domain, snapshots[index]?.exists === true))\n }\n for (const [email, domain] of domainOf) answers.set(email, listed.get(domain) ?? null)\n } catch (error) {\n console.error('[outreach] do-not-contact domain lookup failed; reading as unchecked', error)\n for (const email of domainOf.keys()) answers.set(email, null)\n }\n return answers\n}\n\n/** Every domain on the list, alphabetically. */\nexport async function listOutreachDoNotContactDomains(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<OutreachDoNotContactDomainEntry[]> {\n const snapshot = await outreachDoNotContactDomainCollection(firestore, orgId).get()\n return snapshot.docs\n .map((doc) => readOutreachDoNotContactDomainEntry(doc.id, doc.data()))\n .filter((entry): entry is OutreachDoNotContactDomainEntry => entry !== null)\n .sort((a, b) => a.domain.localeCompare(b.domain))\n}\n\nexport interface AddOutreachDoNotContactDomainInput extends Omit<AddOutreachDoNotContactInput, 'email'> {\n /** The domain, or an address at it; spelled by `normalizeOutreachDomain`. */\n domain: string\n}\n\nexport interface AddOutreachDoNotContactDomainResult {\n domain: string\n /** False when the domain was on the list already; its entry is unchanged. */\n created: boolean\n entry: OutreachDoNotContactDomainEntry\n}\n\n/** Puts a domain on the list; a second add changes nothing. Throws for a value that is not a domain. */\nexport async function addOutreachDoNotContactDomain(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactDomainInput,\n): Promise<AddOutreachDoNotContactDomainResult> {\n const domain = normalizeOutreachDomain(input.domain)\n if (!domain) throw new Error('[outreach] cannot key a do-not-contact entry for a value that is not a domain')\n const entry: OutreachDoNotContactDomainEntry = { domain, ...entryFields(input) }\n const ref = outreachDoNotContactDomainCollection(firestore, input.orgId).doc(domain)\n try {\n await ref.create(entry)\n return { domain, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n domain,\n created: false,\n entry: readOutreachDoNotContactDomainEntry(domain, existing.data()) ?? entry,\n }\n }\n}\n\n/** Takes a domain off the list; answers whether it was on it. */\nexport async function removeOutreachDoNotContactDomain(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n value: string,\n): Promise<boolean> {\n const domain = normalizeOutreachDomain(value)\n if (!domain) return false\n const ref = outreachDoNotContactDomainCollection(firestore, orgId).doc(domain)\n if (!(await ref.get()).exists) return false\n await ref.delete()\n return true\n}\n"],"names":["outreachDoNotContactKey","normalizeOutreachDomain","outreachEmailDomain","OUTREACH_COLLECTIONS","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_DETAIL_MAX","GET_ALL_CHUNK","ALREADY_EXISTS","outreachDoNotContactCollection","firestore","orgId","collection","doc","doNotContact","readOutreachDoNotContactEntry","key","data","reason","source","text","value","addedAtMs","Number","includes","addedByUid","isFinite","enrollmentId","sequenceId","detail","lookupOutreachDoNotContact","emails","answers","Map","keyed","email","push","set","length","list","start","chunk","slice","snapshots","getAll","map","entry","forEach","index","exists","error","console","isOutreachDoNotContact","get","getOutreachDoNotContactEntry","snapshot","addOutreachDoNotContact","input","Error","entryFields","ref","create","created","code","existing","String","replace","trim","nowMs","outreachDoNotContactDomainCollection","doNotContactDomains","readOutreachDoNotContactDomainEntry","domain","rest","lookupOutreachDoNotContactDomains","domainOf","domains","Set","values","listed","keys","listOutreachDoNotContactDomains","docs","id","filter","sort","a","b","localeCompare","addOutreachDoNotContactDomain","removeOutreachDoNotContactDomain","delete"],"mappings":"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"}