Skip to content

fix(email): structured error handling, delivery_failed status, and type fixes - #364

Merged
important-new merged 7 commits into
InspectorHub:mainfrom
important-new:main
Oct 2, 2026
Merged

important-new merged 7 commits into
InspectorHub:mainfrom
important-new:main

Conversation

@important-new

@important-new important-new commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Classify email-provider failures and surface actionable errors instead of silently swallowing platform refusals.

Before: every .catch() on sendEmail discarded the error — cooling windows, suppressed addresses, quota exhaustion, and unverified sender domains all returned 200 OK with no feedback.

After: each failure kind produces a distinct outcome the caller can act on.

What changed

Provider layer (resend.ts / provider.ts)

  • New EmailFailureKind union: suppressed | quota_exceeded | sender_domain | transient
  • classifyResendError() maps all Resend API error codes to one of the four kinds
  • sendEmail return type carries kind so callers can branch on why delivery failed

Base service (base.ts / suppression.ts)

  • suppressed → writes to email_suppressions, throws EMAIL_RECIPIENT_SUPPRESSED (422)
  • quota_exceeded → throws 402
  • sender_domain → throws 422
  • transient → throws EMAIL_DELIVERY_FAILED (502), caller may retry

Call sites

  • agreement-send.ts — rethrow AppError; write delivery_failed to the signer row for transient per-recipient failures
  • admin-esign.ts reminder — same pattern
  • auth.ts password-reset — rethrow AppError so the user sees a 4xx, not silence
  • report-delivery.ts batch — AppError aborts the whole batch instead of being counted as a per-recipient skip
  • admin-data.ts invite — distinguish AppError (warn + code) from transient (error)

DB / UI

  • Add delivery_failed to agreement_requests and agreement_signers status enums
  • SignerList: red chip with tooltip; Remind button stays active (non-terminal)
  • i18n keys for chip label/tooltip in en and es-419

Housekeeping

  • Replace stray console.warn/error in server code with structured logger
  • Patch high-severity advisories: brace-expansion >= 5.0.12, undici >= 7.30.0, ip-address SSRF

Test plan

  • npm run type-check passes
  • npm run lint passes (including advisory gate — 0 high)
  • npm run test:unit passes — 8 new classifyResendError unit tests added
  • Manual smoke-test in npm run dev

Related

  • Fixes Dependabot alert #63

important-new and others added 7 commits October 2, 2026 15:29
Three layers of fixes for outbound email reliability:

Layer 1 - Provider (resend.ts / provider.ts)
- Add EmailFailureKind union: suppressed | quota_exceeded | sender_domain | transient
- classifyResendError() maps all Resend error codes to one of the four kinds
- sendEmail return type carries kind so callers can branch on why delivery failed

Layer 2 - Base service (base.ts / suppression.ts / errors.ts)
- New error codes: EMAIL_RECIPIENT_SUPPRESSED (422), EMAIL_SENDER_DOMAIN (422),
  EMAIL_DELIVERY_FAILED (502) with actionable user-facing messages
- performSend branches on kind: suppressed writes to email_suppressions then
  throws 422; quota_exceeded throws 402; sender_domain throws 422; transient 502
- suppression.ts adds recordSuppression() so a provider-rejected address is
  written locally without waiting for the webhook

Layer 3 - Call sites
- agreement-send.ts: replace blanket .catch() with try/catch that rethrows
  AppError (platform refusals surface to UI) and writes delivery_failed status
  to the signer row for transient per-recipient failures
- admin-esign.ts reminder: same pattern - rethrow AppError, log transient
- auth.ts password-reset: rethrow AppError so user sees 4xx, not silence
- report-delivery.ts batch: AppError aborts the whole batch instead of being
  counted as a per-recipient skip
- admin-data.ts invite: distinguish AppError (warn + code) from transient (error)

DB schema
- Add delivery_failed to agreement_requests and agreement_signers status enums

UI / i18n
- SignerList: add delivery_failed chip (red, defect tone) with tooltip
- messages/en + es-419/checkout.json: two new keys for the chip label/tooltip
- delivery_failed signers keep Remind button active (non-terminal)

Housekeeping
- Replace all console.warn/error/info in server code with structured logger
  (agreements-render.ts, sign-completion-workflow.ts)
- Add 8 new classifyResendError unit tests covering all error kind branches
- postmark/sendgrid/mailgun: add EmailFailureKind to import, update
  sendEmail return type signature, add kind:'transient' to all ok:false
  returns in sendEmail (validateCredentials returns unchanged)
- base.ts: replace inline suppression type with EmailSuppressionPort
  so recordSuppression() is visible to TypeScript
- auth.ts: fix logger.warn() call — was passing 3 args, signature takes 2;
  changed to logger.warn(msg, { error: e.message })
- workers/app.ts: add explicit Request type to paraglideMiddleware callback
  destructuring to fix TS7031 implicit any
- agent/dashboard.tsx: add 'as Referral[]' to rows:[] to fix never[] inference
- agent/repair-items.tsx: add 'as InspectionBlock[]' to blocks:[] same fix

All non-paraglide type errors cleared (tsconfig.api.json and tsconfig.json
both exit 0).
…tsconfig

paraglide-js v2.7+ emits .d.ts files alongside .js when --emit-ts-declarations
is set (fixes issue #566). The outdir's own .gitignore excludes everything with
'*', so the files are never committed but are present on disk after i18n:compile.

tsc's app/**/*.ts glob doesn't pick up .d.ts files without a corresponding .js
and allowJs:true. Adding an explicit 'app/paraglide/**/*.d.ts' include gives
TypeScript the message function signatures without touching allowJs or the
paraglide .gitignore.

Result: all TS7016 'implicitly has any type' errors on ~/paraglide/messages
imports are resolved. tsconfig.json type-check now exits 0 with zero errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@important-new

Copy link
Copy Markdown
Contributor Author

Local UI verification completed against PR head cfe07c2. The inspection detail page rendered the new delivery_failed signer state as a red “Not delivered” chip with its explanatory tooltip; Remind and Copy link remained available. I used a temporary local D1 status change and restored the fixture to viewed afterward; no reminder email was sent. SignerList web tests passed (9/9). All PR CI checks are passing.

@important-new
important-new merged commit b84a42e into InspectorHub:main Oct 2, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant