OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / api-cors-outboxlog-preflight.test.js

94 lines · javascript ref:
/**
 * outboxlog CORS preflight regression (PR #178, shipped v0.24.2).
 *
 * RelayAPI._handle runs the global CORS gate BEFORE the outboxlog HTTP adapter.
 * With the fleet-default empty corsOrigins, a cross-origin OPTIONS preflight was
 * denied 403 — or answered without the X-Pear-Token auth header. Browsers send
 * preflights; Node clients don't, which is why every Node/local E2E passed while
 * a real browser (peerit.site → outbox.peerit.site) hung at "connecting to
 * peers" and community-create failed.
 *
 * The fix treats every outboxlog HTTP route as a public, app-agnostic blind
 * pipe: ACAO '*', ACAH incl. X-Pear-Token / X-Pear-Admin-Token, and never
 * preflight-denied — while NON-outboxlog routes keep deny-by-default. These
 * tests drive the real _handle OPTIONS path, so reverting the publicOutboxLog
 * override (api.js) or dropping a route from isOutboxLogHttpRoute fails here.
 */
import test from 'brittle'
import { RelayAPI } from 'p2p-hiverelay/core/relay-node/api.js'
import { isOutboxLogHttpRoute } from 'p2p-hiverelay/core/relay-node/api-route-mounts.js'

function makeApi (corsOrigins = []) {
  // Minimal node stub; the OPTIONS path touches only corsOrigins/port and the
  // pure CORS helpers, returning before rate limiting or any adapter dispatch.
  const api = new RelayAPI({ emit () {} }, { apiPort: 0, apiHost: '127.0.0.1', corsOrigins })
  api.trustProxy = false
  return api
}

function mockRes () {
  const cap = { statusCode: null, headers: {}, body: null, ended: false }
  const res = {
    setHeader (k, v) { cap.headers[String(k).toLowerCase()] = v },
    getHeader (k) { return cap.headers[String(k).toLowerCase()] },
    removeHeader (k) { delete cap.headers[String(k).toLowerCase()] },
    writeHead (code, arg2, arg3) {
      cap.statusCode = code
      const h = (arg3 && typeof arg3 === 'object') ? arg3 : (arg2 && typeof arg2 === 'object' ? arg2 : null)
      if (h) for (const k of Object.keys(h)) cap.headers[String(k).toLowerCase()] = h[k]
      return res
    },
    end (chunk) {
      if (chunk != null) cap.body = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
      cap.ended = true
    }
  }
  Object.defineProperty(res, 'statusCode', { get () { return cap.statusCode }, set (v) { cap.statusCode = v } })
  return { res, cap }
}

async function preflight (api, path, { origin = 'https://peerit.site' } = {}) {
  const req = { method: 'OPTIONS', url: path, headers: origin ? { origin } : {}, socket: { remoteAddress: '203.0.113.9' } }
  const { res, cap } = mockRes()
  await api._handle(req, res)
  return cap
}

test('isOutboxLogHttpRoute: covers the browser-critical outboxlog surface, excludes the rest', (t) => {
  for (const p of ['/api/token', '/api/sync/heads', '/api/sync/append', '/api/swarm/announce', '/api/identity', '/api/identity/x']) {
    t.ok(isOutboxLogHttpRoute(p), `${p} is an outboxlog route`)
  }
  for (const p of ['/api/config', '/health', '/api/poker/tables', '/api/status']) {
    t.absent(isOutboxLogHttpRoute(p), `${p} is NOT an outboxlog route`)
  }
})

test('outboxlog preflight: cross-origin OPTIONS is allowed with token headers under empty corsOrigins', async (t) => {
  const api = makeApi([]) // fleet default — deny-by-default for managed routes
  for (const path of ['/api/token', '/api/sync/append', '/api/sync/heads', '/api/swarm/announce']) {
    const cap = await preflight(api, path, { origin: 'https://peerit.site' })
    t.is(cap.statusCode, 204, `${path} preflight → 204 (not 403)`)
    t.is(cap.headers['access-control-allow-origin'], '*', `${path} ACAO is wildcard (public blind pipe)`)
    const acah = String(cap.headers['access-control-allow-headers'] || '')
    t.ok(acah.includes('X-Pear-Token'), `${path} ACAH advertises X-Pear-Token`)
    t.ok(acah.includes('X-Pear-Admin-Token'), `${path} ACAH advertises X-Pear-Admin-Token`)
    t.is(cap.headers['access-control-allow-methods'], 'GET, POST, OPTIONS', `${path} advertises methods`)
    t.absent(/CORS origin denied/.test(String(cap.body || '')), `${path} is not denied`)
  }
})

test('non-outboxlog preflight: managed route stays deny-by-default under empty corsOrigins', async (t) => {
  const api = makeApi([])
  const cap = await preflight(api, '/api/config', { origin: 'https://evil.example' })
  t.is(cap.statusCode, 403, 'foreign preflight on a managed route is denied')
  t.ok(/CORS origin denied/.test(String(cap.body || '')), 'denial body is explicit')
})

test('non-outboxlog preflight: allowlisted origin gets the managed (non-public) header contract', async (t) => {
  const api = makeApi(['https://peerit.site'])
  const cap = await preflight(api, '/api/config', { origin: 'https://peerit.site' })
  t.is(cap.statusCode, 204, 'allowlisted origin passes preflight')
  t.is(cap.headers['access-control-allow-origin'], 'https://peerit.site', 'echoes the specific origin, not wildcard')
  t.is(cap.headers['access-control-allow-headers'], 'Content-Type, Authorization', 'managed routes do NOT advertise the outboxlog token headers')
})