import b4a from 'b4a'
import c from 'compact-encoding'
import {
FORWARD_HTTPS_REQUEST_KIND_V1,
FORWARD_HTTPS_REQUEST_ROLE_V1,
FORWARD_HTTPS_RESPONSE_KIND_V1,
assertForwardHttpsForwardedRequestAuthorityV1,
assertForwardHttpsTargetResultForForwardedRequestV1,
blindForwardHttpsOriginForwardTurnRequestV1,
blindForwardHttpsOriginForwardTurnResultV1,
forwardHttpsForwardedRequestCommitmentV1,
forwardHttpsOriginRequestCommitmentV1,
forwardHttpsTargetResultChainHashV1
} from '@hiverelay/blind-protocol'
import { decodeCanonical } from '@hiverelay/blind-protocol/codec'
import { privateBlake2b256 } from './private-hashes.js'
const MAX_U64 = (1n << 64n) - 1n
const ZERO_32 = b4a.alloc(32)
const ORIGIN_AUTHORITY_MAGIC = b4a.from('LFA4', 'ascii')
const TURN_MAGIC = b4a.from('LFT4', 'ascii')
const TARGET_INGRESS_MAGIC = b4a.from('LTI4', 'ascii')
const FORMAT_DOMAIN = b4a.from('hiverelay.blind.private-ipc-format-hash.v4', 'ascii')
const VECTOR_DOMAIN = b4a.from('hiverelay.blind.private-ipc-vector-set-hash.v4', 'ascii')
const SOURCE_EXCHANGE_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-exchange.v4', 'ascii')
const TARGET_EXCHANGE_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-target-exchange.v4', 'ascii')
const SOURCE_REPLAY_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-replay.v4', 'ascii')
const TARGET_REPLAY_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-target-replay.v4', 'ascii')
const TARGET_TLS_CONTEXT_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-target-tls-context.v4', 'ascii')
const TARGET_TLS_BINDING_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-target-tls-binding.v4', 'ascii')
const JOURNAL_SNAPSHOT_DOMAIN = b4a.from('hiverelay.blind.private-ipc-forward-journal-model.v4', 'ascii')
export const PRIVATE_IPC_V4_MAGIC = 'hiverelay-blind-private-ipc-v4'
export const PRIVATE_IPC_V4_FORMAT_VERSION = 4
export const FORWARD_HTTPS_TARGET_TLS_EXPORTER_LABEL_V4 = 'EXPORTER-HiveRelay-Blind-Forward-Target-v1'
export const PRIVATE_IPC_V4_SCHEMA = Object.freeze({
LocalForwardHttpsOriginAuthorityV4: 16,
LocalForwardHttpsTurnV4: 17,
LocalForwardHttpsTargetIngressV4: 18
})
export const LOCAL_FORWARD_HTTPS_DIRECTION_V4 = Object.freeze({
ORIGIN_REQUEST: 1,
RESULT: 2
})
export const PRIVATE_IPC_V4_LIMITS = Object.freeze({
ORIGIN_AUTHORITY_BYTES: 292,
TURN_HEADER_BYTES: 148,
TURN_BODY_BYTES: 65_536,
TURN_BYTES: 65_684,
TARGET_AUTHORITY_BYTES: 292,
TARGET_INGRESS_BYTES: 65_828,
SOURCE_ORIGIN_TRANSCRIPT_BYTES: 65_976,
TARGET_INGRESS_TRANSCRIPT_BYTES: 65_828,
RESULT_TRANSCRIPT_BYTES: 65_684,
TARGET_LOCAL_REQUEST_PLUS_RESULT_BYTES: 131_512,
EXCHANGE_PREIMAGE_BYTES: 260,
REPLAY_PAYLOAD_BYTES: 232,
REPLAY_CAPACITY: 4096,
MAX_DEADLINE_MILLIS: 15_000
})
const targetDeclarationTail = Object.freeze([
'targetTlsExporterLabel:ASCII(EXPORTER-HiveRelay-Blind-Forward-Target-v1)',
'targetTlsExporterContext:BLAKE2b256(ASCII(hiverelay.blind.private-ipc-forward-target-tls-context.v4)||stableSessionId32||u64be(sequence)||forwardedRequestCommitment32)',
'targetTlsExporterBindingHash:BLAKE2b256(ASCII(hiverelay.blind.private-ipc-forward-target-tls-binding.v4)||u64be(64)||targetTlsExporterSecret32||targetTlsExporterContext32)',
'targetLocalExchangeId:BLAKE2b256(ASCII(hiverelay.blind.private-ipc-forward-target-exchange.v4)||u64be(260)||bytes[0:260])',
'body:exact canonical WIRE ID76 requestRole FORWARDED; commitment/session/sequence match header; no origin/outer-envelope/native-stream/fallback form',
'raw target TLS exporter, source address, URL, host, IP, cookies, authorization, credentials, and app metadata are unrepresentable'
])
export const PRIVATE_IPC_V4_ADDITIONAL_SCHEMAS = Object.freeze([
Object.freeze({
schemaId: 16,
schemaName: 'LocalForwardHttpsOriginAuthorityV4',
fields: Object.freeze([
'0:4 magic=LFA4', '4 version=4', '5 authorityKind=1[TLS_EXPORTER_BINDING_HASH_BY_PEERCRED_EDGE]', '6 transportId=1[HTTPS_DIRECT]', '7 endpointId:1..255',
'8:10 flags=0', '10:12 reserved=0', '12:44 wireV3AbiHash', '44:76 signedLaunchTopologyHash',
'76:108 edgeProcessNonce', '108:140 localChannelNonce', '140:172 tlsExporterBindingHash',
'172:204 originRequestCommitment', '204:236 stableSessionId', '236:244 sequence:u64be',
'244:252 acceptedMonotonicMillis:u64be', '252:260 absoluteDeadlineMonotonicMillis:u64be', '260:292 localExchangeId'
])
}),
Object.freeze({
schemaId: 17,
schemaName: 'LocalForwardHttpsTurnV4',
fields: Object.freeze([
'0:4 magic=LFT4', '4 version=4', '5 direction[ORIGIN_REQUEST=1|RESULT=2]', '6 wireRole[ORIGIN_REQUEST:ORIGIN_TEMPLATE=0|RESULT:TARGET_RESULT=1|SOURCE_PRE_FORWARD_ERROR=2|SOURCE_POST_FORWARD_AMBIGUOUS=3]', '7 flags=0', '8:40 wireV3AbiHash',
'40:72 localExchangeId', '72:104 originRequestCommitment', '104:136 stableSessionId', '136:144 sequence:u64be',
'144:148 bodyLength=65536', '148:65684 canonical WIRE v3 body[ORIGIN_REQUEST:ID76 role ORIGIN_TEMPLATE|RESULT:ID77 resultRole equals wireRole]'
])
}),
Object.freeze({
schemaId: 18,
schemaName: 'LocalForwardHttpsTargetIngressV4',
fields: Object.freeze([
'0:4 magic=LTI4', '4 version=4', '5 authorityKind=2[TARGET_TLS_EXPORTER_BINDING_HASH_BY_PEERCRED_EDGE]', '6 transportId=1[HTTPS_DIRECT]', '7 endpointId:1..255',
'8:10 flags=0', '10:12 reserved=0', '12:44 wireV3AbiHash[nonzero]', '44:76 signedLaunchTopologyHash[nonzero]',
'76:108 edgeProcessNonce[nonzero]', '108:140 localChannelNonce[nonzero;single-use]', '140:172 targetTlsExporterBindingHash[nonzero]',
'172:204 forwardedRequestCommitment[nonzero]', '204:236 stableSessionId[nonzero]', '236:244 sequence:u64be',
'244:252 acceptedMonotonicMillis:u64be', '252:260 absoluteDeadlineMonotonicMillis:u64be', '260:292 targetLocalExchangeId[nonzero]',
'292:65828 exact canonical WIRE ID76 role FORWARDED body',
...targetDeclarationTail
])
})
])
function fail (message, code = 'BAD_PRIVATE_IPC_V4_CONTRACT') {
const error = new Error(message)
error.code = code
throw error
}
function bytes (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 u64 (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 > MAX_U64) fail(`${field} is outside u64`)
return value
}
function endpointId (value) {
if (!Number.isInteger(value) || value < 1 || value > 255) fail('endpointId is outside 1..255')
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 writeU16 (output, value, offset, field) {
if (!Number.isInteger(value) || value < 0 || value > 0xffff) fail(`${field} is outside u16`)
output[offset] = value >>> 8
output[offset + 1] = value
return offset + 2
}
function readU16 (input, offset) {
return input[offset] * 0x100 + input[offset + 1]
}
function writeU32 (output, value, offset, field) {
if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) fail(`${field} is outside u32`)
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, 'hash input length')
return output
}
function domainLengthHash (domain, input) {
input = b4a.from(input)
return privateBlake2b256(b4a.concat([domain, length64(input.byteLength), input]))
}
function assertNoAuthorityLeaks (value, boundary) {
for (const forbidden of [
'tlsExporter', 'targetTlsExporterSecret', 'sourceAddress', 'remoteAddress', 'url', 'host', 'hostname', 'ip',
'ipAddress', 'dialAddress', 'cookies', 'authorization', 'credentials', 'appMetadata'
]) {
if (forbidden in value) fail(`${boundary}.${forbidden} is forbidden`)
}
}
function validateDeadline (value) {
const accepted = u64(value.acceptedMonotonicMillis, 'acceptedMonotonicMillis')
const deadline = u64(value.absoluteDeadlineMonotonicMillis, 'absoluteDeadlineMonotonicMillis')
if (deadline <= accepted || deadline - accepted > BigInt(PRIVATE_IPC_V4_LIMITS.MAX_DEADLINE_MILLIS)) {
fail('absolute deadline must be after acceptance and at most 15000ms later')
}
return { accepted, deadline }
}
function writeAuthorityPrefix (output, value, magic, authorityKind, commitmentField, bindingField) {
assertNoAuthorityLeaks(value, magic === ORIGIN_AUTHORITY_MAGIC ? 'originAuthority' : 'targetIngress')
if (value.version !== 4 || value.authorityKind !== authorityKind || value.transportId !== 1 || value.flags !== 0) {
fail('local authority fixed header is invalid')
}
endpointId(value.endpointId)
const { accepted, deadline } = validateDeadline(value)
let offset = put(output, magic, 0)
output[offset++] = 4
output[offset++] = authorityKind
output[offset++] = 1
output[offset++] = value.endpointId
offset = writeU16(output, 0, offset, 'flags')
offset = writeU16(output, 0, offset, 'reserved')
for (const field of [
'wireV3AbiHash', 'signedLaunchTopologyHash', 'edgeProcessNonce', 'localChannelNonce', bindingField,
commitmentField, 'stableSessionId'
]) offset = put(output, bytes(value[field], 32, field, true), offset)
offset = writeU64(output, value.sequence, offset, 'sequence')
offset = writeU64(output, accepted, offset, 'acceptedMonotonicMillis')
offset = writeU64(output, deadline, offset, 'absoluteDeadlineMonotonicMillis')
if (offset !== PRIVATE_IPC_V4_LIMITS.EXCHANGE_PREIMAGE_BYTES) fail('local authority prefix length mismatch')
return offset
}
function decodeAuthorityPrefix (input, magic, authorityKind, commitmentField, bindingField) {
if (!b4a.equals(input.subarray(0, 4), magic)) fail('local authority magic is invalid')
let offset = 4
const value = {
version: input[offset++],
authorityKind: input[offset++],
transportId: input[offset++],
endpointId: input[offset++],
flags: readU16(input, offset)
}
offset += 2
const reserved = readU16(input, offset); offset += 2
if (reserved !== 0) fail('local authority reserved bytes must be zero')
value.wireV3AbiHash = take(input, offset, 32); offset += 32
value.signedLaunchTopologyHash = take(input, offset, 32); offset += 32
value.edgeProcessNonce = take(input, offset, 32); offset += 32
value.localChannelNonce = take(input, offset, 32); offset += 32
value[bindingField] = take(input, offset, 32); offset += 32
value[commitmentField] = take(input, offset, 32); offset += 32
value.stableSessionId = take(input, offset, 32); offset += 32
value.sequence = readU64(input, offset); offset += 8
value.acceptedMonotonicMillis = readU64(input, offset); offset += 8
value.absoluteDeadlineMonotonicMillis = readU64(input, offset); offset += 8
if (offset !== PRIVATE_IPC_V4_LIMITS.EXCHANGE_PREIMAGE_BYTES) fail('local authority prefix decode length mismatch')
return value
}
export function localForwardHttpsExchangeIdV4 (originAuthorityOrPrefix) {
const prefix = originAuthorityOrPrefix && originAuthorityOrPrefix.byteLength != null
? bytes(originAuthorityOrPrefix, 260, 'origin authority prefix')
: (() => {
const output = b4a.alloc(260)
writeAuthorityPrefix(output, originAuthorityOrPrefix, ORIGIN_AUTHORITY_MAGIC, 1, 'originRequestCommitment', 'tlsExporterBindingHash')
return output
})()
return domainLengthHash(SOURCE_EXCHANGE_DOMAIN, prefix)
}
export function targetLocalForwardHttpsExchangeIdV4 (targetIngressOrPrefix) {
let prefix
if (targetIngressOrPrefix && targetIngressOrPrefix.byteLength != null) {
const input = b4a.from(targetIngressOrPrefix)
if (input.byteLength !== 260 && input.byteLength !== PRIVATE_IPC_V4_LIMITS.TARGET_INGRESS_BYTES) {
fail('target ingress or prefix must be exactly 260 or 65828 bytes')
}
prefix = input.subarray(0, 260)
} else {
prefix = b4a.alloc(260)
writeAuthorityPrefix(prefix, targetIngressOrPrefix, TARGET_INGRESS_MAGIC, 2, 'forwardedRequestCommitment', 'targetTlsExporterBindingHash')
}
return domainLengthHash(TARGET_EXCHANGE_DOMAIN, prefix)
}
export function createLocalForwardHttpsOriginAuthorityV4 (value) {
const output = { ...value, localExchangeId: ZERO_32 }
output.localExchangeId = localForwardHttpsExchangeIdV4(output)
return output
}
export function encodeLocalForwardHttpsOriginAuthorityV4 (value) {
const output = b4a.alloc(PRIVATE_IPC_V4_LIMITS.ORIGIN_AUTHORITY_BYTES)
let offset = writeAuthorityPrefix(output, value, ORIGIN_AUTHORITY_MAGIC, 1, 'originRequestCommitment', 'tlsExporterBindingHash')
const expected = localForwardHttpsExchangeIdV4(output.subarray(0, 260))
const actual = bytes(value.localExchangeId, 32, 'localExchangeId', true)
if (!b4a.equals(actual, expected)) fail('localExchangeId does not bind exact ID16 bytes 0:260')
offset = put(output, actual, offset)
if (offset !== output.byteLength) fail('origin authority encoder length mismatch')
return output
}
export function decodeLocalForwardHttpsOriginAuthorityV4 (input) {
input = bytes(input, PRIVATE_IPC_V4_LIMITS.ORIGIN_AUTHORITY_BYTES, 'origin authority')
const value = decodeAuthorityPrefix(input, ORIGIN_AUTHORITY_MAGIC, 1, 'originRequestCommitment', 'tlsExporterBindingHash')
value.localExchangeId = take(input, 260, 32)
if (!b4a.equals(encodeLocalForwardHttpsOriginAuthorityV4(value), input)) fail('origin authority is not canonical')
return value
}
function decodeWireRequest (body) {
return decodeCanonical(blindForwardHttpsOriginForwardTurnRequestV1, body, { copyBytes: true })
}
function decodeWireResult (body) {
return decodeCanonical(blindForwardHttpsOriginForwardTurnResultV1, body, { copyBytes: true })
}
export function encodeLocalForwardHttpsTurnV4 (value) {
if (!value || typeof value !== 'object' || value.version !== 4 || value.flags !== 0) fail('local turn fixed header is invalid')
const wireV3AbiHash = bytes(value.wireV3AbiHash, 32, 'wireV3AbiHash', true)
const localExchangeId = bytes(value.localExchangeId, 32, 'localExchangeId', true)
const originRequestCommitment = bytes(value.originRequestCommitment, 32, 'originRequestCommitment', true)
const stableSessionId = bytes(value.stableSessionId, 32, 'stableSessionId', true)
const sequence = u64(value.sequence, 'sequence')
const body = bytes(value.body, PRIVATE_IPC_V4_LIMITS.TURN_BODY_BYTES, 'local turn body')
let decoded
if (value.direction === LOCAL_FORWARD_HTTPS_DIRECTION_V4.ORIGIN_REQUEST) {
if (value.wireRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE) fail('origin request turn wireRole must be ORIGIN_TEMPLATE')
decoded = decodeWireRequest(body)
if (decoded.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE ||
!b4a.equals(forwardHttpsOriginRequestCommitmentV1(body), originRequestCommitment)) {
fail('origin turn body role or commitment is invalid')
}
} else if (value.direction === LOCAL_FORWARD_HTTPS_DIRECTION_V4.RESULT) {
if (![1, 2, 3].includes(value.wireRole)) fail('result turn wireRole is invalid')
decoded = decodeWireResult(body)
if (decoded.resultRole !== value.wireRole || !b4a.equals(decoded.originRequestCommitment, originRequestCommitment)) {
fail('result turn body role or commitment is invalid')
}
} else fail('local turn direction is invalid')
if (!b4a.equals(decoded.stableSessionId, stableSessionId) || decoded.sequence !== sequence) {
fail('local turn sessionId or sequence does not match body')
}
const output = b4a.alloc(PRIVATE_IPC_V4_LIMITS.TURN_BYTES)
let offset = put(output, TURN_MAGIC, 0)
output[offset++] = 4
output[offset++] = value.direction
output[offset++] = value.wireRole
output[offset++] = 0
offset = put(output, wireV3AbiHash, offset)
offset = put(output, localExchangeId, offset)
offset = put(output, originRequestCommitment, offset)
offset = put(output, stableSessionId, offset)
offset = writeU64(output, sequence, offset, 'sequence')
offset = writeU32(output, body.byteLength, offset, 'bodyLength')
offset = put(output, body, offset)
if (offset !== output.byteLength) fail('local turn encoder length mismatch')
return output
}
export function decodeLocalForwardHttpsTurnV4 (input) {
input = bytes(input, PRIVATE_IPC_V4_LIMITS.TURN_BYTES, 'local turn')
if (!b4a.equals(input.subarray(0, 4), TURN_MAGIC) || input[4] !== 4 || input[7] !== 0) {
fail('local turn fixed header is invalid')
}
const value = {
version: 4,
direction: input[5],
wireRole: input[6],
flags: 0,
wireV3AbiHash: take(input, 8, 32),
localExchangeId: take(input, 40, 32),
originRequestCommitment: take(input, 72, 32),
stableSessionId: take(input, 104, 32),
sequence: readU64(input, 136),
body: take(input, 148, 65_536)
}
if (readU32(input, 144) !== 65_536) fail('local turn bodyLength must be exactly 65536')
if (!b4a.equals(encodeLocalForwardHttpsTurnV4(value), input)) fail('local turn is not canonical')
return value
}
export function forwardHttpsTargetTlsExporterContextV4 (stableSessionId, sequence, forwardedRequestCommitment) {
const payload = b4a.concat([
bytes(stableSessionId, 32, 'stableSessionId', true),
(() => {
const output = b4a.alloc(8)
writeU64(output, sequence, 0, 'sequence')
return output
})(),
bytes(forwardedRequestCommitment, 32, 'forwardedRequestCommitment', true)
])
if (payload.byteLength !== 72) fail('target TLS exporter context payload length mismatch')
return privateBlake2b256(b4a.concat([TARGET_TLS_CONTEXT_DOMAIN, payload]))
}
export function forwardHttpsTargetTlsExporterBindingHashV4 (targetTlsExporterSecret, context) {
const secret = b4a.from(bytes(targetTlsExporterSecret, 32, 'target TLS exporter secret', true))
context = bytes(context, 32, 'target TLS exporter context', true)
try {
return privateBlake2b256(b4a.concat([TARGET_TLS_BINDING_DOMAIN, length64(64), secret, context]))
} finally {
secret.fill(0)
}
}
export function createLocalForwardHttpsTargetIngressV4 (value) {
const body = bytes(value.body, 65_536, 'target ingress body')
const request = decodeWireRequest(body)
if (request.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('target ingress requires role FORWARDED ID76')
const output = {
...value,
version: 4,
authorityKind: 2,
transportId: 1,
flags: 0,
forwardedRequestCommitment: forwardHttpsForwardedRequestCommitmentV1(body),
stableSessionId: request.stableSessionId,
sequence: request.sequence,
targetLocalExchangeId: ZERO_32,
body
}
output.targetLocalExchangeId = targetLocalForwardHttpsExchangeIdV4(output)
return output
}
export function encodeLocalForwardHttpsTargetIngressV4 (value) {
const body = bytes(value.body, 65_536, 'target ingress body')
const request = decodeWireRequest(body)
if (request.requestRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.FORWARDED) fail('target ingress requires role FORWARDED ID76')
const commitment = forwardHttpsForwardedRequestCommitmentV1(body)
if (!b4a.equals(commitment, bytes(value.forwardedRequestCommitment, 32, 'forwardedRequestCommitment', true)) ||
!b4a.equals(request.stableSessionId, bytes(value.stableSessionId, 32, 'stableSessionId', true)) ||
request.sequence !== u64(value.sequence, 'sequence')) {
fail('target ingress commitment, session, or sequence does not match body')
}
const output = b4a.alloc(PRIVATE_IPC_V4_LIMITS.TARGET_INGRESS_BYTES)
let offset = writeAuthorityPrefix(output, value, TARGET_INGRESS_MAGIC, 2, 'forwardedRequestCommitment', 'targetTlsExporterBindingHash')
const expected = targetLocalForwardHttpsExchangeIdV4(output.subarray(0, 260))
const actual = bytes(value.targetLocalExchangeId, 32, 'targetLocalExchangeId', true)
if (!b4a.equals(actual, expected)) fail('targetLocalExchangeId does not bind exact ID18 bytes 0:260')
offset = put(output, actual, offset)
offset = put(output, body, offset)
if (offset !== output.byteLength) fail('target ingress encoder length mismatch')
return output
}
export function decodeLocalForwardHttpsTargetIngressV4 (input) {
input = bytes(input, PRIVATE_IPC_V4_LIMITS.TARGET_INGRESS_BYTES, 'target ingress')
const value = decodeAuthorityPrefix(input, TARGET_INGRESS_MAGIC, 2, 'forwardedRequestCommitment', 'targetTlsExporterBindingHash')
value.targetLocalExchangeId = take(input, 260, 32)
value.body = take(input, 292, 65_536)
if (!b4a.equals(encodeLocalForwardHttpsTargetIngressV4(value), input)) fail('target ingress is not canonical')
return value
}
export function decodeLocalForwardHttpsSourceOriginTranscriptV4 (input, options = {}) {
input = bytes(input, PRIVATE_IPC_V4_LIMITS.SOURCE_ORIGIN_TRANSCRIPT_BYTES, 'source-origin transcript')
if (options.eof !== true) fail('source-origin transcript requires daemon-observed EOF before response')
const authority = decodeLocalForwardHttpsOriginAuthorityV4(input.subarray(0, 292))
const turn = decodeLocalForwardHttpsTurnV4(input.subarray(292))
if (turn.direction !== LOCAL_FORWARD_HTTPS_DIRECTION_V4.ORIGIN_REQUEST ||
turn.wireRole !== FORWARD_HTTPS_REQUEST_ROLE_V1.ORIGIN_TEMPLATE ||
!b4a.equals(turn.wireV3AbiHash, authority.wireV3AbiHash) ||
!b4a.equals(turn.localExchangeId, authority.localExchangeId) ||
!b4a.equals(turn.originRequestCommitment, authority.originRequestCommitment) ||
!b4a.equals(turn.stableSessionId, authority.stableSessionId) || turn.sequence !== authority.sequence) {
fail('source-origin ID16 and ID17 records do not bind one exact exchange')
}
return Object.freeze({ authority, turn })
}
export function encodeLocalForwardHttpsSourceOriginTranscriptV4 (authority, turn) {
const output = b4a.concat([
encodeLocalForwardHttpsOriginAuthorityV4(authority),
encodeLocalForwardHttpsTurnV4(turn)
])
decodeLocalForwardHttpsSourceOriginTranscriptV4(output, { eof: true })
return output
}
export function decodeLocalForwardHttpsTargetIngressTranscriptV4 (input, options = {}) {
if (options.eof !== true) fail('target-ingress transcript requires daemon-observed EOF before response')
return decodeLocalForwardHttpsTargetIngressV4(
bytes(input, PRIVATE_IPC_V4_LIMITS.TARGET_INGRESS_TRANSCRIPT_BYTES, 'target-ingress transcript')
)
}
export function assertLocalForwardHttpsResultTranscriptV4 (input, authority, requiredRole = null) {
const turn = decodeLocalForwardHttpsTurnV4(bytes(input, PRIVATE_IPC_V4_LIMITS.RESULT_TRANSCRIPT_BYTES, 'result transcript'))
const exchangeId = authority.targetLocalExchangeId || authority.localExchangeId
const commitment = authority.originRequestCommitment || decodeWireRequest(authority.body).originRequestCommitment
if (turn.direction !== LOCAL_FORWARD_HTTPS_DIRECTION_V4.RESULT ||
(requiredRole != null && turn.wireRole !== requiredRole) ||
!b4a.equals(turn.wireV3AbiHash, authority.wireV3AbiHash) ||
!b4a.equals(turn.localExchangeId, exchangeId) ||
!b4a.equals(turn.originRequestCommitment, commitment) ||
!b4a.equals(turn.stableSessionId, authority.stableSessionId) || turn.sequence !== authority.sequence) {
fail('result transcript does not bind the authenticated local exchange')
}
return turn
}
export function assertLocalForwardHttpsSocketSeparationV4 (identities) {
if (!identities || typeof identities !== 'object') fail('socket identities must be an object')
const fields = ['sourceOrigin', 'targetIngress', 'genericUnary', 'nativeV2Stream']
const values = fields.map(field => {
const value = identities[field]
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint') fail(`${field} socket identity is invalid`)
return String(value)
})
if (new Set(values).size !== values.length) fail('private IPC v4 socket identities must not alias')
return true
}
export class LocalForwardHttpsTranscriptAccumulatorV4 {
#role
#limit
#chunks
#length
#ended
#validated
constructor (role) {
if (role !== 'SOURCE_ORIGIN' && role !== 'TARGET_INGRESS') fail('transcript accumulator role is invalid')
this.#role = role
this.#limit = role === 'SOURCE_ORIGIN'
? PRIVATE_IPC_V4_LIMITS.SOURCE_ORIGIN_TRANSCRIPT_BYTES
: PRIVATE_IPC_V4_LIMITS.TARGET_INGRESS_TRANSCRIPT_BYTES
this.#chunks = []
this.#length = 0
this.#ended = false
this.#validated = false
Object.defineProperty(this, 'responseAllowed', {
enumerable: true,
configurable: false,
get: () => this.#validated
})
Object.freeze(this)
}
write (chunk) {
if (this.#ended) fail('transcript cannot accept bytes after EOF')
if (!chunk || typeof chunk.byteLength !== 'number') fail('transcript chunk must be bytes')
chunk = b4a.from(chunk)
if (this.#length + chunk.byteLength > this.#limit) fail('transcript exceeds its exact byte limit')
this.#chunks.push(b4a.from(chunk))
this.#length += chunk.byteLength
return this.#length
}
end () {
if (this.#ended) fail('transcript EOF may occur only once')
this.#ended = true
if (this.#length !== this.#limit) fail('transcript EOF arrived before its exact byte limit')
const input = b4a.concat(this.#chunks, this.#length)
const transcript = this.#role === 'SOURCE_ORIGIN'
? decodeLocalForwardHttpsSourceOriginTranscriptV4(input, { eof: true })
: decodeLocalForwardHttpsTargetIngressTranscriptV4(input, { eof: true })
this.#validated = true
return transcript
}
}
function replayPayload (value, commitmentField, exchangeField) {
const output = b4a.alloc(PRIVATE_IPC_V4_LIMITS.REPLAY_PAYLOAD_BYTES)
let offset = 0
for (const field of [
'wireV3AbiHash', 'signedLaunchTopologyHash', 'edgeProcessNonce', 'localChannelNonce', commitmentField, 'stableSessionId'
]) offset = put(output, bytes(value[field], 32, field, true), offset)
offset = writeU64(output, value.sequence, offset, 'sequence')
offset = put(output, bytes(value[exchangeField], 32, exchangeField, true), offset)
if (offset !== output.byteLength) fail('private IPC v4 replay payload length mismatch')
return output
}
export function localForwardHttpsSourceReplayTupleV4 (authority) {
const encoded = encodeLocalForwardHttpsOriginAuthorityV4(authority)
const decoded = decodeLocalForwardHttpsOriginAuthorityV4(encoded)
const recomputed = localForwardHttpsExchangeIdV4(encoded.subarray(0, 260))
if (!b4a.equals(recomputed, decoded.localExchangeId)) fail('source replay requires recomputed localExchangeId')
return domainLengthHash(SOURCE_REPLAY_DOMAIN, replayPayload(decoded, 'originRequestCommitment', 'localExchangeId'))
}
export function localForwardHttpsTargetReplayTupleV4 (ingress) {
const encoded = encodeLocalForwardHttpsTargetIngressV4(ingress)
const decoded = decodeLocalForwardHttpsTargetIngressV4(encoded)
const recomputed = targetLocalForwardHttpsExchangeIdV4(encoded.subarray(0, 260))
if (!b4a.equals(recomputed, decoded.targetLocalExchangeId)) fail('target replay requires recomputed targetLocalExchangeId')
return domainLengthHash(TARGET_REPLAY_DOMAIN, replayPayload(decoded, 'forwardedRequestCommitment', 'targetLocalExchangeId'))
}
export class LocalForwardHttpsReplayJournalModelV4 {
#namespace
#capacity
#entries
#lastMonotonicMillis
#quarantined
constructor (namespace, options = {}) {
if (namespace !== 'SOURCE_ORIGIN' && namespace !== 'TARGET_INGRESS') fail('journal namespace must be SOURCE_ORIGIN or TARGET_INGRESS')
this.#namespace = namespace
this.#capacity = options.capacity == null ? PRIVATE_IPC_V4_LIMITS.REPLAY_CAPACITY : options.capacity
if (!Number.isInteger(this.#capacity) || this.#capacity < 1 || this.#capacity > 4096) fail('journal capacity is outside 1..4096')
this.#entries = new Map()
this.#lastMonotonicMillis = 0n
this.#quarantined = false
}
get namespace () { return this.#namespace }
get capacity () { return this.#capacity }
get lastMonotonicMillis () { return this.#lastMonotonicMillis }
get quarantined () { return this.#quarantined }
get size () { return this.#entries.size }
reserve (tuple, acceptedMonotonicMillis, deadlineMonotonicMillis, nowMonotonicMillis) {
this.#assertActive()
tuple = bytes(tuple, 32, 'replay tuple', true)
const accepted = u64(acceptedMonotonicMillis, 'acceptedMonotonicMillis')
const deadline = u64(deadlineMonotonicMillis, 'deadlineMonotonicMillis')
const now = u64(nowMonotonicMillis, 'nowMonotonicMillis')
if (now < this.#lastMonotonicMillis) {
this.#quarantined = true
fail('journal model monotonic clock regressed', 'PRIVATE_IPC_V4_CLOCK_REGRESSION')
}
if (accepted > now || now > deadline || deadline <= accepted || deadline - accepted > 15_000n) {
fail('journal replay record is outside its bounded deadline')
}
const entries = new Map()
for (const [entryKey, entry] of this.#entries) {
if (entry.state !== 'CONSUMED' || now <= entry.deadline) entries.set(entryKey, { ...entry })
}
const key = b4a.toString(tuple, 'hex')
if (entries.has(key)) fail('private IPC v4 replay tuple already exists', 'PRIVATE_IPC_V4_REPLAY')
if (entries.size >= this.#capacity) fail('private IPC v4 replay journal capacity exhausted', 'PRIVATE_IPC_V4_JOURNAL_CAPACITY')
entries.set(key, { accepted, deadline, state: 'PENDING' })
this.#entries = entries
this.#lastMonotonicMillis = now
return key
}
commit (key) {
this.#assertActive()
const entry = this.#entries.get(String(key))
if (!entry || entry.state !== 'PENDING') fail('journal model has no pending reservation')
this.#entries.set(String(key), { accepted: entry.accepted, deadline: entry.deadline, state: 'CONSUMED' })
return true
}
crashSnapshot () {
const records = [...this.#entries].map(([key, value]) => ({
key,
accepted: value.accepted.toString(),
deadline: value.deadline.toString(),
state: value.state
})).sort((left, right) => left.key.localeCompare(right.key))
const payload = b4a.from(JSON.stringify({
version: 1,
namespace: this.#namespace,
capacity: this.#capacity,
lastMonotonicMillis: this.#lastMonotonicMillis.toString(),
quarantined: this.#quarantined,
records
}), 'utf8')
return Object.freeze({ payload: b4a.from(payload), checksum: domainLengthHash(JOURNAL_SNAPSHOT_DOMAIN, payload) })
}
inspectEntries () {
return Object.freeze([...this.#entries].map(([key, entry]) => Object.freeze({
key,
accepted: entry.accepted,
deadline: entry.deadline,
state: entry.state
})).sort((left, right) => left.key.localeCompare(right.key)))
}
static restart (snapshot, options = {}) {
const expectedNamespace = options.expectedNamespace
const expectedCapacity = options.expectedCapacity
const now = u64(options.nowMonotonicMillis, 'nowMonotonicMillis')
const clean = new LocalForwardHttpsReplayJournalModelV4(expectedNamespace, { capacity: expectedCapacity })
const quarantine = () => {
const model = new LocalForwardHttpsReplayJournalModelV4(expectedNamespace, { capacity: expectedCapacity })
model.#lastMonotonicMillis = now
model.#quarantined = true
return model
}
try {
if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot) ||
Object.keys(snapshot).length !== 2 || !Object.prototype.hasOwnProperty.call(snapshot, 'payload') ||
!Object.prototype.hasOwnProperty.call(snapshot, 'checksum')) return quarantine()
const payload = bytes(snapshot.payload, snapshot.payload.byteLength, 'journal snapshot payload')
const checksum = bytes(snapshot.checksum, 32, 'journal checksum')
if (!b4a.equals(domainLengthHash(JOURNAL_SNAPSHOT_DOMAIN, payload), checksum)) return quarantine()
const parsed = JSON.parse(b4a.toString(payload, 'utf8'))
const keys = ['version', 'namespace', 'capacity', 'lastMonotonicMillis', 'quarantined', 'records']
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) ||
Object.keys(parsed).length !== keys.length || Object.keys(parsed).some((key, index) => key !== keys[index]) ||
!b4a.equals(b4a.from(JSON.stringify(parsed), 'utf8'), payload)) return quarantine()
if (parsed.version !== 1 || parsed.namespace !== expectedNamespace || parsed.capacity !== expectedCapacity ||
typeof parsed.quarantined !== 'boolean' || !Array.isArray(parsed.records) || parsed.records.length > expectedCapacity) return quarantine()
const decimalU64 = (value, field) => {
if (typeof value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(value)) fail(`${field} is not canonical u64 text`)
return u64(BigInt(value), field)
}
const last = decimalU64(parsed.lastMonotonicMillis, 'lastMonotonicMillis')
if (last > now || parsed.quarantined) return quarantine()
const entries = new Map()
let previousKey = null
for (const record of parsed.records) {
const recordKeys = ['key', 'accepted', 'deadline', 'state']
if (!record || typeof record !== 'object' || Array.isArray(record) ||
Object.keys(record).length !== recordKeys.length || Object.keys(record).some((key, index) => key !== recordKeys[index]) ||
typeof record.key !== 'string' || !/^[0-9a-f]{64}$/.test(record.key) ||
(previousKey != null && previousKey.localeCompare(record.key) >= 0) ||
(record.state !== 'PENDING' && record.state !== 'CONSUMED')) return quarantine()
const accepted = decimalU64(record.accepted, 'accepted')
const deadline = decimalU64(record.deadline, 'deadline')
if (accepted > last || accepted > now || deadline <= accepted || deadline - accepted > 15_000n || record.state === 'PENDING') return quarantine()
if (now <= deadline) entries.set(record.key, { accepted, deadline, state: 'CONSUMED' })
previousKey = record.key
}
clean.#entries = entries
clean.#lastMonotonicMillis = now
return clean
} catch {
return quarantine()
}
}
#assertActive () {
if (this.#quarantined) fail('journal model is quarantined', 'PRIVATE_IPC_V4_JOURNAL_QUARANTINED')
}
}
export class LocalForwardHttpsTargetClaimModelV4 {
#stableSessionId
#claims
#outstandingKey
#nextSequence
#previousTargetResultHash
#terminal
#terminalReason
#closed
#targetFin
constructor () {
this.#stableSessionId = null
this.#claims = new Map()
this.#outstandingKey = null
this.#nextSequence = 0n
this.#previousTargetResultHash = b4a.alloc(32)
this.#terminal = false
this.#terminalReason = null
this.#closed = false
this.#targetFin = false
}
get stableSessionId () { return this.#stableSessionId && 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 claimCount () { return this.#claims.size }
claim (forwardedRequestBytes) {
this.#assertActive()
forwardedRequestBytes = b4a.from(bytes(forwardedRequestBytes, 65_536, 'forwarded request'))
let request
try {
request = assertForwardHttpsForwardedRequestAuthorityV1(forwardedRequestBytes).request
} catch {
return this.#terminate('target claim forwarded authority is invalid')
}
const key = `${b4a.toString(request.stableSessionId, 'hex')}:${request.sequence}`
const commitment = forwardHttpsForwardedRequestCommitmentV1(forwardedRequestBytes)
const existing = this.#claims.get(key)
if (existing) {
if (!b4a.equals(existing.requestBytes, forwardedRequestBytes) || !b4a.equals(existing.commitment, commitment)) {
return this.#terminate('changed forwarded bytes reuse a target claim')
}
return Object.freeze({ disposition: 'EXACT_RETRY', resultBytes: existing.resultBytes && b4a.from(existing.resultBytes) })
}
if (this.#closed) return this.#terminate('target session is normally closed')
if (this.#stableSessionId == null) {
if (request.requestKind !== FORWARD_HTTPS_REQUEST_KIND_V1.OPEN || request.sequence !== 0n) return this.#terminate('target session must begin with OPEN at sequence zero')
this.#stableSessionId = b4a.from(request.stableSessionId)
} else if (!b4a.equals(request.stableSessionId, this.#stableSessionId)) {
return this.#terminate('target claim model cannot bind a second stable session')
}
if (this.#outstandingKey != null) return this.#terminate('target session already has an outstanding request')
if (request.sequence !== this.#nextSequence || !b4a.equals(request.previousTargetResultHash, this.#previousTargetResultHash)) {
return this.#terminate('target sequence or previous result chain is invalid')
}
if (request.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.OPEN && request.sequence !== 0n) return this.#terminate('target OPEN may occur only once')
if (request.requestKind !== FORWARD_HTTPS_REQUEST_KIND_V1.OPEN && request.sequence === 0n) return this.#terminate('target sequence zero must be OPEN')
this.#claims.set(key, { requestBytes: b4a.from(forwardedRequestBytes), commitment: b4a.from(commitment), resultBytes: null })
this.#outstandingKey = key
return Object.freeze({ disposition: 'CLAIMED', resultBytes: null })
}
persistResult (forwardedRequestBytes, resultBytes) {
this.#assertActive()
forwardedRequestBytes = b4a.from(bytes(forwardedRequestBytes, 65_536, 'forwarded request'))
resultBytes = b4a.from(bytes(resultBytes, 65_536, 'target result'))
let authority
try {
authority = assertForwardHttpsTargetResultForForwardedRequestV1(forwardedRequestBytes, resultBytes)
} catch {
return this.#terminate('target result does not bind the exact authenticated claim')
}
const { request, result } = authority
const key = `${b4a.toString(request.stableSessionId, 'hex')}:${request.sequence}`
const claim = this.#claims.get(key)
if (!claim || !b4a.equals(claim.requestBytes, forwardedRequestBytes)) return this.#terminate('target result has no exact request claim')
if (claim.resultBytes) {
if (!b4a.equals(claim.resultBytes, resultBytes)) return this.#terminate('target claim cannot remint a definitive result')
return b4a.from(claim.resultBytes)
}
if (this.#outstandingKey !== key) return this.#terminate('target result is not for the outstanding request')
claim.resultBytes = b4a.from(resultBytes)
this.#previousTargetResultHash = forwardHttpsTargetResultChainHashV1(resultBytes)
this.#nextSequence++
this.#outstandingKey = null
if (result.responseKind === FORWARD_HTTPS_RESPONSE_KIND_V1.CLOSE) this.#targetFin = true
if (request.requestKind === FORWARD_HTTPS_REQUEST_KIND_V1.CLOSE) this.#closed = true
return b4a.from(claim.resultBytes)
}
inspectClaim (forwardedRequestBytes) {
forwardedRequestBytes = bytes(forwardedRequestBytes, 65_536, 'forwarded request')
const request = decodeWireRequest(forwardedRequestBytes)
const key = `${b4a.toString(request.stableSessionId, 'hex')}:${request.sequence}`
const claim = this.#claims.get(key)
if (!claim || !b4a.equals(claim.requestBytes, forwardedRequestBytes)) return null
return Object.freeze({
requestBytes: b4a.from(claim.requestBytes),
commitment: b4a.from(claim.commitment),
resultBytes: claim.resultBytes && b4a.from(claim.resultBytes)
})
}
#assertActive () {
if (this.#terminal) fail('target claim model is terminal', this.#terminalReason)
}
#terminate (message) {
if (!this.#terminal) {
this.#terminal = true
this.#terminalReason = 'TERMINAL_FORWARD_HTTPS_TARGET_CLAIM'
}
fail(message, this.#terminalReason)
}
}
function list (encoding) {
return {
preencode (state, values) {
c.uint.preencode(state, values.length)
for (const value of values) encoding.preencode(state, value)
},
encode (state, values) {
c.uint.encode(state, values.length)
for (const value of values) encoding.encode(state, value)
},
decode (state) {
const length = c.uint.decode(state)
const values = []
for (let i = 0; i < length; i++) values.push(encoding.decode(state))
return values
}
}
}
const schemaEncoding = {
preencode (state, value) {
c.uint.preencode(state, value.schemaId)
c.string.preencode(state, value.schemaName)
c.buffer.preencode(state, value.canonicalDeclarationBytes)
},
encode (state, value) {
c.uint.encode(state, value.schemaId)
c.string.encode(state, value.schemaName)
c.buffer.encode(state, value.canonicalDeclarationBytes)
},
decode (state) {
return {
schemaId: c.uint.decode(state),
schemaName: c.string.decode(state),
canonicalDeclarationBytes: b4a.from(c.buffer.decode(state))
}
}
}
const schemasEncoding = list(schemaEncoding)
export const privateIpcV4RegistryEncoding = {
preencode (state, value) {
c.string.preencode(state, value.magic)
c.uint.preencode(state, value.formatVersion)
c.buffer.preencode(state, value.importedWireV3AbiHash)
c.buffer.preencode(state, value.basePrivateIpcV3FormatHash)
c.uint.preencode(state, value.baseSchemaCount)
schemasEncoding.preencode(state, value.additionalSchemas)
c.uint.preencode(state, value.forwardReadinessOperationBits)
},
encode (state, value) {
c.string.encode(state, value.magic)
c.uint.encode(state, value.formatVersion)
c.buffer.encode(state, value.importedWireV3AbiHash)
c.buffer.encode(state, value.basePrivateIpcV3FormatHash)
c.uint.encode(state, value.baseSchemaCount)
schemasEncoding.encode(state, value.additionalSchemas)
c.uint.encode(state, value.forwardReadinessOperationBits)
},
decode (state) {
return {
magic: c.string.decode(state),
formatVersion: c.uint.decode(state),
importedWireV3AbiHash: b4a.from(c.buffer.decode(state)),
basePrivateIpcV3FormatHash: b4a.from(c.buffer.decode(state)),
baseSchemaCount: c.uint.decode(state),
additionalSchemas: schemasEncoding.decode(state),
forwardReadinessOperationBits: c.uint.decode(state)
}
}
}
const declarationBytes = schema => b4a.from(JSON.stringify({ name: schema.schemaName, fields: schema.fields }), 'utf8')
export function createPrivateIpcV4RegistryValue (importedWireV3AbiHash, basePrivateIpcV3FormatHash) {
return {
magic: PRIVATE_IPC_V4_MAGIC,
formatVersion: 4,
importedWireV3AbiHash: bytes(importedWireV3AbiHash, 32, 'importedWireV3AbiHash', true),
basePrivateIpcV3FormatHash: bytes(basePrivateIpcV3FormatHash, 32, 'basePrivateIpcV3FormatHash', true),
baseSchemaCount: 15,
additionalSchemas: PRIVATE_IPC_V4_ADDITIONAL_SCHEMAS.map(schema => ({
schemaId: schema.schemaId,
schemaName: schema.schemaName,
canonicalDeclarationBytes: declarationBytes(schema)
})),
forwardReadinessOperationBits: 0
}
}
export function encodePrivateIpcV4Registry (value) {
const state = { start: 0, end: 0, buffer: null }
privateIpcV4RegistryEncoding.preencode(state, value)
state.buffer = b4a.alloc(state.end)
state.start = 0
privateIpcV4RegistryEncoding.encode(state, value)
if (state.start !== state.end) fail('private IPC v4 registry encoder length mismatch')
return state.buffer
}
export function decodePrivateIpcV4Registry (input) {
input = b4a.from(input)
const state = { start: 0, end: input.byteLength, buffer: input }
const value = privateIpcV4RegistryEncoding.decode(state)
if (state.start !== state.end || !b4a.equals(encodePrivateIpcV4Registry(value), input)) fail('private IPC v4 registry is not canonical')
if (value.magic !== PRIVATE_IPC_V4_MAGIC || value.formatVersion !== 4 || value.baseSchemaCount !== 15 ||
value.forwardReadinessOperationBits !== 0 || value.additionalSchemas.length !== 3 ||
value.additionalSchemas.some((schema, index) => schema.schemaId !== index + 16 ||
!b4a.equals(schema.canonicalDeclarationBytes, declarationBytes(PRIVATE_IPC_V4_ADDITIONAL_SCHEMAS[index])))) {
fail('private IPC v4 registry fixed allocation or declarations are invalid')
}
return value
}
export const hashPrivateIpcV4Registry = input => domainLengthHash(FORMAT_DOMAIN, input)
export const hashPrivateIpcV4VectorManifest = input => domainLengthHash(VECTOR_DOMAIN, input)