OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-ipc / index.js

476 lines · javascript ref:
import b4a from 'b4a'
import {
  FAMILY,
  OUTER_CLASS,
  TRANSPORT_ID,
  TRANSPORT_SUPPORT
} from '@hiverelay/blind-protocol/wire-runtime-authority'
import { PRIVATE_IPC_LIMITS, PRIVATE_IPC_TIMING_MILLIS } from './policy.js'

export const LOCAL_FRAME_PREFIX_BYTES = 4
export const LOCAL_DISPATCH_BASE_HEADER_BYTES = PRIVATE_IPC_LIMITS.UNARY_BASE_HEADER_BYTES
export const LOCAL_DISPATCH_ADJACENT_HEADER_BYTES = PRIVATE_IPC_LIMITS.UNARY_ADJACENT_HEADER_BYTES
export const LOCAL_RESPONSE_HEADER_BYTES = PRIVATE_IPC_LIMITS.UNARY_RESPONSE_HEADER_BYTES
export const LOCAL_STREAM_FRAME_HEADER_BYTES = PRIVATE_IPC_LIMITS.STREAM_FRAME_HEADER_BYTES
export const MAX_LOCAL_BODY_BYTES = Math.max(...Object.values(OUTER_CLASS))
export const LOCAL_READY_PROBE_BODY_BYTES = PRIVATE_IPC_LIMITS.READY_PROBE_BODY_BYTES
export const LOCAL_READY_ACK_BODY_BYTES = PRIVATE_IPC_LIMITS.READY_ACK_BODY_BYTES
const MAX_U64 = (1n << 64n) - 1n

export const LOCAL_RESPONSE_KIND = Object.freeze({
  EXTERNAL_CANONICAL: 1,
  LOCAL_BROKER_ERROR: 2,
  LOCAL_READY_ACK: 3
})

export const LOCAL_CONTROL_KIND = Object.freeze({ EDGE_READY: 1 })

export const LOCAL_BROKER_ERROR = Object.freeze({
  MALFORMED_IPC: 1,
  UNAUTHORIZED_EDGE_PEER: 2,
  TOPOLOGY_PROFILE_ENDPOINT_MISMATCH: 3,
  CLASS_LENGTH_CAP: 4,
  DAEMON_DRAINING: 5,
  INTERNAL_IPC_FAILURE: 6
})

export const PRIVATE_IPC_STATUS = Object.freeze({
  profile: 'private-ipc-authority-v1',
  authorityArtifactPath: 'packages/blind-ipc/hiverelay-blind-private-ipc-v1.cenc',
  vectorManifestPath: 'packages/blind-ipc/vector-manifest-v1.cenc',
  authorityMetadataPath: 'packages/blind-ipc/hiverelay-blind-private-ipc-authority-v1.json',
  importedWireAbiHash: 'aaf29c8225ee33a59a02f1d27b898aa5b4f9aec005c6e509dee450ffc87b1b0d',
  privateIpcFormatHash: '02aa29d91367bbb9a6ade977017a148cd78e7c184a4234084b82498ef5cc2bed',
  privateIpcVectorSetHash: 'fa58b38f6edf4a9d0390c7af7f454150ccfe2d1398b105686ff8e0f2883893cb',
  schemaCount: 7,
  vectorCount: 79,
  requiredSchemaNames: Object.freeze([
    'LocalDispatchV1',
    'LocalUnaryResponseV1',
    'LocalStreamOpenV1',
    'LocalStreamFrameV1',
    'LocalAuthenticatedChannelV1',
    'LocalStreamAttachContextV1',
    'LocalStreamControlV1'
  ]),
  implementedSchemaNames: Object.freeze([
    'LocalDispatchV1',
    'LocalUnaryResponseV1',
    'LocalStreamOpenV1',
    'LocalStreamFrameV1',
    'LocalAuthenticatedChannelV1',
    'LocalStreamAttachContextV1',
    'LocalStreamControlV1'
  ]),
  missingSchemaNames: Object.freeze([]),
  releaseBlockers: Object.freeze([]),
  releaseReady: true
})

