Add Bitwarden push relay support

This commit is contained in:
shuaiplus
2026-06-22 22:09:38 +08:00
parent 9a21504f40
commit 79ed7c9f85
12 changed files with 492 additions and 31 deletions
+66 -3
View File
@@ -1,4 +1,5 @@
import type { Device, TrustedDeviceTokenSummary, User } from '../types';
import { generateUUID } from '../utils/uuid';
type GetUserByEmail = (email: string) => Promise<User | null>;
type TrustedTokenKeyFn = (token: string) => Promise<string>;
@@ -14,6 +15,8 @@ function mapDeviceRow(row: any): Device {
encryptedUserKey: row.encrypted_user_key ?? null,
encryptedPublicKey: row.encrypted_public_key ?? null,
encryptedPrivateKey: row.encrypted_private_key ?? null,
pushUuid: row.push_uuid ?? null,
pushToken: row.push_token ?? null,
lastSeenAt: row.last_seen_at ?? null,
createdAt: row.created_at,
updatedAt: row.updated_at,
@@ -38,13 +41,15 @@ export async function upsertDevice(
const existingDevice = await getDeviceById(userId, deviceIdentifier);
const effectiveSessionStamp = String(sessionStamp || '').trim() || existingDevice?.sessionStamp || '';
const effectiveName = String(name || '').trim() || String(existingDevice?.name || '').trim();
const effectivePushUuid = String(existingDevice?.pushUuid || '').trim() || generateUUID();
await db
.prepare(
'INSERT INTO devices(user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, banned, banned_at, device_note, last_seen_at, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, ?) ' +
'INSERT INTO devices(user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, push_uuid, banned, banned_at, device_note, last_seen_at, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, ?) ' +
'ON CONFLICT(user_id, device_identifier) DO UPDATE SET name=excluded.name, type=excluded.type, session_stamp=excluded.session_stamp, ' +
'encrypted_user_key=COALESCE(excluded.encrypted_user_key, encrypted_user_key), ' +
'encrypted_public_key=COALESCE(excluded.encrypted_public_key, encrypted_public_key), ' +
'encrypted_private_key=COALESCE(excluded.encrypted_private_key, encrypted_private_key), ' +
'push_uuid=COALESCE(push_uuid, excluded.push_uuid), ' +
'last_seen_at=excluded.last_seen_at, ' +
'updated_at=excluded.updated_at'
)
@@ -57,6 +62,7 @@ export async function upsertDevice(
keys?.encryptedUserKey ?? null,
keys?.encryptedPublicKey ?? null,
keys?.encryptedPrivateKey ?? null,
effectivePushUuid,
existingDevice?.deviceNote ?? null,
now,
now,
@@ -166,7 +172,7 @@ export async function isKnownDeviceByEmail(
export async function getDevicesByUserId(db: D1Database, userId: string): Promise<Device[]> {
const res = await db
.prepare(
'SELECT user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, banned, banned_at, device_note, last_seen_at, created_at, updated_at ' +
'SELECT user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, push_uuid, push_token, banned, banned_at, device_note, last_seen_at, created_at, updated_at ' +
'FROM devices WHERE user_id = ? ORDER BY COALESCE(last_seen_at, created_at) DESC, updated_at DESC'
)
.bind(userId)
@@ -177,7 +183,7 @@ export async function getDevicesByUserId(db: D1Database, userId: string): Promis
export async function getDevice(db: D1Database, userId: string, deviceIdentifier: string): Promise<Device | null> {
const row = await db
.prepare(
'SELECT user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, banned, banned_at, device_note, last_seen_at, created_at, updated_at ' +
'SELECT user_id, device_identifier, name, type, session_stamp, encrypted_user_key, encrypted_public_key, encrypted_private_key, push_uuid, push_token, banned, banned_at, device_note, last_seen_at, created_at, updated_at ' +
'FROM devices WHERE user_id = ? AND device_identifier = ? LIMIT 1'
)
.bind(userId, deviceIdentifier)
@@ -185,6 +191,63 @@ export async function getDevice(db: D1Database, userId: string, deviceIdentifier
return row ? mapDeviceRow(row) : null;
}
export async function updateDevicePushToken(
db: D1Database,
userId: string,
deviceIdentifier: string,
pushUuid: string,
pushToken: string
): Promise<boolean> {
const now = new Date().toISOString();
const result = await db
.prepare(
'UPDATE devices SET push_uuid = ?, push_token = ?, updated_at = ? ' +
'WHERE user_id = ? AND device_identifier = ?'
)
.bind(pushUuid, pushToken, now, userId, deviceIdentifier)
.run();
return Number(result.meta.changes ?? 0) > 0;
}
export async function clearDevicePushToken(
db: D1Database,
userId: string,
deviceIdentifier: string
): Promise<{ pushUuid: string | null } | null> {
const existing = await db
.prepare('SELECT push_uuid FROM devices WHERE user_id = ? AND device_identifier = ? LIMIT 1')
.bind(userId, deviceIdentifier)
.first<{ push_uuid: string | null }>();
if (!existing) return null;
await db
.prepare('UPDATE devices SET push_token = NULL, updated_at = ? WHERE user_id = ? AND device_identifier = ?')
.bind(new Date().toISOString(), userId, deviceIdentifier)
.run();
return { pushUuid: existing.push_uuid ?? null };
}
export async function getDevicePushUuid(
db: D1Database,
userId: string,
deviceIdentifier: string
): Promise<string | null> {
const row = await db
.prepare('SELECT push_uuid FROM devices WHERE user_id = ? AND device_identifier = ? LIMIT 1')
.bind(userId, deviceIdentifier)
.first<{ push_uuid: string | null }>();
return row?.push_uuid ?? null;
}
export async function userHasPushDevice(db: D1Database, userId: string): Promise<boolean> {
const row = await db
.prepare('SELECT 1 FROM devices WHERE user_id = ? AND push_token IS NOT NULL AND push_token <> ? LIMIT 1')
.bind(userId, '')
.first<{ '1': number }>();
return !!row;
}
export async function deleteDevice(db: D1Database, userId: string, deviceIdentifier: string): Promise<boolean> {
const result = await db
.prepare('DELETE FROM devices WHERE user_id = ? AND device_identifier = ?')