mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: enhance cipher handling with nested object merging and additional fields
This commit is contained in:
@@ -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 }))
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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')
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user