@aglyn/plugins-inbox 1.0.0-beta.143

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +51 -0
  4. package/src/index.d.ts +18 -0
  5. package/src/index.js +19 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/contacts-card.component.d.ts +16 -0
  8. package/src/lib/components/contacts-card.component.js +351 -0
  9. package/src/lib/components/contacts-card.component.js.map +1 -0
  10. package/src/lib/components/inbox-attribution-zone.d.ts +52 -0
  11. package/src/lib/components/inbox-attribution-zone.js +53 -0
  12. package/src/lib/components/inbox-attribution-zone.js.map +1 -0
  13. package/src/lib/components/inbox-console-page.d.ts +28 -0
  14. package/src/lib/components/inbox-console-page.js +219 -0
  15. package/src/lib/components/inbox-console-page.js.map +1 -0
  16. package/src/lib/components/inbox-console-sections.d.ts +40 -0
  17. package/src/lib/components/inbox-console-sections.js +66 -0
  18. package/src/lib/components/inbox-console-sections.js.map +1 -0
  19. package/src/lib/components/inbox-glance-card.component.d.ts +26 -0
  20. package/src/lib/components/inbox-glance-card.component.js +215 -0
  21. package/src/lib/components/inbox-glance-card.component.js.map +1 -0
  22. package/src/lib/components/submission-list-assignment.component.d.ts +29 -0
  23. package/src/lib/components/submission-list-assignment.component.js +257 -0
  24. package/src/lib/components/submission-list-assignment.component.js.map +1 -0
  25. package/src/lib/components/submission-reply.component.d.ts +30 -0
  26. package/src/lib/components/submission-reply.component.js +249 -0
  27. package/src/lib/components/submission-reply.component.js.map +1 -0
  28. package/src/lib/components/submissions-card.component.d.ts +35 -0
  29. package/src/lib/components/submissions-card.component.js +577 -0
  30. package/src/lib/components/submissions-card.component.js.map +1 -0
  31. package/src/lib/components/use-record-route-context.d.ts +15 -0
  32. package/src/lib/components/use-record-route-context.js +41 -0
  33. package/src/lib/components/use-record-route-context.js.map +1 -0
  34. package/src/lib/constants/bundle-common.d.ts +8 -0
  35. package/src/lib/constants/bundle-common.js +9 -0
  36. package/src/lib/constants/bundle-common.js.map +1 -0
  37. package/src/lib/model/reply-policy.d.ts +134 -0
  38. package/src/lib/model/reply-policy.js +141 -0
  39. package/src/lib/model/reply-policy.js.map +1 -0
  40. package/src/lib/model/submission-presenter.d.ts +81 -0
  41. package/src/lib/model/submission-presenter.js +152 -0
  42. package/src/lib/model/submission-presenter.js.map +1 -0
  43. package/src/lib/plugin.d.ts +25 -0
  44. package/src/lib/plugin.js +102 -0
  45. package/src/lib/plugin.js.map +1 -0
  46. package/src/lib/server.d.ts +179 -0
  47. package/src/lib/server.js +666 -0
  48. package/src/lib/server.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/inbox/src/lib/server.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 Inbox's two acts on the person who sent a submission: answer them, and\n * put them on a marketing list.\n *\n * `POST inbox/reply` sends one message. `POST inbox/assign-list` enrolls, and\n * `POST inbox/list-options` tells the merchant what they may do before they\n * do it.\n *\n * ## The two acts are kept apart everywhere\n *\n * A reply is TRANSACTIONAL — the person asked to be contacted by submitting\n * the form, and answering them is the transaction they started. A list is\n * MARKETING — a standing invitation to mail them about things they never\n * asked about. So they are separate routes, separate records and separate\n * cards on screen, and neither is a side effect of the other: replying\n * enrolls nobody and writes no consent, and enrolling sends nothing and\n * meters nothing. Folding either into the other would make a merchant's\n * ordinary act of answering a customer into an act with consequences they\n * did not choose. The enrollment rule itself is the framework's\n * `list-assignment-policy`, shared with the Emails console's audience card so\n * that both surfaces refuse the same people for the same stated reasons.\n *\n * ## The boundary this feature sits on, stated because the UI must say it too\n *\n * The Inbox is not a mailbox. Nothing in the INBOX receives mail: no route\n * of its own, no MX record pointed at a submission, no parser. A submission\n * arrived as an HTTP POST, not as a message, so there is no `Message-ID` to\n * thread against and a reply is always the FIRST message in its conversation.\n * (The platform's one receiving door is the CRM's capture address —\n * `POST /api/crm/inbound` in the console app, AGL-2657 — which files a\n * forwarded or copied message on a contact's timeline. It threads nothing\n * here: a submission is not mail, and the Inbox's statement stands.)\n *\n * That decides two things:\n *\n * - **No `In-Reply-To` or `References` header is sent.** There is nothing to\n * put in them. Inventing an identifier would produce headers that reference\n * a message no mail server has ever seen, which threads nothing and makes\n * some filters treat the message as forged.\n * - **`Reply-To` is the sender's own console account address**, so when the\n * recipient answers, the answer reaches a human in a real mailbox. It does\n * not come back here, and the composer says so. A reply that went to the\n * platform's unmonitored address would be a message the merchant never sees\n * and the customer believes was received.\n *\n * The thread the merchant sees is ours, stored under the submission, and it\n * holds what we sent — never what came back, because nothing comes back.\n *\n * ## Sending identity\n *\n * The `From:` address is whatever THIS SITE sends as — `hostSendingIdentity`,\n * the same resolution a campaign gets — so a site with a domain of its own\n * replies on it, and a site without one replies on the shared pool, which is\n * correct for a reply because a reply is transactional.\n *\n * The display name varies separately, through `resolveBrandingProfile`, which\n * is entitlement-gated: a merchant without white-label replies under the\n * platform's name even on their own domain.\n *\n * `Reply-To` stays load-bearing either way, and not as a stopgap. Nothing\n * here receives mail, so the answer has to be routed to a mailbox somebody\n * actually reads — and `Reply-To` is the one header that may name an address\n * on a domain this platform has never verified.\n *\n * ## Consent and suppression\n *\n * A reply is transactional: the recipient asked to be contacted by submitting\n * the form, so no marketing-consent record is required and none is read. The\n * suppression lists still apply — both of them — and this is the first send\n * path in the product to consult BOTH on one address:\n *\n * - the platform list, through `isEmailSuppressed`, which fails closed;\n * - this site's list, under `hosts/{hostId}/suppressions`.\n *\n * Both are keyed with `emailSuppressionKey`, one derivation, so the two reads\n * cannot disagree about which document to look for.\n */\n\nimport {\n ASSIGNMENT_REFUSAL_MESSAGES,\n assignmentBasis,\n assignmentReadout,\n isOrgWideMember,\n readMarketingBasis,\n registerPluginApiRoute,\n resolveBrandingProfile,\n type AssignmentRefusal,\n type ConsentGroup,\n type MarketingConsentRecord,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport {\n emailSuppressionKey,\n enrollListMember,\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n isEmailSuppressed,\n meterHostEmail,\n consentGroupForSite,\n orgDataCollectionForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\n// The leaf, not the barrel: this plugin's specs substitute the barrel\n// wholesale, and the lookup must reach the real index logic under them.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n REPLY_BODY_MAX,\n REPLY_SUBJECT_MAX,\n composeReplyBody,\n replyRecipient,\n type ReplyRefusal,\n} from './model/reply-policy'\n\n/** Where a reply is stored, under the submission it answers. */\nexport const REPLIES_SUBCOLLECTION = 'replies'\n\n/** `context` on the send: the log label and the Resend attribution tag. */\nexport const REPLY_CONTEXT = 'inbox-reply'\n\n/** What a refusal says to the merchant. One line, naming the cause. */\nconst REFUSAL_MESSAGES: Record<ReplyRefusal, string> = {\n 'no-address': 'This submission has no email field, so there is nobody to reply to.',\n 'unroutable-address':\n 'The email address on this submission is not a valid address.',\n 'suppressed-platform':\n 'This address bounced or reported a message as spam, so it cannot be mailed.',\n 'suppressed-host':\n 'This address unsubscribed from this site, so it cannot be mailed.',\n}\n\n/**\n * Is this address suppressed, on either list?\n *\n * Order matters only for the reason reported, not the outcome. The platform\n * list is read first because it is the one that carries a bounce learned\n * anywhere — including on a send that named no site — and that is the more\n * useful thing to tell a merchant who is about to retype the message.\n *\n * Named for the ADDRESS rather than for either act, because both use it: a\n * reply must not go to a dead or complaining mailbox, and a list enrollment\n * must not put one on a standing audience. A second copy for the second act\n * would be two answers to \"may this address be mailed\".\n */\nexport async function addressSuppression(\n hostId: string,\n email: string,\n firestore?: unknown,\n): Promise<ReplyRefusal | null> {\n const key = emailSuppressionKey(email)\n // `emailSuppressionKey` returns null for an address it cannot key, and\n // `isEmailSuppressed` already answers `true` for that case. Treating it as\n // suppressed here keeps the two halves agreeing: an address we cannot key\n // is an address we cannot prove is safe to mail.\n if (!key) return 'suppressed-platform'\n if (await isEmailSuppressed(email, firestore)) return 'suppressed-platform'\n const db = (firestore ?? firebaseAdmin.app().firestore()) as any\n const doc = await db\n .collection('hosts')\n .doc(hostId)\n .collection('suppressions')\n .doc(key)\n .get()\n return doc.exists ? 'suppressed-host' : null\n}\n\n/**\n * Reply to one submission.\n *\n * Body: `{ hostId, submissionId, subject, message }`. The recipient is\n * deliberately absent — it is read off the stored submission, so this route\n * cannot be pointed at an address of the caller's choosing.\n */\nexport const inboxReplyHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n\n const hostId = String(req.body?.hostId ?? '')\n const submissionId = String(req.body?.submissionId ?? '')\n const subject = String(req.body?.subject ?? '')\n .trim()\n .slice(0, REPLY_SUBJECT_MAX)\n const message = String(req.body?.message ?? '')\n .trim()\n .slice(0, REPLY_BODY_MAX)\n if (!hostId || !submissionId) {\n return res.status(400).json({ error: 'Missing hostId or submissionId' })\n }\n if (!subject || !message) {\n return res.status(400).json({ error: 'Missing subject or message' })\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not a site admin or editor' })\n }\n\n // The address the reply goes back to, and the only thing that makes this\n // send worth making. A console account with no address on it would put\n // the customer's answer nowhere, so it refuses rather than sending a\n // message that invites a reply into a void.\n const replyTo = String(decoded.email ?? '')\n if (!replyTo) {\n return res.status(400).json({\n error:\n 'Your account has no email address, so a reply would have nowhere to come back to.',\n })\n }\n\n const submissionRef = hostRef\n .collection('formSubmissions')\n .doc(submissionId)\n const submission = await submissionRef.get()\n if (!submission.exists) {\n return res.status(404).json({ error: 'Unknown submission' })\n }\n\n const fields = (submission.get('fields') ?? {}) as Record<string, unknown>\n const recipient = replyRecipient(fields)\n if ('refusal' in recipient) {\n return res\n .status(422)\n .json({ error: REFUSAL_MESSAGES[recipient.refusal], reason: recipient.refusal })\n }\n\n const suppressed = await addressSuppression(hostId, recipient.email)\n if (suppressed) {\n return res\n .status(409)\n .json({ error: REFUSAL_MESSAGES[suppressed], reason: suppressed })\n }\n\n const siteName = String(\n hostSnapshot.get('displayName') ?? hostSnapshot.get('subdomain') ?? '',\n )\n const branding = resolveBrandingProfile(\n (await getOrgForHost(hostId).catch(() => null))?.org as never,\n )\n\n // No `html` is passed on purpose. `sendEmail` synthesizes the HTML part\n // from `text`, which is the single place that guarantee is enforced; a\n // hand-built one here would be a second place it can be forgotten.\n // No `priority` either: absent, it resolves to transactional, which the\n // platform governor may never refuse. That is the correct class — a reply\n // is a person answering a person, and it cannot be retried by a sweep.\n const result = await sendEmail({\n to: recipient.email,\n subject,\n text: composeReplyBody({ message, fields, siteName }),\n replyTo,\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: REPLY_CONTEXT,\n })\n\n if (!result.sent) {\n return res.status(502).json({\n error: 'The reply could not be sent.',\n reason: (result as { reason?: string }).reason ?? 'unknown',\n })\n }\n\n // Metered as transactional, so it enters the cost meter and never the\n // campaign meter a plan limit can refuse.\n void meterHostEmail(hostId, 1, 'transactional')\n\n const sentAtMs = Date.now()\n const replyRef = await submissionRef\n .collection(REPLIES_SUBCOLLECTION)\n .add({\n to: recipient.email,\n subject,\n // The merchant's own words, not the composed wire body. The quote and\n // the attribution line are rendered from the submission every time, so\n // storing them would be storing a copy of a document one field away.\n message,\n replyTo,\n fromName: branding.fromName,\n sentByUid: decoded.uid,\n providerMessageId: (result as { id?: string | null }).id ?? null,\n sentAtMs,\n createdAt: FieldValue.serverTimestamp(),\n })\n\n // A replied submission is a handled one, so it stops being unread in the\n // same write that records the reply. `repliedAtMs` is what the row reads;\n // the count of replies is a subcollection read the list must not make.\n await submissionRef.set(\n { read: true, repliedAtMs: sentAtMs },\n { merge: true },\n )\n\n return res.status(200).json({\n sent: true,\n replyId: replyRef.id,\n to: recipient.email,\n replyTo,\n sentAtMs,\n })\n } catch (error) {\n console.error('[inbox] reply failed', error)\n return res.status(500).json({ error: 'The reply could not be sent.' })\n }\n}\n\n/** Where an assignment is recorded, under the submission that occasioned it. */\nexport const LIST_ASSIGNMENTS_SUBCOLLECTION = 'listAssignments'\n\n/** The `source` stamped on a member enrolled from the Inbox. */\nexport const ASSIGNMENT_SOURCE = 'inbox'\n\n/**\n * How many lists the picker offers.\n *\n * A ceiling on the READ, so a merchant with an unusual number of lists costs\n * one bounded query rather than a scan, and the response says when it is a\n * floor rather than presenting a slice as the whole set.\n */\nexport const LIST_OPTIONS_LIMIT = 100\n\n/** Everything both list-assignment handlers need, or the refusal to send. */\ntype AssignmentContext =\n | {\n ok: true\n uid: string\n orgId: string\n firestore: FirebaseFirestore.Firestore\n submissionRef: FirebaseFirestore.DocumentReference\n email: string\n }\n | { ok: false; status: number; body: Record<string, unknown> }\n\n/**\n * Who is asking, about which submission, and for which person.\n *\n * ## Two gates, not one\n *\n * A host role is necessary and NOT sufficient. Lists live at\n * `orgs/{orgId}/lists` and their members are contacts, so the security rules\n * put both behind `isOrgWideMember()` — an editor invited to ONE site is an\n * org member with `allHosts: false`, and gating an org-wide write on the host\n * role alone would let a single-site collaborator enroll people into an\n * audience every other site in the org can mail. The Admin SDK evaluates no\n * rules, so this route is the enforcement rather than an echo of it.\n *\n * ## The address is read off the submission\n *\n * Through `replyRecipient`, the same resolver the reply and the Inbox row use.\n * A `to` in the request body would let a site editor enroll an address of\n * their choosing into a marketing audience, which is the same defect the\n * reply handler refuses for the same reason.\n */\nasync function resolveAssignmentContext(\n req: Parameters<PluginApiHandler>[0],\n): Promise<AssignmentContext> {\n const hostId = String(req.body?.hostId ?? '')\n const submissionId = String(req.body?.submissionId ?? '')\n if (!hostId || !submissionId) {\n return {\n ok: false,\n status: 400,\n body: { error: 'Missing hostId or submissionId' },\n }\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return { ok: false, status: 401, body: { error: 'Unauthenticated' } }\n }\n\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return { ok: false, status: 404, body: { error: 'Unknown site' } }\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return {\n ok: false,\n status: 403,\n body: { error: 'Not a site admin or editor' },\n }\n }\n\n const orgId = String((await getOrgForHost(hostId).catch(() => null))?.orgId ?? '')\n if (!orgId) {\n return {\n ok: false,\n status: 404,\n body: { error: 'This site has no organization, so it has no lists.' },\n }\n }\n const membership = await resolveOrgMembership(decoded.uid, orgId).catch(\n () => null,\n )\n const member = membership?.member\n const orgWideWriter =\n isOrgWideMember(member) &&\n (member?.role === 'owner' ||\n member?.role === 'admin' ||\n member?.role === 'editor') &&\n (member as { orgSuspended?: boolean } | undefined)?.orgSuspended !== true\n if (!orgWideWriter) {\n return {\n ok: false,\n status: 403,\n body: {\n error:\n 'Marketing lists belong to the whole organization, so adding ' +\n 'someone to one needs organization-wide access rather than access ' +\n 'to this site.',\n },\n }\n }\n\n const submissionRef = hostRef.collection('formSubmissions').doc(submissionId)\n const submission = await submissionRef.get()\n if (!submission.exists) {\n return { ok: false, status: 404, body: { error: 'Unknown submission' } }\n }\n const recipient = replyRecipient(\n (submission.get('fields') ?? {}) as Record<string, unknown>,\n )\n if ('refusal' in recipient) {\n return {\n ok: false,\n status: 422,\n body: {\n error: ASSIGNMENT_REFUSAL_MESSAGES[recipient.refusal],\n reason: recipient.refusal,\n },\n }\n }\n\n return {\n ok: true,\n uid: decoded.uid,\n orgId,\n firestore,\n submissionRef,\n email: recipient.email,\n }\n}\n\n/**\n * The person's own consent facts, read off the org contact for this address.\n *\n * The CRM record is where a refusal lives: `marketingConsent` is written\n * `false` by exactly one path in the product and it writes a contact. So this\n * is the read that makes `declined` mean something at enrollment time, and an\n * absent contact is honestly `unrecorded` rather than a reason to guess.\n *\n * Read UNSCOPED, deliberately. `scopedToHost` narrows an org collection to\n * what one site may see, and a refusal filtered out by that narrowing is a\n * refusal this route would then step over — the failure mode is mailing\n * somebody who said no. It is safe here because the caller has already been\n * proved an org-wide member, which is the tier the rules grant the whole\n * org's contacts to.\n *\n * Through the org's address index (AGL-2633), for the same failure mode\n * from the other side: a refusal recorded on a record that was later\n * merged into another is the survivor's refusal, and the sender's address\n * may be the one that became an alternate.\n */\nasync function storedConsentForAddress(\n hostId: string,\n group: ConsentGroup,\n email: string,\n): Promise<MarketingConsentRecord> {\n try {\n const contacts = await orgDataCollectionForHost(hostId, 'contacts')\n const found = await findContactByEmail(contacts, email)\n return readMarketingBasis(\n found ? (found.data() as Record<string, unknown>) : null,\n group,\n )\n } catch (error) {\n console.error('[inbox] consent lookup failed', error)\n /*\n * FAILS TO `declined`, which reads oddly until you name the alternative.\n * A throwing read cannot say the person consented and cannot say they\n * refused; the question is which way the unknown should fall. `unrecorded`\n * would leave the attestation control on screen and let the merchant add\n * somebody whose stored refusal this route simply failed to see, and no\n * later surface would ever revisit it. A refusal costs a retry.\n */\n return {\n ...readMarketingBasis(null, group),\n basis: 'declined',\n // Attributed to nobody, because nobody asserted this: it is what a\n // failed read falls back to, not a refusal anyone recorded. Claiming\n // `'person'` here would put a refusal in the audit trail that the\n // person never made.\n assertedBy: null,\n source: null,\n basisAtMs: null,\n capturedAtMs: null,\n }\n }\n}\n\n/**\n * `POST inbox/list-options` — what the merchant may do with this sender.\n *\n * Reads only. It exists because the answer needs three things the browser\n * cannot have: the org's lists (rules put them behind org-wide membership,\n * which the acting console session may not hold), the person's consent record\n * (an org contact, same gate), and both suppression lists. Computing any of\n * it client-side would be a second copy of the rule, on the surface whose\n * whole job is to tell the merchant the truth about what is about to happen.\n *\n * Reached by an explicit expansion in the reader, never on mount: it is three\n * reads and a bounded query, and paying them once per opened submission would\n * charge every merchant who never touches lists.\n */\nexport const inboxListOptionsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n try {\n const context = await resolveAssignmentContext(req)\n if (context.ok === false) {\n return res.status(context.status).json(context.body)\n }\n const hostId = String(req.body?.hostId ?? '')\n\n const suppression = await addressSuppression(hostId, context.email)\n // The controller this enrollment is made for — the declared group of\n // sites that are one sender, or this site alone.\n const group = await consentGroupForSite(hostId)\n const stored = await storedConsentForAddress(hostId, group, context.email)\n const readout = assignmentReadout({ stored, suppression })\n\n const listsRef = context.firestore\n .collection('orgs')\n .doc(context.orgId)\n .collection('lists')\n /*\n * Ordered by document id, which is the one key every list has.\n * `limit()` alone answers in that order anyway but does not SAY so, and a\n * page of results whose order is an implementation detail is the shape\n * that turned an audience into a random sample. Ordering by `name` would\n * be worse than either: `orderBy` drops documents missing the field, so a\n * list created without a name would vanish from its own picker.\n */\n const snapshot = await listsRef\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(LIST_OPTIONS_LIMIT + 1)\n .get()\n const lists = snapshot.docs\n .slice(0, LIST_OPTIONS_LIMIT)\n .map((doc: FirebaseFirestore.QueryDocumentSnapshot) => ({\n id: doc.id,\n name: String(doc.get('name') ?? doc.id),\n }))\n\n return res.status(200).json({\n to: context.email,\n lists,\n listsTruncated: snapshot.docs.length > LIST_OPTIONS_LIMIT,\n basis: stored.basis,\n basisAtMs: stored.basisAtMs,\n ...readout,\n })\n } catch (error) {\n console.error('[inbox] list options failed', error)\n return res.status(500).json({ error: 'The lists could not be read.' })\n }\n}\n\n/**\n * `POST inbox/assign-list` — put this sender on a marketing list.\n *\n * Body: `{ hostId, submissionId, listId, attestConsent? }`. `attestConsent` is\n * the merchant STATING that they have this person's permission; it is not a\n * way to name a basis, because the pass-through basis is derived server-side\n * from the person's own record.\n *\n * ## What this act is, and what it is not\n *\n * It is marketing enrollment: the only consumer of a list is a campaign. It\n * is therefore kept entirely separate from the reply — different route,\n * different card in the UI, different record — and replying enrolls nobody.\n * The reverse holds too: enrolling somebody sends them nothing, meters\n * nothing, and is not a promise that they are still mailable when a campaign\n * eventually runs. Suppression is consulted again at send time, in\n * `filterSendableForHost`, because an address can be suppressed the day after\n * it is enrolled and an enrollment-time check that licensed every later send\n * would be a check that passes once and pays out forever.\n */\nexport const inboxAssignListHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const listId = String(req.body?.listId ?? '')\n if (!listId) return res.status(400).json({ error: 'Missing listId' })\n const attested = req.body?.attestConsent === true\n\n try {\n const context = await resolveAssignmentContext(req)\n if (context.ok === false) {\n return res.status(context.status).json(context.body)\n }\n const hostId = String(req.body?.hostId ?? '')\n\n const refuse = (status: number, reason: AssignmentRefusal) =>\n res\n .status(status)\n .json({ error: ASSIGNMENT_REFUSAL_MESSAGES[reason], reason })\n\n const suppressed = await addressSuppression(hostId, context.email)\n if (suppressed) return refuse(409, suppressed)\n\n const listRef = context.firestore\n .collection('orgs')\n .doc(context.orgId)\n .collection('lists')\n .doc(listId)\n const listSnapshot = await listRef.get()\n // A stale or mistyped id must not CREATE a list: a campaign's `list`\n // audience would then read a list nobody set up.\n if (!listSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown list' })\n }\n\n // The controller this enrollment is made for — the declared group of\n // sites that are one sender, or this site alone.\n const group = await consentGroupForSite(hostId)\n const stored = await storedConsentForAddress(hostId, group, context.email)\n const nowMs = Date.now()\n const decision = assignmentBasis({\n stored,\n attested,\n actingUid: context.uid,\n nowMs,\n })\n if ('refusal' in decision) {\n // 409 for a refusal that no answer can change, 422 for the one the\n // merchant can answer by asserting a basis. The distinction is what\n // lets the UI offer the attestation control on exactly one of them.\n return refuse(decision.refusal === 'declined' ? 409 : 422, decision.refusal)\n }\n\n const enrollment = await enrollListMember({\n listRef,\n group,\n email: context.email,\n source: ASSIGNMENT_SOURCE,\n // Never `'rule'`: the dynamic-list materializer reconciles its own rows\n // away when a person stops matching, and a decision somebody made by\n // hand is not a rule match that lapsed.\n via: 'manual',\n consent: decision,\n })\n if (enrollment.enrolled === false) {\n /*\n * The membership itself records a refusal that the CRM record did not.\n * `enrollListMember` is the only writer of the collection and holds the\n * row, so it is the backstop for every enrollment route; reaching it\n * here means the two records disagree, and the refusal wins.\n */\n return refuse(409, enrollment.refusal === 'declined' ? 'declined' : 'no-address')\n }\n\n /*\n * The attestation trail, beside the reply record and for the same reason:\n * a support question about a marketing list starts with \"who put this\n * person on it, and on what basis\". The member document carries the basis\n * so the send-time join can read it; this carries the same facts where\n * the act happened, which is the only place that survives the person\n * later being removed from the list.\n */\n await context.submissionRef\n .collection(LIST_ASSIGNMENTS_SUBCOLLECTION)\n .add({\n to: context.email,\n listId,\n listName: String(listSnapshot.get('name') ?? listId),\n memberId: enrollment.memberId,\n basis: decision.basis,\n basisAtMs: decision.atMs,\n assertedByUid: decision.byUid,\n addedByUid: context.uid,\n addedAtMs: nowMs,\n createdAt: FieldValue.serverTimestamp(),\n })\n\n return res.status(200).json({\n enrolled: true,\n to: context.email,\n listId,\n listName: String(listSnapshot.get('name') ?? listId),\n memberId: enrollment.memberId,\n basis: decision.basis,\n })\n } catch (error) {\n console.error('[inbox] list assignment failed', error)\n return res.status(500).json({ error: 'The list assignment failed.' })\n }\n}\n\n/**\n * Console API registration.\n *\n * None of these is on the machine-path exemption list in\n * `plugin-api-rate-limit.ts`. Each is reached by a person pressing a button in\n * a browser, so the visitor limiter's per-(site, IP) budget is far above any\n * real use of them and is the right ceiling for surfaces that put mail on the\n * wire or a person into a marketing audience.\n */\nexport function registerInboxConsoleApi(): void {\n registerPluginApiRoute('inbox/reply', inboxReplyHandler)\n registerPluginApiRoute('inbox/list-options', inboxListOptionsHandler)\n registerPluginApiRoute('inbox/assign-list', inboxAssignListHandler)\n}\n"],"names":["ASSIGNMENT_REFUSAL_MESSAGES","assignmentBasis","assignmentReadout","isOrgWideMember","readMarketingBasis","registerPluginApiRoute","resolveBrandingProfile","emailSuppressionKey","enrollListMember","firebaseAdmin","getOrgForHost","hostSendingIdentity","isEmailSuppressed","meterHostEmail","consentGroupForSite","orgDataCollectionForHost","resolveOrgMembership","findContactByEmail","sendEmail","FieldValue","REPLY_BODY_MAX","REPLY_SUBJECT_MAX","composeReplyBody","replyRecipient","REPLIES_SUBCOLLECTION","REPLY_CONTEXT","REFUSAL_MESSAGES","addressSuppression","hostId","email","firestore","key","db","app","doc","collection","get","exists","inboxReplyHandler","req","res","method","status","json","error","String","body","submissionId","subject","trim","slice","message","authorization","headers","idToken","startsWith","length","undefined","hostSnapshot","decoded","submission","auth","verifyIdToken","hostRef","memberRole","uid","replyTo","submissionRef","fields","recipient","refusal","reason","suppressed","siteName","branding","catch","org","result","to","text","fromName","sendingIdentity","audience","context","sent","sentAtMs","Date","now","replyRef","add","sentByUid","providerMessageId","id","createdAt","serverTimestamp","set","read","repliedAtMs","merge","replyId","console","LIST_ASSIGNMENTS_SUBCOLLECTION","ASSIGNMENT_SOURCE","LIST_OPTIONS_LIMIT","resolveAssignmentContext","ok","orgId","membership","member","orgWideWriter","role","orgSuspended","storedConsentForAddress","group","contacts","found","data","basis","assertedBy","source","basisAtMs","capturedAtMs","inboxListOptionsHandler","suppression","stored","readout","listsRef","snapshot","orderBy","FieldPath","documentId","limit","lists","docs","map","name","listsTruncated","inboxAssignListHandler","listId","attested","attestConsent","listSnapshot","refuse","listRef","nowMs","decision","actingUid","enrollment","via","consent","enrolled","listName","memberId","atMs","assertedByUid","byUid","addedByUid","addedAtMs","registerInboxConsoleApi"],"mappings":"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"}