@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.
- package/README.md +4 -0
- package/package.json +13 -13
- package/src/lib/components/compliance-section.d.ts +3 -2
- package/src/lib/components/compliance-section.js +7 -2
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/do-not-contact-domains.d.ts +29 -0
- package/src/lib/components/do-not-contact-domains.js +208 -0
- package/src/lib/components/do-not-contact-domains.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +12 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +8 -1
- package/src/lib/components/use-outreach-data.js +32 -0
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +5 -0
- package/src/lib/constants/api-routes.js +4 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/do-not-contact-domain.d.ts +39 -0
- package/src/lib/engine/do-not-contact-domain.js +51 -0
- package/src/lib/engine/do-not-contact-domain.js.map +1 -0
- package/src/lib/engine/gates.d.ts +6 -1
- package/src/lib/engine/gates.js +12 -1
- package/src/lib/engine/gates.js.map +1 -1
- package/src/lib/engine/gateway-block.d.ts +31 -0
- package/src/lib/engine/gateway-block.js +54 -0
- package/src/lib/engine/gateway-block.js.map +1 -0
- package/src/lib/engine/index.d.ts +3 -0
- package/src/lib/engine/index.js +3 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/mailbox-health.d.ts +5 -0
- package/src/lib/engine/mailbox-health.js +12 -5
- package/src/lib/engine/mailbox-health.js.map +1 -1
- package/src/lib/engine/mailbox-notice.d.ts +46 -0
- package/src/lib/engine/mailbox-notice.js +64 -0
- package/src/lib/engine/mailbox-notice.js.map +1 -0
- package/src/lib/enrollment/enroll-people.js +6 -3
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +7 -3
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +23 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +33 -1
- package/src/lib/model/outreach.types.js +11 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/routes/do-not-contact-routes.d.ts +35 -0
- package/src/lib/routes/do-not-contact-routes.js +92 -0
- package/src/lib/routes/do-not-contact-routes.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +1 -0
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/enrollment-routes.js +13 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +2 -1
- package/src/lib/routes/platform-deps.js +2 -1
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/register-routes.js +8 -1
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-deps.d.ts +6 -0
- package/src/lib/routes/route-deps.js.map +1 -1
- package/src/lib/runtime/enrollment-events.d.ts +6 -2
- package/src/lib/runtime/enrollment-events.js +20 -3
- package/src/lib/runtime/enrollment-events.js.map +1 -1
- package/src/lib/runtime/mailbox-health-store.d.ts +1 -1
- package/src/lib/runtime/mailbox-health-store.js +17 -4
- package/src/lib/runtime/mailbox-health-store.js.map +1 -1
- package/src/lib/runtime/mailbox-notices.d.ts +39 -0
- package/src/lib/runtime/mailbox-notices.js +117 -0
- package/src/lib/runtime/mailbox-notices.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +4 -0
- package/src/lib/runtime/person-erasure.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.js +32 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/runtime-deps.d.ts +38 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +6 -6
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.d.ts +2 -0
- package/src/lib/runtime/sync-job.js +84 -15
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/runtime/timeline.d.ts +10 -1
- package/src/lib/runtime/timeline.js +23 -1
- package/src/lib/runtime/timeline.js.map +1 -1
- package/src/lib/runtime/unsubscribe-route.d.ts +1 -1
- package/src/lib/runtime/unsubscribe-route.js.map +1 -1
- package/src/lib/server.d.ts +1 -1
- package/src/lib/server.js +1 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +28 -1
- package/src/lib/storage/do-not-contact-store.js +117 -9
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
+
domain,
|
|
153
253
|
created: true,
|
|
154
254
|
entry
|
|
155
255
|
};
|
|
156
256
|
} catch (error) {
|
|
157
|
-
var
|
|
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
|
-
|
|
261
|
+
domain,
|
|
162
262
|
created: false,
|
|
163
|
-
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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"}
|