feat(encryptid): fix DID consistency, add PRF key derivation, stepper signup, and magic link login
- Fix DID mismatch: server now stores and reads proper did🔑z6Mk... DIDs from database instead of deriving truncated did🔑${slice(0,32)} - Add PRF extension to WebAuthn create/get flows for client-side key derivation - Derive DID, signing keys, encryption keys, and EOA wallet from passkey PRF - Auto-upgrade truncated DIDs to proper format on sign-in - Add POST /api/account/upgrade-did endpoint for DID migration - Add 5-step educational registration wizard (identity, passkey, DID, wallet, security) - Add email/username field to sign-in for scoped passkey selection - Add magic link email login for external devices without passkeys - Add POST /api/auth/magic-link and GET /magic-login verification page - Add mintWelcomeBalance() for 5 fUSDC to new users - Store EOA wallet address during registration when PRF available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
db61e54d7b
commit
8bd8348529
|
|
@ -150,3 +150,45 @@ export async function seedCUSDC() {
|
||||||
console.error('[TokenService] Failed to mint cUSDC to jeff');
|
console.error('[TokenService] Failed to mint cUSDC to jeff');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mint $5 fUSDC welcome balance for a new user. */
|
||||||
|
export function mintWelcomeBalance(did: string, username: string): boolean {
|
||||||
|
if (!_syncServer) {
|
||||||
|
console.warn('[TokenService] SyncServer not initialized, skipping welcome balance');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenId = 'fusdc';
|
||||||
|
const docId = tokenDocId(tokenId);
|
||||||
|
let doc = _syncServer.getDoc<TokenLedgerDoc>(docId);
|
||||||
|
|
||||||
|
// Create fUSDC token if it doesn't exist
|
||||||
|
if (!doc) {
|
||||||
|
doc = Automerge.change(Automerge.init<TokenLedgerDoc>(), 'init fUSDC ledger', (d) => {
|
||||||
|
const init = tokenLedgerSchema.init();
|
||||||
|
Object.assign(d, init);
|
||||||
|
});
|
||||||
|
_syncServer.setDoc(docId, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.token.name) {
|
||||||
|
_syncServer.changeDoc<TokenLedgerDoc>(docId, 'define fUSDC token', (d) => {
|
||||||
|
d.token.id = 'fusdc';
|
||||||
|
d.token.name = 'Fake USDC';
|
||||||
|
d.token.symbol = 'fUSDC';
|
||||||
|
d.token.decimals = 6;
|
||||||
|
d.token.description = 'Test stablecoin for new users — $5 welcome balance';
|
||||||
|
d.token.icon = '💵';
|
||||||
|
d.token.color = '#2775ca';
|
||||||
|
d.token.createdAt = Date.now();
|
||||||
|
d.token.createdBy = 'system';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mint 5 fUSDC (5 × 10^6 base units at 6 decimals)
|
||||||
|
const success = mintTokens(tokenId, did, username, 5_000_000, 'Welcome balance', 'system');
|
||||||
|
if (success) {
|
||||||
|
console.log(`[TokenService] Welcome balance: 5 fUSDC minted to ${username} (${did})`);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ export async function migrateSpaceMemberDid(oldDid: string, newDid: string): Pro
|
||||||
export interface StoredRecoveryToken {
|
export interface StoredRecoveryToken {
|
||||||
token: string;
|
token: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
type: 'email_verify' | 'account_recovery';
|
type: 'email_verify' | 'account_recovery' | 'magic_login';
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
expiresAt: number;
|
expiresAt: number;
|
||||||
used: boolean;
|
used: boolean;
|
||||||
|
|
@ -282,7 +282,7 @@ export async function getRecoveryToken(token: string): Promise<StoredRecoveryTok
|
||||||
return {
|
return {
|
||||||
token: row.token,
|
token: row.token,
|
||||||
userId: row.user_id,
|
userId: row.user_id,
|
||||||
type: row.type as 'email_verify' | 'account_recovery',
|
type: row.type as 'email_verify' | 'account_recovery' | 'magic_login',
|
||||||
createdAt: new Date(row.created_at).getTime(),
|
createdAt: new Date(row.created_at).getTime(),
|
||||||
expiresAt: new Date(row.expires_at).getTime(),
|
expiresAt: new Date(row.expires_at).getTime(),
|
||||||
used: row.used,
|
used: row.used,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ CREATE INDEX IF NOT EXISTS idx_challenges_expires_at ON challenges(expires_at);
|
||||||
CREATE TABLE IF NOT EXISTS recovery_tokens (
|
CREATE TABLE IF NOT EXISTS recovery_tokens (
|
||||||
token TEXT PRIMARY KEY,
|
token TEXT PRIMARY KEY,
|
||||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
type TEXT NOT NULL CHECK (type IN ('email_verify', 'account_recovery')),
|
type TEXT NOT NULL CHECK (type IN ('email_verify', 'account_recovery', 'magic_login')),
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
used BOOLEAN DEFAULT FALSE
|
used BOOLEAN DEFAULT FALSE
|
||||||
|
|
@ -65,6 +65,15 @@ CREATE TABLE IF NOT EXISTS recovery_tokens (
|
||||||
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_user_id ON recovery_tokens(user_id);
|
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_user_id ON recovery_tokens(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_expires_at ON recovery_tokens(expires_at);
|
CREATE INDEX IF NOT EXISTS idx_recovery_tokens_expires_at ON recovery_tokens(expires_at);
|
||||||
|
|
||||||
|
-- Migration: add magic_login type to existing recovery_tokens CHECK constraint
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE recovery_tokens DROP CONSTRAINT IF EXISTS recovery_tokens_type_check;
|
||||||
|
ALTER TABLE recovery_tokens ADD CONSTRAINT recovery_tokens_type_check
|
||||||
|
CHECK (type IN ('email_verify', 'account_recovery', 'magic_login'));
|
||||||
|
EXCEPTION WHEN others THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
-- Space membership: source of truth for user roles across the r*.online ecosystem
|
-- Space membership: source of truth for user roles across the r*.online ecosystem
|
||||||
CREATE TABLE IF NOT EXISTS space_members (
|
CREATE TABLE IF NOT EXISTS space_members (
|
||||||
space_slug TEXT NOT NULL,
|
space_slug TEXT NOT NULL,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue