# Local Pro Entitlement Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Verify backend-bound Pro certificates locally and enforce one free or five Pro execution accounts without exposing purchase data.

**Architecture:** A security store owns an installation Ed25519 key, short-lived challenges, and the last verified public entitlement claims. Pure policy functions calculate account capacity and execution access. Account creation, session execution, and safe account projections consume that single policy. Purchase tokens never reach this layer.

**Tech Stack:** Node.js 22 ESM, built-in `crypto`, `node:test`, existing backend HTTP/router patterns.

## Global Constraints

- Preserve all AGENTS.md security and account ownership invariants.
- Count only configured accounts where `historyOnly !== true`.
- Free allows the first configured execution account; Pro allows the first five in config order.
- Excess existing accounts remain readable and are never deleted, but cannot execute.
- Persist only the compact signed certificate and normalized public claims; never a purchase token.
- Before every task run `git status --short`; preserve unrelated changes and commit only task hunks.

---

### Task 1: Pure certificate verification and account policy

**Files:**
- Create: `apps/backend/src/entitlement-token.js`
- Create: `apps/backend/src/entitlement-token.test.js`
- Create: `apps/backend/src/entitlement-policy.js`
- Create: `apps/backend/src/entitlement-policy.test.js`

- [ ] Write token tests for valid Ed25519 signatures and rejection of an unknown `kid`, wrong
  `aud`, product other than `pro_multi_account`, `maxAccounts !== 5`, mismatched backend-key
  thumbprint, mismatched challenge hash, future `iat`, and expired `exp`.
- [ ] Write policy tests for free 1-account access, Pro 5-account access, exclusion of
  `historyOnly`, deterministic locking in config order, 30-day active state, 7-day `grace`, and
  expiration after grace.
- [ ] Run `npm.cmd test -- apps/backend/src/entitlement-token.test.js apps/backend/src/entitlement-policy.test.js` and confirm failure because the modules do not exist.
- [ ] Implement these exact public interfaces:

```js
verifyEntitlementToken(token, {
  publicKeys,
  backendKeyThumbprint,
  challengeHash,
  now = Date.now(),
})

getPublicEntitlementStatus({ certificate, accounts, now = Date.now() })
getAccountExecutionAccess({ accountId, accounts, entitlement })
assertCanAddExecutionAccount({ accounts, entitlement })
```

  `verifyEntitlementToken` returns normalized claims only after strict header, claim, time, and
  signature checks. Policy assertion errors expose only `status`, `code`, and safe `message`.
- [ ] Run the focused tests and confirm all pass.
- [ ] Commit with `feat(backend): add Pro entitlement policy`.

### Task 2: Security store, installation binding, and challenge lifecycle

**Files:**
- Create: `apps/backend/src/entitlement-store.js`
- Create: `apps/backend/src/entitlement-store.test.js`
- Modify: `apps/backend/src/application-data-reset.test.js`

- [ ] Write tests proving initialization creates an Ed25519 installation key under
  `dataDir/security`, permissions and atomic replacement follow `device-auth-store.js`, public
  status never contains private key material, and restart preserves the same public-key
  thumbprint.
- [ ] Add challenge tests: opaque random challenge, SHA-256 hash persisted, five-minute expiry,
  one-time consumption, certificate activation bound to both challenge and installation key, and
  raw challenges absent from the persisted record.
- [ ] Add reset coverage proving the existing full security reset removes entitlement state and a
  later Android restore can activate a new certificate.
- [ ] Run `npm.cmd test -- apps/backend/src/entitlement-store.test.js apps/backend/src/application-data-reset.test.js` and confirm the new tests fail.
- [ ] Implement:

```js
createEntitlementStore({ dataDir, publicKeys, accountsProvider, now, randomBytes })
// initialize(), getStatus(), issueChallenge(), activate({ challenge, certificate }),
// getAccountExecutionAccess(accountId), assertCanAddExecutionAccount()
```

  Store `security/entitlement.json` with schema version, certificate, normalized claims, and
  consumed challenge hashes. Cap expired challenge records and use the existing serialized/atomic
  write pattern.
- [ ] Run focused tests and confirm all pass.
- [ ] Commit with `feat(backend): persist bound Pro entitlement`.

### Task 3: Safe entitlement HTTP API

**Files:**
- Create: `apps/backend/src/entitlement-routes.js`
- Create: `apps/backend/src/entitlement-routes.test.js`
- Modify: `apps/backend/src/index.js`
- Modify: `apps/backend/src/index.test.js`

