@aglyn/plugins-outreach 1.0.0-beta.152 → 1.0.0-beta.154

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.
Files changed (51) hide show
  1. package/README.md +5 -1
  2. package/package.json +12 -12
  3. package/src/lib/components/enroll-dialog.js +142 -32
  4. package/src/lib/components/enroll-dialog.js.map +1 -1
  5. package/src/lib/components/enrollments-table.js +37 -8
  6. package/src/lib/components/enrollments-table.js.map +1 -1
  7. package/src/lib/components/mailbox-card.d.ts +1 -0
  8. package/src/lib/components/mailbox-card.js +25 -5
  9. package/src/lib/components/mailbox-card.js.map +1 -1
  10. package/src/lib/components/use-outreach-crm.d.ts +20 -1
  11. package/src/lib/components/use-outreach-crm.js +78 -4
  12. package/src/lib/components/use-outreach-crm.js.map +1 -1
  13. package/src/lib/declarations.console-server.js +14 -0
  14. package/src/lib/declarations.console-server.js.map +1 -1
  15. package/src/lib/engine/enrollment-state.d.ts +6 -1
  16. package/src/lib/engine/enrollment-state.js +3 -1
  17. package/src/lib/engine/enrollment-state.js.map +1 -1
  18. package/src/lib/enrollment/enroll-people.d.ts +52 -7
  19. package/src/lib/enrollment/enroll-people.js +109 -12
  20. package/src/lib/enrollment/enroll-people.js.map +1 -1
  21. package/src/lib/enrollment/gate-lookups.d.ts +12 -3
  22. package/src/lib/enrollment/gate-lookups.js +17 -14
  23. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  24. package/src/lib/mailboxes/mailbox-api.d.ts +6 -0
  25. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  26. package/src/lib/mailboxes/mailbox-routes.js +11 -0
  27. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  28. package/src/lib/model/outreach-api.d.ts +39 -8
  29. package/src/lib/model/outreach-api.js.map +1 -1
  30. package/src/lib/model/outreach.types.d.ts +28 -3
  31. package/src/lib/model/outreach.types.js +9 -0
  32. package/src/lib/model/outreach.types.js.map +1 -1
  33. package/src/lib/model/stored-records.js +7 -0
  34. package/src/lib/model/stored-records.js.map +1 -1
  35. package/src/lib/routes/enroll-routes.js +112 -39
  36. package/src/lib/routes/enroll-routes.js.map +1 -1
  37. package/src/lib/routes/preview-routes.js +18 -5
  38. package/src/lib/routes/preview-routes.js.map +1 -1
  39. package/src/lib/runtime/lead-records.d.ts +26 -0
  40. package/src/lib/runtime/lead-records.js +84 -0
  41. package/src/lib/runtime/lead-records.js.map +1 -0
  42. package/src/lib/runtime/send-job.js +30 -12
  43. package/src/lib/runtime/send-job.js.map +1 -1
  44. package/src/lib/runtime/sync-job.js +11 -3
  45. package/src/lib/runtime/sync-job.js.map +1 -1
  46. package/src/lib/runtime/timeline.d.ts +6 -4
  47. package/src/lib/runtime/timeline.js +3 -7
  48. package/src/lib/runtime/timeline.js.map +1 -1
  49. package/src/lib/storage/outreach-records.d.ts +21 -2
  50. package/src/lib/storage/outreach-records.js +31 -2
  51. package/src/lib/storage/outreach-records.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type {\n OutreachMailbox,\n OutreachMailboxStatus,\n} from '../model/outreach.types'\nimport { createGmailClient } from '../transport/gmail-client'\nimport { GmailTransportError } from '../transport/gmail-errors'\nimport {\n buildGoogleAuthorizationUrl,\n exchangeGoogleAuthorizationCode,\n grantedScopesInclude,\n OUTREACH_REQUIRED_GRANTED_SCOPES,\n pkceChallenge,\n readGoogleIdToken,\n type GoogleTokenResponse,\n} from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachMessage } from '../transport/send-message'\nimport {\n buildConnectReturnFragment,\n type OutreachApiRefusalReason,\n type OutreachConnectReturn,\n type OutreachMailboxAvailability,\n type OutreachMailboxAvailabilityGate,\n type OutreachMailboxDisconnectResponse,\n} from './mailbox-api'\nimport {\n mailboxCredentialsRef,\n mailboxRef,\n outreachMailboxId,\n readMailboxCredentials,\n sealMailboxRefreshToken,\n type OutreachGoogleMailboxCredentials,\n} from './mailbox-credentials'\nimport { revokeMailboxGrant } from './mailbox-revoke'\nimport {\n outreachMemberGate,\n refusal,\n type OutreachGateContext,\n type OutreachGateDeps,\n} from './mailbox-gate'\nimport {\n defaultSendAs,\n isValidTimezone,\n OUTREACH_DEFAULT_DAILY_CAP,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAX_MAILBOXES_PER_MEMBER,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n verifiedSendAsAddresses,\n} from './mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n} from './mailbox-transport'\nimport {\n consumeOutreachOAuthState,\n mintOutreachOAuthState,\n outreachOidcNonce,\n outreachPkceVerifier,\n readOutreachOAuthState,\n recordOutreachOAuthState,\n} from './oauth-state'\nimport {\n OUTREACH_NOT_CONFIGURED_MESSAGE,\n type OutreachGoogleConfigResult,\n} from './outreach-config'\n\n/**\n * THE MAILBOX ROUTES (AGL-2978): connect a rep's Google mailbox, keep its\n * settings, pause it, test it, and disconnect it.\n *\n * ## The connect, end to end\n *\n * 1. `connect` (the member's session) mints a signed, single-use state bound\n * to the member and the organization, records its pending entry, and\n * answers Google's consent address — offline access, the consent prompt,\n * a PKCE challenge and an OpenID nonce, both derived from the state.\n * 2. Google redirects the browser to `oauth/callback`. That request carries\n * no bearer token, so it DOES nothing: it verifies the state's signature,\n * finds the organization's console path, and redirects to the Mailboxes\n * page with the code in the URL FRAGMENT, where no server and no `Referer`\n * sees it. Its release gate was asked about the organization and the\n * member the state names (the route's declared subject).\n * 3. The page takes the code out of the address bar and posts it to\n * `connect/complete` with the member's own session. That is where the\n * state is consumed, where it is refused if it was minted for anyone else,\n * and where the code is exchanged. So a consent screen someone else was\n * tricked into completing cannot connect THEIR mailbox to the member who\n * started it: the code arrives in the wrong browser, whose session is not\n * the state's member.\n *\n * ## Refusals, in order, on `connect/complete`\n *\n * A state that is invalid, minted for another member or organization,\n * expired, already used or superseded; a code Google refuses; a grant with no\n * refresh token; a grant missing `gmail.send` or `gmail.readonly` (the consent\n * screen lets a person untick them); an ID token that is not about this\n * exchange or whose address Google has not verified; a Gmail profile whose\n * address is not the ID token's; and a member at their mailbox limit.\n *\n * A grant refused after the code exchange is dropped, not revoked. Google's\n * revocation ends the whole grant this client holds for the account, and the\n * same account may already be connected — by this member in another\n * organization, or by a teammate — so revoking a token this route merely\n * declines to keep could disconnect a mailbox that is working.\n *\n * ## Who may do what\n *\n * Every route but the callback climbs `outreachMemberGate`. The member who\n * connected a mailbox manages it; an organization owner or admin may also\n * change its settings, pause it and disconnect it — a departing rep's mailbox\n * has to be stoppable by somebody. Only its own member can send a test from\n * it, since the test lands in their inbox.\n */\n\nexport interface OutreachMailboxRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachGateDeps\n readConfig(): OutreachGoogleConfigResult\n /** Whether the signing secret the OAuth state needs is configured. */\n stateSigningConfigured(): boolean\n redirectUri(requestUrl: string): string | null\n now(): number\n /** The network and retry policy every Google call uses. */\n transport: TransportDeps\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof OUTREACH_MAILBOX_ACTIVITY_TARGET; id: string; name: string },\n ): Promise<void>\n confirmAliasesByProvider(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; uid: string; addresses: readonly string[]; nowMs: number },\n ): Promise<{ ok: boolean; confirmed?: string[] }>\n}\n\nexport interface OutreachMailboxRoutes {\n availability: PluginWebApiHandler\n connect: PluginWebApiHandler\n oauthCallback: PluginWebApiHandler\n connectComplete: PluginWebApiHandler\n settings: PluginWebApiHandler\n status: PluginWebApiHandler\n test: PluginWebApiHandler\n disconnect: PluginWebApiHandler\n}\n\n/** Test sends one mailbox may make in an hour. */\nexport const OUTREACH_TEST_SENDS_PER_HOUR = 5\n\n/**\n * The org activity target a mailbox's rows are filed under: Outreach's own\n * namespaced resource, which the feed reads as \"Mailbox\".\n */\nexport const OUTREACH_MAILBOX_ACTIVITY_TARGET = 'outreach:mailbox'\n\nconst MAILBOX_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst MAX_CODE_CHARS = 2048\n\nconst noStore = { 'Cache-Control': 'no-store' }\n\nconst ok = (body: unknown) => Response.json(body, { status: 200, headers: noStore })\n\nfunction methodNotAllowed(allow: string): Response {\n return Response.json(\n { error: 'Method not allowed', reason: 'method-not-allowed' satisfies OutreachApiRefusalReason },\n { status: 405, headers: { ...noStore, Allow: allow } },\n )\n}\n\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {}\n}\n\nfunction notConfigured(): Response {\n return refusal(503, 'not-configured', OUTREACH_NOT_CONFIGURED_MESSAGE)\n}\n\n/** A mailbox document read defensively into the model shape. */\nfunction readMailbox(id: string, data: Record<string, unknown> | undefined): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n\nconst activityName = (mailbox: Pick<OutreachMailbox, 'email'>) => mailbox.email\n\nexport function createOutreachMailboxRoutes(deps: OutreachMailboxRouteDeps): OutreachMailboxRoutes {\n /** The gate, then the mailbox the body names, then who may touch it. */\n async function loadManagedMailbox(\n request: Request,\n body: Record<string, unknown>,\n who: 'member-or-admin' | 'member',\n ): Promise<Response | { gate: OutreachGateContext; mailbox: OutreachMailbox }> {\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const own = mailbox.connectedByUid === gate.uid\n if (!own && !(who === 'member-or-admin' && gate.isOrgAdmin)) {\n return refusal(\n 403,\n 'not-your-mailbox',\n who === 'member'\n ? 'Only the member who connected this mailbox can do that.'\n : 'Only the member who connected this mailbox, or an organization owner or admin, can change it.',\n )\n }\n return { gate, mailbox }\n }\n\n const availability: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const gate = await outreachMemberGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate)\n if (gate instanceof Response) return gate\n // Every gate is named, not the first: an operator fixing a deployment\n // should see the whole list once rather than one variable per redeploy.\n const missing: OutreachMailboxAvailabilityGate[] = []\n const config = deps.readConfig()\n if (config.configured === false) missing.push({ gate: 'google', missing: config.missing })\n if (!deps.stateSigningConfigured()) missing.push({ gate: 'state' })\n if (!deps.redirectUri(request.url)) missing.push({ gate: 'redirect' })\n return ok({\n configured: missing.length === 0,\n canManageAll: gate.isOrgAdmin,\n ...(missing.length ? { missing } : {}),\n } satisfies OutreachMailboxAvailability)\n }\n\n const connect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n const redirectUri = deps.redirectUri(request.url)\n if (!config.configured || !redirectUri || !deps.stateSigningConfigured()) return notConfigured()\n\n const nowMs = deps.now()\n const { state, claims } = mintOutreachOAuthState({ orgId: gate.orgId, uid: gate.uid, nowMs })\n await recordOutreachOAuthState(deps.firestore(), { claims, redirectUri, nowMs })\n const url = buildGoogleAuthorizationUrl({\n clientId: config.config.clientId,\n redirectUri,\n state,\n codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),\n nonce: outreachOidcNonce(claims.nonce),\n loginHint: gate.email,\n })\n return ok({ url })\n }\n\n const oauthCallback: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = new URL(request.url).searchParams\n const read = readOutreachOAuthState(params.get('state'), deps.now())\n const plain = (status: number, text: string) =>\n new Response(text, {\n status,\n headers: { ...noStore, 'Content-Type': 'text/plain; charset=utf-8', 'Referrer-Policy': 'no-referrer' },\n })\n if (read.ok === false && read.refusal === 'state-invalid') {\n return plain(400, 'This connection link is not valid. Return to the console and connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: { orgId: string } }).claims\n const org = await deps.gate.readOrg(claims.orgId)\n const slug = typeof org?.['slug'] === 'string' ? org['slug'] : ''\n if (!slug) return plain(404, 'That organization could not be found.')\n\n let fragment: OutreachConnectReturn\n const googleError = params.get('error')\n const code = params.get('code') ?? ''\n if (googleError) {\n fragment = { kind: 'error', reason: googleError === 'access_denied' ? 'access_denied' : 'google_error' }\n } else if (!read.ok) {\n fragment = { kind: 'error', reason: 'expired' }\n } else if (!code || code.length > MAX_CODE_CHARS) {\n fragment = { kind: 'error', reason: 'google_error' }\n } else {\n fragment = { kind: 'code', code, state: params.get('state') ?? '' }\n }\n // A RELATIVE location on the callback's own origin: the path is built from\n // the organization's slug, never from anything the request supplied.\n return new Response(null, {\n status: 303,\n headers: {\n ...noStore,\n 'Referrer-Policy': 'no-referrer',\n Location: `/${encodeURIComponent(slug)}/outreach/mailboxes#${buildConnectReturnFragment(fragment)}`,\n },\n })\n }\n\n const connectComplete: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n if (!config.configured || !deps.stateSigningConfigured()) return notConfigured()\n const code = typeof body['code'] === 'string' ? body['code'] : ''\n if (!code || code.length > MAX_CODE_CHARS) return refusal(400, 'invalid-request', 'The connection did not return a code.')\n\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const read = readOutreachOAuthState(body['state'], nowMs)\n if (read.ok === false && read.refusal === 'state-invalid') {\n return refusal(400, 'state-invalid', 'This connection could not be verified. Connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: typeof read.claims }).claims\n if (claims.uid !== gate.uid) {\n return refusal(\n 403,\n 'state-user-mismatch',\n 'This connection was started by another member. Sign in as that member, or connect the mailbox again.',\n )\n }\n if (claims.orgId !== gate.orgId) {\n return refusal(400, 'state-org-mismatch', 'This connection was started in another organization.')\n }\n if (!read.ok) return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n\n const consumed = await consumeOutreachOAuthState(firestore, { claims, nowMs })\n if (consumed.ok === false) {\n if (consumed.refusal === 'state-expired') {\n return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n }\n return refusal(\n 409,\n consumed.refusal,\n consumed.refusal === 'state-superseded'\n ? 'A newer connection was started. Finish that one, or connect the mailbox again.'\n : 'This connection was already used. Connect the mailbox again.',\n )\n }\n\n let grant: GoogleTokenResponse\n try {\n grant = await exchangeGoogleAuthorizationCode(\n {\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n code,\n redirectUri: consumed.redirectUri,\n codeVerifier: outreachPkceVerifier(claims.nonce),\n },\n deps.transport,\n )\n } catch (error) {\n if (error instanceof GmailTransportError && error.code === 'invalid_grant') {\n return refusal(400, 'code-rejected', \"Google's sign-in expired or was already used. Connect the mailbox again.\")\n }\n if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {\n return refusal(503, 'not-configured', \"Google refused this deployment's OAuth client.\")\n }\n return googleUnavailable(error)\n }\n if (!grant.refreshToken) {\n return refusal(\n 422,\n 'refresh-token-missing',\n 'Google did not grant offline access, so the mailbox could not send later. Connect it again.',\n )\n }\n if (!grantedScopesInclude(grant.scope, OUTREACH_REQUIRED_GRANTED_SCOPES)) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n const identity = readGoogleIdToken(grant.idToken, {\n clientId: config.config.clientId,\n nonce: outreachOidcNonce(claims.nonce),\n nowMs,\n })\n if (identity.ok === false) {\n return refusal(\n 422,\n 'identity-unverified',\n identity.refusal === 'email-unverified'\n ? \"Google has not verified this account's email address.\"\n : \"Google's answer could not be verified. Connect the mailbox again.\",\n )\n }\n\n const client = createGmailClient({\n ...deps.transport,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n refreshToken: grant.refreshToken,\n })\n let profileEmail: string\n let sendAsOptions: ReturnType<typeof verifiedSendAsAddresses>\n try {\n const [profile, sendAs] = await Promise.all([client.getProfile(), client.listSendAs()])\n profileEmail = profile.emailAddress\n sendAsOptions = verifiedSendAsAddresses(sendAs)\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleUnavailable(error)\n }\n if (profileEmail !== identity.identity.email) {\n return refusal(422, 'account-mismatch', 'The Gmail account and the Google sign-in did not match. Connect again.')\n }\n if (!sendAsOptions.some((option) => option.email === profileEmail)) {\n sendAsOptions = [\n { email: profileEmail, displayName: '', isPrimary: true, isDefault: !sendAsOptions.some((o) => o.isDefault) },\n ...sendAsOptions,\n ]\n }\n\n const mailboxId = outreachMailboxId(gate.orgId, gate.uid, identity.identity.sub)\n const mailboxDoc = mailboxRef(firestore, gate.orgId, mailboxId)\n const credentialDoc = mailboxCredentialsRef(firestore, mailboxId)\n const [existingMailbox, existingCredential] = await Promise.all([mailboxDoc.get(), credentialDoc.get()])\n const current = readMailbox(mailboxId, existingMailbox.exists ? existingMailbox.data() : undefined)\n if (!current) {\n const mine = await firestore\n .collection('orgs')\n .doc(gate.orgId)\n .collection('outreachMailboxes')\n .where('connectedByUid', '==', gate.uid)\n .limit(OUTREACH_MAX_MAILBOXES_PER_MEMBER)\n .get()\n if (mine.size >= OUTREACH_MAX_MAILBOXES_PER_MEMBER) {\n return refusal(\n 409,\n 'mailbox-limit',\n `You can connect up to ${OUTREACH_MAX_MAILBOXES_PER_MEMBER} mailboxes. Disconnect one before connecting another.`,\n )\n }\n }\n\n const defaultOption = sendAsOptions.find((option) => option.email === defaultSendAs(sendAsOptions, profileEmail))\n let mailbox: OutreachMailbox\n /** On a reconnect, only the fields a connect decides — see the write below. */\n let reconnected: Partial<OutreachMailbox> | null = null\n if (current) {\n const keepSendAs = sendAsOptions.some((option) => option.email === current.sendAs)\n reconnected = {\n provider: 'google',\n email: profileEmail,\n sendAs: keepSendAs ? current.sendAs : (defaultOption?.email ?? profileEmail),\n sendAsOptions,\n // A reconnect restores a mailbox Google had cut off, and leaves a\n // mailbox its member paused paused.\n status: (current.status === 'paused' ? 'paused' : 'connected') satisfies OutreachMailboxStatus,\n connectedAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n mailbox = { ...current, ...reconnected }\n } else {\n const timezone = isValidTimezone(body['timezone']) ? String(body['timezone']) : 'UTC'\n const displayName = validateDisplayName(defaultOption?.displayName || gate.name || '')\n mailbox = {\n id: mailboxId,\n provider: 'google',\n email: profileEmail,\n sendAs: defaultOption?.email ?? profileEmail,\n sendAsOptions,\n displayName: typeof displayName === 'string' ? displayName : '',\n status: 'connected',\n dailyCap: OUTREACH_DEFAULT_DAILY_CAP,\n window: { ...OUTREACH_DEFAULT_WINDOW, days: [...OUTREACH_DEFAULT_WINDOW.days] },\n timezone,\n rampStartedAtMs: nowMs,\n health: {\n sentToday: 0,\n sentOnDay: null,\n bounces: 0,\n replies: 0,\n lastSentAtMs: null,\n lastErrorAtMs: null,\n lastErrorCode: null,\n daily: {},\n },\n connectedByUid: gate.uid,\n connectedAtMs: nowMs,\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n }\n const credential: OutreachGoogleMailboxCredentials = {\n id: mailboxId,\n orgId: gate.orgId,\n mailboxId,\n provider: 'google',\n connectedByUid: gate.uid,\n providerAccountId: identity.identity.sub,\n email: profileEmail,\n scopes: grant.scope.split(/\\s+/).filter(Boolean),\n ...sealMailboxRefreshToken(grant.refreshToken, mailboxId, config.config.keyring),\n createdAtMs: Number(existingCredential.exists ? existingCredential.get('createdAtMs') : 0) || nowMs,\n updatedAtMs: nowMs,\n }\n const batch = firestore.batch()\n // A reconnect MERGES only what a connect decides, so the settings a member\n // chose and the health counters the sending runtime increments are never\n // written back from a read that may already be stale.\n if (reconnected) batch.set(mailboxDoc, reconnected, { merge: true })\n else batch.set(mailboxDoc, mailbox)\n batch.set(credentialDoc, credential)\n await batch.commit()\n\n const aliases: { ok: boolean; confirmed?: string[] } = await deps\n .confirmAliasesByProvider(firestore, {\n orgId: gate.orgId,\n uid: gate.uid,\n addresses: sendAsOptions.map((option) => option.email),\n nowMs,\n })\n .catch((error: unknown) => {\n console.error('[outreach] confirming member aliases from send-as failed', error)\n return { ok: false }\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n current ? 'Reconnected a mailbox in Sequences' : 'Connected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailboxId, name: activityName(mailbox) },\n )\n return ok({\n ok: true,\n mailbox,\n created: !current,\n confirmedAliases: aliases.ok ? (aliases.confirmed ?? []) : [],\n })\n }\n\n const settings: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const update: Partial<OutreachMailbox> = {}\n const invalid = (message: string) => refusal(400, 'invalid-settings', message)\n\n if (body['sendAs'] !== undefined) {\n const sendAs = typeof body['sendAs'] === 'string' ? body['sendAs'].trim().toLowerCase() : ''\n if (!(mailbox.sendAsOptions ?? []).some((option) => option.email === sendAs)) {\n return invalid('Choose one of the addresses Gmail has verified for this account.')\n }\n update.sendAs = sendAs\n }\n if (body['displayName'] !== undefined) {\n const name = validateDisplayName(body['displayName'])\n if (typeof name !== 'string') return invalid(name.message)\n update.displayName = name\n }\n if (body['dailyCap'] !== undefined) {\n const cap = validateDailyCap(body['dailyCap'])\n if (typeof cap !== 'number') return invalid(cap.message)\n update.dailyCap = cap\n }\n if (body['window'] !== undefined) {\n const window = validateSendWindow(body['window'])\n if (!('days' in window)) return invalid(window.message)\n update.window = window\n }\n if (body['timezone'] !== undefined) {\n if (!isValidTimezone(body['timezone'])) return invalid('Choose a timezone from the list.')\n update.timezone = body['timezone']\n }\n if (!Object.keys(update).length) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({ ...update, updatedAtMs: nowMs })\n return ok({ ok: true, mailbox: { ...mailbox, ...update, updatedAtMs: nowMs } })\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n if (typeof body['paused'] !== 'boolean') return refusal(400, 'invalid-request', 'Say whether to pause the mailbox.')\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before pausing or resuming it.')\n }\n const next: OutreachMailboxStatus = body['paused'] ? 'paused' : 'connected'\n if (mailbox.status === next) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n // A member's pause or resume answers an automatic pause (AGL-2981): its\n // reason goes, and the next bounce is judged as new evidence.\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({\n status: next,\n autoPause: null,\n updatedAtMs: nowMs,\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n next === 'paused' ? 'Paused a mailbox in Sequences' : 'Resumed a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, mailbox: { ...mailbox, status: next, autoPause: null, updatedAtMs: nowMs } })\n }\n\n const test: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before sending from it.')\n }\n if (!deps.readConfig().configured) return notConfigured()\n const limited = await deps.consumeRateLimit(`outreach-mailbox-test:${mailbox.id}`, {\n limit: OUTREACH_TEST_SENDS_PER_HOUR,\n windowMs: 60 * 60 * 1000,\n })\n if (!limited.allowed) {\n return refusal(429, 'rate-limited', `A mailbox can send ${OUTREACH_TEST_SENDS_PER_HOUR} tests an hour. Try again later.`)\n }\n /*\n * To the member's own address unless they name another (AGL-3228). A\n * test to oneself never leaves Google, so it arrives with no\n * Authentication-Results at all and cannot show whether the send-as\n * domain's DKIM and DMARC hold; an outside mailbox the member can read\n * is the only place that can be seen. The rate limit above is the\n * whole guard on who may be written to — it is the member's own\n * account sending, as it could from its own compose window.\n */\n const named = body['to']\n const to =\n named === undefined || named === null || String(named).trim() === ''\n ? mailbox.email\n : normalizeContactEmail(String(named))\n if (!to) {\n return refusal(400, 'invalid-request', `\"${String(named).trim()}\" isn't a valid email address.`)\n }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await openOutreachMailboxClient(\n firestore,\n { mailboxId: mailbox.id },\n { ...deps.transport, readConfig: deps.readConfig, now: deps.now },\n )\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return notConfigured()\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'This mailbox needs to be connected again before it can send.')\n }\n try {\n const sent = await sendOutreachMessage(opened.client, {\n from: { address: mailbox.sendAs, name: mailbox.displayName },\n to,\n subject: 'Sequences test message',\n text: [\n 'This is a test message from Sequences.',\n '',\n `It was sent through the connected mailbox ${mailbox.email} as ${mailbox.sendAs}. ` +\n 'If it reached the inbox, this mailbox can send.',\n ...(to === mailbox.email\n ? []\n : [\n '',\n 'To read whether the sender is authenticated, open this message\\u2019s original source ' +\n 'and look for dkim=pass, spf=pass and dmarc=pass in Authentication-Results.',\n ]),\n ].join('\\n'),\n })\n return ok({ ok: true, sentTo: to, gmailMessageId: sent.gmailMessageId, sentAtMs: nowMs })\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'Google refused this mailbox’s access. Connect it again.')\n }\n if (error instanceof Rfc5322MessageError) {\n return refusal(400, 'invalid-settings', `The test could not be written: ${error.message}`)\n }\n return googleUnavailable(error)\n }\n }\n\n const disconnect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const firestore = deps.firestore()\n const credentialDoc = mailboxCredentialsRef(firestore, mailbox.id)\n const snapshot = await credentialDoc.get()\n const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null)\n const revocation = credential\n ? await revokeMailboxGrant(firestore, credential, deps)\n : ('already-invalid' as const)\n const batch = firestore.batch()\n batch.delete(credentialDoc)\n batch.delete(mailboxRef(firestore, gate.orgId, mailbox.id))\n await batch.commit()\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n 'Disconnected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, revocation } satisfies OutreachMailboxDisconnectResponse)\n }\n\n return { availability, connect, oauthCallback, connectComplete, settings, status, test, disconnect }\n}\n\nfunction googleUnavailable(error: unknown): Response {\n if (!(error instanceof GmailTransportError)) throw error\n return refusal(\n 502,\n 'google-unavailable',\n error.retryable ? 'Google did not answer. Try again in a moment.' : `Google refused the request: ${error.message}`,\n )\n}\n"],"names":["normalizeContactEmail","createGmailClient","GmailTransportError","buildGoogleAuthorizationUrl","exchangeGoogleAuthorizationCode","grantedScopesInclude","OUTREACH_REQUIRED_GRANTED_SCOPES","pkceChallenge","readGoogleIdToken","Rfc5322MessageError","sendOutreachMessage","buildConnectReturnFragment","mailboxCredentialsRef","mailboxRef","outreachMailboxId","readMailboxCredentials","sealMailboxRefreshToken","revokeMailboxGrant","outreachMemberGate","refusal","defaultSendAs","isValidTimezone","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAX_MAILBOXES_PER_MEMBER","validateDailyCap","validateDisplayName","validateSendWindow","verifiedSendAsAddresses","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","consumeOutreachOAuthState","mintOutreachOAuthState","outreachOidcNonce","outreachPkceVerifier","readOutreachOAuthState","recordOutreachOAuthState","OUTREACH_NOT_CONFIGURED_MESSAGE","OUTREACH_TEST_SENDS_PER_HOUR","OUTREACH_MAILBOX_ACTIVITY_TARGET","MAILBOX_ID","MAX_CODE_CHARS","noStore","ok","body","Response","json","status","headers","methodNotAllowed","allow","error","reason","Allow","readBody","request","catch","Array","isArray","notConfigured","readMailbox","id","data","activityName","mailbox","email","createOutreachMailboxRoutes","deps","loadManagedMailbox","who","gate","mailboxId","test","snapshot","firestore","orgId","get","exists","undefined","own","connectedByUid","uid","isOrgAdmin","availability","method","URL","url","searchParams","missing","config","readConfig","configured","push","stateSigningConfigured","redirectUri","length","canManageAll","connect","nowMs","now","state","claims","clientId","codeChallenge","nonce","loginHint","oauthCallback","params","read","plain","text","org","readOrg","slug","fragment","googleError","code","kind","Location","encodeURIComponent","connectComplete","aliases","consumed","grant","clientSecret","codeVerifier","transport","googleUnavailable","refreshToken","scope","identity","idToken","client","profileEmail","sendAsOptions","profile","sendAs","Promise","all","getProfile","listSendAs","emailAddress","reconnectRequired","some","option","displayName","isPrimary","isDefault","o","sub","mailboxDoc","credentialDoc","existingMailbox","existingCredential","current","mine","collection","doc","where","limit","size","defaultOption","find","reconnected","keepSendAs","provider","connectedAtMs","updatedAtMs","timezone","String","name","dailyCap","window","days","rampStartedAtMs","health","sentToday","sentOnDay","bounces","replies","lastSentAtMs","lastErrorAtMs","lastErrorCode","daily","createdAtMs","credential","providerAccountId","scopes","split","filter","Boolean","keyring","Number","batch","set","merge","commit","confirmAliasesByProvider","addresses","map","console","logOrgActivity","type","created","confirmedAliases","confirmed","settings","loaded","update","invalid","message","trim","toLowerCase","cap","Object","keys","next","autoPause","limited","consumeRateLimit","windowMs","allowed","named","to","opened","errorCode","sent","from","address","subject","join","sentTo","gmailMessageId","sentAtMs","disconnect","revocation","delete","retryable"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/mailbox-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 { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport type {\n OutreachMailbox,\n OutreachMailboxStatus,\n} from '../model/outreach.types'\nimport { createGmailClient } from '../transport/gmail-client'\nimport { GmailTransportError } from '../transport/gmail-errors'\nimport {\n buildGoogleAuthorizationUrl,\n exchangeGoogleAuthorizationCode,\n grantedScopesInclude,\n OUTREACH_REQUIRED_GRANTED_SCOPES,\n pkceChallenge,\n readGoogleIdToken,\n type GoogleTokenResponse,\n} from '../transport/google-oauth'\nimport type { TransportDeps } from '../transport/http'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachMessage } from '../transport/send-message'\nimport {\n buildConnectReturnFragment,\n type OutreachApiRefusalReason,\n type OutreachConnectReturn,\n type OutreachMailboxAvailability,\n type OutreachMailboxAvailabilityGate,\n type OutreachMailboxDisconnectResponse,\n} from './mailbox-api'\nimport {\n mailboxCredentialsRef,\n mailboxRef,\n outreachMailboxId,\n readMailboxCredentials,\n sealMailboxRefreshToken,\n type OutreachGoogleMailboxCredentials,\n} from './mailbox-credentials'\nimport { revokeMailboxGrant } from './mailbox-revoke'\nimport {\n outreachMemberGate,\n refusal,\n type OutreachGateContext,\n type OutreachGateDeps,\n} from './mailbox-gate'\nimport {\n defaultSendAs,\n isValidTimezone,\n OUTREACH_DEFAULT_DAILY_CAP,\n OUTREACH_DEFAULT_WINDOW,\n OUTREACH_MAX_MAILBOXES_PER_MEMBER,\n validateDailyCap,\n validateDisplayName,\n validateSendWindow,\n verifiedSendAsAddresses,\n} from './mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n} from './mailbox-transport'\nimport {\n consumeOutreachOAuthState,\n mintOutreachOAuthState,\n outreachOidcNonce,\n outreachPkceVerifier,\n readOutreachOAuthState,\n recordOutreachOAuthState,\n} from './oauth-state'\nimport {\n OUTREACH_NOT_CONFIGURED_MESSAGE,\n type OutreachGoogleConfigResult,\n} from './outreach-config'\n\n/**\n * THE MAILBOX ROUTES (AGL-2978): connect a rep's Google mailbox, keep its\n * settings, pause it, test it, and disconnect it.\n *\n * ## The connect, end to end\n *\n * 1. `connect` (the member's session) mints a signed, single-use state bound\n * to the member and the organization, records its pending entry, and\n * answers Google's consent address — offline access, the consent prompt,\n * a PKCE challenge and an OpenID nonce, both derived from the state.\n * 2. Google redirects the browser to `oauth/callback`. That request carries\n * no bearer token, so it DOES nothing: it verifies the state's signature,\n * finds the organization's console path, and redirects to the Mailboxes\n * page with the code in the URL FRAGMENT, where no server and no `Referer`\n * sees it. Its release gate was asked about the organization and the\n * member the state names (the route's declared subject).\n * 3. The page takes the code out of the address bar and posts it to\n * `connect/complete` with the member's own session. That is where the\n * state is consumed, where it is refused if it was minted for anyone else,\n * and where the code is exchanged. So a consent screen someone else was\n * tricked into completing cannot connect THEIR mailbox to the member who\n * started it: the code arrives in the wrong browser, whose session is not\n * the state's member.\n *\n * ## Refusals, in order, on `connect/complete`\n *\n * A state that is invalid, minted for another member or organization,\n * expired, already used or superseded; a code Google refuses; a grant with no\n * refresh token; a grant missing `gmail.send` or `gmail.readonly` (the consent\n * screen lets a person untick them); an ID token that is not about this\n * exchange or whose address Google has not verified; a Gmail profile whose\n * address is not the ID token's; and a member at their mailbox limit.\n *\n * A grant refused after the code exchange is dropped, not revoked. Google's\n * revocation ends the whole grant this client holds for the account, and the\n * same account may already be connected — by this member in another\n * organization, or by a teammate — so revoking a token this route merely\n * declines to keep could disconnect a mailbox that is working.\n *\n * ## Who may do what\n *\n * Every route but the callback climbs `outreachMemberGate`. The member who\n * connected a mailbox manages it; an organization owner or admin may also\n * change its settings, pause it and disconnect it — a departing rep's mailbox\n * has to be stoppable by somebody. Only its own member can send a test from\n * it, since the test lands in their inbox.\n */\n\nexport interface OutreachMailboxRouteDeps {\n firestore(): FirebaseFirestore.Firestore\n gate: OutreachGateDeps\n readConfig(): OutreachGoogleConfigResult\n /** Whether the signing secret the OAuth state needs is configured. */\n stateSigningConfigured(): boolean\n redirectUri(requestUrl: string): string | null\n now(): number\n /** The network and retry policy every Google call uses. */\n transport: TransportDeps\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n logOrgActivity(\n orgId: string,\n actor: { uid: string; email?: string | null },\n action: string,\n target: { type: typeof OUTREACH_MAILBOX_ACTIVITY_TARGET; id: string; name: string },\n ): Promise<void>\n confirmAliasesByProvider(\n firestore: FirebaseFirestore.Firestore,\n input: { orgId: string; uid: string; addresses: readonly string[]; nowMs: number },\n ): Promise<{ ok: boolean; confirmed?: string[] }>\n}\n\nexport interface OutreachMailboxRoutes {\n availability: PluginWebApiHandler\n connect: PluginWebApiHandler\n oauthCallback: PluginWebApiHandler\n connectComplete: PluginWebApiHandler\n settings: PluginWebApiHandler\n status: PluginWebApiHandler\n test: PluginWebApiHandler\n disconnect: PluginWebApiHandler\n}\n\n/** Test sends one mailbox may make in an hour. */\nexport const OUTREACH_TEST_SENDS_PER_HOUR = 5\n\n/**\n * The org activity target a mailbox's rows are filed under: Outreach's own\n * namespaced resource, which the feed reads as \"Mailbox\".\n */\nexport const OUTREACH_MAILBOX_ACTIVITY_TARGET = 'outreach:mailbox'\n\nconst MAILBOX_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst MAX_CODE_CHARS = 2048\n\nconst noStore = { 'Cache-Control': 'no-store' }\n\nconst ok = (body: unknown) => Response.json(body, { status: 200, headers: noStore })\n\nfunction methodNotAllowed(allow: string): Response {\n return Response.json(\n { error: 'Method not allowed', reason: 'method-not-allowed' satisfies OutreachApiRefusalReason },\n { status: 405, headers: { ...noStore, Allow: allow } },\n )\n}\n\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await request.json().catch(() => null)\n return body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {}\n}\n\nfunction notConfigured(): Response {\n return refusal(503, 'not-configured', OUTREACH_NOT_CONFIGURED_MESSAGE)\n}\n\n/** A mailbox document read defensively into the model shape. */\nfunction readMailbox(id: string, data: Record<string, unknown> | undefined): OutreachMailbox | null {\n if (!data) return null\n return { ...(data as unknown as OutreachMailbox), id }\n}\n\nconst activityName = (mailbox: Pick<OutreachMailbox, 'email'>) => mailbox.email\n\nexport function createOutreachMailboxRoutes(deps: OutreachMailboxRouteDeps): OutreachMailboxRoutes {\n /** The gate, then the mailbox the body names, then who may touch it. */\n async function loadManagedMailbox(\n request: Request,\n body: Record<string, unknown>,\n who: 'member-or-admin' | 'member',\n ): Promise<Response | { gate: OutreachGateContext; mailbox: OutreachMailbox }> {\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const mailboxId = typeof body['mailboxId'] === 'string' ? body['mailboxId'] : ''\n if (!MAILBOX_ID.test(mailboxId)) return refusal(400, 'invalid-request', 'Name the mailbox.')\n const snapshot = await mailboxRef(deps.firestore(), gate.orgId, mailboxId).get()\n const mailbox = readMailbox(mailboxId, snapshot.exists ? snapshot.data() : undefined)\n if (!mailbox) return refusal(404, 'mailbox-not-found', 'That mailbox is not connected to this organization.')\n const own = mailbox.connectedByUid === gate.uid\n if (!own && !(who === 'member-or-admin' && gate.isOrgAdmin)) {\n return refusal(\n 403,\n 'not-your-mailbox',\n who === 'member'\n ? 'Only the member who connected this mailbox can do that.'\n : 'Only the member who connected this mailbox, or an organization owner or admin, can change it.',\n )\n }\n return { gate, mailbox }\n }\n\n const availability: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const gate = await outreachMemberGate(request, new URL(request.url).searchParams.get('orgId'), deps.gate)\n if (gate instanceof Response) return gate\n // Every gate is named, not the first: an operator fixing a deployment\n // should see the whole list once rather than one variable per redeploy.\n const missing: OutreachMailboxAvailabilityGate[] = []\n const config = deps.readConfig()\n if (config.configured === false) missing.push({ gate: 'google', missing: config.missing })\n if (!deps.stateSigningConfigured()) missing.push({ gate: 'state' })\n if (!deps.redirectUri(request.url)) missing.push({ gate: 'redirect' })\n return ok({\n configured: missing.length === 0,\n canManageAll: gate.isOrgAdmin,\n ...(missing.length ? { missing } : {}),\n } satisfies OutreachMailboxAvailability)\n }\n\n const connect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n const redirectUri = deps.redirectUri(request.url)\n if (!config.configured || !redirectUri || !deps.stateSigningConfigured()) return notConfigured()\n\n const nowMs = deps.now()\n const { state, claims } = mintOutreachOAuthState({ orgId: gate.orgId, uid: gate.uid, nowMs })\n await recordOutreachOAuthState(deps.firestore(), { claims, redirectUri, nowMs })\n const url = buildGoogleAuthorizationUrl({\n clientId: config.config.clientId,\n redirectUri,\n state,\n codeChallenge: pkceChallenge(outreachPkceVerifier(claims.nonce)),\n nonce: outreachOidcNonce(claims.nonce),\n loginHint: gate.email,\n })\n return ok({ url })\n }\n\n const oauthCallback: PluginWebApiHandler = async (request) => {\n if (request.method !== 'GET') return methodNotAllowed('GET')\n const params = new URL(request.url).searchParams\n const read = readOutreachOAuthState(params.get('state'), deps.now())\n const plain = (status: number, text: string) =>\n new Response(text, {\n status,\n headers: { ...noStore, 'Content-Type': 'text/plain; charset=utf-8', 'Referrer-Policy': 'no-referrer' },\n })\n if (read.ok === false && read.refusal === 'state-invalid') {\n return plain(400, 'This connection link is not valid. Return to the console and connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: { orgId: string } }).claims\n const org = await deps.gate.readOrg(claims.orgId)\n const slug = typeof org?.['slug'] === 'string' ? org['slug'] : ''\n if (!slug) return plain(404, 'That organization could not be found.')\n\n let fragment: OutreachConnectReturn\n const googleError = params.get('error')\n const code = params.get('code') ?? ''\n if (googleError) {\n fragment = { kind: 'error', reason: googleError === 'access_denied' ? 'access_denied' : 'google_error' }\n } else if (!read.ok) {\n fragment = { kind: 'error', reason: 'expired' }\n } else if (!code || code.length > MAX_CODE_CHARS) {\n fragment = { kind: 'error', reason: 'google_error' }\n } else {\n fragment = { kind: 'code', code, state: params.get('state') ?? '' }\n }\n // A RELATIVE location on the callback's own origin: the path is built from\n // the organization's slug, never from anything the request supplied.\n return new Response(null, {\n status: 303,\n headers: {\n ...noStore,\n 'Referrer-Policy': 'no-referrer',\n Location: `/${encodeURIComponent(slug)}/outreach/mailboxes#${buildConnectReturnFragment(fragment)}`,\n },\n })\n }\n\n const connectComplete: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const gate = await outreachMemberGate(request, body['orgId'], deps.gate)\n if (gate instanceof Response) return gate\n const config = deps.readConfig()\n if (!config.configured || !deps.stateSigningConfigured()) return notConfigured()\n const code = typeof body['code'] === 'string' ? body['code'] : ''\n if (!code || code.length > MAX_CODE_CHARS) return refusal(400, 'invalid-request', 'The connection did not return a code.')\n\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const read = readOutreachOAuthState(body['state'], nowMs)\n if (read.ok === false && read.refusal === 'state-invalid') {\n return refusal(400, 'state-invalid', 'This connection could not be verified. Connect the mailbox again.')\n }\n const claims = read.ok ? read.claims : (read as { claims: typeof read.claims }).claims\n if (claims.uid !== gate.uid) {\n return refusal(\n 403,\n 'state-user-mismatch',\n 'This connection was started by another member. Sign in as that member, or connect the mailbox again.',\n )\n }\n if (claims.orgId !== gate.orgId) {\n return refusal(400, 'state-org-mismatch', 'This connection was started in another organization.')\n }\n if (!read.ok) return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n\n const consumed = await consumeOutreachOAuthState(firestore, { claims, nowMs })\n if (consumed.ok === false) {\n if (consumed.refusal === 'state-expired') {\n return refusal(410, 'state-expired', 'This connection took too long. Connect the mailbox again.')\n }\n return refusal(\n 409,\n consumed.refusal,\n consumed.refusal === 'state-superseded'\n ? 'A newer connection was started. Finish that one, or connect the mailbox again.'\n : 'This connection was already used. Connect the mailbox again.',\n )\n }\n\n let grant: GoogleTokenResponse\n try {\n grant = await exchangeGoogleAuthorizationCode(\n {\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n code,\n redirectUri: consumed.redirectUri,\n codeVerifier: outreachPkceVerifier(claims.nonce),\n },\n deps.transport,\n )\n } catch (error) {\n if (error instanceof GmailTransportError && error.code === 'invalid_grant') {\n return refusal(400, 'code-rejected', \"Google's sign-in expired or was already used. Connect the mailbox again.\")\n }\n if (error instanceof GmailTransportError && error.code === 'client_misconfigured') {\n return refusal(503, 'not-configured', \"Google refused this deployment's OAuth client.\")\n }\n return googleUnavailable(error)\n }\n if (!grant.refreshToken) {\n return refusal(\n 422,\n 'refresh-token-missing',\n 'Google did not grant offline access, so the mailbox could not send later. Connect it again.',\n )\n }\n if (!grantedScopesInclude(grant.scope, OUTREACH_REQUIRED_GRANTED_SCOPES)) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n const identity = readGoogleIdToken(grant.idToken, {\n clientId: config.config.clientId,\n nonce: outreachOidcNonce(claims.nonce),\n nowMs,\n })\n if (identity.ok === false) {\n return refusal(\n 422,\n 'identity-unverified',\n identity.refusal === 'email-unverified'\n ? \"Google has not verified this account's email address.\"\n : \"Google's answer could not be verified. Connect the mailbox again.\",\n )\n }\n\n const client = createGmailClient({\n ...deps.transport,\n clientId: config.config.clientId,\n clientSecret: config.config.clientSecret,\n refreshToken: grant.refreshToken,\n })\n let profileEmail: string\n let sendAsOptions: ReturnType<typeof verifiedSendAsAddresses>\n try {\n const [profile, sendAs] = await Promise.all([client.getProfile(), client.listSendAs()])\n profileEmail = profile.emailAddress\n sendAsOptions = verifiedSendAsAddresses(sendAs)\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n return refusal(\n 422,\n 'scopes-missing',\n 'Sequences needs permission to send and to read your mail. Connect again and allow both.',\n )\n }\n return googleUnavailable(error)\n }\n if (profileEmail !== identity.identity.email) {\n return refusal(422, 'account-mismatch', 'The Gmail account and the Google sign-in did not match. Connect again.')\n }\n if (!sendAsOptions.some((option) => option.email === profileEmail)) {\n sendAsOptions = [\n { email: profileEmail, displayName: '', isPrimary: true, isDefault: !sendAsOptions.some((o) => o.isDefault) },\n ...sendAsOptions,\n ]\n }\n\n const mailboxId = outreachMailboxId(gate.orgId, gate.uid, identity.identity.sub)\n const mailboxDoc = mailboxRef(firestore, gate.orgId, mailboxId)\n const credentialDoc = mailboxCredentialsRef(firestore, mailboxId)\n const [existingMailbox, existingCredential] = await Promise.all([mailboxDoc.get(), credentialDoc.get()])\n const current = readMailbox(mailboxId, existingMailbox.exists ? existingMailbox.data() : undefined)\n if (!current) {\n const mine = await firestore\n .collection('orgs')\n .doc(gate.orgId)\n .collection('outreachMailboxes')\n .where('connectedByUid', '==', gate.uid)\n .limit(OUTREACH_MAX_MAILBOXES_PER_MEMBER)\n .get()\n if (mine.size >= OUTREACH_MAX_MAILBOXES_PER_MEMBER) {\n return refusal(\n 409,\n 'mailbox-limit',\n `You can connect up to ${OUTREACH_MAX_MAILBOXES_PER_MEMBER} mailboxes. Disconnect one before connecting another.`,\n )\n }\n }\n\n const defaultOption = sendAsOptions.find((option) => option.email === defaultSendAs(sendAsOptions, profileEmail))\n let mailbox: OutreachMailbox\n /** On a reconnect, only the fields a connect decides — see the write below. */\n let reconnected: Partial<OutreachMailbox> | null = null\n if (current) {\n const keepSendAs = sendAsOptions.some((option) => option.email === current.sendAs)\n reconnected = {\n provider: 'google',\n email: profileEmail,\n sendAs: keepSendAs ? current.sendAs : (defaultOption?.email ?? profileEmail),\n sendAsOptions,\n // A reconnect restores a mailbox Google had cut off, and leaves a\n // mailbox its member paused paused.\n status: (current.status === 'paused' ? 'paused' : 'connected') satisfies OutreachMailboxStatus,\n connectedAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n mailbox = { ...current, ...reconnected }\n } else {\n const timezone = isValidTimezone(body['timezone']) ? String(body['timezone']) : 'UTC'\n const displayName = validateDisplayName(defaultOption?.displayName || gate.name || '')\n mailbox = {\n id: mailboxId,\n provider: 'google',\n email: profileEmail,\n sendAs: defaultOption?.email ?? profileEmail,\n sendAsOptions,\n displayName: typeof displayName === 'string' ? displayName : '',\n status: 'connected',\n dailyCap: OUTREACH_DEFAULT_DAILY_CAP,\n window: { ...OUTREACH_DEFAULT_WINDOW, days: [...OUTREACH_DEFAULT_WINDOW.days] },\n timezone,\n rampStartedAtMs: nowMs,\n health: {\n sentToday: 0,\n sentOnDay: null,\n bounces: 0,\n replies: 0,\n lastSentAtMs: null,\n lastErrorAtMs: null,\n lastErrorCode: null,\n daily: {},\n },\n connectedByUid: gate.uid,\n connectedAtMs: nowMs,\n createdAtMs: nowMs,\n updatedAtMs: nowMs,\n }\n }\n const credential: OutreachGoogleMailboxCredentials = {\n id: mailboxId,\n orgId: gate.orgId,\n mailboxId,\n provider: 'google',\n connectedByUid: gate.uid,\n providerAccountId: identity.identity.sub,\n email: profileEmail,\n scopes: grant.scope.split(/\\s+/).filter(Boolean),\n ...sealMailboxRefreshToken(grant.refreshToken, mailboxId, config.config.keyring),\n createdAtMs: Number(existingCredential.exists ? existingCredential.get('createdAtMs') : 0) || nowMs,\n updatedAtMs: nowMs,\n }\n const batch = firestore.batch()\n // A reconnect MERGES only what a connect decides, so the settings a member\n // chose and the health counters the sending runtime increments are never\n // written back from a read that may already be stale.\n if (reconnected) batch.set(mailboxDoc, reconnected, { merge: true })\n else batch.set(mailboxDoc, mailbox)\n batch.set(credentialDoc, credential)\n await batch.commit()\n\n const aliases: { ok: boolean; confirmed?: string[] } = await deps\n .confirmAliasesByProvider(firestore, {\n orgId: gate.orgId,\n uid: gate.uid,\n addresses: sendAsOptions.map((option) => option.email),\n nowMs,\n })\n .catch((error: unknown) => {\n console.error('[outreach] confirming member aliases from send-as failed', error)\n return { ok: false }\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n current ? 'Reconnected a mailbox in Sequences' : 'Connected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailboxId, name: activityName(mailbox) },\n )\n return ok({\n ok: true,\n mailbox,\n created: !current,\n confirmedAliases: aliases.ok ? (aliases.confirmed ?? []) : [],\n })\n }\n\n const settings: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const update: Partial<OutreachMailbox> = {}\n const invalid = (message: string) => refusal(400, 'invalid-settings', message)\n\n if (body['sendAs'] !== undefined) {\n const sendAs = typeof body['sendAs'] === 'string' ? body['sendAs'].trim().toLowerCase() : ''\n if (!(mailbox.sendAsOptions ?? []).some((option) => option.email === sendAs)) {\n return invalid('Choose one of the addresses Gmail has verified for this account.')\n }\n update.sendAs = sendAs\n }\n if (body['displayName'] !== undefined) {\n const name = validateDisplayName(body['displayName'])\n if (typeof name !== 'string') return invalid(name.message)\n update.displayName = name\n }\n if (body['dailyCap'] !== undefined) {\n const cap = validateDailyCap(body['dailyCap'])\n if (typeof cap !== 'number') return invalid(cap.message)\n update.dailyCap = cap\n }\n if (body['window'] !== undefined) {\n const window = validateSendWindow(body['window'])\n if (!('days' in window)) return invalid(window.message)\n update.window = window\n }\n if (body['timezone'] !== undefined) {\n if (!isValidTimezone(body['timezone'])) return invalid('Choose a timezone from the list.')\n update.timezone = body['timezone']\n }\n if (body['warmUp'] !== undefined) {\n /*\n * The ramp guards a NEW sender's reputation; a mailbox that has sent\n * mail for years has one already, and ramping it to ten a day guards\n * nothing (AGL-3228). Its member decides. Turning the ramp back on\n * starts a fresh one from today, which is what \"warm up\" means.\n */\n if (typeof body['warmUp'] !== 'boolean') return invalid('Say whether to warm the mailbox up gradually.')\n const running = typeof mailbox.rampStartedAtMs === 'number'\n if (body['warmUp'] && !running) update.rampStartedAtMs = deps.now()\n if (!body['warmUp'] && running) update.rampStartedAtMs = null\n }\n if (!Object.keys(update).length) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({ ...update, updatedAtMs: nowMs })\n return ok({ ok: true, mailbox: { ...mailbox, ...update, updatedAtMs: nowMs } })\n }\n\n const status: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n if (typeof body['paused'] !== 'boolean') return refusal(400, 'invalid-request', 'Say whether to pause the mailbox.')\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before pausing or resuming it.')\n }\n const next: OutreachMailboxStatus = body['paused'] ? 'paused' : 'connected'\n if (mailbox.status === next) return ok({ ok: true, mailbox })\n const nowMs = deps.now()\n // A member's pause or resume answers an automatic pause (AGL-2981): its\n // reason goes, and the next bounce is judged as new evidence.\n await mailboxRef(deps.firestore(), gate.orgId, mailbox.id).update({\n status: next,\n autoPause: null,\n updatedAtMs: nowMs,\n })\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n next === 'paused' ? 'Paused a mailbox in Sequences' : 'Resumed a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, mailbox: { ...mailbox, status: next, autoPause: null, updatedAtMs: nowMs } })\n }\n\n const test: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n if (mailbox.status === 'reconnect_required' || mailbox.status === 'disconnected') {\n return refusal(409, 'reconnect-required', 'Reconnect this mailbox before sending from it.')\n }\n if (!deps.readConfig().configured) return notConfigured()\n const limited = await deps.consumeRateLimit(`outreach-mailbox-test:${mailbox.id}`, {\n limit: OUTREACH_TEST_SENDS_PER_HOUR,\n windowMs: 60 * 60 * 1000,\n })\n if (!limited.allowed) {\n return refusal(429, 'rate-limited', `A mailbox can send ${OUTREACH_TEST_SENDS_PER_HOUR} tests an hour. Try again later.`)\n }\n /*\n * To the member's own address unless they name another (AGL-3228). A\n * test to oneself never leaves Google, so it arrives with no\n * Authentication-Results at all and cannot show whether the send-as\n * domain's DKIM and DMARC hold; an outside mailbox the member can read\n * is the only place that can be seen. The rate limit above is the\n * whole guard on who may be written to — it is the member's own\n * account sending, as it could from its own compose window.\n */\n const named = body['to']\n const to =\n named === undefined || named === null || String(named).trim() === ''\n ? mailbox.email\n : normalizeContactEmail(String(named))\n if (!to) {\n return refusal(400, 'invalid-request', `\"${String(named).trim()}\" isn't a valid email address.`)\n }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await openOutreachMailboxClient(\n firestore,\n { mailboxId: mailbox.id },\n { ...deps.transport, readConfig: deps.readConfig, now: deps.now },\n )\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return notConfigured()\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'This mailbox needs to be connected again before it can send.')\n }\n try {\n const sent = await sendOutreachMessage(opened.client, {\n from: { address: mailbox.sendAs, name: mailbox.displayName },\n to,\n subject: 'Sequences test message',\n text: [\n 'This is a test message from Sequences.',\n '',\n `It was sent through the connected mailbox ${mailbox.email} as ${mailbox.sendAs}. ` +\n 'If it reached the inbox, this mailbox can send.',\n ...(to === mailbox.email\n ? []\n : [\n '',\n 'To read whether the sender is authenticated, open this message\\u2019s original source ' +\n 'and look for dkim=pass, spf=pass and dmarc=pass in Authentication-Results.',\n ]),\n ].join('\\n'),\n })\n return ok({ ok: true, sentTo: to, gmailMessageId: sent.gmailMessageId, sentAtMs: nowMs })\n } catch (error) {\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: gate.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return refusal(409, 'reconnect-required', 'Google refused this mailbox’s access. Connect it again.')\n }\n if (error instanceof Rfc5322MessageError) {\n return refusal(400, 'invalid-settings', `The test could not be written: ${error.message}`)\n }\n return googleUnavailable(error)\n }\n }\n\n const disconnect: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return methodNotAllowed('POST')\n const body = await readBody(request)\n const loaded = await loadManagedMailbox(request, body, 'member-or-admin')\n if (loaded instanceof Response) return loaded\n const { gate, mailbox } = loaded\n const firestore = deps.firestore()\n const credentialDoc = mailboxCredentialsRef(firestore, mailbox.id)\n const snapshot = await credentialDoc.get()\n const credential = readMailboxCredentials(snapshot.exists ? snapshot.data() : null)\n const revocation = credential\n ? await revokeMailboxGrant(firestore, credential, deps)\n : ('already-invalid' as const)\n const batch = firestore.batch()\n batch.delete(credentialDoc)\n batch.delete(mailboxRef(firestore, gate.orgId, mailbox.id))\n await batch.commit()\n await deps.logOrgActivity(\n gate.orgId,\n { uid: gate.uid, email: gate.email },\n 'Disconnected a mailbox in Sequences',\n { type: OUTREACH_MAILBOX_ACTIVITY_TARGET, id: mailbox.id, name: activityName(mailbox) },\n )\n return ok({ ok: true, revocation } satisfies OutreachMailboxDisconnectResponse)\n }\n\n return { availability, connect, oauthCallback, connectComplete, settings, status, test, disconnect }\n}\n\nfunction googleUnavailable(error: unknown): Response {\n if (!(error instanceof GmailTransportError)) throw error\n return refusal(\n 502,\n 'google-unavailable',\n error.retryable ? 'Google did not answer. Try again in a moment.' : `Google refused the request: ${error.message}`,\n )\n}\n"],"names":["normalizeContactEmail","createGmailClient","GmailTransportError","buildGoogleAuthorizationUrl","exchangeGoogleAuthorizationCode","grantedScopesInclude","OUTREACH_REQUIRED_GRANTED_SCOPES","pkceChallenge","readGoogleIdToken","Rfc5322MessageError","sendOutreachMessage","buildConnectReturnFragment","mailboxCredentialsRef","mailboxRef","outreachMailboxId","readMailboxCredentials","sealMailboxRefreshToken","revokeMailboxGrant","outreachMemberGate","refusal","defaultSendAs","isValidTimezone","OUTREACH_DEFAULT_DAILY_CAP","OUTREACH_DEFAULT_WINDOW","OUTREACH_MAX_MAILBOXES_PER_MEMBER","validateDailyCap","validateDisplayName","validateSendWindow","verifiedSendAsAddresses","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","consumeOutreachOAuthState","mintOutreachOAuthState","outreachOidcNonce","outreachPkceVerifier","readOutreachOAuthState","recordOutreachOAuthState","OUTREACH_NOT_CONFIGURED_MESSAGE","OUTREACH_TEST_SENDS_PER_HOUR","OUTREACH_MAILBOX_ACTIVITY_TARGET","MAILBOX_ID","MAX_CODE_CHARS","noStore","ok","body","Response","json","status","headers","methodNotAllowed","allow","error","reason","Allow","readBody","request","catch","Array","isArray","notConfigured","readMailbox","id","data","activityName","mailbox","email","createOutreachMailboxRoutes","deps","loadManagedMailbox","who","gate","mailboxId","test","snapshot","firestore","orgId","get","exists","undefined","own","connectedByUid","uid","isOrgAdmin","availability","method","URL","url","searchParams","missing","config","readConfig","configured","push","stateSigningConfigured","redirectUri","length","canManageAll","connect","nowMs","now","state","claims","clientId","codeChallenge","nonce","loginHint","oauthCallback","params","read","plain","text","org","readOrg","slug","fragment","googleError","code","kind","Location","encodeURIComponent","connectComplete","aliases","consumed","grant","clientSecret","codeVerifier","transport","googleUnavailable","refreshToken","scope","identity","idToken","client","profileEmail","sendAsOptions","profile","sendAs","Promise","all","getProfile","listSendAs","emailAddress","reconnectRequired","some","option","displayName","isPrimary","isDefault","o","sub","mailboxDoc","credentialDoc","existingMailbox","existingCredential","current","mine","collection","doc","where","limit","size","defaultOption","find","reconnected","keepSendAs","provider","connectedAtMs","updatedAtMs","timezone","String","name","dailyCap","window","days","rampStartedAtMs","health","sentToday","sentOnDay","bounces","replies","lastSentAtMs","lastErrorAtMs","lastErrorCode","daily","createdAtMs","credential","providerAccountId","scopes","split","filter","Boolean","keyring","Number","batch","set","merge","commit","confirmAliasesByProvider","addresses","map","console","logOrgActivity","type","created","confirmedAliases","confirmed","settings","loaded","update","invalid","message","trim","toLowerCase","cap","running","Object","keys","next","autoPause","limited","consumeRateLimit","windowMs","allowed","named","to","opened","errorCode","sent","from","address","subject","join","sentTo","gmailMessageId","sentAtMs","disconnect","revocation","delete","retryable"],"mappings":"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"}
