Skip to content

fix: verify signed online license entitlements#1442

Merged
brendan-kellam merged 5 commits into
mainfrom
brendan/fix-SOU-1465
Jul 15, 2026
Merged

fix: verify signed online license entitlements#1442
brendan-kellam merged 5 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 licensing and entitlement enforcement; misconfiguration or Lighthouse rollout timing could strip paid features until assertions sync successfully.

Overview
Online EE licensing now requires a cryptographically verified Lighthouse assertion to grant paid features, instead of trusting mutable License columns (status, entitlements, lastSyncAt).

The PR adds licenseAssertion on License, implements verifyOnlineLicenseAssertion (signature, audience/version, installId, issuance/expiry, max 7-day lifetime), and changes getValidOnlineLicense so unsigned rows never grant online entitlements. Entitlements and active status come from the signed snapshot; invalid or missing assertions fail closed with no fallback to DB fields.

Service ping verifies any returned assertion before persistence, writes license fields from the verified payload, and errors if Lighthouse sends a bad assertion or assertion without license data. Startup runs an initial syncWithLighthouse before the daily ping cron (failures are logged/Sentry but do not block boot).

Lighthouse Zod schemas (onlineLicenseSnapshotSchema, assertion claims, ping response with optional licenseAssertion) move into packages/shared/src/lighthouseTypes.ts; web billing imports shift to @sourcebot/shared. Offline license keys keep prior behavior.

Reviewed by Cursor Bugbot for commit a672a09. 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 online license assertions are successfully authenticated and validated.
    • Invalid, expired, canceled, or mismatched assertions no longer unlock paid features; unsigned assertions are not trusted.
    • Background license syncing now verifies assertions and persists the verified assertion data.
  • New Features
    • Added support for storing and validating signed online license assertions.
  • Tests
    • Expanded coverage for assertion verification, entitlement behavior, and startup license-sync error recovery.

@github-actions

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b843965-7032-44c8-a883-dafd5317e01a

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6e8b8 and a672a09.

📒 Files selected for processing (1)
  • CHANGELOG.md

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 and concisely summarizes the main change: verifying signed online license entitlements.
✨ Finishing Touches
🧪 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
Comment thread packages/shared/src/entitlements.ts
Comment thread packages/web/src/initialize.ts

@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

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

License Audit

⚠️ Status: PASS

Metric Count
Total packages 2222
Resolved (non-standard) 17
Unresolved 0
Strong copyleft 0
Weak copyleft 38

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
lightningcss 1.32.0 MPL-2.0
lightningcss-android-arm64 1.32.0 MPL-2.0
lightningcss-darwin-arm64 1.32.0 MPL-2.0
lightningcss-darwin-x64 1.32.0 MPL-2.0
lightningcss-freebsd-x64 1.32.0 MPL-2.0
lightningcss-linux-arm-gnueabihf 1.32.0 MPL-2.0
lightningcss-linux-arm64-gnu 1.32.0 MPL-2.0
lightningcss-linux-arm64-musl 1.32.0 MPL-2.0
lightningcss-linux-x64-gnu 1.32.0 MPL-2.0
lightningcss-linux-x64-musl 1.32.0 MPL-2.0
lightningcss-win32-arm64-msvc 1.32.0 MPL-2.0
lightningcss-win32-x64-msvc 1.32.0 MPL-2.0
Resolved Packages (17)
Package Version Original Resolved Source
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 GitHub repo (livebook-dev/codemirror-lang-elixir LICENSE)
khroma 2.1.0 UNKNOWN MIT GitHub repo (fabiospampinato/khroma)
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 GitHub repo (livebook-dev/lezer-elixir LICENSE)
map-stream 0.1.0 UNKNOWN MIT GitHub repo (dominictarr/map-stream)
memorystream 0.3.1 UNKNOWN MIT npm registry (licenses array: {type:MIT})
valid-url 1.0.9 UNKNOWN MIT GitHub repo (ogt/valid-url LICENSE file)
posthog-js 1.369.0 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo (PostHog/posthog-js LICENSE file, primary license Apache-2.0)
pause-stream 0.0.11 ["MIT","Apache2"] (MIT OR Apache-2.0) extracted from object (license array [MIT, Apache2])
@sentry/cli 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-darwin 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-linux-arm 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-linux-arm64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-linux-i686 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-linux-x64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-win32-arm64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-win32-i686 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)
@sentry/cli-win32-x64 2.58.5 FSL-1.1-MIT FSL-1.1-MIT npm registry / repo (Functional Source License 1.1, MIT Future License; non-SPDX but explicitly named)

@brendan-kellam brendan-kellam merged commit af6f1e6 into main Jul 15, 2026
9 of 10 checks passed
@brendan-kellam brendan-kellam deleted the brendan/fix-SOU-1465 branch July 15, 2026 23:25

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

Reviewed by Cursor Bugbot for commit a672a09. Configure here.

return {
entitlements: assertion.license.entitlements.filter(isKnownEntitlement),
status: assertion.license.status,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Staleness banners mismatch assertions

Medium Severity

The new online license enforcement relies on the signed assertion's expiresAt, but staleness banners still use lastSyncAt. This mismatch can cause the UI to incorrectly show an enforced outage while entitlements are active, or fail to warn when entitlements have actually expired.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a672a09. Configure here.

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