From f7485f452360beae96280fd212f52c7904871784 Mon Sep 17 00:00:00 2001 From: moon Date: Sun, 14 Sep 2025 13:47:32 +0330 Subject: moved to 3rd party --- client/3rd-party/client.ts | 674 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 client/3rd-party/client.ts (limited to 'client/3rd-party/client.ts') diff --git a/client/3rd-party/client.ts b/client/3rd-party/client.ts new file mode 100644 index 0000000..44f4634 --- /dev/null +++ b/client/3rd-party/client.ts @@ -0,0 +1,674 @@ +// ai-generated (do not trust) + +import { dlopen, FFIType, JSCallback, suffix } from 'bun:ffi' +import * as path from 'path' +import * as fs from 'fs' + +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 \ No newline at end of file -- cgit v1.2.3