Skip to content

feat(12.3.1): Pre-wipe identity cleanup#127

Merged
FSM1 merged 20 commits into
mainfrom
feat/phase-12.3.1-pre-wipe-identity-cleanup
Feb 14, 2026
Merged

feat(12.3.1): Pre-wipe identity cleanup#127
FSM1 merged 20 commits into
mainfrom
feat/phase-12.3.1-pre-wipe-identity-cleanup

Conversation

@FSM1

@FSM1 FSM1 commented Feb 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Deterministic vault IPNS derivation: deriveVaultIpnsKeypair(privateKey) via HKDF (context: cipherbox-vault-ipns-v1) replaces random keygen — enables self-sovereign vault recovery from recovery phrase alone
  • SHA-256 hashed identifiers for all auth methods: Google (SHA-256(sub)), email (SHA-256(normalized_email)), wallet (SHA-256(checksummed_address)) — privacy-preserving lookup, no plaintext identifiers stored
  • Independent auth methods: Cross-method email auto-linking removed — each method (google/email/wallet) is a separate identity; users link explicitly via Settings
  • rootIpnsPublicKey fully removed: From crypto types, vault entity/DTO/service, desktop Rust, E2E helpers, API client — derivable from privateKey, no longer stored

Changes

Crypto (packages/crypto)

  • New deriveVaultIpnsKeypair() using HKDF-SHA256 with domain separation
  • initializeVault(userPrivateKey) now deterministic; EncryptedVaultKeys simplified
  • decryptVaultKeys derives public key from IPNS private key
  • 8 new tests for determinism, domain separation, error handling (169 total pass)

API (apps/api)

  • Generic hashIdentifier() in siwe.service for all auth types
  • identifier_hash + identifier_display columns in auth_method entity
  • findOrCreateUserByIdentifier searches by (type, identifierHash) only — no cross-method lookup
  • rootIpnsPublicKey removed from vault entity, DTOs, service, migration
  • All 460 API tests pass

Web (apps/web)

  • useAuth calls initializeVault(userKeypair.privateKey) for deterministic IPNS
  • rootIpnsPublicKey removed from vault API types and init flow

Desktop (apps/desktop)

  • Rust types, state, and commands cleaned of root_ipns_public_key
  • cargo check passes

E2E (tests/e2e)

  • Web3Auth helpers derive IPNS public key from private key via ed.getPublicKeyAsync()

Test plan

  • 169 crypto package tests pass (including 8 new vault IPNS derivation tests)
  • 460 API tests pass (identity controller, auth service, siwe service, vault controller)
  • cargo check passes for desktop app
  • Verifier confirmed 5/5 success criteria met against codebase
  • Manual: Login with Google → new user created with hashed identifier
  • Manual: Login with email → separate user (no auto-link to Google even if same email)
  • Manual: Vault initializes with deterministic IPNS name

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Deterministic vault keys: Vault IPNS keys are now derived from user keys for consistent recovery.
    • Hashed identifiers: All sign-in methods store hashed identifiers with human-readable display values.
  • Bug Fixes / Improvements

    • Auth methods no longer auto-link by email — each method is independent.
    • Vault public key removed from API payloads/responses to reduce exposed surface and improve privacy.
  • Tests / Docs

    • Tests and API client regenerated; roadmap Phase 12.3.1 marked complete.

FSM1 and others added 16 commits February 14, 2026 17:26
Phase 12.3.1: Pre-Wipe Identity Cleanup
- 3 plans in 2 waves
- Wave 1: crypto vault IPNS derivation (01) + backend identity hashing (02) in parallel
- Wave 2: backend vault schema + frontend vault init (03)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Plan 02: Hash googlePayload.sub (immutable) not email for Google auth
- Plan 02: Split Task 1 into schema+hashing tasks (was 6 files in one task)
- Plan 03: Remove ipns.service.ts from files_modified (no rootIpnsPublicKey refs)
- Plan 03: Add cross-reference to Plan 01 test case 8 for recovery path verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Plan 01: Add EncryptedVaultKeys, encryptVaultKeys, decryptVaultKeys updates + vault.test.ts
- Plan 03: Fix SC-5 wording to passive compatibility, add grep verification
- Plan 04: New plan for desktop Rust, E2E helpers, controller spec cleanup
- ROADMAP: Update plan count to 4, fix SC-5 wording

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add hashIdentifier(value) to SiweService for non-wallet identifiers
- Widen identifier_display from varchar(15) to varchar(255) for emails
- Add composite index on (type, identifier_hash) for efficient lookups
- Update entity and migration column comments for all auth method types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ltKeys cleanup

