Skip to content

feat: extract the login orchestration into a host-agnostic package - #1276

Open
claude[bot] wants to merge 4 commits into
mainfrom
claude/cipher-box-1259-orchestration
Open

feat: extract the login orchestration into a host-agnostic package#1276
claude[bot] wants to merge 4 commits into
mainfrom
claude/cipher-box-1259-orchestration

Conversation

@claude

@claude claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Closes #1259. Part of #1253.

The login sequencing — provider credential → API exchange → Core Kit login → secret export → start(secret) — now lives in one host-agnostic package that apps/web imports and that apps/desktop will import unchanged (ADR 0008 D3). v1 drew this boundary at the bearer token and the two hosts drifted; it now sits one step earlier, at credential collection.

What moved to packages/login (@cipherbox/login)

Now in the package Was
flow.ts — the sequencing, the mutex, the restore latch, logout the body of apps/web/src/auth/useAuth.ts
identity.ts — the API identity surface and credential types apps/web/src/auth/identityExchange.ts (moved, unchanged)
secret.ts — login-secret export, hex decode, the transferred handoff to start apps/web/src/engine/loginHandoff.ts
session.ts — the CoreKitSession seam plus the host seams (AccountRecord, SecretRearm, LoginProgress) interfaces in apps/web/src/auth/coreKit.ts
collector.ts — the injected credential-collection interface new

The network map moved with the sequencing on purpose: v1's email-OTP break had one half reading a compile-time API URL and the other a runtime one, which a shared exchange makes unrepresentable.

What stayed in apps/web, and why

  • auth/coreKit.ts — the Web3Auth adapter. It builds the SDK from the Vite build environment and stores through SealedStore over localStorage, IndexedDB and navigator.locks. Construction is host-shaped; only the seam it satisfies is shared.
  • engine/loginHandoff.ts — reduced to LoginSecretSource. Re-export on leader promotion is tab leadership, which blueprint/desktop.md rules out on desktop.
  • stores/auth.store.ts — UI chrome, injected as AccountRecord.
  • auth/useAuth.ts — now a React binding: it supplies the web host's parts and renders the flow's transitions as component state. The Auth interface it returns is byte-for-byte the same, so LoginPage and the login components are untouched.
  • auth/webCollector.ts — new, and the only web-side collection code.

It is deliberately not packages/client: that package is one of the three web-side units and its non-type surface assumes Workers, navigator.locks, BroadcastChannel, Service Workers, IndexedDB and OPFS.

The collector, and how a host offers a subset

interface CredentialCollector<C extends CollectedMaterial = CollectedMaterial> {
  google?(collected: C['google']): Promise<string>;
  email?(collected: C['email']): Promise<EmailAnswer>;
  wallet?(collected: C['wallet']): Promise<WalletProof>;
}

Presence is availability. A host omits a member and the flow refuses that method ("wallet sign-in is not available on this device") before it touches the exchange or the Core Kit; flow.methods and flow.offers() report what is on offer. Desktop omits wallet and can type it never, so the call is unconstructable there rather than present and unable to complete.

C is what this host's UI already holds when it calls. Web collects in the DOM — GIS renders its own button, wagmi signs in the page — so webCollector passes that material through; a host that drives its own flow, such as desktop's loopback OAuth listener, does the work inside the collector instead. Per-method availability lives there too: a build with no VITE_GOOGLE_CLIENT_ID yields a collector with no google member, disabling that one method and nothing else. The sequencing branches on no environment at all.

The facade is a parameter for the same reason: LoginFacade is { start, logout }, satisfied by EngineClient.facade on web and by Tauri IPC on desktop.

How the no-browser-API/no-React rule is enforced

Not by inspection:

  1. packages/login/tsconfig.json sets "lib": ["ES2022"] with no DOM. A browser API does not typecheck, and the Typecheck gate runs it.
  2. src/hostAgnostic.test.ts drives a whole Google login — real exchange over a stubbed fetch, fake session, fake facade — with window, document, navigator, location, localStorage, sessionStorage, indexedDB, caches, BroadcastChannel, Worker and XMLHttpRequest replaced by getters that throw. Touching one fails the test.
  3. The same file asserts import('react') rejects: React is not a declared dependency, so the package's module graph cannot resolve it.

