Skip to content

fix: verify signed online license entitlements#1442

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

fix: verify signed online license entitlements#1442
brendan-kellam wants to merge 3 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 core EE authorization: invalid or missing assertions strip entitlements immediately once enforcement applies, and service ping failures can block persisting license updates—coordinate rollout with the Lighthouse signer companion PR.

Overview
Online EE licensing now fails closed on unsigned or tampered state. Paid features and anonymous-access gating no longer trust mutable License columns (status, entitlements, lastSyncAt); they require a persisted Lighthouse-signed licenseAssertion whose claims are verified (signature, audience, installId, issuance/expiry, max lifetime, and license snapshot schema).

Service ping verifies any returned assertion before writing it (and refuses assertion-without-license or invalid signatures so legacy fields cannot be used as a downgrade path). License rows are updated from the verified snapshot, not raw response fields alone.

Shared lighthouseTypes now defines the online license snapshot and ping response shape (including optional licenseAssertion); web billing imports these instead of local duplicates. Startup runs an initial Lighthouse sync before the daily ping cron (errors logged to Sentry, startup continues).

Extensive entitlements tests cover claim validation and the no-fallback-to-DB-columns behavior.

Reviewed by Cursor Bugbot for commit 6fdad4f. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes

    • Online license entitlements are now granted only after signed license assertions are successfully verified.
    • Invalid, expired, canceled, or mismatched assertions no longer provide paid feature access.
    • License synchronization now rejects unauthenticated online license data.
  • Tests

    • Expanded coverage for assertion validation, entitlement handling, and startup error recovery.

@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

The change adds signed online license assertions to the license schema and Lighthouse sync flow, verifies assertion claims before deriving entitlements, updates shared billing contracts, and makes startup explicitly await license synchronization before starting background jobs.

Changes

Online license assertion flow

