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). readLicenseStatecasts whatever parses toLicenseStateafter checking one thing,typeof state.checkedAt === 'number'(app/Uplink/license.ts#L159-L169). Every other field is trusted.planis typed as a three-value union but never checked against it, sodescribeLicenserenders whatever string the file holds.isLicensedneeds onlyvalidand 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#L185and 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
- Write a
versionalongside the state inwriteLicenseState. - In
readLicenseState, returnnullfor any file whose version this build does not understand. Returning null costs nothing: it falls through to the launch-timerefreshLicense(), which asks the server. - While there, check
validis a boolean andplanis 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.jsonwith an unknown version and assertsreadLicenseStatereturns null. - A test writes one with a non-boolean
validor an unrecognizedplanand asserts the same. - A test asserts a file written by
writeLicenseStatereads back unchanged.