Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ VITE_PIMLICO_API_KEY=op://GreenGoods/VITE_PIMLICO_API_KEY/credential
# @public
VITE_PASSKEY_RP_ID=

# Hosted Pimlico passkey server path. Keep false by default until QA approves
# staging evidence for account recovery.
# @public
VITE_PASSKEY_SERVER_ENABLED=false

# @public
# @type=number
VITE_CHAIN_ID=42161
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,71 @@ Implement Pimlico passkey-server-first auth continuity in `packages/shared`.
- Run `node scripts/harness/plan-hub.mjs validate`.
- Run `bun run build:shared`.

## 2026-06-09 Codex Result

### Implemented

- Added shared Pimlico hosted passkey-server helpers in `packages/shared/src/config/passkeyServer.ts`:
- `VITE_PASSKEY_SERVER_ENABLED` gate remains default-off.
- `createPasskeyServerClient(chainId)` uses the existing Pimlico bundler URL/API-key config.
- `normalizePasskeyAccountIdentifier()` and `buildPasskeyRecoveryContext()` centralize username/ENS-handle context.
- `classifyPasskeyCeremonyContext()` classifies unsupported browser/origin/RP contexts without starting a ceremony.
- Updated `packages/shared/src/workflows/authServices.ts`:
- flag-on registration calls `startRegistration({ context })`, `createWebAuthnCredential()`, `verifyRegistration({ credential, context })`, then rebuilds the existing Kernel smart account from the verified credential.
- flag-on recovery calls `getCredentials({ context })`, `startAuthentication()` with no context, WebAuthn `navigator.credentials.get()`, `verifyAuthentication({ raw, uuid })`, then rebuilds the smart account.
- local credential cache remains the flag-off path and the fallback when the server has no credential or is unavailable.
- server/network failure does not clear credential, username, auth mode, or expected-address metadata.
- expected smart-account address is cached and recovered server metadata fails closed on mismatch.
- Updated auth storage/provider behavior:
- regular `signOut()` keeps username, credential, and expected smart-account address for same-device fallback.
- explicit `clearPasskey()` clears credential, username, and expected smart-account address.
- Updated auth telemetry payloads to source/outcome/reason codes only; no username, credential ID, wallet address, or smart-account address.
- Updated client login recovery UI:
- missing local cache shows username/ENS recovery before lookup.
- failed recovery shows retry/fallback guidance and a guarded separate-account confirmation before new account creation.
- unsupported browser/in-app-browser guidance blocks passkey ceremonies before starting registration/login.
- one-tap passkey login remains for users with a local credential.
- new strings added to `en`, `es`, and `pt`.
- Updated `.env.template` with `VITE_PASSKEY_SERVER_ENABLED=false` and refreshed stale onboarding/sequence docs away from IndexedDB/ENS recovery claims.

### Installed API Proof

- Verified local `permissionless@0.2.57` exports `createPasskeyServerClient` from `permissionless/clients/passkeyServer`.
- Verified installed passkey-server methods:
- `startRegistration({ context })`
- `verifyRegistration({ credential, context })`
- `getCredentials({ context })`
- `startAuthentication()` with no context parameter
- `verifyAuthentication({ raw, uuid })`
- No installed hosted-server import/migration API for existing local-only credentials was found. Legacy local-only credentials remain fallback-only and re-enrollment/migration stays a release risk for QA/support.

### Validation Evidence