Layer / File(s) Summary
License assertion storage
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/...
Adds an optional licenseAssertion text field to the License model and database table.
Shared assertion contracts and consumer alignment
packages/shared/src/lighthouseTypes.ts, packages/shared/src/index.*.ts, packages/web/src/features/billing/*, packages/web/src/app/..., packages/backend/package.json, packages/shared/package.json
Defines signed assertion schemas and shared license snapshot types, exports them through client/server barrels, updates billing type imports, and bumps Zod versions.
Assertion validation and entitlement resolution
packages/shared/src/entitlements.ts, packages/shared/src/entitlements.test.ts, CHANGELOG.md
Verifies assertion signatures and claims, requires active validated assertions for online entitlements, filters unknown entitlements, and adds coverage for invalid, expired, canceled, and mismatched assertions.
Lighthouse sync and persistence
packages/web/src/features/billing/servicePing.ts, packages/web/src/app/(app)/components/banners/bannerResolver.test.ts
Validates Lighthouse assertions, uses their embedded license snapshot, persists returned assertions, and updates license fixtures.
Startup synchronization orchestration
packages/web/src/initialize.ts, packages/web/src/initialize.test.ts, packages/web/src/instrumentation.ts
Exports and tests an initialization flow that awaits Lighthouse synchronization, reports failures, starts background jobs, and is explicitly awaited by instrumentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Lighthouse
  participant syncWithLighthouse
  participant verifyOnlineLicenseAssertion
  participant PrismaLicense
  participant getEntitlements
  Lighthouse->>syncWithLighthouse: signed license assertion
  syncWithLighthouse->>verifyOnlineLicenseAssertion: verify assertion
  verifyOnlineLicenseAssertion-->>syncWithLighthouse: validated claims
  syncWithLighthouse->>PrismaLicense: persist assertion
  PrismaLicense->>getEntitlements: stored license
  getEntitlements->>verifyOnlineLicenseAssertion: validate stored assertion
  verifyOnlineLicenseAssertion-->>getEntitlements: active entitlements
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: verifying signed online license entitlements.
✨ 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

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.

Comment thread packages/web/src/features/billing/servicePing.ts Outdated

@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 thread packages/shared/src/entitlements.ts Outdated
Comment thread packages/web/src/features/billing/servicePing.ts Outdated

@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 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6fdad4f. Configure here.

// Unsigned database columns never grant online entitlements.
if (!_license?.licenseAssertion) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unsigned rollout compatibility missing

High Severity

getValidOnlineLicense returns null whenever licenseAssertion is absent, with no legacy unsigned path. The PR describes retaining ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES for initial rollout, but that flag is not implemented anywhere in this repository, so existing rows with active mutable license data and a null assertion lose all online entitlements immediately after upgrade until a signed assertion is persisted.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6fdad4f. Configure here.

startChangelogPollingJob();
warmModelCapabilitiesCatalog();
})();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Search context wipe removed

Medium Severity

Refactoring initialize removed the startup check that deleted org search contexts when the search-contexts entitlement was missing. With stricter assertion-based entitlements, revoked customers can keep persisted contexts and EE chat can still expand reposet scopes from the database without that entitlement.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6fdad4f. 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/shared/src/entitlements.ts (1)

182-206: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Assertion isn't bound to the persisted License row it's being validated against, and the rollout legacy-flag fallback isn't visible here.

Two concerns in getValidOnlineLicense:

  1. Per a past review round on this PR, reusing a valid assertion across different License rows on the same installation (since only installId is checked) was flagged and marked "Addressed", requesting that the upstream licenseId be persisted and compared before accepting an assertion. The code shown here still only checks payload.installId !== env.SOURCEBOT_INSTALL_ID inside verifyOnlineLicenseAssertion (Line 93) — there's no comparison of assertion.licenseId against any identifier on _license in getValidOnlineLicense (Lines 187-203), and entitlements.test.ts has no cross-license replay test. Please confirm whether the binding is enforced elsewhere (e.g. a DB-level unique constraint guaranteeing one License row per install) or whether this gap is still open.
  2. PR objectives state an unsigned compatibility path remains available via ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES for the initial rollout, but this function returns null unconditionally whenever licenseAssertion is absent (Lines 189-191), with no reference to that flag. If this fallback is meant to live here, licenses that haven't yet received a signed assertion would lose all online entitlements the moment this code ships, even with the flag enabled.
🔍 Verification script
#!/bin/bash
rg -n -C5 '\blicenseId\b' packages/shared/src/entitlements.ts packages/shared/src/entitlements.test.ts packages/db/prisma/schema.prisma
rg -n -C5 'ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES' packages
🤖 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 182 - 206, Update
getValidOnlineLicense to bind the verified assertion’s licenseId to the
persisted License row before accepting entitlements, using the existing database
field or enforce the documented one-row-per-install invariant if that is the
established contract. Add the ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES fallback for
licenses without licenseAssertion, preserving unsigned entitlements only when
the flag is enabled. Add coverage in entitlements.test.ts for cross-license
assertion replay and the enabled/disabled unsigned-license behavior.
🧹 Nitpick comments (1)
packages/shared/src/entitlements.ts (1)

66-113: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Minor: date parsing failure silently passes validation.

If payload.issuedAt/expiresAt fail to parse, new Date(...).getTime() returns NaN, and every comparison in the OR-chain (Lines 93-102) involving NaN evaluates to false — so the whole guard would be false and the function would fall through to return payload instead of rejecting. This is presumably masked today because onlineLicenseAssertionClaimsSchema.parse (in lighthouseTypes.ts, not in this batch) likely enforces strict ISO datetime strings before this code runs — the test rejects an invalid issuedAt timestamp implies this. Still, adding an explicit Number.isNaN(issuedAt) || Number.isNaN(expiresAt) check here would make the invariant self-evident and not solely dependent on an external schema.

🛡️ Optional defensive check
         const issuedAt = new Date(payload.issuedAt).getTime();
         const expiresAt = new Date(payload.expiresAt).getTime();
         const now = Date.now();

         // A license is considered invalid under the following
         // circumstances:
         if (
+            // 0. Its timestamps failed to parse.
+            Number.isNaN(issuedAt) || Number.isNaN(expiresAt) ||
             // 1. It was issued for a different Sourcebot installation.
             payload.installId !== env.SOURCEBOT_INSTALL_ID ||
🤖 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 66 - 113, Update
verifyOnlineLicenseAssertion to explicitly reject invalid date parsing by
checking Number.isNaN(issuedAt) or Number.isNaN(expiresAt) in the license-claims
validation guard before the existing timestamp comparisons. Preserve the current
rejection and logging behavior for invalid claims.
🤖 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.

Outside diff comments:
In `@packages/shared/src/entitlements.ts`:
- Around line 182-206: Update getValidOnlineLicense to bind the verified
assertion’s licenseId to the persisted License row before accepting
entitlements, using the existing database field or enforce the documented
one-row-per-install invariant if that is the established contract. Add the
ALLOW_LEGACY_UNSIGNED_ONLINE_LICENSES fallback for licenses without
licenseAssertion, preserving unsigned entitlements only when the flag is
enabled. Add coverage in entitlements.test.ts for cross-license assertion replay
and the enabled/disabled unsigned-license behavior.

---

Nitpick comments:
In `@packages/shared/src/entitlements.ts`:
- Around line 66-113: Update verifyOnlineLicenseAssertion to explicitly reject
invalid date parsing by checking Number.isNaN(issuedAt) or
Number.isNaN(expiresAt) in the license-claims validation guard before the
existing timestamp comparisons. Preserve the current rejection and logging
behavior for invalid claims.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e611b56-5878-4926-a773-424ec7f44ba7

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (21)
  • packages/backend/package.json
  • packages/db/prisma/schema.prisma
  • packages/shared/package.json
  • packages/shared/src/entitlements.test.ts
  • packages/shared/src/entitlements.ts
  • packages/shared/src/index.client.ts
  • packages/shared/src/index.server.ts
  • packages/shared/src/lighthouseTypes.ts
  • packages/web/src/app/(app)/settings/license/recentInvoicesCard.tsx
  • packages/web/src/app/(app)/settings/license/types.ts
  • packages/web/src/app/api/(client)/client.ts
  • packages/web/src/ee/features/lighthouse/actions.ts
  • packages/web/src/features/billing/CLAUDE.md
  • packages/web/src/features/billing/client.ts
  • packages/web/src/features/billing/planComparisonTable.tsx
  • packages/web/src/features/billing/servicePing.ts
  • packages/web/src/features/billing/systemInfo.ts
  • packages/web/src/features/billing/upsellDialog.tsx
  • packages/web/src/initialize.test.ts
  • packages/web/src/initialize.ts
  • packages/web/src/instrumentation.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/prisma/schema.prisma
  • packages/web/src/features/billing/servicePing.ts

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