export function assertPrivateIpcReady () {
  if (PRIVATE_IPC_STATUS.releaseReady) return
  const error = new Error(`blind private IPC authority is incomplete; ${PRIVATE_IPC_STATUS.releaseBlockers.length} release blockers remain`)
  error.code = 'BLIND_PRIVATE_IPC_INCOMPLETE'
  error.missingSchemaNames = [...PRIVATE_IPC_STATUS.missingSchemaNames]
  error.releaseBlockers = [...PRIVATE_IPC_STATUS.releaseBlockers]
  throw error
}

export * from './private-ipc-v3-contract.js'
export * from './private-ipc-v3-status.js'
export * from './private-ipc-v4-contract.js'
export * from './private-ipc-v4-status.js'

function fail (message) {
  const error = new Error(message)
  error.code = 'BAD_LOCAL_DISPATCH'
  throw error
}

function asBuffer (value, field) {
  if (!value || typeof value.byteLength !== 'number') fail(`${field} must be bytes`)
  if (b4a.isBuffer(value)) return value
  if (ArrayBuffer.isView(value)) return b4a.from(value.buffer, value.byteOffset, value.byteLength)
  return b4a.from(value)
}

function snapshotBuffer (value, field) {
  return b4a.from(asBuffer(value, field))
}

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

function knownValue (registry, value, field) {
  if (!Number.isInteger(value) || !Object.values(registry).includes(value)) fail(`${field} is not registered`)
  return value
}

function oneHotTransportSupport (value) {
  const mask = Object.values(TRANSPORT_SUPPORT).reduce((sum, bit) => sum | bit, 0)
  if (!Number.isInteger(value) || value < 1 || value > 0xffff ||
      (value & (value - 1)) !== 0 || (value & ~mask) !== 0) {
    fail('transportSupportBit must be one explicit registered one-hot bit')
  }
  return value
}

function endpointId (value) {
  if (!Number.isInteger(value) || value < 1 || value > 0xff) fail('endpointId is outside 1..255')
  return value
}

function classForLength (length) {
  for (const [id, bytes] of Object.entries(OUTER_CLASS)) {
    if (bytes === length) return Number(id)
  }
  return null
}

function asU64 (value, field) {
  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 > ((1n << 64n) - 1n)) fail(`${field} is outside u64`)
  return value
}

function readU64BE (buffer, offset) {
  let value = 0n
  for (let index = 0; index < 8; index++) value = (value << 8n) | BigInt(buffer[offset + index])
  return value
}

function writeU64BE (buffer, value, offset) {
  for (let index = 7; index >= 0; index--) {
    buffer[offset + index] = Number(value & 0xffn)
    value >>= 8n
  }
}

export function encodeLocalReadyProbeBody (input) {
  if (!input || typeof input !== 'object') fail('ready probe must be an object')
  const edgeInstanceNonce = snapshotBuffer(input.edgeInstanceNonce, 'edgeInstanceNonce')
  const launchTopologyHash = snapshotBuffer(input.launchTopologyHash, 'launchTopologyHash')
  if (edgeInstanceNonce.byteLength !== 32) fail('edgeInstanceNonce must be exactly 32 bytes')
  if (launchTopologyHash.byteLength !== 32) fail('launchTopologyHash must be exactly 32 bytes')
  const output = b4a.alloc(LOCAL_READY_PROBE_BODY_BYTES)
  output[0] = LOCAL_CONTROL_KIND.EDGE_READY
  b4a.copy(edgeInstanceNonce, output, 1)
  b4a.copy(launchTopologyHash, output, 33)
  return output
}

export function decodeLocalReadyProbeBody (input) {
  const body = snapshotBuffer(input, 'ready probe')
  if (body.byteLength !== LOCAL_READY_PROBE_BODY_BYTES || body[0] !== LOCAL_CONTROL_KIND.EDGE_READY) {
    fail('ready probe has an invalid control shape')
  }
  return {
    controlKind: LOCAL_CONTROL_KIND.EDGE_READY,
    edgeInstanceNonce: b4a.from(body.subarray(1, 33)),
    launchTopologyHash: b4a.from(body.subarray(33, 65))
  }
}

