mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add support for new cipher properties and enhance import functionality
This commit is contained in:
+82
-33
@@ -1,4 +1,15 @@
|
|||||||
import { Env, Cipher, CipherResponse, Attachment } from '../types';
|
import {
|
||||||
|
Env,
|
||||||
|
Cipher,
|
||||||
|
CipherCard,
|
||||||
|
CipherIdentity,
|
||||||
|
CipherLogin,
|
||||||
|
CipherResponse,
|
||||||
|
CipherSecureNote,
|
||||||
|
CipherSshKey,
|
||||||
|
Attachment,
|
||||||
|
PasswordHistory,
|
||||||
|
} from '../types';
|
||||||
import { StorageService } from '../services/storage';
|
import { StorageService } from '../services/storage';
|
||||||
import { notifyUserVaultSync } from '../durable/notifications-hub';
|
import { notifyUserVaultSync } from '../durable/notifications-hub';
|
||||||
import { jsonResponse, errorResponse } from '../utils/response';
|
import { jsonResponse, errorResponse } from '../utils/response';
|
||||||
@@ -13,26 +24,6 @@ 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,
|
||||||
@@ -52,6 +43,10 @@ function getAliasedProp(source: any, aliases: string[]): { present: boolean; val
|
|||||||
return { present: false, value: undefined };
|
return { present: false, value: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCipherProp<T = unknown>(source: any, aliases: string[]): { present: boolean; value: T | undefined } {
|
||||||
|
return getAliasedProp(source, aliases) as { present: boolean; value: T | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCipherTimestamp(value: unknown): string | null {
|
function normalizeCipherTimestamp(value: unknown): string | null {
|
||||||
if (value == null || value === '') return null;
|
if (value == null || value === '') return null;
|
||||||
const parsed = new Date(String(value));
|
const parsed = new Date(String(value));
|
||||||
@@ -64,6 +59,19 @@ function readCipherArchivedAt(source: any, fallback: string | null = null): stri
|
|||||||
return archived.present ? normalizeCipherTimestamp(archived.value) : fallback;
|
return archived.present ? normalizeCipherTimestamp(archived.value) : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readCipherRevisionDate(source: any): string | null {
|
||||||
|
const revision = getAliasedProp(source, ['lastKnownRevisionDate', 'LastKnownRevisionDate']);
|
||||||
|
return revision.present ? normalizeCipherTimestamp(revision.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStaleCipherUpdate(existingUpdatedAt: string, clientRevisionDate: string | null): boolean {
|
||||||
|
if (!clientRevisionDate) return false;
|
||||||
|
const existingTs = Date.parse(existingUpdatedAt);
|
||||||
|
const clientTs = Date.parse(clientRevisionDate);
|
||||||
|
if (Number.isNaN(existingTs) || Number.isNaN(clientTs)) return false;
|
||||||
|
return existingTs - clientTs > 1000;
|
||||||
|
}
|
||||||
|
|
||||||
function syncCipherComputedAliases(cipher: Cipher): Cipher {
|
function syncCipherComputedAliases(cipher: Cipher): Cipher {
|
||||||
cipher.archivedDate = cipher.archivedAt ?? null;
|
cipher.archivedDate = cipher.archivedAt ?? null;
|
||||||
cipher.deletedDate = cipher.deletedAt ?? null;
|
cipher.deletedDate = cipher.deletedAt ?? null;
|
||||||
@@ -151,8 +159,8 @@ export function cipherToResponse(
|
|||||||
// Server-computed / enforced fields (always override)
|
// Server-computed / enforced fields (always override)
|
||||||
folderId: normalizeOptionalId(cipher.folderId),
|
folderId: normalizeOptionalId(cipher.folderId),
|
||||||
type: Number(cipher.type) || 1,
|
type: Number(cipher.type) || 1,
|
||||||
organizationId: null,
|
organizationId: normalizeOptionalId((passthrough as any).organizationId ?? null),
|
||||||
organizationUseTotp: false,
|
organizationUseTotp: !!((passthrough as any).organizationUseTotp ?? false),
|
||||||
creationDate: createdAt,
|
creationDate: createdAt,
|
||||||
revisionDate: updatedAt,
|
revisionDate: updatedAt,
|
||||||
deletedDate: deletedAt,
|
deletedDate: deletedAt,
|
||||||
@@ -163,12 +171,12 @@ export function cipherToResponse(
|
|||||||
delete: true,
|
delete: true,
|
||||||
restore: true,
|
restore: true,
|
||||||
},
|
},
|
||||||
object: 'cipher',
|
object: 'cipherDetails',
|
||||||
collectionIds: [],
|
collectionIds: Array.isArray((passthrough as any).collectionIds) ? (passthrough as any).collectionIds : [],
|
||||||
attachments: formatAttachments(attachments),
|
attachments: formatAttachments(attachments),
|
||||||
login: normalizedLogin,
|
login: normalizedLogin,
|
||||||
sshKey: normalizedSshKey,
|
sshKey: normalizedSshKey,
|
||||||
encryptedFor: null,
|
encryptedFor: (passthrough as any).encryptedFor ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +259,14 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
// Handle nested cipher object (from some clients)
|
// Handle nested cipher object (from some clients)
|
||||||
// Android client sends PascalCase "Cipher" for organization ciphers
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||||
const cipherData = body.Cipher || body.cipher || body;
|
const cipherData = body.Cipher || body.cipher || body;
|
||||||
|
const createFolderId = readCipherProp<string | null>(cipherData, ['folderId', 'FolderId']);
|
||||||
|
const createKey = readCipherProp<string | null>(cipherData, ['key', 'Key']);
|
||||||
|
const createLogin = readCipherProp<CipherLogin | null>(cipherData, ['login', 'Login']);
|
||||||
|
const createCard = readCipherProp<CipherCard | null>(cipherData, ['card', 'Card']);
|
||||||
|
const createIdentity = readCipherProp<CipherIdentity | null>(cipherData, ['identity', 'Identity']);
|
||||||
|
const createSecureNote = readCipherProp<CipherSecureNote | null>(cipherData, ['secureNote', 'SecureNote']);
|
||||||
|
const createSshKey = readCipherProp<CipherSshKey | null>(cipherData, ['sshKey', 'SshKey']);
|
||||||
|
const createPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
// Opaque passthrough: spread ALL client fields to preserve unknown/future ones,
|
// Opaque passthrough: spread ALL client fields to preserve unknown/future ones,
|
||||||
@@ -268,6 +284,14 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
archivedAt: readCipherArchivedAt(cipherData, null),
|
archivedAt: readCipherArchivedAt(cipherData, null),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
|
cipher.folderId = createFolderId.present ? normalizeOptionalId(createFolderId.value) : normalizeOptionalId(cipher.folderId);
|
||||||
|
cipher.key = createKey.present ? (createKey.value ?? null) : (cipher.key ?? null);
|
||||||
|
cipher.login = createLogin.present ? (createLogin.value ?? null) : (cipher.login ?? null);
|
||||||
|
cipher.card = createCard.present ? (createCard.value ?? null) : (cipher.card ?? null);
|
||||||
|
cipher.identity = createIdentity.present ? (createIdentity.value ?? null) : (cipher.identity ?? null);
|
||||||
|
cipher.secureNote = createSecureNote.present ? (createSecureNote.value ?? null) : (cipher.secureNote ?? null);
|
||||||
|
cipher.sshKey = createSshKey.present ? (createSshKey.value ?? null) : (cipher.sshKey ?? null);
|
||||||
|
cipher.passwordHistory = createPasswordHistory.present ? (createPasswordHistory.value ?? null) : (cipher.passwordHistory ?? null);
|
||||||
const createFields = getAliasedProp(cipherData, ['fields', 'Fields']);
|
const createFields = getAliasedProp(cipherData, ['fields', 'Fields']);
|
||||||
cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null);
|
cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null);
|
||||||
normalizeCipherForStorage(cipher);
|
normalizeCipherForStorage(cipher);
|
||||||
@@ -307,6 +331,21 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
// Handle nested cipher object
|
// Handle nested cipher object
|
||||||
// Android client sends PascalCase "Cipher" for organization ciphers
|
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||||
const cipherData = body.Cipher || body.cipher || body;
|
const cipherData = body.Cipher || body.cipher || body;
|
||||||
|
const incomingFolderId = readCipherProp<string | null>(cipherData, ['folderId', 'FolderId']);
|
||||||
|
const incomingKey = readCipherProp<string | null>(cipherData, ['key', 'Key']);
|
||||||
|
const incomingLogin = readCipherProp<CipherLogin | null>(cipherData, ['login', 'Login']);
|
||||||
|
const incomingCard = readCipherProp<CipherCard | null>(cipherData, ['card', 'Card']);
|
||||||
|
const incomingIdentity = readCipherProp<CipherIdentity | null>(cipherData, ['identity', 'Identity']);
|
||||||
|
const incomingSecureNote = readCipherProp<CipherSecureNote | null>(cipherData, ['secureNote', 'SecureNote']);
|
||||||
|
const incomingSshKey = readCipherProp<CipherSshKey | null>(cipherData, ['sshKey', 'SshKey']);
|
||||||
|
const incomingPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
|
||||||
|
const incomingRevisionDate = readCipherRevisionDate(cipherData);
|
||||||
|
|
||||||
|
if (isStaleCipherUpdate(existingCipher.updatedAt, incomingRevisionDate)) {
|
||||||
|
return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextType = Number(cipherData.type) || existingCipher.type;
|
||||||
|
|
||||||
// Opaque passthrough: merge existing stored data with ALL incoming client fields.
|
// Opaque passthrough: merge existing stored data with ALL incoming client fields.
|
||||||
// Unknown/future fields from the client are preserved; server-controlled fields are protected.
|
// Unknown/future fields from the client are preserved; server-controlled fields are protected.
|
||||||
@@ -316,7 +355,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
// Server-controlled fields (never from client)
|
// Server-controlled fields (never from client)
|
||||||
id: existingCipher.id,
|
id: existingCipher.id,
|
||||||
userId: existingCipher.userId,
|
userId: existingCipher.userId,
|
||||||
type: Number(cipherData.type) || existingCipher.type,
|
type: nextType,
|
||||||
favorite: cipherData.favorite ?? existingCipher.favorite,
|
favorite: cipherData.favorite ?? existingCipher.favorite,
|
||||||
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
||||||
createdAt: existingCipher.createdAt,
|
createdAt: existingCipher.createdAt,
|
||||||
@@ -324,11 +363,20 @@ 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);
|
if (incomingFolderId.present) {
|
||||||
cipher.card = mergeCipherNestedObject(existingCipher.card, cipherData.card);
|
cipher.folderId = normalizeOptionalId(incomingFolderId.value);
|
||||||
cipher.identity = mergeCipherNestedObject(existingCipher.identity, cipherData.identity);
|
}
|
||||||
cipher.secureNote = mergeCipherNestedObject(existingCipher.secureNote, cipherData.secureNote);
|
if (incomingKey.present) {
|
||||||
cipher.sshKey = mergeCipherNestedObject(existingCipher.sshKey, cipherData.sshKey);
|
cipher.key = incomingKey.value ?? null;
|
||||||
|
}
|
||||||
|
cipher.login = nextType === 1 ? (incomingLogin.present ? (incomingLogin.value ?? null) : (existingCipher.login ?? null)) : null;
|
||||||
|
cipher.secureNote = nextType === 2 ? (incomingSecureNote.present ? (incomingSecureNote.value ?? null) : (existingCipher.secureNote ?? null)) : null;
|
||||||
|
cipher.card = nextType === 3 ? (incomingCard.present ? (incomingCard.value ?? null) : (existingCipher.card ?? null)) : null;
|
||||||
|
cipher.identity = nextType === 4 ? (incomingIdentity.present ? (incomingIdentity.value ?? null) : (existingCipher.identity ?? null)) : null;
|
||||||
|
cipher.sshKey = nextType === 5 ? (incomingSshKey.present ? (incomingSshKey.value ?? null) : (existingCipher.sshKey ?? null)) : null;
|
||||||
|
if (incomingPasswordHistory.present) {
|
||||||
|
cipher.passwordHistory = incomingPasswordHistory.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom fields deletion compatibility:
|
// Custom fields deletion compatibility:
|
||||||
// - Accept both camelCase "fields" and PascalCase "Fields".
|
// - Accept both camelCase "fields" and PascalCase "Fields".
|
||||||
@@ -351,9 +399,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
await storage.saveCipher(cipher);
|
await storage.saveCipher(cipher);
|
||||||
const revisionDate = await storage.updateRevisionDate(userId);
|
const revisionDate = await storage.updateRevisionDate(userId);
|
||||||
await notifyVaultSyncForRequest(request, env, userId, revisionDate);
|
await notifyVaultSyncForRequest(request, env, userId, revisionDate);
|
||||||
|
const attachments = await storage.getAttachmentsByCipher(cipher.id);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(
|
||||||
cipherToResponse(cipher, [])
|
cipherToResponse(cipher, attachments)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-43
@@ -82,6 +82,16 @@ function bindNull(v: any): any {
|
|||||||
return v === undefined ? null : v;
|
return v === undefined ? null : v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readAliasedImportProp<T = unknown>(source: any, aliases: string[]): T | undefined {
|
||||||
|
if (!source || typeof source !== 'object') return undefined;
|
||||||
|
for (const key of aliases) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||||
|
return source[key] as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function runBatchInChunks(db: D1Database, statements: D1PreparedStatement[], chunkSize: number): Promise<void> {
|
async function runBatchInChunks(db: D1Database, statements: D1PreparedStatement[], chunkSize: number): Promise<void> {
|
||||||
for (let i = 0; i < statements.length; i += chunkSize) {
|
for (let i = 0; i < statements.length; i += chunkSize) {
|
||||||
const chunk = statements.slice(i, i + chunkSize);
|
const chunk = statements.slice(i, i + chunkSize);
|
||||||
@@ -158,9 +168,16 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
|||||||
const cipherMapRows: Array<{ index: number; sourceId: string | null; id: string }> = [];
|
const cipherMapRows: Array<{ index: number; sourceId: string | null; id: string }> = [];
|
||||||
for (let i = 0; i < ciphers.length; i++) {
|
for (let i = 0; i < ciphers.length; i++) {
|
||||||
const c = ciphers[i];
|
const c = ciphers[i];
|
||||||
const folderId = cipherFolderMap.get(i) || c.folderId || null;
|
const folderId = cipherFolderMap.get(i) || readAliasedImportProp<string | null>(c, ['folderId', 'FolderId']) || null;
|
||||||
const sourceIdRaw = String(c?.id ?? '').trim();
|
const sourceIdRaw = String(c?.id ?? '').trim();
|
||||||
const sourceId = sourceIdRaw || null;
|
const sourceId = sourceIdRaw || null;
|
||||||
|
const login = readAliasedImportProp<any | null>(c, ['login', 'Login']);
|
||||||
|
const card = readAliasedImportProp<any | null>(c, ['card', 'Card']);
|
||||||
|
const identity = readAliasedImportProp<any | null>(c, ['identity', 'Identity']);
|
||||||
|
const secureNote = readAliasedImportProp<any | null>(c, ['secureNote', 'SecureNote']);
|
||||||
|
const fields = readAliasedImportProp<any[] | null>(c, ['fields', 'Fields']);
|
||||||
|
const passwordHistory = readAliasedImportProp<any[] | null>(c, ['passwordHistory', 'PasswordHistory']);
|
||||||
|
const key = readAliasedImportProp<string | null>(c, ['key', 'Key']);
|
||||||
|
|
||||||
const cipher: Cipher = {
|
const cipher: Cipher = {
|
||||||
...c,
|
...c,
|
||||||
@@ -171,64 +188,64 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
|||||||
name: c.name ?? 'Untitled',
|
name: c.name ?? 'Untitled',
|
||||||
notes: c.notes ?? null,
|
notes: c.notes ?? null,
|
||||||
favorite: c.favorite ?? false,
|
favorite: c.favorite ?? false,
|
||||||
login: c.login ? {
|
login: login ? {
|
||||||
...c.login,
|
...login,
|
||||||
username: c.login.username ?? null,
|
username: login.username ?? null,
|
||||||
password: c.login.password ?? null,
|
password: login.password ?? null,
|
||||||
uris: c.login.uris?.map(u => ({
|
uris: login.uris?.map((u: any) => ({
|
||||||
...u,
|
...u,
|
||||||
uri: u.uri ?? null,
|
uri: u.uri ?? null,
|
||||||
uriChecksum: null,
|
uriChecksum: null,
|
||||||
match: u.match ?? null,
|
match: u.match ?? null,
|
||||||
})) || null,
|
})) || null,
|
||||||
totp: c.login.totp ?? null,
|
totp: login.totp ?? null,
|
||||||
autofillOnPageLoad: c.login.autofillOnPageLoad ?? null,
|
autofillOnPageLoad: login.autofillOnPageLoad ?? null,
|
||||||
fido2Credentials: Array.isArray(c.login.fido2Credentials) ? c.login.fido2Credentials : null,
|
fido2Credentials: Array.isArray(login.fido2Credentials) ? login.fido2Credentials : null,
|
||||||
uri: c.login.uri ?? null,
|
uri: login.uri ?? null,
|
||||||
passwordRevisionDate: c.login.passwordRevisionDate ?? null,
|
passwordRevisionDate: login.passwordRevisionDate ?? null,
|
||||||
} : null,
|
} : null,
|
||||||
card: c.card ? {
|
card: card ? {
|
||||||
...c.card,
|
...card,
|
||||||
cardholderName: c.card.cardholderName ?? null,
|
cardholderName: card.cardholderName ?? null,
|
||||||
brand: c.card.brand ?? null,
|
brand: card.brand ?? null,
|
||||||
number: c.card.number ?? null,
|
number: card.number ?? null,
|
||||||
expMonth: c.card.expMonth ?? null,
|
expMonth: card.expMonth ?? null,
|
||||||
expYear: c.card.expYear ?? null,
|
expYear: card.expYear ?? null,
|
||||||
code: c.card.code ?? null,
|
code: card.code ?? null,
|
||||||
} : null,
|
} : null,
|
||||||
identity: c.identity ? {
|
identity: identity ? {
|
||||||
...c.identity,
|
...identity,
|
||||||
title: c.identity.title ?? null,
|
title: identity.title ?? null,
|
||||||
firstName: c.identity.firstName ?? null,
|
firstName: identity.firstName ?? null,
|
||||||
middleName: c.identity.middleName ?? null,
|
middleName: identity.middleName ?? null,
|
||||||
lastName: c.identity.lastName ?? null,
|
lastName: identity.lastName ?? null,
|
||||||
address1: c.identity.address1 ?? null,
|
address1: identity.address1 ?? null,
|
||||||
address2: c.identity.address2 ?? null,
|
address2: identity.address2 ?? null,
|
||||||
address3: c.identity.address3 ?? null,
|
address3: identity.address3 ?? null,
|
||||||
city: c.identity.city ?? null,
|
city: identity.city ?? null,
|
||||||
state: c.identity.state ?? null,
|
state: identity.state ?? null,
|
||||||
postalCode: c.identity.postalCode ?? null,
|
postalCode: identity.postalCode ?? null,
|
||||||
country: c.identity.country ?? null,
|
country: identity.country ?? null,
|
||||||
company: c.identity.company ?? null,
|
company: identity.company ?? null,
|
||||||
email: c.identity.email ?? null,
|
email: identity.email ?? null,
|
||||||
phone: c.identity.phone ?? null,
|
phone: identity.phone ?? null,
|
||||||
ssn: c.identity.ssn ?? null,
|
ssn: identity.ssn ?? null,
|
||||||
username: c.identity.username ?? null,
|
username: identity.username ?? null,
|
||||||
passportNumber: c.identity.passportNumber ?? null,
|
passportNumber: identity.passportNumber ?? null,
|
||||||
licenseNumber: c.identity.licenseNumber ?? null,
|
licenseNumber: identity.licenseNumber ?? null,
|
||||||
} : null,
|
} : null,
|
||||||
secureNote: c.secureNote ?? null,
|
secureNote: secureNote ?? null,
|
||||||
fields: c.fields?.map(f => ({
|
fields: fields?.map((f: any) => ({
|
||||||
...f,
|
...f,
|
||||||
name: f.name ?? null,
|
name: f.name ?? null,
|
||||||
value: f.value ?? null,
|
value: f.value ?? null,
|
||||||
type: f.type,
|
type: f.type,
|
||||||
linkedId: f.linkedId ?? null,
|
linkedId: f.linkedId ?? null,
|
||||||
})) || null,
|
})) || null,
|
||||||
passwordHistory: c.passwordHistory ?? null,
|
passwordHistory: passwordHistory ?? null,
|
||||||
reprompt: c.reprompt ?? 0,
|
reprompt: c.reprompt ?? 0,
|
||||||
sshKey: normalizeCipherSshKeyForCompatibility((c as any).sshKey ?? null),
|
sshKey: normalizeCipherSshKeyForCompatibility((c as any).sshKey ?? null),
|
||||||
key: (c as any).key ?? null,
|
key: key ?? null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
archivedAt: null,
|
archivedAt: null,
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ function buildConfigResponse(origin: string) {
|
|||||||
_icon_service_url: buildIconServiceTemplate(origin),
|
_icon_service_url: buildIconServiceTemplate(origin),
|
||||||
_icon_service_csp: buildIconServiceCsp(origin),
|
_icon_service_csp: buildIconServiceCsp(origin),
|
||||||
featureStates: {
|
featureStates: {
|
||||||
|
'cipher-key-encryption': true,
|
||||||
'duo-redirect': true,
|
'duo-redirect': true,
|
||||||
'email-verification': true,
|
'email-verification': true,
|
||||||
'pm-19051-send-email-verification': false,
|
'pm-19051-send-email-verification': false,
|
||||||
|
|||||||
Reference in New Issue
Block a user