feat: provision a mailbox straight from the request (close the loop) - #253
Conversation
Add mailboxRequest.provisionFromRequest — admin-only mutation that stands up the hosted mailbox through the shared provisioning path and marks the request fulfilled, notifying the requester in-app (onboarding flips to mailbox-ready; their fresh-start guard admits them). Honours the reservation/claim machinery and is idempotent/redelivery-safe.
Close the loop from the dashboard: provision the hosted mailbox inline, or keep the plain acknowledge/decline via 'Mark done'. Provision is disabled with a one-line reason when no sending domain is verified.
Review — round 1Verdict: REQUEST_CHANGES CI is green and the admin-gate/org-scope/idempotency work is solid (secure-by-default wrapper, Blocking
Improvements
|
- No-reservation branch now stands up localpart@<verified sending domain> instead of the requester's external login address, and enforces the verified-domain floor server-side (the UI disable is only an affordance). Provisioning at an external address created a dead inbox the MTA wrongly claimed, yet marked the request fulfilled. - Move-raised requests (mailboxMove.start) are refused with a pointer to the move flow rather than mis-fulfilled against the mover's live external mailbox, which stranded the move at 'provisioning'. - The already-live short-circuit now matches hosted mailboxes only, so a mover's external mailbox is never mistaken for a fulfilled hosted one. - resolve() no longer downgrades a decided (fulfilled/resolved) row. - Reservation lookup is org-scoped so a foreign-org row can't shadow it. - Extract claimReservedMailbox() in pendingMailbox.ts, shared by claimForInvitation and provisionFromRequest, plus a local fulfil() helper, so claim/consume and fulfil-stamp semantics can't drift.
- Disable a row's sibling action while it is busy (the busy guard silently swallowed the click before), keying on both id and action. - Only show the disabled 'Provision now' + 'Verify a sending domain first' once the verified-domains query has loaded, so it no longer flashes on every mount when domains actually exist.
…equest - Seed a verified domain in the provision/idempotent tests and assert the hosted mailbox lands on it, not the requester's external address. - Add: refusal when no domain is verified (no mailbox stood up, row stays open). - Add: a move-raised request is refused and the move is left at 'provisioning'. - Add: resolve() does not downgrade an already-fulfilled request.
Author response — round 2All findings addressed. Blocking
Improvements
|
Review — round 2Verdict: APPROVE Re-reviewed against round 1. Both blocking findings and all six improvements are addressed, no new defects introduced by the fix commits, and GitHub CI is fully green (Lint & Typecheck, Test apps/api + apps/web, SAST, secret scan all pass). Blockingnone
Improvementsnone
Spec delivered: admin-gated ( |
Review — round 1 (independent re-verification; PR was already merged after the prior round-2 approve)Verdict: APPROVE Full independent pass over the merged head (b080665): security, spec, brief conformance, code smells, best practices, tests. CI 32/32 green. Blockingnone
Improvements
|
What & why
Part of the next-layer UX plan — workstream A: an admin can set up everything in-app, locked principle #1 NO CLI CLIFFS ("every setup action is reachable from the dashboard") and #5 HONESTY ("never mark a step done that isn't real").
Today a mailbox request could only be acknowledged — the admin still had to go provision the mailbox elsewhere and then click "Mark done", a step that lied about what happened. This closes the loop: an admin provisions the hosted mailbox straight from the request row, and it resolves as FULFILLED, not merely acknowledged.
mail.mailboxRequest.provisionFromRequeststands the hosted mailbox up through the existing shared provisioning path (createProvisionedMailbox/provisionMailbox) — admin scope is never bypassed.mailboxReadyand their fresh-start guard (freshStartStatus) now reportshasMailbox, soPostboxMailboxGuardadmits them to the inbox live.Acceptance criteria
fulfilledstatus +fulfilledMailboxId), not merely acknowledged.mailboxReady; fresh-start guard admits them).Preserved behavior
request/freshStartStatus/listPending/resolveunchanged in behavior;resolveremains the acknowledge/decline path.statusunion widenedopen | resolved→open | fulfilled | resolved; sweep/erasure/tenant-table consumers only delete rows (no exhaustive switch), so no break.createProvisionedMailbox/provisionMailbox) reused unchanged; no new mailbox insert path.Tests (vitest / convex-test, CI-gated)
apps/api/convex/__tests__/mailboxRequest.integration.test.ts:freshStartStatus.hasMailbox).Auto-merge pipeline: squash-merges on reviewer approval + green CI.