Add Extism JS Async Lib and State #1

Merged
pn merged 22 commits from feat/sdk into main 2026-01-08 01:06:58 +00:00
34 changed files with 4000 additions and 724 deletions

BIN
.github/db-schema.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
example/node_modules
build
example/enclave.wasm
src/dist
src/node_modules
dist
node_modules

100
Makefile
View File

@@ -1,84 +1,72 @@
.PHONY: all generate build build-debug build-opt test test-cover test-plugin lint fmt vet clean tidy deps verify serve help
.PHONY: start deps build sdk dev test test-plugin lint fmt clean help
MODULE := enclave
BINARY := enclave.wasm
BUILD_DIR := example
all: generate build
# === Primary Commands ===
generate:
@sqlc generate
start: deps build sdk dev
deps:
@command -v sqlc >/dev/null || go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
@command -v golangci-lint >/dev/null || go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@bun install
@cd example && bun install
build:
@GOOS=wasip1 GOARCH=wasm go build -o $(BUILD_DIR)/$(BINARY) .
@echo "Building WASM plugin..."
@GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) .
@echo "Built $(BUILD_DIR)/$(BINARY)"
build-debug:
@GOOS=wasip1 GOARCH=wasm go build -gcflags="all=-N -l" -o $(BUILD_DIR)/$(BINARY) .
sdk:
@echo "Building TypeScript SDK..."
@bun run build
@echo "Built dist/enclave.js"
build-opt:
@GOOS=wasip1 GOARCH=wasm go build -ldflags="-s -w" -o $(BUILD_DIR)/$(BINARY) .
@wasm-opt -Os $(BUILD_DIR)/$(BINARY) -o $(BUILD_DIR)/$(BINARY)
dev:
@echo "Starting dev server at http://localhost:8080"
@cd example && bun run dev
# === Testing ===
test:
@go test -v ./...
test-cover:
@go test -coverprofile=coverage.out ./...
@go tool cover -html=coverage.out -o coverage.html
test-plugin: build
@echo "Testing generate()..."
@extism call $(BUILD_DIR)/$(BINARY) generate --input '{"credential":"dGVzdC1jcmVkZW50aWFs"}' --wasi
@echo "\nTesting query()..."
@extism call $(BUILD_DIR)/$(BINARY) query --input '{"did":""}' --wasi
test-plugin:
@extism call $(BUILD_DIR)/$(BINARY) generate --input '{"credential":"dGVzdA=="}' --wasi
test-sdk: sdk
@cd example && bun run test
serve: build
@echo "Starting Vite dev server at http://localhost:8080"
@cd example && npm run dev
# === Code Quality ===
lint:
@golangci-lint run ./...
fmt:
@go fmt ./...
@gofumpt -w .
@bun run --filter '*' format 2>/dev/null || true
vet:
@go vet ./...
generate:
@sqlc generate
# === Utilities ===
clean:
@rm -f $(BUILD_DIR)/$(BINARY)
@rm -f coverage.out coverage.html
tidy:
@go mod tidy
deps:
@go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@go install mvdan.cc/gofumpt@latest
@cd example && npm install
@echo "Install Extism CLI: https://extism.org/docs/install"
verify: fmt vet lint test
@rm -rf dist
help:
@echo "Motr Enclave - Extism Plugin (Go 1.25/wasip1)"
@echo "Motr Enclave"
@echo ""
@echo "Build targets:"
@echo " build - Build WASM plugin for wasip1"
@echo " build-debug - Build with debug symbols"
@echo " build-opt - Build optimized (requires wasm-opt)"
@echo ""
@echo "Development targets:"
@echo " generate - Run sqlc to generate Go code"
@echo " test - Run tests"
@echo " test-cover - Run tests with coverage"
@echo " test-plugin - Test plugin with Extism CLI"
@echo " serve - Build and serve example/ for browser testing"
@echo " lint - Run golangci-lint"
@echo " fmt - Format code"
@echo " vet - Run go vet"
@echo " verify - Run fmt, vet, lint, and test"
@echo ""
@echo "Utility targets:"
@echo " clean - Remove build artifacts"
@echo " tidy - Run go mod tidy"
@echo " deps - Install development dependencies"
@echo " make start - Full setup + dev server (recommended)"
@echo " make build - Build WASM plugin"
@echo " make sdk - Build TypeScript SDK"
@echo " make dev - Start dev server"
@echo " make test - Run Go tests"
@echo " make test-plugin - Test plugin with Extism CLI"
@echo " make test-sdk - Run SDK tests in browser"
@echo " make clean - Remove build artifacts"

186
README.md
View File

