diff --git a/src/handlers/identity.ts b/src/handlers/identity.ts index eb9e2d9..6603c09 100644 --- a/src/handlers/identity.ts +++ b/src/handlers/identity.ts @@ -18,9 +18,7 @@ function twoFactorRequiredResponse(message: string = 'Two factor required.'): Re error_description: message, TwoFactorProviders: [0], TwoFactorProviders2: { - '0': { - Priority: 1, - }, + '0': null, }, ErrorModel: { Message: message, @@ -115,6 +113,10 @@ export async function handleToken(request: Request, env: Env): Promise // Optional 2FA: enabled only when TOTP_SECRET is configured in Workers env. let trustedTwoFactorTokenToReturn: string | undefined; if (isTotpEnabled(env.TOTP_SECRET)) { + if (twoFactorProvider !== undefined && String(twoFactorProvider) !== '0') { + return identityErrorResponse('Unsupported two-factor provider', 'invalid_grant', 400); + } + const rememberRequested = ['1', 'true', 'True', 'TRUE', 'on', 'yes', 'Yes', 'YES'].includes(String(twoFactorRemember || '').trim()); // Bitwarden may reuse twoFactorToken as a remembered-device token on subsequent logins. @@ -142,7 +144,7 @@ export async function handleToken(request: Request, env: Env): Promise 429 ); } - return identityErrorResponse('Invalid two-factor token', 'invalid_grant', 400); + return twoFactorRequiredResponse(); } } @@ -287,3 +289,30 @@ export async function handlePrelogin(request: Request, env: Env): Promise { + const storage = new StorageService(env.DB); + + let body: Record; + const contentType = request.headers.get('content-type') || ''; + try { + if (contentType.includes('application/x-www-form-urlencoded')) { + const formData = await request.formData(); + body = Object.fromEntries(formData.entries()) as Record; + } else { + body = await request.json(); + } + } catch { + return new Response(null, { status: 200 }); + } + + const token = String(body.token || '').trim(); + if (token) { + await storage.deleteRefreshToken(token); + } + + return new Response(null, { status: 200 }); +} diff --git a/src/handlers/import.ts b/src/handlers/import.ts index 30317ed..e3630a0 100644 --- a/src/handlers/import.ts +++ b/src/handlers/import.ts @@ -8,10 +8,12 @@ import { LIMITS } from '../config/limits'; interface CiphersImportRequest { ciphers: Array<{ type: number; - name: string; + name?: string | null; notes?: string | null; favorite?: boolean; reprompt?: number; + sshKey?: any | null; + key?: string | null; login?: { uris?: Array<{ uri: string | null; match?: number | null }> | null; username?: string | null; @@ -62,6 +64,7 @@ interface CiphersImportRequest { password: string; lastUsedDate: string; }> | null; + [key: string]: any; }>; folders: Array<{ name: string; @@ -153,61 +156,65 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st userId: userId, type: c.type as CipherType, folderId: folderId, - name: c.name || 'Untitled', - notes: c.notes || null, - favorite: c.favorite || false, + name: c.name ?? 'Untitled', + notes: c.notes ?? null, + favorite: c.favorite ?? false, login: c.login ? { ...c.login, - username: c.login.username || null, - password: c.login.password || null, + username: c.login.username ?? null, + password: c.login.password ?? null, uris: c.login.uris?.map(u => ({ - uri: u.uri || null, + ...u, + uri: u.uri ?? null, uriChecksum: null, match: u.match ?? null, })) || null, - totp: c.login.totp || null, + totp: c.login.totp ?? null, autofillOnPageLoad: c.login.autofillOnPageLoad ?? null, fido2Credentials: c.login.fido2Credentials ?? null, uri: c.login.uri ?? null, passwordRevisionDate: c.login.passwordRevisionDate ?? null, } : null, card: c.card ? { - cardholderName: c.card.cardholderName || null, - brand: c.card.brand || null, - number: c.card.number || null, - expMonth: c.card.expMonth || null, - expYear: c.card.expYear || null, - code: c.card.code || null, + ...c.card, + cardholderName: c.card.cardholderName ?? null, + brand: c.card.brand ?? null, + number: c.card.number ?? null, + expMonth: c.card.expMonth ?? null, + expYear: c.card.expYear ?? null, + code: c.card.code ?? null, } : null, identity: c.identity ? { - title: c.identity.title || null, - firstName: c.identity.firstName || null, - middleName: c.identity.middleName || null, - lastName: c.identity.lastName || null, - address1: c.identity.address1 || null, - address2: c.identity.address2 || null, - address3: c.identity.address3 || null, - city: c.identity.city || null, - state: c.identity.state || null, - postalCode: c.identity.postalCode || null, - country: c.identity.country || null, - company: c.identity.company || null, - email: c.identity.email || null, - phone: c.identity.phone || null, - ssn: c.identity.ssn || null, - username: c.identity.username || null, - passportNumber: c.identity.passportNumber || null, - licenseNumber: c.identity.licenseNumber || null, + ...c.identity, + title: c.identity.title ?? null, + firstName: c.identity.firstName ?? null, + middleName: c.identity.middleName ?? null, + lastName: c.identity.lastName ?? null, + address1: c.identity.address1 ?? null, + address2: c.identity.address2 ?? null, + address3: c.identity.address3 ?? null, + city: c.identity.city ?? null, + state: c.identity.state ?? null, + postalCode: c.identity.postalCode ?? null, + country: c.identity.country ?? null, + company: c.identity.company ?? null, + email: c.identity.email ?? null, + phone: c.identity.phone ?? null, + ssn: c.identity.ssn ?? null, + username: c.identity.username ?? null, + passportNumber: c.identity.passportNumber ?? null, + licenseNumber: c.identity.licenseNumber ?? null, } : null, - secureNote: c.secureNote || null, + secureNote: c.secureNote ?? null, fields: c.fields?.map(f => ({ - name: f.name || null, - value: f.value || null, + ...f, + name: f.name ?? null, + value: f.value ?? null, type: f.type, linkedId: f.linkedId ?? null, })) || null, - passwordHistory: c.passwordHistory || null, - reprompt: c.reprompt || 0, + passwordHistory: c.passwordHistory ?? null, + reprompt: c.reprompt ?? 0, sshKey: (c as any).sshKey ?? null, key: (c as any).key ?? null, createdAt: now, diff --git a/src/index.ts b/src/index.ts index 0847301..85ccd3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,21 +7,6 @@ let dbInitialized = false; let dbInitError: string | null = null; let dbInitPromise: Promise | null = null; -function shouldSkipDatabaseInit(request: Request): boolean { - const url = new URL(request.url); - const path = url.pathname; - const method = request.method; - - if (method === 'OPTIONS') return true; - if (method === 'GET' && (path === '/favicon.ico' || path === '/favicon.svg')) return true; - if (method === 'GET' && path === '/.well-known/appspecific/com.chrome.devtools.json') return true; - if (method === 'GET' && path.startsWith('/icons/')) return true; - if (path.startsWith('/notifications/')) return true; - if (method === 'GET' && (path === '/config' || path === '/api/config' || path === '/api/version')) return true; - - return false; -} - async function ensureDatabaseInitialized(env: Env): Promise { if (dbInitialized) return; @@ -47,24 +32,20 @@ async function ensureDatabaseInitialized(env: Env): Promise { export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { void ctx; - const requiresDatabase = !shouldSkipDatabaseInit(request); - - if (requiresDatabase) { - await ensureDatabaseInitialized(env); - if (dbInitError) { - const resp = jsonResponse( - { - error: 'Database not initialized', - error_description: dbInitError, - ErrorModel: { - Message: dbInitError, - Object: 'error', - }, + await ensureDatabaseInitialized(env); + if (dbInitError) { + const resp = jsonResponse( + { + error: 'Database not initialized', + error_description: dbInitError, + ErrorModel: { + Message: dbInitError, + Object: 'error', }, - 500 - ); - return applyCors(request, resp); - } + }, + 500 + ); + return applyCors(request, resp); } const resp = await handleRequest(request, env); diff --git a/src/router.ts b/src/router.ts index e5341df..dc49840 100644 --- a/src/router.ts +++ b/src/router.ts @@ -5,7 +5,7 @@ import { handleCors, errorResponse, jsonResponse } from './utils/response'; import { LIMITS } from './config/limits'; // Identity handlers -import { handleToken, handlePrelogin } from './handlers/identity'; +import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity'; // Account handlers import { handleRegister, handleGetProfile, handleUpdateProfile, handleSetKeys, handleGetRevisionDate, handleVerifyPassword } from './handlers/accounts'; @@ -229,6 +229,10 @@ export async function handleRequest(request: Request, env: Env): Promise { if (StorageService.schemaVerified) return; await this.db.prepare('PRAGMA foreign_keys = ON').run(); await this.db.prepare('CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run(); - - const schemaHash = await this.sha256Hex(SCHEMA_STATEMENTS.join('\n')); - const current = await this.db.prepare('SELECT value FROM config WHERE key = ?') - .bind(SCHEMA_HASH_CONFIG_KEY) - .first<{ value: string }>(); - - if (current?.value !== schemaHash) { - for (const stmt of SCHEMA_STATEMENTS) { - await this.executeSchemaStatement(stmt); - } - - await this.db.prepare( - 'INSERT INTO config(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value' - ) - .bind(SCHEMA_HASH_CONFIG_KEY, schemaHash) - .run(); + for (const stmt of SCHEMA_STATEMENTS) { + await this.executeSchemaStatement(stmt); } StorageService.schemaVerified = true;