export function encodeLocalReadyAckBody (input) {
  if (!input || typeof input !== 'object') fail('ready ACK must be an object')
  const edgeInstanceNonce = snapshotBuffer(input.edgeInstanceNonce, 'edgeInstanceNonce')
  const launchTopologyHash = snapshotBuffer(input.launchTopologyHash, 'launchTopologyHash')
  const descriptorHash = snapshotBuffer(input.descriptorHash, 'descriptorHash')
  if (edgeInstanceNonce.byteLength !== 32) fail('edgeInstanceNonce must be exactly 32 bytes')
  if (launchTopologyHash.byteLength !== 32) fail('launchTopologyHash must be exactly 32 bytes')
  if (descriptorHash.byteLength !== 32) fail('descriptorHash must be exactly 32 bytes')
  const endpoint = endpointId(input.endpointId)
  const descriptorSequence = asU64(input.descriptorSequence, 'descriptorSequence')
  const expiresMonotonicMillis = asU64(input.expiresMonotonicMillis, 'expiresMonotonicMillis')
  if (!Number.isInteger(input.readyRoleBits) || input.readyRoleBits < 0 || input.readyRoleBits > 0xffff) {
    fail('readyRoleBits is outside u16')
  }
  if (!Number.isInteger(input.readyOperationBits) || input.readyOperationBits < 0 || input.readyOperationBits > 0xffffffff) {
    fail('readyOperationBits is outside u32')
  }
  const output = b4a.alloc(LOCAL_READY_ACK_BODY_BYTES)
  output[0] = LOCAL_CONTROL_KIND.EDGE_READY
  b4a.copy(edgeInstanceNonce, output, 1)
  b4a.copy(launchTopologyHash, output, 33)
  output[65] = endpoint
  writeU64BE(output, descriptorSequence, 66)
  b4a.copy(descriptorHash, output, 74)
  output[106] = input.readyRoleBits >>> 8
  output[107] = input.readyRoleBits & 0xff
  b4a.writeUInt32BE(output, input.readyOperationBits, 108)
  writeU64BE(output, expiresMonotonicMillis, 112)
  return output
}

export function decodeLocalReadyAckBody (input) {
  const body = snapshotBuffer(input, 'ready ACK')
  if (body.byteLength !== LOCAL_READY_ACK_BODY_BYTES || body[0] !== LOCAL_CONTROL_KIND.EDGE_READY) {
    fail('ready ACK has an invalid control shape')
  }
  return {
    controlKind: LOCAL_CONTROL_KIND.EDGE_READY,
    edgeInstanceNonce: b4a.from(body.subarray(1, 33)),
    launchTopologyHash: b4a.from(body.subarray(33, 65)),
    endpointId: endpointId(body[65]),
    descriptorSequence: readU64BE(body, 66),
    descriptorHash: b4a.from(body.subarray(74, 106)),
    readyRoleBits: (body[106] << 8) | body[107],
    readyOperationBits: b4a.readUInt32BE(body, 108),
    expiresMonotonicMillis: readU64BE(body, 112)
  }
}

function declaredFrameLength (buffer, field) {
  if (buffer.byteLength < LOCAL_FRAME_PREFIX_BYTES) return null
  const totalLength = b4a.readUInt32BE(buffer, 0)
  if (totalLength > LOCAL_DISPATCH_ADJACENT_HEADER_BYTES - LOCAL_FRAME_PREFIX_BYTES + MAX_LOCAL_BODY_BYTES) {
    fail(`${field} totalLength exceeds the private IPC cap`)
  }
  return LOCAL_FRAME_PREFIX_BYTES + totalLength
}