- Create deriveVaultIpnsKeypair() using HKDF with info "cipherbox-vault-ipns-v1"
- Update initializeVault() to require userPrivateKey and derive IPNS deterministically
- Remove rootIpnsPublicKey from EncryptedVaultKeys type
- Update encryptVaultKeys() to return only encrypted fields
- Update decryptVaultKeys() to derive public key from private key via ed25519

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 8 new tests: determinism, different keys, domain separation, error handling, IPNS format
- Add 3 tests for initializeVault: deterministic IPNS, random folder key, keypair consistency
- Update vault.test.ts: initializeVault calls pass privateKey
- Remove rootIpnsPublicKey assertions (field removed from EncryptedVaultKeys)
- Update uniqueness test to use different private keys (IPNS is now deterministic)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…to-linking

- Google login hashes googlePayload.sub (immutable Google user ID)
- Email login hashes normalized email for identifier lookup
- Remove cross-method email auto-linking block entirely
- Refactor findOrCreateUserByEmail to findOrCreateUserByIdentifier
- Update auth.service login/linkJwtMethod/testLogin to use identifierHash
- Return identifierDisplay (human-readable) from getLinkedMethods and refreshByToken

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2
- deriveVaultIpnsKeypair + updated vault init/encrypt/decrypt
- Unit tests for vault IPNS derivation + updated vault tests

SUMMARY: .planning/phases/12.3.1-pre-wipe-identity-cleanup/
12.3.1-01-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…auth methods

- Add hashIdentifier tests to siwe.service.spec (consistent hash, no normalization)
- Update identity controller tests: Google hashes sub, email hashes normalized email
- Add test: Google + email same email creates SEPARATE users (no auto-linking)
- Update auth.service tests: identifierHash lookups, identifierDisplay in responses
- Update linkJwtMethod tests: hash-based collision and duplicate detection
- Update testLogin tests: hash-based lookup and creation with identifierHash
- Update refreshByToken test: returns identifierDisplay not hashed identifier
- Regenerate API client
- All 460 tests pass across 24 suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 3/3
- Schema, entity, migration, and SiweService hash utility
- Hash all auth identifiers and remove cross-method auto-linking
- Update tests for hashed identifiers and independent auth methods

SUMMARY: .planning/phases/12.3.1-pre-wipe-identity-cleanup/12.3.1-02-SUMMARY.md
… and DTOs

- Remove rootIpnsPublicKey column from Vault entity (derivable from privateKey via HKDF)
- Remove rootIpnsPublicKey from InitVaultDto and VaultResponseDto
- Update VaultService.initializeVault and toVaultResponse mappings
- Remove root_ipns_public_key from FullSchema migration
- Update vault.service.spec.ts test fixtures and assertions
- Remove rootIpnsPublicKey from desktop Rust types, commands, and state
- Regenerate API client (openapi.json, orval models)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lt flow

- Update initializeOrLoadVault to pass privateKey to initializeVault()
- Remove rootIpnsPublicKey from decryptVaultKeys call (derived internally)
- Remove rootIpnsPublicKey from vaultApi.initVault DTO
- Remove rootIpnsPublicKey from VaultResponse and InitVaultDto types
- Update vault.controller.spec.ts fixtures (remove rootIpnsPublicKey)
- Update e2e web3auth-helpers (deterministic initializeVault, no rootIpnsPublicKey)
- TEE compatibility verified: ipns.service.ts has no rootIpnsPublicKey references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 (both already committed by Plan 03)
- Task 1: Remove rootIpnsPublicKey from desktop Rust app (07caf1b)
- Task 2: Update E2E helpers and vault controller spec (da2bc83)

