406 lines
13 KiB
JavaScript
406 lines
13 KiB
JavaScript
import test from 'brittle'
|
|
import b4a from 'b4a'
|
|
import crypto from 'hypercore-crypto'
|
|
import {
|
|
topicKey,
|
|
buildMbr,
|
|
parseMbr,
|
|
BLOCK_SIZE,
|
|
TOPIC_STRING,
|
|
PROTOCOL_APP_CHANNEL_NAME,
|
|
PROTOCOL_CAP_CHANNEL_NAME,
|
|
PROTOCOL_CHAT_CHANNEL_NAME,
|
|
PROTOCOL_MESHDROP_CHANNEL_NAME
|
|
} from './constants.js'
|
|
import {
|
|
BARE_OS_SEED_RPC_METHOD_SHORT_NAMES,
|
|
BARE_OS_SEED_RPC_METHODS,
|
|
bareOsIsAllowedSeedRpcMethodShort
|
|
} from './lib/seed-rpc-methods.js'
|
|
import {
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_RLIMITS_DELEGATES_SHELL,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_OFFLINE_NET_EXTENSIONS,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE
|
|
} from './lib/kernel-feature-bits.js'
|
|
import {
|
|
BARE_OS_KERNEL_CAPABILITY_WORDS_JSON_KEY,
|
|
BARE_OS_KERNEL_CAPABILITY_WORD_KEYS,
|
|
buildKernelCapabilityWordsObject,
|
|
getKernelCapabilityWords,
|
|
readKernelCapabilityWord
|
|
} from './lib/kernel-capability-wire.js'
|
|
import { msgDataEncoding, msgSearchReqEncoding } from './lib/messages.js'
|
|
import {
|
|
msgChatHelloEncoding,
|
|
msgChatEventEncoding,
|
|
msgChatJoinEncoding,
|
|
BARE_OS_CHAT_WIRE_SCHEMA_VERSION
|
|
} from './lib/chat-messages.js'
|
|
import { pearMultisigShapeOk } from './lib/bare-os-pear-multisig-shape.js'
|
|
import { BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION } from './lib/protomux-channel-schema.js'
|
|
import { BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION as protomuxSchemaFromChannel } from './lib/channel.js'
|
|
import { setupSeedChannel } from './lib/channel.js'
|
|
import { bareOsProtMuxChatChannelEnabled } from './lib/chat-env.js'
|
|
import { setupBareOsChatChannel } from './lib/chat-channel.js'
|
|
test('topicKey is deterministic 32-byte hash', (t) => {
|
|
const a = topicKey()
|
|
const b = topicKey()
|
|
t.is(a.byteLength, 32)
|
|
t.alike(a, b)
|
|
})
|
|
|
|
test('topicKey differs from other string', (t) => {
|
|
const k1 = topicKey()
|
|
const k2 = crypto.hash(b4a.from('other-topic'))
|
|
t.unlike(k1, k2)
|
|
})
|
|
|
|
test('topicKey golden hash fixture for bare-os-v1', (t) => {
|
|
const k = topicKey()
|
|
t.is(
|
|
b4a.toString(k, 'hex'),
|
|
'9863e44075709205829ba35d40dccf5d8d07e3924bb71a1c941bb5149a0edc4b'
|
|
)
|
|
})
|
|
|
|
test('buildMbr + parseMbr roundtrip', (t) => {
|
|
const key = crypto.hash(b4a.from('primary-drive'))
|
|
const mbr = buildMbr(key)
|
|
t.is(mbr.byteLength, BLOCK_SIZE)
|
|
const { keys } = parseMbr(mbr)
|
|
t.is(keys.length, 1)
|
|
t.alike(keys[0], key)
|
|
})
|
|
|
|
test('buildMbr + parseMbr preserves deterministic failover order', (t) => {
|
|
const primary = crypto.hash(b4a.from('primary-drive-failover'))
|
|
const failoverA = crypto.hash(b4a.from('failover-a'))
|
|
const failoverB = crypto.hash(b4a.from('failover-b'))
|
|
const mbr = buildMbr(primary, [failoverA, failoverB])
|
|
const parsed = parseMbr(mbr)
|
|
t.is(parsed.keys.length, 3)
|
|
t.alike(parsed.keys[0], primary)
|
|
t.alike(parsed.keys[1], failoverA)
|
|
t.alike(parsed.keys[2], failoverB)
|
|
|
|
const parsed2 = parseMbr(buildMbr(primary, [failoverA, failoverB]))
|
|
t.alike(parsed2.keys, parsed.keys)
|
|
})
|
|
|
|
test('msgDataEncoding roundtrip', (t) => {
|
|
const m = { index: 0, data: b4a.from('hello') }
|
|
const pre = { buffer: null, start: 0, end: 0 }
|
|
msgDataEncoding.preencode(pre, m)
|
|
const buf = b4a.alloc(pre.end)
|
|
const state = { buffer: buf, start: 0, end: 0 }
|
|
msgDataEncoding.encode(state, m)
|
|
const decState = { buffer: buf, start: 0, end: buf.byteLength }
|
|
const dec = msgDataEncoding.decode(decState)
|
|
t.is(dec.index, 0)
|
|
t.alike(dec.data, m.data)
|
|
})
|
|
|
|
test('msgSearchReqEncoding roundtrip', (t) => {
|
|
const m = { id: 7, query: 'foo' }
|
|
const pre = { buffer: null, start: 0, end: 0 }
|
|
msgSearchReqEncoding.preencode(pre, m)
|
|
const buf = b4a.alloc(pre.end)
|
|
const state = { buffer: buf, start: 0, end: 0 }
|
|
msgSearchReqEncoding.encode(state, m)
|
|
const decState = { buffer: buf, start: 0, end: buf.byteLength }
|
|
const dec = msgSearchReqEncoding.decode(decState)
|
|
t.is(dec.id, 7)
|
|
t.is(dec.query, 'foo')
|
|
})
|
|
|
|
test('PROTOCOL_APP_CHANNEL_NAME is bare-os-app-v1', (t) => {
|
|
t.is(PROTOCOL_APP_CHANNEL_NAME, 'bare-os-app-v1')
|
|
})
|
|
|
|
test('PROTOCOL_CAP_CHANNEL_NAME is bare-os-cap-v1', (t) => {
|
|
t.is(PROTOCOL_CAP_CHANNEL_NAME, 'bare-os-cap-v1')
|
|
})
|
|
|
|
test('PROTOCOL_CHAT_CHANNEL_NAME is bare-os-chat-v1', (t) => {
|
|
t.is(PROTOCOL_CHAT_CHANNEL_NAME, 'bare-os-chat-v1')
|
|
})
|
|
|
|
test('protocol channel compatibility fixture (app/cap/chat/meshdrop)', (t) => {
|
|
const fixture = {
|
|
app: PROTOCOL_APP_CHANNEL_NAME,
|
|
cap: PROTOCOL_CAP_CHANNEL_NAME,
|
|
chat: PROTOCOL_CHAT_CHANNEL_NAME,
|
|
meshdrop: PROTOCOL_MESHDROP_CHANNEL_NAME
|
|
}
|
|
t.alike(fixture, {
|
|
app: 'bare-os-app-v1',
|
|
cap: 'bare-os-cap-v1',
|
|
chat: 'bare-os-chat-v1',
|
|
meshdrop: 'bare-os-meshdrop-v1'
|
|
})
|
|
t.ok(Object.values(fixture).every((name) => name.startsWith('bare-os-')))
|
|
t.ok(Object.values(fixture).every((name) => name.endsWith('-v1')))
|
|
})
|
|
|
|
test('setupSeedChannel batches initial sends with stream cork/uncork when available', (t) => {
|
|
let corkCalls = 0
|
|
let uncorkCalls = 0
|
|
let replicateCalls = 0
|
|
const primarySends = []
|
|
const pairedOpens = []
|
|
function makeChannelRecorder() {
|
|
const msgs = []
|
|
return {
|
|
messages: msgs,
|
|
addMessage() {
|
|
const slot = { send: (msg) => primarySends.push(msg) }
|
|
msgs.push(slot)
|
|
},
|
|
open() {
|
|
pairedOpens.push('open')
|
|
}
|
|
}
|
|
}
|
|
const mux = {
|
|
stream: {
|
|
cork() {
|
|
corkCalls++
|
|
},
|
|
uncork() {
|
|
uncorkCalls++
|
|
}
|
|
},
|
|
createChannel() {
|
|
return makeChannelRecorder()
|
|
},
|
|
pair(_opts, onpair) {
|
|
const achan = {
|
|
messages: [],
|
|
addMessage() {
|
|
this.messages.push({ send() {} })
|
|
},
|
|
open() {
|
|
pairedOpens.push('app-open')
|
|
}
|
|
}
|
|
onpair(achan)
|
|
return achan
|
|
}
|
|
}
|
|
const localRAM = new Map([[0, b4a.from('mbr')]])
|
|
setupSeedChannel(
|
|
/** @type {any} */ (mux),
|
|
localRAM,
|
|
() => {
|
|
replicateCalls++
|
|
},
|
|
{ enableBareOsAppChannel: true, enableBareOsChatChannel: false }
|
|
)
|
|
t.is(corkCalls, 1)
|
|
t.is(uncorkCalls, 1)
|
|
t.ok(pairedOpens.includes('open'))
|
|
t.ok(pairedOpens.includes('app-open'))
|
|
t.is(replicateCalls, 1)
|
|
t.ok(primarySends.some((v) => v instanceof Uint8Array), 'bitfield sent')
|
|
})
|
|
|
|
function roundtrip(enc, m, t) {
|
|
const pre = { buffer: null, start: 0, end: 0 }
|
|
enc.preencode(pre, m)
|
|
const buf = b4a.alloc(pre.end)
|
|
const state = { buffer: buf, start: 0, end: 0 }
|
|
enc.encode(state, m)
|
|
const decState = { buffer: buf, start: 0, end: buf.byteLength }
|
|
return enc.decode(decState)
|
|
}
|
|
|
|
test('msgChatHelloEncoding roundtrip', (t) => {
|
|
const m = { chatSchemaVersion: 1, swarmChatCapability: true }
|
|
const dec = roundtrip(msgChatHelloEncoding, m, t)
|
|
t.is(dec.chatSchemaVersion, 1)
|
|
t.is(dec.swarmChatCapability, true)
|
|
})
|
|
|
|
test('msgChatJoinEncoding roundtrip', (t) => {
|
|
const m = {
|
|
roomId: 'general',
|
|
displayName: 'alice',
|
|
senderPk: b4a.alloc(32, 3),
|
|
clientInstanceId: b4a.alloc(16, 7)
|
|
}
|
|
const dec = roundtrip(msgChatJoinEncoding, m, t)
|
|
t.is(dec.roomId, 'general')
|
|
t.is(dec.displayName, 'alice')
|
|
t.alike(dec.senderPk, m.senderPk)
|
|
t.alike(dec.clientInstanceId, m.clientInstanceId)
|
|
})
|
|
|
|
test('msgChatEventEncoding roundtrip', (t) => {
|
|
const m = {
|
|
schemaVersion: BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
|
|
evtKind: 1,
|
|
roomId: 'general',
|
|
evtId: b4a.alloc(16, 9),
|
|
tsMs: Date.now(),
|
|
senderPk: b4a.alloc(32, 2),
|
|
displayName: 'bob',
|
|
body: 'hello swarm',
|
|
ttl: 4,
|
|
sigDetached: null
|
|
}
|
|
const dec = roundtrip(msgChatEventEncoding, m, t)
|
|
t.is(dec.schemaVersion, m.schemaVersion)
|
|
t.is(dec.body, 'hello swarm')
|
|
t.is(dec.ttl, 4)
|
|
t.is(dec.sigDetached, null)
|
|
})
|
|
|
|
test('TOPIC_STRING is bare-os-v1', (t) => {
|
|
t.is(TOPIC_STRING, 'bare-os-v1')
|
|
})
|
|
|
|
test('seed RPC method list matches kernel_info surface', (t) => {
|
|
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('capabilities'))
|
|
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('replication_queue'))
|
|
t.is(BARE_OS_SEED_RPC_METHODS.length, BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.length)
|
|
t.ok(BARE_OS_SEED_RPC_METHODS.every((m) => m.startsWith('bare_os.')))
|
|
})
|
|
|
|
test('stock primary capability word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY >>> 0) > 0)
|
|
})
|
|
|
|
test('stock extended seeding platform word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM >>> 0) > 0)
|
|
})
|
|
|
|
test('stock host-transport-delegates word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES >>> 0) > 0)
|
|
})
|
|
|
|
test('stock replication-operator-surface word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE >>> 0) > 0)
|
|
})
|
|
|
|
test('stock pear-corestore-hrpc word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC >>> 0) > 0)
|
|
})
|
|
|
|
test('stock bare-runtime-proto-mux word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX >>> 0) > 0)
|
|
})
|
|
|
|
test('stock bare-module-crypto-staging word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING >>> 0) > 0)
|
|
})
|
|
|
|
test('stock pear-inspect-logger-tls word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS >>> 0) > 0)
|
|
})
|
|
|
|
test('stock hypercore-pack-hrpc-lifecycle word is non-zero', (t) => {
|
|
t.ok((BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE >>> 0) > 0)
|
|
})
|
|
|
|
test('kernelCapabilityWords round-trip fixture covers all 11 words', (t) => {
|
|
const wordsObj = buildKernelCapabilityWordsObject(
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_EXTENDED_SEEDING_PLATFORM,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_RLIMITS_DELEGATES_SHELL,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_OFFLINE_NET_EXTENSIONS,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HOST_TRANSPORT_DELEGATES,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_REPLICATION_OPERATOR_SURFACE,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_CORESTORE_HRPC,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_RUNTIME_PROTO_MUX,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_BARE_MODULE_CRYPTO_STAGING,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PEAR_INSPECT_LOGGER_TLS,
|
|
BARE_OS_KERNEL_FEATURES_STOCK_WORD_HYPERCORE_PACK_HRPC_LIFECYCLE
|
|
)
|
|
const cap = { [BARE_OS_KERNEL_CAPABILITY_WORDS_JSON_KEY]: wordsObj }
|
|
const roundTrip = getKernelCapabilityWords(cap)
|
|
t.ok(roundTrip)
|
|
t.is(BARE_OS_KERNEL_CAPABILITY_WORD_KEYS.length, 11)
|
|
t.alike(Object.keys(roundTrip).sort(), [...BARE_OS_KERNEL_CAPABILITY_WORD_KEYS].sort())
|
|
for (const key of BARE_OS_KERNEL_CAPABILITY_WORD_KEYS) {
|
|
t.is(readKernelCapabilityWord(roundTrip, key), readKernelCapabilityWord(wordsObj, key))
|
|
}
|
|
})
|
|
|
|
test('seed RPC registry includes HyperDHT / blind-peer operator hints', (t) => {
|
|
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('dht_bootstrap_hint'))
|
|
t.ok(BARE_OS_SEED_RPC_METHOD_SHORT_NAMES.includes('blind_peer_topology_v2'))
|
|
})
|
|
|
|
test('bareOsIsAllowedSeedRpcMethodShort matches registry gate', (t) => {
|
|
t.ok(bareOsIsAllowedSeedRpcMethodShort('version'))
|
|
t.ok(bareOsIsAllowedSeedRpcMethodShort('capabilities'))
|
|
t.absent(bareOsIsAllowedSeedRpcMethodShort('not_a_registered_bare_os_method'))
|
|
t.absent(bareOsIsAllowedSeedRpcMethodShort(''))
|
|
})
|
|
|
|
test('pearMultisigShapeOk accepts valid multisig metadata', (t) => {
|
|
t.ok(pearMultisigShapeOk({ signers: ['a', 'b'], quorum: 1 }))
|
|
t.ok(pearMultisigShapeOk({ signers: ['x'], quorum: 1 }))
|
|
})
|
|
|
|
test('pearMultisigShapeOk rejects tampered shapes', (t) => {
|
|
t.absent(pearMultisigShapeOk(null))
|
|
t.absent(pearMultisigShapeOk({ signers: 'x', quorum: 1 }))
|
|
t.absent(pearMultisigShapeOk({ signers: ['a'], quorum: 2 }))
|
|
t.absent(pearMultisigShapeOk({ signers: [], quorum: 0 }))
|
|
})
|
|
|
|
test('BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION matches channel re-export', (t) => {
|
|
t.is(BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION, protomuxSchemaFromChannel)
|
|
t.ok(BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION >= 1)
|
|
})
|
|
|
|
test('bareOsProtMuxChatChannelEnabled defaults on; explicit opt-out tokens', (t) => {
|
|
t.ok(bareOsProtMuxChatChannelEnabled(undefined))
|
|
t.ok(bareOsProtMuxChatChannelEnabled({}))
|
|
t.ok(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: '' }))
|
|
t.ok(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'TRUE' }))
|
|
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: '0' }))
|
|
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'false' }))
|
|
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'OFF' }))
|
|
t.absent(bareOsProtMuxChatChannelEnabled({ BARE_OS_PROTOMUX_CHAT_CHANNEL: 'no' }))
|
|
})
|
|
|
|
test('setupBareOsChatChannel defers onChannelOpened until fullyOpened resolves', async (t) => {
|
|
/** @type {((v: boolean) => void) | null} */
|
|
let resolveOpened = null
|
|
const chan = {
|
|
addMessage() {},
|
|
open() {},
|
|
fullyOpened() {
|
|
return new Promise((resolve) => {
|
|
resolveOpened = resolve
|
|
})
|
|
}
|
|
}
|
|
const mux = {
|
|
createChannel() {
|
|
return chan
|
|
}
|
|
}
|
|
let called = false
|
|
setupBareOsChatChannel(/** @type {any} */ (mux), {
|
|
onChannelOpened() {
|
|
called = true
|
|
}
|
|
})
|
|
t.absent(called)
|
|
resolveOpened(true)
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
t.ok(called)
|
|
})
|