@@ -88,13 +88,30 @@ export interface OutreachSequenceDeleteRequest {
88
88
  }
89
89
  /** The most people one preview or one enroll reads. */
90
90
  export declare const OUTREACH_ENROLL_BATCH_MAX = 50;
91
- /** Where the people to enroll come from: a saved Contacts view, or contacts picked by search. */
91
+ /**
92
+ * Which CRM record a person to enroll is (AGL-3234): a contact by its
93
+ * document id, or a lead the sequence's site holds by its person key.
94
+ */
95
+ export type OutreachPersonRef = {
96
+ kind: 'contact';
97
+ id: string;
98
+ } | {
99
+ kind: 'lead';
100
+ id: string;
101
+ };
102
+ /**
103
+ * Where the people to enroll come from: a saved Contacts or Leads view,
104
+ * contacts picked by search, or leads picked from the sequence's site.
105
+ */
92
106
  export type OutreachEnrollSource = {
93
107
  kind: 'view';
94
108
  viewId: string;
95
109
  } | {
96
110
  kind: 'contacts';
97
111
  contactIds: string[];
112
+ } | {
113
+ kind: 'leads';
114
+ leadIds: string[];
98
115
  };
99
116
  /** `POST outreach/enroll/preview` */
100
117
  export interface OutreachEnrollPreviewRequest {
@@ -117,7 +134,14 @@ export interface OutreachEnrollBlock {
117
134
  reason: string;
118
135
  }
119
136
  export interface OutreachEnrollPreviewPerson {
137
+ /** The key the dialog and the confirm name this person by: the record's own id. */
138
+ personId: string;
139
+ /** The record the person is (AGL-3234). */
140
+ target: 'contact' | 'lead';
141
+ /** The contact's id; `''` for a lead. */
120
142
  contactId: string;
143
+ /** The lead's person key; `null` for a contact. */
144
+ leadId: string | null;
121
145
  name: string;
122
146
  /** The address the steps would go to, or `null` when the contact has none. */
123
147
  email: string | null;
@@ -142,9 +166,10 @@ export interface OutreachEnrollPreviewResponse {
142
166
  /** True when the source named more people than one enrollment reads. */
143
167
  truncated: boolean;
144
168
  }
145
- /** One person to enroll, with what the rep supplied for them. */
169
+ /** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */
146
170
  export interface OutreachEnrollPersonRequest {
147
- contactId: string;
171
+ contactId?: string;
172
+ leadId?: string;
148
173
  personalLine?: string;
149
174
  attestations?: OutreachAttestationKind[];
150
175
  }
@@ -154,17 +179,20 @@ export interface OutreachEnrollRequest {
154
179
  sequenceId: string;
155
180
  people: OutreachEnrollPersonRequest[];
156
181
  }
157
- export type OutreachEnrollOutcome = {
182
+ interface OutreachEnrollOutcomePerson {
183
+ personId: string;
184
+ target: 'contact' | 'lead';
158
185
  contactId: string;
186
+ leadId: string | null;
159
187
  email: string | null;
188
+ }
189
+ export type OutreachEnrollOutcome = (OutreachEnrollOutcomePerson & {
160
190
  outcome: 'enrolled';
161
191
  enrollmentId: string;
162
- } | {
163
- contactId: string;
164
- email: string | null;
192
+ }) | (OutreachEnrollOutcomePerson & {
165
193
  outcome: 'blocked';
166
194
  blocks: OutreachEnrollBlock[];
167
- };
195
+ });
168
196
  export interface OutreachEnrollResponse {
169
197
  ok: true;
170
198
  results: OutreachEnrollOutcome[];
@@ -194,6 +222,8 @@ export interface OutreachPreviewRequest {
194
222
  sequenceId: string;
195
223
  /** The contact to write it to; a sample person when absent. */
196
224
  contactId?: string;
225
+ /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */
226
+ leadId?: string;
197
227
  personalLine?: string;
198
228
  /** Which step; the first email when absent. */
199
229
  stepIndex?: number;
@@ -208,3 +238,4 @@ export interface OutreachPreviewResponse {
208
238
  /** Why it could not be written, when it could not. */
209
239
  error: OutreachComposeError | null;
210
240
  }
241
+ export {};
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachEnrollment,\n OutreachSequence,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/** Where the people to enroll come from: a saved Contacts view, or contacts picked by search. */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\nexport interface OutreachEnrollPreviewPerson {\n contactId: string\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/** One person to enroll, with what the rep supplied for them. */\nexport interface OutreachEnrollPersonRequest {\n contactId: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n}\n\nexport type OutreachEnrollOutcome =\n | { contactId: string; email: string | null; outcome: 'enrolled'; enrollmentId: string }\n | { contactId: string; email: string | null; outcome: 'blocked'; blocks: OutreachEnrollBlock[] }\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAgHD;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach-api.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CONTRACT OF OUTREACH'S SETTINGS, SEQUENCE AND ENROLLMENT ROUTES\n * (AGL-2980): what the console sends, what the routes answer, and every\n * reason one refuses. Client-safe — types and the batch limit only.\n *\n * Every request names its organization (`orgId`, in the query of a `GET`\n * and the JSON body of a `POST`): the routes serve an organization-level\n * surface that names no site.\n */\n\nimport type { OutreachComposeError } from '../engine/compose'\nimport type { OutreachComplianceIssue } from './compliance-settings'\nimport type {\n OutreachAttestationKind,\n OutreachComplianceSettingsDocument,\n OutreachEnrollment,\n OutreachSequence,\n} from './outreach.types'\nimport type { OutreachSequenceDraft, OutreachSequenceIssue } from './sequence-draft'\n\n/** Every reason these routes refuse with, for a caller to branch on. */\nexport type OutreachRouteRefusalReason =\n | 'unauthenticated'\n | 'email-unverified'\n | 'org-required'\n | 'not-a-member'\n | 'not-org-wide'\n | 'permission'\n | 'entitlement'\n | 'method-not-allowed'\n | 'invalid-request'\n | 'invalid-settings'\n | 'invalid-sequence'\n | 'sequence-not-found'\n | 'sequence-archived'\n | 'sequence-not-active'\n | 'activation-refused'\n | 'delete-refused'\n | 'mailbox-unavailable'\n | 'enrollment-not-found'\n | 'transition-refused'\n | 'view-not-found'\n | 'view-unsupported'\n | 'contact-not-found'\n\n/** A refusal, in the one shape every route answers with. */\nexport interface OutreachRouteRefusal {\n /** A sentence written for the person reading it. */\n error: string\n reason: OutreachRouteRefusalReason\n /** The field-level reasons behind an `invalid-*` or `activation-refused` refusal. */\n issues?: ReadonlyArray<OutreachComplianceIssue | OutreachSequenceIssue>\n}\n\n/*==========================================\n * COMPLIANCE SETTINGS\n *==========================================*/\n\n/** `GET outreach/settings?orgId` — and the answer to a save. */\nexport interface OutreachSettingsResponse {\n ok: true\n settings: OutreachComplianceSettingsDocument\n}\n\n/** `POST outreach/settings` */\nexport interface OutreachSettingsSaveRequest {\n orgId: string\n legalName: string\n brandName: string\n postalAddress: string\n allowedCountries: string[]\n}\n\n/** The answer to a save: the settings as stored, and whether anything changed. */\nexport interface OutreachSettingsSaveResponse extends OutreachSettingsResponse {\n changed: boolean\n}\n\n/*==========================================\n * SEQUENCES\n *==========================================*/\n\n/** `POST outreach/sequences/save` — a new sequence without `sequenceId`, an edit with one. */\nexport interface OutreachSequenceSaveRequest {\n orgId: string\n sequenceId?: string\n sequence: OutreachSequenceDraft\n}\n\nexport interface OutreachSequenceSaveResponse {\n ok: true\n sequence: OutreachSequence\n created: boolean\n /** What the save allowed but probably isn't what was meant. */\n warnings: OutreachSequenceIssue[]\n}\n\n/** What `outreach/sequences/status` does to a sequence. */\nexport type OutreachSequenceAction = 'activate' | 'pause' | 'archive'\n\n/** `POST outreach/sequences/status` */\nexport interface OutreachSequenceStatusRequest {\n orgId: string\n sequenceId: string\n action: OutreachSequenceAction\n}\n\nexport interface OutreachSequenceStatusResponse {\n ok: true\n sequence: OutreachSequence\n /** The open enrollments an archive stopped; `0` for anything else. */\n stoppedEnrollments: number\n}\n\n/** `POST outreach/sequences/delete` — a draft nobody was ever enrolled in. */\nexport interface OutreachSequenceDeleteRequest {\n orgId: string\n sequenceId: string\n}\n\n/*==========================================\n * ENROLLING\n *==========================================*/\n\n/** The most people one preview or one enroll reads. */\nexport const OUTREACH_ENROLL_BATCH_MAX = 50\n\n/**\n * Which CRM record a person to enroll is (AGL-3234): a contact by its\n * document id, or a lead the sequence's site holds by its person key.\n */\nexport type OutreachPersonRef = { kind: 'contact'; id: string } | { kind: 'lead'; id: string }\n\n/**\n * Where the people to enroll come from: a saved Contacts or Leads view,\n * contacts picked by search, or leads picked from the sequence's site.\n */\nexport type OutreachEnrollSource =\n | { kind: 'view'; viewId: string }\n | { kind: 'contacts'; contactIds: string[] }\n | { kind: 'leads'; leadIds: string[] }\n\n/** `POST outreach/enroll/preview` */\nexport interface OutreachEnrollPreviewRequest {\n orgId: string\n sequenceId: string\n source: OutreachEnrollSource\n}\n\n/**\n * Where one person stands:\n *\n * - `eligible` — enrolled on Confirm as they are;\n * - `needs_confirmation` — enrolled once the rep writes the personal line\n * and ticks the attestations `requires` names;\n * - `blocked` — refused whatever the rep confirms, for the `blocks` given.\n */\nexport type OutreachEnrollPreviewStatus = 'eligible' | 'needs_confirmation' | 'blocked'\n\n/** One reason a person can't be enrolled, in a sentence a rep can act on. */\nexport interface OutreachEnrollBlock {\n code: string\n reason: string\n}\n\nexport interface OutreachEnrollPreviewPerson {\n /** The key the dialog and the confirm name this person by: the record's own id. */\n personId: string\n /** The record the person is (AGL-3234). */\n target: 'contact' | 'lead'\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key; `null` for a contact. */\n leadId: string | null\n name: string\n /** The address the steps would go to, or `null` when the contact has none. */\n email: string | null\n /** No inbound capture behind them — the heavier rules apply. */\n cold: boolean\n /** ISO-3166-1 alpha-2, when anything names one. */\n country: string | null\n status: OutreachEnrollPreviewStatus\n /** Why a `blocked` person is refused; empty otherwise. */\n blocks: OutreachEnrollBlock[]\n /** What the rep must supply before a `needs_confirmation` person is enrolled. */\n requires: {\n personalLine: boolean\n attestations: OutreachAttestationKind[]\n }\n}\n\nexport interface OutreachEnrollPreviewResponse {\n ok: true\n people: OutreachEnrollPreviewPerson[]\n /** Everyone the source named, before the batch limit. */\n total: number\n /** True when the source named more people than one enrollment reads. */\n truncated: boolean\n}\n\n/** One person to enroll, with what the rep supplied for them: a contact by id, or a lead by key. */\nexport interface OutreachEnrollPersonRequest {\n contactId?: string\n leadId?: string\n personalLine?: string\n attestations?: OutreachAttestationKind[]\n}\n\n/** `POST outreach/enroll` — the gates are checked again, here, before anything is written. */\nexport interface OutreachEnrollRequest {\n orgId: string\n sequenceId: string\n people: OutreachEnrollPersonRequest[]\n}\n\ninterface OutreachEnrollOutcomePerson {\n personId: string\n target: 'contact' | 'lead'\n contactId: string\n leadId: string | null\n email: string | null\n}\n\nexport type OutreachEnrollOutcome =\n | (OutreachEnrollOutcomePerson & { outcome: 'enrolled'; enrollmentId: string })\n | (OutreachEnrollOutcomePerson & { outcome: 'blocked'; blocks: OutreachEnrollBlock[] })\n\nexport interface OutreachEnrollResponse {\n ok: true\n results: OutreachEnrollOutcome[]\n enrolled: number\n}\n\n/*==========================================\n * ENROLLMENTS\n *==========================================*/\n\n/** What a member may do to one enrollment. */\nexport type OutreachEnrollmentAction = 'pause' | 'resume' | 'stop' | 'do_not_contact'\n\n/** `POST outreach/enrollments/action` */\nexport interface OutreachEnrollmentActionRequest {\n orgId: string\n enrollmentId: string\n action: OutreachEnrollmentAction\n /** Why, for a stop or a do-not-contact. */\n detail?: string\n}\n\nexport interface OutreachEnrollmentActionResponse {\n ok: true\n /** False when the enrollment was already where the action would put it. */\n changed: boolean\n enrollment: OutreachEnrollment\n /** Other open enrollments of the same address a do-not-contact stopped. */\n stoppedOthers: number\n}\n\n/*==========================================\n * PREVIEW\n *==========================================*/\n\n/** `POST outreach/preview` — one email of a saved sequence, as a person would get it. */\nexport interface OutreachPreviewRequest {\n orgId: string\n sequenceId: string\n /** The contact to write it to; a sample person when absent. */\n contactId?: string\n /** Or a lead the sequence's site holds (AGL-3234), written to as the contact it would be. */\n leadId?: string\n personalLine?: string\n /** Which step; the first email when absent. */\n stepIndex?: number\n}\n\nexport interface OutreachPreviewResponse {\n ok: true\n stepIndex: number\n subject: string\n /** The plain text, footer included. */\n text: string\n unresolvedFields: string[]\n /** Why it could not be written, when it could not. */\n error: OutreachComposeError | null\n}\n"],"names":["OUTREACH_ENROLL_BATCH_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,GAgHD;;4CAE4C,GAE5C,qDAAqD,GACrD,OAAO,MAAMA,4BAA4B,GAAE"}
@@ -402,16 +402,41 @@ export interface OutreachAttestation {
402
402
  export type OutreachAttestations = Partial<Record<OutreachAttestationKind, OutreachAttestation>>;
403
403
  /** The longest personal line an enrollment keeps: one sentence, not a letter. */
404
404
  export declare const OUTREACH_PERSONAL_LINE_MAX = 300;
405
+ /**
406
+ * Which CRM record an enrollment names (AGL-3234). A person is a LEAD until
407
+ * somebody qualifies them and a CONTACT after, and a sequence works either:
408
+ * an enrollment made on a lead follows the lead to the contact it becomes,
409
+ * so the thread and the steps carry on as one enrollment.
410
+ */
411
+ export declare const OUTREACH_ENROLLMENT_TARGETS: readonly ["contact", "lead"];
412
+ export type OutreachEnrollmentTarget = (typeof OUTREACH_ENROLLMENT_TARGETS)[number];
405
413
  /** One person in one sequence (`orgs/{orgId}/outreachEnrollments/{id}`). */
406
414
  export interface OutreachEnrollment extends OutreachTimestamps {
407
415
  id: string;
408
416
  sequenceId: string;
409
- /** The CRM contact the person is. */
417
+ /**
418
+ * The record the person is (AGL-3234): `contact` for a CRM contact,
419
+ * `lead` for a lead the sequence's site holds. A stored enrollment with
420
+ * no target is a contact's — every enrollment was, before leads could be
421
+ * sequenced.
422
+ */
423
+ target: OutreachEnrollmentTarget;
424
+ /**
425
+ * The CRM contact the person is — `''` while the enrollment targets a
426
+ * lead that has not converted. Filled in, beside `target: 'contact'`, the
427
+ * moment the lead becomes a contact.
428
+ */
410
429
  contactId: string;
411
430
  /**
412
- * The contact's name as the sending site knew it at enrollment, `''` when
431
+ * `hosts/{hostId}/leads/{leadId}` — the person key — for an enrollment
432
+ * made on a lead; kept once the lead converts, so the lead's page still
433
+ * lists the sequence. `null` for an enrollment made on a contact.
434
+ */
435
+ leadId: string | null;
436
+ /**
437
+ * The person's name as the sending site knew it at enrollment, `''` when
413
438
  * it had none — what the enrollments table shows beside the address,
414
- * without a read of every contact on the page (AGL-2980).
439
+ * without a read of every record on the page (AGL-2980).
415
440
  */
416
441
  contactName: string;
417
442
  /** The address the steps go to, normalized, captured at enrollment. */
@@ -196,6 +196,15 @@ export const OUTREACH_TASK_KIND_LABELS = {
196
196
  verified_deliverable: 'This address was verified as deliverable'
197
197
  };
198
198
  /** The longest personal line an enrollment keeps: one sentence, not a letter. */ export const OUTREACH_PERSONAL_LINE_MAX = 300;
199
+ /**
200
+ * Which CRM record an enrollment names (AGL-3234). A person is a LEAD until
201
+ * somebody qualifies them and a CONTACT after, and a sequence works either:
202
+ * an enrollment made on a lead follows the lead to the contact it becomes,
203
+ * so the thread and the steps carry on as one enrollment.
204
+ */ export const OUTREACH_ENROLLMENT_TARGETS = [
205
+ 'contact',
206
+ 'lead'
207
+ ];
199
208
  /** The id of the compliance document in the `settings` collection. */ export const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance';
200
209
  /*==========================================
201
210
  * DO NOT CONTACT (AGL-2980).