export function localRequestFrameLength (input) {
  const buffer = asBuffer(input, 'local request')
  const frameLength = declaredFrameLength(buffer, 'local request')
  if (frameLength == null || buffer.byteLength < 28) return null
  if (buffer[4] !== 1) fail('local dispatch version must be 1')
  if (buffer[27] > 1) fail('adjacentRelayKey presence tag must be 0 or 1')
  const family = knownValue(FAMILY, buffer[5], 'family')
  const transportId = buffer[6]
  const transportSupportBit = (buffer[7] << 8) | buffer[8]
  endpointId(buffer[9])
  const outerClass = buffer[10]
  const accepted = readU64BE(buffer, 11)
  const deadline = readU64BE(buffer, 19)
  if (deadline <= accepted) fail('absoluteDeadlineMonotonicMillis must be after acceptedMonotonicMillis')
  const adjacentPresent = buffer[27] === 1
  const control = transportId === 0 || transportSupportBit === 0 || outerClass === 0
  if (control) {
    if (family !== FAMILY.DESCRIBE || transportId !== 0 || transportSupportBit !== 0 || outerClass !== 0 || adjacentPresent) {
      fail('local readiness control tuple is invalid')
    }
  } else {
    knownValue(TRANSPORT_ID, transportId, 'transportId')
    oneHotTransportSupport(transportSupportBit)
    if (!OUTER_CLASS[outerClass]) fail('outerClass is not registered')
  }
  const headerBytes = adjacentPresent ? LOCAL_DISPATCH_ADJACENT_HEADER_BYTES : LOCAL_DISPATCH_BASE_HEADER_BYTES
  if (frameLength < headerBytes) fail('local request totalLength is shorter than its header')
  if (buffer.byteLength < headerBytes) return null
  if (adjacentPresent && isZero(buffer.subarray(28, 60))) fail('adjacentRelayKey must be nonzero')
  const bodyLengthOffset = adjacentPresent ? 60 : 28
  const bodyLength = b4a.readUInt32BE(buffer, bodyLengthOffset)
  if (control) {
    if (bodyLength !== LOCAL_READY_PROBE_BODY_BYTES || deadline - accepted !== BigInt(PRIVATE_IPC_TIMING_MILLIS.READY_PROBE_ABSOLUTE)) {
      fail('local readiness control body or deadline is invalid')
    }
  } else if (!classForLength(bodyLength)) fail('local request body length is not an outer class')
  if (frameLength !== headerBytes + bodyLength) fail('local request totalLength does not match its fields')
  if (control && buffer.byteLength >= frameLength) decodeLocalReadyProbeBody(buffer.subarray(headerBytes, frameLength))
  return frameLength
}

export function encodeLocalRequest (input) {
  if (!input || typeof input !== 'object') fail('local request must be an object')
  const family = knownValue(FAMILY, input.family, 'family')
  const transportId = input.transportId
  const transportSupportBit = input.transportSupportBit
  const endpoint = endpointId(input.endpointId)
  const outerClass = Number(input.outerClass)
  const acceptedMonotonicMillis = asU64(input.acceptedMonotonicMillis, 'acceptedMonotonicMillis')
  const absoluteDeadlineMonotonicMillis = asU64(input.absoluteDeadlineMonotonicMillis, 'absoluteDeadlineMonotonicMillis')
  if (absoluteDeadlineMonotonicMillis <= acceptedMonotonicMillis) {
    fail('absoluteDeadlineMonotonicMillis must be after acceptedMonotonicMillis')
  }
  const adjacentRelayKeyValue = input.adjacentRelayKey
  const adjacentRelayKey = adjacentRelayKeyValue == null ? null : snapshotBuffer(adjacentRelayKeyValue, 'adjacentRelayKey')
  if (adjacentRelayKey && adjacentRelayKey.byteLength !== 32) fail('adjacentRelayKey must be exactly 32 bytes')
  if (adjacentRelayKey && isZero(adjacentRelayKey)) fail('adjacentRelayKey must be nonzero')
  const bodyValue = input.body
  const body = snapshotBuffer(bodyValue == null ? input.externalCanonicalBytes : bodyValue, 'externalCanonicalBytes')
  const control = transportId === 0 || transportSupportBit === 0 || outerClass === 0
  if (control) {
    if (family !== FAMILY.DESCRIBE || transportId !== 0 || transportSupportBit !== 0 || outerClass !== 0 || adjacentRelayKey) {
      fail('local readiness control tuple is invalid')
    }
    if (absoluteDeadlineMonotonicMillis - acceptedMonotonicMillis !== BigInt(PRIVATE_IPC_TIMING_MILLIS.READY_PROBE_ABSOLUTE)) {
      fail('local readiness control deadline must be exactly two seconds')
    }
    decodeLocalReadyProbeBody(body)
  } else {
    knownValue(TRANSPORT_ID, transportId, 'transportId')
    oneHotTransportSupport(transportSupportBit)
    if (!OUTER_CLASS[outerClass]) fail('outerClass is not registered')
    if (body.byteLength !== OUTER_CLASS[outerClass]) fail('body length does not match outerClass')
  }

  const headerBytes = adjacentRelayKey ? LOCAL_DISPATCH_ADJACENT_HEADER_BYTES : LOCAL_DISPATCH_BASE_HEADER_BYTES
  const output = b4a.alloc(headerBytes + body.byteLength)
  b4a.writeUInt32BE(output, output.byteLength - LOCAL_FRAME_PREFIX_BYTES, 0)
  output[4] = 1
  output[5] = family
  output[6] = transportId
  output[7] = transportSupportBit >>> 8
  output[8] = transportSupportBit & 0xff
  output[9] = endpoint
  output[10] = outerClass
  writeU64BE(output, acceptedMonotonicMillis, 11)
  writeU64BE(output, absoluteDeadlineMonotonicMillis, 19)
  output[27] = adjacentRelayKey ? 1 : 0
  let offset = 28
  if (adjacentRelayKey) {
    b4a.copy(adjacentRelayKey, output, offset)
    offset += adjacentRelayKey.byteLength
  }
  b4a.writeUInt32BE(output, body.byteLength, offset)
  b4a.copy(body, output, offset + 4)
  return output
}