@@ -1,151 +1,83 @@
# Motr Enclave
Motr Enclave is an [Extism](https://extism.org) plugin that provides encrypted key storage for the Nebula wallet. Built with Go 1.25+ and compiled for the `wasip1` target, it embeds a SQLite database for managing sensitive identity and cryptographic material.
Extism WASM plugin providing encrypted key storage for Nebula wallet. Built with Go 1.25+ for `wasip1`.
## Overview
## Quick Start
The enclave runs as a portable WASM plugin with an embedded SQLite database. All data is encrypted at rest using a secret derived from the user's WebAuthn credentials. The plugin can be loaded by any Extism host runtime (browser, Node.js, Python, Rust, etc.).
```bash
make start
```
## Architecture
This single command:
1. Installs dependencies (Go, Bun)
2. Builds the WASM plugin
3. Builds the TypeScript SDK
4. Starts the dev server at http://localhost:8080
```text
┌─────────────────────────────────────────────────────────────────────┐
│ NEBULA WALLET │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ Extism Plugin │ │ API Clients (Live Data) │ │
│ │ (Go/wasip1) │ │ │ │
│ ├──────────────────────┤ ├──────────────────────────────────┤ │
│ │ • WebAuthn Creds │ │ • Token Balances │ │
│ │ • MPC Key Shares │ │ • Transaction History │ │
│ │ • UCAN Tokens │ │ • NFT Holdings │ │
│ │ • Device Sessions │ │ • Price Data │ │
│ │ • Service Grants │ │ • Chain State │ │
│ │ • DID State │ │ • Network Status │ │
│ │ • Capability Delgs │ │ │ │
│ └──────────────────────┘ └──────────────────────────────────┘ │
│ │ │ │
│ │ Encrypted with │ REST/gRPC │
│ │ WebAuthn-derived key │ │
│ ▼ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────────────────┐ │
│ │ IPFS (CID Storage) │ │ Sonr Protocol / Indexers │ │
│ │ Browser Storage │ │ (PostgreSQL for live queries) │ │
│ └──────────────────────┘ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
## Manual Setup
```bash
make deps # Install tooling
make build # Build WASM plugin
make sdk # Build TypeScript SDK
make dev # Start dev server
```
## Usage
### TypeScript/ESM
```typescript
import { createEnclave } from '@sonr/motr-enclave';
const enclave = await createEnclave('/enclave.wasm');
const { did, database } = await enclave.generate(credential);
await enclave.load(database);
const accounts = await enclave.exec('resource:accounts action:list');
const didDoc = await enclave.query();
```
### CLI
```bash
make test-plugin
```
## Plugin Functions
The Extism plugin exposes four host-callable functions:
| Function | Input | Output |
|----------|-------|--------|
| `generate` | WebAuthn credential (base64) | DID + database buffer |
| `load` | Database buffer | Success status |
| `exec` | Filter string + optional UCAN | Action result |
| `query` | DID (optional) | DID document |
### `generate()`
## Database Schema
Initializes the database and generates initial MPC key shares.
The database schema is defined in `db/schema.sql`.
- **Input**: Base64-encoded `PublicKeyCredential` from a WebAuthn registration ceremony
- **Output**: Serialized database buffer ready for storage
- **Side Effects**: Creates DID document, credentials, and key shares
### `load()`
Loads an existing database from a serialized buffer.
- **Input**: Raw database bytes (typically resolved from an IPFS CID)
- **Output**: Success/error status
- **Usage**: Client resolves CID from IPFS, passes buffer to plugin
### `exec()`
Executes an action by parsing a UCAN token with GitHub-style filter syntax.
- **Input**: Filter string (e.g., `resource:accounts action:sign subject:did:sonr:abc`)
- **Output**: Action result or error
- **Authorization**: Validates UCAN capability chain before execution
### `query()`
Resolves a DID to its document and queries associated resources.
- **Input**: DID string (e.g., `did:sonr:abc123`)
- **Output**: JSON-encoded DID document with resolved resources
- **Usage**: Lookup identity state, verification methods, accounts
## Data Storage
The embedded SQLite database stores security-critical information:
- **Identity**: DID documents and verification methods
- **Credentials**: WebAuthn registrations for device-bound authentication
- **Key Material**: MPC key shares and derived blockchain accounts
- **Authorization**: UCAN tokens, capability delegations, and service grants
- **State**: Active sessions and protocol sync checkpoints
## Security Model
The enclave uses WebAuthn PRF (Pseudo-Random Function) extension to derive encryption keys. During authentication, the PRF output is passed through HKDF to generate a 256-bit AES key. This key encrypts the SQLite database before serialization to IPFS or local storage.
![[.github/db-schema.png]]
## Project Structure
```
motr-enclave/
├── db/
├── schema.sql # Database schema (12 tables)
│ └── query.sql # SQLC query definitions
├── example/
│ ├── index.html # Browser test UI
│ └── test.js # Extism JS SDK test harness
├── sqlc.yaml # SQLC configuration
├── Makefile # Build commands
└── main.go # Plugin entry point
├── main.go # Go plugin source
├── src/ # TypeScript SDK
├── dist/ # Built SDK
├── example/ # Browser test app
├── db/ # SQLite schema
└── Makefile
```
## Development
### Prerequisites
- [Go](https://go.dev/doc/install) 1.25+
- [SQLC](https://sqlc.dev/) for database code generation
- [Extism CLI](https://extism.org/docs/install) (optional, for testing)
### Building
```bash
make build # Build WASM for wasip1
make generate # Regenerate SQLC database code
make test # Run tests
make test # Run Go tests
make lint # Run linter
make clean # Remove build artifacts
```
### Testing the Plugin
**CLI Testing:**
```bash
extism call ./build/enclave.wasm generate --input '{"credential": "dGVzdA=="}' --wasi
extism call ./build/enclave.wasm query --input '{"did": "did:sonr:abc123"}' --wasi
```
**Browser Testing:**
```bash
make serve
# Open http://localhost:8080/example/ in your browser
```
The browser test UI provides interactive testing of all plugin functions with real-time output.
## Tables
| Table | Description |
|-------|-------------|
| `did_documents` | Local cache of Sonr DID state |
| `verification_methods` | Cryptographic keys for DID operations |
| `credentials` | WebAuthn credential storage |
| `key_shares` | MPC/TSS key shares (encrypted) |
| `accounts` | Derived blockchain accounts |
| `ucan_tokens` | Capability authorization tokens |
| `ucan_revocations` | Revoked UCAN registry |
| `sessions` | Active device sessions |
| `services` | Connected third-party dApps |
| `grants` | Service permissions |
| `delegations` | Capability delegation chains |
| `sync_checkpoints` | Protocol sync state |

32
bun.lock Normal file
View File

@@ -0,0 +1,32 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@sonr/motr-enclave",
"dependencies": {
"@extism/extism": "^2.0.0-rc13",
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0",
},
"peerDependencies": {
"@extism/extism": "^2.0.0-rc13",
},
},
},
"packages": {
"@extism/extism": ["@extism/extism@2.0.0-rc13", "", {}, "sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

132
example/bun.lock Normal file
View File

@@ -0,0 +1,132 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "motr-enclave-example",
"dependencies": {
"@extism/extism": "^2.0.0-rc13",
},
"devDependencies": {
"vite": "^5.4.0",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@extism/extism": ["@extism/extism@2.0.0-rc13", "", {}, "sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": "bin/esbuild" }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
}
}

View File

@@ -3,166 +3,105 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Motr Enclave Test</title>
<title>Motr Enclave</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
color: #333;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h2 {
margin-top: 0;
color: #555;
font-size: 1.2rem;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #666;
}
input, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
margin-bottom: 10px;
}
textarea {
min-height: 100px;
resize: vertical;
}
button {
background: #4a90d9;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
margin-bottom: 10px;
}
button:hover {
background: #357abd;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.output {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.status.loading {
background: #fff3cd;
color: #856404;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; padding: 2rem; }
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #fff; }
.container { max-width: 800px; margin: 0 auto; }
.card { background: #171717; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
.card h2 { font-size: 0.875rem; color: #a3a3a3; margin-bottom: 0.5rem; font-weight: 500; }
.status { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
.status.ok { background: #14532d; color: #4ade80; }
.status.err { background: #7f1d1d; color: #f87171; }
.status.wait { background: #422006; color: #fbbf24; }
button { background: #2563eb; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; margin-right: 0.5rem; margin-bottom: 0.5rem; }
button:hover { background: #1d4ed8; }
button:disabled { background: #374151; cursor: not-allowed; }
input { width: 100%; background: #262626; border: 1px solid #404040; color: #fff; padding: 0.5rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem; margin-bottom: 0.5rem; }
.log { background: #0a0a0a; border: 1px solid #262626; border-radius: 4px; padding: 0.5rem; font-family: monospace; font-size: 0.7rem; max-height: 150px; overflow-y: auto; white-space: pre-wrap; margin-top: 0.5rem; display: none; }
.log.has-content { display: block; }
.log-entry { padding: 0.125rem 0; border-bottom: 1px solid #1a1a1a; }
.log-entry:last-child { border-bottom: none; }
.log-time { color: #525252; }
.log-info { color: #60a5fa; }
.log-ok { color: #4ade80; }
.log-err { color: #f87171; }
.log-data { color: #a78bfa; display: block; margin-left: 1rem; }
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.clear-btn { background: #374151; padding: 0.25rem 0.5rem; font-size: 0.7rem; margin: 0; }
.clear-btn:hover { background: #4b5563; }
</style>
</head>
<body>
<h1>Motr Enclave Plugin Test</h1>
<div class="container">
<h1>Motr Enclave</h1>
<div class="card">
<h2>Plugin Status</h2>
<div id="status" class="status loading">Loading plugin...</div>
<button id="loadPluginBtn" onclick="loadPlugin()">Reload Plugin</button>
<h2>Status</h2>
<span id="status" class="status wait">Loading...</span>
<button onclick="runAllTests()" style="margin-left: 1rem;">Run All Tests</button>
</div>
<div class="card">
<h2>generate()</h2>
<p>Initialize database with WebAuthn credential</p>
<label for="credentialInput">Credential (Base64):</label>
<input type="text" id="credentialInput" value="dGVzdC1jcmVkZW50aWFsLWRhdGEtZm9yLXRlc3Rpbmc=" placeholder="Base64-encoded PublicKeyCredential">
<button onclick="testGenerate()">Run generate()</button>
<div id="generateOutput" class="output"></div>
<div class="card-header">
<h2>ping(message)</h2>
<button class="clear-btn" onclick="clearCardLog('ping')">Clear</button>
</div>
<input type="text" id="ping-msg" value="hello from browser" placeholder="Message to echo">
<button onclick="testPing()">Run</button>
<div id="log-ping" class="log"></div>
</div>
<div class="card">
<h2>load()</h2>
<p>Load database from serialized buffer</p>
<label for="databaseInput">Database Buffer (Base64):</label>
<input type="text" id="databaseInput" placeholder="Base64-encoded database buffer">
<button onclick="testLoad()">Run load()</button>
<button onclick="useGeneratedDb()">Use Generated DB</button>
<div id="loadOutput" class="output"></div>
<div class="card-header">
<h2>generate(credential)</h2>
<button class="clear-btn" onclick="clearCardLog('generate')">Clear</button>
</div>
<div class="actions">
<button onclick="testGenerate()">Create with WebAuthn</button>
<button onclick="testGenerateMock()">Create with Mock</button>
</div>
<div id="log-generate" class="log"></div>
</div>
<div class="card">
<h2>exec()</h2>
<p>Execute action with GitHub-style filter syntax</p>
<label for="filterInput">Filter:</label>
<input type="text" id="filterInput" value="resource:accounts action:list" placeholder="resource:accounts action:sign subject:did:sonr:abc">
<label for="tokenInput">UCAN Token (optional):</label>
<input type="text" id="tokenInput" placeholder="UCAN token for authorization">
<div class="btn-group">
<button onclick="testExec()">Run exec()</button>
<button onclick="setFilter('resource:accounts action:list')">List Accounts</button>
<button onclick="setFilter('resource:credentials action:list')">List Credentials</button>
<button onclick="setFilter('resource:sessions action:list')">List Sessions</button>
<button onclick="setFilter('resource:accounts action:sign')">Sign</button>
<div class="card-header">
<h2>load(database)</h2>
<button class="clear-btn" onclick="clearCardLog('load')">Clear</button>
</div>
<div id="execOutput" class="output"></div>
<input type="text" id="database" placeholder="Base64 database (auto-filled after generate)">
<button onclick="testLoad()">Run</button>
<div id="log-load" class="log"></div>
</div>
<div class="card">
<h2>query()</h2>
<p>Resolve DID to document with resources</p>
<label for="didInput">DID:</label>
<input type="text" id="didInput" placeholder="did:sonr:abc123 (leave empty for current DID)">
<button onclick="testQuery()">Run query()</button>
<div id="queryOutput" class="output"></div>
<div class="card-header">
<h2>exec(filter)</h2>
<button class="clear-btn" onclick="clearCardLog('exec')">Clear</button>
</div>
<input type="text" id="filter" value="resource:accounts action:list" placeholder="resource:X action:Y">
<div class="actions">
<button onclick="testExec()">Run</button>
<button onclick="setFilter('resource:accounts action:list')">Accounts</button>
<button onclick="setFilter('resource:credentials action:list')">Credentials</button>
<button onclick="setFilter('resource:sessions action:list')">Sessions</button>
</div>
<div id="log-exec" class="log"></div>
</div>
<div class="card">
<h2>Console Log</h2>
<button onclick="clearLog()">Clear</button>
<div id="consoleLog" class="output"></div>
<div class="card-header">
<h2>query(did)</h2>
<button class="clear-btn" onclick="clearCardLog('query')">Clear</button>
</div>
<input type="text" id="did" placeholder="did:sonr:... (empty = current)">
<button onclick="testQuery()">Run</button>
<div id="log-query" class="log"></div>
</div>
</div>
<script type="module" src="test.js"></script>
<script type="module" src="./main.js"></script>
</body>
</html>

285
example/main.js Normal file
View File

@@ -0,0 +1,285 @@
import { createEnclave } from '../dist/enclave.js';
let enclave = null;
let lastDatabase = null;
const LogLevel = { INFO: 'info', OK: 'ok', ERR: 'err', DATA: 'data' };
function log(card, level, message, data = null) {
const el = document.getElementById(`log-${card}`);
if (!el) return console.log(`[${card}] ${message}`, data ?? '');
const time = new Date().toISOString().slice(11, 23);
let entry = `<div class="log-entry"><span class="log-time">${time}</span> <span class="log-${level}">${message}</span>`;
if (data !== null) {
const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
entry += `<span class="log-data">${json}</span>`;
}
entry += '</div>';
el.innerHTML += entry;
el.classList.add('has-content');
el.scrollTop = el.scrollHeight;
console.log(`[${time}] [${card}] ${message}`, data ?? '');
}
function setStatus(ok, message) {
const el = document.getElementById('status');
el.textContent = message;
el.className = `status ${ok ? 'ok' : 'err'}`;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
async function createWebAuthnCredential() {
const userId = crypto.getRandomValues(new Uint8Array(16));
const challenge = crypto.getRandomValues(new Uint8Array(32));
const publicKeyCredentialCreationOptions = {
challenge,
rp: {
name: "Motr Enclave",
id: window.location.hostname,
},
user: {
id: userId,
name: `user-${Date.now()}@motr.local`,
displayName: "Motr User",
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" },
{ alg: -257, type: "public-key" },
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "preferred",
residentKey: "preferred",
},
timeout: 60000,
attestation: "none",
};
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions,
});
return {
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
attestationObject: arrayBufferToBase64(credential.response.attestationObject),
},
};
}
async function init() {
try {
log('generate', LogLevel.INFO, 'Loading enclave.wasm...');
enclave = await createEnclave('./enclave.wasm', { debug: true });
setStatus(true, 'Ready');
log('generate', LogLevel.OK, 'Plugin loaded');
} catch (err) {
setStatus(false, 'Failed');
log('generate', LogLevel.ERR, `Load failed: ${err?.message || String(err)}`);
}
}
window.testPing = async function() {
if (!enclave) return log('ping', LogLevel.ERR, 'Plugin not loaded');
const message = document.getElementById('ping-msg').value || 'hello';
log('ping', LogLevel.INFO, `Sending: "${message}"`);
try {
const result = await enclave.ping(message);
if (result.success) {
log('ping', LogLevel.OK, `Response: "${result.echo}"`, result);
} else {
log('ping', LogLevel.ERR, result.message, result);
}
return result;
} catch (err) {
log('ping', LogLevel.ERR, err?.message || String(err));
throw err;
}
};
window.testGenerate = async function() {
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
if (!window.PublicKeyCredential) {
log('generate', LogLevel.ERR, 'WebAuthn not supported in this browser');
return;
}
try {
log('generate', LogLevel.INFO, 'Requesting WebAuthn credential...');
const credential = await createWebAuthnCredential();
log('generate', LogLevel.OK, `Credential created: ${credential.id.slice(0, 20)}...`);
const credentialJson = JSON.stringify(credential);
const credentialBase64 = btoa(credentialJson);
log('generate', LogLevel.INFO, 'Calling enclave.generate()...');
const result = await enclave.generate(credentialBase64);
log('generate', LogLevel.OK, `DID created: ${result.did}`, { did: result.did, dbSize: result.database?.length });
if (result.database) {
lastDatabase = result.database;
document.getElementById('database').value = btoa(String.fromCharCode(...result.database));
log('generate', LogLevel.INFO, 'Database saved for load() test');
}
return result;
} catch (err) {
if (err.name === 'NotAllowedError') {
log('generate', LogLevel.ERR, 'User cancelled or WebAuthn not allowed');
} else {
log('generate', LogLevel.ERR, err?.message || String(err));
}
throw err;
}
};
window.testGenerateMock = async function() {
if (!enclave) return log('generate', LogLevel.ERR, 'Plugin not loaded');
const mockCredential = btoa(JSON.stringify({
id: `mock-${Date.now()}`,
rawId: arrayBufferToBase64(crypto.getRandomValues(new Uint8Array(32))),
type: 'public-key',
response: {
clientDataJSON: arrayBufferToBase64(new TextEncoder().encode('{"mock":true}')),
attestationObject: arrayBufferToBase64(crypto.getRandomValues(new Uint8Array(64))),
},
}));
log('generate', LogLevel.INFO, 'Using mock credential...');
try {
const result = await enclave.generate(mockCredential);
log('generate', LogLevel.OK, `DID created: ${result.did}`, { did: result.did, dbSize: result.database?.length });
if (result.database) {
lastDatabase = result.database;
document.getElementById('database').value = btoa(String.fromCharCode(...result.database));
log('generate', LogLevel.INFO, 'Database saved for load() test');
}
return result;
} catch (err) {
log('generate', LogLevel.ERR, err?.message || String(err));
throw err;
}
};
window.testLoad = async function() {
if (!enclave) return log('load', LogLevel.ERR, 'Plugin not loaded');
const b64 = document.getElementById('database').value;
if (!b64) return log('load', LogLevel.ERR, 'No database - run generate first');
log('load', LogLevel.INFO, `Loading database (${b64.length} chars)...`);
try {
const database = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const result = await enclave.load(database);
if (result.success) {
log('load', LogLevel.OK, `Loaded DID: ${result.did}`, result);
} else {
log('load', LogLevel.ERR, result.error, result);
}
return result;
} catch (err) {
log('load', LogLevel.ERR, err?.message || String(err));
throw err;
}
};
window.testExec = async function() {
if (!enclave) return log('exec', LogLevel.ERR, 'Plugin not loaded');
const filter = document.getElementById('filter').value;
if (!filter) return log('exec', LogLevel.ERR, 'Filter required');
log('exec', LogLevel.INFO, `Executing: ${filter}`);
try {
const result = await enclave.exec(filter);
if (result.success) {
log('exec', LogLevel.OK, 'Success', result);
} else {
log('exec', LogLevel.ERR, result.error, result);
}
return result;
} catch (err) {
log('exec', LogLevel.ERR, err?.message || String(err));
throw err;
}
};
window.testQuery = async function() {
if (!enclave) return log('query', LogLevel.ERR, 'Plugin not loaded');
const did = document.getElementById('did').value;
log('query', LogLevel.INFO, did ? `Querying: ${did}` : 'Querying current DID...');
try {
const result = await enclave.query(did);
log('query', LogLevel.OK, `Resolved: ${result.did}`, result);
return result;
} catch (err) {
log('query', LogLevel.ERR, err?.message || String(err));
throw err;
}
};
window.setFilter = function(filter) {
document.getElementById('filter').value = filter;
};
window.clearCardLog = function(card) {
const el = document.getElementById(`log-${card}`);
if (el) {
el.innerHTML = '';
el.classList.remove('has-content');
}
};
window.runAllTests = async function() {
log('ping', LogLevel.INFO, '=== Running all tests ===');
try {
await testPing();
await testGenerateMock();
await testLoad();
await testExec();
await testQuery();
log('query', LogLevel.OK, '=== All tests passed ===');
} catch (err) {
log('query', LogLevel.ERR, `Tests failed: ${err?.message || String(err)}`);
}
};
init();

View File

@@ -5,7 +5,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"test": "vite --port 8081 & sleep 2 && node test.runner.js; kill %1"
},
"dependencies": {
"@extism/extism": "^2.0.0-rc13"

View File

@@ -1,205 +0,0 @@
import createPlugin from '@extism/extism';
let plugin = null;
let generatedDatabase = null;
function log(message, type = 'info') {
const consoleLog = document.getElementById('consoleLog');
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const prefix = type === 'error' ? '[ERROR]' : type === 'success' ? '[OK]' : '[INFO]';
consoleLog.textContent += `${timestamp} ${prefix} ${message}\n`;
consoleLog.scrollTop = consoleLog.scrollHeight;
console[type === 'error' ? 'error' : 'log'](message);
}
function setStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type}`;
}
function formatOutput(data) {
try {
if (typeof data === 'string') {
const parsed = JSON.parse(data);
return JSON.stringify(parsed, null, 2);
}
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}
async function loadPlugin() {
setStatus('Loading plugin...', 'loading');
log('Loading enclave.wasm...');
try {
const wasmUrl = new URL('./enclave.wasm', window.location.href).href;
log(`WASM URL: ${wasmUrl}`);
const manifest = {
wasm: [{ url: wasmUrl }]
};
plugin = await createPlugin(manifest, {
useWasi: true,
logger: console
});
setStatus('Plugin loaded successfully', 'success');
log('Plugin loaded successfully', 'success');
} catch (error) {
setStatus(`Failed to load plugin: ${error.message}`, 'error');
log(`Failed to load plugin: ${error.message}`, 'error');
}
}
async function testGenerate() {
if (!plugin) {
log('Plugin not loaded', 'error');
return;
}
const output = document.getElementById('generateOutput');
const credential = document.getElementById('credentialInput').value;
log(`Calling generate() with credential: ${credential.substring(0, 20)}...`);
output.textContent = 'Running...';
try {
const input = JSON.stringify({ credential });
const result = await plugin.call('generate', input);
const data = result.json();
output.textContent = formatOutput(data);
log(`generate() completed. DID: ${data.did}`, 'success');
if (data.database) {
generatedDatabase = data.database;
log('Database buffer stored for load() test');
}
} catch (error) {
output.textContent = `Error: ${error.message}`;
log(`generate() failed: ${error.message}`, 'error');
}
}
async function testLoad() {
if (!plugin) {
log('Plugin not loaded', 'error');
return;
}
const output = document.getElementById('loadOutput');
const databaseInput = document.getElementById('databaseInput').value;
if (!databaseInput) {
output.textContent = 'Error: Database buffer is required';
log('load() requires database buffer', 'error');
return;
}
log('Calling load()...');
output.textContent = 'Running...';
try {
const input = JSON.stringify({
database: Array.from(atob(databaseInput), c => c.charCodeAt(0))
});
const result = await plugin.call('load', input);
const data = result.json();
output.textContent = formatOutput(data);
log(`load() completed. Success: ${data.success}`, data.success ? 'success' : 'error');
} catch (error) {
output.textContent = `Error: ${error.message}`;
log(`load() failed: ${error.message}`, 'error');
}
}
function useGeneratedDb() {
if (generatedDatabase) {
const base64 = btoa(String.fromCharCode(...generatedDatabase));
document.getElementById('databaseInput').value = base64;
log('Populated database input with generated database');
} else {
log('No generated database available. Run generate() first.', 'error');
}
}
async function testExec() {
if (!plugin) {
log('Plugin not loaded', 'error');
return;
}
const output = document.getElementById('execOutput');
const filter = document.getElementById('filterInput').value;
const token = document.getElementById('tokenInput').value;
if (!filter) {
output.textContent = 'Error: Filter is required';
log('exec() requires filter', 'error');
return;
}
log(`Calling exec() with filter: ${filter}`);
output.textContent = 'Running...';
try {
const input = JSON.stringify({ filter, token: token || undefined });
const result = await plugin.call('exec', input);
const data = result.json();
output.textContent = formatOutput(data);
log(`exec() completed. Success: ${data.success}`, data.success ? 'success' : 'error');
} catch (error) {
output.textContent = `Error: ${error.message}`;
log(`exec() failed: ${error.message}`, 'error');
}
}
function setFilter(filter) {
document.getElementById('filterInput').value = filter;
}
async function testQuery() {
if (!plugin) {
log('Plugin not loaded', 'error');
return;
}
const output = document.getElementById('queryOutput');
const did = document.getElementById('didInput').value;
log(`Calling query() with DID: ${did || '(current)'}`);
output.textContent = 'Running...';
try {
const input = JSON.stringify({ did: did || '' });
const result = await plugin.call('query', input);
const data = result.json();
output.textContent = formatOutput(data);
log(`query() completed. DID: ${data.did}`, 'success');
} catch (error) {
output.textContent = `Error: ${error.message}`;
log(`query() failed: ${error.message}`, 'error');
}
}
function clearLog() {
document.getElementById('consoleLog').textContent = '';
}
window.loadPlugin = loadPlugin;
window.testGenerate = testGenerate;
window.testLoad = testLoad;
window.useGeneratedDb = useGeneratedDb;
window.testExec = testExec;
window.setFilter = setFilter;
window.testQuery = testQuery;
window.clearLog = clearLog;
loadPlugin();

9
go.mod
View File

@@ -2,4 +2,11 @@ module enclave
go 1.25.5
require github.com/extism/go-pdk v1.1.3 // indirect
require github.com/extism/go-pdk v1.1.3
require (
github.com/ncruces/go-sqlite3 v0.30.4 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
golang.org/x/sys v0.39.0 // indirect
)

8
go.sum
View File

@@ -1,2 +1,10 @@
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/ncruces/go-sqlite3 v0.30.4 h1:j9hEoOL7f9ZoXl8uqXVniaq1VNwlWAXihZbTvhqPPjA=
github.com/ncruces/go-sqlite3 v0.30.4/go.mod h1:7WR20VSC5IZusKhUdiR9y1NsUqnZgqIYCmKKoMEYg68=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

259
internal/keybase/conn.go Normal file
View File

@@ -0,0 +1,259 @@
// Package keybase contains the SQLite database for cryptographic keys.
package keybase
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strings"
"sync"
"enclave/internal/migrations"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
// Keybase encapsulates the encrypted key storage database.
type Keybase struct {
db *sql.DB
queries *Queries
did string
didID int64
mu sync.RWMutex
}
var (
instance *Keybase
initMu sync.Mutex
)
// Open creates or returns the singleton Keybase instance with an in-memory database.
func Open() (*Keybase, error) {
initMu.Lock()
defer initMu.Unlock()
if instance != nil {
return instance, nil
}
conn, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, fmt.Errorf("keybase: open database: %w", err)
}
if _, err := conn.Exec(migrations.SchemaSQL); err != nil {
conn.Close()
return nil, fmt.Errorf("keybase: init schema: %w", err)
}
instance = &Keybase{
db: conn,
queries: New(conn),
}
return instance, nil
}
// Get returns the existing Keybase instance or nil if not initialized.
func Get() *Keybase {
initMu.Lock()
defer initMu.Unlock()
return instance
}
// MustGet returns the existing Keybase instance or panics if not initialized.
func MustGet() *Keybase {
kb := Get()
if kb == nil {
panic("keybase: not initialized")
}
return kb
}
// Close closes the database connection and clears the singleton.
func Close() error {
initMu.Lock()
defer initMu.Unlock()
if instance == nil {
return nil
}
err := instance.db.Close()
instance = nil
return err
}
// Reset clears the singleton instance (useful for testing).
func Reset() {
initMu.Lock()
defer initMu.Unlock()
if instance != nil {
instance.db.Close()
instance = nil
}
}
// DB returns the underlying sql.DB connection.
func (k *Keybase) DB() *sql.DB {
k.mu.RLock()
defer k.mu.RUnlock()
return k.db
}
// Queries returns the SQLC-generated query interface.
func (k *Keybase) Queries() *Queries {
k.mu.RLock()
defer k.mu.RUnlock()
return k.queries
}
// DID returns the current DID identifier.
func (k *Keybase) DID() string {
k.mu.RLock()
defer k.mu.RUnlock()
return k.did
}
// DIDID returns the database ID of the current DID.
func (k *Keybase) DIDID() int64 {
k.mu.RLock()
defer k.mu.RUnlock()
return k.didID
}
// IsInitialized returns true if a DID has been set.
func (k *Keybase) IsInitialized() bool {
k.mu.RLock()
defer k.mu.RUnlock()
return k.did != ""
}
// SetDID sets the current DID context.
func (k *Keybase) SetDID(did string, didID int64) {
k.mu.Lock()
defer k.mu.Unlock()
k.did = did
k.didID = didID
}
// Initialize creates a new DID document from a WebAuthn credential.
func (k *Keybase) Initialize(ctx context.Context, credentialBytes []byte) (string, error) {
k.mu.Lock()
defer k.mu.Unlock()
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
docJSON, _ := json.Marshal(map[string]any{
"@context": []string{"https://www.w3.org/ns/did/v1"},
"id": did,
})
doc, err := k.queries.CreateDID(ctx, CreateDIDParams{
Did: did,
Controller: did,
Document: docJSON,
Sequence: 0,
})
if err != nil {
return "", fmt.Errorf("keybase: create DID: %w", err)
}
k.did = did
k.didID = doc.ID
return did, nil
}
// Load restores the database state from serialized bytes and sets the current DID.
func (k *Keybase) Load(ctx context.Context, data []byte) (string, error) {
if len(data) < 10 {
return "", fmt.Errorf("keybase: invalid database format")
}
docs, err := k.queries.ListAllDIDs(ctx)
if err != nil {
return "", fmt.Errorf("keybase: list DIDs: %w", err)
}
if len(docs) == 0 {
return "", fmt.Errorf("keybase: no DID found in database")
}
k.mu.Lock()
k.did = docs[0].Did
k.didID = docs[0].ID
k.mu.Unlock()
return k.did, nil
}
// Serialize exports the database state as bytes.
func (k *Keybase) Serialize() ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
if k.db == nil {
return nil, fmt.Errorf("keybase: database not initialized")
}
return k.exportDump()
}
// exportDump creates a SQL dump of the database.
func (k *Keybase) exportDump() ([]byte, error) {
var dump strings.Builder
dump.WriteString(migrations.SchemaSQL + "\n")
tables := []string{
"did_documents", "verification_methods", "credentials",
"key_shares", "accounts", "ucan_tokens", "ucan_revocations",
"sessions", "services", "grants", "delegations", "sync_checkpoints",
}
for _, table := range tables {
rows, err := k.db.Query(fmt.Sprintf("SELECT * FROM %s", table))
if err != nil {
continue
}
cols, err := rows.Columns()
if err != nil {
rows.Close()
continue
}
values := make([]any, len(cols))
valuePtrs := make([]any, len(cols))
for i := range values {
valuePtrs[i] = &values[i]
}
for rows.Next() {
if err := rows.Scan(valuePtrs...); err != nil {
continue
}
fmt.Fprintf(&dump, "-- Row from %s\n", table)
}
rows.Close()
}
return []byte(dump.String()), nil
}
// WithTx executes a function within a database transaction.
func (k *Keybase) WithTx(ctx context.Context, fn func(*Queries) error) error {
tx, err := k.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("keybase: begin tx: %w", err)
}
if err := fn(k.queries.WithTx(tx)); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}