- RED: `bun run --cwd packages/shared test -- src/__tests__/workflows/authServices.test.ts` failed before implementation for hosted server registration/login, no-credential fallback, server-unavailable fallback, and address mismatch.
- GREEN: `bun run --cwd packages/shared test -- src/__tests__/config/passkeyServer.test.ts src/__tests__/workflows/authServices.test.ts src/__tests__/modules/session.test.ts src/__tests__/workflows/authMachine.test.ts src/__tests__/hooks/useAuth.test.ts` passed: 5 files, 92 tests.
- GREEN: `bun run --cwd packages/client test -- src/__tests__/views/Login.test.tsx` passed: 18 tests.
- GREEN: `bun run build:shared` passed; shared is source-consumed and has no separate build artifact.
- GREEN: `bun run --cwd packages/client build` passed. Existing Rollup pure-annotation/chunk-size and Browserslist warnings remain.
- GREEN: `bun run lint:vocab`, `bun run check:design-md`, and `bun run check:design-tokens` passed.
- CAVEAT: `bun run check:design-generated` failed on unrelated stale generated artifact `docs/docs/builders/packages/client-pwa-token-audit.generated.md`.
- CAVEAT: `node scripts/harness/plan-hub.mjs validate` failed on unrelated malformed active hub `.plans/active/sentry-stack-observability`; no feature-scoped validate command exists.

### Browser Proof

Using the in-app Browser against `https://127.0.0.1:5173/home/login?presentation=pwa`:

- Default no-local-cache state rendered username/ENS recovery input, synced/legacy passkey guidance, disabled recovery until input, wallet fallback, separate-account link, and address-continuity notice.
- Failed recovery with no local cache and flag-off behavior rendered a recoverable error, retained the typed username, kept retry/fallback actions visible, and exposed guarded separate-account creation without overlap on a 1280x720 viewport.
- Separate-account confirmation rendered explicit copy that the new account uses a different address and will not recover access tied to the previous passkey, with `Continue to new account` and `Back to recovery` actions.

### Remaining QA Risks

- Real synced-passkey recovery still needs staging provider evidence across browser/PWA/platform combinations.
- Production enablement still needs RP/origin and staging/prod passkey-server isolation evidence.
- Address mismatch is unit-simulated; live mismatch proof depends on QA harness capability.
- Legacy local-only hosted-server import/migration is explicitly not implemented or proven.
- `PRD-540` can start QA pass 1 from this handoff; `PRD-541` remains blocked until QA pass 1 completes.

## Boundaries

- Do not add a Green Goods-owned passkey API or database.
Expand Down
52 changes: 27 additions & 25 deletions .plans/active/account-recovery-hardening/plan.todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@

| Requirement | Lane | Planned Step | Status |
|---|---|---|---|
| Server-backed passkey registration/login | `state_api` | Step 1 | todo |
| Staging-first rollout flag | `state_api` | Step 1 | todo |
| Legacy local credential fallback | `state_api`, `ui` | Step 2 | todo |
| Username recovery prompt and guarded new-account path | `ui`, `state_api` | Step 3 | todo |
| Production RP/origin/address gates | `state_api`, `qa_pass_1` | Step 4 | todo |
| Server-backed passkey registration/login | `state_api` | Step 1 | implemented |
| Staging-first rollout flag | `state_api` | Step 1 | implemented |
| Legacy local credential fallback | `state_api`, `ui` | Step 2 | implemented |
| Username recovery prompt and guarded new-account path | `ui`, `state_api` | Step 3 | implemented |
| Production RP/origin/address gates | `state_api`, `qa_pass_1` | Step 4 | classified; live QA pending |
| Kernel/ENS recovery boundary | `state_api` | Step 5 | spike |
| Browser/PWA/reinstall QA matrix | `qa_pass_1`, `qa_pass_2` | Step 6 | blocked |
| Docs/support readiness | `ui`, `state_api`, `qa_pass_2` | Step 7 | todo |
| Docs/support readiness | `ui`, `state_api`, `qa_pass_2` | Step 7 | implemented; QA support review pending |

## Implementation Steps