export function decodeLocalRequest (input, options = {}) {
  const buffer = snapshotBuffer(input, 'local request')
  const expectedLength = localRequestFrameLength(buffer)
  if (expectedLength == null || buffer.byteLength < expectedLength) fail('local request is truncated')
  if (buffer.byteLength !== expectedLength) fail('local request length mismatch or trailing bytes')

  const family = knownValue(FAMILY, buffer[5], 'family')
  const transportId = buffer[6]
  const transportSupportBit = (buffer[7] << 8) | buffer[8]
  const endpoint = endpointId(buffer[9])
  const outerClass = buffer[10]
  const control = transportId === 0 || transportSupportBit === 0 || outerClass === 0
  if (!control) {
    knownValue(TRANSPORT_ID, transportId, 'transportId')
    oneHotTransportSupport(transportSupportBit)
    if (!OUTER_CLASS[outerClass]) fail('outerClass is not registered')
  }
  const adjacentPresent = buffer[27] === 1
  const bodyOffset = adjacentPresent ? LOCAL_DISPATCH_ADJACENT_HEADER_BYTES : LOCAL_DISPATCH_BASE_HEADER_BYTES
  const body = options.copyBody === true ? b4a.from(buffer.subarray(bodyOffset)) : buffer.subarray(bodyOffset)
  if (!control && body.byteLength !== OUTER_CLASS[outerClass]) fail('body length does not match outerClass')

  const decoded = {
    version: 1,
    family,
    transportId,
    transportSupportBit,
    endpointId: endpoint,
    outerClass,
    acceptedMonotonicMillis: readU64BE(buffer, 11),
    absoluteDeadlineMonotonicMillis: readU64BE(buffer, 19),
    adjacentRelayKey: adjacentPresent ? b4a.from(buffer.subarray(28, 60)) : null,
    externalCanonicalBytes: body
  }
  if (control) decoded.readyProbe = decodeLocalReadyProbeBody(body)
  return decoded
}

export function encodeLocalReadyProbe (input) {
  if (!input || typeof input !== 'object') fail('ready probe must be an object')
  const acceptedMonotonicMillis = asU64(input.acceptedMonotonicMillis, 'acceptedMonotonicMillis')
  if (acceptedMonotonicMillis > MAX_U64 - BigInt(PRIVATE_IPC_TIMING_MILLIS.READY_PROBE_ABSOLUTE)) {
    fail('ready probe deadline overflows u64')
  }
  return encodeLocalRequest({
    family: FAMILY.DESCRIBE,
    transportId: 0,
    transportSupportBit: 0,
    endpointId: input.endpointId,
    outerClass: 0,
    acceptedMonotonicMillis,
    absoluteDeadlineMonotonicMillis: acceptedMonotonicMillis + BigInt(PRIVATE_IPC_TIMING_MILLIS.READY_PROBE_ABSOLUTE),
    body: encodeLocalReadyProbeBody(input)
  })
}

