← all branches

feat/subscription-gating

needs attention
c807731 · incrementalPR #234reviewed 2026-07-30 17:24 UTC0H · 6M · 8L · 1I
The branch
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)
The changes (whole branch)
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+6212apps/platform/src/app+91apps/platform/src/messages+806packages/api+104
Blast
11 files, ~155 lines net across API contracts, handlers, mappers, UI layout, i18n, and API type definitions. No migrations.
no-migration fail-open-subscription-gate toctou-fix
CI checks· Not accessible via personal access tokenCodeRabbit· Review paused (active dev branch)

Findings · 15

correctness3

medium

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.

low

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.

low

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

medium

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.

low

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.

low

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

low

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

medium

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.

medium

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.

low

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.

low

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

medium

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.

medium

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.

low

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.

info

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

  1. c807731needs attentionincremental0H · 6M · 8L2026-07-30 17:24current
  2. 372882bneeds attentionincremental0H · 1M · 2L2026-07-21 17:31
  3. 1fac5adneeds attentionincremental0H · 5M · 4L2026-07-07 02:50
  4. 99bf169safeincremental0H · 0M · 1L2026-07-04 04:01
  5. 6305b3cneeds attentionincremental0H · 1M · 4L2026-07-04 03:53
  6. f00e3caneeds attentionincremental0H · 7M · 16L2026-07-04 03:15
  7. 1ae74bdneeds attentionfull6H · 9M · 8L2026-07-04 02:36