Phase 12.3.1 complete (4/4 plans).

SUMMARY: .planning/phases/12.3.1-pre-wipe-identity-cleanup/12.3.1-04-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove rootIpnsPublicKey column from Vault entity
- Remove rootIpnsPublicKey from InitVaultDto and VaultResponseDto
- Remove rootIpnsPublicKey from VaultService create/response mappings
- Remove root_ipns_public_key from FullSchema migration
- Update vault.service.spec.ts test fixtures
- Regenerate openapi.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2
- Task 1: Update backend vault schema, service, and DTOs
- Task 2: Update frontend vault init, API client, and verify TEE compatibility

SUMMARY: .planning/phases/12.3.1-pre-wipe-identity-cleanup/12.3.1-03-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown

Walkthrough

Phase 12.3.1 (Pre-Wipe Identity Cleanup) marked complete (2026-02-14). Changes add deterministic IPNS keypair derivation from user private keys, switch auth identifiers to SHA‑256 per auth method (removing cross-method email auto‑linking), and remove the stored rootIpnsPublicKey across backend, frontend, desktop, tests, migrations, and API client.

Changes

Cohort / File(s) Summary
Planning & Documentation
.planning/ROADMAP.md, .planning/STATE.md, .planning/phases/12.3.1-pre-wipe-identity-cleanup/*
Declare Phase 12.3.1 complete (2026-02-14); add four plan docs (12.3.1-01 → 12.3.1-04), verification report, updated STATE metrics, decisions, and timestamps.
Crypto: Vault IPNS Derivation & API
packages/crypto/src/vault/derive-ipns.ts, packages/crypto/src/vault/init.ts, packages/crypto/src/vault/types.ts, packages/crypto/src/vault/index.ts, packages/crypto/src/index.ts
Add deriveVaultIpnsKeypair(userPrivateKey) (HKDF→Ed25519→IPNS); initializeVault now requires userPrivateKey; remove stored rootIpnsPublicKey from encrypted types; decrypt path derives public key from private key; exports updated.
Crypto Tests
packages/crypto/src/__tests__/vault-ipns.test.ts, packages/crypto/src/__tests__/vault.test.ts
New deterministic/IPNS tests; update vault tests to use per-user private keys and reflect removed plaintext IPNS public key storage.
Auth: Hash-Based Identifiers
apps/api/src/auth/entities/auth-method.entity.ts, apps/api/src/auth/services/siwe.service.ts, apps/api/src/auth/services/siwe.service.spec.ts, apps/api/src/auth/auth.service.ts, apps/api/src/auth/auth.service.spec.ts, apps/api/src/auth/controllers/identity.controller.ts, apps/api/src/auth/controllers/identity.controller.spec.ts
Introduce SHA‑256 identifierHash for Google sub, email, and wallets; add SiweService.hashIdentifier and truncateEmail; widen identifier_display to varchar(255); replace findOrCreateUserByEmail with findOrCreateUserByIdentifier; remove cross-method auto-linking; update tests.
API Vault: DTOs, Entity, Service, Tests
apps/api/src/vault/entities/vault.entity.ts, apps/api/src/vault/dto/init-vault.dto.ts, apps/api/src/vault/vault.service.ts, apps/api/src/vault/vault.service.spec.ts, apps/api/src/vault/vault.controller.spec.ts
Remove rootIpnsPublicKey from Vault entity, InitVaultDto, VaultResponseDto; update service mappings, export shapes and tests accordingly.
Migrations
apps/api/src/migrations/1700000000000-FullSchema.ts
Widen identifier_display (varchar(15) → varchar(255)); add composite index IDX_auth_methods_type_hash (type, identifier_hash); drop root_ipns_public_key from vaults table.
Frontend Web: Hooks & Types
apps/web/src/hooks/useAuth.ts, apps/web/src/lib/api/vault.ts, apps/web/src/api/models/initVaultDto.ts, apps/web/src/api/models/vaultResponseDto.ts
useAuth passes userKeypair.privateKey to initializeVault(); remove rootIpnsPublicKey from decrypt/init payloads; API models regenerated to remove rootIpnsPublicKey.
Desktop (Rust) App
apps/desktop/src-tauri/src/api/types.rs, apps/desktop/src-tauri/src/state.rs, apps/desktop/src-tauri/src/commands.rs
Remove root_ipns_public_key from InitVaultRequest/VaultResponse and AppState; commands and fetch/decrypt flows no longer decode/store the public key.
API Client & E2E Helpers
packages/api-client/openapi.json, tests/e2e/utils/web3auth-helpers.ts, tests/e2e/package.json
OpenAPI spec/client updated to remove rootIpnsPublicKey; E2E helpers derive IPNS public key from private key using ed25519; add @noble/ed25519 devDependency.
Misc Tests / Fixtures
apps/api/src/auth/*, apps/api/src/vault/*, packages/crypto/src/__tests__/*, tests/e2e/*
Many tests/fixtures updated: remove rootIpnsPublicKey, add identifierHash/identifierDisplay expectations, and update initialization flows to supply user private keys.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Web as "Web App"
    participant Crypto as "@cipherbox/crypto"
    participant API as "API Server"
    participant DB as "Database"

    User->>Web: Create account / Init vault
    Web->>Crypto: initializeVault(userPrivateKey)
    Crypto->>Crypto: deriveVaultIpnsKeypair(userPrivateKey)
    Note over Crypto: HKDF-SHA256(salt="CipherBox-v1", info="cipherbox-vault-ipns-v1")<br/>→ Ed25519 seed → publicKey → IPNS name
    Crypto->>Crypto: generate random rootFolderKey
    Crypto-->>Web: VaultInit { rootIpnsKeypair, rootFolderKey, encryptedRootIpnsPrivateKey }

    Web->>Crypto: encryptVaultKeys(vaultInit)
    Crypto-->>Web: EncryptedVaultKeys { encryptedRootFolderKey, encryptedIpnsPrivateKey }
    Web->>API: POST /vault (InitVaultDto without rootIpnsPublicKey)
    API->>DB: Store vault (no root_ipns_public_key)
    API-->>Web: VaultResponseDto { rootIpnsName, encryptedRootIpnsPrivateKey }

    Web->>API: GET /vault
    API->>DB: Fetch vault
    API-->>Web: VaultResponseDto
    Web->>Crypto: decryptVaultKeys(encryptedKeys)
    Crypto->>Crypto: decrypt IPNS private key
    Crypto->>Crypto: derive public key from private key
    Crypto-->>Web: rootIpnsKeypair { publicKey, privateKey }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(12.3.1): Pre-wipe identity cleanup' directly and specifically summarizes the main change in the pull request, following conventional commit format and clearly identifying the phase and primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/phase-12.3.1-pre-wipe-identity-cleanup

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 and usage tips.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/api/src/auth/controllers/identity.controller.ts (1)

302-306: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc reference to removed method.

Line 305 says "Same placeholder publicKey pattern as findOrCreateUserByEmail" but that method was renamed to findOrCreateUserByIdentifier.

Proposed fix
-   * Same placeholder publicKey pattern as findOrCreateUserByEmail.
+   * Same placeholder publicKey pattern as findOrCreateUserByIdentifier.
apps/api/src/auth/auth.service.ts (1)

80-105: ⚠️ Potential issue | 🟠 Major

Identifier hash in login() is inconsistent with the identity controller's hash for Google users.

The identity controller hashes googlePayload.sub (the immutable Google user ID) for Google auth methods (line 97 of identity.controller.ts). However, here at line 83–84, identifier resolves to payload.email (since it's checked first), and the hash is computed from the email — not the Google sub. This means the primary lookup at line 85–86 will never match for Google users.

The fallback at lines 90–92 (find any auth method for the user) masks this, but:

  • It may update lastUsedAt on the wrong auth method if the user has multiple linked methods.
  • The safety-net type inference at line 97 doesn't account for 'google' (only 'wallet' or 'email').

Consider aligning the hash computation with the identity controller by checking for a Google-specific indicator, or restructuring the lookup to use userId alone since the user is already resolved by publicKey.

🤖 Fix all issues with AI agents
In `@apps/api/src/auth/controllers/identity.controller.ts`:
- Around line 155-167: The JWT is being signed with the raw dto.email while the
identifier hash was computed from normalizedEmail, causing lookup mismatches;
update the call to jwtIssuerService.signIdentityJwt in identity.controller.ts to
pass the normalizedEmail (the same value used to produce identifierHash) instead
of dto.email so the payload.email used later will match the hashed identifier;
verify jwtIssuerService.signIdentityJwt accepts the normalized string (adjust
its parameter handling if necessary) and keep identifierHash computed from
normalizedEmail unchanged.

In `@tests/e2e/utils/web3auth-helpers.ts`:
- Line 10: The E2E tests import ed from '@noble/ed25519' (see import * as ed in
web3auth-helpers.ts) but that package is not declared in the tests/e2e
package.json, causing test-time resolution failures; add "@noble/ed25519":
"^2.2.3" (or compatible version) to the tests/e2e package.json devDependencies
so the test package can resolve the ed import at runtime and re-run the E2E
suite.
🧹 Nitpick comments (4)
packages/crypto/src/__tests__/vault-ipns.test.ts (1)

57-69: Minor: Consolidate the two-call error assertion into a single check.

The try/catch block (lines 64–69) has no guard against the function unexpectedly resolving — if rejects.toThrow on line 60 were removed, the test could silently pass. You can merge both assertions into one using rejects.toMatchObject or a single try/catch with a fail sentinel.

♻️ Suggested consolidation
   it('throws CryptoError with INVALID_KEY_SIZE for 31-byte key', async () => {
     const shortKey = new Uint8Array(31);
 
-    await expect(deriveVaultIpnsKeypair(shortKey)).rejects.toThrow(
-      'Invalid private key size for vault derivation'
-    );
-
-    try {
-      await deriveVaultIpnsKeypair(shortKey);
-    } catch (error) {
-      expect(error).toBeInstanceOf(CryptoError);
-      expect((error as CryptoError).code).toBe('INVALID_KEY_SIZE');
-    }
+    await expect(deriveVaultIpnsKeypair(shortKey)).rejects.toSatisfy((error: unknown) => {
+      expect(error).toBeInstanceOf(CryptoError);
+      expect((error as CryptoError).code).toBe('INVALID_KEY_SIZE');
+      expect((error as CryptoError).message).toContain('Invalid private key size for vault derivation');
+      return true;
+    });
   });
packages/crypto/src/__tests__/vault.test.ts (1)

48-64: Nit: test title could be clearer.

"should produce unique folder keys but deterministic IPNS keys for different private keys" could be read as "same IPNS keys for different inputs." Consider something like "should produce unique keys per user: random folder keys and distinct deterministic IPNS keys."

apps/api/src/auth/controllers/identity.controller.ts (1)

260-300: TOCTOU race in findOrCreateUserByIdentifier — concurrent first-time logins can create duplicate users.

The check at line 266 and the create at lines 282–297 are not wrapped in a transaction or protected by a unique constraint on (type, identifier_hash). The IDX_auth_methods_type_hash index (per the migration plan) is a regular index, not a UNIQUE constraint. Two concurrent first-login requests for the same identifier could both pass the findOne check and create two separate users.

Consider either:

  1. Making IDX_auth_methods_type_hash a UNIQUE index and catching the constraint violation, or
  2. Wrapping the check-and-create in a serializable transaction.
apps/api/src/auth/auth.service.ts (1)

288-333: linkJwtMethod hashing is consistent and well-structured.

Cross-account collision check, duplicate check, and creation all use identifierHash. However, note the same DRY concern: this uses createHash('sha256') directly (line 291) while identity.controller.ts uses siweService.hashIdentifier(). Both produce the same SHA-256 hex output, but centralizing on siweService.hashIdentifier() would reduce the risk of divergence.

Comment thread apps/api/src/auth/controllers/identity.controller.ts Outdated
Comment thread tests/e2e/utils/web3auth-helpers.ts
- Use normalizedEmail (not raw dto.email) in signIdentityJwt call
- Add @noble/ed25519 as direct devDependency in tests/e2e
- Add tests for missing SIWE nonce and duplicate wallet link branches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/auth/controllers/identity.controller.ts (1)

302-306: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc reference.

The comment says "Same placeholder publicKey pattern as findOrCreateUserByEmail" but that method was renamed to findOrCreateUserByIdentifier.

Proposed fix
   /**
    * Find an existing user by wallet address hash, or create a new user.
    *
-   * Same placeholder publicKey pattern as findOrCreateUserByEmail.
+   * Same placeholder publicKey pattern as findOrCreateUserByIdentifier.
    */
