@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/routes/sequence-routes.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { applyOutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport {\n validateOutreachSequence,\n validateOutreachSequenceActivation,\n} from '../engine/sequence-validation'\nimport type {\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n} from '../model/outreach-api'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachMailbox, OutreachSequence, OutreachSequenceStatus } from '../model/outreach.types'\nimport {\n outreachEnrolledSequenceIssues,\n outreachMailboxActivationIssue,\n readOutreachSequenceDraft,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE SEQUENCE ROUTES (AGL-2980): save, activate, pause, archive, delete.\n *\n * ## Who may change a sequence\n *\n * A sequence sends as the member whose mailbox it names, so it is theirs to\n * change: the member who connected the mailbox, or an organization owner or\n * admin, may save it, activate it and send from it — the same two the\n * mailbox's own routes let manage the mailbox. Anyone with `outreach.use`\n * may pause or archive any sequence, because stopping mail is never the act\n * that needs guarding.\n *\n * ## Saving\n *\n * The draft is read with the editor's reader and judged by the engine's\n * validator, so the save refuses exactly what the editor showed beside a\n * field. On top of that, the site must be one of the organization's, the\n * mailbox one of its connected mailboxes, the countries ones the\n * organization allows, and — once anyone is enrolled — the steps, site and\n * mailbox fixed (`outreachEnrolledSequenceIssues`). A new sequence is a\n * draft; an edit keeps the status it had, and an active sequence stays\n * active only because the edit passed the same validator activation does.\n *\n * ## Activating\n *\n * `validateOutreachSequenceActivation`: everything a save refuses on, and an\n * organization whose footer cannot be written — no legal name, or no postal\n * address. And its mailbox must be sending (`outreachMailboxActivationIssue`):\n * chosen, still connected, not paused, and not waiting to be reconnected —\n * the same words the sequence page shows beside its disabled Activate.\n *\n * ## Archiving\n *\n * Final. Every person still active or paused in the sequence is stopped\n * with `sequence_archived`, in batches, so no archived sequence goes on\n * sending.\n */\n\n/** A batch holds 500 writes; a page of enrollments stops this many. */\nconst ARCHIVE_PAGE = 400\n\n/** The activity line each lifecycle act writes. */\nexport const OUTREACH_SEQUENCE_ACTIVITY = {\n create: 'Created a sequence',\n activate: 'Activated a sequence',\n pause: 'Paused a sequence',\n archive: 'Archived a sequence',\n delete: 'Deleted a sequence',\n} as const\n\n/** Where each action may start, and where it ends. */\nconst TRANSITIONS: Record<\n OutreachSequenceAction,\n { from: readonly OutreachSequenceStatus[]; to: OutreachSequenceStatus }\n> = {\n activate: { from: ['draft', 'paused'], to: 'active' },\n pause: { from: ['active'], to: 'paused' },\n archive: { from: ['draft', 'active', 'paused'], to: 'archived' },\n}\n\nconst ACTION_REFUSALS: Record<OutreachSequenceAction, string> = {\n activate: 'Only a draft or paused sequence can be activated.',\n pause: 'Only an active sequence can be paused.',\n archive: 'This sequence is already archived.',\n}\n\nexport interface OutreachSequenceRoutes {\n save: PluginWebApiHandler\n status: PluginWebApiHandler\n remove: PluginWebApiHandler\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\nasync function loadSequence(\n firestore: Firestore,\n orgId: string,\n sequenceId: string,\n): Promise<OutreachSequence | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n return readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n}\n\n/** Whether anyone was ever enrolled in the sequence. */\nasync function hasEnrollments(firestore: Firestore, orgId: string, sequenceId: string): Promise<boolean> {\n const found = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('sequenceId', '==', sequenceId)\n .limit(1)\n .get()\n return !found.empty\n}\n\nconst issue = (\n path: string,\n code: OutreachSequenceIssue['code'],\n message: string,\n): OutreachSequenceIssue => ({ path, code, message, severity: 'error' })\n\n/**\n * The organization-level facts a draft has to agree with: its site is the\n * organization's, its mailbox is connected and the caller may send from it,\n * and its countries are ones the organization allows. `activating` adds\n * that the mailbox is sending now — chosen, and neither paused nor waiting\n * to be reconnected.\n */\nasync function draftPlacementIssues(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n draft: OutreachSequenceDraft,\n options: { activating?: boolean } = {},\n): Promise<OutreachSequenceIssue[]> {\n const issues: OutreachSequenceIssue[] = []\n if (draft.hostId) {\n const host = readOutreachDocumentId(draft.hostId)\n ? await firestore.collection('hosts').doc(draft.hostId).get()\n : null\n if (!host?.exists || host.get('orgId') !== caller.orgId) {\n issues.push(issue('hostId', 'host_unknown', \"Choose one of this organization's sites.\"))\n }\n } else {\n issues.push(issue('hostId', 'host_unknown', \"Choose the site whose contacts this sequence emails.\"))\n }\n const mailbox = draft.mailboxId ? await loadMailbox(firestore, caller.orgId, draft.mailboxId) : null\n const activation = options.activating ? outreachMailboxActivationIssue(draft.mailboxId, mailbox) : null\n if (draft.mailboxId) {\n const placement = mailboxIssues(caller, mailbox)\n // A mailbox that is gone is refused once, in activation's words when\n // activating: they say what to do next.\n issues.push(\n ...(activation?.code === 'mailbox_unknown'\n ? placement.filter((entry) => entry.code !== 'mailbox_unknown')\n : placement),\n )\n }\n if (activation && !issues.some((entry) => entry.code === 'mailbox_not_yours')) issues.push(activation)\n const settings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const outside = draft.settings.allowedCountries.filter(\n (code) => !settings.allowedCountries.includes(code),\n )\n if (outside.length) {\n issues.push(\n issue(\n 'settings.allowedCountries',\n 'country_not_in_org',\n `${outside.join(', ')} ${outside.length === 1 ? \"isn't\" : \"aren't\"} among the countries your organization allows. Add ${outside.length === 1 ? 'it' : 'them'} in Sequences compliance settings first.`,\n ),\n )\n }\n return issues\n}\n\n/** A mailbox of this organization's, as stored; `null` when there is none by that id. */\nasync function loadMailbox(firestore: Firestore, orgId: string, mailboxId: string): Promise<OutreachMailbox | null> {\n const snapshot = readOutreachDocumentId(mailboxId)\n ? await outreachOrgCollection(firestore, orgId, 'mailboxes').doc(mailboxId).get()\n : null\n return readStoredOutreachMailbox(mailboxId, snapshot?.exists ? snapshot.data() : undefined)\n}\n\n/** Whether the mailbox is connected here, and the caller may send from it. */\nfunction mailboxIssues(caller: OutreachRouteCaller, mailbox: OutreachMailbox | null): OutreachSequenceIssue[] {\n if (!mailbox || mailbox.status === 'disconnected') {\n return [issue('mailboxId', 'mailbox_unknown', 'That mailbox is not connected to this organization.')]\n }\n if (mailbox.connectedByUid !== caller.uid && !caller.isOrgAdmin) {\n return [\n issue(\n 'mailboxId',\n 'mailbox_not_yours',\n 'Only the member who connected this mailbox, or an organization owner or admin, can send from it.',\n ),\n ]\n }\n return []\n}\n\n/** One issue per field and code: activation and the engine can both ask for a mailbox. */\nfunction distinctIssues(issues: readonly OutreachSequenceIssue[]): OutreachSequenceIssue[] {\n const seen = new Set<string>()\n return issues.filter((entry) => {\n const key = `${entry.path}|${entry.code}`\n if (seen.has(key)) return false\n seen.add(key)\n return true\n })\n}\n\nexport function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes {\n const activity = (caller: OutreachRouteCaller, action: string, sequence: Pick<OutreachSequence, 'id' | 'name'>) =>\n deps.logOrgActivity(caller.orgId, { uid: caller.uid, email: caller.email }, action, {\n type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,\n id: sequence.id,\n name: sequence.name,\n })\n\n const save: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n\n const rawId = body['sequenceId']\n const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId)\n if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.')\n const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null\n if (sequenceId && !existing) {\n return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n }\n if (existing?.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', \"An archived sequence can't be edited.\")\n }\n\n const draft = readOutreachSequenceDraft(body['sequence'])\n // A sequence sends as its mailbox's member, so it is theirs (or an\n // admin's) to change — the stored mailbox as well as a new one: moving\n // a colleague's sequence onto your own mailbox is still changing theirs.\n if (existing?.mailboxId && existing.mailboxId !== draft.mailboxId) {\n const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId))\n const refused = stored.find((entry) => entry.code === 'mailbox_not_yours')\n if (refused) return outreachRefusal(403, 'permission', refused.message)\n }\n const judged: OutreachSequenceIssue[] = [\n ...validateOutreachSequence(draft),\n ...(await draftPlacementIssues(firestore, caller, draft)),\n ]\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n if (ownership) return outreachRefusal(403, 'permission', ownership.message)\n if (existing && (await hasEnrollments(firestore, caller.orgId, existing.id))) {\n judged.push(...outreachEnrolledSequenceIssues(existing, draft))\n }\n const errors = judged.filter((entry) => entry.severity === 'error')\n if (errors.length) {\n return outreachRefusal(400, 'invalid-sequence', errors[0].message, { issues: judged })\n }\n\n const nowMs = deps.now()\n const ref = existing\n ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id)\n : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid())\n const sequence: OutreachSequence = {\n id: ref.id,\n ...draft,\n status: existing?.status ?? 'draft',\n createdAtMs: existing?.createdAtMs || nowMs,\n updatedAtMs: nowMs,\n }\n await ref.set(sequence)\n if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence)\n return outreachOk({\n ok: true,\n sequence,\n created: !existing,\n warnings: judged.filter((entry) => entry.severity === 'warning'),\n } satisfies OutreachSequenceSaveResponse)\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const action = body['action'] as OutreachSequenceAction\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId || !Object.prototype.hasOwnProperty.call(TRANSITIONS, action)) {\n return outreachRefusal(400, 'invalid-request', 'Name the sequence, and activate, pause or archive.')\n }\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n const transition = TRANSITIONS[action]\n if (sequence.status === transition.to) {\n return outreachOk({ ok: true, sequence, stoppedEnrollments: 0 } satisfies OutreachSequenceStatusResponse)\n }\n if (!transition.from.includes(sequence.status)) {\n return outreachRefusal(409, 'transition-refused', ACTION_REFUSALS[action])\n }\n\n if (action === 'activate') {\n const orgSettings = await readOutreachComplianceSettingsDoc(firestore, caller.orgId)\n const judged: OutreachSequenceIssue[] = distinctIssues([\n ...validateOutreachSequenceActivation(sequence, orgSettings),\n ...(await draftPlacementIssues(firestore, caller, sequence, { activating: true })),\n ]).filter((entry) => entry.severity === 'error')\n if (judged.length) {\n const ownership = judged.find((entry) => entry.code === 'mailbox_not_yours')\n return ownership\n ? outreachRefusal(403, 'permission', ownership.message)\n : outreachRefusal(409, 'activation-refused', judged[0].message, { issues: judged })\n }\n }\n\n const nowMs = deps.now()\n const next: OutreachSequence = { ...sequence, status: transition.to, updatedAtMs: nowMs }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences')\n .doc(sequence.id)\n .update({ status: next.status, updatedAtMs: nowMs })\n const stoppedEnrollments =\n action === 'archive' ? await stopOpenEnrollments(firestore, caller, sequence.id, nowMs) : 0\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY[action], sequence)\n return outreachOk({ ok: true, sequence: next, stoppedEnrollments } satisfies OutreachSequenceStatusResponse)\n }\n\n const remove: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (caller instanceof Response) return caller\n const sequenceId = readOutreachDocumentId(body['sequenceId'])\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.')\n const firestore = deps.firestore()\n const sequence = await loadSequence(firestore, caller.orgId, sequenceId)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status !== 'draft' || (await hasEnrollments(firestore, caller.orgId, sequence.id))) {\n // What people were sent, and why they stopped, is the record of what\n // this organization did; only a draft nobody was enrolled in has none.\n return outreachRefusal(\n 409,\n 'delete-refused',\n 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.',\n )\n }\n await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete()\n await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence)\n return outreachOk({ ok: true })\n }\n\n return { save, status, remove }\n}\n\n/**\n * Stops everyone still active or paused in an archived sequence, a page at\n * a time, through the engine's own transition. Returns how many it stopped.\n */\nasync function stopOpenEnrollments(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequenceId: string,\n nowMs: number,\n): Promise<number> {\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n let stopped = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n let page = enrollments.where('sequenceId', '==', sequenceId).orderBy('__name__').limit(ARCHIVE_PAGE)\n if (after) page = page.startAfter(after)\n const snapshot = await page.get()\n if (snapshot.empty) break\n const batch = firestore.batch()\n let writes = 0\n for (const doc of snapshot.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment || !OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment.status)) continue\n const { patch } = applyOutreachEnrollmentEvent(enrollment, {\n type: 'stop',\n reason: 'sequence_archived',\n atMs: nowMs,\n byUid: caller.uid,\n })\n if (!patch) continue\n batch.update(doc.ref, { ...patch, updatedAtMs: nowMs })\n writes += 1\n }\n if (writes) await batch.commit()\n stopped += writes\n after = snapshot.docs[snapshot.docs.length - 1]\n if (snapshot.size < ARCHIVE_PAGE) break\n }\n return stopped\n}\n"],"names":["createResourceUid","applyOutreachEnrollmentEvent","validateOutreachSequence","validateOutreachSequenceActivation","OUTREACH_OPEN_ENROLLMENT_STATUSES","outreachEnrolledSequenceIssues","outreachMailboxActivationIssue","readOutreachSequenceDraft","readOutreachComplianceSettingsDoc","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","ARCHIVE_PAGE","OUTREACH_SEQUENCE_ACTIVITY","create","activate","pause","archive","delete","TRANSITIONS","from","to","ACTION_REFUSALS","loadSequence","firestore","orgId","sequenceId","snapshot","doc","get","exists","data","undefined","hasEnrollments","found","where","limit","empty","issue","path","code","message","severity","draftPlacementIssues","caller","draft","options","issues","hostId","host","collection","push","mailbox","mailboxId","loadMailbox","activation","activating","placement","mailboxIssues","filter","entry","some","settings","outside","allowedCountries","includes","length","join","status","connectedByUid","uid","isOrgAdmin","distinctIssues","seen","Set","key","has","add","createOutreachSequenceRoutes","deps","activity","action","sequence","logOrgActivity","email","type","id","name","save","request","method","body","gate","Response","rawId","existing","stored","refused","find","judged","ownership","errors","nowMs","now","ref","createdAtMs","updatedAtMs","set","ok","created","warnings","Object","prototype","hasOwnProperty","call","transition","stoppedEnrollments","orgSettings","next","update","stopOpenEnrollments","remove","enrollments","stopped","after","page","orderBy","startAfter","batch","writes","docs","enrollment","patch","reason","atMs","byUid","commit","size"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
import type { OutreachRouteDeps } from './route-deps';
|
|
19
|
+
/**
|
|
20
|
+
* THE COMPLIANCE SETTINGS ROUTE (AGL-2980): `outreach/settings`.
|
|
21
|
+
*
|
|
22
|
+
* `GET ?orgId` answers the organization's settings with the defaults
|
|
23
|
+
* applied; `POST` validates a save with the function the Compliance page
|
|
24
|
+
* validates with and stores it. A save that changes something writes one
|
|
25
|
+
* line to the organization's activity feed, because the footer every email
|
|
26
|
+
* carries is a legal statement and who last changed it is worth knowing.
|
|
27
|
+
*/
|
|
28
|
+
/** The activity line a changed save writes. */
|
|
29
|
+
export declare const OUTREACH_SETTINGS_ACTIVITY = "Updated Outreach compliance settings";
|
|
30
|
+
export declare function createOutreachSettingsRoute(deps: OutreachRouteDeps): PluginWebApiHandler;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/ import { validateOutreachComplianceSettings } from "../model/compliance-settings.js";
|
|
17
|
+
import { readOutreachComplianceSettingsDoc, writeOutreachComplianceSettings } from "../storage/compliance-settings-store.js";
|
|
18
|
+
import { outreachRouteGate } from "./route-gate.js";
|
|
19
|
+
import { outreachMethodNotAllowed, outreachOk, outreachRefusal, readOutreachJsonBody } from "./route-http.js";
|
|
20
|
+
/**
|
|
21
|
+
* THE COMPLIANCE SETTINGS ROUTE (AGL-2980): `outreach/settings`.
|
|
22
|
+
*
|
|
23
|
+
* `GET ?orgId` answers the organization's settings with the defaults
|
|
24
|
+
* applied; `POST` validates a save with the function the Compliance page
|
|
25
|
+
* validates with and stores it. A save that changes something writes one
|
|
26
|
+
* line to the organization's activity feed, because the footer every email
|
|
27
|
+
* carries is a legal statement and who last changed it is worth knowing.
|
|
28
|
+
*/ /** The activity line a changed save writes. */ export const OUTREACH_SETTINGS_ACTIVITY = 'Updated Outreach compliance settings';
|
|
29
|
+
export function createOutreachSettingsRoute(deps) {
|
|
30
|
+
return async (request)=>{
|
|
31
|
+
if (request.method === 'GET') {
|
|
32
|
+
const gate = await outreachRouteGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate);
|
|
33
|
+
if (gate instanceof Response) return gate;
|
|
34
|
+
const settings = await readOutreachComplianceSettingsDoc(deps.firestore(), gate.orgId);
|
|
35
|
+
return outreachOk({
|
|
36
|
+
ok: true,
|
|
37
|
+
settings
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (request.method !== 'POST') return outreachMethodNotAllowed('GET, POST');
|
|
41
|
+
const body = await readOutreachJsonBody(request);
|
|
42
|
+
const gate = await outreachRouteGate(request, body['orgId'], deps.gate);
|
|
43
|
+
if (gate instanceof Response) return gate;
|
|
44
|
+
const { settings, issues } = validateOutreachComplianceSettings(body);
|
|
45
|
+
if (issues.length) {
|
|
46
|
+
return outreachRefusal(400, 'invalid-settings', issues[0].message, {
|
|
47
|
+
issues
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const result = await writeOutreachComplianceSettings(deps.firestore(), {
|
|
51
|
+
orgId: gate.orgId,
|
|
52
|
+
settings,
|
|
53
|
+
byUid: gate.uid,
|
|
54
|
+
nowMs: deps.now()
|
|
55
|
+
});
|
|
56
|
+
if (result.changed) {
|
|
57
|
+
await deps.logOrgActivity(gate.orgId, {
|
|
58
|
+
uid: gate.uid,
|
|
59
|
+
email: gate.email
|
|
60
|
+
}, OUTREACH_SETTINGS_ACTIVITY, {
|
|
61
|
+
type: 'org',
|
|
62
|
+
id: gate.orgId
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return outreachOk({
|
|
66
|
+
ok: true,
|
|
67
|
+
settings: result.settings,
|
|
68
|
+
changed: result.changed
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=settings-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/settings-routes.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 { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { validateOutreachComplianceSettings } from '../model/compliance-settings'\nimport type {\n OutreachSettingsResponse,\n OutreachSettingsSaveResponse,\n} from '../model/outreach-api'\nimport {\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n} from '../storage/compliance-settings-store'\nimport type { OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * THE COMPLIANCE SETTINGS ROUTE (AGL-2980): `outreach/settings`.\n *\n * `GET ?orgId` answers the organization's settings with the defaults\n * applied; `POST` validates a save with the function the Compliance page\n * validates with and stores it. A save that changes something writes one\n * line to the organization's activity feed, because the footer every email\n * carries is a legal statement and who last changed it is worth knowing.\n */\n\n/** The activity line a changed save writes. */\nexport const OUTREACH_SETTINGS_ACTIVITY = 'Updated Outreach compliance settings'\n\nexport function createOutreachSettingsRoute(deps: OutreachRouteDeps): PluginWebApiHandler {\n return async (request) => {\n if (request.method === 'GET') {\n const gate = await outreachRouteGate(\n request,\n new URL(request.url).searchParams.get('orgId'),\n deps.gate,\n )\n if (gate instanceof Response) return gate\n const settings = await readOutreachComplianceSettingsDoc(deps.firestore(), gate.orgId)\n return outreachOk({ ok: true, settings } satisfies OutreachSettingsResponse)\n }\n if (request.method !== 'POST') return outreachMethodNotAllowed('GET, POST')\n\n const body = await readOutreachJsonBody(request)\n const gate = await outreachRouteGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const { settings, issues } = validateOutreachComplianceSettings(body)\n if (issues.length) {\n return outreachRefusal(400, 'invalid-settings', issues[0].message, { issues })\n }\n const result = await writeOutreachComplianceSettings(deps.firestore(), {\n orgId: gate.orgId,\n settings,\n byUid: gate.uid,\n nowMs: deps.now(),\n })\n if (result.changed) {\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n OUTREACH_SETTINGS_ACTIVITY,\n { type: 'org', id: gate.orgId },\n )\n }\n return outreachOk({\n ok: true,\n settings: result.settings,\n changed: result.changed,\n } satisfies OutreachSettingsSaveResponse)\n }\n}\n"],"names":["validateOutreachComplianceSettings","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachJsonBody","OUTREACH_SETTINGS_ACTIVITY","createOutreachSettingsRoute","deps","request","method","gate","URL","url","searchParams","get","Response","settings","firestore","orgId","ok","body","issues","length","message","result","byUid","uid","nowMs","now","changed","logOrgActivity","email","type","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,kCAAkC,QAAQ,kCAA8B;AAKjF,SACEC,iCAAiC,EACjCC,+BAA+B,QAC1B,0CAAsC;AAE7C,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SACEC,wBAAwB,EACxBC,UAAU,EACVC,eAAe,EACfC,oBAAoB,QACf,kBAAc;AAErB;;;;;;;;CAQC,GAED,6CAA6C,GAC7C,OAAO,MAAMC,6BAA6B,uCAAsC;AAEhF,OAAO,SAASC,4BAA4BC,IAAuB;IACjE,OAAO,OAAOC;QACZ,IAAIA,QAAQC,MAAM,KAAK,OAAO;YAC5B,MAAMC,OAAO,MAAMV,kBACjBQ,SACA,IAAIG,IAAIH,QAAQI,GAAG,EAAEC,YAAY,CAACC,GAAG,CAAC,UACtCP,KAAKG,IAAI;YAEX,IAAIA,gBAAgBK,UAAU,OAAOL;YACrC,MAAMM,WAAW,MAAMlB,kCAAkCS,KAAKU,SAAS,IAAIP,KAAKQ,KAAK;YACrF,OAAOhB,WAAW;gBAAEiB,IAAI;gBAAMH;YAAS;QACzC;QACA,IAAIR,QAAQC,MAAM,KAAK,QAAQ,OAAOR,yBAAyB;QAE/D,MAAMmB,OAAO,MAAMhB,qBAAqBI;QACxC,MAAME,OAAO,MAAMV,kBAAkBQ,SAASY,IAAI,CAAC,QAAQ,EAAEb,KAAKG,IAAI;QACtE,IAAIA,gBAAgBK,UAAU,OAAOL;QACrC,MAAM,EAAEM,QAAQ,EAAEK,MAAM,EAAE,GAAGxB,mCAAmCuB;QAChE,IAAIC,OAAOC,MAAM,EAAE;YACjB,OAAOnB,gBAAgB,KAAK,oBAAoBkB,MAAM,CAAC,EAAE,CAACE,OAAO,EAAE;gBAAEF;YAAO;QAC9E;QACA,MAAMG,SAAS,MAAMzB,gCAAgCQ,KAAKU,SAAS,IAAI;YACrEC,OAAOR,KAAKQ,KAAK;YACjBF;YACAS,OAAOf,KAAKgB,GAAG;YACfC,OAAOpB,KAAKqB,GAAG;QACjB;QACA,IAAIJ,OAAOK,OAAO,EAAE;YAClB,MAAMtB,KAAKuB,cAAc,CACvBpB,KAAKQ,KAAK,EACV;gBAAEQ,KAAKhB,KAAKgB,GAAG;gBAAEK,OAAOrB,KAAKqB,KAAK;YAAC,GACnC1B,4BACA;gBAAE2B,MAAM;gBAAOC,IAAIvB,KAAKQ,KAAK;YAAC;QAElC;QACA,OAAOhB,WAAW;YAChBiB,IAAI;YACJH,UAAUQ,OAAOR,QAAQ;YACzBa,SAASL,OAAOK,OAAO;QACzB;IACF;AACF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
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 OutreachEnrollmentEvent } from '../engine/enrollment-state';
|
|
18
|
+
import type { OutreachDoNotContactReason, OutreachEnrollment } from '../model/outreach.types';
|
|
19
|
+
import type { OutreachRuntimeDeps } from './runtime-deps';
|
|
20
|
+
/**
|
|
21
|
+
* WHAT THE RUNTIME DOES TO ENROLLMENTS, AND TO THE LISTS BESIDE THEM
|
|
22
|
+
* (AGL-2981).
|
|
23
|
+
*
|
|
24
|
+
* Every status change is the engine's (`applyOutreachEnrollmentEvent`),
|
|
25
|
+
* written in a transaction over the one enrollment so a member's pause and a
|
|
26
|
+
* run's step completion never interleave. The runtime's events name no
|
|
27
|
+
* member: `stoppedByUid` stays `null`.
|
|
28
|
+
*/
|
|
29
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
30
|
+
/** The enrollment after an event, and whether the event changed it. */
|
|
31
|
+
export interface OutreachEventOutcome {
|
|
32
|
+
enrollment: OutreachEnrollment | null;
|
|
33
|
+
changed: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Applies one event to one enrollment, with any fields the caller writes
|
|
37
|
+
* beside it — its handled message ids, a postponed due time. A transition
|
|
38
|
+
* the engine refuses writes only the extra fields.
|
|
39
|
+
*/
|
|
40
|
+
export declare function applyOutreachEvent(firestore: Firestore, input: {
|
|
41
|
+
orgId: string;
|
|
42
|
+
enrollmentId: string;
|
|
43
|
+
event: OutreachEnrollmentEvent | null;
|
|
44
|
+
extra?: (enrollment: OutreachEnrollment) => Record<string, unknown> | null;
|
|
45
|
+
nowMs: number;
|
|
46
|
+
}): Promise<OutreachEventOutcome>;
|
|
47
|
+
/**
|
|
48
|
+
* Opts every OTHER open enrollment of an address in the organization out,
|
|
49
|
+
* as `do_not_contact`: the person asked not to be emailed, so no sequence
|
|
50
|
+
* is next. Answers how many moved.
|
|
51
|
+
*/
|
|
52
|
+
export declare function optOutOtherOutreachEnrollments(firestore: Firestore, input: {
|
|
53
|
+
orgId: string;
|
|
54
|
+
email: string;
|
|
55
|
+
exceptEnrollmentId: string | null;
|
|
56
|
+
nowMs: number;
|
|
57
|
+
detail: string;
|
|
58
|
+
}): Promise<number>;
|
|
59
|
+
/** Where an opt-out came from. */
|
|
60
|
+
export type OutreachOptOutSource = Extract<OutreachDoNotContactReason, 'opt_out_reply' | 'unsubscribe'>;
|
|
61
|
+
/**
|
|
62
|
+
* A person asked not to be emailed — by the unsubscribe link, the `mailto:`
|
|
63
|
+
* or a reply — and every list that has to remember it does:
|
|
64
|
+
*
|
|
65
|
+
* 1. the organization's do-not-contact list, which every sequence checks
|
|
66
|
+
* before every send (its first entry for the address stands);
|
|
67
|
+
* 2. the `sales` topic of each site named, which the site's own sales
|
|
68
|
+
* email reads too;
|
|
69
|
+
* 3. every open enrollment of the address in the organization: the one it
|
|
70
|
+
* came from with the reason given, the others as `do_not_contact`.
|
|
71
|
+
*
|
|
72
|
+
* Each is idempotent, so a second request — the link used twice, a reply
|
|
73
|
+
* read by two runs — changes nothing.
|
|
74
|
+
*/
|
|
75
|
+
export declare function recordOutreachOptOut(deps: Pick<OutreachRuntimeDeps, 'firestore' | 'optOutOfSalesTopic'>, input: {
|
|
76
|
+
orgId: string;
|
|
77
|
+
email: string;
|
|
78
|
+
source: OutreachOptOutSource;
|
|
79
|
+
enrollment: Pick<OutreachEnrollment, 'id' | 'sequenceId' | 'hostId'> | null;
|
|
80
|
+
hostIds: readonly string[];
|
|
81
|
+
detail: string | null;
|
|
82
|
+
nowMs: number;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
stopped: number;
|
|
85
|
+
}>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
*/ import { normalizeContactEmail } from "@aglyn/aglyn/app-utils/contacts";
|
|
18
|
+
import { applyOutreachEnrollmentEvent } from "../engine/enrollment-state.js";
|
|
19
|
+
import { OUTREACH_OPEN_ENROLLMENT_STATUSES } from "../engine/gates.js";
|
|
20
|
+
import { addOutreachDoNotContact } from "../storage/do-not-contact-store.js";
|
|
21
|
+
import { outreachOrgCollection, readStoredOutreachEnrollment } from "../storage/outreach-records.js";
|
|
22
|
+
/**
|
|
23
|
+
* Applies one event to one enrollment, with any fields the caller writes
|
|
24
|
+
* beside it — its handled message ids, a postponed due time. A transition
|
|
25
|
+
* the engine refuses writes only the extra fields.
|
|
26
|
+
*/ export async function applyOutreachEvent(firestore, input) {
|
|
27
|
+
const ref = outreachOrgCollection(firestore, input.orgId, 'enrollments').doc(input.enrollmentId);
|
|
28
|
+
return firestore.runTransaction(async (transaction)=>{
|
|
29
|
+
var _ref;
|
|
30
|
+
const snapshot = await transaction.get(ref);
|
|
31
|
+
const enrollment = readStoredOutreachEnrollment(input.enrollmentId, snapshot.exists ? snapshot.data() : undefined);
|
|
32
|
+
if (!enrollment) return {
|
|
33
|
+
enrollment: null,
|
|
34
|
+
changed: false
|
|
35
|
+
};
|
|
36
|
+
const patch = input.event ? applyOutreachEnrollmentEvent(enrollment, input.event).patch : null;
|
|
37
|
+
const extra = (_ref = input.extra == null ? void 0 : input.extra.call(input, enrollment)) != null ? _ref : null;
|
|
38
|
+
if (!patch && !extra) return {
|
|
39
|
+
enrollment,
|
|
40
|
+
changed: false
|
|
41
|
+
};
|
|
42
|
+
const written = _extends({}, patch != null ? patch : {}, extra != null ? extra : {}, {
|
|
43
|
+
updatedAtMs: input.nowMs
|
|
44
|
+
});
|
|
45
|
+
transaction.update(ref, written);
|
|
46
|
+
return {
|
|
47
|
+
enrollment: _extends({}, enrollment, written),
|
|
48
|
+
changed: patch !== null
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Opts every OTHER open enrollment of an address in the organization out,
|
|
54
|
+
* as `do_not_contact`: the person asked not to be emailed, so no sequence
|
|
55
|
+
* is next. Answers how many moved.
|
|
56
|
+
*/ export async function optOutOtherOutreachEnrollments(firestore, input) {
|
|
57
|
+
const found = await outreachOrgCollection(firestore, input.orgId, 'enrollments').where('email', '==', input.email).get();
|
|
58
|
+
let moved = 0;
|
|
59
|
+
for (const doc of found.docs){
|
|
60
|
+
if (doc.id === input.exceptEnrollmentId) continue;
|
|
61
|
+
if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(doc.get('status'))) continue;
|
|
62
|
+
const outcome = await applyOutreachEvent(firestore, {
|
|
63
|
+
orgId: input.orgId,
|
|
64
|
+
enrollmentId: doc.id,
|
|
65
|
+
event: {
|
|
66
|
+
type: 'opt_out',
|
|
67
|
+
atMs: input.nowMs,
|
|
68
|
+
reason: 'do_not_contact',
|
|
69
|
+
detail: input.detail
|
|
70
|
+
},
|
|
71
|
+
nowMs: input.nowMs
|
|
72
|
+
});
|
|
73
|
+
if (outcome.changed) moved += 1;
|
|
74
|
+
}
|
|
75
|
+
return moved;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A person asked not to be emailed — by the unsubscribe link, the `mailto:`
|
|
79
|
+
* or a reply — and every list that has to remember it does:
|
|
80
|
+
*
|
|
81
|
+
* 1. the organization's do-not-contact list, which every sequence checks
|
|
82
|
+
* before every send (its first entry for the address stands);
|
|
83
|
+
* 2. the `sales` topic of each site named, which the site's own sales
|
|
84
|
+
* email reads too;
|
|
85
|
+
* 3. every open enrollment of the address in the organization: the one it
|
|
86
|
+
* came from with the reason given, the others as `do_not_contact`.
|
|
87
|
+
*
|
|
88
|
+
* Each is idempotent, so a second request — the link used twice, a reply
|
|
89
|
+
* read by two runs — changes nothing.
|
|
90
|
+
*/ export async function recordOutreachOptOut(deps, input) {
|
|
91
|
+
var _ref, _ref1, _ref2;
|
|
92
|
+
var _input_enrollment, _input_enrollment1, _input_enrollment2;
|
|
93
|
+
const email = normalizeContactEmail(input.email);
|
|
94
|
+
if (!email) return {
|
|
95
|
+
stopped: 0
|
|
96
|
+
};
|
|
97
|
+
const firestore = deps.firestore();
|
|
98
|
+
await addOutreachDoNotContact(firestore, {
|
|
99
|
+
orgId: input.orgId,
|
|
100
|
+
email,
|
|
101
|
+
reason: input.source,
|
|
102
|
+
source: 'runtime',
|
|
103
|
+
nowMs: input.nowMs,
|
|
104
|
+
enrollmentId: (_ref = (_input_enrollment = input.enrollment) == null ? void 0 : _input_enrollment.id) != null ? _ref : null,
|
|
105
|
+
sequenceId: (_ref1 = (_input_enrollment1 = input.enrollment) == null ? void 0 : _input_enrollment1.sequenceId) != null ? _ref1 : null,
|
|
106
|
+
detail: input.detail
|
|
107
|
+
});
|
|
108
|
+
for (const hostId of new Set(input.hostIds.filter(Boolean))){
|
|
109
|
+
await deps.optOutOfSalesTopic({
|
|
110
|
+
hostId,
|
|
111
|
+
email
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
let stopped = 0;
|
|
115
|
+
if (input.enrollment) {
|
|
116
|
+
const own = await applyOutreachEvent(firestore, {
|
|
117
|
+
orgId: input.orgId,
|
|
118
|
+
enrollmentId: input.enrollment.id,
|
|
119
|
+
event: {
|
|
120
|
+
type: 'opt_out',
|
|
121
|
+
atMs: input.nowMs,
|
|
122
|
+
reason: input.source,
|
|
123
|
+
detail: input.detail
|
|
124
|
+
},
|
|
125
|
+
nowMs: input.nowMs
|
|
126
|
+
});
|
|
127
|
+
if (own.changed) stopped += 1;
|
|
128
|
+
}
|
|
129
|
+
stopped += await optOutOtherOutreachEnrollments(firestore, {
|
|
130
|
+
orgId: input.orgId,
|
|
131
|
+
email,
|
|
132
|
+
exceptEnrollmentId: (_ref2 = (_input_enrollment2 = input.enrollment) == null ? void 0 : _input_enrollment2.id) != null ? _ref2 : null,
|
|
133
|
+
nowMs: input.nowMs,
|
|
134
|
+
detail: input.source === 'unsubscribe' ? 'Unsubscribed from another sequence.' : 'Asked not to be emailed in a reply to another sequence.'
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
stopped
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=enrollment-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/enrollment-events.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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport {\n applyOutreachEnrollmentEvent,\n type OutreachEnrollmentEvent,\n} from '../engine/enrollment-state'\nimport { OUTREACH_OPEN_ENROLLMENT_STATUSES } from '../engine/gates'\nimport type { OutreachDoNotContactReason, OutreachEnrollment } from '../model/outreach.types'\nimport { addOutreachDoNotContact } from '../storage/do-not-contact-store'\nimport { outreachOrgCollection, readStoredOutreachEnrollment } from '../storage/outreach-records'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * WHAT THE RUNTIME DOES TO ENROLLMENTS, AND TO THE LISTS BESIDE THEM\n * (AGL-2981).\n *\n * Every status change is the engine's (`applyOutreachEnrollmentEvent`),\n * written in a transaction over the one enrollment so a member's pause and a\n * run's step completion never interleave. The runtime's events name no\n * member: `stoppedByUid` stays `null`.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The enrollment after an event, and whether the event changed it. */\nexport interface OutreachEventOutcome {\n enrollment: OutreachEnrollment | null\n changed: boolean\n}\n\n/**\n * Applies one event to one enrollment, with any fields the caller writes\n * beside it — its handled message ids, a postponed due time. A transition\n * the engine refuses writes only the extra fields.\n */\nexport async function applyOutreachEvent(\n firestore: Firestore,\n input: {\n orgId: string\n enrollmentId: string\n event: OutreachEnrollmentEvent | null\n extra?: (enrollment: OutreachEnrollment) => Record<string, unknown> | null\n nowMs: number\n },\n): Promise<OutreachEventOutcome> {\n const ref = outreachOrgCollection(firestore, input.orgId, 'enrollments').doc(input.enrollmentId)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const enrollment = readStoredOutreachEnrollment(input.enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return { enrollment: null, changed: false }\n const patch = input.event ? applyOutreachEnrollmentEvent(enrollment, input.event).patch : null\n const extra = input.extra?.(enrollment) ?? null\n if (!patch && !extra) return { enrollment, changed: false }\n const written = { ...(patch ?? {}), ...(extra ?? {}), updatedAtMs: input.nowMs }\n transaction.update(ref, written)\n return { enrollment: { ...enrollment, ...written } as OutreachEnrollment, changed: patch !== null }\n })\n}\n\n/**\n * Opts every OTHER open enrollment of an address in the organization out,\n * as `do_not_contact`: the person asked not to be emailed, so no sequence\n * is next. Answers how many moved.\n */\nexport async function optOutOtherOutreachEnrollments(\n firestore: Firestore,\n input: { orgId: string; email: string; exceptEnrollmentId: string | null; nowMs: number; detail: string },\n): Promise<number> {\n const found = await outreachOrgCollection(firestore, input.orgId, 'enrollments')\n .where('email', '==', input.email)\n .get()\n let moved = 0\n for (const doc of found.docs) {\n if (doc.id === input.exceptEnrollmentId) continue\n if (!OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(doc.get('status'))) continue\n const outcome = await applyOutreachEvent(firestore, {\n orgId: input.orgId,\n enrollmentId: doc.id,\n event: { type: 'opt_out', atMs: input.nowMs, reason: 'do_not_contact', detail: input.detail },\n nowMs: input.nowMs,\n })\n if (outcome.changed) moved += 1\n }\n return moved\n}\n\n/** Where an opt-out came from. */\nexport type OutreachOptOutSource = Extract<OutreachDoNotContactReason, 'opt_out_reply' | 'unsubscribe'>\n\n/**\n * A person asked not to be emailed — by the unsubscribe link, the `mailto:`\n * or a reply — and every list that has to remember it does:\n *\n * 1. the organization's do-not-contact list, which every sequence checks\n * before every send (its first entry for the address stands);\n * 2. the `sales` topic of each site named, which the site's own sales\n * email reads too;\n * 3. every open enrollment of the address in the organization: the one it\n * came from with the reason given, the others as `do_not_contact`.\n *\n * Each is idempotent, so a second request — the link used twice, a reply\n * read by two runs — changes nothing.\n */\nexport async function recordOutreachOptOut(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'optOutOfSalesTopic'>,\n input: {\n orgId: string\n email: string\n source: OutreachOptOutSource\n enrollment: Pick<OutreachEnrollment, 'id' | 'sequenceId' | 'hostId'> | null\n hostIds: readonly string[]\n detail: string | null\n nowMs: number\n },\n): Promise<{ stopped: number }> {\n const email = normalizeContactEmail(input.email)\n if (!email) return { stopped: 0 }\n const firestore = deps.firestore()\n await addOutreachDoNotContact(firestore, {\n orgId: input.orgId,\n email,\n reason: input.source,\n source: 'runtime',\n nowMs: input.nowMs,\n enrollmentId: input.enrollment?.id ?? null,\n sequenceId: input.enrollment?.sequenceId ?? null,\n detail: input.detail,\n })\n for (const hostId of new Set(input.hostIds.filter(Boolean))) {\n await deps.optOutOfSalesTopic({ hostId, email })\n }\n let stopped = 0\n if (input.enrollment) {\n const own = await applyOutreachEvent(firestore, {\n orgId: input.orgId,\n enrollmentId: input.enrollment.id,\n event: { type: 'opt_out', atMs: input.nowMs, reason: input.source, detail: input.detail },\n nowMs: input.nowMs,\n })\n if (own.changed) stopped += 1\n }\n stopped += await optOutOtherOutreachEnrollments(firestore, {\n orgId: input.orgId,\n email,\n exceptEnrollmentId: input.enrollment?.id ?? null,\n nowMs: input.nowMs,\n detail:\n input.source === 'unsubscribe'\n ? 'Unsubscribed from another sequence.'\n : 'Asked not to be emailed in a reply to another sequence.',\n })\n return { stopped }\n}\n"],"names":["normalizeContactEmail","applyOutreachEnrollmentEvent","OUTREACH_OPEN_ENROLLMENT_STATUSES","addOutreachDoNotContact","outreachOrgCollection","readStoredOutreachEnrollment","applyOutreachEvent","firestore","input","ref","orgId","doc","enrollmentId","runTransaction","transaction","snapshot","get","enrollment","exists","data","undefined","changed","patch","event","extra","written","updatedAtMs","nowMs","update","optOutOtherOutreachEnrollments","found","where","email","moved","docs","id","exceptEnrollmentId","includes","outcome","type","atMs","reason","detail","recordOutreachOptOut","deps","stopped","source","sequenceId","hostId","Set","hostIds","filter","Boolean","optOutOfSalesTopic","own"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SACEC,4BAA4B,QAEvB,gCAA4B;AACnC,SAASC,iCAAiC,QAAQ,qBAAiB;AAEnE,SAASC,uBAAuB,QAAQ,qCAAiC;AACzE,SAASC,qBAAqB,EAAEC,4BAA4B,QAAQ,iCAA6B;AAqBjG;;;;CAIC,GACD,OAAO,eAAeC,mBACpBC,SAAoB,EACpBC,KAMC;IAED,MAAMC,MAAML,sBAAsBG,WAAWC,MAAME,KAAK,EAAE,eAAeC,GAAG,CAACH,MAAMI,YAAY;IAC/F,OAAOL,UAAUM,cAAc,CAAC,OAAOC;;QACrC,MAAMC,WAAW,MAAMD,YAAYE,GAAG,CAACP;QACvC,MAAMQ,aAAaZ,6BAA6BG,MAAMI,YAAY,EAAEG,SAASG,MAAM,GAAGH,SAASI,IAAI,KAAKC;QACxG,IAAI,CAACH,YAAY,OAAO;YAAEA,YAAY;YAAMI,SAAS;QAAM;QAC3D,MAAMC,QAAQd,MAAMe,KAAK,GAAGtB,6BAA6BgB,YAAYT,MAAMe,KAAK,EAAED,KAAK,GAAG;QAC1F,MAAME,gBAAQhB,MAAMgB,KAAK,oBAAXhB,MAAMgB,KAAK,MAAXhB,OAAcS,8BAAe;QAC3C,IAAI,CAACK,SAAS,CAACE,OAAO,OAAO;YAAEP;YAAYI,SAAS;QAAM;QAC1D,MAAMI,UAAU,aAAMH,gBAAAA,QAAS,CAAC,GAAQE,gBAAAA,QAAS,CAAC;YAAIE,aAAalB,MAAMmB,KAAK;;QAC9Eb,YAAYc,MAAM,CAACnB,KAAKgB;QACxB,OAAO;YAAER,YAAY,aAAKA,YAAeQ;YAAiCJ,SAASC,UAAU;QAAK;IACpG;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeO,+BACpBtB,SAAoB,EACpBC,KAAyG;IAEzG,MAAMsB,QAAQ,MAAM1B,sBAAsBG,WAAWC,MAAME,KAAK,EAAE,eAC/DqB,KAAK,CAAC,SAAS,MAAMvB,MAAMwB,KAAK,EAChChB,GAAG;IACN,IAAIiB,QAAQ;IACZ,KAAK,MAAMtB,OAAOmB,MAAMI,IAAI,CAAE;QAC5B,IAAIvB,IAAIwB,EAAE,KAAK3B,MAAM4B,kBAAkB,EAAE;QACzC,IAAI,CAAClC,kCAAkCmC,QAAQ,CAAC1B,IAAIK,GAAG,CAAC,YAAY;QACpE,MAAMsB,UAAU,MAAMhC,mBAAmBC,WAAW;YAClDG,OAAOF,MAAME,KAAK;YAClBE,cAAcD,IAAIwB,EAAE;YACpBZ,OAAO;gBAAEgB,MAAM;gBAAWC,MAAMhC,MAAMmB,KAAK;gBAAEc,QAAQ;gBAAkBC,QAAQlC,MAAMkC,MAAM;YAAC;YAC5Ff,OAAOnB,MAAMmB,KAAK;QACpB;QACA,IAAIW,QAAQjB,OAAO,EAAEY,SAAS;IAChC;IACA,OAAOA;AACT;AAKA;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeU,qBACpBC,IAAmE,EACnEpC,KAQC;;QAWeA,mBACFA,oBAmBQA;IA7BtB,MAAMwB,QAAQhC,sBAAsBQ,MAAMwB,KAAK;IAC/C,IAAI,CAACA,OAAO,OAAO;QAAEa,SAAS;IAAE;IAChC,MAAMtC,YAAYqC,KAAKrC,SAAS;IAChC,MAAMJ,wBAAwBI,WAAW;QACvCG,OAAOF,MAAME,KAAK;QAClBsB;QACAS,QAAQjC,MAAMsC,MAAM;QACpBA,QAAQ;QACRnB,OAAOnB,MAAMmB,KAAK;QAClBf,YAAY,WAAEJ,oBAAAA,MAAMS,UAAU,qBAAhBT,kBAAkB2B,EAAE,mBAAI;QACtCY,UAAU,YAAEvC,qBAAAA,MAAMS,UAAU,qBAAhBT,mBAAkBuC,UAAU,oBAAI;QAC5CL,QAAQlC,MAAMkC,MAAM;IACtB;IACA,KAAK,MAAMM,UAAU,IAAIC,IAAIzC,MAAM0C,OAAO,CAACC,MAAM,CAACC,UAAW;QAC3D,MAAMR,KAAKS,kBAAkB,CAAC;YAAEL;YAAQhB;QAAM;IAChD;IACA,IAAIa,UAAU;IACd,IAAIrC,MAAMS,UAAU,EAAE;QACpB,MAAMqC,MAAM,MAAMhD,mBAAmBC,WAAW;YAC9CG,OAAOF,MAAME,KAAK;YAClBE,cAAcJ,MAAMS,UAAU,CAACkB,EAAE;YACjCZ,OAAO;gBAAEgB,MAAM;gBAAWC,MAAMhC,MAAMmB,KAAK;gBAAEc,QAAQjC,MAAMsC,MAAM;gBAAEJ,QAAQlC,MAAMkC,MAAM;YAAC;YACxFf,OAAOnB,MAAMmB,KAAK;QACpB;QACA,IAAI2B,IAAIjC,OAAO,EAAEwB,WAAW;IAC9B;IACAA,WAAW,MAAMhB,+BAA+BtB,WAAW;QACzDG,OAAOF,MAAME,KAAK;QAClBsB;QACAI,kBAAkB,YAAE5B,qBAAAA,MAAMS,UAAU,qBAAhBT,mBAAkB2B,EAAE,oBAAI;QAC5CR,OAAOnB,MAAMmB,KAAK;QAClBe,QACElC,MAAMsC,MAAM,KAAK,gBACb,wCACA;IACR;IACA,OAAO;QAAED;IAAQ;AACnB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { GmailApiMessage, GmailApiMessagePart } from '../../engine/thread-message';
|
|
18
|
+
import type { GmailClient, GmailMessageMetadata } from '../../transport/gmail-client';
|
|
19
|
+
/**
|
|
20
|
+
* A MAILBOX IN MEMORY, for the runtime's specs (AGL-2981).
|
|
21
|
+
*
|
|
22
|
+
* Implements the whole `GmailClient` the runtime reaches a mailbox through,
|
|
23
|
+
* over messages held here: a send is parsed back out of the raw RFC 5322 it
|
|
24
|
+
* was handed and filed in its thread, and mail "arrives" through
|
|
25
|
+
* {@link FakeGmail.deliver}. The searches the runtime makes are answered with
|
|
26
|
+
* the Gmail operators it uses — `after:`, `-from:me`, `from:`, `to:`,
|
|
27
|
+
* `rfc822msgid:` — so a spec exercises the real queries. Nothing reaches
|
|
28
|
+
* Google and nothing is sent anywhere.
|
|
29
|
+
*/
|
|
30
|
+
interface Stored {
|
|
31
|
+
message: GmailApiMessage;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
internalDateMs: number;
|
|
35
|
+
sent: boolean;
|
|
36
|
+
labelIds: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface FakeGmailOptions {
|
|
39
|
+
/** The mailbox's own addresses: what `from:me` means. */
|
|
40
|
+
self: readonly string[];
|
|
41
|
+
/** Makes the next `sendMessage` throw this. */
|
|
42
|
+
failNextSend?: Error | null;
|
|
43
|
+
}
|
|
44
|
+
export declare class FakeGmail implements GmailClient {
|
|
45
|
+
readonly messages: Map<string, Stored>;
|
|
46
|
+
readonly sent: Array<{
|
|
47
|
+
raw: string;
|
|
48
|
+
threadId: string | null;
|
|
49
|
+
headers: Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
value: string;
|
|
52
|
+
}>;
|
|
53
|
+
body: string;
|
|
54
|
+
}>;
|
|
55
|
+
readonly calls: string[];
|
|
56
|
+
failNextSend: Error | null;
|
|
57
|
+
private readonly self;
|
|
58
|
+
private sequence;
|
|
59
|
+
constructor(options: FakeGmailOptions);
|
|
60
|
+
private nextId;
|
|
61
|
+
/** Files an inbound message, as the mailbox would receive it. */
|
|
62
|
+
deliver(input: {
|
|
63
|
+
threadId?: string;
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
subject: string;
|
|
67
|
+
text?: string;
|
|
68
|
+
atMs: number;
|
|
69
|
+
messageId?: string;
|
|
70
|
+
headers?: Record<string, string>;
|
|
71
|
+
/** A multipart/report's parts, for a delivery status notification. */
|
|
72
|
+
parts?: GmailApiMessagePart[];
|
|
73
|
+
contentType?: string;
|
|
74
|
+
}): GmailApiMessage;
|
|
75
|
+
/** Every message the runtime sent, parsed. */
|
|
76
|
+
sentHeaders(index: number): Record<string, string>;
|
|
77
|
+
private matches;
|
|
78
|
+
getAccessToken(): Promise<string>;
|
|
79
|
+
getProfile(): Promise<{
|
|
80
|
+
emailAddress: string;
|
|
81
|
+
messagesTotal: number;
|
|
82
|
+
threadsTotal: number;
|
|
83
|
+
historyId: string;
|
|
84
|
+
}>;
|
|
85
|
+
listSendAs(): Promise<any[]>;
|
|
86
|
+
sendMessage(input: {
|
|
87
|
+
raw: string;
|
|
88
|
+
threadId?: string | null;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
id: string;
|
|
91
|
+
threadId: string;
|
|
92
|
+
labelIds: string[];
|
|
93
|
+
}>;
|
|
94
|
+
getThread(threadId: string): Promise<{
|
|
95
|
+
id: string;
|
|
96
|
+
historyId: any;
|
|
97
|
+
messages: GmailMessageMetadata[];
|
|
98
|
+
}>;
|
|
99
|
+
getFullThread(threadId: string): Promise<{
|
|
100
|
+
id: string;
|
|
101
|
+
historyId: any;
|
|
102
|
+
messages: GmailApiMessage[];
|
|
103
|
+
}>;
|
|
104
|
+
private metadataOf;
|
|
105
|
+
getMessage(messageId: string): Promise<GmailMessageMetadata>;
|
|
106
|
+
getFullMessage(messageId: string): Promise<GmailApiMessage>;
|
|
107
|
+
listMessages(options: {
|
|
108
|
+
q: string;
|
|
109
|
+
maxResults?: number;
|
|
110
|
+
pageToken?: string | null;
|
|
111
|
+
}): Promise<{
|
|
112
|
+
messages: {
|
|
113
|
+
id: string;
|
|
114
|
+
threadId: string;
|
|
115
|
+
}[];
|
|
116
|
+
nextPageToken: any;
|
|
117
|
+
resultSizeEstimate: number;
|
|
118
|
+
}>;
|
|
119
|
+
revoke(): Promise<'revoked'>;
|
|
120
|
+
}
|
|
121
|
+
export {};
|