feat(enclave): add ipfs support

This commit is contained in:
2026-01-07 20:16:57 -05:00
parent 68c4632f58
commit 2e2b5e8fa8
6 changed files with 1393 additions and 79 deletions

1233
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,7 @@
<div class="actions">
<button onclick="testExec()">Run</button>
<button onclick="setFilter('resource:accounts action:list')">Accounts</button>
<button onclick="setFilter('resource:accounts action:balances subject:sonr1example')">Balances</button>
<button onclick="setFilter('resource:credentials action:list')">Credentials</button>
<button onclick="setFilter('resource:sessions action:list')">Sessions</button>
</div>

36
main.go
View File

@@ -352,6 +352,8 @@ func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) {
},
}
return json.Marshal(accounts)
case "balances":
return fetchAccountBalances(params.Subject)
case "sign":
return json.Marshal(map[string]string{"signature": "placeholder"})
default:
@@ -359,6 +361,40 @@ func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) {
}
}
func fetchAccountBalances(address string) (json.RawMessage, error) {
if address == "" {
address = state.GetDID()
}
apiBase, ok := state.GetConfig("api_endpoint")
if !ok {
apiBase = "https://api.sonr.io"
}
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", apiBase, address)
pdk.Log(pdk.LogInfo, fmt.Sprintf("fetchAccountBalances: GET %s", url))
req := pdk.NewHTTPRequest(pdk.MethodGet, url)
req.SetHeader("Accept", "application/json")
res := req.Send()
status := res.Status()
if status < 200 || status >= 300 {
pdk.Log(pdk.LogError, fmt.Sprintf("fetchAccountBalances: HTTP %d", status))
return json.Marshal(map[string]any{
"error": "failed to fetch balances",
"status": status,
"address": address,
})
}
body := res.Body()
pdk.Log(pdk.LogDebug, fmt.Sprintf("fetchAccountBalances: received %d bytes", len(body)))
return body, nil
}
func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Action {
case "list":

View File

@@ -15,7 +15,7 @@
"dist"
],
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --format esm --target browser --sourcemap=external --external @extism/extism --entry-naming enclave.js && bun run tsc --emitDeclarationOnly --declaration -p src/tsconfig.json --outDir dist",
"build": "bun build ./src/index.ts --outdir ./dist --format esm --target browser --sourcemap=external --external @extism/extism --external @helia/unixfs --external multiformats/cid --external helia --entry-naming enclave.js && bun run tsc --emitDeclarationOnly --declaration -p src/tsconfig.json --outDir dist",
"typecheck": "tsc --noEmit -p src/tsconfig.json",
"clean": "rm -rf dist"
},
@@ -28,5 +28,10 @@
},
"peerDependencies": {
"@extism/extism": "^2.0.0-rc13"
},
"optionalDependencies": {
"@helia/unixfs": "^4.0.0",
"helia": "^5.0.0",
"multiformats": "^13.0.0"
}
}

View File

