mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
Normalize bounded Base64 input consistently across desktop, browser, and mobile pairing paths while preserving lossless legacy padding and trailing-bit spellings. Retain offer validation and authentication behavior.
Adapted from #12027. Compatibility verified across 378 spellings with no previously accepted inputs lost; full PR workflow and aggregate gate passed for a84dfaee35.
Co-authored-by: Henry Su <henrysu4707@gmail.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
211 lines
7.2 KiB
TypeScript
211 lines
7.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
encodePairingOffer,
|
|
decodePairingOffer,
|
|
parsePairingCode,
|
|
type PairingOffer
|
|
} from './pairing'
|
|
import { PAIRING_INPUT_MAX_CHARACTERS } from './mobile-pairing-protocol-limits'
|
|
|
|
describe('pairing offer', () => {
|
|
const offer: PairingOffer = {
|
|
v: 2,
|
|
endpoint: 'ws://192.168.1.10:6768',
|
|
deviceToken: 'abcdef1234567890abcdef1234567890abcdef1234567890',
|
|
publicKeyB64: 'dGVzdC1wdWJsaWMta2V5LWJhc2U2NC1lbmNvZGVk'
|
|
}
|
|
|
|
it('encode then decode round-trips correctly', () => {
|
|
const url = encodePairingOffer(offer)
|
|
expect(url).toMatch(/^orca:\/\/pair\?code=/)
|
|
|
|
const decoded = decodePairingOffer(url)
|
|
expect(decoded).toEqual(offer)
|
|
})
|
|
|
|
it('decodes in browser runtimes without Buffer', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const nodeBuffer = Buffer
|
|
let decoded: PairingOffer | null = null
|
|
try {
|
|
vi.stubGlobal('Buffer', undefined)
|
|
decoded = decodePairingOffer(url)
|
|
} finally {
|
|
vi.stubGlobal('Buffer', nodeBuffer)
|
|
}
|
|
expect(decoded).toEqual(offer)
|
|
})
|
|
|
|
it('preserves partial padding consistently across Node and browser runtimes', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(code.length % 4).toBe(2)
|
|
const malformedCode = `${code}=`
|
|
const nodeDecoded = parsePairingCode(malformedCode)
|
|
const nodeBuffer = Buffer
|
|
let browserDecoded: PairingOffer | null = null
|
|
try {
|
|
vi.stubGlobal('Buffer', undefined)
|
|
browserDecoded = parsePairingCode(malformedCode)
|
|
} finally {
|
|
vi.stubGlobal('Buffer', nodeBuffer)
|
|
}
|
|
|
|
expect({ nodeDecoded, browserDecoded }).toEqual({
|
|
nodeDecoded: offer,
|
|
browserDecoded: offer
|
|
})
|
|
})
|
|
|
|
it('preserves non-zero pad bits consistently across Node and browser runtimes', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(code.length % 4).toBe(2)
|
|
const alias = `${code.slice(0, -1)}${base64Alias(code.at(-1)!)}`
|
|
const nodeDecoded = parsePairingCode(alias)
|
|
const nodeBuffer = Buffer
|
|
let browserDecoded: PairingOffer | null = null
|
|
try {
|
|
vi.stubGlobal('Buffer', undefined)
|
|
browserDecoded = parsePairingCode(alias)
|
|
} finally {
|
|
vi.stubGlobal('Buffer', nodeBuffer)
|
|
}
|
|
|
|
expect({ nodeDecoded, browserDecoded }).toEqual({
|
|
nodeDecoded: offer,
|
|
browserDecoded: offer
|
|
})
|
|
})
|
|
|
|
it('accepts canonical base64 padding', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(code.length % 4).toBe(2)
|
|
|
|
expect(parsePairingCode(`${code}==`)).toEqual(offer)
|
|
})
|
|
|
|
it('preserves optional device scope metadata', () => {
|
|
const scopedOffer: PairingOffer = { ...offer, scope: 'mobile' }
|
|
expect(decodePairingOffer(encodePairingOffer(scopedOffer))).toEqual(scopedOffer)
|
|
})
|
|
|
|
it('round-trips a TLS reverse-proxy endpoint with an explicit port and path', () => {
|
|
const proxiedOffer = {
|
|
...offer,
|
|
endpoint: 'wss://proxy.example:443/orca/runtime'
|
|
}
|
|
|
|
expect(decodePairingOffer(encodePairingOffer(proxiedOffer))).toEqual(proxiedOffer)
|
|
})
|
|
|
|
it('encoded URL uses base64url (no +, /, or = characters)', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(code).not.toMatch(/[+/=]/)
|
|
})
|
|
|
|
it('rejects URLs with wrong scheme', () => {
|
|
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')
|
|
})
|
|
|
|
it('decodes legacy hash URLs', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(decodePairingOffer(`orca://pair#${code}`)).toEqual(offer)
|
|
})
|
|
|
|
it('rejects oversized whole URLs before parsing', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const oversizedUrl = `${url}&ignored=${'A'.repeat(PAIRING_INPUT_MAX_CHARACTERS)}`
|
|
|
|
expect(() => decodePairingOffer(oversizedUrl)).toThrow(
|
|
'Invalid pairing URL: pairing code exceeds safe size'
|
|
)
|
|
})
|
|
|
|
it('rejects payloads with missing fields', () => {
|
|
const partial = { v: 2, endpoint: 'ws://host:1234' }
|
|
const base64 = Buffer.from(JSON.stringify(partial)).toString('base64')
|
|
expect(() => decodePairingOffer(`orca://pair#${base64}`)).toThrow()
|
|
})
|
|
|
|
it('rejects payloads with wrong version', () => {
|
|
const wrong = { ...offer, v: 1 }
|
|
const base64 = Buffer.from(JSON.stringify(wrong)).toString('base64')
|
|
expect(() => decodePairingOffer(`orca://pair#${base64}`)).toThrow()
|
|
})
|
|
|
|
it('rejects payloads with missing publicKeyB64', () => {
|
|
const wrong = { v: 2, endpoint: 'ws://host:1234', deviceToken: 'tok' }
|
|
const base64 = Buffer.from(JSON.stringify(wrong)).toString('base64')
|
|
expect(() => decodePairingOffer(`orca://pair#${base64}`)).toThrow()
|
|
})
|
|
})
|
|
|
|
describe('parsePairingCode', () => {
|
|
const offer: PairingOffer = {
|
|
v: 2,
|
|
endpoint: 'ws://192.168.1.10:6768',
|
|
deviceToken: 'token-abc',
|
|
publicKeyB64: 'pubkey-xyz'
|
|
}
|
|
|
|
it('parses a full orca://pair# URL', () => {
|
|
const url = encodePairingOffer(offer)
|
|
expect(parsePairingCode(url)).toEqual(offer)
|
|
})
|
|
|
|
it('parses a bare base64url payload (without scheme prefix)', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const base64url = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
expect(parsePairingCode(base64url)).toEqual(offer)
|
|
})
|
|
|
|
it('tolerates surrounding whitespace from clipboard', () => {
|
|
const url = encodePairingOffer(offer)
|
|
expect(parsePairingCode(` ${url}\n`)).toEqual(offer)
|
|
})
|
|
|
|
it('rejects oversized whole raw input before trimming', () => {
|
|
const url = encodePairingOffer(offer)
|
|
const code = new URLSearchParams(url.slice(url.indexOf('?') + 1)).get('code')!
|
|
|
|
expect(parsePairingCode(`${code}${' '.repeat(PAIRING_INPUT_MAX_CHARACTERS)}`)).toBeNull()
|
|
})
|
|
|
|
it('returns null for empty input', () => {
|
|
expect(parsePairingCode('')).toBeNull()
|
|
expect(parsePairingCode(' ')).toBeNull()
|
|
})
|
|
|
|
it('returns null for garbage input', () => {
|
|
expect(parsePairingCode('not a pairing code')).toBeNull()
|
|
expect(parsePairingCode('https://example.com')).toBeNull()
|
|
})
|
|
|
|
it('returns null for valid base64 of unrelated JSON', () => {
|
|
const bogus = Buffer.from(JSON.stringify({ hello: 'world' })).toString('base64')
|
|
expect(parsePairingCode(bogus)).toBeNull()
|
|
})
|
|
})
|
|
|
|
function base64Alias(character: string): string {
|
|
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
|
return alphabet[alphabet.indexOf(character) + 1]!
|
|
}
|