Refactor browser extension typescript item field management (#1404)

This commit is contained in:
Leendert de Borst
2025-12-21 12:53:30 +01:00
parent 4f57b8fdee
commit 74b37f137b
8 changed files with 263 additions and 159 deletions

View File

@@ -6,7 +6,7 @@ import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
import { CreateIdentityGenerator, IdentityHelperUtils } from '@/utils/dist/core/identity-generator';
import type { Item, ItemField } from '@/utils/dist/core/models/vault';
import { ItemTypes, FieldTypes, FieldKey } from '@/utils/dist/core/models/vault';
import { ItemTypes, FieldKey, createSystemField } from '@/utils/dist/core/models/vault';
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/core/password-generator';
import { ClickValidator } from '@/utils/security/ClickValidator';
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
@@ -249,64 +249,21 @@ export async function createAutofillPopup(input: HTMLInputElement, items: Item[]
if (result.isCustomCredential) {
// Create custom item with information provided by user in popup.
const faviconBytes = await getFaviconBytes(document);
const fields: ItemField[] = [];
let displayOrder = 0;
// Add URL field
const serviceUrl = getValidServiceUrl();
// Build fields using factory - only include non-empty values
const fields: ItemField[] = [];
if (serviceUrl) {
fields.push({
FieldKey: FieldKey.LoginUrl,
Label: 'URL',
FieldType: FieldTypes.URL,
Value: serviceUrl,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push(createSystemField(FieldKey.LoginUrl, { value: serviceUrl }));
}
// Add username if provided
if (result.customUsername) {
fields.push({
FieldKey: FieldKey.LoginUsername,
Label: 'Username',
FieldType: FieldTypes.Text,
Value: result.customUsername,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push(createSystemField(FieldKey.LoginUsername, { value: result.customUsername }));
}
// Add email if provided
if (result.customEmail) {
fields.push({
FieldKey: FieldKey.LoginEmail,
Label: 'Email',
FieldType: FieldTypes.Email,
Value: result.customEmail,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push(createSystemField(FieldKey.LoginEmail, { value: result.customEmail }));
}
// Add password if provided
if (result.customPassword) {
fields.push({
FieldKey: FieldKey.LoginPassword,
Label: 'Password',
FieldType: FieldTypes.Password,
Value: result.customPassword,
IsHidden: true,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: true
});
fields.push(createSystemField(FieldKey.LoginPassword, { value: result.customPassword }));
}
newItem = {
@@ -340,106 +297,20 @@ export async function createAutofillPopup(input: HTMLInputElement, items: Item[]
// Extract favicon from page and get the bytes
const faviconBytes = await getFaviconBytes(document);
// Build fields array for the Alias item type
const fields: ItemField[] = [];
let displayOrder = 0;
// Add URL field
const serviceUrl = getValidServiceUrl();
// Build fields using factory - metadata comes from SystemFieldRegistry
const fields: ItemField[] = [];
if (serviceUrl) {
fields.push({
FieldKey: FieldKey.LoginUrl,
Label: 'URL',
FieldType: FieldTypes.URL,
Value: serviceUrl,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push(createSystemField(FieldKey.LoginUrl, { value: serviceUrl }));
}
// Add username
fields.push({
FieldKey: FieldKey.LoginUsername,
Label: 'Username',
FieldType: FieldTypes.Text,
Value: identity.nickName,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
// Add email
fields.push({
FieldKey: FieldKey.LoginEmail,
Label: 'Email',
FieldType: FieldTypes.Email,
Value: `${identity.emailPrefix}@${domain}`,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
// Add password
fields.push({
FieldKey: FieldKey.LoginPassword,
Label: 'Password',
FieldType: FieldTypes.Password,
Value: password,
IsHidden: true,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: true
});
// Add alias fields
fields.push({
FieldKey: FieldKey.AliasFirstName,
Label: 'First Name',
FieldType: FieldTypes.Text,
Value: identity.firstName,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push({
FieldKey: FieldKey.AliasLastName,
Label: 'Last Name',
FieldType: FieldTypes.Text,
Value: identity.lastName,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push({
FieldKey: FieldKey.AliasBirthdate,
Label: 'Birth Date',
FieldType: FieldTypes.Date,
Value: IdentityHelperUtils.normalizeBirthDate(identity.birthDate.toISOString()),
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push({
FieldKey: FieldKey.AliasGender,
Label: 'Gender',
FieldType: FieldTypes.Text,
Value: identity.gender,
IsHidden: false,
DisplayOrder: displayOrder++,
IsCustomField: false,
EnableHistory: false
});
fields.push(createSystemField(FieldKey.LoginUsername, { value: identity.nickName }));
fields.push(createSystemField(FieldKey.LoginEmail, { value: `${identity.emailPrefix}@${domain}` }));
fields.push(createSystemField(FieldKey.LoginPassword, { value: password }));
fields.push(createSystemField(FieldKey.AliasFirstName, { value: identity.firstName }));
fields.push(createSystemField(FieldKey.AliasLastName, { value: identity.lastName }));
fields.push(createSystemField(FieldKey.AliasBirthdate, { value: IdentityHelperUtils.normalizeBirthDate(identity.birthDate.toISOString()) }));
fields.push(createSystemField(FieldKey.AliasGender, { value: identity.gender }));
newItem = {
Id: '',

View File

@@ -17,7 +17,7 @@ import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
import { extractDomain, extractRootDomain, filterItems, AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
import type { Item, Passkey } from '@/utils/dist/core/models/vault';
import { FieldKey, FieldTypes, ItemTypes, getFieldValue } from '@/utils/dist/core/models/vault';
import { FieldKey, ItemTypes, getFieldValue, createSystemField } from '@/utils/dist/core/models/vault';
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
import type { CreateRequest, PasskeyCreateCredentialResponse, PendingPasskeyCreateRequest } from '@/utils/passkey/types';
@@ -325,8 +325,8 @@ const PasskeyCreate: React.FC = () => {
Logo: faviconLogo ?? existingItem.Logo,
Fields: [
...(existingItem.Fields || []).filter((f) => f.FieldKey !== FieldKey.LoginUrl && f.FieldKey !== FieldKey.LoginUsername),
{ FieldKey: FieldKey.LoginUrl, Label: 'URL', FieldType: FieldTypes.URL, Value: request.origin, IsHidden: false, DisplayOrder: 0, IsCustomField: false, EnableHistory: false },
{ FieldKey: FieldKey.LoginUsername, Label: 'Username', FieldType: FieldTypes.Text, Value: request.publicKey.user.name, IsHidden: false, DisplayOrder: 1, IsCustomField: false, EnableHistory: false }
createSystemField(FieldKey.LoginUrl, { value: request.origin }),
createSystemField(FieldKey.LoginUsername, { value: request.publicKey.user.name }),
]
},
[],
@@ -398,8 +398,8 @@ const PasskeyCreate: React.FC = () => {
ItemType: ItemTypes.Login,
Logo: faviconLogo,
Fields: [
{ FieldKey: FieldKey.LoginUrl, Label: 'URL', FieldType: FieldTypes.URL, Value: request.origin, IsHidden: false, DisplayOrder: 0, IsCustomField: false, EnableHistory: false },
{ FieldKey: FieldKey.LoginUsername, Label: 'Username', FieldType: FieldTypes.Text, Value: request.publicKey.user.name, IsHidden: false, DisplayOrder: 1, IsCustomField: false, EnableHistory: false }
createSystemField(FieldKey.LoginUrl, { value: request.origin }),
createSystemField(FieldKey.LoginUsername, { value: request.publicKey.user.name }),
],
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString()

View File

@@ -361,6 +361,53 @@ declare function groupFieldsByCategory(item: Item): Record<string, ItemField[]>;
* @deprecated Use Item model directly. This is a temporary compatibility layer.
*/
declare function itemToCredential(item: Item): Credential;
/**
* Options for creating a system field.
* Only `value` is required; metadata is derived from SystemFieldRegistry.
*/
type CreateSystemFieldOptions = {
/** The value for the field (string or string[] for multi-value) */
value: string | string[];
/** Override display order (optional, defaults from registry) */
displayOrder?: number;
/** Override label (optional, normally derived from FieldKey for translation) */
label?: string;
};
/**
* Options for creating a custom field.
*/
type CreateCustomFieldOptions = {
/** Unique identifier for the custom field (UUID) */
fieldKey: string;
/** Display label for the field */
label: string;
/** The value for the field */
value: string | string[];
/** Field type for rendering */
fieldType?: FieldType;
/** Whether the field is hidden/masked */
isHidden?: boolean;
/** Display order */
displayOrder?: number;
/** Whether to track history (defaults to false for custom fields) */
enableHistory?: boolean;
};
/**
* Create a system field (ItemField) by FieldKey with metadata derived from SystemFieldRegistry.
*
* @param fieldKey - The system field key (e.g., 'login.username', FieldKey.LoginPassword)
* @param options - Field creation options (value required, displayOrder optional)
* @returns ItemField with proper metadata from SystemFieldRegistry
* @throws Error if fieldKey is not found in SystemFieldRegistry
*/
declare function createSystemField(fieldKey: string, options: CreateSystemFieldOptions): ItemField;
/**
* Create a custom field (ItemField) with sensible defaults.
*
* @param options - Custom field options
* @returns ItemField configured as a custom field
*/
declare function createCustomField(options: CreateCustomFieldOptions): ItemField;
/**
* Per-item-type configuration for a system field.
@@ -499,4 +546,4 @@ type FieldHistory = {
*/
declare const MAX_FIELD_HISTORY_RECORDS = 10;
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
export { type Alias, type Attachment, type CreateCustomFieldOptions, type CreateSystemFieldOptions, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, createCustomField, createSystemField, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };

View File

@@ -405,10 +405,40 @@ function itemToCredential(item) {
HasAttachment: item.HasAttachment
};
}
function createSystemField(fieldKey, options) {
const systemField = getSystemField(fieldKey);
if (!systemField) {
throw new Error(`Unknown system field: ${fieldKey}. Use createCustomField for custom fields.`);
}
return {
FieldKey: fieldKey,
Label: options.label ?? fieldKey,
// UI layer translates via fieldLabels.*
FieldType: systemField.FieldType,
Value: options.value,
IsHidden: systemField.IsHidden,
DisplayOrder: options.displayOrder ?? systemField.DefaultDisplayOrder,
IsCustomField: false,
EnableHistory: systemField.EnableHistory
};
}
function createCustomField(options) {
return {
FieldKey: options.fieldKey,
Label: options.label,
FieldType: options.fieldType ?? FieldTypes.Text,
Value: options.value,
IsHidden: options.isHidden ?? false,
DisplayOrder: options.displayOrder ?? 0,
IsCustomField: true,
EnableHistory: options.enableHistory ?? false
// Custom fields don't track history by default
};
}
// src/vault/FieldHistory.ts
var MAX_FIELD_HISTORY_RECORDS = 10;
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, createCustomField, createSystemField, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

View File

@@ -361,6 +361,53 @@ declare function groupFieldsByCategory(item: Item): Record<string, ItemField[]>;
* @deprecated Use Item model directly. This is a temporary compatibility layer.
*/
declare function itemToCredential(item: Item): Credential;
/**
* Options for creating a system field.
* Only `value` is required; metadata is derived from SystemFieldRegistry.
*/
type CreateSystemFieldOptions = {
/** The value for the field (string or string[] for multi-value) */
value: string | string[];
/** Override display order (optional, defaults from registry) */
displayOrder?: number;
/** Override label (optional, normally derived from FieldKey for translation) */
label?: string;
};
/**
* Options for creating a custom field.
*/
type CreateCustomFieldOptions = {
/** Unique identifier for the custom field (UUID) */
fieldKey: string;
/** Display label for the field */
label: string;
/** The value for the field */
value: string | string[];
/** Field type for rendering */
fieldType?: FieldType;
/** Whether the field is hidden/masked */
isHidden?: boolean;
/** Display order */
displayOrder?: number;
/** Whether to track history (defaults to false for custom fields) */
enableHistory?: boolean;
};
/**
* Create a system field (ItemField) by FieldKey with metadata derived from SystemFieldRegistry.
*
* @param fieldKey - The system field key (e.g., 'login.username', FieldKey.LoginPassword)
* @param options - Field creation options (value required, displayOrder optional)
* @returns ItemField with proper metadata from SystemFieldRegistry
* @throws Error if fieldKey is not found in SystemFieldRegistry
*/
declare function createSystemField(fieldKey: string, options: CreateSystemFieldOptions): ItemField;
/**
* Create a custom field (ItemField) with sensible defaults.
*
* @param options - Custom field options
* @returns ItemField configured as a custom field
*/
declare function createCustomField(options: CreateCustomFieldOptions): ItemField;
/**
* Per-item-type configuration for a system field.
@@ -499,4 +546,4 @@ type FieldHistory = {
*/
declare const MAX_FIELD_HISTORY_RECORDS = 10;
export { type Alias, type Attachment, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
export { type Alias, type Attachment, type CreateCustomFieldOptions, type CreateSystemFieldOptions, type Credential, type EncryptionKey, FieldCategories, type FieldCategory, type FieldHistory, FieldKey, type FieldKeyValue, type FieldType, FieldTypes, type Item, type ItemField, type ItemTag, type ItemTagRef, type ItemType, type ItemTypeFieldConfig, ItemTypes, MAX_FIELD_HISTORY_RECORDS, type Passkey, type PasswordSettings, type SystemFieldDefinition, SystemFieldRegistry, type Tag, type TotpCode, createCustomField, createSystemField, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };

View File

@@ -405,10 +405,40 @@ function itemToCredential(item) {
HasAttachment: item.HasAttachment
};
}
function createSystemField(fieldKey, options) {
const systemField = getSystemField(fieldKey);
if (!systemField) {
throw new Error(`Unknown system field: ${fieldKey}. Use createCustomField for custom fields.`);
}
return {
FieldKey: fieldKey,
Label: options.label ?? fieldKey,
// UI layer translates via fieldLabels.*
FieldType: systemField.FieldType,
Value: options.value,
IsHidden: systemField.IsHidden,
DisplayOrder: options.displayOrder ?? systemField.DefaultDisplayOrder,
IsCustomField: false,
EnableHistory: systemField.EnableHistory
};
}
function createCustomField(options) {
return {
FieldKey: options.fieldKey,
Label: options.label,
FieldType: options.fieldType ?? FieldTypes.Text,
Value: options.value,
IsHidden: options.isHidden ?? false,
DisplayOrder: options.displayOrder ?? 0,
IsCustomField: true,
EnableHistory: options.enableHistory ?? false
// Custom fields don't track history by default
};
}
// src/vault/FieldHistory.ts
var MAX_FIELD_HISTORY_RECORDS = 10;
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
export { FieldCategories, FieldKey, FieldTypes, ItemTypes, MAX_FIELD_HISTORY_RECORDS, SystemFieldRegistry, createCustomField, createSystemField, fieldAppliesToType, getAllSystemFieldKeys, getDefaultFieldsForItemType, getFieldConfigForType, getFieldValue, getFieldValues, getOptionalFieldsForItemType, getSystemField, getSystemFieldsForItemType, groupFields, groupFieldsByCategory, hasField, isFieldShownByDefault, isSystemField, isSystemFieldPrefix, itemToCredential };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map

View File

@@ -1,7 +1,8 @@
import type { Item, ItemField } from './Item';
import type { Item, ItemField, FieldType } from './Item';
import { FieldTypes } from './Item';
import type { Credential } from './Credential';
import { FieldKey } from './FieldKey';
import { FieldCategories } from './SystemFieldRegistry';
import { FieldCategories, getSystemField } from './SystemFieldRegistry';
/**
* Helper functions for working with Item model
@@ -105,3 +106,81 @@ export function itemToCredential(item: Item): Credential {
HasAttachment: item.HasAttachment
};
}
/**
* Options for creating a system field.
* Only `value` is required; metadata is derived from SystemFieldRegistry.
*/
export type CreateSystemFieldOptions = {
/** The value for the field (string or string[] for multi-value) */
value: string | string[];
/** Override display order (optional, defaults from registry) */
displayOrder?: number;
/** Override label (optional, normally derived from FieldKey for translation) */
label?: string;
};
/**
* Options for creating a custom field.
*/
export type CreateCustomFieldOptions = {
/** Unique identifier for the custom field (UUID) */
fieldKey: string;
/** Display label for the field */
label: string;
/** The value for the field */
value: string | string[];
/** Field type for rendering */
fieldType?: FieldType;
/** Whether the field is hidden/masked */
isHidden?: boolean;
/** Display order */
displayOrder?: number;
/** Whether to track history (defaults to false for custom fields) */
enableHistory?: boolean;
};
/**
* Create a system field (ItemField) by FieldKey with metadata derived from SystemFieldRegistry.
*
* @param fieldKey - The system field key (e.g., 'login.username', FieldKey.LoginPassword)
* @param options - Field creation options (value required, displayOrder optional)
* @returns ItemField with proper metadata from SystemFieldRegistry
* @throws Error if fieldKey is not found in SystemFieldRegistry
*/
export function createSystemField(fieldKey: string, options: CreateSystemFieldOptions): ItemField {
const systemField = getSystemField(fieldKey);
if (!systemField) {
throw new Error(`Unknown system field: ${fieldKey}. Use createCustomField for custom fields.`);
}
return {
FieldKey: fieldKey,
Label: options.label ?? fieldKey, // UI layer translates via fieldLabels.*
FieldType: systemField.FieldType,
Value: options.value,
IsHidden: systemField.IsHidden,
DisplayOrder: options.displayOrder ?? systemField.DefaultDisplayOrder,
IsCustomField: false,
EnableHistory: systemField.EnableHistory,
};
}
/**
* Create a custom field (ItemField) with sensible defaults.
*
* @param options - Custom field options
* @returns ItemField configured as a custom field
*/
export function createCustomField(options: CreateCustomFieldOptions): ItemField {
return {
FieldKey: options.fieldKey,
Label: options.label,
FieldType: options.fieldType ?? FieldTypes.Text,
Value: options.value,
IsHidden: options.isHidden ?? false,
DisplayOrder: options.displayOrder ?? 0,
IsCustomField: true,
EnableHistory: options.enableHistory ?? false, // Custom fields don't track history by default
};
}