@aglyn/plugins-outreach 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +93 -0
- package/package.json +52 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/compliance-section.d.ts +20 -0
- package/src/lib/components/compliance-section.js +291 -0
- package/src/lib/components/compliance-section.js.map +1 -0
- package/src/lib/components/enroll-dialog.d.ts +36 -0
- package/src/lib/components/enroll-dialog.js +656 -0
- package/src/lib/components/enroll-dialog.js.map +1 -0
- package/src/lib/components/enrollments-table.d.ts +28 -0
- package/src/lib/components/enrollments-table.js +375 -0
- package/src/lib/components/enrollments-table.js.map +1 -0
- package/src/lib/components/mailbox-card.d.ts +46 -0
- package/src/lib/components/mailbox-card.js +517 -0
- package/src/lib/components/mailbox-card.js.map +1 -0
- package/src/lib/components/mailboxes-section.d.ts +24 -0
- package/src/lib/components/mailboxes-section.js +258 -0
- package/src/lib/components/mailboxes-section.js.map +1 -0
- package/src/lib/components/outreach-console-page.d.ts +17 -0
- package/src/lib/components/outreach-console-page.js +81 -0
- package/src/lib/components/outreach-console-page.js.map +1 -0
- package/src/lib/components/outreach-console-sections.d.ts +43 -0
- package/src/lib/components/outreach-console-sections.js +55 -0
- package/src/lib/components/outreach-console-sections.js.map +1 -0
- package/src/lib/components/outreach-ui.d.ts +51 -0
- package/src/lib/components/outreach-ui.js +171 -0
- package/src/lib/components/outreach-ui.js.map +1 -0
- package/src/lib/components/send-window-fields.d.ts +25 -0
- package/src/lib/components/send-window-fields.js +178 -0
- package/src/lib/components/send-window-fields.js.map +1 -0
- package/src/lib/components/sequence-detail.d.ts +54 -0
- package/src/lib/components/sequence-detail.js +435 -0
- package/src/lib/components/sequence-detail.js.map +1 -0
- package/src/lib/components/sequence-editor.d.ts +37 -0
- package/src/lib/components/sequence-editor.js +485 -0
- package/src/lib/components/sequence-editor.js.map +1 -0
- package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
- package/src/lib/components/sequence-mailbox-picker.js +137 -0
- package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
- package/src/lib/components/sequence-preview.d.ts +27 -0
- package/src/lib/components/sequence-preview.js +146 -0
- package/src/lib/components/sequence-preview.js.map +1 -0
- package/src/lib/components/sequence-step-card.d.ts +31 -0
- package/src/lib/components/sequence-step-card.js +326 -0
- package/src/lib/components/sequence-step-card.js.map +1 -0
- package/src/lib/components/sequences-section.d.ts +40 -0
- package/src/lib/components/sequences-section.js +310 -0
- package/src/lib/components/sequences-section.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +38 -0
- package/src/lib/components/use-outreach-api.js +139 -0
- package/src/lib/components/use-outreach-api.js.map +1 -0
- package/src/lib/components/use-outreach-crm.d.ts +50 -0
- package/src/lib/components/use-outreach-crm.js +200 -0
- package/src/lib/components/use-outreach-crm.js.map +1 -0
- package/src/lib/components/use-outreach-data.d.ts +55 -0
- package/src/lib/components/use-outreach-data.js +199 -0
- package/src/lib/components/use-outreach-data.js.map +1 -0
- package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
- package/src/lib/components/use-outreach-mailbox-api.js +101 -0
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
- package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
- package/src/lib/components/use-outreach-mailboxes.js +68 -0
- package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
- package/src/lib/components/use-outreach-settings.d.ts +17 -0
- package/src/lib/components/use-outreach-settings.js +64 -0
- package/src/lib/components/use-outreach-settings.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +68 -0
- package/src/lib/constants/api-routes.js +57 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +46 -0
- package/src/lib/constants/bundle-common.js +55 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/runtime-jobs.d.ts +24 -0
- package/src/lib/constants/runtime-jobs.js +24 -0
- package/src/lib/constants/runtime-jobs.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +99 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/engine/compose.d.ts +86 -0
- package/src/lib/engine/compose.js +212 -0
- package/src/lib/engine/compose.js.map +1 -0
- package/src/lib/engine/delivery-status.d.ts +53 -0
- package/src/lib/engine/delivery-status.js +184 -0
- package/src/lib/engine/delivery-status.js.map +1 -0
- package/src/lib/engine/do-not-contact.d.ts +18 -0
- package/src/lib/engine/do-not-contact.js +34 -0
- package/src/lib/engine/do-not-contact.js.map +1 -0
- package/src/lib/engine/enrollment-state.d.ts +156 -0
- package/src/lib/engine/enrollment-state.js +303 -0
- package/src/lib/engine/enrollment-state.js.map +1 -0
- package/src/lib/engine/gates.d.ts +125 -0
- package/src/lib/engine/gates.js +232 -0
- package/src/lib/engine/gates.js.map +1 -0
- package/src/lib/engine/index.d.ts +28 -0
- package/src/lib/engine/index.js +33 -0
- package/src/lib/engine/index.js.map +1 -0
- package/src/lib/engine/mailbox-health.d.ts +49 -0
- package/src/lib/engine/mailbox-health.js +83 -0
- package/src/lib/engine/mailbox-health.js.map +1 -0
- package/src/lib/engine/opt-out-intent.d.ts +41 -0
- package/src/lib/engine/opt-out-intent.js +220 -0
- package/src/lib/engine/opt-out-intent.js.map +1 -0
- package/src/lib/engine/recipient-country.d.ts +41 -0
- package/src/lib/engine/recipient-country.js +171 -0
- package/src/lib/engine/recipient-country.js.map +1 -0
- package/src/lib/engine/schedule.d.ts +70 -0
- package/src/lib/engine/schedule.js +115 -0
- package/src/lib/engine/schedule.js.map +1 -0
- package/src/lib/engine/sending-capacity.d.ts +96 -0
- package/src/lib/engine/sending-capacity.js +172 -0
- package/src/lib/engine/sending-capacity.js.map +1 -0
- package/src/lib/engine/sequence-validation.d.ts +90 -0
- package/src/lib/engine/sequence-validation.js +291 -0
- package/src/lib/engine/sequence-validation.js.map +1 -0
- package/src/lib/engine/thread-classification.d.ts +78 -0
- package/src/lib/engine/thread-classification.js +224 -0
- package/src/lib/engine/thread-classification.js.map +1 -0
- package/src/lib/engine/thread-message.d.ts +80 -0
- package/src/lib/engine/thread-message.js +143 -0
- package/src/lib/engine/thread-message.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +81 -0
- package/src/lib/enrollment/enroll-people.js +250 -0
- package/src/lib/enrollment/enroll-people.js.map +1 -0
- package/src/lib/enrollment/gate-lookups.d.ts +32 -0
- package/src/lib/enrollment/gate-lookups.js +154 -0
- package/src/lib/enrollment/gate-lookups.js.map +1 -0
- package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
- package/src/lib/mailboxes/mailbox-api.js +66 -0
- package/src/lib/mailboxes/mailbox-api.js.map +1 -0
- package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
- package/src/lib/mailboxes/mailbox-credentials.js +102 -0
- package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
- package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
- package/src/lib/mailboxes/mailbox-erasure.js +148 -0
- package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
- package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
- package/src/lib/mailboxes/mailbox-gate.js +98 -0
- package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
- package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
- package/src/lib/mailboxes/mailbox-revoke.js +41 -0
- package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
- package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
- package/src/lib/mailboxes/mailbox-routes.js +605 -0
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
- package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
- package/src/lib/mailboxes/mailbox-settings.js +212 -0
- package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
- package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
- package/src/lib/mailboxes/mailbox-transport.js +94 -0
- package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
- package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
- package/src/lib/mailboxes/oauth-redirect.js +63 -0
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
- package/src/lib/mailboxes/oauth-state.d.ts +152 -0
- package/src/lib/mailboxes/oauth-state.js +212 -0
- package/src/lib/mailboxes/oauth-state.js.map +1 -0
- package/src/lib/mailboxes/outreach-config.d.ts +64 -0
- package/src/lib/mailboxes/outreach-config.js +77 -0
- package/src/lib/mailboxes/outreach-config.js.map +1 -0
- package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
- package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
- package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
- package/src/lib/model/compliance-settings.d.ts +78 -0
- package/src/lib/model/compliance-settings.js +155 -0
- package/src/lib/model/compliance-settings.js.map +1 -0
- package/src/lib/model/outreach-api.d.ts +210 -0
- package/src/lib/model/outreach-api.js +28 -0
- package/src/lib/model/outreach-api.js.map +1 -0
- package/src/lib/model/outreach.types.d.ts +563 -0
- package/src/lib/model/outreach.types.js +225 -0
- package/src/lib/model/outreach.types.js.map +1 -0
- package/src/lib/model/sequence-draft.d.ts +84 -0
- package/src/lib/model/sequence-draft.js +239 -0
- package/src/lib/model/sequence-draft.js.map +1 -0
- package/src/lib/model/step-preview.d.ts +45 -0
- package/src/lib/model/step-preview.js +98 -0
- package/src/lib/model/step-preview.js.map +1 -0
- package/src/lib/model/stored-records.d.ts +23 -0
- package/src/lib/model/stored-records.js +84 -0
- package/src/lib/model/stored-records.js.map +1 -0
- package/src/lib/plugin.d.ts +46 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/routes/enroll-routes.d.ts +64 -0
- package/src/lib/routes/enroll-routes.js +350 -0
- package/src/lib/routes/enroll-routes.js.map +1 -0
- package/src/lib/routes/enrollment-routes.d.ts +19 -0
- package/src/lib/routes/enrollment-routes.js +171 -0
- package/src/lib/routes/enrollment-routes.js.map +1 -0
- package/src/lib/routes/platform-deps.d.ts +51 -0
- package/src/lib/routes/platform-deps.js +66 -0
- package/src/lib/routes/platform-deps.js.map +1 -0
- package/src/lib/routes/preview-routes.d.ts +33 -0
- package/src/lib/routes/preview-routes.js +117 -0
- package/src/lib/routes/preview-routes.js.map +1 -0
- package/src/lib/routes/register-routes.d.ts +22 -0
- package/src/lib/routes/register-routes.js +92 -0
- package/src/lib/routes/register-routes.js.map +1 -0
- package/src/lib/routes/route-deps.d.ts +49 -0
- package/src/lib/routes/route-deps.js +27 -0
- package/src/lib/routes/route-deps.js.map +1 -0
- package/src/lib/routes/route-gate.d.ts +94 -0
- package/src/lib/routes/route-gate.js +87 -0
- package/src/lib/routes/route-gate.js.map +1 -0
- package/src/lib/routes/route-http.d.ts +32 -0
- package/src/lib/routes/route-http.js +62 -0
- package/src/lib/routes/route-http.js.map +1 -0
- package/src/lib/routes/sequence-routes.d.ts +32 -0
- package/src/lib/routes/sequence-routes.js +359 -0
- package/src/lib/routes/sequence-routes.js.map +1 -0
- package/src/lib/routes/settings-routes.d.ts +30 -0
- package/src/lib/routes/settings-routes.js +73 -0
- package/src/lib/routes/settings-routes.js.map +1 -0
- package/src/lib/runtime/enrollment-events.d.ts +86 -0
- package/src/lib/runtime/enrollment-events.js +141 -0
- package/src/lib/runtime/enrollment-events.js.map +1 -0
- package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
- package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
- package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
- package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
- package/src/lib/runtime/mailbox-health-store.js +168 -0
- package/src/lib/runtime/mailbox-health-store.js.map +1 -0
- package/src/lib/runtime/person-erasure.d.ts +39 -0
- package/src/lib/runtime/person-erasure.js +57 -0
- package/src/lib/runtime/person-erasure.js.map +1 -0
- package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
- package/src/lib/runtime/platform-runtime-deps.js +98 -0
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
- package/src/lib/runtime/runtime-deps.d.ts +77 -0
- package/src/lib/runtime/runtime-deps.js +18 -0
- package/src/lib/runtime/runtime-deps.js.map +1 -0
- package/src/lib/runtime/send-job.d.ts +42 -0
- package/src/lib/runtime/send-job.js +780 -0
- package/src/lib/runtime/send-job.js.map +1 -0
- package/src/lib/runtime/sync-job.d.ts +41 -0
- package/src/lib/runtime/sync-job.js +480 -0
- package/src/lib/runtime/sync-job.js.map +1 -0
- package/src/lib/runtime/timeline.d.ts +67 -0
- package/src/lib/runtime/timeline.js +93 -0
- package/src/lib/runtime/timeline.js.map +1 -0
- package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
- package/src/lib/runtime/unsubscribe-link.js +121 -0
- package/src/lib/runtime/unsubscribe-link.js.map +1 -0
- package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
- package/src/lib/runtime/unsubscribe-route.js +103 -0
- package/src/lib/runtime/unsubscribe-route.js.map +1 -0
- package/src/lib/server.d.ts +53 -0
- package/src/lib/server.js +94 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/storage/compliance-settings-store.d.ts +44 -0
- package/src/lib/storage/compliance-settings-store.js +78 -0
- package/src/lib/storage/compliance-settings-store.js.map +1 -0
- package/src/lib/storage/do-not-contact-store.d.ts +60 -0
- package/src/lib/storage/do-not-contact-store.js +168 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -0
- package/src/lib/storage/outreach-records.d.ts +33 -0
- package/src/lib/storage/outreach-records.js +35 -0
- package/src/lib/storage/outreach-records.js.map +1 -0
- package/src/lib/subprocessors.d.ts +48 -0
- package/src/lib/subprocessors.js +70 -0
- package/src/lib/subprocessors.js.map +1 -0
- package/src/lib/transport/gmail-client.d.ts +145 -0
- package/src/lib/transport/gmail-client.js +229 -0
- package/src/lib/transport/gmail-client.js.map +1 -0
- package/src/lib/transport/gmail-errors.d.ts +84 -0
- package/src/lib/transport/gmail-errors.js +138 -0
- package/src/lib/transport/gmail-errors.js.map +1 -0
- package/src/lib/transport/google-oauth.d.ts +138 -0
- package/src/lib/transport/google-oauth.js +247 -0
- package/src/lib/transport/google-oauth.js.map +1 -0
- package/src/lib/transport/http.d.ts +53 -0
- package/src/lib/transport/http.js +79 -0
- package/src/lib/transport/http.js.map +1 -0
- package/src/lib/transport/rfc5322.d.ts +116 -0
- package/src/lib/transport/rfc5322.js +330 -0
- package/src/lib/transport/rfc5322.js.map +1 -0
- package/src/lib/transport/send-message.d.ts +69 -0
- package/src/lib/transport/send-message.js +77 -0
- package/src/lib/transport/send-message.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/enroll-people.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 * WHO CAN BE ENROLLED, AND WHAT THE REP STILL OWES (AGL-2980).\n *\n * The enroll dialog shows every person a view or a search named, each\n * marked eligible, blocked, or waiting on the rep; Confirm asks the same\n * question again, on the server, with what the rep supplied. Both answers\n * come from the engine's `evaluateOutreachGates` — this module only reads\n * the people and asks it the right questions.\n *\n * ## Three questions of one pure function\n *\n * The gates refuse a cold contact until the rep writes a personal line and\n * ticks the attestations, and refuse an address in an unknown country until\n * the rep confirms it is a US business address. So a preview asks:\n *\n * 1. as the person stands, with nothing confirmed — allowed means\n * ELIGIBLE;\n * 2. as if the rep confirmed everything they can — refused means BLOCKED,\n * for the reasons that remain, which no tick can clear;\n * 3. otherwise the person is WAITING ON THE REP, for what (1) found\n * missing.\n *\n * The confirmation in (2) is hypothetical and never stored: Confirm stamps\n * only what the rep ticked, with their uid and the time, and asks again.\n *\n * ## One enrollment per person per sequence, ever\n *\n * The enrollment's id is the sequence and the contact\n * (`outreachEnrollmentId`), and it is written with `create()`. A person who\n * finished, replied or was stopped is not sent the same emails a second\n * time by being enrolled again, and two members confirming the same person\n * at once write one enrollment.\n *==========================================*/\n\nimport { normalizeContactEmail, contactDisplayName, readContactFacet } from '@aglyn/aglyn/app-utils/contacts'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n evaluateOutreachGates,\n type OutreachGateBlock,\n type OutreachGateLookups,\n} from '../engine/gates'\nimport { outreachAttestationsFrom } from '../engine/enrollment-state'\nimport {\n OUTREACH_ATTESTATION_KINDS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachSequenceSettings,\n} from '../model/outreach.types'\nimport type { OutreachEnrollBlock, OutreachEnrollPreviewPerson } from '../model/outreach-api'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** One person as the preview and the enroll read them. */\nexport interface OutreachEnrollCandidate {\n contactId: string\n /** The contact document, or `null` when it no longer exists. */\n contact: Record<string, unknown> | null\n /** Whether the sequence's site may see the contact. */\n visible: boolean\n /** The name the sequence's site knows them by; `''` for none. */\n name: string\n /** The primary address, normalized, or `null` when there is none. */\n email: string | null\n /** The company the contact is filed under at that site, when there is one. */\n company: Record<string, unknown> | null\n}\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * The contacts, as the sequence's site knows them, and the company each is\n * filed under there — two batched reads for everyone.\n */\nexport async function readOutreachEnrollCandidates(\n firestore: Firestore,\n input: { orgId: string; hostId: string; contactGroupId: string; contactIds: readonly string[] },\n): Promise<OutreachEnrollCandidate[]> {\n if (!input.contactIds.length) return []\n const org = firestore.collection('orgs').doc(input.orgId)\n const snapshots = await getAllChunked(\n firestore,\n input.contactIds.map((id) => org.collection('contacts').doc(id)),\n )\n const read = input.contactIds.map((contactId, index) => {\n const data = snapshots[index]?.exists ? (snapshots[index].data() as Record<string, unknown>) : null\n return {\n contactId,\n contact: data,\n visible: data ? visibleToHost(data['visibleTo'] as string[] | undefined, input.hostId) : false,\n name: data ? contactDisplayName(data, input.contactGroupId).trim() : '',\n email: data ? normalizeContactEmail(data['email']) : null,\n companyId: data ? String(readContactFacet(data, input.contactGroupId).companyId ?? '') : '',\n }\n })\n const companyIds = [...new Set(read.map((entry) => entry.companyId).filter(Boolean))]\n const companies = new Map<string, Record<string, unknown>>()\n if (companyIds.length) {\n const found = await getAllChunked(\n firestore,\n companyIds.map((id) => org.collection('companies').doc(id)),\n )\n found.forEach((snapshot, index) => {\n if (snapshot.exists) companies.set(companyIds[index], snapshot.data() as Record<string, unknown>)\n })\n }\n return read.map(({ companyId, ...candidate }) => ({\n ...candidate,\n company: companyId ? (companies.get(companyId) ?? null) : null,\n }))\n}\n\n/** What stands in the way before the gates are asked anything. */\nexport function outreachCandidateBlocks(\n candidate: OutreachEnrollCandidate,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollBlock[] {\n if (!candidate.contact) {\n return [{ code: 'contact_missing', reason: 'This contact no longer exists in the CRM.' }]\n }\n const blocks: OutreachEnrollBlock[] = []\n if (!candidate.visible) {\n blocks.push({\n code: 'not_in_site',\n reason: `This contact isn't in ${facts.siteName || \"this sequence's site\"}'s CRM.`,\n })\n }\n if (!candidate.email) {\n blocks.push({ code: 'no_email', reason: 'This contact has no email address.' })\n }\n if (facts.alreadyInSequence) {\n blocks.push({\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n })\n }\n return blocks\n}\n\n/** Every attestation, stamped for a hypothetical question only — never stored. */\nconst EVERYTHING_CONFIRMED: OutreachAttestations = Object.fromEntries(\n OUTREACH_ATTESTATION_KINDS.map((kind) => [kind, { uid: 'preview', atMs: 1 }]),\n)\n\nconst blocksOf = (blocks: readonly OutreachGateBlock[]): OutreachEnrollBlock[] =>\n blocks.map(({ code, reason }) => ({ code, reason }))\n\nexport interface OutreachGateQuestion {\n candidate: OutreachEnrollCandidate\n lookups: OutreachGateLookups\n /** The sequence's settings, its countries already capped by the organization's. */\n settings: OutreachSequenceSettings\n contactGroupId: string\n}\n\n/** Where one person stands before the rep confirms anything — see the module note. */\nexport function previewOutreachPerson(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n): OutreachEnrollPreviewPerson {\n const { candidate } = question\n const person = {\n contactId: candidate.contactId,\n name: candidate.name,\n email: candidate.email,\n }\n const early = outreachCandidateBlocks(candidate, facts)\n if (early.length) {\n return {\n ...person,\n cold: false,\n country: null,\n status: 'blocked',\n blocks: early,\n requires: { personalLine: false, attestations: [] },\n }\n }\n const ask = (personalLine: string, attestations: OutreachAttestations) =>\n evaluateOutreachGates({\n email: candidate.email ?? '',\n contact: candidate.contact,\n contactGroupId: question.contactGroupId,\n company: candidate.company,\n settings: question.settings,\n personalLine,\n attestations,\n lookups: question.lookups,\n })\n const asIs = ask('', {})\n const known = { cold: asIs.cold, country: asIs.country.country }\n if (asIs.allowed) {\n return {\n ...person,\n ...known,\n status: 'eligible',\n blocks: [],\n requires: { personalLine: false, attestations: [] },\n }\n }\n // The line is only checked for being present, so any sentence stands in.\n const confirmed = ask('A personal line.', EVERYTHING_CONFIRMED)\n if (!confirmed.allowed) {\n return {\n ...person,\n ...known,\n status: 'blocked',\n blocks: blocksOf(confirmed.blocks),\n requires: { personalLine: false, attestations: [] },\n }\n }\n const attestations: OutreachAttestationKind[] = asIs.cold\n ? [...asIs.missingAttestations]\n : asIs.blocks.some((block) => block.code === 'country_unknown')\n ? ['us_business_address']\n : []\n return {\n ...person,\n ...known,\n status: 'needs_confirmation',\n blocks: [],\n requires: {\n personalLine: asIs.blocks.some((block) => block.code === 'personal_line_missing'),\n attestations,\n },\n }\n}\n\nexport interface OutreachEnrollDecision {\n allowed: boolean\n blocks: OutreachEnrollBlock[]\n cold: boolean\n /** The address as the gates normalized it. */\n email: string | null\n /** What the rep confirmed, stamped with who and when. */\n attestations: OutreachAttestations\n}\n\n/** Confirm's answer for one person, with what the rep supplied — see the module note. */\nexport function decideOutreachEnrollment(\n question: OutreachGateQuestion,\n facts: { siteName: string; alreadyInSequence: boolean },\n supplied: {\n personalLine: string\n attestations: readonly unknown[]\n uid: string\n nowMs: number\n },\n): OutreachEnrollDecision {\n const attestations = outreachAttestationsFrom(supplied.attestations, supplied.uid, supplied.nowMs)\n const early = outreachCandidateBlocks(question.candidate, facts)\n if (early.length) {\n return { allowed: false, blocks: early, cold: false, email: question.candidate.email, attestations }\n }\n const result = evaluateOutreachGates({\n email: question.candidate.email ?? '',\n contact: question.candidate.contact,\n contactGroupId: question.contactGroupId,\n company: question.candidate.company,\n settings: question.settings,\n personalLine: supplied.personalLine,\n attestations,\n lookups: question.lookups,\n })\n return {\n allowed: result.allowed,\n blocks: blocksOf(result.blocks),\n cold: result.cold,\n email: result.email,\n attestations,\n }\n}\n"],"names":["normalizeContactEmail","contactDisplayName","readContactFacet","visibleToHost","evaluateOutreachGates","outreachAttestationsFrom","OUTREACH_ATTESTATION_KINDS","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","readOutreachEnrollCandidates","input","contactIds","org","collection","doc","orgId","map","id","read","contactId","index","data","exists","contact","visible","hostId","name","contactGroupId","trim","email","companyId","String","companyIds","Set","entry","filter","Boolean","companies","Map","found","forEach","snapshot","set","candidate","company","get","outreachCandidateBlocks","facts","code","reason","blocks","siteName","alreadyInSequence","EVERYTHING_CONFIRMED","Object","fromEntries","kind","uid","atMs","blocksOf","previewOutreachPerson","question","person","early","cold","country","status","requires","personalLine","attestations","ask","settings","lookups","asIs","known","allowed","confirmed","missingAttestations","some","block","decideOutreachEnrollment","supplied","nowMs","result"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4CAgC4C,GAE5C,SAASA,qBAAqB,EAAEC,kBAAkB,EAAEC,gBAAgB,QAAQ,kCAAiC;AAC7G,SAASC,aAAa,QAAQ,sCAAqC;AACnE,SACEC,qBAAqB,QAGhB,qBAAiB;AACxB,SAASC,wBAAwB,QAAQ,gCAA4B;AACrE,SACEC,0BAA0B,QAIrB,6BAAyB;AAoBhC,0DAA0D,GAC1D,MAAMC,gBAAgB;AAEtB,eAAeC,cACbC,SAAoB,EACpBC,IAAoD;IAEpD,MAAMC,YAAkD,EAAE;IAC1D,IAAK,IAAIC,QAAQ,GAAGA,QAAQF,KAAKG,MAAM,EAAED,SAASL,cAAe;QAC/DI,UAAUG,IAAI,IAAK,MAAML,UAAUM,MAAM,IAAIL,KAAKM,KAAK,CAACJ,OAAOA,QAAQL;IACzE;IACA,OAAOI;AACT;AAEA;;;CAGC,GACD,OAAO,eAAeM,6BACpBR,SAAoB,EACpBS,KAA+F;IAE/F,IAAI,CAACA,MAAMC,UAAU,CAACN,MAAM,EAAE,OAAO,EAAE;IACvC,MAAMO,MAAMX,UAAUY,UAAU,CAAC,QAAQC,GAAG,CAACJ,MAAMK,KAAK;IACxD,MAAMZ,YAAY,MAAMH,cACtBC,WACAS,MAAMC,UAAU,CAACK,GAAG,CAAC,CAACC,KAAOL,IAAIC,UAAU,CAAC,YAAYC,GAAG,CAACG;IAE9D,MAAMC,OAAOR,MAAMC,UAAU,CAACK,GAAG,CAAC,CAACG,WAAWC;YAQjB1B;YAPdS;QAAb,MAAMkB,OAAOlB,EAAAA,mBAAAA,SAAS,CAACiB,MAAM,qBAAhBjB,iBAAkBmB,MAAM,IAAInB,SAAS,CAACiB,MAAM,CAACC,IAAI,KAAiC;QAC/F,OAAO;YACLF;YACAI,SAASF;YACTG,SAASH,OAAO1B,cAAc0B,IAAI,CAAC,YAAY,EAA0BX,MAAMe,MAAM,IAAI;YACzFC,MAAML,OAAO5B,mBAAmB4B,MAAMX,MAAMiB,cAAc,EAAEC,IAAI,KAAK;YACrEC,OAAOR,OAAO7B,sBAAsB6B,IAAI,CAAC,QAAQ,IAAI;YACrDS,WAAWT,OAAOU,QAAOrC,8BAAAA,iBAAiB2B,MAAMX,MAAMiB,cAAc,EAAEG,SAAS,YAAtDpC,8BAA0D,MAAM;QAC3F;IACF;IACA,MAAMsC,aAAa;WAAI,IAAIC,IAAIf,KAAKF,GAAG,CAAC,CAACkB,QAAUA,MAAMJ,SAAS,EAAEK,MAAM,CAACC;KAAU;IACrF,MAAMC,YAAY,IAAIC;IACtB,IAAIN,WAAW3B,MAAM,EAAE;QACrB,MAAMkC,QAAQ,MAAMvC,cAClBC,WACA+B,WAAWhB,GAAG,CAAC,CAACC,KAAOL,IAAIC,UAAU,CAAC,aAAaC,GAAG,CAACG;QAEzDsB,MAAMC,OAAO,CAAC,CAACC,UAAUrB;YACvB,IAAIqB,SAASnB,MAAM,EAAEe,UAAUK,GAAG,CAACV,UAAU,CAACZ,MAAM,EAAEqB,SAASpB,IAAI;QACrE;IACF;IACA,OAAOH,KAAKF,GAAG,CAAC;YAAC,EAAEc,SAAS,EAAgB,OAAXa;;;YAETN;eAF0B,aAC7CM;YACHC,SAASd,aAAaO,iBAAAA,UAAUQ,GAAG,CAACf,sBAAdO,iBAA4B,OAAQ;;;AAE9D;AAEA,gEAAgE,GAChE,OAAO,SAASS,wBACdH,SAAkC,EAClCI,KAAuD;IAEvD,IAAI,CAACJ,UAAUpB,OAAO,EAAE;QACtB,OAAO;YAAC;gBAAEyB,MAAM;gBAAmBC,QAAQ;YAA4C;SAAE;IAC3F;IACA,MAAMC,SAAgC,EAAE;IACxC,IAAI,CAACP,UAAUnB,OAAO,EAAE;QACtB0B,OAAO5C,IAAI,CAAC;YACV0C,MAAM;YACNC,QAAQ,CAAC,sBAAsB,EAAEF,MAAMI,QAAQ,IAAI,uBAAuB,OAAO,CAAC;QACpF;IACF;IACA,IAAI,CAACR,UAAUd,KAAK,EAAE;QACpBqB,OAAO5C,IAAI,CAAC;YAAE0C,MAAM;YAAYC,QAAQ;QAAqC;IAC/E;IACA,IAAIF,MAAMK,iBAAiB,EAAE;QAC3BF,OAAO5C,IAAI,CAAC;YACV0C,MAAM;YACNC,QAAQ;QACV;IACF;IACA,OAAOC;AACT;AAEA,gFAAgF,GAChF,MAAMG,uBAA6CC,OAAOC,WAAW,CACnEzD,2BAA2BkB,GAAG,CAAC,CAACwC,OAAS;QAACA;QAAM;YAAEC,KAAK;YAAWC,MAAM;QAAE;KAAE;AAG9E,MAAMC,WAAW,CAACT,SAChBA,OAAOlC,GAAG,CAAC,CAAC,EAAEgC,IAAI,EAAEC,MAAM,EAAE,GAAM,CAAA;YAAED;YAAMC;QAAO,CAAA;AAUnD,oFAAoF,GACpF,OAAO,SAASW,sBACdC,QAA8B,EAC9Bd,KAAuD;IAEvD,MAAM,EAAEJ,SAAS,EAAE,GAAGkB;IACtB,MAAMC,SAAS;QACb3C,WAAWwB,UAAUxB,SAAS;QAC9BO,MAAMiB,UAAUjB,IAAI;QACpBG,OAAOc,UAAUd,KAAK;IACxB;IACA,MAAMkC,QAAQjB,wBAAwBH,WAAWI;IACjD,IAAIgB,MAAM1D,MAAM,EAAE;QAChB,OAAO,aACFyD;YACHE,MAAM;YACNC,SAAS;YACTC,QAAQ;YACRhB,QAAQa;YACRI,UAAU;gBAAEC,cAAc;gBAAOC,cAAc,EAAE;YAAC;;IAEtD;IACA,MAAMC,MAAM,CAACF,cAAsBC;YAExB1B;eADT/C,sBAAsB;YACpBiC,KAAK,GAAEc,mBAAAA,UAAUd,KAAK,YAAfc,mBAAmB;YAC1BpB,SAASoB,UAAUpB,OAAO;YAC1BI,gBAAgBkC,SAASlC,cAAc;YACvCiB,SAASD,UAAUC,OAAO;YAC1B2B,UAAUV,SAASU,QAAQ;YAC3BH;YACAC;YACAG,SAASX,SAASW,OAAO;QAC3B;;IACF,MAAMC,OAAOH,IAAI,IAAI,CAAC;IACtB,MAAMI,QAAQ;QAAEV,MAAMS,KAAKT,IAAI;QAAEC,SAASQ,KAAKR,OAAO,CAACA,OAAO;IAAC;IAC/D,IAAIQ,KAAKE,OAAO,EAAE;QAChB,OAAO,aACFb,QACAY;YACHR,QAAQ;YACRhB,QAAQ,EAAE;YACViB,UAAU;gBAAEC,cAAc;gBAAOC,cAAc,EAAE;YAAC;;IAEtD;IACA,yEAAyE;IACzE,MAAMO,YAAYN,IAAI,oBAAoBjB;IAC1C,IAAI,CAACuB,UAAUD,OAAO,EAAE;QACtB,OAAO,aACFb,QACAY;YACHR,QAAQ;YACRhB,QAAQS,SAASiB,UAAU1B,MAAM;YACjCiB,UAAU;gBAAEC,cAAc;gBAAOC,cAAc,EAAE;YAAC;;IAEtD;IACA,MAAMA,eAA0CI,KAAKT,IAAI,GACrD;WAAIS,KAAKI,mBAAmB;KAAC,GAC7BJ,KAAKvB,MAAM,CAAC4B,IAAI,CAAC,CAACC,QAAUA,MAAM/B,IAAI,KAAK,qBACzC;QAAC;KAAsB,GACvB,EAAE;IACR,OAAO,aACFc,QACAY;QACHR,QAAQ;QACRhB,QAAQ,EAAE;QACViB,UAAU;YACRC,cAAcK,KAAKvB,MAAM,CAAC4B,IAAI,CAAC,CAACC,QAAUA,MAAM/B,IAAI,KAAK;YACzDqB;QACF;;AAEJ;AAYA,uFAAuF,GACvF,OAAO,SAASW,yBACdnB,QAA8B,EAC9Bd,KAAuD,EACvDkC,QAKC;QAQQpB;IANT,MAAMQ,eAAexE,yBAAyBoF,SAASZ,YAAY,EAAEY,SAASxB,GAAG,EAAEwB,SAASC,KAAK;IACjG,MAAMnB,QAAQjB,wBAAwBe,SAASlB,SAAS,EAAEI;IAC1D,IAAIgB,MAAM1D,MAAM,EAAE;QAChB,OAAO;YAAEsE,SAAS;YAAOzB,QAAQa;YAAOC,MAAM;YAAOnC,OAAOgC,SAASlB,SAAS,CAACd,KAAK;YAAEwC;QAAa;IACrG;IACA,MAAMc,SAASvF,sBAAsB;QACnCiC,KAAK,GAAEgC,4BAAAA,SAASlB,SAAS,CAACd,KAAK,YAAxBgC,4BAA4B;QACnCtC,SAASsC,SAASlB,SAAS,CAACpB,OAAO;QACnCI,gBAAgBkC,SAASlC,cAAc;QACvCiB,SAASiB,SAASlB,SAAS,CAACC,OAAO;QACnC2B,UAAUV,SAASU,QAAQ;QAC3BH,cAAca,SAASb,YAAY;QACnCC;QACAG,SAASX,SAASW,OAAO;IAC3B;IACA,OAAO;QACLG,SAASQ,OAAOR,OAAO;QACvBzB,QAAQS,SAASwB,OAAOjC,MAAM;QAC9Bc,MAAMmB,OAAOnB,IAAI;QACjBnC,OAAOsD,OAAOtD,KAAK;QACnBwC;IACF;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import type { OutreachGateLookups } from '../engine/gates';
|
|
18
|
+
/** One person to look up: the contact, and the address the steps go to. */
|
|
19
|
+
export interface OutreachGateLookupPerson {
|
|
20
|
+
contactId: string;
|
|
21
|
+
email: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ReadOutreachGateLookupsInput {
|
|
24
|
+
orgId: string;
|
|
25
|
+
/** The sequence's site: whose suppression list and topic opt-outs apply. */
|
|
26
|
+
hostId: string;
|
|
27
|
+
people: readonly OutreachGateLookupPerson[];
|
|
28
|
+
}
|
|
29
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
30
|
+
/** Every lookup the gates take, for each person, keyed by contact id. */
|
|
31
|
+
export declare function readOutreachGateLookups(firestore: Firestore, input: ReadOutreachGateLookupsInput): Promise<Map<string, OutreachGateLookups>>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* THE LOOKUPS THE ENROLLMENT GATES DECIDE ON (AGL-2980).
|
|
18
|
+
*
|
|
19
|
+
* `evaluateOutreachGates` is pure: it reads a contact as stored and the
|
|
20
|
+
* RESULTS of the reads the runtime makes. This is those reads, for many
|
|
21
|
+
* people at once — the enroll preview asks about everyone a view names in
|
|
22
|
+
* one pass, and the sending runtime asks about one person before each send
|
|
23
|
+
* with the same function, so the two can never consult different lists.
|
|
24
|
+
*
|
|
25
|
+
* Server only. Every list keyed by address is read with one `getAll` over
|
|
26
|
+
* `personKey` ids — the suppression lists, the sales topic opt-outs, the
|
|
27
|
+
* do-not-contact list — so a lookup costs one round trip however many
|
|
28
|
+
* people it covers, and needs no index that could go missing. The roster
|
|
29
|
+
* is read once. The two per-person facts that are queries — the person's
|
|
30
|
+
* other enrollments and whether they ever wrote in — are equality filters
|
|
31
|
+
* on single fields.
|
|
32
|
+
*
|
|
33
|
+
* ## A read that failed is `null`, and `null` refuses
|
|
34
|
+
*
|
|
35
|
+
* Each lookup is caught on its own and answers `null` for everyone it
|
|
36
|
+
* covered, which the gates read as "we couldn't check" and refuse: a
|
|
37
|
+
* person who asked not to be emailed must not be emailed because a read
|
|
38
|
+
* timed out. One failed list does not blank the others.
|
|
39
|
+
*/ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
40
|
+
import { EMAIL_TOPIC_SALES, readTopicSubscriptionState, TOPIC_OPT_OUTS_SUBCOLLECTION } from "@aglyn/aglyn/app-utils/email-topics";
|
|
41
|
+
import { personKey } from "@aglyn/aglyn/app-utils/person-key";
|
|
42
|
+
import { loadCrmInboundRoster } from "@aglyn/tenant-data-admin/server/crm-inbound-email";
|
|
43
|
+
import { EMAIL_SUPPRESSIONS_COLLECTION, HOST_SUPPRESSIONS_SUBCOLLECTION } from "@aglyn/tenant-data-admin/server/email-suppression";
|
|
44
|
+
import { lookupOutreachDoNotContact } from "../storage/do-not-contact-store.js";
|
|
45
|
+
import { outreachOrgCollection } from "../storage/outreach-records.js";
|
|
46
|
+
/** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
|
|
47
|
+
async function getAllChunked(firestore, refs) {
|
|
48
|
+
const snapshots = [];
|
|
49
|
+
for(let start = 0; start < refs.length; start += GET_ALL_CHUNK){
|
|
50
|
+
snapshots.push(...await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK)));
|
|
51
|
+
}
|
|
52
|
+
return snapshots;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One keyed list, read for every person at once: `answer` turns each
|
|
56
|
+
* person's document into their answer. An address that cannot be keyed has
|
|
57
|
+
* no document anywhere and is answered as unchecked, and so is everyone
|
|
58
|
+
* when the read fails.
|
|
59
|
+
*/ async function keyedLookup(firestore, people, collection, answer, label) {
|
|
60
|
+
const answers = new Map();
|
|
61
|
+
const keyed = [];
|
|
62
|
+
for (const person of people){
|
|
63
|
+
const key = personKey(person.email);
|
|
64
|
+
if (key) keyed.push({
|
|
65
|
+
contactId: person.contactId,
|
|
66
|
+
key
|
|
67
|
+
});
|
|
68
|
+
else answers.set(person.contactId, null);
|
|
69
|
+
}
|
|
70
|
+
if (!keyed.length) return answers;
|
|
71
|
+
try {
|
|
72
|
+
const snapshots = await getAllChunked(firestore, keyed.map((entry)=>collection(entry.key)));
|
|
73
|
+
keyed.forEach((entry, index)=>answers.set(entry.contactId, answer(snapshots[index])));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`[outreach] ${label} lookup failed; reading as unchecked`, error);
|
|
76
|
+
for (const entry of keyed)answers.set(entry.contactId, null);
|
|
77
|
+
}
|
|
78
|
+
return answers;
|
|
79
|
+
}
|
|
80
|
+
/** Every lookup the gates take, for each person, keyed by contact id. */ export async function readOutreachGateLookups(firestore, input) {
|
|
81
|
+
const { orgId, hostId, people } = input;
|
|
82
|
+
const host = firestore.collection('hosts').doc(hostId);
|
|
83
|
+
const enrollments = outreachOrgCollection(firestore, orgId, 'enrollments');
|
|
84
|
+
const activities = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities);
|
|
85
|
+
const [platform, site, sales, doNotContact, roster, perPerson] = await Promise.all([
|
|
86
|
+
keyedLookup(firestore, people, (key)=>firestore.collection(EMAIL_SUPPRESSIONS_COLLECTION).doc(key), // A released record no longer suppresses.
|
|
87
|
+
(snapshot)=>snapshot.exists && !snapshot.get('releasedAt'), 'platform suppression'),
|
|
88
|
+
keyedLookup(firestore, people, (key)=>host.collection(HOST_SUPPRESSIONS_SUBCOLLECTION).doc(key), (snapshot)=>snapshot.exists, 'site suppression'),
|
|
89
|
+
keyedLookup(firestore, people, (key)=>host.collection(TOPIC_OPT_OUTS_SUBCOLLECTION).doc(key), (snapshot)=>{
|
|
90
|
+
var _ref;
|
|
91
|
+
return readTopicSubscriptionState(((_ref = snapshot.exists ? snapshot.get('topics') : null) != null ? _ref : {})[EMAIL_TOPIC_SALES]);
|
|
92
|
+
}, 'sales topic'),
|
|
93
|
+
lookupOutreachDoNotContact(firestore, orgId, people.map((person)=>person.email)),
|
|
94
|
+
loadCrmInboundRoster(firestore, orgId).catch((error)=>{
|
|
95
|
+
console.error('[outreach] roster lookup failed; reading as unchecked', error);
|
|
96
|
+
return null;
|
|
97
|
+
}),
|
|
98
|
+
Promise.all(people.map(async (person)=>{
|
|
99
|
+
const open = await Promise.all([
|
|
100
|
+
enrollments.where('contactId', '==', person.contactId).get(),
|
|
101
|
+
enrollments.where('email', '==', person.email).get()
|
|
102
|
+
]).then(([byContact, byEmail])=>{
|
|
103
|
+
const found = new Map();
|
|
104
|
+
for (const doc of [
|
|
105
|
+
...byContact.docs,
|
|
106
|
+
...byEmail.docs
|
|
107
|
+
]){
|
|
108
|
+
var _doc_get;
|
|
109
|
+
found.set(doc.id, {
|
|
110
|
+
id: doc.id,
|
|
111
|
+
sequenceId: String((_doc_get = doc.get('sequenceId')) != null ? _doc_get : ''),
|
|
112
|
+
status: doc.get('status')
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
...found.values()
|
|
117
|
+
];
|
|
118
|
+
}).catch((error)=>{
|
|
119
|
+
console.error('[outreach] enrollment lookup failed; reading as unchecked', error);
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
const inbound = await activities.where('contactId', '==', person.contactId).where('direction', '==', 'inbound').limit(1).get().then((snapshot)=>!snapshot.empty).catch((error)=>{
|
|
123
|
+
console.error('[outreach] inbound email lookup failed; reading as cold', error);
|
|
124
|
+
return null;
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
contactId: person.contactId,
|
|
128
|
+
open,
|
|
129
|
+
inbound
|
|
130
|
+
};
|
|
131
|
+
}))
|
|
132
|
+
]);
|
|
133
|
+
const byPerson = new Map(perPerson.map((entry)=>[
|
|
134
|
+
entry.contactId,
|
|
135
|
+
entry
|
|
136
|
+
]));
|
|
137
|
+
const lookups = new Map();
|
|
138
|
+
for (const person of people){
|
|
139
|
+
var _platform_get, _site_get, _sales_get, _doNotContact_get, _ref, _ref1;
|
|
140
|
+
const own = byPerson.get(person.contactId);
|
|
141
|
+
lookups.set(person.contactId, {
|
|
142
|
+
platformSuppressed: (_platform_get = platform.get(person.contactId)) != null ? _platform_get : null,
|
|
143
|
+
hostSuppressed: (_site_get = site.get(person.contactId)) != null ? _site_get : null,
|
|
144
|
+
salesTopicState: (_sales_get = sales.get(person.contactId)) != null ? _sales_get : null,
|
|
145
|
+
doNotContact: (_doNotContact_get = doNotContact.get(person.email)) != null ? _doNotContact_get : null,
|
|
146
|
+
workspaceMembers: roster,
|
|
147
|
+
openEnrollments: (_ref = own == null ? void 0 : own.open) != null ? _ref : null,
|
|
148
|
+
hasInboundEmail: (_ref1 = own == null ? void 0 : own.inbound) != null ? _ref1 : null
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return lookups;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
//# sourceMappingURL=gate-lookups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/gate-lookups.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 LOOKUPS THE ENROLLMENT GATES DECIDE ON (AGL-2980).\n *\n * `evaluateOutreachGates` is pure: it reads a contact as stored and the\n * RESULTS of the reads the runtime makes. This is those reads, for many\n * people at once — the enroll preview asks about everyone a view names in\n * one pass, and the sending runtime asks about one person before each send\n * with the same function, so the two can never consult different lists.\n *\n * Server only. Every list keyed by address is read with one `getAll` over\n * `personKey` ids — the suppression lists, the sales topic opt-outs, the\n * do-not-contact list — so a lookup costs one round trip however many\n * people it covers, and needs no index that could go missing. The roster\n * is read once. The two per-person facts that are queries — the person's\n * other enrollments and whether they ever wrote in — are equality filters\n * on single fields.\n *\n * ## A read that failed is `null`, and `null` refuses\n *\n * Each lookup is caught on its own and answers `null` for everyone it\n * covered, which the gates read as \"we couldn't check\" and refuse: a\n * person who asked not to be emailed must not be emailed because a read\n * timed out. One failed list does not blank the others.\n */\n\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n EMAIL_TOPIC_SALES,\n readTopicSubscriptionState,\n TOPIC_OPT_OUTS_SUBCOLLECTION,\n type TopicSubscriptionState,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport type { MemberAddresses } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { loadCrmInboundRoster } from '@aglyn/tenant-data-admin/server/crm-inbound-email'\nimport {\n EMAIL_SUPPRESSIONS_COLLECTION,\n HOST_SUPPRESSIONS_SUBCOLLECTION,\n} from '@aglyn/tenant-data-admin/server/email-suppression'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport type { OutreachEnrollmentStatus } from '../model/outreach.types'\nimport { lookupOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\n/** One person to look up: the contact, and the address the steps go to. */\nexport interface OutreachGateLookupPerson {\n contactId: string\n email: string\n}\n\nexport interface ReadOutreachGateLookupsInput {\n orgId: string\n /** The sequence's site: whose suppression list and topic opt-outs apply. */\n hostId: string\n people: readonly OutreachGateLookupPerson[]\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * One keyed list, read for every person at once: `answer` turns each\n * person's document into their answer. An address that cannot be keyed has\n * no document anywhere and is answered as unchecked, and so is everyone\n * when the read fails.\n */\nasync function keyedLookup<T>(\n firestore: Firestore,\n people: readonly OutreachGateLookupPerson[],\n collection: (key: string) => FirebaseFirestore.DocumentReference,\n answer: (snapshot: FirebaseFirestore.DocumentSnapshot) => T,\n label: string,\n): Promise<Map<string, T | null>> {\n const answers = new Map<string, T | null>()\n const keyed: Array<{ contactId: string; key: string }> = []\n for (const person of people) {\n const key = personKey(person.email)\n if (key) keyed.push({ contactId: person.contactId, key })\n else answers.set(person.contactId, null)\n }\n if (!keyed.length) return answers\n try {\n const snapshots = await getAllChunked(\n firestore,\n keyed.map((entry) => collection(entry.key)),\n )\n keyed.forEach((entry, index) => answers.set(entry.contactId, answer(snapshots[index])))\n } catch (error) {\n console.error(`[outreach] ${label} lookup failed; reading as unchecked`, error)\n for (const entry of keyed) answers.set(entry.contactId, null)\n }\n return answers\n}\n\n/** Every lookup the gates take, for each person, keyed by contact id. */\nexport async function readOutreachGateLookups(\n firestore: Firestore,\n input: ReadOutreachGateLookupsInput,\n): Promise<Map<string, OutreachGateLookups>> {\n const { orgId, hostId, people } = input\n const host = firestore.collection('hosts').doc(hostId)\n const enrollments = outreachOrgCollection(firestore, orgId, 'enrollments')\n const activities = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities)\n\n const [platform, site, sales, doNotContact, roster, perPerson] = await Promise.all([\n keyedLookup(\n firestore,\n people,\n (key) => firestore.collection(EMAIL_SUPPRESSIONS_COLLECTION).doc(key),\n // A released record no longer suppresses.\n (snapshot) => snapshot.exists && !snapshot.get('releasedAt'),\n 'platform suppression',\n ),\n keyedLookup(\n firestore,\n people,\n (key) => host.collection(HOST_SUPPRESSIONS_SUBCOLLECTION).doc(key),\n (snapshot) => snapshot.exists,\n 'site suppression',\n ),\n keyedLookup<TopicSubscriptionState>(\n firestore,\n people,\n (key) => host.collection(TOPIC_OPT_OUTS_SUBCOLLECTION).doc(key),\n (snapshot) =>\n readTopicSubscriptionState(\n ((snapshot.exists ? snapshot.get('topics') : null) ?? {})[EMAIL_TOPIC_SALES],\n ),\n 'sales topic',\n ),\n lookupOutreachDoNotContact(\n firestore,\n orgId,\n people.map((person) => person.email),\n ),\n loadCrmInboundRoster(firestore, orgId).catch((error: unknown): MemberAddresses[] | null => {\n console.error('[outreach] roster lookup failed; reading as unchecked', error)\n return null\n }),\n Promise.all(\n people.map(async (person) => {\n const open = await Promise.all([\n enrollments.where('contactId', '==', person.contactId).get(),\n enrollments.where('email', '==', person.email).get(),\n ])\n .then(([byContact, byEmail]) => {\n const found = new Map<string, { id: string; sequenceId: string; status: OutreachEnrollmentStatus }>()\n for (const doc of [...byContact.docs, ...byEmail.docs]) {\n found.set(doc.id, {\n id: doc.id,\n sequenceId: String(doc.get('sequenceId') ?? ''),\n status: doc.get('status') as OutreachEnrollmentStatus,\n })\n }\n return [...found.values()]\n })\n .catch((error: unknown) => {\n console.error('[outreach] enrollment lookup failed; reading as unchecked', error)\n return null\n })\n const inbound = await activities\n .where('contactId', '==', person.contactId)\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n .then((snapshot) => !snapshot.empty)\n .catch((error: unknown) => {\n console.error('[outreach] inbound email lookup failed; reading as cold', error)\n return null\n })\n return { contactId: person.contactId, open, inbound }\n }),\n ),\n ])\n\n const byPerson = new Map(perPerson.map((entry) => [entry.contactId, entry]))\n const lookups = new Map<string, OutreachGateLookups>()\n for (const person of people) {\n const own = byPerson.get(person.contactId)\n lookups.set(person.contactId, {\n platformSuppressed: platform.get(person.contactId) ?? null,\n hostSuppressed: site.get(person.contactId) ?? null,\n salesTopicState: sales.get(person.contactId) ?? null,\n doNotContact: doNotContact.get(person.email) ?? null,\n workspaceMembers: roster,\n openEnrollments: own?.open ?? null,\n hasInboundEmail: own?.inbound ?? null,\n })\n }\n return lookups\n}\n"],"names":["CRM_COLLECTIONS","EMAIL_TOPIC_SALES","readTopicSubscriptionState","TOPIC_OPT_OUTS_SUBCOLLECTION","personKey","loadCrmInboundRoster","EMAIL_SUPPRESSIONS_COLLECTION","HOST_SUPPRESSIONS_SUBCOLLECTION","lookupOutreachDoNotContact","outreachOrgCollection","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","keyedLookup","people","collection","answer","label","answers","Map","keyed","person","key","email","contactId","set","map","entry","forEach","index","error","console","readOutreachGateLookups","input","orgId","hostId","host","doc","enrollments","activities","platform","site","sales","doNotContact","roster","perPerson","Promise","all","snapshot","exists","get","catch","open","where","then","byContact","byEmail","found","docs","id","sequenceId","String","status","values","inbound","limit","empty","byPerson","lookups","own","platformSuppressed","hostSuppressed","salesTopicState","workspaceMembers","openEnrollments","hasInboundEmail"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
import type { OutreachMailbox, OutreachSendWindow } from '../model/outreach.types';
|
|
18
|
+
/**
|
|
19
|
+
* THE MAILBOX ROUTES' CONTRACT (AGL-2978): what the Mailboxes panel sends,
|
|
20
|
+
* what the routes answer, and the fragment a connect comes back through.
|
|
21
|
+
* Client-safe — types, constants and two string functions.
|
|
22
|
+
*/
|
|
23
|
+
/** Every reason a mailbox route refuses with, for a caller to branch on. */
|
|
24
|
+
export type OutreachApiRefusalReason = 'unauthenticated' | 'email-unverified' | 'org-required' | 'not-a-member' | 'not-org-wide' | 'permission' | 'entitlement' | 'not-configured' | 'method-not-allowed' | 'invalid-request' | 'mailbox-not-found' | 'not-your-mailbox' | 'mailbox-limit' | 'state-invalid' | 'state-expired' | 'state-user-mismatch' | 'state-org-mismatch' | 'state-replayed' | 'state-superseded' | 'code-rejected' | 'refresh-token-missing' | 'scopes-missing' | 'identity-unverified' | 'account-mismatch' | 'reconnect-required' | 'invalid-settings' | 'rate-limited' | 'google-unavailable';
|
|
25
|
+
export interface OutreachApiRefusal {
|
|
26
|
+
error: string;
|
|
27
|
+
reason: OutreachApiRefusalReason;
|
|
28
|
+
}
|
|
29
|
+
/** `GET outreach/mailboxes/availability` */
|
|
30
|
+
export interface OutreachMailboxAvailability {
|
|
31
|
+
configured: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the viewer is an organization owner or admin, who may change,
|
|
34
|
+
* pause and disconnect any member's mailbox. The routes decide this on
|
|
35
|
+
* every call; the panel reads it to offer only what will be allowed.
|
|
36
|
+
*/
|
|
37
|
+
canManageAll: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** `POST outreach/mailboxes/connect` */
|
|
40
|
+
export interface OutreachConnectRequest {
|
|
41
|
+
orgId: string;
|
|
42
|
+
}
|
|
43
|
+
export interface OutreachConnectResponse {
|
|
44
|
+
/** Google's consent address; the browser goes there. */
|
|
45
|
+
url: string;
|
|
46
|
+
}
|
|
47
|
+
/** `POST outreach/mailboxes/connect/complete` */
|
|
48
|
+
export interface OutreachConnectCompleteRequest {
|
|
49
|
+
orgId: string;
|
|
50
|
+
code: string;
|
|
51
|
+
state: string;
|
|
52
|
+
/** The browser's IANA timezone, for a new mailbox's sending window. */
|
|
53
|
+
timezone?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface OutreachConnectCompleteResponse {
|
|
56
|
+
ok: true;
|
|
57
|
+
mailbox: OutreachMailbox;
|
|
58
|
+
/** False when an existing mailbox for the account was reconnected. */
|
|
59
|
+
created: boolean;
|
|
60
|
+
/** Pending addresses of the member's that Gmail's send-as list confirmed. */
|
|
61
|
+
confirmedAliases: string[];
|
|
62
|
+
}
|
|
63
|
+
/** `POST outreach/mailboxes/settings` — every field but the ids is optional. */
|
|
64
|
+
export interface OutreachMailboxSettingsRequest {
|
|
65
|
+
orgId: string;
|
|
66
|
+
mailboxId: string;
|
|
67
|
+
sendAs?: string;
|
|
68
|
+
displayName?: string;
|
|
69
|
+
dailyCap?: number;
|
|
70
|
+
window?: OutreachSendWindow;
|
|
71
|
+
timezone?: string;
|
|
72
|
+
}
|
|
73
|
+
/** `POST outreach/mailboxes/status` */
|
|
74
|
+
export interface OutreachMailboxStatusRequest {
|
|
75
|
+
orgId: string;
|
|
76
|
+
mailboxId: string;
|
|
77
|
+
paused: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface OutreachMailboxResponse {
|
|
80
|
+
ok: true;
|
|
81
|
+
mailbox: OutreachMailbox;
|
|
82
|
+
}
|
|
83
|
+
/** `POST outreach/mailboxes/test` */
|
|
84
|
+
export interface OutreachMailboxTestResponse {
|
|
85
|
+
ok: true;
|
|
86
|
+
/** The account's own address the test went to. */
|
|
87
|
+
sentTo: string;
|
|
88
|
+
gmailMessageId: string;
|
|
89
|
+
sentAtMs: number;
|
|
90
|
+
}
|
|
91
|
+
/** `POST outreach/mailboxes/disconnect` */
|
|
92
|
+
export interface OutreachMailboxDisconnectResponse {
|
|
93
|
+
ok: true;
|
|
94
|
+
/**
|
|
95
|
+
* What became of the grant at Google: `revoked`, `already-invalid`,
|
|
96
|
+
* `kept-for-other-mailbox` when another mailbox still uses the account,
|
|
97
|
+
* or `failed` when Google could not be told — the stored grant is deleted
|
|
98
|
+
* either way.
|
|
99
|
+
*/
|
|
100
|
+
revocation: 'revoked' | 'already-invalid' | 'kept-for-other-mailbox' | 'failed';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The fragment key a connect returns through (AGL-2978).
|
|
104
|
+
*
|
|
105
|
+
* Google redirects to the callback route with the authorization code in the
|
|
106
|
+
* query; the callback moves it into the FRAGMENT of the Mailboxes page, which
|
|
107
|
+
* no server receives — not in a request line, not in a `Referer` — and the
|
|
108
|
+
* page takes it out of the address bar before it finishes the connect with
|
|
109
|
+
* the member's own session. The cross-domain session handoff uses the
|
|
110
|
+
* fragment for the same reason.
|
|
111
|
+
*/
|
|
112
|
+
export declare const OUTREACH_CONNECT_FRAGMENT_KEY = "outreachConnect";
|
|
113
|
+
/** Why a connect came back without a code. */
|
|
114
|
+
export type OutreachConnectReturnError = 'access_denied' | 'expired' | 'google_error';
|
|
115
|
+
export type OutreachConnectReturn = {
|
|
116
|
+
kind: 'code';
|
|
117
|
+
code: string;
|
|
118
|
+
state: string;
|
|
119
|
+
} | {
|
|
120
|
+
kind: 'error';
|
|
121
|
+
reason: OutreachConnectReturnError;
|
|
122
|
+
};
|
|
123
|
+
/** The fragment for a connect's return, without the leading `#`. */
|
|
124
|
+
export declare function buildConnectReturnFragment(value: OutreachConnectReturn): string;
|
|
125
|
+
/** A connect's return read off a location hash, or `null` when it carries none. */
|
|
126
|
+
export declare function parseConnectReturnFragment(hash: string | null | undefined): OutreachConnectReturn | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The fragment key a connect returns through (AGL-2978).
|
|
18
|
+
*
|
|
19
|
+
* Google redirects to the callback route with the authorization code in the
|
|
20
|
+
* query; the callback moves it into the FRAGMENT of the Mailboxes page, which
|
|
21
|
+
* no server receives — not in a request line, not in a `Referer` — and the
|
|
22
|
+
* page takes it out of the address bar before it finishes the connect with
|
|
23
|
+
* the member's own session. The cross-domain session handoff uses the
|
|
24
|
+
* fragment for the same reason.
|
|
25
|
+
*/ export const OUTREACH_CONNECT_FRAGMENT_KEY = 'outreachConnect';
|
|
26
|
+
/** The fragment for a connect's return, without the leading `#`. */ export function buildConnectReturnFragment(value) {
|
|
27
|
+
const params = new URLSearchParams();
|
|
28
|
+
if (value.kind === 'code') {
|
|
29
|
+
params.set(OUTREACH_CONNECT_FRAGMENT_KEY, 'code');
|
|
30
|
+
params.set('code', value.code);
|
|
31
|
+
params.set('state', value.state);
|
|
32
|
+
} else {
|
|
33
|
+
params.set(OUTREACH_CONNECT_FRAGMENT_KEY, 'error');
|
|
34
|
+
params.set('reason', value.reason);
|
|
35
|
+
}
|
|
36
|
+
return params.toString();
|
|
37
|
+
}
|
|
38
|
+
const RETURN_ERRORS = [
|
|
39
|
+
'access_denied',
|
|
40
|
+
'expired',
|
|
41
|
+
'google_error'
|
|
42
|
+
];
|
|
43
|
+
/** A connect's return read off a location hash, or `null` when it carries none. */ export function parseConnectReturnFragment(hash) {
|
|
44
|
+
const params = new URLSearchParams(String(hash != null ? hash : '').replace(/^#/, ''));
|
|
45
|
+
const kind = params.get(OUTREACH_CONNECT_FRAGMENT_KEY);
|
|
46
|
+
if (kind === 'code') {
|
|
47
|
+
var _params_get, _params_get1;
|
|
48
|
+
const code = (_params_get = params.get('code')) != null ? _params_get : '';
|
|
49
|
+
const state = (_params_get1 = params.get('state')) != null ? _params_get1 : '';
|
|
50
|
+
return code && state ? {
|
|
51
|
+
kind: 'code',
|
|
52
|
+
code,
|
|
53
|
+
state
|
|
54
|
+
} : null;
|
|
55
|
+
}
|
|
56
|
+
if (kind === 'error') {
|
|
57
|
+
const reason = params.get('reason');
|
|
58
|
+
return {
|
|
59
|
+
kind: 'error',
|
|
60
|
+
reason: reason && RETURN_ERRORS.includes(reason) ? reason : 'google_error'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=mailbox-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-api.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\nimport type { OutreachMailbox, OutreachSendWindow } from '../model/outreach.types'\n\n/**\n * THE MAILBOX ROUTES' CONTRACT (AGL-2978): what the Mailboxes panel sends,\n * what the routes answer, and the fragment a connect comes back through.\n * Client-safe — types, constants and two string functions.\n */\n\n/** Every reason a mailbox route refuses with, for a caller to branch on. */\nexport type OutreachApiRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'not-configured'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'mailbox-not-found'\n | 'not-your-mailbox'\n | 'mailbox-limit'\n | 'state-invalid'\n | 'state-expired'\n | 'state-user-mismatch'\n | 'state-org-mismatch'\n | 'state-replayed'\n | 'state-superseded'\n | 'code-rejected'\n | 'refresh-token-missing'\n | 'scopes-missing'\n | 'identity-unverified'\n | 'account-mismatch'\n | 'reconnect-required'\n | 'invalid-settings'\n | 'rate-limited'\n | 'google-unavailable'\n\nexport interface OutreachApiRefusal {\n error: string\n reason: OutreachApiRefusalReason\n}\n\n/** `GET outreach/mailboxes/availability` */\nexport interface OutreachMailboxAvailability {\n configured: boolean\n /**\n * Whether the viewer is an organization owner or admin, who may change,\n * pause and disconnect any member's mailbox. The routes decide this on\n * every call; the panel reads it to offer only what will be allowed.\n */\n canManageAll: boolean\n}\n\n/** `POST outreach/mailboxes/connect` */\nexport interface OutreachConnectRequest {\n orgId: string\n}\nexport interface OutreachConnectResponse {\n /** Google's consent address; the browser goes there. */\n url: string\n}\n\n/** `POST outreach/mailboxes/connect/complete` */\nexport interface OutreachConnectCompleteRequest {\n orgId: string\n code: string\n state: string\n /** The browser's IANA timezone, for a new mailbox's sending window. */\n timezone?: string\n}\nexport interface OutreachConnectCompleteResponse {\n ok: true\n mailbox: OutreachMailbox\n /** False when an existing mailbox for the account was reconnected. */\n created: boolean\n /** Pending addresses of the member's that Gmail's send-as list confirmed. */\n confirmedAliases: string[]\n}\n\n/** `POST outreach/mailboxes/settings` — every field but the ids is optional. */\nexport interface OutreachMailboxSettingsRequest {\n orgId: string\n mailboxId: string\n sendAs?: string\n displayName?: string\n dailyCap?: number\n window?: OutreachSendWindow\n timezone?: string\n}\n\n/** `POST outreach/mailboxes/status` */\nexport interface OutreachMailboxStatusRequest {\n orgId: string\n mailboxId: string\n paused: boolean\n}\n\nexport interface OutreachMailboxResponse {\n ok: true\n mailbox: OutreachMailbox\n}\n\n/** `POST outreach/mailboxes/test` */\nexport interface OutreachMailboxTestResponse {\n ok: true\n /** The account's own address the test went to. */\n sentTo: string\n gmailMessageId: string\n sentAtMs: number\n}\n\n/** `POST outreach/mailboxes/disconnect` */\nexport interface OutreachMailboxDisconnectResponse {\n ok: true\n /**\n * What became of the grant at Google: `revoked`, `already-invalid`,\n * `kept-for-other-mailbox` when another mailbox still uses the account,\n * or `failed` when Google could not be told — the stored grant is deleted\n * either way.\n */\n revocation: 'revoked' | 'already-invalid' | 'kept-for-other-mailbox' | 'failed'\n}\n\n/**\n * The fragment key a connect returns through (AGL-2978).\n *\n * Google redirects to the callback route with the authorization code in the\n * query; the callback moves it into the FRAGMENT of the Mailboxes page, which\n * no server receives — not in a request line, not in a `Referer` — and the\n * page takes it out of the address bar before it finishes the connect with\n * the member's own session. The cross-domain session handoff uses the\n * fragment for the same reason.\n */\nexport const OUTREACH_CONNECT_FRAGMENT_KEY = 'outreachConnect'\n\n/** Why a connect came back without a code. */\nexport type OutreachConnectReturnError = 'access_denied' | 'expired' | 'google_error'\n\nexport type OutreachConnectReturn =\n | { kind: 'code'; code: string; state: string }\n | { kind: 'error'; reason: OutreachConnectReturnError }\n\n/** The fragment for a connect's return, without the leading `#`. */\nexport function buildConnectReturnFragment(value: OutreachConnectReturn): string {\n const params = new URLSearchParams()\n if (value.kind === 'code') {\n params.set(OUTREACH_CONNECT_FRAGMENT_KEY, 'code')\n params.set('code', value.code)\n params.set('state', value.state)\n } else {\n params.set(OUTREACH_CONNECT_FRAGMENT_KEY, 'error')\n params.set('reason', value.reason)\n }\n return params.toString()\n}\n\nconst RETURN_ERRORS: readonly OutreachConnectReturnError[] = ['access_denied', 'expired', 'google_error']\n\n/** A connect's return read off a location hash, or `null` when it carries none. */\nexport function parseConnectReturnFragment(hash: string | null | undefined): OutreachConnectReturn | null {\n const params = new URLSearchParams(String(hash ?? '').replace(/^#/, ''))\n const kind = params.get(OUTREACH_CONNECT_FRAGMENT_KEY)\n if (kind === 'code') {\n const code = params.get('code') ?? ''\n const state = params.get('state') ?? ''\n return code && state ? { kind: 'code', code, state } : null\n }\n if (kind === 'error') {\n const reason = params.get('reason') as OutreachConnectReturnError | null\n return { kind: 'error', reason: reason && RETURN_ERRORS.includes(reason) ? reason : 'google_error' }\n }\n return null\n}\n"],"names":["OUTREACH_CONNECT_FRAGMENT_KEY","buildConnectReturnFragment","value","params","URLSearchParams","kind","set","code","state","reason","toString","RETURN_ERRORS","parseConnectReturnFragment","hash","String","replace","get","includes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GA+HD;;;;;;;;;CASC,GACD,OAAO,MAAMA,gCAAgC,kBAAiB;AAS9D,kEAAkE,GAClE,OAAO,SAASC,2BAA2BC,KAA4B;IACrE,MAAMC,SAAS,IAAIC;IACnB,IAAIF,MAAMG,IAAI,KAAK,QAAQ;QACzBF,OAAOG,GAAG,CAACN,+BAA+B;QAC1CG,OAAOG,GAAG,CAAC,QAAQJ,MAAMK,IAAI;QAC7BJ,OAAOG,GAAG,CAAC,SAASJ,MAAMM,KAAK;IACjC,OAAO;QACLL,OAAOG,GAAG,CAACN,+BAA+B;QAC1CG,OAAOG,GAAG,CAAC,UAAUJ,MAAMO,MAAM;IACnC;IACA,OAAON,OAAOO,QAAQ;AACxB;AAEA,MAAMC,gBAAuD;IAAC;IAAiB;IAAW;CAAe;AAEzG,iFAAiF,GACjF,OAAO,SAASC,2BAA2BC,IAA+B;IACxE,MAAMV,SAAS,IAAIC,gBAAgBU,OAAOD,eAAAA,OAAQ,IAAIE,OAAO,CAAC,MAAM;IACpE,MAAMV,OAAOF,OAAOa,GAAG,CAAChB;IACxB,IAAIK,SAAS,QAAQ;YACNF,aACCA;QADd,MAAMI,QAAOJ,cAAAA,OAAOa,GAAG,CAAC,mBAAXb,cAAsB;QACnC,MAAMK,SAAQL,eAAAA,OAAOa,GAAG,CAAC,oBAAXb,eAAuB;QACrC,OAAOI,QAAQC,QAAQ;YAAEH,MAAM;YAAQE;YAAMC;QAAM,IAAI;IACzD;IACA,IAAIH,SAAS,SAAS;QACpB,MAAMI,SAASN,OAAOa,GAAG,CAAC;QAC1B,OAAO;YAAEX,MAAM;YAASI,QAAQA,UAAUE,cAAcM,QAAQ,CAACR,UAAUA,SAAS;QAAe;IACrG;IACA,OAAO;AACT"}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
import { type SecretBoxKeyring } from '@aglyn/shared-util-tools/secret-box';
|
|
18
|
+
import { type OutreachMailboxCredentials } from '../model/outreach.types';
|
|
19
|
+
/**
|
|
20
|
+
* A MAILBOX'S GRANT AT REST (AGL-2978): `outreachMailboxCredentials/{mailboxId}`.
|
|
21
|
+
*
|
|
22
|
+
* The refresh token is the only secret, and it is stored only sealed —
|
|
23
|
+
* AES-256-GCM under `OUTREACH_TOKEN_KEY` through the shared secret box, bound
|
|
24
|
+
* to this document by the seal's context, so a sealed token copied into
|
|
25
|
+
* another mailbox's credential refuses to open there. Every other field is
|
|
26
|
+
* bookkeeping a reader needs without the key: which account, which scopes,
|
|
27
|
+
* which member, which key sealed it.
|
|
28
|
+
*
|
|
29
|
+
* Sealed fields carry `token` in their names. The personal-data export
|
|
30
|
+
* redacts by field name first, and a neutral name would be judged by its
|
|
31
|
+
* value's shape alone.
|
|
32
|
+
*/
|
|
33
|
+
/** A connected Google mailbox's stored credential. */
|
|
34
|
+
export interface OutreachGoogleMailboxCredentials extends OutreachMailboxCredentials {
|
|
35
|
+
provider: 'google';
|
|
36
|
+
/** The member who connected the mailbox. */
|
|
37
|
+
connectedByUid: string;
|
|
38
|
+
/** Google's stable account id (`sub`), so one account's grants can be found. */
|
|
39
|
+
providerAccountId: string;
|
|
40
|
+
/** The account's address, as Google verified it. */
|
|
41
|
+
email: string;
|
|
42
|
+
/** The scopes Google granted, as it listed them. */
|
|
43
|
+
scopes: string[];
|
|
44
|
+
/** The refresh token, sealed — see the module comment. */
|
|
45
|
+
sealedRefreshToken: string;
|
|
46
|
+
/** The id of the key that sealed it, for rotation. */
|
|
47
|
+
tokenKeyId: string;
|
|
48
|
+
}
|
|
49
|
+
/** The context a mailbox's refresh token is sealed under. */
|
|
50
|
+
export declare function refreshTokenSealContext(mailboxId: string): string;
|
|
51
|
+
/** Seals a refresh token for one mailbox under the keyring's current key. */
|
|
52
|
+
export declare function sealMailboxRefreshToken(refreshToken: string, mailboxId: string, keyring: SecretBoxKeyring): {
|
|
53
|
+
sealedRefreshToken: string;
|
|
54
|
+
tokenKeyId: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Opens a stored credential's refresh token. Throws `SecretBoxError` when it
|
|
58
|
+
* cannot be opened — a missing or rotated-away key, or a tampered value.
|
|
59
|
+
*/
|
|
60
|
+
export declare function openMailboxRefreshToken(credential: Pick<OutreachGoogleMailboxCredentials, 'mailboxId' | 'sealedRefreshToken'>, keyring: SecretBoxKeyring): {
|
|
61
|
+
refreshToken: string;
|
|
62
|
+
needsReseal: boolean;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The id a member's mailbox for one Google account has in one organization.
|
|
66
|
+
*
|
|
67
|
+
* Derived rather than random, so connecting the same account again updates
|
|
68
|
+
* the same mailbox instead of adding a second one, and two members — or two
|
|
69
|
+
* organizations — connecting one account never share an id. The credential
|
|
70
|
+
* collection is top-level, keyed by this id, so it must be unique across
|
|
71
|
+
* every organization, which is why the org is part of it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function outreachMailboxId(orgId: string, uid: string, providerAccountId: string): string;
|
|
74
|
+
export declare function mailboxCredentialsRef(firestore: FirebaseFirestore.Firestore, mailboxId: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
75
|
+
export declare function mailboxRef(firestore: FirebaseFirestore.Firestore, orgId: string, mailboxId: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
76
|
+
/** A stored credential read defensively, or `null` when it is not one. */
|
|
77
|
+
export declare function readMailboxCredentials(data: unknown): OutreachGoogleMailboxCredentials | null;
|
|
78
|
+
/**
|
|
79
|
+
* How many OTHER stored credentials hold a grant for the same Google account.
|
|
80
|
+
*
|
|
81
|
+
* Google's revocation ends the whole grant this client holds for an account,
|
|
82
|
+
* not one token, so revoking while another mailbox still uses the account —
|
|
83
|
+
* the same shared inbox connected by two members, or one account connected
|
|
84
|
+
* in two organizations — would silently break that mailbox. A disconnect or
|
|
85
|
+
* an erasure asks this first and leaves the grant alone while it is shared.
|
|
86
|
+
*
|
|
87
|
+
* An erasure names what it is ending anyway — a whole organization's
|
|
88
|
+
* credentials, or every one of a person's — and those are not counted.
|
|
89
|
+
*/
|
|
90
|
+
export declare function countOtherCredentialsForAccount(firestore: FirebaseFirestore.Firestore, input: {
|
|
91
|
+
providerAccountId: string;
|
|
92
|
+
mailboxId: string;
|
|
93
|
+
excludeOrgId?: string;
|
|
94
|
+
excludeMailboxIds?: ReadonlySet<string>;
|
|
95
|
+
}): Promise<number>;
|