OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-protocol / wire-v3.js

1371 lines · javascript ref:
import b4a from 'b4a'
import sodium from './crypto.js'
import { decodeCanonical, encodeCanonical } from './codec.js'
import { protocolError } from './errors.js'
import { blake2b256 } from './hashes.js'
import {
  blindErrorV1,
  blindForwardCloseV1,
  blindForwardDataV1,
  blindForwardOpenResultV1,
  blindForwardOpenV1,
  blindForwardWindowV1
} from './schemas.js'
import {
  forwardHttpsParentCapabilitySignaturePayloadV1
} from './wire-v2.js'

const MAX_U64 = (1n << 64n) - 1n
const ZERO_32 = b4a.alloc(32)
const ZERO_64 = b4a.alloc(64)
const REQUEST_MAGIC = b4a.from('HFOQ', 'ascii')
const RESULT_MAGIC = b4a.from('HFOS', 'ascii')
const CAPABILITY_MAGIC = b4a.from('HFPC', 'ascii')

export const WIRE_V3_PROTOCOL = Object.freeze({ major: 1, minor: 2, abiFormatVersion: 3 })

export const WIRE_V3_SCHEMA = Object.freeze({
  BlindForwardHttpsOriginForwardTurnRequestV1: 76,
  BlindForwardHttpsOriginForwardTurnResultV1: 77
})

export const FORWARD_HTTPS_REQUEST_ROLE_V1 = Object.freeze({
  ORIGIN_TEMPLATE: 0,
  FORWARDED: 1
})

export const FORWARD_HTTPS_REQUEST_KIND_V1 = Object.freeze({
  OPEN: 1,
  DATA: 2,
  WINDOW: 3,
  CLOSE: 4,
  POLL: 5
})

export const FORWARD_HTTPS_RESULT_ROLE_V1 = Object.freeze({
  TARGET_RESULT: 1,
  SOURCE_PRE_FORWARD_ERROR: 2,
  SOURCE_POST_FORWARD_AMBIGUOUS: 3
})

export const FORWARD_HTTPS_RESPONSE_KIND_V1 = Object.freeze({
  OPEN_ACCEPT: 1,
  ACK: 2,
  NOOP: 3,
  DATA: 4,
  WINDOW: 5,
  CLOSE: 6,
  ERROR: 7,
  AMBIGUOUS: 8
})

export const FORWARD_HTTPS_DOMAIN_V3 = Object.freeze({
  REQUEST: Object.freeze({
    domainId: 18,
    purpose: 'REQUEST_COMMITMENT',
    purposeId: 1,
    recipeId: 1,
    name: 'FORWARD_HTTPS_ORIGIN_FORWARD_TURN',
    exactAsciiBytes: 'hiverelay.blind.forward-https-origin-forward-turn-request.v1'
  }),
  TARGET_RESULT: Object.freeze({
    domainId: 113,
    purpose: 'RESULT_SIGNATURE',
    purposeId: 2,
    recipeId: 2,
    name: 'FORWARD_HTTPS_TARGET_TURN_RESULT',
    exactAsciiBytes: 'hiverelay.blind.forward-https-target-turn-result.v1'
  }),
  SOURCE_PRE_FORWARD_ERROR: Object.freeze({
    domainId: 114,
    purpose: 'RESULT_SIGNATURE',
    purposeId: 2,
    recipeId: 2,
    name: 'FORWARD_HTTPS_SOURCE_PRE_FORWARD_ERROR',
    exactAsciiBytes: 'hiverelay.blind.forward-https-source-pre-forward-error.v1'
  }),
  SOURCE_POST_FORWARD_AMBIGUOUS: Object.freeze({
    domainId: 115,
    purpose: 'RESULT_SIGNATURE',
    purposeId: 2,
    recipeId: 2,
    name: 'FORWARD_HTTPS_SOURCE_POST_FORWARD_AMBIGUOUS',
    exactAsciiBytes: 'hiverelay.blind.forward-https-source-post-forward-ambiguous.v1'
  }),
  SOURCE_TRANSFORM: Object.freeze({
    domainId: 215,
    purpose: 'AUXILIARY_SIGNATURE',
    purposeId: 3,
    recipeId: 2,
    name: 'FORWARD_HTTPS_SOURCE_TRANSFORM',
    exactAsciiBytes: 'hiverelay.blind.forward-https-source-transform.v1'
  }),
  STABLE_SESSION: Object.freeze({
    domainId: 216,
    purpose: 'HASH_DOMAIN',
    purposeId: 4,
    recipeId: 4,
    name: 'FORWARD_HTTPS_STABLE_SESSION',
    exactAsciiBytes: 'hiverelay.blind.forward-https-stable-session.v1'
  }),
  TARGET_RESULT_CHAIN: Object.freeze({
    domainId: 217,
    purpose: 'HASH_DOMAIN',
    purposeId: 4,
    recipeId: 3,
    name: 'FORWARD_HTTPS_TARGET_RESULT_CHAIN',
    exactAsciiBytes: 'hiverelay.blind.forward-https-target-result-chain.v1'
  }),
  TLS_EXPORTER_CONTEXT: Object.freeze({
    domainId: 218,
    purpose: 'HASH_DOMAIN',
    purposeId: 4,
    recipeId: 3,
    name: 'FORWARD_HTTPS_TLS_EXPORTER_CONTEXT',
    exactAsciiBytes: 'hiverelay.blind.forward-https-tls-exporter-context.v1'
  }),
  TLS_EXPORTER_BINDING: Object.freeze({
    domainId: 219,
    purpose: 'HASH_DOMAIN',
    purposeId: 4,
    recipeId: 4,
    name: 'FORWARD_HTTPS_TLS_EXPORTER_BINDING',
    exactAsciiBytes: 'hiverelay.blind.forward-https-tls-exporter-binding.v1'
  })
})

export const WIRE_V3_HASH_DOMAIN_PURPOSE = Object.freeze({ purposeId: 4, name: 'HASH_DOMAIN' })

export const WIRE_V3_HASH_RECIPES = Object.freeze([
  Object.freeze({
    recipeId: 3,
    name: 'BLAKE2B256_ASCII_DOMAIN_PAYLOAD',
    canonicalPreimage: 'ASCII(domain) || exact fixed-shape payload'
  }),
  Object.freeze({
    recipeId: 4,
    name: 'BLAKE2B256_ASCII_DOMAIN_LEN64_PAYLOAD',
    canonicalPreimage: 'ASCII(domain) || u64be(payloadLength) || payload'
  })
])

export const FORWARD_HTTPS_V3_LIMITS = Object.freeze({
  EXACT_REQUEST_BYTES: 65_536,
  EXACT_RESULT_BYTES: 65_536,
  REQUEST_HEADER_BYTES: 668,
  RESULT_HEADER_BYTES: 773,
  MAX_REQUEST_INNER_BYTES: 64_868,
  MAX_RESULT_INNER_BYTES: 64_763,
  MAX_DATA_BYTES: 64_000,
  CAPABILITY_BYTES: 390,
  CAPABILITY_IMMUTABLE_PREFIX_BYTES: 294,
  CAPABILITY_EXPORTER_OFFSET: 294,
  CAPABILITY_SIGNATURE_OFFSET: 326,
  SOURCE_TRANSFORM_UNSIGNED_BYTES: 65_472,
  RESULT_UNSIGNED_BYTES: 65_472,
  TRANSPORT_EXCHANGE_BYTES: 131_072,
  TRANSPORT_BUDGET_BYTES: 16 * 1024 * 1024
})

export const FORWARD_HTTPS_V3_RESULT_MATRIX = Object.freeze({
  [FORWARD_HTTPS_REQUEST_KIND_V1.OPEN]: Object.freeze([
    FORWARD_HTTPS_RESPONSE_KIND_V1.OPEN_ACCEPT,
    FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR
  ]),
  [FORWARD_HTTPS_REQUEST_KIND_V1.DATA]: Object.freeze([
    FORWARD_HTTPS_RESPONSE_KIND_V1.ACK,
    FORWARD_HTTPS_RESPONSE_KIND_V1.DATA,
    FORWARD_HTTPS_RESPONSE_KIND_V1.WINDOW,
    FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE,
    FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR
  ]),
  [FORWARD_HTTPS_REQUEST_KIND_V1.WINDOW]: Object.freeze([
    FORWARD_HTTPS_RESPONSE_KIND_V1.ACK,
    FORWARD_HTTPS_RESPONSE_KIND_V1.DATA,
    FORWARD_HTTPS_RESPONSE_KIND_V1.WINDOW,
    FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE,
    FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR
  ]),
  [FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE]: Object.freeze([
    FORWARD_HTTPS_RESPONSE_KIND_V1.ACK,
    FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE,
    FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR
  ]),
  [FORWARD_HTTPS_REQUEST_KIND_V1.POLL]: Object.freeze([
    FORWARD_HTTPS_RESPONSE_KIND_V1.NOOP,
    FORWARD_HTTPS_RESPONSE_KIND_V1.DATA,
    FORWARD_HTTPS_RESPONSE_KIND_V1.WINDOW,
    FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE,
    FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR
  ])
})

