fix(webapp): resolve three errors in the webapp typecheck (#356)

`npx tsc -p webapp/tsconfig.json --noEmit`, one of the checks recommended
in CONTRIBUTING.md, currently fails on main with three errors. All three
are declaration defects with correct runtime behavior; none changes
observable behavior.

- api/backup.ts: downloadAdminBackupAttachmentBlob declared a bare
  Uint8Array return. Since TypeScript made typed arrays generic, that
  widens to Uint8Array<ArrayBufferLike> and no longer satisfies fflate's
  Uint8Array<ArrayBuffer>. The body already returns an ArrayBuffer-backed
  value, so this only annotates what it produces.

- backup-center.ts: invalidateRemoteBrowserCacheForDestination declared a
  full PersistedRemoteBrowserState but builds four of its five fields.
  The sole caller reads .cache only, so the return type is narrowed to
  match what the function actually returns.

- password-security-cache.ts: getPasswordSecurityState declared the public
  PasswordSecurityState, but startPasswordSecurityScan needs `controller`,
  which lives on InternalPasswordSecurityState. An internal accessor keeps
  `controller` off the exported type rather than widening the public API.

No change to backup payload shape, archive/import whitelists, or any
persisted format.

Verified with tsc 5.9.3, 6.0.3, and 7.0.2 (all exit 0 for both
webapp/tsconfig.json and tsconfig.json), plus npm run build and
npm run i18n:validate.
This commit is contained in:
Cordero Core
2026-08-30 01:31:37 +08:00
committed by GitHub
parent b3630bdaf5
commit 13bb0a0308
3 changed files with 8 additions and 4 deletions
+1 -1
View File
@@ -197,7 +197,7 @@ export async function downloadAdminBackupAttachmentBlob(
authedFetch: AuthedFetch, authedFetch: AuthedFetch,
blobName: string, blobName: string,
masterPasswordHash: string masterPasswordHash: string
): Promise<Uint8Array> { ): Promise<Uint8Array<ArrayBuffer>> {
const resp = await authedFetch('/api/admin/backup/blob', { const resp = await authedFetch('/api/admin/backup/blob', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
+1 -1
View File
@@ -186,7 +186,7 @@ export function invalidateRemoteBrowserCacheForDestination(
cache: Record<string, RemoteBackupBrowserResponse>, cache: Record<string, RemoteBackupBrowserResponse>,
pathByDestination: Record<string, string>, pathByDestination: Record<string, string>,
pageByKey: Record<string, number> pageByKey: Record<string, number>
): PersistedRemoteBrowserState { ): Omit<PersistedRemoteBrowserState, 'refreshedAt'> {
return { return {
cache: Object.fromEntries(Object.entries(cache).filter(([key]) => !key.startsWith(`${destinationId}:`))), cache: Object.fromEntries(Object.entries(cache).filter(([key]) => !key.startsWith(`${destinationId}:`))),
pathByDestination: Object.fromEntries(Object.entries(pathByDestination).filter(([key]) => key !== destinationId)), pathByDestination: Object.fromEntries(Object.entries(pathByDestination).filter(([key]) => key !== destinationId)),
+6 -2
View File
@@ -23,7 +23,7 @@ function createState(fingerprint: string): InternalPasswordSecurityState {
return { fingerprint, report: null, scannedAt: null, scanning: false, progress: { checked: 0, total: 0 }, scanError: false, controller: null }; return { fingerprint, report: null, scannedAt: null, scanning: false, progress: { checked: 0, total: 0 }, scanError: false, controller: null };
} }
export function getPasswordSecurityState(fingerprint: string): PasswordSecurityState { function ensurePasswordSecurityState(fingerprint: string): InternalPasswordSecurityState {
if (state?.fingerprint !== fingerprint) { if (state?.fingerprint !== fingerprint) {
state?.controller?.abort(); state?.controller?.abort();
state = createState(fingerprint); state = createState(fingerprint);
@@ -31,6 +31,10 @@ export function getPasswordSecurityState(fingerprint: string): PasswordSecurityS
return state; return state;
} }
export function getPasswordSecurityState(fingerprint: string): PasswordSecurityState {
return ensurePasswordSecurityState(fingerprint);
}
export function readPasswordSecurityState(fingerprint: string): PasswordSecurityState | null { export function readPasswordSecurityState(fingerprint: string): PasswordSecurityState | null {
return state?.fingerprint === fingerprint ? state : null; return state?.fingerprint === fingerprint ? state : null;
} }
@@ -41,7 +45,7 @@ export function subscribePasswordSecurityState(listener: () => void): () => void
} }
export function startPasswordSecurityScan(fingerprint: string, ciphers: Cipher[]): void { export function startPasswordSecurityScan(fingerprint: string, ciphers: Cipher[]): void {
const current = getPasswordSecurityState(fingerprint); const current = ensurePasswordSecurityState(fingerprint);
current.controller?.abort(); current.controller?.abort();
const controller = new AbortController(); const controller = new AbortController();
const total = ciphers.filter((cipher) => Number(cipher.type) === 1 && !cipher.deletedDate && !(cipher as { deletedAt?: string | null }).deletedAt && !!cipher.login?.decPassword).length; const total = ciphers.filter((cipher) => Number(cipher.type) === 1 && !cipher.deletedDate && !(cipher as { deletedAt?: string | null }).deletedAt && !!cipher.login?.decPassword).length;