Expand Down Expand Up @@ -98,6 +98,8 @@
- Verify unsupported/in-app browser contexts do not start a passkey ceremony.
- Verify address mismatch fails closed and does not authenticate as the wrong account.
- Record tested platform/provider combinations, including desktop Chrome, Android Chrome/PWA, iOS Safari/PWA if available, and unsupported/in-app browser negative coverage.
- Run device-matrix passkey ceremonies from a `*.greengoods.app` staging origin (or set `VITE_PASSKEY_RP_ID` to the test host). Cloudflare-tunnel and other non-RP domains are blocked by design (`rp_origin_mismatch`) — record those as logistics constraints, not defects.
- When simulating "passkey server unavailable", include HTTP 5xx and timeout shapes (not only network-off/fetch failures); the legacy fallback must engage for all of them.

### Step 7: Clean Up Docs And Support Surfaces

Expand All @@ -110,24 +112,24 @@

### UI (`claude/ui/account-recovery-hardening`)

- [ ] Add recovery/fallback copy and i18n.
- [ ] Show legacy local-only re-enrollment guidance without blocking same-device login.
- [ ] Avoid overclaiming recovery when provider sync is unavailable.
- [x] Add recovery/fallback copy and i18n.
- [x] Show legacy local-only re-enrollment guidance without blocking same-device login.
- [x] Avoid overclaiming recovery when provider sync is unavailable.
- [ ] Write `handoffs/claude-ui.md`.

### State / API (`codex/state-api/account-recovery-hardening`)

- [ ] Add shared Pimlico passkey-server client.
- [ ] Add and respect `VITE_PASSKEY_SERVER_ENABLED`.
- [ ] Centralize normalized username/ENS-handle context construction.
- [ ] Replace local-only registration/login with server-backed first path.
- [ ] Keep local credential fallback and failure-safe storage behavior.
- [ ] Prove or explicitly defer legacy credential server migration/import.
- [ ] Prove canonical RP/origin and same-address continuity for server-backed login.
- [ ] Add privacy-safe recovery telemetry or redact reused auth telemetry for recovery paths.
- [ ] Document exact installed `permissionless` passkey-server API usage and any upgrade requirement.
- [ ] Add focused auth service/machine tests.
- [ ] Write `handoffs/codex-state-api.md`.
- [x] Add shared Pimlico passkey-server client.
- [x] Add and respect `VITE_PASSKEY_SERVER_ENABLED`.
- [x] Centralize normalized username/ENS-handle context construction.
- [x] Replace local-only registration/login with server-backed first path.
- [x] Keep local credential fallback and failure-safe storage behavior.
- [x] Prove or explicitly defer legacy credential server migration/import.
- [x] Prove canonical RP/origin and same-address continuity for server-backed login.
- [x] Add privacy-safe recovery telemetry or redact reused auth telemetry for recovery paths.
- [x] Document exact installed `permissionless` passkey-server API usage and any upgrade requirement.
- [x] Add focused auth service/machine tests.
- [x] Write `handoffs/codex-state-api.md`.

### Contracts (`n/a`)

Expand All @@ -151,8 +153,8 @@

## Validation

