types.ts
1/**2 * types.ts — cross-boundary types and the `MessageType` union.3 *4 * `MessageType` mixes background-bound and content-script-bound messages.5 * Background-bound types have a case in `src/background/messages.ts`.6 * Content-script-bound types (PING, SUMMON_AUTOFILL, VAULT_LOCKED,7 * RESTART_AUTO_LOCK, etc.) are sent *to* content scripts or the popup and8 * have no handler in messages.ts by design — that absence is not a missing9 * handler. Add new cross-boundary types here, not inline in call sites.10 */1112export interface ManualSummonRecord {13 firstSummonedAt: number;14 summonCount: number;15 lastSummonedAt: number;16}1718export interface Canary {19 webhookUrl: string;20 label?: string;21 lastPinged?: number;22 pingCount?: number;23}2425export type CardNetwork = "visa" | "mastercard" | "amex" | "discover" | "unknown";2627/**28 * Passkey credential data attached to a VaultEntry. New in v2.11.0 — passkeys29 * are no longer a separate entry type; instead they hang off any login entry.30 * A passkey-only entry is a `type:"login"` entry with no username/password31 * but a `passkey` sub-object set. A login that ALSO has a passkey is just a32 * normal login with `passkey` populated.33 *34 * privateKeyJwk is present only on Chrome-created passkeys, where Peach is35 * the authenticator. Firefox-created passkeys delegate to the platform36 * authenticator, so the field is absent — `browser` distinguishes the two.37 */38export interface PasskeyData {39 rpId: string;40 rpName: string;41 credentialId: string;42 publicKeyCose: string;43 privateKeyJwk?: JsonWebKey;44 userHandle: string;45 userName: string;46 userDisplayName: string;47 algorithm: -7 | -8;48 signCount: number;49 aaguid: string;50 backupEligible: boolean;51 backupState: boolean;52 lastUsedAt?: number;53 browser?: "chrome" | "firefox";54 transports?: string[];55 /** When the credential was first registered with Peach. */56 createdAt: number;57}5859/** One past password kept in an entry's {@link VaultEntry.passwordHistory}. */60export interface PasswordHistoryItem {61 /** The old password value, replaced by a newer one. */62 password: string;63 /**64 * When this password stopped being current — i.e. the `passwordChangedAt`65 * (or `createdAt`) it carried at the moment it was swapped out.66 */67 changedAt: number;68}6970export interface VaultEntry {71 id: string; // crypto.randomUUID()72 /**73 * Entry type. As of v2.11.0 passkeys are stored on `type:"login"` entries74 * via the `passkey` sub-object — `"passkey"` here is a legacy value still75 * accepted on read but rewritten to `"login"` on next vault save.76 */77 type?: "login" | "note" | "passkey" | "card";78 title: string;79 username?: string; // only for login entries80 email?: string; // separate email address — for sites that require both a username and an email81 password?: string; // only for login entries82 url?: string; // only for login entries83 /**84 * Additional sites this entry should match for autofill. Login entries only.85 * Each value is a full origin or URL (e.g. `https://login.foo.com`).86 * The vault domain index keys against the hostname of every entry in this87 * list, in addition to `url`.88 */89 additionalUrls?: string[];90 totp?: string; // base32 seed, stored encrypted — only for login entries91 /**92 * Optional TOTP parameters parsed from an otpauth:// URI. Absent fields fall93 * back to RFC defaults (SHA-1 / 6 digits / 30-second period), so legacy entries94 * carrying only a bare `totp` seed keep generating identical codes95 * (PCH-01-019). Login entries only.96 */97 totpAlgorithm?: "SHA1" | "SHA256" | "SHA512";98 totpDigits?: number;99 totpPeriod?: number;100 notes?: string;101 breach?: number; // HIBP exposure count; 0 or undefined = clean — only for login entries102 /**103 * When the password field was last changed. Set on first-write of `password`104 * and refreshed on every subsequent change. Defaults to `createdAt` when105 * legacy entries are read by the advanced health analyser.106 */107 passwordChangedAt?: number;108 /**109 * Previously-used passwords for this entry, newest first. A snapshot of the110 * old password is pushed here automatically whenever the password changes111 * (see `updateEntry` in vault.ts). Capped at PASSWORD_HISTORY_MAX. Lets the112 * user reveal, copy, or restore a password they used before — handy when a113 * site rejects a rotation or an old credential resurfaces.114 */115 passwordHistory?: PasswordHistoryItem[];116 canary?: Canary;117 createdAt: number; // Date.now()118 modifiedAt: number;119 deletedAt?: number;120 tags?: string[];121 /** Passkey credential attached to this entry, if any. */122 passkey?: PasskeyData;123 // ── Legacy passkey flat fields (pre-v2.11.0). Populated by vault read124 // migration so that the old import/sanitize paths keep typing; new code125 // should always read `entry.passkey?.*` instead. The migration drops these126 // on next vault save. Do not introduce new readers of these fields.127 rpId?: string;128 rpName?: string;129 credentialId?: string;130 publicKeyCose?: string;131 privateKeyJwk?: JsonWebKey;132 userHandle?: string;133 userName?: string;134 userDisplayName?: string;135 algorithm?: -7 | -8;136 signCount?: number;137 aaguid?: string;138 backupEligible?: boolean;139 backupState?: boolean;140 lastUsedAt?: number;141 browser?: "chrome" | "firefox";142 transports?: string[];143 // Card entry fields144 cardholderName?: string;145 cardNumber?: string; // full PAN, stored encrypted in vault146 expMonth?: string;147 expYear?: string;148 cvc?: string;149 cardNickname?: string;150 network?: CardNetwork;151 last4?: string;152}153154/**155 * Returns true when an entry has a passkey credential attached. After v2.11.0156 * migration this is the canonical check; pre-migration entries with a flat157 * `rpId`/`credentialId` shape get rewritten by the vault read pass before158 * any consumer sees them.159 */160export function hasPasskey(entry: VaultEntry): boolean {161 return entry.passkey !== undefined && entry.passkey.credentialId.length > 0;162}163164/**165 * Returns true when an entry is "passkey-only" — has a passkey credential166 * but no password to fill into a form. Login fields hide for these entries167 * and the inline autofill selector filters them out (signing in via the168 * passkey is a separate WebAuthn flow). Username may still be populated169 * (the migration mirrors passkey.userName onto the entry's username so the170 * domain-index/search keep working) and is not part of this discriminator.171 */172export function isPasskeyOnly(entry: VaultEntry): boolean {173 // An entry is "passkey only" when it holds SOLELY a passkey credential —174 // no password, no TOTP, and no dedicated email. Username is intentionally175 // excluded (the migration copies passkey.userName onto username for search).176 // Entries with passkey + TOTP or passkey + email are NOT passkey-only: they177 // carry fillable credentials and must show the full login-fields UI.178 return hasPasskey(entry) && !entry.password && !entry.totp && !entry.email;179}180181export interface LegacyVaultEntry {182 id: string;183 title: string;184 username: string;185 password: string;186 url: string;187 totp?: string;188 totpAlgorithm?: "SHA1" | "SHA256" | "SHA512";189 totpDigits?: number;190 totpPeriod?: number;191 notes?: string;192 breach?: number;193 created: number;194 updated: number;195 deletedAt?: number;196}197198export interface PendingPasskeyRequest {199 requestId: number;200 type: "create" | "get";201 rpId: string;202 rpName: string;203 userName: string;204 userDisplayName?: string;205 /**206 * Firefox path: when present, the popup performs the navigator.credentials207 * call itself rather than completing a chrome.webAuthenticationProxy208 * request. Absent for Chrome (the existing webAuthenticationProxy flow).209 */210 browser?: "chrome" | "firefox";211 /** Firefox create: base64url challenge minted by the popup-context flow. */212 challenge?: string;213 /** Firefox create: base64url user handle. Generated if absent. */214 userHandle?: string;215}216217export interface PasskeyProviderState {218 supported: boolean;219 enabled: boolean;220 attached: boolean;221 browserLabel: string;222 statusLabel: string;223 conflict?: string;224}225226export interface StoredVault {227 version: 1; // AAD for AES-GCM --- prevents downgrade228 salt: string; // hex --- 16 bytes, CSPRNG, stored not secret229 nonce: string; // hex --- 12 bytes, CSPRNG, fresh per write230 ciphertext: string; // hex --- AES-256-GCM output231 tag: string; // hex --- 16-byte GCM auth tag232 // Recovery escrow: root key wrapped under recovery words via AES-256-GCM.233 // Absent on vaults created before v2.12 (migrateToRecovery adds them on first unlock).234 recovery_salt?: string; // hex --- 16 bytes, CSPRNG, HKDF salt for recovery key235 recovery_wrapped_root_key?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key236 recovery_nonce?: string; // hex --- 12 bytes, fresh nonce for the above237 recovery_tag?: string; // hex --- 16-byte GCM auth tag for the above238 // Second wrapping of the same root key under the password-derived key.239 // Lets unlock() recover the rootKey bytes without the user's recovery240 // words, so GET_RECOVERY_WORDS works across lock/unlock cycles. Refreshed241 // on every password change so the words stay valid (H2/COR-006).242 pw_root_ciphertext?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key (hex-encoded)243 pw_root_nonce?: string; // hex --- 12-byte nonce244 pw_root_tag?: string; // hex --- 16-byte GCM auth tag245}246247// Background-bound message types — sent from popup/content scripts,248// handled in src/background/messages.ts. Everything in this group must249// have a matching case in that file's switch statement.250export type MessageType =251 | "GET_SYNC_ALERTS"252 | "CLEAR_CLIPBOARD"253 | "GET_PENDING_VOUCHED_DEVICES"254 | "CONFIRM_VOUCHED_DEVICE"255 | "REJECT_VOUCHED_DEVICE"256 | "VOUCHED_DEVICE_PENDING"257 | "ROTATE_SYNC_IDENTITY"258 | "DEVICE_KEY_ROTATED"259 | "UNLOCK"260 | "UNLOCK_WITH_PIN"261 | "LOCK"262 | "IS_UNLOCKED"263 | "HAS_VAULT"264 | "GET_ENTRIES"265 | "ADD_ENTRY"266 | "BATCH_ADD_ENTRIES"267 | "RESTORE_ENTRIES"268 | "CLEAR_VAULT_ENTRIES"269 | "UPDATE_ENTRY"270 | "ADD_ADDITIONAL_URL"271 | "DELETE_ENTRY"272 | "AUTOFILL_QUERY"273 | "COPY"274 | "CREATE_VAULT"275 | "CREATE_VAULT_FROM_ROOT_KEY"276 | "GET_ENTRY_FOR_FILL"277 | "GET_ENTRY_FOR_EDIT"278 | "CHECK_ALL_BREACHES"279 | "GET_SETTINGS"280 | "UPDATE_SETTINGS"281 | "SETUP_PIN"282 | "DISABLE_PIN"283 | "FIRE_CANARY_PING"284 | "ACTIVATE_LICENSE"285 | "GET_LICENSE"286 | "GET_LICENSE_KEY"287 | "IS_DURESS_ENABLED"288 | "SETUP_DURESS"289 | "DISABLE_DURESS"290 | "VERIFY_MAIN_PASSWORD"291 | "CHANGE_PASSWORD"292 | "GET_SITE_PASSWORD_POLICY"293 | "GET_LOCAL_IDENTITY"294 | "LIST_PAIRED_DEVICES"295 | "RENAME_PAIRED_DEVICE"296 | "UNPAIR_DEVICE"297 | "RENAME_LOCAL_DEVICE"298 // Relay pairing. START/CONFIRM/CANCEL are popup→bg requests handled in the299 // switch; ADOPTION_SOURCE_SAS / ADOPTION_SOURCE_RESULT are bg→popup pushes.300 | "START_ADOPTION_SOURCE"301 | "START_ADOPTION_RECEIVER"302 | "CANCEL_ADOPTION_SOURCE"303 | "SOURCE_SAS_DECISION"304 | "ADOPTION_SOURCE_SAS"305 | "ADOPTION_SOURCE_RESULT"306 | "TRIGGER_SYNC"307 | "GET_SYNC_STATE"308 | "GET_SYNC_SETTINGS"309 | "SET_SYNC_SETTINGS"310 | "GET_PENDING_PASSKEY"311 | "APPROVE_PASSKEY"312 | "DENY_PASSKEY"313 | "GET_PASSKEY_PROVIDER_STATE"314 | "SET_PASSKEY_PROVIDER_ENABLED"315 | "GET_DELETED_ENTRIES"316 | "RESTORE_ENTRY"317 | "PURGE_ENTRY"318 | "PURGE_EXPIRED_TRASH"319 | "GET_VAULT_HISTORY"320 | "RESTORE_VAULT_SNAPSHOT"321 | "COUNT_DUPLICATES"322 | "DEDUPLICATE_ENTRIES"323 | "RECORD_MANUAL_SUMMON"324 | "DECLINE_CARD_SAVE"325 | "IS_CARD_DECLINED"326 | "GET_VAULT_STATUS"327 | "GET_ADVANCED_HEALTH_REPORT"328 | "DEACTIVATE_LICENSE"329 | "GET_IDENTITIES"330 | "DESTROY_VAULT"331 | "INITIATE_FIREFOX_PASSKEY_CREATE"332 | "COMPLETE_FIREFOX_PASSKEY_CREATE"333 | "PASSKEY_OFFER_DECLINE"334 | "GET_PASSKEY_OFFER_DECLINED"335 | "GET_PASSKEY_FOR_DOMAIN"336 // Firefox content-script proxy path (v2.12.0). Sent FROM the page-world337 // monkey-patch through the extension-world relay TO the background. See338 // src/shared/passkey-flags.ts for removal conditions.339 | "PASSKEY_HAS_CREDENTIALS"340 | "PASSKEY_CREATE"341 | "PASSKEY_GET"342 // Recovery word messages343 | "VALIDATE_RECOVERY_WORDS"344 | "RECOVER_WITH_WORDS"345 | "GET_RECOVERY_WORDS"346 | "GET_IDENTITY_SUGGESTIONS"347 | "TOUCH_AUTO_LOCK"348 | "SAVE_NOTE"349 | "DELETE_NOTE"350 | "GET_NOTES"351 | "SEARCH_VAULT"352 | "ML_INIT"353 | "ML_PREDICT_BATCH"354 // Content-script-bound message types — sent BY the background TO content355 // scripts. They never reach messages.ts's switch. Kept in this union so356 // existing call sites type-check; do not add cases for them in the357 // background handler.358 | "PING" // background → content: liveness probe359 | "SUMMON_AUTOFILL" // background → content: open the in-page palette360 | "VAULT_LOCKED" // background → content/popup: vault just locked361 | "VAULT_UNLOCKED" // background → content/popup: vault just unlocked362 | "PRO_GATE_TRIGGERED" // background → popup: a Pro feature was attempted on free363 | "PASSKEY_PRO_GATE" // background → content: show passkey Pro gate in inline popup364 | "OPEN_PRO_GATE" // content → background: open popup and navigate to License section365 | "OPEN_POPUP" // content → background: open the extension popup (e.g. to unlock)366 | "OPEN_DETACHED_POPUP" // popup → background: open the popup HTML in a standalone window (file-picker flows)367 | "SHOW_PASSKEY_TOAST" // background → content tab: show passkey auth toast368 | "DISMISS_PASSKEY_TOAST" // background → content tab: dismiss passkey auth toast369 | "PASSKEY_AUTH_RESPONSE" // content → background: user approved or denied the toast370 | "SHOULD_PLAY_CELEBRATION"371 | "MARK_CELEBRATION_PLAYED"372 // Peach Codex (v2.42.0). Generation runs in the popup — the SW only373 // sources vault data and applies the restored slots.374 | "GET_CODEX_VAULT_DATA"375 | "ESTIMATE_CODEX_PAGES"376 | "GENERATE_CODEX"377 | "RESTORE_FROM_CODEX";378379export type ProFeature =380 | "sync"381 | "passkeys"382 | "advanced_health"383 | "ios_app"384 | "codex"385 | "canary_entry"386 | "duress_vault";387388/**389 * License export shape included in PeachScript and encrypted JSON backups.390 * `celebrationPlayed` is intentionally absent — a new device deserves the moment.391 */392export interface PeachLicenseExport {393 key: string;394 activatedAt?: number;395}396397export interface AdvancedHealthEntryReport {398 id: string;399 title: string;400 domain: string;401 url?: string;402 score: number;403 ageDays: number;404 reasons: string[];405 factors: {406 breached: boolean;407 breachCount: number;408 reused: boolean;409 aged: boolean;410 weak: boolean;411 missingTotpOnSensitive: boolean;412 };413}414415export interface AdvancedHealthReport {416 vaultScore: number;417 generatedAt: number;418 entryCount: number;419 reusedGroups: number;420 agedCount: number;421 remediation: AdvancedHealthEntryReport[];422}423424export const ACCENT_COLORS = [425 { name: "peach", hex: "#F49C70" },426 { name: "rose", hex: "#FF8AA6" },427 { name: "plum", hex: "#D597F2" },428 { name: "lavender", hex: "#A6A8FF" },429 { name: "sky", hex: "#5BC8EE" },430 { name: "mint", hex: "#5EE0B0" },431 { name: "sage", hex: "#B5D870" },432 { name: "butter", hex: "#FFD45A" },433 { name: "sand", hex: "#EDBC85" },434 { name: "clay", hex: "#F08660" },435 { name: "slate", hex: "#80BEDE" },436 { name: "stone", hex: "#C8B8A8" },437] as const;438439export type AccentColorName = typeof ACCENT_COLORS[number]["name"];440441export interface SavedIdentity {442 value: string;443 type: "email" | "username";444 lastUsed: number;445 useCount: number;446 exampleDomain?: string;447}448449export interface PasswordPolicy {450 minLength?: number;451 maxLength?: number;452 requiresUppercase?: boolean;453 requiresLowercase?: boolean;454 requiresNumbers?: boolean;455 requiresSymbols?: boolean;456 forbidsSymbols?: boolean;457 allowedSymbols?: string;458 /** Characters the site explicitly disallows. Filtered from every pool before generation. */459 forbiddenChars?: string;460}461462/**463 * A single vault history snapshot. The `vault_a` and `vault_b` fields are the464 * raw encrypted blobs from chrome.storage.local — already AES-256-GCM465 * protected by the master password, so storing them in history adds no466 * security surface.467 *468 * Snapshots are taken BEFORE every vault write so the saved blob represents469 * what is about to be lost, not what was just written. The reason field470 * documents the trigger so the UI can render "saved" / "synced" / "imported"471 * / "restored" labels next to each row.472 */473export interface VaultSnapshot {474 id: string;475 savedAt: number;476 reason: "local_save" | "sync_received" | "import" | "restore";477 vault_a: string;478 vault_b: string;479 /**480 * Coarse size bucket (ceiling) of active entries when the snapshot was taken;481 * null if vault was locked or in decoy mode. PCH-01-023: bucketed, not exact,482 * so the plaintext value cannot leak vault size or per-write deltas. UI should483 * render it as "up to N", never as an exact count.484 */485 entryCount: number | null;486}487488export interface VaultHistory {489 /** Newest first; capped at HISTORY_MAX (10). */490 snapshots: VaultSnapshot[];491}492493export interface Message {494 type: MessageType;495 payload?: unknown;496}497498export interface MessageResponse {499 ok: boolean;500 data?: unknown;501 error?: string;502 vaultId?: string;503 unlocked?: boolean;504 // Set on UNLOCK responses when the vault decrypted but failed schema505 // validation (M7/COR-007). The popup may offer a history-restore flow.506 canRestoreFromHistory?: boolean;507 // Set on COPY responses: the effective clipboard auto-clear delay in seconds,508 // so the content script's TOTP toast can advertise the real configured time.509 clearSeconds?: number;510}