diff --git a/src/handlers/devices.ts b/src/handlers/devices.ts index 4a312b6..a07b3b4 100644 --- a/src/handlers/devices.ts +++ b/src/handlers/devices.ts @@ -48,6 +48,8 @@ function buildDeviceResponse(device: Device): DeviceResponse { creationDate: device.createdAt, RevisionDate: device.updatedAt, revisionDate: device.updatedAt, + LastActivityDate: device.lastSeenAt, + lastActivityDate: device.lastSeenAt, LastSeenAt: device.lastSeenAt, lastSeenAt: device.lastSeenAt, HasStoredDevice: true, diff --git a/src/handlers/fill-assist.ts b/src/handlers/fill-assist.ts new file mode 100644 index 0000000..3605449 --- /dev/null +++ b/src/handlers/fill-assist.ts @@ -0,0 +1,36 @@ +const EMPTY_FORMS_FILENAME = 'forms.v1.json'; +const EMPTY_FORMS_BODY = JSON.stringify({ + schemaVersion: '1.0.0', + hosts: {}, +}); +const EMPTY_MANIFEST_BODY = JSON.stringify({ + maps: { + forms: { + v1: { + filename: EMPTY_FORMS_FILENAME, + cid: 'sha256:nodewarden-empty-fill-assist-v1', + }, + }, + }, +}); + +function fillAssistJsonResponse(body: string): Response { + return new Response(body, { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'public, max-age=3600', + }, + }); +} + +export function handleFillAssistManifest(): Response { + return fillAssistJsonResponse(EMPTY_MANIFEST_BODY); +} + +export function handleFillAssistForms(filename: string): Response { + if (String(filename || '').trim() !== EMPTY_FORMS_FILENAME) { + return new Response('Not found', { status: 404 }); + } + return fillAssistJsonResponse(EMPTY_FORMS_BODY); +} diff --git a/src/router-public.ts b/src/router-public.ts index 051c441..b22c0c9 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -8,6 +8,7 @@ import { handleDownloadSendFile, } from './handlers/sends'; import { handleKnownDevice } from './handlers/devices'; +import { handleFillAssistForms, handleFillAssistManifest } from './handlers/fill-assist'; import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity'; import { handleGetAccountPasskeyAssertionOptions } from './handlers/account-passkeys'; import { @@ -97,6 +98,7 @@ function buildIconServiceCsp(origin: string): string { } function buildConfigResponse(origin: string) { + const fillAssistBase = `${origin}/fill-assist`; return { version: LIMITS.compatibility.bitwardenServerVersion, gitHash: 'nodewarden', @@ -109,7 +111,7 @@ function buildConfigResponse(origin: string) { notifications: origin + '/notifications', icons: origin, sso: '', - fillAssistRules: null, + fillAssistRules: fillAssistBase, }, push: { pushTechnology: 0, @@ -125,8 +127,10 @@ function buildConfigResponse(origin: string) { 'cipher-key-encryption': LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled, 'duo-redirect': true, 'email-verification': true, + 'fill-assist-targeting-rules': true, 'pm-19051-send-email-verification': false, 'pm-19148-innovation-archive': true, + 'pm-4516-devices-add-last-activity-date': true, 'pm-30529-webauthn-related-origins': true, 'unauth-ui-refresh': true, 'web-push': false, @@ -343,6 +347,19 @@ export async function handlePublicRoute( return jsonResponse(await buildWebBootstrapResponse(env)); } + if (path === '/fill-assist/manifest.json' && method === 'GET') { + const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); + if (blocked) return blocked; + return handleFillAssistManifest(); + } + + const fillAssistFormsMatch = path.match(/^\/fill-assist\/([^/]+)$/i); + if (fillAssistFormsMatch && method === 'GET') { + const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); + if (blocked) return blocked; + return handleFillAssistForms(fillAssistFormsMatch[1]); + } + const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i); if (iconMatch && method === 'GET') { const blocked = await enforcePublicRateLimit('public-icon', LIMITS.rateLimit.publicIconRequestsPerMinute); diff --git a/src/types/index.ts b/src/types/index.ts index c7ab2e2..d8db18b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -307,6 +307,7 @@ export interface DeviceResponse { type: number; creationDate: string; revisionDate: string; + lastActivityDate?: string | null; lastSeenAt?: string | null; hasStoredDevice?: boolean; isTrusted: boolean;