fix(browser): move cookie scoping off psl's stale suffix list (#20421)

* fix(browser): move cookie scoping off psl's stale suffix list

psl@1.15.0 is its latest release and ships a Dec-2024 snapshot of the
public suffix list. Measured against the current upstream list, it fails
to recognise 600 of 10,030 suffixes; tldts misses 2.

That gap is a cookie-isolation bug. psl does not know `api.br` is a
suffix, so it falls back to the `br` rule and maps foo.api.br, bar.api.br
and example.api.br all onto the single family `api.br`. Unrelated
registrants then share a removal scope, and a replace-mode import for one
clears the others' cookies. The same holds for seg.ar, co.az, gov.cz and
~597 more.

tldts is called with allowPrivateDomains, without which the PSL's PRIVATE
section is ignored and every *.github.io / *.s3.amazonaws.com / *.vercel.app
tenant collapses into one family — 21 of 49 probed hosts changed family
under the default. The new test pins that boundary.

One deliberate behaviour change: hosts under `.local` (not in the PSL)
were their own family under psl, which returned an all-null parse for
them; they now resolve to the two-label boundary (app.orca.local ->
orca.local), matching what Chromium treats as the registrable domain.

* fix(build): bundle tldts into the main process like psl was

psl sat in BUNDLED_MAIN_DEPENDENCIES, so it was inlined into the main
bundle rather than externalized and copied into resources/node_modules.
Swapping the dependency without moving that entry left a bare tldts
import that afterPack's runtime-closure check rejects.

* fix(build): point the output contract at tldts and drop the psl shim

The contract test still asserted psl was in BUNDLED_MAIN_DEPENDENCIES, so
it failed once the entry became tldts. src/types/psl.ts declared a module
that no longer resolves; tldts ships its own types.

* test(browser): pin the suffix boundaries the tldts swap moved

Three semantic changes shipped untested:

- `.local` is unlisted, and the libraries disagreed on what that means. psl
  returned an all-null parse so every `*.orca.local` host was its own family;
  tldts stops at `orca.local`. The consequence is wider than the family name —
  importDomainAncestors now yields the shared parent, so a replace-mode import
  of one host clears non-host-only cookies every sibling shares.
- psl's snapshot had `compute.amazonaws.com` as a literal PRIVATE suffix; the
  current list only carries the wildcard, so the bare host is ICANN now.
- The renderer's `psl.isValid` gate had no direct test at all — nothing imported
  the module from a test.

