feat: enhance cipher handling with nested object merging and additional fields

This commit is contained in:
shuaiplus
2026-04-16 22:29:55 +08:00
parent 681705ee13
commit 92d1f07998
5 changed files with 60 additions and 4 deletions
+25
View File
@@ -13,6 +13,26 @@ function normalizeOptionalId(value: unknown): string | null {
return normalized ? normalized : null; return normalized ? normalized : null;
} }
function mergeCipherNestedObject<T>(
existingValue: T | null | undefined,
incomingValue: unknown
): T | null {
if (incomingValue === undefined) {
return (existingValue ?? null) as T | null;
}
if (incomingValue === null || typeof incomingValue !== 'object' || Array.isArray(incomingValue)) {
return incomingValue as T | null;
}
const existingObject =
existingValue && typeof existingValue === 'object' && !Array.isArray(existingValue)
? (existingValue as Record<string, unknown>)
: {};
return {
...existingObject,
...(incomingValue as Record<string, unknown>),
} as T;
}
async function notifyVaultSyncForRequest( async function notifyVaultSyncForRequest(
request: Request, request: Request,
env: Env, env: Env,
@@ -302,6 +322,11 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null), archivedAt: readCipherArchivedAt(cipherData, existingCipher.archivedAt ?? null),
deletedAt: existingCipher.deletedAt, deletedAt: existingCipher.deletedAt,
}; };
cipher.login = mergeCipherNestedObject(existingCipher.login, cipherData.login);
cipher.card = mergeCipherNestedObject(existingCipher.card, cipherData.card);
cipher.identity = mergeCipherNestedObject(existingCipher.identity, cipherData.identity);
cipher.secureNote = mergeCipherNestedObject(existingCipher.secureNote, cipherData.secureNote);
cipher.sshKey = mergeCipherNestedObject(existingCipher.sshKey, cipherData.sshKey);
// Custom fields deletion compatibility: // Custom fields deletion compatibility:
// - Accept both camelCase "fields" and PascalCase "Fields". // - Accept both camelCase "fields" and PascalCase "Fields".
@@ -165,7 +165,7 @@ export function websiteIconUrl(host: string): string {
} }
export function createEmptyLoginUri(): VaultDraftLoginUri { export function createEmptyLoginUri(): VaultDraftLoginUri {
return { uri: '', match: null }; return { uri: '', match: null, originalUri: '', extra: {} };
} }
export function websiteMatchLabel(value: number | null | undefined): string { export function websiteMatchLabel(value: number | null | undefined): string {
@@ -313,6 +313,10 @@ export function draftFromCipher(cipher: Cipher): VaultDraft {
draft.loginUris = (cipher.login.uris || []).map((x) => ({ draft.loginUris = (cipher.login.uris || []).map((x) => ({
uri: x.decUri || x.uri || '', uri: x.decUri || x.uri || '',
match: x.match ?? null, match: x.match ?? null,
originalUri: x.decUri || x.uri || '',
extra: Object.fromEntries(
Object.entries(x as Record<string, unknown>).filter(([key]) => !['uri', 'match', 'decUri'].includes(key))
),
})); }));
draft.loginFido2Credentials = Array.isArray(cipher.login.fido2Credentials) draft.loginFido2Credentials = Array.isArray(cipher.login.fido2Credentials)
? cipher.login.fido2Credentials.map((credential) => ({ ...credential })) ? cipher.login.fido2Credentials.map((credential) => ({ ...credential }))
+15 -2
View File
@@ -372,12 +372,20 @@ async function encryptUris(
uris: VaultDraft['loginUris'], uris: VaultDraft['loginUris'],
enc: Uint8Array, enc: Uint8Array,
mac: Uint8Array mac: Uint8Array
): Promise<Array<{ uri: string | null; match: number | null }>> { ): Promise<Array<Record<string, unknown>>> {
const out: Array<{ uri: string | null; match: number | null }> = []; const out: Array<Record<string, unknown>> = [];
for (const entry of uris || []) { for (const entry of uris || []) {
const trimmed = String(entry?.uri || '').trim(); const trimmed = String(entry?.uri || '').trim();
if (!trimmed) continue; if (!trimmed) continue;
const preservedExtra =
entry?.extra && typeof entry.extra === 'object'
? { ...entry.extra }
: {};
if (String(entry?.originalUri || '').trim() !== trimmed) {
delete preservedExtra.uriChecksum;
}
out.push({ out.push({
...preservedExtra,
uri: await encryptTextValue(trimmed, enc, mac), uri: await encryptTextValue(trimmed, enc, mac),
match: typeof entry?.match === 'number' && Number.isFinite(entry.match) ? entry.match : null, match: typeof entry?.match === 'number' && Number.isFinite(entry.match) ? entry.match : null,
}); });
@@ -495,7 +503,12 @@ async function buildCipherPayload(
cipher?.login && Array.isArray((cipher.login as any).fido2Credentials) cipher?.login && Array.isArray((cipher.login as any).fido2Credentials)
? (cipher.login as any).fido2Credentials ? (cipher.login as any).fido2Credentials
: draft.loginFido2Credentials; : draft.loginFido2Credentials;
const existingLogin =
cipher?.login && typeof cipher.login === 'object'
? { ...(cipher.login as Record<string, unknown>) }
: {};
payload.login = { payload.login = {
...existingLogin,
username: await encryptTextValue(draft.loginUsername, keys.enc, keys.mac), username: await encryptTextValue(draft.loginUsername, keys.enc, keys.mac),
password: await encryptTextValue(draft.loginPassword, keys.enc, keys.mac), password: await encryptTextValue(draft.loginPassword, keys.enc, keys.mac),
totp: await encryptTextValue(draft.loginTotp, keys.enc, keys.mac), totp: await encryptTextValue(draft.loginTotp, keys.enc, keys.mac),
+5 -1
View File
@@ -170,10 +170,14 @@ export function importCipherToDraft(cipher: Record<string, unknown>, folderId: s
return { return {
uri, uri,
match: typeof matchRaw === 'number' && Number.isFinite(matchRaw) ? matchRaw : null, match: typeof matchRaw === 'number' && Number.isFinite(matchRaw) ? matchRaw : null,
originalUri: uri,
extra: Object.fromEntries(
Object.entries(row).filter(([key]) => !['uri', 'match'].includes(key))
),
}; };
}) })
.filter((u) => !!u.uri); .filter((u) => !!u.uri);
draft.loginUris = uris.length ? uris : [{ uri: '', match: null }]; draft.loginUris = uris.length ? uris : [{ uri: '', match: null, originalUri: '', extra: {} }];
draft.loginFido2Credentials = Array.isArray(login.fido2Credentials) draft.loginFido2Credentials = Array.isArray(login.fido2Credentials)
? login.fido2Credentials.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object') ? login.fido2Credentials.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object')
: []; : [];
+10
View File
@@ -29,13 +29,18 @@ export interface Folder {
export interface CipherLoginUri { export interface CipherLoginUri {
uri?: string | null; uri?: string | null;
uriChecksum?: string | null;
match?: number | null; match?: number | null;
response?: unknown | null;
decUri?: string; decUri?: string;
[key: string]: unknown;
} }
export interface VaultDraftLoginUri { export interface VaultDraftLoginUri {
uri: string; uri: string;
match: number | null; match: number | null;
originalUri?: string;
extra?: Record<string, unknown>;
} }
export interface CipherAttachment { export interface CipherAttachment {
@@ -60,9 +65,14 @@ export interface CipherLogin {
totp?: string | null; totp?: string | null;
uris?: CipherLoginUri[] | null; uris?: CipherLoginUri[] | null;
fido2Credentials?: CipherLoginPasskey[] | null; fido2Credentials?: CipherLoginPasskey[] | null;
autofillOnPageLoad?: boolean | null;
uri?: string | null;
passwordRevisionDate?: string | null;
response?: unknown | null;
decUsername?: string; decUsername?: string;
decPassword?: string; decPassword?: string;
decTotp?: string; decTotp?: string;
[key: string]: unknown;
} }
export interface CipherCard { export interface CipherCard {