ReviewOS

stacks/uplink

fix(app): license.json is unversioned, so an old file is reinterpreted rather than refused

#45
Closed glennmichael123 opened this 9 days ago ·
9 days ago

license.json is a persisted artifact with no version and almost no validation, read by a build that assumes its own shape. That is the same class as #9, on the file that now decides whether Uplink answers at all.

It has not broken yet because LicenseState has existed for one release (f326bde), which is exactly when it is cheap to fix.

What is there today

  • The state is cached at ~/Library/Application Support/Uplink/license.json (app/Uplink/license.ts#L28).
  • readLicenseState casts whatever parses to LicenseState after checking one thing, typeof state.checkedAt === 'number' (app/Uplink/license.ts#L159-L169). Every other field is trusted.
  • plan is typed as a three-value union but never checked against it, so describeLicense renders whatever string the file holds.
  • isLicensed needs only valid and freshness (app/Uplink/license.ts#L49-L51), and the offline grace is 14 days (app/Uplink/license.ts#L24).
  • It is read at app/Uplink/desktop-agent.ts#L185 and written in three places.

The benign direction is covered by accident: a field a future build adds reads as undefined, and refreshLicense() runs at launch, so an online Mac re-derives the truth in seconds.

The other direction is not. A { valid: true, checkedAt: <recent> } written by any build, or typed into the file by hand, entitles that Mac for 14 days, and nothing in the file says which build's meaning of valid it was written under. The day the shape changes, an old file is reinterpreted rather than rejected.

Proposal

  1. Write a version alongside the state in writeLicenseState.
  2. In readLicenseState, return null for any file whose version this build does not understand. Returning null costs nothing: it falls through to the launch-time refreshLicense(), which asks the server.
  3. While there, check valid is a boolean and plan is one of the three literals or null.

Requirements

  • Rejecting is free, reinterpreting is not. A null read means one network call. A misread means a wrong entitlement for two weeks.
  • Do not make a version mismatch user-visible. An online Mac should never notice; only a Mac that is offline at the moment it upgrades sees a difference, and that is the correct time to ask.
  • This is not a licence-enforcement change. The grace period and the checks stay as they are.

Acceptance

  • A test writes a license.json with an unknown version and asserts readLicenseState returns null.
  • A test writes one with a non-boolean valid or an unrecognized plan and asserts the same.
  • A test asserts a file written by writeLicenseState reads back unchanged.

Sign in to comment on this issue.