ac28263c9b98

test: revive two orphaned mesh-propagation receiver tests (v4.8.6)

Found during biome repair: 'applies the source's device manifest after the snapshot, then acks' and 'does not call applyIncomingDeviceManifest when the source omits the manifest frame' were declared INSIDE the vi.mock(../shared/rendezvous) factory, after its return statement — unreachable, so they never ran. Revived into the receiver describe and updated to the current protocol: - sync-identity mock now partial (importOriginal) so the real deriveSyncSigningKeypair/pubkeyToHex build a verifiable signed manifest. - Frame sequences now include the ADOPT_SNAPSHOT chunk frame that ends the snapshot phase (same shape as the passing duress test). Both pass: the manifest-apply and snapshot-ack behavior in relay-adoption-bg is correct; the tests were stale, not the code. 16/16 in file, full suite 3687 passed / 0 failed.

time
2026-08-04 15:36 UTC · 2 months ago
machine
koh2
parent
41b6448ed641
21 files biome.json+43 −10manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/messages.ts+1 −1src/background/relay-adoption-bg.test.ts+149 −136src/background/sync-vault-adapter.ts+2 −2src/content/detect.ts+1 −1src/content/index.ts+3 −3src/content/inject.ts+1 −1src/content/policy.ts+2 −1src/mobile/chrome-shim.ts+1 −1src/popup/codex/pdf-import.ts+1 −3src/popup/components/GeneratorDrawer.tsx+1 −1src/popup/components/PairingBirds.tsx+1 −1src/popup/components/SyncModeSelector.tsx+1 −1src/popup/views/Lock.tsx+1 −1src/popup/views/PairDevice.tsx+1 −1src/popup/views/Settings.tsx+1 −0src/shared/device-manifest.test.ts+1 −1src/shared/domain.ts+1 −0
biome.json+43 −10
@@ -1,20 +1,53 @@11 {2-  "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",2+  "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",33   "files": {4-    "ignore": [5-      "dist-chrome/**",6-      "dist-firefox/**",7-      "dist-mobile/**",8-      "node_modules/**",9-      ".koh/**",10-      ".deepsec/**",11-      "crypto-core/**"4+    "includes": [5+      "**",6+      "!dist-chrome/**",7+      "!dist-firefox/**",8+      "!dist-mobile/**",9+      "!node_modules/**",10+      "!.koh/**",11+      "!.deepsec/**",12+      "!crypto-core/**",13+      "!src/drafts/**",14+      "!src/shared/noise-core-wasm/**"1215     ]1316   },17+  "formatter": {18+    "enabled": false19+  },20+  "assist": {21+    "actions": {22+      "source": {23+        "organizeImports": "off"24+      }25+    }26+  },1427   "linter": {1528     "enabled": true,1629     "rules": {17-      "recommended": true30+      "recommended": true,31+      "a11y": "off",32+      "style": {33+        "noNonNullAssertion": "off",34+        "useTemplate": "off",35+        "noDescendingSpecificity": "off"36+      },37+      "complexity": {38+        "noImportantStyles": "off",39+        "noBannedTypes": "off",40+        "useLiteralKeys": "off",41+        "useArrowFunction": "off",42+        "useOptionalChain": "off"43+      },44+      "correctness": {45+        "noInnerDeclarations": "off"46+      },47+      "suspicious": {48+        "noExplicitAny": "off",49+        "useIterableCallbackReturn": "off"50+      }1851     }1952   }2053 }

view file

manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.8.5",4+  "version": "4.8.6",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "4.8.5",5+  "version": "4.8.6",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.8.5",3+  "version": "4.8.6",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/messages.ts+1 −1
@@ -411,7 +411,7 @@411411         try {412412           const words = await vault.getRecoveryWords();413413           return { ok: true, data: words };414-        } catch (e) {414+        } catch {415415           // pre-v2.12 vault that hasn't migrated416416           return { ok: false, error: "recovery_not_setup" };417417         }

view file

src/background/relay-adoption-bg.test.ts+149 −136
@@ -39,139 +39,6 @@3939     ...actual,4040     generatePairingRendezvousToken: () => "AAECAwQFBgcICQoLDA0ODw",4141   };42-  it("applies the source's device manifest after the snapshot, then acks", async () => {43-    // Mesh propagation: a 3rd browser that completes an adoption with a44-    // snapshot-capable source must also receive the source's signed device45-    // manifest and forward it to the sync adapter before the receiver46-    // resolves. The relay stays an opaque forwarder, so the whole path47-    // lives in `relay-adoption-bg.ts` and `sync-vault-adapter.ts`.48-    const rootKeyHex = "f0".repeat(32);49-    const vaultId = "b".repeat(64);50-    const longTermPubkeyHex = "99".repeat(32);51-    const { buildSignedDeviceManifest } = await import("../shared/device-manifest");52-    const { deriveSyncSigningKeypair, pubkeyToHex } = await import(53-      "../shared/sync-identity"54-    );55-    const issuerKp = await deriveSyncSigningKeypair(56-      new Uint8Array(32).fill(0x55),57-      "phone-source-id",58-    );59-    const manifest = await buildSignedDeviceManifest(60-      "phone-source-id",61-      [62-        {63-          deviceId: "peer-b",64-          deviceName: "Pool B",65-          deviceType: "extension",66-          longTermPublicKeyHex: "11".repeat(32),67-          signingPublicKeyHex: "22".repeat(32),68-          wsPort: 49152,69-          relayUrl: "wss://relay.peachpasswords.com",70-          pairedAt: 1,71-        },72-      ],73-      issuerKp.privateKey,74-    );75-    const socket = socketWithFrames([76-      { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },77-      {78-        type: "ADOPT_GRANT",79-        root_key_hex: rootKeyHex,80-        vault_id: vaultId,81-        license: "PPK-test",82-        snapshot_capable: true,83-        source: {84-          device_id: "phone-source-id",85-          device_name: "June's phone",86-          device_type: "android",87-          long_term_public_key_hex: longTermPubkeyHex,88-          signing_public_key_hex: pubkeyToHex(issuerKp.publicKeyRaw),89-          relay_url: "wss://relay.peachpasswords.com",90-        },91-      },92-      // Inline snapshot ack loop ends with ADOPT_COMPLETE; the manifest is93-      // an optional follow-up frame on the same channel.94-      { type: "ADOPT_COMPLETE" },95-      { type: "ADOPT_DEVICE_MANIFEST", manifest },96-    ], { closeOnEmpty: true });97-    mocks.createVault.mockResolvedValue({ vaultId });98-    mocks.openSocket.mockResolvedValue(socket);99-    mocks.__applyIncomingDeviceManifest.mockClear();100-101-    const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");102-    await startRelayAdoptionReceiver("receiver-password");103-    await waitForPush("ADOPTION_SOURCE_RESULT");104-105-    expect(mocks.__applyIncomingDeviceManifest).toHaveBeenCalledTimes(1);106-    const [forwarded, pubkey] = mocks.__applyIncomingDeviceManifest.mock.calls[0];107-    expect(forwarded).toEqual(manifest);108-    expect(pubkey).toBeInstanceOf(Uint8Array);109-    expect(pubkey.length).toBe(32);110-    // The receiver must NOT have sent ADOPT_DEVICE_MANIFEST_APPLIED if the111-    // adapter path was skipped (the ack only follows a real apply).112-    const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })113-      .mock.calls114-      .map(([bytes]) => {115-        try {116-          return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;117-        } catch {118-          return undefined;119-        }120-      });121-    expect(sentTypes).toContain("ADOPT_DEVICE_MANIFEST_APPLIED");122-    // And the source's ADOPT_DEVICE_MANIFEST frame must have been consumed.123-    expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST");124-  });125-126-  it("does not call applyIncomingDeviceManifest when the source omits the manifest frame", async () => {127-    // The source is snapshot-capable but does not send an ADOPT_DEVICE_MANIFEST128-    // frame. The receiver should commit the snapshot, send ADOPT_SNAPSHOT_APPLIED,129-    // and quietly time out the optional 5s wait without acking a manifest130-    // or invoking the adapter path.131-    const rootKeyHex = "f0".repeat(32);132-    const vaultId = "b".repeat(64);133-    const longTermPubkeyHex = "99".repeat(32);134-    const socket = socketWithFrames([135-      { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },136-      {137-        type: "ADOPT_GRANT",138-        root_key_hex: rootKeyHex,139-        vault_id: vaultId,140-        license: "PPK-test",141-        snapshot_capable: true,142-        source: {143-          device_id: "phone-source-id",144-          device_name: "June's phone",145-          device_type: "android",146-          long_term_public_key_hex: longTermPubkeyHex,147-          signing_public_key_hex: "aa".repeat(32),148-          relay_url: "wss://relay.peachpasswords.com",149-        },150-      },151-      { type: "ADOPT_COMPLETE" },152-      // No ADOPT_DEVICE_MANIFEST follow-up.153-    ], { closeOnEmpty: true });154-    mocks.createVault.mockResolvedValue({ vaultId });155-    mocks.openSocket.mockResolvedValue(socket);156-    mocks.__applyIncomingDeviceManifest.mockClear();157-158-    const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");159-    await startRelayAdoptionReceiver("receiver-password");160-    await waitForPush("ADOPTION_SOURCE_RESULT");161-162-    expect(mocks.__applyIncomingDeviceManifest).not.toHaveBeenCalled();163-    const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })164-      .mock.calls165-      .map(([bytes]) => {166-        try {167-          return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;168-        } catch {169-          return undefined;170-        }171-      });172-    expect(sentTypes).toContain("ADOPT_SNAPSHOT_APPLIED");173-    expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST_APPLIED");174-  });17542 });17643 17744 @@ -214,9 +81,17 @@21481   installAdoptedDuressSlot: vi.fn(async () => undefined),21582 }));21683 217-vi.mock("../shared/sync-identity", () => ({218-  computeVaultId: vi.fn(async () => "b".repeat(64)),219-}));84+vi.mock("../shared/sync-identity", async (importOriginal) => {85+  // Partial mock: the manifest test needs the REAL deriveSyncSigningKeypair /86+  // pubkeyToHex to build a verifiable signed manifest (2026-08-04: this test87+  // was orphaned inside the rendezvous mock factory and never ran; the mock88+  // below had since lost the real exports).89+  const actual = await importOriginal<typeof import("../shared/sync-identity")>();90+  return {91+    ...actual,92+    computeVaultId: vi.fn(async () => "b".repeat(64)),93+  };94+});22095 22196 vi.mock("../shared/storage", () => ({22297   storageSet: mocks.storageSet,@@ -953,6 +828,144 @@953828     );954829   });955830 831+  it("applies the source's device manifest after the snapshot, then acks", async () => {832+    // Mesh propagation: a 3rd browser that completes an adoption with a833+    // snapshot-capable source must also receive the source's signed device834+    // manifest and forward it to the sync adapter before the receiver835+    // resolves. The relay stays an opaque forwarder, so the whole path836+    // lives in `relay-adoption-bg.ts` and `sync-vault-adapter.ts`.837+    const rootKeyHex = "f0".repeat(32);838+    const vaultId = "b".repeat(64);839+    const longTermPubkeyHex = "99".repeat(32);840+    const { buildSignedDeviceManifest } = await import("../shared/device-manifest");841+    const { deriveSyncSigningKeypair, pubkeyToHex } = await import(842+      "../shared/sync-identity"843+    );844+    const issuerKp = await deriveSyncSigningKeypair(845+      new Uint8Array(32).fill(0x55),846+      "phone-source-id",847+    );848+    const manifest = await buildSignedDeviceManifest(849+      "phone-source-id",850+      [851+        {852+          deviceId: "peer-b",853+          deviceName: "Pool B",854+          deviceType: "extension",855+          longTermPublicKeyHex: "11".repeat(32),856+          signingPublicKeyHex: "22".repeat(32),857+          wsPort: 49152,858+          relayUrl: "wss://relay.peachpasswords.com",859+          pairedAt: 1,860+        },861+      ],862+      issuerKp.privateKey,863+    );864+    const socket = socketWithFrames([865+      { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },866+      {867+        type: "ADOPT_GRANT",868+        root_key_hex: rootKeyHex,869+        vault_id: vaultId,870+        license: "PPK-test",871+        snapshot_capable: true,872+        source: {873+          device_id: "phone-source-id",874+          device_name: "June's phone",875+          device_type: "android",876+          long_term_public_key_hex: longTermPubkeyHex,877+          signing_public_key_hex: pubkeyToHex(issuerKp.publicKeyRaw),878+          relay_url: "wss://relay.peachpasswords.com",879+        },880+      },881+      // Inline snapshot ack loop ends with ADOPT_COMPLETE; the manifest is882+      // an optional follow-up frame on the same channel.883+      { type: "ADOPT_COMPLETE" },884+      // Current protocol: the snapshot arrives as ADOPT_SNAPSHOT chunk frames885+      // after the handshake; one empty chunk ends the phase.886+      { type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 1, entries: {}, tombstones: {} },887+      { type: "ADOPT_DEVICE_MANIFEST", manifest },888+    ], { closeOnEmpty: true });889+    mocks.createVault.mockResolvedValue({ vaultId });890+    mocks.openSocket.mockResolvedValue(socket);891+    mocks.__applyIncomingDeviceManifest.mockClear();892+893+    const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");894+    await startRelayAdoptionReceiver("receiver-password");895+    await waitForPush("ADOPTION_SOURCE_RESULT");896+897+    expect(mocks.__applyIncomingDeviceManifest).toHaveBeenCalledTimes(1);898+    const [forwarded, pubkey] = mocks.__applyIncomingDeviceManifest.mock.calls[0];899+    expect(forwarded).toEqual(manifest);900+    expect(pubkey).toBeInstanceOf(Uint8Array);901+    expect(pubkey.length).toBe(32);902+    // The receiver must NOT have sent ADOPT_DEVICE_MANIFEST_APPLIED if the903+    // adapter path was skipped (the ack only follows a real apply).904+    const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })905+      .mock.calls906+      .map(([bytes]) => {907+        try {908+          return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;909+        } catch {910+          return undefined;911+        }912+      });913+    expect(sentTypes).toContain("ADOPT_DEVICE_MANIFEST_APPLIED");914+    // And the source's ADOPT_DEVICE_MANIFEST frame must have been consumed.915+    expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST");916+  });917+918+  it("does not call applyIncomingDeviceManifest when the source omits the manifest frame", async () => {919+    // The source is snapshot-capable but does not send an ADOPT_DEVICE_MANIFEST920+    // frame. The receiver should commit the snapshot, send ADOPT_SNAPSHOT_APPLIED,921+    // and quietly time out the optional 5s wait without acking a manifest922+    // or invoking the adapter path.923+    const rootKeyHex = "f0".repeat(32);924+    const vaultId = "b".repeat(64);925+    const longTermPubkeyHex = "99".repeat(32);926+    const socket = socketWithFrames([927+      { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },928+      {929+        type: "ADOPT_GRANT",930+        root_key_hex: rootKeyHex,931+        vault_id: vaultId,932+        license: "PPK-test",933+        snapshot_capable: true,934+        source: {935+          device_id: "phone-source-id",936+          device_name: "June's phone",937+          device_type: "android",938+          long_term_public_key_hex: longTermPubkeyHex,939+          signing_public_key_hex: "aa".repeat(32),940+          relay_url: "wss://relay.peachpasswords.com",941+        },942+      },943+      { type: "ADOPT_COMPLETE" },944+      // Current protocol: one empty ADOPT_SNAPSHOT chunk ends the snapshot945+      // phase; then NO ADOPT_DEVICE_MANIFEST follow-up.946+      { type: "ADOPT_SNAPSHOT", chunk_index: 0, chunk_count: 1, entries: {}, tombstones: {} },947+    ], { closeOnEmpty: true });948+    mocks.createVault.mockResolvedValue({ vaultId });949+    mocks.openSocket.mockResolvedValue(socket);950+    mocks.__applyIncomingDeviceManifest.mockClear();951+952+    const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");953+    await startRelayAdoptionReceiver("receiver-password");954+    await waitForPush("ADOPTION_SOURCE_RESULT");955+956+    expect(mocks.__applyIncomingDeviceManifest).not.toHaveBeenCalled();957+    const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })958+      .mock.calls959+      .map(([bytes]) => {960+        try {961+          return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;962+        } catch {963+          return undefined;964+        }965+      });966+    expect(sentTypes).toContain("ADOPT_SNAPSHOT_APPLIED");967+    expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST_APPLIED");968+  });956969 });957970 958971 function decodeInvitation(encoded: string): { vault?: string } | null {

view file

src/background/sync-vault-adapter.ts+2 −2
@@ -301,7 +301,7 @@301301       manifest,302302       hexToPubkey(issuer!.signingPublicKeyHex!),303303     );304-  } catch (storedError) {304+  } catch {305305     // Stored key may be stale (older builds persisted the key derived at306306     // first-pair and never refreshed it). The signing keypair is deterministic307307     // from (masterKey, deviceId) on both sides, so retry once with the locally@@ -314,7 +314,7 @@314314         issuer.deviceId,315315       );316316       body = await verifyDeviceManifest(manifest, derived.publicKeyRaw);317-    } catch (derivedError) {317+    } catch {318318       return;319319     } finally {320320       masterForIssuer.fill(0);

view file

src/content/detect.ts+1 −1
@@ -13,7 +13,7 @@1313 1414 // Personal name fields (display name, first/last name) — not credential fields.1515 // Applied to prevent the fallback from picking up name inputs on signup forms.16-const NAME_FIELD_PATTERNS = /\b(first[\s_\-]?name|last[\s_\-]?name|full[\s_\-]?name|display[\s_\-]?name|real[\s_\-]?name|given[\s_\-]?name|family[\s_\-]?name|middle[\s_\-]?name)\b|^(name|fname|lname|fullname|displayname|realname)$/i;16+const NAME_FIELD_PATTERNS = /\b(first[\s_-]?name|last[\s_-]?name|full[\s_-]?name|display[\s_-]?name|real[\s_-]?name|given[\s_-]?name|family[\s_-]?name|middle[\s_-]?name)\b|^(name|fname|lname|fullname|displayname|realname)$/i;1717 1818 // Input types that are never text-entry fields and must never get autofill1919 // icons. submit/button/reset/image are interactive controls — putting the

view file

src/content/index.ts+3 −3
@@ -12,9 +12,9 @@1212   isVisible,1313 } from "./detect";1414 import {15-  CardSelectorEntry,15+  type CardSelectorEntry,1616   injectFieldIcon,17-  SelectorEntry,17+  type SelectorEntry,1818   showAutofillSelector,1919   showCardSelector,2020   showPasskeyAvailable,@@ -669,7 +669,7 @@669669       reattachListeners(field, autofillFieldType);670670     }671671 672-    let fields = detectLoginFields();672+    const fields = detectLoginFields();673673     const targets = resolveAutofillInteractionTargets(field, fields, explicitSummon);674674     if (!targets.shouldOpen) {675675       return;

view file

src/content/inject.ts+1 −1
@@ -377,7 +377,7 @@377377     requestAnimationFrame(() => li.select());378378     function commitLenEdit() {379379       const n = parseInt(li.value, 10);380-      if (!isNaN(n)) {380+      if (!Number.isNaN(n)) {381381         length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));382382         slider.value = String(length);383383         regenerate();

view file

src/content/policy.ts+2 −1
@@ -117,8 +117,9 @@117117   if (negation) {118118     const window = negation[0].slice(0, NEGATION_WINDOW);119119     const seen = new Set<string>();120-    const tokenRegex = /(?:^|[\s`"',.(\[])([!-/:-@\[-`{-~])(?=[\s`"',.)\]]|$)/g;120+    const tokenRegex = /(?:^|[\s`"',.([])([!-/:-@[-`{-~])(?=[\s`"',.)\]]|$)/g;121121     let m: RegExpExecArray | null;122+    // biome-ignore lint/suspicious/noAssignInExpressions: standard RegExp.exec loop idiom122123     while ((m = tokenRegex.exec(window)) !== null) {123124       const ch = m[1]!;124125       if (FORBIDDEN_CHAR_SET.has(ch)) seen.add(ch);

view file

src/mobile/chrome-shim.ts+1 −1
@@ -34,7 +34,7 @@3434   message: unknown,3535   sender: unknown,3636   sendResponse: (response: unknown) => void37-) => boolean | void;37+) => boolean | undefined;3838 3939 type NativeKV = {4040   kvGet(key: string): Promise<string | null>;

view file

src/popup/codex/pdf-import.ts+1 −3
@@ -93,9 +93,7 @@9393   errors: string[];9494 }9595 96-export interface PDFImportProgress {97-  (page: number, totalPages: number): void;98-}96+export type PDFImportProgress = (page: number, totalPages: number) => void9997 10098 /**10199  * Render scales that produce a 1:1 canvas-to-source-PNG mapping for known

view file

src/popup/components/GeneratorDrawer.tsx+1 −1
@@ -187,7 +187,7 @@187187 188188   function commitLength() {189189     const n = parseInt(lengthDraft(), 10);190-    if (!isNaN(n)) {190+    if (!Number.isNaN(n)) {191191       const clamped = Math.max(sliderMin(), Math.min(sliderMax(), n));192192       handleLengthChange(clamped);193193     }

view file

src/popup/components/PairingBirds.tsx+1 −1
@@ -5,7 +5,7 @@55 // code-entry, connecting, SAS and paired screens (PairDevice.tsx's bird stage).66 // The `-chirping` pose names are kept on purpose — the bird visibly chirps.77 8-import { Component } from "solid-js";8+import type { Component } from "solid-js";99 1010 const FINCH = {1111   hi: "#f4a6b6", mid: "#dc8294", lo: "#a85870", shade: "#7a3e54",

view file

src/popup/components/SyncModeSelector.tsx+1 −1
@@ -6,7 +6,7 @@66 // value, so the two surfaces cannot drift apart77 // (docs/sync-relay-surfacing.md).88 9-import { Component, Show, createSignal, onMount } from "solid-js";9+import { type Component, Show, createSignal, onMount } from "solid-js";1010 import { sendMessage } from "../../shared/bridge";1111 import { DEFAULT_RELAY_URL } from "../../shared/rendezvous";1212 

view file

src/popup/views/Lock.tsx+1 −1
@@ -724,7 +724,7 @@724724 725725     function drawSnap(now: number) {726726       const t = Math.min((now - start) / DURATION, 1);727-      const ease = t < 1 ? 1 - Math.pow(1 - t, 3) : 1;727+      const ease = t < 1 ? 1 - (1 - t) ** 3 : 1;728728       ctx.clearRect(0, 0, W, H);729729       runes.slice(0, targets.length).forEach((r, i) => {730730         const tx = targets[i]!.x;

view file

src/popup/views/PairDevice.tsx+1 −1
@@ -14,7 +14,7 @@1414 // reaches this view via the ADOPTION_SOURCE_* messages.1515 1616 import {17-  Component,17+  type Component,1818   Show,1919   createSignal,2020   onCleanup,

view file

src/popup/views/Settings.tsx+1 −0
@@ -90,6 +90,7 @@9090   return value9191     .trim()9292     .slice(0, MAX_FEEDBACK_MESSAGE_LENGTH)93+    // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — strips control characters from feedback text9394     .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")9495     .replace(/&/g, "&amp;")9596     .replace(/</g, "&lt;")

view file

src/shared/device-manifest.test.ts+1 −1
@@ -2,7 +2,7 @@22 import {33   buildSignedDeviceManifest,44   canonicalise,5-  DeviceManifestError,5+  type DeviceManifestError,66   verifyDeviceManifest,77 } from "./device-manifest";88 import { deriveSyncSigningKeypair } from "./sync-identity";

view file

src/shared/domain.ts+1 −0
@@ -40,6 +40,7 @@4040   try {4141     const hostname = new URL(url.includes("://") ? url : `https://${url}`).hostname;4242     return (43+      // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — detects non-ASCII hostnames4344       /[^\x00-\x7F]/.test(hostname) ||4445       hostname.split(".").some((l) => l.startsWith("xn--"))4546     );

view file