mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
* 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.
193 lines
6.2 KiB
TypeScript
193 lines
6.2 KiB
TypeScript
import { z } from 'zod'
|
|
import type { RawData } from 'ws'
|
|
import {
|
|
DeviceCredentialInstalledSchema,
|
|
DeviceResumeConfirmedSchema
|
|
} from '../../../shared/mobile-relay-credential-contract'
|
|
|
|
const OpaqueIdSchema = z.string().min(1).max(128)
|
|
const Base64Url32ByteSchema = z.string().regex(/^[A-Za-z0-9_-]{43}$/)
|
|
const Base6432ByteSchema = z.string().regex(/^[A-Za-z0-9+/]{43}=$/)
|
|
const Base64Raw24ByteSchema = z.string().regex(/^[A-Za-z0-9+/]{32}$/)
|
|
const EpochMsSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
const GenerationSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
|
|
|
|
export const RelayHostChallengeMessageSchema = z
|
|
.object({
|
|
type: z.literal('host-challenge'),
|
|
challengeId: OpaqueIdSchema,
|
|
relayEphemeralPublicKeyB64: Base6432ByteSchema,
|
|
nonceB64: Base64Raw24ByteSchema,
|
|
ciphertextB64: z
|
|
.string()
|
|
.min(1)
|
|
.max(16 * 1024),
|
|
expiresAt: EpochMsSchema
|
|
})
|
|
.strict()
|
|
|
|
const ConnectionKindSchema = z.enum(['invite', 'resume'])
|
|
|
|
// Mirrors RELAY_HOST_CAPABILITIES_HEADER in the relay contract. It rides the
|
|
// control upgrade rather than host-hello because the cell parses host-hello
|
|
// strictly: a new hello key is refused by every already-deployed cell.
|
|
export const RELAY_HOST_CAPABILITY_HEADERS = {
|
|
'x-orca-host-capabilities': 'pending-conn-details,idle-regional-rehome-v1'
|
|
} as const
|
|
|
|
// Mirrors RELAY_PROTOCOL_LIMITS.hostAttachDeadlineMs in the relay contract: the
|
|
// window the cell keeps a phone waiting for the host's data socket.
|
|
export const RELAY_HOST_ATTACH_DEADLINE_MS = 10_000
|
|
|
|
// kind/relayDeviceId are accepted but not required: today's cells restate only
|
|
// the identifiers, and a strict schema would make adding them a breaking change.
|
|
const PendingConnectionSchema = z
|
|
.object({
|
|
connId: OpaqueIdSchema,
|
|
connTicket: Base64Url32ByteSchema,
|
|
kind: ConnectionKindSchema.optional(),
|
|
relayDeviceId: OpaqueIdSchema.optional()
|
|
})
|
|
.strict()
|
|
|
|
export const RelayHostHelloAckMessageSchema = z
|
|
.object({
|
|
type: z.literal('host-hello-ack'),
|
|
v: z.literal(1),
|
|
generation: GenerationSchema,
|
|
controlResumeSecret: Base64Url32ByteSchema,
|
|
leaseExpiresAt: EpochMsSchema,
|
|
activeConnIds: z.array(OpaqueIdSchema).max(8),
|
|
pendingConns: z.array(PendingConnectionSchema).max(8)
|
|
})
|
|
.strict()
|
|
|
|
export const RelayConnectionOpenMessageSchema = z
|
|
.object({
|
|
type: z.literal('conn-open'),
|
|
connId: OpaqueIdSchema,
|
|
connTicket: Base64Url32ByteSchema,
|
|
kind: ConnectionKindSchema,
|
|
relayDeviceId: OpaqueIdSchema,
|
|
attachDeadlineMs: z.number().int().positive().max(60_000)
|
|
})
|
|
.strict()
|
|
|
|
export const RelayDrainMessageSchema = z
|
|
.object({
|
|
type: z.literal('drain'),
|
|
graceMs: z
|
|
.number()
|
|
.int()
|
|
.nonnegative()
|
|
.max(60 * 60 * 1000),
|
|
recovery: z.literal('resolve-director')
|
|
})
|
|
.strict()
|
|
|
|
export const RelayPingMessageSchema = z
|
|
.object({ type: z.literal('ping'), t: EpochMsSchema })
|
|
.strict()
|
|
|
|
export const RelayInviteCreatedMessageSchema = z
|
|
.object({
|
|
type: z.literal('invite-created'),
|
|
reqId: OpaqueIdSchema,
|
|
inviteToken: Base64Url32ByteSchema,
|
|
expiresAt: EpochMsSchema,
|
|
maxAttempts: z.number().int().positive().max(16)
|
|
})
|
|
.strict()
|
|
|
|
export const RelayDeviceRevokedMessageSchema = z
|
|
.object({ type: z.literal('device-revoked'), reqId: OpaqueIdSchema })
|
|
.strict()
|
|
|
|
export const RelayDeviceCredentialInstalledMessageSchema = DeviceCredentialInstalledSchema.extend({
|
|
type: z.literal('device-credential-installed')
|
|
}).strict()
|
|
|
|
export const RelayDeviceCredentialInstallStatusResultMessageSchema = z.union([
|
|
z
|
|
.object({
|
|
type: z.literal('device-credential-install-status-result'),
|
|
v: z.literal(1),
|
|
reqId: OpaqueIdSchema,
|
|
state: z.literal('not-found')
|
|
})
|
|
.strict(),
|
|
z
|
|
.object({
|
|
type: z.literal('device-credential-install-status-result'),
|
|
v: z.literal(1),
|
|
reqId: OpaqueIdSchema,
|
|
state: z.literal('committed'),
|
|
result: DeviceCredentialInstalledSchema
|
|
})
|
|
.strict()
|
|
])
|
|
|
|
export const RelayDeviceResumeConfirmedMessageSchema = DeviceResumeConfirmedSchema.extend({
|
|
type: z.literal('device-resume-confirmed')
|
|
}).strict()
|
|
|
|
export const RelayControlErrorMessageSchema = z
|
|
.object({
|
|
type: z.literal('control-error'),
|
|
reqId: OpaqueIdSchema.optional(),
|
|
code: z.string().min(1).max(128)
|
|
})
|
|
.strict()
|
|
|
|
export type RelayPendingConnection = z.infer<typeof PendingConnectionSchema>
|
|
export type RelayHostHelloAckMessage = z.infer<typeof RelayHostHelloAckMessageSchema>
|
|
export type RelayConnectionOpenMessage = z.infer<typeof RelayConnectionOpenMessageSchema>
|
|
export type RelayDrainMessage = z.infer<typeof RelayDrainMessageSchema>
|
|
export type RelayInviteCreatedMessage = z.infer<typeof RelayInviteCreatedMessageSchema>
|
|
export type RelayDeviceCredentialInstalledMessage = z.infer<
|
|
typeof RelayDeviceCredentialInstalledMessageSchema
|
|
>
|
|
export type RelayDeviceCredentialInstallStatusResultMessage = z.infer<
|
|
typeof RelayDeviceCredentialInstallStatusResultMessageSchema
|
|
>
|
|
export type RelayDeviceResumeConfirmedMessage = z.infer<
|
|
typeof RelayDeviceResumeConfirmedMessageSchema
|
|
>
|
|
|
|
export function parseRelayControlMessage(raw: RawData): Record<string, unknown> | null {
|
|
try {
|
|
const parsed = JSON.parse(raw.toString()) as unknown
|
|
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
? (parsed as Record<string, unknown>)
|
|
: null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export type RelayHostHello = {
|
|
relayHostId: string
|
|
assignmentEpoch: number
|
|
hostPublicKeyB64: string
|
|
appVersion: string
|
|
previousGeneration?: number
|
|
controlResumeSecret?: string
|
|
}
|
|
|
|
// Optional members are omitted rather than sent as undefined: the cell parses
|
|
// host-hello strictly and an explicit null is not the same as absent.
|
|
export function encodeRelayHostHello(hello: RelayHostHello): string {
|
|
return JSON.stringify({
|
|
type: 'host-hello',
|
|
v: 1,
|
|
relayHostId: hello.relayHostId,
|
|
assignmentEpoch: hello.assignmentEpoch,
|
|
hostPublicKeyB64: hello.hostPublicKeyB64,
|
|
appVersion: hello.appVersion,
|
|
...(hello.previousGeneration === undefined
|
|
? {}
|
|
: { previousGeneration: hello.previousGeneration }),
|
|
...(hello.controlResumeSecret ? { controlResumeSecret: hello.controlResumeSecret } : {})
|
|
})
|
|
}
|