// moonpaste-client.ts import { dlopen, FFIType, JSCallback, suffix } from 'bun:ffi' import * as path from 'path' import * as fs from 'fs' // Type definitions export interface RelayNeighbor { active: boolean hasIpv4: boolean hasIpv6: boolean v4Addr: Uint8Array // 4 bytes v6Addr: Uint8Array // 16 bytes } export interface RelayCapabilities { retentionHours: number canStore: boolean leadingZeros: number acceptsPlain: boolean } export interface RelayStatistics { neighbors: number heldPackets: bigint quickConnect: RelayNeighbor[] // 4 elements } export interface RelayInfo { majorVersion: number minorVersion: number motd: string // 128 bytes max capabilities: RelayCapabilities statistics: RelayStatistics } export interface DataPacketContents { encrypted: boolean createdTimeMs: bigint author: Uint8Array // 32 bytes authorSigningPub: Uint8Array // 32 bytes recipientCount: number recipients: Uint8Array[] // 32 arrays of 32 bytes each data: Uint8Array // 65536 bytes signature: Uint8Array // 64 bytes } export interface DataPacket { nonce: number hash: Uint8Array // 32 bytes contents: DataPacketContents } export interface KeyPair { encryptionPub: Uint8Array // 32 bytes encryptionPriv: Uint8Array // 32 bytes signingPub: Uint8Array // 32 bytes signingPriv: Uint8Array // 64 bytes } export interface DataPacketList { packets: DataPacket[] count: number newOffset: bigint } export enum ClientError { Success = 0, NetworkFail = 1, InvalidAddress = 2, ConnectionFailed = 3, SendFailed = 4, RecvFailed = 5, ServerBusy = 6, InvalidResponse = 7, DeserializeFailed = 8, AllocFailed = 9, FileNotFound = 10, FileReadFailed = 11, BadArgument = 12 } function getRuntime(): 'bun' | 'deno' | 'node' { if (typeof Bun !== 'undefined') return 'bun' if (typeof Deno !== 'undefined') return 'deno' return 'node' } function findLibrary(): string { const runtime = getRuntime() const libNames = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll'] const searchPaths = [ './', './lib/', '../lib/', '/usr/local/lib/', '/usr/lib/', process.env.MOONPASTE_LIB_PATH || '' ].filter(p => p.length > 0) for (const searchPath of searchPaths) { for (const libName of libNames) { const fullPath = path.join(searchPath, libName) if (fs.existsSync(fullPath)) { return fullPath } } } throw new Error('Could not find moonpaste client library. Searched for: ' + libNames.join(', ')) } class MoonpasteFFI { private lib: any private runtime: string constructor() { this.runtime = getRuntime() const libPath = findLibrary() switch (this.runtime) { case 'bun': this.lib = dlopen(libPath, { mp_get_server_info: { args: [FFIType.cstring, FFIType.i32, FFIType.ptr], returns: FFIType.i32 }, mp_get_server_neighbors: { args: [FFIType.cstring, FFIType.i32, FFIType.u8, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, mp_push_packet: { args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, mp_get_packet: { args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, mp_get_firehose_packets: { args: [FFIType.cstring, FFIType.i32, FFIType.u64, FFIType.u16, FFIType.ptr], returns: FFIType.i32 }, mp_create_packet_from_bytes: { args: [FFIType.ptr, FFIType.usize, FFIType.u8, FFIType.ptr, FFIType.ptr], returns: FFIType.ptr }, mp_create_packet_from_file: { args: [FFIType.cstring, FFIType.u8, FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, mp_run_pow: { args: [FFIType.ptr, FFIType.u8], returns: FFIType.ptr }, mp_generate_keypair: { args: [], returns: FFIType.ptr } }) break case 'deno': this.lib = Deno.dlopen(libPath, { mp_get_server_info: { parameters: ['buffer', 'i32', 'pointer'], result: 'i32' }, mp_get_server_neighbors: { parameters: ['buffer', 'i32', 'u8', 'pointer', 'pointer'], result: 'i32' }, mp_push_packet: { parameters: ['buffer', 'i32', 'pointer', 'pointer'], result: 'i32' }, mp_get_packet: { parameters: ['buffer', 'i32', 'pointer', 'pointer'], result: 'i32' }, mp_get_firehose_packets: { parameters: ['buffer', 'i32', 'u64', 'u16', 'pointer'], result: 'i32' }, mp_create_packet_from_bytes: { parameters: ['pointer', 'usize', 'u8', 'pointer', 'pointer'], result: 'pointer' }, mp_create_packet_from_file: { parameters: ['buffer', 'u8', 'pointer', 'pointer', 'pointer'], result: 'i32' }, mp_run_pow: { parameters: ['pointer', 'u8'], result: 'pointer' }, mp_generate_keypair: { parameters: [], result: 'pointer' } }) break case 'node': try { const ffi = require('ffi-napi') // dependency const ref = require('ref-napi') // dependency this.lib = ffi.Library(libPath, { mp_get_server_info: ['int', ['string', 'int', 'pointer']], mp_get_server_neighbors: ['int', ['string', 'int', 'uint8', 'pointer', 'pointer']], mp_push_packet: ['int', ['string', 'int', 'pointer', 'pointer']], mp_get_packet: ['int', ['string', 'int', 'pointer', 'pointer']], mp_get_firehose_packets: ['int', ['string', 'int', 'uint64', 'uint16', 'pointer']], mp_create_packet_from_bytes: ['pointer', ['pointer', 'size_t', 'uint8', 'pointer', 'pointer']], mp_create_packet_from_file: ['int', ['string', 'uint8', 'pointer', 'pointer', 'pointer']], mp_run_pow: ['pointer', ['pointer', 'uint8']], mp_generate_keypair: ['pointer', []] }) } catch (e) { throw new Error('Node.js FFI requires ffi-napi and ref-napi packages: npm install ffi-napi ref-napi') } break } } private allocateBuffer(size: number): any { switch (this.runtime) { case 'bun': return new ArrayBuffer(size) case 'deno': return new ArrayBuffer(size) case 'node': const ref = require('ref-napi') return ref.alloc(size) } } private stringToBuffer(str: string): any { switch (this.runtime) { case 'bun': case 'deno': return new TextEncoder().encode(str + '\0') case 'node': return Buffer.from(str + '\0', 'utf8') } } async getServerInfo(ip: string, port: number): Promise { const infoBuffer = this.allocateBuffer(1024) const ipBuffer = this.stringToBuffer(ip) const result = this.lib.symbols.mp_get_server_info(ipBuffer, port, infoBuffer) if (result !== ClientError.Success) { throw new Error(`Failed to get server info: ${result}`) } return this.parseRelayInfo(infoBuffer) } async getServerNeighbors(ip: string, port: number, count: number): Promise { const neighborsPtr = this.allocateBuffer(8) // pointer const countPtr = this.allocateBuffer(1) // uint8 const ipBuffer = this.stringToBuffer(ip) const result = this.lib.symbols.mp_get_server_neighbors(ipBuffer, port, count, neighborsPtr, countPtr) if (result !== ClientError.Success) { throw new Error(`Failed to get neighbors: ${result}`) } return this.parseNeighbors(neighborsPtr, countPtr) } async pushPacket(ip: string, port: number, packet: DataPacket): Promise { const successPtr = this.allocateBuffer(1) const ipBuffer = this.stringToBuffer(ip) const packetBuffer = this.serializePacket(packet) const result = this.lib.symbols.mp_push_packet(ipBuffer, port, packetBuffer, successPtr) if (result !== ClientError.Success) { throw new Error(`Failed to push packet: ${result}`) } return this.readBool(successPtr) } async getPacket(ip: string, port: number, hash: Uint8Array): Promise { const packetBuffer = this.allocateBuffer(70000) // approximation const ipBuffer = this.stringToBuffer(ip) const result = this.lib.symbols.mp_get_packet(ipBuffer, port, hash, packetBuffer) if (result !== ClientError.Success) { throw new Error(`Failed to get packet: ${result}`) } return this.parseDataPacket(packetBuffer) } async getFirehosePackets(ip: string, port: number, offset: bigint, count: number): Promise { const listBuffer = this.allocateBuffer(1024) const ipBuffer = this.stringToBuffer(ip) const result = this.lib.symbols.mp_get_firehose_packets(ipBuffer, port, offset, count, listBuffer) if (result !== ClientError.Success) { throw new Error(`Failed to get firehose packets: ${result}`) } return this.parseDataPacketList(listBuffer) } createPacketFromBytes(input: Uint8Array, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): DataPacket { const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null const keysBuffer = this.serializeKeyPair(authorKeys) const result = this.lib.symbols.mp_create_packet_from_bytes( input, input.length, recipientCount, recipientsBuffer, keysBuffer ) return this.parseDataPacket(result) } async createPacketFromFile(filename: string, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): Promise { const packetBuffer = this.allocateBuffer(4096) const filenameBuffer = this.stringToBuffer(filename) const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null const keysBuffer = this.serializeKeyPair(authorKeys) const result = this.lib.symbols.mp_create_packet_from_file( filenameBuffer, recipientCount, recipientsBuffer, keysBuffer, packetBuffer ) if (result !== ClientError.Success) { throw new Error(`Failed to create packet from file: ${result}`) } return this.parseDataPacket(packetBuffer) } runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { const packetBuffer = this.serializePacket(packet) const result = this.lib.symbols.mp_run_pow(packetBuffer, leadingZeros) return this.parseDataPacket(result) } generateKeyPair(): KeyPair { const result = this.lib.symbols.mp_generate_keypair() return this.parseKeyPair(result) } private parseRelayInfo(buffer: any): RelayInfo { const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) let offset = 0 const majorVersion = view.getUint8(offset++) const minorVersion = view.getUint8(offset++) const motdBytes = new Uint8Array(buffer, offset, 128) const motd = new TextDecoder().decode(motdBytes).replace(/\0.*$/, '') offset += 128 const retentionHours = view.getUint16(offset, true) // little endian offset += 2 const canStore = view.getUint8(offset++) !== 0 const leadingZeros = view.getUint8(offset++) const acceptsPlain = view.getUint8(offset++) !== 0 const neighbors = view.getUint16(offset, true) offset += 2 const heldPackets = view.getBigUint64(offset, true) offset += 8 const quickConnect: RelayNeighbor[] = [] for (let i = 0; i < 4; i++) { quickConnect.push(this.parseRelayNeighborAt(buffer, offset)) offset += 23 // 1 + 1 + 1 + 4 + 16 } return { majorVersion, minorVersion, motd, capabilities: { retentionHours, canStore, leadingZeros, acceptsPlain }, statistics: { neighbors, heldPackets, quickConnect } } } private parseRelayNeighborAt(buffer: any, offset: number): RelayNeighbor { const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) const active = view.getUint8(offset) !== 0 const hasIpv4 = view.getUint8(offset + 1) !== 0 const hasIpv6 = view.getUint8(offset + 2) !== 0 const v4Addr = new Uint8Array(buffer, offset + 3, 4) const v6Addr = new Uint8Array(buffer, offset + 7, 16) return { active, hasIpv4, hasIpv6, v4Addr, v6Addr } } private parseNeighbors(neighborsPtr: any, countPtr: any): RelayNeighbor[] { const count = this.readUint8(countPtr) const neighbors: RelayNeighbor[] = [] if (count > 0 && neighborsPtr) { for (let i = 0; i < count; i++) { const neighborOffset = i * 23 // RELAY_NEIGHBOR_WIRE_LEN neighbors.push(this.parseRelayNeighborAt(neighborsPtr, neighborOffset)) } } return neighbors } private parseDataPacket(buffer: any): DataPacket { const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) let offset = 0 const nonce = view.getUint32(offset, true) offset += 4 const hash = new Uint8Array(buffer, offset, 32) offset += 32 const encrypted = view.getUint8(offset++) !== 0 const createdTimeMs = view.getBigUint64(offset, true) offset += 8 const author = new Uint8Array(buffer, offset, 32) offset += 32 const authorSigningPub = new Uint8Array(buffer, offset, 32) offset += 32 const recipientCount = view.getUint8(offset++) const recipients: Uint8Array[] = [] for (let i = 0; i < 32; i++) { recipients.push(new Uint8Array(buffer, offset, 32)) offset += 32 } const data = new Uint8Array(buffer, offset, 65536) offset += 65536 const signature = new Uint8Array(buffer, offset, 64) return { nonce, hash, contents: { encrypted, createdTimeMs, author, authorSigningPub, recipientCount, recipients, data, signature } } } private parseDataPacketList(buffer: any): DataPacketList { const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer) const newOffset = view.getBigUint64(0, true) const availableBytes = buffer.byteLength - 8 const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) // PACKET_SIZE const packetCount = Math.floor(availableBytes / packetSize) const packets: DataPacket[] = [] for (let i = 0; i < packetCount; i++) { const packetOffset = 8 + (i * packetSize) const packetBuffer = buffer.slice(packetOffset, packetOffset + packetSize) packets.push(this.parseDataPacket(packetBuffer)) } return { packets, count: packetCount, newOffset } } private parseKeyPair(buffer: any): KeyPair { const encryptionPub = new Uint8Array(buffer, 0, 32) const encryptionPriv = new Uint8Array(buffer, 32, 32) const signingPub = new Uint8Array(buffer, 64, 32) const signingPriv = new Uint8Array(buffer, 96, 64) return { encryptionPub, encryptionPriv, signingPub, signingPriv } } private serializePacket(packet: DataPacket): any { const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) const buffer = new ArrayBuffer(packetSize) const view = new DataView(buffer) let offset = 0 view.setUint32(offset, packet.nonce, true) offset += 4 new Uint8Array(buffer, offset, 32).set(packet.hash) offset += 32 view.setUint8(offset++, packet.contents.encrypted ? 1 : 0) view.setBigUint64(offset, packet.contents.createdTimeMs, true) offset += 8 new Uint8Array(buffer, offset, 32).set(packet.contents.author) offset += 32 new Uint8Array(buffer, offset, 32).set(packet.contents.authorSigningPub) offset += 32 view.setUint8(offset++, packet.contents.recipientCount) for (let i = 0; i < 32; i++) { if (i < packet.contents.recipients.length) { new Uint8Array(buffer, offset, 32).set(packet.contents.recipients[i]) } offset += 32 } new Uint8Array(buffer, offset, 65536).set(packet.contents.data) offset += 65536 new Uint8Array(buffer, offset, 64).set(packet.contents.signature) return buffer } private serializeRecipients(recipients: Uint8Array[]): any { const buffer = new ArrayBuffer(32 * 32) // MAX_RECIPIENTS * 32 bytes each for (let i = 0; i < Math.min(recipients.length, 32); i++) { new Uint8Array(buffer, i * 32, 32).set(recipients[i]) } return buffer } private serializeKeyPair(keyPair: KeyPair): any { const buffer = new ArrayBuffer(32 + 32 + 32 + 64) // total keypair size let offset = 0 new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPub) offset += 32 new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPriv) offset += 32 new Uint8Array(buffer, offset, 32).set(keyPair.signingPub) offset += 32 new Uint8Array(buffer, offset, 64).set(keyPair.signingPriv) return buffer } private readBool(ptr: any): boolean { return this.readUint8(ptr) !== 0 } private readUint8(ptr: any): number { switch (this.runtime) { case 'bun': case 'deno': return new Uint8Array(ptr)[0] case 'node': return ptr.readUInt8(0) default: return 0 } } } // Main client class export class MoonpasteClient { private ffi: MoonpasteFFI constructor() { this.ffi = new MoonpasteFFI() } async getServerInfo(ip: string, port: number): Promise { return this.ffi.getServerInfo(ip, port) } async getNeighbors(ip: string, port: number, count: number = 5): Promise { return this.ffi.getServerNeighbors(ip, port, count) } async pushPacket(ip: string, port: number, packet: DataPacket): Promise { return this.ffi.pushPacket(ip, port, packet) } async getPacket(ip: string, port: number, hash: Uint8Array): Promise { return this.ffi.getPacket(ip, port, hash) } async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise { return this.ffi.getFirehosePackets(ip, port, offset, count) } createPacket(message: string | Uint8Array, recipients?: Uint8Array[], authorKeys?: KeyPair): DataPacket { const data = typeof message === 'string' ? new TextEncoder().encode(message) : message const keys = authorKeys || this.generateKeyPair() const recipientCount = recipients ? recipients.length : 0 return this.ffi.createPacketFromBytes(data, recipientCount, recipients || null, keys) } async createPacketFromFile(filename: string, recipients?: Uint8Array[], authorKeys?: KeyPair): Promise { const keys = authorKeys || this.generateKeyPair() const recipientCount = recipients ? recipients.length : 0 return this.ffi.createPacketFromFile(filename, recipientCount, recipients || null, keys) } runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket { return this.ffi.runProofOfWork(packet, leadingZeros) } generateKeyPair(): KeyPair { return this.ffi.generateKeyPair() } // Utility methods static hashToHex(hash: Uint8Array): string { return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('') } static hexToHash(hex: string): Uint8Array { const result = new Uint8Array(32) for (let i = 0; i < 32; i++) { result[i] = parseInt(hex.substr(i * 2, 2), 16) } return result } static formatIpAddress(neighbor: RelayNeighbor): string { if (neighbor.hasIpv4) { return Array.from(neighbor.v4Addr).join('.') } else if (neighbor.hasIpv6) { const parts: string[] = [] for (let i = 0; i < 8; i++) { const word = (neighbor.v6Addr[i * 2] << 8) | neighbor.v6Addr[i * 2 + 1] parts.push(word.toString(16)) } return parts.join(':') } return 'N/A' } } export default MoonpasteClient