fix: verify signed online license entitlements#1442
Conversation
|
@brendan-kellam your pull request is missing a changelog! |
WalkthroughAdds signed online license assertion storage, verification, entitlement precedence rules, Lighthouse synchronization validation, and coverage for invalid, expired, mismatched, and canceled assertions. ChangesOnline license assertion flow
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Lighthouse
participant syncWithLighthouse
participant verifyOnlineLicenseAssertion
participant LicenseDatabase
participant getEntitlements
Lighthouse->>syncWithLighthouse: licenseAssertion
syncWithLighthouse->>verifyOnlineLicenseAssertion: signed assertion
verifyOnlineLicenseAssertion-->>syncWithLighthouse: validated payload or null
syncWithLighthouse->>LicenseDatabase: persist validated assertion
LicenseDatabase->>getEntitlements: license record
getEntitlements->>verifyOnlineLicenseAssertion: stored assertion
verifyOnlineLicenseAssertion-->>getEntitlements: entitlements and status
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fe50da2. Configure here.
| yearlyPeakSeats: yearlyTermStatus?.peakSeats ?? null, | ||
| lastSyncAt: new Date(), | ||
| lastSyncErrorCode: null, | ||
| ...(response.licenseAssertion && { licenseAssertion: response.licenseAssertion }), |
There was a problem hiding this comment.
Stale assertion survives license sync
High Severity
The license sync updates mutable fields but retains the existing licenseAssertion if Lighthouse doesn't provide a new one. This allows stale, signed entitlements to persist, granting continued access even after a subscription is canceled or downgraded.
Reviewed by Cursor Bugbot for commit fe50da2. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/shared/src/entitlements.ts`:
- Around line 61-62: Update the online assertion persistence and validation flow
around the entitlement schema fields licenseId and installId to retain the
upstream licenseId with each persisted assertion, and require it to match the
current License row before accepting the assertion. Add a test covering reuse of
a valid assertion across different License rows on the same installation,
ensuring the cross-license replay is rejected.
In `@packages/web/src/features/billing/servicePing.ts`:
- Around line 129-136: Update the service-ping handling around
verifyOnlineLicenseAssertion so assertion presence is checked explicitly and
validated before the response.license synchronization block, rejecting empty or
invalid assertions. Handle assertion-only responses independently: persist them
when supported, or reject the response as malformed rather than skipping
persistence and leaving stale entitlements active; apply the same behavior to
the corresponding later handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 68eac570-e897-4f14-b103-bf840cd35d86
📒 Files selected for processing (9)
CHANGELOG.mdpackages/db/prisma/migrations/20260713000000_add_license_assertion/migration.sqlpackages/db/prisma/schema.prismapackages/shared/src/entitlements.test.tspackages/shared/src/entitlements.tspackages/shared/src/index.server.tspackages/web/src/app/(app)/components/banners/bannerResolver.test.tspackages/web/src/features/billing/servicePing.tspackages/web/src/features/billing/types.ts
| licenseId: z.string().min(1), | ||
| installId: z.string().min(1), |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate the licenseId contract and the synchronization mapping without executing repository code.
rg -n -C4 '\blicenseId\b' packages
rg -n -C4 '\blicenseAssertion\b' packages/web packages/shared packages/dbRepository: sourcebot-dev/sourcebot
Length of output: 11713
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' packages/shared/src/entitlements.ts
printf '\n---\n'
sed -n '1,280p' packages/web/src/features/billing/servicePing.ts
printf '\n---\n'
sed -n '340,420p' packages/db/prisma/schema.prismaRepository: sourcebot-dev/sourcebot
Length of output: 19338
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C8 'function verifyOnlineLicenseAssertion|const verifyOnlineLicenseAssertion|verifyOnlineLicenseAssertion\(' packages/shared/src/entitlements.ts packages/web/src/features/billing/servicePing.ts packages/shared/src/entitlements.test.tsRepository: sourcebot-dev/sourcebot
Length of output: 4348
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C3 '\blicenseId\b' packages/db packages/web packages/shared | sed -n '1,240p'Repository: sourcebot-dev/sourcebot
Length of output: 1211
Bind online assertions to a persisted license ID.
installId alone still lets a valid assertion be reused across different License rows on the same installation. Persist the upstream licenseId, compare it before accepting the assertion, and add a cross-license replay test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/shared/src/entitlements.ts` around lines 61 - 62, Update the online
assertion persistence and validation flow around the entitlement schema fields
licenseId and installId to retain the upstream licenseId with each persisted
assertion, and require it to match the current License row before accepting the
assertion. Add a test covering reuse of a valid assertion across different
License rows on the same installation, ensuring the cross-license replay is
rejected.
| // If we have a license and Lighthouse returned license data, sync it | ||
| if (license && response.license) { | ||
| if (response.licenseAssertion && !verifyOnlineLicenseAssertion(response.licenseAssertion)) { | ||
| // Never persist an assertion we cannot authenticate. In particular, | ||
| // do not silently write only the legacy fields and create a | ||
| // signature-downgrade path. | ||
| throw new Error('Lighthouse returned an invalid online license assertion'); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Handle assertion presence independently and fail closed.
response.licenseAssertion must be checked with an explicit presence test, not truthiness, and validation must occur before the response.license block. As written, an empty assertion or an assertion returned without response.license is ignored; the latter also prevents persistence and can leave stale entitlements active. Persist assertion-only responses if supported, or reject them as malformed protocol responses.
Proposed fix
+ const hasLicenseAssertion = response.licenseAssertion !== undefined;
+ if (hasLicenseAssertion && !response.license) {
+ throw new Error('Lighthouse returned an assertion without license data');
+ }
+ if (hasLicenseAssertion && !verifyOnlineLicenseAssertion(response.licenseAssertion)) {
+ throw new Error('Lighthouse returned an invalid online license assertion');
+ }
+
// If we have a license and Lighthouse returned license data, sync it
if (license && response.license) {
- if (response.licenseAssertion && !verifyOnlineLicenseAssertion(response.licenseAssertion)) {
- throw new Error('Lighthouse returned an invalid online license assertion');
- }
-
// ...
- ...(response.licenseAssertion && { licenseAssertion: response.licenseAssertion }),
+ ...(hasLicenseAssertion && { licenseAssertion: response.licenseAssertion }),
}Also applies to: 185-185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/features/billing/servicePing.ts` around lines 129 - 136,
Update the service-ping handling around verifyOnlineLicenseAssertion so
assertion presence is checked explicitly and validated before the
response.license synchronization block, rejecting empty or invalid assertions.
Handle assertion-only responses independently: persist them when supported, or
reject the response as malformed rather than skipping persistence and leaving
stale entitlements active; apply the same behavior to the corresponding later
handling.


Fixes SOU-1465
Companion Lighthouse PR: https://github.com/sourcebot-dev/lighthouse/pull/27
Summary
Rollout
Deploy the Lighthouse signer first. After it has issued assertions for at least one seven-day assertion lifetime, disable
ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSESin the enforcement release.Testing
yarn build:depsyarn workspace @sourcebot/shared test --run src/entitlements.test.tsyarn workspace @sourcebot/web test --run src/app/\(app\)/components/banners/bannerResolver.test.tsNote
High Risk
Changes the authorization path for paid EE features and license sync behavior; bugs could wrongly grant or deny entitlements or break Lighthouse sync when assertions are present.
Overview
Online EE licensing now treats Lighthouse’s compact Ed25519-signed
licenseAssertionas the source of truth when it is stored on theLicenserow, instead of trusting mutable DB columns (status,entitlements,lastSyncAt) alone.verifyOnlineLicenseAssertionvalidates signature, audience, install binding, issuance/expiry (with clock skew), and max assertion lifetime.getEntitlements/ online license active checks use signed status and entitlements when an assertion is present; invalid or expired assertions fail closed with no fallback to unsigned columns. A temporaryALLOW_LEGACY_UNSIGNED_ONLINE_LICENSESflag keeps the prior staleness-based path for rollout.Service ping accepts an optional
licenseAssertionon the Lighthouse response, verifies it before any license update, persists it with the rest of the sync, and aborts the sync if Lighthouse sends an assertion that cannot be verified (blocking a signature-downgrade where only legacy fields would be written).Schema adds
License.licenseAssertion; shared exports expose the verifier and payload type; tests cover assertion precedence and rejection cases.Reviewed by Cursor Bugbot for commit fe50da2. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes