// ai generated, do not trust const MAX_BUFFER = 1 << 18; const MAX_BODY = 1 << 22; const MAX_RECIPIENTS = 32; const PACKET_SIZE = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * MAX_RECIPIENTS) + 65536 + 64); const RELAY_NEIGHBOR_WIRE_LEN = 1 + 1 + 1 + 4 + 16 + 2; enum MpClientError { SUCCESS = 0, NETWORK_FAIL, CONNECTION_FAILED, SERVER_BUSY, INVALID_RESPONSE, DESERIALIZE_FAILED, STRING_ENCODE_FAILED, BAD_ARGUMENT, RECV_FAILED } interface RelayNeighbor { active: boolean; has_ipv4: boolean; has_ipv6: boolean; v4_addr: Uint8Array; v6_addr: Uint8Array; port: number; } interface RelayInfo { major_version: number; minor_version: number; motd: Uint8Array; capabilities: { retention_hours: number; can_store: boolean; leading_zeros: number; accepts_plain: boolean; }; statistics: { neighbors: number; held_packets: bigint; quick_connect: RelayNeighbor[]; }; } interface DataPacketContents { encrypted: boolean; created_time_ms: bigint; author: Uint8Array; author_signing_pub: Uint8Array; recipient_count: number; recipients: Uint8Array[]; data: Uint8Array; signature: Uint8Array; } interface DataPacket { nonce: number; hash: Uint8Array; contents: DataPacketContents; } interface DataPacketList { packets: DataPacket[]; count: number; new_offset: bigint; } interface KeyPair { encryption_pub: Uint8Array; encryption_priv: Uint8Array; signing_pub: Uint8Array; signing_priv: Uint8Array; } class MoonPasteClient { private ip: string | null = null; private port: number | null = null; connect(ip: string, port: number): void { this.ip = ip; this.port = port; } private checkConnected(): boolean { return this.ip !== null && this.port !== null; } private readU16LE(buffer: Uint8Array, offset: number): number { return buffer[offset] | (buffer[offset + 1] << 8); } private writeU16LE(buffer: Uint8Array, value: number, offset: number): void { buffer[offset] = value & 0xFF; buffer[offset + 1] = (value >> 8) & 0xFF; } private writeU32LE(buffer: Uint8Array, value: number, offset: number): void { buffer[offset] = value & 0xFF; buffer[offset + 1] = (value >> 8) & 0xFF; buffer[offset + 2] = (value >> 16) & 0xFF; buffer[offset + 3] = (value >> 24) & 0xFF; } private readU32LE(buffer: Uint8Array, offset: number): number { return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); } private readU64LE(buffer: Uint8Array, offset: number): bigint { return ( BigInt(buffer[offset]) | (BigInt(buffer[offset + 1]) << 8n) | (BigInt(buffer[offset + 2]) << 16n) | (BigInt(buffer[offset + 3]) << 24n) | (BigInt(buffer[offset + 4]) << 32n) | (BigInt(buffer[offset + 5]) << 40n) | (BigInt(buffer[offset + 6]) << 48n) | (BigInt(buffer[offset + 7]) << 56n) ); } private writeU64LE(buffer: Uint8Array, value: bigint, offset: number): void { buffer[offset] = Number(value & 0xFFn); buffer[offset + 1] = Number((value >> 8n) & 0xFFn); buffer[offset + 2] = Number((value >> 16n) & 0xFFn); buffer[offset + 3] = Number((value >> 24n) & 0xFFn); buffer[offset + 4] = Number((value >> 32n) & 0xFFn); buffer[offset + 5] = Number((value >> 40n) & 0xFFn); buffer[offset + 6] = Number((value >> 48n) & 0xFFn); buffer[offset + 7] = Number((value >> 56n) & 0xFFn); } private checkLeadingZeros(hash: Uint8Array, requiredZeros: number): boolean { if (requiredZeros === 0) return true; const fullBytes = Math.floor(requiredZeros / 8); const remainingBits = requiredZeros % 8; for (let i = 0; i < fullBytes; i++) { if (hash[i] !== 0) return false; } if (remainingBits > 0) { const mask = 0xFF << (8 - remainingBits); if ((hash[fullBytes] & mask) !== 0) return false; } return true; } private async sha256(data: Uint8Array): Promise { const digest = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(digest); } async generateKeyPair(): Promise { const { publicKey: encPub, privateKey: encPriv } = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'] ); const { publicKey: signPub, privateKey: signPriv } = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'] ); const encPubBytes = new Uint8Array(await crypto.subtle.exportKey('raw', encPub)); const encPrivBytes = new Uint8Array(await crypto.subtle.exportKey('raw', encPriv)); const signPubBytes = new Uint8Array(await crypto.subtle.exportKey('raw', signPub)); const signPrivBytes = new Uint8Array(await crypto.subtle.exportKey('raw', signPriv)); return { encryption_pub: encPubBytes.slice(0, 32), encryption_priv: encPrivBytes.slice(0, 32), signing_pub: signPubBytes.slice(0, 32), signing_priv: signPrivBytes.slice(0, 32), }; } private async signData(data: Uint8Array, privateKey: Uint8Array): Promise { const key = await crypto.subtle.importKey( 'raw', privateKey, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign'] ); const signature = await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, key, data ); return new Uint8Array(signature).slice(0, 64); } private s2relayNeighbor(len: number, stream: Uint8Array): { neighbor: RelayNeighbor; success: boolean } { if (len !== RELAY_NEIGHBOR_WIRE_LEN) { return { neighbor: { active: false, has_ipv4: false, has_ipv6: false, v4_addr: new Uint8Array(4), v6_addr: new Uint8Array(16), port: 0 }, success: false }; } const neighbor: RelayNeighbor = { active: stream[0] !== 0, has_ipv4: stream[1] !== 0, has_ipv6: stream[2] !== 0, v4_addr: stream.slice(3, 7), v6_addr: stream.slice(7, 23), port: this.readU16LE(stream, 23), }; return { neighbor, success: true }; } private s2relayInfo(len: number, stream: Uint8Array): { info: RelayInfo; success: boolean } { const expectedLen = 1 + 1 + 128 + 2 + 1 + 1 + 1 + 2 + 8 + (4 * RELAY_NEIGHBOR_WIRE_LEN); if (len !== expectedLen) { return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, success: false }; } let off = 0; const info: RelayInfo = { major_version: stream[off++], minor_version: stream[off++], motd: stream.slice(off, off + 128), capabilities: { retention_hours: this.readU16LE(stream, off), can_store: stream[off + 2] !== 0, leading_zeros: stream[off + 3], accepts_plain: stream[off + 4] !== 0, }, statistics: { neighbors: this.readU16LE(stream, off + 5), held_packets: this.readU64LE(stream, off + 7), quick_connect: [], }, }; off += 15; for (let i = 0; i < 4; i++) { const { neighbor, success } = this.s2relayNeighbor(RELAY_NEIGHBOR_WIRE_LEN, stream.slice(off, off + RELAY_NEIGHBOR_WIRE_LEN)); if (!success) { return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, success: false }; } info.statistics.quick_connect.push(neighbor); off += RELAY_NEIGHBOR_WIRE_LEN; } return { info, success: true }; } private s2dataPacket(len: number, stream: Uint8Array): { packet: DataPacket; success: boolean } { if (len !== PACKET_SIZE) { return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, success: false }; } let off = 0; const packet: DataPacket = { nonce: this.readU32LE(stream, off), hash: stream.slice(off + 4, off + 36), contents: { encrypted: stream[off + 36] !== 0, created_time_ms: this.readU64LE(stream, off + 37), author: stream.slice(off + 45, off + 77), author_signing_pub: stream.slice(off + 77, off + 109), recipient_count: stream[off + 109], recipients: [], data: stream.slice(off + 110 + 32 * MAX_RECIPIENTS, off + 110 + 32 * MAX_RECIPIENTS + 65536), signature: stream.slice(off + 110 + 32 * MAX_RECIPIENTS + 65536, off + 110 + 32 * MAX_RECIPIENTS + 65536 + 64), }, }; off += 110; for (let i = 0; i < MAX_RECIPIENTS; i++) { packet.contents.recipients.push(stream.slice(off, off + 32)); off += 32; } return { packet, success: true }; } private dataPacket2s(data: DataPacket): Uint8Array | null { if (data.contents.recipient_count > MAX_RECIPIENTS) { return null; } const stream = new Uint8Array(PACKET_SIZE); let off = 0; this.writeU32LE(stream, data.nonce, off); off += 4; stream.set(data.hash, off); off += 32; stream[off++] = data.contents.encrypted ? 1 : 0; this.writeU64LE(stream, data.contents.created_time_ms, off); off += 8; stream.set(data.contents.author, off); off += 32; stream.set(data.contents.author_signing_pub, off); off += 32; stream[off++] = data.contents.recipient_count; for (let i = 0; i < MAX_RECIPIENTS; i++) { stream.set(data.contents.recipients[i] || new Uint8Array(32), off); off += 32; } stream.set(data.contents.data, off); off += 65536; stream.set(data.contents.signature, off); return stream; } private async clientRequest(data: Uint8Array, dataLen: number): Promise<{ response: Uint8Array | null; respLen: number; error: MpClientError }> { if (!this.checkConnected()) { return { response: null, respLen: 0, error: MpClientError.CONNECTION_FAILED }; } const isIpv6 = this.ip!.includes(':'); const url = isIpv6 ? `http://[${this.ip}]:${this.port}/` : `http://${this.ip}:${this.port}/`; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Length': dataLen.toString(), 'Connection': 'close', }, body: data.slice(0, dataLen), }); if (!response.ok) { if (response.status === 418) { return { response: null, respLen: 0, error: MpClientError.SERVER_BUSY }; } return { response: null, respLen: 0, error: MpClientError.NETWORK_FAIL }; } const arrayBuffer = await response.arrayBuffer(); const responseData = new Uint8Array(arrayBuffer); const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10); if (contentLength > 0 && responseData.length !== contentLength) { return { response: null, respLen: 0, error: MpClientError.RECV_FAILED }; } if (responseData.length > MAX_BODY) { return { response: null, respLen: 0, error: MpClientError.INVALID_RESPONSE }; } return { response: responseData, respLen: responseData.length, error: MpClientError.SUCCESS }; } catch (err) { return { response: null, respLen: 0, error: MpClientError.CONNECTION_FAILED }; } } async getServerInfo(): Promise<{ info: RelayInfo; error: MpClientError }> { const data = new Uint8Array([0x10]); const { response, respLen, error } = await this.clientRequest(data, data.length); if (error !== MpClientError.SUCCESS) { return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, error }; } if (respLen < 1 || response![0] !== 0x17) { return { info: { major_version: 0, minor_version: 0, motd: new Uint8Array(128), capabilities: { retention_hours: 0, can_store: false, leading_zeros: 0, accepts_plain: false }, statistics: { neighbors: 0, held_packets: 0n, quick_connect: [] } }, error: MpClientError.INVALID_RESPONSE }; } const { info, success } = this.s2relayInfo(respLen - 1, response!.slice(1)); return { info, error: success ? MpClientError.SUCCESS : MpClientError.DESERIALIZE_FAILED }; } async getServerNeighbors(count: number): Promise<{ neighbors: RelayNeighbor[]; count: number; error: MpClientError }> { const data = new Uint8Array([0x11, count]); const { response, respLen, error } = await this.clientRequest(data, data.length); if (error !== MpClientError.SUCCESS) { return { neighbors: [], count: 0, error }; } if (respLen < 1 || response![0] !== 0x18) { return { neighbors: [], count: 0, error: MpClientError.INVALID_RESPONSE }; } const numNeighbors = Math.floor((respLen - 1) / RELAY_NEIGHBOR_WIRE_LEN); const neighbors: RelayNeighbor[] = []; let actualCount = 0; for (let i = 0; i < numNeighbors; i++) { const { neighbor, success } = this.s2relayNeighbor(RELAY_NEIGHBOR_WIRE_LEN, response!.slice(1 + i * RELAY_NEIGHBOR_WIRE_LEN)); if (success) { neighbors.push(neighbor); actualCount++; } } return { neighbors, count: actualCount, error: MpClientError.SUCCESS }; } async pushPacket(packet: DataPacket): Promise<{ success: boolean; error: MpClientError }> { const sendData = this.dataPacket2s(packet); if (!sendData) { return { success: false, error: MpClientError.BAD_ARGUMENT }; } const sendLen = 1 + PACKET_SIZE; const fullData = new Uint8Array(sendLen); fullData[0] = 0x13; fullData.set(sendData, 1); const { response, respLen, error } = await this.clientRequest(fullData, sendLen); if (error !== MpClientError.SUCCESS) { return { success: false, error }; } if (respLen === 2 && response![0] === 0x1a) { return { success: response![1] !== 0, error: MpClientError.SUCCESS }; } return { success: false, error: MpClientError.INVALID_RESPONSE }; } async getPacket(packetHash: Uint8Array): Promise<{ packet: DataPacket; error: MpClientError }> { const sendLen = 1 + 32; const sendData = new Uint8Array(sendLen); sendData[0] = 0x14; sendData.set(packetHash, 1); const { response, respLen, error } = await this.clientRequest(sendData, sendLen); if (error !== MpClientError.SUCCESS) { return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error }; } if (respLen < 1 || response![0] !== 0x1b) { return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error: MpClientError.INVALID_RESPONSE }; } const { packet, success } = this.s2dataPacket(respLen - 1, response!.slice(1)); return { packet, error: success ? MpClientError.SUCCESS : MpClientError.DESERIALIZE_FAILED }; } async getFirehosePackets(offset: bigint, count: number): Promise<{ list: DataPacketList; error: MpClientError }> { const sendLen = 1 + 1 + 8; const sendData = new Uint8Array(sendLen); sendData[0] = 0x12; sendData[1] = count; this.writeU64LE(sendData, offset, 2); const { response, respLen, error } = await this.clientRequest(sendData, sendLen); if (error !== MpClientError.SUCCESS) { return { list: { packets: [], count: 0, new_offset: offset }, error }; } if (respLen < 9 || response![0] !== 0x19) { return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; } const newOffset = this.readU64LE(response!, 1); const availableBytes = respLen - 9; const availablePackets = Math.floor(availableBytes / PACKET_SIZE); const packets: DataPacket[] = []; let packetCount = 0; for (let i = 0; i < availablePackets; i++) { const { packet, success } = this.s2dataPacket(PACKET_SIZE, response!.slice(9 + i * PACKET_SIZE)); if (success) { packets.push(packet); packetCount++; } } return { list: { packets, count: packetCount, new_offset: newOffset }, error: MpClientError.SUCCESS }; } async createPacketFromBytes( input: Uint8Array, inputLen: number, recipientCount: number, recipients: Uint8Array[], authorKeys: KeyPair ): Promise { const packet: DataPacket = { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: BigInt(Date.now()), author: authorKeys.encryption_pub, author_signing_pub: authorKeys.signing_pub, recipient_count: recipientCount > MAX_RECIPIENTS ? MAX_RECIPIENTS : recipientCount, recipients: new Array(MAX_RECIPIENTS).fill(new Uint8Array(32)), data: new Uint8Array(65536), signature: new Uint8Array(64), }, }; const copyLen = inputLen > 65536 ? 65536 : inputLen; if (input && copyLen > 0) { packet.contents.data.set(input.slice(0, copyLen)); } if (recipients) { for (let i = 0; i < packet.contents.recipient_count; i++) { packet.contents.recipients[i] = recipients[i]; } } const msgLen = 1 + 8 + 32 + 32 + 1 + 32 * MAX_RECIPIENTS + 65536; const msgBuf = new Uint8Array(msgLen); msgBuf[0] = packet.contents.encrypted ? 1 : 0; this.writeU64LE(msgBuf, packet.contents.created_time_ms, 1); msgBuf.set(packet.contents.author, 9); msgBuf.set(packet.contents.author_signing_pub, 41); msgBuf[73] = packet.contents.recipient_count; for (let i = 0; i < MAX_RECIPIENTS; i++) { msgBuf.set(packet.contents.recipients[i], 74 + 32 * i); } msgBuf.set(packet.contents.data, 74 + 32 * MAX_RECIPIENTS); packet.contents.signature = await this.signData(msgBuf, authorKeys.signing_priv); return packet; } async createPacketFromString( input: string, recipientCount: number, recipients: Uint8Array[], authorKeys: KeyPair ): Promise<{ packet: DataPacket; error: MpClientError }> { try { const encoder = new TextEncoder(); const inputBytes = encoder.encode(input); const packet = await this.createPacketFromBytes(inputBytes, inputBytes.length, recipientCount, recipients, authorKeys); return { packet, error: MpClientError.SUCCESS }; } catch (err) { return { packet: { nonce: 0, hash: new Uint8Array(32), contents: { encrypted: false, created_time_ms: 0n, author: new Uint8Array(32), author_signing_pub: new Uint8Array(32), recipient_count: 0, recipients: [], data: new Uint8Array(65536), signature: new Uint8Array(64) } }, error: MpClientError.STRING_ENCODE_FAILED }; } } async runPow(packet: DataPacket, leadingZeros: number): Promise { const hashInputLen = 4 + PACKET_SIZE - 4; const hashInputBuffer = new Uint8Array(hashInputLen); const contentsBuffer = hashInputBuffer.subarray(4); contentsBuffer[0] = packet.contents.encrypted ? 1 : 0; this.writeU64LE(contentsBuffer, packet.contents.created_time_ms, 1); contentsBuffer.set(packet.contents.author, 9); contentsBuffer.set(packet.contents.author_signing_pub, 41); contentsBuffer[73] = packet.contents.recipient_count; for (let i = 0; i < MAX_RECIPIENTS; i++) { contentsBuffer.set(packet.contents.recipients[i], 74 + 32 * i); } contentsBuffer.set(packet.contents.data, 74 + 32 * MAX_RECIPIENTS); contentsBuffer.set(packet.contents.signature, 74 + 32 * MAX_RECIPIENTS + 65536); let nonce = 0; while (true) { this.writeU32LE(hashInputBuffer, nonce, 0); const hash = await this.sha256(hashInputBuffer); if (this.checkLeadingZeros(hash, leadingZeros)) { packet.nonce = nonce; packet.hash = hash; break; } nonce++; } return packet; } async queryAuthorPackets( authorKeys: KeyPair, count: number, offset: bigint ): Promise<{ list: DataPacketList; error: MpClientError }> { const msg = new Uint8Array(32 + 32 + 2 + 8); msg.set(authorKeys.encryption_pub, 0); msg.set(authorKeys.signing_pub, 32); this.writeU16LE(msg, count, 64); this.writeU64LE(msg, offset, 66); const sig = await this.signData(msg, authorKeys.signing_priv); const sendLen = 1 + 32 + 32 + 64 + 2 + 8; const sendData = new Uint8Array(sendLen); let off = 0; sendData[off++] = 0x16; sendData.set(authorKeys.encryption_pub, off); off += 32; sendData.set(authorKeys.signing_pub, off); off += 32; sendData.set(sig, off); off += 64; this.writeU16LE(sendData, count, off); off += 2; this.writeU64LE(sendData, offset, off); const { response, respLen, error } = await this.clientRequest(sendData, sendLen); if (error !== MpClientError.SUCCESS) { return { list: { packets: [], count: 0, new_offset: offset }, error }; } if (respLen < 11 || response![0] !== 0x1d) { return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; } const respCount = this.readU16LE(response!, 1); const newOffset = this.readU64LE(response!, 3); const expectedLen = 11 + respCount * PACKET_SIZE; if (respLen !== expectedLen) { return { list: { packets: [], count: 0, new_offset: offset }, error: MpClientError.INVALID_RESPONSE }; } const packets: DataPacket[] = []; let packetCount = 0; for (let i = 0; i < respCount; i++) { const { packet, success } = this.s2dataPacket(PACKET_SIZE, response!.slice(11 + i * PACKET_SIZE)); if (success) { packets.push(packet); packetCount++; } } return { list: { packets, count: packetCount, new_offset: newOffset }, error: MpClientError.SUCCESS }; } } export { MpClientError, RelayNeighbor, RelayInfo, DataPacket, DataPacketList, KeyPair, MoonPasteClient, };