🧹 Nitpick comments (2)
apps/api/src/auth/controllers/identity.controller.ts (2)

96-107: Google email used as identifierDisplay is not normalized, unlike the email-OTP path.

Line 156 normalizes email for the OTP flow (toLowerCase().trim()), but the Google flow passes googlePayload.email directly as identifierDisplay (Line 102) and into the JWT (Line 107). While the hash is correctly derived from sub (not email), the display value could carry inconsistent casing across flows. Consider normalizing for consistency:

Proposed fix
     // 2. Hash Google sub (immutable user ID) -- NOT email (which can change)
     const identifierHash = this.siweService.hashIdentifier(googlePayload.sub);
+    const normalizedEmail = googlePayload.email?.toLowerCase().trim() ?? googlePayload.email;

     // 3. Find or create user by hashed identifier
     const { user, isNewUser } = await this.findOrCreateUserByIdentifier(
       identifierHash,
-      googlePayload.email,
+      normalizedEmail,
       'google'
     );

     // 4. Sign CipherBox identity JWT (include email for auth method identifier)
-    const idToken = await this.jwtIssuerService.signIdentityJwt(user.id, googlePayload.email);
+    const idToken = await this.jwtIssuerService.signIdentityJwt(user.id, normalizedEmail);

307-346: findOrCreateUserByWallet duplicates most of findOrCreateUserByIdentifier.

