import { binToHex, flattenBinArray, secp256k1, sha256 } from "@bitauth/libauth"; import { toExtendedJson } from "@xo-cash/utils"; import type { WriteRequest } from "../types.js"; export type AuthenticatedRequestHeaders = { 'X-Public-Key': string; 'X-Timestamp': string; 'X-Signature': string; }; export type SignedPayload = { publicKey: string; signature: string }; export abstract class SyncClient { abstract connect(): Promise; abstract disconnect(): Promise; abstract write(resourceId: string, value: Record): Promise; abstract read(resourceId: string): Promise; abstract subscribe(resourceId: string): Promise; abstract unsubscribe(resourceId: string): Promise; /** * Derives the resource private key from the private key and resource ID * @param privateKey - The private key to derive the resource private key from * @param resourceId - The resource ID to derive the resource private key from * @returns The resource private key */ static deriveResourcePrivateKey(privateKey: Uint8Array, resourceId: string): Uint8Array { // Convert the resource ID to bytes const resourceIdBytes = new TextEncoder().encode(resourceId); // Hash the private key and resource ID return sha256.hash(flattenBinArray([ privateKey, resourceIdBytes ])); } /** * Signs a payload with the private key and returns the public key and signature * @param privateKey - The private key to sign the payload with * @param payload - The payload to sign * @returns The public key and signature */ static signPayload(privateKey: Uint8Array, payload: string): SignedPayload { // Convert the payload to Binary and hash it const payloadHash = sha256.hash(new TextEncoder().encode(payload)); // Derive the public key const publicKey = secp256k1.derivePublicKeyCompressed(privateKey); // If the public key is a string, throw an error if (typeof publicKey === 'string') { throw new Error('Failed to derive public key'); } // Sign the payload const signature = secp256k1.signMessageHashDER(privateKey, payloadHash); // If the signature is a string, throw an error if (typeof signature === 'string') { throw new Error('Failed to sign message'); } return { publicKey: binToHex(publicKey), signature: binToHex(signature), }; } /** * Signs a write request with the private key and returns the write request * @param privateKey - The private key to sign the write request with * @param resourceId - The resource ID to write to * @param value - The value to write * @returns The write request */ static signWriteRequest(privateKey: Uint8Array, resourceId: string, value: Uint8Array): WriteRequest { // Derive the resource private key const derivedKey = this.deriveResourcePrivateKey(privateKey, resourceId); // Create the payload const timestamp = Date.now(); const payload = `${timestamp}:${resourceId}:${toExtendedJson(value)}`; // Sign the payload const { publicKey, signature } = this.signPayload(derivedKey, payload); return { id: resourceId, value, publicKey, timestamp, signature, }; } /** * Signs a request with the private key and returns the public key, timestamp, and signature * @param privateKey - The private key to sign the request with * @param path - The path of the request * @param body - The body of the request * @returns The public key, timestamp, and signature */ static signRequest(privateKey: Uint8Array, path: string, body: unknown): AuthenticatedRequestHeaders { // Create the payload const timestamp = Date.now(); // Convert the body to a string if it isn't already a string const bodyStr = typeof body === 'string' ? body : toExtendedJson(body); // Create the payload as `Timestamp:Path:Body` const payload = `${timestamp}:${path}:${bodyStr}`; // Sign the payload const { publicKey, signature } = this.signPayload(privateKey, payload); return { 'X-Public-Key': publicKey, 'X-Timestamp': timestamp.toString(), 'X-Signature': signature, }; } }