fix: require exact pairing deep link route (#4282)
This commit is contained in:
parent
93bba155de
commit
58f10fa2da
|
|
@ -24,4 +24,9 @@ describe('web pairing input', () => {
|
|||
it('still parses legacy hash-form pairing URLs', () => {
|
||||
expect(parseWebPairingInput(`orca://pair#${encodeOffer()}`)).toEqual(offer)
|
||||
})
|
||||
|
||||
it('rejects orca URLs outside the exact pairing route', () => {
|
||||
expect(parseWebPairingInput(`orca://pairing?code=${encodeOffer()}`)).toBeNull()
|
||||
expect(parseWebPairingInput(`orca://pair-extra?code=${encodeOffer()}`)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,19 +14,9 @@ export function parseWebPairingInput(input: string): WebPairingOffer | null {
|
|||
}
|
||||
|
||||
try {
|
||||
if (trimmed.startsWith('orca://pair')) {
|
||||
const queryIndex = trimmed.indexOf('?')
|
||||
if (queryIndex !== -1) {
|
||||
const query = trimmed.slice(queryIndex + 1).split('#')[0] ?? ''
|
||||
const params = new URLSearchParams(query)
|
||||
const code = params.get('code')
|
||||
return code ? decodePairingPayload(code) : null
|
||||
}
|
||||
const hashIndex = trimmed.indexOf('#')
|
||||
if (hashIndex === -1) {
|
||||
return null
|
||||
}
|
||||
return decodePairingPayload(trimmed.slice(hashIndex + 1))
|
||||
if (trimmed.toLowerCase().startsWith('orca://')) {
|
||||
const code = extractPairingCodeFromUrl(trimmed)
|
||||
return code ? decodePairingPayload(code) : null
|
||||
}
|
||||
return decodePairingPayload(trimmed)
|
||||
} catch {
|
||||
|
|
@ -92,6 +82,28 @@ function decodePairingPayload(base64url: string): WebPairingOffer | null {
|
|||
}
|
||||
}
|
||||
|
||||
function extractPairingCodeFromUrl(url: string): string | null {
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
// Why: prefix checks accepted routes like `orca://pairing?...`; only the
|
||||
// pairing deep-link host may carry runtime auth material.
|
||||
if (parsed.protocol !== 'orca:' || parsed.hostname !== 'pair') {
|
||||
return null
|
||||
}
|
||||
if (parsed.pathname !== '' && parsed.pathname !== '/') {
|
||||
return null
|
||||
}
|
||||
const code = parsed.searchParams.get('code')
|
||||
if (code) {
|
||||
return code
|
||||
}
|
||||
return parsed.hash ? parsed.hash.slice(1) || null : null
|
||||
}
|
||||
|
||||
function base64UrlToBytes(value: string): Uint8Array {
|
||||
const base64 = value.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ describe('pairing offer', () => {
|
|||
expect(() => decodePairingOffer('https://example.com#abc')).toThrow('Invalid pairing URL')
|
||||
})
|
||||
|
||||
it('rejects orca URLs outside the exact pairing route', () => {
|
||||
const url = encodePairingOffer(offer)
|
||||
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
||||
|
||||
expect(parsePairingCode(`orca://pairing?code=${code}`)).toBeNull()
|
||||
expect(parsePairingCode(`orca://pair-extra?code=${code}`)).toBeNull()
|
||||
expect(() => decodePairingOffer(`orca://pairing?code=${code}`)).toThrow('Invalid pairing URL')
|
||||
})
|
||||
|
||||
it('rejects URLs without a pairing code', () => {
|
||||
expect(() => decodePairingOffer('orca://pair')).toThrow('Invalid pairing URL')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,23 +34,25 @@ export function decodePairingOffer(url: string): PairingOffer {
|
|||
}
|
||||
|
||||
function extractPairingCodeFromUrl(url: string): string | null {
|
||||
if (!url.startsWith('orca://pair')) {
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const queryIndex = url.indexOf('?')
|
||||
if (queryIndex !== -1) {
|
||||
const query = url.slice(queryIndex + 1).split('#')[0] ?? ''
|
||||
const params = new URLSearchParams(query)
|
||||
const code = params.get('code')
|
||||
if (code) {
|
||||
return code
|
||||
}
|
||||
// Why: prefix checks accepted routes like `orca://pairing?...`; only the
|
||||
// pairing deep-link host may carry runtime auth material.
|
||||
if (parsed.protocol !== 'orca:' || parsed.hostname !== 'pair') {
|
||||
return null
|
||||
}
|
||||
const hashIndex = url.indexOf('#')
|
||||
if (hashIndex !== -1) {
|
||||
return url.slice(hashIndex + 1) || null
|
||||
if (parsed.pathname !== '' && parsed.pathname !== '/') {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
const code = parsed.searchParams.get('code')
|
||||
if (code) {
|
||||
return code
|
||||
}
|
||||
return parsed.hash ? parsed.hash.slice(1) || null : null
|
||||
}
|
||||
|
||||
// Why: accept either an `orca://pair?...` URL or the bare base64
|
||||
|
|
@ -62,7 +64,7 @@ export function parsePairingCode(input: string): PairingOffer | null {
|
|||
return null
|
||||
}
|
||||
try {
|
||||
if (trimmed.startsWith('orca://pair')) {
|
||||
if (trimmed.toLowerCase().startsWith('orca://')) {
|
||||
return decodePairingOffer(trimmed)
|
||||
}
|
||||
return decodePairingBase64(trimmed)
|
||||
|
|
|
|||
Loading…
Reference in New Issue