/**
* R9 — Public HTTPS Hive gateway conformance suite.
*
* Executable conformance harness for the frozen public-gateway contract in
* docs/PUBLIC-HTTPS-HIVE-GATEWAY-SPEC.md (§7.4 policy table, §12.3 production
* posture). Any relay build can run this file headlessly before admission:
* every enforceable frozen limit is asserted against an in-process
* GatewayServer/HyperGateway with stubbed drive/proxy edges, and the two
* behaviors that live one hop out in the TLS-terminating proxy carry
* gate-named skips at the bottom.
*
* Frozen tuple (PUBLIC_T1_GATEWAY_FINITE_LIMITS):
* response cap 64 MiB (legacy) / 4 MiB transform cell class,
* egress 256 MiB per client-IP × app per 60 s window,
* 15-minute response lifetime.
*
* Edge discipline: default-421 for misdirected/unknown SNI, SNI==Host
* binding, exactly one canonical z32 label per app origin, GET/HEAD-only
* with no request bodies, 30 s drive-op timeout, 20 s empty-drive wait,
* drive LRU 20, directory listing ≤ 1000 entries / ≤ 1 MiB, in-flight
* concurrency 256 global / 32 per app, 10 s headersTimeout.
*
* Scoping notes (honesty, not aspiration):
* - The frozen egress tuple is keyed per client-IP × app origin. The
* legacy /v1/hyper/KEY compatibility lane deliberately keeps only its
* request-rate limit (spec §15 Phase 1: the path gateway stays
* unchanged), so egress is asserted on the app-origin lane.
* - Wall-clock ceilings (30 s drive-op, 20 s empty-drive wait, 10 s
* header reaping) are asserted at full scale. The 15-minute response
* lifetime is asserted by exact static value plus a scaled destroy
* probe; the full-scale soak belongs to the canary window (G8 skip).
* - The nginx default_server 421 vhost is proxy-runtime configuration,
* not gateway-process behavior (G12 skip). Its in-process halves —
* misdirected Host → 421 and forwarded-SNI≠Host → 421 — are asserted.
*/
import test from 'brittle'
import http from 'http'
import net from 'net'
import { Readable } from 'stream'
import { randomBytes } from 'crypto'
import { mkdtemp, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import Corestore from 'corestore'
import { GatewayServer, assertHiveAppGatewayIsolation } from 'p2p-hiverelay/core/relay-node/gateway-server.js'
import { HyperGateway } from 'p2p-hiverelay/gateway'
import { encodeHiveAppKey } from 'p2p-hiverelay/gateway/hive-host.js'
import {
PUBLIC_T1_GATEWAY_FINITE_LIMITS,
assertPublicHiveGatewayFiniteLimits
} from '../../packages/core/config/public-hive-gateway-env.js'
const SUFFIX = 'hive-conformance.test'
// Restated from the spec so the suite fails on drift in either direction
// (code ≠ spec or spec ≠ code).
const FROZEN_FINITE_LIMITS = {
gatewayMaxResponseBytes: 64 * 1024 * 1024,
gatewayMaxTransformBytes: 4 * 1024 * 1024,
gatewayEgressBytesPerWindow: 256 * 1024 * 1024,
gatewayEgressWindowMs: 60 * 1000,
gatewayMaxResponseLifetimeMs: 15 * 60 * 1000
}
const FROZEN_MAX_IN_FLIGHT = 256
const FROZEN_MAX_IN_FLIGHT_PER_APP = 32
const FROZEN_DRIVE_OPERATION_TIMEOUT_MS = 30_000
const FROZEN_EMPTY_DRIVE_WAIT_MS = 20_000
const FROZEN_MAX_CACHED_DRIVES = 20
const FROZEN_HEADERS_TIMEOUT_MS = 10_000
const FROZEN_MAX_LISTING_ENTRIES = 1000
const FROZEN_MAX_LISTING_BYTES = 1024 * 1024
function newAppKey () {
return randomBytes(32).toString('hex')
}
function appHostFor (keyHex) {
return `${encodeHiveAppKey(Buffer.from(keyHex, 'hex'))}.${SUFFIX}`
}
function appHeaders (host, extra = {}) {
return { Host: host, 'X-Hive-Forwarded-SNI': host, ...extra }
}
function fakeDrive (files = {}, sizes = {}) {
return {
closed: false,
closing: false,
version: 1,
async ready () {},
async update () {},
async close () { this.closed = true },
async entry (filePath) {
const data = files[filePath]
if (!data) return null
return { value: { blob: { byteLength: sizes[filePath] ?? data.byteLength } } }
},
async get (filePath) {
return files[filePath] || null
},
createReadStream (filePath, opts = {}) {
const data = files[filePath] || Buffer.alloc(0)
const start = opts.start || 0
const end = opts.length == null ? data.length : start + opts.length
return Readable.from([data.subarray(start, end)])
},
checkout () { return fakeDrive(files, sizes) },
async * list () {}
}
}
function seededEntry (drive) {
return {
drive,
blind: false,
privacyTier: 'public',
storageClass: 'persistent',
availabilityClass: 'always-on',
storageProvedDriveVersion: drive.version
}
}
function mockPublicNode (seededApps = new Map()) {
const node = {
config: { gatewayPublicOnlyPrivacyTier: true },
store: null,
seededApps
}
node.appLifecycle = {
acquireDriveReadLease (key) {
const drive = node.seededApps.get(key)?.drive
if (!drive || drive.closed || drive.closing) return null
return { drive, release () {} }
}
}
return node
}
function request (port, path, opts = {}) {
return new Promise((resolve, reject) => {
const req = http.request({
hostname: '127.0.0.1',
port,
method: opts.method || 'GET',
path,
agent: false,
headers: opts.headers || {}
}, (res) => {
const chunks = []
res.on('data', chunk => { chunks.push(chunk) })
res.on('end', () => {
const raw = Buffer.concat(chunks)
let body = raw
try { body = JSON.parse(raw.toString('utf8')) } catch (_) {}
resolve({ statusCode: res.statusCode, headers: res.headers, body, raw })
})
res.on('error', reject)
})
req.on('error', reject)
req.end(opts.body)
})
}
async function startGatewayServer (t, opts = {}) {
const { node = mockPublicNode(), ...serverOpts } = opts
const server = new GatewayServer(node, {
gatewayPort: 0,
gatewayHost: '127.0.0.1',
...serverOpts
})
await server.start()
t.teardown(async () => {
try { await server.stop() } catch (_) {}
})
return server
}
// App-origin lane through the production wiring: GatewayServer builds its own
// HyperGateway with requireLifecycleDriveAuthority, so the served bytes cross
// the same admission/lease/proof chain a public-t1 relay would use.
async function bootAppGateway (t, apps, serverOpts = {}) {
const seededApps = new Map()
for (const { keyHex, drive } of apps) seededApps.set(keyHex, seededEntry(drive))
const server = await startGatewayServer(t, {
node: mockPublicNode(seededApps),
hiveAppHostSuffix: SUFFIX,
hiveAppPublicKeys: apps.map(app => app.keyHex),
hiveAppPublicVersions: Object.fromEntries(apps.map(app => [app.keyHex, app.drive.version])),
trustProxy: true,
trustedProxyAddresses: ['127.0.0.1'],
...serverOpts
})
return server
}
// Legacy /v1/hyper/KEY lane: HyperGateway in front of a bare HTTP server.
async function bootLegacyGateway (t, drive, gatewayOpts = {}) {
const keyHex = newAppKey()
const node = {
config: { gatewayPublicOnlyPrivacyTier: true },
seededApps: new Map([[keyHex, { drive, blind: false, privacyTier: 'public' }]])
}
const gateway = new HyperGateway(node, gatewayOpts)
const server = http.createServer((req, res) => gateway.handle(req, res))
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
t.teardown(async () => {
if (typeof server.closeAllConnections === 'function') {
try { server.closeAllConnections() } catch (_) {}
}
await new Promise(resolve => server.close(resolve))
await gateway.close()
})
return {
gateway,
port: server.address().port,
path: (filePath) => `/v1/hyper/${keyHex}${filePath}`
}
}
test('conformance: frozen limits tuple is pinned exactly and config drift fails closed', (t) => {
t.ok(Object.isFrozen(PUBLIC_T1_GATEWAY_FINITE_LIMITS), 'tuple object is frozen')
t.alike({ ...PUBLIC_T1_GATEWAY_FINITE_LIMITS }, FROZEN_FINITE_LIMITS,
'frozen tuple matches the spec integers')
t.is(
assertPublicHiveGatewayFiniteLimits({ ...FROZEN_FINITE_LIMITS, productProfile: 'public-t1-gateway' }).gatewayMaxResponseBytes,
FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes,
'frozen values pass validation under the production profile'
)
for (const [field, expected] of Object.entries(FROZEN_FINITE_LIMITS)) {
t.exception(() => assertPublicHiveGatewayFiniteLimits({
...FROZEN_FINITE_LIMITS,
productProfile: 'public-t1-gateway',
[field]: expected - 1
}), new RegExp(`${field} must equal ${expected}`), `${field} cannot drift below the frozen value`)
t.exception(() => assertPublicHiveGatewayFiniteLimits({
...FROZEN_FINITE_LIMITS,
productProfile: 'public-t1-gateway',
[field]: null
}), new RegExp(`Invalid ${field}`), `${field} cannot be nulled out`)
}
t.exception(() => assertPublicHiveGatewayFiniteLimits({
...FROZEN_FINITE_LIMITS,
gatewayMaxTransformBytes: FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes + 1
}), /must not exceed gatewayMaxResponseBytes/, 'transform class cannot outgrow the response cap')
})
test('conformance: public-t1 isolation contract pins the frozen tuple and the G1 admission gate', (t) => {
const appKey = newAppKey()
const t1 = {
hiveAppHostSuffix: SUFFIX,
hiveAppPublicKeys: [appKey],
hiveAppPublicVersions: { [appKey]: 7 },
gatewayPort: 9200,
gatewayHost: '127.0.0.1',
gatewayTrustProxy: true,
gatewayRequireForwardedSNI: true,
...FROZEN_FINITE_LIMITS,
apiPort: 9100,
apiHost: '127.0.0.1',
enableAPI: true,
enableSeeding: true,
enableRelay: false,
enableServices: false,
plugins: [],
custody: { enabled: false },
requirePhysicalEnforcement: true,
mode: 'public-t1-gateway',
productProfile: 'public-t1-gateway',
enforceCompiledAdmission: false
}
t.is(assertHiveAppGatewayIsolation(t1), SUFFIX, 'frozen reference posture is admitted')
for (const [field, expected] of Object.entries(FROZEN_FINITE_LIMITS)) {
t.exception(() => assertHiveAppGatewayIsolation({ ...t1, [field]: expected + 1 }),
new RegExp(`${field} to equal ${expected}`), `isolation refuses a weakened ${field}`)
}
// G1 role boundary: the compiled admission capability in this build is
// still the transitional operator allowlist, so a fleet posture must keep
// refusing to boot until the blind substrate ships the frozen T1 predicate.
t.exception(() => assertHiveAppGatewayIsolation({ ...t1, enforceCompiledAdmission: true }),
/fleet-ready non-transitional admission/, 'fleet posture stays closed while admission is transitional')
})
test('conformance: runtime defaults materialize the frozen values', (t) => {
const server = new GatewayServer(mockPublicNode(), { gatewayPort: 0, gatewayHost: '127.0.0.1' })
t.is(server._maxInFlight, FROZEN_MAX_IN_FLIGHT, 'global in-flight ceiling is 256')
t.is(server._maxInFlightPerApp, FROZEN_MAX_IN_FLIGHT_PER_APP, 'per-app in-flight ceiling is 32')
t.is(server._egressBytesPerWindow, FROZEN_FINITE_LIMITS.gatewayEgressBytesPerWindow, 'egress budget is 256 MiB')
t.is(server._egressWindowMs, FROZEN_FINITE_LIMITS.gatewayEgressWindowMs, 'egress window is 60 s')
t.is(server._maxResponseLifetimeMs, FROZEN_FINITE_LIMITS.gatewayMaxResponseLifetimeMs, 'response lifetime is 15 min')
const gateway = new HyperGateway({ config: {} })
t.is(gateway._maxResponseBytes, FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes, 'response cap is 64 MiB')
t.is(gateway._maxTransformBytes, FROZEN_FINITE_LIMITS.gatewayMaxTransformBytes, 'transform class is 4 MiB')
t.is(gateway._driveOperationTimeout, FROZEN_DRIVE_OPERATION_TIMEOUT_MS, 'drive-op timeout is 30 s')
t.is(gateway._drives.maxSize, FROZEN_MAX_CACHED_DRIVES, 'drive LRU is 20')
})
test('conformance: over-64-MiB legacy responses are refused, ranges stay the bounded paging path', async (t) => {
const overCap = FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes + 1
const small = Buffer.from('conformance-small\n')
const drive = fakeDrive(
{ '/huge.bin': Buffer.alloc(1024, 0x62), '/small.bin': small },
{ '/huge.bin': overCap }
)
const { port, path } = await bootLegacyGateway(t, drive)
const full = await request(port, path('/huge.bin'))
t.is(full.statusCode, 413, 'over-cap full response is refused')
t.is(full.body.error, 'A bounded single byte range is required')
t.is(full.body.maxResponseBytes, FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes)
const head = await request(port, path('/huge.bin'), { method: 'HEAD' })
t.is(head.statusCode, 413, 'over-cap HEAD representation is refused')
const openRange = await request(port, path('/huge.bin'), { headers: { Range: 'bytes=0-' } })
t.is(openRange.statusCode, 416, 'a range spanning past the cap is refused')
t.is(openRange.headers['content-range'], `bytes */${overCap}`)
const multiRange = await request(port, path('/huge.bin'), { headers: { Range: 'bytes=0-1,2-3' } })
t.is(multiRange.statusCode, 416, 'multi-range cannot launder an over-cap object into a full 200')
const paged = await request(port, path('/huge.bin'), { headers: { Range: 'bytes=0-99' } })
t.is(paged.statusCode, 206, 'a bounded range inside the cap is served')
t.is(paged.headers['content-range'], `bytes 0-99/${overCap}`)
t.is(paged.raw.byteLength, 100)
// RFC 9110 §14.2: an in-cap legacy object may ignore an unsupported range
// with a full 200. The frozen app surface is stricter — the exact-lane 416
// is asserted in the app-origin test below.
const legacyMulti = await request(port, path('/small.bin'), { headers: { Range: 'bytes=0-1,2-3' } })
t.is(legacyMulti.statusCode, 200, 'in-cap legacy multi-range keeps the RFC allowance')
})
test('conformance: transform cell class refuses HTML rewrite inputs over 4 MiB', async (t) => {
const drive = fakeDrive(
{ '/big.html': Buffer.from('<!doctype html><h1>conformance</h1>') },
{ '/big.html': FROZEN_FINITE_LIMITS.gatewayMaxTransformBytes + 1 }
)
const { port, path } = await bootLegacyGateway(t, drive)
const res = await request(port, path('/big.html'))
t.is(res.statusCode, 413)
t.is(res.body.error, 'Response exceeds gateway transform limit')
t.is(res.body.maxResponseBytes, FROZEN_FINITE_LIMITS.gatewayMaxTransformBytes)
})
test('conformance: app origin enforces the same caps and proof mode stays closed', async (t) => {
const keyHex = newAppKey()
const host = appHostFor(keyHex)
const overCap = FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes + 1
const indexBytes = Buffer.from('<!doctype html><h1>conformance</h1>')
const drive = fakeDrive(
{ '/index.html': indexBytes, '/huge.bin': Buffer.alloc(1024, 0x63) },
{ '/huge.bin': overCap }
)
const server = await bootAppGateway(t, [{ keyHex, drive }])
const port = server.server.address().port
const exact = await request(port, '/', { headers: appHeaders(host) })
t.is(exact.statusCode, 200, 'approved app origin serves exact bytes')
t.is(exact.headers['x-hive-byte-mode'], 'exact')
t.is(exact.headers['x-hive-app-key'], keyHex)
const full = await request(port, '/huge.bin', { headers: appHeaders(host) })
t.is(full.statusCode, 413, 'over-cap exact response is refused')
t.is(full.body.maxResponseBytes, FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes)
const head = await request(port, '/huge.bin', { method: 'HEAD', headers: appHeaders(host) })
t.is(head.statusCode, 413, 'over-cap exact HEAD is refused')
const multi = await request(port, '/index.html', { headers: appHeaders(host, { Range: 'bytes=0-1,2-3' }) })
t.is(multi.statusCode, 416, 'exact lane rejects multi-range even inside the cap')
t.is(multi.headers['content-range'], `bytes */${indexBytes.byteLength}`)
const proof = await request(port, '/', {
headers: appHeaders(host, { Accept: 'application/vnd.hiverelay.proof+binary;version=1' })
})
t.is(proof.statusCode, 406, 'deferred proof media type stays refused')
t.is(proof.body.error, 'Proof-carrying HTTP mode is not available')
})
test('conformance: exactly one canonical z32 label selects the app, deformations fail closed', async (t) => {
const keyHex = newAppKey()
const host = appHostFor(keyHex)
const label = host.slice(0, host.indexOf('.'))
const indexBytes = Buffer.from('<!doctype html><h1>conformance</h1>')
const drive = fakeDrive({ '/index.html': indexBytes })
const server = await bootAppGateway(t, [{ keyHex, drive }])
const port = server.server.address().port
const ok = await request(port, '/', { headers: appHeaders(host) })
t.is(ok.statusCode, 200)
t.ok(ok.raw.equals(indexBytes), 'canonical 52-char z32 label serves the pinned app')
// DNS case-folding is canonicalized before decoding, so an uppercase
// spelling of the same label is the same origin, not a second app.
const folded = await request(port, '/', { headers: appHeaders(host.toUpperCase()) })
t.is(folded.statusCode, 200, 'DNS case-fold canonicalizes to the same origin')
const cases = [
[`${label.slice(0, 26)}.${label.slice(26)}.${SUFFIX}`, 'two labels'],
[SUFFIX, 'suffix without a label'],
[`${label.slice(0, 51)}.${SUFFIX}`, 'truncated 51-char label'],
[`0${label.slice(1)}.${SUFFIX}`, 'label outside the z32 alphabet']
]
for (const [badHost, name] of cases) {
const res = await request(port, '/', { headers: { Host: badHost } })
t.is(res.statusCode, 400, `${name} is refused`)
}
const stranger = appHostFor(newAppKey())
const unapproved = await request(port, '/', { headers: { Host: stranger } })
t.is(unapproved.statusCode, 403, 'well-formed but unapproved key fails closed')
t.is(unapproved.body.error, 'App unavailable through public Hive gateway')
})
test('conformance: only GET/HEAD serve and request bodies are refused', async (t) => {
const keyHex = newAppKey()
const host = appHostFor(keyHex)
const drive = fakeDrive({ '/index.html': Buffer.from('<!doctype html>') })
const server = await bootAppGateway(t, [{ keyHex, drive }])
const port = server.server.address().port
const post = await request(port, '/', { method: 'POST', headers: appHeaders(host) })
t.is(post.statusCode, 405)
t.is(post.headers.allow, 'GET, HEAD')
const put = await request(port, `/v1/hyper/${keyHex}/index.html`, {
method: 'PUT',
headers: { Host: '127.0.0.1' }
})
t.is(put.statusCode, 405, 'legacy content lane refuses mutations too')
t.is(put.headers.allow, 'GET, HEAD')
const body = await request(port, '/', { headers: appHeaders(host, { 'Content-Length': '1' }), body: 'x' })
t.is(body.statusCode, 400)
t.is(body.body.error, 'Request body is not allowed')
t.is(body.headers.connection, 'close')
const chunked = await request(port, '/', {
headers: appHeaders(host, { 'Transfer-Encoding': 'chunked' }),
body: 'x'
})
t.is(chunked.statusCode, 400)
t.is(chunked.body.error, 'Request body is not allowed')
const options = await request(port, '/', { method: 'OPTIONS', headers: appHeaders(host) })
t.is(options.statusCode, 204, 'preflight stays non-mutating')
t.is(options.headers['access-control-allow-methods'], 'GET, HEAD, OPTIONS')
})
test('conformance: misdirected Host gets 421 and forwarded SNI must bind to Host', async (t) => {
const keyHex = newAppKey()
const host = appHostFor(keyHex)
const otherHost = appHostFor(newAppKey())
const drive = fakeDrive({ '/index.html': Buffer.from('<!doctype html>') })
const server = await bootAppGateway(t, [{ keyHex, drive }], { requireForwardedSNI: true })
const port = server.server.address().port
const misdirected = await request(port, '/', { headers: { Host: 'unrelated.example' } })
t.is(misdirected.statusCode, 421, 'unknown Host on the app listener is misdirected')
t.is(misdirected.body.error, 'Misdirected request')
const compatibility = await request(port, '/health', { headers: { Host: '127.0.0.1' } })
t.is(compatibility.statusCode, 200, 'allowlisted compatibility host keeps legacy routes')
const missing = await request(port, '/', { headers: { Host: host } })
t.is(missing.statusCode, 421, 'missing forwarded SNI is refused')
t.is(missing.body.error, 'TLS SNI and Host must match')
const mismatch = await request(port, '/', { headers: { Host: host, 'X-Hive-Forwarded-SNI': otherHost } })
t.is(mismatch.statusCode, 421, 'SNI naming a sibling app is refused')
const foreign = await request(port, '/', { headers: { Host: host, 'X-Hive-Forwarded-SNI': 'example.com' } })
t.is(foreign.statusCode, 421, 'non-app SNI is refused')
const bound = await request(port, '/', { headers: appHeaders(host) })
t.is(bound.statusCode, 200, 'edge-bound SNI==Host is admitted')
// A forwarded-SNI header is only meaningful from a trusted proxy peer; a
// loopback peer outside the proxy allowlist cannot present one.
const untrusted = await startGatewayServer(t, {
node: mockPublicNode(new Map([[keyHex, seededEntry(drive)]])),
hiveAppHostSuffix: SUFFIX,
hiveAppPublicKeys: [keyHex],
hiveAppPublicVersions: { [keyHex]: drive.version },
trustProxy: true,
trustedProxyAddresses: ['10.9.9.9'],
requireForwardedSNI: true
})
const forged = await request(untrusted.server.address().port, '/', { headers: appHeaders(host) })
t.is(forged.statusCode, 421, 'untrusted peer cannot forward SNI')
})
test('conformance: 256 MiB per client-IP x app per 60 s egress budget trips 429', async (t) => {
const keyHexA = newAppKey()
const keyHexB = newAppKey()
const hostA = appHostFor(keyHexA)
const hostB = appHostFor(keyHexB)
const cap = FROZEN_FINITE_LIMITS.gatewayMaxResponseBytes
const big = Buffer.alloc(cap, 0x61)
const small = Buffer.from('egress-probe\n')
const server = await bootAppGateway(t, [
{ keyHex: keyHexA, drive: fakeDrive({ '/big.bin': big, '/small.txt': small }) },
{ keyHex: keyHexB, drive: fakeDrive({ '/small.txt': small }) }
], { requireForwardedSNI: true })
const port = server.server.address().port
// Four 64 MiB responses exactly fill the frozen 256 MiB window.
for (let i = 1; i <= 4; i++) {
const res = await request(port, '/big.bin', { headers: appHeaders(hostA) })
t.is(res.statusCode, 200, `window response ${i}/4 is inside budget`)
t.is(res.raw.byteLength, cap)
}
const tripped = await request(port, '/small.txt', { headers: appHeaders(hostA) })
t.is(tripped.statusCode, 429, 'the next byte over 256 MiB is refused')
t.is(tripped.body.error, 'Gateway byte-rate limit exceeded')
t.is(tripped.headers['retry-after'], '60', 'refusal names the frozen 60 s window')
const head = await request(port, '/big.bin', { method: 'HEAD', headers: appHeaders(hostA) })
t.is(head.statusCode, 200, 'body-free HEAD is not charged egress')
const otherClient = await request(port, '/small.txt', {
headers: appHeaders(hostA, { 'X-Forwarded-For': '203.0.113.9' })
})
t.is(otherClient.statusCode, 200, 'budget is keyed per client IP')
const otherApp = await request(port, '/small.txt', { headers: appHeaders(hostB) })
t.is(otherApp.statusCode, 200, 'budget is keyed per app')
})
test('conformance: egress window rolls over (scaled probe, frozen 60 s asserted above)', async (t) => {
const keyHex = newAppKey()
const host = appHostFor(keyHex)
const drive = fakeDrive({ '/small.txt': Buffer.alloc(600, 0x73) })
const server = await bootAppGateway(t, [{ keyHex, drive }], {
egressBytesPerWindow: 1024,
egressWindowMs: 300
})
const port = server.server.address().port
const headers = { Host: host }
t.is((await request(port, '/small.txt', { headers })).statusCode, 200)
const tripped = await request(port, '/small.txt', { headers })
t.is(tripped.statusCode, 429, 'scaled budget trips like the frozen one')
t.is(tripped.headers['retry-after'], '1', 'retry-after reflects the configured window')
await new Promise(resolve => setTimeout(resolve, 350))
t.is((await request(port, '/small.txt', { headers })).statusCode, 200, 'window rollover admits again')
})
test('conformance: response lifetime destroys stalled responses (scaled probe)', async (t) => {
const hanging = {
async close () {},
handle (req, res) {
return new Promise(resolve => res.once('close', resolve))
}
}
const server = await startGatewayServer(t, { gateway: hanging, maxResponseLifetimeMs: 150 })
const timeouts = []
server.on('request-timeout', event => timeouts.push(event))
const startedAt = Date.now()
await new Promise((resolve) => {
const req = http.request({
hostname: '127.0.0.1',
port: server.server.address().port,
path: `/v1/hyper/${'a'.repeat(64)}/slow.bin`,
agent: false
}, (res) => {
res.resume()
res.on('close', resolve)
res.on('end', resolve)
})
req.on('error', () => resolve())
req.end()
})
const elapsed = Date.now() - startedAt
t.ok(elapsed >= 150 && elapsed < 10_000, `stalled response is destroyed at the configured ceiling (${elapsed}ms)`)
t.is(timeouts.length, 1, 'operator observes the lifetime trip once')
t.is(timeouts[0].bucket, 'compatibility')
// The slot release rides the server-side 'close' one event-loop turn after
// the client's ECONNRESET; wait for the drain instead of racing it.
let released = false
const drainDeadline = Date.now() + 5_000
while (!released && Date.now() < drainDeadline) {
await new Promise(resolve => setImmediate(resolve))
released = server._activeRequests === 0
}
t.ok(released, 'destroyed response releases its admission slot')
})
test('conformance: concurrency ceilings of 32 per app and 256 global return 503', async (t) => {
let handleCalls = 0
const hanging = {
async close () {},
handle (req, res) {
handleCalls++
return new Promise(resolve => res.once('close', resolve))
}
}
const server = await startGatewayServer(t, { gateway: hanging, hiveAppHostSuffix: SUFFIX })
const port = server.server.address().port
const appHosts = Array.from({ length: 9 }, (_, i) => appHostFor(Buffer.alloc(32, i + 1).toString('hex')))
const inFlight = []
const fire = (host) => {
const promise = new Promise((resolve) => {
const req = http.request({
hostname: '127.0.0.1',
port,
path: '/',
agent: false,
headers: { Host: host }
}, (res) => {
const chunks = []
res.on('data', chunk => { chunks.push(chunk) })
res.on('end', () => {
const raw = Buffer.concat(chunks)
let body = raw
try { body = JSON.parse(raw.toString('utf8')) } catch (_) {}
resolve({ statusCode: res.statusCode, headers: res.headers, body })
})
})
req.on('error', () => resolve({ statusCode: 0 }))
req.end()
inFlight.push(req)
})
return promise
}
const waitForHandleCalls = async (expected) => {
const startedAt = Date.now()
while (Date.now() - startedAt < 10_000) {
if (handleCalls >= expected) return
await new Promise(resolve => setImmediate(resolve))
}
throw new Error(`gateway saw ${handleCalls}/${expected} in-flight requests`)
}
const answered = []
for (let i = 0; i < FROZEN_MAX_IN_FLIGHT_PER_APP; i++) answered.push(fire(appHosts[0]))
await waitForHandleCalls(FROZEN_MAX_IN_FLIGHT_PER_APP)
const perApp = await request(port, '/', { headers: { Host: appHosts[0] } })
t.is(perApp.statusCode, 503, '33rd in-flight request on one app origin is refused')
t.is(perApp.body.error, 'Gateway busy')
t.is(perApp.headers['retry-after'], '1')
for (let app = 1; app < appHosts.length - 1; app++) {
for (let i = 0; i < FROZEN_MAX_IN_FLIGHT_PER_APP; i++) answered.push(fire(appHosts[app]))
}
await waitForHandleCalls(FROZEN_MAX_IN_FLIGHT)
const global = await request(port, '/', { headers: { Host: appHosts[appHosts.length - 1] } })
t.is(global.statusCode, 503, '257th in-flight request across origins hits the global ceiling')
t.is(global.body.error, 'Gateway busy')
for (const req of inFlight) req.destroy()
await Promise.allSettled(answered)
let drained = false
for (let i = 0; i < 100 && !drained; i++) {
await new Promise(resolve => setImmediate(resolve))
drained = server._activeRequests === 0
}
t.ok(drained, 'every admission slot drains when clients disconnect')
})
test('conformance: drive LRU holds at most 20 drives and retires evictions', async (t) => {
const gateway = new HyperGateway({ config: {} })
t.teardown(async () => { await gateway.close() })
const opened = new Map()
gateway._openDrive = async (keyHex) => {
const drive = fakeDrive({})
opened.set(keyHex, drive)
gateway._drives.set(keyHex, drive)
return drive
}
const keys = Array.from({ length: FROZEN_MAX_CACHED_DRIVES + 1 }, () => newAppKey())
for (const keyHex of keys) {
const lease = await gateway._acquireDrive(keyHex)
t.ok(lease && lease.drive, 'dynamic open answers')
lease.release()
}
t.is(gateway._drives.size, FROZEN_MAX_CACHED_DRIVES, 'cache is capped at the frozen 20')
t.absent(gateway._drives.has(keys[0]), 'least recently used drive is evicted first')
t.ok(gateway._drives.has(keys[keys.length - 1]), 'newest drive stays cached')
for (let i = 0; i < 100 && !opened.get(keys[0]).closed; i++) {
await new Promise(resolve => setImmediate(resolve))
}
t.ok(opened.get(keys[0]).closed, 'evicted drive is retired and closed')
t.absent(opened.get(keys[keys.length - 1]).closed, 'cached drive stays open')
})
test('conformance: directory listing is bounded to 1000 entries and 1 MiB', async (t) => {
const manyEntries = fakeDrive({})
manyEntries.list = async function * () {
for (let i = 0; i < 1500; i++) yield { key: `dir/file-${String(i).padStart(4, '0')}.txt` }
}
const capped = await bootLegacyGateway(t, manyEntries)
const counted = await request(capped.port, capped.path('/'))
t.is(counted.statusCode, 200)
t.is(counted.body.entries.length, FROZEN_MAX_LISTING_ENTRIES + 1,
'listing stops at 1000 entries plus the truncation marker')
t.is(counted.body.entries[FROZEN_MAX_LISTING_ENTRIES], '... (truncated)')
const wideEntries = fakeDrive({})
wideEntries.list = async function * () {
for (let i = 0; i < 2000; i++) yield { key: `dir/${'x'.repeat(1400)}${i}` }
}
const wide = await bootLegacyGateway(t, wideEntries)
const bounded = await request(wide.port, wide.path('/'))
t.is(bounded.statusCode, 200)
t.ok(bounded.raw.byteLength <= FROZEN_MAX_LISTING_BYTES,
`listing payload stays within 1 MiB (${bounded.raw.byteLength} bytes)`)
t.ok(bounded.body.entries.includes('... (truncated by byte limit)'), 'byte-bound marker is emitted')
t.absent(bounded.body.entries.includes('... (truncated)'), 'byte bound trips before the entry count')
})
test('conformance: drive operation timeout fails a hung read at the frozen 30 s', async (t) => {
const hung = fakeDrive({})
hung.entry = () => new Promise(() => {})
const { port, path } = await bootLegacyGateway(t, hung)
const startedAt = Date.now()
const res = await request(port, path('/hang.bin'))
const elapsed = Date.now() - startedAt
t.is(res.statusCode, 502, 'hung read fails closed')
t.is(res.body.error, 'Gateway read failed')
t.ok(
elapsed >= FROZEN_DRIVE_OPERATION_TIMEOUT_MS - 1000 && elapsed < 45_000,
`read gave up at the 30 s drive-op ceiling (${elapsed}ms)`
)
})
test('conformance: empty-drive wait abandons an unknown drive inside the frozen 20 s', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'hiverelay-gateway-conformance-'))
const store = new Corestore(join(root, 'store'))
const peer = new Corestore(join(root, 'peer'))
const replication = { store: null, peer: null }
t.teardown(async () => {
if (replication.store) {
try { replication.store.destroy() } catch (_) {}
try { replication.peer.destroy() } catch (_) {}
}
try { await store.close() } catch (_) {}
try { await peer.close() } catch (_) {}
await rm(root, { recursive: true, force: true })
})
await store.ready()
await peer.ready()
// A one-directional replication pipe opens the core's channel but never
// delivers an answer, so the empty drive's update pends exactly the way it
// does behind a peer that advertises but never delivers.
replication.store = store.replicate(true)
replication.peer = peer.replicate(false)
replication.store.pipe(replication.peer)
const gateway = new HyperGateway({ config: {} }, { store })
const server = http.createServer((req, res) => gateway.handle(req, res))
await new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', resolve)
})
t.teardown(async () => {
if (typeof server.closeAllConnections === 'function') {
try { server.closeAllConnections() } catch (_) {}
}
await new Promise(resolve => server.close(resolve))
await gateway.close()
})
const startedAt = Date.now()
const res = await request(server.address().port, `/v1/hyper/${newAppKey()}/index.html`)
const elapsed = Date.now() - startedAt
t.is(res.statusCode, 404, 'a drive no peer delivers never fabricates content')
t.is(res.body.error, 'Drive not available yet — still replicating')
t.ok(
elapsed >= FROZEN_EMPTY_DRIVE_WAIT_MS - 1000 && elapsed < FROZEN_DRIVE_OPERATION_TIMEOUT_MS,
`empty drive is abandoned inside the 20 s wait, below the 30 s drive-op ceiling (${elapsed}ms)`
)
})
test('conformance: incomplete headers are reaped at the frozen 10 s headersTimeout', async (t) => {
const server = await startGatewayServer(t, {})
t.is(server.server.headersTimeout, FROZEN_HEADERS_TIMEOUT_MS, 'listener pins the frozen headers timeout')
const startedAt = Date.now()
const received = await new Promise((resolve) => {
const socket = net.connect(server.server.address().port, '127.0.0.1')
const chunks = []
socket.setTimeout(30_000, () => socket.destroy())
socket.on('connect', () => socket.write('GET /health HTTP/1.1\r\nHost: 127.0.0.1\r\n'))
socket.on('data', chunk => chunks.push(chunk))
socket.on('close', () => resolve(Buffer.concat(chunks).toString('utf8')))
socket.on('error', () => {})
})
const elapsed = Date.now() - startedAt
t.ok(
elapsed >= FROZEN_HEADERS_TIMEOUT_MS - 500 && elapsed < 25_000,
`incomplete headers are reaped at 10 s (${elapsed}ms)`
)
// The gateway answers every client error with one bounded 400 + close, so
// the reaped socket must not hang open or answer with anything richer.
t.ok(received.startsWith('HTTP/1.1 400 Bad Request'), 'reaped socket gets the bounded client-error answer')
t.ok(received.includes('Connection: close'), 'reaped socket is closed')
})
// ─── TLS-edge items that need the pinned proxy runtime ─────────────────────
// Every limit above is enforced by the gateway process itself. Two frozen
// behaviors live one hop out, in the TLS-terminating proxy, and cannot be
// exercised headlessly against an in-process gateway; they keep gate-named
// skips so a relay build reports them honestly instead of claiming coverage.
// Gate G12 (proxy runtime): the default `421` vhost for unrelated TLS SNI is
// nginx configuration in the pinned production image; the spec owns its
// evidence through `nginx -T` structural inspection plus a pinned-IP TLS
// probe. The in-process halves — misdirected Host → 421 and forwarded
// SNI≠Host → 421 — are asserted above, and the simulated TLS edge in
// public-hive-gateway-live.test.js covers the same contract over a real
// handshake.
test.skip('conformance: default_server returns 421 for unrelated TLS SNI (G12 proxy runtime)', async (t) => {})
// Gate G8 (release evidence / canary observation window): the frozen 15-minute
// value and the destroy mechanism are asserted above (static tuple + scaled
// probe); holding one response open for 15 wall-clock minutes belongs to the
// canary soak, not a pre-admission suite.
test.skip('conformance: full-scale 15-minute response lifetime soak (G8 observation window)', async (t) => {})