feat: implement NotificationsHub for real-time vault sync notifications

- Added NotificationsHub durable object to handle WebSocket connections for vault sync notifications.
- Integrated SignalR protocol for message framing and communication.
- Updated storage service methods to return revision date and user ID for vault sync notifications.
- Enhanced existing handlers (attachments, ciphers, folders, sends, and import) to notify users of vault sync events.
- Created new notifications handler for WebSocket negotiation and binding user IDs.
- Updated frontend to establish WebSocket connection for receiving vault sync notifications.
- Improved CORS headers to support new notification endpoints.
- Bumped wrangler version in package.json to 4.71.0.
This commit is contained in:
shuaiplus
2026-03-09 00:25:34 +08:00
parent 54cf1ff718
commit 899f1004a3
18 changed files with 779 additions and 76 deletions
+4
View File
@@ -72,3 +72,7 @@ export function readKnownDeviceProbe(request: Request): { email: string | null;
return { email, deviceIdentifier };
}
export function readActingDeviceIdentifier(request: Request): string | null {
return normalizeDeviceIdentifier(request.headers.get('X-NodeWarden-Acting-Device-Id'));
}
+28 -2
View File
@@ -1,7 +1,21 @@
import { LIMITS } from '../config/limits';
const CORS_METHODS = 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
const CORS_HEADERS = 'Content-Type, Authorization, Accept, Device-Type, Bitwarden-Client-Name, Bitwarden-Client-Version, X-Request-Email, X-Device-Identifier, X-Device-Name';
const DEFAULT_CORS_HEADERS = [
'Content-Type',
'Authorization',
'Accept',
'Device-Type',
'Device-Identifier',
'Device-Name',
'Bitwarden-Client-Name',
'Bitwarden-Client-Version',
'Bitwarden-Package-Type',
'Is-Prerelease',
'X-Request-Email',
'X-Device-Identifier',
'X-Device-Name',
];
function isTrustedClientOrigin(origin: string): boolean {
// Official browser extension / desktop-webview common origins.
@@ -25,9 +39,15 @@ function getAllowedOrigin(request: Request): string | null {
}
function buildCorsHeaders(request: Request): Record<string, string> {
const requestedHeaders = String(request.headers.get('Access-Control-Request-Headers') || '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const allowHeaders = Array.from(new Set([...DEFAULT_CORS_HEADERS, ...requestedHeaders]));
const headers: Record<string, string> = {
'Access-Control-Allow-Methods': CORS_METHODS,
'Access-Control-Allow-Headers': CORS_HEADERS,
'Access-Control-Allow-Headers': allowHeaders.join(', '),
'Access-Control-Max-Age': String(LIMITS.cors.preflightMaxAgeSeconds),
};
@@ -44,6 +64,12 @@ export function applyCors(
request: Request,
response: Response
): Response {
// WebSocket upgrade responses must be returned untouched.
const webSocket = (response as Response & { webSocket?: unknown }).webSocket;
if (response.status === 101 || webSocket) {
return response;
}
const headers = new Headers(response.headers);
const corsHeaders = buildCorsHeaders(request);
for (const [k, v] of Object.entries(corsHeaders)) {