/** * types.ts — cross-boundary types and the `MessageType` union. * * `MessageType` mixes background-bound and content-script-bound messages. * Background-bound types have a case in `src/background/messages.ts`. * Content-script-bound types (PING, SUMMON_AUTOFILL, VAULT_LOCKED, * RESTART_AUTO_LOCK, etc.) are sent *to* content scripts or the popup and * have no handler in messages.ts by design — that absence is not a missing * handler. Add new cross-boundary types here, not inline in call sites. */ export interface ManualSummonRecord { firstSummonedAt: number; summonCount: number; lastSummonedAt: number; } export interface Canary { webhookUrl: string; label?: string; lastPinged?: number; pingCount?: number; } export type CardNetwork = "visa" | "mastercard" | "amex" | "discover" | "unknown"; /** * Passkey credential data attached to a VaultEntry. New in v2.11.0 — passkeys * are no longer a separate entry type; instead they hang off any login entry. * A passkey-only entry is a `type:"login"` entry with no username/password * but a `passkey` sub-object set. A login that ALSO has a passkey is just a * normal login with `passkey` populated. * * privateKeyJwk is present only on Chrome-created passkeys, where Peach is * the authenticator. Firefox-created passkeys delegate to the platform * authenticator, so the field is absent — `browser` distinguishes the two. */ export interface PasskeyData { rpId: string; rpName: string; credentialId: string; publicKeyCose: string; privateKeyJwk?: JsonWebKey; userHandle: string; userName: string; userDisplayName: string; algorithm: -7 | -8; signCount: number; aaguid: string; backupEligible: boolean; backupState: boolean; lastUsedAt?: number; browser?: "chrome" | "firefox"; transports?: string[]; /** When the credential was first registered with Peach. */ createdAt: number; } /** One past password kept in an entry's {@link VaultEntry.passwordHistory}. */ export interface PasswordHistoryItem { /** The old password value, replaced by a newer one. */ password: string; /** * When this password stopped being current — i.e. the `passwordChangedAt` * (or `createdAt`) it carried at the moment it was swapped out. */ changedAt: number; } export interface VaultEntry { id: string; // crypto.randomUUID() /** Entry type. Passkey credentials are carried in the nested `passkey` field. */ type?: "login" | "note" | "passkey" | "card"; title: string; username?: string; // only for login entries email?: string; // separate email address — for sites that require both a username and an email password?: string; // only for login entries url?: string; // only for login entries /** * Additional sites this entry should match for autofill. Login entries only. * Each value is a full origin or URL (e.g. `https://login.foo.com`). * The vault domain index keys against the hostname of every entry in this * list, in addition to `url`. */ additionalUrls?: string[]; totp?: string; // base32 seed, stored encrypted — only for login entries /** * Optional TOTP parameters parsed from an otpauth:// URI. Absent fields fall * back to RFC defaults (SHA-1 / 6 digits / 30-second period). Login entries only. */ totpAlgorithm?: "SHA1" | "SHA256" | "SHA512"; totpDigits?: number; totpPeriod?: number; notes?: string; breach?: number; // HIBP exposure count; 0 or undefined = clean — only for login entries /** * When the password field was last changed. Set on first-write of `password` * and refreshed on every subsequent change. */ passwordChangedAt?: number; /** * Previously-used passwords for this entry, newest first. A snapshot of the * old password is pushed here automatically whenever the password changes * (see `updateEntry` in vault.ts). Capped at PASSWORD_HISTORY_MAX. Lets the * user reveal, copy, or restore a password they used before — handy when a * site rejects a rotation or an old credential resurfaces. */ passwordHistory?: PasswordHistoryItem[]; canary?: Canary; createdAt: number; // Date.now() modifiedAt: number; deletedAt?: number; tags?: string[]; /** Passkey credential attached to this entry, if any. */ passkey?: PasskeyData; // Card entry fields cardholderName?: string; cardNumber?: string; // full PAN, stored encrypted in vault expMonth?: string; expYear?: string; cvc?: string; cardNickname?: string; network?: CardNetwork; last4?: string; } /** Returns true when an entry has a passkey credential attached. */ export function hasPasskey(entry: VaultEntry): boolean { return entry.passkey !== undefined && entry.passkey.credentialId.length > 0; } /** Returns true when an entry is passkey-only and has no password to fill. */ export function isPasskeyOnly(entry: VaultEntry): boolean { // An entry is "passkey only" when it holds SOLELY a passkey credential — // no password, no TOTP, and no dedicated email. Username is intentionally // excluded because it is useful for searching passkey credentials. // Entries with passkey + TOTP or passkey + email are NOT passkey-only: they // carry fillable credentials and must show the full login-fields UI. return hasPasskey(entry) && !entry.password && !entry.totp && !entry.email; } export interface PendingPasskeyRequest { requestId: number; type: "create" | "get"; rpId: string; rpName: string; userName: string; userDisplayName?: string; /** Originating tab and frame binding for content-script approval responses. */ tabId?: number; origin?: string; /** * Firefox path: when present, the popup performs the navigator.credentials * call itself rather than completing a chrome.webAuthenticationProxy * request. Absent for Chrome (the existing webAuthenticationProxy flow). */ browser?: "chrome" | "firefox"; /** Firefox create: base64url challenge minted by the popup-context flow. */ challenge?: string; /** Firefox create: base64url user handle. Generated if absent. */ userHandle?: string; } export interface PasskeyProviderState { supported: boolean; enabled: boolean; attached: boolean; browserLabel: string; statusLabel: string; conflict?: string; } export interface StoredVault { // Not authenticated as AAD — the actual AES-GCM AAD is the slot's constant // aadTag string (see background/aes.ts). The version field is unauthenticated // metadata; downgrade prevention does not rest on it. version: 1; salt: string; // hex --- 16 bytes, CSPRNG, stored not secret nonce: string; // hex --- 12 bytes, CSPRNG, fresh per write ciphertext: string; // hex --- AES-256-GCM output tag: string; // hex --- 16-byte GCM auth tag // Recovery escrow: root key wrapped under recovery words via AES-256-GCM. recovery_salt?: string; // hex --- 16 bytes, CSPRNG, HKDF salt for recovery key recovery_wrapped_root_key?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key recovery_nonce?: string; // hex --- 12 bytes, fresh nonce for the above recovery_tag?: string; // hex --- 16-byte GCM auth tag for the above // Second wrapping of the same root key under the password-derived key. // Lets unlock() recover the rootKey bytes without the user's recovery // words, so GET_RECOVERY_WORDS works across lock/unlock cycles. Refreshed // on every password change so the words stay valid (H2/COR-006). pw_root_ciphertext?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key (hex-encoded) pw_root_nonce?: string; // hex --- 12-byte nonce pw_root_tag?: string; // hex --- 16-byte GCM auth tag } // Background-bound message types — sent from popup/content scripts, // handled in src/background/messages.ts. Everything in this group must // have a matching case in that file's switch statement. export type MessageType = | "GET_SYNC_ALERTS" | "CLEAR_CLIPBOARD" | "GET_PENDING_VOUCHED_DEVICES" | "CONFIRM_VOUCHED_DEVICE" | "REJECT_VOUCHED_DEVICE" | "VOUCHED_DEVICE_PENDING" | "ROTATE_SYNC_IDENTITY" | "DEVICE_KEY_ROTATED" | "UNLOCK" | "UNLOCK_WITH_PIN" | "GET_PIN_STATUS" | "LOCK" | "IS_UNLOCKED" | "HAS_VAULT" | "GET_ENTRIES" | "EXPORT_ENTRIES" | "ADD_ENTRY" | "BATCH_ADD_ENTRIES" | "RESTORE_ENTRIES" | "CLEAR_VAULT_ENTRIES" | "UPDATE_ENTRY" | "ADD_ADDITIONAL_URL" | "REQUEST_REMEMBER_SITE" | "DELETE_ENTRY" | "AUTOFILL_QUERY" | "COPY" | "CREATE_VAULT" | "CREATE_VAULT_FROM_ROOT_KEY" | "GET_ENTRY_FOR_FILL" | "GET_ENTRY_FOR_EDIT" | "CHECK_ALL_BREACHES" | "GET_SETTINGS" | "UPDATE_SETTINGS" | "SETUP_PIN" | "DISABLE_PIN" | "FIRE_CANARY_PING" | "ACTIVATE_LICENSE" | "GET_LICENSE" | "GET_LICENSE_KEY" | "IS_DURESS_ENABLED" | "SETUP_DURESS" | "DISABLE_DURESS" | "VERIFY_MAIN_PASSWORD" | "CHANGE_PASSWORD" | "GET_SITE_PASSWORD_POLICY" | "GET_LOCAL_IDENTITY" | "LIST_PAIRED_DEVICES" | "RENAME_PAIRED_DEVICE" | "UNPAIR_DEVICE" | "RENAME_LOCAL_DEVICE" // Relay pairing. START/CONFIRM/CANCEL are popup→bg requests handled in the // switch; ADOPTION_SOURCE_SAS / ADOPTION_SOURCE_RESULT are bg→popup pushes. | "START_ADOPTION_SOURCE" | "START_ADOPTION_RECEIVER" | "LIST_PENDING_ADOPTION_HOLDERS" | "REMOVE_PENDING_ADOPTION_HOLDER" | "CANCEL_ADOPTION_SOURCE" | "SOURCE_SAS_DECISION" | "RECEIVER_SAS_DECISION" | "ADOPTION_SOURCE_SAS" | "ADOPTION_SOURCE_RESULT" | "TRIGGER_SYNC" | "GET_SYNC_STATE" | "GET_SYNC_GENERATION" | "SECURE_RESET" | "GET_SYNC_SETTINGS" | "SET_SYNC_SETTINGS" | "GET_PENDING_PASSKEY" | "APPROVE_PASSKEY" | "DENY_PASSKEY" | "GET_PASSKEY_PROVIDER_STATE" | "SET_PASSKEY_PROVIDER_ENABLED" | "GET_DELETED_ENTRIES" | "RESTORE_ENTRY" | "PURGE_ENTRY" | "PURGE_EXPIRED_TRASH" | "GET_VAULT_HISTORY" | "RESTORE_VAULT_SNAPSHOT" | "COUNT_DUPLICATES" | "DEDUPLICATE_ENTRIES" | "RECORD_MANUAL_SUMMON" | "DECLINE_CARD_SAVE" | "IS_CARD_DECLINED" | "GET_VAULT_STATUS" | "GET_ADVANCED_HEALTH_REPORT" | "DEACTIVATE_LICENSE" | "GET_IDENTITIES" | "DESTROY_VAULT" | "INITIATE_FIREFOX_PASSKEY_CREATE" | "COMPLETE_FIREFOX_PASSKEY_CREATE" | "PASSKEY_OFFER_DECLINE" | "GET_PASSKEY_OFFER_DECLINED" | "GET_PASSKEY_FOR_DOMAIN" | "MARK_PASSKEY_BANNER_SHOWN" // Firefox content-script proxy path (v2.12.0). Sent FROM the page-world // monkey-patch through the extension-world relay TO the background. See // src/shared/passkey-flags.ts for removal conditions. | "PASSKEY_HAS_CREDENTIALS" | "PASSKEY_CREATE" | "PASSKEY_GET" // Recovery word messages | "VALIDATE_RECOVERY_WORDS" | "RECOVER_WITH_WORDS" | "GET_RECOVERY_WORDS" | "GET_RECOVERY_CONFIRM_PENDING" | "SET_RECOVERY_CONFIRM_PENDING" | "CLEAR_RECOVERY_CONFIRM_PENDING" | "GET_IDENTITY_SUGGESTIONS" | "TOUCH_AUTO_LOCK" | "SAVE_NOTE" | "DELETE_NOTE" | "GET_NOTES" | "SEARCH_VAULT" | "ML_INIT" | "ML_PREDICT_BATCH" // Content-script-bound message types — sent BY the background TO content // scripts. They never reach messages.ts's switch. Kept in this union so // existing call sites type-check; do not add cases for them in the // background handler. | "PING" // background → content: liveness probe | "SUMMON_AUTOFILL" // background → content: open the in-page palette | "VAULT_LOCKED" // background → content/popup: vault just locked | "VAULT_UNLOCKED" // background → content/popup: vault just unlocked | "PRO_GATE_TRIGGERED" // background → popup: a Pro feature was attempted on free | "PASSKEY_PRO_GATE" // background → content: show passkey Pro gate in inline popup | "OPEN_PRO_GATE" // content → background: open popup and navigate to License section | "OPEN_POPUP" // content → background: open the extension popup (e.g. to unlock) | "REQUEST_REMEMBER_SITE" // content → background: queue "use this entry on this site" for the popup to confirm | "OPEN_DETACHED_POPUP" // popup → background: open the popup HTML in a standalone window (file-picker flows) | "SHOW_PASSKEY_TOAST" // background → content tab: show passkey auth toast | "DISMISS_PASSKEY_TOAST" // background → content tab: dismiss passkey auth toast | "PASSKEY_AUTH_RESPONSE" // content → background: user approved or denied the toast | "SHOULD_PLAY_CELEBRATION" | "MARK_CELEBRATION_PLAYED" // Peach Codex (v2.42.0). Generation runs in the popup — the SW only // sources vault data and applies the restored slots. | "GET_CODEX_VAULT_DATA" | "ESTIMATE_CODEX_PAGES" | "GENERATE_CODEX" | "RESTORE_FROM_CODEX"; export type ProFeature = | "sync" | "passkeys" | "advanced_health" | "ios_app" | "codex" | "canary_entry" | "duress_vault"; /** * License export shape included in PeachScript and encrypted JSON backups. * `celebrationPlayed` is intentionally absent — a new device deserves the moment. */ export interface PeachLicenseExport { key: string; activatedAt?: number; } export interface AdvancedHealthEntryReport { id: string; title: string; domain: string; url?: string; score: number; ageDays: number; reasons: string[]; factors: { breached: boolean; breachCount: number; reused: boolean; aged: boolean; weak: boolean; missingTotpOnSensitive: boolean; }; } export interface AdvancedHealthReport { vaultScore: number; generatedAt: number; entryCount: number; reusedGroups: number; agedCount: number; remediation: AdvancedHealthEntryReport[]; } export const ACCENT_COLORS = [ { name: "peach", hex: "#F49C70" }, { name: "rose", hex: "#FF8AA6" }, { name: "plum", hex: "#D597F2" }, { name: "lavender", hex: "#A6A8FF" }, { name: "sky", hex: "#5BC8EE" }, { name: "mint", hex: "#5EE0B0" }, { name: "sage", hex: "#B5D870" }, { name: "butter", hex: "#FFD45A" }, { name: "sand", hex: "#EDBC85" }, { name: "clay", hex: "#F08660" }, { name: "slate", hex: "#80BEDE" }, { name: "stone", hex: "#C8B8A8" }, ] as const; export type AccentColorName = typeof ACCENT_COLORS[number]["name"]; export interface SavedIdentity { value: string; type: "email" | "username"; lastUsed: number; useCount: number; exampleDomain?: string; } export interface PasswordPolicy { minLength?: number; maxLength?: number; requiresUppercase?: boolean; requiresLowercase?: boolean; requiresNumbers?: boolean; requiresSymbols?: boolean; forbidsSymbols?: boolean; allowedSymbols?: string; /** Characters the site explicitly disallows. Filtered from every pool before generation. */ forbiddenChars?: string; } /** * A single vault history snapshot. The `vault_a` and `vault_b` fields are the * raw encrypted blobs from chrome.storage.local — already AES-256-GCM * protected by the master password, so storing them in history adds no * security surface. * * Snapshots are taken BEFORE every vault write so the saved blob represents * what is about to be lost, not what was just written. The reason field * documents the trigger so the UI can render "saved" / "synced" / "imported" * / "restored" labels next to each row. */ export interface VaultSnapshot { id: string; savedAt: number; reason: "local_save" | "sync_received" | "import" | "restore"; vault_a: string; vault_b: string; /** * Coarse size bucket (ceiling) of active entries when the snapshot was taken; * null if vault was locked or in decoy mode. PCH-01-023: bucketed, not exact, * so the plaintext value cannot leak vault size or per-write deltas. UI should * render it as "up to N", never as an exact count. */ entryCount: number | null; } export interface VaultHistory { /** Newest first; capped at HISTORY_MAX (10). */ snapshots: VaultSnapshot[]; } export interface Message { type: MessageType; payload?: unknown; } export interface MessageResponse { ok: boolean; data?: unknown; error?: string; vaultId?: string; unlocked?: boolean; // Set on UNLOCK responses when the vault decrypted but failed schema // validation (M7/COR-007). The popup may offer a history-restore flow. canRestoreFromHistory?: boolean; // Set on COPY responses: the effective clipboard auto-clear delay in seconds, // so the content script's TOTP toast can advertise the real configured time. clearSeconds?: number; }