Formatting
This commit is contained in:
+213
-214
@@ -1,267 +1,266 @@
|
||||
import { binToHex, flattenBinArray, secp256k1, sha256 } from "@bitauth/libauth";
|
||||
import { binToHex, flattenBinArray, secp256k1, sha256 } from '@bitauth/libauth';
|
||||
|
||||
import { toExtendedJson, EventEmitter, fromExtendedJson } from "@xo-cash/utils";
|
||||
import type { WriteRequest } from "../types.js";
|
||||
import z from "zod";
|
||||
import { toExtendedJson, EventEmitter, fromExtendedJson } from '@xo-cash/utils';
|
||||
import type { WriteRequest } from '../types.ts';
|
||||
import z from 'zod';
|
||||
|
||||
export type AuthenticatedRequestHeaders = {
|
||||
'X-Public-Key': string;
|
||||
'X-Timestamp': string;
|
||||
'X-Signature': string;
|
||||
'X-Public-Key': string;
|
||||
'X-Timestamp': string;
|
||||
'X-Signature': string;
|
||||
};
|
||||
|
||||
export type SignedPayload = { publicKey: string; signature: string };
|
||||
|
||||
export type XOSyncClientEventMap<T = unknown> = {
|
||||
'connection-updated': 'connected' | 'disconnected';
|
||||
'resource-instance-updated': Resource<T> & {
|
||||
resourceId: string;
|
||||
};
|
||||
'subscriptions-updated': Set<string>;
|
||||
'error': Error;
|
||||
'connection-updated': 'connected' | 'disconnected';
|
||||
'resource-instance-updated': Resource<T> & {
|
||||
resourceId: string;
|
||||
};
|
||||
'subscriptions-updated': Set<string>;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type Resource<PayloadType = unknown> = z.infer<typeof BaseSyncClient.resourceInstanceSchema> & {
|
||||
value: PayloadType;
|
||||
value: PayloadType;
|
||||
};
|
||||
|
||||
export type ResourcesRecord<PayloadType = unknown> = Record<string, { instances: Resource<PayloadType>[] }>;
|
||||
|
||||
export abstract class BaseSyncClient<T = unknown> extends EventEmitter<XOSyncClientEventMap<T>> {
|
||||
/**
|
||||
* Connects to the sync server
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Connects to the sync server
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnects from the sync server
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Disconnects from the sync server
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Sign and write one resource
|
||||
* @param resourceId - The resource ID to write to
|
||||
* @param value - The value to write
|
||||
* @returns The Resource Object with the decoded value
|
||||
*/
|
||||
async write(resourceId: string, value: T): Promise<Resource<T>[]> {
|
||||
// Write the resource to the server
|
||||
const response = await this.writeMany({ [resourceId]: value });
|
||||
|
||||
// Get the resource from the response
|
||||
const resource = response[resourceId];
|
||||
/**
|
||||
* Sign and write one resource
|
||||
* @param resourceId - The resource ID to write to
|
||||
* @param value - The value to write
|
||||
* @returns The Resource Object with the decoded value
|
||||
*/
|
||||
async write(resourceId: string, value: T): Promise<Resource<T>[]> {
|
||||
// Write the resource to the server
|
||||
const response = await this.writeMany({ [resourceId]: value });
|
||||
|
||||
// If the resource is not found, throw an error
|
||||
if (!resource) {
|
||||
throw new Error(`Resource ${resourceId} not found`);
|
||||
// Get the resource from the response
|
||||
const resource = response[resourceId];
|
||||
|
||||
// If the resource is not found, throw an error
|
||||
if (!resource) {
|
||||
throw new Error(`Resource ${resourceId} not found`);
|
||||
}
|
||||
|
||||
// Return the resource instances
|
||||
return resource.instances;
|
||||
}
|
||||
|
||||
// Return the resource instances
|
||||
return resource.instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes multiple values to resources
|
||||
* @param resources - The resources to write to
|
||||
* @returns The resources with the written values
|
||||
*/
|
||||
abstract writeMany(resources: Record<string, T>): Promise<ResourcesRecord<T>>;
|
||||
/**
|
||||
* Writes multiple values to resources
|
||||
* @param resources - The resources to write to
|
||||
* @returns The resources with the written values
|
||||
*/
|
||||
abstract writeMany(resources: Record<string, T>): Promise<ResourcesRecord<T>>;
|
||||
|
||||
/**
|
||||
* Reads a value from a resource
|
||||
* @param resourceId - The resource ID to read from
|
||||
* @returns The resource with the read value
|
||||
*/
|
||||
async read(resourceId: string): Promise<Resource<T>[]> {
|
||||
// Read the resource from the server
|
||||
const response = await this.readMany([ resourceId ]);
|
||||
/**
|
||||
* Reads a value from a resource
|
||||
* @param resourceId - The resource ID to read from
|
||||
* @returns The resource with the read value
|
||||
*/
|
||||
async read(resourceId: string): Promise<Resource<T>[]> {
|
||||
// Read the resource from the server
|
||||
const response = await this.readMany([ resourceId ]);
|
||||
|
||||
// Get the resource from the response
|
||||
const resource = response[resourceId];
|
||||
// Get the resource from the response
|
||||
const resource = response[resourceId];
|
||||
|
||||
// If the resource is not found, throw an error
|
||||
if (!resource) {
|
||||
throw new Error(`Resource ${resourceId} not found`);
|
||||
// If the resource is not found, throw an error
|
||||
if (!resource) {
|
||||
throw new Error(`Resource ${resourceId} not found`);
|
||||
}
|
||||
|
||||
// Return the resource instances
|
||||
return resource.instances;
|
||||
}
|
||||
|
||||
// Return the resource instances
|
||||
return resource.instances;
|
||||
}
|
||||
/**
|
||||
* Reads multiple values from resources
|
||||
* @param resourceIds - The resource IDs to read from
|
||||
* @returns The resources with the read values
|
||||
*/
|
||||
abstract readMany(resourceIds: string[]): Promise<ResourcesRecord<T>>;
|
||||
|
||||
/**
|
||||
* Reads multiple values from resources
|
||||
* @param resourceIds - The resource IDs to read from
|
||||
* @returns The resources with the read values
|
||||
*/
|
||||
abstract readMany(resourceIds: string[]): Promise<ResourcesRecord<T>>;
|
||||
/**
|
||||
* Subscribes to a resource
|
||||
* @param resourceId - The resource ID to subscribe to
|
||||
*/
|
||||
abstract subscribe(resourceId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Subscribes to a resource
|
||||
* @param resourceId - The resource ID to subscribe to
|
||||
*/
|
||||
abstract subscribe(resourceId: string): Promise<void>;
|
||||
/**
|
||||
* Unsubscribes from a resource
|
||||
* @param resourceId - The resource ID to unsubscribe from
|
||||
*/
|
||||
abstract unsubscribe(resourceId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Unsubscribes from a resource
|
||||
* @param resourceId - The resource ID to unsubscribe from
|
||||
*/
|
||||
abstract unsubscribe(resourceId: string): Promise<void>;
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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');
|
||||
// Hash the private key and resource ID
|
||||
return sha256.hash(flattenBinArray([ privateKey, resourceIdBytes ]));
|
||||
}
|
||||
|
||||
// Sign the payload
|
||||
const signature = secp256k1.signMessageHashDER(privateKey, payloadHash);
|
||||
/**
|
||||
* 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));
|
||||
|
||||
// If the signature is a string, throw an error
|
||||
if (typeof signature === 'string') {
|
||||
throw new Error('Failed to sign message');
|
||||
// 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),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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)}`;
|
||||
|
||||
// Create the payload
|
||||
const timestamp = Date.now();
|
||||
const payload = `${timestamp}:${resourceId}:${toExtendedJson(value)}`;
|
||||
// Sign the payload
|
||||
const { publicKey, signature } = this.signPayload(derivedKey, payload);
|
||||
|
||||
// Sign the payload
|
||||
const { publicKey, signature } = this.signPayload(derivedKey, payload);
|
||||
return {
|
||||
id: resourceId,
|
||||
value,
|
||||
publicKey,
|
||||
timestamp,
|
||||
signature,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// 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}`;
|
||||
|
||||
// Create the payload as `Timestamp:Path:Body`
|
||||
const payload = `${timestamp}:${path}:${bodyStr}`;
|
||||
// Sign the payload
|
||||
const { publicKey, signature } = this.signPayload(privateKey, payload);
|
||||
|
||||
// Sign the payload
|
||||
const { publicKey, signature } = this.signPayload(privateKey, payload);
|
||||
return {
|
||||
'X-Public-Key': publicKey,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Signature': signature,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'X-Public-Key': publicKey,
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
'X-Signature': signature,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Decodes the 'blob' field of the Resource Object from the server from a read/write
|
||||
* @param response - The ResourceResponse Object from the server from a read/write
|
||||
* @returns The Resource Object with the decoded value
|
||||
*/
|
||||
static decodeResourcesResponse<T = unknown>(response: z.infer<typeof BaseSyncClient.resourcesResponseSchema>): ResourcesRecord<T> {
|
||||
return Object.entries(response.resources).reduce<ResourcesRecord<T>>((acc, [ resourceId, resource ]) => {
|
||||
// Get the instances
|
||||
const instances = resource.instances;
|
||||
|
||||
/**
|
||||
* Decodes the 'blob' field of the Resource Object from the server from a read/write
|
||||
* @param response - The ResourceResponse Object from the server from a read/write
|
||||
* @returns The Resource Object with the decoded value
|
||||
*/
|
||||
static decodeResourcesResponse<T = unknown>(response: z.infer<typeof BaseSyncClient.resourcesResponseSchema>): ResourcesRecord<T> {
|
||||
return Object.entries(response.resources).reduce<ResourcesRecord<T>>((acc, [resourceId, resource]) => {
|
||||
// Get the instances
|
||||
const instances = resource.instances;
|
||||
// Decode the instances
|
||||
const decodedInstances = instances.map((instance) => {
|
||||
return BaseSyncClient.decodeBlobValue<T>(instance);
|
||||
});
|
||||
|
||||
// Decode the instances
|
||||
const decodedInstances = instances.map((instance) => {
|
||||
return BaseSyncClient.decodeBlobValue<T>(instance);
|
||||
});
|
||||
// Add the decoded instances to the accumulator
|
||||
acc[resourceId] = {
|
||||
...resource,
|
||||
instances: decodedInstances,
|
||||
};
|
||||
|
||||
// Add the decoded instances to the accumulator
|
||||
acc[resourceId] = {
|
||||
...resource,
|
||||
instances: decodedInstances,
|
||||
};
|
||||
// Return the accumulator
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Return the accumulator
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
static decodeBlobValue<T = unknown>(instance: z.infer<typeof BaseSyncClient.resourceInstanceSchema>): Resource<T> {
|
||||
const decodedBlob = new TextDecoder().decode(instance.blob);
|
||||
const value = fromExtendedJson(decodedBlob) as T;
|
||||
|
||||
static decodeBlobValue<T = unknown>(instance: z.infer<typeof BaseSyncClient.resourceInstanceSchema>): Resource<T> {
|
||||
const decodedBlob = new TextDecoder().decode(instance.blob);
|
||||
const value = fromExtendedJson(decodedBlob) as T;
|
||||
return {
|
||||
...instance,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...instance,
|
||||
value,
|
||||
};
|
||||
}
|
||||
static resourceInstanceSchema = z.object({
|
||||
resourceId: z.string(),
|
||||
blob: z.instanceof(Uint8Array),
|
||||
timestamp: z.number(),
|
||||
publicKey: z.string(),
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
static resourceInstanceSchema = z.object({
|
||||
resourceId: z.string(),
|
||||
blob: z.instanceof(Uint8Array),
|
||||
timestamp: z.number(),
|
||||
publicKey: z.string(),
|
||||
signature: z.string(),
|
||||
});
|
||||
static rawResourcesSchema = z.object({
|
||||
instances: z.array(BaseSyncClient.resourceInstanceSchema),
|
||||
});
|
||||
|
||||
static rawResourcesSchema = z.object({
|
||||
instances: z.array(BaseSyncClient.resourceInstanceSchema),
|
||||
});
|
||||
|
||||
static resourcesResponseSchema = z.object({
|
||||
resources: z.record(
|
||||
// Resource ID
|
||||
z.string(),
|
||||
// Resource instances
|
||||
BaseSyncClient.rawResourcesSchema,
|
||||
),
|
||||
});
|
||||
static resourcesResponseSchema = z.object({
|
||||
resources: z.record(
|
||||
// Resource ID
|
||||
z.string(),
|
||||
// Resource instances
|
||||
BaseSyncClient.rawResourcesSchema,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user