types.ts

16.0 KB · raw

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  /** Entry type. Passkey credentials are carried in the nested `passkey` field. */73  type?: "login" | "note" | "passkey" | "card";74  title: string;75  username?: string; // only for login entries76  email?: string; // separate email address — for sites that require both a username and an email77  password?: string; // only for login entries78  url?: string; // only for login entries79  /**80   * Additional sites this entry should match for autofill. Login entries only.81   * Each value is a full origin or URL (e.g. `https://login.foo.com`).82   * The vault domain index keys against the hostname of every entry in this83   * list, in addition to `url`.84   */85  additionalUrls?: string[];86  totp?: string; // base32 seed, stored encrypted — only for login entries87  /**88   * Optional TOTP parameters parsed from an otpauth:// URI. Absent fields fall89   * back to RFC defaults (SHA-1 / 6 digits / 30-second period). Login entries only.90   */91  totpAlgorithm?: "SHA1" | "SHA256" | "SHA512";92  totpDigits?: number;93  totpPeriod?: number;94  notes?: string;95  breach?: number; // HIBP exposure count; 0 or undefined = clean — only for login entries96  /**97   * When the password field was last changed. Set on first-write of `password`98   * and refreshed on every subsequent change.99   */100  passwordChangedAt?: number;101  /**102   * Previously-used passwords for this entry, newest first. A snapshot of the103   * old password is pushed here automatically whenever the password changes104   * (see `updateEntry` in vault.ts). Capped at PASSWORD_HISTORY_MAX. Lets the105   * user reveal, copy, or restore a password they used before — handy when a106   * site rejects a rotation or an old credential resurfaces.107   */108  passwordHistory?: PasswordHistoryItem[];109  canary?: Canary;110  createdAt: number; // Date.now()111  modifiedAt: number;112  deletedAt?: number;113  tags?: string[];114  /** Passkey credential attached to this entry, if any. */115  passkey?: PasskeyData;116  // Card entry fields117  cardholderName?: string;118  cardNumber?: string;       // full PAN, stored encrypted in vault119  expMonth?: string;120  expYear?: string;121  cvc?: string;122  cardNickname?: string;123  network?: CardNetwork;124  last4?: string;125}126127/** Returns true when an entry has a passkey credential attached. */128export function hasPasskey(entry: VaultEntry): boolean {129  return entry.passkey !== undefined && entry.passkey.credentialId.length > 0;130}131132/** Returns true when an entry is passkey-only and has no password to fill. */133export function isPasskeyOnly(entry: VaultEntry): boolean {134  // An entry is "passkey only" when it holds SOLELY a passkey credential —135  // no password, no TOTP, and no dedicated email. Username is intentionally136  // excluded because it is useful for searching passkey credentials.137  // Entries with passkey + TOTP or passkey + email are NOT passkey-only: they138  // carry fillable credentials and must show the full login-fields UI.139  return hasPasskey(entry) && !entry.password && !entry.totp && !entry.email;140}141142export interface PendingPasskeyRequest {143  requestId: number;144  type: "create" | "get";145  rpId: string;146  rpName: string;147  userName: string;148  userDisplayName?: string;149  /** Originating tab and frame binding for content-script approval responses. */150  tabId?: number;151  origin?: string;152  /**153   * Firefox path: when present, the popup performs the navigator.credentials154   * call itself rather than completing a chrome.webAuthenticationProxy155   * request. Absent for Chrome (the existing webAuthenticationProxy flow).156   */157  browser?: "chrome" | "firefox";158  /** Firefox create: base64url challenge minted by the popup-context flow. */159  challenge?: string;160  /** Firefox create: base64url user handle. Generated if absent. */161  userHandle?: string;162}163164export interface PasskeyProviderState {165  supported: boolean;166  enabled: boolean;167  attached: boolean;168  browserLabel: string;169  statusLabel: string;170  conflict?: string;171}172173export interface StoredVault {174  // Not authenticated as AAD — the actual AES-GCM AAD is the slot's constant175  // aadTag string (see background/aes.ts). The version field is unauthenticated176  // metadata; downgrade prevention does not rest on it.177  version: 1;178  salt: string; // hex --- 16 bytes, CSPRNG, stored not secret179  nonce: string; // hex --- 12 bytes, CSPRNG, fresh per write180  ciphertext: string; // hex --- AES-256-GCM output181  tag: string; // hex --- 16-byte GCM auth tag182  // Recovery escrow: root key wrapped under recovery words via AES-256-GCM.183  recovery_salt?: string; // hex --- 16 bytes, CSPRNG, HKDF salt for recovery key184  recovery_wrapped_root_key?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key185  recovery_nonce?: string; // hex --- 12 bytes, fresh nonce for the above186  recovery_tag?: string; // hex --- 16-byte GCM auth tag for the above187  // Second wrapping of the same root key under the password-derived key.188  // Lets unlock() recover the rootKey bytes without the user's recovery189  // words, so GET_RECOVERY_WORDS works across lock/unlock cycles. Refreshed190  // on every password change so the words stay valid (H2/COR-006).191  pw_root_ciphertext?: string; // hex --- AES-256-GCM ciphertext of 32-byte root key (hex-encoded)192  pw_root_nonce?: string; // hex --- 12-byte nonce193  pw_root_tag?: string; // hex --- 16-byte GCM auth tag194}195196// Background-bound message types — sent from popup/content scripts,197// handled in src/background/messages.ts. Everything in this group must198// have a matching case in that file's switch statement.199export type MessageType =200  | "CLEAR_CLIPBOARD"201  | "GET_SYNC_ALERTS"202  | "DISMISS_SYNC_ALERTS"203  | "GET_PENDING_VOUCHED_DEVICES"204  | "CONFIRM_VOUCHED_DEVICE"205  | "REJECT_VOUCHED_DEVICE"206  | "VOUCHED_DEVICE_PENDING"207  | "ROTATE_SYNC_IDENTITY"208  | "DEVICE_KEY_ROTATED"209  | "UNLOCK"210  | "UNLOCK_WITH_PIN"211  | "GET_PIN_STATUS"212  | "LOCK"213  | "IS_UNLOCKED"214  | "HAS_VAULT"215  | "GET_ENTRIES"216  | "EXPORT_ENTRIES"217  | "ADD_ENTRY"218  | "BATCH_ADD_ENTRIES"219  | "RESTORE_ENTRIES"220  | "CLEAR_VAULT_ENTRIES"221  | "UPDATE_ENTRY"222  | "ADD_ADDITIONAL_URL"223  | "REQUEST_REMEMBER_SITE"224  | "DELETE_ENTRY"225  | "AUTOFILL_QUERY"226  | "COPY"227  | "CREATE_VAULT"228  | "CREATE_VAULT_FROM_ROOT_KEY"229  | "GET_ENTRY_FOR_FILL"230  | "GET_ENTRY_FOR_EDIT"231  | "CHECK_ALL_BREACHES"232  | "GET_SETTINGS"233  | "UPDATE_SETTINGS"234  | "SETUP_PIN"235  | "DISABLE_PIN"236  | "FIRE_CANARY_PING"237  | "ACTIVATE_LICENSE"238  | "GET_LICENSE"239  | "GET_LICENSE_KEY"240  | "IS_DURESS_ENABLED"241  | "SETUP_DURESS"242  | "DISABLE_DURESS"243  | "VERIFY_MAIN_PASSWORD"244  | "CHANGE_PASSWORD"245  | "GET_SITE_PASSWORD_POLICY"246  | "GET_LOCAL_IDENTITY"247  | "LIST_PAIRED_DEVICES"248  | "RENAME_PAIRED_DEVICE"249  | "UNPAIR_DEVICE"250  | "RENAME_LOCAL_DEVICE"251  // Relay pairing. START/CONFIRM/CANCEL are popup→bg requests handled in the252  // switch; ADOPTION_SOURCE_SAS / ADOPTION_SOURCE_RESULT are bg→popup pushes.253  | "START_ADOPTION_SOURCE"254  | "START_ADOPTION_RECEIVER"255  | "LIST_PENDING_ADOPTION_HOLDERS"256  | "REMOVE_PENDING_ADOPTION_HOLDER"257  | "CANCEL_ADOPTION_SOURCE"258  | "SOURCE_SAS_DECISION"259  | "RECEIVER_SAS_DECISION"260  | "ADOPTION_SOURCE_SAS"261  | "ADOPTION_SOURCE_RESULT"262  | "TRIGGER_SYNC"263  | "GET_SYNC_STATE"264  | "GET_SYNC_GENERATION"265  | "SECURE_RESET"266  | "GET_SYNC_SETTINGS"267  | "SET_SYNC_SETTINGS"268  | "GET_PENDING_PASSKEY"269  | "APPROVE_PASSKEY"270  | "DENY_PASSKEY"271  | "GET_PASSKEY_PROVIDER_STATE"272  | "SET_PASSKEY_PROVIDER_ENABLED"273  | "GET_DELETED_ENTRIES"274  | "RESTORE_ENTRY"275  | "PURGE_ENTRY"276  | "PURGE_EXPIRED_TRASH"277  | "GET_VAULT_HISTORY"278  | "RESTORE_VAULT_SNAPSHOT"279  | "COUNT_DUPLICATES"280  | "DEDUPLICATE_ENTRIES"281  | "RECORD_MANUAL_SUMMON"282  | "DECLINE_CARD_SAVE"283  | "IS_CARD_DECLINED"284  | "GET_VAULT_STATUS"285  | "GET_ADVANCED_HEALTH_REPORT"286  | "DEACTIVATE_LICENSE"287  | "GET_IDENTITIES"288  | "DESTROY_VAULT"289  | "INITIATE_FIREFOX_PASSKEY_CREATE"290  | "COMPLETE_FIREFOX_PASSKEY_CREATE"291  | "PASSKEY_OFFER_DECLINE"292  | "GET_PASSKEY_OFFER_DECLINED"293  | "GET_PASSKEY_FOR_DOMAIN"294  | "MARK_PASSKEY_BANNER_SHOWN"295  // Firefox content-script proxy path (v2.12.0). Sent FROM the page-world296  // monkey-patch through the extension-world relay TO the background. See297  // src/shared/passkey-flags.ts for removal conditions.298  | "PASSKEY_HAS_CREDENTIALS"299  | "PASSKEY_CREATE"300  | "PASSKEY_GET"301  // Recovery word messages302  | "VALIDATE_RECOVERY_WORDS"303  | "RECOVER_WITH_WORDS"304  | "GET_RECOVERY_WORDS"305  | "GET_RECOVERY_CONFIRM_PENDING"306  | "SET_RECOVERY_CONFIRM_PENDING"307  | "CLEAR_RECOVERY_CONFIRM_PENDING"308  | "GET_IDENTITY_SUGGESTIONS"309  | "TOUCH_AUTO_LOCK"310  | "SAVE_NOTE"311  | "DELETE_NOTE"312  | "GET_NOTES"313  | "SEARCH_VAULT"314  | "ML_INIT"315  | "ML_PREDICT_BATCH"316  // Content-script-bound message types — sent BY the background TO content317  // scripts. They never reach messages.ts's switch. Kept in this union so318  // existing call sites type-check; do not add cases for them in the319  // background handler.320  | "PING"            // background → content: liveness probe321  | "SUMMON_AUTOFILL" // background → content: open the in-page palette322  | "VAULT_LOCKED"    // background → content/popup: vault just locked323  | "VAULT_UNLOCKED"  // background → content/popup: vault just unlocked324  | "PRO_GATE_TRIGGERED" // background → popup: a Pro feature was attempted on free325  | "PASSKEY_PRO_GATE" // background → content: show passkey Pro gate in inline popup326  | "OPEN_PRO_GATE" // content → background: open popup and navigate to License section327  | "OPEN_POPUP" // content → background: open the extension popup (e.g. to unlock)328  | "REQUEST_REMEMBER_SITE" // content → background: queue "use this entry on this site" for the popup to confirm329  | "OPEN_DETACHED_POPUP" // popup → background: open the popup HTML in a standalone window (file-picker flows)330  | "SHOW_PASSKEY_TOAST"    // background → content tab: show passkey auth toast331  | "DISMISS_PASSKEY_TOAST" // background → content tab: dismiss passkey auth toast332  | "PASSKEY_AUTH_RESPONSE" // content → background: user approved or denied the toast333  | "SHOULD_PLAY_CELEBRATION"334  | "MARK_CELEBRATION_PLAYED"335  // Peach Codex (v2.42.0). Generation runs in the popup — the SW only336  // sources vault data and applies the restored slots.337  | "GET_CODEX_VAULT_DATA"338  | "ESTIMATE_CODEX_PAGES"339  | "GENERATE_CODEX"340  | "RESTORE_FROM_CODEX";341342export type ProFeature =343  | "sync"344  | "passkeys"345  | "advanced_health"346  | "ios_app"347  | "codex"348  | "canary_entry"349  | "duress_vault";350351/**352 * License export shape included in PeachScript and encrypted JSON backups.353 * `celebrationPlayed` is intentionally absent — a new device deserves the moment.354 */355export interface PeachLicenseExport {356  key: string;357  activatedAt?: number;358}359360export interface AdvancedHealthEntryReport {361  id: string;362  title: string;363  domain: string;364  url?: string;365  score: number;366  ageDays: number;367  reasons: string[];368  factors: {369    breached: boolean;370    breachCount: number;371    reused: boolean;372    aged: boolean;373    weak: boolean;374    missingTotpOnSensitive: boolean;375  };376}377378export interface AdvancedHealthReport {379  vaultScore: number;380  generatedAt: number;381  entryCount: number;382  reusedGroups: number;383  agedCount: number;384  remediation: AdvancedHealthEntryReport[];385}386387export const ACCENT_COLORS = [388  { name: "peach",    hex: "#F49C70" },389  { name: "rose",     hex: "#FF8AA6" },390  { name: "plum",     hex: "#D597F2" },391  { name: "lavender", hex: "#A6A8FF" },392  { name: "sky",      hex: "#5BC8EE" },393  { name: "mint",     hex: "#5EE0B0" },394  { name: "sage",     hex: "#B5D870" },395  { name: "butter",   hex: "#FFD45A" },396  { name: "sand",     hex: "#EDBC85" },397  { name: "clay",     hex: "#F08660" },398  { name: "slate",    hex: "#80BEDE" },399  { name: "stone",    hex: "#C8B8A8" },400] as const;401402export type AccentColorName = typeof ACCENT_COLORS[number]["name"];403404export interface SavedIdentity {405  value: string;406  type: "email" | "username";407  lastUsed: number;408  useCount: number;409  exampleDomain?: string;410}411412export interface PasswordPolicy {413  minLength?: number;414  maxLength?: number;415  requiresUppercase?: boolean;416  requiresLowercase?: boolean;417  requiresNumbers?: boolean;418  requiresSymbols?: boolean;419  forbidsSymbols?: boolean;420  allowedSymbols?: string;421  /** Characters the site explicitly disallows. Filtered from every pool before generation. */422  forbiddenChars?: string;423}424425/**426 * A single vault history snapshot. The `vault_a` and `vault_b` fields are the427 * raw encrypted blobs from chrome.storage.local — already AES-256-GCM428 * protected by the master password, so storing them in history adds no429 * security surface.430 *431 * Snapshots are taken BEFORE every vault write so the saved blob represents432 * what is about to be lost, not what was just written. The reason field433 * documents the trigger so the UI can render "saved" / "synced" / "imported"434 * / "restored" labels next to each row.435 */436export interface VaultSnapshot {437  id: string;438  savedAt: number;439  reason: "local_save" | "sync_received" | "import" | "restore";440  vault_a: string;441  vault_b: string;442  /**443   * Coarse size bucket (ceiling) of active entries when the snapshot was taken;444   * null if vault was locked or in decoy mode. PCH-01-023: bucketed, not exact,445   * so the plaintext value cannot leak vault size or per-write deltas. UI should446   * render it as "up to N", never as an exact count.447   */448  entryCount: number | null;449}450451export interface VaultHistory {452  /** Newest first; capped at HISTORY_MAX (10). */453  snapshots: VaultSnapshot[];454}455456export interface Message {457  type: MessageType;458  payload?: unknown;459}460461export interface MessageResponse {462  ok: boolean;463  data?: unknown;464  error?: string;465  vaultId?: string;466  unlocked?: boolean;467  // Set on UNLOCK responses when the vault decrypted but failed schema468  // validation (M7/COR-007). The popup may offer a history-restore flow.469  canRestoreFromHistory?: boolean;470  // Set on COPY responses: the effective clipboard auto-clear delay in seconds,471  // so the content script's TOTP toast can advertise the real configured time.472  clearSeconds?: number;473}