Behaviour is unchanged, and the existing tests say so

apps/web/src/auth/useAuth.test.tsx is unchanged — all 11 cases still pass through the extracted flow, covering the three methods, the SIWE nonce read, logout with and without an engine failure, client rebuild, reload restore, the refused-secret disarm path, the metadata-throw path, the in-flight collision, and secret containment. LoginPage, GoogleLoginButton, EmailLoginForm, WalletLoginButton and their suites are untouched.

Tests that changed, each a move rather than a weakening:

  • apps/web/src/auth/identityExchange.test.tspackages/login/src/identity.test.ts: import path only.
  • apps/web/src/engine/loginHandoff.test.ts: the export/handoff and secret-containment cases moved to packages/login/src/secret.test.ts with the same assertions, taking a LoginFacade where they took an EngineClient. What stayed in web is LoginSecretSource and the origin-storage containment check, which need jsdom.
  • apps/web/src/auth/coreKit.test.ts, src/test/authFakes.tsx: import paths only.

New coverage: packages/login/src/flow.test.ts (sequencing, subset refusal, mutex, resume-once, disarm-on-refusal, logout legs) and apps/web/src/auth/webCollector.test.ts (the missing client ID drops google alone).

CI

The package carries test, typecheck and build scripts, so the recursive Test, Typecheck and Build gates pick its suite up the day it lands, as packages/client does. packages/login/** is added to the web paths filter so Web Bundle and Web E2E Smoke trigger on it.

Run locally, all passing: pnpm test (login 30, client 444, api 227, web 311), pnpm typecheck, pnpm --filter @cipherbox/api typecheck, pnpm lint, pnpm lint:md, pnpm lint:tracker-refs, the Build filter, and pnpm --filter @cipherbox/web build:bundle.

Not verified here

  • The browser and e2e suites (packages/client test:browser, Web E2E Smoke) were not run in this environment; the bundle builds and no packages/client surface changed.
  • No desktop consumer exists yet, so the second host is proven only by the constraints above, not by a build.
  • The API is untouched, so the integration suite was not exercised.

Note

Extract login orchestration into host-agnostic @cipherbox/login package

  • Creates a new packages/login package (@cipherbox/login) that houses the login flow, identity exchange, credential collector interface, and secret handoff logic previously spread across the web app.
  • Implements createLoginFlow as the core host-agnostic orchestrator that sequences credential collection → identity exchange → Core Kit login → secret export → facade start, with serialized in-flight guards, progress reporting, and session resume.
  • Moves createIdentityExchange, isIdentityMethod, exportLoginSecret, and handOffLoginSecret into the shared package; web app now imports these from @cipherbox/login.
  • Adds webCollector and WebCollected in webCollector.ts to supply a web-specific CredentialCollector to the shared flow; Google collection is disabled when no client ID is provided.
  • Rewrites useAuth in useAuth.ts to delegate all sequencing to createLoginFlow, removing local in-flight guards and per-method login assembly; externally visible behavior is functionally equivalent.
  • The tsconfig.json for the login package intentionally omits DOM libs to enforce host-agnostic constraints, enforced by tests in hostAgnostic.test.ts.

Macroscope summarized 8b602d1.

Summary by CodeRabbit

  • New Features

    • Added support for Google, email-code, and wallet sign-in methods.
    • Improved session restoration so returning users can resume authentication automatically.
    • Added clearer authentication progress, error handling, and account state updates.
    • Strengthened protection for login secrets during handoff and failure scenarios.
  • Documentation

    • Updated web and desktop authentication documentation to describe the shared login experience.
  • Tests

    • Expanded coverage for sign-in methods, session recovery, logout, errors, and secret handling.

claude added 2 commits August 12, 2026 16:55
The sequencing — provider credential → API exchange → Core Kit login →
secret export → start(secret) — moves to packages/login, which both hosts
import (ADR 0008 D3). v1 drew this boundary at the bearer token and the two
hosts drifted; the boundary is now credential collection, one step earlier.

Credential collection is injected: CredentialCollector carries one optional
member per method, so a host offers a subset by omitting the rest, and the
flow refuses a method it cannot collect rather than pretending it exists.
Desktop will omit wallet on those terms. The material each collector is
handed is host-shaped — web's UI already holds it when it calls, a host that
drives its own flow does the work inside the collector.

The facade is a parameter too, since the transport differs per host: a WASM
worker facade on web, Tauri IPC on desktop.

The package imports no browser API and no React. tsconfig drops the DOM lib,
so a browser API cannot typecheck; hostAgnostic.test.ts runs a whole login
with the browser globals booby-trapped and asserts React cannot resolve.

apps/web keeps what is web-only: the Web3Auth session construction over
localStorage and IndexedDB, the LoginSecretSource a leader promotion
re-exports through, the auth chrome store, and useAuth, now a React binding
over the shared flow. Its auth suite is unchanged and still passes.
ADR 0008 D3 consequence 2: both host blueprints describe the shared
orchestration, and now that it exists they can name it.
@FSM1
FSM1 marked this pull request as ready for review August 12, 2026 19:22
@FSM1

FSM1 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

@coderabbitai full review please

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

@FSM1: I will perform a complete review of pull request #1276.

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e7ef4ed-d37e-4f56-9e3c-3c75c18bd5ea

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR adds @cipherbox/login as a host-agnostic authentication package. It centralizes provider flows, identity exchange, secret handoff, session lifecycle, and tests. The web app supplies its credential collector and host integrations.

Changes

Shared login package

Layer / File(s) Summary
Login contracts and identity exchange
packages/login/package.json, packages/login/src/{collector,identity,session,index}.ts, packages/login/tsconfig*.json
Defines credential collectors, identity methods, session contracts, identity API exchanges, public exports, and package build configuration.
Secret export and facade handoff
packages/login/src/secret.ts, packages/login/src/secret.test.ts, apps/web/src/engine/loginHandoff.ts, apps/web/src/engine/loginHandoff.test.ts, apps/web/src/engine/introspection.ts
Centralizes hexadecimal secret validation, buffer scrubbing, facade handoff, and related web integration.
Provider login flow and lifecycle
packages/login/src/flow.ts, packages/login/src/flow.test.ts, packages/login/src/testFakes.ts, packages/login/src/hostAgnostic.test.ts
Sequences credential collection, identity exchange, Core Kit login, secret forwarding, account updates, resume, logout, progress, and cleanup.
Web credential and auth integration
apps/web/src/auth/*, apps/web/src/main.tsx, apps/web/src/test/authFakes.tsx, apps/web/package.json, apps/web/tsconfig.json, .github/workflows/ci.yml, blueprint/*.md
Adds the web credential collector, delegates authentication to the shared flow, updates package imports and project references, enables CI coverage, and documents the shared package.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WebAuth
  participant LoginFlow
  participant CredentialCollector
  participant IdentityExchange
  participant CoreKitSession
  participant LoginFacade
  WebAuth->>LoginFlow: start provider login
  LoginFlow->>CredentialCollector: collect credentials
  CredentialCollector-->>LoginFlow: provider material
  LoginFlow->>IdentityExchange: exchange credentials
  IdentityExchange-->>LoginFlow: identity credential
  LoginFlow->>CoreKitSession: log in
  LoginFlow->>LoginFacade: start with exported secret
Loading

Possibly related issues

  • FSM1/cipher-box#1253 — The PR implements shared login orchestration with injected credential collection and wallet support.
  • FSM1/cipher-box#642 — The PR changes web authentication orchestration, Core Kit integration, secret handoff, and introspection.

Suggested reviewers: fsm1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the shared login flow, injected host adapters, web integration, and browser/React isolation required by issue #1259.
Out of Scope Changes check ✅ Passed The CI, package configuration, documentation, tests, and web adapter changes directly support the host-agnostic login extraction.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: extracting login orchestration into a host-agnostic package.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/cipher-box-1259-orchestration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/login/src/secret.test.ts (1)

47-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the odd-length and uppercase-hex decode cases.

Two decoder branches stay untested. 'nothex' has even length and fails on the non-hex character check. SECRET_HEX.slice(2) has even length and fails the 32-byte check. So the odd-length guard in fromHex never runs. The uppercase nibble range in nibble also never runs, because line 165 only asserts that the uppercase string is absent from error text.

Both cases are cheap to add and cover a security-relevant decoder.

As per path instructions for **/*.{test,itest}.ts: "Focus on test coverage, edge cases, and test quality."

