@aglyn/shared-util-email 1.0.0-beta.271 → 1.0.0-beta.273
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 +3 -3
- package/src/lib/mail-provider-resend.d.ts +1 -1
- package/src/lib/mail-provider-resend.js +3 -1
- package/src/lib/mail-provider-resend.js.map +1 -1
- package/src/lib/received-email.d.ts +18 -0
- package/src/lib/received-email.js +22 -1
- package/src/lib/received-email.js.map +1 -1
- package/src/lib/site-care-emails.d.ts +0 -1
- package/src/lib/site-care-emails.js +4 -29
- package/src/lib/site-care-emails.js.map +1 -1
- package/src/lib/system-email-catalog.js +58 -0
- package/src/lib/system-email-catalog.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-util-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.273",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.273",
|
|
29
|
+
"@aglyn/shared-util-http": "1.0.0-beta.273",
|
|
30
30
|
"@msgpack/msgpack": "^3.1.3",
|
|
31
31
|
"@swc/helpers": "0.5.23"
|
|
32
32
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { type EmailDeliveryEvent, type EmailDeliveryHistorySource, type EmailDeliveryMessage, type EmailDeliveryMessageSource, type EmailDeliverySnapshot } from './email-delivery-events';
|
|
36
36
|
import type { EmailCredentialReport } from './email-health';
|
|
37
37
|
import { type MailInboundEvent, type MailProvider, type MailProviderMessage, type MailSendingDomain } from './mail-provider';
|
|
38
|
-
import type
|
|
38
|
+
import { type ReceivedEmail, type ReceivedEmailSource } from './received-email';
|
|
39
39
|
/** The slug stored on every delivery record this provider produces. */
|
|
40
40
|
export declare const RESEND_PROVIDER_ID = "resend";
|
|
41
41
|
/** The sending key's environment variable. */
|
|
@@ -34,6 +34,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
34
34
|
*/ import { bareSenderAddress, normalizeEventTags } from "./email-delivery-events.js";
|
|
35
35
|
import { readBounceText } from "./mail-bounce.js";
|
|
36
36
|
import { providerRetryAtMs } from "./mail-provider.js";
|
|
37
|
+
import { isAutoSubmittedMail } from "./received-email.js";
|
|
37
38
|
/** The slug stored on every delivery record this provider produces. */ export const RESEND_PROVIDER_ID = 'resend';
|
|
38
39
|
/** The sending key's environment variable. */ export const RESEND_SEND_KEY_SETTING = 'RESEND_API_KEY';
|
|
39
40
|
/** The full-access read key's environment variable. */ export const RESEND_READ_KEY_SETTING = 'RESEND_READ_API_KEY';
|
|
@@ -582,7 +583,8 @@ const text = (value)=>typeof value === 'string' ? value : '';
|
|
|
582
583
|
subject: String((_record_subject = record['subject']) != null ? _record_subject : '').trim(),
|
|
583
584
|
text: text(record['text']),
|
|
584
585
|
html: text(record['html']),
|
|
585
|
-
receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs
|
|
586
|
+
receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs,
|
|
587
|
+
autoSubmitted: isAutoSubmittedMail((name)=>header(headers, name))
|
|
586
588
|
};
|
|
587
589
|
}
|
|
588
590
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-resend.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 * RESEND, as one {@link MailProvider} — the only module in the platform that\n * knows Resend's endpoints, payloads, event names and error names.\n *\n * It is the provider a deployment gets when it names none, which is how\n * every deployment that set `RESEND_API_KEY` before the contract existed\n * keeps sending exactly as it did. Two credentials, on purpose:\n *\n * - `RESEND_API_KEY` sends. It is provisioned sending-scoped, so a leaked\n * copy cannot enumerate everyone the platform ever wrote to.\n * - `RESEND_READ_API_KEY` is full-access and only ever reads: history, one\n * message's body, received mail and the account's sending domains. A\n * sending-scoped key answers every such read `401 restricted_api_key`.\n *\n * Pure but for the `fetch` calls, each inside a function that must be called\n * to do anything, so a client component that reaches this module through the\n * library's barrel executes nothing on load.\n */\n\nimport {\n bareSenderAddress,\n normalizeEventTags,\n type EmailDeliveryEvent,\n type EmailDeliveryEventType,\n type EmailDeliveryHistorySource,\n type EmailDeliveryMessage,\n type EmailDeliveryMessageSource,\n type EmailDeliverySnapshot,\n} from './email-delivery-events'\nimport type { EmailCredentialReport } from './email-health'\nimport { readBounceText } from './mail-bounce'\nimport {\n providerRetryAtMs,\n type MailInboundEvent,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderReads,\n type MailProviderSendResult,\n type MailSendingDomain,\n} from './mail-provider'\nimport type { ReceivedEmail, ReceivedEmailSource } from './received-email'\n\n/** The slug stored on every delivery record this provider produces. */\nexport const RESEND_PROVIDER_ID = 'resend'\n\n/** The sending key's environment variable. */\nexport const RESEND_SEND_KEY_SETTING = 'RESEND_API_KEY'\n\n/** The full-access read key's environment variable. */\nexport const RESEND_READ_KEY_SETTING = 'RESEND_READ_API_KEY'\n\ntype Env = Record<string, string | undefined>\n\nfunction setting(env: Env, name: string): string {\n return String(env[name] ?? '').trim()\n}\n\n/*==========================================\n * SENDING.\n *=========================================*/\n\n/** The send endpoint. */\nexport const RESEND_SEND_ENDPOINT = 'https://api.resend.com/emails'\n\n/** A Resend send payload in the provider's own wire shape. */\nexport interface ResendSendPayload {\n to?: unknown\n from?: unknown\n subject?: unknown\n [field: string]: unknown\n}\n\n/**\n * The one place that POSTs to Resend's send endpoint, and the last thing\n * standing between a payload and the network.\n *\n * A payload carrying no recipient cannot become a message. Resend answers it\n * `422 missing_required_field`, which costs an API call and then shows up in\n * the vendor dashboard as a red line indistinguishable from mail that\n * genuinely failed to deliver — carrying no subject, no recipient and nothing\n * naming the code that produced it. Diagnosing that means reading a log\n * outside the deployment and guessing. So the refusal happens here, before\n * the fetch, and names the caller's `context`.\n *\n * It throws rather than returning a result: this is a programming error,\n * not a delivery outcome. `sendEmail` filters recipients well before it\n * reaches this call, so nothing on the ordinary path can trip it. The guard\n * exists because `RESEND_SEND_ENDPOINT` is exported and any module can\n * therefore reach the send endpoint on its own, bypassing every check\n * `sendEmail` owns.\n */\nexport async function postResendEmail(\n apiKey: string,\n payload: ResendSendPayload,\n context?: string,\n): Promise<Response> {\n const raw = payload?.to\n const recipients = (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n if (!recipients.length) {\n throw new Error(\n `${context ? `${context} ` : ''}send refused before the network — a ` +\n 'Resend payload with no `to` field cannot become a message, and the ' +\n 'attempt would surface only as a 422 in the Resend dashboard',\n )\n }\n\n return fetch(RESEND_SEND_ENDPOINT, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n })\n}\n\n/** One message as Resend's send endpoint takes it. */\nexport function resendSendPayload(message: MailProviderMessage): ResendSendPayload {\n return {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { reply_to: message.replyTo } : {}),\n ...(message.bcc?.length ? { bcc: message.bcc } : {}),\n }\n}\n\n/**\n * Sends one message and reads the answer.\n *\n * A 429 is a request to slow down, not a statement about the recipient, so\n * it comes back `rateLimited` with the wait Resend asked for. Its quota\n * errors — `daily_quota_exceeded`, `monthly_quota_exceeded` — are 429s as\n * well, and clear with time the same way.\n */\nasync function sendThroughResend(\n apiKey: string,\n message: MailProviderMessage,\n context?: string,\n): Promise<MailProviderSendResult> {\n const response = await postResendEmail(apiKey, resendSendPayload(message), context)\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n if (response.status === 429) {\n return {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n }\n return { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: string } | null\n return { accepted: true, id: body?.id ?? null }\n}\n\n/*==========================================\n * THE CREDENTIAL PROBE.\n *=========================================*/\n\n/**\n * Where the credential probe and the domain read ask their questions.\n *\n * Deliberately NOT the send endpoint. A probe aimed at `/emails` is a send\n * attempt however empty its body is: it consumes an API call, and Resend\n * records it in the account's logs as a `422` on `POST /emails` with no\n * recipient, no subject and nothing identifying the caller — a line an\n * operator reading that dashboard has to treat as failed mail. A domain read\n * cannot create a message and cannot be mistaken for one.\n */\nexport const RESEND_DOMAINS_ENDPOINT = 'https://api.resend.com/domains'\n\n/**\n * Error names that mean the key itself was not accepted, as opposed to a key\n * that authenticated and merely lacks read scope. Matched by NAME, not\n * status: `401` and `403` each cover both meanings.\n */\nconst REFUSED_KEY_ERRORS = new Set([\n 'missing_api_key',\n 'validation_error',\n 'suspended_api_key',\n])\n\n/**\n * Error names that mean the key authenticated and was then denied this\n * particular read. A sending-scoped key — the shape Aglyn provisions — always\n * lands here, and reaching this answer at all required Resend to recognize\n * the credential, which is exactly what the probe is asking.\n */\nconst AUTHENTICATED_BUT_UNSCOPED_ERRORS = new Set([\n 'restricted_api_key',\n 'invalid_permission',\n])\n\n/** The `name` Resend puts on an error body, or `''` for anything else. */\nfunction resendErrorName(body: string): string {\n try {\n const parsed = JSON.parse(body) as { name?: unknown }\n return typeof parsed?.name === 'string' ? parsed.name : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Whether the sending key is accepted — without sending anything to anybody,\n * and without leaving anything behind that reads as failed mail.\n *\n * How: a `GET` of the domains collection. The question is only ever \"does\n * Resend recognize this credential\", so the probe reads the ERROR NAME rather\n * than the status, because `401` and `403` each carry both meanings:\n *\n * - `2xx` — the key is accepted and has read scope → `ok`\n * - `restricted_api_key` / `invalid_permission` — Resend authenticated the\n * key and then denied it this read. A sending-scoped key always answers\n * this way, and getting the answer proves the credential works → `ok`\n * - `missing_api_key` / `validation_error` / `suspended_api_key` — the key\n * itself was refused → `invalid-key`\n * - anything else → `unknown`\n *\n * An unrecognized rejection is `unknown`, never `invalid-key`: this feeds a\n * staff diagnostics screen whose whole value is that a red line means\n * something, and a shape we have not seen before is not evidence that a\n * working key is broken.\n */\nexport async function checkResendCredentials(\n apiKey: string,\n): Promise<EmailCredentialReport> {\n if (!apiKey) return { status: 'unconfigured' }\n try {\n const response = await fetch(RESEND_DOMAINS_ENDPOINT, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n const detail = (await response.text().catch(() => '')).slice(0, 300)\n\n if (response.status >= 200 && response.status < 300) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (response.status === 401 || response.status === 403) {\n const name = resendErrorName(detail)\n if (AUTHENTICATED_BUT_UNSCOPED_ERRORS.has(name)) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (REFUSED_KEY_ERRORS.has(name)) {\n return { status: 'invalid-key', probeStatus: response.status, detail }\n }\n }\n return { status: 'unknown', probeStatus: response.status, detail }\n } catch (error) {\n return {\n status: 'unknown',\n detail: String((error as Error)?.message ?? error).slice(0, 300),\n }\n }\n}\n\n/**\n * Every sending domain on the account, with its verification and tracking\n * state. Throws with the status when Resend does not answer 2xx, so a caller\n * never reads \"I could not look\" as \"there is nothing there\".\n */\nexport async function readResendSendingDomains(\n apiKey: string,\n endpoint: string = RESEND_DOMAINS_ENDPOINT,\n): Promise<MailSendingDomain[]> {\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`The provider answered ${response.status} to the domain read.`)\n }\n const payload = (await response.json().catch(() => null)) as\n | {\n data?: Array<{\n name?: unknown\n status?: unknown\n click_tracking?: unknown\n open_tracking?: unknown\n }>\n }\n | null\n /**\n * A tri-state, and the third state matters: a listing that does not carry\n * the field must not be reported as the field being false. Off is a fault\n * somebody should fix; unknown is a question this read could not ask.\n */\n const flag = (value: unknown): boolean | null =>\n typeof value === 'boolean' ? value : null\n const domains: MailSendingDomain[] = []\n for (const row of payload?.data ?? []) {\n const name = String(row?.name ?? '').toLowerCase()\n if (!name) continue\n domains.push({\n name,\n status: String(row?.status ?? 'unknown'),\n clickTracking: flag(row?.click_tracking),\n openTracking: flag(row?.open_tracking),\n })\n }\n return domains\n}\n\n/*==========================================\n * THE DELIVERY FEED.\n *=========================================*/\n\n/** Epoch ms from an ISO string or a number, or `null` when unreadable. */\nfunction eventTimeMs(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n const parsed = Date.parse(String(value ?? ''))\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeBounceType(\n value: unknown,\n): EmailDeliveryEvent['bounceType'] {\n const lowered = String(value ?? '')\n .trim()\n .toLowerCase()\n if (lowered === 'permanent') return 'permanent'\n if (lowered === 'transient') return 'transient'\n return lowered ? 'undetermined' : null\n}\n\n/** The Resend event names we understand, mapped onto ours. */\nconst RESEND_EVENT_TYPES: Record<string, EmailDeliveryEventType> = {\n 'email.sent': 'sent',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delayed',\n 'email.opened': 'opened',\n 'email.clicked': 'clicked',\n 'email.bounced': 'bounced',\n 'email.complained': 'complained',\n 'email.failed': 'failed',\n}\n\n/**\n * Turns one Resend webhook payload into zero or more of our events.\n *\n * One event per recipient, not per message: a send addressed to three people\n * produces one webhook, and a staff view keyed on a person has to be able to\n * find it under each of them.\n *\n * Returns an empty array for anything unrecognised — a contact or domain\n * event, an inbound `email.received`, a type added after this was written.\n * Silence rather than a throw, because a webhook handler that 500s on an\n * unfamiliar event teaches the provider to retry it forever.\n *\n * @param payload The parsed webhook body.\n * @param receivedAtMs Fallback timestamp for a payload that carries none.\n */\nexport function normalizeResendDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n): EmailDeliveryEvent[] {\n const event = (payload ?? {}) as Record<string, any>\n const type = RESEND_EVENT_TYPES[String(event.type ?? '')]\n if (!type) return []\n\n const data = (event.data ?? {}) as Record<string, any>\n const providerMessageId = String(data.email_id ?? data.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(data.to) ? data.to : [data.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n // The per-state timestamp when the provider gives one, because an open\n // three days after the send is the whole point of recording an open.\n const at =\n eventTimeMs(data.click?.timestamp) ??\n eventTimeMs(data.open?.timestamp) ??\n eventTimeMs(event.created_at) ??\n eventTimeMs(data.created_at) ??\n receivedAtMs\n\n const subject = String(data.subject ?? '').trim() || null\n const tags = normalizeEventTags(data.tags)\n const from = bareSenderAddress(data.from)\n // The provider folds a DSN's fields into one sentence; the structured\n // ones are read first where a payload carries them.\n const bounceText =\n type === 'bounced'\n ? readBounceText(\n [data.bounce?.diagnosticCode, data.bounce?.diagnostic_code, data.bounce?.remoteMta, data.bounce?.remote_mta, data.bounce?.message]\n .filter((part: unknown) => typeof part === 'string' && part.trim())\n .join(' '),\n )\n : null\n\n return recipients.map((to: string) => ({\n type,\n at,\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject,\n context: tags['context'] || null,\n tags,\n link: type === 'clicked' ? String(data.click?.link ?? '') || null : null,\n bounceType: type === 'bounced' ? normalizeBounceType(data.bounce?.type) : null,\n detail:\n String(data.bounce?.message ?? data.failed?.reason ?? '').trim() || null,\n ...(from ? { from } : {}),\n ...(bounceText?.status ? { bounceStatus: bounceText.status } : {}),\n ...(bounceText?.remoteMta ? { remoteMta: bounceText.remoteMta } : {}),\n }))\n}\n\n/*==========================================\n * THE ACCOUNT'S HISTORY.\n *\n * Resend's list endpoint reports a single `last_event` per message and no\n * engagement detail, so a snapshot can say \"this was opened at least once\"\n * and can never say \"three times\" — see `EmailDeliverySnapshot`.\n *=========================================*/\n\n/**\n * A `last_event` string, mapped onto our lifecycle.\n *\n * Deliberately the bare state names rather than the `email.*` event names:\n * the list endpoint reports `\"delivered\"`, the webhook reports\n * `\"email.delivered\"`, and they are two different vocabularies for one\n * concept.\n */\nconst RESEND_LAST_EVENTS: Record<string, EmailDeliveryEventType> = {\n sent: 'sent',\n delivered: 'delivered',\n delivery_delayed: 'delayed',\n opened: 'opened',\n clicked: 'clicked',\n bounced: 'bounced',\n complained: 'complained',\n failed: 'failed',\n canceled: 'failed',\n queued: 'sent',\n scheduled: 'sent',\n}\n\n/**\n * Turns one entry from Resend's `GET /emails` list into zero or more\n * snapshots — one per recipient, for the same reason the event adapter fans\n * out: the staff view is keyed on a person.\n *\n * An unrecognised `last_event` falls back to `sent` rather than being\n * dropped. The message demonstrably exists and was addressed to somebody, and\n * \"we sent this and cannot characterise what happened next\" is a far more\n * useful row than no row — which is the state that sent a staffer to the\n * vendor dashboard in the first place.\n */\nexport function normalizeResendSentEmails(\n raw: unknown,\n): EmailDeliverySnapshot[] {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(record.to) ? record.to : [record.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n const parsed = Date.parse(String(record.created_at ?? ''))\n const sentAt = Number.isFinite(parsed) ? parsed : 0\n // A snapshot with no timestamp cannot be ordered, and the log's read drops\n // any document missing its sort key — so it is refused rather than written\n // somewhere nothing will look for it.\n if (!sentAt) return []\n\n const status =\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? 'sent'\n\n return recipients.map((to: string) => ({\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject: String(record.subject ?? '').trim() || null,\n sentAt,\n status,\n }))\n}\n\n/** The list endpoint. Paginates with `after=<id>`; caps at 100. */\nexport const RESEND_EMAILS_ENDPOINT = 'https://api.resend.com/emails'\n\n/**\n * {@link EmailDeliveryHistorySource} for Resend. Needs the full-access key.\n *\n * The list endpoint takes `limit`, `after` and `before` and no recipient\n * filter, which is why the source is a cursor-paged sweep.\n */\nexport function resendDeliveryHistorySource(\n apiKey: string,\n): EmailDeliveryHistorySource {\n return async ({ cursor, limit } = {}) => {\n const params = new URLSearchParams({\n limit: String(Math.min(Math.max(1, limit ?? 100), 100)),\n })\n if (cursor) params.set('after', cursor)\n const response = await fetch(`${RESEND_EMAILS_ENDPOINT}?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `email history read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n const body = (await response.json()) as {\n data?: unknown[]\n has_more?: boolean\n }\n const entries = Array.isArray(body?.data) ? body.data : []\n const snapshots = entries.flatMap((entry) => normalizeResendSentEmails(entry))\n // The cursor is the LAST RAW entry's id, not the last snapshot's: a page\n // whose final entry fanned out to zero snapshots (no recipient, no\n // timestamp) would otherwise rewind the cursor to an earlier message and\n // loop over the same page forever.\n const lastId = String(\n (entries[entries.length - 1] as { id?: unknown })?.id ?? '',\n ).trim()\n return {\n snapshots,\n nextCursor: body?.has_more && lastId ? lastId : null,\n }\n }\n}\n\nfunction addressList(raw: unknown): string[] {\n return (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n}\n\n/** Resend's `GET /emails/:id` payload, in our vocabulary. */\nexport function normalizeResendMessage(raw: unknown): EmailDeliveryMessage | null {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return null\n const parsed = Date.parse(String(record.created_at ?? ''))\n return {\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to: addressList(record.to),\n cc: addressList(record.cc),\n bcc: addressList(record.bcc),\n from: String(record.from ?? '').trim() || null,\n replyTo: addressList(record.reply_to).length\n ? addressList(record.reply_to)\n : null,\n subject: String(record.subject ?? '').trim() || null,\n // Empty string is NOT null here, and the difference is the point: a\n // message that went out text-only really does have an empty HTML part,\n // and a reader has to be able to tell that from \"we could not fetch it\".\n html: typeof record.html === 'string' ? record.html : null,\n text: typeof record.text === 'string' ? record.text : null,\n sentAt: Number.isFinite(parsed) ? parsed : null,\n status:\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? null,\n }\n}\n\n/** Resend's single-message endpoint. Needs the full-access key. */\nexport function resendDeliveryMessageSource(\n apiKey: string,\n): EmailDeliveryMessageSource {\n return async (providerMessageId: string) => {\n const response = await fetch(\n `${RESEND_EMAILS_ENDPOINT}/${encodeURIComponent(providerMessageId)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n // A message the provider has aged out is a 404, and that is an ANSWER —\n // \"we know this was sent and the body is gone\" — not a failure to report\n // as an error the staffer must act on.\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `message read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendMessage(await response.json())\n }\n}\n\n/*==========================================\n * RECEIVED MAIL.\n *\n * Resend announces a received message as an `email.received` webhook that\n * carries only its metadata — who it was from and to, its subject, its\n * `Message-ID` — and the message itself is read afterwards from the\n * receiving API by the id the event named.\n *=========================================*/\n\n/** The webhook event type a received message arrives as. */\nexport const RESEND_RECEIVED_EVENT = 'email.received'\n\n/** `GET {endpoint}/{id}` — one received message with its text and headers. */\nexport const RESEND_RECEIVING_ENDPOINT = 'https://api.resend.com/emails/receiving'\n\nconst list = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.map((entry) => String(entry ?? '').trim()).filter((entry) => entry !== '')\n : typeof value === 'string' && value.trim()\n ? [value.trim()]\n : []\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A header by name, case-insensitively, off whatever map the provider sent. */\nfunction header(headers: unknown, name: string): string {\n if (!headers || typeof headers !== 'object') return ''\n const wanted = name.toLowerCase()\n for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {\n if (key.toLowerCase() === wanted) {\n return Array.isArray(value) ? String(value[0] ?? '').trim() : String(value ?? '').trim()\n }\n }\n return ''\n}\n\n/**\n * The provider's id of the message an `email.received` event announces,\n * or `null` for any other event — including a malformed one, which is\n * acknowledged and dropped rather than retried forever.\n */\nexport function resendReceivedEventId(event: unknown): string | null {\n const record = event as { type?: unknown; data?: { email_id?: unknown } } | null\n if (!record || record.type !== RESEND_RECEIVED_EVENT) return null\n const id = String(record.data?.email_id ?? '').trim()\n return id || null\n}\n\n/**\n * Every recipient the event names — To, Cc, Bcc and the addresses the\n * message was received for — so a route can find what it is looking for in\n * the metadata before it spends a read on the body.\n */\nexport function resendReceivedEventRecipients(event: unknown): string[] {\n const data = (event as { data?: Record<string, unknown> } | null)?.data ?? {}\n return [\n ...list(data['to']),\n ...list(data['cc']),\n ...list(data['bcc']),\n ...list(data['received_for']),\n ]\n}\n\n/**\n * One received message as the receiving API answers it, in the neutral\n * shape. `null` for a payload with no id, which is not a message.\n */\nexport function normalizeResendReceivedEmail(\n raw: unknown,\n nowMs: number = Date.now(),\n): ReceivedEmail | null {\n const record = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const id = String(record['id'] ?? '').trim()\n if (!id) return null\n const headers = record['headers']\n const created = Date.parse(String(record['created_at'] ?? ''))\n return {\n id,\n messageId: String(record['message_id'] ?? '').trim() || header(headers, 'message-id'),\n inReplyTo: header(headers, 'in-reply-to'),\n from: String(record['from'] ?? '').trim(),\n to: list(record['to']),\n cc: list(record['cc']),\n bcc: list(record['bcc']),\n receivedFor: list(record['received_for']),\n subject: String(record['subject'] ?? '').trim(),\n text: text(record['text']),\n html: text(record['html']),\n receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs,\n }\n}\n\n/**\n * {@link ReceivedEmailSource} for Resend. Needs the full-access key. A `404`\n * is `null` — the message expired or never existed — and anything else\n * throws with the status, so the caller decides whether the provider should\n * retry.\n */\nexport function resendReceivedEmailSource(\n apiKey: string,\n fetchImpl: typeof fetch = fetch,\n): ReceivedEmailSource {\n return async (id) => {\n const response = await fetchImpl(\n `${RESEND_RECEIVING_ENDPOINT}/${encodeURIComponent(id)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `received email read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendReceivedEmail(await response.json())\n }\n}\n\n/** A received-mail notification, or `null` for any other event. */\nexport function resendInboundEvent(event: unknown): MailInboundEvent | null {\n const id = resendReceivedEventId(event)\n return id ? { id, recipients: resendReceivedEventRecipients(event) } : null\n}\n\n/*==========================================\n * THE PROVIDER.\n *=========================================*/\n\n/**\n * Resend as a {@link MailProvider}, reading its two keys from `env` at each\n * call — never at module load, because these run in serverless handlers\n * whose module may be evaluated during a build, long before the runtime\n * environment exists.\n */\nexport function resendMailProvider(\n env: Env = process.env as Env,\n): MailProvider {\n const sendKey = () => setting(env, RESEND_SEND_KEY_SETTING)\n const readKey = () => {\n const key = setting(env, RESEND_READ_KEY_SETTING)\n if (!key) throw new Error(`${RESEND_READ_KEY_SETTING} is not set`)\n return key\n }\n const reads: MailProviderReads = {\n unmet: () =>\n setting(env, RESEND_READ_KEY_SETTING)\n ? null\n : `Set ${RESEND_READ_KEY_SETTING} to a full-access key. The sending ` +\n 'key cannot read mail.',\n // Async, so a missing key REJECTS like every other failed read rather\n // than throwing before the caller has a promise to catch.\n history: async (options) => resendDeliveryHistorySource(readKey())(options),\n message: async (id) => resendDeliveryMessageSource(readKey())(id),\n received: async (id) => resendReceivedEmailSource(readKey())(id),\n sendingDomains: async () => readResendSendingDomains(readKey()),\n }\n return {\n id: RESEND_PROVIDER_ID,\n missingSettings: () => (sendKey() ? [] : [RESEND_SEND_KEY_SETTING]),\n send: (message, context) => sendThroughResend(sendKey(), message, context),\n checkCredentials: () => checkResendCredentials(sendKey()),\n deliveryEvents: normalizeResendDeliveryEvents,\n inboundEvent: resendInboundEvent,\n reads,\n }\n}\n"],"names":["bareSenderAddress","normalizeEventTags","readBounceText","providerRetryAtMs","RESEND_PROVIDER_ID","RESEND_SEND_KEY_SETTING","RESEND_READ_KEY_SETTING","setting","env","name","String","trim","RESEND_SEND_ENDPOINT","postResendEmail","apiKey","payload","context","raw","to","recipients","Array","isArray","map","address","filter","Boolean","length","Error","fetch","method","headers","Authorization","body","JSON","stringify","resendSendPayload","message","from","subject","text","html","Object","keys","tags","replyTo","reply_to","bcc","sendThroughResend","response","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json","id","RESEND_DOMAINS_ENDPOINT","REFUSED_KEY_ERRORS","Set","AUTHENTICATED_BUT_UNSCOPED_ERRORS","resendErrorName","parsed","parse","checkResendCredentials","slice","probeStatus","has","error","readResendSendingDomains","endpoint","flag","value","domains","row","data","toLowerCase","push","clickTracking","click_tracking","openTracking","open_tracking","eventTimeMs","Number","isFinite","Date","normalizeBounceType","lowered","RESEND_EVENT_TYPES","normalizeResendDeliveryEvents","receivedAtMs","event","type","providerMessageId","email_id","includes","at","click","timestamp","open","created_at","bounceText","bounce","diagnosticCode","diagnostic_code","remoteMta","remote_mta","part","join","provider","link","bounceType","failed","reason","bounceStatus","RESEND_LAST_EVENTS","sent","delivered","delivery_delayed","opened","clicked","bounced","complained","canceled","queued","scheduled","normalizeResendSentEmails","record","sentAt","last_event","RESEND_EMAILS_ENDPOINT","resendDeliveryHistorySource","cursor","limit","entries","params","URLSearchParams","Math","min","max","set","snapshots","flatMap","entry","lastId","nextCursor","has_more","addressList","normalizeResendMessage","cc","resendDeliveryMessageSource","encodeURIComponent","RESEND_RECEIVED_EVENT","RESEND_RECEIVING_ENDPOINT","list","header","wanted","key","resendReceivedEventId","resendReceivedEventRecipients","normalizeResendReceivedEmail","nowMs","now","created","messageId","inReplyTo","receivedFor","resendReceivedEmailSource","fetchImpl","resendInboundEvent","resendMailProvider","process","sendKey","readKey","reads","unmet","history","options","received","sendingDomains","missingSettings","send","checkCredentials","deliveryEvents","inboundEvent"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-resend.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 * RESEND, as one {@link MailProvider} — the only module in the platform that\n * knows Resend's endpoints, payloads, event names and error names.\n *\n * It is the provider a deployment gets when it names none, which is how\n * every deployment that set `RESEND_API_KEY` before the contract existed\n * keeps sending exactly as it did. Two credentials, on purpose:\n *\n * - `RESEND_API_KEY` sends. It is provisioned sending-scoped, so a leaked\n * copy cannot enumerate everyone the platform ever wrote to.\n * - `RESEND_READ_API_KEY` is full-access and only ever reads: history, one\n * message's body, received mail and the account's sending domains. A\n * sending-scoped key answers every such read `401 restricted_api_key`.\n *\n * Pure but for the `fetch` calls, each inside a function that must be called\n * to do anything, so a client component that reaches this module through the\n * library's barrel executes nothing on load.\n */\n\nimport {\n bareSenderAddress,\n normalizeEventTags,\n type EmailDeliveryEvent,\n type EmailDeliveryEventType,\n type EmailDeliveryHistorySource,\n type EmailDeliveryMessage,\n type EmailDeliveryMessageSource,\n type EmailDeliverySnapshot,\n} from './email-delivery-events'\nimport type { EmailCredentialReport } from './email-health'\nimport { readBounceText } from './mail-bounce'\nimport {\n providerRetryAtMs,\n type MailInboundEvent,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderReads,\n type MailProviderSendResult,\n type MailSendingDomain,\n} from './mail-provider'\nimport { isAutoSubmittedMail, type ReceivedEmail, type ReceivedEmailSource } from './received-email'\n\n/** The slug stored on every delivery record this provider produces. */\nexport const RESEND_PROVIDER_ID = 'resend'\n\n/** The sending key's environment variable. */\nexport const RESEND_SEND_KEY_SETTING = 'RESEND_API_KEY'\n\n/** The full-access read key's environment variable. */\nexport const RESEND_READ_KEY_SETTING = 'RESEND_READ_API_KEY'\n\ntype Env = Record<string, string | undefined>\n\nfunction setting(env: Env, name: string): string {\n return String(env[name] ?? '').trim()\n}\n\n/*==========================================\n * SENDING.\n *=========================================*/\n\n/** The send endpoint. */\nexport const RESEND_SEND_ENDPOINT = 'https://api.resend.com/emails'\n\n/** A Resend send payload in the provider's own wire shape. */\nexport interface ResendSendPayload {\n to?: unknown\n from?: unknown\n subject?: unknown\n [field: string]: unknown\n}\n\n/**\n * The one place that POSTs to Resend's send endpoint, and the last thing\n * standing between a payload and the network.\n *\n * A payload carrying no recipient cannot become a message. Resend answers it\n * `422 missing_required_field`, which costs an API call and then shows up in\n * the vendor dashboard as a red line indistinguishable from mail that\n * genuinely failed to deliver — carrying no subject, no recipient and nothing\n * naming the code that produced it. Diagnosing that means reading a log\n * outside the deployment and guessing. So the refusal happens here, before\n * the fetch, and names the caller's `context`.\n *\n * It throws rather than returning a result: this is a programming error,\n * not a delivery outcome. `sendEmail` filters recipients well before it\n * reaches this call, so nothing on the ordinary path can trip it. The guard\n * exists because `RESEND_SEND_ENDPOINT` is exported and any module can\n * therefore reach the send endpoint on its own, bypassing every check\n * `sendEmail` owns.\n */\nexport async function postResendEmail(\n apiKey: string,\n payload: ResendSendPayload,\n context?: string,\n): Promise<Response> {\n const raw = payload?.to\n const recipients = (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n if (!recipients.length) {\n throw new Error(\n `${context ? `${context} ` : ''}send refused before the network — a ` +\n 'Resend payload with no `to` field cannot become a message, and the ' +\n 'attempt would surface only as a 422 in the Resend dashboard',\n )\n }\n\n return fetch(RESEND_SEND_ENDPOINT, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n })\n}\n\n/** One message as Resend's send endpoint takes it. */\nexport function resendSendPayload(message: MailProviderMessage): ResendSendPayload {\n return {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { reply_to: message.replyTo } : {}),\n ...(message.bcc?.length ? { bcc: message.bcc } : {}),\n }\n}\n\n/**\n * Sends one message and reads the answer.\n *\n * A 429 is a request to slow down, not a statement about the recipient, so\n * it comes back `rateLimited` with the wait Resend asked for. Its quota\n * errors — `daily_quota_exceeded`, `monthly_quota_exceeded` — are 429s as\n * well, and clear with time the same way.\n */\nasync function sendThroughResend(\n apiKey: string,\n message: MailProviderMessage,\n context?: string,\n): Promise<MailProviderSendResult> {\n const response = await postResendEmail(apiKey, resendSendPayload(message), context)\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n if (response.status === 429) {\n return {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n }\n return { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: string } | null\n return { accepted: true, id: body?.id ?? null }\n}\n\n/*==========================================\n * THE CREDENTIAL PROBE.\n *=========================================*/\n\n/**\n * Where the credential probe and the domain read ask their questions.\n *\n * Deliberately NOT the send endpoint. A probe aimed at `/emails` is a send\n * attempt however empty its body is: it consumes an API call, and Resend\n * records it in the account's logs as a `422` on `POST /emails` with no\n * recipient, no subject and nothing identifying the caller — a line an\n * operator reading that dashboard has to treat as failed mail. A domain read\n * cannot create a message and cannot be mistaken for one.\n */\nexport const RESEND_DOMAINS_ENDPOINT = 'https://api.resend.com/domains'\n\n/**\n * Error names that mean the key itself was not accepted, as opposed to a key\n * that authenticated and merely lacks read scope. Matched by NAME, not\n * status: `401` and `403` each cover both meanings.\n */\nconst REFUSED_KEY_ERRORS = new Set([\n 'missing_api_key',\n 'validation_error',\n 'suspended_api_key',\n])\n\n/**\n * Error names that mean the key authenticated and was then denied this\n * particular read. A sending-scoped key — the shape Aglyn provisions — always\n * lands here, and reaching this answer at all required Resend to recognize\n * the credential, which is exactly what the probe is asking.\n */\nconst AUTHENTICATED_BUT_UNSCOPED_ERRORS = new Set([\n 'restricted_api_key',\n 'invalid_permission',\n])\n\n/** The `name` Resend puts on an error body, or `''` for anything else. */\nfunction resendErrorName(body: string): string {\n try {\n const parsed = JSON.parse(body) as { name?: unknown }\n return typeof parsed?.name === 'string' ? parsed.name : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Whether the sending key is accepted — without sending anything to anybody,\n * and without leaving anything behind that reads as failed mail.\n *\n * How: a `GET` of the domains collection. The question is only ever \"does\n * Resend recognize this credential\", so the probe reads the ERROR NAME rather\n * than the status, because `401` and `403` each carry both meanings:\n *\n * - `2xx` — the key is accepted and has read scope → `ok`\n * - `restricted_api_key` / `invalid_permission` — Resend authenticated the\n * key and then denied it this read. A sending-scoped key always answers\n * this way, and getting the answer proves the credential works → `ok`\n * - `missing_api_key` / `validation_error` / `suspended_api_key` — the key\n * itself was refused → `invalid-key`\n * - anything else → `unknown`\n *\n * An unrecognized rejection is `unknown`, never `invalid-key`: this feeds a\n * staff diagnostics screen whose whole value is that a red line means\n * something, and a shape we have not seen before is not evidence that a\n * working key is broken.\n */\nexport async function checkResendCredentials(\n apiKey: string,\n): Promise<EmailCredentialReport> {\n if (!apiKey) return { status: 'unconfigured' }\n try {\n const response = await fetch(RESEND_DOMAINS_ENDPOINT, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n const detail = (await response.text().catch(() => '')).slice(0, 300)\n\n if (response.status >= 200 && response.status < 300) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (response.status === 401 || response.status === 403) {\n const name = resendErrorName(detail)\n if (AUTHENTICATED_BUT_UNSCOPED_ERRORS.has(name)) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (REFUSED_KEY_ERRORS.has(name)) {\n return { status: 'invalid-key', probeStatus: response.status, detail }\n }\n }\n return { status: 'unknown', probeStatus: response.status, detail }\n } catch (error) {\n return {\n status: 'unknown',\n detail: String((error as Error)?.message ?? error).slice(0, 300),\n }\n }\n}\n\n/**\n * Every sending domain on the account, with its verification and tracking\n * state. Throws with the status when Resend does not answer 2xx, so a caller\n * never reads \"I could not look\" as \"there is nothing there\".\n */\nexport async function readResendSendingDomains(\n apiKey: string,\n endpoint: string = RESEND_DOMAINS_ENDPOINT,\n): Promise<MailSendingDomain[]> {\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`The provider answered ${response.status} to the domain read.`)\n }\n const payload = (await response.json().catch(() => null)) as\n | {\n data?: Array<{\n name?: unknown\n status?: unknown\n click_tracking?: unknown\n open_tracking?: unknown\n }>\n }\n | null\n /**\n * A tri-state, and the third state matters: a listing that does not carry\n * the field must not be reported as the field being false. Off is a fault\n * somebody should fix; unknown is a question this read could not ask.\n */\n const flag = (value: unknown): boolean | null =>\n typeof value === 'boolean' ? value : null\n const domains: MailSendingDomain[] = []\n for (const row of payload?.data ?? []) {\n const name = String(row?.name ?? '').toLowerCase()\n if (!name) continue\n domains.push({\n name,\n status: String(row?.status ?? 'unknown'),\n clickTracking: flag(row?.click_tracking),\n openTracking: flag(row?.open_tracking),\n })\n }\n return domains\n}\n\n/*==========================================\n * THE DELIVERY FEED.\n *=========================================*/\n\n/** Epoch ms from an ISO string or a number, or `null` when unreadable. */\nfunction eventTimeMs(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n const parsed = Date.parse(String(value ?? ''))\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeBounceType(\n value: unknown,\n): EmailDeliveryEvent['bounceType'] {\n const lowered = String(value ?? '')\n .trim()\n .toLowerCase()\n if (lowered === 'permanent') return 'permanent'\n if (lowered === 'transient') return 'transient'\n return lowered ? 'undetermined' : null\n}\n\n/** The Resend event names we understand, mapped onto ours. */\nconst RESEND_EVENT_TYPES: Record<string, EmailDeliveryEventType> = {\n 'email.sent': 'sent',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delayed',\n 'email.opened': 'opened',\n 'email.clicked': 'clicked',\n 'email.bounced': 'bounced',\n 'email.complained': 'complained',\n 'email.failed': 'failed',\n}\n\n/**\n * Turns one Resend webhook payload into zero or more of our events.\n *\n * One event per recipient, not per message: a send addressed to three people\n * produces one webhook, and a staff view keyed on a person has to be able to\n * find it under each of them.\n *\n * Returns an empty array for anything unrecognised — a contact or domain\n * event, an inbound `email.received`, a type added after this was written.\n * Silence rather than a throw, because a webhook handler that 500s on an\n * unfamiliar event teaches the provider to retry it forever.\n *\n * @param payload The parsed webhook body.\n * @param receivedAtMs Fallback timestamp for a payload that carries none.\n */\nexport function normalizeResendDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n): EmailDeliveryEvent[] {\n const event = (payload ?? {}) as Record<string, any>\n const type = RESEND_EVENT_TYPES[String(event.type ?? '')]\n if (!type) return []\n\n const data = (event.data ?? {}) as Record<string, any>\n const providerMessageId = String(data.email_id ?? data.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(data.to) ? data.to : [data.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n // The per-state timestamp when the provider gives one, because an open\n // three days after the send is the whole point of recording an open.\n const at =\n eventTimeMs(data.click?.timestamp) ??\n eventTimeMs(data.open?.timestamp) ??\n eventTimeMs(event.created_at) ??\n eventTimeMs(data.created_at) ??\n receivedAtMs\n\n const subject = String(data.subject ?? '').trim() || null\n const tags = normalizeEventTags(data.tags)\n const from = bareSenderAddress(data.from)\n // The provider folds a DSN's fields into one sentence; the structured\n // ones are read first where a payload carries them.\n const bounceText =\n type === 'bounced'\n ? readBounceText(\n [data.bounce?.diagnosticCode, data.bounce?.diagnostic_code, data.bounce?.remoteMta, data.bounce?.remote_mta, data.bounce?.message]\n .filter((part: unknown) => typeof part === 'string' && part.trim())\n .join(' '),\n )\n : null\n\n return recipients.map((to: string) => ({\n type,\n at,\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject,\n context: tags['context'] || null,\n tags,\n link: type === 'clicked' ? String(data.click?.link ?? '') || null : null,\n bounceType: type === 'bounced' ? normalizeBounceType(data.bounce?.type) : null,\n detail:\n String(data.bounce?.message ?? data.failed?.reason ?? '').trim() || null,\n ...(from ? { from } : {}),\n ...(bounceText?.status ? { bounceStatus: bounceText.status } : {}),\n ...(bounceText?.remoteMta ? { remoteMta: bounceText.remoteMta } : {}),\n }))\n}\n\n/*==========================================\n * THE ACCOUNT'S HISTORY.\n *\n * Resend's list endpoint reports a single `last_event` per message and no\n * engagement detail, so a snapshot can say \"this was opened at least once\"\n * and can never say \"three times\" — see `EmailDeliverySnapshot`.\n *=========================================*/\n\n/**\n * A `last_event` string, mapped onto our lifecycle.\n *\n * Deliberately the bare state names rather than the `email.*` event names:\n * the list endpoint reports `\"delivered\"`, the webhook reports\n * `\"email.delivered\"`, and they are two different vocabularies for one\n * concept.\n */\nconst RESEND_LAST_EVENTS: Record<string, EmailDeliveryEventType> = {\n sent: 'sent',\n delivered: 'delivered',\n delivery_delayed: 'delayed',\n opened: 'opened',\n clicked: 'clicked',\n bounced: 'bounced',\n complained: 'complained',\n failed: 'failed',\n canceled: 'failed',\n queued: 'sent',\n scheduled: 'sent',\n}\n\n/**\n * Turns one entry from Resend's `GET /emails` list into zero or more\n * snapshots — one per recipient, for the same reason the event adapter fans\n * out: the staff view is keyed on a person.\n *\n * An unrecognised `last_event` falls back to `sent` rather than being\n * dropped. The message demonstrably exists and was addressed to somebody, and\n * \"we sent this and cannot characterise what happened next\" is a far more\n * useful row than no row — which is the state that sent a staffer to the\n * vendor dashboard in the first place.\n */\nexport function normalizeResendSentEmails(\n raw: unknown,\n): EmailDeliverySnapshot[] {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(record.to) ? record.to : [record.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n const parsed = Date.parse(String(record.created_at ?? ''))\n const sentAt = Number.isFinite(parsed) ? parsed : 0\n // A snapshot with no timestamp cannot be ordered, and the log's read drops\n // any document missing its sort key — so it is refused rather than written\n // somewhere nothing will look for it.\n if (!sentAt) return []\n\n const status =\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? 'sent'\n\n return recipients.map((to: string) => ({\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject: String(record.subject ?? '').trim() || null,\n sentAt,\n status,\n }))\n}\n\n/** The list endpoint. Paginates with `after=<id>`; caps at 100. */\nexport const RESEND_EMAILS_ENDPOINT = 'https://api.resend.com/emails'\n\n/**\n * {@link EmailDeliveryHistorySource} for Resend. Needs the full-access key.\n *\n * The list endpoint takes `limit`, `after` and `before` and no recipient\n * filter, which is why the source is a cursor-paged sweep.\n */\nexport function resendDeliveryHistorySource(\n apiKey: string,\n): EmailDeliveryHistorySource {\n return async ({ cursor, limit } = {}) => {\n const params = new URLSearchParams({\n limit: String(Math.min(Math.max(1, limit ?? 100), 100)),\n })\n if (cursor) params.set('after', cursor)\n const response = await fetch(`${RESEND_EMAILS_ENDPOINT}?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `email history read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n const body = (await response.json()) as {\n data?: unknown[]\n has_more?: boolean\n }\n const entries = Array.isArray(body?.data) ? body.data : []\n const snapshots = entries.flatMap((entry) => normalizeResendSentEmails(entry))\n // The cursor is the LAST RAW entry's id, not the last snapshot's: a page\n // whose final entry fanned out to zero snapshots (no recipient, no\n // timestamp) would otherwise rewind the cursor to an earlier message and\n // loop over the same page forever.\n const lastId = String(\n (entries[entries.length - 1] as { id?: unknown })?.id ?? '',\n ).trim()\n return {\n snapshots,\n nextCursor: body?.has_more && lastId ? lastId : null,\n }\n }\n}\n\nfunction addressList(raw: unknown): string[] {\n return (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n}\n\n/** Resend's `GET /emails/:id` payload, in our vocabulary. */\nexport function normalizeResendMessage(raw: unknown): EmailDeliveryMessage | null {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return null\n const parsed = Date.parse(String(record.created_at ?? ''))\n return {\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to: addressList(record.to),\n cc: addressList(record.cc),\n bcc: addressList(record.bcc),\n from: String(record.from ?? '').trim() || null,\n replyTo: addressList(record.reply_to).length\n ? addressList(record.reply_to)\n : null,\n subject: String(record.subject ?? '').trim() || null,\n // Empty string is NOT null here, and the difference is the point: a\n // message that went out text-only really does have an empty HTML part,\n // and a reader has to be able to tell that from \"we could not fetch it\".\n html: typeof record.html === 'string' ? record.html : null,\n text: typeof record.text === 'string' ? record.text : null,\n sentAt: Number.isFinite(parsed) ? parsed : null,\n status:\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? null,\n }\n}\n\n/** Resend's single-message endpoint. Needs the full-access key. */\nexport function resendDeliveryMessageSource(\n apiKey: string,\n): EmailDeliveryMessageSource {\n return async (providerMessageId: string) => {\n const response = await fetch(\n `${RESEND_EMAILS_ENDPOINT}/${encodeURIComponent(providerMessageId)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n // A message the provider has aged out is a 404, and that is an ANSWER —\n // \"we know this was sent and the body is gone\" — not a failure to report\n // as an error the staffer must act on.\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `message read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendMessage(await response.json())\n }\n}\n\n/*==========================================\n * RECEIVED MAIL.\n *\n * Resend announces a received message as an `email.received` webhook that\n * carries only its metadata — who it was from and to, its subject, its\n * `Message-ID` — and the message itself is read afterwards from the\n * receiving API by the id the event named.\n *=========================================*/\n\n/** The webhook event type a received message arrives as. */\nexport const RESEND_RECEIVED_EVENT = 'email.received'\n\n/** `GET {endpoint}/{id}` — one received message with its text and headers. */\nexport const RESEND_RECEIVING_ENDPOINT = 'https://api.resend.com/emails/receiving'\n\nconst list = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.map((entry) => String(entry ?? '').trim()).filter((entry) => entry !== '')\n : typeof value === 'string' && value.trim()\n ? [value.trim()]\n : []\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A header by name, case-insensitively, off whatever map the provider sent. */\nfunction header(headers: unknown, name: string): string {\n if (!headers || typeof headers !== 'object') return ''\n const wanted = name.toLowerCase()\n for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {\n if (key.toLowerCase() === wanted) {\n return Array.isArray(value) ? String(value[0] ?? '').trim() : String(value ?? '').trim()\n }\n }\n return ''\n}\n\n/**\n * The provider's id of the message an `email.received` event announces,\n * or `null` for any other event — including a malformed one, which is\n * acknowledged and dropped rather than retried forever.\n */\nexport function resendReceivedEventId(event: unknown): string | null {\n const record = event as { type?: unknown; data?: { email_id?: unknown } } | null\n if (!record || record.type !== RESEND_RECEIVED_EVENT) return null\n const id = String(record.data?.email_id ?? '').trim()\n return id || null\n}\n\n/**\n * Every recipient the event names — To, Cc, Bcc and the addresses the\n * message was received for — so a route can find what it is looking for in\n * the metadata before it spends a read on the body.\n */\nexport function resendReceivedEventRecipients(event: unknown): string[] {\n const data = (event as { data?: Record<string, unknown> } | null)?.data ?? {}\n return [\n ...list(data['to']),\n ...list(data['cc']),\n ...list(data['bcc']),\n ...list(data['received_for']),\n ]\n}\n\n/**\n * One received message as the receiving API answers it, in the neutral\n * shape. `null` for a payload with no id, which is not a message.\n */\nexport function normalizeResendReceivedEmail(\n raw: unknown,\n nowMs: number = Date.now(),\n): ReceivedEmail | null {\n const record = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const id = String(record['id'] ?? '').trim()\n if (!id) return null\n const headers = record['headers']\n const created = Date.parse(String(record['created_at'] ?? ''))\n return {\n id,\n messageId: String(record['message_id'] ?? '').trim() || header(headers, 'message-id'),\n inReplyTo: header(headers, 'in-reply-to'),\n from: String(record['from'] ?? '').trim(),\n to: list(record['to']),\n cc: list(record['cc']),\n bcc: list(record['bcc']),\n receivedFor: list(record['received_for']),\n subject: String(record['subject'] ?? '').trim(),\n text: text(record['text']),\n html: text(record['html']),\n receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs,\n autoSubmitted: isAutoSubmittedMail((name) => header(headers, name)),\n }\n}\n\n/**\n * {@link ReceivedEmailSource} for Resend. Needs the full-access key. A `404`\n * is `null` — the message expired or never existed — and anything else\n * throws with the status, so the caller decides whether the provider should\n * retry.\n */\nexport function resendReceivedEmailSource(\n apiKey: string,\n fetchImpl: typeof fetch = fetch,\n): ReceivedEmailSource {\n return async (id) => {\n const response = await fetchImpl(\n `${RESEND_RECEIVING_ENDPOINT}/${encodeURIComponent(id)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `received email read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendReceivedEmail(await response.json())\n }\n}\n\n/** A received-mail notification, or `null` for any other event. */\nexport function resendInboundEvent(event: unknown): MailInboundEvent | null {\n const id = resendReceivedEventId(event)\n return id ? { id, recipients: resendReceivedEventRecipients(event) } : null\n}\n\n/*==========================================\n * THE PROVIDER.\n *=========================================*/\n\n/**\n * Resend as a {@link MailProvider}, reading its two keys from `env` at each\n * call — never at module load, because these run in serverless handlers\n * whose module may be evaluated during a build, long before the runtime\n * environment exists.\n */\nexport function resendMailProvider(\n env: Env = process.env as Env,\n): MailProvider {\n const sendKey = () => setting(env, RESEND_SEND_KEY_SETTING)\n const readKey = () => {\n const key = setting(env, RESEND_READ_KEY_SETTING)\n if (!key) throw new Error(`${RESEND_READ_KEY_SETTING} is not set`)\n return key\n }\n const reads: MailProviderReads = {\n unmet: () =>\n setting(env, RESEND_READ_KEY_SETTING)\n ? null\n : `Set ${RESEND_READ_KEY_SETTING} to a full-access key. The sending ` +\n 'key cannot read mail.',\n // Async, so a missing key REJECTS like every other failed read rather\n // than throwing before the caller has a promise to catch.\n history: async (options) => resendDeliveryHistorySource(readKey())(options),\n message: async (id) => resendDeliveryMessageSource(readKey())(id),\n received: async (id) => resendReceivedEmailSource(readKey())(id),\n sendingDomains: async () => readResendSendingDomains(readKey()),\n }\n return {\n id: RESEND_PROVIDER_ID,\n missingSettings: () => (sendKey() ? [] : [RESEND_SEND_KEY_SETTING]),\n send: (message, context) => sendThroughResend(sendKey(), message, context),\n checkCredentials: () => checkResendCredentials(sendKey()),\n deliveryEvents: normalizeResendDeliveryEvents,\n inboundEvent: resendInboundEvent,\n reads,\n }\n}\n"],"names":["bareSenderAddress","normalizeEventTags","readBounceText","providerRetryAtMs","isAutoSubmittedMail","RESEND_PROVIDER_ID","RESEND_SEND_KEY_SETTING","RESEND_READ_KEY_SETTING","setting","env","name","String","trim","RESEND_SEND_ENDPOINT","postResendEmail","apiKey","payload","context","raw","to","recipients","Array","isArray","map","address","filter","Boolean","length","Error","fetch","method","headers","Authorization","body","JSON","stringify","resendSendPayload","message","from","subject","text","html","Object","keys","tags","replyTo","reply_to","bcc","sendThroughResend","response","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json","id","RESEND_DOMAINS_ENDPOINT","REFUSED_KEY_ERRORS","Set","AUTHENTICATED_BUT_UNSCOPED_ERRORS","resendErrorName","parsed","parse","checkResendCredentials","slice","probeStatus","has","error","readResendSendingDomains","endpoint","flag","value","domains","row","data","toLowerCase","push","clickTracking","click_tracking","openTracking","open_tracking","eventTimeMs","Number","isFinite","Date","normalizeBounceType","lowered","RESEND_EVENT_TYPES","normalizeResendDeliveryEvents","receivedAtMs","event","type","providerMessageId","email_id","includes","at","click","timestamp","open","created_at","bounceText","bounce","diagnosticCode","diagnostic_code","remoteMta","remote_mta","part","join","provider","link","bounceType","failed","reason","bounceStatus","RESEND_LAST_EVENTS","sent","delivered","delivery_delayed","opened","clicked","bounced","complained","canceled","queued","scheduled","normalizeResendSentEmails","record","sentAt","last_event","RESEND_EMAILS_ENDPOINT","resendDeliveryHistorySource","cursor","limit","entries","params","URLSearchParams","Math","min","max","set","snapshots","flatMap","entry","lastId","nextCursor","has_more","addressList","normalizeResendMessage","cc","resendDeliveryMessageSource","encodeURIComponent","RESEND_RECEIVED_EVENT","RESEND_RECEIVING_ENDPOINT","list","header","wanted","key","resendReceivedEventId","resendReceivedEventRecipients","normalizeResendReceivedEmail","nowMs","now","created","messageId","inReplyTo","receivedFor","autoSubmitted","resendReceivedEmailSource","fetchImpl","resendInboundEvent","resendMailProvider","process","sendKey","readKey","reads","unmet","history","options","received","sendingDomains","missingSettings","send","checkCredentials","deliveryEvents","inboundEvent"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAED,SACEA,iBAAiB,EACjBC,kBAAkB,QAOb,6BAAyB;AAEhC,SAASC,cAAc,QAAQ,mBAAe;AAC9C,SACEC,iBAAiB,QAOZ,qBAAiB;AACxB,SAASC,mBAAmB,QAAsD,sBAAkB;AAEpG,qEAAqE,GACrE,OAAO,MAAMC,qBAAqB,SAAQ;AAE1C,4CAA4C,GAC5C,OAAO,MAAMC,0BAA0B,iBAAgB;AAEvD,qDAAqD,GACrD,OAAO,MAAMC,0BAA0B,sBAAqB;AAI5D,SAASC,QAAQC,GAAQ,EAAEC,IAAY;QACvBD;IAAd,OAAOE,QAAOF,YAAAA,GAAG,CAACC,KAAK,YAATD,YAAa,IAAIG,IAAI;AACrC;AAEA;;2CAE2C,GAE3C,uBAAuB,GACvB,OAAO,MAAMC,uBAAuB,gCAA+B;AAUnE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,eAAeC,gBACpBC,MAAc,EACdC,OAA0B,EAC1BC,OAAgB;IAEhB,MAAMC,MAAMF,2BAAAA,QAASG,EAAE;IACvB,MAAMC,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAACJ,OAAOA,MAAMA,OAAO,OAAO,EAAE,GAAG;QAACA;KAAI,AAAD,EACnEK,GAAG,CAAC,CAACC,UAAYb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,IAC3Ca,MAAM,CAACC;IACV,IAAI,CAACN,WAAWO,MAAM,EAAE;QACtB,MAAM,IAAIC,MACR,GAAGX,UAAU,GAAGA,QAAQ,CAAC,CAAC,GAAG,GAAG,oCAAoC,CAAC,GACnE,wEACA;IAEN;IAEA,OAAOY,MAAMhB,sBAAsB;QACjCiB,QAAQ;QACRC,SAAS;YACPC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YACjC,gBAAgB;QAClB;QACAkB,MAAMC,KAAKC,SAAS,CAACnB;IACvB;AACF;AAEA,oDAAoD,GACpD,OAAO,SAASoB,kBAAkBC,OAA4B;QAUtDA,eAEAA;IAXN,OAAO;QACLC,MAAMD,QAAQC,IAAI;QAClBnB,IAAIkB,QAAQlB,EAAE;QACdoB,SAASF,QAAQE,OAAO;OACpBF,QAAQG,IAAI,GAAG;QAAEA,MAAMH,QAAQG,IAAI;IAAC,IAAI,CAAC,GACzCH,QAAQI,IAAI,GAAG;QAAEA,MAAMJ,QAAQI,IAAI;IAAC,IAAI,CAAC,GACzCJ,QAAQN,OAAO,IAAIW,OAAOC,IAAI,CAACN,QAAQN,OAAO,EAAEJ,MAAM,GACtD;QAAEI,SAASM,QAAQN,OAAO;IAAC,IAC3B,CAAC,GACDM,EAAAA,gBAAAA,QAAQO,IAAI,qBAAZP,cAAcV,MAAM,IAAG;QAAEiB,MAAMP,QAAQO,IAAI;IAAC,IAAI,CAAC,GACjDP,QAAQQ,OAAO,GAAG;QAAEC,UAAUT,QAAQQ,OAAO;IAAC,IAAI,CAAC,GACnDR,EAAAA,eAAAA,QAAQU,GAAG,qBAAXV,aAAaV,MAAM,IAAG;QAAEoB,KAAKV,QAAQU,GAAG;IAAC,IAAI,CAAC;AAEtD;AAEA;;;;;;;CAOC,GACD,eAAeC,kBACbjC,MAAc,EACdsB,OAA4B,EAC5BpB,OAAgB;;IAEhB,MAAMgC,WAAW,MAAMnC,gBAAgBC,QAAQqB,kBAAkBC,UAAUpB;IAC3E,IAAI,CAACgC,SAASC,EAAE,EAAE;QAChB,MAAMC,SAAS,MAAMF,SAAST,IAAI,GAAGY,KAAK,CAAC,IAAM;QACjD,IAAIH,SAASI,MAAM,KAAK,KAAK;YAC3B,OAAO;gBACLC,UAAU;gBACVC,aAAa;gBACbC,WAAWrD,kBAAkB8C,SAASlB,OAAO;gBAC7CsB,QAAQJ,SAASI,MAAM;gBACvBF;YACF;QACF;QACA,OAAO;YAAEG,UAAU;YAAOC,aAAa;YAAOF,QAAQJ,SAASI,MAAM;YAAEF;QAAO;IAChF;IACA,MAAMlB,OAAQ,MAAMgB,SAASQ,IAAI,GAAGL,KAAK,CAAC,IAAM;IAChD,OAAO;QAAEE,UAAU;QAAMI,EAAE,UAAEzB,wBAAAA,KAAMyB,EAAE,mBAAI;IAAK;AAChD;AAEA;;2CAE2C,GAE3C;;;;;;;;;CASC,GACD,OAAO,MAAMC,0BAA0B,iCAAgC;AAEvE;;;;CAIC,GACD,MAAMC,qBAAqB,IAAIC,IAAI;IACjC;IACA;IACA;CACD;AAED;;;;;CAKC,GACD,MAAMC,oCAAoC,IAAID,IAAI;IAChD;IACA;CACD;AAED,wEAAwE,GACxE,SAASE,gBAAgB9B,IAAY;IACnC,IAAI;QACF,MAAM+B,SAAS9B,KAAK+B,KAAK,CAAChC;QAC1B,OAAO,QAAO+B,0BAAAA,OAAQtD,IAAI,MAAK,WAAWsD,OAAOtD,IAAI,GAAG;IAC1D,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,eAAewD,uBACpBnD,MAAc;IAEd,IAAI,CAACA,QAAQ,OAAO;QAAEsC,QAAQ;IAAe;IAC7C,IAAI;QACF,MAAMJ,WAAW,MAAMpB,MAAM8B,yBAAyB;YACpD7B,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAC/C;QACA,MAAMoC,SAAS,AAAC,CAAA,MAAMF,SAAST,IAAI,GAAGY,KAAK,CAAC,IAAM,GAAE,EAAGe,KAAK,CAAC,GAAG;QAEhE,IAAIlB,SAASI,MAAM,IAAI,OAAOJ,SAASI,MAAM,GAAG,KAAK;YACnD,OAAO;gBAAEA,QAAQ;gBAAMe,aAAanB,SAASI,MAAM;YAAC;QACtD;QACA,IAAIJ,SAASI,MAAM,KAAK,OAAOJ,SAASI,MAAM,KAAK,KAAK;YACtD,MAAM3C,OAAOqD,gBAAgBZ;YAC7B,IAAIW,kCAAkCO,GAAG,CAAC3D,OAAO;gBAC/C,OAAO;oBAAE2C,QAAQ;oBAAMe,aAAanB,SAASI,MAAM;gBAAC;YACtD;YACA,IAAIO,mBAAmBS,GAAG,CAAC3D,OAAO;gBAChC,OAAO;oBAAE2C,QAAQ;oBAAee,aAAanB,SAASI,MAAM;oBAAEF;gBAAO;YACvE;QACF;QACA,OAAO;YAAEE,QAAQ;YAAWe,aAAanB,SAASI,MAAM;YAAEF;QAAO;IACnE,EAAE,OAAOmB,OAAO;;QACd,OAAO;YACLjB,QAAQ;YACRF,QAAQxC,eAAQ2D,yBAAD,AAACA,MAAiBjC,OAAO,mBAAIiC,OAAOH,KAAK,CAAC,GAAG;QAC9D;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeI,yBACpBxD,MAAc,EACdyD,WAAmBb,uBAAuB;;IAE1C,MAAMV,WAAW,MAAMpB,MAAM2C,UAAU;QACrC1C,QAAQ;QACRC,SAAS;YAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;QAAC;IAC/C;IACA,IAAIkC,SAASI,MAAM,GAAG,OAAOJ,SAASI,MAAM,IAAI,KAAK;QACnD,MAAM,IAAIzB,MAAM,CAAC,sBAAsB,EAAEqB,SAASI,MAAM,CAAC,oBAAoB,CAAC;IAChF;IACA,MAAMrC,UAAW,MAAMiC,SAASQ,IAAI,GAAGL,KAAK,CAAC,IAAM;IAUnD;;;;GAIC,GACD,MAAMqB,OAAO,CAACC,QACZ,OAAOA,UAAU,YAAYA,QAAQ;IACvC,MAAMC,UAA+B,EAAE;IACvC,KAAK,MAAMC,eAAO5D,2BAAAA,QAAS6D,IAAI,mBAAI,EAAE,CAAE;;QACrC,MAAMnE,OAAOC,gBAAOiE,uBAAAA,IAAKlE,IAAI,oBAAI,IAAIoE,WAAW;QAChD,IAAI,CAACpE,MAAM;QACXiE,QAAQI,IAAI,CAAC;YACXrE;YACA2C,QAAQ1C,gBAAOiE,uBAAAA,IAAKvB,MAAM,oBAAI;YAC9B2B,eAAeP,KAAKG,uBAAAA,IAAKK,cAAc;YACvCC,cAAcT,KAAKG,uBAAAA,IAAKO,aAAa;QACvC;IACF;IACA,OAAOR;AACT;AAEA;;2CAE2C,GAE3C,wEAAwE,GACxE,SAASS,YAAYV,KAAc;IACjC,IAAI,OAAOA,UAAU,YAAYW,OAAOC,QAAQ,CAACZ,QAAQ,OAAOA;IAChE,MAAMV,SAASuB,KAAKtB,KAAK,CAACtD,OAAO+D,gBAAAA,QAAS;IAC1C,OAAOW,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;AAC5C;AAEA,SAASwB,oBACPd,KAAc;IAEd,MAAMe,UAAU9E,OAAO+D,gBAAAA,QAAS,IAC7B9D,IAAI,GACJkE,WAAW;IACd,IAAIW,YAAY,aAAa,OAAO;IACpC,IAAIA,YAAY,aAAa,OAAO;IACpC,OAAOA,UAAU,iBAAiB;AACpC;AAEA,4DAA4D,GAC5D,MAAMC,qBAA6D;IACjE,cAAc;IACd,mBAAmB;IACnB,0BAA0B;IAC1B,gBAAgB;IAChB,iBAAiB;IACjB,iBAAiB;IACjB,oBAAoB;IACpB,gBAAgB;AAClB;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,8BACd3E,OAAgB,EAChB4E,YAAoB;QAGmBC,aAGzBA,aACmBhB,MAAAA,gBAW/BO,OAAAA,OAAAA,OAAAA,cAMqBP;QANTA,aACAA,YAaLA,cAA6BA,eAA8BA,eAAwBA,eAAyBA;IA9BrH,MAAMgB,QAAS7E,kBAAAA,UAAW,CAAC;IAC3B,MAAM8E,OAAOJ,kBAAkB,CAAC/E,QAAOkF,cAAAA,MAAMC,IAAI,YAAVD,cAAc,IAAI;IACzD,IAAI,CAACC,MAAM,OAAO,EAAE;IAEpB,MAAMjB,QAAQgB,cAAAA,MAAMhB,IAAI,YAAVgB,cAAc,CAAC;IAC7B,MAAME,oBAAoBpF,QAAOkE,QAAAA,iBAAAA,KAAKmB,QAAQ,YAAbnB,iBAAiBA,KAAKnB,EAAE,YAAxBmB,OAA4B,IAAIjE,IAAI;IACrE,IAAI,CAACmF,mBAAmB,OAAO,EAAE;IAEjC,MAAM3E,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAACuD,KAAK1D,EAAE,IAAI0D,KAAK1D,EAAE,GAAG;QAAC0D,KAAK1D,EAAE;KAAC,AAAD,EAC5DI,GAAG,CAAC,CAACC,UAAqBb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,GAAGkE,WAAW,IAClErD,MAAM,CAAC,CAACD,UAAoBA,QAAQyE,QAAQ,CAAC;IAChD,IAAI,CAAC7E,WAAWO,MAAM,EAAE,OAAO,EAAE;IAEjC,uEAAuE;IACvE,qEAAqE;IACrE,MAAMuE,MACJd,SAAAA,SAAAA,SAAAA,eAAAA,aAAYP,cAAAA,KAAKsB,KAAK,qBAAVtB,YAAYuB,SAAS,aAAjChB,eACAA,aAAYP,aAAAA,KAAKwB,IAAI,qBAATxB,WAAWuB,SAAS,aADhChB,QAEAA,YAAYS,MAAMS,UAAU,aAF5BlB,QAGAA,YAAYP,KAAKyB,UAAU,aAH3BlB,QAIAQ;IAEF,MAAMrD,UAAU5B,QAAOkE,gBAAAA,KAAKtC,OAAO,YAAZsC,gBAAgB,IAAIjE,IAAI,MAAM;IACrD,MAAMgC,OAAO3C,mBAAmB4E,KAAKjC,IAAI;IACzC,MAAMN,OAAOtC,kBAAkB6E,KAAKvC,IAAI;IACxC,sEAAsE;IACtE,oDAAoD;IACpD,MAAMiE,aACJT,SAAS,YACL5F,eACE;SAAC2E,eAAAA,KAAK2B,MAAM,qBAAX3B,aAAa4B,cAAc;SAAE5B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa6B,eAAe;SAAE7B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa8B,SAAS;SAAE9B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa+B,UAAU;SAAE/B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAaxC,OAAO;KAAC,CAC/HZ,MAAM,CAAC,CAACoF,OAAkB,OAAOA,SAAS,YAAYA,KAAKjG,IAAI,IAC/DkG,IAAI,CAAC,QAEV;IAEN,OAAO1F,WAAWG,GAAG,CAAC,CAACJ;kBAYZ0D;YAHyBA,aACmBA,cAE5CA,eAAwBA;eAZI;YACrCiB;YACAI;YACAa,UAAU1G;YACV0F;YACA5E;YACAoB;YACAtB,SAAS2B,IAAI,CAAC,UAAU,IAAI;YAC5BA;YACAoE,MAAMlB,SAAS,YAAYnF,gBAAOkE,cAAAA,KAAKsB,KAAK,qBAAVtB,YAAYmC,IAAI,mBAAI,OAAO,OAAO;YACpEC,YAAYnB,SAAS,YAAYN,qBAAoBX,eAAAA,KAAK2B,MAAM,qBAAX3B,aAAaiB,IAAI,IAAI;YAC1E3C,QACExC,QAAOkE,kBAAAA,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAaxC,OAAO,qBAAIwC,eAAAA,KAAKqC,MAAM,qBAAXrC,aAAasC,MAAM,YAA3CtC,QAA+C,IAAIjE,IAAI,MAAM;WAClE0B,OAAO;YAAEA;QAAK,IAAI,CAAC,GACnBiE,CAAAA,8BAAAA,WAAYlD,MAAM,IAAG;YAAE+D,cAAcb,WAAWlD,MAAM;QAAC,IAAI,CAAC,GAC5DkD,CAAAA,8BAAAA,WAAYI,SAAS,IAAG;YAAEA,WAAWJ,WAAWI,SAAS;QAAC,IAAI,CAAC;;AAEvE;AAEA;;;;;;2CAM2C,GAE3C;;;;;;;CAOC,GACD,MAAMU,qBAA6D;IACjEC,MAAM;IACNC,WAAW;IACXC,kBAAkB;IAClBC,QAAQ;IACRC,SAAS;IACTC,SAAS;IACTC,YAAY;IACZV,QAAQ;IACRW,UAAU;IACVC,QAAQ;IACRC,WAAW;AACb;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,0BACd9G,GAAY;QAGqB+G,YAQAA,oBAQ/BZ,6CACSY;IAlBX,MAAMA,SAAU/G,cAAAA,MAAO,CAAC;IACxB,MAAM6E,oBAAoBpF,QAAOsH,aAAAA,OAAOvE,EAAE,YAATuE,aAAa,IAAIrH,IAAI;IACtD,IAAI,CAACmF,mBAAmB,OAAO,EAAE;IAEjC,MAAM3E,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAAC2G,OAAO9G,EAAE,IAAI8G,OAAO9G,EAAE,GAAG;QAAC8G,OAAO9G,EAAE;KAAC,AAAD,EAClEI,GAAG,CAAC,CAACC,UAAqBb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,GAAGkE,WAAW,IAClErD,MAAM,CAAC,CAACD,UAAoBA,QAAQyE,QAAQ,CAAC;IAChD,IAAI,CAAC7E,WAAWO,MAAM,EAAE,OAAO,EAAE;IAEjC,MAAMqC,SAASuB,KAAKtB,KAAK,CAACtD,QAAOsH,qBAAAA,OAAO3B,UAAU,YAAjB2B,qBAAqB;IACtD,MAAMC,SAAS7C,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;IAClD,2EAA2E;IAC3E,2EAA2E;IAC3E,sCAAsC;IACtC,IAAI,CAACkE,QAAQ,OAAO,EAAE;IAEtB,MAAM7E,UACJgE,8CAAAA,kBAAkB,CAChB1G,QAAOsH,qBAAAA,OAAOE,UAAU,YAAjBF,qBAAqB,IACzBrH,IAAI,GACJkE,WAAW,GACf,YAJDuC,8CAIK;IAEP,OAAOjG,WAAWG,GAAG,CAAC,CAACJ;YAIL8G;eAJqB;YACrClB,UAAU1G;YACV0F;YACA5E;YACAoB,SAAS5B,QAAOsH,kBAAAA,OAAO1F,OAAO,YAAd0F,kBAAkB,IAAIrH,IAAI,MAAM;YAChDsH;YACA7E;QACF;;AACF;AAEA,iEAAiE,GACjE,OAAO,MAAM+E,yBAAyB,gCAA+B;AAErE;;;;;CAKC,GACD,OAAO,SAASC,4BACdtH,MAAc;IAEd,OAAO,OAAO,EAAEuH,MAAM,EAAEC,KAAK,EAAE,GAAG,CAAC,CAAC;;YAyB/BC;QAxBH,MAAMC,SAAS,IAAIC,gBAAgB;YACjCH,OAAO5H,OAAOgI,KAAKC,GAAG,CAACD,KAAKE,GAAG,CAAC,GAAGN,gBAAAA,QAAS,MAAM;QACpD;QACA,IAAID,QAAQG,OAAOK,GAAG,CAAC,SAASR;QAChC,MAAMrF,WAAW,MAAMpB,MAAM,GAAGuG,uBAAuB,CAAC,EAAEK,QAAQ,EAAE;YAClE1G,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAC/C;QACA,IAAI,CAACkC,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAAST,IAAI,GAAGY,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIxB,MACR,CAAC,gCAAgC,EAAEqB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAEhF;QACA,MAAMlC,OAAQ,MAAMgB,SAASQ,IAAI;QAIjC,MAAM+E,UAAUnH,MAAMC,OAAO,CAACW,wBAAAA,KAAM4C,IAAI,IAAI5C,KAAK4C,IAAI,GAAG,EAAE;QAC1D,MAAMkE,YAAYP,QAAQQ,OAAO,CAAC,CAACC,QAAUjB,0BAA0BiB;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,yEAAyE;QACzE,mCAAmC;QACnC,MAAMC,SAASvI,gBACZ6H,YAAAA,OAAO,CAACA,QAAQ7G,MAAM,GAAG,EAAE,qBAA5B,AAAC6G,UAAkD9E,EAAE,mBAAI,IACzD9C,IAAI;QACN,OAAO;YACLmI;YACAI,YAAYlH,CAAAA,wBAAAA,KAAMmH,QAAQ,KAAIF,SAASA,SAAS;QAClD;IACF;AACF;AAEA,SAASG,YAAYnI,GAAY;IAC/B,OAAO,AAACG,CAAAA,MAAMC,OAAO,CAACJ,OAAOA,MAAMA,OAAO,OAAO,EAAE,GAAG;QAACA;KAAI,AAAD,EACvDK,GAAG,CAAC,CAACC,UAAYb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,IAC3Ca,MAAM,CAACC;AACZ;AAEA,2DAA2D,GAC3D,OAAO,SAAS4H,uBAAuBpI,GAAY;QAEhB+G,YAEAA,oBAOlBA,cAIGA,iBAQdZ,6CACSY;IAvBb,MAAMA,SAAU/G,cAAAA,MAAO,CAAC;IACxB,MAAM6E,oBAAoBpF,QAAOsH,aAAAA,OAAOvE,EAAE,YAATuE,aAAa,IAAIrH,IAAI;IACtD,IAAI,CAACmF,mBAAmB,OAAO;IAC/B,MAAM/B,SAASuB,KAAKtB,KAAK,CAACtD,QAAOsH,qBAAAA,OAAO3B,UAAU,YAAjB2B,qBAAqB;IACtD,OAAO;QACLlB,UAAU1G;QACV0F;QACA5E,IAAIkI,YAAYpB,OAAO9G,EAAE;QACzBoI,IAAIF,YAAYpB,OAAOsB,EAAE;QACzBxG,KAAKsG,YAAYpB,OAAOlF,GAAG;QAC3BT,MAAM3B,QAAOsH,eAAAA,OAAO3F,IAAI,YAAX2F,eAAe,IAAIrH,IAAI,MAAM;QAC1CiC,SAASwG,YAAYpB,OAAOnF,QAAQ,EAAEnB,MAAM,GACxC0H,YAAYpB,OAAOnF,QAAQ,IAC3B;QACJP,SAAS5B,QAAOsH,kBAAAA,OAAO1F,OAAO,YAAd0F,kBAAkB,IAAIrH,IAAI,MAAM;QAChD,oEAAoE;QACpE,uEAAuE;QACvE,yEAAyE;QACzE6B,MAAM,OAAOwF,OAAOxF,IAAI,KAAK,WAAWwF,OAAOxF,IAAI,GAAG;QACtDD,MAAM,OAAOyF,OAAOzF,IAAI,KAAK,WAAWyF,OAAOzF,IAAI,GAAG;QACtD0F,QAAQ7C,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;QAC3CX,MAAM,GACJgE,8CAAAA,kBAAkB,CAChB1G,QAAOsH,qBAAAA,OAAOE,UAAU,YAAjBF,qBAAqB,IACzBrH,IAAI,GACJkE,WAAW,GACf,YAJDuC,8CAIK;IACT;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASmC,4BACdzI,MAAc;IAEd,OAAO,OAAOgF;QACZ,MAAM9C,WAAW,MAAMpB,MACrB,GAAGuG,uBAAuB,CAAC,EAAEqB,mBAAmB1D,oBAAoB,EACpE;YAAEhE,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAAE;QAEnD,wEAAwE;QACxE,yEAAyE;QACzE,uCAAuC;QACvC,IAAIkC,SAASI,MAAM,KAAK,KAAK,OAAO;QACpC,IAAI,CAACJ,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAAST,IAAI,GAAGY,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIxB,MACR,CAAC,0BAA0B,EAAEqB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAE1E;QACA,OAAOmF,uBAAuB,MAAMrG,SAASQ,IAAI;IACnD;AACF;AAEA;;;;;;;2CAO2C,GAE3C,0DAA0D,GAC1D,OAAO,MAAMiG,wBAAwB,iBAAgB;AAErD,4EAA4E,GAC5E,OAAO,MAAMC,4BAA4B,0CAAyC;AAElF,MAAMC,OAAO,CAAClF,QACZrD,MAAMC,OAAO,CAACoD,SACVA,MAAMnD,GAAG,CAAC,CAAC0H,QAAUtI,OAAOsI,gBAAAA,QAAS,IAAIrI,IAAI,IAAIa,MAAM,CAAC,CAACwH,QAAUA,UAAU,MAC7E,OAAOvE,UAAU,YAAYA,MAAM9D,IAAI,KACrC;QAAC8D,MAAM9D,IAAI;KAAG,GACd,EAAE;AAEV,MAAM4B,OAAO,CAACkC,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;AAE9E,8EAA8E,GAC9E,SAASmF,OAAO9H,OAAgB,EAAErB,IAAY;IAC5C,IAAI,CAACqB,WAAW,OAAOA,YAAY,UAAU,OAAO;IACpD,MAAM+H,SAASpJ,KAAKoE,WAAW;IAC/B,KAAK,MAAM,CAACiF,KAAKrF,MAAM,IAAIhC,OAAO8F,OAAO,CAACzG,SAAqC;QAC7E,IAAIgI,IAAIjF,WAAW,OAAOgF,QAAQ;gBACKpF;YAArC,OAAOrD,MAAMC,OAAO,CAACoD,SAAS/D,QAAO+D,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,IAAI9D,IAAI,KAAKD,OAAO+D,gBAAAA,QAAS,IAAI9D,IAAI;QACxF;IACF;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASoJ,sBAAsBnE,KAAc;;QAGhCoC;IAFlB,MAAMA,SAASpC;IACf,IAAI,CAACoC,UAAUA,OAAOnC,IAAI,KAAK4D,uBAAuB,OAAO;IAC7D,MAAMhG,KAAK/C,gBAAOsH,eAAAA,OAAOpD,IAAI,qBAAXoD,aAAajC,QAAQ,mBAAI,IAAIpF,IAAI;IACnD,OAAO8C,MAAM;AACf;AAEA;;;;CAIC,GACD,OAAO,SAASuG,8BAA8BpE,KAAc;;IAC1D,MAAMhB,eAAQgB,yBAAD,AAACA,MAAqDhB,IAAI,mBAAI,CAAC;IAC5E,OAAO;WACF+E,KAAK/E,IAAI,CAAC,KAAK;WACf+E,KAAK/E,IAAI,CAAC,KAAK;WACf+E,KAAK/E,IAAI,CAAC,MAAM;WAChB+E,KAAK/E,IAAI,CAAC,eAAe;KAC7B;AACH;AAEA;;;CAGC,GACD,OAAO,SAASqF,6BACdhJ,GAAY,EACZiJ,QAAgB5E,KAAK6E,GAAG,EAAE;QAGRnC,YAGgBA,oBAGdA,oBAELA,cAKGA;IAdlB,MAAMA,SAAU/G,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACxD,MAAMwC,KAAK/C,QAAOsH,aAAAA,MAAM,CAAC,KAAK,YAAZA,aAAgB,IAAIrH,IAAI;IAC1C,IAAI,CAAC8C,IAAI,OAAO;IAChB,MAAM3B,UAAUkG,MAAM,CAAC,UAAU;IACjC,MAAMoC,UAAU9E,KAAKtB,KAAK,CAACtD,QAAOsH,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB;IAC1D,OAAO;QACLvE;QACA4G,WAAW3J,QAAOsH,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB,IAAIrH,IAAI,MAAMiJ,OAAO9H,SAAS;QACxEwI,WAAWV,OAAO9H,SAAS;QAC3BO,MAAM3B,QAAOsH,eAAAA,MAAM,CAAC,OAAO,YAAdA,eAAkB,IAAIrH,IAAI;QACvCO,IAAIyI,KAAK3B,MAAM,CAAC,KAAK;QACrBsB,IAAIK,KAAK3B,MAAM,CAAC,KAAK;QACrBlF,KAAK6G,KAAK3B,MAAM,CAAC,MAAM;QACvBuC,aAAaZ,KAAK3B,MAAM,CAAC,eAAe;QACxC1F,SAAS5B,QAAOsH,kBAAAA,MAAM,CAAC,UAAU,YAAjBA,kBAAqB,IAAIrH,IAAI;QAC7C4B,MAAMA,KAAKyF,MAAM,CAAC,OAAO;QACzBxF,MAAMD,KAAKyF,MAAM,CAAC,OAAO;QACzBrC,cAAcP,OAAOC,QAAQ,CAAC+E,YAAYA,UAAU,IAAIA,UAAUF;QAClEM,eAAerK,oBAAoB,CAACM,OAASmJ,OAAO9H,SAASrB;IAC/D;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASgK,0BACd3J,MAAc,EACd4J,YAA0B9I,KAAK;IAE/B,OAAO,OAAO6B;QACZ,MAAMT,WAAW,MAAM0H,UACrB,GAAGhB,0BAA0B,CAAC,EAAEF,mBAAmB/F,KAAK,EACxD;YAAE3B,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAAE;QAEnD,IAAIkC,SAASI,MAAM,KAAK,KAAK,OAAO;QACpC,IAAI,CAACJ,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAAST,IAAI,GAAGY,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIxB,MACR,CAAC,iCAAiC,EAAEqB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAEjF;QACA,OAAO+F,6BAA6B,MAAMjH,SAASQ,IAAI;IACzD;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASmH,mBAAmB/E,KAAc;IAC/C,MAAMnC,KAAKsG,sBAAsBnE;IACjC,OAAOnC,KAAK;QAAEA;QAAItC,YAAY6I,8BAA8BpE;IAAO,IAAI;AACzE;AAEA;;2CAE2C,GAE3C;;;;;CAKC,GACD,OAAO,SAASgF,mBACdpK,MAAWqK,QAAQrK,GAAG,AAAO;IAE7B,MAAMsK,UAAU,IAAMvK,QAAQC,KAAKH;IACnC,MAAM0K,UAAU;QACd,MAAMjB,MAAMvJ,QAAQC,KAAKF;QACzB,IAAI,CAACwJ,KAAK,MAAM,IAAInI,MAAM,GAAGrB,wBAAwB,WAAW,CAAC;QACjE,OAAOwJ;IACT;IACA,MAAMkB,QAA2B;QAC/BC,OAAO,IACL1K,QAAQC,KAAKF,2BACT,OACA,CAAC,IAAI,EAAEA,wBAAwB,mCAAmC,CAAC,GACnE;QACN,sEAAsE;QACtE,0DAA0D;QAC1D4K,SAAS,OAAOC,UAAY/C,4BAA4B2C,WAAWI;QACnE/I,SAAS,OAAOqB,KAAO8F,4BAA4BwB,WAAWtH;QAC9D2H,UAAU,OAAO3H,KAAOgH,0BAA0BM,WAAWtH;QAC7D4H,gBAAgB,UAAY/G,yBAAyByG;IACvD;IACA,OAAO;QACLtH,IAAIrD;QACJkL,iBAAiB,IAAOR,YAAY,EAAE,GAAG;gBAACzK;aAAwB;QAClEkL,MAAM,CAACnJ,SAASpB,UAAY+B,kBAAkB+H,WAAW1I,SAASpB;QAClEwK,kBAAkB,IAAMvH,uBAAuB6G;QAC/CW,gBAAgB/F;QAChBgG,cAAcf;QACdK;IACF;AACF"}
|
|
@@ -44,6 +44,24 @@ export interface ReceivedEmail {
|
|
|
44
44
|
html: string;
|
|
45
45
|
/** When the provider received it, epoch ms; the read's clock when it said nothing usable. */
|
|
46
46
|
receivedAtMs: number;
|
|
47
|
+
/**
|
|
48
|
+
* Whether its headers say a machine sent it — an out-of-office, a vacation
|
|
49
|
+
* responder ({@link isAutoSubmittedMail}). Absent from a reader that sees
|
|
50
|
+
* no headers, which reads as a person.
|
|
51
|
+
*/
|
|
52
|
+
autoSubmitted?: boolean;
|
|
47
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a message's headers say a machine sent it rather than a person
|
|
56
|
+
* (RFC 3834, AGL-3997): an `Auto-Submitted` other than `no`, the
|
|
57
|
+
* `X-Autoreply` or `X-Autorespond` older responders stamp instead, or
|
|
58
|
+
* `Precedence: auto_reply`. A route that files received mail skips these,
|
|
59
|
+
* so an out-of-office answering an email never becomes a request.
|
|
60
|
+
*
|
|
61
|
+
* `Precedence: list` and `bulk` are deliberately not read: a mailbox that
|
|
62
|
+
* delivers a group's mail onward stamps them on every message a person
|
|
63
|
+
* wrote. `header` answers a header's value by name, `''` when it is absent.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isAutoSubmittedMail(header: (name: string) => string): boolean;
|
|
48
66
|
/** Reads one received message by the provider's id; `null` when it has none. */
|
|
49
67
|
export type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>;
|
|
@@ -23,6 +23,27 @@
|
|
|
23
23
|
* both answer in the neutral {@link ReceivedEmail} everything downstream
|
|
24
24
|
* works with, so a route that files received mail never reads a vendor's
|
|
25
25
|
* payload.
|
|
26
|
-
*/ /** One received message, provider-neutral. Addresses are as the headers spelled them. */ /**
|
|
26
|
+
*/ /** One received message, provider-neutral. Addresses are as the headers spelled them. */ /**
|
|
27
|
+
* Whether a message's headers say a machine sent it rather than a person
|
|
28
|
+
* (RFC 3834, AGL-3997): an `Auto-Submitted` other than `no`, the
|
|
29
|
+
* `X-Autoreply` or `X-Autorespond` older responders stamp instead, or
|
|
30
|
+
* `Precedence: auto_reply`. A route that files received mail skips these,
|
|
31
|
+
* so an out-of-office answering an email never becomes a request.
|
|
32
|
+
*
|
|
33
|
+
* `Precedence: list` and `bulk` are deliberately not read: a mailbox that
|
|
34
|
+
* delivers a group's mail onward stamps them on every message a person
|
|
35
|
+
* wrote. `header` answers a header's value by name, `''` when it is absent.
|
|
36
|
+
*/ export function isAutoSubmittedMail(header) {
|
|
37
|
+
const submitted = header('auto-submitted').split(';')[0].trim().toLowerCase();
|
|
38
|
+
if (submitted && submitted !== 'no') return true;
|
|
39
|
+
for (const name of [
|
|
40
|
+
'x-autoreply',
|
|
41
|
+
'x-autorespond'
|
|
42
|
+
]){
|
|
43
|
+
const value = header(name).trim().toLowerCase();
|
|
44
|
+
if (value && value !== 'no') return true;
|
|
45
|
+
}
|
|
46
|
+
return header('precedence').trim().toLowerCase() === 'auto_reply';
|
|
47
|
+
}
|
|
27
48
|
|
|
28
49
|
//# sourceMappingURL=received-email.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/received-email.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 * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).\n *\n * A provider that receives mail announces each message with a notification\n * that carries its metadata, and the message itself is read afterwards by\n * the id the notification named. Both halves are the provider's to read —\n * its `inboundEvent` and `reads.received` (see `mail-provider.ts`) — and\n * both answer in the neutral {@link ReceivedEmail} everything downstream\n * works with, so a route that files received mail never reads a vendor's\n * payload.\n */\n\n/** One received message, provider-neutral. Addresses are as the headers spelled them. */\nexport interface ReceivedEmail {\n /** The provider's id for the message — what a redelivered event repeats. */\n id: string\n /** The `Message-ID` header, angle brackets included, or `''`. */\n messageId: string\n /** The `In-Reply-To` header, or `''`. */\n inReplyTo: string\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n /** The addresses the provider received the message FOR — a forwarded alias's real target. */\n receivedFor: string[]\n subject: string\n text: string\n html: string\n /** When the provider received it, epoch ms; the read's clock when it said nothing usable. */\n receivedAtMs: number\n}\n\n/** Reads one received message by the provider's id; `null` when it has none. */\nexport type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,uFAAuF,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/received-email.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 * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).\n *\n * A provider that receives mail announces each message with a notification\n * that carries its metadata, and the message itself is read afterwards by\n * the id the notification named. Both halves are the provider's to read —\n * its `inboundEvent` and `reads.received` (see `mail-provider.ts`) — and\n * both answer in the neutral {@link ReceivedEmail} everything downstream\n * works with, so a route that files received mail never reads a vendor's\n * payload.\n */\n\n/** One received message, provider-neutral. Addresses are as the headers spelled them. */\nexport interface ReceivedEmail {\n /** The provider's id for the message — what a redelivered event repeats. */\n id: string\n /** The `Message-ID` header, angle brackets included, or `''`. */\n messageId: string\n /** The `In-Reply-To` header, or `''`. */\n inReplyTo: string\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n /** The addresses the provider received the message FOR — a forwarded alias's real target. */\n receivedFor: string[]\n subject: string\n text: string\n html: string\n /** When the provider received it, epoch ms; the read's clock when it said nothing usable. */\n receivedAtMs: number\n /**\n * Whether its headers say a machine sent it — an out-of-office, a vacation\n * responder ({@link isAutoSubmittedMail}). Absent from a reader that sees\n * no headers, which reads as a person.\n */\n autoSubmitted?: boolean\n}\n\n/**\n * Whether a message's headers say a machine sent it rather than a person\n * (RFC 3834, AGL-3997): an `Auto-Submitted` other than `no`, the\n * `X-Autoreply` or `X-Autorespond` older responders stamp instead, or\n * `Precedence: auto_reply`. A route that files received mail skips these,\n * so an out-of-office answering an email never becomes a request.\n *\n * `Precedence: list` and `bulk` are deliberately not read: a mailbox that\n * delivers a group's mail onward stamps them on every message a person\n * wrote. `header` answers a header's value by name, `''` when it is absent.\n */\nexport function isAutoSubmittedMail(header: (name: string) => string): boolean {\n const submitted = header('auto-submitted').split(';')[0].trim().toLowerCase()\n if (submitted && submitted !== 'no') return true\n for (const name of ['x-autoreply', 'x-autorespond']) {\n const value = header(name).trim().toLowerCase()\n if (value && value !== 'no') return true\n }\n return header('precedence').trim().toLowerCase() === 'auto_reply'\n}\n\n/** Reads one received message by the provider's id; `null` when it has none. */\nexport type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>\n"],"names":["isAutoSubmittedMail","header","submitted","split","trim","toLowerCase","name","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,uFAAuF,GA2BvF;;;;;;;;;;CAUC,GACD,OAAO,SAASA,oBAAoBC,MAAgC;IAClE,MAAMC,YAAYD,OAAO,kBAAkBE,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,IAAI,GAAGC,WAAW;IAC3E,IAAIH,aAAaA,cAAc,MAAM,OAAO;IAC5C,KAAK,MAAMI,QAAQ;QAAC;QAAe;KAAgB,CAAE;QACnD,MAAMC,QAAQN,OAAOK,MAAMF,IAAI,GAAGC,WAAW;QAC7C,IAAIE,SAASA,UAAU,MAAM,OAAO;IACtC;IACA,OAAON,OAAO,cAAcG,IAAI,GAAGC,WAAW,OAAO;AACvD"}
|
|
@@ -22,5 +22,4 @@ export declare const SITE_CARE_APPROVAL_EMAIL_KEY = "site-care-approval-confirma
|
|
|
22
22
|
export declare const SITE_CARE_CHARGE_REVERSED_EMAIL_KEY = "site-care-charge-reversed";
|
|
23
23
|
export declare const SITE_CARE_STATEMENT_EMAIL_KEY = "site-care-statement";
|
|
24
24
|
export declare const SITE_CARE_QUOTE_EMAIL_KEY = "site-care-quote";
|
|
25
|
-
export declare const SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY = "site-care-unknown-sender";
|
|
26
25
|
export declare const SITE_CARE_SYSTEM_EMAIL_TEMPLATES: readonly SystemEmailTemplateDefinition[];
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* statement on demand. Each is its own key so staff can redesign one
|
|
24
24
|
* without the rest. The merge tokens are what
|
|
25
25
|
* `@aglyn/tenant-data-admin/server/site-care-emails` fills.
|
|
26
|
+
*
|
|
27
|
+
* None answers a sender the intake does not recognize. Site Care mail
|
|
28
|
+
* arrives at the support address (AGL-3997), so such a message is ordinary
|
|
29
|
+
* support mail and the support inbox answers it.
|
|
26
30
|
*=========================================*/ export const SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY = 'site-care-request-received';
|
|
27
31
|
export const SITE_CARE_REQUEST_DONE_EMAIL_KEY = 'site-care-request-done';
|
|
28
32
|
export const SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY = 'site-care-request-corrected';
|
|
@@ -30,7 +34,6 @@ export const SITE_CARE_APPROVAL_EMAIL_KEY = 'site-care-approval-confirmation';
|
|
|
30
34
|
export const SITE_CARE_CHARGE_REVERSED_EMAIL_KEY = 'site-care-charge-reversed';
|
|
31
35
|
export const SITE_CARE_STATEMENT_EMAIL_KEY = 'site-care-statement';
|
|
32
36
|
export const SITE_CARE_QUOTE_EMAIL_KEY = 'site-care-quote';
|
|
33
|
-
export const SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY = 'site-care-unknown-sender';
|
|
34
37
|
const SOURCE = 'libs/tenant/data/admin/src/lib/server/site-care-emails.ts';
|
|
35
38
|
const ORG_NAME = {
|
|
36
39
|
name: 'org.name',
|
|
@@ -427,34 +430,6 @@ export const SITE_CARE_SYSTEM_EMAIL_TEMPLATES = [
|
|
|
427
430
|
],
|
|
428
431
|
footerReason: FOOTER,
|
|
429
432
|
source: SOURCE
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
key: SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY,
|
|
433
|
-
name: 'Site Care: sender not recognized',
|
|
434
|
-
description: 'Sent to someone who emailed the Site Care address from an address no Site Care workspace has as a member.',
|
|
435
|
-
deliveredBy: 'resend',
|
|
436
|
-
defaultSubject: 'We could not match your email to an account',
|
|
437
|
-
mergeTokens: [
|
|
438
|
-
{
|
|
439
|
-
name: 'sender.email',
|
|
440
|
-
description: 'The address the message came from',
|
|
441
|
-
sample: 'someone@example.com'
|
|
442
|
-
}
|
|
443
|
-
],
|
|
444
|
-
defaultBody: [
|
|
445
|
-
{
|
|
446
|
-
block: 'text',
|
|
447
|
-
text: 'We could not match your email to an account',
|
|
448
|
-
variant: 'heading'
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
block: 'text',
|
|
452
|
-
text: 'We got your message from {{sender.email}}, but no workspace with Site Care has that address as a member. Please send it again from the email address you sign in to {{brand.productName}} with.',
|
|
453
|
-
variant: 'body'
|
|
454
|
-
}
|
|
455
|
-
],
|
|
456
|
-
footerReason: 'You’re receiving this because you emailed {{brand.productName}} Site Care.',
|
|
457
|
-
source: SOURCE
|
|
458
433
|
}
|
|
459
434
|
];
|
|
460
435
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/site-care-emails.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 * SITE CARE, AS SYSTEM EMAILS (AGL-3959).\n *\n * A Site Care owner hears about their requests by email, because they do not\n * use the console: that it arrived, that it is done (with what changed and\n * the time it took), that a bigger job needs a quote, that they approved\n * extra time, that a charge was corrected or reversed, and the month's\n * statement on demand. Each is its own key so staff can redesign one\n * without the rest. The merge tokens are what\n * `@aglyn/tenant-data-admin/server/site-care-emails` fills.\n *=========================================*/\n\nimport type {\n SystemEmailMergeToken,\n SystemEmailTemplateDefinition,\n} from './system-email-catalog'\n\nexport const SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY = 'site-care-request-received'\nexport const SITE_CARE_REQUEST_DONE_EMAIL_KEY = 'site-care-request-done'\nexport const SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY = 'site-care-request-corrected'\nexport const SITE_CARE_APPROVAL_EMAIL_KEY = 'site-care-approval-confirmation'\nexport const SITE_CARE_CHARGE_REVERSED_EMAIL_KEY = 'site-care-charge-reversed'\nexport const SITE_CARE_STATEMENT_EMAIL_KEY = 'site-care-statement'\nexport const SITE_CARE_QUOTE_EMAIL_KEY = 'site-care-quote'\nexport const SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY = 'site-care-unknown-sender'\n\nconst SOURCE = 'libs/tenant/data/admin/src/lib/server/site-care-emails.ts'\n\nconst ORG_NAME: SystemEmailMergeToken = {\n name: 'org.name',\n description: 'The workspace the request is for',\n sample: 'Test Org',\n}\nconst REQUEST_TITLE: SystemEmailMergeToken = {\n name: 'request.title',\n description: 'The request, as the owner titled it',\n sample: 'Update our holiday hours',\n}\n\nconst FOOTER =\n 'You’re receiving this because {{org.name}} has Site Care on {{brand.productName}}.'\n\nexport const SITE_CARE_SYSTEM_EMAIL_TEMPLATES: readonly SystemEmailTemplateDefinition[] = [\n {\n key: SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY,\n name: 'Site Care: request received',\n description:\n 'Sent to the person who asked when a Site Care request arrives, by email or from the console.',\n deliveredBy: 'resend',\n defaultSubject: 'We have your request: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'request.message',\n description: 'What they asked for',\n sample: 'Please change Saturday to 9 to 2 for December.',\n },\n {\n name: 'request.due',\n description: 'When it will be done at the latest',\n sample: 'Wednesday, October 21',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'We have your request', variant: 'heading' },\n {\n block: 'text',\n text: 'Thanks. We received “{{request.title}}” and will have it done by {{request.due}}. Reply to this email to add anything, and it joins the same request.',\n variant: 'body',\n },\n { block: 'text', text: 'Request: {{request.title}}\\nDone by: {{request.due}}', variant: 'details' },\n { block: 'text', text: '{{request.message}}', variant: 'body' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_REQUEST_DONE_EMAIL_KEY,\n name: 'Site Care: request done',\n description:\n 'Sent to the workspace owner when staff close a Site Care request: what changed, the time it took, the time left this month, or the extra time billed.',\n deliveredBy: 'resend',\n defaultSubject: 'Done: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'request.changes',\n description: 'What changed, from the request and the publish that shipped it',\n sample: 'Published “Home” on Oct 19, 2026, 3:04 PM CDT',\n },\n { name: 'request.minutes', description: 'The time it took', sample: '35 min' },\n {\n name: 'month.minutesLeft',\n description: 'Included time left this month',\n sample: '25 min of this month’s included hour',\n },\n {\n name: 'request.billing',\n description: 'Whether anything bills, in one sentence',\n sample: 'No extra charge: this fit in your included hour.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'Done: {{request.title}}', variant: 'heading' },\n { block: 'text', text: 'We finished your request. {{request.billing}}', variant: 'body' },\n {\n block: 'text',\n text: 'What changed: {{request.changes}}\\nTime: {{request.minutes}}\\nLeft this month: {{month.minutesLeft}}',\n variant: 'details',\n },\n {\n block: 'text',\n text: 'Something not right? Reply to this email and we will fix it.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY,\n name: 'Site Care: time corrected',\n description:\n 'Sent to the workspace owner when staff correct the minutes on a finished request, with the before and after.',\n deliveredBy: 'resend',\n defaultSubject: 'Corrected: the time on {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n { name: 'correction.before', description: 'The time before', sample: '75 min' },\n { name: 'correction.after', description: 'The time now', sample: '45 min' },\n {\n name: 'correction.reason',\n description: 'Why it changed',\n sample: 'The timer ran while we waited for your photos.',\n },\n {\n name: 'request.billing',\n description: 'What bills now, in one sentence',\n sample: 'No extra charge: this now fits in your included hour.',\n },\n {\n name: 'month.minutesLeft',\n description: 'Included time left this month',\n sample: '15 min of this month’s included hour',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'We corrected the time on “{{request.title}}”', variant: 'heading' },\n { block: 'text', text: '{{correction.reason}} {{request.billing}}', variant: 'body' },\n {\n block: 'text',\n text: 'Before: {{correction.before}}\\nNow: {{correction.after}}\\nLeft this month: {{month.minutesLeft}}',\n variant: 'details',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_APPROVAL_EMAIL_KEY,\n name: 'Site Care: extra time approved',\n description:\n 'Sent to the workspace owner the moment staff record their yes to extra time, and again, marked corrected, when the record is corrected. It is the written record of the approval.',\n deliveredBy: 'resend',\n defaultSubject: '{{approval.heading}}: extra time for {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'approval.heading',\n description: '“You approved extra time”, or “Corrected” on a correction',\n sample: 'You approved extra time',\n },\n {\n name: 'approval.sentence',\n description: 'What was approved, how and when',\n sample:\n 'You approved up to 2 hours of extra time at $79 an hour for “Update our holiday hours”, by phone on Oct 19, 2026, 2:10 PM CDT.',\n },\n {\n name: 'approval.correction',\n description: 'What was corrected and why, on a correction; blank otherwise',\n sample: 'Corrected from 1 hour to 2 hours: the owner asked for the gallery too.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: '{{approval.heading}}', variant: 'heading' },\n { block: 'text', text: '{{approval.sentence}} {{approval.correction}}', variant: 'body' },\n {\n block: 'text',\n text: 'Extra time is billed in whole hours, rounded up, on your next invoice, and never more than you approved without asking you again. If this is not what you agreed to, reply to this email before we start.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_CHARGE_REVERSED_EMAIL_KEY,\n name: 'Site Care: charge removed, credited or refunded',\n description:\n 'Sent to the workspace owner when staff void, credit or refund an extra-time charge from the request.',\n deliveredBy: 'resend',\n defaultSubject: '{{charge.heading}}: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'charge.heading',\n description: '“Charge removed”, “Charge credited” or “Charge refunded”',\n sample: 'Charge removed',\n },\n {\n name: 'charge.sentence',\n description: 'What happened to the money',\n sample: 'The $79 for 1 hour of extra time will not be on your next invoice.',\n },\n {\n name: 'charge.reason',\n description: 'Why',\n sample: 'We took longer than we should have on this one.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: '{{charge.heading}}', variant: 'heading' },\n { block: 'text', text: '{{charge.sentence}}', variant: 'body' },\n { block: 'text', text: 'Request: {{request.title}}\\nWhy: {{charge.reason}}', variant: 'details' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_STATEMENT_EMAIL_KEY,\n name: 'Site Care: monthly statement',\n description:\n 'Sent to the workspace owner when staff press Send statement: every request of the month, the minutes, the extra time billed and every correction.',\n deliveredBy: 'resend',\n defaultSubject: 'Your Site Care statement for {{month.label}}',\n mergeTokens: [\n ORG_NAME,\n { name: 'month.label', description: 'The month', sample: 'October 2026' },\n {\n name: 'statement.summary',\n description: 'The month in one sentence',\n sample: 'October 2026: 3 requests, 1 h 35 min of work, 60 of 60 included minutes used, 1 hour of extra time billed.',\n },\n {\n name: 'statement.lines',\n description: 'One line per request',\n sample:\n 'Update our holiday hours: 35 min, included\\nNew staff photo: 1 h, 1 hour billed ($79), on your next invoice',\n },\n {\n name: 'statement.history',\n description: 'Everything recorded this month, one dated line each',\n sample: 'Oct 19, 2026, 3:04 PM CDT: Finished “Update our holiday hours”: 35 min.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'Your statement for {{month.label}}', variant: 'heading' },\n { block: 'text', text: '{{statement.summary}}', variant: 'body' },\n { block: 'text', text: '{{statement.lines}}', variant: 'details' },\n { block: 'text', text: 'Everything we recorded', variant: 'subheading' },\n { block: 'text', text: '{{statement.history}}', variant: 'body' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_QUOTE_EMAIL_KEY,\n name: 'Site Care: bigger than Site Care covers',\n description:\n 'Sent from a Site Care request by staff when the work adds something new (a page, a form, a design, a campaign): Site Care covers changes to what exists, so this is a flat quote.',\n deliveredBy: 'resend',\n defaultSubject: 'A quote for {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n { name: 'quote.amount', description: 'The flat price', sample: '$249' },\n {\n name: 'quote.scope',\n description: 'What the price covers',\n sample: 'A new landing page for the holiday sale, with a sign-up form.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'This one is bigger than Site Care covers', variant: 'heading' },\n {\n block: 'text',\n text: 'Site Care covers changes to what your site already has. “{{request.title}}” adds something new, so it is a project with one flat price, fixed once given.',\n variant: 'body',\n },\n { block: 'text', text: 'Price: {{quote.amount}}\\nWhat it covers: {{quote.scope}}', variant: 'details' },\n {\n block: 'text',\n text: 'Reply to say yes and we will send the invoice to pay from your phone. Nothing is charged until you do.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY,\n name: 'Site Care: sender not recognized',\n description:\n 'Sent to someone who emailed the Site Care address from an address no Site Care workspace has as a member.',\n deliveredBy: 'resend',\n defaultSubject: 'We could not match your email to an account',\n mergeTokens: [\n {\n name: 'sender.email',\n description: 'The address the message came from',\n sample: 'someone@example.com',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'We could not match your email to an account', variant: 'heading' },\n {\n block: 'text',\n text: 'We got your message from {{sender.email}}, but no workspace with Site Care has that address as a member. Please send it again from the email address you sign in to {{brand.productName}} with.',\n variant: 'body',\n },\n ],\n footerReason: 'You’re receiving this because you emailed {{brand.productName}} Site Care.',\n source: SOURCE,\n },\n]\n"],"names":["SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY","SITE_CARE_REQUEST_DONE_EMAIL_KEY","SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY","SITE_CARE_APPROVAL_EMAIL_KEY","SITE_CARE_CHARGE_REVERSED_EMAIL_KEY","SITE_CARE_STATEMENT_EMAIL_KEY","SITE_CARE_QUOTE_EMAIL_KEY","SITE_CARE_UNKNOWN_SENDER_EMAIL_KEY","SOURCE","ORG_NAME","name","description","sample","REQUEST_TITLE","FOOTER","SITE_CARE_SYSTEM_EMAIL_TEMPLATES","key","deliveredBy","defaultSubject","mergeTokens","defaultBody","block","text","variant","footerReason","source"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;2CAU2C,GAO3C,OAAO,MAAMA,uCAAuC,6BAA4B;AAChF,OAAO,MAAMC,mCAAmC,yBAAwB;AACxE,OAAO,MAAMC,wCAAwC,8BAA6B;AAClF,OAAO,MAAMC,+BAA+B,kCAAiC;AAC7E,OAAO,MAAMC,sCAAsC,4BAA2B;AAC9E,OAAO,MAAMC,gCAAgC,sBAAqB;AAClE,OAAO,MAAMC,4BAA4B,kBAAiB;AAC1D,OAAO,MAAMC,qCAAqC,2BAA0B;AAE5E,MAAMC,SAAS;AAEf,MAAMC,WAAkC;IACtCC,MAAM;IACNC,aAAa;IACbC,QAAQ;AACV;AACA,MAAMC,gBAAuC;IAC3CH,MAAM;IACNC,aAAa;IACbC,QAAQ;AACV;AAEA,MAAME,SACJ;AAEF,OAAO,MAAMC,mCAA6E;IACxF;QACEC,KAAKhB;QACLU,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBACEH,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAwBC,SAAS;YAAU;YAClE;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;YACA;gBAAEF,OAAO;gBAAQC,MAAM;gBAAwDC,SAAS;YAAU;YAClG;gBAAEF,OAAO;gBAAQC,MAAM;gBAAuBC,SAAS;YAAO;SAC/D;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKf;QACLS,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBACEH,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBAAEF,MAAM;gBAAmBC,aAAa;gBAAoBC,QAAQ;YAAS;YAC7E;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAA2BC,SAAS;YAAU;YACrE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAiDC,SAAS;YAAO;YACxF;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;YACA;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;SACD;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKd;QACLQ,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBAAEH,MAAM;gBAAqBC,aAAa;gBAAmBC,QAAQ;YAAS;YAC9E;gBAAEF,MAAM;gBAAoBC,aAAa;gBAAgBC,QAAQ;YAAS;YAC1E;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAgDC,SAAS;YAAU;YAC1F;gBAAEF,OAAO;gBAAQC,MAAM;gBAA6CC,SAAS;YAAO;YACpF;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;SACD;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKb;QACLO,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBACEH,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QACE;YACJ;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAwBC,SAAS;YAAU;YAClE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAiDC,SAAS;YAAO;YACxF;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;SACD;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKZ;QACLM,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBACEH,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAsBC,SAAS;YAAU;YAChE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAuBC,SAAS;YAAO;YAC9D;gBAAEF,OAAO;gBAAQC,MAAM;gBAAsDC,SAAS;YAAU;SACjG;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKX;QACLK,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACA;gBAAEC,MAAM;gBAAeC,aAAa;gBAAaC,QAAQ;YAAe;YACxE;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QACE;YACJ;YACA;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAsCC,SAAS;YAAU;YAChF;gBAAEF,OAAO;gBAAQC,MAAM;gBAAyBC,SAAS;YAAO;YAChE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAuBC,SAAS;YAAU;YACjE;gBAAEF,OAAO;gBAAQC,MAAM;gBAA0BC,SAAS;YAAa;YACvE;gBAAEF,OAAO;gBAAQC,MAAM;gBAAyBC,SAAS;YAAO;SACjE;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKV;QACLI,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACXV;YACAI;YACA;gBAAEH,MAAM;gBAAgBC,aAAa;gBAAkBC,QAAQ;YAAO;YACtE;gBACEF,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA4BC,SAAS;YAAU;YACtE;gBAAEF,OAAO;gBAAQC,MAAM;gBAA4CC,SAAS;YAAU;YACtF;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;YACA;gBAAEF,OAAO;gBAAQC,MAAM;gBAA4DC,SAAS;YAAU;YACtG;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;SACD;QACDC,cAAcV;QACdW,QAAQjB;IACV;IACA;QACEQ,KAAKT;QACLG,MAAM;QACNC,aACE;QACFM,aAAa;QACbC,gBAAgB;QAChBC,aAAa;YACX;gBACET,MAAM;gBACNC,aAAa;gBACbC,QAAQ;YACV;SACD;QACDQ,aAAa;YACX;gBAAEC,OAAO;gBAAQC,MAAM;gBAA+CC,SAAS;YAAU;YACzF;gBACEF,OAAO;gBACPC,MAAM;gBACNC,SAAS;YACX;SACD;QACDC,cAAc;QACdC,QAAQjB;IACV;CACD,CAAA"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/site-care-emails.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 * SITE CARE, AS SYSTEM EMAILS (AGL-3959).\n *\n * A Site Care owner hears about their requests by email, because they do not\n * use the console: that it arrived, that it is done (with what changed and\n * the time it took), that a bigger job needs a quote, that they approved\n * extra time, that a charge was corrected or reversed, and the month's\n * statement on demand. Each is its own key so staff can redesign one\n * without the rest. The merge tokens are what\n * `@aglyn/tenant-data-admin/server/site-care-emails` fills.\n *\n * None answers a sender the intake does not recognize. Site Care mail\n * arrives at the support address (AGL-3997), so such a message is ordinary\n * support mail and the support inbox answers it.\n *=========================================*/\n\nimport type {\n SystemEmailMergeToken,\n SystemEmailTemplateDefinition,\n} from './system-email-catalog'\n\nexport const SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY = 'site-care-request-received'\nexport const SITE_CARE_REQUEST_DONE_EMAIL_KEY = 'site-care-request-done'\nexport const SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY = 'site-care-request-corrected'\nexport const SITE_CARE_APPROVAL_EMAIL_KEY = 'site-care-approval-confirmation'\nexport const SITE_CARE_CHARGE_REVERSED_EMAIL_KEY = 'site-care-charge-reversed'\nexport const SITE_CARE_STATEMENT_EMAIL_KEY = 'site-care-statement'\nexport const SITE_CARE_QUOTE_EMAIL_KEY = 'site-care-quote'\n\nconst SOURCE = 'libs/tenant/data/admin/src/lib/server/site-care-emails.ts'\n\nconst ORG_NAME: SystemEmailMergeToken = {\n name: 'org.name',\n description: 'The workspace the request is for',\n sample: 'Test Org',\n}\nconst REQUEST_TITLE: SystemEmailMergeToken = {\n name: 'request.title',\n description: 'The request, as the owner titled it',\n sample: 'Update our holiday hours',\n}\n\nconst FOOTER =\n 'You’re receiving this because {{org.name}} has Site Care on {{brand.productName}}.'\n\nexport const SITE_CARE_SYSTEM_EMAIL_TEMPLATES: readonly SystemEmailTemplateDefinition[] = [\n {\n key: SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY,\n name: 'Site Care: request received',\n description:\n 'Sent to the person who asked when a Site Care request arrives, by email or from the console.',\n deliveredBy: 'resend',\n defaultSubject: 'We have your request: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'request.message',\n description: 'What they asked for',\n sample: 'Please change Saturday to 9 to 2 for December.',\n },\n {\n name: 'request.due',\n description: 'When it will be done at the latest',\n sample: 'Wednesday, October 21',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'We have your request', variant: 'heading' },\n {\n block: 'text',\n text: 'Thanks. We received “{{request.title}}” and will have it done by {{request.due}}. Reply to this email to add anything, and it joins the same request.',\n variant: 'body',\n },\n { block: 'text', text: 'Request: {{request.title}}\\nDone by: {{request.due}}', variant: 'details' },\n { block: 'text', text: '{{request.message}}', variant: 'body' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_REQUEST_DONE_EMAIL_KEY,\n name: 'Site Care: request done',\n description:\n 'Sent to the workspace owner when staff close a Site Care request: what changed, the time it took, the time left this month, or the extra time billed.',\n deliveredBy: 'resend',\n defaultSubject: 'Done: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'request.changes',\n description: 'What changed, from the request and the publish that shipped it',\n sample: 'Published “Home” on Oct 19, 2026, 3:04 PM CDT',\n },\n { name: 'request.minutes', description: 'The time it took', sample: '35 min' },\n {\n name: 'month.minutesLeft',\n description: 'Included time left this month',\n sample: '25 min of this month’s included hour',\n },\n {\n name: 'request.billing',\n description: 'Whether anything bills, in one sentence',\n sample: 'No extra charge: this fit in your included hour.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'Done: {{request.title}}', variant: 'heading' },\n { block: 'text', text: 'We finished your request. {{request.billing}}', variant: 'body' },\n {\n block: 'text',\n text: 'What changed: {{request.changes}}\\nTime: {{request.minutes}}\\nLeft this month: {{month.minutesLeft}}',\n variant: 'details',\n },\n {\n block: 'text',\n text: 'Something not right? Reply to this email and we will fix it.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY,\n name: 'Site Care: time corrected',\n description:\n 'Sent to the workspace owner when staff correct the minutes on a finished request, with the before and after.',\n deliveredBy: 'resend',\n defaultSubject: 'Corrected: the time on {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n { name: 'correction.before', description: 'The time before', sample: '75 min' },\n { name: 'correction.after', description: 'The time now', sample: '45 min' },\n {\n name: 'correction.reason',\n description: 'Why it changed',\n sample: 'The timer ran while we waited for your photos.',\n },\n {\n name: 'request.billing',\n description: 'What bills now, in one sentence',\n sample: 'No extra charge: this now fits in your included hour.',\n },\n {\n name: 'month.minutesLeft',\n description: 'Included time left this month',\n sample: '15 min of this month’s included hour',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'We corrected the time on “{{request.title}}”', variant: 'heading' },\n { block: 'text', text: '{{correction.reason}} {{request.billing}}', variant: 'body' },\n {\n block: 'text',\n text: 'Before: {{correction.before}}\\nNow: {{correction.after}}\\nLeft this month: {{month.minutesLeft}}',\n variant: 'details',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_APPROVAL_EMAIL_KEY,\n name: 'Site Care: extra time approved',\n description:\n 'Sent to the workspace owner the moment staff record their yes to extra time, and again, marked corrected, when the record is corrected. It is the written record of the approval.',\n deliveredBy: 'resend',\n defaultSubject: '{{approval.heading}}: extra time for {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'approval.heading',\n description: '“You approved extra time”, or “Corrected” on a correction',\n sample: 'You approved extra time',\n },\n {\n name: 'approval.sentence',\n description: 'What was approved, how and when',\n sample:\n 'You approved up to 2 hours of extra time at $79 an hour for “Update our holiday hours”, by phone on Oct 19, 2026, 2:10 PM CDT.',\n },\n {\n name: 'approval.correction',\n description: 'What was corrected and why, on a correction; blank otherwise',\n sample: 'Corrected from 1 hour to 2 hours: the owner asked for the gallery too.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: '{{approval.heading}}', variant: 'heading' },\n { block: 'text', text: '{{approval.sentence}} {{approval.correction}}', variant: 'body' },\n {\n block: 'text',\n text: 'Extra time is billed in whole hours, rounded up, on your next invoice, and never more than you approved without asking you again. If this is not what you agreed to, reply to this email before we start.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_CHARGE_REVERSED_EMAIL_KEY,\n name: 'Site Care: charge removed, credited or refunded',\n description:\n 'Sent to the workspace owner when staff void, credit or refund an extra-time charge from the request.',\n deliveredBy: 'resend',\n defaultSubject: '{{charge.heading}}: {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n {\n name: 'charge.heading',\n description: '“Charge removed”, “Charge credited” or “Charge refunded”',\n sample: 'Charge removed',\n },\n {\n name: 'charge.sentence',\n description: 'What happened to the money',\n sample: 'The $79 for 1 hour of extra time will not be on your next invoice.',\n },\n {\n name: 'charge.reason',\n description: 'Why',\n sample: 'We took longer than we should have on this one.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: '{{charge.heading}}', variant: 'heading' },\n { block: 'text', text: '{{charge.sentence}}', variant: 'body' },\n { block: 'text', text: 'Request: {{request.title}}\\nWhy: {{charge.reason}}', variant: 'details' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_STATEMENT_EMAIL_KEY,\n name: 'Site Care: monthly statement',\n description:\n 'Sent to the workspace owner when staff press Send statement: every request of the month, the minutes, the extra time billed and every correction.',\n deliveredBy: 'resend',\n defaultSubject: 'Your Site Care statement for {{month.label}}',\n mergeTokens: [\n ORG_NAME,\n { name: 'month.label', description: 'The month', sample: 'October 2026' },\n {\n name: 'statement.summary',\n description: 'The month in one sentence',\n sample: 'October 2026: 3 requests, 1 h 35 min of work, 60 of 60 included minutes used, 1 hour of extra time billed.',\n },\n {\n name: 'statement.lines',\n description: 'One line per request',\n sample:\n 'Update our holiday hours: 35 min, included\\nNew staff photo: 1 h, 1 hour billed ($79), on your next invoice',\n },\n {\n name: 'statement.history',\n description: 'Everything recorded this month, one dated line each',\n sample: 'Oct 19, 2026, 3:04 PM CDT: Finished “Update our holiday hours”: 35 min.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'Your statement for {{month.label}}', variant: 'heading' },\n { block: 'text', text: '{{statement.summary}}', variant: 'body' },\n { block: 'text', text: '{{statement.lines}}', variant: 'details' },\n { block: 'text', text: 'Everything we recorded', variant: 'subheading' },\n { block: 'text', text: '{{statement.history}}', variant: 'body' },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n {\n key: SITE_CARE_QUOTE_EMAIL_KEY,\n name: 'Site Care: bigger than Site Care covers',\n description:\n 'Sent from a Site Care request by staff when the work adds something new (a page, a form, a design, a campaign): Site Care covers changes to what exists, so this is a flat quote.',\n deliveredBy: 'resend',\n defaultSubject: 'A quote for {{request.title}}',\n mergeTokens: [\n ORG_NAME,\n REQUEST_TITLE,\n { name: 'quote.amount', description: 'The flat price', sample: '$249' },\n {\n name: 'quote.scope',\n description: 'What the price covers',\n sample: 'A new landing page for the holiday sale, with a sign-up form.',\n },\n ],\n defaultBody: [\n { block: 'text', text: 'Site Care · {{org.name}}', variant: 'eyebrow' },\n { block: 'text', text: 'This one is bigger than Site Care covers', variant: 'heading' },\n {\n block: 'text',\n text: 'Site Care covers changes to what your site already has. “{{request.title}}” adds something new, so it is a project with one flat price, fixed once given.',\n variant: 'body',\n },\n { block: 'text', text: 'Price: {{quote.amount}}\\nWhat it covers: {{quote.scope}}', variant: 'details' },\n {\n block: 'text',\n text: 'Reply to say yes and we will send the invoice to pay from your phone. Nothing is charged until you do.',\n variant: 'caption',\n },\n ],\n footerReason: FOOTER,\n source: SOURCE,\n },\n]\n"],"names":["SITE_CARE_REQUEST_RECEIVED_EMAIL_KEY","SITE_CARE_REQUEST_DONE_EMAIL_KEY","SITE_CARE_REQUEST_CORRECTED_EMAIL_KEY","SITE_CARE_APPROVAL_EMAIL_KEY","SITE_CARE_CHARGE_REVERSED_EMAIL_KEY","SITE_CARE_STATEMENT_EMAIL_KEY","SITE_CARE_QUOTE_EMAIL_KEY","SOURCE","ORG_NAME","name","description","sample","REQUEST_TITLE","FOOTER","SITE_CARE_SYSTEM_EMAIL_TEMPLATES","key","deliveredBy","defaultSubject","mergeTokens","defaultBody","block","text","variant","footerReason","source"],"mappings":"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"}
|
|
@@ -1813,6 +1813,64 @@ const BASE_SYSTEM_EMAIL_TEMPLATES = [
|
|
|
1813
1813
|
footerReason: 'You’re receiving this because a member of {{org.name}} on ' + '{{brand.productName}} asked to add this address to their account.',
|
|
1814
1814
|
source: 'libs/tenant/data/admin/src/lib/server/member-email-aliases.ts'
|
|
1815
1815
|
},
|
|
1816
|
+
{
|
|
1817
|
+
key: 'domain-ownership-code',
|
|
1818
|
+
name: 'Website ownership code',
|
|
1819
|
+
description: 'Sent to an address at a website’s own domain when a workspace ' + 'proves it owns that website before bringing it into the product. ' + 'Carries a six-digit code that works for 15 minutes.',
|
|
1820
|
+
deliveredBy: 'resend',
|
|
1821
|
+
defaultSubject: 'Your code to verify {{domain}}',
|
|
1822
|
+
mergeTokens: [
|
|
1823
|
+
{
|
|
1824
|
+
name: 'org.name',
|
|
1825
|
+
description: 'The workspace that asked',
|
|
1826
|
+
sample: 'Test Org'
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: 'domain',
|
|
1830
|
+
description: 'The website’s domain',
|
|
1831
|
+
sample: 'example.com'
|
|
1832
|
+
},
|
|
1833
|
+
{
|
|
1834
|
+
name: 'code',
|
|
1835
|
+
description: 'The one-time code',
|
|
1836
|
+
sample: '123456'
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: 'expiresInMinutes',
|
|
1840
|
+
description: 'How many minutes the code works',
|
|
1841
|
+
sample: '15'
|
|
1842
|
+
}
|
|
1843
|
+
],
|
|
1844
|
+
defaultBody: [
|
|
1845
|
+
{
|
|
1846
|
+
block: 'text',
|
|
1847
|
+
text: '{{domain}}',
|
|
1848
|
+
variant: 'eyebrow'
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
block: 'text',
|
|
1852
|
+
text: 'Your verification code',
|
|
1853
|
+
variant: 'heading'
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
block: 'text',
|
|
1857
|
+
text: '{{org.name}} asked to confirm that {{domain}} belongs to them, so they can bring the site into {{brand.productName}}. Type this code where you asked for it.',
|
|
1858
|
+
variant: 'body'
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
block: 'text',
|
|
1862
|
+
text: 'Code: {{code}}\nWebsite: {{domain}}\nWorks for: {{expiresInMinutes}} minutes',
|
|
1863
|
+
variant: 'details'
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
block: 'text',
|
|
1867
|
+
text: 'If you did not ask for this, ignore this email. Nothing is copied from your site without the code.',
|
|
1868
|
+
variant: 'caption'
|
|
1869
|
+
}
|
|
1870
|
+
],
|
|
1871
|
+
footerReason: 'You’re receiving this because someone in {{org.name}} on ' + '{{brand.productName}} asked to verify a website at this address’s domain.',
|
|
1872
|
+
source: 'libs/tenant/data/admin/src/lib/server/domain-ownership.ts'
|
|
1873
|
+
},
|
|
1816
1874
|
{
|
|
1817
1875
|
key: 'crm-daily-digest',
|
|
1818
1876
|
name: 'Daily CRM digest',
|