@aglyn/plugins-outreach 1.0.0-beta.151 → 1.0.0-beta.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -12
- package/src/lib/components/compliance-section.js +1 -1
- package/src/lib/components/compliance-section.js.map +1 -1
- package/src/lib/components/mailbox-card.d.ts +2 -0
- package/src/lib/components/mailbox-card.js +19 -3
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/use-outreach-mailbox-api.d.ts +2 -1
- package/src/lib/components/use-outreach-mailbox-api.js +5 -3
- package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
- package/src/lib/engine/compose.d.ts +7 -2
- package/src/lib/engine/compose.js +11 -5
- package/src/lib/engine/compose.js.map +1 -1
- package/src/lib/engine/opt-out-intent.js +1 -1
- package/src/lib/engine/opt-out-intent.js.map +1 -1
- package/src/lib/mailboxes/mailbox-api.d.ts +21 -2
- package/src/lib/mailboxes/mailbox-api.js.map +1 -1
- package/src/lib/mailboxes/mailbox-routes.js +41 -8
- package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
- package/src/lib/mailboxes/oauth-redirect.d.ts +15 -4
- package/src/lib/mailboxes/oauth-redirect.js +15 -6
- package/src/lib/mailboxes/oauth-redirect.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +1 -1
- package/src/lib/model/outreach.types.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 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 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 const configured =\n deps.readConfig().configured && deps.stateSigningConfigured() && Boolean(deps.redirectUri(request.url))\n return ok({ configured, canManageAll: gate.isOrgAdmin } 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 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: mailbox.email,\n subject: 'Sequences test message',\n text: [\n 'This is a test message from Sequences.',\n '',\n `It was sent through your connected mailbox as ${mailbox.sendAs}. ` +\n 'If it reached your inbox, this mailbox can send.',\n ].join('\\n'),\n })\n return ok({ ok: true, sentTo: mailbox.email, 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":["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","configured","readConfig","stateSigningConfigured","Boolean","redirectUri","canManageAll","connect","config","nowMs","now","state","claims","clientId","codeChallenge","nonce","loginHint","oauthCallback","params","read","plain","text","org","readOrg","slug","fragment","googleError","code","kind","length","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","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","opened","errorCode","sent","from","address","to","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 (!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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"}
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* Google redirects only to an address registered on the OAuth client, matched
|
|
21
21
|
* exactly, so the redirect cannot follow whichever host a rep happened to open
|
|
22
22
|
* the console on: it is the deployment's canonical console origin,
|
|
23
|
-
* `NEXT_PUBLIC_CONSOLE_URL
|
|
24
|
-
*
|
|
23
|
+
* `NEXT_PUBLIC_CONSOLE_URL` (the production console when unset, as every
|
|
24
|
+
* other reader of that variable treats it), plus the callback route. That is
|
|
25
|
+
* the one address an operator registers.
|
|
25
26
|
*
|
|
26
27
|
* Local development is the one exception, and never in production: a request
|
|
27
28
|
* from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer
|
|
@@ -32,10 +33,20 @@
|
|
|
32
33
|
*/
|
|
33
34
|
/** The callback's path on the console. */
|
|
34
35
|
export declare const OUTREACH_OAUTH_CALLBACK_PATH: string;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The console's own address when a deployment names none — the same fallback
|
|
38
|
+
* `apps/console/app/api/_lib/auth-action-url.ts` and the layout use, so an
|
|
39
|
+
* unset variable means the production console rather than "not configured".
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_CONSOLE_ORIGIN = "https://app.aglyn.com";
|
|
42
|
+
/**
|
|
43
|
+
* The canonical console origin: `NEXT_PUBLIC_CONSOLE_URL`, the production
|
|
44
|
+
* console when that is unset, or `null` when it is set to something that is
|
|
45
|
+
* not an http(s) address — a value nobody could have registered on the client.
|
|
46
|
+
*/
|
|
36
47
|
export declare function canonicalConsoleOrigin(): string | null;
|
|
37
48
|
/**
|
|
38
49
|
* The redirect address for a connect started by `requestUrl`, or `null` when
|
|
39
|
-
* the deployment
|
|
50
|
+
* the deployment's console origin is set to something unusable.
|
|
40
51
|
*/
|
|
41
52
|
export declare function outreachOAuthRedirectUri(requestUrl: string): string | null;
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* Google redirects only to an address registered on the OAuth client, matched
|
|
21
21
|
* exactly, so the redirect cannot follow whichever host a rep happened to open
|
|
22
22
|
* the console on: it is the deployment's canonical console origin,
|
|
23
|
-
* `NEXT_PUBLIC_CONSOLE_URL
|
|
24
|
-
*
|
|
23
|
+
* `NEXT_PUBLIC_CONSOLE_URL` (the production console when unset, as every
|
|
24
|
+
* other reader of that variable treats it), plus the callback route. That is
|
|
25
|
+
* the one address an operator registers.
|
|
25
26
|
*
|
|
26
27
|
* Local development is the one exception, and never in production: a request
|
|
27
28
|
* from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer
|
|
@@ -31,10 +32,18 @@
|
|
|
31
32
|
* code arrives, and a request header does not get to choose it.
|
|
32
33
|
*/ /** The callback's path on the console. */ export const OUTREACH_OAUTH_CALLBACK_PATH = `/api/${OUTREACH_API_ROUTES.mailboxesOAuthCallback}`;
|
|
33
34
|
const stripTrailingSlash = (value)=>value.replace(/\/+$/, '');
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The console's own address when a deployment names none — the same fallback
|
|
37
|
+
* `apps/console/app/api/_lib/auth-action-url.ts` and the layout use, so an
|
|
38
|
+
* unset variable means the production console rather than "not configured".
|
|
39
|
+
*/ export const DEFAULT_CONSOLE_ORIGIN = 'https://app.aglyn.com';
|
|
40
|
+
/**
|
|
41
|
+
* The canonical console origin: `NEXT_PUBLIC_CONSOLE_URL`, the production
|
|
42
|
+
* console when that is unset, or `null` when it is set to something that is
|
|
43
|
+
* not an http(s) address — a value nobody could have registered on the client.
|
|
44
|
+
*/ export function canonicalConsoleOrigin() {
|
|
35
45
|
var _process_env_NEXT_PUBLIC_CONSOLE_URL;
|
|
36
|
-
const raw = stripTrailingSlash(String((_process_env_NEXT_PUBLIC_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL) != null ? _process_env_NEXT_PUBLIC_CONSOLE_URL : '').trim());
|
|
37
|
-
if (!raw) return null;
|
|
46
|
+
const raw = stripTrailingSlash(String((_process_env_NEXT_PUBLIC_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL) != null ? _process_env_NEXT_PUBLIC_CONSOLE_URL : '').trim()) || DEFAULT_CONSOLE_ORIGIN;
|
|
38
47
|
try {
|
|
39
48
|
const url = new URL(raw);
|
|
40
49
|
return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : null;
|
|
@@ -44,7 +53,7 @@ const stripTrailingSlash = (value)=>value.replace(/\/+$/, '');
|
|
|
44
53
|
}
|
|
45
54
|
/**
|
|
46
55
|
* The redirect address for a connect started by `requestUrl`, or `null` when
|
|
47
|
-
* the deployment
|
|
56
|
+
* the deployment's console origin is set to something unusable.
|
|
48
57
|
*/ export function outreachOAuthRedirectUri(requestUrl) {
|
|
49
58
|
if (process.env['NODE_ENV'] !== 'production') {
|
|
50
59
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/oauth-redirect.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 { OUTREACH_API_ROUTES } from '../constants/api-routes'\n\n/**\n * WHERE GOOGLE SENDS A REP BACK TO (AGL-2978).\n *\n * Google redirects only to an address registered on the OAuth client, matched\n * exactly, so the redirect cannot follow whichever host a rep happened to open\n * the console on: it is the deployment's canonical console origin,\n * `NEXT_PUBLIC_CONSOLE_URL
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/mailboxes/oauth-redirect.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 { OUTREACH_API_ROUTES } from '../constants/api-routes'\n\n/**\n * WHERE GOOGLE SENDS A REP BACK TO (AGL-2978).\n *\n * Google redirects only to an address registered on the OAuth client, matched\n * exactly, so the redirect cannot follow whichever host a rep happened to open\n * the console on: it is the deployment's canonical console origin,\n * `NEXT_PUBLIC_CONSOLE_URL` (the production console when unset, as every\n * other reader of that variable treats it), plus the callback route. That is\n * the one address an operator registers.\n *\n * Local development is the one exception, and never in production: a request\n * from `http://localhost` or `127.0.0.1` is sent back to itself, so a developer\n * registering their local address on a test client is not bounced to the\n * deployed console after consenting. Anything else a request claims about its\n * origin is ignored — the host Google redirects to is where the authorization\n * code arrives, and a request header does not get to choose it.\n */\n\n/** The callback's path on the console. */\nexport const OUTREACH_OAUTH_CALLBACK_PATH = `/api/${OUTREACH_API_ROUTES.mailboxesOAuthCallback}`\n\nconst stripTrailingSlash = (value: string) => value.replace(/\\/+$/, '')\n\n/**\n * The console's own address when a deployment names none — the same fallback\n * `apps/console/app/api/_lib/auth-action-url.ts` and the layout use, so an\n * unset variable means the production console rather than \"not configured\".\n */\nexport const DEFAULT_CONSOLE_ORIGIN = 'https://app.aglyn.com'\n\n/**\n * The canonical console origin: `NEXT_PUBLIC_CONSOLE_URL`, the production\n * console when that is unset, or `null` when it is set to something that is\n * not an http(s) address — a value nobody could have registered on the client.\n */\nexport function canonicalConsoleOrigin(): string | null {\n const raw = stripTrailingSlash(String(process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim()) || DEFAULT_CONSOLE_ORIGIN\n try {\n const url = new URL(raw)\n return url.protocol === 'https:' || url.protocol === 'http:' ? url.origin : null\n } catch {\n return null\n }\n}\n\n/**\n * The redirect address for a connect started by `requestUrl`, or `null` when\n * the deployment's console origin is set to something unusable.\n */\nexport function outreachOAuthRedirectUri(requestUrl: string): string | null {\n if (process.env['NODE_ENV'] !== 'production') {\n try {\n const url = new URL(requestUrl)\n if (url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {\n return `${url.origin}${OUTREACH_OAUTH_CALLBACK_PATH}`\n }\n } catch {\n // Not a URL: fall through to the canonical origin.\n }\n }\n const origin = canonicalConsoleOrigin()\n return origin ? `${origin}${OUTREACH_OAUTH_CALLBACK_PATH}` : null\n}\n"],"names":["OUTREACH_API_ROUTES","OUTREACH_OAUTH_CALLBACK_PATH","mailboxesOAuthCallback","stripTrailingSlash","value","replace","DEFAULT_CONSOLE_ORIGIN","canonicalConsoleOrigin","process","raw","String","env","NEXT_PUBLIC_CONSOLE_URL","trim","url","URL","protocol","origin","outreachOAuthRedirectUri","requestUrl","hostname"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,6BAAyB;AAE7D;;;;;;;;;;;;;;;;CAgBC,GAED,wCAAwC,GACxC,OAAO,MAAMC,+BAA+B,CAAC,KAAK,EAAED,oBAAoBE,sBAAsB,EAAE,CAAA;AAEhG,MAAMC,qBAAqB,CAACC,QAAkBA,MAAMC,OAAO,CAAC,QAAQ;AAEpE;;;;CAIC,GACD,OAAO,MAAMC,yBAAyB,wBAAuB;AAE7D;;;;CAIC,GACD,OAAO,SAASC;QACwBC;IAAtC,MAAMC,MAAMN,mBAAmBO,QAAOF,uCAAAA,QAAQG,GAAG,CAACC,uBAAuB,YAAnCJ,uCAAuC,IAAIK,IAAI,OAAOP;IAC5F,IAAI;QACF,MAAMQ,MAAM,IAAIC,IAAIN;QACpB,OAAOK,IAAIE,QAAQ,KAAK,YAAYF,IAAIE,QAAQ,KAAK,UAAUF,IAAIG,MAAM,GAAG;IAC9E,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,SAASC,yBAAyBC,UAAkB;IACzD,IAAIX,QAAQG,GAAG,CAAC,WAAW,KAAK,cAAc;QAC5C,IAAI;YACF,MAAMG,MAAM,IAAIC,IAAII;YACpB,IAAIL,IAAIE,QAAQ,KAAK,WAAYF,CAAAA,IAAIM,QAAQ,KAAK,eAAeN,IAAIM,QAAQ,KAAK,WAAU,GAAI;gBAC9F,OAAO,GAAGN,IAAIG,MAAM,GAAGhB,8BAA8B;YACvD;QACF,EAAE,eAAM;QACN,mDAAmD;QACrD;IACF;IACA,MAAMgB,SAASV;IACf,OAAOU,SAAS,GAAGA,SAAShB,8BAA8B,GAAG;AAC/D"}
|
|
@@ -506,7 +506,7 @@ export interface OutreachOrgSettings {
|
|
|
506
506
|
legalName: string;
|
|
507
507
|
/**
|
|
508
508
|
* The name the solicitation sentence uses when it is not the legal name:
|
|
509
|
-
* "This is a
|
|
509
|
+
* "This is a sales email from Example Co." `''` uses the legal
|
|
510
510
|
* name.
|
|
511
511
|
*/
|
|
512
512
|
brandName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach.types.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 OUTREACH DOCUMENT MODEL (AGL-2974).\n *\n * One file both halves import — the console reads these documents, the server\n * routes write them — so a field is spelled once. Client-safe: types and\n * constants only, nothing that reaches Firestore.\n *\n * EVERY DOCUMENT HERE IS SERVER-WRITTEN. The Firestore rules let an\n * org-wide member holding `outreach.use` READ the org collections on a\n * workspace that carries `features.outreach`, and refuse every client write,\n * because these documents are the sending engine's state: a client that\n * could write a mailbox could lift its daily cap, and one that could write\n * an enrollment could send a step twice. The credential collection is closed\n * to clients entirely.\n *\n * Lean on purpose. Each type carries what the mailbox and sequence work is\n * known to need and nothing speculative; a field is added where the code\n * that reads it lands. Times are epoch milliseconds throughout, the unit\n * `nextDueAtMs` has to be compared in.\n */\n\n/**\n * Where Outreach keeps its records.\n *\n * Five collections under the organization, and one at the top level. The\n * org-scoped ones are erased with the org, whose erasure deletes the whole\n * `orgs/{orgId}` tree. The top-level one holds credentials, so it is keyed by\n * an `orgId` FIELD that a path-scoped delete of `orgs/{orgId}` cannot see:\n * the org erasure sweeps it by that field\n * (`libs/tenant/data/admin/src/lib/server/erase.ts` names it as a literal,\n * which the boundary forces, and `outreach.types.spec.ts` holds the two\n * spellings together, along with the rules').\n */\nexport const OUTREACH_COLLECTIONS = {\n /** `orgs/{orgId}/outreachMailboxes/{mailboxId}` — a connected mailbox. */\n mailboxes: 'outreachMailboxes',\n /** `orgs/{orgId}/outreachSequences/{sequenceId}` — the steps to send. */\n sequences: 'outreachSequences',\n /** `orgs/{orgId}/outreachEnrollments/{enrollmentId}` — one person in one sequence. */\n enrollments: 'outreachEnrollments',\n /**\n * `orgs/{orgId}/outreachSettings/{settingsId}` — the organization's own\n * Outreach settings, one document per concern. `compliance` holds who\n * every email says sent it and the countries Outreach may send to.\n */\n settings: 'outreachSettings',\n /**\n * `orgs/{orgId}/outreachDoNotContact/{key}` — the addresses Outreach never\n * emails for this organization, keyed by `outreachDoNotContactKey`.\n */\n doNotContact: 'outreachDoNotContact',\n /**\n * `outreachMailboxCredentials/{mailboxId}` — the provider grant behind a\n * mailbox. TOP-LEVEL and closed to every client, staff included, so the\n * token material never shares a readable path with the mailbox it serves.\n */\n mailboxCredentials: 'outreachMailboxCredentials',\n} as const\n\n/** The org-scoped collections, by their {@link OUTREACH_COLLECTIONS} key. */\nexport type OutreachOrgCollection = Exclude<\n keyof typeof OUTREACH_COLLECTIONS,\n 'mailboxCredentials'\n>\n\n/** `orgs/{orgId}/<collection>` for one of the org-scoped collections. */\nexport function outreachOrgCollectionPath(\n orgId: string,\n collection: OutreachOrgCollection,\n): string {\n return `orgs/${orgId}/${OUTREACH_COLLECTIONS[collection]}`\n}\n\n/** Stamped by the server on every write. */\nexport interface OutreachTimestamps {\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** The mail providers a mailbox can be connected through. */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google'] as const\nexport type OutreachMailboxProvider =\n (typeof OUTREACH_MAILBOX_PROVIDERS)[number]\n\n/**\n * Where a mailbox stands.\n *\n * - `connected` — the grant works and the mailbox may send.\n * - `paused` — a member stopped it; enrollments on it wait.\n * - `reconnect_required` — the provider refused the grant (revoked, expired,\n * password changed); nothing sends until the rep connects it again.\n * - `disconnected` — a member removed it; its credential is gone.\n */\nexport const OUTREACH_MAILBOX_STATUSES = [\n 'connected',\n 'paused',\n 'reconnect_required',\n 'disconnected',\n] as const\nexport type OutreachMailboxStatus = (typeof OUTREACH_MAILBOX_STATUSES)[number]\n\n/**\n * The hours a mailbox sends in, in its own {@link OutreachMailbox.timezone}.\n * A step that comes due outside the window waits for the next opening.\n */\nexport interface OutreachSendWindow {\n /** Days of the week it sends on, `0` Sunday through `6` Saturday. */\n days: number[]\n /** Minutes after local midnight the window opens, inclusive. */\n startMinute: number\n /** Minutes after local midnight the window closes, exclusive. */\n endMinute: number\n}\n\n/**\n * One mailbox-local day's sending, as the health panel sums it (AGL-2978).\n */\nexport interface OutreachMailboxDailyHealth {\n sent: number\n bounces: number\n replies: number\n}\n\n/**\n * The counters a mailbox's sending is judged by.\n *\n * `sentToday` is what `dailyCap` refuses against, and it belongs to one\n * mailbox-local day — `sentOnDay` — so a counter from yesterday is read as\n * zero rather than as today's spend.\n */\nexport interface OutreachMailboxHealth {\n sentToday: number\n /** The local day (`YYYY-MM-DD` in the mailbox's timezone) `sentToday` counts. */\n sentOnDay: string | null\n /** Hard bounces on mail this mailbox sent. */\n bounces: number\n /** Replies detected on Outreach threads. */\n replies: number\n lastSentAtMs: number | null\n /** The last provider error, kept until a later send succeeds. */\n lastErrorAtMs: number | null\n lastErrorCode: string | null\n /**\n * Per mailbox-local day (`YYYY-MM-DD`), the sends, bounces and replies\n * counted on it (AGL-2978). The Mailboxes panel shows the last seven days\n * summed; the sending runtime increments the day it counts on and may drop\n * days older than a week. Absent, or empty, until the first send.\n */\n daily?: Record<string, OutreachMailboxDailyHealth>\n /**\n * The mailbox's most recent sends, newest last, at most\n * `OUTREACH_BOUNCE_RATE_WINDOW_SENDS` of them, each marked when a hard\n * bounce came back for it (AGL-2981): the window the bounce-rate pause is\n * judged over. A send is named by a digest of its `Message-ID`, never by\n * its recipient.\n */\n recentSends?: OutreachRecentSend[]\n /** When a reply last called this mailbox's email spam (AGL-2981). */\n lastComplaintAtMs?: number | null\n}\n\n/** One send in {@link OutreachMailboxHealth.recentSends}. */\nexport interface OutreachRecentSend {\n /** A digest of the send's `Message-ID`: what a bounce is matched to. */\n id: string\n atMs: number\n bounced: boolean\n}\n\n/** Why a mailbox paused itself (AGL-2981): the engine's health decision, kept. */\nexport interface OutreachMailboxAutoPause {\n reason: 'bounces_today' | 'bounce_rate' | 'complaint'\n /** The sentence the mailbox's card shows. */\n message: string\n atMs: number\n /** For a complaint, when resuming stops being premature; `null` otherwise. */\n untilMs: number | null\n}\n\n/** Where a mailbox's reply and bounce sync has read to (AGL-2981). */\nexport interface OutreachMailboxSync {\n /** Everything received before this was read by an earlier run, epoch ms. */\n throughMs: number\n /** Gmail ids of messages outside any enrollment's thread already handled. */\n handledMessageIds: string[]\n}\n\n/**\n * An address a connected Google account may send as, which Gmail has\n * verified — its own address, or an alias whose ownership Gmail confirmed\n * (AGL-2978). Only these are offered as a mailbox's `sendAs`.\n */\nexport interface OutreachSendAsAddress {\n email: string\n /** The name Gmail holds for the address, `''` when it holds none. */\n displayName: string\n /** The account's own address. */\n isPrimary: boolean\n /** The address Gmail sends as by default. */\n isDefault: boolean\n}\n\n/**\n * A rep's own mailbox, connected to send Outreach mail\n * (`orgs/{orgId}/outreachMailboxes/{id}`).\n *\n * Its grant lives in {@link OutreachMailboxCredentials} under the same id.\n */\nexport interface OutreachMailbox extends OutreachTimestamps {\n id: string\n provider: OutreachMailboxProvider\n /** The account's own address. */\n email: string\n /** The address mail goes out as: the account's, or an alias it may send as. */\n sendAs: string\n /**\n * The verified addresses `sendAs` may be, as Gmail listed them at the last\n * connect (AGL-2978). A changed alias shows up here on the next reconnect.\n */\n sendAsOptions: OutreachSendAsAddress[]\n /** The From name recipients see. */\n displayName: string\n status: OutreachMailboxStatus\n /** The most mail it sends in one local day, ramp included. */\n dailyCap: number\n window: OutreachSendWindow\n /** IANA zone the window and the day boundary are read in. */\n timezone: string\n /**\n * When the warm-up ramp began, or `null` for a mailbox sending at its full\n * cap. The ramp is measured from here, not from connection, so a paused\n * mailbox can restart it.\n */\n rampStartedAtMs: number | null\n health: OutreachMailboxHealth\n /** The member who connected it, and whose mail it is. */\n connectedByUid: string\n /** When the grant behind it was last connected (AGL-2978). */\n connectedAtMs: number\n /**\n * Set when the mailbox paused ITSELF on its health (AGL-2981), and cleared\n * when a member pauses or resumes it; absent otherwise.\n */\n autoPause?: OutreachMailboxAutoPause | null\n /** The reply and bounce sync's place in the mailbox (AGL-2981). */\n sync?: OutreachMailboxSync | null\n}\n\n/**\n * The provider grant behind one mailbox\n * (`outreachMailboxCredentials/{mailboxId}`).\n *\n * Only the fields every reader relies on are declared here; the token\n * material is added by the code that stores it. `orgId` is REQUIRED, not\n * decorative: the org erasure finds these documents by it, and a credential\n * written without one would outlive its workspace.\n *\n * NAME EVERY SEALED FIELD WITH A SECRET WORD — `refreshToken`,\n * `tokenCiphertext`. The personal-data export discloses these documents with\n * secrets redacted, and its redaction reads field names first: a name\n * carrying `token`, `secret` or `credential` is withheld whatever its value,\n * while a neutral name such as `ciphertext` is judged by the value's shape\n * alone and can be disclosed.\n */\nexport interface OutreachMailboxCredentials extends OutreachTimestamps {\n /** The mailbox id, which is also this document's id. */\n id: string\n orgId: string\n mailboxId: string\n provider: OutreachMailboxProvider\n}\n\n/** A sequence's lifecycle. Only an `active` sequence advances its enrollments. */\nexport const OUTREACH_SEQUENCE_STATUSES = [\n 'draft',\n 'active',\n 'paused',\n 'archived',\n] as const\nexport type OutreachSequenceStatus =\n (typeof OUTREACH_SEQUENCE_STATUSES)[number]\n\n/*==========================================\n * SEQUENCES (AGL-2979).\n *\n * The limits are the outbound playbook's: a person gets at most four emails\n * from one sequence, spaced by business days, and the tasks between them are\n * the rep's own touches — a LinkedIn note, a call. The engine\n * (`../engine/sequence-validation.ts`) holds a stored sequence to them.\n *==========================================*/\n\n/** The most steps one sequence holds, emails and tasks together. */\nexport const OUTREACH_MAX_STEPS = 8\n\n/** The most of those steps that send an email. */\nexport const OUTREACH_MAX_EMAIL_STEPS = 4\n\n/** The longest wait one step may carry, in business days. */\nexport const OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS = 30\n\n/**\n * The shortest wait before an email that follows an earlier email. The first\n * email may wait `0` — \"the next opening of the window\", or the same day as\n * a task before it — and so may a task, but each later email waits at least\n * this long after the step before it.\n */\nexport const OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS = 1\n\n/**\n * One email step.\n *\n * Steps are a union on `kind`, so a later kind joins without reshaping the\n * ones already stored.\n */\nexport interface OutreachEmailStep {\n /** Stable within the sequence, so an edit that reorders steps is traceable. */\n id: string\n kind: 'email'\n /**\n * Business days to wait after the previous step, counted in the mailbox's\n * zone — after enrollment for the first step, where `0` means the next\n * opening of the sending window.\n */\n delayBusinessDays: number\n /**\n * The subject, merge fields allowed. Required on the first email and on\n * any email that starts a new thread; an in-thread email is sent as `Re:`\n * the thread's own subject, so its field is not read.\n */\n subject: string\n /**\n * Whether the email goes out as a reply in the thread the earlier emails\n * started. True by default for every email after the first; the first\n * email always starts the thread, whatever this says.\n */\n replyInThread: boolean\n /** Plain text with merge fields, as its author wrote it; `''` when `templateId` is set. */\n body: string\n /**\n * A CRM email template (`orgs/{orgId}/crmEmailTemplates/{id}`) whose body\n * is sent instead of `body`, or `null`. Only the body is taken from it: the\n * step's own subject is the one that is sent.\n */\n templateId: string | null\n}\n\n/** The rep's own touches a task step asks for. */\nexport const OUTREACH_TASK_KINDS = ['linkedin', 'call', 'todo'] as const\nexport type OutreachTaskKind = (typeof OUTREACH_TASK_KINDS)[number]\n\nexport const OUTREACH_TASK_KIND_LABELS: Record<OutreachTaskKind, string> = {\n linkedin: 'LinkedIn',\n call: 'Call',\n todo: 'To-do',\n}\n\n/** One task step: a CRM task for the rep, created when the step comes due. */\nexport interface OutreachTaskStep {\n id: string\n kind: 'task'\n taskKind: OutreachTaskKind\n /** What the task says, e.g. \"Connect on LinkedIn\". */\n title: string\n /** Business days after the previous step; `0` is the same day. */\n delayBusinessDays: number\n}\n\nexport type OutreachSequenceStep = OutreachEmailStep | OutreachTaskStep\n\n/** The countries a new sequence sends to: the United States alone. */\nexport const OUTREACH_DEFAULT_ALLOWED_COUNTRIES: readonly string[] = ['US']\n\n/** Behavior a sequence applies to every enrollment in it. */\nexport interface OutreachSequenceSettings {\n /**\n * The hours this sequence sends in, replacing its mailbox's own window;\n * `null` sends in the mailbox's. Read in the mailbox's zone either way.\n */\n window: OutreachSendWindow | null\n /**\n * ISO-3166-1 alpha-2 codes a recipient may be in, uppercase. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}: Canada, the United Kingdom\n * and most of the EU need a consent basis a cold email does not have.\n */\n allowedCountries: string[]\n /** Whether a contact who is already a customer may be enrolled. Default `false`. */\n allowCustomers: boolean\n}\n\n/** An ordered set of steps sent from one mailbox (`orgs/{orgId}/outreachSequences/{id}`). */\nexport interface OutreachSequence extends OutreachTimestamps {\n id: string\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /**\n * The mailbox every email step is sent from. A setting of the sequence,\n * kept beside `settings` rather than inside it because enrollments and a\n * mailbox's own screens select sequences by it.\n */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n}\n\n/*==========================================\n * ENROLLMENTS (AGL-2979).\n *==========================================*/\n\n/**\n * Where one person's run through a sequence stands.\n *\n * `active` sends and `paused` waits. The rest end the sending, and each says\n * why: `finished` sent every step, `replied` heard back, `bounced` reached a\n * mailbox that does not exist, `opted_out` was asked to stop, `stopped` was\n * ended by a member or a gate, `failed` could not be sent. The transitions\n * between them are the engine's (`../engine/enrollment-state.ts`).\n */\nexport const OUTREACH_ENROLLMENT_STATUSES = [\n 'active',\n 'paused',\n 'finished',\n 'replied',\n 'bounced',\n 'opted_out',\n 'stopped',\n 'failed',\n] as const\nexport type OutreachEnrollmentStatus =\n (typeof OUTREACH_ENROLLMENT_STATUSES)[number]\n\n/**\n * Why an enrollment is not `active`, recorded beside the status. `finished`\n * needs none; each other status allows the reasons\n * `OUTREACH_STOP_REASONS_BY_STATUS` names.\n */\nexport const OUTREACH_STOP_REASONS = [\n /** A person wrote back in the thread. */\n 'reply',\n /** The recipient's server refused the address for good. */\n 'hard_bounce',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** The address is on the organization's do-not-contact list. */\n 'do_not_contact',\n /** A gate refused the next send — the person became a customer, say. */\n 'gate',\n /** A member paused or stopped it. */\n 'manual',\n /** The sequence was archived with the person still in it. */\n 'sequence_archived',\n /** The provider refused the send for good, or the email could not be composed. */\n 'send_failed',\n] as const\nexport type OutreachStopReason = (typeof OUTREACH_STOP_REASONS)[number]\n\n/** The statuses a stop reason is recorded with, and the reasons each allows. */\nexport const OUTREACH_STOP_REASONS_BY_STATUS: Readonly<\n Record<\n Exclude<OutreachEnrollmentStatus, 'active' | 'finished'>,\n readonly OutreachStopReason[]\n >\n> = {\n paused: ['manual'],\n replied: ['reply'],\n bounced: ['hard_bounce'],\n opted_out: ['opt_out_reply', 'unsubscribe', 'do_not_contact'],\n stopped: ['manual', 'gate', 'sequence_archived'],\n failed: ['send_failed'],\n}\n\n/**\n * What a rep confirms before a COLD contact is enrolled — one with no\n * inbound capture behind them — each recorded with who confirmed it and\n * when, because each is a fact only the rep can know:\n *\n * - `us_business_address`: the address is a business address in the US;\n * - `published_or_given`: they or their company published it, or they gave\n * it to us — never guessed from a name or bought on a list;\n * - `verified_deliverable`: a verifier said it accepts mail.\n */\nexport const OUTREACH_ATTESTATION_KINDS = [\n 'us_business_address',\n 'published_or_given',\n 'verified_deliverable',\n] as const\nexport type OutreachAttestationKind = (typeof OUTREACH_ATTESTATION_KINDS)[number]\n\n/** The sentence a rep ticks for each attestation. */\nexport const OUTREACH_ATTESTATION_LABELS: Record<OutreachAttestationKind, string> = {\n us_business_address: 'This is a US business address',\n published_or_given:\n 'They or their company published this address, or they gave it to us',\n verified_deliverable: 'This address was verified as deliverable',\n}\n\n/** One confirmation, as stored. */\nexport interface OutreachAttestation {\n /** The member who confirmed it. */\n uid: string\n atMs: number\n}\n\n/** The confirmations an enrollment carries, by kind. */\nexport type OutreachAttestations = Partial<\n Record<OutreachAttestationKind, OutreachAttestation>\n>\n\n/** The longest personal line an enrollment keeps: one sentence, not a letter. */\nexport const OUTREACH_PERSONAL_LINE_MAX = 300\n\n/** One person in one sequence (`orgs/{orgId}/outreachEnrollments/{id}`). */\nexport interface OutreachEnrollment extends OutreachTimestamps {\n id: string\n sequenceId: string\n /** The CRM contact the person is. */\n contactId: string\n /**\n * The contact's name as the sending site knew it at enrollment, `''` when\n * it had none — what the enrollments table shows beside the address,\n * without a read of every contact on the page (AGL-2980).\n */\n contactName: string\n /** The address the steps go to, normalized, captured at enrollment. */\n email: string\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /** When that step comes due, or `null` when nothing is waiting. */\n nextDueAtMs: number | null\n status: OutreachEnrollmentStatus\n /** Why the status is not `active`; `null` while active and once finished. */\n stopReason: OutreachStopReason | null\n /** Plain-language detail: the gate's reason, the bounce's diagnostic. */\n stopDetail: string | null\n stoppedAtMs: number | null\n /** The member who paused or stopped it; `null` when the engine did. */\n stoppedByUid: string | null\n /**\n * The rep's signal sentence — why this person, now — merged into the\n * steps as `{{enrollment.personalLine}}`. Required for a cold contact.\n */\n personalLine: string\n /** Whether the contact was cold (no inbound capture) when enrolled. */\n cold: boolean\n attestations: OutreachAttestations\n /** The member who enrolled the person. */\n enrolledByUid: string\n /** The provider thread the next in-thread email is sent into, once one exists. */\n gmailThreadId: string | null\n /** Every provider thread this enrollment has sent into, oldest first — what reply sync watches. */\n gmailThreadIds: string[]\n /** The subject the current thread was started with, as sent. */\n threadSubject: string | null\n /**\n * The `Message-ID`s of the emails sent into the current thread, oldest\n * first, angle brackets included: the next in-thread email answers the\n * last and names them all in `References`.\n */\n messageIds: string[]\n /** When the last step ran. */\n lastSentAtMs: number | null\n /**\n * A sending run's hold on the step it is running (AGL-2981): taken in a\n * transaction before the step runs, cleared when it is recorded. Absent\n * or `null` while no run holds one.\n */\n sendClaim?: OutreachSendClaim | null\n /** Every step the runtime ran, oldest first (AGL-2981). */\n stepRecords?: OutreachStepRecord[]\n /** Gmail ids of this enrollment's thread messages the sync has handled, the last hundred. */\n syncedMessageIds?: string[]\n}\n\n/** A sending run's claim on one enrollment's step (AGL-2981). */\nexport interface OutreachSendClaim {\n /** The run that holds it. */\n token: string\n atMs: number\n stepIndex: number\n /**\n * The `Message-ID` an email step goes out with, minted before the send, so\n * a claim a run died holding is settled by looking for the message rather\n * than by sending it again. `null` for a task step.\n */\n messageId: string | null\n}\n\n/** One step the runtime ran for an enrollment (AGL-2981). */\nexport interface OutreachStepRecord {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n atMs: number\n /** Gmail's id for the sent message. */\n gmailMessageId?: string\n /** Gmail's thread the message went into. */\n gmailThreadId?: string\n /** The `Message-ID` it went out with, angle brackets included. */\n messageId?: string\n /** The subject as sent. */\n subject?: string\n /** The record system's task for a task step; `null` when none could be filed. */\n taskId?: string | null\n}\n\n/*==========================================\n * ORGANIZATION SETTINGS (AGL-2979).\n *==========================================*/\n\n/**\n * What every Outreach email's footer says about who sent it — the\n * identification and the postal address CAN-SPAM requires on commercial\n * mail. The composer refuses to write an email without a legal name and a\n * postal address, and a sequence cannot be activated without them.\n */\nexport interface OutreachOrgSettings {\n /** The organization's legal name, as the footer prints it: \"Example Co LLC\". */\n legalName: string\n /**\n * The name the solicitation sentence uses when it is not the legal name:\n * \"This is a business solicitation from Example Co.\" `''` uses the legal\n * name.\n */\n brandName: string\n /**\n * A valid physical postal address: a street address, a USPS PO box, or a\n * private mailbox registered with the USPS. Line breaks are printed as\n * commas.\n */\n postalAddress: string\n}\n\n/** The id of the compliance document in the `settings` collection. */\nexport const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance'\n\n/**\n * The organization's Outreach compliance settings\n * (`orgs/{orgId}/outreachSettings/compliance`): the footer's sender identity,\n * and the countries Outreach may send to at all.\n *\n * `allowedCountries` is a CEILING over every sequence's own list — a\n * sequence sends only to the countries both name — so narrowing it here\n * narrows every sequence at once, without editing any of them. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}.\n */\nexport interface OutreachComplianceSettings extends OutreachOrgSettings {\n /** ISO-3166-1 alpha-2 codes, uppercase, in the order they were chosen. */\n allowedCountries: string[]\n}\n\n/** The compliance settings as stored, with who changed them last. */\nexport interface OutreachComplianceSettingsDocument\n extends OutreachComplianceSettings {\n /** `0` until the organization first saves them. */\n updatedAtMs: number\n updatedByUid: string | null\n}\n\n/*==========================================\n * DO NOT CONTACT (AGL-2980).\n *\n * The organization's own list of addresses Outreach never emails, whoever\n * enrolls them and from whichever site. A member adds one by hand; the\n * sending runtime adds one when a reply asks to be left alone, when the\n * unsubscribe link is used, and when an address bounces for good.\n *\n * AN ENTRY CARRIES NO ADDRESS. Its id is `outreachDoNotContactKey(email)` —\n * the same hash the platform's suppression lists key by — and that is all a\n * lookup needs, since every caller holds the address it is about to use.\n * So the list keeps working after a person is erased from the workspace,\n * which is exactly when a promise not to email them must still hold, while\n * holding nothing that identifies them.\n *==========================================*/\n\n/** Why an address is on the list. */\nexport const OUTREACH_DO_NOT_CONTACT_REASONS = [\n /** A member put it there. */\n 'manual',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** Mail to it bounced for good. */\n 'hard_bounce',\n] as const\nexport type OutreachDoNotContactReason =\n (typeof OUTREACH_DO_NOT_CONTACT_REASONS)[number]\n\n/** What put an address on the list: a member, or the sending runtime. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCES = ['member', 'runtime'] as const\nexport type OutreachDoNotContactSource =\n (typeof OUTREACH_DO_NOT_CONTACT_SOURCES)[number]\n\n/** One address on the list (`orgs/{orgId}/outreachDoNotContact/{key}`). */\nexport interface OutreachDoNotContactEntry {\n /** `outreachDoNotContactKey` of the address, which is also the document id. */\n key: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member who added it; `null` when the runtime did. */\n addedByUid: string | null\n addedAtMs: number\n /** The enrollment that led here, when one did. */\n enrollmentId: string | null\n /** That enrollment's sequence. */\n sequenceId: string | null\n /** Plain-language detail: why the member added it, the bounce's diagnostic. */\n detail: string | null\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","mailboxCredentials","outreachOrgCollectionPath","orgId","collection","OUTREACH_MAILBOX_PROVIDERS","OUTREACH_MAILBOX_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_MAX_STEPS","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","OUTREACH_TASK_KIND_LABELS","linkedin","call","todo","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_STOP_REASONS_BY_STATUS","paused","replied","bounced","opted_out","stopped","failed","OUTREACH_ATTESTATION_KINDS","OUTREACH_ATTESTATION_LABELS","us_business_address","published_or_given","verified_deliverable","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMA,uBAAuB;IAClC,wEAAwE,GACxEC,WAAW;IACX,uEAAuE,GACvEC,WAAW;IACX,oFAAoF,GACpFC,aAAa;IACb;;;;GAIC,GACDC,UAAU;IACV;;;GAGC,GACDC,cAAc;IACd;;;;GAIC,GACDC,oBAAoB;AACtB,EAAU;AAQV,uEAAuE,GACvE,OAAO,SAASC,0BACdC,KAAa,EACbC,UAAiC;IAEjC,OAAO,CAAC,KAAK,EAAED,MAAM,CAAC,EAAER,oBAAoB,CAACS,WAAW,EAAE;AAC5D;AAQA,2DAA2D,GAC3D,OAAO,MAAMC,6BAA6B;IAAC;CAAS,CAAS;AAI7D;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IACvC;IACA;IACA;IACA;CACD,CAAS;AA8KV,gFAAgF,GAChF,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;;;;4CAO4C,GAE5C,kEAAkE,GAClE,OAAO,MAAMC,qBAAqB,EAAC;AAEnC,gDAAgD,GAChD,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,2DAA2D,GAC3D,OAAO,MAAMC,wCAAwC,GAAE;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,6CAA6C,EAAC;AAwC3D,gDAAgD,GAChD,OAAO,MAAMC,sBAAsB;IAAC;IAAY;IAAQ;CAAO,CAAS;AAGxE,OAAO,MAAMC,4BAA8D;IACzEC,UAAU;IACVC,MAAM;IACNC,MAAM;AACR,EAAC;AAeD,oEAAoE,GACpE,OAAO,MAAMC,qCAAwD;IAAC;CAAK,CAAA;AAoC3E;;4CAE4C,GAE5C;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAA+B;IAC1C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB;IACnC,uCAAuC,GACvC;IACA,yDAAyD,GACzD;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,8DAA8D,GAC9D;IACA,sEAAsE,GACtE;IACA,mCAAmC,GACnC;IACA,2DAA2D,GAC3D;IACA,gFAAgF,GAChF;CACD,CAAS;AAGV,8EAA8E,GAC9E,OAAO,MAAMC,kCAKT;IACFC,QAAQ;QAAC;KAAS;IAClBC,SAAS;QAAC;KAAQ;IAClBC,SAAS;QAAC;KAAc;IACxBC,WAAW;QAAC;QAAiB;QAAe;KAAiB;IAC7DC,SAAS;QAAC;QAAU;QAAQ;KAAoB;IAChDC,QAAQ;QAAC;KAAc;AACzB,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;CACD,CAAS;AAGV,mDAAmD,GACnD,OAAO,MAAMC,8BAAuE;IAClFC,qBAAqB;IACrBC,oBACE;IACFC,sBAAsB;AACxB,EAAC;AAcD,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6B,IAAG;AA6H7C,oEAAoE,GACpE,OAAO,MAAMC,kCAAkC,aAAY;AAyB3D;;;;;;;;;;;;;;4CAc4C,GAE5C,mCAAmC,GACnC,OAAO,MAAMC,kCAAkC;IAC7C,2BAA2B,GAC3B;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,iCAAiC,GACjC;CACD,CAAS;AAIV,uEAAuE,GACvE,OAAO,MAAMC,kCAAkC;IAAC;IAAU;CAAU,CAAS"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/model/outreach.types.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 OUTREACH DOCUMENT MODEL (AGL-2974).\n *\n * One file both halves import — the console reads these documents, the server\n * routes write them — so a field is spelled once. Client-safe: types and\n * constants only, nothing that reaches Firestore.\n *\n * EVERY DOCUMENT HERE IS SERVER-WRITTEN. The Firestore rules let an\n * org-wide member holding `outreach.use` READ the org collections on a\n * workspace that carries `features.outreach`, and refuse every client write,\n * because these documents are the sending engine's state: a client that\n * could write a mailbox could lift its daily cap, and one that could write\n * an enrollment could send a step twice. The credential collection is closed\n * to clients entirely.\n *\n * Lean on purpose. Each type carries what the mailbox and sequence work is\n * known to need and nothing speculative; a field is added where the code\n * that reads it lands. Times are epoch milliseconds throughout, the unit\n * `nextDueAtMs` has to be compared in.\n */\n\n/**\n * Where Outreach keeps its records.\n *\n * Five collections under the organization, and one at the top level. The\n * org-scoped ones are erased with the org, whose erasure deletes the whole\n * `orgs/{orgId}` tree. The top-level one holds credentials, so it is keyed by\n * an `orgId` FIELD that a path-scoped delete of `orgs/{orgId}` cannot see:\n * the org erasure sweeps it by that field\n * (`libs/tenant/data/admin/src/lib/server/erase.ts` names it as a literal,\n * which the boundary forces, and `outreach.types.spec.ts` holds the two\n * spellings together, along with the rules').\n */\nexport const OUTREACH_COLLECTIONS = {\n /** `orgs/{orgId}/outreachMailboxes/{mailboxId}` — a connected mailbox. */\n mailboxes: 'outreachMailboxes',\n /** `orgs/{orgId}/outreachSequences/{sequenceId}` — the steps to send. */\n sequences: 'outreachSequences',\n /** `orgs/{orgId}/outreachEnrollments/{enrollmentId}` — one person in one sequence. */\n enrollments: 'outreachEnrollments',\n /**\n * `orgs/{orgId}/outreachSettings/{settingsId}` — the organization's own\n * Outreach settings, one document per concern. `compliance` holds who\n * every email says sent it and the countries Outreach may send to.\n */\n settings: 'outreachSettings',\n /**\n * `orgs/{orgId}/outreachDoNotContact/{key}` — the addresses Outreach never\n * emails for this organization, keyed by `outreachDoNotContactKey`.\n */\n doNotContact: 'outreachDoNotContact',\n /**\n * `outreachMailboxCredentials/{mailboxId}` — the provider grant behind a\n * mailbox. TOP-LEVEL and closed to every client, staff included, so the\n * token material never shares a readable path with the mailbox it serves.\n */\n mailboxCredentials: 'outreachMailboxCredentials',\n} as const\n\n/** The org-scoped collections, by their {@link OUTREACH_COLLECTIONS} key. */\nexport type OutreachOrgCollection = Exclude<\n keyof typeof OUTREACH_COLLECTIONS,\n 'mailboxCredentials'\n>\n\n/** `orgs/{orgId}/<collection>` for one of the org-scoped collections. */\nexport function outreachOrgCollectionPath(\n orgId: string,\n collection: OutreachOrgCollection,\n): string {\n return `orgs/${orgId}/${OUTREACH_COLLECTIONS[collection]}`\n}\n\n/** Stamped by the server on every write. */\nexport interface OutreachTimestamps {\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** The mail providers a mailbox can be connected through. */\nexport const OUTREACH_MAILBOX_PROVIDERS = ['google'] as const\nexport type OutreachMailboxProvider =\n (typeof OUTREACH_MAILBOX_PROVIDERS)[number]\n\n/**\n * Where a mailbox stands.\n *\n * - `connected` — the grant works and the mailbox may send.\n * - `paused` — a member stopped it; enrollments on it wait.\n * - `reconnect_required` — the provider refused the grant (revoked, expired,\n * password changed); nothing sends until the rep connects it again.\n * - `disconnected` — a member removed it; its credential is gone.\n */\nexport const OUTREACH_MAILBOX_STATUSES = [\n 'connected',\n 'paused',\n 'reconnect_required',\n 'disconnected',\n] as const\nexport type OutreachMailboxStatus = (typeof OUTREACH_MAILBOX_STATUSES)[number]\n\n/**\n * The hours a mailbox sends in, in its own {@link OutreachMailbox.timezone}.\n * A step that comes due outside the window waits for the next opening.\n */\nexport interface OutreachSendWindow {\n /** Days of the week it sends on, `0` Sunday through `6` Saturday. */\n days: number[]\n /** Minutes after local midnight the window opens, inclusive. */\n startMinute: number\n /** Minutes after local midnight the window closes, exclusive. */\n endMinute: number\n}\n\n/**\n * One mailbox-local day's sending, as the health panel sums it (AGL-2978).\n */\nexport interface OutreachMailboxDailyHealth {\n sent: number\n bounces: number\n replies: number\n}\n\n/**\n * The counters a mailbox's sending is judged by.\n *\n * `sentToday` is what `dailyCap` refuses against, and it belongs to one\n * mailbox-local day — `sentOnDay` — so a counter from yesterday is read as\n * zero rather than as today's spend.\n */\nexport interface OutreachMailboxHealth {\n sentToday: number\n /** The local day (`YYYY-MM-DD` in the mailbox's timezone) `sentToday` counts. */\n sentOnDay: string | null\n /** Hard bounces on mail this mailbox sent. */\n bounces: number\n /** Replies detected on Outreach threads. */\n replies: number\n lastSentAtMs: number | null\n /** The last provider error, kept until a later send succeeds. */\n lastErrorAtMs: number | null\n lastErrorCode: string | null\n /**\n * Per mailbox-local day (`YYYY-MM-DD`), the sends, bounces and replies\n * counted on it (AGL-2978). The Mailboxes panel shows the last seven days\n * summed; the sending runtime increments the day it counts on and may drop\n * days older than a week. Absent, or empty, until the first send.\n */\n daily?: Record<string, OutreachMailboxDailyHealth>\n /**\n * The mailbox's most recent sends, newest last, at most\n * `OUTREACH_BOUNCE_RATE_WINDOW_SENDS` of them, each marked when a hard\n * bounce came back for it (AGL-2981): the window the bounce-rate pause is\n * judged over. A send is named by a digest of its `Message-ID`, never by\n * its recipient.\n */\n recentSends?: OutreachRecentSend[]\n /** When a reply last called this mailbox's email spam (AGL-2981). */\n lastComplaintAtMs?: number | null\n}\n\n/** One send in {@link OutreachMailboxHealth.recentSends}. */\nexport interface OutreachRecentSend {\n /** A digest of the send's `Message-ID`: what a bounce is matched to. */\n id: string\n atMs: number\n bounced: boolean\n}\n\n/** Why a mailbox paused itself (AGL-2981): the engine's health decision, kept. */\nexport interface OutreachMailboxAutoPause {\n reason: 'bounces_today' | 'bounce_rate' | 'complaint'\n /** The sentence the mailbox's card shows. */\n message: string\n atMs: number\n /** For a complaint, when resuming stops being premature; `null` otherwise. */\n untilMs: number | null\n}\n\n/** Where a mailbox's reply and bounce sync has read to (AGL-2981). */\nexport interface OutreachMailboxSync {\n /** Everything received before this was read by an earlier run, epoch ms. */\n throughMs: number\n /** Gmail ids of messages outside any enrollment's thread already handled. */\n handledMessageIds: string[]\n}\n\n/**\n * An address a connected Google account may send as, which Gmail has\n * verified — its own address, or an alias whose ownership Gmail confirmed\n * (AGL-2978). Only these are offered as a mailbox's `sendAs`.\n */\nexport interface OutreachSendAsAddress {\n email: string\n /** The name Gmail holds for the address, `''` when it holds none. */\n displayName: string\n /** The account's own address. */\n isPrimary: boolean\n /** The address Gmail sends as by default. */\n isDefault: boolean\n}\n\n/**\n * A rep's own mailbox, connected to send Outreach mail\n * (`orgs/{orgId}/outreachMailboxes/{id}`).\n *\n * Its grant lives in {@link OutreachMailboxCredentials} under the same id.\n */\nexport interface OutreachMailbox extends OutreachTimestamps {\n id: string\n provider: OutreachMailboxProvider\n /** The account's own address. */\n email: string\n /** The address mail goes out as: the account's, or an alias it may send as. */\n sendAs: string\n /**\n * The verified addresses `sendAs` may be, as Gmail listed them at the last\n * connect (AGL-2978). A changed alias shows up here on the next reconnect.\n */\n sendAsOptions: OutreachSendAsAddress[]\n /** The From name recipients see. */\n displayName: string\n status: OutreachMailboxStatus\n /** The most mail it sends in one local day, ramp included. */\n dailyCap: number\n window: OutreachSendWindow\n /** IANA zone the window and the day boundary are read in. */\n timezone: string\n /**\n * When the warm-up ramp began, or `null` for a mailbox sending at its full\n * cap. The ramp is measured from here, not from connection, so a paused\n * mailbox can restart it.\n */\n rampStartedAtMs: number | null\n health: OutreachMailboxHealth\n /** The member who connected it, and whose mail it is. */\n connectedByUid: string\n /** When the grant behind it was last connected (AGL-2978). */\n connectedAtMs: number\n /**\n * Set when the mailbox paused ITSELF on its health (AGL-2981), and cleared\n * when a member pauses or resumes it; absent otherwise.\n */\n autoPause?: OutreachMailboxAutoPause | null\n /** The reply and bounce sync's place in the mailbox (AGL-2981). */\n sync?: OutreachMailboxSync | null\n}\n\n/**\n * The provider grant behind one mailbox\n * (`outreachMailboxCredentials/{mailboxId}`).\n *\n * Only the fields every reader relies on are declared here; the token\n * material is added by the code that stores it. `orgId` is REQUIRED, not\n * decorative: the org erasure finds these documents by it, and a credential\n * written without one would outlive its workspace.\n *\n * NAME EVERY SEALED FIELD WITH A SECRET WORD — `refreshToken`,\n * `tokenCiphertext`. The personal-data export discloses these documents with\n * secrets redacted, and its redaction reads field names first: a name\n * carrying `token`, `secret` or `credential` is withheld whatever its value,\n * while a neutral name such as `ciphertext` is judged by the value's shape\n * alone and can be disclosed.\n */\nexport interface OutreachMailboxCredentials extends OutreachTimestamps {\n /** The mailbox id, which is also this document's id. */\n id: string\n orgId: string\n mailboxId: string\n provider: OutreachMailboxProvider\n}\n\n/** A sequence's lifecycle. Only an `active` sequence advances its enrollments. */\nexport const OUTREACH_SEQUENCE_STATUSES = [\n 'draft',\n 'active',\n 'paused',\n 'archived',\n] as const\nexport type OutreachSequenceStatus =\n (typeof OUTREACH_SEQUENCE_STATUSES)[number]\n\n/*==========================================\n * SEQUENCES (AGL-2979).\n *\n * The limits are the outbound playbook's: a person gets at most four emails\n * from one sequence, spaced by business days, and the tasks between them are\n * the rep's own touches — a LinkedIn note, a call. The engine\n * (`../engine/sequence-validation.ts`) holds a stored sequence to them.\n *==========================================*/\n\n/** The most steps one sequence holds, emails and tasks together. */\nexport const OUTREACH_MAX_STEPS = 8\n\n/** The most of those steps that send an email. */\nexport const OUTREACH_MAX_EMAIL_STEPS = 4\n\n/** The longest wait one step may carry, in business days. */\nexport const OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS = 30\n\n/**\n * The shortest wait before an email that follows an earlier email. The first\n * email may wait `0` — \"the next opening of the window\", or the same day as\n * a task before it — and so may a task, but each later email waits at least\n * this long after the step before it.\n */\nexport const OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS = 1\n\n/**\n * One email step.\n *\n * Steps are a union on `kind`, so a later kind joins without reshaping the\n * ones already stored.\n */\nexport interface OutreachEmailStep {\n /** Stable within the sequence, so an edit that reorders steps is traceable. */\n id: string\n kind: 'email'\n /**\n * Business days to wait after the previous step, counted in the mailbox's\n * zone — after enrollment for the first step, where `0` means the next\n * opening of the sending window.\n */\n delayBusinessDays: number\n /**\n * The subject, merge fields allowed. Required on the first email and on\n * any email that starts a new thread; an in-thread email is sent as `Re:`\n * the thread's own subject, so its field is not read.\n */\n subject: string\n /**\n * Whether the email goes out as a reply in the thread the earlier emails\n * started. True by default for every email after the first; the first\n * email always starts the thread, whatever this says.\n */\n replyInThread: boolean\n /** Plain text with merge fields, as its author wrote it; `''` when `templateId` is set. */\n body: string\n /**\n * A CRM email template (`orgs/{orgId}/crmEmailTemplates/{id}`) whose body\n * is sent instead of `body`, or `null`. Only the body is taken from it: the\n * step's own subject is the one that is sent.\n */\n templateId: string | null\n}\n\n/** The rep's own touches a task step asks for. */\nexport const OUTREACH_TASK_KINDS = ['linkedin', 'call', 'todo'] as const\nexport type OutreachTaskKind = (typeof OUTREACH_TASK_KINDS)[number]\n\nexport const OUTREACH_TASK_KIND_LABELS: Record<OutreachTaskKind, string> = {\n linkedin: 'LinkedIn',\n call: 'Call',\n todo: 'To-do',\n}\n\n/** One task step: a CRM task for the rep, created when the step comes due. */\nexport interface OutreachTaskStep {\n id: string\n kind: 'task'\n taskKind: OutreachTaskKind\n /** What the task says, e.g. \"Connect on LinkedIn\". */\n title: string\n /** Business days after the previous step; `0` is the same day. */\n delayBusinessDays: number\n}\n\nexport type OutreachSequenceStep = OutreachEmailStep | OutreachTaskStep\n\n/** The countries a new sequence sends to: the United States alone. */\nexport const OUTREACH_DEFAULT_ALLOWED_COUNTRIES: readonly string[] = ['US']\n\n/** Behavior a sequence applies to every enrollment in it. */\nexport interface OutreachSequenceSettings {\n /**\n * The hours this sequence sends in, replacing its mailbox's own window;\n * `null` sends in the mailbox's. Read in the mailbox's zone either way.\n */\n window: OutreachSendWindow | null\n /**\n * ISO-3166-1 alpha-2 codes a recipient may be in, uppercase. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}: Canada, the United Kingdom\n * and most of the EU need a consent basis a cold email does not have.\n */\n allowedCountries: string[]\n /** Whether a contact who is already a customer may be enrolled. Default `false`. */\n allowCustomers: boolean\n}\n\n/** An ordered set of steps sent from one mailbox (`orgs/{orgId}/outreachSequences/{id}`). */\nexport interface OutreachSequence extends OutreachTimestamps {\n id: string\n name: string\n /** The site whose CRM the enrolled people are records of. */\n hostId: string\n /**\n * The mailbox every email step is sent from. A setting of the sequence,\n * kept beside `settings` rather than inside it because enrollments and a\n * mailbox's own screens select sequences by it.\n */\n mailboxId: string\n steps: OutreachSequenceStep[]\n settings: OutreachSequenceSettings\n status: OutreachSequenceStatus\n}\n\n/*==========================================\n * ENROLLMENTS (AGL-2979).\n *==========================================*/\n\n/**\n * Where one person's run through a sequence stands.\n *\n * `active` sends and `paused` waits. The rest end the sending, and each says\n * why: `finished` sent every step, `replied` heard back, `bounced` reached a\n * mailbox that does not exist, `opted_out` was asked to stop, `stopped` was\n * ended by a member or a gate, `failed` could not be sent. The transitions\n * between them are the engine's (`../engine/enrollment-state.ts`).\n */\nexport const OUTREACH_ENROLLMENT_STATUSES = [\n 'active',\n 'paused',\n 'finished',\n 'replied',\n 'bounced',\n 'opted_out',\n 'stopped',\n 'failed',\n] as const\nexport type OutreachEnrollmentStatus =\n (typeof OUTREACH_ENROLLMENT_STATUSES)[number]\n\n/**\n * Why an enrollment is not `active`, recorded beside the status. `finished`\n * needs none; each other status allows the reasons\n * `OUTREACH_STOP_REASONS_BY_STATUS` names.\n */\nexport const OUTREACH_STOP_REASONS = [\n /** A person wrote back in the thread. */\n 'reply',\n /** The recipient's server refused the address for good. */\n 'hard_bounce',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** The address is on the organization's do-not-contact list. */\n 'do_not_contact',\n /** A gate refused the next send — the person became a customer, say. */\n 'gate',\n /** A member paused or stopped it. */\n 'manual',\n /** The sequence was archived with the person still in it. */\n 'sequence_archived',\n /** The provider refused the send for good, or the email could not be composed. */\n 'send_failed',\n] as const\nexport type OutreachStopReason = (typeof OUTREACH_STOP_REASONS)[number]\n\n/** The statuses a stop reason is recorded with, and the reasons each allows. */\nexport const OUTREACH_STOP_REASONS_BY_STATUS: Readonly<\n Record<\n Exclude<OutreachEnrollmentStatus, 'active' | 'finished'>,\n readonly OutreachStopReason[]\n >\n> = {\n paused: ['manual'],\n replied: ['reply'],\n bounced: ['hard_bounce'],\n opted_out: ['opt_out_reply', 'unsubscribe', 'do_not_contact'],\n stopped: ['manual', 'gate', 'sequence_archived'],\n failed: ['send_failed'],\n}\n\n/**\n * What a rep confirms before a COLD contact is enrolled — one with no\n * inbound capture behind them — each recorded with who confirmed it and\n * when, because each is a fact only the rep can know:\n *\n * - `us_business_address`: the address is a business address in the US;\n * - `published_or_given`: they or their company published it, or they gave\n * it to us — never guessed from a name or bought on a list;\n * - `verified_deliverable`: a verifier said it accepts mail.\n */\nexport const OUTREACH_ATTESTATION_KINDS = [\n 'us_business_address',\n 'published_or_given',\n 'verified_deliverable',\n] as const\nexport type OutreachAttestationKind = (typeof OUTREACH_ATTESTATION_KINDS)[number]\n\n/** The sentence a rep ticks for each attestation. */\nexport const OUTREACH_ATTESTATION_LABELS: Record<OutreachAttestationKind, string> = {\n us_business_address: 'This is a US business address',\n published_or_given:\n 'They or their company published this address, or they gave it to us',\n verified_deliverable: 'This address was verified as deliverable',\n}\n\n/** One confirmation, as stored. */\nexport interface OutreachAttestation {\n /** The member who confirmed it. */\n uid: string\n atMs: number\n}\n\n/** The confirmations an enrollment carries, by kind. */\nexport type OutreachAttestations = Partial<\n Record<OutreachAttestationKind, OutreachAttestation>\n>\n\n/** The longest personal line an enrollment keeps: one sentence, not a letter. */\nexport const OUTREACH_PERSONAL_LINE_MAX = 300\n\n/** One person in one sequence (`orgs/{orgId}/outreachEnrollments/{id}`). */\nexport interface OutreachEnrollment extends OutreachTimestamps {\n id: string\n sequenceId: string\n /** The CRM contact the person is. */\n contactId: string\n /**\n * The contact's name as the sending site knew it at enrollment, `''` when\n * it had none — what the enrollments table shows beside the address,\n * without a read of every contact on the page (AGL-2980).\n */\n contactName: string\n /** The address the steps go to, normalized, captured at enrollment. */\n email: string\n hostId: string\n mailboxId: string\n /** The index into `steps` of the NEXT step to run. */\n stepIndex: number\n /** When that step comes due, or `null` when nothing is waiting. */\n nextDueAtMs: number | null\n status: OutreachEnrollmentStatus\n /** Why the status is not `active`; `null` while active and once finished. */\n stopReason: OutreachStopReason | null\n /** Plain-language detail: the gate's reason, the bounce's diagnostic. */\n stopDetail: string | null\n stoppedAtMs: number | null\n /** The member who paused or stopped it; `null` when the engine did. */\n stoppedByUid: string | null\n /**\n * The rep's signal sentence — why this person, now — merged into the\n * steps as `{{enrollment.personalLine}}`. Required for a cold contact.\n */\n personalLine: string\n /** Whether the contact was cold (no inbound capture) when enrolled. */\n cold: boolean\n attestations: OutreachAttestations\n /** The member who enrolled the person. */\n enrolledByUid: string\n /** The provider thread the next in-thread email is sent into, once one exists. */\n gmailThreadId: string | null\n /** Every provider thread this enrollment has sent into, oldest first — what reply sync watches. */\n gmailThreadIds: string[]\n /** The subject the current thread was started with, as sent. */\n threadSubject: string | null\n /**\n * The `Message-ID`s of the emails sent into the current thread, oldest\n * first, angle brackets included: the next in-thread email answers the\n * last and names them all in `References`.\n */\n messageIds: string[]\n /** When the last step ran. */\n lastSentAtMs: number | null\n /**\n * A sending run's hold on the step it is running (AGL-2981): taken in a\n * transaction before the step runs, cleared when it is recorded. Absent\n * or `null` while no run holds one.\n */\n sendClaim?: OutreachSendClaim | null\n /** Every step the runtime ran, oldest first (AGL-2981). */\n stepRecords?: OutreachStepRecord[]\n /** Gmail ids of this enrollment's thread messages the sync has handled, the last hundred. */\n syncedMessageIds?: string[]\n}\n\n/** A sending run's claim on one enrollment's step (AGL-2981). */\nexport interface OutreachSendClaim {\n /** The run that holds it. */\n token: string\n atMs: number\n stepIndex: number\n /**\n * The `Message-ID` an email step goes out with, minted before the send, so\n * a claim a run died holding is settled by looking for the message rather\n * than by sending it again. `null` for a task step.\n */\n messageId: string | null\n}\n\n/** One step the runtime ran for an enrollment (AGL-2981). */\nexport interface OutreachStepRecord {\n stepIndex: number\n stepId: string\n kind: 'email' | 'task'\n atMs: number\n /** Gmail's id for the sent message. */\n gmailMessageId?: string\n /** Gmail's thread the message went into. */\n gmailThreadId?: string\n /** The `Message-ID` it went out with, angle brackets included. */\n messageId?: string\n /** The subject as sent. */\n subject?: string\n /** The record system's task for a task step; `null` when none could be filed. */\n taskId?: string | null\n}\n\n/*==========================================\n * ORGANIZATION SETTINGS (AGL-2979).\n *==========================================*/\n\n/**\n * What every Outreach email's footer says about who sent it — the\n * identification and the postal address CAN-SPAM requires on commercial\n * mail. The composer refuses to write an email without a legal name and a\n * postal address, and a sequence cannot be activated without them.\n */\nexport interface OutreachOrgSettings {\n /** The organization's legal name, as the footer prints it: \"Example Co LLC\". */\n legalName: string\n /**\n * The name the solicitation sentence uses when it is not the legal name:\n * \"This is a sales email from Example Co.\" `''` uses the legal\n * name.\n */\n brandName: string\n /**\n * A valid physical postal address: a street address, a USPS PO box, or a\n * private mailbox registered with the USPS. Line breaks are printed as\n * commas.\n */\n postalAddress: string\n}\n\n/** The id of the compliance document in the `settings` collection. */\nexport const OUTREACH_COMPLIANCE_SETTINGS_ID = 'compliance'\n\n/**\n * The organization's Outreach compliance settings\n * (`orgs/{orgId}/outreachSettings/compliance`): the footer's sender identity,\n * and the countries Outreach may send to at all.\n *\n * `allowedCountries` is a CEILING over every sequence's own list — a\n * sequence sends only to the countries both name — so narrowing it here\n * narrows every sequence at once, without editing any of them. Default\n * {@link OUTREACH_DEFAULT_ALLOWED_COUNTRIES}.\n */\nexport interface OutreachComplianceSettings extends OutreachOrgSettings {\n /** ISO-3166-1 alpha-2 codes, uppercase, in the order they were chosen. */\n allowedCountries: string[]\n}\n\n/** The compliance settings as stored, with who changed them last. */\nexport interface OutreachComplianceSettingsDocument\n extends OutreachComplianceSettings {\n /** `0` until the organization first saves them. */\n updatedAtMs: number\n updatedByUid: string | null\n}\n\n/*==========================================\n * DO NOT CONTACT (AGL-2980).\n *\n * The organization's own list of addresses Outreach never emails, whoever\n * enrolls them and from whichever site. A member adds one by hand; the\n * sending runtime adds one when a reply asks to be left alone, when the\n * unsubscribe link is used, and when an address bounces for good.\n *\n * AN ENTRY CARRIES NO ADDRESS. Its id is `outreachDoNotContactKey(email)` —\n * the same hash the platform's suppression lists key by — and that is all a\n * lookup needs, since every caller holds the address it is about to use.\n * So the list keeps working after a person is erased from the workspace,\n * which is exactly when a promise not to email them must still hold, while\n * holding nothing that identifies them.\n *==========================================*/\n\n/** Why an address is on the list. */\nexport const OUTREACH_DO_NOT_CONTACT_REASONS = [\n /** A member put it there. */\n 'manual',\n /** A reply asked not to be emailed again. */\n 'opt_out_reply',\n /** The unsubscribe link or header was used. */\n 'unsubscribe',\n /** Mail to it bounced for good. */\n 'hard_bounce',\n] as const\nexport type OutreachDoNotContactReason =\n (typeof OUTREACH_DO_NOT_CONTACT_REASONS)[number]\n\n/** What put an address on the list: a member, or the sending runtime. */\nexport const OUTREACH_DO_NOT_CONTACT_SOURCES = ['member', 'runtime'] as const\nexport type OutreachDoNotContactSource =\n (typeof OUTREACH_DO_NOT_CONTACT_SOURCES)[number]\n\n/** One address on the list (`orgs/{orgId}/outreachDoNotContact/{key}`). */\nexport interface OutreachDoNotContactEntry {\n /** `outreachDoNotContactKey` of the address, which is also the document id. */\n key: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member who added it; `null` when the runtime did. */\n addedByUid: string | null\n addedAtMs: number\n /** The enrollment that led here, when one did. */\n enrollmentId: string | null\n /** That enrollment's sequence. */\n sequenceId: string | null\n /** Plain-language detail: why the member added it, the bounce's diagnostic. */\n detail: string | null\n}\n"],"names":["OUTREACH_COLLECTIONS","mailboxes","sequences","enrollments","settings","doNotContact","mailboxCredentials","outreachOrgCollectionPath","orgId","collection","OUTREACH_MAILBOX_PROVIDERS","OUTREACH_MAILBOX_STATUSES","OUTREACH_SEQUENCE_STATUSES","OUTREACH_MAX_STEPS","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEP_DELAY_BUSINESS_DAYS","OUTREACH_MIN_EMAIL_FOLLOW_UP_BUSINESS_DAYS","OUTREACH_TASK_KINDS","OUTREACH_TASK_KIND_LABELS","linkedin","call","todo","OUTREACH_DEFAULT_ALLOWED_COUNTRIES","OUTREACH_ENROLLMENT_STATUSES","OUTREACH_STOP_REASONS","OUTREACH_STOP_REASONS_BY_STATUS","paused","replied","bounced","opted_out","stopped","failed","OUTREACH_ATTESTATION_KINDS","OUTREACH_ATTESTATION_LABELS","us_business_address","published_or_given","verified_deliverable","OUTREACH_PERSONAL_LINE_MAX","OUTREACH_COMPLIANCE_SETTINGS_ID","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;CAmBC,GAED;;;;;;;;;;;CAWC,GACD,OAAO,MAAMA,uBAAuB;IAClC,wEAAwE,GACxEC,WAAW;IACX,uEAAuE,GACvEC,WAAW;IACX,oFAAoF,GACpFC,aAAa;IACb;;;;GAIC,GACDC,UAAU;IACV;;;GAGC,GACDC,cAAc;IACd;;;;GAIC,GACDC,oBAAoB;AACtB,EAAU;AAQV,uEAAuE,GACvE,OAAO,SAASC,0BACdC,KAAa,EACbC,UAAiC;IAEjC,OAAO,CAAC,KAAK,EAAED,MAAM,CAAC,EAAER,oBAAoB,CAACS,WAAW,EAAE;AAC5D;AAQA,2DAA2D,GAC3D,OAAO,MAAMC,6BAA6B;IAAC;CAAS,CAAS;AAI7D;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B;IACvC;IACA;IACA;IACA;CACD,CAAS;AA8KV,gFAAgF,GAChF,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;;;;4CAO4C,GAE5C,kEAAkE,GAClE,OAAO,MAAMC,qBAAqB,EAAC;AAEnC,gDAAgD,GAChD,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,2DAA2D,GAC3D,OAAO,MAAMC,wCAAwC,GAAE;AAEvD;;;;;CAKC,GACD,OAAO,MAAMC,6CAA6C,EAAC;AAwC3D,gDAAgD,GAChD,OAAO,MAAMC,sBAAsB;IAAC;IAAY;IAAQ;CAAO,CAAS;AAGxE,OAAO,MAAMC,4BAA8D;IACzEC,UAAU;IACVC,MAAM;IACNC,MAAM;AACR,EAAC;AAeD,oEAAoE,GACpE,OAAO,MAAMC,qCAAwD;IAAC;CAAK,CAAA;AAoC3E;;4CAE4C,GAE5C;;;;;;;;CAQC,GACD,OAAO,MAAMC,+BAA+B;IAC1C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAIV;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB;IACnC,uCAAuC,GACvC;IACA,yDAAyD,GACzD;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,8DAA8D,GAC9D;IACA,sEAAsE,GACtE;IACA,mCAAmC,GACnC;IACA,2DAA2D,GAC3D;IACA,gFAAgF,GAChF;CACD,CAAS;AAGV,8EAA8E,GAC9E,OAAO,MAAMC,kCAKT;IACFC,QAAQ;QAAC;KAAS;IAClBC,SAAS;QAAC;KAAQ;IAClBC,SAAS;QAAC;KAAc;IACxBC,WAAW;QAAC;QAAiB;QAAe;KAAiB;IAC7DC,SAAS;QAAC;QAAU;QAAQ;KAAoB;IAChDC,QAAQ;QAAC;KAAc;AACzB,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B;IACxC;IACA;IACA;CACD,CAAS;AAGV,mDAAmD,GACnD,OAAO,MAAMC,8BAAuE;IAClFC,qBAAqB;IACrBC,oBACE;IACFC,sBAAsB;AACxB,EAAC;AAcD,+EAA+E,GAC/E,OAAO,MAAMC,6BAA6B,IAAG;AA6H7C,oEAAoE,GACpE,OAAO,MAAMC,kCAAkC,aAAY;AAyB3D;;;;;;;;;;;;;;4CAc4C,GAE5C,mCAAmC,GACnC,OAAO,MAAMC,kCAAkC;IAC7C,2BAA2B,GAC3B;IACA,2CAA2C,GAC3C;IACA,6CAA6C,GAC7C;IACA,iCAAiC,GACjC;CACD,CAAS;AAIV,uEAAuE,GACvE,OAAO,MAAMC,kCAAkC;IAAC;IAAU;CAAU,CAAS"}
|