Skip to content

fix(hub): soften license gate to permissive when JWKS unavailable#62

Merged
finedesignz merged 1 commit into
mainfrom
fix/profile-license-gate
May 26, 2026
Merged

fix(hub): soften license gate to permissive when JWKS unavailable#62
finedesignz merged 1 commit into
mainfrom
fix/profile-license-gate

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

Root cause

GET /api/profile returned 402 in prod because:

  1. License gate runs on every /api/* route (mounted globally in hub/src/index.ts).
  2. Stale cache → refreshLicensevalidateLicenseKeyverifyLicenseJwt → jose createRemoteJWKSet fetches ${KEYGEN_URL}/v1/accounts/{id}/.well-known/jwks.json404.
  3. jose throws a non-mapped error → mapJoseError falls through → TitaniumVerifyError('malformed', 'Unknown verify error: ...').
  4. refreshLicense catches it → maps to INVALID → persists → gate denies with 402.

The Keygen JWKS 404 is a separate infra issue. This PR makes the gate resilient to that failure so identity/REST reads aren't gated by Keygen health.

Changes

  1. LICENSE_REQUIRED env flag (default true, set false to bypass). When false, requireActiveLicense short-circuits to next() and logs a one-shot warning. Escape hatch for the JWKS-down period.

  2. Transient verify errors no longer flip cache to INVALID. refreshLicense now treats TitaniumVerifyError of kind network/malformed (or message matching /jwks/i) as transient: preserves cached status, does not persist. Matches existing TitaniumApiError semantics — decoupled-for-read: a healthy ACTIVE user must not be locked out by a transient verify failure.

Tests

  • 15 existing license-gate cases still pass.
  • 2 new cases: LICENSE_REQUIRED=false bypass + JWKS-unreachable preserves cached ACTIVE without persisting INVALID.
  • 5 failing tests in insert-run-started-at.test.ts are pre-existing on main (unrelated).

Deploy plan

Set LICENSE_REQUIRED=false in Coolify env for app.remo-code.com until Keygen JWKS path is sorted. Even without the env var, the transient-handling change alone prevents ACTIVE→INVALID flips going forward.

🤖 Generated with Claude Code

Root cause: GET /api/profile returned 402 in prod because the license gate
ran for every /api/* route, hit a stale cache, called validateLicenseKey
which then called verifyLicenseJwt -> jose's createRemoteJWKSet got a 404
from Keygen's JWKS endpoint, jose threw a non-mapped error, mapJoseError
fell through to TitaniumVerifyError('malformed', 'Unknown verify error: ...'),
and refreshLicense flipped the cached ACTIVE status to INVALID -> 402.

Two surgical fixes:

1. LICENSE_REQUIRED env flag (default true). When false, requireActiveLicense
   short-circuits to next() and logs a one-shot warning. Escape hatch for
   the JWKS-is-down period without redeploying schema.

2. refreshLicense now treats TitaniumVerifyError of kind 'network'/'malformed'
   (or any message containing /jwks/i) as TRANSIENT: preserves cached status,
   does NOT persist a flip to INVALID. Matches the existing TitaniumApiError
   semantics ("decoupled-for-read: a healthy ACTIVE user must not be locked
   out by a transient verify failure").

Tests: 15 existing license-gate cases still pass + 2 new cases covering the
permissive bypass and the JWKS-transient preservation. The 5 failing tests
in insert-run-started-at.test.ts are pre-existing on main (unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@finedesignz finedesignz merged commit 8bd8b18 into main May 26, 2026
1 check passed
@finedesignz finedesignz deleted the fix/profile-license-gate branch May 26, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant