fix(license): scope license lookup per-org in cloud mode#219
Merged
Conversation
The cloud was leaking license entitlement across tenants: getCurrentLicense falls back to site_settings.license_key (a single instance-wide pointer) when the calling org has no license of its own, and any prior call had been auto-binding orphan licenses to whichever org happened to ask first. Combined with the unscoped POST /api/license/verify (which read the same global pointer), an org with zero licenses could see another org's key as "verified successfully". This change isolates lookups per organizationId in cloud mode while keeping the existing single-tenant fallbacks for self-hosted, where the site_settings pointer is the legitimate "this is the instance license" mechanism. - getCurrentLicense: in cloud, returns null when the calling org has no license; no more global pointer fallback, no auto-binding of orphan licenses to a random caller. Self-hosted behavior unchanged. - verifyLicense: accepts organizationId and, in cloud, resolves the key to verify from the org's own license; falls back to the global setting only in self-hosted. - setLicenseKey / requestTrialLicense: stop overwriting the global site_settings.license_key in cloud mode; persist organizationId on the trial record so future lookups can find it. - verifyOnStartup: no-op in cloud (per-org background verification belongs to a separate cron, not to instance boot). - POST /api/license/verify: passes req.user.organizationId. - POST /api/license/activate-trial: passes req.user.organizationId. Added 6 unit tests covering the bug scenario and the self-hosted fallback path. Full suite green (752 passed, 5 skipped).
This was referenced May 19, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a cross-tenant entitlement leak in cloud mode.
getCurrentLicensefalls back tosite_settings.license_key(a single instance-wide pointer) when the calling org has no license of its own, and on hit it auto-binds the unassigned license to whichever org happens to ask first. Combined with the unscopedPOST /api/license/verify(which reads the same global pointer), an org with zero licenses can see another org's key as "License verified successfully".Reproducer on the live cloud:
keysersoft@gmail.com→Matteo's Workspace)./settings/licenseand click Verify Now.sales@helpcode.ai).What changes
This isolates lookups per
organizationIdin cloud mode while keeping the existing single-tenant fallbacks for self-hosted, wheresite_settings.license_keyis the legitimate "this is the instance license" mechanism.getCurrentLicense: in cloud, returnsnullwhen the calling org has no license; no more global pointer fallback, no auto-binding of orphan licenses to a random caller. Self-hosted behavior unchanged.verifyLicense: now acceptsorganizationIdand, in cloud, resolves the key to verify from the org's own license; falls back to the global setting only in self-hosted.setLicenseKey/requestTrialLicense: stop overwritingsite_settings.license_keyin cloud; persistorganizationIdon the trial record so future lookups can find it.verifyOnStartup: no-op in cloud (per-org background verification belongs to a separate cron, not to instance boot).POST /api/license/verify: passesreq.user.organizationId.POST /api/license/activate-trial: passesreq.user.organizationId.Pre-deploy data backfill
Performed directly on the cloud Postgres (one-off, environment-specific — not a Prisma migration on purpose): 3 orphan trial licenses re-assigned to their owning orgs based on
licenses.email↔users.email. One enterprise license (AMCP-3076-FFD0-DFCC-D619) has no corresponding row in the marketing-site Mongo and was left orphan; after this PR it will no longer be visible to any org.Tests
6 new unit tests in
license.service.spec.ts:nulleven whensite_settings.license_keypoints at the first org's key (the exact bug)site_settings.license_keyfallback still worksverifyOnStartupis a no-op in cloudFull suite: 752 passed, 5 skipped (unchanged from before this PR).
Test plan post-deploy
keysersoft@gmail.com→/settings/licenseshows "No license" (no plan info, no "Verify now" success)sales@helpcode.ai→ still seesAMCP-3887-…-15D8(starter, active)matteo.morelli@kochfreiburg.de→ still seesAMCP-96ED-…-6B51(enterprise, active)site_settings.license_keyandgetCurrentLicense()resolves to it