The only differences are the hardcoded 'wallet' type and the truncateWalletAddress call for display. Consider whether this could delegate to findOrCreateUserByIdentifier with the truncated address as identifierDisplay, removing ~30 lines of duplication.

Sketch
  private async findOrCreateUserByWallet(
    addressHash: string,
    walletAddress: string
  ): Promise<{ user: User; isNewUser: boolean }> {
-   // ... duplicated lookup + create logic ...
+   const truncated = this.siweService.truncateWalletAddress(walletAddress);
+   return this.findOrCreateUserByIdentifier(addressHash, truncated, 'wallet');
  }

This would require widening the authMethodType union to include 'wallet'.

FSM1 and others added 2 commits February 14, 2026 19:03
…ation

- Normalize googlePayload.email for identifierDisplay and JWT signing
- Widen findOrCreateUserByIdentifier to accept 'wallet' type
- Remove duplicate findOrCreateUserByWallet method (~40 lines)
- Fix stale JSDoc reference to findOrCreateUserByEmail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SiweService.truncateEmail() — first 3 + "..." + last 2 of local
part + full domain (e.g. "michael@gmail.com" → "mic...el@gmail.com").
Short local parts (≤5 chars) shown in full.

Applied to all email identifierDisplay storage paths: Google login,
email OTP login, link method, test login, and safety net fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/auth/auth.service.spec.ts (1)

324-362: ⚠️ Potential issue | 🟠 Major

truncateWalletAddress mock is not configured — test assertion likely fails.

In this test, the safety-net code path in auth.service.ts calls this.siweService.truncateWalletAddress(identifier) for wallet-type identifiers (Line 100). However, truncateWalletAddress is initialized as a bare jest.fn() (Line 71) with no return value, so it returns undefined. The assertion on Line 359 expects identifierDisplay: walletAddr, which won't match.

Proposed fix — mock `truncateWalletAddress` for this test
      userRepository.findOne.mockResolvedValue(mockUser);
+     siweService.truncateWalletAddress.mockReturnValue('0xd8dA...6045');
      authMethodRepository.findOne
        .mockResolvedValueOnce(null) // identifierHash lookup
        .mockResolvedValueOnce(null); // userId fallback
      const savedMethod = {
        id: 'am-wallet',
        userId: 'user-id',
        type: 'wallet',
        identifier: identifierHash,
        identifierHash,
-       identifierDisplay: walletAddr,
+       identifierDisplay: '0xd8dA...6045',
        lastUsedAt: null,
      };

And update the assertion accordingly:

      expect(authMethodRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          userId: 'user-id',
          type: 'wallet',
          identifier: identifierHash,
          identifierHash,
-         identifierDisplay: walletAddr,
+         identifierDisplay: '0xd8dA...6045',
        })
      );