@@ -6,36 +6,28 @@ import type {
ExecOutput,
QueryOutput,
Resource,
HeliaInstance,
} 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);
* ```
*/
type UnixFS = {
addBytes(bytes: Uint8Array): Promise<{ toString(): string }>;
cat(cid: { toString(): string }): AsyncIterable<Uint8Array>;
};
export class Enclave {
private plugin: Plugin;
private logger: EnclaveOptions['logger'];
private debug: boolean;
private helia: HeliaInstance | null;
private unixfs: UnixFS | null = null;
private constructor(plugin: Plugin, options: EnclaveOptions = {}) {
this.plugin = plugin;
this.logger = options.logger ?? console;
this.debug = options.debug ?? false;
this.helia = options.ipfs ?? null;
}
/**
* 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 = {}
@@ -51,15 +43,23 @@ export class Enclave {
logger: options.debug ? (options.logger as Console) : undefined,
});
return new Enclave(plugin, options);
const enclave = new Enclave(plugin, options);
if (options.ipfs) {
await enclave.initIPFS();
}
return enclave;
}
private async initIPFS(): Promise<void> {
if (!this.helia) return;
const { unixfs } = await import('@helia/unixfs');
this.unixfs = unixfs(this.helia as Parameters<typeof unixfs>[0]);
this.log('IPFS initialized');
}
/**
* 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');
@@ -68,21 +68,55 @@ export class Enclave {
if (!result) throw new Error('generate: plugin returned no output');
const output = result.json() as GenerateOutput;
if (this.unixfs && output.database) {
const bytes = new Uint8Array(output.database);
const cid = await this.unixfs.addBytes(bytes);
output.cid = cid.toString();
this.log(`generate: stored to IPFS ${output.cid}`);
}
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');
async load(source: string | Uint8Array | number[]): Promise<LoadOutput> {
this.log('load: loading database');
const dbArray = database instanceof Uint8Array ? Array.from(database) : database;
const input = JSON.stringify({ database: dbArray });
let database: number[];
if (typeof source === 'string' && source.startsWith('bafy')) {
if (!this.unixfs) {
throw new Error('load: IPFS not configured, cannot resolve CID');
}
this.log(`load: fetching from IPFS ${source}`);
const { CID } = await import('multiformats/cid');
const cid = CID.parse(source);
const chunks: Uint8Array[] = [];
for await (const chunk of this.unixfs.cat(cid)) {
chunks.push(chunk);
}
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
const bytes = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.length;
}
database = Array.from(bytes);
this.log(`load: fetched ${database.length} bytes from IPFS`);
} else if (source instanceof Uint8Array) {
database = Array.from(source);
} else if (Array.isArray(source)) {
database = source;
} else {
throw new Error('load: invalid source type');
}
const input = JSON.stringify({ database });
const result = await this.plugin.call('load', input);
if (!result) throw new Error('load: plugin returned no output');
const output = result.json() as LoadOutput;
@@ -96,13 +130,41 @@ export class Enclave {
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 storeToIPFS(data: Uint8Array): Promise<string> {
if (!this.unixfs) {
throw new Error('storeToIPFS: IPFS not configured');
}
const cid = await this.unixfs.addBytes(data);
this.log(`storeToIPFS: stored ${data.length} bytes as ${cid.toString()}`);
return cid.toString();
}
async fetchFromIPFS(cidString: string): Promise<Uint8Array> {
if (!this.unixfs) {
throw new Error('fetchFromIPFS: IPFS not configured');
}
const { CID } = await import('multiformats/cid');
const cid = CID.parse(cidString);
const chunks: Uint8Array[] = [];
for await (const chunk of this.unixfs.cat(cid)) {
chunks.push(chunk);
}
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0);
const bytes = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.length;
}
this.log(`fetchFromIPFS: fetched ${bytes.length} bytes from ${cidString}`);
return bytes;
}
async exec(filter: string, token?: string): Promise<ExecOutput> {
this.log(`exec: executing filter "${filter}"`);
@@ -120,13 +182,6 @@ export class Enclave {
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,
@@ -139,12 +194,6 @@ export class Enclave {
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)'}`);
@@ -169,22 +218,20 @@ export class Enclave {
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();
}
get ipfsEnabled(): boolean {
return this.unixfs !== null;
}
private log(message: string, level: 'log' | 'error' | 'warn' | 'info' | 'debug' = 'debug'): void {
if (this.debug && this.logger) {
this.logger[level](`[Enclave] ${message}`);
@@ -192,22 +239,6 @@ export class Enclave {
}
}
/**
* 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 = {}

View File

@@ -17,6 +17,8 @@ export interface GenerateOutput {
did: string;
/** Serialized database buffer for storage */
database: number[];
/** IPFS CID of stored database (if IPFS enabled) */
cid?: string;
}
// ============================================================================
@@ -105,10 +107,16 @@ export interface Credential {
// ============================================================================
export interface EnclaveOptions {
/** Custom logger (defaults to console) */
logger?: Pick<Console, 'log' | 'error' | 'warn' | 'info' | 'debug'>;
/** Enable debug logging */
debug?: boolean;
ipfs?: HeliaInstance;
}
export interface HeliaInstance {
blockstore: unknown;
datastore: unknown;
start(): Promise<void>;
stop(): Promise<void>;
}
// ============================================================================