mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add compatibility for fido2Credentials counter and implement no-op device token update handler
This commit is contained in:
@@ -5,6 +5,31 @@ import { generateUUID } from '../utils/uuid';
|
|||||||
import { deleteAllAttachmentsForCipher } from './attachments';
|
import { deleteAllAttachmentsForCipher } from './attachments';
|
||||||
import { parsePagination, encodeContinuationToken } from '../utils/pagination';
|
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
|
// Format attachments for API response
|
||||||
export function formatAttachments(attachments: Attachment[]): any[] | null {
|
export function formatAttachments(attachments: Attachment[]): any[] | null {
|
||||||
if (attachments.length === 0) return 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 {
|
export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): CipherResponse {
|
||||||
// Strip internal-only fields that must not appear in the API response
|
// Strip internal-only fields that must not appear in the API response
|
||||||
const { userId, createdAt, updatedAt, deletedAt, ...passthrough } = cipher;
|
const { userId, createdAt, updatedAt, deletedAt, ...passthrough } = cipher;
|
||||||
|
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Pass through ALL stored cipher fields (known + unknown)
|
// Pass through ALL stored cipher fields (known + unknown)
|
||||||
@@ -48,6 +74,7 @@ export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = [])
|
|||||||
object: 'cipher',
|
object: 'cipher',
|
||||||
collectionIds: [],
|
collectionIds: [],
|
||||||
attachments: formatAttachments(attachments),
|
attachments: formatAttachments(attachments),
|
||||||
|
login: normalizedLogin,
|
||||||
encryptedFor: null,
|
encryptedFor: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -137,6 +164,7 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
cipher.login = normalizeCipherLoginForCompatibility(cipher.login);
|
||||||
|
|
||||||
await storage.saveCipher(cipher);
|
await storage.saveCipher(cipher);
|
||||||
await storage.updateRevisionDate(userId);
|
await storage.updateRevisionDate(userId);
|
||||||
@@ -179,6 +207,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
deletedAt: existingCipher.deletedAt,
|
deletedAt: existingCipher.deletedAt,
|
||||||
};
|
};
|
||||||
|
cipher.login = normalizeCipherLoginForCompatibility(cipher.login);
|
||||||
|
|
||||||
await storage.saveCipher(cipher);
|
await storage.saveCipher(cipher);
|
||||||
await storage.updateRevisionDate(userId);
|
await storage.updateRevisionDate(userId);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { StorageService } from '../services/storage';
|
|||||||
import { errorResponse } from '../utils/response';
|
import { errorResponse } from '../utils/response';
|
||||||
import { generateUUID } from '../utils/uuid';
|
import { generateUUID } from '../utils/uuid';
|
||||||
import { LIMITS } from '../config/limits';
|
import { LIMITS } from '../config/limits';
|
||||||
|
import { normalizeCipherLoginForCompatibility } from './ciphers';
|
||||||
|
|
||||||
// Bitwarden client import request format
|
// Bitwarden client import request format
|
||||||
interface CiphersImportRequest {
|
interface CiphersImportRequest {
|
||||||
@@ -221,6 +222,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
cipher.login = normalizeCipherLoginForCompatibility(cipher.login);
|
||||||
|
|
||||||
cipherRows.push(cipher);
|
cipherRows.push(cipher);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -38,7 +38,7 @@ import { handleSync } from './handlers/sync';
|
|||||||
|
|
||||||
// Setup handlers
|
// Setup handlers
|
||||||
import { handleSetupPage, handleSetupStatus, handleDisableSetup } from './handlers/setup';
|
import { handleSetupPage, handleSetupStatus, handleDisableSetup } from './handlers/setup';
|
||||||
import { handleKnownDevice, handleGetDevices } from './handlers/devices';
|
import { handleKnownDevice, handleGetDevices, handleUpdateDeviceToken } from './handlers/devices';
|
||||||
|
|
||||||
// Import handler
|
// Import handler
|
||||||
import { handleCiphersImport } from './handlers/import';
|
import { handleCiphersImport } from './handlers/import';
|
||||||
@@ -547,6 +547,13 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
|
|||||||
return handleGetDevices(request, env, userId);
|
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
|
// Not found
|
||||||
return errorResponse('Not found', 404);
|
return errorResponse('Not found', 404);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user