mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* fix(den-api): surface invitation email send failures instead of swallowing Loops failures in sendDenOrganizationInvitationEmail and sendDenVerificationEmail were being caught and logged at warn level, so the HTTP handlers still returned 201 'Invitation created' even when no email ever left the process. Ben observed this with two live invitations: the DB row was pending, the UI showed it, but one of two recipients never received the email and clicking resend re-ran the same silent-failure path. Root cause (from an explore audit): - email.ts:129,132 swallowed non-2xx Loops responses and fetch throws. - invitations.ts awaited the send and unconditionally returned 2xx. - There is no 'skip email if user already exists' branch anywhere; the Slack hypothesis was wrong. The failure mode is provider-side and was invisible because of the swallow. Changes: - Introduce DenEmailSendError with a stable reason tagged union (loops_not_configured | loops_rejected | loops_network). - sendDenOrganizationInvitationEmail and sendDenVerificationEmail now throw DenEmailSendError on failure. Dev-mode short-circuit is preserved (still logs the payload and returns cleanly). - POST /v1/orgs/:orgId/invitations catches DenEmailSendError, logs via console.error with a stable [auth][invite_email_failed] prefix (greppable across deployments), and returns 502 invitation_email_failed with a human-readable message and the invitationId so the UI can correlate and offer a retry. The row is left pending intentionally so the next submit becomes a real resend. - Document the 502 response in the OpenAPI describeRoute. Operator note: if LOOPS_TRANSACTIONAL_ID_DEN_VERIFY_EMAIL is unset the signup OTP endpoint will now return a real error instead of silently stranding the user at the OTP screen forever. This is intentional; the previous behavior was a latent signup-breaking bug. * fix(den-api): tolerate missing apps/desktop/package.json in Docker build PR #1476 introduced a build step that reads apps/desktop/package.json to bake in a default latest-app-version, but packaging/docker/Dockerfile.den does not ship the Tauri desktop sources. As a result, the den-dev Docker stack fails to build after the PR landed. Gracefully fall back to 0.0.0 (matching the runtime default) when the file is absent, and allow a DEN_API_LATEST_APP_VERSION env override so deployers can still pin a real value. * test(den-api): add smoke script for invite email failure paths scripts/smoke-email-failures.mjs exercises the DenEmailSendError paths against the built dist/ of den-api. Ships with instructions so a reviewer can rerun it inside the docker-compose den-dev container with a single command. Also parameterises OPENWORK_DEV_MODE in the den compose service so the failure paths can be reached from outside the container when needed (defaults to 1; override with OPENWORK_DEV_MODE=0 at compose time).
57 lines
1.7 KiB
JavaScript
57 lines
1.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Standalone smoke test for the invitation-email failure paths.
|
|
*
|
|
* Run inside the den-api container (or any environment where the package has
|
|
* been built to `dist/`):
|
|
*
|
|
* docker exec -e OPENWORK_DEV_MODE=0 \
|
|
* openwork-den-dev-<id>-den-1 \
|
|
* node ee/apps/den-api/scripts/smoke-email-failures.mjs
|
|
*
|
|
* Expected output:
|
|
* [smoke] ok loops_not_configured { reason: 'loops_not_configured', ... }
|
|
*
|
|
* Add `-e LOOPS_API_KEY=bogus -e LOOPS_TRANSACTIONAL_ID_DEN_ORG_INVITE_EMAIL=bogus`
|
|
* to also reach the `loops_rejected` path (Loops returns 401).
|
|
*
|
|
* Intentionally side-effect free: no DB writes, no auth.
|
|
*/
|
|
|
|
const { sendDenOrganizationInvitationEmail, DenEmailSendError } = await import(
|
|
"../dist/email.js"
|
|
)
|
|
|
|
const recipient = process.argv[2] ?? "smoke-test@example.com"
|
|
|
|
try {
|
|
await sendDenOrganizationInvitationEmail({
|
|
email: recipient,
|
|
inviteLink: "https://example.com/join?invite=smoke",
|
|
invitedByName: "Smoke Test",
|
|
invitedByEmail: "smoke@example.com",
|
|
organizationName: "Smoke Org",
|
|
role: "member",
|
|
})
|
|
|
|
if (process.env.OPENWORK_DEV_MODE === "1" || !process.env.OPENWORK_DEV_MODE) {
|
|
console.log("[smoke] ok dev_mode_noop (no email sent, no throw — expected)")
|
|
process.exit(0)
|
|
}
|
|
|
|
console.error("[smoke] FAIL: expected throw when Loops is not configured or rejects")
|
|
process.exit(1)
|
|
} catch (error) {
|
|
if (!(error instanceof DenEmailSendError)) {
|
|
console.error("[smoke] FAIL: wrong error class:", error)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`[smoke] ok ${error.reason}`, {
|
|
reason: error.reason,
|
|
template: error.template,
|
|
recipient: error.recipient,
|
|
detail: error.detail,
|
|
})
|
|
}
|