Skip to content

fix: verify signed online license entitlements#1442

Open
brendan-kellam wants to merge 2 commits into
mainfrom
brendan/fix-SOU-1465
Open

fix: verify signed online license entitlements#1442
brendan-kellam wants to merge 2 commits into
mainfrom
brendan/fix-SOU-1465

Conversation

@brendan-kellam

@brendan-kellam brendan-kellam commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes SOU-1465

Companion Lighthouse PR: https://github.com/sourcebot-dev/lighthouse/pull/27

Summary

  • persist and verify compact Ed25519-signed online license assertions
  • derive authorization from signed status and entitlement claims when an assertion is present
  • validate installation binding, issuance, expiry, maximum lifetime, schema version, and audience
  • fail closed for malformed or invalid assertions without falling back to mutable columns
  • retain a documented unsigned compatibility path for the initial rollout

Rollout

Deploy the Lighthouse signer first. After it has issued assertions for at least one seven-day assertion lifetime, disable ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES in the enforcement release.

Testing

  • yarn build:deps
  • yarn workspace @sourcebot/shared test --run src/entitlements.test.ts
  • yarn workspace @sourcebot/web test --run src/app/\(app\)/components/banners/bannerResolver.test.ts
  • ESLint on changed web files

Note

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 licenseAssertion as the source of truth when it is stored on the License row, instead of trusting mutable DB columns (status, entitlements, lastSyncAt) alone.

verifyOnlineLicenseAssertion validates 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 temporary ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES flag keeps the prior staleness-based path for rollout.

Service ping accepts an optional licenseAssertion on 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

    • Added verification for signed online license assertions before granting paid feature entitlements.
    • License status and entitlements now reflect verified, installation-specific, non-expired assertions.
    • License assertions are securely saved during licensing synchronization.
  • Bug Fixes

    • Invalid, expired, canceled, or mismatched license assertions no longer grant paid features.
    • Prevented fallback to potentially outdated license information when an assertion is present but invalid.

@github-actions

Copy link
Copy Markdown
Contributor

@brendan-kellam your pull request is missing a changelog!

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds signed online license assertion storage, verification, entitlement precedence rules, Lighthouse synchronization validation, and coverage for invalid, expired, mismatched, and canceled assertions.

Changes

Online license assertion flow

Layer / File(s) Summary
License assertion storage
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/...
Adds a nullable licenseAssertion text field to the License model and database table.
Assertion validation and entitlement resolution
packages/shared/src/entitlements.ts, packages/shared/src/entitlements.test.ts, packages/shared/src/index.server.ts, CHANGELOG.md
Verifies assertion signatures and claims, uses valid active assertion payloads for entitlements, disables unsigned fallback when assertions are invalid, and tests the resulting cases.
Lighthouse sync and persistence
packages/web/src/features/billing/*, packages/web/src/app/(app)/components/banners/bannerResolver.test.ts
Extends the Lighthouse response schema, validates returned assertions before synchronization, persists them, and updates license fixtures.

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
Loading

Possibly related PRs

Suggested reviewers: msukkari

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: verifying signed online license entitlements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/fix-SOU-1465
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch brendan/fix-SOU-1465

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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 }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fe50da2. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a50ca10 and fe50da2.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260713000000_add_license_assertion/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/shared/src/entitlements.test.ts
  • packages/shared/src/entitlements.ts
  • packages/shared/src/index.server.ts
  • packages/web/src/app/(app)/components/banners/bannerResolver.test.ts
  • packages/web/src/features/billing/servicePing.ts
  • packages/web/src/features/billing/types.ts

Comment on lines +61 to +62
licenseId: z.string().min(1),
installId: z.string().min(1),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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/db

Repository: 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.prisma

Repository: 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.ts

Repository: 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.

Comment on lines 129 to +136
// 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');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

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