export function localResponseFrameLength (input) {
  const buffer = asBuffer(input, 'local response')
  const frameLength = declaredFrameLength(buffer, 'local response')
  if (frameLength == null || buffer.byteLength < LOCAL_RESPONSE_HEADER_BYTES) return null
  if (buffer[4] !== 1) fail('local response version must be 1')
  const kind = buffer[5]
  const localError = buffer[6]
  const bodyLength = b4a.readUInt32BE(buffer, 7)
  if (kind === LOCAL_RESPONSE_KIND.EXTERNAL_CANONICAL) {
    if (localError !== 0 || !classForLength(bodyLength)) fail('external local response shape is invalid')
  } else if (kind === LOCAL_RESPONSE_KIND.LOCAL_BROKER_ERROR) {
    if (!Object.values(LOCAL_BROKER_ERROR).includes(localError) || bodyLength !== 0) fail('local broker error shape is invalid')
  } else if (kind === LOCAL_RESPONSE_KIND.LOCAL_READY_ACK) {
    if (localError !== 0 || bodyLength !== LOCAL_READY_ACK_BODY_BYTES) fail('local ready ACK shape is invalid')
  } else fail('local response kind is not registered')
  if (frameLength !== LOCAL_RESPONSE_HEADER_BYTES + bodyLength) fail('local response totalLength does not match its fields')
  if (kind === LOCAL_RESPONSE_KIND.LOCAL_READY_ACK && buffer.byteLength >= frameLength) {
    decodeLocalReadyAckBody(buffer.subarray(LOCAL_RESPONSE_HEADER_BYTES, frameLength))
  }
  return frameLength
}

export function encodeLocalResponse (input, options = {}) {
  let responseKind = options.responseKind == null ? LOCAL_RESPONSE_KIND.EXTERNAL_CANONICAL : options.responseKind
  let localBrokerError = options.localBrokerError == null ? 0 : options.localBrokerError
  let body = input
  if (input && typeof input === 'object' && typeof input.byteLength !== 'number' && !ArrayBuffer.isView(input)) {
    responseKind = input.responseKind
    localBrokerError = input.localBrokerError
    body = input.body == null ? input.externalCanonicalBytes : input.body
  }
  if (responseKind === LOCAL_RESPONSE_KIND.EXTERNAL_CANONICAL) {
    body = snapshotBuffer(body, 'externalCanonicalBytes')
    if (localBrokerError !== 0 || !classForLength(body.byteLength)) fail('external local response shape is invalid')
    if (options.outerClass != null && body.byteLength !== OUTER_CLASS[options.outerClass]) fail('response body does not match selected outerClass')
  } else if (responseKind === LOCAL_RESPONSE_KIND.LOCAL_BROKER_ERROR) {
    if (!Object.values(LOCAL_BROKER_ERROR).includes(localBrokerError)) fail('localBrokerError is not registered')
    body = b4a.alloc(0)
  } else if (responseKind === LOCAL_RESPONSE_KIND.LOCAL_READY_ACK) {
    body = snapshotBuffer(body, 'ready ACK')
    if (localBrokerError !== 0) fail('local ready ACK cannot carry a broker error')
    decodeLocalReadyAckBody(body)
  } else fail('local response kind is not registered')

  const output = b4a.alloc(LOCAL_RESPONSE_HEADER_BYTES + body.byteLength)
  b4a.writeUInt32BE(output, output.byteLength - LOCAL_FRAME_PREFIX_BYTES, 0)
  output[4] = 1
  output[5] = responseKind
  output[6] = localBrokerError
  b4a.writeUInt32BE(output, body.byteLength, 7)
  if (body.byteLength > 0) b4a.copy(body, output, LOCAL_RESPONSE_HEADER_BYTES)
  return output
}

export function decodeLocalResponse (input, options = {}) {
  const buffer = snapshotBuffer(input, 'local response')
  const expectedLength = localResponseFrameLength(buffer)
  if (expectedLength == null || buffer.byteLength < expectedLength) fail('local response is truncated')
  if (buffer.byteLength !== expectedLength) fail('local response length mismatch or trailing bytes')
  const view = buffer.subarray(LOCAL_RESPONSE_HEADER_BYTES)
  const body = options.copyBody === true ? b4a.from(view) : view
  const decoded = {
    version: 1,
    responseKind: buffer[5],
    localBrokerError: buffer[6],
    externalCanonicalBytes: body
  }
  if (decoded.responseKind === LOCAL_RESPONSE_KIND.LOCAL_READY_ACK) {
    decoded.readyAck = decodeLocalReadyAckBody(body)
  }
  return decoded
}

export function encodeLocalReadyAck (input) {
  return encodeLocalResponse({
    responseKind: LOCAL_RESPONSE_KIND.LOCAL_READY_ACK,
    localBrokerError: 0,
    body: encodeLocalReadyAckBody(input)
  })
}

export * from './registry.js'
export * from './policy.js'
export * from './stream.js'
export * from './framing-vectors.js'