feat/subscription-gating
needs attentionc807731 · incrementalPR #234reviewed 2026-07-30 17:24 UTC0H · 6M · 8L · 1I- Purpose
- Gate cost-incurring CFE downloads for organizations with inactive Stripe subscriptions, while keeping reads and billing pages open for self-cure.
- Goal
- Subscription access gating: 402 on collection jobs for canceled orgs, amber/red dashboard banner, daily subscriber sweep skips inactive orgs.
- Sub-goals
- Pure domain decision decideFeatureAccess + SubscriptionInactiveError
- requireActiveSubscription request guard (fail-open, fresh PK lookup)
- Enforcement at internal cfe-jobs collect/batch/retry + public POST /v1/jobs
- Frontend SubscriptionBanner in dashboard layout
- billingExempt metadata override for comped orgs
- Active-first contract lookup (findByContractNumberActiveFirst)
- TOCTOU batch insert fix (insertBatchSkippingActive + pairBatchInsertResults)
- What
- This incremental adds: routeMeta + 402/403 surface on public-v1 jobs contract; active-first contract lookup across single/retry/batch paths; TOCTOU batch insert fix; enabledModules tri-state for SoftLaunchGuard; i18n additions; insufficient_scope error type; publicIdRegex refactor; API key error code cleanup.
- Why
- Follow-up hardening after initial subscription gating: correct contract lookup order (active wins), prevent TOCTOU 23505 crashes on concurrent batch submissions, propagate module entitlements correctly during org data loading.
- Areas
- apps/platform/src/api+62−12apps/platform/src/app+9−1apps/platform/src/messages+80−6packages/api+10−4
- Blast
- 11 files, ~155 lines net across API contracts, handlers, mappers, UI layout, i18n, and API type definitions. No migrations.
Findings · 15
correctness3
GET /v1/jobs/:id missing 500 in contract responses
apps/platform/src/api/contracts/public-v1/jobs.contract.ts
Handler catch block returns 500 via InternalError but detail route only declares 200/400/401/403/404. Add 500: PublicApiErrorResponseSchema.
Batch serviceNameMap active-first bypassed for blank active serviceName
apps/platform/src/api/handlers/cfe-jobs.handler.ts:690
When active contract has serviceName='' and terminated contract has a real name, the blank active skips the loop guard and terminated name wins. Contradicts active-first invariant.
Analytics rpu_count should use dispatch.length not insertedJobs.length
apps/platform/src/api/handlers/cfe-jobs.handler.ts:942
dispatch.length is the count of jobs actually sent to the pipeline post-pairing. insertedJobs.length could diverge in edge cases.
security3
Fail-open subscription gate on DB transient failures
packages/api/src/middleware/require-active-subscription.ts
requireActiveSubscription returns ok() when findById returns null — a canceled org bypasses the 402 gate on any DB connection fault. Documented design choice, not a bug.
Subscription gate fires after context resolution in createCfeJobHandler
apps/platform/src/api/handlers/cfe-jobs.handler.ts:217
A canceled org completes contract + secret lookups before receiving 402 — timing-only leakage, no data returned.
Internal cfe-jobs contract missing 403 on create/retry/batch
apps/platform/src/api/contracts/cfe-jobs.contract.ts
If scope enforcement lands on internal routes, undeclared 403 collapses to 500 from client view.
conventions1
403 in public-v1 contract emitted by middleware not handler
apps/platform/src/api/contracts/public-v1/jobs.contract.ts
Handler error mapper union has no 403 branch — 403 is produced by withMetaGate middleware. Contract is technically accurate but misleading.
tests4
No handler-level test for dispatch.length > 0 SFN guard
apps/platform/src/api/handlers/__tests__/
pairBatchInsertResults unit tests verify empty dispatch on all-conflict input but there's no handler test that sfnClient.send is NOT called when dispatch is empty.
SoftLaunchGuard tri-state has no component test
apps/platform/src/components/__tests__/SoftLaunchGuard.test.tsx
undefined (loading), [] (error/fail-closed), populated array — three states, zero component test coverage.
latest_total mapper mapping not covered in mapper tests
apps/platform/src/api/mappers/public-v1/__tests__/jobs.mapper.test.ts
No test passes latest_total through toCfeJobConfig to assert it maps to latestTotal.
API_KEY_LIMIT_REACHED has no enforcement test
apps/platform/src/api/handlers/__tests__/
Neither old nor new error code name appears in any test.
improvement4
Retry handler returns 422 not declared in cfe-jobs contract
apps/platform/src/api/contracts/cfe-jobs.contract.ts
retryCfeJobHandler emits 422 (ServiceNameMissing) but the retry contract only lists 200/401/402/404/409. ts-rest collapses undeclared statuses to 500. Add 422 to the retry contract responses.
subscriptionInactiveResponse uses hardcoded error string
apps/platform/src/api/handlers/cfe-jobs.handler.ts
error: "SUBSCRIPTION_INACTIVE" is a bare string literal. Use the typed catalog constant from packages/api/src/responses/codes.ts for compile-time safety.
Sidebar enabledModules ?? [] masks the tri-state loading signal
apps/platform/src/app/[locale]/(dashboard)/layout.tsx:162
Sidebar gets [] during org fetch flight, showing no enabled modules briefly. Pass raw tri-state and handle undefined inside DashboardSidebar to match SoftLaunchGuard.
Batch handler step comment numbering is stale
apps/platform/src/api/handlers/cfe-jobs.handler.ts
Analytics block labeled '// 7. Analytics' but follows step 8 (SFN dispatch). Should be '// 9.' or scheme consolidated.
History · 7 commits
- c807731needs attentionincremental0H · 6M · 8L2026-07-30 17:24current
- 372882bneeds attentionincremental0H · 1M · 2L2026-07-21 17:31
- 1fac5adneeds attentionincremental0H · 5M · 4L2026-07-07 02:50
- 99bf169safeincremental0H · 0M · 1L2026-07-04 04:01
- 6305b3cneeds attentionincremental0H · 1M · 4L2026-07-04 03:53
- f00e3caneeds attentionincremental0H · 7M · 16L2026-07-04 03:15
- 1ae74bdneeds attentionfull6H · 9M · 8L2026-07-04 02:36