- [ ] `node scripts/harness/plan-hub.mjs validate`
- [ ] Targeted shared auth tests, for example `bun run --cwd packages/shared test -- src/__tests__/workflows/authServices.test.ts src/__tests__/workflows/authMachine.test.ts`
- [ ] `bun run build:shared`
- [ ] Manual passkey QA matrix evidence in `reports/`
- [ ] Docs/support cleanup evidence in `reports/` or lane handoffs
- [ ] `node scripts/harness/plan-hub.mjs validate` — failed on unrelated `.plans/active/sentry-stack-observability` hub shape; no feature-scoped validate command exists.
- [x] Targeted shared auth tests: `bun run --cwd packages/shared test -- src/__tests__/workflows/authServices.test.ts src/__tests__/modules/session.test.ts src/__tests__/workflows/authMachine.test.ts src/__tests__/hooks/useAuth.test.ts`
- [x] `bun run build:shared`
- [ ] Manual passkey QA matrix evidence in `reports/` — live provider/PWA matrix remains for `PRD-540`.
- [x] Docs/support cleanup evidence in `handoffs/codex-state-api.md`
66 changes: 53 additions & 13 deletions .plans/active/account-recovery-hardening/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"overall_status": "active",
"priority": "p1",
"created_at": "2026-05-24T04:21:07.501Z",
"updated_at": "2026-05-24T04:59:58Z",
"updated_at": "2026-06-10T01:45:00Z",
"target_date": "2026-06-10"
},
"links": {
Expand Down Expand Up @@ -79,16 +79,16 @@
"handoff": "handoffs/claude-ui.md",
"tdd": {
"mode": "required",
"status": "pending",
"status": "green_recorded",
"red": {
"command": "",
"evidence": ""
"command": "bun run --cwd packages/client test -- src/__tests__/views/Login.test.tsx",
"evidence": "RED during recovery-first UI implementation: 3 failures caught stale address-continuity default copy and no-passkey recovery error classification."
},
"green": {
"command": "",
"evidence": ""
"command": "bun run --cwd packages/client test -- src/__tests__/views/Login.test.tsx",
"evidence": "GREEN after UI fixes: Login view recovery tests passed, 18 tests total."
},
"note": "UI lane should prove recovery/fallback copy and i18n; provider-sync behavior may require manual QA evidence."
"note": ""
},
"skill_tags": ["ui", "auth", "i18n", "recovery"]
},
Expand All @@ -100,16 +100,16 @@
"handoff": "handoffs/codex-state-api.md",
"tdd": {
"mode": "required",
"status": "pending",
"status": "green_recorded",
"red": {
"command": "",
"evidence": ""
"command": "bun run --cwd packages/shared test -- src/__tests__/workflows/authServices.test.ts",
"evidence": "RED before implementation: hosted server registration/login, server no-credential fallback, server-unavailable fallback, and address-mismatch recovery tests failed (5 failures)."
},
"green": {
"command": "",
"evidence": ""
"command": "bun run --cwd packages/shared test -- src/__tests__/config/passkeyServer.test.ts src/__tests__/workflows/authServices.test.ts src/__tests__/modules/session.test.ts src/__tests__/workflows/authMachine.test.ts src/__tests__/hooks/useAuth.test.ts",
"evidence": "GREEN after implementation: passkey-server config plus shared auth/session/machine/hook tests passed, 5 files and 92 tests total."
},
"note": "Target authServices/authMachine tests for server-backed register/login, legacy fallback, and failure-safe storage."
"note": ""
},
"skill_tags": ["react", "xstate", "permissionless", "pimlico", "passkey"]
},
Expand Down Expand Up @@ -220,6 +220,46 @@
"status": "linear_synced",
"branch": null,
"note": "Updated PRD-521, PRD-537, PRD-538, PRD-540, and PRD-541 with production readiness gates."
},
{
"timestamp": "2026-06-09T02:03:59.842Z",
"actor": "codex",
"lane": "state_api",
"status": "tdd_recorded",
"branch": "codex/state-api/account-recovery-hardening",
"note": "Recorded RED/GREEN TDD proof"
},
{
"timestamp": "2026-06-09T02:04:28.712Z",
"actor": "codex",
"lane": "ui",
"status": "tdd_recorded",
"branch": "claude/ui/account-recovery-hardening",
"note": "Recorded RED/GREEN TDD proof"
},
{
"timestamp": "2026-06-09T02:04:56.227Z",
"actor": "codex",
"lane": "qa_pass_1",
"status": "ready",
"branch": "claude/qa-pass-1/account-recovery-hardening",
"note": "State/API and UI recovery dependencies have green targeted validation and browser proof; ready for PRD-540 QA matrix execution."
},
{
"timestamp": "2026-06-09T02:11:57.885Z",
"actor": "codex",
"lane": "state_api",
"status": "tdd_recorded",
"branch": "codex/state-api/account-recovery-hardening",
"note": "Recorded RED/GREEN TDD proof"
},
{
"timestamp": "2026-06-10T01:45:00Z",
"actor": "claude",
"lane": "state_api",
"status": "review_fixes_applied",
"branch": "codex/account-recovery-hardening",
"note": "Production-readiness review fixes: lookup-phase server failures (viem HTTP/timeout shapes) now reach legacy local fallback, registration no longer dead-ends on stale expected-address state, session restore fails closed on address drift, one-tap login no longer fabricates a placeholder username, and returning users gained a guarded username-recovery entry. Targeted shared (103) + client (31) tests green; PR opened for CI gate."
}
]
}
24 changes: 21 additions & 3 deletions docs/docs/builders/architecture/sequence-diagrams.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,23 +218,31 @@ The three-way split is configured per garden in basis points (bps):