💚 Proposed additional cases
   it('rejects a malformed export without echoing it', async () => {
     await expect(exportLoginSecret(exporter('nothex'))).rejects.toThrow(
       /^login secret export is not hex$/
     );
+    // Odd-length input: the decoder rejects before it reads a byte pair.
+    await expect(exportLoginSecret(exporter(SECRET_HEX.slice(1)))).rejects.toThrow(
+      /^login secret export is not hex$/
+    );
     await expect(exportLoginSecret(exporter(''))).rejects.toThrow(/32-byte scalar/);
     // Short of a full secp256k1 scalar: rejected here, not after a transfer.
     await expect(exportLoginSecret(exporter(SECRET_HEX.slice(2)))).rejects.toThrow(
       /32-byte scalar/
     );
   });
+
+  it('decodes an uppercase hex export', async () => {
+    expect(new Uint8Array(await exportLoginSecret(exporter(SECRET_HEX.toUpperCase())))).toEqual(
+      SECRET_BYTES
+    );
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/login/src/secret.test.ts` around lines 47 - 56, Add tests in the
existing malformed-export case for an odd-length hex input to exercise the
fromHex guard, and for uppercase hex input to exercise the uppercase branch in
nibble; assert the expected successful decode or validation behavior and
preserve the existing no-echo and scalar-length checks.

Source: Path instructions

packages/login/src/secret.ts (1)

74-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep the login decoder separate from the client decoder.

@cipherbox/login is host-agnostic, while @cipherbox/client is browser-specific. No host-agnostic shared package exists. Update the client barrel comment that calls this “the one hex codec in TypeScript.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/login/src/secret.ts` around lines 74 - 94, Update the client barrel
comment referencing “the one hex codec in TypeScript” to clarify that the login
decoder is intentionally separate from the browser-specific client decoder.
Preserve the existing host-agnostic login implementation in fromHex and nibble
without combining or relocating the codecs.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/auth/webCollector.test.ts`:
- Around line 20-26: Extend the test in the existing “passes the material the
page already collected straight through” case to call collector.email with an
email-material object and assert it resolves to the identical object, while
preserving the existing Google and wallet assertions.

In `@blueprint/desktop.md`:
- Line 254: Update the desktop integration description near the “packages/login”
reference to describe orchestration as shared in the login package, while
stating that the shell provides both credential collection and its start facade.
Remove the stale wording that assigns orchestration ownership to the web client,
and make the host-provided facade explicit.

In `@blueprint/web-client.md`:
- Around line 203-206: Update the ADR 0008 D3 web collector description to
mention email collection alongside the existing Google popup and wallet
collection, accurately reflecting the credential methods exposed by
apps/web/src/auth/webCollector.ts.

---

Nitpick comments:
In `@packages/login/src/secret.test.ts`:
- Around line 47-56: Add tests in the existing malformed-export case for an
odd-length hex input to exercise the fromHex guard, and for uppercase hex input
to exercise the uppercase branch in nibble; assert the expected successful
decode or validation behavior and preserve the existing no-echo and
scalar-length checks.

In `@packages/login/src/secret.ts`:
- Around line 74-94: Update the client barrel comment referencing “the one hex
codec in TypeScript” to clarify that the login decoder is intentionally separate
from the browser-specific client decoder. Preserve the existing host-agnostic
login implementation in fromHex and nibble without combining or relocating the
codecs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d4e3c66-98da-4319-8638-4bdcf2f3a234

📥 Commits

Reviewing files that changed from the base of the PR and between 69a8a72 and 864e665.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • .github/workflows/ci.yml
  • apps/web/package.json
  • apps/web/src/auth/CoreKitProvider.tsx
  • apps/web/src/auth/IdentityProvider.tsx
  • apps/web/src/auth/coreKit.test.ts
  • apps/web/src/auth/coreKit.ts
  • apps/web/src/auth/useAuth.ts
  • apps/web/src/auth/webCollector.test.ts
  • apps/web/src/auth/webCollector.ts
  • apps/web/src/engine/introspection.ts
  • apps/web/src/engine/loginHandoff.test.ts
  • apps/web/src/engine/loginHandoff.ts
  • apps/web/src/main.tsx
  • apps/web/src/test/authFakes.tsx
  • apps/web/tsconfig.json
  • blueprint/desktop.md
  • blueprint/web-client.md
  • packages/login/package.json
  • packages/login/src/collector.ts
  • packages/login/src/flow.test.ts
  • packages/login/src/flow.ts
  • packages/login/src/hostAgnostic.test.ts
  • packages/login/src/identity.test.ts
  • packages/login/src/identity.ts
  • packages/login/src/index.ts
  • packages/login/src/secret.test.ts
  • packages/login/src/secret.ts
  • packages/login/src/session.ts
  • packages/login/src/testFakes.ts
  • packages/login/tsconfig.build.json
  • packages/login/tsconfig.json

Comment thread apps/web/src/auth/webCollector.test.ts
Comment thread blueprint/desktop.md Outdated
Comment thread blueprint/web-client.md Outdated

FSM1 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Nitpick disposition

Both nitpicks from the review body are taken. They create no threads, so recording them here.

packages/login/src/secret.test.ts — odd-length and uppercase-hex decode cases. Taken (db020f9).

The claim checked out exactly: 'nothex' is six characters, so it fails on the non-hex character check rather than the length guard, and SECRET_HEX.slice(2) is even too — so fromHex's odd-length guard and nibble's uppercase branch were both unexercised. Added a SECRET_HEX.slice(1) case and an uppercase round-trip, with no secret material reaching failure output.

Mutation-testing them turned up something worth more than the coverage. Deleting the odd-length guard does not throw: new Uint8Array(31.5) truncates rather than raising, so the decoder silently produces 31 bytes and the failure surfaces later as not a 32-byte scalar. On a secret-bearing decoder that guard is load-bearing in a way nothing in the suite could previously see. Three mutations now fail: removing the uppercase branch, deleting the guard, and truncating instead of rejecting.

packages/client/src/index.ts — the "one hex codec" claim. Taken (8b602d1).

The comment now reads "The browser hex codec", with one line noting that host-agnostic packages/login carries its own and cannot depend on this package. The codecs are deliberately not merged: @cipherbox/login is host-agnostic by construction — enforced by a lib with no DOM, a test that runs a full login with browser globals replaced by throwing getters, and an assertion that import('react') rejects — so collapsing them would mean either dragging browser-specific code into the host-agnostic package or making that package depend on a browser one. The duplication is a consequence of the boundary ADR 0008 D3 draws; the stale claim of uniqueness was the actual defect.


The three inline findings are fixed, replied to, and resolved. Head is now 8b602d1; pnpm test, pnpm typecheck and all three lint gates pass on it.


Generated by Claude Code

@FSM1
FSM1 marked this pull request as draft August 12, 2026 23:36
@FSM1
FSM1 marked this pull request as ready for review August 12, 2026 23:36
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.

client: extract the login orchestration into a host-agnostic package

2 participants