@aglyn/plugins-outreach 1.0.0-beta.220 → 1.0.0-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/do-not-contact-domains.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-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 { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { pluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { outreachShortLinkUrl } from '../runtime/click-link'\nimport { createOutreachCurateRoutes } from './curate-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport {\n createOutreachEnrollmentActionRoute,\n createOutreachEnrollmentGatewayRoute,\n} from './enrollment-routes'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport { createOutreachLinkDomainsRoute, type OutreachLinkDomainRouteDeps } from './link-domain-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\nimport { createOutreachStepTestRoute, type OutreachStepTestDeps } from './step-test-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n crmViewEmails: async (input) => (await platform()).crmViewEmails(input),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await (await platform()).creditCampaignSequenceOutcome(\n { hostId, orgId, campaignIds, outcome, atMs },\n firebaseAdmin.app().firestore(),\n )\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n // The workspace's text generator (AGL-3324), resolved per call the same\n // way: the AI plugin registers at boot, and a request before it did is\n // told drafting is unavailable rather than caching a `null`.\n textGenerator: () => pluginTextGenerator()?.generator ?? null,\n }\n}\n\n/** The link domains route's reach (AGL-3306): the tracking-host store, loaded on first use. */\nexport function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRouteDeps {\n return {\n hosts: async () => {\n const loaded = await platform()\n return {\n list: loaded.listTrackingHosts,\n setUp: loaded.setUpTrackingHost,\n verify: loaded.verifyTrackingHost,\n remove: loaded.removeTrackingHost,\n }\n },\n consoleOrigin: canonicalConsoleOrigin,\n }\n}\n\n/**\n * The test send's reach (AGL-3325): the mailbox's Gmail through the same\n * door the runtime and the Mailboxes test use, the platform's rate limit,\n * and the short links a real send would mint.\n */\nexport function defaultOutreachStepTestDeps(): OutreachStepTestDeps {\n return {\n openMailbox: (mailboxId) => openOutreachMailboxClient(firebaseAdmin.app().firestore(), { mailboxId }),\n consumeRateLimit: async (key, options) =>\n (await import('@aglyn/tenant-data-admin/server/rate-limit-store')).consumeRateLimit(key, options),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n clickLinkOrigin: async ({ orgId, senderAddress }) =>\n (await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const curate = createOutreachCurateRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsGateway,\n { web: createOutreachEnrollmentGatewayRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateDraft, { web: curate.draft }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, { web: curate.save }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.stepTest,\n { web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.linkDomains,\n { web: createOutreachLinkDomainsRoute(deps, defaultOutreachLinkDomainDeps()) },\n orgSubject,\n )\n}\n"],"names":["pluginRecordTimelineWriter","pluginTextGenerator","registerPluginApiRoute","firebaseAdmin","platformMailDnsResolver","OUTREACH_API_ROUTES","openOutreachMailboxClient","outreachOrgSubject","outreachShortLinkUrl","createOutreachCurateRoutes","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachEnrollmentGatewayRoute","canonicalConsoleOrigin","createOutreachLinkDomainsRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","createOutreachStepTestRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","crmViewEmails","input","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","creditCampaignSequenceOutcome","timeline","writer","resolveMx","domain","resolveAddress","Promise","resolve","textGenerator","generator","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","defaultOutreachStepTestDeps","openMailbox","mailboxId","consumeRateLimit","clickLinkUrl","linkId","trackingOrigin","origin","clickLinkOrigin","senderAddress","resolveTrackingLinkOrigin","registerOutreachRoutes","deps","sequences","enroll","curate","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","enrollmentsGateway","curateDraft","draft","curateSave","stepTest","doNotContactDomains","linkDomains"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/register-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 { creditConversionOutcome } from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { pluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { pluginTextGenerator } from '@aglyn/aglyn/plugin-manager/plugin-text-generation'\nimport { registerPluginApiRoute } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { platformMailDnsResolver } from '@aglyn/tenant-data-admin/server/email-deliverability'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport { openOutreachMailboxClient } from '../mailboxes/mailbox-transport'\nimport { outreachOrgSubject } from '../mailboxes/register-mailbox-routes'\nimport { outreachShortLinkUrl } from '../runtime/click-link'\nimport { createOutreachCurateRoutes } from './curate-routes'\nimport { createOutreachDoNotContactDomainsRoute } from './do-not-contact-routes'\nimport { createOutreachEnrollRoutes, type OutreachEnrollRouteDeps } from './enroll-routes'\nimport {\n createOutreachEnrollmentActionRoute,\n createOutreachEnrollmentGatewayRoute,\n} from './enrollment-routes'\nimport { canonicalConsoleOrigin } from '../mailboxes/oauth-redirect'\nimport { createOutreachLinkDomainsRoute, type OutreachLinkDomainRouteDeps } from './link-domain-routes'\nimport { createOutreachPreviewRoute } from './preview-routes'\nimport type { OutreachRouteGateDeps } from './route-gate'\nimport { createOutreachSequenceRoutes } from './sequence-routes'\nimport { createOutreachSettingsRoute } from './settings-routes'\nimport { createOutreachStepTestRoute, type OutreachStepTestDeps } from './step-test-routes'\n\n/**\n * Wires the settings, sequence and enrollment routes into the console\n * dispatcher (AGL-2980) with the platform's own dependencies. Specs build\n * their own.\n *\n * Every one names its organization (`orgId`, in the query or the JSON body)\n * and none names a site, so each declares the mailbox routes' subject reader:\n * the dispatcher's release gate then asks about that organization, and a\n * `release_outreach` override for one organization reaches these routes as it\n * reaches Mailboxes. Without it the gate reads the request as anonymous, which\n * only a fully enabled flag or a staff session passes.\n */\n\n/** The platform's heavier modules, loaded the first time a request needs one. */\nconst platform = () => import('./platform-deps')\n\n/** The gate's reach into the platform. */\nexport function defaultOutreachRouteGateDeps(): OutreachRouteGateDeps {\n return {\n verifyIdToken: (idToken) => firebaseAdmin.app().auth().verifyIdToken(idToken),\n resolveOrgPermissions: async (uid, context) => (await platform()).resolveOrgPermissions(uid, context),\n holdsOrgCatalogPermission: async (uid, orgId, key) =>\n (await platform()).holdsOrgCatalogPermission(uid, orgId, key),\n readOrg: async (orgId) => {\n const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).get()\n return snapshot.exists ? (snapshot.data() ?? {}) : null\n },\n lockdownRefusal: async (options) => (await platform()).lockdownRefusal(options),\n }\n}\n\nexport function defaultOutreachRouteDeps(): OutreachEnrollRouteDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n gate: defaultOutreachRouteGateDeps(),\n now: Date.now,\n random: Math.random,\n logOrgActivity: async (orgId, actor, action, target) =>\n (await platform()).logOrgActivity(orgId, actor, action, target),\n stampRecordEmailState: async (stamp) => {\n await (await platform()).stampRecordEmailState(stamp)\n },\n // Through the plugin that credits outcomes (`plugin-conversion-credit`),\n // which never throws and answers nothing where no plugin credits them.\n creditCampaign: async ({ hostId, orgId, campaignIds, outcome, atMs }) => {\n await creditConversionOutcome({ hostId, orgId, containerIds: campaignIds, outcome, atMs })\n },\n // The record system's writer (AGL-3274), resolved per call the way the\n // runtime resolves it: the CRM registers at boot, and a request that\n // arrives before it did files nothing rather than caching a `null`.\n timeline: () => pluginRecordTimelineWriter()?.writer ?? null,\n // The domain's MX (AGL-3326) and, for a domain with none, its address\n // record — through the platform's pinned resolver (AGL-3328), which\n // asks twice before it says a domain takes no mail.\n resolveMx: (domain) => platformMailDnsResolver().resolveMx(domain),\n resolveAddress: (domain) => platformMailDnsResolver().resolveAddress?.(domain) ?? Promise.resolve(false),\n // The workspace's text generator (AGL-3324), resolved per call the same\n // way: the AI plugin registers at boot, and a request before it did is\n // told drafting is unavailable rather than caching a `null`.\n textGenerator: () => pluginTextGenerator()?.generator ?? null,\n }\n}\n\n/** The link domains route's reach (AGL-3306): the tracking-host store, loaded on first use. */\nexport function defaultOutreachLinkDomainDeps(): OutreachLinkDomainRouteDeps {\n return {\n hosts: async () => {\n const loaded = await platform()\n return {\n list: loaded.listTrackingHosts,\n setUp: loaded.setUpTrackingHost,\n verify: loaded.verifyTrackingHost,\n remove: loaded.removeTrackingHost,\n }\n },\n consoleOrigin: canonicalConsoleOrigin,\n }\n}\n\n/**\n * The test send's reach (AGL-3325): the mailbox's Gmail through the same\n * door the runtime and the Mailboxes test use, the platform's rate limit,\n * and the short links a real send would mint.\n */\nexport function defaultOutreachStepTestDeps(): OutreachStepTestDeps {\n return {\n openMailbox: (mailboxId) => openOutreachMailboxClient(firebaseAdmin.app().firestore(), { mailboxId }),\n consumeRateLimit: async (key, options) =>\n (await import('@aglyn/tenant-data-admin/server/rate-limit-store')).consumeRateLimit(key, options),\n clickLinkUrl: (linkId, trackingOrigin) =>\n outreachShortLinkUrl({ origin: canonicalConsoleOrigin(), linkId, trackingOrigin }),\n clickLinkOrigin: async ({ orgId, senderAddress }) =>\n (await platform()).resolveTrackingLinkOrigin(firebaseAdmin.app().firestore(), orgId, senderAddress),\n }\n}\n\nexport function registerOutreachRoutes(\n deps: OutreachEnrollRouteDeps = defaultOutreachRouteDeps(),\n): void {\n const sequences = createOutreachSequenceRoutes(deps)\n const enroll = createOutreachEnrollRoutes(deps)\n const curate = createOutreachCurateRoutes(deps)\n const orgSubject = { subject: outreachOrgSubject }\n registerPluginApiRoute(OUTREACH_API_ROUTES.settings, { web: createOutreachSettingsRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesSave, { web: sequences.save }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesStatus, { web: sequences.status }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.sequencesDelete, { web: sequences.remove }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enrollPreview, { web: enroll.preview }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.enroll, { web: enroll.confirm }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsAction,\n { web: createOutreachEnrollmentActionRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.enrollmentsGateway,\n { web: createOutreachEnrollmentGatewayRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(OUTREACH_API_ROUTES.preview, { web: createOutreachPreviewRoute(deps) }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateDraft, { web: curate.draft }, orgSubject)\n registerPluginApiRoute(OUTREACH_API_ROUTES.curateSave, { web: curate.save }, orgSubject)\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.stepTest,\n { web: createOutreachStepTestRoute(deps, defaultOutreachStepTestDeps()) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.doNotContactDomains,\n { web: createOutreachDoNotContactDomainsRoute(deps) },\n orgSubject,\n )\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.linkDomains,\n { web: createOutreachLinkDomainsRoute(deps, defaultOutreachLinkDomainDeps()) },\n orgSubject,\n )\n}\n"],"names":["creditConversionOutcome","pluginRecordTimelineWriter","pluginTextGenerator","registerPluginApiRoute","firebaseAdmin","platformMailDnsResolver","OUTREACH_API_ROUTES","openOutreachMailboxClient","outreachOrgSubject","outreachShortLinkUrl","createOutreachCurateRoutes","createOutreachDoNotContactDomainsRoute","createOutreachEnrollRoutes","createOutreachEnrollmentActionRoute","createOutreachEnrollmentGatewayRoute","canonicalConsoleOrigin","createOutreachLinkDomainsRoute","createOutreachPreviewRoute","createOutreachSequenceRoutes","createOutreachSettingsRoute","createOutreachStepTestRoute","platform","defaultOutreachRouteGateDeps","verifyIdToken","idToken","app","auth","resolveOrgPermissions","uid","context","holdsOrgCatalogPermission","orgId","key","readOrg","snapshot","firestore","collection","doc","get","exists","data","lockdownRefusal","options","defaultOutreachRouteDeps","gate","now","Date","random","Math","logOrgActivity","actor","action","target","stampRecordEmailState","stamp","creditCampaign","hostId","campaignIds","outcome","atMs","containerIds","timeline","writer","resolveMx","domain","resolveAddress","Promise","resolve","textGenerator","generator","defaultOutreachLinkDomainDeps","hosts","loaded","list","listTrackingHosts","setUp","setUpTrackingHost","verify","verifyTrackingHost","remove","removeTrackingHost","consoleOrigin","defaultOutreachStepTestDeps","openMailbox","mailboxId","consumeRateLimit","clickLinkUrl","linkId","trackingOrigin","origin","clickLinkOrigin","senderAddress","resolveTrackingLinkOrigin","registerOutreachRoutes","deps","sequences","enroll","curate","orgSubject","subject","settings","web","sequencesSave","save","sequencesStatus","status","sequencesDelete","enrollPreview","preview","confirm","enrollmentsAction","enrollmentsGateway","curateDraft","draft","curateSave","stepTest","doNotContactDomains","linkDomains"],"mappings":"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"}
|
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
-
import { isEmailVerified, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
16
|
+
*/ import { isEmailVerified, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
17
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
19
|
-
import {
|
|
18
|
+
import { outreachEntitlementRefusal, outreachMembershipRefusal } from "../engine/outreach-access.js";
|
|
20
19
|
import { outreachRefusal, readOutreachDocumentId } from "./route-http.js";
|
|
21
20
|
function bearer(request) {
|
|
22
21
|
var _request_headers_get;
|
|
@@ -46,26 +45,16 @@ export async function outreachRouteGate(request, rawOrgId, deps, extra = []) {
|
|
|
46
45
|
const membership = await deps.resolveOrgPermissions(decoded.uid, {
|
|
47
46
|
orgId
|
|
48
47
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (!membership.role || membership.orgId !== orgId) {
|
|
52
|
-
return outreachRefusal(403, 'not-a-member', 'You are not a member of that organization.');
|
|
53
|
-
}
|
|
54
|
-
if (!membership.orgWide) {
|
|
55
|
-
return outreachRefusal(403, 'not-org-wide', 'Sequences covers the whole organization, and your access is to particular sites.');
|
|
56
|
-
}
|
|
57
|
-
if (membership.permissions[OUTREACH_USE_PERMISSION] !== true) {
|
|
58
|
-
return outreachRefusal(403, 'permission', 'Your role does not include Use Sequences.');
|
|
59
|
-
}
|
|
48
|
+
const refused = outreachMembershipRefusal(membership, orgId);
|
|
49
|
+
if (refused) return outreachRefusal(refused.status, refused.code, refused.message);
|
|
60
50
|
for (const permission of extra){
|
|
61
51
|
if (!await deps.holdsOrgCatalogPermission(decoded.uid, orgId, permission.key)) {
|
|
62
52
|
return outreachRefusal(403, 'permission', permission.refusal);
|
|
63
53
|
}
|
|
64
54
|
}
|
|
65
55
|
const org = (_ref = await deps.readOrg(orgId)) != null ? _ref : {};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
56
|
+
const unentitled = outreachEntitlementRefusal(org);
|
|
57
|
+
if (unentitled) return outreachRefusal(unentitled.status, unentitled.code, unentitled.message);
|
|
69
58
|
const staff = decoded['staff'] === true;
|
|
70
59
|
const locked = await deps.lockdownRefusal({
|
|
71
60
|
request,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-gate.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 {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/route-gate.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 {\n isEmailVerified,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { outreachEntitlementRefusal, outreachMembershipRefusal } from '../engine/outreach-access'\nimport { outreachRefusal, readOutreachDocumentId } from './route-http'\n\n/**\n * THE GATE EVERY SETTINGS, SEQUENCE AND ENROLLMENT ROUTE CLIMBS (AGL-2980).\n *\n * The console dispatcher has already refused a request while\n * `release_outreach` is off for it, an unverified account's token, a\n * lockdown with no org in scope, and a caller over the console rate limit —\n * every route here is registered under the `outreach/` prefix, which is what\n * puts it behind those. What the dispatcher cannot know is anything about\n * the member, so each rung below is this plugin's:\n *\n * 401 no bearer token, or one the verifier refused\n * 403 the account's address is unverified (an impersonation session is exempt)\n * 400 the request did not name an organization\n * 403 not a member of it, a site collaborator without org-wide reach, or\n * without `outreach.use` — or without a permission the route adds\n * 403 the organization does not hold the `outreach` entitlement\n * 423 lockdown, now with the organization and the account in scope\n *\n * The reach is org-wide for the reason the rules give: a sequence enrolls\n * people from every site of the organization, so none of it is a fact\n * about the one site a collaborator was invited to.\n *\n * `resolveOrgPermissions` is always given the concrete org id. Called\n * without one it resolves a caller with no org at all as that future org's\n * owner, which is right for a fresh signup and wrong for every question\n * asked here.\n */\n\nexport interface OutreachRouteGateDeps {\n verifyIdToken(idToken: string): Promise<DecodedIdToken>\n resolveOrgPermissions(\n uid: string,\n context: { orgId: string },\n ): Promise<{\n orgId: string | null\n /** `null` when the account is not on the organization's roster. */\n role: string | null\n isOwner: boolean\n orgWide: boolean\n permissions: Partial<Record<string, boolean | undefined>>\n }>\n /**\n * Whether the member holds a CATALOG permission (`data.manage`) as the\n * org's roles resolve it. Asked separately because the map\n * `resolveOrgPermissions` answers carries the legacy keys and the plugins'\n * own, never a catalog key: read there, a catalog key refuses everyone.\n */\n holdsOrgCatalogPermission(uid: string, orgId: string, key: string): Promise<boolean>\n readOrg(orgId: string): Promise<Record<string, unknown> | null>\n lockdownRefusal(options: {\n request: Request\n staff: boolean\n uid: string\n org: Record<string, unknown>\n }): Promise<Response | null>\n}\n\n/** Who is asking, once the gate let them through. */\nexport interface OutreachRouteCaller {\n uid: string\n /** The account's sign-in address, lower-cased, when the token carries one. */\n email: string | null\n staff: boolean\n orgId: string\n /** The organization document the entitlement was read from. */\n org: Record<string, unknown>\n /** An owner or admin of the organization, who may send from any member's mailbox. */\n isOrgAdmin: boolean\n}\n\n/**\n * A catalog permission a route asks for beyond `outreach.use`, with its\n * refusal — `data.manage` for a route that reads the CRM's people, which\n * the contacts' own rules gate on.\n */\nexport interface OutreachRouteExtraPermission {\n key: string\n /** Why the route needs it, as the refusal says it. */\n refusal: string\n}\n\nfunction bearer(request: Request): string | null {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : null\n}\n\nexport async function outreachRouteGate(\n request: Request,\n rawOrgId: unknown,\n deps: OutreachRouteGateDeps,\n extra: readonly OutreachRouteExtraPermission[] = [],\n): Promise<Response | OutreachRouteCaller> {\n const token = bearer(request)\n if (!token) return outreachRefusal(401, 'unauthenticated', 'Sign in to use Sequences.')\n let decoded: DecodedIdToken\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // A refused credential is the caller's; an Auth outage is ours and\n // propagates, so it answers 500 instead of \"sign in again\".\n if (invalidIdTokenResponse(error)) {\n return outreachRefusal(401, 'unauthenticated', 'Your sign-in could not be confirmed. Sign in again.')\n }\n throw error\n }\n if (!isEmailVerified(decoded) && !isImpersonationSession(decoded)) {\n return outreachRefusal(403, 'email-unverified', 'Verify your email address before using Sequences.')\n }\n\n const orgId = readOutreachDocumentId(rawOrgId)\n if (!orgId) return outreachRefusal(400, 'org-required', 'Open an organization before using Sequences.')\n\n const membership = await deps.resolveOrgPermissions(decoded.uid, { orgId })\n const refused = outreachMembershipRefusal(membership, orgId)\n if (refused) return outreachRefusal(refused.status, refused.code, refused.message)\n for (const permission of extra) {\n if (!(await deps.holdsOrgCatalogPermission(decoded.uid, orgId, permission.key))) {\n return outreachRefusal(403, 'permission', permission.refusal)\n }\n }\n\n const org = (await deps.readOrg(orgId)) ?? {}\n const unentitled = outreachEntitlementRefusal(org)\n if (unentitled) return outreachRefusal(unentitled.status, unentitled.code, unentitled.message)\n const staff = decoded['staff'] === true\n const locked = await deps.lockdownRefusal({ request, staff, uid: decoded.uid, org })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email.trim().toLowerCase() : null,\n staff,\n orgId,\n org,\n isOrgAdmin: membership.isOwner,\n }\n}\n"],"names":["isEmailVerified","isImpersonationSession","invalidIdTokenResponse","outreachEntitlementRefusal","outreachMembershipRefusal","outreachRefusal","readOutreachDocumentId","bearer","request","authorization","headers","get","startsWith","slice","length","outreachRouteGate","rawOrgId","deps","extra","token","decoded","verifyIdToken","error","orgId","membership","resolveOrgPermissions","uid","refused","status","code","message","permission","holdsOrgCatalogPermission","key","refusal","org","readOrg","unentitled","staff","locked","lockdownRefusal","email","trim","toLowerCase","isOrgAdmin","isOwner"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,eAAe,EACfC,sBAAsB,QACjB,iDAAgD;AACvD,SAASC,sBAAsB,QAAQ,mDAAkD;AAEzF,SAASC,0BAA0B,EAAEC,yBAAyB,QAAQ,+BAA2B;AACjG,SAASC,eAAe,EAAEC,sBAAsB,QAAQ,kBAAc;AAmFtE,SAASC,OAAOC,OAAgB;QACRA;IAAtB,MAAMC,iBAAgBD,uBAAAA,QAAQE,OAAO,CAACC,GAAG,CAAC,4BAApBH,uBAAwC;IAC9D,OAAOC,cAAcG,UAAU,CAAC,aAAaH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IAAI;AACvF;AAEA,OAAO,eAAeC,kBACpBP,OAAgB,EAChBQ,QAAiB,EACjBC,IAA2B,EAC3BC,QAAiD,EAAE;QA+BtC;IA7Bb,MAAMC,QAAQZ,OAAOC;IACrB,IAAI,CAACW,OAAO,OAAOd,gBAAgB,KAAK,mBAAmB;IAC3D,IAAIe;IACJ,IAAI;QACFA,UAAU,MAAMH,KAAKI,aAAa,CAACF;IACrC,EAAE,OAAOG,OAAO;QACd,mEAAmE;QACnE,4DAA4D;QAC5D,IAAIpB,uBAAuBoB,QAAQ;YACjC,OAAOjB,gBAAgB,KAAK,mBAAmB;QACjD;QACA,MAAMiB;IACR;IACA,IAAI,CAACtB,gBAAgBoB,YAAY,CAACnB,uBAAuBmB,UAAU;QACjE,OAAOf,gBAAgB,KAAK,oBAAoB;IAClD;IAEA,MAAMkB,QAAQjB,uBAAuBU;IACrC,IAAI,CAACO,OAAO,OAAOlB,gBAAgB,KAAK,gBAAgB;IAExD,MAAMmB,aAAa,MAAMP,KAAKQ,qBAAqB,CAACL,QAAQM,GAAG,EAAE;QAAEH;IAAM;IACzE,MAAMI,UAAUvB,0BAA0BoB,YAAYD;IACtD,IAAII,SAAS,OAAOtB,gBAAgBsB,QAAQC,MAAM,EAAED,QAAQE,IAAI,EAAEF,QAAQG,OAAO;IACjF,KAAK,MAAMC,cAAcb,MAAO;QAC9B,IAAI,CAAE,MAAMD,KAAKe,yBAAyB,CAACZ,QAAQM,GAAG,EAAEH,OAAOQ,WAAWE,GAAG,GAAI;YAC/E,OAAO5B,gBAAgB,KAAK,cAAc0B,WAAWG,OAAO;QAC9D;IACF;IAEA,MAAMC,OAAO,OAAA,MAAMlB,KAAKmB,OAAO,CAACb,kBAAnB,OAA8B,CAAC;IAC5C,MAAMc,aAAalC,2BAA2BgC;IAC9C,IAAIE,YAAY,OAAOhC,gBAAgBgC,WAAWT,MAAM,EAAES,WAAWR,IAAI,EAAEQ,WAAWP,OAAO;IAC7F,MAAMQ,QAAQlB,OAAO,CAAC,QAAQ,KAAK;IACnC,MAAMmB,SAAS,MAAMtB,KAAKuB,eAAe,CAAC;QAAEhC;QAAS8B;QAAOZ,KAAKN,QAAQM,GAAG;QAAES;IAAI;IAClF,IAAII,QAAQ,OAAOA;IAEnB,OAAO;QACLb,KAAKN,QAAQM,GAAG;QAChBe,OAAO,OAAOrB,QAAQqB,KAAK,KAAK,WAAWrB,QAAQqB,KAAK,CAACC,IAAI,GAAGC,WAAW,KAAK;QAChFL;QACAf;QACAY;QACAS,YAAYpB,WAAWqB,OAAO;IAChC;AACF"}
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { PluginWebApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
import type { OutreachSequence } from '../model/outreach.types';
|
|
19
|
+
import { type OutreachSequenceDraft, type OutreachSequenceIssue } from '../model/sequence-draft';
|
|
18
20
|
import { type OutreachRouteDeps } from './route-deps';
|
|
21
|
+
import { type OutreachRouteCaller } from './route-gate';
|
|
22
|
+
import type { OutreachRouteRefusalReason } from '../model/outreach-api';
|
|
19
23
|
/** The activity line each lifecycle act writes. */
|
|
20
24
|
export declare const OUTREACH_SEQUENCE_ACTIVITY: {
|
|
21
25
|
readonly create: "Created a sequence";
|
|
@@ -29,4 +33,46 @@ export interface OutreachSequenceRoutes {
|
|
|
29
33
|
status: PluginWebApiHandler;
|
|
30
34
|
remove: PluginWebApiHandler;
|
|
31
35
|
}
|
|
36
|
+
/** What saving a sequence came to: the stored sequence, or the refusal the route answers with. */
|
|
37
|
+
export type OutreachSequenceSaveOutcome = {
|
|
38
|
+
ok: true;
|
|
39
|
+
sequence: OutreachSequence;
|
|
40
|
+
created: boolean;
|
|
41
|
+
warnings: OutreachSequenceIssue[];
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
status: number;
|
|
45
|
+
code: OutreachRouteRefusalReason;
|
|
46
|
+
message: string;
|
|
47
|
+
issues?: OutreachSequenceIssue[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Saves a sequence the way the editor's Save does (see "Saving" above):
|
|
51
|
+
* the editor's validator, the organization's placement rules and — once
|
|
52
|
+
* anyone is enrolled — the fixed steps, site and mailbox. A new sequence is
|
|
53
|
+
* a draft; an edit keeps its status. The save route and the package import
|
|
54
|
+
* (AGL-3535) both write through here. `newId` names a new sequence's id
|
|
55
|
+
* (an import keeps the package's); `judgeOnly` answers the verdict without
|
|
56
|
+
* writing, for an import's dry run.
|
|
57
|
+
*/
|
|
58
|
+
export declare function saveOutreachSequence(deps: Pick<OutreachRouteDeps, 'firestore' | 'now' | 'logOrgActivity'>, caller: OutreachRouteCaller, input: {
|
|
59
|
+
sequenceId: string | null;
|
|
60
|
+
draft: OutreachSequenceDraft;
|
|
61
|
+
newId?: string;
|
|
62
|
+
judgeOnly?: boolean;
|
|
63
|
+
}): Promise<OutreachSequenceSaveOutcome>;
|
|
64
|
+
/**
|
|
65
|
+
* Deletes a draft nobody was ever enrolled in — the only sequence that may
|
|
66
|
+
* be deleted: what people were sent, and why they stopped, is the record of
|
|
67
|
+
* what the organization did. The delete route and a package import's undo
|
|
68
|
+
* (AGL-3535) both delete through here.
|
|
69
|
+
*/
|
|
70
|
+
export declare function removeOutreachSequence(deps: Pick<OutreachRouteDeps, 'firestore' | 'logOrgActivity'>, caller: Pick<OutreachRouteCaller, 'orgId' | 'uid' | 'email'>, sequenceId: string): Promise<{
|
|
71
|
+
ok: true;
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
status: number;
|
|
75
|
+
code: OutreachRouteRefusalReason;
|
|
76
|
+
message: string;
|
|
77
|
+
}>;
|
|
32
78
|
export declare function createOutreachSequenceRoutes(deps: OutreachRouteDeps): OutreachSequenceRoutes;
|
|
@@ -194,6 +194,148 @@ draft, options = {}) {
|
|
|
194
194
|
return true;
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Saves a sequence the way the editor's Save does (see "Saving" above):
|
|
199
|
+
* the editor's validator, the organization's placement rules and — once
|
|
200
|
+
* anyone is enrolled — the fixed steps, site and mailbox. A new sequence is
|
|
201
|
+
* a draft; an edit keeps its status. The save route and the package import
|
|
202
|
+
* (AGL-3535) both write through here. `newId` names a new sequence's id
|
|
203
|
+
* (an import keeps the package's); `judgeOnly` answers the verdict without
|
|
204
|
+
* writing, for an import's dry run.
|
|
205
|
+
*/ export async function saveOutreachSequence(deps, caller, input) {
|
|
206
|
+
var _ref, _ref1, _ref2;
|
|
207
|
+
const firestore = deps.firestore();
|
|
208
|
+
const { sequenceId, draft } = input;
|
|
209
|
+
const existing = sequenceId ? await loadSequence(firestore, caller.orgId, sequenceId) : null;
|
|
210
|
+
if (sequenceId && !existing && !input.newId) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
status: 404,
|
|
214
|
+
code: 'sequence-not-found',
|
|
215
|
+
message: 'That sequence no longer exists.'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if ((existing == null ? void 0 : existing.status) === 'archived') {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
status: 409,
|
|
222
|
+
code: 'sequence-archived',
|
|
223
|
+
message: "An archived sequence can't be edited."
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// A sequence sends as its mailbox's member, so it is theirs (or an
|
|
227
|
+
// admin's) to change — the stored mailbox as well as a new one: moving
|
|
228
|
+
// a colleague's sequence onto your own mailbox is still changing theirs.
|
|
229
|
+
if ((existing == null ? void 0 : existing.mailboxId) && existing.mailboxId !== draft.mailboxId) {
|
|
230
|
+
const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId));
|
|
231
|
+
const refused = stored.find((entry)=>entry.code === 'mailbox_not_yours');
|
|
232
|
+
if (refused) return {
|
|
233
|
+
ok: false,
|
|
234
|
+
status: 403,
|
|
235
|
+
code: 'permission',
|
|
236
|
+
message: refused.message
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const judged = [
|
|
240
|
+
...validateOutreachSequence(draft),
|
|
241
|
+
...await draftPlacementIssues(firestore, caller, draft)
|
|
242
|
+
];
|
|
243
|
+
const ownership = judged.find((entry)=>entry.code === 'mailbox_not_yours');
|
|
244
|
+
if (ownership) return {
|
|
245
|
+
ok: false,
|
|
246
|
+
status: 403,
|
|
247
|
+
code: 'permission',
|
|
248
|
+
message: ownership.message
|
|
249
|
+
};
|
|
250
|
+
if (existing && await hasEnrollments(firestore, caller.orgId, existing.id)) {
|
|
251
|
+
judged.push(...outreachEnrolledSequenceIssues(existing, draft));
|
|
252
|
+
}
|
|
253
|
+
const errors = judged.filter((entry)=>entry.severity === 'error');
|
|
254
|
+
if (errors.length) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 400,
|
|
258
|
+
code: 'invalid-sequence',
|
|
259
|
+
message: errors[0].message,
|
|
260
|
+
issues: judged
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const nowMs = deps.now();
|
|
264
|
+
const ref = outreachOrgCollection(firestore, caller.orgId, 'sequences').doc((_ref = (_ref1 = existing == null ? void 0 : existing.id) != null ? _ref1 : input.newId) != null ? _ref : createResourceUid());
|
|
265
|
+
const sequence = _extends({
|
|
266
|
+
id: ref.id
|
|
267
|
+
}, draft, {
|
|
268
|
+
status: (_ref2 = existing == null ? void 0 : existing.status) != null ? _ref2 : 'draft'
|
|
269
|
+
}, (existing == null ? void 0 : existing.stats) ? {
|
|
270
|
+
stats: existing.stats
|
|
271
|
+
} : {}, {
|
|
272
|
+
createdAtMs: (existing == null ? void 0 : existing.createdAtMs) || nowMs,
|
|
273
|
+
updatedAtMs: nowMs
|
|
274
|
+
});
|
|
275
|
+
const warnings = judged.filter((entry)=>entry.severity === 'warning');
|
|
276
|
+
if (input.judgeOnly) return {
|
|
277
|
+
ok: true,
|
|
278
|
+
sequence,
|
|
279
|
+
created: !existing,
|
|
280
|
+
warnings
|
|
281
|
+
};
|
|
282
|
+
/*
|
|
283
|
+
* The list's search fields ride with every write of the name (AGL-3321):
|
|
284
|
+
* the sequences list asks Firestore for them, and a sequence missing
|
|
285
|
+
* them would list normally and never be found.
|
|
286
|
+
*/ await ref.set(_extends({}, sequence, nameSearchFields(sequence.name)));
|
|
287
|
+
if (!existing) {
|
|
288
|
+
await deps.logOrgActivity(caller.orgId, {
|
|
289
|
+
uid: caller.uid,
|
|
290
|
+
email: caller.email
|
|
291
|
+
}, OUTREACH_SEQUENCE_ACTIVITY.create, {
|
|
292
|
+
type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,
|
|
293
|
+
id: sequence.id,
|
|
294
|
+
name: sequence.name
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
sequence,
|
|
300
|
+
created: !existing,
|
|
301
|
+
warnings
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Deletes a draft nobody was ever enrolled in — the only sequence that may
|
|
306
|
+
* be deleted: what people were sent, and why they stopped, is the record of
|
|
307
|
+
* what the organization did. The delete route and a package import's undo
|
|
308
|
+
* (AGL-3535) both delete through here.
|
|
309
|
+
*/ export async function removeOutreachSequence(deps, caller, sequenceId) {
|
|
310
|
+
const firestore = deps.firestore();
|
|
311
|
+
const sequence = await loadSequence(firestore, caller.orgId, sequenceId);
|
|
312
|
+
if (!sequence) return {
|
|
313
|
+
ok: false,
|
|
314
|
+
status: 404,
|
|
315
|
+
code: 'sequence-not-found',
|
|
316
|
+
message: 'That sequence no longer exists.'
|
|
317
|
+
};
|
|
318
|
+
if (sequence.status !== 'draft' || await hasEnrollments(firestore, caller.orgId, sequence.id)) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
status: 409,
|
|
322
|
+
code: 'delete-refused',
|
|
323
|
+
message: 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete();
|
|
327
|
+
await deps.logOrgActivity(caller.orgId, {
|
|
328
|
+
uid: caller.uid,
|
|
329
|
+
email: caller.email
|
|
330
|
+
}, OUTREACH_SEQUENCE_ACTIVITY.delete, {
|
|
331
|
+
type: OUTREACH_SEQUENCE_ACTIVITY_TARGET,
|
|
332
|
+
id: sequence.id,
|
|
333
|
+
name: sequence.name
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
ok: true
|
|
337
|
+
};
|
|
338
|
+
}
|
|
197
339
|
export function createOutreachSequenceRoutes(deps) {
|
|
198
340
|
const activity = (caller, action, sequence)=>deps.logOrgActivity(caller.orgId, {
|
|
199
341
|
uid: caller.uid,
|
|
@@ -204,69 +346,27 @@ export function createOutreachSequenceRoutes(deps) {
|
|
|
204
346
|
name: sequence.name
|
|
205
347
|
});
|
|
206
348
|
const save = async (request)=>{
|
|
207
|
-
var _ref;
|
|
208
349
|
if (request.method !== 'POST') return outreachMethodNotAllowed('POST');
|
|
209
350
|
const body = await readOutreachJsonBody(request);
|
|
210
351
|
const caller = await outreachRouteGate(request, body['orgId'], deps.gate);
|
|
211
352
|
if (caller instanceof Response) return caller;
|
|
212
|
-
const firestore = deps.firestore();
|
|
213
353
|
const rawId = body['sequenceId'];
|
|
214
354
|
const sequenceId = rawId === undefined || rawId === null || rawId === '' ? null : readOutreachDocumentId(rawId);
|
|
215
355
|
if (rawId && !sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to save.');
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
if ((existing == null ? void 0 : existing.status) === 'archived') {
|
|
221
|
-
return outreachRefusal(409, 'sequence-archived', "An archived sequence can't be edited.");
|
|
222
|
-
}
|
|
223
|
-
const draft = readOutreachSequenceDraft(body['sequence']);
|
|
224
|
-
// A sequence sends as its mailbox's member, so it is theirs (or an
|
|
225
|
-
// admin's) to change — the stored mailbox as well as a new one: moving
|
|
226
|
-
// a colleague's sequence onto your own mailbox is still changing theirs.
|
|
227
|
-
if ((existing == null ? void 0 : existing.mailboxId) && existing.mailboxId !== draft.mailboxId) {
|
|
228
|
-
const stored = mailboxIssues(caller, await loadMailbox(firestore, caller.orgId, existing.mailboxId));
|
|
229
|
-
const refused = stored.find((entry)=>entry.code === 'mailbox_not_yours');
|
|
230
|
-
if (refused) return outreachRefusal(403, 'permission', refused.message);
|
|
231
|
-
}
|
|
232
|
-
const judged = [
|
|
233
|
-
...validateOutreachSequence(draft),
|
|
234
|
-
...await draftPlacementIssues(firestore, caller, draft)
|
|
235
|
-
];
|
|
236
|
-
const ownership = judged.find((entry)=>entry.code === 'mailbox_not_yours');
|
|
237
|
-
if (ownership) return outreachRefusal(403, 'permission', ownership.message);
|
|
238
|
-
if (existing && await hasEnrollments(firestore, caller.orgId, existing.id)) {
|
|
239
|
-
judged.push(...outreachEnrolledSequenceIssues(existing, draft));
|
|
240
|
-
}
|
|
241
|
-
const errors = judged.filter((entry)=>entry.severity === 'error');
|
|
242
|
-
if (errors.length) {
|
|
243
|
-
return outreachRefusal(400, 'invalid-sequence', errors[0].message, {
|
|
244
|
-
issues: judged
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
const nowMs = deps.now();
|
|
248
|
-
const ref = existing ? outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(existing.id) : outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(createResourceUid());
|
|
249
|
-
const sequence = _extends({
|
|
250
|
-
id: ref.id
|
|
251
|
-
}, draft, {
|
|
252
|
-
status: (_ref = existing == null ? void 0 : existing.status) != null ? _ref : 'draft'
|
|
253
|
-
}, (existing == null ? void 0 : existing.stats) ? {
|
|
254
|
-
stats: existing.stats
|
|
255
|
-
} : {}, {
|
|
256
|
-
createdAtMs: (existing == null ? void 0 : existing.createdAtMs) || nowMs,
|
|
257
|
-
updatedAtMs: nowMs
|
|
356
|
+
const outcome = await saveOutreachSequence(deps, caller, {
|
|
357
|
+
sequenceId,
|
|
358
|
+
draft: readOutreachSequenceDraft(body['sequence'])
|
|
258
359
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (!existing) await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.create, sequence);
|
|
360
|
+
if (outcome.ok === false) {
|
|
361
|
+
return outreachRefusal(outcome.status, outcome.code, outcome.message, outcome.issues ? {
|
|
362
|
+
issues: outcome.issues
|
|
363
|
+
} : undefined);
|
|
364
|
+
}
|
|
265
365
|
return outreachOk({
|
|
266
366
|
ok: true,
|
|
267
|
-
sequence,
|
|
268
|
-
created:
|
|
269
|
-
warnings:
|
|
367
|
+
sequence: outcome.sequence,
|
|
368
|
+
created: outcome.created,
|
|
369
|
+
warnings: outcome.warnings
|
|
270
370
|
});
|
|
271
371
|
};
|
|
272
372
|
const status = async (request)=>{
|
|
@@ -332,16 +432,8 @@ export function createOutreachSequenceRoutes(deps) {
|
|
|
332
432
|
if (caller instanceof Response) return caller;
|
|
333
433
|
const sequenceId = readOutreachDocumentId(body['sequenceId']);
|
|
334
434
|
if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to delete.');
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.');
|
|
338
|
-
if (sequence.status !== 'draft' || await hasEnrollments(firestore, caller.orgId, sequence.id)) {
|
|
339
|
-
// What people were sent, and why they stopped, is the record of what
|
|
340
|
-
// this organization did; only a draft nobody was enrolled in has none.
|
|
341
|
-
return outreachRefusal(409, 'delete-refused', 'Only a draft nobody was enrolled in can be deleted. Archive this sequence instead.');
|
|
342
|
-
}
|
|
343
|
-
await outreachOrgCollection(firestore, caller.orgId, 'sequences').doc(sequence.id).delete();
|
|
344
|
-
await activity(caller, OUTREACH_SEQUENCE_ACTIVITY.delete, sequence);
|
|
435
|
+
const outcome = await removeOutreachSequence(deps, caller, sequenceId);
|
|
436
|
+
if (outcome.ok === false) return outreachRefusal(outcome.status, outcome.code, outcome.message);
|
|
345
437
|
return outreachOk({
|
|
346
438
|
ok: true
|
|
347
439
|
});
|