basquetWi + New ticket
pluto PLUTO-32

Pluto P3 residual: bulkInviteUsers welcome-email loop bypasses the /send-email HTTP rate limiter (PLUTO-20) — uncapped in-process sends on bulk invite. Decide cap (e.g. 100/batch or inter-send delay) vs accept (admin-initiated, low-freq, in-process-mail-undroppable per PLUTO-20 design). From audit round-1.

Done low cpcoder-pluto-cc

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 2026-06-10
  • wi-cli-venus assigned · 7w ago
    coder-pluto-cc
  • wi-cli-venus completed · 7w ago
    Shipped v2.23.57 (2e61ec3). The WI premise was partly wrong and is corrected in-code: sendWelcomeEmail does not set immediate, so bulkInviteUsers never did in-request SMTP - the loop is one emailOutbox INSERT per row, so there was no uncapped in-process send and no timeout exposure to cap. ACCEPTED (not fixed), documented at the call site: the /api/internal/send-email 300/hr limiter bypass is real and correct to leave - that limiter bounds the blast radius of a leaked INTERNAL_EMAIL_KEY, while this path is behind an admin session plus the inviteUsers capability (different threat model), and its sends still write emailOutbox so they count toward that limiter's own denominator. send-email/route.ts:18 already said exactly this. FIXED (the real exposure the WI did not name): each enqueue also scheduled its own after(deliverOutboxRow), so N invites became N unpaced post-response SMTP sends - precisely the burst drain-email-outbox/route.ts:11 caps at 10-rows/5min 'so a backlog can't re-trip Gmail's bulk-421'. A 421 does not lose mail (row stays pending and is retried) but throttles the SENDER on shared credentials, degrading every other stream including alerts. New opt-in deferDelivery on sendEmailCore + sendWelcomeEmail: row is enqueued identically, after() is not scheduled, pg_cron drain delivers. bulkInviteUsers passes it; approveAccessRequest and single-user inviteUser deliberately do not (one send is not a burst; queue latency on an interactive invite would be a regression). Latency is QUEUE-DEPENDENT, not a 5-minute bound - 10 rows/5min tick means 25 invites tail around 15 min, larger batches scale, behind any existing backlog; audit caught an earlier comment that wrongly claimed ~5min and it was corrected before push. r.success already meant durable enqueue rather than SMTP delivery, so the admin's 'Invitados: N' tally is not newly weakened (audit concurred). 4 tests in src/lib/email-defer-delivery.test.ts incl. a source scan asserting exactly 3 sendWelcomeEmail call sites with exactly 1 deferred - deferDelivery leaves no runtime trace so source is the only auditable surface. audit-pluto-ca PASS after one MED correction. LIMITS: unpaced per-row after() is proven from source only; no historical bulk-421 on this path has been measured and none is claimed, and no prod send was exercised.
--priority
2026-06-10 by wi-cli-venus
7w ago
2026-08-22 14:23