export const FORWARD_HTTPS_SUCCESSOR_TRANSPORT_VARIANTS_V3 = Object.freeze([
  ['OPEN', FORWARD_HTTPS_REQUEST_KIND_V1.OPEN],
  ['DATA', FORWARD_HTTPS_REQUEST_KIND_V1.DATA],
  ['WINDOW', FORWARD_HTTPS_REQUEST_KIND_V1.WINDOW],
  ['CLOSE', FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE]
].map(([operation, requestKind]) => Object.freeze({
  family: 'FORWARD',
  operation,
  routeKind: 7,
  releaseProfileId: 2,
  requestSchemaId: 76,
  resultSchemaId: 77,
  requestBytes: 65_536,
  resultBytes: 65_536,
  requestKind
})))

export const FORWARD_HTTPS_ADAPTER_LEGALITY_V3 = Object.freeze({
  requestKinds: FORWARD_HTTPS_REQUEST_KIND_V1,
  responseKinds: FORWARD_HTTPS_RESPONSE_KIND_V1,
  resultMatrix: FORWARD_HTTPS_V3_RESULT_MATRIX,
  adapterOnly: Object.freeze(['POLL', 'ACK', 'NOOP']),
  globalOperations: Object.freeze(['OPEN', 'DATA', 'WINDOW', 'CLOSE'])
})

export const WIRE_V3_SCHEMA_DECLARATIONS = Object.freeze([
  Object.freeze({
    schemaId: 76,
    schemaName: 'BlindForwardHttpsOriginForwardTurnRequestV1',
    fields: Object.freeze([
      'magic:fixed4=HFOQ', 'version:u8=1', 'routeKind:u8=7', 'releaseProfileId:u8=2',
      'requestRole:u8[ORIGIN_TEMPLATE=0|FORWARDED=1]',
      'requestKind:u8[OPEN=1|DATA=2|WINDOW=3|CLOSE=4|POLL=5]', 'flags:u8=0',
      'stableSessionId:fixed32[nonzero]', 'sequence:u64be', 'clientSessionNonce:fixed32[nonzero]',
      'requestNonce:fixed32[nonzero]', 'previousTargetResultHash:fixed32',
      'parentCapabilitySlot:fixed390[origin-zero-exporter-signature|forwarded-finalized]',
      'turnTlsExporterBindingHash:fixed32', 'originRequestCommitment:fixed32',
      'sourceTransformSignature:fixed64', 'innerLength:u32be',
      'inner:closed(requestKind){OPEN:BlindForwardOpenV1|DATA:BlindForwardDataV1[maxBytes=64000]|WINDOW:BlindForwardWindowV1|CLOSE:BlindForwardCloseV1|POLL:empty}',
      'padding:zero[65536-668-innerLength]', 'exactBytes:65536'
    ])
  }),
  Object.freeze({
    schemaId: 77,
    schemaName: 'BlindForwardHttpsOriginForwardTurnResultV1',
    fields: Object.freeze([
      'magic:fixed4=HFOS', 'version:u8=1', 'routeKind:u8=7', 'releaseProfileId:u8=2',
      'resultRole:u8[TARGET_RESULT=1|SOURCE_PRE_FORWARD_ERROR=2|SOURCE_POST_FORWARD_AMBIGUOUS=3]',
      'requestKind:u8[OPEN=1|DATA=2|WINDOW=3|CLOSE=4|POLL=5]',
      'responseKind:u8[OPEN_ACCEPT=1|ACK=2|NOOP=3|DATA=4|WINDOW=5|CLOSE=6|ERROR=7|AMBIGUOUS=8]',
      'flags:u8=0', 'stableSessionId:fixed32[nonzero]', 'sequence:u64be',
      'previousTargetResultHash:fixed32', 'originRequestCommitment:fixed32[nonzero]',
      'forwardedRequestCommitment:fixed32[nonzero]', 'finalizedParentCapability:fixed390',
      'turnTlsExporterBindingHash:fixed32[nonzero]', 'sourceTransformSignature:fixed64[nonzero]',
      'signerPublicKey:fixed32[nonzero]', 'signerDescriptorSequence:u64be[nonzero]',
      'signerDescriptorHash:fixed32[nonzero]', 'resultSignature:fixed64', 'innerLength:u32be',
      'inner:closed(resultRole,requestKind,responseKind)',
      'padding:zero[65536-773-innerLength]', 'exactBytes:65536'
    ])
  })
])

function fail (message, code = 'BAD_FORWARD_HTTPS_ORIGIN_TURN') {
  protocolError(code, message)
}

function asBytes (value, length, field, nonzero = false) {
  if (!value || typeof value.byteLength !== 'number') fail(`${field} must be bytes`)
  value = b4a.isBuffer(value)
    ? value
    : ArrayBuffer.isView(value)
      ? b4a.from(value.buffer, value.byteOffset, value.byteLength)
      : b4a.from(value)
  if (value.byteLength !== length) fail(`${field} must be exactly ${length} bytes`)
  if (nonzero && isZero(value)) fail(`${field} must be nonzero`)
  return value
}

function isZero (value) {
  for (const byte of value) if (byte !== 0) return false
  return true
}

function assertZero (value, field) {
  if (!isZero(value)) fail(`${field} must be all zero`)
  return value
}

