ac28263c9b98
test: revive two orphaned mesh-propagation receiver tests (v4.8.6)
- time
- 2026-08-04 15:36 UTC · 2 months ago
- machine
- koh2
- parent
- 41b6448ed641
↓ peach-browser-ac28263c.tar.gzatlas at this save
files changed21 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 −0biome.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 }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": {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",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",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 }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 {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);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 thesrc/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;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();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);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>;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 knownsrc/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 }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",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 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;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,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, "&")9596 .replace(/</g, "<")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";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 );