@aglyn/plugins-outreach 1.0.0-beta.199 → 1.0.0-beta.200
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/package.json +14 -14
- package/src/lib/components/do-not-contact-domains.js +60 -42
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +4 -2
- package/src/lib/components/enrollments-table.js +70 -89
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -3
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequences-section.d.ts +9 -0
- package/src/lib/components/sequences-section.js +96 -82
- package/src/lib/components/sequences-section.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +30 -25
- package/src/lib/components/use-outreach-data.js +93 -104
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/enrollment/enrollment-search.d.ts +32 -0
- package/src/lib/enrollment/enrollment-search.js +40 -0
- package/src/lib/enrollment/enrollment-search.js.map +1 -0
- package/src/lib/model/do-not-contact-domain-list-query.d.ts +32 -0
- package/src/lib/model/do-not-contact-domain-list-query.js +107 -0
- package/src/lib/model/do-not-contact-domain-list-query.js.map +1 -0
- package/src/lib/model/enrollment-list-query.d.ts +22 -0
- package/src/lib/model/enrollment-list-query.js +117 -0
- package/src/lib/model/enrollment-list-query.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +12 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-list-query.d.ts +22 -0
- package/src/lib/model/sequence-list-query.js +99 -0
- package/src/lib/model/sequence-list-query.js.map +1 -0
- package/src/lib/routes/enroll-routes.js +8 -0
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/sequence-routes.js +6 -1
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.js +6 -1
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
|
@@ -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 * 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":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCC,GAED,SAASA,uBAAuB,QAAQ,8BAA0B;AAClE,SAASC,uBAAuB,EAAEC,mBAAmB,QAAQ,qCAAiC;AAC9F,SACEC,oBAAoB,EACpBC,+BAA+B,EAC/BC,+BAA+B,QAK1B,6BAAyB;AAEhC,uCAAuC,GACvC,OAAO,MAAMC,qCAAqC,IAAG;AAErD,0DAA0D,GAC1D,MAAMC,gBAAgB;AAEtB,qFAAqF,GACrF,MAAMC,iBAAiB;AAEvB,yCAAyC,GACzC,OAAO,SAASC,+BACdC,SAAsC,EACtCC,KAAa;IAEb,OAAOD,UACJE,UAAU,CAAC,QACXC,GAAG,CAACF,OACJC,UAAU,CAACT,qBAAqBW,YAAY;AACjD;AAEA,gFAAgF,GAChF,OAAO,SAASC,8BACdC,GAAW,EACXC,IAAyC;IAEzC,IAAI,CAACA,MAAM,OAAO;IAClB,MAAMC,SAASD,IAAI,CAAC,SAAS;IAC7B,MAAME,SAASF,IAAI,CAAC,SAAS;IAC7B,MAAMG,OAAO,CAACC,QACZ,OAAOA,UAAU,YAAYA,QAAQA,QAAQ;IAC/C,MAAMC,YAAYC,OAAON,IAAI,CAAC,YAAY;IAC1C,OAAO;QACLD;QACA,sEAAsE;QACtE,gDAAgD;QAChDE,QAAQ,AAACd,gCAAuDoB,QAAQ,CAACN,UACpEA,SACD;QACJC,QAAQ,AAACd,gCAAuDmB,QAAQ,CAACL,UACpEA,SACD;QACJM,YAAYL,KAAKH,IAAI,CAAC,aAAa;QACnCK,WAAWC,OAAOG,QAAQ,CAACJ,cAAcA,YAAY,IAAIA,YAAY;QACrEK,cAAcP,KAAKH,IAAI,CAAC,eAAe;QACvCW,YAAYR,KAAKH,IAAI,CAAC,aAAa;QACnCY,QAAQT,KAAKH,IAAI,CAAC,SAAS;IAC7B;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAea,2BACpBpB,SAAsC,EACtCC,KAAa,EACboB,MAAyB;IAEzB,MAAMC,UAAU,IAAIC;IACpB,MAAMC,QAA+C,EAAE;IACvD,KAAK,MAAMC,SAASJ,OAAQ;QAC1B,MAAMf,MAAMhB,wBAAwBmC;QACpC,IAAInB,KAAKkB,MAAME,IAAI,CAAC;YAAED;YAAOnB;QAAI;aAC5BgB,QAAQK,GAAG,CAACF,OAAO;IAC1B;IACA,IAAI,CAACD,MAAMI,MAAM,EAAE,OAAON;IAC1B,MAAMO,OAAO9B,+BAA+BC,WAAWC;IACvD,IAAI;QACF,IAAK,IAAI6B,QAAQ,GAAGA,QAAQN,MAAMI,MAAM,EAAEE,SAASjC,cAAe;YAChE,MAAMkC,QAAQP,MAAMQ,KAAK,CAACF,OAAOA,QAAQjC;YACzC,MAAMoC,YAAY,MAAMjC,UAAUkC,MAAM,IAAIH,MAAMI,GAAG,CAAC,CAACC,QAAUP,KAAK1B,GAAG,CAACiC,MAAM9B,GAAG;YACnFyB,MAAMM,OAAO,CAAC,CAACD,OAAOE;oBACKL;gBAAzBX,QAAQK,GAAG,CAACS,MAAMX,KAAK,EAAEQ,EAAAA,mBAAAA,SAAS,CAACK,MAAM,qBAAhBL,iBAAkBM,MAAM,MAAK;YACxD;QACF;IACF,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,iEAAiEA;QAC/E,KAAK,MAAMJ,SAASZ,MAAOF,QAAQK,GAAG,CAACS,MAAMX,KAAK,EAAE;IACtD;IACA,OAAOH;AACT;AAEA,4EAA4E,GAC5E,OAAO,eAAeoB,uBACpB1C,SAAsC,EACtCC,KAAa,EACbwB,KAAa;QAEN;IAAP,QAAO,OAAA,AAAC,CAAA,MAAML,2BAA2BpB,WAAWC,OAAO;QAACwB;KAAM,CAAA,EAAGkB,GAAG,CAAClB,kBAAlE,OAA4E;AACrF;AAEA,0DAA0D,GAC1D,OAAO,eAAemB,6BACpB5C,SAAsC,EACtCC,KAAa,EACbwB,KAAa;IAEb,MAAMnB,MAAMhB,wBAAwBmC;IACpC,IAAI,CAACnB,KAAK,OAAO;IACjB,MAAMuC,WAAW,MAAM9C,+BAA+BC,WAAWC,OAAOE,GAAG,CAACG,KAAKqC,GAAG;IACpF,OAAOE,SAASN,MAAM,GAAGlC,8BAA8BC,KAAKuC,SAAStC,IAAI,MAAM;AACjF;AAuBA;;;;;;CAMC,GACD,OAAO,eAAeuC,wBACpB9C,SAAsC,EACtC+C,KAAmC;IAEnC,MAAMzC,MAAMhB,wBAAwByD,MAAMtB,KAAK;IAC/C,IAAI,CAACnB,KAAK,MAAM,IAAI0C,MAAM;IAC1B,MAAMZ,QAAmC;QAAE9B;OAAQ2C,YAAYF;IAC/D,MAAMG,MAAMnD,+BAA+BC,WAAW+C,MAAM9C,KAAK,EAAEE,GAAG,CAACG;IACvE,IAAI;QACF,MAAM4C,IAAIC,MAAM,CAACf;QACjB,OAAO;YAAE9B;YAAK8C,SAAS;YAAMhB;QAAM;IACrC,EAAE,OAAOI,OAAO;YAMLnC;QALT,IAAI,CAACmC,yBAAD,AAACA,MAA8Ba,IAAI,MAAKvD,gBAAgB,MAAM0C;QAClE,MAAMc,WAAW,MAAMJ,IAAIP,GAAG;QAC9B,OAAO;YACLrC;YACA8C,SAAS;YACThB,KAAK,GAAE/B,iCAAAA,8BAA8BC,KAAKgD,SAAS/C,IAAI,eAAhDF,iCAAuD+B;QAChE;IACF;AACF;AAEA;;;;CAIC,GACD,SAASa,YACPF,KAA4D;QAYtCA;IAVtB,IAAI,CAAC,AAACrD,gCAAsDoB,QAAQ,CAACiC,MAAMvC,MAAM,GAAG;QAClF,MAAM,IAAIwC,MAAM,CAAC,YAAY,EAAEO,OAAOR,MAAMvC,MAAM,EAAE,gCAAgC,CAAC;IACvF;IACA,IAAI,CAAC,AAACb,gCAAsDmB,QAAQ,CAACiC,MAAMtC,MAAM,GAAG;QAClF,MAAM,IAAIuC,MAAM,CAAC,YAAY,EAAEO,OAAOR,MAAMtC,MAAM,EAAE,gCAAgC,CAAC;IACvF;IACA,MAAMM,aAAa,OAAOgC,MAAMhC,UAAU,KAAK,YAAYgC,MAAMhC,UAAU,GAAGgC,MAAMhC,UAAU,GAAG;IACjG,IAAIgC,MAAMtC,MAAM,KAAK,YAAY,CAACM,YAAY;QAC5C,MAAM,IAAIiC,MAAM;IAClB;IACA,MAAM7B,SAASoC,QAAOR,gBAAAA,MAAM5B,MAAM,YAAZ4B,gBAAgB,IAAIS,OAAO,CAAC,QAAQ,KAAKC,IAAI;IACnE,OAAO;QACLjD,QAAQuC,MAAMvC,MAAM;QACpBC,QAAQsC,MAAMtC,MAAM;QACpBM;QACAH,WAAWmC,MAAMW,KAAK;QACtBzC,cAAc8B,MAAM9B,YAAY,IAAI;QACpCC,YAAY6B,MAAM7B,UAAU,IAAI;QAChCC,QAAQA,SAASA,OAAOa,KAAK,CAAC,GAAGpC,sCAAsC;IACzE;AACF;AAEA;;;;;;;4CAO4C,GAE5C,gDAAgD,GAChD,OAAO,SAAS+D,qCACd3D,SAAsC,EACtCC,KAAa;IAEb,OAAOD,UACJE,UAAU,CAAC,QACXC,GAAG,CAACF,OACJC,UAAU,CAACT,qBAAqBmE,mBAAmB;AACxD;AAEA,uFAAuF,GACvF,OAAO,SAASC,oCACdC,MAAc,EACdvD,IAAyC;QAKxBhB;IAHjB,MAAM6C,QAAQ/B,8BAA8ByD,QAAQvD;IACpD,IAAI,CAAC6B,OAAO,OAAO;IACnB,MAAM,EAAE9B,GAAG,EAAW,GAAG8B,OAAT2B,wCAAS3B;;;IACzB,OAAO;QAAE0B,MAAM,GAAEvE,2BAAAA,wBAAwBgB,wBAAAA,IAAM,CAAC,SAAS,aAAxChB,2BAA6Ce;OAAQyD;AACxE;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,kCACpBhE,SAAsC,EACtCC,KAAa,EACboB,MAAyB;IAEzB,MAAMC,UAAU,IAAIC;IACpB,MAAM0C,WAAW,IAAI1C;IACrB,KAAK,MAAME,SAASJ,OAAQ;QAC1B,MAAMyC,SAAStE,oBAAoBiC;QACnC,IAAIqC,QAAQG,SAAStC,GAAG,CAACF,OAAOqC;aAC3BxC,QAAQK,GAAG,CAACF,OAAO;IAC1B;IACA,MAAMyC,UAAU;WAAI,IAAIC,IAAIF,SAASG,MAAM;KAAI;IAC/C,IAAI,CAACF,QAAQtC,MAAM,EAAE,OAAON;IAC5B,MAAMO,OAAO8B,qCAAqC3D,WAAWC;IAC7D,MAAMoE,SAAS,IAAI9C;IACnB,IAAI;YAMyD8C;QAL3D,IAAK,IAAIvC,QAAQ,GAAGA,QAAQoC,QAAQtC,MAAM,EAAEE,SAASjC,cAAe;YAClE,MAAMkC,QAAQmC,QAAQlC,KAAK,CAACF,OAAOA,QAAQjC;YAC3C,MAAMoC,YAAY,MAAMjC,UAAUkC,MAAM,IAAIH,MAAMI,GAAG,CAAC,CAAC2B,SAAWjC,KAAK1B,GAAG,CAAC2D;YAC3E/B,MAAMM,OAAO,CAAC,CAACyB,QAAQxB;oBAA6BL;uBAAnBoC,OAAO1C,GAAG,CAACmC,QAAQ7B,EAAAA,mBAAAA,SAAS,CAACK,MAAM,qBAAhBL,iBAAkBM,MAAM,MAAK;;QACnF;QACA,KAAK,MAAM,CAACd,OAAOqC,OAAO,IAAIG,SAAU3C,QAAQK,GAAG,CAACF,QAAO4C,cAAAA,OAAO1B,GAAG,CAACmB,mBAAXO,cAAsB;IACnF,EAAE,OAAO7B,OAAO;QACdC,QAAQD,KAAK,CAAC,wEAAwEA;QACtF,KAAK,MAAMf,SAASwC,SAASK,IAAI,GAAIhD,QAAQK,GAAG,CAACF,OAAO;IAC1D;IACA,OAAOH;AACT;AAEA,8CAA8C,GAC9C,OAAO,eAAeiD,gCACpBvE,SAAsC,EACtCC,KAAa;IAEb,MAAM4C,WAAW,MAAMc,qCAAqC3D,WAAWC,OAAO0C,GAAG;IACjF,OAAOE,SAAS2B,IAAI,CACjBrC,GAAG,CAAC,CAAChC,MAAQ0D,oCAAoC1D,IAAIsE,EAAE,EAAEtE,IAAII,IAAI,KACjEmE,MAAM,CAAC,CAACtC,QAAoDA,UAAU,MACtEuC,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEd,MAAM,CAACgB,aAAa,CAACD,EAAEf,MAAM;AACnD;AAcA,sGAAsG,GACtG,OAAO,eAAeiB,8BACpB/E,SAAsC,EACtC+C,KAAyC;IAEzC,MAAMe,SAASvE,wBAAwBwD,MAAMe,MAAM;IACnD,IAAI,CAACA,QAAQ,MAAM,IAAId,MAAM;IAC7B,MAAMZ,QAAyC;QAAE0B;OAAWb,YAAYF;IACxE,MAAMG,MAAMS,qCAAqC3D,WAAW+C,MAAM9C,KAAK,EAAEE,GAAG,CAAC2D;IAC7E,IAAI;QACF,MAAMZ,IAAIC,MAAM,CAACf;QACjB,OAAO;YAAE0B;YAAQV,SAAS;YAAMhB;QAAM;IACxC,EAAE,OAAOI,OAAO;YAMLqB;QALT,IAAI,CAACrB,yBAAD,AAACA,MAA8Ba,IAAI,MAAKvD,gBAAgB,MAAM0C;QAClE,MAAMc,WAAW,MAAMJ,IAAIP,GAAG;QAC9B,OAAO;YACLmB;YACAV,SAAS;YACThB,KAAK,GAAEyB,uCAAAA,oCAAoCC,QAAQR,SAAS/C,IAAI,eAAzDsD,uCAAgEzB;QACzE;IACF;AACF;AAEA,+DAA+D,GAC/D,OAAO,eAAe4C,iCACpBhF,SAAsC,EACtCC,KAAa,EACbU,KAAa;IAEb,MAAMmD,SAASvE,wBAAwBoB;IACvC,IAAI,CAACmD,QAAQ,OAAO;IACpB,MAAMZ,MAAMS,qCAAqC3D,WAAWC,OAAOE,GAAG,CAAC2D;IACvE,IAAI,CAAC,AAAC,CAAA,MAAMZ,IAAIP,GAAG,EAAC,EAAGJ,MAAM,EAAE,OAAO;IACtC,MAAMW,IAAI+B,MAAM;IAChB,OAAO;AACT"}
|
|
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_DOMAIN_SEARCH_TOKENS,\n outreachDomainSearchTokens,\n} from '../model/do-not-contact-domain-list-query'\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 // With what the domains list searches it by (AGL-3321): an entry is\n // created once and never edited, so this is the one write that stamps it.\n await ref.create({\n ...entry,\n [OUTREACH_DOMAIN_SEARCH_TOKENS]: outreachDomainSearchTokens(entry),\n })\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_DOMAIN_SEARCH_TOKENS","outreachDomainSearchTokens","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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"}
|