Also drops comments that explained a boundary in terms of psl's internals. One
was wrong under tldts: bracketed IPv6 does not reach an error branch, it parses
with the brackets stripped and falls through the unlisted path.
This commit is contained in:
Neil
2026-09-12 16:00:54 -07:00
committed by GitHub
parent 53eb639983
commit 56fcb544e0
16 changed files with 216 additions and 80 deletions
@@ -116,9 +116,9 @@ describe('Electron Vite output contract', () => {
expect(external('node:fs', undefined, false)).toBe(true)
expect(external('@xterm/headless', undefined, false)).toBe(false)
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
expect(external('psl', undefined, false)).toBe(false)
expect(external('tldts', undefined, false)).toBe(false)
expect(external('zod', undefined, false)).toBe(false)
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('psl')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('tldts')
expect(electronViteConfig.main?.build?.externalizeDeps?.exclude).toContain('zod')
})
@@ -583,8 +583,8 @@ breakaway hands the whole tree its escape. The per-PTY job therefore omits
`BREAKAWAY_OK` whenever `msys-2.0.dll` or `cygwin1.dll` sits on the shell's DLL
search path — beside the executable, or under `usr/bin` for Git's `bin`
launcher. Native shells keep explicit breakaway. Denying it costs Cygwin
nothing, because it *pre-checks* the limit rather than retrying, so no spawn
fails; but a *native* program that passes `CREATE_BREAKAWAY_FROM_JOB` itself
nothing, because it _pre-checks_ the limit rather than retrying, so no spawn
fails; but a _native_ program that passes `CREATE_BREAKAWAY_FROM_JOB` itself
inside such a pane now gets `ERROR_ACCESS_DENIED`. `nohup` and `disown` are
unaffected — they are Cygwin signal/session concepts, unrelated to job
membership. The daemon's host job is unchanged.
+1 -1
View File
@@ -10,7 +10,7 @@ import packageJson from './package.json' with { type: 'json' }
const BUNDLED_MAIN_DEPENDENCIES = new Set([
'@xterm/headless',
'@xterm/addon-serialize',
'psl',
'tldts',
// Why: Windows NSIS deploys app.asar before external resources; bootstrap must
// not race the later resources/node_modules copy.
'zod'
+1 -1
View File
@@ -177,12 +177,12 @@
"node-pty": "^1.1.0",
"posthog-node": "^5.33.3",
"proper-lockfile": "4.1.2",
"psl": "1.15.0",
"qrcode": "^1.5.4",
"react-i18next": "17.0.13",
"serve-sim": "^0.1.40",
"sherpa-onnx": "1.12.37",
"ssh2": "^1.17.0",
"tldts": "7.4.12",
"tweetnacl": "^1.0.3",
"ws": "^8.21.3",
"yaml": "^2.8.4",
+16 -16
View File
@@ -164,9 +164,6 @@ importers:
proper-lockfile:
specifier: 4.1.2
version: 4.1.2
psl:
specifier: 1.15.0
version: 1.15.0
qrcode:
specifier: ^1.5.4
version: 1.5.4
@@ -182,6 +179,9 @@ importers:
ssh2:
specifier: ^1.17.0
version: 1.17.0
tldts:
specifier: 7.4.12
version: 7.4.12
tweetnacl:
specifier: ^1.0.3
version: 1.0.3
@@ -5951,16 +5951,9 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
@@ -6605,10 +6598,17 @@ packages:
tldts-core@7.4.10:
resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==}
tldts-core@7.4.12:
resolution: {integrity: sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==}
tldts@7.4.10:
resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==}
hasBin: true
tldts@7.4.12:
resolution: {integrity: sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==}
hasBin: true
tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
@@ -12706,17 +12706,11 @@ snapshots:
forwarded: 0.2.0
ipaddr.js: 1.9.1
psl@1.15.0:
dependencies:
punycode: 2.3.1
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode@2.3.1: {}
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1
@@ -13490,11 +13484,17 @@ snapshots:
tldts-core@7.4.10:
optional: true
tldts-core@7.4.12: {}
tldts@7.4.10:
dependencies:
tldts-core: 7.4.10
optional: true
tldts@7.4.12:
dependencies:
tldts-core: 7.4.12
tmp-promise@3.0.3:
dependencies:
tmp: 0.2.7
@@ -171,8 +171,9 @@ describe('removeTransplantableCookies — preserved families on a POPULATED jar'
})
it('preserves a family named by an IPv4 literal', async () => {
// Why: psl reads 127.0.0.1 as the dotted DNS name '0.1'. If registrableFamily returned that,
// the live 127.0.0.1 session would not match the preserve set and would be erased.
// Why: an IPv4 literal has no registrable domain, so the family must come from the IP branch.
// If registrableFamily fell through to the suffix parser, the live 127.0.0.1 session would not
// match the preserve set and would be erased.
const target = jar([cookie('127.0.0.1', 'loopback-session'), cookie('.other.example', 'stale')])
await removeTransplantableCookies(
@@ -1,6 +1,6 @@
import { isIP } from 'node:net'
import type { Cookie, Cookies } from 'electron'
import { parse as parseDomain } from 'psl'
import { parse as parseDomain } from 'tldts'
// Why: type-only, so this does not create a runtime cycle with the clear module.
import type { CookieClearIdentity } from './browser-cookie-import-clear'
@@ -41,13 +41,24 @@ export function normalizeCookieDomain(domain: string): string | null {
}
}
// Why allowPrivateDomains: the PSL's PRIVATE section is what keeps one tenant's cookies out of
// another's — without it `foo.github.io` and `bar.github.io` collapse to the same family, and a
// replace-mode import for one would clear the other. tldts defaults this off; cookie scoping needs
// it on.
const PUBLIC_SUFFIX_OPTIONS = { allowPrivateDomains: true } as const
// psl exposed a single `listed` flag; tldts splits the same question across the two list sections.
function isListedSuffix(parsed: { isIcann: boolean | null; isPrivate: boolean | null }): boolean {
return parsed.isIcann === true || parsed.isPrivate === true
}
// Why (STA-4300): one definition of "family" for every consumer of the partition skip set — the
// planner, the per-coordinate removal filter, and the path A domain comparison. Deriving it inline
// in several places is what let the removal scope and the write set disagree (STA-4090, STA-4170).
//
// The IP test MUST run on normalizeCookieDomain's output, never the raw string: Chromium accepts
// many spellings of one address and psl mangles all of them (psl.parse('2130706433').domain is
// null, psl.parse('127.0.0.1').domain is '0.1'). normalizeCookieDomain runs the value through
// many spellings of one address and the suffix parser mangles all of them (tldts.parse('2130706433')
// .domain is null, tldts.parse('127.1').domain is '127.1'). normalizeCookieDomain runs the value through
// `new URL()`, which canonicalises 127.1 / 2130706433 / 0x7f.1 / 010.0.0.1 / a trailing dot to a
// dotted quad first, so isIP() then recognises every one of them.
//
@@ -65,12 +76,12 @@ export function registrableFamily(domain: string): string | null {
if (host.startsWith('[') && host.endsWith(']') && isIP(host.slice(1, -1)) === 6) {
return host
}
const parsed = parseDomain(host)
if ('error' in parsed) {
const parsed = parseDomain(host, PUBLIC_SUFFIX_OPTIONS)
if (parsed.hostname === null) {
return host
}
if (parsed.domain === null) {
return parsed.listed ? null : host
return isListedSuffix(parsed) ? null : host
}
return parsed.domain
}
@@ -80,11 +91,11 @@ export function normalizeCookieImportDomain(domain: string): string | null {
if (!normalized) {
return null
}
const parsed = parseDomain(normalized)
if ('error' in parsed) {
const parsed = parseDomain(normalized, PUBLIC_SUFFIX_OPTIONS)
if (parsed.hostname === null) {
return normalized.startsWith('[') && normalized.endsWith(']') ? normalized : null
}
if (parsed.domain === null && parsed.listed) {
if (parsed.domain === null && isListedSuffix(parsed)) {
return null
}
return normalized
@@ -129,8 +140,8 @@ function domainSuffixes(domain: string): string[] {
}
function importDomainAncestors(domain: string): string[] {
const parsed = parseDomain(domain)
const boundary = 'error' in parsed ? domain : (parsed.domain ?? domain)
const parsed = parseDomain(domain, PUBLIC_SUFFIX_OPTIONS)
const boundary = parsed.hostname === null ? domain : (parsed.domain ?? domain)
const ancestors: string[] = []
for (const suffix of domainSuffixes(domain)) {
ancestors.push(suffix)
@@ -0,0 +1,89 @@
import { describe, expect, it } from 'vitest'
import {
domainIsInImportedScope,
importedDomainScope,
normalizeCookieImportDomain,
registrableFamily
} from './browser-cookie-import-policy'
// Why this file exists: the public-suffix engine decides which cookies share a removal scope, so a
// library swap silently re-partitions the jar. These cases pin the boundaries that moved (or had to
// be held) when this moved off `psl`.
describe('registrable family across public-suffix sections', () => {
it('keeps each PRIVATE-section tenant in its own family', () => {
// psl and tldts disagree here unless allowPrivateDomains is set; without it every
// *.github.io tenant collapses into one family and a replace-mode import clears siblings.
expect(registrableFamily('foo.github.io')).toBe('foo.github.io')
expect(registrableFamily('bar.github.io')).toBe('bar.github.io')
expect(registrableFamily('bar.s3.amazonaws.com')).toBe('bar.s3.amazonaws.com')
expect(registrableFamily('foo.vercel.app')).toBe('foo.vercel.app')
})
it('refuses to name a bare public suffix as a family', () => {
expect(registrableFamily('com')).toBeNull()
expect(registrableFamily('co.uk')).toBeNull()
expect(registrableFamily('github.io')).toBeNull()
// Absent from psl 1.15.0's 2024 snapshot; naming it a family would preserve a whole suffix.
expect(registrableFamily('api.br')).toBeNull()
expect(registrableFamily('seg.ar')).toBeNull()
})
it('resolves ICANN suffixes to the registrable domain', () => {
expect(registrableFamily('a.b.example.co.uk')).toBe('example.co.uk')
expect(registrableFamily('www.example.com')).toBe('example.com')
expect(registrableFamily('foo.example.api.br')).toBe('example.api.br')
})
it('returns the canonicalised address for every IP spelling', () => {
expect(registrableFamily('127.0.0.1')).toBe('127.0.0.1')
expect(registrableFamily('127.1')).toBe('127.0.0.1')
expect(registrableFamily('2130706433')).toBe('127.0.0.1')
expect(registrableFamily('[::1]')).toBe('[::1]')
})
it('treats an unlisted suffix as its own boundary', () => {
expect(registrableFamily('example.notaruleatall')).toBe('example.notaruleatall')
})
it('rejects a bare suffix as an import domain but keeps real hosts', () => {
expect(normalizeCookieImportDomain('co.uk')).toBeNull()
expect(normalizeCookieImportDomain('api.br')).toBeNull()
expect(normalizeCookieImportDomain('.example.com')).toBe('example.com')
expect(normalizeCookieImportDomain('foo.github.io')).toBe('foo.github.io')
})
})
// Why: `.local` is absent from the PSL, and the two libraries disagreed about what that means. psl
// returned an all-null parse, so every `*.orca.local` host was its own family; tldts applies the
// default single-label rule and stops at `orca.local`, which is what Chromium treats as registrable.
// The widening is deliberate, so it is pinned here rather than left to the next library bump.
describe('unlisted .local suffix', () => {
it('stops at the two-label boundary', () => {
expect(registrableFamily('app.orca.local')).toBe('orca.local')
expect(registrableFamily('orca.local')).toBe('orca.local')
})
// The consequence of the boundary move: a replace-mode import of one host now also clears
// non-host-only cookies scoped to `.orca.local`, which every sibling `*.orca.local` host shares.
it('pulls the shared parent into the removal scope', () => {
const scope = importedDomainScope(['app.orca.local'])
expect(domainIsInImportedScope(scope, 'orca.local', false)).toBe(true)
expect(domainIsInImportedScope(scope, 'orca.local', true)).toBe(false)
})
})
// Why: psl's 2024 snapshot carried `compute.amazonaws.com` as a literal PRIVATE suffix; the current
// list only has the `*.compute.amazonaws.com` wildcard, so the bare host is an ordinary ICANN domain
// now. That moves a real host shape from "no family" to `amazonaws.com`.
describe('suffix entries that changed shape upstream', () => {
it('reads bare compute.amazonaws.com as a registrable domain', () => {
expect(registrableFamily('compute.amazonaws.com')).toBe('amazonaws.com')
expect(normalizeCookieImportDomain('compute.amazonaws.com')).toBe('compute.amazonaws.com')
})
it('still refuses the wildcard child and the sibling private suffix', () => {
expect(registrableFamily('foo.compute.amazonaws.com')).toBeNull()
expect(registrableFamily('s3.amazonaws.com')).toBeNull()
})
})
@@ -21,9 +21,10 @@ describe('registrableFamily', () => {
expect(registrableFamily(host)).toBe(expected)
})
// Why: psl treats an IPv4 literal as a dotted DNS name — psl.parse('127.0.0.1').domain is '0.1'.
// These pass only because the IP check runs on normalizeCookieDomain's canonicalised output.
// Moving the check before normalisation reintroduces a wrong, destructive family.
// Why: the suffix parser reads a non-dotted-quad IPv4 spelling as a DNS name —
// tldts.parse('127.1').domain is '127.1' and tldts.parse('2130706433').domain is null. These pass
// only because the IP check runs on normalizeCookieDomain's canonicalised output. Moving the check
// before normalisation reintroduces a wrong, destructive family.
it.each([
['127.0.0.1', '127.0.0.1'],
['192.168.1.1', '192.168.1.1'],
@@ -32,15 +33,15 @@ describe('registrableFamily', () => {
['2130706433', '127.0.0.1'],
['0x7f.1', '127.0.0.1'],
['127.0.0.1.', '127.0.0.1'],
// Octal, and 8.0.0.1 is the correct reading — psl would have produced '0.1'.
// Octal, and 8.0.0.1 is the correct reading — unnormalised, this parses as a DNS name.
['010.0.0.1', '8.0.0.1']
])('recognises the IPv4 literal %s as %s', (host, expected) => {
expect(registrableFamily(host)).toBe(expected)
})
// Why: isIP('[::1]') is 0, so the bracketed form needs its own branch. Without it these fall
// through to psl, which throws, which happens to return the host — right answer, wrong reason,
// and it stops being right the moment the error branch is touched.
// through to the parser, which strips the brackets and reports no suffix — the unlisted path then
// happens to return the host. Right answer, wrong reason, and only while that path is untouched.
it.each([
['[::1]', '[::1]'],
['[2001:db8::1]', '[2001:db8::1]']
@@ -1,9 +1,6 @@
import type WebSocket from 'ws'
import type { E2EEKeypair } from '../e2ee-keypair'
import type {
RelayConnectionOpenMessage,
RelayDrainMessage,
} from './relay-control-protocol'
import type { RelayConnectionOpenMessage, RelayDrainMessage } from './relay-control-protocol'
export type RelayControlClientOptions = {
cellUrl: string
@@ -76,7 +76,11 @@ export const RelayConnectionOpenMessageSchema = z
export const RelayDrainMessageSchema = z
.object({
type: z.literal('drain'),
graceMs: z.number().int().nonnegative().max(60 * 60 * 1000),
graceMs: z
.number()
.int()
.nonnegative()
.max(60 * 60 * 1000),
recovery: z.literal('resolve-director')
})
.strict()
+3 -13
View File
@@ -187,19 +187,14 @@ export class RelayOriginPool {
}
this.deferredAssignment = null
if (assignment.cellUrl === origin.cellUrl) {
let rebound = false
let rebound = false
try {
await origin.rebind(this.relayJwt, assignment)
rebound = true
} catch {
// Why: a restarted cell cannot know the prior process's resume secret;
// after rebind fails, a fresh generation is the only recoverable path.
await this.activateTarget(
origin,
assignment,
this.relayJwt,
message.graceMs,
)
await this.activateTarget(origin, assignment, this.relayJwt, message.graceMs)
}
if (rebound) {
this.assertCurrent()
@@ -208,12 +203,7 @@ export class RelayOriginPool {
this.drainingOrigins.delete(origin)
}
} else {
await this.activateTarget(
origin,
assignment,
this.relayJwt,
message.graceMs,
)
await this.activateTarget(origin, assignment, this.relayJwt, message.graceMs)
}
this.options.onStatus('registered')
this.drainRetry.reset()
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import { classifyHostUrl } from './tab-create-entry-url-classification'
// Why this file exists: the suffix check behind `host/path` navigation moved off `psl.isValid`, and
// nothing else in the tree exercises it directly. These cases pin the listed/unlisted split that
// decides whether a typed string navigates or falls through to search.
describe('classifyHostUrl suffix gate', () => {
it('navigates for a listed suffix carrying a path', () => {
expect(classifyHostUrl('example.com/foo')).toEqual({
kind: 'host-url',
url: 'https://example.com/foo'
})
expect(classifyHostUrl('example.co.uk/foo')).toEqual({
kind: 'host-url',
url: 'https://example.co.uk/foo'
})
})
// The PRIVATE section has to stay in: these are navigable hosts, not search terms.
it('navigates for a private-section suffix carrying a path', () => {
expect(classifyHostUrl('foo.github.io/bar')).toEqual({
kind: 'host-url',
url: 'https://foo.github.io/bar'
})
expect(classifyHostUrl('foo.vercel.app/bar')).toEqual({
kind: 'host-url',
url: 'https://foo.vercel.app/bar'
})
})
// Why this is the fix: psl's 2024 snapshot did not know `api.br`, so `isValid` called it a domain
// and a typed `api.br/x` navigated to a bare public suffix instead of searching.
it('refuses a bare public suffix carrying a path', () => {
expect(classifyHostUrl('api.br/foo')).toBeNull()
expect(classifyHostUrl('co.uk/foo')).toBeNull()
expect(classifyHostUrl('github.io/foo')).toBeNull()
})
it('keeps localhost and IPv4 on http without consulting the suffix list', () => {
expect(classifyHostUrl('localhost:3000/foo')).toEqual({
kind: 'host-url',
url: 'http://localhost:3000/foo'
})
expect(classifyHostUrl('127.0.0.1:8080/foo')).toEqual({
kind: 'host-url',
url: 'http://127.0.0.1:8080/foo'
})
})
// The gate only applies once an authority ends; a bare host still navigates so unlisted intranet
// names typed on their own are not forced into search.
it('leaves a bare host unfiltered by the suffix list', () => {
expect(classifyHostUrl('api.br')).toEqual({ kind: 'host-url', url: 'https://api.br/' })
})
})
@@ -1,5 +1,5 @@
import { translate } from '@/i18n/i18n'
import { isValid as isListedDomain } from 'psl'
import { parse as parseDomain } from 'tldts'
import { classifySchemeLessLocalDevAddress } from '../../../../shared/browser-url'
const HOST_FILE_EXTENSIONS = new Set([
@@ -54,6 +54,13 @@ function parseHttpUrl(query: string): ExplicitUrlClassification {
}
}
// Why: a bare public suffix (`api.br`) is not a navigable host, so the typed string stays a search.
// tldts' PRIVATE section is included so `foo.github.io` still reads as a domain.
function isListedDomain(host: string): boolean {
const parsed = parseDomain(host, { allowPrivateDomains: true })
return parsed.domain !== null && (parsed.isIcann === true || parsed.isPrivate === true)
}
function splitHostCandidate(query: string): { host: string; port: string | null } | null {
if (/[\\\s]/.test(query)) {
return null
-17
View File
@@ -1,17 +0,0 @@
declare module 'psl' {
export type ParsedDomain = {
input: string
tld: string | null
sld: string | null
domain: string | null
subdomain: string | null
listed: boolean
}
export type ParseError = {
input: string
error: { code: string; message: string }
}
export function parse(input: string): ParsedDomain | ParseError
}
@@ -75,9 +75,7 @@ describe('relay correction mixed-version wire contracts', () => {
appVersion: 'test'
}
expect(BaselineHello.parse(HostHelloSchema.parse(hello))).toEqual(hello)
expect(BaselineHello.safeParse({ ...hello, idleRegionalRehome: true }).success).toBe(
false
)
expect(BaselineHello.safeParse({ ...hello, idleRegionalRehome: true }).success).toBe(false)
})
it('the idle cutover uses a drain frame understood by the pinned old desktop', () => {