feat(relay): B7 — close browser DPoP-subprotocol loop + harden staging mint
- Read DPoP proof from term.dpop.<b64u> WS subprotocol (browsers can't set WS headers); header wins, else subprotocol. Fail-closed decoder. Unblocks real browser connect. - F1: rate-limit /auth/mint per-IP via Redis token bucket (salted-hash key, 429 on burst, before password compare). - F2: wire real per-tenant active WS count into activeSessionCount (was hardcoded 0). - F5: scrub error logs to e.message/.code (no DSN leak, INV9). relay-run: tsc clean, 92 tests pass (+18). F3/F4 -> Phase 2 backlog.
This commit is contained in:
68
relay-run/tests/browser-server.test.ts
Normal file
68
relay-run/tests/browser-server.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* B7 · unit tests for `buildUpgradeRequest` — the browser upgrade → P1 `UpgradeRequest` builder.
|
||||
* Focus: the DPoP proof transport (FUNCTIONAL BLOCKER). A browser cannot set the `dpop` request
|
||||
* header, so the proof rides the `term.dpop.<b64u>` subprotocol entry; the builder must read it there
|
||||
* while keeping the header path working (header WINS if both). Also pins `activeSessionCount` plumbing.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { IncomingMessage } from 'node:http'
|
||||
import { encodeBase64UrlString, APP_SUBPROTOCOL } from 'relay-contracts'
|
||||
import { buildUpgradeRequest } from '../src/servers/browser-server.js'
|
||||
import { DPOP_SUBPROTOCOL_PREFIX } from '../src/servers/dpop-subprotocol.js'
|
||||
|
||||
const PROOF = 'eyJhbGciOiJFZERTQSJ9.eyJodHUiOiJodHRwczovL2FsaWNlL3dzIn0.c2ln'
|
||||
|
||||
function dpopEntry(proofJws: string): string {
|
||||
return DPOP_SUBPROTOCOL_PREFIX + encodeBase64UrlString(proofJws)
|
||||
}
|
||||
|
||||
function mkReq(headers: Record<string, string | undefined>): IncomingMessage {
|
||||
return {
|
||||
headers: { host: 'alice.example.com', ...headers },
|
||||
url: '/ws',
|
||||
socket: { remoteAddress: '1.2.3.4' },
|
||||
} as unknown as IncomingMessage
|
||||
}
|
||||
|
||||
describe('buildUpgradeRequest — DPoP transport', () => {
|
||||
it('reads the proof from the term.dpop.<b64u> subprotocol when no header is set (the browser path)', () => {
|
||||
const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}` })
|
||||
const upgrade = buildUpgradeRequest(req, 0)
|
||||
expect(upgrade.dpop.proof).toBe(PROOF)
|
||||
})
|
||||
|
||||
it('prefers the dpop HEADER over the subprotocol entry when both are present (header wins)', () => {
|
||||
const req = mkReq({
|
||||
dpop: 'header-proof-jws',
|
||||
'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}`,
|
||||
})
|
||||
expect(buildUpgradeRequest(req, 0).dpop.proof).toBe('header-proof-jws')
|
||||
})
|
||||
|
||||
it('still honours the dpop header alone (header path unchanged)', () => {
|
||||
const req = mkReq({ dpop: 'header-only', 'sec-websocket-protocol': APP_SUBPROTOCOL })
|
||||
expect(buildUpgradeRequest(req, 0).dpop.proof).toBe('header-only')
|
||||
})
|
||||
|
||||
it('yields proof=null when neither header nor subprotocol carries a proof', () => {
|
||||
const req = mkReq({ 'sec-websocket-protocol': APP_SUBPROTOCOL })
|
||||
expect(buildUpgradeRequest(req, 0).dpop.proof).toBeNull()
|
||||
})
|
||||
|
||||
it('yields proof=null (fail-closed) for a malformed term.dpop. subprotocol entry', () => {
|
||||
const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${DPOP_SUBPROTOCOL_PREFIX}not*b64u!` })
|
||||
expect(buildUpgradeRequest(req, 0).dpop.proof).toBeNull()
|
||||
})
|
||||
|
||||
it('parses the comma-separated subprotocol list and always carries the app subprotocol through', () => {
|
||||
const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}` })
|
||||
expect(buildUpgradeRequest(req, 0).subprotocols).toContain(APP_SUBPROTOCOL)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildUpgradeRequest — activeSessionCount plumbing (F2)', () => {
|
||||
it('carries the caller-supplied active-session count verbatim', () => {
|
||||
const req = mkReq({ 'sec-websocket-protocol': APP_SUBPROTOCOL })
|
||||
expect(buildUpgradeRequest(req, 3).activeSessionCount).toBe(3)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user