122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
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<void>;
|
|
abstract disconnect(): Promise<void>;
|
|
abstract write(resourceId: string, value: Record<string, unknown>): Promise<unknown>;
|
|
abstract read(resourceId: string): Promise<unknown>;
|
|
abstract subscribe(resourceId: string): Promise<void>;
|
|
abstract unsubscribe(resourceId: string): Promise<unknown>;
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
}
|
|
} |