Merge pull request 'Add Extism JS Async Lib and State' (#1) from feat/sdk into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
BIN
.github/db-schema.png
vendored
Normal file
BIN
.github/db-schema.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
example/node_modules
|
||||
build
|
||||
example/enclave.wasm
|
||||
src/dist
|
||||
src/node_modules
|
||||
dist
|
||||
node_modules
|
||||
|
||||
100
Makefile
100
Makefile
@@ -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
186
README.md
@@ -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
32
bun.lock
Normal 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
132
example/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
@@ -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
285
example/main.js
Normal 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();
|
||||
@@ -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"
|
||||
|
||||
205
example/test.js
205
example/test.js
@@ -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
9
go.mod
@@ -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
8
go.sum
@@ -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
259
internal/keybase/conn.go
Normal 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
31
internal/keybase/db.go
Normal 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
170
internal/keybase/models.go
Normal 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
118
internal/keybase/querier.go
Normal 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)
|
||||
1996
internal/keybase/query.sql.go
Normal file
1996
internal/keybase/query.sql.go
Normal file
File diff suppressed because it is too large
Load Diff
12
internal/migrations/embed.go
Normal file
12
internal/migrations/embed.go
Normal 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
|
||||
@@ -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
113
internal/state/state.go
Normal 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
16
internal/types/exec.go
Normal 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"`
|
||||
}
|
||||
9
internal/types/filters.go
Normal file
9
internal/types/filters.go
Normal 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
|
||||
}
|
||||
12
internal/types/generate.go
Normal file
12
internal/types/generate.go
Normal 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
13
internal/types/load.go
Normal 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
11
internal/types/ping.go
Normal 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
46
internal/types/query.go
Normal 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
283
main.go
@@ -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
32
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
216
src/enclave.ts
Normal 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
31
src/index.ts
Normal 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
17
src/tsconfig.json
Normal 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
129
src/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user