ReviewOS

also looking at this

stacks/uplink

fix(app): refuse a license cache this build cannot vouch for

#47
Merged glennmichael123 wants to merge fix/license-state-version into main
2 files +158 -7
9 days ago

Closes #45. The third file in the same family as #9 and #44, and the one that decides whether Uplink answers at all.

What was broken

license.json entitles a Mac for 14 days without reaching the server, and readLicenseState checked one thing: that checkedAt is a number. Everything else was cast and trusted, and nothing in the file recorded which build's meaning it carried.

So { "valid": true, "checkedAt": <recent> }, typed in by hand or left behind by a build whose fields meant something else, was believed for a fortnight. plan is typed as a three-value union and was never checked against it, so describeLicense rendered whatever string was in the file.

What it does

The file leads with a version, and a version this build does not understand is refused rather than read. Refusing is cheap: the key is in the Keychain, the launch asks the server anyway, and an online Mac never notices. Every field is checked too, because a file that is wrong about any of them was not written by this build, so none of it is evidence.

Measured, end to end:

file on diskresult
what 0.1.4 wrote, no versionaccepted, and still licensed offline
{"valid": true, "checkedAt": <now>}refused
{"version": 2, ...} from a newer buildrefused
written by this buildaccepted

The exception that matters

0.1.4 shipped before the version existed, so anyone who activated already has an unstamped file. The shape has not changed since, so an unstamped file is this version and is accepted.

Refusing them would have been the tidier rule and the wrong one. It would have asked exactly the wrong people to go and find an internet connection: the ones on a boat, which is what the cache exists for. That is the first row of the table above, and there is a test holding it.

One thing on the write side

checkLicenseKey took body.plan ?? null from the server unchecked. A plan name this build does not know would have been stored and then refused on the next read, costing the Mac its cache for no reason. It is validated on the way in now, so what gets written always reads back, which is the same rule #44 applied to settings.json.

Verification

  • 128 unit tests pass, 6 new here
  • a round-trip test, so a file the app itself wrote always reads back
  • ./buddy lint and ./buddy typecheck clean
  • bun build --compile still produces a binary

Not in scope

This changes nothing about enforcement: the 14 day grace, the recheck interval, and what counts as valid are all as they were.

🤖 Generated with Claude Code

2 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.