From 431cc0d5d7189ca5b2c7a36ce13b702da90dc56f Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Mon, 23 Feb 2026 23:29:00 +0800 Subject: [PATCH] feat: add compatibility for fido2Credentials counter and implement no-op device token update handler --- src/handlers/ciphers.ts | 29 +++++++++++++++++++++++++++++ src/handlers/devices.ts | 16 ++++++++++++++++ src/handlers/import.ts | 2 ++ src/router.ts | 9 ++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 6abe6ac..db14cb0 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -5,6 +5,31 @@ import { generateUUID } from '../utils/uuid'; import { deleteAllAttachmentsForCipher } from './attachments'; import { parsePagination, encodeContinuationToken } from '../utils/pagination'; +// Android 2026.2.0 expects fido2Credentials[].counter to be a string. +export function normalizeCipherLoginForCompatibility(login: any): any { + if (!login || typeof login !== 'object') return login ?? null; + + const fido2 = Array.isArray(login.fido2Credentials) + ? login.fido2Credentials.map((cred: any) => { + if (!cred || typeof cred !== 'object') return cred; + const rawCounter = cred.counter; + const counter = + rawCounter === null || rawCounter === undefined + ? '0' + : String(rawCounter); + return { + ...cred, + counter, + }; + }) + : login.fido2Credentials; + + return { + ...login, + fido2Credentials: fido2, + }; +} + // Format attachments for API response export function formatAttachments(attachments: Attachment[]): any[] | null { if (attachments.length === 0) return null; @@ -27,6 +52,7 @@ export function formatAttachments(attachments: Attachment[]): any[] | null { export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): CipherResponse { // Strip internal-only fields that must not appear in the API response const { userId, createdAt, updatedAt, deletedAt, ...passthrough } = cipher; + const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null); return { // Pass through ALL stored cipher fields (known + unknown) @@ -48,6 +74,7 @@ export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []) object: 'cipher', collectionIds: [], attachments: formatAttachments(attachments), + login: normalizedLogin, encryptedFor: null, }; } @@ -137,6 +164,7 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str updatedAt: now, deletedAt: null, }; + cipher.login = normalizeCipherLoginForCompatibility(cipher.login); await storage.saveCipher(cipher); await storage.updateRevisionDate(userId); @@ -179,6 +207,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str updatedAt: new Date().toISOString(), deletedAt: existingCipher.deletedAt, }; + cipher.login = normalizeCipherLoginForCompatibility(cipher.login); await storage.saveCipher(cipher); await storage.updateRevisionDate(userId); diff --git a/src/handlers/devices.ts b/src/handlers/devices.ts index 9187ac4..9339044 100644 --- a/src/handlers/devices.ts +++ b/src/handlers/devices.ts @@ -40,3 +40,19 @@ export async function handleGetDevices(request: Request, env: Env, userId: strin }); } +// PUT /api/devices/identifier/{deviceIdentifier}/token +// Bitwarden mobile reports push token updates to this endpoint. +// NodeWarden does not implement push notifications, so accept and no-op. +export async function handleUpdateDeviceToken( + request: Request, + env: Env, + userId: string, + deviceIdentifier: string +): Promise { + void request; + void env; + void userId; + void deviceIdentifier; + return new Response(null, { status: 200 }); +} + diff --git a/src/handlers/import.ts b/src/handlers/import.ts index e3630a0..ff8d046 100644 --- a/src/handlers/import.ts +++ b/src/handlers/import.ts @@ -3,6 +3,7 @@ import { StorageService } from '../services/storage'; import { errorResponse } from '../utils/response'; import { generateUUID } from '../utils/uuid'; import { LIMITS } from '../config/limits'; +import { normalizeCipherLoginForCompatibility } from './ciphers'; // Bitwarden client import request format interface CiphersImportRequest { @@ -221,6 +222,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st updatedAt: now, deletedAt: null, }; + cipher.login = normalizeCipherLoginForCompatibility(cipher.login); cipherRows.push(cipher); } diff --git a/src/router.ts b/src/router.ts index c8226dc..f34a6ca 100644 --- a/src/router.ts +++ b/src/router.ts @@ -38,7 +38,7 @@ import { handleSync } from './handlers/sync'; // Setup handlers import { handleSetupPage, handleSetupStatus, handleDisableSetup } from './handlers/setup'; -import { handleKnownDevice, handleGetDevices } from './handlers/devices'; +import { handleKnownDevice, handleGetDevices, handleUpdateDeviceToken } from './handlers/devices'; // Import handler import { handleCiphersImport } from './handlers/import'; @@ -547,6 +547,13 @@ export async function handleRequest(request: Request, env: Env): Promise