Skip to content

Conversation

@islandbitcoin
Copy link
Contributor

@islandbitcoin islandbitcoin commented Sep 16, 2025

Summary

Implements email-only authentication flow for new user registration, allowing users to sign up with just an email address (no phone required).

Key Changes:

  • New GraphQL mutations: newUserEmailRegistrationInitiate and newUserEmailRegistrationValidate
  • Kratos identity schema for email-only accounts (email_no_password_v0)
  • Account upgrade path: device accounts can upgrade to email accounts
  • Registration payload validation for email-based flows

How It Works

  1. Client calls newUserEmailRegistrationInitiate with email
  2. Backend creates Kratos identity, sends OTP via recovery flow
  3. Client calls newUserEmailRegistrationValidate with flowId + code
  4. Backend validates code, creates account if new, returns auth token

Files Changed (43 files, +855/-216)

Core Implementation:

  • src/graphql/public/root/mutation/new-user-email-registration-*.ts - GraphQL mutations
  • src/app/authentication/email.ts - Email authentication logic
  • src/services/kratos/auth-email-no-password.ts - Kratos integration
  • src/app/accounts/create-account.ts - Account creation updates
  • src/app/accounts/upgrade-device-account.ts - Device → Email upgrade
  • src/domain/authentication/registration-payload-validator.ts - Validation logic
  • dev/ory/kratos.yml - Kratos configuration

Test Infrastructure:

  • Fixed TypeScript errors from main rebase (branded types, mock updates, API changes)

Known Issues / Follow-up Tickets

🔴 HIGH PRIORITY (security review needed)

  1. Account Enumeration Vulnerability

    • Location: src/services/kratos/auth-email-no-password.ts lines 73-78
    • Issue: createIdentityForEmailRegistration() reveals whether email is already registered
    • Mitigation: Should return consistent response regardless of email existence
  2. Missing TOTP Flow Completion

    • Location: newUserEmailRegistrationValidate returns totpRequired but no follow-up verification
    • Issue: If TOTP is required, there's no mutation to complete the flow
    • Needs: Document expected client behavior or implement TOTP verification step
  3. Race Condition in Account Creation

    • Location: new-user-email-registration-validate.ts lines 63-78
    • Issue: Concurrent code validations could create duplicate accounts
    • Mitigation: Add distributed lock or database constraint

🟡 MEDIUM PRIORITY (tech debt)

  1. Inconsistent Error Handling - "dead branch" error message (line 162-163)
  2. Hardcoded Schema ID - Should use SchemaIdType.EmailNoPasswordV0 enum
  3. Known Account Enumeration TODO - FIXME at lines 415-419 not addressed
  4. Missing Transaction Handling - Multi-step operations in upgrade-device-account.ts