#!/bin/bash
# Verify that truncateWalletAddress mock has no default return in the test setup
rg -n 'truncateWalletAddress' apps/api/src/auth/auth.service.spec.ts
🤖 Fix all issues with AI agents
In `@apps/api/src/auth/auth.service.ts`:
- Around line 293-295: The code uses a fixed 'unknown' fallback for identifier
which can cause collisions; in the block that reads authMethodType =
linkDto.loginType and computes identifierHash via
createHash('sha256').update(identifier).digest('hex'), replace the 'unknown'
fallback with a guard that fails fast when neither payload.email nor payload.sub
is present (e.g., throw a BadRequestException or return an error) and only
compute identifierHash when a real identifier exists; this ensures
identifierHash is always derived from a real, unique identifier rather than the
constant 'unknown'.
- Line 83: The current fallback const identifier = payload.email || payload.sub
|| 'unknown' silently produces a deterministic collision; instead, validate
payload in the same scope (where identifier is computed) and throw a clear error
when neither payload.email nor payload.sub is present so you never hash a shared
'unknown' value; update the logic around identifier (the variable named
identifier in auth.service.ts) to reject/throw (e.g., throw new Error or custom
AuthError) when both are missing and ensure callers handle that exception.
- Around line 83-87: Replace the inline createHash usage with the centralized
siweService.hashIdentifier() to ensure a single hashing strategy: instead of
computing identifierHash via createHash('sha256') for the local variable
identifier (from payload.email || payload.sub || 'unknown') in auth.service.ts,
call this.siweService.hashIdentifier(identifier) and use that result in the
authMethod lookup (authMethodRepository.findOne where: { userId: user.id,
identifierHash }). Make the same change at the other two places in this file
where createHash is used (the occurrences you noted around the sections
currently at lines ~295 and ~479) so all identifier hashing goes through
siweService.hashIdentifier.

In `@apps/api/src/auth/controllers/identity.controller.ts`:
- Around line 96-105: normalizedEmail can be undefined and will cause TypeErrors
when passed to siweService.truncateEmail and later signIdentityJwt; update the
handler to first compute a safe normalizedEmail (e.g. const normalizedEmail =
googlePayload.email ? googlePayload.email.toLowerCase().trim() : ''), or
explicitly validate and return a 400/appropriate error when email is required,
then pass that safe value into findOrCreateUserByIdentifier and signIdentityJwt;
ensure references to siweService.truncateEmail, findOrCreateUserByIdentifier,
and signIdentityJwt handle the safe fallback or are skipped when email is
absent.
- Around line 287-293: The current two-step create in identity.controller.ts
uses a constant placeholder publicKey causing unique-constraint races; change
the initial save to use a unique temporary value (e.g.,
`pending-core-kit-${randomUUID()}`) so concurrent inserts won't collide: in the
block that calls this.userRepository.save({ publicKey:
`pending-core-kit-placeholder` }), generate a UUID (or use
crypto.randomUUID()/uuid.v4()) and save `{ publicKey:
\`pending-core-kit-${uuid}\` }`, then update newUser.publicKey =
`pending-core-kit-${newUser.id}` and save as before (reference:
userRepository.save and newUser.publicKey).
🧹 Nitpick comments (3)
apps/api/src/auth/auth.service.spec.ts (1)

