types.ts

12.8 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}5859export interface VaultEntry {60  id: string; // crypto.randomUUID()61  /**62   * Entry type. As of v2.11.0 passkeys are stored on `type:"login"` entries63   * via the `passkey` sub-object — `"passkey"` here is a legacy value still64   * accepted on read but rewritten to `"login"` on next vault save.65   */66  type?: "login" | "note" | "passkey" | "card";67  title: string;68  username?: string; // only for login entries69  password?: string; // only for login entries70  url?: string; // only for login entries71  /**72   * Additional sites this entry should match for autofill. Login entries only.73   * Each value is a full origin or URL (e.g. `https://login.foo.com`).74   * The vault domain index keys against the hostname of every entry in this75   * list, in addition to `url`.76   */77  additionalUrls?: string[];78  totp?: string; // base32 seed, stored encrypted — only for login entries79  notes?: string;80  breach?: number; // HIBP exposure count; 0 or undefined = clean — only for login entries81  /**82   * When the password field was last changed. Set on first-write of `password`83   * and refreshed on every subsequent change. Defaults to `createdAt` when84   * legacy entries are read by the advanced health analyser.85   */86  passwordChangedAt?: number;87  canary?: Canary;88  createdAt: number; // Date.now()89  modifiedAt: number;90  deletedAt?: number;91  tags?: string[];92  /** Passkey credential attached to this entry, if any. */93  passkey?: PasskeyData;94  // ── Legacy passkey flat fields (pre-v2.11.0). Populated by vault read95  // migration so that the old import/sanitize paths keep typing; new code96  // should always read `entry.passkey?.*` instead. The migration drops these97  // on next vault save. Do not introduce new readers of these fields.98  rpId?: string;99  rpName?: string;100  credentialId?: string;101  publicKeyCose?: string;102  privateKeyJwk?: JsonWebKey;103  userHandle?: string;104  userName?: string;105  userDisplayName?: string;106  algorithm?: -7 | -8;107  signCount?: number;108  aaguid?: string;109  backupEligible?: boolean;110  backupState?: boolean;111  lastUsedAt?: number;112  browser?: "chrome" | "firefox";113  transports?: string[];114  // Card entry fields115  cardholderName?: string;116  cardNumber?: string;       // full PAN, stored encrypted in vault117  expMonth?: string;118  expYear?: string;119  cvc?: string;120  cardNickname?: string;121  network?: CardNetwork;122  last4?: string;123}124125/**126 * Returns true when an entry has a passkey credential attached. After v2.11.0127 * migration this is the canonical check; pre-migration entries with a flat128 * `rpId`/`credentialId` shape get rewritten by the vault read pass before129 * any consumer sees them.130 */131export function hasPasskey(entry: VaultEntry): boolean {132  return entry.passkey !== undefined && entry.passkey.credentialId.length > 0;133}134135/**136 * Returns true when an entry is "passkey-only" — has a passkey credential137 * but no password to fill into a form. Login fields hide for these entries138 * and the inline autofill selector filters them out (signing in via the139 * passkey is a separate WebAuthn flow). Username may still be populated140 * (the migration mirrors passkey.userName onto the entry's username so the141 * domain-index/search keep working) and is not part of this discriminator.142 */143export function isPasskeyOnly(entry: VaultEntry): boolean {144  return hasPasskey(entry) && !entry.password;145}146147export interface LegacyVaultEntry {148  id: string;149  title: string;150  username: string;151  password: string;152  url: string;153  totp?: string;154  notes?: string;155  breach?: number;156  created: number;157  updated: number;158  deletedAt?: number;159}160161export interface CardEntry extends VaultEntry {162  type: "card";163  cardholderName: string;164  cardNumber: string;165  expMonth: string;166  expYear: string;167  cvc: string;168}169170171export interface PendingPasskeyRequest {172  requestId: number;173  type: "create" | "get";174  rpId: string;175  rpName: string;176  userName: string;177  userDisplayName?: string;178  /**179   * Firefox path: when present, the popup performs the navigator.credentials180   * call itself rather than completing a chrome.webAuthenticationProxy181   * request. Absent for Chrome (the existing webAuthenticationProxy flow).182   */183  browser?: "chrome" | "firefox";184  /** Firefox create: base64url challenge minted by the popup-context flow. */185  challenge?: string;186  /** Firefox create: base64url user handle. Generated if absent. */187  userHandle?: string;188}189190export interface PasskeyProviderState {191  supported: boolean;192  enabled: boolean;193  attached: boolean;194  browserLabel: string;195  statusLabel: string;196  conflict?: string;197}198199export interface StoredVault {200  version: 1; // AAD for AES-GCM --- prevents downgrade201  salt: string; // hex --- 16 bytes, CSPRNG, stored not secret202  nonce: string; // hex --- 12 bytes, CSPRNG, fresh per write203  ciphertext: string; // hex --- AES-256-GCM output204  tag: string; // hex --- 16-byte GCM auth tag205}206207// Background-bound message types — sent from popup/content scripts,208// handled in src/background/messages.ts. Everything in this group must209// have a matching case in that file's switch statement.210export type MessageType =211  | "UNLOCK"212  | "LOCK"213  | "IS_UNLOCKED"214  | "HAS_VAULT"215  | "GET_ENTRIES"216  | "ADD_ENTRY"217  | "BATCH_ADD_ENTRIES"218  | "RESTORE_ENTRIES"219  | "CLEAR_VAULT_ENTRIES"220  | "UPDATE_ENTRY"221  | "ADD_ADDITIONAL_URL"222  | "DELETE_ENTRY"223  | "AUTOFILL_QUERY"224  | "COPY"225  | "CREATE_VAULT"226  | "GET_ENTRY_FOR_FILL"227  | "CHECK_BREACH"228  | "CHECK_ALL_BREACHES"229  | "GET_SETTINGS"230  | "UPDATE_SETTINGS"231  | "FIRE_CANARY_PING"232  | "ACTIVATE_LICENSE"233  | "GET_LICENSE"234  | "GET_LICENSE_KEY"235  | "IS_DURESS_ENABLED"236  | "SETUP_DURESS"237  | "DISABLE_DURESS"238  | "VERIFY_MAIN_PASSWORD"239  | "CHANGE_PASSWORD"240  | "GET_SITE_PASSWORD_POLICY"241  | "GET_SYNC_STATE"242  | "TRIGGER_SYNC"243  | "START_PAIRING"244  | "CANCEL_PAIRING"245  | "ACCEPT_PAIRING"246  | "REMOVE_PAIRED_DEVICE"247  | "REMOVE_TRUSTED_NETWORK"248  | "MARK_NETWORK_TRUSTED"249  | "RESET_SYNC_IDENTITY"250  | "GET_INCOMING_PAIRING_REQUEST"251  | "RESPOND_INCOMING_PAIRING"252  | "DEBUG_INJECT_INCOMING_PAIRING"253  | "START_PEER_DISCOVERY"254  | "STOP_PEER_DISCOVERY"255  | "INITIATE_PAIR_WITH_PEER"256  | "GET_PENDING_PASSKEY"257  | "APPROVE_PASSKEY"258  | "DENY_PASSKEY"259  | "DELETE_PASSKEY"260  | "GET_PASSKEYS"261  | "GET_PASSKEY_PROVIDER_STATE"262  | "SET_PASSKEY_PROVIDER_ENABLED"263  | "GET_DELETED_ENTRIES"264  | "RESTORE_ENTRY"265  | "PURGE_ENTRY"266  | "PURGE_EXPIRED_TRASH"267  | "GET_VAULT_HISTORY"268  | "RESTORE_VAULT_SNAPSHOT"269  | "COUNT_DUPLICATES"270  | "DEDUPLICATE_ENTRIES"271  | "RECORD_MANUAL_SUMMON"272  | "DECLINE_CARD_SAVE"273  | "IS_CARD_DECLINED"274  | "SUBMIT_FEEDBACK"275  | "GET_VAULT_STATUS"276  | "GET_ADVANCED_HEALTH_REPORT"277  | "INITIATE_PASSWORD_CHANGE"278  | "DEACTIVATE_LICENSE"279  | "GET_IDENTITIES"280  | "DESTROY_VAULT"281  | "INITIATE_FIREFOX_PASSKEY_CREATE"282  | "COMPLETE_FIREFOX_PASSKEY_CREATE"283  | "PASSKEY_OFFER_DECLINE"284  | "GET_PASSKEY_OFFER_DECLINED"285  | "GET_PASSKEY_FOR_DOMAIN"286  // Firefox content-script proxy path (v2.12.0). Sent FROM the page-world287  // monkey-patch through the extension-world relay TO the background. See288  // src/shared/passkey-flags.ts for removal conditions.289  | "PASSKEY_HAS_CREDENTIALS"290  | "PASSKEY_CREATE"291  | "PASSKEY_GET"292  // Content-script-bound message types — sent BY the background TO content293  // scripts. They never reach messages.ts's switch. Kept in this union so294  // existing call sites type-check; do not add cases for them in the295  // background handler.296  | "PING"            // background → content: liveness probe297  | "SUMMON_AUTOFILL" // background → content: open the in-page palette298  | "VAULT_LOCKED"    // background → content/popup: vault just locked299  | "VAULT_UNLOCKED"  // background → content/popup: vault just unlocked300  | "PRO_GATE_TRIGGERED" // background → popup: a Pro feature was attempted on free301  | "PASSKEY_PRO_GATE" // background → content: show passkey Pro gate in inline popup302  | "OPEN_PRO_GATE" // content → background: open popup and navigate to License section303  | "SHOULD_PLAY_CELEBRATION"304  | "MARK_CELEBRATION_PLAYED";305306export type ProFeature =307  | "sync"308  | "passkeys"309  | "advanced_health"310  | "smart_password_change"311  | "ios_app";312313/**314 * License export shape included in PeachScript and encrypted JSON backups.315 * `celebrationPlayed` is intentionally absent — a new device deserves the moment.316 */317export interface PeachLicenseExport {318  key: string;319  activatedAt?: number;320}321322export interface AdvancedHealthEntryReport {323  id: string;324  title: string;325  domain: string;326  url?: string;327  score: number;328  ageDays: number;329  reasons: string[];330  factors: {331    breached: boolean;332    breachCount: number;333    reused: boolean;334    aged: boolean;335    weak: boolean;336    missingTotpOnSensitive: boolean;337  };338}339340export interface AdvancedHealthReport {341  vaultScore: number;342  generatedAt: number;343  entryCount: number;344  reusedGroups: number;345  agedCount: number;346  remediation: AdvancedHealthEntryReport[];347}348349export const ACCENT_COLORS = [350  { name: "peach",    hex: "#F49C70" },351  { name: "rose",     hex: "#FF8AA6" },352  { name: "plum",     hex: "#D597F2" },353  { name: "lavender", hex: "#A6A8FF" },354  { name: "sky",      hex: "#5BC8EE" },355  { name: "mint",     hex: "#5EE0B0" },356  { name: "sage",     hex: "#B5D870" },357  { name: "butter",   hex: "#FFD45A" },358  { name: "sand",     hex: "#EDBC85" },359  { name: "clay",     hex: "#F08660" },360  { name: "slate",    hex: "#80BEDE" },361  { name: "stone",    hex: "#C8B8A8" },362] as const;363364export type AccentColorName = typeof ACCENT_COLORS[number]["name"];365366export interface SavedIdentity {367  value: string;368  type: "email" | "username";369  lastUsed: number;370  useCount: number;371  exampleDomain?: string;372}373374export interface PasswordPolicy {375  minLength?: number;376  maxLength?: number;377  requiresUppercase?: boolean;378  requiresLowercase?: boolean;379  requiresNumbers?: boolean;380  requiresSymbols?: boolean;381  forbidsSymbols?: boolean;382  allowedSymbols?: string;383  /** Characters the site explicitly disallows. Filtered from every pool before generation. */384  forbiddenChars?: string;385}386387/**388 * A single vault history snapshot. The `vault_a` and `vault_b` fields are the389 * raw encrypted blobs from chrome.storage.local — already AES-256-GCM390 * protected by the master password, so storing them in history adds no391 * security surface.392 *393 * Snapshots are taken BEFORE every vault write so the saved blob represents394 * what is about to be lost, not what was just written. The reason field395 * documents the trigger so the UI can render "saved" / "synced" / "imported"396 * / "restored" labels next to each row.397 */398export interface VaultSnapshot {399  id: string;400  savedAt: number;401  reason: "local_save" | "sync_received" | "import" | "restore";402  vault_a: string;403  vault_b: string;404  /** Active entry count when the snapshot was taken; null if vault was locked. */405  entryCount: number | null;406}407408export interface VaultHistory {409  /** Newest first; capped at HISTORY_MAX (10). */410  snapshots: VaultSnapshot[];411}412413export interface Message {414  type: MessageType;415  payload?: unknown;416}417418export interface MessageResponse {419  ok: boolean;420  data?: unknown;421  error?: string;422  unlocked?: boolean;423}