🟢 LOW PRIORITY

  1. Unused import pattern in validate mutation
  2. Undocumented GraphQL complexity value (120)
  3. Direct Kratos DB access (documented workaround for issue #3163)

Testing

  • TypeScript compiles (yarn tsc --noEmit - 0 errors)
  • Integration tests (pre-existing failures unrelated to this PR)

Checklist

  • Code follows project conventions
  • TypeScript errors resolved
  • Security issues documented for follow-up
  • Ready for review

@islandbitcoin islandbitcoin requested a review from brh28 September 16, 2025 20:00
@islandbitcoin islandbitcoin self-assigned this Sep 16, 2025
@islandbitcoin islandbitcoin added the enhancement New feature or request label Sep 16, 2025
@islandbitcoin islandbitcoin force-pushed the feat/email-registration branch from 81894da to 66e733f Compare September 17, 2025 15:54
Copy link
Contributor

@brh28 brh28 left a comment

Choose a reason for hiding this comment

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

There's already the following definition in the graphql schema, which appears to the same or similar to what's being done in this PR:

  userEmailRegistrationInitiate(input: UserEmailRegistrationInitiateInput!): UserEmailRegistrationInitiatePayload!
  userEmailRegistrationValidate(input: UserEmailRegistrationValidateInput!): UserEmailRegistrationValidatePayload!

@islandbitcoin
Copy link
Contributor Author

There's already the following definition in the graphql schema, which appears to the same or similar to what's being done in this PR:

  userEmailRegistrationInitiate(input: UserEmailRegistrationInitiateInput!): UserEmailRegistrationInitiatePayload!
  userEmailRegistrationValidate(input: UserEmailRegistrationValidateInput!): UserEmailRegistrationValidatePayload!

These definitions do not allow for a email registration without an existing account. The choice was to modify these, to handle both cases (with account and without account) or create a new schema definition for new accounts.

@islandbitcoin islandbitcoin force-pushed the feat/email-registration branch from 66e733f to 90f293e Compare October 17, 2025 22:08
@islandbitcoin islandbitcoin requested a review from brh28 October 17, 2025 22:33
@islandbitcoin
Copy link
Contributor Author

@brh28 please review

// so that if one fails, the other is rolled back

// 1. Update user record with email (deviceId is preserved via spread)
const userUpdated = await UsersRepository().findById(userId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Lines 60-65 can be consolidated to a single database update. The function would be something like:

addEmail: (userId, email) => db.updateOne( { userId }, { $set: { email })

userId: UserId
email: EmailAddress
}): Promise<Account | RepositoryError> => {
// TODO: ideally both 1. and 2. should be done in a transaction,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe there's a way to make this atomic unless we completely rewrite our data model


import { createAccountWithEmailIdentifier } from "@app/accounts"

export const createAccountFromEmailRegistrationPayload = async ({
Copy link
Contributor

Choose a reason for hiding this comment

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

where is this function being called?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nowhere! apparently its leftover from a previous attempt at getting this to work. Removed the file and the export reference

Copy link
Contributor

@brh28 brh28 left a comment

Choose a reason for hiding this comment

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

Can we do a code walk through on this one? I'm having a hard time following

@brh28
Copy link
Contributor

brh28 commented Oct 30, 2025

Related to: #237

@islandbitcoin
Copy link
Contributor Author

@brh28 I think this is ready for a code review again, since we patched the orphaned accounts issue.

@islandbitcoin islandbitcoin changed the title [feat] account creation using only email (no phone) feat/email-registration Dec 29, 2025
const accountsRepo = AccountsRepository()
let account = await accountsRepo.findByUserId(kratosUserId)

if (account instanceof Error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

probably should be checking for a specific response, such as AccountNotFoundError

} else {
// Create new identity with email
const createIdentityBody = {
credentials: { password: { config: { password } } },
Copy link
Contributor

Choose a reason for hiding this comment

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

does password have a value?

}

// Send OTP code via recovery flow
const { data: recoveryFlow } = await kratosPublic.createNativeRecoveryFlow()
Copy link
Contributor

Choose a reason for hiding this comment

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

verify we want createNativeRecoveryFlow rather than createNativeRegistrationFlow

type: [String],
},
deviceId: {
email: {
Copy link
Contributor

Choose a reason for hiding this comment

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

emails are already stored in Kratos. Is there any reason not to use the postrgres database here?

  - New GraphQL mutations: newUserEmailRegistrationInitiate and newUserEmailRegistrationValidate for email-only account creation
  - Kratos integration: Successfully using email recovery flow for OTP delivery
  - Account creation: Fixed critical bug where accounts weren't being created after validation
  - Code cleanup: Removed all debug console.log statements

  ✅ Key Changes Made

  1. Fixed validation logic - Changed from checking User existence to Account existence
  2. Proper account creation - Creates account with wallets when none exists
  3. Clean production code - Removed debug statements for production readiness
This commit consolidates all TypeScript fixes required after rebasing
the email-registration feature branch on main:

- Update core Account and Wallet mocks with mandatory properties (npub, lnurlp)
- Adapt to upstream API changes (@ory/client IdentityState → IdentityStateEnum)
- Fix test infrastructure mock type conversions and exports
- Replace deleted BTC wallet functions with USD equivalents
- Use factory functions for branded types (OnChainAddress, FractionalCentAmount)
- Fix OffersManager constructability and CSV export method names
- Add type casts for transaction and wallet type incompatibilities

Result: yarn tsc --noEmit returns 0 errors
@islandbitcoin islandbitcoin force-pushed the feat/email-registration branch from fdea49c to 204ac45 Compare January 27, 2026 12:28
@brh28 brh28 self-requested a review January 27, 2026 15:25
islandbitcoin added a commit that referenced this pull request Jan 27, 2026
…lidation deferral

- Add ValidationError import to redeem-invite.ts to fix TypeScript errors
- Document that email validation is deferred until email-only registration
  feature is available (see PR #212)
@islandbitcoin
Copy link
Contributor Author

Closed as deferred

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants