File: apps/api/src/middleware/auth.ts lines 67–87; packages/db/src/schema/auth.ts organizations table
Vulnerability: The organizations table has no status, suspended_at, or cancelled_at column. The apiKeyAuth middleware joins api_keys → organizations and checks only key expiry and revocation — it has no gate for org-level suspension:
.where(
and(
eq(apiKeys.keyHash, keyHash),
or(isNull(apiKeys.expiresAt), gt(apiKeys.expiresAt, new Date())),
isNull(apiKeys.revokedAt),
),
)
If an org's subscription is cancelled, the only available remediation is:
- Delete the entire org (destroys all audit data).
- Revoke every API key one by one.
- Remove
productsEnabled (only gates requireProduct routes — non-product routes remain accessible).
There is no single-operation "suspend org" that blocks all API access while preserving the audit trail.
Impact: A churned or payment-lapsed org retains full API access until every individual API key is manually revoked. An automated billing webhook that sets suspended_at cannot stop access with the current schema.
Fix:
- Add
suspended_at timestamp to organizations.
- In
apiKeyAuth, add isNull(organizations.suspendedAt) to the WHERE clause.
- Expose a
PATCH /v1/admin/organizations/:id field (suspended: true) in the admin route.
File:
apps/api/src/middleware/auth.tslines 67–87;packages/db/src/schema/auth.tsorganizationstableVulnerability: The
organizationstable has nostatus,suspended_at, orcancelled_atcolumn. TheapiKeyAuthmiddleware joinsapi_keys → organizationsand checks only key expiry and revocation — it has no gate for org-level suspension:If an org's subscription is cancelled, the only available remediation is:
productsEnabled(only gatesrequireProductroutes — non-product routes remain accessible).There is no single-operation "suspend org" that blocks all API access while preserving the audit trail.
Impact: A churned or payment-lapsed org retains full API access until every individual API key is manually revoked. An automated billing webhook that sets
suspended_atcannot stop access with the current schema.Fix:
suspended_at timestamptoorganizations.apiKeyAuth, addisNull(organizations.suspendedAt)to the WHERE clause.PATCH /v1/admin/organizations/:idfield (suspended: true) in the admin route.