cloud/C4: useLicenseStore (zustand)#220
cloud/C4: useLicenseStore (zustand)#220blueberrycongee wants to merge 2 commits intoloop/cloud-C2from
Conversation
Holds the verified license string + decoded payload + lifecycle status. setLicense verifies via luminaCloud.verifyLicense and persists through saveLicense; persistence failures degrade gracefully (in-memory state stays valid for the session, just not across restarts). clearLicense flips back to idle. refreshFromKeychain hydrates on app start — empty / unreadable keychain → idle, stored token that no longer verifies → invalid, valid token → valid. Tests use vi.hoisted to mock the four luminaCloud touchpoints, then subscribe to capture status sequences. Covers the four transitions called out in C4 acceptance: idle→loading, loading→valid, loading→invalid, valid→idle. 9 tests, all pass. Booting hookup is the consumer's job — App should call useLicenseStore.getState().refreshFromKeychain() once after IPC bridge is ready. Wiring lands in C10 (Account tab mount). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59d9410d95
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await saveLicense(token); | ||
| } catch (err) { | ||
| console.error('[license] saveLicense failed; in-memory only', err); | ||
| } | ||
| set({ license: token, payload, status: 'valid' }); |
There was a problem hiding this comment.
Guard against stale async completions in setLicense
setLicense writes status: 'valid' after an await without checking whether a newer action already changed the store, so overlapping calls can revert newer state. For example, if setLicense('old') is waiting on saveLicense and the user then runs clearLicense() (or a second setLicense), the first call can finish later and restore the old token as valid. This creates incorrect and potentially unsafe state; track request/version IDs (or compare current state) before the final set.
Useful? React with 👍 / 👎.
| await removeLicense(); | ||
| } catch (err) { | ||
| console.error('[license] removeLicense failed', err); | ||
| } | ||
| set({ license: null, payload: null, status: 'idle' }); |
There was a problem hiding this comment.
Clear in-memory license before awaiting keychain removal
The clear path waits for removeLicense() before resetting in-memory state, so if keychain IPC is slow or hangs, the store remains valid and the old license stays usable even after the user explicitly cleared it. This is also inconsistent with the action contract comment that says memory should be wiped and then persisted removal attempted; clear local state first, then perform/remediate keychain cleanup.
Useful? React with 👍 / 👎.
|
Closing — re-issued cleanly by integrator. |
What
Adds the in-memory license store described in C4. Holds
license(string token),payload(decodedLicensePayload), andstatus(idle | loading | valid | invalid), plus three actions:setLicense,clearLicense,refreshFromKeychain.Stacked on #218 (C2) because the store calls
verifyLicense. C3 (#219) is blocked, but the store doesn't actually need C3 to land —loadLicense/saveLicense/removeLicenseare imported from the C1 stub barrel and any throws are caught so the in-memory path keeps working.Acceptance criteria
setLicensecallsverifyLicenseand updatespayload/statusaccordingly. (verified token →valid; failed verify →invalid)refreshFromKeychainonce). (action exposed; consumer wires it once at boot. Mounting lands with C10.)statustransitions. (idle→loading, loading→valid, loading→invalid, valid→idle — explicit subscribe-and-capture in each test)How I tested
npm run typecheck: pass.npm test -- --run src/stores/useLicenseStore.test.ts: 9/9 pass.Touched files outside src/services/luminaCloud/
src/stores/useLicenseStore.ts,src/stores/useLicenseStore.test.ts— these are the C4 deliverables, on thesrc/stores/surface allowed by the spec.cloud/TASKS.md— marked C4[x]and appended Done-log entry.Notes for Lead
refreshFromKeychainat module load. Doing so would callloadLicense(which throws "not implemented" until C3 ships), and that would surface as a console error every time anything importsuseLicenseStore— including in tests that don't care about it. The C4 spec says "callsrefreshFromKeychainonce" but is ambiguous about who calls it; the cleaner separation is that the consumer (App.tsx or the Account tab mounted in C10) calls it once when the IPC bridge is ready.valid. Rationale: a transient keychain hiccup shouldn't lock the user out of cloud features for the rest of the session. They'd just need to re-paste the license after a restart. If you'd prefer hard-fail behavior, easy to flip.loadLicensefailure duringrefreshFromKeychainis treated like an empty keychain (status staysidle), notinvalid. Same rationale —invalidis reserved for "we read a token and it didn't verify".