- [ ] Write route tests for `GET /api/entitlement`, `POST /api/entitlement/challenges`, and
  `POST /api/entitlement/activate`, including malformed JSON, request-size limits, expired/replayed
  challenges, invalid certificates, and safe redacted errors.
- [ ] Assert public responses contain only `tier`, `state`, `maxAccounts`, `configuredAccounts`,
  `expiresAt`, `graceEndsAt`, and for a challenge only `challenge` plus
  `backendKeyThumbprint`; never certificate text, paths, keys, or verifier errors.
- [ ] Run `npm.cmd test -- apps/backend/src/entitlement-routes.test.js apps/backend/src/index.test.js` and confirm expected failures.
- [ ] Implement a focused route factory and initialize one entitlement store in `index.js` after
  config loading. Follow existing response helpers and body limits; keep business logic out of
  `index.js`.
- [ ] Run focused tests and confirm all pass.
- [ ] Commit with `feat(backend): expose safe entitlement API`.

### Task 4: Enforce capacity during account creation

**Files:**
- Modify: `apps/backend/src/account-login-attempts.js`
- Modify: `apps/backend/src/account-login-attempts.test.js`
- Modify: `apps/backend/src/accounts.js`
- Modify: `apps/backend/src/accounts.test.js`
- Modify: `apps/backend/src/index.js`

- [ ] Add tests that a free user receives `402 pro_required` before starting a second account login,
  while a Pro user can start accounts two through five and receives `409 account_limit_reached` for
  the sixth.
- [ ] Add race coverage proving the final `persistCreatedAccount` check runs inside the existing
  `serializeConfigMutation`; two concurrent completions must not exceed the limit.
- [ ] Prove a duplicate authenticated identity returns the existing account without consuming a
  slot, and `historyOnly` sources do not consume a slot.
- [ ] Run the focused account tests and confirm they fail for missing enforcement.
- [ ] Inject `assertCanAddExecutionAccount` into login-attempt creation and account persistence.
  Perform the early check for UX and the serialized final check for correctness.
- [ ] Run focused tests and confirm all pass.
- [ ] Commit with `feat(backend): enforce Pro account capacity`.

### Task 5: Lock excess accounts at every execution boundary

**Files:**
- Modify: `apps/backend/src/accounts.js`
- Modify: `apps/backend/src/accounts.test.js`
- Modify: `apps/backend/src/sessions.js`
- Modify: `apps/backend/src/sessions.test.js`
- Modify: `apps/backend/src/index.js`

- [ ] Test safe account projections add only `entitlementLocked: true` for excess accounts and omit
  or set it false for usable accounts. Verify history-only rows and unified chat history remain
  readable.
- [ ] Add session tests for create, continue/send, capabilities, queue dispatch, steering, and
  cross-account handoff. Every execution path for a locked account must fail with `402
  pro_required` before an app-server process starts or receives a request.
- [ ] Test downgrade/refund behavior: configured accounts and logical chats remain present; config
  order chooses the one usable free account; selecting a locked account does not silently change
  the selected account.
- [ ] Run focused backend tests and confirm failures.
- [ ] Extend `initializeSessions` with a synchronous `getAccountExecutionAccess` dependency and call
  it from the existing central availability assertion. Pass the same policy into safe account
  projection.
- [ ] Run focused tests and confirm all pass.
- [ ] Commit with `feat(backend): lock accounts outside entitlement`.

### Task 6: Local integration gate

**Files:**
- Modify if assertions require it: `config.example.json`
- Modify: `docs/PRE_RELEASE_CHECKLIST.md`

- [ ] Add or update integration fixtures for one free execution account, five Pro accounts, a sixth
  rejection, `historyOnly` exclusion, restart persistence, expired certificate, grace, and refund
  downgrade without deletion.
- [ ] Run `npm.cmd test`; record pass/fail/skip counts.
- [ ] Run `npm.cmd --workspace apps/pwa exec vite build`; require exit code 0.
- [ ] Run `git diff --check`; require exit code 0.
- [ ] Confirm logs and serialized public session/account data contain no certificate, challenge,
  installation private key, purchase token, credential, or raw path.
- [ ] Update the checklist with concise evidence and advance only to the verifier plan.
- [ ] Commit with `test: verify local Pro entitlement enforcement`.