16-19: Helper sha256Hex is duplicated across test files.

The same helper appears in both auth.service.spec.ts and identity.controller.spec.ts. Consider extracting it into a shared test utility to keep things DRY.

apps/api/src/auth/auth.service.ts (2)

325-333: truncateEmail called on potential non-email identifiers in link flow.

Line 331 calls truncateEmail(identifier) where identifier is payload.email || payload.sub. For Google linking, payload.sub could be an opaque ID like 'user-123'. While truncateEmail handles no-@ inputs gracefully (returns as-is per tests), the display value would be the raw sub string rather than a user-friendly identifier. Consider using the email specifically for display when available:

Proposed fix
-      identifierDisplay: this.siweService.truncateEmail(identifier),
+      identifierDisplay: payload.email
+        ? this.siweService.truncateEmail(payload.email)
+        : identifier,

354-356: Dynamic import('viem/siwe') in a method body — inconsistent with the static import in identity.controller.ts.

The top-level identity.controller.ts uses a static import for parseSiweMessage. Using a dynamic import here adds a minor first-call latency and is inconsistent. Consider a static import at the top of the file.

Comment thread apps/api/src/auth/auth.service.ts Outdated
Comment thread apps/api/src/auth/auth.service.ts Outdated
Comment thread apps/api/src/auth/auth.service.ts Outdated
Comment thread apps/api/src/auth/controllers/identity.controller.ts
Comment thread apps/api/src/auth/controllers/identity.controller.ts Outdated
…ralization, race condition

- Replace 'unknown' fallback with throw when JWT has no email/sub (prevents hash collisions)
- Centralize hashing through siweService.hashIdentifier() instead of inline createHash
- Use unique tempId for placeholder publicKey to prevent concurrent insert race
- Fix normalizedEmail undefined fallback for Google login (use '' not undefined)
- Use payload.email specifically for identifierDisplay in link flow
- Convert dynamic import('viem/siwe') to static import
- Fix truncateWalletAddress mock and assertion in wallet safety-net test
- Add test for missing identifier exception

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/auth/auth.service.ts (1)

99-113: ⚠️ Potential issue | 🟡 Minor

Safety-net type inference can never produce 'google'.

Line 102 infers only 'wallet' or 'email', so a Google-authenticated user hitting this safety net would get type: 'email'. The comment says this "shouldn't happen in practice," but if it does, subsequent identifierHash-based lookups by type would fail to match because the auth method was stored with the wrong type.

Consider adding a 'google' inference branch (e.g. checking if the identifier looks like a Google sub), or passing the type hint from the JWT payload.

🧹 Nitpick comments (2)
apps/api/src/auth/auth.service.ts (1)

333-341: identifierDisplay falls back to raw sub for non-email Google links.

When payload.email is falsy (e.g., a Google account without an email claim), Line 339 sets identifierDisplay to the raw identifier (which is payload.sub). A Google sub like "104578923..." isn't a useful display value. Consider using this.siweService.truncateEmail(identifier) only when it's actually an email, and provide a more informative fallback otherwise (e.g., 'Google account').

apps/api/src/auth/controllers/identity.controller.ts (1)

53-58: Redis connection created in constructor without error handling.

The Redis client is instantiated eagerly in the constructor with lazyConnect: true, but there's no connection error listener. If Redis becomes unavailable, the getWalletNonce and nonce-consumption calls will throw unhandled errors. Consider adding an error listener to log connection failures gracefully.

Suggested improvement
     this.redis = new Redis({
       host: configService.get('REDIS_HOST', 'localhost'),
       port: configService.get<number>('REDIS_PORT', 6379),
       password: configService.get('REDIS_PASSWORD', undefined),
       lazyConnect: true,
     });
+    this.redis.on('error', (err) => {
+      this.logger.error(`Redis connection error: ${err.message}`);
+    });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant