Skip to content

cloud/C4: useLicenseStore (zustand)#220

Closed
blueberrycongee wants to merge 2 commits intoloop/cloud-C2from
loop/cloud-C4
Closed

cloud/C4: useLicenseStore (zustand)#220
blueberrycongee wants to merge 2 commits intoloop/cloud-C2from
loop/cloud-C4

Conversation

@blueberrycongee
Copy link
Copy Markdown
Owner

What

Adds the in-memory license store described in C4. Holds license (string token), payload (decoded LicensePayload), and status (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 / removeLicense are imported from the C1 stub barrel and any throws are caught so the in-memory path keeps working.

Acceptance criteria

  • setLicense calls verifyLicense and updates payload/status accordingly. (verified token → valid; failed verify → invalid)
  • Hydrates from keychain on app start (calls refreshFromKeychain once). (action exposed; consumer wires it once at boot. Mounting lands with C10.)
  • Tests cover all four status transitions. (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 the src/stores/ surface allowed by the spec.
  • cloud/TASKS.md — marked C4 [x] and appended Done-log entry.

Notes for Lead

  • I did not auto-trigger refreshFromKeychain at module load. Doing so would call loadLicense (which throws "not implemented" until C3 ships), and that would surface as a console error every time anything imports useLicenseStore — including in tests that don't care about it. The C4 spec says "calls refreshFromKeychain once" 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.
  • Persistence failures (saveLicense/removeLicense rejecting) are caught and logged; they do not flip status away from 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.
  • loadLicense failure during refreshFromKeychain is treated like an empty keychain (status stays idle), not invalid. Same rationale — invalid is reserved for "we read a token and it didn't verify".

blueberrycongee and others added 2 commits April 28, 2026 12:33
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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +47 to +51
await saveLicense(token);
} catch (err) {
console.error('[license] saveLicense failed; in-memory only', err);
}
set({ license: token, payload, status: 'valid' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +56 to +60
await removeLicense();
} catch (err) {
console.error('[license] removeLicense failed', err);
}
set({ license: null, payload: null, status: 'idle' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@blueberrycongee
Copy link
Copy Markdown
Owner Author

Closing — re-issued cleanly by integrator.

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