function assertObject (value, field) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${field} must be an object`)
  return value
}

function unsigned (value, maximum, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > maximum) fail(`${field} is outside its unsigned range`)
  return value
}

function u64 (value, field, nonzero = false) {
  if (typeof value === 'number') {
    if (!Number.isSafeInteger(value) || value < 0) fail(`${field} must be an unsigned safe integer or bigint`)
    value = BigInt(value)
  }
  if (typeof value !== 'bigint' || value < 0n || value > MAX_U64 || (nonzero && value === 0n)) fail(`${field} is outside u64`)
  return value
}

function put (output, value, offset) {
  b4a.copy(value, output, offset)
  return offset + value.byteLength
}

function take (input, offset, length) {
  return b4a.from(input.subarray(offset, offset + length))
}

function writeU32 (output, value, offset, field) {
  value = unsigned(value, 0xffffffff, field)
  output[offset] = value >>> 24
  output[offset + 1] = value >>> 16
  output[offset + 2] = value >>> 8
  output[offset + 3] = value
  return offset + 4
}

function readU32 (input, offset) {
  return input[offset] * 0x1000000 + input[offset + 1] * 0x10000 + input[offset + 2] * 0x100 + input[offset + 3]
}

function writeU64 (output, value, offset, field) {
  value = u64(value, field)
  for (let i = 7; i >= 0; i--) {
    output[offset + i] = Number(value & 0xffn)
    value >>= 8n
  }
  return offset + 8
}

function readU64 (input, offset) {
  let value = 0n
  for (let i = 0; i < 8; i++) value = (value << 8n) | BigInt(input[offset + i])
  return value
}

function length64 (length) {
  const output = b4a.alloc(8)
  writeU64(output, BigInt(length), 0, 'preimage length')
  return output
}

function signaturePayload (domain, unsignedBytes) {
  return b4a.concat([b4a.from(domain.exactAsciiBytes, 'ascii'), length64(unsignedBytes.byteLength), unsignedBytes])
}

function capabilityFields (value, role) {
  assertObject(value, 'parentCapability')
  for (const forbidden of ['url', 'host', 'hostname', 'ip', 'ipAddress', 'dialAddress']) {
    if (forbidden in value) fail(`parentCapability.${forbidden} is forbidden`)
  }
  if (value.version !== 1 || value.routeKind !== 7 || value.releaseProfileId !== 2) fail('parent capability fixed header is invalid')
  const sourceRelayPublicKey = asBytes(value.sourceRelayPublicKey, 32, 'sourceRelayPublicKey', true)
  const targetRelayPublicKey = asBytes(value.targetRelayPublicKey, 32, 'targetRelayPublicKey', true)
  const routePrefixRelayPublicKey = asBytes(value.routePrefixRelayPublicKey, 32, 'routePrefixRelayPublicKey', true)
  if (b4a.equals(sourceRelayPublicKey, targetRelayPublicKey)) fail('source and target relay keys must differ')
  if (!b4a.equals(sourceRelayPublicKey, routePrefixRelayPublicKey)) fail('route prefix must contain exactly the source relay')
  for (const field of ['sourceDescriptorHash', 'targetDescriptorHash', 'targetCatalogEntryId', 'circuitNonce']) {
    asBytes(value[field], 32, field, true)
  }
  asBytes(value.routeId, 16, 'routeId', true)
  u64(value.sourceDescriptorSequence, 'sourceDescriptorSequence', true)
  u64(value.targetDescriptorSequence, 'targetDescriptorSequence', true)
  if (value.maxRelayCount !== 2 || value.remainingTransitions !== 1 || value.circuitClass !== 1 ||
      u64(value.maxCircuitBytes, 'maxCircuitBytes') !== 16n * 1024n * 1024n ||
      value.initialWindowBytes !== 65_536 || value.idleMillis !== 30_000 || value.lifetimeMillis !== 600_000) {
    fail('parent capability fixed bounds are invalid')
  }
  unsigned(value.issuedAtEpoch, 0xffffffff, 'issuedAtEpoch')
  unsigned(value.expiresAtEpoch, 0xffffffff, 'expiresAtEpoch')
  if (value.expiresAtEpoch !== value.issuedAtEpoch + 600) fail('parent capability lifetime must be exactly 600 seconds')
  const exporter = asBytes(value.tlsExporterBindingHash, 32, 'tlsExporterBindingHash')
  const signature = asBytes(value.signature, 64, 'signature')
  if (role === FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) {
    assertZero(exporter, 'origin capability exporter')
    assertZero(signature, 'origin capability signature')
  } else if (role === FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) {
    if (isZero(exporter) || isZero(signature)) fail('forwarded capability exporter and signature must be nonzero')
  } else fail('requestRole is unknown')
  return value
}

export function encodeForwardHttpsParentCapabilitySlotV1 (value, role) {
  capabilityFields(value, role)
  const output = b4a.alloc(FORWARD_HTTPS_V3_LIMITS.CAPABILITY_BYTES)
  let offset = put(output, CAPABILITY_MAGIC, 0)
  output[offset++] = 1
  output[offset++] = 7
  output[offset++] = 2
  offset = put(output, value.sourceRelayPublicKey, offset)
  offset = writeU64(output, value.sourceDescriptorSequence, offset, 'sourceDescriptorSequence')
  offset = put(output, value.sourceDescriptorHash, offset)
  offset = put(output, value.targetRelayPublicKey, offset)
  offset = writeU64(output, value.targetDescriptorSequence, offset, 'targetDescriptorSequence')
  offset = put(output, value.targetDescriptorHash, offset)
  offset = put(output, value.targetCatalogEntryId, offset)
  offset = put(output, value.routeId, offset)
  offset = put(output, value.routePrefixRelayPublicKey, offset)
  output[offset++] = value.maxRelayCount
  output[offset++] = value.remainingTransitions
  output[offset++] = value.circuitClass
  offset = writeU64(output, value.maxCircuitBytes, offset, 'maxCircuitBytes')
  offset = writeU32(output, value.initialWindowBytes, offset, 'initialWindowBytes')
  offset = writeU32(output, value.idleMillis, offset, 'idleMillis')
  offset = writeU32(output, value.lifetimeMillis, offset, 'lifetimeMillis')
  offset = writeU32(output, value.issuedAtEpoch, offset, 'issuedAtEpoch')
  offset = writeU32(output, value.expiresAtEpoch, offset, 'expiresAtEpoch')
  offset = put(output, value.circuitNonce, offset)
  offset = put(output, value.tlsExporterBindingHash, offset)
  offset = put(output, value.signature, offset)
  if (offset !== output.byteLength) fail('parent capability slot encoder length mismatch')
  return output
}

export function decodeForwardHttpsParentCapabilitySlotV1 (input, role) {
  input = asBytes(input, FORWARD_HTTPS_V3_LIMITS.CAPABILITY_BYTES, 'parent capability slot')
  if (!b4a.equals(input.subarray(0, 4), CAPABILITY_MAGIC)) fail('parent capability magic is invalid')
  let offset = 4
  const value = {
    version: input[offset++],
    routeKind: input[offset++],
    releaseProfileId: input[offset++],
    sourceRelayPublicKey: take(input, offset, 32)
  }
  offset += 32
  value.sourceDescriptorSequence = readU64(input, offset); offset += 8
  value.sourceDescriptorHash = take(input, offset, 32); offset += 32
  value.targetRelayPublicKey = take(input, offset, 32); offset += 32
  value.targetDescriptorSequence = readU64(input, offset); offset += 8
  value.targetDescriptorHash = take(input, offset, 32); offset += 32
  value.targetCatalogEntryId = take(input, offset, 32); offset += 32
  value.routeId = take(input, offset, 16); offset += 16
  value.routePrefixRelayPublicKey = take(input, offset, 32); offset += 32
  value.maxRelayCount = input[offset++]
  value.remainingTransitions = input[offset++]
  value.circuitClass = input[offset++]
  value.maxCircuitBytes = readU64(input, offset); offset += 8
  value.initialWindowBytes = readU32(input, offset); offset += 4
  value.idleMillis = readU32(input, offset); offset += 4
  value.lifetimeMillis = readU32(input, offset); offset += 4
  value.issuedAtEpoch = readU32(input, offset); offset += 4
  value.expiresAtEpoch = readU32(input, offset); offset += 4
  value.circuitNonce = take(input, offset, 32); offset += 32
  value.tlsExporterBindingHash = take(input, offset, 32); offset += 32
  value.signature = take(input, offset, 64); offset += 64
  if (offset !== input.byteLength) fail('parent capability slot decoder length mismatch')
  capabilityFields(value, role)
  if (!b4a.equals(encodeForwardHttpsParentCapabilitySlotV1(value, role), input)) fail('parent capability slot is not canonical')
  return value
}

function capabilityPrefixBytes (capability) {
  return encodeForwardHttpsParentCapabilitySlotV1(
    { ...capability, tlsExporterBindingHash: ZERO_32, signature: ZERO_64 },
    FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE
  ).subarray(0, FORWARD_HTTPS_V3_LIMITS.CAPABILITY_IMMUTABLE_PREFIX_BYTES)
}

export function forwardHttpsStableSessionIdV1 (parentCapability, clientSessionNonce) {
  const prefix = capabilityPrefixBytes(parentCapability)
  clientSessionNonce = asBytes(clientSessionNonce, 32, 'clientSessionNonce', true)
  return blake2b256(b4a.concat([
    b4a.from(FORWARD_HTTPS_DOMAIN_V3.STABLE_SESSION.exactAsciiBytes, 'ascii'),
    length64(prefix.byteLength + clientSessionNonce.byteLength),
    prefix,
    clientSessionNonce
  ]))
}

function requestCodec (requestKind) {
  switch (requestKind) {
    case FORWARD_HTTPS_REQUEST_KIND_V1.OPEN: return blindForwardOpenV1
    case FORWARD_HTTPS_REQUEST_KIND_V1.DATA: return blindForwardDataV1
    case FORWARD_HTTPS_REQUEST_KIND_V1.WINDOW: return blindForwardWindowV1
    case FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE: return blindForwardCloseV1
    case FORWARD_HTTPS_REQUEST_KIND_V1.POLL: return null
    default: fail('requestKind is unknown')
  }
}

function resultCodec (responseKind) {
  switch (responseKind) {
    case FORWARD_HTTPS_RESPONSE_KIND_V1.OPEN_ACCEPT: return blindForwardOpenResultV1
    case FORWARD_HTTPS_RESPONSE_KIND_V1.DATA: return blindForwardDataV1
    case FORWARD_HTTPS_RESPONSE_KIND_V1.WINDOW: return blindForwardWindowV1
    case FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE: return blindForwardCloseV1
    case FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR: return blindErrorV1
    case FORWARD_HTTPS_RESPONSE_KIND_V1.ACK:
    case FORWARD_HTTPS_RESPONSE_KIND_V1.NOOP:
    case FORWARD_HTTPS_RESPONSE_KIND_V1.AMBIGUOUS:
      return null
    default: fail('responseKind is unknown')
  }
}

function validateRequestInner (value) {
  const codec = requestCodec(value.requestKind)
  if (!codec) {
    if (value.inner != null) fail('POLL inner must be absent')
    return b4a.alloc(0)
  }
  assertObject(value.inner, 'request inner')
  const capability = value.parentCapability
  if (value.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.OPEN) {
    if (value.sequence !== 0n || !isZero(value.previousTargetResultHash)) fail('OPEN requires sequence zero and zero previous result hash')
    if (!b4a.equals(asBytes(value.inner.routeId, 16, 'inner.routeId'), capability.routeId) ||
        u64(value.inner.nextDescriptorSequence, 'inner.nextDescriptorSequence') !== u64(capability.targetDescriptorSequence, 'targetDescriptorSequence') ||
        !b4a.equals(asBytes(value.inner.nextDescriptorHash, 32, 'inner.nextDescriptorHash'), capability.targetDescriptorHash) ||
        value.inner.circuitClass !== capability.circuitClass ||
        !b4a.equals(asBytes(value.inner.circuitNonce, 32, 'inner.circuitNonce'), capability.circuitNonce) ||
        !isZero(asBytes(value.inner.parentRouteScopeHash, 32, 'inner.parentRouteScopeHash'))) {
      fail('OPEN inner does not bind the parent capability')
    }
  } else {
    if (value.sequence === 0n || isZero(value.previousTargetResultHash)) fail('non-OPEN request requires sequence and previous target result hash')
    if (!b4a.equals(asBytes(value.inner.circuitNonce, 32, 'inner.circuitNonce'), capability.circuitNonce)) {
      fail('request inner circuitNonce does not bind the parent capability')
    }
  }
  if (value.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.DATA &&
      (!value.inner.bytes || value.inner.bytes.byteLength > FORWARD_HTTPS_V3_LIMITS.MAX_DATA_BYTES)) {
    fail('DATA bytes exceed the direct-HTTPS 64000-byte clamp', 'TOO_LARGE')
  }
  return encodeCanonical(codec, value.inner)
}

function validateRequest (value) {
  assertObject(value, 'BlindForwardHttpsOriginForwardTurnRequestV1')
  if (value.version !== 1 || value.routeKind !== 7 || value.releaseProfileId !== 2 || value.flags !== 0) fail('request fixed header is invalid')
  if (!Object.values(FORWARD_HTTPS_REQUEST_ROLE_V1).includes(value.requestRole)) fail('requestRole is unknown')
  if (!Object.values(FORWARD_HTTPS_REQUEST_KIND_V1).includes(value.requestKind)) fail('requestKind is unknown')
  value.sequence = u64(value.sequence, 'sequence')
  value.stableSessionId = asBytes(value.stableSessionId, 32, 'stableSessionId', true)
  value.clientSessionNonce = asBytes(value.clientSessionNonce, 32, 'clientSessionNonce', true)
  value.requestNonce = asBytes(value.requestNonce, 32, 'requestNonce', true)
  value.previousTargetResultHash = asBytes(value.previousTargetResultHash, 32, 'previousTargetResultHash')
  capabilityFields(value.parentCapability, value.requestRole)
  if (!b4a.equals(value.stableSessionId, forwardHttpsStableSessionIdV1(value.parentCapability, value.clientSessionNonce))) {
    fail('stableSessionId does not bind the immutable capability prefix and client nonce')
  }
  value.turnTlsExporterBindingHash = asBytes(value.turnTlsExporterBindingHash, 32, 'turnTlsExporterBindingHash')
  value.originRequestCommitment = asBytes(value.originRequestCommitment, 32, 'originRequestCommitment')
  value.sourceTransformSignature = asBytes(value.sourceTransformSignature, 64, 'sourceTransformSignature')
  if (value.requestRole === FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) {
    assertZero(value.turnTlsExporterBindingHash, 'origin exporter mirror')
    assertZero(value.originRequestCommitment, 'origin commitment slot')
    assertZero(value.sourceTransformSignature, 'origin transform signature')
  } else {
    if (isZero(value.turnTlsExporterBindingHash) || isZero(value.originRequestCommitment) || isZero(value.sourceTransformSignature)) {
      fail('forwarded exporter, origin commitment, and transform signature must be nonzero')
    }
    if (!b4a.equals(value.turnTlsExporterBindingHash, value.parentCapability.tlsExporterBindingHash)) {
      fail('forwarded exporter mirror does not match the finalized capability')
    }
  }
  if (value.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.POLL) {
    if (value.sequence === 0n || isZero(value.previousTargetResultHash)) fail('POLL requires a prior definitive target result')
  }
  return validateRequestInner(value)
}

function encodeRequest (value) {
  const inner = validateRequest(value)
  if (inner.byteLength > FORWARD_HTTPS_V3_LIMITS.MAX_REQUEST_INNER_BYTES) fail('request inner exceeds exact-body capacity', 'TOO_LARGE')
  const output = b4a.alloc(FORWARD_HTTPS_V3_LIMITS.EXACT_REQUEST_BYTES)
  let offset = put(output, REQUEST_MAGIC, 0)
  output[offset++] = 1
  output[offset++] = 7
  output[offset++] = 2
  output[offset++] = value.requestRole
  output[offset++] = value.requestKind
  output[offset++] = 0
  offset = put(output, value.stableSessionId, offset)
  offset = writeU64(output, value.sequence, offset, 'sequence')
  offset = put(output, value.clientSessionNonce, offset)
  offset = put(output, value.requestNonce, offset)
  offset = put(output, value.previousTargetResultHash, offset)
  offset = put(output, encodeForwardHttpsParentCapabilitySlotV1(value.parentCapability, value.requestRole), offset)
  offset = put(output, value.turnTlsExporterBindingHash, offset)
  offset = put(output, value.originRequestCommitment, offset)
  offset = put(output, value.sourceTransformSignature, offset)
  offset = writeU32(output, inner.byteLength, offset, 'innerLength')
  offset = put(output, inner, offset)
  const paddingLength = output.byteLength - offset
  if (value.padding != null) assertZero(asBytes(value.padding, paddingLength, 'padding'), 'padding')
  offset += paddingLength
  if (offset !== output.byteLength) fail('request encoder length mismatch')
  return output
}

function decodeRequest (input) {
  input = asBytes(input, FORWARD_HTTPS_V3_LIMITS.EXACT_REQUEST_BYTES, 'request body')
  if (!b4a.equals(input.subarray(0, 4), REQUEST_MAGIC)) fail('request magic is invalid')
  let offset = 4
  const value = {
    version: input[offset++],
    routeKind: input[offset++],
    releaseProfileId: input[offset++],
    requestRole: input[offset++],
    requestKind: input[offset++],
    flags: input[offset++],
    stableSessionId: take(input, offset, 32)
  }
  offset += 32
  value.sequence = readU64(input, offset); offset += 8
  value.clientSessionNonce = take(input, offset, 32); offset += 32
  value.requestNonce = take(input, offset, 32); offset += 32
  value.previousTargetResultHash = take(input, offset, 32); offset += 32
  value.parentCapability = decodeForwardHttpsParentCapabilitySlotV1(input.subarray(offset, offset + 390), value.requestRole); offset += 390
  value.turnTlsExporterBindingHash = take(input, offset, 32); offset += 32
  value.originRequestCommitment = take(input, offset, 32); offset += 32
  value.sourceTransformSignature = take(input, offset, 64); offset += 64
  const innerLength = readU32(input, offset); offset += 4
  if (innerLength > FORWARD_HTTPS_V3_LIMITS.MAX_REQUEST_INNER_BYTES) fail('request innerLength exceeds exact-body capacity')
  const codec = requestCodec(value.requestKind)
  if (codec) value.inner = decodeCanonical(codec, input.subarray(offset, offset + innerLength), { copyBytes: true })
  else if (innerLength !== 0) fail('POLL innerLength must be zero')
  else value.inner = null
  offset += innerLength
  value.padding = take(input, offset, input.byteLength - offset)
  assertZero(value.padding, 'padding')
  if (!b4a.equals(encodeRequest(value), input)) fail('request body is not canonical')
  return value
}

function resultDomain (role) {
  if (role === FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT) return FORWARD_HTTPS_DOMAIN_V3.TARGET_RESULT
  if (role === FORWARD_HTTPS_RESULT_ROLE_V1.SOURCE_PRE_FORWARD_ERROR) return FORWARD_HTTPS_DOMAIN_V3.SOURCE_PRE_FORWARD_ERROR
  if (role === FORWARD_HTTPS_RESULT_ROLE_V1.SOURCE_POST_FORWARD_AMBIGUOUS) return FORWARD_HTTPS_DOMAIN_V3.SOURCE_POST_FORWARD_AMBIGUOUS
  fail('resultRole is unknown')
}

function validateResultInner (value) {
  const codec = resultCodec(value.responseKind)
  if (!codec) {
    if (value.inner != null) fail('zero-inner response must not carry inner data')
    return b4a.alloc(0)
  }
  assertObject(value.inner, 'result inner')
  if (value.responseKind === FORWARD_HTTPS_RESPONSE_KIND_V1.DATA &&
      (!value.inner.bytes || value.inner.bytes.byteLength > FORWARD_HTTPS_V3_LIMITS.MAX_DATA_BYTES)) {
    fail('result DATA bytes exceed the direct-HTTPS 64000-byte clamp', 'TOO_LARGE')
  }
  if ([
    FORWARD_HTTPS_RESPONSE_KIND_V1.DATA,
    FORWARD_HTTPS_RESPONSE_KIND_V1.WINDOW,
    FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE
  ].includes(value.responseKind) && !b4a.equals(asBytes(value.inner.circuitNonce, 32, 'result inner circuitNonce'), value.finalizedParentCapability.circuitNonce)) {
    fail('result inner circuitNonce does not bind the finalized capability')
  }
  return encodeCanonical(codec, value.inner)
}

function validateResult (value) {
  assertObject(value, 'BlindForwardHttpsOriginForwardTurnResultV1')
  if (value.version !== 1 || value.routeKind !== 7 || value.releaseProfileId !== 2 || value.flags !== 0) fail('result fixed header is invalid')
  resultDomain(value.resultRole)
  if (!Object.values(FORWARD_HTTPS_REQUEST_KIND_V1).includes(value.requestKind)) fail('result requestKind is unknown')
  if (!Object.values(FORWARD_HTTPS_RESPONSE_KIND_V1).includes(value.responseKind)) fail('responseKind is unknown')
  if (value.resultRole === FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT) {
    if (!FORWARD_HTTPS_V3_RESULT_MATRIX[value.requestKind].includes(value.responseKind)) fail('target response is illegal for requestKind')
  } else if (value.resultRole === FORWARD_HTTPS_RESULT_ROLE_V1.SOURCE_PRE_FORWARD_ERROR) {
    if (value.responseKind !== FORWARD_HTTPS_RESPONSE_KIND_V1.ERROR) fail('source pre-forward result must be ERROR')
  } else if (value.responseKind !== FORWARD_HTTPS_RESPONSE_KIND_V1.AMBIGUOUS) {
    fail('source post-forward result must be AMBIGUOUS')
  }
  value.stableSessionId = asBytes(value.stableSessionId, 32, 'stableSessionId', true)
  value.sequence = u64(value.sequence, 'sequence')
  value.previousTargetResultHash = asBytes(value.previousTargetResultHash, 32, 'previousTargetResultHash')
  if ((value.sequence === 0n) !== isZero(value.previousTargetResultHash)) fail('result previous target hash does not match sequence')
  for (const field of ['originRequestCommitment', 'forwardedRequestCommitment', 'turnTlsExporterBindingHash', 'signerPublicKey', 'signerDescriptorHash']) {
    value[field] = asBytes(value[field], 32, field, true)
  }
  value.sourceTransformSignature = asBytes(value.sourceTransformSignature, 64, 'sourceTransformSignature', true)
  value.resultSignature = asBytes(value.resultSignature, 64, 'resultSignature')
  capabilityFields(value.finalizedParentCapability, FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED)
  // A result intentionally does not repeat clientSessionNonce. Exact stable-session
  // reconstruction is enforced when the result is bound to its origin request.
  if (!b4a.equals(value.turnTlsExporterBindingHash, value.finalizedParentCapability.tlsExporterBindingHash)) {
    fail('result exporter mirror does not match finalized capability')
  }
  const sourceRole = value.resultRole !== FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT
  const expectedKey = sourceRole ? value.finalizedParentCapability.sourceRelayPublicKey : value.finalizedParentCapability.targetRelayPublicKey
  const expectedSequence = sourceRole ? value.finalizedParentCapability.sourceDescriptorSequence : value.finalizedParentCapability.targetDescriptorSequence
  const expectedHash = sourceRole ? value.finalizedParentCapability.sourceDescriptorHash : value.finalizedParentCapability.targetDescriptorHash
  if (!b4a.equals(value.signerPublicKey, expectedKey) || u64(value.signerDescriptorSequence, 'signerDescriptorSequence', true) !== expectedSequence ||
      !b4a.equals(value.signerDescriptorHash, expectedHash)) {
    fail('result signer does not match the role-selected capability descriptor')
  }
  return validateResultInner(value)
}

function encodeResult (value) {
  const inner = validateResult(value)
  if (inner.byteLength > FORWARD_HTTPS_V3_LIMITS.MAX_RESULT_INNER_BYTES) fail('result inner exceeds exact-body capacity', 'TOO_LARGE')
  const output = b4a.alloc(FORWARD_HTTPS_V3_LIMITS.EXACT_RESULT_BYTES)
  let offset = put(output, RESULT_MAGIC, 0)
  output[offset++] = 1
  output[offset++] = 7
  output[offset++] = 2
  output[offset++] = value.resultRole
  output[offset++] = value.requestKind
  output[offset++] = value.responseKind
  output[offset++] = 0
  offset = put(output, value.stableSessionId, offset)
  offset = writeU64(output, value.sequence, offset, 'sequence')
  offset = put(output, value.previousTargetResultHash, offset)
  offset = put(output, value.originRequestCommitment, offset)
  offset = put(output, value.forwardedRequestCommitment, offset)
  offset = put(output, encodeForwardHttpsParentCapabilitySlotV1(value.finalizedParentCapability, FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED), offset)
  offset = put(output, value.turnTlsExporterBindingHash, offset)
  offset = put(output, value.sourceTransformSignature, offset)
  offset = put(output, value.signerPublicKey, offset)
  offset = writeU64(output, value.signerDescriptorSequence, offset, 'signerDescriptorSequence')
  offset = put(output, value.signerDescriptorHash, offset)
  offset = put(output, value.resultSignature, offset)
  offset = writeU32(output, inner.byteLength, offset, 'innerLength')
  offset = put(output, inner, offset)
  const paddingLength = output.byteLength - offset
  if (value.padding != null) assertZero(asBytes(value.padding, paddingLength, 'padding'), 'padding')
  offset += paddingLength
  if (offset !== output.byteLength) fail('result encoder length mismatch')
  return output
}

function decodeResult (input) {
  input = asBytes(input, FORWARD_HTTPS_V3_LIMITS.EXACT_RESULT_BYTES, 'result body')
  if (!b4a.equals(input.subarray(0, 4), RESULT_MAGIC)) fail('result magic is invalid')
  let offset = 4
  const value = {
    version: input[offset++],
    routeKind: input[offset++],
    releaseProfileId: input[offset++],
    resultRole: input[offset++],
    requestKind: input[offset++],
    responseKind: input[offset++],
    flags: input[offset++],
    stableSessionId: take(input, offset, 32)
  }
  offset += 32
  value.sequence = readU64(input, offset); offset += 8
  value.previousTargetResultHash = take(input, offset, 32); offset += 32
  value.originRequestCommitment = take(input, offset, 32); offset += 32
  value.forwardedRequestCommitment = take(input, offset, 32); offset += 32
  value.finalizedParentCapability = decodeForwardHttpsParentCapabilitySlotV1(input.subarray(offset, offset + 390), FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED); offset += 390
  value.turnTlsExporterBindingHash = take(input, offset, 32); offset += 32
  value.sourceTransformSignature = take(input, offset, 64); offset += 64
  value.signerPublicKey = take(input, offset, 32); offset += 32
  value.signerDescriptorSequence = readU64(input, offset); offset += 8
  value.signerDescriptorHash = take(input, offset, 32); offset += 32
  value.resultSignature = take(input, offset, 64); offset += 64
  const innerLength = readU32(input, offset); offset += 4
  if (innerLength > FORWARD_HTTPS_V3_LIMITS.MAX_RESULT_INNER_BYTES) fail('result innerLength exceeds exact-body capacity')
  const codec = resultCodec(value.responseKind)
  if (codec) value.inner = decodeCanonical(codec, input.subarray(offset, offset + innerLength), { copyBytes: true })
  else if (innerLength !== 0) fail('zero-inner response innerLength must be zero')
  else value.inner = null
  offset += innerLength
  value.padding = take(input, offset, input.byteLength - offset)
  assertZero(value.padding, 'padding')
  if (!b4a.equals(encodeResult(value), input)) fail('result body is not canonical')
  return value
}

function fixedCodec (length, encode, decode) {
  return Object.freeze({
    preencode (state, value) {
      encode(value)
      state.end += length
    },
    encode (state, value) {
      const encoded = encode(value)
      if (state.end - state.start < length) fail('truncated output allocation')
      b4a.copy(encoded, state.buffer, state.start)
      state.start += length
    },
    decode (state) {
      if (state.end - state.start < length) fail('truncated exact-size body')
      const value = decode(state.buffer.subarray(state.start, state.start + length))
      state.start += length
      return value
    }
  })
}

export const blindForwardHttpsOriginForwardTurnRequestV1 = fixedCodec(65_536, encodeRequest, decodeRequest)
export const blindForwardHttpsOriginForwardTurnResultV1 = fixedCodec(65_536, encodeResult, decodeResult)

function exactRequestBytes (value) {
  return value && typeof value === 'object' && typeof value.byteLength !== 'number'
    ? encodeCanonical(blindForwardHttpsOriginForwardTurnRequestV1, value)
    : asBytes(value, 65_536, 'request body')
}

function exactResultBytes (value) {
  return value && typeof value === 'object' && typeof value.byteLength !== 'number'
    ? encodeCanonical(blindForwardHttpsOriginForwardTurnResultV1, value)
    : asBytes(value, 65_536, 'result body')
}

export function forwardHttpsOriginOrForwardedRequestCommitmentV1 (requestOrBytes) {
  const encoded = exactRequestBytes(requestOrBytes)
  return blake2b256(b4a.concat([b4a.from(FORWARD_HTTPS_DOMAIN_V3.REQUEST.exactAsciiBytes, 'ascii'), encoded]))
}

export function forwardHttpsOriginRequestCommitmentV1 (requestOrBytes) {
  const encoded = exactRequestBytes(requestOrBytes)
  if (encoded[7] !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) fail('origin commitment requires an origin request')
  return forwardHttpsOriginOrForwardedRequestCommitmentV1(encoded)
}

export function forwardHttpsForwardedRequestCommitmentV1 (requestOrBytes) {
  const encoded = exactRequestBytes(requestOrBytes)
  if (encoded[7] !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('forwarded commitment requires a forwarded request')
  return forwardHttpsOriginOrForwardedRequestCommitmentV1(encoded)
}

export function forwardHttpsParentCapabilityPrefixHashV1 (parentCapability) {
  return blake2b256(capabilityPrefixBytes(parentCapability))
}

export function forwardHttpsCapabilityPrefixHashV1 (originOrBytes) {
  const encoded = exactRequestBytes(originOrBytes)
  const origin = decodeRequest(encoded)
  if (origin.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) fail('capability prefix hash requires an origin request')
  const hash = forwardHttpsParentCapabilityPrefixHashV1(origin.parentCapability)
  if (!b4a.equals(hash, blake2b256(encoded.subarray(146, 440)))) fail('origin capability prefix hash is inconsistent')
  return hash
}

export function forwardHttpsSourceTransformSignaturePayloadV1 (requestOrBytes) {
  const encoded = exactRequestBytes(requestOrBytes)
  if (encoded[7] !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('source transform signature requires a forwarded request')
  const unsigned = b4a.concat([encoded.subarray(0, 600), encoded.subarray(664)])
  if (unsigned.byteLength !== FORWARD_HTTPS_V3_LIMITS.SOURCE_TRANSFORM_UNSIGNED_BYTES) fail('source transform unsigned length mismatch')
  return signaturePayload(FORWARD_HTTPS_DOMAIN_V3.SOURCE_TRANSFORM, unsigned)
}

export function forwardHttpsResultSignaturePayloadV1 (resultOrBytes) {
  const encoded = exactResultBytes(resultOrBytes)
  const role = encoded[7]
  const unsigned = b4a.concat([encoded.subarray(0, 705), encoded.subarray(769)])
  if (unsigned.byteLength !== FORWARD_HTTPS_V3_LIMITS.RESULT_UNSIGNED_BYTES) fail('result unsigned length mismatch')
  return signaturePayload(resultDomain(role), unsigned)
}

export function forwardHttpsTargetResultChainHashV1 (resultOrBytes) {
  const encoded = exactResultBytes(resultOrBytes)
  if (encoded[7] !== FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT) fail('only a TARGET_RESULT may advance the result chain')
  return blake2b256(b4a.concat([b4a.from(FORWARD_HTTPS_DOMAIN_V3.TARGET_RESULT_CHAIN.exactAsciiBytes, 'ascii'), encoded]))
}

export const FORWARD_HTTPS_TLS_EXPORTER_LABEL_V1 = 'EXPORTER-HiveRelay-Blind-Forward-Origin-v1'

export function forwardHttpsTlsExporterContextV1 (stableSessionId, sequence, originRequestCommitment) {
  return blake2b256(b4a.concat([
    b4a.from(FORWARD_HTTPS_DOMAIN_V3.TLS_EXPORTER_CONTEXT.exactAsciiBytes, 'ascii'),
    asBytes(stableSessionId, 32, 'stableSessionId', true),
    (() => {
      const encoded = b4a.alloc(8)
      writeU64(encoded, sequence, 0, 'sequence')
      return encoded
    })(),
    asBytes(originRequestCommitment, 32, 'originRequestCommitment', true)
  ]))
}

export function forwardHttpsTlsExporterBindingHashV1 (exporterSecret, context) {
  exporterSecret = asBytes(exporterSecret, 32, 'TLS exporter secret', true)
  context = asBytes(context, 32, 'TLS exporter context', true)
  return blake2b256(b4a.concat([
    b4a.from(FORWARD_HTTPS_DOMAIN_V3.TLS_EXPORTER_BINDING.exactAsciiBytes, 'ascii'),
    length64(64),
    exporterSecret,
    context
  ]))
}

export function assertForwardHttpsSourceTransformationV1 (originOrBytes, forwardedOrBytes) {
  const origin = exactRequestBytes(originOrBytes)
  const forwarded = exactRequestBytes(forwardedOrBytes)
  if (origin[7] !== 0 || forwarded[7] !== 1) fail('transformation roles must be origin then forwarded')
  const mutable = new Set([7])
  for (let i = 440; i < 664; i++) mutable.add(i)
  for (let i = 0; i < origin.byteLength; i++) {
    if (!mutable.has(i) && origin[i] !== forwarded[i]) fail(`source transform changed forbidden byte ${i}`)
  }
  const decodedOrigin = decodeRequest(origin)
  const decodedForwarded = decodeRequest(forwarded)
  if (!b4a.equals(decodedForwarded.originRequestCommitment, forwardHttpsOriginRequestCommitmentV1(origin))) {
    fail('forwarded request does not contain the exact origin commitment')
  }
  if (!b4a.equals(decodedOrigin.stableSessionId, decodedForwarded.stableSessionId) || decodedOrigin.sequence !== decodedForwarded.sequence) {
    fail('source transform changed stable session or sequence')
  }
  return true
}

export function createForwardHttpsForwardedRequestV1 (originOrBytes, finalizedCapability, tlsExporterBindingHash, sourceSecretKey) {
  const originBytes = exactRequestBytes(originOrBytes)
  const origin = decodeRequest(originBytes)
  if (origin.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) fail('source transform requires an origin request')
  finalizedCapability = { ...finalizedCapability }
  const binding = asBytes(tlsExporterBindingHash, 32, 'tlsExporterBindingHash', true)
  if (!b4a.equals(asBytes(finalizedCapability.tlsExporterBindingHash, 32, 'capability exporter'), binding)) {
    fail('finalized capability exporter does not equal selected TLS binding')
  }
  const forwarded = {
    ...origin,
    requestRole: FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED,
    parentCapability: finalizedCapability,
    turnTlsExporterBindingHash: binding,
    originRequestCommitment: forwardHttpsOriginRequestCommitmentV1(originBytes),
    sourceTransformSignature: b4a.alloc(64),
    padding: origin.padding
  }
  // The structural encoder requires a nonzero signature; use a deterministic placeholder only for payload construction.
  forwarded.sourceTransformSignature = b4a.alloc(64, 1)
  const provisional = encodeRequest(forwarded)
  const signature = b4a.alloc(64)
  sodium.crypto_sign_detached(signature, forwardHttpsSourceTransformSignaturePayloadV1(provisional), asBytes(sourceSecretKey, 64, 'sourceSecretKey', true))
  forwarded.sourceTransformSignature = signature
  const encoded = encodeRequest(forwarded)
  assertForwardHttpsSourceTransformationV1(originBytes, encoded)
  return encoded
}

export function verifyForwardHttpsParentCapabilitySignatureV1 (capability) {
  capabilityFields(capability, FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED)
  return sodium.crypto_sign_verify_detached(
    capability.signature,
    forwardHttpsParentCapabilitySignaturePayloadV1(capability),
    capability.sourceRelayPublicKey
  )
}

export function verifyForwardHttpsSourceTransformSignatureV1 (requestOrBytes) {
  const encoded = exactRequestBytes(requestOrBytes)
  const request = decodeRequest(encoded)
  if (request.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) return false
  return sodium.crypto_sign_verify_detached(
    request.sourceTransformSignature,
    forwardHttpsSourceTransformSignaturePayloadV1(encoded),
    request.parentCapability.sourceRelayPublicKey
  )
}

export function verifyForwardHttpsResultSignatureV1 (resultOrBytes) {
  const encoded = exactResultBytes(resultOrBytes)
  const result = decodeResult(encoded)
  return sodium.crypto_sign_verify_detached(
    result.resultSignature,
    forwardHttpsResultSignaturePayloadV1(encoded),
    result.signerPublicKey
  )
}

function assertForwardHttpsResultBindsForwardedRequestV1 (forwardedBytes, resultBytes, requiredRole = null) {
  const forwarded = decodeRequest(forwardedBytes)
  const result = decodeResult(resultBytes)
  if (forwarded.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('result binding requires a forwarded request')
  if (requiredRole != null && result.resultRole !== requiredRole) fail('result role is not authorized for this binding')
  if (!b4a.equals(result.stableSessionId, forwarded.stableSessionId) || result.sequence !== forwarded.sequence ||
      result.requestKind !== forwarded.requestKind || !b4a.equals(result.previousTargetResultHash, forwarded.previousTargetResultHash) ||
      !b4a.equals(result.originRequestCommitment, forwarded.originRequestCommitment) ||
      !b4a.equals(result.forwardedRequestCommitment, forwardHttpsForwardedRequestCommitmentV1(forwardedBytes)) ||
      !b4a.equals(encodeForwardHttpsParentCapabilitySlotV1(result.finalizedParentCapability, 1), encodeForwardHttpsParentCapabilitySlotV1(forwarded.parentCapability, 1)) ||
      !b4a.equals(result.turnTlsExporterBindingHash, forwarded.turnTlsExporterBindingHash) ||
      !b4a.equals(result.sourceTransformSignature, forwarded.sourceTransformSignature)) {
    fail('result provenance does not bind the exact forwarded request')
  }
  if (!verifyForwardHttpsParentCapabilitySignatureV1(forwarded.parentCapability)) fail('parent capability signature is invalid')
  if (!verifyForwardHttpsSourceTransformSignatureV1(forwardedBytes)) fail('source transform signature is invalid')
  if (!verifyForwardHttpsResultSignatureV1(resultBytes)) fail('result signature is invalid')
  return { forwarded, result }
}

export function assertForwardHttpsForwardedRequestAuthorityV1 (forwardedOrBytes) {
  const requestBytes = exactRequestBytes(forwardedOrBytes)
  const request = decodeRequest(requestBytes)
  if (request.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('forwarded authority requires role FORWARDED')
  if (!verifyForwardHttpsParentCapabilitySignatureV1(request.parentCapability)) fail('parent capability signature is invalid')
  if (!verifyForwardHttpsSourceTransformSignatureV1(requestBytes)) fail('source transform signature is invalid')
  return Object.freeze({
    request,
    requestBytes: b4a.from(requestBytes),
    forwardedRequestCommitment: forwardHttpsForwardedRequestCommitmentV1(requestBytes)
  })
}

export function assertForwardHttpsTargetResultForForwardedRequestV1 (forwardedOrBytes, resultOrBytes) {
  const requestBytes = exactRequestBytes(forwardedOrBytes)
  const resultBytes = exactResultBytes(resultOrBytes)
  const { forwarded: request, result } = assertForwardHttpsResultBindsForwardedRequestV1(
    requestBytes,
    resultBytes,
    FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT
  )
  return Object.freeze({
    request,
    result,
    requestBytes: b4a.from(requestBytes),
    resultBytes: b4a.from(resultBytes),
    forwardedRequestCommitment: forwardHttpsForwardedRequestCommitmentV1(requestBytes),
    targetResultChainHash: forwardHttpsTargetResultChainHashV1(resultBytes)
  })
}

export function assertForwardHttpsResultForRequestV1 (originOrBytes, forwardedOrBytes, resultOrBytes) {
  const originBytes = exactRequestBytes(originOrBytes)
  const forwardedBytes = exactRequestBytes(forwardedOrBytes)
  const resultBytes = exactResultBytes(resultOrBytes)
  assertForwardHttpsSourceTransformationV1(originBytes, forwardedBytes)
  const origin = decodeRequest(originBytes)
  const { result } = assertForwardHttpsResultBindsForwardedRequestV1(forwardedBytes, resultBytes)
  if (!b4a.equals(result.stableSessionId, origin.stableSessionId) || result.sequence !== origin.sequence ||
      result.requestKind !== origin.requestKind || !b4a.equals(result.previousTargetResultHash, origin.previousTargetResultHash) ||
      !b4a.equals(result.originRequestCommitment, forwardHttpsOriginRequestCommitmentV1(originBytes))) {
    fail('result provenance does not bind the exact origin and forwarded request')
  }
  return result
}

export function assertForwardHttpsResultForOriginRequestV1 (originOrBytes, resultOrBytes) {
  const originBytes = exactRequestBytes(originOrBytes)
  const resultBytes = exactResultBytes(resultOrBytes)
  const origin = decodeRequest(originBytes)
  const result = decodeResult(resultBytes)
  if (origin.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) fail('origin result assertion requires role ORIGIN_TEMPLATE')
  const forwardedBytes = encodeRequest({
    ...origin,
    requestRole: FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED,
    parentCapability: result.finalizedParentCapability,
    turnTlsExporterBindingHash: result.turnTlsExporterBindingHash,
    originRequestCommitment: result.originRequestCommitment,
    sourceTransformSignature: result.sourceTransformSignature
  })
  const verified = assertForwardHttpsResultForRequestV1(originBytes, forwardedBytes, resultBytes)
  const target = verified.resultRole === FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT
  return Object.freeze({
    origin,
    forwarded: decodeRequest(forwardedBytes),
    result: verified,
    originBytes: b4a.from(originBytes),
    forwardedBytes: b4a.from(forwardedBytes),
    resultBytes: b4a.from(resultBytes),
    targetResultChainHash: target ? forwardHttpsTargetResultChainHashV1(resultBytes) : null,
    normalClose: target && origin.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE,
    targetFin: target && verified.responseKind === FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE
  })
}

export class ForwardHttpsTransportBudgetV1 {
  #limit
  #reserved

  constructor (limit = FORWARD_HTTPS_V3_LIMITS.TRANSPORT_BUDGET_BYTES) {
    this.#limit = unsigned(limit, Number.MAX_SAFE_INTEGER, 'transport budget limit')
    this.#reserved = 0
  }

  get limit () { return this.#limit }
  get reserved () { return this.#reserved }

  reserveExchange () {
    const next = this.#reserved + FORWARD_HTTPS_V3_LIMITS.TRANSPORT_EXCHANGE_BYTES
    if (next > this.#limit) fail('independent transport budget exhausted', 'FORWARD_HTTPS_BUDGET_EXHAUSTED')
    this.#reserved = next
    return this.#reserved
  }
}

export class ForwardHttpsDefinitiveResultCacheV1 {
  #relayRole
  #budget
  #records
  #terminal
  #terminalReason
  #lastNowEpoch

  constructor (relayRole, budget = new ForwardHttpsTransportBudgetV1()) {
    if (relayRole !== 'SOURCE' && relayRole !== 'TARGET') fail('cache relayRole must be SOURCE or TARGET')
    this.#relayRole = relayRole
    this.#budget = budget
    this.#records = new Map()
    this.#terminal = false
    this.#terminalReason = null
    this.#lastNowEpoch = 0
  }

  get relayRole () { return this.#relayRole }
  get budget () { return this.#budget }
  get terminal () { return this.#terminal }
  get terminalReason () { return this.#terminalReason }
  get recordCount () { return this.#records.size }

  persist (requestOrBytes, resultOrBytes) {
    this.#assertActive()
    const candidate = this.#candidate(requestOrBytes)
    const resultBytes = exactResultBytes(resultOrBytes)
    const existing = this.#records.get(candidate.key)
    let verified
    try {
      verified = this.#relayRole === 'SOURCE'
        ? assertForwardHttpsResultForOriginRequestV1(candidate.requestBytes, resultBytes)
        : assertForwardHttpsTargetResultForForwardedRequestV1(candidate.requestBytes, resultBytes)
      if (verified.result.resultRole !== FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT) fail('definitive cache accepts only TARGET_RESULT')
    } catch (error) {
      if (existing) return this.#terminate('definitive result cache remint or request conflict')
      throw error
    }
    const expiresAtEpoch = unsigned(verified.result.finalizedParentCapability.expiresAtEpoch, 0xffffffff, 'expiresAtEpoch')
    if (existing) {
      if (!b4a.equals(existing.requestBytes, candidate.requestBytes) ||
          !b4a.equals(existing.resultBytes, resultBytes) || existing.expiresAtEpoch !== expiresAtEpoch) {
        return this.#terminate('definitive result cache remint or request conflict')
      }
      return b4a.from(existing.resultBytes)
    }
    this.#records.set(candidate.key, {
      requestBytes: b4a.from(candidate.requestBytes),
      resultBytes: b4a.from(resultBytes),
      expiresAtEpoch
    })
    return b4a.from(resultBytes)
  }

  lookup (requestOrBytes, nowEpoch) {
    this.#assertActive()
    const candidate = this.#candidate(requestOrBytes)
    nowEpoch = unsigned(nowEpoch, 0xffffffff, 'nowEpoch')
    const record = this.#records.get(candidate.key)
    if (!record || !b4a.equals(record.requestBytes, candidate.requestBytes)) return this.#terminate('definitive result cache authentication failed')
    if (nowEpoch > this.#lastNowEpoch) this.#lastNowEpoch = nowEpoch
    if (this.#lastNowEpoch > record.expiresAtEpoch + 900) {
      return this.#terminate('definitive target result recovery grace expired', 'FORWARD_HTTPS_RECOVERY_GRACE_EXPIRED')
    }
    try {
      this.#budget.reserveExchange()
    } catch (error) {
      if (error && error.code === 'FORWARD_HTTPS_BUDGET_EXHAUSTED') {
        this.#terminal = true
        this.#terminalReason = error.code
      }
      throw error
    }
    return b4a.from(record.resultBytes)
  }

  inspectRecord (requestOrBytes) {
    const candidate = this.#candidate(requestOrBytes)
    const record = this.#records.get(candidate.key)
    if (!record || !b4a.equals(record.requestBytes, candidate.requestBytes)) return null
    return Object.freeze({
      requestBytes: b4a.from(record.requestBytes),
      resultBytes: b4a.from(record.resultBytes),
      expiresAtEpoch: record.expiresAtEpoch
    })
  }

  #candidate (requestOrBytes) {
    const requestBytes = exactRequestBytes(requestOrBytes)
    const request = decodeRequest(requestBytes)
    const expectedRole = this.#relayRole === 'SOURCE'
      ? FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE
      : FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED
    if (request.requestRole !== expectedRole) fail(`definitive ${this.#relayRole} cache request role is invalid`)
    if (this.#relayRole === 'TARGET') assertForwardHttpsForwardedRequestAuthorityV1(requestBytes)
    return {
      requestBytes,
      request,
      key: `${this.#relayRole}:${b4a.toString(request.stableSessionId, 'hex')}:${request.sequence}`
    }
  }

  #assertActive () {
    if (this.#terminal) fail('definitive result cache is terminal', this.#terminalReason)
  }

  #terminate (message, reason = 'TERMINAL_FORWARD_HTTPS_CACHE') {
    if (!this.#terminal) {
      this.#terminal = true
      this.#terminalReason = reason
    }
    fail(message, this.#terminalReason)
  }
}