## Passkey onboarding flow

New gardeners onboard using WebAuthn passkeys -- no seed phrases, no browser extensions. The passkey creates a smart account (ERC-4337) that serves as the gardener's on-chain identity.
New gardeners onboard using WebAuthn passkeys -- no seed phrases, no browser extensions. The passkey creates a smart account (ERC-4337) that serves as the gardener's on-chain identity. When `VITE_PASSKEY_SERVER_ENABLED=true`, registration and lookup use Pimlico's hosted passkey server with a normalized username or ENS-handle context; with the flag off, Green Goods stays on the legacy local-only passkey path.

```mermaid
sequenceDiagram
actor G as New Gardener
participant PWA as Client PWA
participant PKS as Pimlico Passkey Server
participant WebAuthn as WebAuthn API
participant Bundler as ERC-4337 Bundler
participant Factory as Account Factory
participant Chain as On-Chain

G->>PWA: Tap "Join Garden"
opt Hosted passkey server enabled
PWA->>PKS: startRegistration(context)
end
PWA->>WebAuthn: navigator.credentials.create()
WebAuthn-->>PWA: PublicKeyCredential (passkey)
opt Hosted passkey server enabled
PWA->>PKS: verifyRegistration(credential, context)
PKS-->>PWA: credential id + public key
end

PWA->>PWA: Derive smart account address (CREATE2)
PWA->>PWA: Store credential ID in IndexedDB
PWA->>PWA: Cache credential metadata, username, RP ID, expected address in localStorage

Note over PWA,Bundler: First transaction sponsors gas

Expand All @@ -250,8 +258,17 @@ sequenceDiagram
Note over G,Chain: Subsequent transactions

G->>PWA: Submit work
opt Hosted passkey server enabled and local cache missing
PWA->>PKS: getCredentials(context)
PKS-->>PWA: credential id + public key
PWA->>PKS: startAuthentication()
end
PWA->>WebAuthn: navigator.credentials.get()
WebAuthn-->>PWA: Signed assertion
opt Hosted passkey server enabled
PWA->>PKS: verifyAuthentication(assertion)
PWA->>PWA: Fail closed if rebuilt address mismatches expected address
end
PWA->>Bundler: UserOperation (signed by passkey)
Bundler->>Chain: Execute via smart account
```
Expand All @@ -261,7 +278,8 @@ sequenceDiagram
- **No wallet required**: Gardeners authenticate with device biometrics (fingerprint, face) via WebAuthn.
- **Counterfactual deployment**: The smart account address is derived from the passkey public key before deployment. The account is only deployed on-chain with the first real transaction.
- **Gas sponsorship**: The first transaction uses a paymaster so new users do not need to hold ETH.
- **Credential storage**: The passkey credential ID is stored locally in IndexedDB and (optionally) synced to ENS text records for cross-device recovery.
- **Credential storage and recovery**: LocalStorage caches the credential metadata, normalized username, RP ID, and expected smart-account address. With `VITE_PASSKEY_SERVER_ENABLED=true`, Pimlico's hosted passkey server is the lookup source for storage-loss recovery; local cache remains the legacy same-device fallback.
- **Rollback**: Set `VITE_PASSKEY_SERVER_ENABLED=false` to keep legacy local-only passkey behavior.

---

Expand Down
Loading
Loading