feat: CipherBox issues the identity token, and wallet login is a first login - #1273
Conversation
WalkthroughThis change adds API-issued identity tokens and Google, email, and wallet authentication exchanges. The web client passes returned credentials to Core Kit JWT login. It also adds identity persistence, OTP delivery, configuration, OpenAPI definitions, UI flows, tests, and CI setup. ChangesIdentity authentication
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant LoginPage
participant IdentityExchange
participant IdentityController
participant IdentityExchangeService
participant IdentityTokenService
participant CoreKit
LoginPage->>IdentityExchange: submit Google, email, or wallet credential
IdentityExchange->>IdentityController: POST identity exchange
IdentityController->>IdentityExchangeService: verify credential and resolve subject
IdentityExchangeService->>IdentityTokenService: sign identity claims
IdentityTokenService-->>IdentityController: identity token and expiry
IdentityController-->>IdentityExchange: identity grant
IdentityExchange->>CoreKit: loginWithJWT(identity token)
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review Generated by Claude Code |
|
|
|
@coderabbitai review Generated by Claude Code |
|
|
|
@coderabbitai full review please |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/engine/config.test.ts (1)
100-123: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKeep Google configuration optional for email and wallet login.
Lines 100-123 and Lines 190-205 make
VITE_GOOGLE_CLIENT_IDa deployment andloginEnv()prerequisite.createCoreKitSession()callsloginEnv()before method selection, so a missing Google client ID prevents Core Kit initialization and makes email and wallet login unavailable. This contradicts Lines 213-218, which define a missing Google client ID as an unavailable Google method only.Remove
VITE_GOOGLE_CLIENT_IDfrom the global Core Kit and deployment requirements. Keep it as a Google-button-specific configuration value. Add coverage that email and wallet login remain available without it.Also applies to: 190-205, 208-218
🤖 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 `@apps/web/src/engine/config.test.ts` around lines 100 - 123, Remove VITE_GOOGLE_CLIENT_ID from the global deployment validation and loginEnv() prerequisite while retaining it for Google-specific method availability. Update createCoreKitSession() so missing Google configuration does not block Core Kit initialization or email and wallet login, and adjust the related tests to verify those methods remain available without the Google client ID.
🧹 Nitpick comments (11)
apps/api/src/auth/dto/identity.dto.test.ts (1)
17-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: cover the
forbidNonWhitelistedpath.The pipe is configured with
whitelist: trueandforbidNonWhitelisted: true, but no case sends an unexpected property. Add one so a later DTO change that drops a decorator cannot silently start accepting extra fields.♻️ Proposed additional case
it('applies the same trim on the verify request', async () => {+ it('refuses a property the DTO does not declare', async () => { + await expect( + pipe.transform({ email: 'member@example.com', role: 'admin' }, body(EmailCodeRequestDto)) + ).rejects.toBeInstanceOf(BadRequestException); + }); +🤖 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 `@apps/api/src/auth/dto/identity.dto.test.ts` around lines 17 - 45, Add a test in the EmailCodeRequestDto or EmailCodeVerifyRequestDto suite that passes an unexpected property alongside valid fields to pipe.transform and asserts it rejects with BadRequestException, covering the configured forbidNonWhitelisted behavior.apps/api/src/auth/services/email-otp.service.ts (1)
49-59: 🧹 Nitpick | 🔵 TrivialConfirm the API runs as a single instance.
trackedholds issued codes, send budgets, and attempt budgets in process memory. With more than one API replica behind a load balancer,POST /auth/identity/email/verify-codecan reach a replica that never issued the code, and the member receives 401. The per-address send cap is also enforced per replica, so the effective limit becomes 5 × replica count.ChallengeServiceshares the same constraint, so this may already be an accepted deployment property. If horizontal scaling is planned, move this state to a shared store, or pin these routes with sticky sessions.🤖 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 `@apps/api/src/auth/services/email-otp.service.ts` around lines 49 - 59, Confirm and document that the API deployment is single-instance for EmailOtpService and the related ChallengeService in-memory state. If horizontal scaling is required, replace the per-process tracked state with a shared store; otherwise configure sticky sessions for the affected authentication routes and preserve the existing code, send-budget, and attempt-budget behavior.apps/api/src/auth/services/email-otp.service.test.ts (1)
78-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPin the attempt-cap boundary in this test.
Every rejection path in
EmailOtpService.verifythrowsUnauthorizedException, so this test cannot tell "budget spent" from "incorrect code".verifydecrementsattemptsLeftand voids the code only when the value drops below zero, so after the five wrong guesses in this loop the code is still live and the sixth call is the one that reports "Too many attempts". Assert the message to fix that boundary, and add a case that a correct code still works after a smaller run of wrong guesses.As per path instructions for
**/*.{test,itest}.ts: "Focus on test coverage, edge cases, and test quality. Ensure tests are meaningful and not just for coverage metrics."♻️ Proposed test tightening
for (let attempt = 0; attempt < 5; attempt += 1) { - expect(() => service.verify(EMAIL, wrong)).toThrow(UnauthorizedException); + expect(() => service.verify(EMAIL, wrong)).toThrow(/Incorrect verification code/); } // The budget is spent, so even the right code no longer opens it. - expect(() => service.verify(EMAIL, code)).toThrow(UnauthorizedException); + expect(() => service.verify(EMAIL, code)).toThrow(/Too many attempts/); + }); + + it('keeps the code usable while attempts remain', async () => { + await service.send(EMAIL); + const code = lastCode(); + const wrong = code === '000000' ? '111111' : '000000'; + + expect(() => service.verify(EMAIL, wrong)).toThrow(/Incorrect verification code/); + expect(() => service.verify(EMAIL, code)).not.toThrow(); });🤖 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 `@apps/api/src/auth/services/email-otp.service.test.ts` around lines 78 - 88, Update the “voids the code after a run of wrong guesses” test to assert the UnauthorizedException message, covering the boundary where the sixth verification attempt—not the fifth—reports “Too many attempts.” Add a separate case proving the correct code succeeds after fewer wrong guesses, using the existing EmailOtp service test helpers.Source: Path instructions
apps/api/src/auth/identity.http.itest.ts (1)
160-197: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise
kid-based key selection in the JWKS test.
jose.importJWKtakes the first key and an explicit algorithm, so the verification here never uses thekidin the token header. Web3Auth selects the key from the JWKS bykid. A mismatch between the headerkidwritten byIdentityTokenService.signand thekidpublished in the JWKS would break real verification while this suite stays green. Verify throughcreateLocalJWKSetinstead, and assert the token lifetime, so both halves of the D1 contract are covered.♻️ Proposed verification through the JWK set
it('verifies a minted token, and refuses one signed by anything else', async () => { const jwks = await request(http()).get('/auth/.well-known/jwks.json').expect(200); - const key = await jose.importJWK(jwks.body.keys[0], 'RS256'); + const key = jose.createLocalJWKSet(jwks.body); const grant = await emailGrant(freshEmail()); const { payload } = await jose.jwtVerify(grant.body.token, key, { issuer: 'cipherbox', audience: 'web3auth', }); expect(payload.sub).toBe(grant.body.verifierId); expect(payload.method).toBe('email'); + expect(payload.exp! - payload.iat!).toBe(300); + expect(new Date(grant.body.expiresAt).getTime()).toBe(payload.exp! * 1000);🤖 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 `@apps/api/src/auth/identity.http.itest.ts` around lines 160 - 197, Update the JWKS verification test around the minted token to use jose.createLocalJWKSet with the complete response body instead of importing the first key via jose.importJWK, allowing verification to select by the token header kid. Preserve the issuer, audience, and payload assertions, and add an assertion that the token expiration reflects the required lifetime.apps/api/src/auth/auth.module.test.ts (1)
52-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake the environment explicit and assert the new identity providers.
ignoreEnvFile: trueleavesNODE_ENVto the ambient process. Two providers in this graph read it and fail closed:IdentityTokenService.onModuleInitthrows withoutIDENTITY_JWT_PRIVATE_KEY, andbuildMailProviderthrows withoutMAIL_PROVIDER. Any runner that does not setNODE_ENVtodevelopmentortestturns this suite red for an environmental reason instead of a graph defect. Supply the value throughloadso the test states its own preconditions.The assertion also names only
GoogleOAuthService. Fetch the providers this PR adds, so a later constructor change is attributed to the right service.♻️ Proposed test hardening
const builder = Test.createTestingModule({ imports: [ - ConfigModule.forRoot({ isGlobal: true, ignoreEnvFile: true }), + ConfigModule.forRoot({ + isGlobal: true, + ignoreEnvFile: true, + load: [() => ({ NODE_ENV: 'test' })], + }), RuntimeModule, AuthModule, ], }); @@ try { expect(moduleRef.get(GoogleOAuthService)).toBeInstanceOf(GoogleOAuthService); + expect(moduleRef.get(EmailOtpService)).toBeInstanceOf(EmailOtpService); + expect(moduleRef.get(IdentityExchangeService)).toBeInstanceOf(IdentityExchangeService); + expect(moduleRef.get(IdentityTokenService)).toBeInstanceOf(IdentityTokenService); + expect(moduleRef.get(IdentityController)).toBeInstanceOf(IdentityController); } finally {Add the matching imports for
EmailOtpService,IdentityExchangeService,IdentityTokenService, andIdentityController.🤖 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 `@apps/api/src/auth/auth.module.test.ts` around lines 52 - 73, Make the AuthModule dependency-graph test self-contained by configuring ConfigModule.forRoot with an explicit test/development NODE_ENV and the required identity JWT and mail-provider values through load, preventing ambient environment dependence. Update the imports and assertions to retrieve EmailOtpService, IdentityExchangeService, IdentityTokenService, and IdentityController alongside GoogleOAuthService so each added provider/controller is instantiated and attributed independently.apps/api/src/auth/services/mail.provider.test.ts (1)
53-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore the
fetchstub inafterEach.Both tests call
vi.unstubAllGlobals()as the last statement of the test body. If an assertion fails or the awaited call rejects, that line never runs and the stubbedfetchleaks into the following tests in this worker. Move the restore into anafterEachhook so it runs on failure too.♻️ Proposed cleanup
describe('SendGridMailProvider', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it('addresses the message to the recipient and reports the code', async () => { const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 202 })); vi.stubGlobal('fetch', fetchMock); @@ expect((init.headers as Record<string, string>).authorization).toBe('Bearer sg-key'); - - vi.unstubAllGlobals(); }); it('treats a refused send as a failure rather than reporting success', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('detail', { status: 429 }))); @@ ).rejects.toThrow(/status 429/); - - vi.unstubAllGlobals(); }); });Add
afterEachto thevitestimport.🤖 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 `@apps/api/src/auth/services/mail.provider.test.ts` around lines 53 - 83, Move global cleanup for the fetch stubs from the individual tests into an afterEach hook, adding afterEach to the existing Vitest imports. Remove the vi.unstubAllGlobals() calls from the tests so cleanup runs even when assertions or awaited operations fail.apps/api/src/auth/services/identity-token.service.ts (1)
8-9: 🧹 Nitpick | 🔵 TrivialPlan for signing-key rotation.
KIDis a fixed constant and the JWKS exposes exactly one key. A future rotation ofIDENTITY_JWT_PRIVATE_KEYreplaces the only published key under the samekid. Torus caches the JWKS per URL, which is the exact failure mode the comment on Line 50 describes. Consider derivingkidfrom a thumbprint of the public key and returning the previous key alongside the new one during an overlap window, so verification stays valid while caches refresh.🤖 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 `@apps/api/src/auth/services/identity-token.service.ts` around lines 8 - 9, Update the identity-token signing and JWKS key publication around KID and ALGORITHM to derive each key ID from its public-key thumbprint instead of using the fixed KID, and expose the previous public key alongside the current key during a rotation overlap window. Preserve RS256 signing and ensure both published keys remain verifiable while JWKS caches refresh.apps/web/src/engine/config.ts (1)
126-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRead the Google variable through the named constant.
Line 116 uses
env[GOOGLE_CLIENT_ID_ENV], and Line 126 repeats the literalVITE_GOOGLE_CLIENT_ID. The comment on Line 30 states the constant exists so the name cannot drift. Use the constant in both places. If you apply the fix above, this line goes away.🤖 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 `@apps/web/src/engine/config.ts` at line 126, Update the Google client ID lookup near configured in config.ts to use the existing GOOGLE_CLIENT_ID_ENV constant instead of the literal VITE_GOOGLE_CLIENT_ID, and ensure both Google variable reads consistently use that named constant.apps/web/src/auth/useAuth.ts (1)
41-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConfirm the string signature against the binary-data guideline.
loginWithWalletnow takessignature: stringinstead ofUint8Array. The coding guidelines state: "Represent binary data withVec<u8>in Rust andUint8Arrayin TypeScript, not strings."A
0x-prefixed hex signature is the practical JSON wire form, and wagmi returns that shape, so the change may be intentional. If it is, keep the hex form at the transport edge only, and document on this line that the value is a0x-prefixed EIP-191 hex signature so callers cannot pass an arbitrary string. Otherwise acceptUint8Arrayhere and encode insideidentityExchange.As per coding guidelines: "Represent binary data with
Vec<u8>in Rust andUint8Arrayin TypeScript, not strings."🤖 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 `@apps/web/src/auth/useAuth.ts` at line 41, Clarify the `loginWithWallet` signature contract: if the JSON transport requires wagmi’s hex representation, retain `signature: string` but document it as a `0x`-prefixed EIP-191 hex signature and ensure only that form reaches `identityExchange`; otherwise change the API to accept `Uint8Array` and perform hex encoding within `identityExchange`.Source: Coding guidelines
apps/web/src/components/auth/EmailLoginForm.test.tsx (1)
52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for a refused verification.
The suite covers a refused send at Lines 52-59. It does not cover a refused
onVerify.EmailLoginFormswallows that rejection at Line 37 ofapps/web/src/components/auth/EmailLoginForm.tsx, so the member must stay on the code step and be able to submit again. Add a case that rejectsonVerifyand then asserts that the code input is still present and that a second submit callsonVerifyagain.💚 Proposed test to add
// A refused code must leave the member on the code step to try again. it('stays on the code step when the verification is refused', async () => { const { onVerify } = renderForm({ onVerify: () => Promise.reject(new Error('incorrect verification code')), }); typeAddress('member@example.test'); const code = await screen.findByTestId('email-code-input'); fireEvent.change(code, { target: { value: '111111' } }); fireEvent.click(screen.getByTestId('email-verify-button')); await waitFor(() => expect(onVerify).toHaveBeenCalledTimes(1)); expect(screen.getByTestId('email-code-input')).toBeDefined(); fireEvent.click(screen.getByTestId('email-verify-button')); await waitFor(() => expect(onVerify).toHaveBeenCalledTimes(2)); });🤖 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 `@apps/web/src/components/auth/EmailLoginForm.test.tsx` around lines 52 - 59, Add a test alongside the existing send-refusal case that configures renderForm’s onVerify to reject, submits a valid-looking code, and verifies the code-step input remains present after the rejection. Then submit again and assert the same onVerify mock is called a second time, covering EmailLoginForm’s refused-verification retry behavior.apps/web/src/components/auth/GoogleLoginButton.tsx (1)
86-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate
deliver.currentin a layout effect. A discarded render can otherwise expose an uncommitted callback to GIS. Do not replace it withuseEffectEvent; GIS invokes this callback outside an Effect.🤖 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 `@apps/web/src/components/auth/GoogleLoginButton.tsx` around lines 86 - 88, Update the deliver ref assignment in GoogleLoginButton so deliver.current is synchronized with onCredential inside a layout effect, preventing discarded renders from exposing uncommitted callbacks while preserving GIS’s external callback invocation.
🤖 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/api/src/auth/identity.http.itest.ts`:
- Around line 53-58: Update the CI workflow job that runs `pnpm --filter
`@cipherbox/api` test:integration` against PostgreSQL so it is exposed as a
required status check rather than being marked optional. Preserve the existing
integration test command and database setup while removing the configuration
that allows this job to be non-blocking.
In `@apps/web/src/auth/coreKit.ts`:
- Around line 92-94: Update the session restore flow associated with CoreKit’s
email() and method() so signed-in email metadata is restored from the SDK user
information rather than only being assigned by login(). Preserve the email()
contract after reloads and add a regression test covering restored sessions
returning the user’s email.
In `@apps/web/src/components/auth/EmailLoginForm.tsx`:
- Around line 29-36: Update the successful onSendCode branch in the sentTo
transition to move focus to the newly rendered code input after
setSentTo(trimmed) completes. Reuse the code field’s existing ref or focus
mechanism, ensuring focus occurs only after a successful send and preserves the
current failure behavior.
In `@apps/web/src/components/auth/GoogleLoginButton.tsx`:
- Around line 90-116: Keep the Google target div rendered throughout the busy
state in the component around the target ref, and display the busy indicator as
an overlay instead of unmounting the target. Preserve the existing target
element so the clientId-dependent effect can retain the initialized button after
busy returns false. Add a test in GoogleLoginButton.test.tsx that rerenders busy
true then false and verifies the target remains present and initialized.
- Line 23: Update the staging CSP in the Caddyfile to allow the Google Identity
Services script origin used by GIS_SRC, including any required Google
authentication origins, while preserving the existing CSP directives.
In `@apps/web/src/engine/config.ts`:
- Around line 119-131: The loginEnv requirement incorrectly makes Google OAuth
configuration mandatory for all Core Kit sessions. In
apps/web/src/engine/config.ts:119-131, remove googleClientId from loginEnv’s
return type, requirement check, and missing-variable error while retaining it in
LOGIN_ENV; update apps/web/src/auth/coreKit.ts:170-170 to destructure only
web3AuthClientId and verifier; update apps/web/src/engine/config.test.ts:196 and
201 so missing or blank Google client IDs no longer cause loginEnv to throw.
In `@apps/web/src/test/authFakes.tsx`:
- Around line 95-106: Update fakeCoreKitSession’s method and email state to
start as null, then assign both from the credential inside login(). Preserve
options.email as an explicit override in the email accessor, while ensuring
wallet credentials return their own metadata after login instead of the default
Google method or test email.
In `@blueprint/api.md`:
- Around line 63-69: The complete data-model list must include the
identity_subjects table described near the auth data-model section. Update the
list around the existing completeness claim to add identity_subjects, preserving
the statement that no other tables exist.
In `@docs/CONFIGURATION.md`:
- Line 88: Update the VITE_ENVIRONMENT entry in the configuration documentation
to include ci among the accepted values, indicating that it is CI-only if
appropriate while preserving the existing local, staging, and production
descriptions.
---
Outside diff comments:
In `@apps/web/src/engine/config.test.ts`:
- Around line 100-123: Remove VITE_GOOGLE_CLIENT_ID from the global deployment
validation and loginEnv() prerequisite while retaining it for Google-specific
method availability. Update createCoreKitSession() so missing Google
configuration does not block Core Kit initialization or email and wallet login,
and adjust the related tests to verify those methods remain available without
the Google client ID.
---
Nitpick comments:
In `@apps/api/src/auth/auth.module.test.ts`:
- Around line 52-73: Make the AuthModule dependency-graph test self-contained by
configuring ConfigModule.forRoot with an explicit test/development NODE_ENV and
the required identity JWT and mail-provider values through load, preventing
ambient environment dependence. Update the imports and assertions to retrieve
EmailOtpService, IdentityExchangeService, IdentityTokenService, and
IdentityController alongside GoogleOAuthService so each added
provider/controller is instantiated and attributed independently.
In `@apps/api/src/auth/dto/identity.dto.test.ts`:
- Around line 17-45: Add a test in the EmailCodeRequestDto or
EmailCodeVerifyRequestDto suite that passes an unexpected property alongside
valid fields to pipe.transform and asserts it rejects with BadRequestException,
covering the configured forbidNonWhitelisted behavior.
In `@apps/api/src/auth/identity.http.itest.ts`:
- Around line 160-197: Update the JWKS verification test around the minted token
to use jose.createLocalJWKSet with the complete response body instead of
importing the first key via jose.importJWK, allowing verification to select by
the token header kid. Preserve the issuer, audience, and payload assertions, and
add an assertion that the token expiration reflects the required lifetime.
In `@apps/api/src/auth/services/email-otp.service.test.ts`:
- Around line 78-88: Update the “voids the code after a run of wrong guesses”
test to assert the UnauthorizedException message, covering the boundary where
the sixth verification attempt—not the fifth—reports “Too many attempts.” Add a
separate case proving the correct code succeeds after fewer wrong guesses, using
the existing EmailOtp service test helpers.
In `@apps/api/src/auth/services/email-otp.service.ts`:
- Around line 49-59: Confirm and document that the API deployment is
single-instance for EmailOtpService and the related ChallengeService in-memory
state. If horizontal scaling is required, replace the per-process tracked state
with a shared store; otherwise configure sticky sessions for the affected
authentication routes and preserve the existing code, send-budget, and
attempt-budget behavior.
In `@apps/api/src/auth/services/identity-token.service.ts`:
- Around line 8-9: Update the identity-token signing and JWKS key publication
around KID and ALGORITHM to derive each key ID from its public-key thumbprint
instead of using the fixed KID, and expose the previous public key alongside the
current key during a rotation overlap window. Preserve RS256 signing and ensure
both published keys remain verifiable while JWKS caches refresh.
In `@apps/api/src/auth/services/mail.provider.test.ts`:
- Around line 53-83: Move global cleanup for the fetch stubs from the individual
tests into an afterEach hook, adding afterEach to the existing Vitest imports.
Remove the vi.unstubAllGlobals() calls from the tests so cleanup runs even when
assertions or awaited operations fail.
In `@apps/web/src/auth/useAuth.ts`:
- Line 41: Clarify the `loginWithWallet` signature contract: if the JSON
transport requires wagmi’s hex representation, retain `signature: string` but
document it as a `0x`-prefixed EIP-191 hex signature and ensure only that form
reaches `identityExchange`; otherwise change the API to accept `Uint8Array` and
perform hex encoding within `identityExchange`.
In `@apps/web/src/components/auth/EmailLoginForm.test.tsx`:
- Around line 52-59: Add a test alongside the existing send-refusal case that
configures renderForm’s onVerify to reject, submits a valid-looking code, and
verifies the code-step input remains present after the rejection. Then submit
again and assert the same onVerify mock is called a second time, covering
EmailLoginForm’s refused-verification retry behavior.
In `@apps/web/src/components/auth/GoogleLoginButton.tsx`:
- Around line 86-88: Update the deliver ref assignment in GoogleLoginButton so
deliver.current is synchronized with onCredential inside a layout effect,
preventing discarded renders from exposing uncommitted callbacks while
preserving GIS’s external callback invocation.
In `@apps/web/src/engine/config.ts`:
- Line 126: Update the Google client ID lookup near configured in config.ts to
use the existing GOOGLE_CLIENT_ID_ENV constant instead of the literal
VITE_GOOGLE_CLIENT_ID, and ensure both Google variable reads consistently use
that named constant.
🪄 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: f1af8b6e-5204-46a4-b80a-e70f3571df12
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!**/pnpm-lock.yaml
📒 Files selected for processing (50)
.github/workflows/ci.yml.github/workflows/web-e2e.ymlREADME.mdapps/api/.env.exampleapps/api/openapi.jsonapps/api/package.jsonapps/api/scripts/generate-openapi.tsapps/api/src/auth/auth.module.test.tsapps/api/src/auth/auth.module.tsapps/api/src/auth/dto/identity.dto.test.tsapps/api/src/auth/dto/identity.dto.tsapps/api/src/auth/entities/identity-subject.entity.tsapps/api/src/auth/identity.controller.tsapps/api/src/auth/identity.http.itest.tsapps/api/src/auth/services/email-otp.service.test.tsapps/api/src/auth/services/email-otp.service.tsapps/api/src/auth/services/google-oauth.service.test.tsapps/api/src/auth/services/google-oauth.service.tsapps/api/src/auth/services/identity-exchange.service.tsapps/api/src/auth/services/identity-subject.service.test.tsapps/api/src/auth/services/identity-subject.service.tsapps/api/src/auth/services/identity-token.service.test.tsapps/api/src/auth/services/identity-token.service.tsapps/api/src/auth/services/mail.provider.test.tsapps/api/src/auth/services/mail.provider.tsapps/api/src/migrations/1784800000000-AddIdentitySubjects.tsapps/api/src/testing/integration-db.tsapps/web/.env.exampleapps/web/src/auth/CoreKitProvider.test.tsxapps/web/src/auth/IdentityProvider.tsxapps/web/src/auth/coreKit.test.tsapps/web/src/auth/coreKit.tsapps/web/src/auth/identityExchange.test.tsapps/web/src/auth/identityExchange.tsapps/web/src/auth/useAuth.test.tsxapps/web/src/auth/useAuth.tsapps/web/src/components/auth/EmailLoginForm.test.tsxapps/web/src/components/auth/EmailLoginForm.tsxapps/web/src/components/auth/GoogleLoginButton.test.tsxapps/web/src/components/auth/GoogleLoginButton.tsxapps/web/src/components/auth/WalletLoginButton.tsxapps/web/src/engine/config.test.tsapps/web/src/engine/config.tsapps/web/src/main.tsxapps/web/src/routes/LoginPage.tsxapps/web/src/styles/login.cssapps/web/src/test/authFakes.tsxapps/web/src/vite-env.d.tsblueprint/api.mddocs/CONFIGURATION.md
| describe('identity exchange HTTP flows (real Postgres)', () => { | ||
| let db: IntegrationDatabase; | ||
| let ctx: HttpIntegrationApp; | ||
| let mail: CapturingMailProvider; | ||
| let googleSigningKey: jose.CryptoKey; | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm an integration script exists and a named CI job runs it with Postgres.
fd -t f 'package.json' -d 3 --exec jq -r '{name, scripts}' {}
rg -n -C6 'itest|integration' .github/workflowsRepository: FSM1/cipher-box
Length of output: 5240
🏁 Script executed:
#!/bin/bash
sed -n '374,426p' .github/workflows/ci.yml
printf '\n--- API integration config and suite references ---\n'
rg -n -C4 'vitest.integration|\\.itest\\.ts|DATABASE_URL|POSTGRES|postgres' apps/api package.json .github/workflows/ci.ymlRepository: FSM1/cipher-box
Length of output: 11600
Make API Integration (real Postgres) a required CI check.
The job runs pnpm --filter @cipherbox/api test:integration with PostgreSQL, but the workflow explicitly marks it as not required. Configure branch protection so this suite blocks merges.
🤖 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 `@apps/api/src/auth/identity.http.itest.ts` around lines 53 - 58, Update the CI
workflow job that runs `pnpm --filter `@cipherbox/api` test:integration` against
PostgreSQL so it is exposed as a required status check rather than being marked
optional. Preserve the existing integration test command and database setup
while removing the configuration that allows this job to be non-blocking.
Source: Coding guidelines
| email(): string | null { | ||
| return this.coreKit.getUserInfo().email ?? null; | ||
| return this.signedInEmail; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find where the stored session email is rendered.
rg -nP -C3 '\bemail\b' apps/web/src/stores --type=ts
rg -nP -C3 'signedIn\(' apps/web/src --type=ts --type=tsxRepository: FSM1/cipher-box
Length of output: 6377
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- coreKit symbols and implementation ---'
rg -n -C6 'signedInEmail|email\(\)|method\(\)|restore\(|handOff\(' apps/web/src/auth/coreKit.ts apps/web/src --glob '*.ts' --glob '*.tsx' | head -n 260
printf '%s\n' '--- candidate auth files ---'
fd -t f -i 'auth|corekit' apps/web/src | head -n 120
printf '%s\n' '--- relevant tests ---'
rg -n -C5 'restore|handOff|email\(\)|method\(\)|signedInEmail|session\.email|session\.method' apps/web/src --glob '*.test.ts' --glob '*.test.tsx' | head -n 320Repository: FSM1/cipher-box
Length of output: 40584
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- coreKit implementation ---'
sed -n '1,220p' apps/web/src/auth/coreKit.ts
printf '%s\n' '--- auth handOff and restore callers ---'
rg -n -C8 'handOff|restore\(|session\.email|session\.method|signedIn\(' apps/web/src --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- related tests ---'
rg -n -C8 'restore|handOff|email\(\)|method\(\)|signedInEmail|sessionId' apps/web/src --glob '*.test.ts' --glob '*.test.tsx'Repository: FSM1/cipher-box
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- identity credential and token claims ---'
sed -n '1,240p' apps/web/src/auth/identityExchange.ts
printf '%s\n' '--- restore and handoff tests ---'
rg -n -C12 'restor|hand.?off|email|method|isAuthenticated' apps/web/src/auth/useAuth.test.tsx apps/web/src/auth/CoreKitProvider.test.tsx apps/web/src/test/authFakes.tsx
printf '%s\n' '--- SDK user-info email references and project docs ---'
rg -n -i -C4 'getUserInfo|userInfo|email.*claim|claim.*email|email' apps/web blueprint apps/web/src/auth --glob '*.md' --glob '*.ts' --glob '*.tsx' | head -n 260Repository: FSM1/cipher-box
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- exact identity exchange definitions ---'
rg -n -C12 'IdentityCredential|IdentityMethod|fromGoogle|fromEmail|email:' apps/web/src/auth/identityExchange.ts
printf '%s\n' '--- exact restore handoff tests ---'
rg -n -C15 'restore|handOff|email|method|isAuthenticated' apps/web/src/auth/useAuth.test.tsx apps/web/src/auth/CoreKitProvider.test.tsx apps/web/src/test/authFakes.tsx
printf '%s\n' '--- SDK user-info usage ---'
rg -n -i -C5 'getUserInfo|userInfo|email' apps/web/src/auth blueprint --glob '*.ts' --glob '*.tsx' --glob '*.md' | head -n 240Repository: FSM1/cipher-box
Length of output: 50371
Preserve email metadata on session restore. signedInEmail is set only by login(), so reloads make email() return null while method() still restores from SDK user info. Use a durable non-storage source, or document that restored sessions have no email and update the auth state contract and UI. Add a restore regression test.
🤖 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 `@apps/web/src/auth/coreKit.ts` around lines 92 - 94, Update the session
restore flow associated with CoreKit’s email() and method() so signed-in email
metadata is restored from the SDK user information rather than only being
assigned by login(). Preserve the email() contract after reloads and add a
regression test covering restored sessions returning the user’s email.
`subVerifierDetails.clientId` is the provider's client ID, not the Web3Auth project's, so every Google sign-in sent Google an identifier it has never issued and got back `401 invalid_client`. `loginEnv` now reads `VITE_GOOGLE_CLIENT_ID` alongside the Web3Auth pair and returns them under names that cannot be swapped, and the session routes each sub-verifier to its own registration: the Google connection takes the Google Cloud OAuth client ID, the Torus-hosted email one the Web3Auth project's. The variable was already set at every staging deploy site and read nowhere. Adding it to `LOGIN_ENV` puts it behind the same build gate as the rest, so a missing one names itself instead of surfacing a provider error.
Implements ADR 0008 D1 and D2 on the server. Each verified method now mints a CipherBox JWT and the Core Kit logs in against a CipherBox custom verifier over the API's own JWKS, rather than delegating to Web3Auth's hosted OAuth. - JWKS endpoint plus an RS256 identity-token mint. The signing key is required in every deployed profile, allowlisted exactly as the access-token secret is: Torus caches the JWKS per URL, so a keypair regenerated on restart makes every later login fail verification behind a message that names neither the key nor the restart. The public JWK is derived from the public key rather than by stripping fields off the private one, so no private field can reach the JWKS by omission. - Passwordless email returns to CipherBox: it issues the code, verifies it, and owns delivery through a configured provider seam. A deployed profile with no provider refuses to boot rather than failing at the first send. Codes are held in memory, hashed and salted, single-use, attempt-capped and expiry-checked, for the reason ChallengeService already documents. - Wallet becomes a first-class first login: a SIWE signature mints the same token as any other method, so it reaches the same derived key. - Google ID tokens are verified against Google's JWKS with a required audience, using the OAuth provider's client ID rather than the Web3Auth project's. The audience is enforced in every profile, not warn-and-skipped. A verified provider identity maps to a stable subject id through a dedicated identity_subjects table carrying no user_id: the account still materializes at POST /auth/login against the derived key, so this cannot fork the account model, and linking a method later is pointing a second provider identity at an existing subject. Insert-then-read on the unique index, so concurrent first logins for one identity yield one subject. No identity endpoint creates a users row. /auth/siwe/login and /auth/siwe/link are untouched.
Implements the client half of ADR 0008 D1 and D2. The Core Kit now redeems a CipherBox-issued token through loginWithJWT instead of driving Web3Auth's hosted OAuth, so a method that authenticates against CipherBox first can produce a login secret. - The identity exchange is spoken over plain fetch against the API, not through the engine: it runs before a login secret exists, and the engine refuses every command until start. It sits behind a seam the login flow reads, so useAuth still touches no browser API and imports only react. - Wallet login stops dead-ending. It no longer routes to the engine's SIWE login, which was refused with NotStarted and surfaced to the member as "engine not started"; it lands on the same mint, the same Core Kit login and the same secret handoff as Google, so a member with no prior session reaches a working vault. Web only. - Google credential collection loads Google Identity Services and hands the API the ID token. The button Google renders is the one path that always presents, so the affordance cannot be clicked into nothing. - The verification code is collected in the app now that CipherBox issues it, rather than in a provider window. The form advances only on a send that actually happened. - The Torus-hosted email_passwordless sub-verifier is gone, and with it the client ID it was carried under. The session tracks how it was established off the token's own method claim, which the SDK parses into its user info and keeps across a restore, since an opaque subject id carries no method the way a typeOfLogin did.
IdentitySubjectService carried its own copy of the SHA-256 identifier hash
that IdentityService already provides, so the two could drift apart while
keying the same rows. It now injects IdentityService and calls that one.
EmailOtpService.verify returns the normalized address it consumed, so the
exchange stops re-normalizing what the verifier already canonicalized, and
normalizeEmail is no longer exported.
The insert takes `.returning('id')`: on Postgres an ignored conflict returns
no row, so the winner answers from the insert and only a loser pays for the
re-read. Unit-tested on both paths, since the concurrency proof against real
Postgres only runs in CI.
…elper The loader cast globalThis and re-checked the installed shape in two places, once before injecting the script and once on load. Both now go through installedGis, which returns the SDK or null.
`GoogleOAuthService` took its fake-JWKS seam as a TypeScript-optional second constructor parameter. Nest reads every position from `design:paramtypes`, where an interface type emits no injectable token, so booting the real module graph died in `InstanceLoader` with UnknownDependenciesException and the API never listened — taking down every check that boots it. `@Optional()` restores the existing `keys ?? createRemoteJWKSet(...)` fallback without changing the production default or removing the seam. Every suite handed this provider in ready-made, so none of them compiled the module Nest actually boots; `AuthModule` is now compiled and initialized in the unit suite, which covers the other identity providers and both controllers too.
`EmailOtpService` keys an address by its trimmed, lowercased form, but the validation pipe ran first and `@IsEmail` rejected any spelling carrying surrounding space — so a member who typed a trailing space got a 400 naming nothing they could act on, for an address the service would have accepted. The transform is null-safe and leaves a non-string payload for `@IsEmail` to refuse. The existing unit tests call the service directly and so never crossed the pipe; the new ones drive it.
`EmailOtpService` caps sends per address over a 15-minute window and holds that budget in memory for the app's whole life, while the suite truncates only the database between tests. Seven tests sharing one literal address spent one five-send budget between them, so the last two answered 429. Each test now mints its own address, which keeps the cap under test intact rather than configuring it away: the bucket is keyed by an address the test chooses freely, unlike the per-IP auth bucket THROTTLE_AUTH_LIMIT exists for, where a whole suite is forced to share one key.
main.tsx called loginEnv at module scope, so a build carrying no login variables threw before the first render and served a blank page. A new googleClientId reads that one variable as optional, and the Google button presents the method as unavailable — naming the missing variable — instead of offering an affordance it cannot serve.
The web e2e job builds a bundle that offers every login method the front door asserts, so it gets a placeholder Google client ID. The contract job's production-mode API refuses to boot without a mail provider, so it gets a placeholder one; neither value is ever redeemed.
A deployed profile fails closed on the identity signing key and the Google client ID as well as the mail provider, so the contract job's production instance needs all three to reach a listening state. The signing key is generated in the step rather than committed.
`loginEnv` required VITE_GOOGLE_CLIENT_ID and `createCoreKitSession` calls it, so a build without the Google ID refused email and wallet logins too — the same class of regression as the boot fix, one layer down. The variable now gates only the method it configures; the deployed-build gate still requires it. Alongside, the Google button no longer unmounts its GIS target during a busy cycle, which left the member with no button after a failed sign-in until a reload, and the staging CSP now admits the GIS script origin so Google sign-in can load at all. - keep the GIS target mounted and overlay the busy state - install the credential callback on commit rather than during render - focus the code field once an email code is on its way - report method and email from the redeemed credential in the auth fake - verify identity tokens through the JWKS key set, exercising kid selection - assert OTP refusal messages, the module's full provider graph, and forbidNonWhitelisted; unstub fetch in afterEach
f8f1658 to
b9202ae
Compare
|
All 21 review items dispositioned. Head is now Fixed (15)Every finding verified against the code before acting; three were genuine user-facing defects. Majors
Others: The JWKS one is worth calling out: the suite verified with Not changed (6), with reasons
On the inconclusive linked-issues checkThe check asks for evidence that desktop hides wallet login. There is no desktop frontend to hide it in — VerificationWeb 327/327, API 227/227, integration 153/153 against real Postgres 16, typecheck, all three lint gates, One pre-existing item noticed while fixing the data-model list: it also omits Generated by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/components/auth/EmailLoginForm.test.tsx`:
- Around line 52-59: Update both tests in
apps/web/src/components/auth/EmailLoginForm.test.tsx (lines 52-59 and 73-90) to
use deferred promises for the send and verify callbacks, reject each promise
inside act after the callback begins, and await settlement before asserting.
Verify the send-rejection case keeps the email input visible; verify the
verify-rejection case keeps the code input visible and permits entering another
code.
🪄 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: 516c9d57-f3ce-43d6-afad-457004ba988e
📒 Files selected for processing (19)
apps/api/src/auth/auth.module.test.tsapps/api/src/auth/dto/identity.dto.test.tsapps/api/src/auth/identity.http.itest.tsapps/api/src/auth/services/email-otp.service.test.tsapps/api/src/auth/services/mail.provider.test.tsapps/web/src/auth/coreKit.test.tsapps/web/src/auth/useAuth.test.tsxapps/web/src/auth/useAuth.tsapps/web/src/components/auth/EmailLoginForm.test.tsxapps/web/src/components/auth/EmailLoginForm.tsxapps/web/src/components/auth/GoogleLoginButton.test.tsxapps/web/src/components/auth/GoogleLoginButton.tsxapps/web/src/engine/config.test.tsapps/web/src/engine/config.tsapps/web/src/styles/login.cssapps/web/src/test/authFakes.tsxblueprint/api.mddocker/Caddyfiledocs/CONFIGURATION.md
🚧 Files skipped from review as they are similar to previous changes (12)
- docs/CONFIGURATION.md
- blueprint/api.md
- apps/api/src/auth/auth.module.test.ts
- apps/api/src/auth/services/mail.provider.test.ts
- apps/api/src/auth/services/email-otp.service.test.ts
- apps/web/src/engine/config.test.ts
- apps/web/src/styles/login.css
- apps/web/src/auth/useAuth.test.tsx
- apps/api/src/auth/identity.http.itest.ts
- apps/web/src/components/auth/GoogleLoginButton.tsx
- apps/web/src/auth/coreKit.test.ts
- apps/web/src/test/authFakes.tsx
The step assertions ran after waitFor happened to drain the rejection, not because the test made it settle. A deferred promise refused inside act ties the assertion to the settlement it is about.
The API refuses to boot in a deployed profile without a signing key, a Google client ID, or a mail provider, but the generated .env.staging carried neither MAIL_PROVIDER nor the SendGrid pair. Adds the three keys and a preflight that fails the job when one is unset, since the deploy has no health gate to catch a crash-looping container. Entire-Checkpoint: 83a07f519199
The retirement row predates ADR 0008 D2, which gives the API a mail surface again. SENDGRID_FROM_EMAIL still retires; MAIL_FROM_ADDRESS replaces it. Entire-Checkpoint: 168093cce9ef
The Identity and auth section names the table, so the list that claims to be complete contradicted it.
Implements ADR 0008 D1 and D2.
Closes #1256. Closes #1257. Closes #1258. Closes #1260. Part of #1253.
Supersedes #1272, which is closed unmerged — its two commits are carried here. There is no staging environment yet, so landing an intermediate fix to a code path this PR deletes bought nothing.
Why these ship together
The Core Kit derives its TSS key from the
(verifier, verifierId)pair. Moving from Web3Auth's hosted verifier to a CipherBox custom verifier changes the derived key. If Google moved to the CipherBox verifier while email stayed on theemail_passwordlesssub-verifier, one person would land on two different accounts depending on which button they pressed. All three methods have to cross in one commit, so they do.Identities created under the old Web3Auth-hosted verifier are not reachable after this change. That is fine because no accounts exist yet; there is deliberately no migration, dual-verifier fallback, or compatibility shim.
What changes
The API issues the identity token (#1257). A JWKS endpoint plus an RS256 mint. Each verified method produces a CipherBox JWT, and the Core Kit redeems it through
loginWithJWTagainst CipherBox's own JWKS instead ofloginWithOAuth.The API owns passwordless email again (#1258). It issues the code, verifies it, and owns delivery behind a provider seam. The code is now collected in the app rather than in a provider window.
Wallet is a first-class first login, web only (#1260).
WalletLoginButtonno longer routes to the engine'sCommand::SiweLogin, which was refused withNotStartedand surfaced to the member as "engine not started". A SIWE signature now mints the same token as any other method and lands on the same Core Kit login and secret handoff, so a member with no prior session reaches a working vault.The two client IDs stop being conflated (#1256).
subVerifierDetails.clientIdis the provider's client ID, but the app was passing the Web3Auth project client ID, so every Google sign-in ended at401 invalid_clientbefore Web3Auth was involved.VITE_GOOGLE_CLIENT_IDis declared, joinsLOGIN_ENVand so the deploy gate, andloginEnvreturns it — which the Google credential collection here consumes viamain.tsx→IdentityProvider→LoginPage. The per-method sub-verifier client-ID map from that fix is deleted by this one:loginWithJWTtakes nosubVerifierDetails.The subject mapping
A verified provider identity maps to a stable subject id through a new
identity_subjectstable. That id is the JWTsuband theverifierIdpassed tologinWithJWT.It carries no
user_id, deliberately. The account still materializes atPOST /auth/loginkeyed by the derivedpublicKey, exactly as before — this table's only job is to yield a stableverifierId, so it cannot fork the account model. Keepinguser_idout is also what leaves linking open later: linking a second method becomes pointing another provider identity at an existing subject id, authorized by an existing session the waysiwe/linkalready works. No linking flow is built here, only left unforeclosed.This is deliberately not v1's shape. v1 created a
usersrow per provider identity with a placeholderpublicKey(pending-core-kit-<id>) and used the user id asverifierId; that breaks the invariant that an account IS the derived key, and leaves junk accounts behind from abandoned login attempts.Resolution is insert-then-read against the unique
(kind, identifier_hash)index rather than an unguarded check-then-insert, so concurrent first logins for one identity yield one subject. Identifiers are stored SHA-256 hashed, following theauth_methodsconvention. No identity endpoint creates ausersrow.Methods do not cross-link: signing in with Google and with email as the same person yields two accounts. That matches v1's documented behavior and is intentional.
Notes for review
/auth/siwe/loginand/auth/siwe/linkare untouched — the engine's Rust client and the contract suite call them. Under D2siwe/loginbecomes largely redundant, since a wallet can now reach its vault as a first login rather than only authenticating an already-linked one. Worth deciding separately whether it survives; removing it here would be a contract break outside this PR's scope.apps/api/src/auth/identity.http.itest.tsagainst real Postgres instead.buildJwtOptionsallowlists the access-token secret — v1 only required it inproduction, so staging hit it. The public JWK is derived from the public key rather than by stripping private fields off the private JWK, so no private field can reach the JWKS by omission.verifierIdis an opaque uuid, so Torus cannot link a subject to an address. The cost is that a page reload restores the Core Kit session but not the display email, soUserMenushows[an0n]until the next explicit login. The login method survives a reload, read off the token's ownmethodclaim. Reversible by adding anemailclaim if the display matters more than the privacy — a deliberate trade, not an oversight.apps/api/.env.example:IDENTITY_JWT_PRIVATE_KEY(base64 PKCS8 PEM — multiline PEM does not survive a.env),GOOGLE_CLIENT_ID(the provider's, not the Web3Auth project's),MAIL_PROVIDERplusSENDGRID_API_KEYandMAIL_FROM_ADDRESS. The API refuses to boot in a deployed profile without the signing key or a mail provider. The Web3Auth dashboard also needs a custom verifier pointed at/auth/.well-known/jwks.jsonwithsubas the verifier ID field.blueprint/api.mdgains theidentity_subjectsrow in its data-model list, since that file is normative.Verification
Ran green:
pnpm --filter @cipherbox/api test,pnpm --filter @cipherbox/web test(tsc -bfirst),pnpm lint,pnpm lint:md,pnpm lint:tracker-refs,pnpm --filter @cipherbox/api typecheck, andopenapi:generate(committed).test:integrationcould not run locally — no Docker or Postgres in this environment, so all integration files fail onECONNREFUSED 127.0.0.1:5432, pre-existing ones included. CI runs them.Tests assert behavior, not source text. Among them: a token signed by a different key is refused; an expired code is refused, as is one CipherBox never issued; a code is single-use and attempt-capped; the JWKS serves no
d/p/q/dp/dq/qi; a wallet signature mints asubstable across repeat logins; the same identity logging in concurrently yields one subject row; identity endpoints leaveusersempty; and boot fails in a deployed profile without the signing key, without a mail provider, and without a Google client ID.Needs human verification
No dev server or configured verifier exists in the build environment, so Puppeteer verification was not attempted. Against a real Web3Auth custom verifier and a real Google client:
IDENTITY_JWT_PRIVATE_KEYset does not break the next login, which is the v1 failure this guards against.Note
Issue CipherBox identity tokens from the API and treat wallet login as a first-login flow
IdentityControllerwith endpoints for Google ID token exchange, email OTP send/verify, SIWE wallet signature, and a JWKS discovery route; all return short-lived RS256 identity tokens signed byIdentityTokenService.EmailOtpService(6-digit uniform codes, single-use, attempt cap, per-address send rate limit) andGoogleOAuthService(jose-backed RS256 ID token verification) as the credential verification layer.IdentitySubjectServiceto resolve or create a stableidentity_subjectsrow per (kind, identifierHash), with concurrency-safe insert-on-conflict; backed by a new DB migration.loginWithJWT(verifier + verifierId + idToken); wallet login now performs a full identity-exchange round-trip via the API instead of the previous engine-facilitated SIWE path.EmailLoginForminto a two-step send-then-verify flow; addsGoogleLoginButtonbacked by Google Identity Services (GIS), loaded once per document.useAuthnow requires anIdentityProviderancestor with an exchange set, andloginWithGooglenow requires a Google ID token argument — both are breaking interface changes for callers.Macroscope summarized 164f1c6.
Summary by CodeRabbit