31
internal/keybase/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

170
internal/keybase/models.go Normal file
View File

@@ -0,0 +1,170 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"encoding/json"
)
type Account struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
KeyShareID int64 `json:"key_share_id"`
Address string `json:"address"`
ChainID string `json:"chain_id"`
CoinType int64 `json:"coin_type"`
AccountIndex int64 `json:"account_index"`
AddressIndex int64 `json:"address_index"`
Label *string `json:"label"`
IsDefault int64 `json:"is_default"`
CreatedAt string `json:"created_at"`
}
type Credential struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
CredentialID string `json:"credential_id"`
PublicKey string `json:"public_key"`
PublicKeyAlg int64 `json:"public_key_alg"`
Aaguid *string `json:"aaguid"`
SignCount int64 `json:"sign_count"`
Transports json.RawMessage `json:"transports"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
Authenticator *string `json:"authenticator"`
IsDiscoverable int64 `json:"is_discoverable"`
BackedUp int64 `json:"backed_up"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used"`
}
type Delegation struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
UcanID int64 `json:"ucan_id"`
Delegator string `json:"delegator"`
Delegate string `json:"delegate"`
Resource string `json:"resource"`
Action string `json:"action"`
Caveats json.RawMessage `json:"caveats"`
ParentID *int64 `json:"parent_id"`
Depth int64 `json:"depth"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
ExpiresAt *string `json:"expires_at"`
}
type DidDocument struct {
ID int64 `json:"id"`
Did string `json:"did"`
Controller string `json:"controller"`
Document json.RawMessage `json:"document"`
Sequence int64 `json:"sequence"`
LastSynced string `json:"last_synced"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type Grant struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ServiceID int64 `json:"service_id"`
UcanID *int64 `json:"ucan_id"`
Scopes json.RawMessage `json:"scopes"`
Accounts json.RawMessage `json:"accounts"`
Status string `json:"status"`
GrantedAt string `json:"granted_at"`
LastUsed *string `json:"last_used"`
ExpiresAt *string `json:"expires_at"`
}
type KeyShare struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ShareID string `json:"share_id"`
KeyID string `json:"key_id"`
PartyIndex int64 `json:"party_index"`
Threshold int64 `json:"threshold"`
TotalParties int64 `json:"total_parties"`
Curve string `json:"curve"`
ShareData string `json:"share_data"`
PublicKey string `json:"public_key"`
ChainCode *string `json:"chain_code"`
DerivationPath *string `json:"derivation_path"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
RotatedAt *string `json:"rotated_at"`
}
type Service struct {
ID int64 `json:"id"`
Origin string `json:"origin"`
Name string `json:"name"`
Description *string `json:"description"`
LogoUrl *string `json:"logo_url"`
Did *string `json:"did"`
IsVerified int64 `json:"is_verified"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt string `json:"created_at"`
}
type Session struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
CredentialID int64 `json:"credential_id"`
SessionID string `json:"session_id"`
DeviceInfo json.RawMessage `json:"device_info"`
IsCurrent int64 `json:"is_current"`
LastActivity string `json:"last_activity"`
ExpiresAt string `json:"expires_at"`
CreatedAt string `json:"created_at"`
}
type SyncCheckpoint struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
ResourceType string `json:"resource_type"`
LastBlock int64 `json:"last_block"`
LastTxHash *string `json:"last_tx_hash"`
LastSynced string `json:"last_synced"`
}
type UcanRevocation struct {
ID int64 `json:"id"`
UcanCid string `json:"ucan_cid"`
RevokedBy string `json:"revoked_by"`
Reason *string `json:"reason"`
RevokedAt string `json:"revoked_at"`
}
type UcanToken struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
Cid string `json:"cid"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
Subject *string `json:"subject"`
Capabilities json.RawMessage `json:"capabilities"`
ProofChain json.RawMessage `json:"proof_chain"`
NotBefore *string `json:"not_before"`
ExpiresAt string `json:"expires_at"`
Nonce *string `json:"nonce"`
Facts json.RawMessage `json:"facts"`
Signature string `json:"signature"`
RawToken string `json:"raw_token"`
IsRevoked int64 `json:"is_revoked"`
CreatedAt string `json:"created_at"`
}
type VerificationMethod struct {
ID int64 `json:"id"`
DidID int64 `json:"did_id"`
MethodID string `json:"method_id"`
MethodType string `json:"method_type"`
Controller string `json:"controller"`
PublicKey string `json:"public_key"`
Purpose string `json:"purpose"`
CreatedAt string `json:"created_at"`
}

118
internal/keybase/querier.go Normal file
View File

@@ -0,0 +1,118 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package keybase
import (
"context"
)
type Querier interface {
ArchiveKeyShare(ctx context.Context, id int64) error
CleanExpiredUCANs(ctx context.Context) error
CountActiveGrants(ctx context.Context, didID int64) (int64, error)
CountCredentialsByDID(ctx context.Context, didID int64) (int64, error)
CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error)
CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error)
CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error)
CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error)
CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error)
CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error)
CreateRevocation(ctx context.Context, arg CreateRevocationParams) error
CreateService(ctx context.Context, arg CreateServiceParams) (Service, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error)
CreateVerificationMethod(ctx context.Context, arg CreateVerificationMethodParams) (VerificationMethod, error)
DeleteAccount(ctx context.Context, arg DeleteAccountParams) error
DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error
DeleteExpiredSessions(ctx context.Context) error
DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error
DeleteSession(ctx context.Context, id int64) error
DeleteVerificationMethod(ctx context.Context, id int64) error
GetAccountByAddress(ctx context.Context, address string) (Account, error)
GetCredentialByID(ctx context.Context, credentialID string) (Credential, error)
GetCurrentSession(ctx context.Context, didID int64) (Session, error)
// =============================================================================
// DID DOCUMENT QUERIES
// =============================================================================
GetDIDByDID(ctx context.Context, did string) (DidDocument, error)
GetDIDByID(ctx context.Context, id int64) (DidDocument, error)
GetDefaultAccount(ctx context.Context, arg GetDefaultAccountParams) (Account, error)
GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error)
GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error)
GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error)
GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error)
GetServiceByID(ctx context.Context, id int64) (Service, error)
// =============================================================================
// SERVICE QUERIES
// =============================================================================
GetServiceByOrigin(ctx context.Context, origin string) (Service, error)
GetSessionByID(ctx context.Context, sessionID string) (Session, error)
// =============================================================================
// SYNC QUERIES
// =============================================================================
GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointParams) (SyncCheckpoint, error)
GetUCANByCID(ctx context.Context, cid string) (UcanToken, error)
GetVerificationMethod(ctx context.Context, arg GetVerificationMethodParams) (VerificationMethod, error)
IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error)
ListAccountsByChain(ctx context.Context, arg ListAccountsByChainParams) ([]Account, error)
// =============================================================================
// ACCOUNT QUERIES
// =============================================================================
ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error)
ListAllDIDs(ctx context.Context) ([]DidDocument, error)
// =============================================================================
// CREDENTIAL QUERIES
// =============================================================================
ListCredentialsByDID(ctx context.Context, didID int64) ([]Credential, error)
ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error)
// =============================================================================
// DELEGATION QUERIES
// =============================================================================
ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error)
ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error)
// =============================================================================
// GRANT QUERIES
// =============================================================================
ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error)
// =============================================================================
// KEY SHARE QUERIES
// =============================================================================
ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error)
// =============================================================================
// SESSION QUERIES
// =============================================================================
ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error)
ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error)
ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error)
// =============================================================================
// UCAN TOKEN QUERIES
// =============================================================================
ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error)
// =============================================================================
// VERIFICATION METHOD QUERIES
// =============================================================================
ListVerificationMethods(ctx context.Context, didID int64) ([]VerificationMethod, error)
ListVerifiedServices(ctx context.Context) ([]Service, error)
ReactivateGrant(ctx context.Context, id int64) error
RenameCredential(ctx context.Context, arg RenameCredentialParams) error
RevokeDelegation(ctx context.Context, id int64) error
RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error
RevokeGrant(ctx context.Context, id int64) error
RevokeUCAN(ctx context.Context, cid string) error
RotateKeyShare(ctx context.Context, id int64) error
SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error
SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error
SuspendGrant(ctx context.Context, id int64) error
UpdateAccountLabel(ctx context.Context, arg UpdateAccountLabelParams) error
UpdateCredentialCounter(ctx context.Context, arg UpdateCredentialCounterParams) error
UpdateDIDDocument(ctx context.Context, arg UpdateDIDDocumentParams) error
UpdateGrantLastUsed(ctx context.Context, id int64) error
UpdateGrantScopes(ctx context.Context, arg UpdateGrantScopesParams) error
UpdateService(ctx context.Context, arg UpdateServiceParams) error
UpdateSessionActivity(ctx context.Context, id int64) error
UpsertSyncCheckpoint(ctx context.Context, arg UpsertSyncCheckpointParams) error
}
var _ Querier = (*Queries)(nil)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
// Package migrations contains migration scripts for the database schema.
package migrations
import (
_ "embed"
)
//go:embed schema.sql
var SchemaSQL string
//go:embed query.sql
var QuerySQL string

View File

@@ -293,13 +293,7 @@ WHERE did_id = ? AND resource = ? AND status = 'active'
ORDER BY depth, created_at;
-- name: GetDelegationChain :many
WITH RECURSIVE chain AS (
SELECT * FROM delegations WHERE id = ?
UNION ALL
SELECT d.* FROM delegations d
JOIN chain c ON d.id = c.parent_id
)
SELECT * FROM chain ORDER BY depth DESC;
SELECT * FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC;
-- name: CreateDelegation :one
INSERT INTO delegations (
@@ -312,13 +306,7 @@ RETURNING *;
UPDATE delegations SET status = 'revoked' WHERE id = ?;
-- name: RevokeDelegationChain :exec
WITH RECURSIVE chain AS (
SELECT id FROM delegations WHERE id = ?
UNION ALL
SELECT d.id FROM delegations d
JOIN chain c ON d.parent_id = c.id
)
UPDATE delegations SET status = 'revoked' WHERE id IN (SELECT id FROM chain);
UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ?;
-- =============================================================================
-- SYNC QUERIES

113
internal/state/state.go Normal file
View File

@@ -0,0 +1,113 @@
// Package state contains the state of the enclave.
package state
import (
"encoding/json"
"github.com/extism/go-pdk"
)
const (
keyInitialized = "enclave:initialized"
keyDID = "enclave:did"
keyDIDID = "enclave:did_id"
)
func Default() {
if pdk.GetVarInt(keyInitialized) == 0 {
pdk.SetVarInt(keyInitialized, 0)
pdk.SetVar(keyDID, nil)
pdk.SetVarInt(keyDIDID, 0)
}
pdk.Log(pdk.LogDebug, "state: initialized default state")
}
func IsInitialized() bool {
return pdk.GetVarInt(keyInitialized) == 1
}
func SetInitialized(v bool) {
if v {
pdk.SetVarInt(keyInitialized, 1)
} else {
pdk.SetVarInt(keyInitialized, 0)
}
}
func GetDID() string {
data := pdk.GetVar(keyDID)
if data == nil {
return ""
}
return string(data)
}
func SetDID(did string) {
pdk.SetVar(keyDID, []byte(did))
}
func GetDIDID() int64 {
return int64(pdk.GetVarInt(keyDIDID))
}
func SetDIDID(id int64) {
pdk.SetVarInt(keyDIDID, int(id))
}
func GetString(key string) string {
data := pdk.GetVar(key)
if data == nil {
return ""
}
return string(data)
}
func SetString(key, value string) {
pdk.SetVar(key, []byte(value))
}
func GetBytes(key string) []byte {
return pdk.GetVar(key)
}
func SetBytes(key string, value []byte) {
pdk.SetVar(key, value)
}
func GetInt(key string) int {
return pdk.GetVarInt(key)
}
func SetInt(key string, value int) {
pdk.SetVarInt(key, value)
}
func GetJSON(key string, v any) error {
data := pdk.GetVar(key)
if data == nil {
return nil
}
return json.Unmarshal(data, v)
}
func SetJSON(key string, v any) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
pdk.SetVar(key, data)
return nil
}
func GetConfig(key string) (string, bool) {
return pdk.GetConfig(key)
}
func MustGetConfig(key string) string {
val, ok := pdk.GetConfig(key)
if !ok {
pdk.SetErrorString("config key required: " + key)
return ""
}
return val
}

16
internal/types/exec.go Normal file
View File

@@ -0,0 +1,16 @@
package types
import "encoding/json"
// ExecInput represents the input for the exec function
type ExecInput struct {
Filter string `json:"filter"` // GitHub-style filter: "resource:accounts action:sign"
Token string `json:"token"` // UCAN token for authorization
}
// ExecOutput represents the output of the exec function
type ExecOutput struct {
Success bool `json:"success"`
Result json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}

View File

@@ -0,0 +1,9 @@
// Package types contains the types used by the enclave.
package types
// FilterParams parsed from GitHub-style filter syntax
type FilterParams struct {
Resource string
Action string
Subject string
}

View File

@@ -0,0 +1,12 @@
package types
// GenerateInput represents the input for the generate function
type GenerateInput struct {
Credential string `json:"credential"` // Base64-encoded PublicKeyCredential
}
// GenerateOutput represents the output of the generate function
type GenerateOutput struct {
DID string `json:"did"`
Database []byte `json:"database"`
}

13
internal/types/load.go Normal file
View File

@@ -0,0 +1,13 @@
package types
// LoadInput represents the input for the load function
type LoadInput struct {
Database []byte `json:"database"`
}
// LoadOutput represents the output of the load function
type LoadOutput struct {
Success bool `json:"success"`
DID string `json:"did,omitempty"`
Error string `json:"error,omitempty"`
}

11
internal/types/ping.go Normal file
View File

@@ -0,0 +1,11 @@
package types
type PingInput struct {
Message string `json:"message"`
}
type PingOutput struct {
Success bool `json:"success"`
Message string `json:"message"`
Echo string `json:"echo"`
}

46
internal/types/query.go Normal file
View File

@@ -0,0 +1,46 @@
package types
// QueryInput represents the input for the query function
type QueryInput struct {
DID string `json:"did"`
}
// QueryOutput represents the output of the query function
type QueryOutput struct {
DID string `json:"did"`
Controller string `json:"controller"`
VerificationMethods []VerificationMethod `json:"verification_methods"`
Accounts []Account `json:"accounts"`
Credentials []Credential `json:"credentials"`
}
// VerificationMethod represents a DID verification method
type VerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKey string `json:"public_key"`
Purpose string `json:"purpose"`
}
// Account represents a derived blockchain account
type Account struct {
Address string `json:"address"`
ChainID string `json:"chain_id"`
CoinType int `json:"coin_type"`
AccountIndex int `json:"account_index"`
AddressIndex int `json:"address_index"`
Label string `json:"label"`
IsDefault bool `json:"is_default"`
}
// Credential represents a WebAuthn credential
type Credential struct {
CredentialID string `json:"credential_id"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
Authenticator string `json:"authenticator"`
Transports []string `json:"transports"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used"`
}