export class ForwardHttpsOriginSessionContractV1 {
  #stableSessionId
  #nextSequence
  #previousTargetResultHash
  #outstanding
  #completed
  #terminal
  #terminalReason
  #closed
  #targetFin
  #budget
  #lastNowEpoch

  constructor (stableSessionId, options = {}) {
    this.#stableSessionId = b4a.from(asBytes(stableSessionId, 32, 'stableSessionId', true))
    this.#nextSequence = 0n
    this.#previousTargetResultHash = b4a.alloc(32)
    this.#outstanding = null
    this.#completed = new Map()
    this.#terminal = false
    this.#terminalReason = null
    this.#closed = false
    this.#targetFin = false
    this.#budget = options.budget || new ForwardHttpsTransportBudgetV1()
    this.#lastNowEpoch = 0
  }

  get stableSessionId () { return b4a.from(this.#stableSessionId) }
  get nextSequence () { return this.#nextSequence }
  get previousTargetResultHash () { return b4a.from(this.#previousTargetResultHash) }
  get terminal () { return this.#terminal }
  get terminalReason () { return this.#terminalReason }
  get closed () { return this.#closed }
  get targetFin () { return this.#targetFin }
  get budget () { return this.#budget }
  get completedCount () { return this.#completed.size }
  get outstanding () {
    if (!this.#outstanding) return null
    return Object.freeze({
      originBytes: b4a.from(this.#outstanding.originBytes),
      forwardedBytes: this.#outstanding.forwardedBytes && b4a.from(this.#outstanding.forwardedBytes),
      request: decodeRequest(this.#outstanding.originBytes)
    })
  }

  acceptOrigin (originOrBytes, options = {}) {
    this.#assertActive()
    const bytes = exactRequestBytes(originOrBytes)
    const request = decodeRequest(bytes)
    if (request.requestRole !== 0 || !b4a.equals(request.stableSessionId, this.#stableSessionId)) return this.#terminate('origin session binding is invalid')
    const cached = this.#completed.get(request.sequence.toString())
    if (cached) {
      if (b4a.equals(cached.originBytes, bytes)) {
        const nowEpoch = options.nowEpoch == null ? cached.expiresAtEpoch : unsigned(options.nowEpoch, 0xffffffff, 'nowEpoch')
        if (nowEpoch > this.#lastNowEpoch) this.#lastNowEpoch = nowEpoch
        if (this.#lastNowEpoch > cached.expiresAtEpoch + 900) return this.#terminate('definitive target result recovery grace expired', 'FORWARD_HTTPS_RECOVERY_GRACE_EXPIRED')
        this.#reserveExchange()
        return Object.freeze({ disposition: 'CACHED_TARGET_RESULT', resultBytes: b4a.from(cached.resultBytes), request })
      }
      return this.#terminate('changed bytes replay a completed sequence')
    }
    if (this.#closed) return this.#terminate('session is closed')
    const nowEpoch = options.nowEpoch == null ? request.parentCapability.issuedAtEpoch : unsigned(options.nowEpoch, 0xffffffff, 'nowEpoch')
    if (nowEpoch > this.#lastNowEpoch) this.#lastNowEpoch = nowEpoch
    if (this.#outstanding) {
      if (b4a.equals(this.#outstanding.originBytes, bytes)) {
        this.#reserveExchange()
        return Object.freeze({ disposition: 'EXACT_RETRY', request })
      }
      return this.#terminate('changed bytes reuse the outstanding sequence')
    }
    if (this.#lastNowEpoch > request.parentCapability.expiresAtEpoch) return this.#terminate('live parent capability expired before new dispatch')
    if (request.sequence !== this.#nextSequence || !b4a.equals(request.previousTargetResultHash, this.#previousTargetResultHash)) {
      return this.#terminate('origin sequence or target result chain is invalid')
    }
    if (request.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.OPEN && request.sequence !== 0n) return this.#terminate('OPEN may occur only once at sequence zero')
    if (request.requestKind !== FORWARD_HTTPS_REQUEST_KIND_V1.OPEN && request.sequence === 0n) return this.#terminate('sequence zero must be OPEN')
    this.#reserveExchange()
    this.#outstanding = { originBytes: b4a.from(bytes), forwardedBytes: null, request }
    return Object.freeze({ disposition: 'ACCEPTED', request })
  }

  recordForwarded (forwardedOrBytes) {
    this.#assertActive()
    if (this.#closed) return this.#terminate('session is closed')
    if (!this.#outstanding) return this.#terminate('forwarded request has no outstanding origin')
    const forwardedBytes = exactRequestBytes(forwardedOrBytes)
    try {
      assertForwardHttpsSourceTransformationV1(this.#outstanding.originBytes, forwardedBytes)
      assertForwardHttpsForwardedRequestAuthorityV1(forwardedBytes)
    } catch {
      return this.#terminate('forwarded request authority or transformation is invalid')
    }
    if (this.#outstanding.forwardedBytes && !b4a.equals(this.#outstanding.forwardedBytes, forwardedBytes)) {
      return this.#terminate('retry attempted to remint forwarded bytes')
    }
    this.#outstanding.forwardedBytes = b4a.from(forwardedBytes)
    return Object.freeze({ disposition: 'PREPARED', forwardedBytes: b4a.from(forwardedBytes) })
  }

  complete (resultOrBytes) {
    this.#assertActive()
    if (this.#closed) return this.#terminate('session is closed')
    if (!this.#outstanding || !this.#outstanding.forwardedBytes) return this.#terminate('result has no prepared forwarded request')
    const resultBytes = exactResultBytes(resultOrBytes)
    let result
    try {
      result = assertForwardHttpsResultForRequestV1(
        this.#outstanding.originBytes,
        this.#outstanding.forwardedBytes,
        resultBytes
      )
    } catch {
      return this.#terminate('result provenance is invalid')
    }
    if (result.resultRole !== FORWARD_HTTPS_RESULT_ROLE_V1.TARGET_RESULT) {
      return Object.freeze({ disposition: result.resultRole === 2 ? 'NONDEFINITIVE_PRE_FORWARD_ERROR' : 'NONDEFINITIVE_POST_FORWARD_AMBIGUOUS', result })
    }
    this.#completed.set(result.sequence.toString(), {
      originBytes: b4a.from(this.#outstanding.originBytes),
      resultBytes: b4a.from(resultBytes),
      expiresAtEpoch: this.#outstanding.request.parentCapability.expiresAtEpoch
    })
    this.#previousTargetResultHash = forwardHttpsTargetResultChainHashV1(resultBytes)
    this.#nextSequence++
    const requestKind = this.#outstanding.request.requestKind
    this.#outstanding = null
    if (result.responseKind === FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE) this.#targetFin = true
    if (requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE) this.#closed = true
    return Object.freeze({ disposition: 'DEFINITIVE_TARGET_RESULT', result })
  }

  inspectCompleted (sequence) {
    const record = this.#completed.get(u64(sequence, 'sequence').toString())
    if (!record) return null
    return Object.freeze({
      originBytes: b4a.from(record.originBytes),
      resultBytes: b4a.from(record.resultBytes),
      expiresAtEpoch: record.expiresAtEpoch
    })
  }

  #assertActive () {
    if (this.#terminal) fail('session is terminal', this.#terminalReason)
  }

  #terminate (message, reason = 'TERMINAL_FORWARD_HTTPS_SESSION') {
    if (!this.#terminal) {
      this.#terminal = true
      this.#terminalReason = reason
    }
    fail(message, this.#terminalReason)
  }

  #reserveExchange () {
    try {
      return this.#budget.reserveExchange()
    } catch (error) {
      if (error && error.code === 'FORWARD_HTTPS_BUDGET_EXHAUSTED') {
        this.#terminal = true
        this.#terminalReason = error.code
      }
      throw error
    }
  }
}

export function assertForwardHttpsV2UnselectableV3 (schemaId) {
  if (schemaId === 74 || schemaId === 75) fail('WIRE v2 IDs 74 and 75 are frozen and unselectable', 'WIRE_V2_FORWARD_UNSELECTABLE')
  if (schemaId !== 76 && schemaId !== 77) fail('schema is not a WIRE v3 direct-HTTPS successor')
  return true
}