217 lines
6.2 KiB
TypeScript
217 lines
6.2 KiB
TypeScript
import createPlugin, { type Plugin } from '@extism/extism';
|
|
import type {
|
|
EnclaveOptions,
|
|
GenerateOutput,
|
|
LoadOutput,
|
|
ExecOutput,
|
|
QueryOutput,
|
|
Resource,
|
|
} from './types';
|
|
|
|
/**
|
|
* Motr Enclave - WebAssembly plugin wrapper for encrypted key storage
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { createEnclave } from '@sonr/motr-enclave';
|
|
*
|
|
* const enclave = await createEnclave('/enclave.wasm');
|
|
* const { did, database } = await enclave.generate(credential);
|
|
* ```
|
|
*/
|
|
export class Enclave {
|
|
private plugin: Plugin;
|
|
private logger: EnclaveOptions['logger'];
|
|
private debug: boolean;
|
|
|
|
private constructor(plugin: Plugin, options: EnclaveOptions = {}) {
|
|
this.plugin = plugin;
|
|
this.logger = options.logger ?? console;
|
|
this.debug = options.debug ?? false;
|
|
}
|
|
|
|
/**
|
|
* Create an Enclave instance from a WASM source
|
|
*
|
|
* @param wasm - URL string, file path, or Uint8Array of WASM bytes
|
|
* @param options - Configuration options
|
|
*/
|
|
static async create(
|
|
wasm: string | Uint8Array,
|
|
options: EnclaveOptions = {}
|
|
): Promise<Enclave> {
|
|
const manifest =
|
|
typeof wasm === 'string'
|
|
? { wasm: [{ url: wasm }] }
|
|
: { wasm: [{ data: wasm }] };
|
|
|
|
const plugin = await createPlugin(manifest, {
|
|
useWasi: true,
|
|
runInWorker: true,
|
|
logger: options.debug ? (options.logger as Console) : undefined,
|
|
});
|
|
|
|
return new Enclave(plugin, options);
|
|
}
|
|
|
|
/**
|
|
* Initialize database with WebAuthn credential
|
|
*
|
|
* @param credential - Base64-encoded PublicKeyCredential from WebAuthn registration
|
|
* @returns DID and serialized database buffer
|
|
*/
|
|
async generate(credential: string): Promise<GenerateOutput> {
|
|
this.log('generate: starting with credential');
|
|
|
|
const input = JSON.stringify({ credential });
|
|
const result = await this.plugin.call('generate', input);
|
|
if (!result) throw new Error('generate: plugin returned no output');
|
|
const output = result.json() as GenerateOutput;
|
|
|
|
this.log(`generate: created DID ${output.did}`);
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Load database from serialized buffer
|
|
*
|
|
* @param database - Raw database bytes (from IPFS or storage)
|
|
* @returns Success status and loaded DID
|
|
*/
|
|
async load(database: Uint8Array | number[]): Promise<LoadOutput> {
|
|
this.log('load: loading database from buffer');
|
|
|
|
const dbArray = database instanceof Uint8Array ? Array.from(database) : database;
|
|
const input = JSON.stringify({ database: dbArray });
|
|
const result = await this.plugin.call('load', input);
|
|
if (!result) throw new Error('load: plugin returned no output');
|
|
const output = result.json() as LoadOutput;
|
|
|
|
if (output.success) {
|
|
this.log(`load: loaded database for DID ${output.did}`);
|
|
} else {
|
|
this.log(`load: failed - ${output.error}`, 'error');
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Execute action with filter syntax
|
|
*
|
|
* @param filter - GitHub-style filter (e.g., "resource:accounts action:list")
|
|
* @param token - Optional UCAN token for authorization
|
|
* @returns Action result
|
|
*/
|
|
async exec(filter: string, token?: string): Promise<ExecOutput> {
|
|
this.log(`exec: executing filter "${filter}"`);
|
|
|
|
const input = JSON.stringify({ filter, token });
|
|
const result = await this.plugin.call('exec', input);
|
|
if (!result) throw new Error('exec: plugin returned no output');
|
|
const output = result.json() as ExecOutput;
|
|
|
|
if (output.success) {
|
|
this.log('exec: completed successfully');
|
|
} else {
|
|
this.log(`exec: failed - ${output.error}`, 'error');
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Execute action with typed parameters
|
|
*
|
|
* @param resource - Resource type (accounts, credentials, sessions, grants)
|
|
* @param action - Action to perform
|
|
* @param options - Additional options
|
|
*/
|
|
async execute(
|
|
resource: Resource,
|
|
action: string,
|
|
options: { subject?: string; token?: string } = {}
|
|
): Promise<ExecOutput> {
|
|
let filter = `resource:${resource} action:${action}`;
|
|
if (options.subject) {
|
|
filter += ` subject:${options.subject}`;
|
|
}
|
|
return this.exec(filter, options.token);
|
|
}
|
|
|
|
/**
|
|
* Query DID document and associated resources
|
|
*
|
|
* @param did - DID to resolve (empty for current DID)
|
|
* @returns Resolved DID document with resources
|
|
*/
|
|
async query(did: string = ''): Promise<QueryOutput> {
|
|
this.log(`query: resolving DID ${did || '(current)'}`);
|
|
|
|
const input = JSON.stringify({ did });
|
|
const result = await this.plugin.call('query', input);
|
|
if (!result) throw new Error('query: plugin returned no output');
|
|
const output = result.json() as QueryOutput;
|
|
|
|
this.log(`query: resolved DID ${output.did}`);
|
|
return output;
|
|
}
|
|
|
|
async ping(message: string = 'hello'): Promise<{ success: boolean; message: string; echo: string }> {
|
|
this.log(`ping: sending "${message}"`);
|
|
|
|
const input = JSON.stringify({ message });
|
|
const result = await this.plugin.call('ping', input);
|
|
if (!result) throw new Error('ping: plugin returned no output');
|
|
const output = result.json() as { success: boolean; message: string; echo: string };
|
|
|
|
this.log(`ping: received ${output.success ? 'pong' : 'error'}`);
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Reset plugin state
|
|
*/
|
|
async reset(): Promise<void> {
|
|
this.log('reset: clearing plugin state');
|
|
await this.plugin.reset();
|
|
}
|
|
|
|
/**
|
|
* Close and cleanup plugin resources
|
|
*/
|
|
async close(): Promise<void> {
|
|
this.log('close: releasing plugin resources');
|
|
await this.plugin.close();
|
|
}
|
|
|
|
private log(message: string, level: 'log' | 'error' | 'warn' | 'info' | 'debug' = 'debug'): void {
|
|
if (this.debug && this.logger) {
|
|
this.logger[level](`[Enclave] ${message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an Enclave instance
|
|
*
|
|
* @param wasm - URL string, file path, or Uint8Array of WASM bytes
|
|
* @param options - Configuration options
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // From URL
|
|
* const enclave = await createEnclave('/enclave.wasm');
|
|
*
|
|
* // From bytes
|
|
* const wasmBytes = await fetch('/enclave.wasm').then(r => r.arrayBuffer());
|
|
* const enclave = await createEnclave(new Uint8Array(wasmBytes));
|
|
* ```
|
|
*/
|
|
export async function createEnclave(
|
|
wasm: string | Uint8Array,
|
|
options: EnclaveOptions = {}
|
|
): Promise<Enclave> {
|
|
return Enclave.create(wasm, options);
|
|
}
|