scripts/lib/smoke-transactional-core.ts
Rama main · réplica de lectura del repositorio original.
/**
* The pure half of `pnpm smoke:transactional`: argument parsing, the request
* bodies it sends, and the assertions it applies to each response.
*
* No `node:*` imports and no I/O, so every judgement the smoke test makes can
* be unit-tested in the Workers Vitest pool (tests/unit/smoke-transactional.test.ts).
* The side-effecting runner is scripts/smoke-transactional.ts.
*
* Why this exists: the unit/integration suites call the mailbox Durable Object
* directly, so four bugs on the real operator + integrator path (a 500 that lost
* the plaintext key, a From display name never sent, wildcard recipient policies
* that never matched, a UI 401) shipped with every test green. This smoke test
* drives a DEPLOYED environment through the same HTTP surface a person uses.
*
* The plaintext API key is a live credential: nothing in this module ever puts
* it in evidence, and `redactSecrets` exists for any response body that might.
*/
import { normalizeHost, originFor, redact } from "./operator-session-core";
export class SmokeInputError extends Error {
constructor(message: string) {
super(message);
this.name = "SmokeInputError";
}
}
export const DEFAULT_SENDER_NAME = "Reccado smoke";
/** An address the key's recipient policy cannot match; `.invalid` is reserved (RFC 2606). */
export const POLICY_REJECT_ADDRESS = "smoke-reject@example.invalid";
/** The throwaway key's lifetime and send quota: small enough that a leaked key is nearly inert. */
export const KEY_TTL_MS = 60 * 60 * 1000;
export const KEY_QUOTA_MAX = 5;
export const MAX_WAIT_DELIVERY_SECONDS = 3600;
export const KEY_SCOPES = [
"transactional:send",
"transactional:templates:use",
"transactional:status",
] as const;
export type SmokeOptions = {
env?: string;
host?: string;
mailboxId: string;
sender: string;
to: string;
senderName: string;
send: boolean;
waitDeliverySeconds: number;
help: boolean;
};
export const SMOKE_USAGE = `Usage: pnpm smoke:transactional --mailbox <mailboxId> --sender <addr> --to <recipient>
[--env dev] [--host <host>] [--sender-name "Reccado smoke"]
[--send] [--wait-delivery <seconds>]
Without --send: checks the operator session and that the mailbox answers, prints the plan, sends nothing.
With --send: creates a throwaway template + live key (1h, quota ${KEY_QUOTA_MAX}, policy = --to),
sends ONE real message to --to, replays it, proves policy rejection, reads status,
then always revokes the key and archives the template.
--wait-delivery N poll the status up to N seconds (max ${MAX_WAIT_DELIVERY_SECONDS}) for a delivery event.
Skipped with a WARN when --to is a verified Email Routing destination address on
the account (those sends never produce an event). The check reads the account's
destination addresses with CLOUDFLARE_API_TOKEN (Account · Email Routing
Addresses · Read) and CLOUDFLARE_ACCOUNT_ID or \`wrangler whoami\`.
Sign in first: pnpm operator login --env <env> --host <host>`;
const BOOLEAN_FLAGS = new Set(["send", "help"]);
const VALUE_FLAGS = new Set([
"env",
"host",
"mailbox",
"sender",
"to",
"sender-name",
"wait-delivery",
]);
const EMAIL_RE = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
const MAILBOX_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
function assertEmail(flag: string, value: string): string {
const trimmed = value.trim();
if (!EMAIL_RE.test(trimmed)) {
throw new SmokeInputError(`--${flag} must be an email address, got "${value}".`);
}
return trimmed;
}
/**
* Parses argv. Unknown flags are refused rather than ignored: a typo in
* `--send` must not silently turn a plan-only run into a real one or vice versa.
*/
export function parseSmokeArgs(argv: readonly string[]): SmokeOptions {
const flags: Record<string, string> = {};
const bools = new Set<string>();
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg) continue;
if (!arg.startsWith("--")) {
throw new SmokeInputError(`Unexpected argument "${arg}".`);
}
const eq = arg.indexOf("=");
const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
if (BOOLEAN_FLAGS.has(key)) {
if (eq !== -1) throw new SmokeInputError(`--${key} takes no value.`);
bools.add(key);
continue;
}
if (!VALUE_FLAGS.has(key)) {
throw new SmokeInputError(`Unknown flag --${key}.`);
}
let value: string | undefined;
if (eq !== -1) {
value = arg.slice(eq + 1);
} else {
value = argv[i + 1];
if (value === undefined || value.startsWith("--")) {
throw new SmokeInputError(`--${key} needs a value.`);
}
i += 1;
}
flags[key] = value;
}
const help = bools.has("help");
if (help) {
return {
mailboxId: "",
sender: "",
to: "",
senderName: DEFAULT_SENDER_NAME,
send: false,
waitDeliverySeconds: 0,
help: true,
};
}
const missing = ["mailbox", "sender", "to"].filter((k) => !flags[k]?.trim());
if (missing.length > 0) {
throw new SmokeInputError(`Missing required ${missing.map((k) => `--${k}`).join(", ")}.`);
}
const mailboxId = (flags.mailbox ?? "").trim();
if (!MAILBOX_ID_RE.test(mailboxId)) {
throw new SmokeInputError(`--mailbox must be a mailbox id like mbx_..., got "${mailboxId}".`);
}
const sender = assertEmail("sender", flags.sender ?? "");
const to = assertEmail("to", flags.to ?? "");
if (to.toLowerCase() === POLICY_REJECT_ADDRESS) {
throw new SmokeInputError(`--to cannot be ${POLICY_REJECT_ADDRESS}; that is the policy probe.`);
}
const senderName = (flags["sender-name"] ?? DEFAULT_SENDER_NAME).trim();
if (!senderName) throw new SmokeInputError("--sender-name cannot be empty.");
let waitDeliverySeconds = 0;
if (flags["wait-delivery"] !== undefined) {
const raw = flags["wait-delivery"].trim();
const n = Number(raw);
if (!/^\d+$/.test(raw) || !Number.isInteger(n) || n > MAX_WAIT_DELIVERY_SECONDS) {
throw new SmokeInputError(
`--wait-delivery must be an integer 0-${MAX_WAIT_DELIVERY_SECONDS}, got "${raw}".`,
);
}
waitDeliverySeconds = n;
}
return {
env: flags.env?.trim() || undefined,
host: flags.host?.trim() ? normalizeHost(flags.host) : undefined,
mailboxId,
sender,
to,
senderName,
send: bools.has("send"),
waitDeliverySeconds,
help: false,
};
}
// --- The run's identifiers and request bodies ---
export type SmokeRunIds = {
/** Template id; matches the send path's `^[a-zA-Z_][a-zA-Z0-9_.-]*$`. */
templateId: string;
/** Unique token placed in the subject (literal) and the body (via `{{token}}`). */
token: string;
/** Idempotency-Key for the real send and its replay. */
idempotencyKey: string;
/** A different Idempotency-Key for the policy probe. */
rejectIdempotencyKey: string;
};
/** `randomHex` must be at least 8 hex chars of fresh randomness (the runner passes 16). */
export function makeRunIds(nowMs: number, randomHex: string): SmokeRunIds {
if (!/^[0-9a-f]{8,}$/.test(randomHex)) {
throw new SmokeInputError("makeRunIds needs at least 8 lowercase hex characters.");
}
const token = `${nowMs.toString(36)}${randomHex.slice(0, 8)}`;
return {
templateId: `smoke-${nowMs}`,
token,
idempotencyKey: `smoke-${token}-send`,
rejectIdempotencyKey: `smoke-${token}-reject`,
};
}
export function buildTemplateBody(ids: SmokeRunIds) {
return {
id: ids.templateId,
subject: `Reccado smoke test ${ids.token}`,
body_text:
"This is an automated Reccado transactional smoke test.\n\n" +
"Token: {{token}}\n\nNo action is needed.",
body_html:
"<p>This is an automated Reccado transactional smoke test.</p>" +
"<p>Token: <code>{{token}}</code></p><p>No action is needed.</p>",
};
}
export function buildKeyBody(input: {
sender: string;
to: string;
templateId: string;
nowMs: number;
}) {
return {
environment: "live" as const,
sender: input.sender,
scopes: [...KEY_SCOPES],
templateAllowlist: [input.templateId],
// Exactly the one recipient: the key cannot mail anyone else even if it leaks.
recipientPolicy: input.to,
quotaMax: KEY_QUOTA_MAX,
expiresAt: new Date(input.nowMs + KEY_TTL_MS).toISOString(),
};
}
export function buildSendPayload(ids: SmokeRunIds, to: string) {
return { template: ids.templateId, to, variables: { token: ids.token } };
}
/** URL + init for an integrator call to the public `/v1` surface, Bearer-authenticated. */
export function buildIntegratorRequest(input: {
host: string;
mailboxId: string;
apiKey: string;
method: "POST" | "GET";
requestId?: string;
idempotencyKey?: string;
body?: unknown;
}): { url: string; init: RequestInit } {
const base = `${originFor(input.host)}/v1/mailboxes/${encodeURIComponent(input.mailboxId)}/transactional/messages`;
const url = input.requestId ? `${base}/${encodeURIComponent(input.requestId)}` : base;
const headers: Record<string, string> = {
authorization: `Bearer ${input.apiKey}`,
accept: "application/json",
};
if (input.method === "POST") {
if (!input.idempotencyKey) {
throw new SmokeInputError("A transactional send needs an Idempotency-Key.");
}
headers["content-type"] = "application/json";
headers["idempotency-key"] = input.idempotencyKey;
}
return {
url,
init: {
method: input.method,
headers,
redirect: "manual",
...(input.body !== undefined ? { body: JSON.stringify(input.body) } : {}),
},
};
}
export function mailboxApiPath(mailboxId: string, suffix: string): string {
return `/api/mailboxes/${encodeURIComponent(mailboxId)}/transactional${suffix}`;
}
// --- Responses and step results ---
export type HttpResult = { status: number; body: unknown; text: string };
/** Parses a response body; a non-JSON body yields `body: undefined` and keeps `text`. */
export function toHttpResult(status: number, text: string): HttpResult {
let body: unknown;
try {
body = text ? JSON.parse(text) : null;
} catch {
body = undefined;
}
return { status, body, text };
}
export type StepOutcome = "PASS" | "FAIL" | "WARN" | "INFO" | "SKIP";
export type StepResult = {
name: string;
outcome: StepOutcome;
evidence: string[];
};
export function step(name: string, outcome: StepOutcome, evidence: string[] = []): StepResult {
return { name, outcome, evidence };
}
export function formatStep(result: StepResult): string {
const lines = [`[${result.outcome}] ${result.name}`];
for (const line of result.evidence) lines.push(` ${line}`);
return lines.join("\n");
}
/** Replaces each secret (the plaintext key, the cookie) with `[redacted]`. */
export function redactSecrets(text: string, secrets: readonly (string | undefined)[]): string {
return redact(text, secrets);
}
/** A short, secret-free rendering of an unexpected response for evidence. */
export function describeResponse(
result: HttpResult,
secrets: readonly (string | undefined)[] = [],
): string {
const text = redactSecrets(result.text, secrets).replace(/\s+/g, " ").trim();
return `HTTP ${result.status} ${text.length > 300 ? `${text.slice(0, 300)}...` : text}`;
}
function record(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function str(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
export type KeyView = {
keyId: string;
status?: string;
senderName: string | null;
sender?: string;
environment?: string;
recipientPolicy?: string | null;
quotaUsed?: number;
quotaMax?: number | null;
displaySuffix?: string;
expiresAt?: string | null;
};
function toKeyView(value: unknown): KeyView | undefined {
const r = record(value);
const keyId = str(r?.keyId);
if (!r || !keyId) return undefined;
return {
keyId,
status: str(r.status),
senderName: typeof r.senderName === "string" ? r.senderName : null,
sender: str(r.sender),
environment: str(r.environment),
recipientPolicy: typeof r.recipientPolicy === "string" ? r.recipientPolicy : null,
quotaUsed: typeof r.quotaUsed === "number" ? r.quotaUsed : undefined,
quotaMax: typeof r.quotaMax === "number" ? r.quotaMax : null,
displaySuffix: str(r.displaySuffix),
expiresAt: typeof r.expiresAt === "string" ? r.expiresAt : null,
};
}
/**
* The key out of a key-admin response, whichever envelope it came in:
* create answers `{ key, plaintextKey, projection }`; PATCH and revoke answer
* `{ key: { key, projection } }`.
*/
export function extractKey(body: unknown): KeyView | undefined {
const outer = record(body);
const key = record(outer?.key);
return (
toKeyView(key?.key) ??
toKeyView(key?.projection) ??
toKeyView(key) ??
toKeyView(outer?.projection)
);
}
export function keyEvidence(key: KeyView): string {
return [
`keyId=${key.keyId}`,
key.displaySuffix ? `suffix=...${key.displaySuffix}` : undefined,
key.environment ? `env=${key.environment}` : undefined,
key.status ? `status=${key.status}` : undefined,
key.sender ? `sender=${key.sender}` : undefined,
key.recipientPolicy ? `policy=${key.recipientPolicy}` : undefined,
key.quotaMax != null ? `quotaMax=${key.quotaMax}` : undefined,
key.expiresAt ? `expires=${key.expiresAt}` : undefined,
]
.filter(Boolean)
.join(" ");
}
/** Step 2a — the regression for the lost-key bug: 201 AND a plaintext key in the body. */
export function assessKeyCreate(result: HttpResult): {
step: StepResult;
keyId?: string;
plaintextKey?: string;
} {
const outer = record(result.body);
const plaintextKey = str(outer?.plaintextKey);
const key = extractKey(result.body);
const secrets = [plaintextKey];
const name = "create live key";
if (result.status !== 201) {
return {
step: step(name, "FAIL", [`expected 201, got ${describeResponse(result, secrets)}`]),
keyId: key?.keyId,
plaintextKey,
};
}
const evidence = [
"HTTP 201",
plaintextKey
? `plaintextKey: present (${plaintextKey.length} chars, not shown)`
: "plaintextKey: MISSING from the 201 body (the key is unusable and unrecoverable)",
];
if (key) evidence.push(keyEvidence(key));
else evidence.push("no key id in the response");
const ok =
Boolean(plaintextKey) &&
Boolean(key) &&
key?.environment !== "test" &&
key?.status !== "revoked";
return { step: step(name, ok ? "PASS" : "FAIL", evidence), keyId: key?.keyId, plaintextKey };
}
/** The keys array out of `GET .../api-keys` (`{ keys: [...] }`). */
export function extractKeyList(body: unknown): KeyView[] | undefined {
const keys = record(body)?.keys;
if (!Array.isArray(keys)) return undefined;
return keys.map(toKeyView).filter((k): k is KeyView => Boolean(k));
}
export function findKey(body: unknown, keyId: string): KeyView | undefined {
return extractKeyList(body)?.find((k) => k.keyId === keyId);
}
/** Step 2b — the new key is visible to the list endpoint. */
export function assessKeyListed(result: HttpResult, keyId: string): StepResult {
const name = "key appears in GET api-keys";
if (result.status !== 200) return step(name, "FAIL", [describeResponse(result)]);
const list = extractKeyList(result.body);
if (!list) return step(name, "FAIL", [`no keys array: ${describeResponse(result)}`]);
const key = list.find((k) => k.keyId === keyId);
if (!key) return step(name, "FAIL", [`${keyId} not among ${list.length} key(s)`]);
return step(name, key.status === "active" ? "PASS" : "FAIL", [keyEvidence(key)]);
}
/** Step 3 — PATCH senderName answered 2xx and the key (as re-read) carries it. */
export function assessSenderName(
patch: HttpResult,
listed: HttpResult | undefined,
keyId: string,
expected: string,
): StepResult {
const name = "set sender name";
if (patch.status < 200 || patch.status >= 300) {
return step(name, "FAIL", [`PATCH: ${describeResponse(patch)}`]);
}
const fromPatch = extractKey(patch.body)?.senderName ?? null;
const evidence = [`PATCH HTTP ${patch.status}, senderName=${JSON.stringify(fromPatch)}`];
let fromList: string | null | undefined;
if (listed) {
fromList =
listed.status === 200 ? (findKey(listed.body, keyId)?.senderName ?? null) : undefined;
evidence.push(
listed.status === 200
? `GET api-keys senderName=${JSON.stringify(fromList)}`
: `GET api-keys: ${describeResponse(listed)}`,
);
}
const ok = fromPatch === expected && (listed === undefined || fromList === expected);
if (!ok) evidence.push(`expected ${JSON.stringify(expected)}`);
return step(name, ok ? "PASS" : "FAIL", evidence);
}
export type SendView = {
status?: string;
requestId?: string;
keyId?: string;
providerMessageId?: string | null;
error?: string;
};
export function extractSend(body: unknown): SendView {
const r = record(body);
return {
status: str(r?.status),
requestId: str(r?.requestId),
keyId: str(r?.keyId),
providerMessageId: typeof r?.providerMessageId === "string" ? r.providerMessageId : null,
error: str(r?.error),
};
}
function sendEvidence(result: HttpResult, view: SendView): string {
return [
`HTTP ${result.status}`,
`status=${view.status ?? "?"}`,
view.requestId ? `requestId=${view.requestId}` : undefined,
view.providerMessageId ? `providerMessageId=${view.providerMessageId}` : undefined,
view.error ? `error=${view.error}` : undefined,
]
.filter(Boolean)
.join(" ");
}
/** Step 4 — a real send: 200, `sent`, a request id and a provider message id. */
export function assessSend(
result: HttpResult,
secrets: readonly (string | undefined)[] = [],
): {
step: StepResult;
send: SendView;
} {
const view = extractSend(result.body);
const name = "send (integrator, Bearer key)";
const ok =
result.status === 200 &&
view.status === "sent" &&
Boolean(view.requestId) &&
Boolean(view.providerMessageId);
const evidence = [sendEvidence(result, view)];
if (!ok && result.body === undefined) evidence.push(describeResponse(result, secrets));
if (!ok && view.status === "unknown") {
evidence.push(
"outcome unknown: do NOT resend with a new Idempotency-Key; check the recipient mailbox",
);
}
return { step: step(name, ok ? "PASS" : "FAIL", evidence), send: view };
}
/**
* Step 5 — a replay with the same Idempotency-Key and payload. The DO answers
* it from the stored request row without reaching the provider, so it must
* return the ORIGINAL requestId and providerMessageId with status `sent` (or
* `duplicate`). A request that reached the provider again would carry a fresh
* requestId (`crypto.randomUUID()` on the new-request path) and a new provider
* message id, so matching ids are the proof that nothing was sent twice.
*
* Not used as proof: `quotaUsed` from GET api-keys. `listApiKeys` reports a
* constant 0 rather than the live usage counter, so it cannot show a double charge.
*/
export function assessReplay(original: SendView, replay: HttpResult): StepResult {
const view = extractSend(replay.body);
const evidence = [sendEvidence(replay, view)];
const sameRequest = Boolean(original.requestId) && view.requestId === original.requestId;
const sameProvider =
Boolean(original.providerMessageId) && view.providerMessageId === original.providerMessageId;
evidence.push(
sameRequest && sameProvider
? "same requestId and providerMessageId as the original: answered from the idempotency row, not re-sent"
: `expected requestId=${original.requestId} providerMessageId=${original.providerMessageId}`,
);
const ok =
replay.status === 200 &&
(view.status === "sent" || view.status === "duplicate") &&
sameRequest &&
sameProvider;
return step("idempotent replay", ok ? "PASS" : "FAIL", evidence);
}
/** Step 6 — an off-policy recipient is refused before anything is reserved or sent. */
export function assessPolicyRejection(result: HttpResult): StepResult {
const view = extractSend(result.body);
const ok =
result.status === 403 &&
view.status === "rejected" &&
view.error === "not_allowed_by_policy" &&
!view.requestId &&
!view.providerMessageId;
const evidence = [sendEvidence(result, view)];
if (!ok) evidence.push('expected HTTP 403 {"status":"rejected","error":"not_allowed_by_policy"}');
return step(`recipient policy rejects ${POLICY_REJECT_ADDRESS}`, ok ? "PASS" : "FAIL", evidence);
}
export type StatusView = {
status?: string;
providerMessageId?: string | null;
deliveryStatus: string | null;
deliveryEventAt: string | null;
resolvedVia: string | null;
feedbackState: string;
feedbackReason: string | null;
};
export function extractStatus(body: unknown): StatusView {
const r = record(body);
const feedback = record(r?.deliveryFeedback);
return {
status: str(r?.status),
providerMessageId: typeof r?.providerMessageId === "string" ? r.providerMessageId : null,
deliveryStatus: typeof r?.deliveryStatus === "string" ? r.deliveryStatus : null,
deliveryEventAt: typeof r?.deliveryEventAt === "string" ? r.deliveryEventAt : null,
resolvedVia: typeof r?.resolvedVia === "string" ? r.resolvedVia : null,
feedbackState: str(feedback?.state) ?? "absent",
feedbackReason: typeof feedback?.reason === "string" ? feedback.reason : null,
};
}
/** Delivery statuses after which no further event is expected for the message. */
const TERMINAL_DELIVERY = new Set(["delivered", "bounced", "complained", "rejected", "failed"]);
const BAD_DELIVERY = new Set(["bounced", "complained", "rejected", "failed"]);
export function isTerminalDelivery(view: StatusView): boolean {
return view.deliveryStatus !== null && TERMINAL_DELIVERY.has(view.deliveryStatus);
}
export function feedbackEvidence(view: StatusView): string {
return `deliveryFeedback=${view.feedbackState}${view.feedbackReason ? ` (${view.feedbackReason})` : ""}`;
}
/** Step 7a — the status endpoint knows the request and agrees it was sent. */
export function assessStatus(result: HttpResult, send: SendView): StepResult {
const name = "status (GET /v1/.../messages/:requestId)";
if (result.status !== 200) return step(name, "FAIL", [describeResponse(result)]);
const view = extractStatus(result.body);
const evidence = [
`status=${view.status ?? "?"} providerMessageId=${view.providerMessageId ?? "null"}`,
`deliveryStatus=${view.deliveryStatus ?? "null"} deliveryEventAt=${view.deliveryEventAt ?? "null"}${view.resolvedVia ? ` resolvedVia=${view.resolvedVia}` : ""}`,
feedbackEvidence(view),
];
const ok = view.status === "sent" && view.providerMessageId === (send.providerMessageId ?? null);
return step(name, ok ? "PASS" : "FAIL", evidence);
}
// --- Email Routing destination addresses ---
export type RoutingDestination = { email: string; verified: boolean };
/**
* The account's Email Routing destination addresses out of
* `GET /accounts/{account_id}/email/routing/addresses` (`result`). An address
* counts as verified when its `verified` timestamp is set; one still awaiting its
* confirmation click is not a destination yet.
*/
export function parseRoutingDestinations(result: unknown): RoutingDestination[] {
if (!Array.isArray(result)) return [];
const destinations: RoutingDestination[] = [];
for (const entry of result) {
const r = record(entry);
const email = str(r?.email);
if (!email) continue;
destinations.push({
email: email.trim().toLowerCase(),
verified: typeof r?.verified === "string" && r.verified.length > 0,
});
}
return destinations;
}
export type RoutingLookup =
| { ok: true; destinations: RoutingDestination[] }
| { ok: false; reason: string };
/**
* Decides whether waiting for a delivery event can mean anything for `--to`.
*
* When the recipient is a verified Email Routing destination address on the
* account, Cloudflare's `send_email` binding delivers through Email Routing, and
* Email Routing produces no Email Sending lifecycle event at all: account
* analytics showed every such send only as an Email Routing `newEmail`, never in
* Email Sending. Waiting would then time out and read as a missing event for a
* message that was delivered. So the wait is skipped with a WARN, not a FAIL.
*
* A failed lookup does not block the wait: it only means the guard could not run.
*/
export function assessRoutingDestination(
to: string,
lookup: RoutingLookup,
): { skipWait: boolean; step: StepResult } {
const name = "recipient vs Email Routing destination addresses";
if (!lookup.ok) {
return {
skipWait: false,
step: step(name, "INFO", [
`could not read the account's Email Routing destination addresses: ${lookup.reason}`,
"waiting anyway; if --to is a verified destination address, no delivery event will arrive",
]),
};
}
const recipient = to.trim().toLowerCase();
const match = lookup.destinations.find((d) => d.email === recipient && d.verified);
if (!match) {
return {
skipWait: false,
step: step(name, "INFO", [
`${to} is not a verified destination address (${lookup.destinations.length} on the account); delivery events are expected`,
]),
};
}
return {
skipWait: true,
step: step(name, "WARN", [
`${to} is a verified Email Routing destination address on this account`,
"send_email delivers to it through Email Routing, which never produces an Email Sending lifecycle event",
"skipping the delivery wait: no event will arrive, and deliveryFeedback may read unobserved for this send without anything being wrong",
"to exercise delivery events, send to an address that is not a destination address on this account",
]),
};
}
/**
* Step 7b — the delivery verdict after waiting. Absence of an event is only a
* failure when the domain's feedback channel is demonstrably live; otherwise
* silence says nothing about the message (AGENTS.md, src/lib/feedback-liveness.ts).
*/
export function assessDelivery(view: StatusView, waitedSeconds: number): StepResult {
const name = "delivery event";
if (view.deliveryStatus !== null) {
const evidence = [
`deliveryStatus=${view.deliveryStatus} at ${view.deliveryEventAt ?? "?"}`,
feedbackEvidence(view),
];
if (view.deliveryStatus === "delivered") return step(name, "PASS", ["delivered", ...evidence]);
if (BAD_DELIVERY.has(view.deliveryStatus)) return step(name, "FAIL", evidence);
return step(name, "WARN", [`non-terminal event after ${waitedSeconds}s`, ...evidence]);
}
const evidence = [
`no event within ${waitedSeconds} s (check the domain's feedback liveness: ${feedbackEvidence(view)})`,
];
if (view.feedbackState === "live") {
evidence.push("the feedback channel is live, so a missing event is a real signal");
return step(name, "FAIL", evidence);
}
evidence.push("feedback liveness is not live, so silence is not evidence about this message");
return step(name, "WARN", evidence);
}
/** Step 8 — revoke answered 2xx and the key now reads revoked. */
export function assessRevoke(result: HttpResult): StepResult {
const key = extractKey(result.body);
const ok = result.status === 200 && key?.status === "revoked";
return step("cleanup: revoke key", ok ? "PASS" : "FAIL", [
ok && key ? keyEvidence(key) : describeResponse(result),
]);
}
export function assessArchive(result: HttpResult, templateId: string): StepResult {
const ok = result.status === 200 && record(result.body)?.ok === true;
return step("cleanup: archive template", ok ? "PASS" : "FAIL", [
ok ? `template ${templateId} archived` : describeResponse(result),
]);
}
/**
* Commands to finish the cleanup by hand. They read the cookie from the
* operator's own session file at run time, so it is never printed.
*/
export function manualCleanupCommands(input: {
host: string;
env?: string;
mailboxId: string;
keyId?: string;
templateId?: string;
sessionPath: string;
}): string[] {
const origin = originFor(input.host);
const auth = `-H "origin: ${origin}" -H "cookie: $(jq -r .cookie '${input.sessionPath}')"`;
const lines = [
`# if the session expired first: pnpm operator login${input.env ? ` --env ${input.env}` : ""} --host ${input.host}`,
];
if (input.keyId) {
lines.push(
`curl -sS -X POST ${auth} ${origin}${mailboxApiPath(input.mailboxId, `/api-keys/${input.keyId}/revoke`)}`,
);
}
if (input.templateId) {
lines.push(
`curl -sS -X POST ${auth} ${origin}${mailboxApiPath(input.mailboxId, `/templates/${input.templateId}/archive`)}`,
);
}
return lines;
}
export function hasFailure(results: readonly StepResult[]): boolean {
return results.some((r) => r.outcome === "FAIL");
}
export function summaryLine(results: readonly StepResult[]): string {
const count = (o: StepOutcome) => results.filter((r) => r.outcome === o).length;
return `RESULT: ${hasFailure(results) ? "FAIL" : "PASS"} (${count("PASS")} pass, ${count("FAIL")} fail, ${count("WARN")} warn, ${count("SKIP")} skipped)`;
}
/** The plan printed before anything happens (and the whole output of a plan-only run). */
export function planLines(opts: SmokeOptions, host: string, ids: SmokeRunIds): string[] {
return [
`Transactional smoke test against ${originFor(host)} (mailbox ${opts.mailboxId})`,
` mode: ${opts.send ? "SEND — one real message will be sent" : "plan only (no --send): read-only checks, nothing is created or sent"}`,
` sender: ${opts.senderName} <${opts.sender}>`,
` recipient: ${opts.to}`,
" steps:",
` 1. create template ${ids.templateId} (subject token ${ids.token}, {{token}} in bodies)`,
` 2. create LIVE key: scopes ${KEY_SCOPES.join(",")}, allowlist [${ids.templateId}], policy "${opts.to}", quota ${KEY_QUOTA_MAX}, expires in 1h; assert 201 + plaintextKey, listed`,
` 3. PATCH senderName "${opts.senderName}"; assert the key reports it`,
` 4. send to ${opts.to} (Idempotency-Key ${ids.idempotencyKey}); assert sent + providerMessageId`,
" 5. replay with the same Idempotency-Key; assert same requestId + providerMessageId (no second send)",
` 6. send to ${POLICY_REJECT_ADDRESS} (same key, new Idempotency-Key); assert rejected not_allowed_by_policy`,
` 7. read status + deliveryFeedback${opts.waitDeliverySeconds > 0 ? `; wait up to ${opts.waitDeliverySeconds}s for a delivery event` : ""}`,
" 8. always: revoke the key, archive the template",
];
}
export function manualCheckLine(
opts: SmokeOptions,
ids: SmokeRunIds,
providerMessageId: string | null | undefined,
): string {
return (
`Manual check: open the message in ${opts.to}'s mailbox (subject "${buildTemplateBody(ids).subject}"` +
`${providerMessageId ? `, provider id ${providerMessageId}` : ""}) and confirm From reads ` +
`"${opts.senderName} <${opts.sender}>" and the headers show SPF, DKIM and DMARC pass ` +
"(the display name is not observable through the API)."
);
}