@aglyn/plugins-email 1.0.0-beta.220 → 1.0.0-beta.222
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 +5 -4
- package/package.json +16 -17
- package/src/lib/components/email-template-detail.d.ts +5 -13
- package/src/lib/components/email-template-detail.js +29 -426
- package/src/lib/components/email-template-detail.js.map +1 -1
- package/src/lib/components/email-zones.d.ts +15 -0
- package/src/lib/components/email-zones.js +1 -0
- package/src/lib/components/email-zones.js.map +1 -1
- package/src/lib/components/list-detail-card.d.ts +0 -36
- package/src/lib/components/list-detail-card.js +125 -84
- package/src/lib/components/list-detail-card.js.map +1 -1
- package/src/lib/components/list-members-panel.d.ts +17 -0
- package/src/lib/components/list-members-panel.js +14 -0
- package/src/lib/components/list-members-panel.js.map +1 -1
- package/src/lib/components/site-package-email-preview.d.ts +13 -0
- package/src/lib/components/site-package-email-preview.js +47 -0
- package/src/lib/components/site-package-email-preview.js.map +1 -0
- package/src/lib/components/suppressions-card.js +75 -6
- package/src/lib/components/suppressions-card.js.map +1 -1
- package/src/lib/components/use-org-email-topics.js +2 -9
- package/src/lib/components/use-org-email-topics.js.map +1 -1
- package/src/lib/constants/site-package.d.ts +21 -0
- package/src/lib/constants/site-package.js +21 -0
- package/src/lib/constants/site-package.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +12 -0
- package/src/lib/declarations.console-server.js +73 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/list-import.d.ts +64 -75
- package/src/lib/list-import.js +43 -158
- package/src/lib/list-import.js.map +1 -1
- package/src/lib/model/email-topic-catalog.d.ts +24 -0
- package/src/lib/model/email-topic-catalog.js +19 -0
- package/src/lib/model/email-topic-catalog.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -25
- package/src/lib/model/index.js +0 -27
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.d.ts +8 -0
- package/src/lib/plugin.js +76 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +39 -0
- package/src/lib/server/person-eraser.js +44 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server-console.js +9 -9
- package/src/lib/server-console.js.map +1 -1
- package/src/lib/server-list-gate.d.ts +7 -0
- package/src/lib/server-list-gate.js +10 -3
- package/src/lib/server-list-gate.js.map +1 -1
- package/src/lib/server-list-import.d.ts +20 -177
- package/src/lib/server-list-import.js +34 -594
- package/src/lib/server-list-import.js.map +1 -1
- package/src/lib/server.js +16 -29
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-send-tally.d.ts +29 -0
- package/src/lib/testing/stand-in-send-tally.js +52 -0
- package/src/lib/testing/stand-in-send-tally.js.map +1 -0
- package/src/lib/transfer/email-transfer-catalog.d.ts +151 -0
- package/src/lib/transfer/email-transfer-catalog.js +536 -0
- package/src/lib/transfer/email-transfer-catalog.js.map +1 -0
- package/src/lib/transfer/list-import-steps.d.ts +44 -0
- package/src/lib/transfer/list-import-steps.js +201 -0
- package/src/lib/transfer/list-import-steps.js.map +1 -0
- package/src/lib/transfer/list-members.server.d.ts +165 -0
- package/src/lib/transfer/list-members.server.js +911 -0
- package/src/lib/transfer/list-members.server.js.map +1 -0
- package/src/lib/transfer/suppressions.server.d.ts +69 -0
- package/src/lib/transfer/suppressions.server.js +282 -0
- package/src/lib/transfer/suppressions.server.js.map +1 -0
- package/src/lib/transfer/topics-package.d.ts +56 -0
- package/src/lib/transfer/topics-package.js +87 -0
- package/src/lib/transfer/topics-package.js.map +1 -0
- package/src/lib/transfer/topics-package.server.d.ts +33 -0
- package/src/lib/transfer/topics-package.server.js +171 -0
- package/src/lib/transfer/topics-package.server.js.map +1 -0
- package/src/lib/transfer/transfer-reads.server.d.ts +25 -0
- package/src/lib/transfer/transfer-reads.server.js +34 -0
- package/src/lib/transfer/transfer-reads.server.js.map +1 -0
- package/src/lib/components/list-import-drawer.d.ts +0 -22
- package/src/lib/components/list-import-drawer.js +0 -662
- package/src/lib/components/list-import-drawer.js.map +0 -1
- package/src/lib/model/template-report.d.ts +0 -158
- package/src/lib/model/template-report.js +0 -249
- package/src/lib/model/template-report.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/transfer/list-members.server.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 * ONE LIST'S MEMBERS, IMPORTED AND EXPORTED (AGL-3529) — the server half of\n * `email.list-members` on the platform's transfer framework.\n *\n * The framework reads the file, maps its columns, finds the rows that repeat\n * an address, plans every row before anything is written, applies the plan\n * a chunk at a time through a ledger that never writes a row twice, resumes\n * where a closed tab stopped, and keeps an undo for seven days. What it\n * cannot know is a list's rules, and every one of them the importer this\n * replaces kept is kept here:\n *\n * - EVERY ADDRESS GOES THROUGH THE CHECKS A TYPED ONE DOES — the same\n * functions, not similar ones. `resolveAddresses` is the resolution the\n * audience card's add runs, `assignmentBasis` is the policy it applies,\n * and `enrollListMember` is the one writer of the membership collection.\n * Suppressed people and people who said no are refused at the write, with\n * the reason, whatever the file or the operator says.\n * - THE STATEMENT OF PERMISSION is the plugin's own wizard step. Its answer\n * reaches the dry run in `extras`; the dry run records WHO made it on the\n * list's import ledger (`orgs/{orgId}/lists/{listId}/imports/{jobId}`),\n * and every write reads the attester from there — never from whoever\n * pressed Resume, and never from the browser. Without it only people with\n * an opt-in on record are added. Neither the statement nor anything in a\n * file can make a refusal an opt-in (`CONSENT_FROM_FILE_REASON`).\n * - THE SCREENING — role accounts and column names that read as a bought or\n * appended list — and a consent check of a bounded sample are a warning on\n * the dry run that has to be acknowledged before anything is written, and\n * the sample is shown again on the permission step.\n * - SOMEBODY ALREADY ON THE LIST is unchanged: their membership is never\n * re-enrolled or rewritten. Their contact details change only when the\n * operator says so on the \"People already on this list\" step.\n * - A FILE'S CONTACT COLUMNS go to the contact record through the platform's\n * contact-capture door (`capturePluginContact`), the record system's own\n * write path — never written here. Each is planned against what the record\n * holds, under the person's field choices; a blank cell never empties one.\n * - 50,000 rows a file (`limits.maxRows`), refused at the upload before\n * anything is staged; repeats of an address are skipped as repeats; a line\n * that is not an address fails its row, by name, rather than vanishing.\n *\n * ## Who may\n *\n * The transfer gate admits a member who may manage the SITE's data. A list\n * is the organization's, and every site in it can mail one, so changing who\n * is on it — or reading their consent through the matching — needs an\n * org-wide owner, admin or editor (`isOrgWideListWriter`), the rule the\n * list routes apply. An export reads through `scopeTokens`: a collaborator\n * scoped to some sites reads no member at all, which is what the rules\n * would answer them.\n */\n\nimport {\n ASSIGNMENT_REFUSAL_MESSAGES,\n assignmentBasis,\n MARKETING_CONSENT_BASIS_FIELD,\n MARKETING_CONSENT_BY_HOST_FIELD,\n normalizeContactEmail,\n readMarketingBasis,\n type AssignmentRefusal,\n type ConsentGroup,\n} from '@aglyn/aglyn/server'\nimport {\n composeContactName,\n readContactFacet,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n TRANSFER_DRAFT_RETENTION_MS,\n TRANSFER_FILE_SAMPLE_ROW,\n TRANSFER_ID_FIELD,\n TRANSFER_UNDO_WINDOW_MS,\n TRANSFER_WARNING_CLASSES,\n buildTransferPlan,\n matchLookupKey,\n normalizeMatchValue,\n planTransferUndo,\n type BuildTransferPlanInput,\n type MatchLookupRequest,\n type PlannedTransferRow,\n type RowMatchOutcome,\n type TransferPlan,\n type TransferPolicy,\n type TransferRowResult,\n type TransferUndoEntry,\n type TransferUndoStep,\n type TransferWarning,\n type TransferWarningSample,\n} from '@aglyn/aglyn/data-transfer'\nimport { capturePluginContact } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport type {\n TransferApplyResult,\n TransferLookupResult,\n TransferReadOptions,\n TransferReadPage,\n TransferRecordsHooks,\n TransferResourceContext,\n TransferRevertResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n consentGroupForSite,\n enrollListMember,\n firebaseAdmin,\n orgDataCollectionForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { TransferEngineError } from '@aglyn/tenant-data-admin/server/transfer-jobs'\nimport { FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n importedBasisReason,\n isRoleAccount,\n purchaseTellColumns,\n} from '../list-import'\nimport {\n isOrgWideListWriter,\n LIST_MEMBER_BATCH_MAX,\n resolveAddresses,\n} from '../server-list-gate'\nimport {\n LIST_MEMBER_ALIASES,\n LIST_MEMBER_CONTACT_FIELD_IDS,\n LIST_MEMBER_LOCKED_RULES,\n LIST_MEMBER_MATCH_KEYS,\n contactFieldKey,\n listIdOfResourceKey,\n listMemberCatalog,\n type ListImportScreeningReport,\n readListConsentAnswer,\n readListExistingAnswer,\n} from './email-transfer-catalog'\nimport { countExisting, slices } from './transfer-reads.server'\n\n/** `source` stamped on every membership an import writes. */\nexport const CONSOLE_IMPORT_SOURCE = 'console:list-import'\n\n/** A list's import ledger: `orgs/{orgId}/lists/{listId}/imports/{jobId}`. */\nexport const LIST_IMPORTS_SUBCOLLECTION = 'imports'\n\n/**\n * How long a list's import ledger is kept (AGL-3549): it holds sample\n * shared-mailbox addresses and who stated permission, and is read only\n * while its job can still write — the longest a planned import may wait\n * to be applied (`TRANSFER_DRAFT_RETENTION_MS`, after which the job itself\n * expires), its undo window, and a day. The Firestore TTL policy on\n * `imports.expiresAt` deletes it; nothing else does.\n */\nexport const LIST_IMPORT_LEDGER_RETENTION_MS = TRANSFER_DRAFT_RETENTION_MS + TRANSFER_UNDO_WINDOW_MS + 24 * 60 * 60 * 1000\n\n/** The `expiresAt` of a list import ledger written at `nowMs`. */\nexport function listImportLedgerExpiry(nowMs: number): Timestamp {\n return Timestamp.fromMillis(nowMs + LIST_IMPORT_LEDGER_RETENTION_MS)\n}\n\n/** Role accounts kept verbatim on the ledger, so the step names some of them. */\nconst ROLE_ACCOUNT_SAMPLE_MAX = 25\n\n/** How many addresses one `in` query may carry. */\nconst IN_QUERY_MAX = 30\n\n/** Reads running at once. */\nconst READ_CONCURRENCY = 8\n\n/** Below this much of a chunk's budget, a write stops at a row boundary. */\nconst APPLY_RESERVE_MS = 3_000\n\n/** Why a contact detail stays as the record holds it. */\nexport const CONTACTS_KEPT_REASON =\n 'You chose to leave the contact details of people the workspace already holds as they are.'\n\n/*==========================================\n * THE LIST, AND WHO IS ASKING\n *=========================================*/\n\n/** The list an import or export is of, proved to exist. */\nexport interface ListTarget {\n hostId: string\n listRef: FirebaseFirestore.DocumentReference\n listName: string\n}\n\n/**\n * The list a request names, after proving the asker may change who is on\n * it. `write: false` is an export read: the route has already scoped it.\n */\nexport async function listTargetFor(\n ctx: TransferResourceContext,\n options: { write: boolean },\n): Promise<ListTarget> {\n const hostId = ctx.hostId ?? ''\n const listId = listIdOfResourceKey(ctx.resource) ?? ''\n if (!hostId || !listId) {\n throw new TransferEngineError('invalid', 400, 'Open this from the list it belongs to.')\n }\n if (options.write) {\n const membership = ctx.actorUid\n ? await resolveOrgMembership(ctx.actorUid, ctx.orgId).catch(() => null)\n : null\n if (!isOrgWideListWriter(membership?.member)) {\n throw new TransferEngineError(\n 'forbidden',\n 403,\n 'Lists belong to the whole organization, so changing who is on one needs organization-wide access rather than access to this site.',\n )\n }\n }\n const listRef = firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(ctx.orgId)\n .collection('lists')\n .doc(listId)\n const list = await listRef.get()\n // A stale id must not CREATE a list: a campaign's audience would then\n // read a list nobody set up.\n if (!list.exists) throw new TransferEngineError('notFound', 404, 'This list no longer exists.')\n return { hostId, listRef, listName: String(list.get('name') ?? listId) }\n}\n\n/*==========================================\n * READING MEMBERS AND THEIR CONTACT RECORDS\n *=========================================*/\n\n/** Runs `fn` over `items`, `limit` at a time, answers in order. */\nasync function mapLimited<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n const out = new Array<R>(items.length)\n let next = 0\n const worker = async () => {\n while (next < items.length) {\n const at = next\n next += 1\n out[at] = await fn(items[at] as T)\n }\n }\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))\n return out\n}\n\n/** A stored time as ISO text, or `null`. */\nfunction isoOf(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'number') return Number.isFinite(value) ? new Date(value).toISOString() : null\n const stamp = value as { toDate?: () => Date }\n if (typeof stamp.toDate === 'function') return stamp.toDate().toISOString()\n return null\n}\n\n/**\n * One member as the framework reads it: the membership by field id, and the\n * consent as the site the list is worked as reads it.\n */\nexport function memberValues(\n id: string,\n data: Record<string, unknown>,\n group: ConsentGroup,\n): Record<string, unknown> {\n const record = readMarketingBasis(data, group)\n const byHost = data[MARKETING_CONSENT_BY_HOST_FIELD] as\n | Record<string, Record<string, unknown>>\n | undefined\n const entry = byHost?.[group.hostId] ?? data\n const text = (value: unknown) => (typeof value === 'string' && value ? value : null)\n return {\n [TRANSFER_ID_FIELD]: id,\n email: text(data['email']),\n name: text(data['name']),\n addedAt: isoOf(data['addedAt']),\n source: text(data['source']),\n via: text(data['via']) ?? 'manual',\n consent: record.basis,\n consentBasis: text(entry?.[MARKETING_CONSENT_BASIS_FIELD]),\n consentAt: isoOf(record.basisAtMs),\n consentReason: text(entry?.['marketingConsentReason']),\n }\n}\n\n/**\n * The contact record's values for these addresses, as this site's holder\n * keeps them, by `contact:<field>` — and only for addresses a record exists\n * for, so \"no record\" and \"a record with nothing in it\" stay apart.\n */\nexport async function readContactValues(\n hostId: string,\n group: ConsentGroup,\n emails: readonly string[],\n): Promise<Map<string, Record<string, unknown>>> {\n const found = new Map<string, Record<string, unknown>>()\n const addresses = [...new Set(emails.filter(Boolean))]\n if (!addresses.length) return found\n const contacts = await orgDataCollectionForHost(hostId, 'contacts')\n const snapshots = await mapLimited(slices(addresses, IN_QUERY_MAX), READ_CONCURRENCY, (chunk) =>\n contacts.where('email', 'in', chunk).get(),\n )\n for (const snapshot of snapshots) {\n for (const doc of snapshot.docs) {\n const email = normalizeContactEmail(doc.get('email'))\n if (!email || found.has(email)) continue\n const facet = readContactFacet(doc.data() as Record<string, unknown>, group.groupId) as unknown as Record<\n string,\n unknown\n >\n found.set(\n email,\n Object.fromEntries(\n LIST_MEMBER_CONTACT_FIELD_IDS.map((fieldId) => {\n const value = facet[contactFieldKey(fieldId) as string]\n return [fieldId, typeof value === 'string' && value ? value : null]\n }),\n ),\n )\n }\n }\n return found\n}\n\n/** The members holding these addresses or ids, by member id. */\nasync function readMembers(\n members: FirebaseFirestore.CollectionReference,\n requests: readonly MatchLookupRequest[],\n): Promise<{ docs: Map<string, Record<string, unknown>>; lookup: Map<string, string[]> }> {\n const docs = new Map<string, Record<string, unknown>>()\n const lookup = new Map<string, string[]>()\n const note = (fieldId: string, value: string, id: string) => {\n const key = matchLookupKey(fieldId, value)\n const ids = lookup.get(key) ?? []\n if (!ids.includes(id)) ids.push(id)\n lookup.set(key, ids)\n }\n for (const request of requests) {\n if (request.fieldId === 'email') {\n const snapshots = await mapLimited(slices(request.values, IN_QUERY_MAX), READ_CONCURRENCY, (chunk) =>\n members.where('email', 'in', chunk).get(),\n )\n for (const snapshot of snapshots) {\n for (const doc of snapshot.docs) {\n const value = normalizeMatchValue('email', doc.get('email'))\n if (!value) continue\n docs.set(doc.id, doc.data() as Record<string, unknown>)\n note('email', value, doc.id)\n }\n }\n } else if (request.fieldId === TRANSFER_ID_FIELD) {\n const ids = request.values.filter((id) => id && !id.includes('/'))\n for (const chunk of slices(ids, 300)) {\n if (!chunk.length) continue\n const snapshots = await members.firestore.getAll(...chunk.map((id) => members.doc(id)))\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n docs.set(snapshot.id, snapshot.data() as Record<string, unknown>)\n note(TRANSFER_ID_FIELD, snapshot.id, snapshot.id)\n }\n }\n }\n }\n return { docs, lookup }\n}\n\n/*==========================================\n * THE PLAN\n *=========================================*/\n\n/** The warnings of several plans as one list, class by class. */\nexport function mergeTransferWarnings(lists: ReadonlyArray<readonly TransferWarning[]>): TransferWarning[] {\n const byClass = new Map<string, TransferWarning>()\n for (const list of lists) {\n for (const warning of list) {\n const held = byClass.get(warning.class)\n byClass.set(\n warning.class,\n held\n ? {\n ...held,\n count: held.count + warning.count,\n rows: held.rows + warning.rows,\n fieldIds: [...new Set([...held.fieldIds, ...warning.fieldIds])],\n samples: [...held.samples, ...warning.samples].slice(0, Math.max(5, held.samples.length)),\n requiresAcknowledgement: held.requiresAcknowledgement || warning.requiresAcknowledgement,\n }\n : { ...warning },\n )\n }\n }\n return TRANSFER_WARNING_CLASSES.filter((entry) => byClass.has(entry)).map(\n (entry) => byClass.get(entry) as TransferWarning,\n )\n}\n\n/** A row's values without the keys `keep` refuses. */\nfunction pick(values: Readonly<Record<string, unknown>>, keep: (fieldId: string) => boolean): Record<string, unknown> {\n return Object.fromEntries(Object.entries(values).filter(([fieldId]) => keep(fieldId)))\n}\n\n/**\n * The dry run (see the module note): the membership planned by the core,\n * the contact columns planned against the contact records, the two joined\n * row by row, and the screening added as one warning to acknowledge.\n * Records who stated permission on the list's import ledger; writes no\n * member and no contact.\n */\nexport async function planListMembers(\n ctx: TransferResourceContext,\n input: BuildTransferPlanInput,\n): Promise<TransferPlan> {\n const list = await listTargetFor(ctx, { write: true })\n const group = await consentGroupForSite(list.hostId)\n const existingAnswer = readListExistingAnswer(ctx.extras)\n const consentAnswer = readListConsentAnswer(ctx.extras)\n const isContact = (fieldId: string) => contactFieldKey(fieldId) !== null\n\n // A person is on a list once: \"create a duplicate\" is an update here.\n const policy: TransferPolicy = {\n ...input.policy,\n record: {\n ...input.policy.record,\n onMatch: input.policy.record.onMatch === 'duplicate' ? 'update' : input.policy.record.onMatch,\n },\n }\n\n // 1. The membership, against the members the lookup found.\n const membership = buildTransferPlan({\n ...input,\n policy,\n fields: input.fields.filter((field) => !isContact(field.id)),\n rows: input.rows.map((row) => ({ ...row, values: pick(row.values, (fieldId) => !isContact(fieldId)) })),\n })\n\n const emailOf = new Map<number, string>()\n for (const row of input.rows) {\n const email = normalizeContactEmail(row.values['email'])\n if (email) emailOf.set(row.index, email)\n }\n\n // 2. The contact columns, against the contact records, for every row that\n // adds somebody — and for somebody already on the list only when the\n // operator chose to update the people the workspace holds.\n const carriesContacts = input.rows.some((row) =>\n Object.keys(row.values).some((fieldId) => isContact(fieldId)),\n )\n const contactPlanned = new Map<number, PlannedTransferRow>()\n let contactWarnings: TransferWarning[] = []\n if (carriesContacts) {\n const eligible = membership.rows.filter(\n (row) =>\n (row.verdict === 'create' ||\n (existingAnswer.updateContacts && (row.verdict === 'unchanged' || row.verdict === 'update') && row.recordId)) &&\n emailOf.has(row.index),\n )\n const records = await readContactValues(\n list.hostId,\n group,\n eligible.map((row) => emailOf.get(row.index) as string),\n )\n const byIndex = new Map(input.rows.map((row) => [row.index, row]))\n const contactRows = eligible.map((row) => ({\n index: row.index,\n values: pick(byIndex.get(row.index)?.values ?? {}, isContact),\n }))\n const matches: RowMatchOutcome[] = eligible.map((row) => {\n const email = emailOf.get(row.index) as string\n return records.has(email)\n ? { kind: 'matched', recordId: email, via: { fieldId: 'email', value: email } }\n : { kind: 'new' }\n })\n const contactPolicy: TransferPolicy = {\n ...policy,\n record: { onMatch: 'update', onNew: 'create', onAmbiguous: 'skip' },\n rows: Object.fromEntries(\n Object.entries(policy.rows).map(([row, override]) => [row, override.fields ? { fields: override.fields } : {}]),\n ),\n locked: existingAnswer.updateContacts\n ? policy.locked\n : [\n ...policy.locked.filter((rule) => !isContact(rule.fieldId)),\n ...LIST_MEMBER_CONTACT_FIELD_IDS.map((fieldId) => ({\n fieldId,\n forced: { mode: 'keepExisting' as const, blank: 'leave' as const },\n reason: CONTACTS_KEPT_REASON,\n })),\n ],\n }\n const contacts = buildTransferPlan({\n fields: input.fields.filter((field) => isContact(field.id)),\n rows: contactRows,\n matches,\n existing: records,\n policy: contactPolicy,\n })\n for (const row of contacts.rows) contactPlanned.set(row.index, row)\n contactWarnings = contacts.warnings\n }\n\n // 3. The two, joined: a new member carries both; somebody already on the\n // list changes only when their contact record does.\n const rows = membership.rows.map((row): PlannedTransferRow => {\n const contact = contactPlanned.get(row.index)\n if (!contact) return row\n const diff = contact.verdict === 'create' || contact.verdict === 'update' ? contact.diff : []\n const joined = {\n ...row,\n heldBack: [...row.heldBack, ...contact.heldBack],\n warnings: [...new Set([...row.warnings, ...contact.warnings])],\n }\n if (row.verdict === 'create') return { ...joined, diff: [...row.diff, ...diff] }\n return diff.length ? { ...joined, verdict: 'update', diff } : joined\n })\n\n // 4. The screening, over the people the import would add.\n const adding = rows\n .filter((row) => row.verdict === 'create' && emailOf.has(row.index))\n .map((row) => ({ index: row.index, email: emailOf.get(row.index) as string }))\n const roles = adding.filter((entry) => isRoleAccount(entry.email))\n const tells = purchaseTellColumns(ctx.headers ?? [])\n const sampled = adding.slice(0, LIST_MEMBER_BATCH_MAX).map((entry) => entry.email)\n const resolution = sampled.length\n ? await resolveAddresses({ hostId: list.hostId, inputs: sampled })\n : { optedIn: 0, needAttestation: 0, refused: 0 }\n const attested = consentAnswer?.attest === true\n const declaresBasis = input.rows.some(\n (row) => !!String(row.values['declaredSource'] ?? '').trim() || !!String(row.values['declaredAt'] ?? '').trim(),\n )\n const report: ListImportScreeningReport = {\n roleAccounts: roles.length,\n roleAccountSamples: roles.slice(0, ROLE_ACCOUNT_SAMPLE_MAX).map((entry) => entry.email),\n purchaseTellColumns: tells,\n declaresBasis,\n sample: {\n size: sampled.length,\n of: adding.length,\n optedIn: resolution.optedIn,\n needAttestation: resolution.needAttestation,\n refused: resolution.refused,\n },\n }\n const screening = screeningWarning(report, roles, attested)\n const warnings = mergeTransferWarnings([membership.warnings, contactWarnings, screening ? [screening] : []])\n\n await list.listRef\n .collection(LIST_IMPORTS_SUBCOLLECTION)\n .doc(String(ctx.jobId ?? 'unknown'))\n .set(\n {\n jobId: ctx.jobId ?? null,\n listName: list.listName,\n hostId: list.hostId,\n status: 'planned',\n total: adding.length,\n screening: report,\n updateContacts: existingAnswer.updateContacts,\n /*\n * WHO stated permission, stored beside WHETHER — from the session\n * that made this dry run, never from the browser's answer. Every\n * write reads the attester from here.\n */\n attested,\n attestedByUid: attested ? ctx.actorUid : null,\n attestedAtMs: attested ? Date.now() : null,\n plannedByUid: ctx.actorUid,\n updatedAt: FieldValue.serverTimestamp(),\n // Re-stamped by every dry run, so the latest one sets the clock.\n expiresAt: listImportLedgerExpiry(Date.now()),\n },\n { merge: true },\n )\n\n const summary = { create: 0, update: 0, unchanged: 0, skip: 0, fail: 0, total: rows.length }\n for (const row of rows) summary[row.verdict] += 1\n return {\n rows,\n summary,\n warnings,\n acknowledgementsRequired: warnings.filter((entry) => entry.requiresAcknowledgement).map((entry) => entry.class),\n }\n}\n\n/**\n * The screening as one warning, or `null` when it found nothing worth a\n * word: the purchase tells first, then what the consent sample says will\n * happen, then the role accounts.\n */\nexport function screeningWarning(\n report: ListImportScreeningReport,\n roles: ReadonlyArray<{ index: number; email: string }>,\n attested: boolean,\n): TransferWarning | null {\n const samples: TransferWarningSample[] = report.purchaseTellColumns.map((column) => ({\n row: TRANSFER_FILE_SAMPLE_ROW,\n value: column,\n detail:\n 'A column named like a bought, rented or appended list. Those lists are not allowed: importing one puts every site sending through this domain at risk. If the name is a coincidence, carry on.',\n }))\n const { sample } = report\n const unlisted = !attested && sample.needAttestation > 0\n if (sample.refused > 0 || unlisted) {\n samples.push({\n row: TRANSFER_FILE_SAMPLE_ROW,\n detail:\n `${sample.size < sample.of ? `Of the first ${sample.size} of ${sample.of} new addresses` : `Of the ${sample.size} new addresses`}, ` +\n `${sample.optedIn} have an opt-in on record, ${sample.needAttestation} have none, and ${sample.refused} can never be added ` +\n '(they unsubscribed, bounced, complained or said no). ' +\n (unlisted\n ? 'You have not stated you have permission, so addresses with no opt-in on record will not be added. '\n : '') +\n 'Every address is checked again as it is added.',\n })\n }\n for (const role of roles.slice(0, 5)) {\n samples.push({\n row: role.index,\n fieldId: 'email',\n value: role.email,\n detail: 'A shared mailbox — common in lists collected rather than signed up to. It is added if you go ahead.',\n })\n }\n const count = report.purchaseTellColumns.length + roles.length + (sample.refused > 0 || unlisted ? 1 : 0)\n if (!count) return null\n return {\n class: 'screening',\n count,\n rows: roles.length,\n fieldIds: roles.length ? ['email'] : [],\n samples,\n requiresAcknowledgement: true,\n }\n}\n\n/*==========================================\n * APPLY\n *=========================================*/\n\n/** A field's planned value in a row's diff, or `undefined` when it does not change. */\nfunction after(row: PlannedTransferRow, fieldId: string): unknown {\n return row.diff.find((change) => change.fieldId === fieldId)?.after\n}\n\n/** The contact columns a row changes, by the record's own field names, blanks left out. */\nfunction contactProfile(row: PlannedTransferRow): Record<string, string> {\n const profile: Record<string, string> = {}\n for (const change of row.diff) {\n const key = contactFieldKey(change.fieldId)\n if (key && typeof change.after === 'string' && change.after.trim()) profile[key] = change.after.trim()\n }\n return profile\n}\n\n/** Hands a row's contact columns to the record system; `null` when it said why not. */\nasync function captureContact(\n ctx: TransferResourceContext,\n list: ListTarget,\n email: string,\n name: string | undefined,\n profile: Record<string, string>,\n): Promise<string | null> {\n if (!Object.keys(profile).length) return null\n const captured = await capturePluginContact({\n orgId: ctx.orgId,\n hostId: list.hostId,\n ...(ctx.actorUid ? { actor: { kind: 'member' as const, uid: ctx.actorUid } } : {}),\n identity: { email, ...(name ? { name } : {}) },\n interaction: { source: 'import', summary: `Imported onto the list “${list.listName}”` },\n surface: 'touch',\n profile,\n })\n return captured && captured.ok === false ? captured.error : null\n}\n\n/** One chunk's writes (see the module note). */\nexport async function applyListMembers(\n ctx: TransferResourceContext,\n chunk: { rows: PlannedTransferRow[] },\n writer: Parameters<TransferRecordsHooks['apply']>[2],\n): Promise<TransferApplyResult> {\n const list = await listTargetFor(ctx, { write: true })\n const results: TransferRowResult[] = []\n const undo: TransferUndoEntry[] = []\n\n /*\n * The ATTESTER's account, read from the ledger the dry run wrote — not\n * the caller's, and never the browser's. A colleague who resumes somebody\n * else's import has asserted nothing, and a consent record naming them\n * would be a claim nobody made. Both halves must agree: the plan this\n * apply runs was made with the statement, and the ledger says who made it.\n */\n const ledger = await list.listRef.collection(LIST_IMPORTS_SUBCOLLECTION).doc(String(ctx.jobId ?? 'unknown')).get()\n const attestingUid = String(ledger.get('attestedByUid') ?? '')\n const attested =\n readListConsentAnswer(ctx.extras)?.attest === true && ledger.get('attested') === true && !!attestingUid\n\n const pending: PlannedTransferRow[] = []\n for (const row of chunk.rows) {\n const done = await writer.alreadyApplied(row.index)\n if (done) results.push(done)\n else pending.push(row)\n }\n\n for (const batch of slices(pending, LIST_MEMBER_BATCH_MAX)) {\n if (writer.timeLeftMs() < APPLY_RESERVE_MS) break\n const creates = batch.filter((row) => row.verdict === 'create')\n const emails = creates.map((row) => normalizeContactEmail(after(row, 'email')) ?? '')\n const resolution = await resolveAddresses({ hostId: list.hostId, inputs: emails.filter(Boolean) })\n const verdicts = new Map(resolution.verdicts.filter((verdict) => verdict.email).map((verdict) => [verdict.email as string, verdict]))\n const nowMs = Date.now()\n\n for (const row of batch) {\n if (writer.timeLeftMs() < APPLY_RESERVE_MS) break\n const result: { result: TransferRowResult; undo?: TransferUndoEntry } =\n row.verdict === 'create'\n ? await addMember(ctx, list, row, { resolution, verdicts, attested, attestingUid, nowMs })\n : await updateMemberContact(ctx, list, row)\n await writer.markApplied(result.result, result.undo)\n results.push(result.result)\n if (result.undo) undo.push(result.undo)\n }\n }\n return { results, undo }\n}\n\n/** A refusal at the write, as a row result. */\nfunction refused(row: PlannedTransferRow, reason: AssignmentRefusal): TransferRowResult {\n return { row: row.index, outcome: 'failed', reason, message: ASSIGNMENT_REFUSAL_MESSAGES[reason] }\n}\n\n/** One new member, through the same gate and writer a typed address goes through. */\nasync function addMember(\n ctx: TransferResourceContext,\n list: ListTarget,\n row: PlannedTransferRow,\n batch: {\n resolution: Awaited<ReturnType<typeof resolveAddresses>>\n verdicts: Map<string, Awaited<ReturnType<typeof resolveAddresses>>['verdicts'][number]>\n attested: boolean\n attestingUid: string\n nowMs: number\n },\n): Promise<{ result: TransferRowResult; undo?: TransferUndoEntry }> {\n const email = normalizeContactEmail(after(row, 'email'))\n if (!email) return { result: refused(row, 'unroutable-address') }\n const verdict = batch.verdicts.get(email)\n if (!verdict || verdict.refusal) return { result: refused(row, verdict?.refusal ?? 'unroutable-address') }\n const decision = assignmentBasis({\n stored: batch.resolution.stored.get(email) ?? readMarketingBasis(null, batch.resolution.group),\n attested: batch.attested,\n actingUid: batch.attestingUid,\n nowMs: batch.nowMs,\n })\n if ('refusal' in decision) return { result: refused(row, decision.refusal) }\n\n const text = (fieldId: string) => {\n const value = after(row, fieldId)\n return typeof value === 'string' ? value.trim() : ''\n }\n const profile = contactProfile(row)\n // The list shows a name: the file's, else the one its first and last names make.\n const name = text('name') || composeContactName(profile['firstName'] ?? '', profile['lastName'] ?? '')\n const enrollment = await enrollListMember({\n listRef: list.listRef,\n group: batch.resolution.group,\n email,\n ...(name ? { name } : {}),\n source: CONSOLE_IMPORT_SOURCE,\n // Never `'rule'`: the dynamic-list materializer reconciles its own rows\n // away when somebody stops matching, and a file a merchant uploaded is\n // not a rule match that can lapse.\n via: 'manual',\n // A pass-through carries the person's grants as their record holds them\n // (AGL-3320); an attestation carries none and is this site's.\n ...(decision.basis === 'contact-opt-in' ? { grantEntries: batch.resolution.grants.get(email) ?? {} } : {}),\n consent: {\n ...decision,\n /*\n * The file's own declaration, carried onto the row it was made about —\n * but only for the basis it is evidence FOR. A pass-through carries the\n * person's own opt-in, and attaching a spreadsheet column's claim to\n * that would be dressing the person's act in the merchant's words.\n */\n ...(decision.basis === 'operator-attested'\n ? { reason: importedBasisReason({ declaredSource: text('declaredSource'), declaredAt: text('declaredAt') }) }\n : {}),\n },\n })\n if (enrollment.enrolled === false) {\n return { result: refused(row, enrollment.refusal === 'declined' ? 'declined' : 'unroutable-address') }\n }\n const contactError = await captureContact(ctx, list, email, name || undefined, profile)\n return {\n result: {\n row: row.index,\n outcome: enrollment.created ? 'created' : 'updated',\n recordId: enrollment.memberId,\n ...(contactError ? { message: `Added to the list. ${contactError}` } : {}),\n },\n /*\n * Undo takes back what the import ADDED and nothing else: a member it\n * created, while their membership still says what the import wrote. A\n * member who was already there when the write landed is not the\n * import's to remove, and the contact record is the record system's.\n */\n ...(enrollment.created\n ? {\n undo: {\n row: row.index,\n recordId: enrollment.memberId,\n action: 'created' as const,\n written: { email, ...(name ? { name } : {}), consent: 'granted' },\n },\n }\n : {}),\n }\n}\n\n/** Somebody already on the list whose contact record the operator chose to update. */\nasync function updateMemberContact(\n ctx: TransferResourceContext,\n list: ListTarget,\n row: PlannedTransferRow,\n): Promise<{ result: TransferRowResult }> {\n const member = row.recordId ? await list.listRef.collection('members').doc(row.recordId).get() : null\n const email = normalizeContactEmail(member?.get('email'))\n if (!member?.exists || !email) {\n return { result: { row: row.index, outcome: 'failed', reason: 'matchedRecordMissing' } }\n }\n const contactError = await captureContact(ctx, list, email, undefined, contactProfile(row))\n return {\n result: contactError\n ? { row: row.index, outcome: 'failed', recordId: member.id, message: contactError }\n : { row: row.index, outcome: 'updated', recordId: member.id },\n }\n}\n\n/*==========================================\n * UNDO\n *=========================================*/\n\n/**\n * Takes the import's new members off the list again. A membership that now\n * records a REFUSAL is never deleted, whatever the person decided: deleting\n * it would delete the record that the person said no.\n */\nexport async function revertListMembers(\n ctx: TransferResourceContext,\n snapshot: { entries: TransferUndoEntry[] },\n decisions?: Readonly<Record<string, 'revert' | 'keep'>>,\n): Promise<TransferRevertResult> {\n const list = await listTargetFor(ctx, { write: true })\n const group = await consentGroupForSite(list.hostId)\n const members = list.listRef.collection('members')\n const done: TransferUndoStep[] = []\n const conflicts: TransferUndoStep[] = []\n for (const entry of snapshot.entries) {\n const doc = await members.doc(entry.recordId).get()\n const current = doc.exists ? memberValues(doc.id, doc.data() as Record<string, unknown>, group) : null\n const step = planTransferUndo(entry, current)\n if (current?.['consent'] === 'declined') {\n done.push({ action: 'nothing', recordId: entry.recordId, why: 'alreadyReverted' })\n continue\n }\n if (step.action === 'delete' || (step.action === 'conflict' && decisions?.[entry.recordId] === 'revert')) {\n await doc.ref.delete()\n done.push({ action: 'delete', recordId: entry.recordId })\n } else if (step.action === 'conflict') {\n conflicts.push(step)\n } else {\n done.push(step)\n }\n }\n return { done, conflicts }\n}\n\n/*==========================================\n * EXPORT\n *=========================================*/\n\n/** The member-table predicates an export may carry: the table's own, nothing else. */\nconst FILTERABLE: Readonly<Record<string, readonly string[]>> = {\n email: ['=='],\n via: ['=='],\n searchTokens: ['array-contains'],\n}\n\n/**\n * The list table's filter as the export reads it — `{ filters }`, each a\n * predicate the table put on its own query — checked clause by clause:\n * only the table's fields and operators, so a request cannot ask the Admin\n * SDK a question the table never could.\n */\nexport function readMemberFilter(\n filter: Readonly<Record<string, unknown>> | undefined,\n): Array<{ path: string; op: '==' | 'array-contains'; value: string }> {\n const clauses = Array.isArray(filter?.['filters']) ? (filter?.['filters'] as unknown[]) : []\n return clauses.map((clause) => {\n const { path, op, value } = (clause ?? {}) as { path?: unknown; op?: unknown; value?: unknown }\n if (\n typeof path !== 'string' ||\n typeof op !== 'string' ||\n typeof value !== 'string' ||\n !FILTERABLE[path]?.includes(op)\n ) {\n throw new TransferEngineError('invalid', 400, 'The filter could not be read.')\n }\n return { path, op: op as '==' | 'array-contains', value }\n })\n}\n\n/** The members query for an export's filter. */\nfunction memberQuery(\n members: FirebaseFirestore.CollectionReference,\n options: TransferReadOptions | undefined,\n): FirebaseFirestore.Query {\n let query: FirebaseFirestore.Query = members\n for (const clause of readMemberFilter(options?.filter)) query = query.where(clause.path, clause.op, clause.value)\n return query\n}\n\n/** One page of members, holding only the chosen fields. */\nexport async function readListMemberPage(\n ctx: TransferResourceContext,\n cursor: string | null,\n fieldIds: readonly string[],\n options?: TransferReadOptions,\n): Promise<TransferReadPage> {\n // A collaborator scoped to some sites reads no member: the rules give the\n // organization's lists to org-wide members only.\n if (options?.scopeTokens) return { rows: [], next: null }\n const list = await listTargetFor(ctx, { write: false })\n const group = await consentGroupForSite(list.hostId)\n const members = list.listRef.collection('members')\n const size = Math.min(Math.max(options?.pageSize ?? 500, 1), 500)\n\n let docs: Array<{ id: string; data: Record<string, unknown> }>\n let next: string | null\n if (options?.ids) {\n const start = cursor ? Number(cursor) : 0\n const ids = options.ids.slice(start, start + size).filter((id) => id && !id.includes('/'))\n const snapshots = ids.length ? await members.firestore.getAll(...ids.map((id) => members.doc(id))) : []\n docs = snapshots.filter((one) => one.exists).map((one) => ({ id: one.id, data: one.data() as Record<string, unknown> }))\n next = start + size < options.ids.length ? String(start + size) : null\n } else {\n let query = memberQuery(members, options).orderBy('__name__').limit(size)\n if (cursor) query = query.startAfter(cursor)\n const snapshot = await query.get()\n docs = snapshot.docs.map((one) => ({ id: one.id, data: one.data() as Record<string, unknown> }))\n next = snapshot.docs.length === size ? (snapshot.docs[snapshot.docs.length - 1]?.id ?? null) : null\n }\n\n const wantsContacts = fieldIds.some((fieldId) => contactFieldKey(fieldId) !== null)\n const contacts = wantsContacts\n ? await readContactValues(\n list.hostId,\n group,\n docs.map((doc) => normalizeContactEmail(doc.data['email']) ?? ''),\n )\n : new Map<string, Record<string, unknown>>()\n const rows = docs.map((doc) => {\n const values: Record<string, unknown> = {\n ...memberValues(doc.id, doc.data, group),\n ...(contacts.get(normalizeContactEmail(doc.data['email']) ?? '') ?? {}),\n }\n return Object.fromEntries(fieldIds.map((fieldId) => [fieldId, values[fieldId] ?? null]))\n })\n return { rows, next }\n}\n\n/** How many members an export reads. */\nexport async function countListMembers(\n ctx: TransferResourceContext,\n options: TransferReadOptions,\n): Promise<number> {\n if (options.scopeTokens) return 0\n const list = await listTargetFor(ctx, { write: false })\n if (options.ids) return countExisting(list.listRef.collection('members'), options.ids)\n const counted = await memberQuery(list.listRef.collection('members'), options).count().get()\n return Number(counted.data().count ?? 0)\n}\n\n/*==========================================\n * LOOKUP, AND THE WHOLE RESOURCE\n *=========================================*/\n\n/**\n * The members holding each requested address or id, and what each holds now\n * — the membership, and their contact record's columns, so the conflicts\n * step can show what an update would replace.\n */\nexport async function lookupListMembers(\n ctx: TransferResourceContext,\n requests: readonly MatchLookupRequest[],\n): Promise<TransferLookupResult> {\n const list = await listTargetFor(ctx, { write: true })\n const group = await consentGroupForSite(list.hostId)\n const { docs, lookup } = await readMembers(list.listRef.collection('members'), requests)\n const contacts = await readContactValues(\n list.hostId,\n group,\n [...docs.values()].map((data) => normalizeContactEmail(data['email']) ?? ''),\n )\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n for (const [id, data] of docs) {\n records.set(id, {\n ...memberValues(id, data, group),\n ...(contacts.get(normalizeContactEmail(data['email']) ?? '') ?? {}),\n })\n }\n return { lookup, records }\n}\n\n/** `email.list-members`, as the framework asks it. */\nexport const listMembersTransferResource: TransferRecordsHooks = {\n fields: () => listMemberCatalog(),\n matchKeys: LIST_MEMBER_MATCH_KEYS,\n aliases: LIST_MEMBER_ALIASES,\n lockedRules: () => LIST_MEMBER_LOCKED_RULES,\n count: countListMembers,\n readPage: readListMemberPage,\n lookup: lookupListMembers,\n plan: planListMembers,\n apply: applyListMembers,\n revert: revertListMembers,\n}\n"],"names":["ASSIGNMENT_REFUSAL_MESSAGES","assignmentBasis","MARKETING_CONSENT_BASIS_FIELD","MARKETING_CONSENT_BY_HOST_FIELD","normalizeContactEmail","readMarketingBasis","composeContactName","readContactFacet","TRANSFER_DRAFT_RETENTION_MS","TRANSFER_FILE_SAMPLE_ROW","TRANSFER_ID_FIELD","TRANSFER_UNDO_WINDOW_MS","TRANSFER_WARNING_CLASSES","buildTransferPlan","matchLookupKey","normalizeMatchValue","planTransferUndo","capturePluginContact","consentGroupForSite","enrollListMember","firebaseAdmin","orgDataCollectionForHost","resolveOrgMembership","TransferEngineError","FieldValue","Timestamp","importedBasisReason","isRoleAccount","purchaseTellColumns","isOrgWideListWriter","LIST_MEMBER_BATCH_MAX","resolveAddresses","LIST_MEMBER_ALIASES","LIST_MEMBER_CONTACT_FIELD_IDS","LIST_MEMBER_LOCKED_RULES","LIST_MEMBER_MATCH_KEYS","contactFieldKey","listIdOfResourceKey","listMemberCatalog","readListConsentAnswer","readListExistingAnswer","countExisting","slices","CONSOLE_IMPORT_SOURCE","LIST_IMPORTS_SUBCOLLECTION","LIST_IMPORT_LEDGER_RETENTION_MS","listImportLedgerExpiry","nowMs","fromMillis","ROLE_ACCOUNT_SAMPLE_MAX","IN_QUERY_MAX","READ_CONCURRENCY","APPLY_RESERVE_MS","CONTACTS_KEPT_REASON","listTargetFor","ctx","options","list","hostId","listId","resource","write","membership","actorUid","orgId","catch","member","listRef","app","firestore","collection","doc","get","exists","listName","String","mapLimited","items","limit","fn","out","Array","length","next","worker","at","Promise","all","from","Math","min","isoOf","value","undefined","Number","isFinite","Date","toISOString","stamp","toDate","memberValues","id","data","group","text","record","byHost","entry","email","name","addedAt","source","via","consent","basis","consentBasis","consentAt","basisAtMs","consentReason","readContactValues","emails","found","Map","addresses","Set","filter","Boolean","contacts","snapshots","chunk","where","snapshot","docs","has","facet","groupId","set","Object","fromEntries","map","fieldId","readMembers","members","requests","lookup","note","key","ids","includes","push","request","values","getAll","mergeTransferWarnings","lists","byClass","warning","held","class","count","rows","fieldIds","samples","slice","max","requiresAcknowledgement","pick","keep","entries","planListMembers","input","existingAnswer","extras","consentAnswer","isContact","policy","onMatch","fields","field","row","emailOf","index","carriesContacts","some","keys","contactPlanned","contactWarnings","eligible","verdict","updateContacts","recordId","records","byIndex","contactRows","matches","kind","contactPolicy","onNew","onAmbiguous","override","locked","rule","forced","mode","blank","reason","existing","warnings","contact","diff","joined","heldBack","adding","roles","tells","headers","sampled","resolution","inputs","optedIn","needAttestation","refused","attested","attest","declaresBasis","trim","report","roleAccounts","roleAccountSamples","sample","size","of","screening","screeningWarning","jobId","status","total","attestedByUid","attestedAtMs","now","plannedByUid","updatedAt","serverTimestamp","expiresAt","merge","summary","create","update","unchanged","skip","fail","acknowledgementsRequired","column","detail","unlisted","role","after","find","change","contactProfile","profile","captureContact","captured","actor","uid","identity","interaction","surface","ok","error","applyListMembers","writer","ledger","results","undo","attestingUid","pending","done","alreadyApplied","batch","timeLeftMs","creates","verdicts","result","addMember","updateMemberContact","markApplied","outcome","message","refusal","decision","stored","actingUid","enrollment","grantEntries","grants","declaredSource","declaredAt","enrolled","contactError","created","memberId","action","written","revertListMembers","decisions","conflicts","current","step","why","ref","delete","FILTERABLE","searchTokens","readMemberFilter","clauses","isArray","clause","path","op","memberQuery","query","readListMemberPage","cursor","scopeTokens","pageSize","start","one","orderBy","startAfter","wantsContacts","countListMembers","counted","lookupListMembers","listMembersTransferResource","matchKeys","aliases","lockedRules","readPage","plan","apply","revert"],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A SITE'S SUPPRESSION LIST, IMPORTED AND EXPORTED (AGL-3529) — the server
|
|
19
|
+
* half of `email.suppressions`.
|
|
20
|
+
*
|
|
21
|
+
* ## An import only ever adds
|
|
22
|
+
*
|
|
23
|
+
* Suppressions arrive from a former provider's export, a spreadsheet of
|
|
24
|
+
* people who asked by phone, a complaint log. Each address the list does
|
|
25
|
+
* not hold is added as the Add drawer adds one (`server-suppressions.ts`):
|
|
26
|
+
* filed under `emailSuppressionKey`, the one derivation every reader and
|
|
27
|
+
* writer shares, with reason `manual` — a person on the team added it — and
|
|
28
|
+
* who did it.
|
|
29
|
+
*
|
|
30
|
+
* An address the list already holds is left EXACTLY as it is. A bounce that
|
|
31
|
+
* has been on the list for a month must not be relabeled `manual` and lose
|
|
32
|
+
* the reason a merchant needs to see before removing it, so the write is a
|
|
33
|
+
* `create` that refuses an existing entry rather than a merge over it, and
|
|
34
|
+
* no field choice in the wizard can say otherwise (`SUPPRESSION_KEPT_REASON`).
|
|
35
|
+
*
|
|
36
|
+
* And nothing here removes one — not the import, and not its undo: the
|
|
37
|
+
* import writes no undo entry, so Undo reports nothing to take back. An
|
|
38
|
+
* opt-out recorded by mistake is removed by hand on the Suppressions card,
|
|
39
|
+
* which names the reason before it does.
|
|
40
|
+
*
|
|
41
|
+
* ## Who may
|
|
42
|
+
*
|
|
43
|
+
* The site's own list, so the transfer gate's `data.manage` on the site is
|
|
44
|
+
* the boundary — the same population the rules let write
|
|
45
|
+
* `hosts/{hostId}/suppressions`. A collaborator who reaches the site reads
|
|
46
|
+
* its list; the export route has already refused one who does not.
|
|
47
|
+
*/
|
|
48
|
+
import { type MatchLookupRequest, type PlannedTransferRow } from '@aglyn/aglyn/data-transfer';
|
|
49
|
+
import type { TransferApplyResult, TransferLookupResult, TransferReadOptions, TransferReadPage, TransferRecordsHooks, TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
|
|
50
|
+
/** One entry by field id. `Since` is `createdAt`, the date the person actually left. */
|
|
51
|
+
export declare function suppressionValues(id: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
52
|
+
/** The entries holding each requested address or id. */
|
|
53
|
+
export declare function lookupSuppressions(ctx: TransferResourceContext, requests: readonly MatchLookupRequest[]): Promise<TransferLookupResult>;
|
|
54
|
+
/** Adds each new address; an address already suppressed is left as it is. */
|
|
55
|
+
export declare function applySuppressions(ctx: TransferResourceContext, chunk: {
|
|
56
|
+
rows: PlannedTransferRow[];
|
|
57
|
+
}, writer: Parameters<TransferRecordsHooks['apply']>[2]): Promise<TransferApplyResult>;
|
|
58
|
+
/** The card's predicates an export may carry: its own fields and operators only. */
|
|
59
|
+
export declare function readSuppressionFilter(filter: Readonly<Record<string, unknown>> | undefined): Array<{
|
|
60
|
+
path: string;
|
|
61
|
+
op: FirebaseFirestore.WhereFilterOp;
|
|
62
|
+
value: unknown;
|
|
63
|
+
}>;
|
|
64
|
+
/** One page of the list, holding only the chosen fields. */
|
|
65
|
+
export declare function readSuppressionPage(ctx: TransferResourceContext, cursor: string | null, fieldIds: readonly string[], options?: TransferReadOptions): Promise<TransferReadPage>;
|
|
66
|
+
/** How many entries an export reads. */
|
|
67
|
+
export declare function countSuppressions(ctx: TransferResourceContext, options: TransferReadOptions): Promise<number>;
|
|
68
|
+
/** `email.suppressions`, as the framework asks it. */
|
|
69
|
+
export declare const suppressionsTransferResource: TransferRecordsHooks;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* A SITE'S SUPPRESSION LIST, IMPORTED AND EXPORTED (AGL-3529) — the server
|
|
19
|
+
* half of `email.suppressions`.
|
|
20
|
+
*
|
|
21
|
+
* ## An import only ever adds
|
|
22
|
+
*
|
|
23
|
+
* Suppressions arrive from a former provider's export, a spreadsheet of
|
|
24
|
+
* people who asked by phone, a complaint log. Each address the list does
|
|
25
|
+
* not hold is added as the Add drawer adds one (`server-suppressions.ts`):
|
|
26
|
+
* filed under `emailSuppressionKey`, the one derivation every reader and
|
|
27
|
+
* writer shares, with reason `manual` — a person on the team added it — and
|
|
28
|
+
* who did it.
|
|
29
|
+
*
|
|
30
|
+
* An address the list already holds is left EXACTLY as it is. A bounce that
|
|
31
|
+
* has been on the list for a month must not be relabeled `manual` and lose
|
|
32
|
+
* the reason a merchant needs to see before removing it, so the write is a
|
|
33
|
+
* `create` that refuses an existing entry rather than a merge over it, and
|
|
34
|
+
* no field choice in the wizard can say otherwise (`SUPPRESSION_KEPT_REASON`).
|
|
35
|
+
*
|
|
36
|
+
* And nothing here removes one — not the import, and not its undo: the
|
|
37
|
+
* import writes no undo entry, so Undo reports nothing to take back. An
|
|
38
|
+
* opt-out recorded by mistake is removed by hand on the Suppressions card,
|
|
39
|
+
* which names the reason before it does.
|
|
40
|
+
*
|
|
41
|
+
* ## Who may
|
|
42
|
+
*
|
|
43
|
+
* The site's own list, so the transfer gate's `data.manage` on the site is
|
|
44
|
+
* the boundary — the same population the rules let write
|
|
45
|
+
* `hosts/{hostId}/suppressions`. A collaborator who reaches the site reads
|
|
46
|
+
* its list; the export route has already refused one who does not.
|
|
47
|
+
*/ import { TRANSFER_ID_FIELD, matchLookupKey, normalizeMatchValue } from "@aglyn/aglyn/data-transfer";
|
|
48
|
+
import { emailSearchTokens, emailSuppressionKey, firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
49
|
+
import { TransferEngineError } from "@aglyn/tenant-data-admin/server/transfer-jobs";
|
|
50
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
51
|
+
import { MANUAL_SUPPRESSION_REASON, SUPPRESSION_NOTE_MAX } from "../server-suppressions.js";
|
|
52
|
+
import { countExisting } from "./transfer-reads.server.js";
|
|
53
|
+
import { SUPPRESSION_ALIASES, SUPPRESSION_LOCKED_RULES, SUPPRESSION_MATCH_KEYS, suppressionCatalog } from "./email-transfer-catalog.js";
|
|
54
|
+
/** Below this much of a chunk's budget, a write stops at a row boundary. */ const APPLY_RESERVE_MS = 2000;
|
|
55
|
+
/** The site's suppression list. */ function suppressionsOf(ctx) {
|
|
56
|
+
if (!ctx.hostId) throw new TransferEngineError('invalid', 400, 'Suppressions belong to a site; name the site.');
|
|
57
|
+
return firebaseAdmin.app().firestore().collection('hosts').doc(ctx.hostId).collection('suppressions');
|
|
58
|
+
}
|
|
59
|
+
/** A stored time as ISO text, or `null`. */ function isoOf(value) {
|
|
60
|
+
const stamp = value;
|
|
61
|
+
return stamp && typeof stamp.toDate === 'function' ? stamp.toDate().toISOString() : null;
|
|
62
|
+
}
|
|
63
|
+
/** One entry by field id. `Since` is `createdAt`, the date the person actually left. */ export function suppressionValues(id, data) {
|
|
64
|
+
var _text, _isoOf;
|
|
65
|
+
const text = (value)=>typeof value === 'string' && value ? value : null;
|
|
66
|
+
return {
|
|
67
|
+
[TRANSFER_ID_FIELD]: id,
|
|
68
|
+
email: text(data['email']),
|
|
69
|
+
note: text(data['note']),
|
|
70
|
+
// An absent reason is an unsubscribe — what it has always meant.
|
|
71
|
+
reason: (_text = text(data['reason'])) != null ? _text : 'unsubscribe',
|
|
72
|
+
suppressedAt: (_isoOf = isoOf(data['createdAt'])) != null ? _isoOf : isoOf(data['suppressedAt']),
|
|
73
|
+
carriedFromHostId: text(data['carriedFromHostId'])
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The entries holding each requested address or id. */ export async function lookupSuppressions(ctx, requests) {
|
|
77
|
+
const suppressions = suppressionsOf(ctx);
|
|
78
|
+
const lookup = new Map();
|
|
79
|
+
const records = new Map();
|
|
80
|
+
for (const request of requests){
|
|
81
|
+
const ids = request.fieldId === 'email' ? request.values.map((value)=>({
|
|
82
|
+
value,
|
|
83
|
+
id: emailSuppressionKey(value)
|
|
84
|
+
})) : request.fieldId === TRANSFER_ID_FIELD ? request.values.map((value)=>({
|
|
85
|
+
value,
|
|
86
|
+
id: value
|
|
87
|
+
})) : [];
|
|
88
|
+
const wanted = ids.filter((entry)=>!!entry.id && !entry.id.includes('/'));
|
|
89
|
+
for(let at = 0; at < wanted.length; at += 300){
|
|
90
|
+
const chunk = wanted.slice(at, at + 300);
|
|
91
|
+
const snapshots = await suppressions.firestore.getAll(...chunk.map((entry)=>suppressions.doc(entry.id)));
|
|
92
|
+
snapshots.forEach((snapshot, index)=>{
|
|
93
|
+
if (!snapshot.exists) return;
|
|
94
|
+
const entry = chunk[index];
|
|
95
|
+
lookup.set(matchLookupKey(request.fieldId, entry.value), [
|
|
96
|
+
snapshot.id
|
|
97
|
+
]);
|
|
98
|
+
records.set(snapshot.id, suppressionValues(snapshot.id, snapshot.data()));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
lookup,
|
|
104
|
+
records
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Adds each new address; an address already suppressed is left as it is. */ export async function applySuppressions(ctx, chunk, writer) {
|
|
108
|
+
const suppressions = suppressionsOf(ctx);
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const row of chunk.rows){
|
|
111
|
+
const done = await writer.alreadyApplied(row.index);
|
|
112
|
+
if (done) {
|
|
113
|
+
results.push(done);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (writer.timeLeftMs() < APPLY_RESERVE_MS) break;
|
|
117
|
+
const result = await addSuppression(ctx, suppressions, row);
|
|
118
|
+
await writer.markApplied(result);
|
|
119
|
+
results.push(result);
|
|
120
|
+
}
|
|
121
|
+
// No undo: an import never takes a suppression away, and neither does its undo.
|
|
122
|
+
return {
|
|
123
|
+
results,
|
|
124
|
+
undo: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** One row: a new entry, or the existing one left alone. */ async function addSuppression(ctx, suppressions, row) {
|
|
128
|
+
var _value;
|
|
129
|
+
const value = (fieldId)=>{
|
|
130
|
+
var _row_diff_find;
|
|
131
|
+
return (_row_diff_find = row.diff.find((change)=>change.fieldId === fieldId)) == null ? void 0 : _row_diff_find.after;
|
|
132
|
+
};
|
|
133
|
+
const email = normalizeMatchValue('email', value('email'));
|
|
134
|
+
const key = email ? emailSuppressionKey(email) : null;
|
|
135
|
+
if (row.verdict !== 'create' || !email || !key) {
|
|
136
|
+
return _extends({
|
|
137
|
+
row: row.index,
|
|
138
|
+
outcome: 'unchanged'
|
|
139
|
+
}, row.recordId ? {
|
|
140
|
+
recordId: row.recordId
|
|
141
|
+
} : {});
|
|
142
|
+
}
|
|
143
|
+
const note = String((_value = value('note')) != null ? _value : '').trim().slice(0, SUPPRESSION_NOTE_MAX);
|
|
144
|
+
try {
|
|
145
|
+
var _ctx_actorUid;
|
|
146
|
+
await suppressions.doc(key).create(_extends({
|
|
147
|
+
email,
|
|
148
|
+
// What the list's search and Address filter query (AGL-3321).
|
|
149
|
+
emailTokens: emailSearchTokens(email),
|
|
150
|
+
reason: MANUAL_SUPPRESSION_REASON
|
|
151
|
+
}, note ? {
|
|
152
|
+
note
|
|
153
|
+
} : {}, {
|
|
154
|
+
// WHO recorded it, and from which import: a suppression is evidence.
|
|
155
|
+
suppressedByUid: (_ctx_actorUid = ctx.actorUid) != null ? _ctx_actorUid : null
|
|
156
|
+
}, ctx.jobId ? {
|
|
157
|
+
importJobId: ctx.jobId
|
|
158
|
+
} : {}, {
|
|
159
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
160
|
+
suppressedAt: FieldValue.serverTimestamp()
|
|
161
|
+
}));
|
|
162
|
+
return {
|
|
163
|
+
row: row.index,
|
|
164
|
+
outcome: 'created',
|
|
165
|
+
recordId: key
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Suppressed since the dry run: the entry that is there stays as it is.
|
|
169
|
+
if ((error == null ? void 0 : error.code) === 6 || /already exists/i.test(String(error == null ? void 0 : error.message))) {
|
|
170
|
+
return {
|
|
171
|
+
row: row.index,
|
|
172
|
+
outcome: 'unchanged',
|
|
173
|
+
recordId: key,
|
|
174
|
+
message: 'Already suppressed; left as it was.'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** The card's predicates an export may carry: its own fields and operators only. */ export function readSuppressionFilter(filter) {
|
|
181
|
+
const clauses = Array.isArray(filter == null ? void 0 : filter['filters']) ? filter == null ? void 0 : filter['filters'] : [];
|
|
182
|
+
return clauses.map((clause)=>{
|
|
183
|
+
const { path, op, value } = clause != null ? clause : {};
|
|
184
|
+
const strings = (list)=>Array.isArray(list) && list.length <= 30 && list.every((item)=>typeof item === 'string');
|
|
185
|
+
if (path === 'emailTokens' && op === 'array-contains' && typeof value === 'string') return {
|
|
186
|
+
path,
|
|
187
|
+
op,
|
|
188
|
+
value
|
|
189
|
+
};
|
|
190
|
+
if (path === 'reason' && op === '==' && typeof value === 'string') return {
|
|
191
|
+
path,
|
|
192
|
+
op,
|
|
193
|
+
value
|
|
194
|
+
};
|
|
195
|
+
if (path === 'reason' && op === 'in' && strings(value)) return {
|
|
196
|
+
path,
|
|
197
|
+
op,
|
|
198
|
+
value
|
|
199
|
+
};
|
|
200
|
+
if (path === 'createdAt' && (op === '<' || op === '<=' || op === '>' || op === '>=') && typeof value === 'string') {
|
|
201
|
+
const at = new Date(value);
|
|
202
|
+
if (!Number.isNaN(at.getTime())) return {
|
|
203
|
+
path,
|
|
204
|
+
op,
|
|
205
|
+
value: at
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
throw new TransferEngineError('invalid', 400, 'The filter could not be read.');
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** The query for an export's filter, and the order it is read in. */ function suppressionQuery(suppressions, options) {
|
|
212
|
+
const clauses = readSuppressionFilter(options == null ? void 0 : options.filter);
|
|
213
|
+
let query = suppressions;
|
|
214
|
+
for (const clause of clauses)query = query.where(clause.path, clause.op, clause.value);
|
|
215
|
+
// A date range reads in the card's own order; anything else by id, which
|
|
216
|
+
// every entry has — `createdAt` is absent on none written today, but the
|
|
217
|
+
// id order cannot drop a row whatever a writer once did.
|
|
218
|
+
return {
|
|
219
|
+
query,
|
|
220
|
+
byDate: clauses.some((clause)=>clause.path === 'createdAt')
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** One page of the list, holding only the chosen fields. */ export async function readSuppressionPage(ctx, cursor, fieldIds, options) {
|
|
224
|
+
var _ref;
|
|
225
|
+
const suppressions = suppressionsOf(ctx);
|
|
226
|
+
const size = Math.min(Math.max((_ref = options == null ? void 0 : options.pageSize) != null ? _ref : 500, 1), 500);
|
|
227
|
+
let docs;
|
|
228
|
+
let next;
|
|
229
|
+
if (options == null ? void 0 : options.ids) {
|
|
230
|
+
const start = cursor ? Number(cursor) : 0;
|
|
231
|
+
const ids = options.ids.slice(start, start + size).filter((id)=>id && !id.includes('/'));
|
|
232
|
+
docs = ids.length ? (await suppressions.firestore.getAll(...ids.map((id)=>suppressions.doc(id)))).filter((doc)=>doc.exists) : [];
|
|
233
|
+
next = start + size < options.ids.length ? String(start + size) : null;
|
|
234
|
+
} else {
|
|
235
|
+
var _ref1;
|
|
236
|
+
var _snapshot_docs_;
|
|
237
|
+
const { query, byDate } = suppressionQuery(suppressions, options);
|
|
238
|
+
let page = (byDate ? query.orderBy('createdAt', 'desc') : query.orderBy('__name__')).limit(size);
|
|
239
|
+
if (cursor) page = byDate ? page.startAfter(await suppressions.doc(cursor).get()) : page.startAfter(cursor);
|
|
240
|
+
const snapshot = await page.get();
|
|
241
|
+
docs = snapshot.docs;
|
|
242
|
+
next = snapshot.docs.length === size ? (_ref1 = (_snapshot_docs_ = snapshot.docs[snapshot.docs.length - 1]) == null ? void 0 : _snapshot_docs_.id) != null ? _ref1 : null : null;
|
|
243
|
+
}
|
|
244
|
+
const rows = docs.map((doc)=>{
|
|
245
|
+
var _doc_data;
|
|
246
|
+
const values = suppressionValues(doc.id, (_doc_data = doc.data()) != null ? _doc_data : {});
|
|
247
|
+
return Object.fromEntries(fieldIds.map((fieldId)=>{
|
|
248
|
+
var _values_fieldId;
|
|
249
|
+
return [
|
|
250
|
+
fieldId,
|
|
251
|
+
(_values_fieldId = values[fieldId]) != null ? _values_fieldId : null
|
|
252
|
+
];
|
|
253
|
+
}));
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
rows,
|
|
257
|
+
next
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** How many entries an export reads. */ export async function countSuppressions(ctx, options) {
|
|
261
|
+
var _counted_data_count;
|
|
262
|
+
if (options.ids) return countExisting(suppressionsOf(ctx), options.ids);
|
|
263
|
+
const counted = await suppressionQuery(suppressionsOf(ctx), options).query.count().get();
|
|
264
|
+
return Number((_counted_data_count = counted.data().count) != null ? _counted_data_count : 0);
|
|
265
|
+
}
|
|
266
|
+
/** `email.suppressions`, as the framework asks it. */ export const suppressionsTransferResource = {
|
|
267
|
+
fields: ()=>suppressionCatalog(),
|
|
268
|
+
matchKeys: SUPPRESSION_MATCH_KEYS,
|
|
269
|
+
aliases: SUPPRESSION_ALIASES,
|
|
270
|
+
lockedRules: ()=>SUPPRESSION_LOCKED_RULES,
|
|
271
|
+
count: countSuppressions,
|
|
272
|
+
readPage: readSuppressionPage,
|
|
273
|
+
lookup: lookupSuppressions,
|
|
274
|
+
apply: applySuppressions,
|
|
275
|
+
// Nothing to take back: the import writes no undo entry (see the module note).
|
|
276
|
+
revert: async ()=>({
|
|
277
|
+
done: [],
|
|
278
|
+
conflicts: []
|
|
279
|
+
})
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=suppressions.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/email/src/lib/transfer/suppressions.server.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 * A SITE'S SUPPRESSION LIST, IMPORTED AND EXPORTED (AGL-3529) — the server\n * half of `email.suppressions`.\n *\n * ## An import only ever adds\n *\n * Suppressions arrive from a former provider's export, a spreadsheet of\n * people who asked by phone, a complaint log. Each address the list does\n * not hold is added as the Add drawer adds one (`server-suppressions.ts`):\n * filed under `emailSuppressionKey`, the one derivation every reader and\n * writer shares, with reason `manual` — a person on the team added it — and\n * who did it.\n *\n * An address the list already holds is left EXACTLY as it is. A bounce that\n * has been on the list for a month must not be relabeled `manual` and lose\n * the reason a merchant needs to see before removing it, so the write is a\n * `create` that refuses an existing entry rather than a merge over it, and\n * no field choice in the wizard can say otherwise (`SUPPRESSION_KEPT_REASON`).\n *\n * And nothing here removes one — not the import, and not its undo: the\n * import writes no undo entry, so Undo reports nothing to take back. An\n * opt-out recorded by mistake is removed by hand on the Suppressions card,\n * which names the reason before it does.\n *\n * ## Who may\n *\n * The site's own list, so the transfer gate's `data.manage` on the site is\n * the boundary — the same population the rules let write\n * `hosts/{hostId}/suppressions`. A collaborator who reaches the site reads\n * its list; the export route has already refused one who does not.\n */\n\nimport {\n TRANSFER_ID_FIELD,\n matchLookupKey,\n normalizeMatchValue,\n type MatchLookupRequest,\n type PlannedTransferRow,\n type TransferRowResult,\n} from '@aglyn/aglyn/data-transfer'\nimport type {\n TransferApplyResult,\n TransferLookupResult,\n TransferReadOptions,\n TransferReadPage,\n TransferRecordsHooks,\n TransferResourceContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { emailSearchTokens, emailSuppressionKey, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { TransferEngineError } from '@aglyn/tenant-data-admin/server/transfer-jobs'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { MANUAL_SUPPRESSION_REASON, SUPPRESSION_NOTE_MAX } from '../server-suppressions'\nimport { countExisting } from './transfer-reads.server'\nimport {\n SUPPRESSION_ALIASES,\n SUPPRESSION_LOCKED_RULES,\n SUPPRESSION_MATCH_KEYS,\n suppressionCatalog,\n} from './email-transfer-catalog'\n\n/** Below this much of a chunk's budget, a write stops at a row boundary. */\nconst APPLY_RESERVE_MS = 2_000\n\n/** The site's suppression list. */\nfunction suppressionsOf(ctx: TransferResourceContext): FirebaseFirestore.CollectionReference {\n if (!ctx.hostId) throw new TransferEngineError('invalid', 400, 'Suppressions belong to a site; name the site.')\n return firebaseAdmin.app().firestore().collection('hosts').doc(ctx.hostId).collection('suppressions')\n}\n\n/** A stored time as ISO text, or `null`. */\nfunction isoOf(value: unknown): string | null {\n const stamp = value as { toDate?: () => Date } | null | undefined\n return stamp && typeof stamp.toDate === 'function' ? stamp.toDate().toISOString() : null\n}\n\n/** One entry by field id. `Since` is `createdAt`, the date the person actually left. */\nexport function suppressionValues(id: string, data: Record<string, unknown>): Record<string, unknown> {\n const text = (value: unknown) => (typeof value === 'string' && value ? value : null)\n return {\n [TRANSFER_ID_FIELD]: id,\n email: text(data['email']),\n note: text(data['note']),\n // An absent reason is an unsubscribe — what it has always meant.\n reason: text(data['reason']) ?? 'unsubscribe',\n suppressedAt: isoOf(data['createdAt']) ?? isoOf(data['suppressedAt']),\n carriedFromHostId: text(data['carriedFromHostId']),\n }\n}\n\n/** The entries holding each requested address or id. */\nexport async function lookupSuppressions(\n ctx: TransferResourceContext,\n requests: readonly MatchLookupRequest[],\n): Promise<TransferLookupResult> {\n const suppressions = suppressionsOf(ctx)\n const lookup = new Map<string, string[]>()\n const records = new Map<string, Readonly<Record<string, unknown>>>()\n for (const request of requests) {\n const ids =\n request.fieldId === 'email'\n ? request.values.map((value) => ({ value, id: emailSuppressionKey(value) }))\n : request.fieldId === TRANSFER_ID_FIELD\n ? request.values.map((value) => ({ value, id: value }))\n : []\n const wanted = ids.filter((entry): entry is { value: string; id: string } => !!entry.id && !entry.id.includes('/'))\n for (let at = 0; at < wanted.length; at += 300) {\n const chunk = wanted.slice(at, at + 300)\n const snapshots = await suppressions.firestore.getAll(...chunk.map((entry) => suppressions.doc(entry.id)))\n snapshots.forEach((snapshot, index) => {\n if (!snapshot.exists) return\n const entry = chunk[index] as { value: string; id: string }\n lookup.set(matchLookupKey(request.fieldId, entry.value), [snapshot.id])\n records.set(snapshot.id, suppressionValues(snapshot.id, snapshot.data() as Record<string, unknown>))\n })\n }\n }\n return { lookup, records }\n}\n\n/** Adds each new address; an address already suppressed is left as it is. */\nexport async function applySuppressions(\n ctx: TransferResourceContext,\n chunk: { rows: PlannedTransferRow[] },\n writer: Parameters<TransferRecordsHooks['apply']>[2],\n): Promise<TransferApplyResult> {\n const suppressions = suppressionsOf(ctx)\n const results: TransferRowResult[] = []\n for (const row of chunk.rows) {\n const done = await writer.alreadyApplied(row.index)\n if (done) {\n results.push(done)\n continue\n }\n if (writer.timeLeftMs() < APPLY_RESERVE_MS) break\n const result = await addSuppression(ctx, suppressions, row)\n await writer.markApplied(result)\n results.push(result)\n }\n // No undo: an import never takes a suppression away, and neither does its undo.\n return { results, undo: [] }\n}\n\n/** One row: a new entry, or the existing one left alone. */\nasync function addSuppression(\n ctx: TransferResourceContext,\n suppressions: FirebaseFirestore.CollectionReference,\n row: PlannedTransferRow,\n): Promise<TransferRowResult> {\n const value = (fieldId: string) => row.diff.find((change) => change.fieldId === fieldId)?.after\n const email = normalizeMatchValue('email', value('email'))\n const key = email ? emailSuppressionKey(email) : null\n if (row.verdict !== 'create' || !email || !key) {\n return { row: row.index, outcome: 'unchanged', ...(row.recordId ? { recordId: row.recordId } : {}) }\n }\n const note = String(value('note') ?? '')\n .trim()\n .slice(0, SUPPRESSION_NOTE_MAX)\n try {\n await suppressions.doc(key).create({\n email,\n // What the list's search and Address filter query (AGL-3321).\n emailTokens: emailSearchTokens(email),\n reason: MANUAL_SUPPRESSION_REASON,\n ...(note ? { note } : {}),\n // WHO recorded it, and from which import: a suppression is evidence.\n suppressedByUid: ctx.actorUid ?? null,\n ...(ctx.jobId ? { importJobId: ctx.jobId } : {}),\n createdAt: FieldValue.serverTimestamp(),\n suppressedAt: FieldValue.serverTimestamp(),\n })\n return { row: row.index, outcome: 'created', recordId: key }\n } catch (error) {\n // Suppressed since the dry run: the entry that is there stays as it is.\n if ((error as { code?: unknown })?.code === 6 || /already exists/i.test(String((error as Error)?.message))) {\n return { row: row.index, outcome: 'unchanged', recordId: key, message: 'Already suppressed; left as it was.' }\n }\n throw error\n }\n}\n\n/** The card's predicates an export may carry: its own fields and operators only. */\nexport function readSuppressionFilter(\n filter: Readonly<Record<string, unknown>> | undefined,\n): Array<{ path: string; op: FirebaseFirestore.WhereFilterOp; value: unknown }> {\n const clauses = Array.isArray(filter?.['filters']) ? (filter?.['filters'] as unknown[]) : []\n return clauses.map((clause) => {\n const { path, op, value } = (clause ?? {}) as { path?: unknown; op?: unknown; value?: unknown }\n const strings = (list: unknown) => Array.isArray(list) && list.length <= 30 && list.every((item) => typeof item === 'string')\n if (path === 'emailTokens' && op === 'array-contains' && typeof value === 'string') return { path, op, value }\n if (path === 'reason' && op === '==' && typeof value === 'string') return { path, op, value }\n if (path === 'reason' && op === 'in' && strings(value)) return { path, op, value }\n if (path === 'createdAt' && (op === '<' || op === '<=' || op === '>' || op === '>=') && typeof value === 'string') {\n const at = new Date(value)\n if (!Number.isNaN(at.getTime())) return { path, op, value: at }\n }\n throw new TransferEngineError('invalid', 400, 'The filter could not be read.')\n })\n}\n\n/** The query for an export's filter, and the order it is read in. */\nfunction suppressionQuery(\n suppressions: FirebaseFirestore.CollectionReference,\n options: TransferReadOptions | undefined,\n): { query: FirebaseFirestore.Query; byDate: boolean } {\n const clauses = readSuppressionFilter(options?.filter)\n let query: FirebaseFirestore.Query = suppressions\n for (const clause of clauses) query = query.where(clause.path, clause.op, clause.value)\n // A date range reads in the card's own order; anything else by id, which\n // every entry has — `createdAt` is absent on none written today, but the\n // id order cannot drop a row whatever a writer once did.\n return { query, byDate: clauses.some((clause) => clause.path === 'createdAt') }\n}\n\n/** One page of the list, holding only the chosen fields. */\nexport async function readSuppressionPage(\n ctx: TransferResourceContext,\n cursor: string | null,\n fieldIds: readonly string[],\n options?: TransferReadOptions,\n): Promise<TransferReadPage> {\n const suppressions = suppressionsOf(ctx)\n const size = Math.min(Math.max(options?.pageSize ?? 500, 1), 500)\n let docs: FirebaseFirestore.DocumentSnapshot[]\n let next: string | null\n if (options?.ids) {\n const start = cursor ? Number(cursor) : 0\n const ids = options.ids.slice(start, start + size).filter((id) => id && !id.includes('/'))\n docs = ids.length ? (await suppressions.firestore.getAll(...ids.map((id) => suppressions.doc(id)))).filter((doc) => doc.exists) : []\n next = start + size < options.ids.length ? String(start + size) : null\n } else {\n const { query, byDate } = suppressionQuery(suppressions, options)\n let page = (byDate ? query.orderBy('createdAt', 'desc') : query.orderBy('__name__')).limit(size)\n if (cursor) page = byDate ? page.startAfter(await suppressions.doc(cursor).get()) : page.startAfter(cursor)\n const snapshot = await page.get()\n docs = snapshot.docs\n next = snapshot.docs.length === size ? (snapshot.docs[snapshot.docs.length - 1]?.id ?? null) : null\n }\n const rows = docs.map((doc) => {\n const values = suppressionValues(doc.id, (doc.data() ?? {}) as Record<string, unknown>)\n return Object.fromEntries(fieldIds.map((fieldId) => [fieldId, values[fieldId] ?? null]))\n })\n return { rows, next }\n}\n\n/** How many entries an export reads. */\nexport async function countSuppressions(ctx: TransferResourceContext, options: TransferReadOptions): Promise<number> {\n if (options.ids) return countExisting(suppressionsOf(ctx), options.ids)\n const counted = await suppressionQuery(suppressionsOf(ctx), options).query.count().get()\n return Number(counted.data().count ?? 0)\n}\n\n/** `email.suppressions`, as the framework asks it. */\nexport const suppressionsTransferResource: TransferRecordsHooks = {\n fields: () => suppressionCatalog(),\n matchKeys: SUPPRESSION_MATCH_KEYS,\n aliases: SUPPRESSION_ALIASES,\n lockedRules: () => SUPPRESSION_LOCKED_RULES,\n count: countSuppressions,\n readPage: readSuppressionPage,\n lookup: lookupSuppressions,\n apply: applySuppressions,\n // Nothing to take back: the import writes no undo entry (see the module note).\n revert: async () => ({ done: [], conflicts: [] }),\n}\n"],"names":["TRANSFER_ID_FIELD","matchLookupKey","normalizeMatchValue","emailSearchTokens","emailSuppressionKey","firebaseAdmin","TransferEngineError","FieldValue","MANUAL_SUPPRESSION_REASON","SUPPRESSION_NOTE_MAX","countExisting","SUPPRESSION_ALIASES","SUPPRESSION_LOCKED_RULES","SUPPRESSION_MATCH_KEYS","suppressionCatalog","APPLY_RESERVE_MS","suppressionsOf","ctx","hostId","app","firestore","collection","doc","isoOf","value","stamp","toDate","toISOString","suppressionValues","id","data","text","email","note","reason","suppressedAt","carriedFromHostId","lookupSuppressions","requests","suppressions","lookup","Map","records","request","ids","fieldId","values","map","wanted","filter","entry","includes","at","length","chunk","slice","snapshots","getAll","forEach","snapshot","index","exists","set","applySuppressions","writer","results","row","rows","done","alreadyApplied","push","timeLeftMs","result","addSuppression","markApplied","undo","diff","find","change","after","key","verdict","outcome","recordId","String","trim","create","emailTokens","suppressedByUid","actorUid","jobId","importJobId","createdAt","serverTimestamp","error","code","test","message","readSuppressionFilter","clauses","Array","isArray","clause","path","op","strings","list","every","item","Date","Number","isNaN","getTime","suppressionQuery","options","query","where","byDate","some","readSuppressionPage","cursor","fieldIds","size","Math","min","max","pageSize","docs","next","start","page","orderBy","limit","startAfter","get","Object","fromEntries","countSuppressions","counted","count","suppressionsTransferResource","fields","matchKeys","aliases","lockedRules","readPage","apply","revert","conflicts"],"mappings":"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"}
|