summaryrefslogtreecommitdiff
path: root/client/client.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/client.ts')
-rw-r--r--client/client.ts674
1 files changed, 674 insertions, 0 deletions
diff --git a/client/client.ts b/client/client.ts
new file mode 100644
index 0000000..4f6031a
--- /dev/null
+++ b/client/client.ts
@@ -0,0 +1,674 @@
+// 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<RelayInfo> {
+ 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<RelayNeighbor[]> {
+ 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<boolean> {
+ 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<DataPacket> {
+ 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<DataPacketList> {
+ 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<DataPacket> {
+ 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<RelayInfo> {
+ return this.ffi.getServerInfo(ip, port)
+ }
+
+ async getNeighbors(ip: string, port: number, count: number = 5): Promise<RelayNeighbor[]> {
+ return this.ffi.getServerNeighbors(ip, port, count)
+ }
+
+ async pushPacket(ip: string, port: number, packet: DataPacket): Promise<boolean> {
+ return this.ffi.pushPacket(ip, port, packet)
+ }
+
+ async getPacket(ip: string, port: number, hash: Uint8Array): Promise<DataPacket> {
+ return this.ffi.getPacket(ip, port, hash)
+ }
+
+ async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise<DataPacketList> {
+ 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<DataPacket> {
+ 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