fix: align fill assist compatibility

This commit is contained in:
shuaiplus
2026-07-06 02:09:28 +08:00
parent f0e523376c
commit 56b301f2d1
4 changed files with 64 additions and 6 deletions
+48 -4
View File
@@ -1,19 +1,46 @@
const EMPTY_FORMS_FILENAME = 'forms.v1.json'; const EMPTY_FORMS_FILENAME = 'forms.v1.json';
const EMPTY_FORMS_SCHEMA_FILENAME = 'forms.v1.schema.json';
const EMPTY_FORMS_CID = 'sha256:189fa7c9bcf8951e65c18b5d9feacf74a5223c75e01667c4235388cbc67091fe';
const EMPTY_FORMS_BODY = JSON.stringify({ const EMPTY_FORMS_BODY = JSON.stringify({
schemaVersion: '1.0.0', schemaVersion: '1.0.0',
hosts: {}, hosts: {},
}); });
const EMPTY_FORMS_SCHEMA_BODY = JSON.stringify({
$schema: 'https://json-schema.org/draft/2020-12/schema',
title: 'Bitwarden Fill Assist Forms v1',
type: 'object',
required: ['schemaVersion', 'hosts'],
properties: {
schemaVersion: { type: 'string' },
hosts: { type: 'object' },
},
additionalProperties: true,
});
const EMPTY_MANIFEST_BODY = JSON.stringify({ const EMPTY_MANIFEST_BODY = JSON.stringify({
buildId: 'nodewarden-empty-fill-assist-v1',
timestamp: '2026-07-06T00:00:00.000Z',
gitSha: 'nodewarden',
maps: { maps: {
forms: { forms: {
v1: { v1: {
filename: EMPTY_FORMS_FILENAME, filename: EMPTY_FORMS_FILENAME,
cid: 'sha256:nodewarden-empty-fill-assist-v1', cid: EMPTY_FORMS_CID,
schema: EMPTY_FORMS_SCHEMA_FILENAME,
deprecated: false,
}, },
}, },
}, },
}); });
const DIGITAL_ASSET_LINK_CHECK_BODY = JSON.stringify({
linked: false,
maxAge: '86400s',
debugString: 'No matching digital asset link policy is configured for this server.',
});
function fillAssistJsonResponse(body: string): Response { function fillAssistJsonResponse(body: string): Response {
return new Response(body, { return new Response(body, {
status: 200, status: 200,
@@ -24,13 +51,30 @@ function fillAssistJsonResponse(body: string): Response {
}); });
} }
function normalizeFilename(filename: string): string {
const raw = String(filename || '').trim();
try {
return decodeURIComponent(raw);
} catch {
return raw;
}
}
export function handleFillAssistManifest(): Response { export function handleFillAssistManifest(): Response {
return fillAssistJsonResponse(EMPTY_MANIFEST_BODY); return fillAssistJsonResponse(EMPTY_MANIFEST_BODY);
} }
export function handleFillAssistForms(filename: string): Response { export function handleFillAssistForms(filename: string): Response {
if (String(filename || '').trim() !== EMPTY_FORMS_FILENAME) { const normalized = normalizeFilename(filename);
return new Response('Not found', { status: 404 }); if (normalized === EMPTY_FORMS_FILENAME) {
return fillAssistJsonResponse(EMPTY_FORMS_BODY);
} }
return fillAssistJsonResponse(EMPTY_FORMS_BODY); if (normalized === EMPTY_FORMS_SCHEMA_FILENAME) {
return fillAssistJsonResponse(EMPTY_FORMS_SCHEMA_BODY);
}
return new Response('Not found', { status: 404 });
}
export function handleDigitalAssetLinkCheck(): Response {
return fillAssistJsonResponse(DIGITAL_ASSET_LINK_CHECK_BODY);
} }
+12 -2
View File
@@ -7,7 +7,11 @@ import {
handleDownloadSendFile, handleDownloadSendFile,
} from './handlers/sends'; } from './handlers/sends';
import { handleKnownDevice } from './handlers/devices'; import { handleKnownDevice } from './handlers/devices';
import { handleFillAssistForms, handleFillAssistManifest } from './handlers/fill-assist'; import {
handleDigitalAssetLinkCheck,
handleFillAssistForms,
handleFillAssistManifest,
} from './handlers/fill-assist';
import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity'; import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity';
import { handleGetAccountPasskeyAssertionOptions } from './handlers/account-passkeys'; import { handleGetAccountPasskeyAssertionOptions } from './handlers/account-passkeys';
import { import {
@@ -97,7 +101,7 @@ function buildIconServiceCsp(origin: string): string {
} }
function buildConfigResponse(origin: string) { function buildConfigResponse(origin: string) {
const fillAssistBase = `${origin}/fill-assist`; const fillAssistBase = `${origin}/fill-assist/`;
return { return {
version: LIMITS.compatibility.bitwardenServerVersion, version: LIMITS.compatibility.bitwardenServerVersion,
gitHash: 'nodewarden', gitHash: 'nodewarden',
@@ -350,6 +354,12 @@ export async function handlePublicRoute(
return handleFillAssistManifest(); return handleFillAssistManifest();
} }
if ((path === '/v1/assetlinks:check' || path === '/api/v1/assetlinks:check') && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
if (blocked) return blocked;
return handleDigitalAssetLinkCheck();
}
const fillAssistFormsMatch = path.match(/^\/fill-assist\/([^/]+)$/i); const fillAssistFormsMatch = path.match(/^\/fill-assist\/([^/]+)$/i);
if (fillAssistFormsMatch && method === 'GET') { if (fillAssistFormsMatch && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
+1
View File
@@ -20,6 +20,7 @@ function canServeWithUnsafeJwtSecret(path: string, method: string): boolean {
if (method === 'GET' && path === '/.well-known/appspecific/com.chrome.devtools.json') return true; if (method === 'GET' && path === '/.well-known/appspecific/com.chrome.devtools.json') return true;
if (method === 'GET' && path === '/fill-assist/manifest.json') return true; if (method === 'GET' && path === '/fill-assist/manifest.json') return true;
if (method === 'GET' && /^\/fill-assist\/[^/]+$/i.test(path)) return true; if (method === 'GET' && /^\/fill-assist\/[^/]+$/i.test(path)) return true;
if (method === 'GET' && (path === '/v1/assetlinks:check' || path === '/api/v1/assetlinks:check')) return true;
if (method === 'GET' && /^\/icons\/[^/]+\/icon\.png$/i.test(path)) return true; if (method === 'GET' && /^\/icons\/[^/]+\/icon\.png$/i.test(path)) return true;
return false; return false;
} }
+3
View File
@@ -29,6 +29,9 @@ function isExtensionOrigin(origin: string): boolean {
function isWildcardCorsPath(path: string): boolean { function isWildcardCorsPath(path: string): boolean {
return ( return (
path.startsWith('/icons/') path.startsWith('/icons/')
|| path.startsWith('/fill-assist/')
|| path === '/v1/assetlinks:check'
|| path === '/api/v1/assetlinks:check'
|| path === '/config' || path === '/config'
|| path === '/api/config' || path === '/api/config'
|| path === '/api/version' || path === '/api/version'