feat: add compatibility for fido2Credentials counter and implement no-op device token update handler

This commit is contained in:
shuaiplus
2026-02-23 23:29:00 +08:00
parent 1dfa96611a
commit 08114762bc
4 changed files with 55 additions and 1 deletions
+29
View File
@@ -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);
+16
View File
@@ -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<Response> {
void request;
void env;
void userId;
void deviceIdentifier;
return new Response(null, { status: 200 });
}
+2
View File
@@ -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);
}
+8 -1
View File
@@ -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<Respons
return handleGetDevices(request, env, userId);
}
// Device push token endpoint (no-op compatibility handler)
const deviceTokenMatch = path.match(/^\/api\/devices\/identifier\/([^/]+)\/token$/i);
if (deviceTokenMatch && (method === 'PUT' || method === 'POST')) {
const deviceIdentifier = decodeURIComponent(deviceTokenMatch[1]);
return handleUpdateDeviceToken(request, env, userId, deviceIdentifier);
}
// Not found
return errorResponse('Not found', 404);