Files
motr-enclave/src/enclave.ts

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);
}