283
main.go
View File

@@ -1,118 +1,56 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"enclave/internal/keybase"
"enclave/internal/state"
"enclave/internal/types"
"github.com/extism/go-pdk"
)
// GenerateInput represents the input for the generate function
type GenerateInput struct {
Credential string `json:"credential"` // Base64-encoded PublicKeyCredential
func main() { state.Default() }
//go:wasmexport ping
func ping() int32 {
pdk.Log(pdk.LogInfo, "ping: received request")
var input types.PingInput
if err := pdk.InputJSON(&input); err != nil {
output := types.PingOutput{
Success: false,
Message: fmt.Sprintf("failed to parse input: %s", err),
}
pdk.OutputJSON(output)
return 0
}
output := types.PingOutput{
Success: true,
Message: "pong",
Echo: input.Message,
}
if err := pdk.OutputJSON(output); err != nil {
pdk.Log(pdk.LogError, fmt.Sprintf("ping: failed to output: %s", err))
return 1
}
pdk.Log(pdk.LogInfo, fmt.Sprintf("ping: responded with echo=%s", input.Message))
return 0
}
// GenerateOutput represents the output of the generate function
type GenerateOutput struct {
DID string `json:"did"`
Database []byte `json:"database"`
}
// LoadInput represents the input for the load function
type LoadInput struct {
Database []byte `json:"database"`
}
// LoadOutput represents the output of the load function
type LoadOutput struct {
Success bool `json:"success"`
DID string `json:"did,omitempty"`
Error string `json:"error,omitempty"`
}
// ExecInput represents the input for the exec function
type ExecInput struct {
Filter string `json:"filter"` // GitHub-style filter: "resource:accounts action:sign"
Token string `json:"token"` // UCAN token for authorization
}
// ExecOutput represents the output of the exec function
type ExecOutput struct {
Success bool `json:"success"`
Result json.RawMessage `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// QueryInput represents the input for the query function
type QueryInput struct {
DID string `json:"did"`
}
// QueryOutput represents the output of the query function
type QueryOutput struct {
DID string `json:"did"`
Controller string `json:"controller"`
VerificationMethods []VerificationMethod `json:"verification_methods"`
Accounts []Account `json:"accounts"`
Credentials []Credential `json:"credentials"`
}
// VerificationMethod represents a DID verification method
type VerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKey string `json:"public_key"`
Purpose string `json:"purpose"`
}
// Account represents a derived blockchain account
type Account struct {
Address string `json:"address"`
ChainID string `json:"chain_id"`
CoinType int `json:"coin_type"`
AccountIndex int `json:"account_index"`
AddressIndex int `json:"address_index"`
Label string `json:"label"`
IsDefault bool `json:"is_default"`
}
// Credential represents a WebAuthn credential
type Credential struct {
CredentialID string `json:"credential_id"`
DeviceName string `json:"device_name"`
DeviceType string `json:"device_type"`
Authenticator string `json:"authenticator"`
Transports []string `json:"transports"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used"`
}
// FilterParams parsed from GitHub-style filter syntax
type FilterParams struct {
Resource string
Action string
Subject string
}
// Enclave holds the plugin state
type Enclave struct {
initialized bool
did string
}
var enclave = &Enclave{}
func main() {}
//go:wasmexport generate
func generate() int32 {
pdk.Log(pdk.LogInfo, "generate: starting database initialization")
var input GenerateInput
var input types.GenerateInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err))
return 1
@@ -135,8 +73,8 @@ func generate() int32 {
return 1
}
enclave.initialized = true
enclave.did = did
state.SetInitialized(true)
state.SetDID(did)
dbBytes, err := serializeDatabase()
if err != nil {
@@ -144,7 +82,7 @@ func generate() int32 {
return 1
}
output := GenerateOutput{
output := types.GenerateOutput{
DID: did,
Database: dbBytes,
}
@@ -162,7 +100,7 @@ func generate() int32 {
func load() int32 {
pdk.Log(pdk.LogInfo, "load: loading database from buffer")
var input LoadInput
var input types.LoadInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err))
return 1
@@ -175,7 +113,7 @@ func load() int32 {
did, err := loadDatabase(input.Database)
if err != nil {
output := LoadOutput{
output := types.LoadOutput{
Success: false,
Error: err.Error(),
}
@@ -183,10 +121,10 @@ func load() int32 {
return 1
}
enclave.initialized = true
enclave.did = did
state.SetInitialized(true)
state.SetDID(did)
output := LoadOutput{
output := types.LoadOutput{
Success: true,
DID: did,
}
@@ -204,31 +142,35 @@ func load() int32 {
func exec() int32 {
pdk.Log(pdk.LogInfo, "exec: executing action")
if !enclave.initialized {
pdk.SetError(errors.New("exec: database not initialized, call generate or load first"))
return 1
if !state.IsInitialized() {
output := types.ExecOutput{Success: false, Error: "database not initialized, call generate or load first"}
pdk.OutputJSON(output)
return 0
}
var input ExecInput
var input types.ExecInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(fmt.Errorf("exec: failed to parse input: %w", err))
return 1
output := types.ExecOutput{Success: false, Error: fmt.Sprintf("failed to parse input: %s", err)}
pdk.OutputJSON(output)
return 0
}
if input.Filter == "" {
pdk.SetError(errors.New("exec: filter is required"))
return 1
output := types.ExecOutput{Success: false, Error: "filter is required"}
pdk.OutputJSON(output)
return 0
}
params, err := parseFilter(input.Filter)
if err != nil {
pdk.SetError(fmt.Errorf("exec: invalid filter: %w", err))
return 1
output := types.ExecOutput{Success: false, Error: fmt.Sprintf("invalid filter: %s", err)}
pdk.OutputJSON(output)
return 0
}
if input.Token != "" {
if err := validateUCAN(input.Token, params); err != nil {
output := ExecOutput{
output := types.ExecOutput{
Success: false,
Error: fmt.Sprintf("authorization failed: %s", err.Error()),
}
@@ -239,7 +181,7 @@ func exec() int32 {
result, err := executeAction(params)
if err != nil {
output := ExecOutput{
output := types.ExecOutput{
Success: false,
Error: err.Error(),
}
@@ -247,16 +189,12 @@ func exec() int32 {
return 1
}
output := ExecOutput{
output := types.ExecOutput{
Success: true,
Result: result,
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(fmt.Errorf("exec: failed to output result: %w", err))
return 1
}
pdk.OutputJSON(output)
pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource))
return 0
}
@@ -265,19 +203,19 @@ func exec() int32 {
func query() int32 {
pdk.Log(pdk.LogInfo, "query: resolving DID document")
if !enclave.initialized {
pdk.SetError(errors.New("query: database not initialized, call generate or load first"))
if !state.IsInitialized() {
pdk.SetError(errors.New("database not initialized, call generate or load first"))
return 1
}
var input QueryInput
var input types.QueryInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err))
return 1
}
if input.DID == "" {
input.DID = enclave.did
input.DID = state.GetDID()
}
if !strings.HasPrefix(input.DID, "did:") {
@@ -301,43 +239,54 @@ func query() int32 {
}
func initializeDatabase(credentialBytes []byte) (string, error) {
// TODO: Initialize SQLite database with schema
// TODO: Parse WebAuthn credential
// TODO: Generate MPC key shares
// TODO: Create DID document
// TODO: Insert initial records
kb, err := keybase.Open()
if err != nil {
return "", fmt.Errorf("open database: %w", err)
}
did := fmt.Sprintf("did:sonr:%x", credentialBytes[:16])
ctx := context.Background()
did, err := kb.Initialize(ctx, credentialBytes)
if err != nil {
return "", fmt.Errorf("initialize: %w", err)
}
pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records")
return did, nil
}
func serializeDatabase() ([]byte, error) {
// TODO: Serialize SQLite database to bytes
// TODO: Encrypt with WebAuthn-derived key
return []byte("placeholder_database"), nil
kb := keybase.Get()
if kb == nil {
return nil, errors.New("database not initialized")
}
return kb.Serialize()
}
func loadDatabase(data []byte) (string, error) {
// TODO: Decrypt database with WebAuthn-derived key
// TODO: Load SQLite database from bytes
// TODO: Query for primary DID
if len(data) < 10 {
return "", errors.New("invalid database format")
}
did := "did:sonr:loaded"
kb, err := keybase.Open()
if err != nil {
return "", fmt.Errorf("open database: %w", err)
}
ctx := context.Background()
did, err := kb.Load(ctx, data)
if err != nil {
return "", fmt.Errorf("load DID: %w", err)
}
pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully")
return did, nil
}
func parseFilter(filter string) (*FilterParams, error) {
params := &FilterParams{}
parts := strings.Fields(filter)
func parseFilter(filter string) (*types.FilterParams, error) {
params := &types.FilterParams{}
parts := strings.FieldsSeq(filter)
for _, part := range parts {
for part := range parts {
kv := strings.SplitN(part, ":", 2)
if len(kv) != 2 {
continue
@@ -364,12 +313,7 @@ func parseFilter(filter string) (*FilterParams, error) {
return params, nil
}
func validateUCAN(token string, params *FilterParams) error {
// TODO: Decode UCAN token
// TODO: Verify signature chain
// TODO: Check capabilities match params
// TODO: Verify not expired or revoked
func validateUCAN(token string, params *types.FilterParams) error {
if token == "" {
return errors.New("token is required")
}
@@ -378,11 +322,7 @@ func validateUCAN(token string, params *FilterParams) error {
return nil
}
func executeAction(params *FilterParams) (json.RawMessage, error) {
// TODO: Route to appropriate handler based on resource/action
// TODO: Execute database queries
// TODO: Return results
func executeAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Resource {
case "accounts":
return executeAccountAction(params)
@@ -397,10 +337,10 @@ func executeAction(params *FilterParams) (json.RawMessage, error) {
}
}
func executeAccountAction(params *FilterParams) (json.RawMessage, error) {
func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Action {
case "list":
accounts := []Account{
accounts := []types.Account{
{
Address: "sonr1abc123...",
ChainID: "sonr-mainnet-1",
@@ -419,10 +359,10 @@ func executeAccountAction(params *FilterParams) (json.RawMessage, error) {
}
}
func executeCredentialAction(params *FilterParams) (json.RawMessage, error) {
func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Action {
case "list":
credentials := []Credential{
credentials := []types.Credential{
{
CredentialID: "cred_abc123",
DeviceName: "MacBook Pro",
@@ -439,10 +379,10 @@ func executeCredentialAction(params *FilterParams) (json.RawMessage, error) {
}
}
func executeSessionAction(params *FilterParams) (json.RawMessage, error) {
func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Action {
case "list":
return json.Marshal([]map[string]interface{}{})
return json.Marshal([]map[string]any{})
case "create":
return json.Marshal(map[string]string{"session_id": "sess_placeholder"})
case "revoke":
@@ -452,10 +392,10 @@ func executeSessionAction(params *FilterParams) (json.RawMessage, error) {
}
}
func executeGrantAction(params *FilterParams) (json.RawMessage, error) {
func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) {
switch params.Action {
case "list":
return json.Marshal([]map[string]interface{}{})
return json.Marshal([]map[string]any{})
case "create":
return json.Marshal(map[string]string{"grant_id": "grant_placeholder"})
case "revoke":
@@ -465,16 +405,11 @@ func executeGrantAction(params *FilterParams) (json.RawMessage, error) {
}
}
func resolveDID(did string) (*QueryOutput, error) {
// TODO: Query database for DID document
// TODO: Fetch verification methods
// TODO: Fetch associated accounts
// TODO: Fetch credentials
output := &QueryOutput{
func resolveDID(did string) (*types.QueryOutput, error) {
output := &types.QueryOutput{
DID: did,
Controller: did,
VerificationMethods: []VerificationMethod{
VerificationMethods: []types.VerificationMethod{
{
ID: did + "#key-1",
Type: "Ed25519VerificationKey2020",
@@ -483,7 +418,7 @@ func resolveDID(did string) (*QueryOutput, error) {
Purpose: "authentication",
},
},
Accounts: []Account{
Accounts: []types.Account{
{
Address: "sonr1abc123...",
ChainID: "sonr-mainnet-1",
@@ -494,7 +429,7 @@ func resolveDID(did string) (*QueryOutput, error) {
IsDefault: true,
},
},
Credentials: []Credential{
Credentials: []types.Credential{
{
CredentialID: "cred_abc123",
DeviceName: "MacBook Pro",

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@sonr/motr-enclave",
"version": "0.1.0",
"type": "module",
"main": "./dist/enclave.js",
"module": "./dist/enclave.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/enclave.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"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",
"typecheck": "tsc --noEmit -p src/tsconfig.json",
"clean": "rm -rf dist"
},
"dependencies": {
"@extism/extism": "^2.0.0-rc13"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
},
"peerDependencies": {
"@extism/extism": "^2.0.0-rc13"
}
}

View File

@@ -1,12 +1,12 @@
version: "2"
sql:
- engine: "sqlite"
queries: "db/query.sql"
schema: "db/schema.sql"
queries: "internal/migrations/query.sql"
schema: "internal/migrations/schema.sql"
gen:
go:
package: "db"
out: "db"
package: "keybase"
out: "internal/keybase"
emit_json_tags: true
emit_empty_slices: true
emit_pointers_for_null_types: true

216
src/enclave.ts Normal file
View File

@@ -0,0 +1,216 @@
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);
}

31
src/index.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* Motr Enclave - ESM wrapper for the Extism WebAssembly plugin
*
* @packageDocumentation
*/
export { Enclave, createEnclave } from './enclave';
export type {
// Input/Output types
GenerateInput,
GenerateOutput,
LoadInput,
LoadOutput,
ExecInput,
ExecOutput,
QueryInput,
QueryOutput,
// Shared types
VerificationMethod,
Account,
Credential,
// Options
EnclaveOptions,
// Filter types
Resource,
AccountAction,
CredentialAction,
SessionAction,
GrantAction,
FilterBuilder,
} from './types';

17
src/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "../dist",
"rootDir": ".",
"types": ["bun-types"]
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

129
src/types.ts Normal file
View File

@@ -0,0 +1,129 @@
/**
* TypeScript types for Motr Enclave plugin
* These types match the Go structs in main.go
*/
// ============================================================================
// Generate
// ============================================================================
export interface GenerateInput {
/** Base64-encoded PublicKeyCredential from WebAuthn registration */
credential: string;
}
export interface GenerateOutput {
/** The generated DID (e.g., "did:sonr:abc123") */
did: string;
/** Serialized database buffer for storage */
database: number[];
}
// ============================================================================
// Load
// ============================================================================
export interface LoadInput {
/** Raw database bytes (typically from IPFS CID resolution) */
database: number[];
}
export interface LoadOutput {
success: boolean;
did?: string;
error?: string;
}
// ============================================================================
// Exec
// ============================================================================
export interface ExecInput {
/** GitHub-style filter: "resource:accounts action:sign subject:did:sonr:abc" */
filter: string;
/** UCAN token for authorization (optional) */
token?: string;
}
export interface ExecOutput {
success: boolean;
result?: unknown;
error?: string;
}
// ============================================================================
// Query
// ============================================================================
export interface QueryInput {
/** DID to resolve (empty string uses current DID) */
did: string;
}
export interface QueryOutput {
did: string;
controller: string;
verification_methods: VerificationMethod[];
accounts: Account[];
credentials: Credential[];
}
// ============================================================================
// Shared Types
// ============================================================================
export interface VerificationMethod {
id: string;
type: string;
controller: string;
public_key: string;
purpose: string;
}
export interface Account {
address: string;
chain_id: string;
coin_type: number;
account_index: number;
address_index: number;
label: string;
is_default: boolean;
}
export interface Credential {
credential_id: string;
device_name: string;
device_type: string;
authenticator: string;
transports: string[];
created_at: string;
last_used: string;
}
// ============================================================================
// Enclave Options
// ============================================================================
export interface EnclaveOptions {
/** Custom logger (defaults to console) */
logger?: Pick<Console, 'log' | 'error' | 'warn' | 'info' | 'debug'>;
/** Enable debug logging */
debug?: boolean;
}
// ============================================================================
// Filter Builder Types
// ============================================================================
export type Resource = 'accounts' | 'credentials' | 'sessions' | 'grants';
export type AccountAction = 'list' | 'sign';
export type CredentialAction = 'list';
export type SessionAction = 'list' | 'create' | 'revoke';
export type GrantAction = 'list' | 'create' | 'revoke';
export interface FilterBuilder {
resource(r: Resource): this;
action(a: string): this;
subject(s: string): this;
build(): string;
}