diff --git a/.github/db-schema.png b/.github/db-schema.png
new file mode 100644
index 0000000..b56e0a9
Binary files /dev/null and b/.github/db-schema.png differ
diff --git a/.gitignore b/.gitignore
index 5c5acf0..e070b11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
example/node_modules
build
example/enclave.wasm
+src/dist
+src/node_modules
+dist
+node_modules
diff --git a/Makefile b/Makefile
index badaa45..609b830 100644
--- a/Makefile
+++ b/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"
diff --git a/README.md b/README.md
index 45b629d..aa9a683 100644
--- a/README.md
+++ b/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 |
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..794b047
--- /dev/null
+++ b/bun.lock
@@ -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=="],
+ }
+}
diff --git a/example/bun.lock b/example/bun.lock
new file mode 100644
index 0000000..885e68b
--- /dev/null
+++ b/example/bun.lock
@@ -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=="],
+ }
+}
diff --git a/example/index.html b/example/index.html
index 70b9eb3..df544c0 100644
--- a/example/index.html
+++ b/example/index.html
@@ -1,168 +1,107 @@
-
-
- Motr Enclave Test
-
+
+
+ Motr Enclave
+
- Motr Enclave Plugin Test
+
+
Motr Enclave
-
Plugin Status
-
Loading plugin...
-
+
Status
+
Loading...
+
-
generate()
-
Initialize database with WebAuthn credential
-
-
-
-
+
+
+
+
-
load()
-
Load database from serialized buffer
-
-
-
-
-
+
+
+
+
+
+
-
exec()
-
Execute action with GitHub-style filter syntax
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
query()
-
Resolve DID to document with resources
-
-
-
-
+
+
+
+
+
+
+
+
+
-
Console Log
-
-
+
+
+
+
+
-
+
diff --git a/example/main.js b/example/main.js
new file mode 100644
index 0000000..e92f33d
--- /dev/null
+++ b/example/main.js
@@ -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 = `${time} ${message}`;
+ if (data !== null) {
+ const json = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
+ entry += `${json}`;
+ }
+ entry += '
';
+
+ 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();
diff --git a/example/package.json b/example/package.json
index c604c65..6a54102 100644
--- a/example/package.json
+++ b/example/package.json
@@ -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"
diff --git a/example/test.js b/example/test.js
deleted file mode 100644
index 9537320..0000000
--- a/example/test.js
+++ /dev/null
@@ -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();
diff --git a/go.mod b/go.mod
index b3fa137..de29d03 100644
--- a/go.mod
+++ b/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
+)
diff --git a/go.sum b/go.sum
index c15d382..4e755c3 100644
--- a/go.sum
+++ b/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=
diff --git a/internal/keybase/conn.go b/internal/keybase/conn.go
new file mode 100644
index 0000000..dbd0f93
--- /dev/null
+++ b/internal/keybase/conn.go
@@ -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()
+}
diff --git a/internal/keybase/db.go b/internal/keybase/db.go
new file mode 100644
index 0000000..111c37b
--- /dev/null
+++ b/internal/keybase/db.go
@@ -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,
+ }
+}
diff --git a/internal/keybase/models.go b/internal/keybase/models.go
new file mode 100644
index 0000000..2f44611
--- /dev/null
+++ b/internal/keybase/models.go
@@ -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"`
+}
diff --git a/internal/keybase/querier.go b/internal/keybase/querier.go
new file mode 100644
index 0000000..c04bbb9
--- /dev/null
+++ b/internal/keybase/querier.go
@@ -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)
diff --git a/internal/keybase/query.sql.go b/internal/keybase/query.sql.go
new file mode 100644
index 0000000..09086ce
--- /dev/null
+++ b/internal/keybase/query.sql.go
@@ -0,0 +1,1996 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.30.0
+// source: query.sql
+
+package keybase
+
+import (
+ "context"
+ "encoding/json"
+)
+
+const archiveKeyShare = `-- name: ArchiveKeyShare :exec
+UPDATE key_shares SET status = 'archived' WHERE id = ?
+`
+
+func (q *Queries) ArchiveKeyShare(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, archiveKeyShare, id)
+ return err
+}
+
+const cleanExpiredUCANs = `-- name: CleanExpiredUCANs :exec
+DELETE FROM ucan_tokens WHERE expires_at < datetime('now', '-30 days')
+`
+
+func (q *Queries) CleanExpiredUCANs(ctx context.Context) error {
+ _, err := q.db.ExecContext(ctx, cleanExpiredUCANs)
+ return err
+}
+
+const countActiveGrants = `-- name: CountActiveGrants :one
+SELECT COUNT(*) FROM grants WHERE did_id = ? AND status = 'active'
+`
+
+func (q *Queries) CountActiveGrants(ctx context.Context, didID int64) (int64, error) {
+ row := q.db.QueryRowContext(ctx, countActiveGrants, didID)
+ var count int64
+ err := row.Scan(&count)
+ return count, err
+}
+
+const countCredentialsByDID = `-- name: CountCredentialsByDID :one
+SELECT COUNT(*) FROM credentials WHERE did_id = ?
+`
+
+func (q *Queries) CountCredentialsByDID(ctx context.Context, didID int64) (int64, error) {
+ row := q.db.QueryRowContext(ctx, countCredentialsByDID, didID)
+ var count int64
+ err := row.Scan(&count)
+ return count, err
+}
+
+const createAccount = `-- name: CreateAccount :one
+INSERT INTO accounts (did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at
+`
+
+type CreateAccountParams struct {
+ 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"`
+}
+
+func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
+ row := q.db.QueryRowContext(ctx, createAccount,
+ arg.DidID,
+ arg.KeyShareID,
+ arg.Address,
+ arg.ChainID,
+ arg.CoinType,
+ arg.AccountIndex,
+ arg.AddressIndex,
+ arg.Label,
+ )
+ var i Account
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.KeyShareID,
+ &i.Address,
+ &i.ChainID,
+ &i.CoinType,
+ &i.AccountIndex,
+ &i.AddressIndex,
+ &i.Label,
+ &i.IsDefault,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const createCredential = `-- name: CreateCredential :one
+INSERT INTO credentials (
+ did_id, credential_id, public_key, public_key_alg, aaguid,
+ transports, device_name, device_type, authenticator, is_discoverable, backed_up
+)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, credential_id, public_key, public_key_alg, aaguid, sign_count, transports, device_name, device_type, authenticator, is_discoverable, backed_up, created_at, last_used
+`
+
+type CreateCredentialParams struct {
+ 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"`
+ 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"`
+}
+
+func (q *Queries) CreateCredential(ctx context.Context, arg CreateCredentialParams) (Credential, error) {
+ row := q.db.QueryRowContext(ctx, createCredential,
+ arg.DidID,
+ arg.CredentialID,
+ arg.PublicKey,
+ arg.PublicKeyAlg,
+ arg.Aaguid,
+ arg.Transports,
+ arg.DeviceName,
+ arg.DeviceType,
+ arg.Authenticator,
+ arg.IsDiscoverable,
+ arg.BackedUp,
+ )
+ var i Credential
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.PublicKey,
+ &i.PublicKeyAlg,
+ &i.Aaguid,
+ &i.SignCount,
+ &i.Transports,
+ &i.DeviceName,
+ &i.DeviceType,
+ &i.Authenticator,
+ &i.IsDiscoverable,
+ &i.BackedUp,
+ &i.CreatedAt,
+ &i.LastUsed,
+ )
+ return i, err
+}
+
+const createDID = `-- name: CreateDID :one
+INSERT INTO did_documents (did, controller, document, sequence)
+VALUES (?, ?, ?, ?)
+RETURNING id, did, controller, document, sequence, last_synced, created_at, updated_at
+`
+
+type CreateDIDParams struct {
+ Did string `json:"did"`
+ Controller string `json:"controller"`
+ Document json.RawMessage `json:"document"`
+ Sequence int64 `json:"sequence"`
+}
+
+func (q *Queries) CreateDID(ctx context.Context, arg CreateDIDParams) (DidDocument, error) {
+ row := q.db.QueryRowContext(ctx, createDID,
+ arg.Did,
+ arg.Controller,
+ arg.Document,
+ arg.Sequence,
+ )
+ var i DidDocument
+ err := row.Scan(
+ &i.ID,
+ &i.Did,
+ &i.Controller,
+ &i.Document,
+ &i.Sequence,
+ &i.LastSynced,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const createDelegation = `-- name: CreateDelegation :one
+INSERT INTO delegations (
+ did_id, ucan_id, delegator, delegate, resource, action, caveats, parent_id, depth, expires_at
+)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at
+`
+
+type CreateDelegationParams struct {
+ 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"`
+ ExpiresAt *string `json:"expires_at"`
+}
+
+func (q *Queries) CreateDelegation(ctx context.Context, arg CreateDelegationParams) (Delegation, error) {
+ row := q.db.QueryRowContext(ctx, createDelegation,
+ arg.DidID,
+ arg.UcanID,
+ arg.Delegator,
+ arg.Delegate,
+ arg.Resource,
+ arg.Action,
+ arg.Caveats,
+ arg.ParentID,
+ arg.Depth,
+ arg.ExpiresAt,
+ )
+ var i Delegation
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.UcanID,
+ &i.Delegator,
+ &i.Delegate,
+ &i.Resource,
+ &i.Action,
+ &i.Caveats,
+ &i.ParentID,
+ &i.Depth,
+ &i.Status,
+ &i.CreatedAt,
+ &i.ExpiresAt,
+ )
+ return i, err
+}
+
+const createGrant = `-- name: CreateGrant :one
+INSERT INTO grants (did_id, service_id, ucan_id, scopes, accounts, expires_at)
+VALUES (?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, service_id, ucan_id, scopes, accounts, status, granted_at, last_used, expires_at
+`
+
+type CreateGrantParams struct {
+ 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"`
+ ExpiresAt *string `json:"expires_at"`
+}
+
+func (q *Queries) CreateGrant(ctx context.Context, arg CreateGrantParams) (Grant, error) {
+ row := q.db.QueryRowContext(ctx, createGrant,
+ arg.DidID,
+ arg.ServiceID,
+ arg.UcanID,
+ arg.Scopes,
+ arg.Accounts,
+ arg.ExpiresAt,
+ )
+ var i Grant
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ServiceID,
+ &i.UcanID,
+ &i.Scopes,
+ &i.Accounts,
+ &i.Status,
+ &i.GrantedAt,
+ &i.LastUsed,
+ &i.ExpiresAt,
+ )
+ return i, err
+}
+
+const createKeyShare = `-- name: CreateKeyShare :one
+INSERT INTO key_shares (
+ did_id, share_id, key_id, party_index, threshold, total_parties,
+ curve, share_data, public_key, chain_code, derivation_path
+)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at
+`
+
+type CreateKeyShareParams struct {
+ 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"`
+}
+
+func (q *Queries) CreateKeyShare(ctx context.Context, arg CreateKeyShareParams) (KeyShare, error) {
+ row := q.db.QueryRowContext(ctx, createKeyShare,
+ arg.DidID,
+ arg.ShareID,
+ arg.KeyID,
+ arg.PartyIndex,
+ arg.Threshold,
+ arg.TotalParties,
+ arg.Curve,
+ arg.ShareData,
+ arg.PublicKey,
+ arg.ChainCode,
+ arg.DerivationPath,
+ )
+ var i KeyShare
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ShareID,
+ &i.KeyID,
+ &i.PartyIndex,
+ &i.Threshold,
+ &i.TotalParties,
+ &i.Curve,
+ &i.ShareData,
+ &i.PublicKey,
+ &i.ChainCode,
+ &i.DerivationPath,
+ &i.Status,
+ &i.CreatedAt,
+ &i.RotatedAt,
+ )
+ return i, err
+}
+
+const createRevocation = `-- name: CreateRevocation :exec
+INSERT INTO ucan_revocations (ucan_cid, revoked_by, reason)
+VALUES (?, ?, ?)
+`
+
+type CreateRevocationParams struct {
+ UcanCid string `json:"ucan_cid"`
+ RevokedBy string `json:"revoked_by"`
+ Reason *string `json:"reason"`
+}
+
+func (q *Queries) CreateRevocation(ctx context.Context, arg CreateRevocationParams) error {
+ _, err := q.db.ExecContext(ctx, createRevocation, arg.UcanCid, arg.RevokedBy, arg.Reason)
+ return err
+}
+
+const createService = `-- name: CreateService :one
+INSERT INTO services (origin, name, description, logo_url, did, is_verified, metadata)
+VALUES (?, ?, ?, ?, ?, ?, ?)
+RETURNING id, origin, name, description, logo_url, did, is_verified, metadata, created_at
+`
+
+type CreateServiceParams struct {
+ 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"`
+}
+
+func (q *Queries) CreateService(ctx context.Context, arg CreateServiceParams) (Service, error) {
+ row := q.db.QueryRowContext(ctx, createService,
+ arg.Origin,
+ arg.Name,
+ arg.Description,
+ arg.LogoUrl,
+ arg.Did,
+ arg.IsVerified,
+ arg.Metadata,
+ )
+ var i Service
+ err := row.Scan(
+ &i.ID,
+ &i.Origin,
+ &i.Name,
+ &i.Description,
+ &i.LogoUrl,
+ &i.Did,
+ &i.IsVerified,
+ &i.Metadata,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const createSession = `-- name: CreateSession :one
+INSERT INTO sessions (did_id, credential_id, session_id, device_info, is_current, expires_at)
+VALUES (?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, credential_id, session_id, device_info, is_current, last_activity, expires_at, created_at
+`
+
+type CreateSessionParams struct {
+ 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"`
+ ExpiresAt string `json:"expires_at"`
+}
+
+func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
+ row := q.db.QueryRowContext(ctx, createSession,
+ arg.DidID,
+ arg.CredentialID,
+ arg.SessionID,
+ arg.DeviceInfo,
+ arg.IsCurrent,
+ arg.ExpiresAt,
+ )
+ var i Session
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.SessionID,
+ &i.DeviceInfo,
+ &i.IsCurrent,
+ &i.LastActivity,
+ &i.ExpiresAt,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const createUCAN = `-- name: CreateUCAN :one
+INSERT INTO ucan_tokens (
+ did_id, cid, issuer, audience, subject, capabilities,
+ proof_chain, not_before, expires_at, nonce, facts, signature, raw_token
+)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at
+`
+
+type CreateUCANParams struct {
+ 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"`
+}
+
+func (q *Queries) CreateUCAN(ctx context.Context, arg CreateUCANParams) (UcanToken, error) {
+ row := q.db.QueryRowContext(ctx, createUCAN,
+ arg.DidID,
+ arg.Cid,
+ arg.Issuer,
+ arg.Audience,
+ arg.Subject,
+ arg.Capabilities,
+ arg.ProofChain,
+ arg.NotBefore,
+ arg.ExpiresAt,
+ arg.Nonce,
+ arg.Facts,
+ arg.Signature,
+ arg.RawToken,
+ )
+ var i UcanToken
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.Cid,
+ &i.Issuer,
+ &i.Audience,
+ &i.Subject,
+ &i.Capabilities,
+ &i.ProofChain,
+ &i.NotBefore,
+ &i.ExpiresAt,
+ &i.Nonce,
+ &i.Facts,
+ &i.Signature,
+ &i.RawToken,
+ &i.IsRevoked,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const createVerificationMethod = `-- name: CreateVerificationMethod :one
+INSERT INTO verification_methods (did_id, method_id, method_type, controller, public_key, purpose)
+VALUES (?, ?, ?, ?, ?, ?)
+RETURNING id, did_id, method_id, method_type, controller, public_key, purpose, created_at
+`
+
+type CreateVerificationMethodParams struct {
+ 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"`
+}
+
+func (q *Queries) CreateVerificationMethod(ctx context.Context, arg CreateVerificationMethodParams) (VerificationMethod, error) {
+ row := q.db.QueryRowContext(ctx, createVerificationMethod,
+ arg.DidID,
+ arg.MethodID,
+ arg.MethodType,
+ arg.Controller,
+ arg.PublicKey,
+ arg.Purpose,
+ )
+ var i VerificationMethod
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.MethodID,
+ &i.MethodType,
+ &i.Controller,
+ &i.PublicKey,
+ &i.Purpose,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const deleteAccount = `-- name: DeleteAccount :exec
+DELETE FROM accounts WHERE id = ? AND did_id = ?
+`
+
+type DeleteAccountParams struct {
+ ID int64 `json:"id"`
+ DidID int64 `json:"did_id"`
+}
+
+func (q *Queries) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error {
+ _, err := q.db.ExecContext(ctx, deleteAccount, arg.ID, arg.DidID)
+ return err
+}
+
+const deleteCredential = `-- name: DeleteCredential :exec
+DELETE FROM credentials WHERE id = ? AND did_id = ?
+`
+
+type DeleteCredentialParams struct {
+ ID int64 `json:"id"`
+ DidID int64 `json:"did_id"`
+}
+
+func (q *Queries) DeleteCredential(ctx context.Context, arg DeleteCredentialParams) error {
+ _, err := q.db.ExecContext(ctx, deleteCredential, arg.ID, arg.DidID)
+ return err
+}
+
+const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
+DELETE FROM sessions WHERE expires_at < datetime('now')
+`
+
+func (q *Queries) DeleteExpiredSessions(ctx context.Context) error {
+ _, err := q.db.ExecContext(ctx, deleteExpiredSessions)
+ return err
+}
+
+const deleteKeyShare = `-- name: DeleteKeyShare :exec
+DELETE FROM key_shares WHERE id = ? AND did_id = ?
+`
+
+type DeleteKeyShareParams struct {
+ ID int64 `json:"id"`
+ DidID int64 `json:"did_id"`
+}
+
+func (q *Queries) DeleteKeyShare(ctx context.Context, arg DeleteKeyShareParams) error {
+ _, err := q.db.ExecContext(ctx, deleteKeyShare, arg.ID, arg.DidID)
+ return err
+}
+
+const deleteSession = `-- name: DeleteSession :exec
+DELETE FROM sessions WHERE id = ?
+`
+
+func (q *Queries) DeleteSession(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, deleteSession, id)
+ return err
+}
+
+const deleteVerificationMethod = `-- name: DeleteVerificationMethod :exec
+DELETE FROM verification_methods WHERE id = ?
+`
+
+func (q *Queries) DeleteVerificationMethod(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, deleteVerificationMethod, id)
+ return err
+}
+
+const getAccountByAddress = `-- name: GetAccountByAddress :one
+SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE address = ? LIMIT 1
+`
+
+func (q *Queries) GetAccountByAddress(ctx context.Context, address string) (Account, error) {
+ row := q.db.QueryRowContext(ctx, getAccountByAddress, address)
+ var i Account
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.KeyShareID,
+ &i.Address,
+ &i.ChainID,
+ &i.CoinType,
+ &i.AccountIndex,
+ &i.AddressIndex,
+ &i.Label,
+ &i.IsDefault,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getCredentialByID = `-- name: GetCredentialByID :one
+SELECT id, did_id, credential_id, public_key, public_key_alg, aaguid, sign_count, transports, device_name, device_type, authenticator, is_discoverable, backed_up, created_at, last_used FROM credentials WHERE credential_id = ? LIMIT 1
+`
+
+func (q *Queries) GetCredentialByID(ctx context.Context, credentialID string) (Credential, error) {
+ row := q.db.QueryRowContext(ctx, getCredentialByID, credentialID)
+ var i Credential
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.PublicKey,
+ &i.PublicKeyAlg,
+ &i.Aaguid,
+ &i.SignCount,
+ &i.Transports,
+ &i.DeviceName,
+ &i.DeviceType,
+ &i.Authenticator,
+ &i.IsDiscoverable,
+ &i.BackedUp,
+ &i.CreatedAt,
+ &i.LastUsed,
+ )
+ return i, err
+}
+
+const getCurrentSession = `-- name: GetCurrentSession :one
+SELECT id, did_id, credential_id, session_id, device_info, is_current, last_activity, expires_at, created_at FROM sessions WHERE did_id = ? AND is_current = 1 LIMIT 1
+`
+
+func (q *Queries) GetCurrentSession(ctx context.Context, didID int64) (Session, error) {
+ row := q.db.QueryRowContext(ctx, getCurrentSession, didID)
+ var i Session
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.SessionID,
+ &i.DeviceInfo,
+ &i.IsCurrent,
+ &i.LastActivity,
+ &i.ExpiresAt,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getDIDByDID = `-- name: GetDIDByDID :one
+
+SELECT id, did, controller, document, sequence, last_synced, created_at, updated_at FROM did_documents WHERE did = ? LIMIT 1
+`
+
+// =============================================================================
+// DID DOCUMENT QUERIES
+// =============================================================================
+func (q *Queries) GetDIDByDID(ctx context.Context, did string) (DidDocument, error) {
+ row := q.db.QueryRowContext(ctx, getDIDByDID, did)
+ var i DidDocument
+ err := row.Scan(
+ &i.ID,
+ &i.Did,
+ &i.Controller,
+ &i.Document,
+ &i.Sequence,
+ &i.LastSynced,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const getDIDByID = `-- name: GetDIDByID :one
+SELECT id, did, controller, document, sequence, last_synced, created_at, updated_at FROM did_documents WHERE id = ? LIMIT 1
+`
+
+func (q *Queries) GetDIDByID(ctx context.Context, id int64) (DidDocument, error) {
+ row := q.db.QueryRowContext(ctx, getDIDByID, id)
+ var i DidDocument
+ err := row.Scan(
+ &i.ID,
+ &i.Did,
+ &i.Controller,
+ &i.Document,
+ &i.Sequence,
+ &i.LastSynced,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const getDefaultAccount = `-- name: GetDefaultAccount :one
+SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? AND is_default = 1 LIMIT 1
+`
+
+type GetDefaultAccountParams struct {
+ DidID int64 `json:"did_id"`
+ ChainID string `json:"chain_id"`
+}
+
+func (q *Queries) GetDefaultAccount(ctx context.Context, arg GetDefaultAccountParams) (Account, error) {
+ row := q.db.QueryRowContext(ctx, getDefaultAccount, arg.DidID, arg.ChainID)
+ var i Account
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.KeyShareID,
+ &i.Address,
+ &i.ChainID,
+ &i.CoinType,
+ &i.AccountIndex,
+ &i.AddressIndex,
+ &i.Label,
+ &i.IsDefault,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getDelegationChain = `-- name: GetDelegationChain :many
+SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations WHERE id = ? OR parent_id = ? ORDER BY depth DESC
+`
+
+type GetDelegationChainParams struct {
+ ID int64 `json:"id"`
+ ParentID *int64 `json:"parent_id"`
+}
+
+func (q *Queries) GetDelegationChain(ctx context.Context, arg GetDelegationChainParams) ([]Delegation, error) {
+ rows, err := q.db.QueryContext(ctx, getDelegationChain, arg.ID, arg.ParentID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Delegation{}
+ for rows.Next() {
+ var i Delegation
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.UcanID,
+ &i.Delegator,
+ &i.Delegate,
+ &i.Resource,
+ &i.Action,
+ &i.Caveats,
+ &i.ParentID,
+ &i.Depth,
+ &i.Status,
+ &i.CreatedAt,
+ &i.ExpiresAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const getGrantByService = `-- name: GetGrantByService :one
+SELECT id, did_id, service_id, ucan_id, scopes, accounts, status, granted_at, last_used, expires_at FROM grants WHERE did_id = ? AND service_id = ? LIMIT 1
+`
+
+type GetGrantByServiceParams struct {
+ DidID int64 `json:"did_id"`
+ ServiceID int64 `json:"service_id"`
+}
+
+func (q *Queries) GetGrantByService(ctx context.Context, arg GetGrantByServiceParams) (Grant, error) {
+ row := q.db.QueryRowContext(ctx, getGrantByService, arg.DidID, arg.ServiceID)
+ var i Grant
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ServiceID,
+ &i.UcanID,
+ &i.Scopes,
+ &i.Accounts,
+ &i.Status,
+ &i.GrantedAt,
+ &i.LastUsed,
+ &i.ExpiresAt,
+ )
+ return i, err
+}
+
+const getKeyShareByID = `-- name: GetKeyShareByID :one
+SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE share_id = ? LIMIT 1
+`
+
+func (q *Queries) GetKeyShareByID(ctx context.Context, shareID string) (KeyShare, error) {
+ row := q.db.QueryRowContext(ctx, getKeyShareByID, shareID)
+ var i KeyShare
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ShareID,
+ &i.KeyID,
+ &i.PartyIndex,
+ &i.Threshold,
+ &i.TotalParties,
+ &i.Curve,
+ &i.ShareData,
+ &i.PublicKey,
+ &i.ChainCode,
+ &i.DerivationPath,
+ &i.Status,
+ &i.CreatedAt,
+ &i.RotatedAt,
+ )
+ return i, err
+}
+
+const getKeyShareByKeyID = `-- name: GetKeyShareByKeyID :one
+SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE did_id = ? AND key_id = ? AND status = 'active' LIMIT 1
+`
+
+type GetKeyShareByKeyIDParams struct {
+ DidID int64 `json:"did_id"`
+ KeyID string `json:"key_id"`
+}
+
+func (q *Queries) GetKeyShareByKeyID(ctx context.Context, arg GetKeyShareByKeyIDParams) (KeyShare, error) {
+ row := q.db.QueryRowContext(ctx, getKeyShareByKeyID, arg.DidID, arg.KeyID)
+ var i KeyShare
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ShareID,
+ &i.KeyID,
+ &i.PartyIndex,
+ &i.Threshold,
+ &i.TotalParties,
+ &i.Curve,
+ &i.ShareData,
+ &i.PublicKey,
+ &i.ChainCode,
+ &i.DerivationPath,
+ &i.Status,
+ &i.CreatedAt,
+ &i.RotatedAt,
+ )
+ return i, err
+}
+
+const getServiceByID = `-- name: GetServiceByID :one
+SELECT id, origin, name, description, logo_url, did, is_verified, metadata, created_at FROM services WHERE id = ? LIMIT 1
+`
+
+func (q *Queries) GetServiceByID(ctx context.Context, id int64) (Service, error) {
+ row := q.db.QueryRowContext(ctx, getServiceByID, id)
+ var i Service
+ err := row.Scan(
+ &i.ID,
+ &i.Origin,
+ &i.Name,
+ &i.Description,
+ &i.LogoUrl,
+ &i.Did,
+ &i.IsVerified,
+ &i.Metadata,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getServiceByOrigin = `-- name: GetServiceByOrigin :one
+
+SELECT id, origin, name, description, logo_url, did, is_verified, metadata, created_at FROM services WHERE origin = ? LIMIT 1
+`
+
+// =============================================================================
+// SERVICE QUERIES
+// =============================================================================
+func (q *Queries) GetServiceByOrigin(ctx context.Context, origin string) (Service, error) {
+ row := q.db.QueryRowContext(ctx, getServiceByOrigin, origin)
+ var i Service
+ err := row.Scan(
+ &i.ID,
+ &i.Origin,
+ &i.Name,
+ &i.Description,
+ &i.LogoUrl,
+ &i.Did,
+ &i.IsVerified,
+ &i.Metadata,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getSessionByID = `-- name: GetSessionByID :one
+SELECT id, did_id, credential_id, session_id, device_info, is_current, last_activity, expires_at, created_at FROM sessions WHERE session_id = ? LIMIT 1
+`
+
+func (q *Queries) GetSessionByID(ctx context.Context, sessionID string) (Session, error) {
+ row := q.db.QueryRowContext(ctx, getSessionByID, sessionID)
+ var i Session
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.SessionID,
+ &i.DeviceInfo,
+ &i.IsCurrent,
+ &i.LastActivity,
+ &i.ExpiresAt,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getSyncCheckpoint = `-- name: GetSyncCheckpoint :one
+
+SELECT id, did_id, resource_type, last_block, last_tx_hash, last_synced FROM sync_checkpoints WHERE did_id = ? AND resource_type = ? LIMIT 1
+`
+
+type GetSyncCheckpointParams struct {
+ DidID int64 `json:"did_id"`
+ ResourceType string `json:"resource_type"`
+}
+
+// =============================================================================
+// SYNC QUERIES
+// =============================================================================
+func (q *Queries) GetSyncCheckpoint(ctx context.Context, arg GetSyncCheckpointParams) (SyncCheckpoint, error) {
+ row := q.db.QueryRowContext(ctx, getSyncCheckpoint, arg.DidID, arg.ResourceType)
+ var i SyncCheckpoint
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ResourceType,
+ &i.LastBlock,
+ &i.LastTxHash,
+ &i.LastSynced,
+ )
+ return i, err
+}
+
+const getUCANByCID = `-- name: GetUCANByCID :one
+SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens WHERE cid = ? LIMIT 1
+`
+
+func (q *Queries) GetUCANByCID(ctx context.Context, cid string) (UcanToken, error) {
+ row := q.db.QueryRowContext(ctx, getUCANByCID, cid)
+ var i UcanToken
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.Cid,
+ &i.Issuer,
+ &i.Audience,
+ &i.Subject,
+ &i.Capabilities,
+ &i.ProofChain,
+ &i.NotBefore,
+ &i.ExpiresAt,
+ &i.Nonce,
+ &i.Facts,
+ &i.Signature,
+ &i.RawToken,
+ &i.IsRevoked,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const getVerificationMethod = `-- name: GetVerificationMethod :one
+SELECT id, did_id, method_id, method_type, controller, public_key, purpose, created_at FROM verification_methods WHERE did_id = ? AND method_id = ? LIMIT 1
+`
+
+type GetVerificationMethodParams struct {
+ DidID int64 `json:"did_id"`
+ MethodID string `json:"method_id"`
+}
+
+func (q *Queries) GetVerificationMethod(ctx context.Context, arg GetVerificationMethodParams) (VerificationMethod, error) {
+ row := q.db.QueryRowContext(ctx, getVerificationMethod, arg.DidID, arg.MethodID)
+ var i VerificationMethod
+ err := row.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.MethodID,
+ &i.MethodType,
+ &i.Controller,
+ &i.PublicKey,
+ &i.Purpose,
+ &i.CreatedAt,
+ )
+ return i, err
+}
+
+const isUCANRevoked = `-- name: IsUCANRevoked :one
+SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE ucan_cid = ?) as revoked
+`
+
+func (q *Queries) IsUCANRevoked(ctx context.Context, ucanCid string) (int64, error) {
+ row := q.db.QueryRowContext(ctx, isUCANRevoked, ucanCid)
+ var revoked int64
+ err := row.Scan(&revoked)
+ return revoked, err
+}
+
+const listAccountsByChain = `-- name: ListAccountsByChain :many
+SELECT id, did_id, key_share_id, address, chain_id, coin_type, account_index, address_index, label, is_default, created_at FROM accounts WHERE did_id = ? AND chain_id = ? ORDER BY account_index, address_index
+`
+
+type ListAccountsByChainParams struct {
+ DidID int64 `json:"did_id"`
+ ChainID string `json:"chain_id"`
+}
+
+func (q *Queries) ListAccountsByChain(ctx context.Context, arg ListAccountsByChainParams) ([]Account, error) {
+ rows, err := q.db.QueryContext(ctx, listAccountsByChain, arg.DidID, arg.ChainID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Account{}
+ for rows.Next() {
+ var i Account
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.KeyShareID,
+ &i.Address,
+ &i.ChainID,
+ &i.CoinType,
+ &i.AccountIndex,
+ &i.AddressIndex,
+ &i.Label,
+ &i.IsDefault,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listAccountsByDID = `-- name: ListAccountsByDID :many
+
+SELECT a.id, a.did_id, a.key_share_id, a.address, a.chain_id, a.coin_type, a.account_index, a.address_index, a.label, a.is_default, a.created_at, k.public_key, k.curve
+FROM accounts a
+JOIN key_shares k ON a.key_share_id = k.id
+WHERE a.did_id = ?
+ORDER BY a.is_default DESC, a.created_at
+`
+
+type ListAccountsByDIDRow 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"`
+ PublicKey string `json:"public_key"`
+ Curve string `json:"curve"`
+}
+
+// =============================================================================
+// ACCOUNT QUERIES
+// =============================================================================
+func (q *Queries) ListAccountsByDID(ctx context.Context, didID int64) ([]ListAccountsByDIDRow, error) {
+ rows, err := q.db.QueryContext(ctx, listAccountsByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []ListAccountsByDIDRow{}
+ for rows.Next() {
+ var i ListAccountsByDIDRow
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.KeyShareID,
+ &i.Address,
+ &i.ChainID,
+ &i.CoinType,
+ &i.AccountIndex,
+ &i.AddressIndex,
+ &i.Label,
+ &i.IsDefault,
+ &i.CreatedAt,
+ &i.PublicKey,
+ &i.Curve,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listAllDIDs = `-- name: ListAllDIDs :many
+SELECT id, did, controller, document, sequence, last_synced, created_at, updated_at FROM did_documents ORDER BY created_at DESC
+`
+
+func (q *Queries) ListAllDIDs(ctx context.Context) ([]DidDocument, error) {
+ rows, err := q.db.QueryContext(ctx, listAllDIDs)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []DidDocument{}
+ for rows.Next() {
+ var i DidDocument
+ if err := rows.Scan(
+ &i.ID,
+ &i.Did,
+ &i.Controller,
+ &i.Document,
+ &i.Sequence,
+ &i.LastSynced,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listCredentialsByDID = `-- name: ListCredentialsByDID :many
+
+SELECT id, did_id, credential_id, public_key, public_key_alg, aaguid, sign_count, transports, device_name, device_type, authenticator, is_discoverable, backed_up, created_at, last_used FROM credentials WHERE did_id = ? ORDER BY last_used DESC
+`
+
+// =============================================================================
+// CREDENTIAL QUERIES
+// =============================================================================
+func (q *Queries) ListCredentialsByDID(ctx context.Context, didID int64) ([]Credential, error) {
+ rows, err := q.db.QueryContext(ctx, listCredentialsByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Credential{}
+ for rows.Next() {
+ var i Credential
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.PublicKey,
+ &i.PublicKeyAlg,
+ &i.Aaguid,
+ &i.SignCount,
+ &i.Transports,
+ &i.DeviceName,
+ &i.DeviceType,
+ &i.Authenticator,
+ &i.IsDiscoverable,
+ &i.BackedUp,
+ &i.CreatedAt,
+ &i.LastUsed,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listDelegationsByDelegate = `-- name: ListDelegationsByDelegate :many
+SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations
+WHERE delegate = ? AND status = 'active' AND (expires_at IS NULL OR expires_at > datetime('now'))
+ORDER BY created_at DESC
+`
+
+func (q *Queries) ListDelegationsByDelegate(ctx context.Context, delegate string) ([]Delegation, error) {
+ rows, err := q.db.QueryContext(ctx, listDelegationsByDelegate, delegate)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Delegation{}
+ for rows.Next() {
+ var i Delegation
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.UcanID,
+ &i.Delegator,
+ &i.Delegate,
+ &i.Resource,
+ &i.Action,
+ &i.Caveats,
+ &i.ParentID,
+ &i.Depth,
+ &i.Status,
+ &i.CreatedAt,
+ &i.ExpiresAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listDelegationsByDelegator = `-- name: ListDelegationsByDelegator :many
+
+SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations
+WHERE delegator = ? AND status = 'active'
+ORDER BY created_at DESC
+`
+
+// =============================================================================
+// DELEGATION QUERIES
+// =============================================================================
+func (q *Queries) ListDelegationsByDelegator(ctx context.Context, delegator string) ([]Delegation, error) {
+ rows, err := q.db.QueryContext(ctx, listDelegationsByDelegator, delegator)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Delegation{}
+ for rows.Next() {
+ var i Delegation
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.UcanID,
+ &i.Delegator,
+ &i.Delegate,
+ &i.Resource,
+ &i.Action,
+ &i.Caveats,
+ &i.ParentID,
+ &i.Depth,
+ &i.Status,
+ &i.CreatedAt,
+ &i.ExpiresAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listDelegationsForResource = `-- name: ListDelegationsForResource :many
+SELECT id, did_id, ucan_id, delegator, delegate, resource, "action", caveats, parent_id, depth, status, created_at, expires_at FROM delegations
+WHERE did_id = ? AND resource = ? AND status = 'active'
+ORDER BY depth, created_at
+`
+
+type ListDelegationsForResourceParams struct {
+ DidID int64 `json:"did_id"`
+ Resource string `json:"resource"`
+}
+
+func (q *Queries) ListDelegationsForResource(ctx context.Context, arg ListDelegationsForResourceParams) ([]Delegation, error) {
+ rows, err := q.db.QueryContext(ctx, listDelegationsForResource, arg.DidID, arg.Resource)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Delegation{}
+ for rows.Next() {
+ var i Delegation
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.UcanID,
+ &i.Delegator,
+ &i.Delegate,
+ &i.Resource,
+ &i.Action,
+ &i.Caveats,
+ &i.ParentID,
+ &i.Depth,
+ &i.Status,
+ &i.CreatedAt,
+ &i.ExpiresAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listGrantsByDID = `-- name: ListGrantsByDID :many
+
+SELECT g.id, g.did_id, g.service_id, g.ucan_id, g.scopes, g.accounts, g.status, g.granted_at, g.last_used, g.expires_at, s.name as service_name, s.origin as service_origin, s.logo_url as service_logo
+FROM grants g
+JOIN services s ON g.service_id = s.id
+WHERE g.did_id = ? AND g.status = 'active'
+ORDER BY g.last_used DESC NULLS LAST
+`
+
+type ListGrantsByDIDRow 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"`
+ ServiceName string `json:"service_name"`
+ ServiceOrigin string `json:"service_origin"`
+ ServiceLogo *string `json:"service_logo"`
+}
+
+// =============================================================================
+// GRANT QUERIES
+// =============================================================================
+func (q *Queries) ListGrantsByDID(ctx context.Context, didID int64) ([]ListGrantsByDIDRow, error) {
+ rows, err := q.db.QueryContext(ctx, listGrantsByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []ListGrantsByDIDRow{}
+ for rows.Next() {
+ var i ListGrantsByDIDRow
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ServiceID,
+ &i.UcanID,
+ &i.Scopes,
+ &i.Accounts,
+ &i.Status,
+ &i.GrantedAt,
+ &i.LastUsed,
+ &i.ExpiresAt,
+ &i.ServiceName,
+ &i.ServiceOrigin,
+ &i.ServiceLogo,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listKeySharesByDID = `-- name: ListKeySharesByDID :many
+
+SELECT id, did_id, share_id, key_id, party_index, threshold, total_parties, curve, share_data, public_key, chain_code, derivation_path, status, created_at, rotated_at FROM key_shares WHERE did_id = ? AND status = 'active' ORDER BY created_at
+`
+
+// =============================================================================
+// KEY SHARE QUERIES
+// =============================================================================
+func (q *Queries) ListKeySharesByDID(ctx context.Context, didID int64) ([]KeyShare, error) {
+ rows, err := q.db.QueryContext(ctx, listKeySharesByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []KeyShare{}
+ for rows.Next() {
+ var i KeyShare
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ShareID,
+ &i.KeyID,
+ &i.PartyIndex,
+ &i.Threshold,
+ &i.TotalParties,
+ &i.Curve,
+ &i.ShareData,
+ &i.PublicKey,
+ &i.ChainCode,
+ &i.DerivationPath,
+ &i.Status,
+ &i.CreatedAt,
+ &i.RotatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listSessionsByDID = `-- name: ListSessionsByDID :many
+
+SELECT s.id, s.did_id, s.credential_id, s.session_id, s.device_info, s.is_current, s.last_activity, s.expires_at, s.created_at, c.device_name, c.authenticator
+FROM sessions s
+JOIN credentials c ON s.credential_id = c.id
+WHERE s.did_id = ? AND s.expires_at > datetime('now')
+ORDER BY s.last_activity DESC
+`
+
+type ListSessionsByDIDRow 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"`
+ DeviceName string `json:"device_name"`
+ Authenticator *string `json:"authenticator"`
+}
+
+// =============================================================================
+// SESSION QUERIES
+// =============================================================================
+func (q *Queries) ListSessionsByDID(ctx context.Context, didID int64) ([]ListSessionsByDIDRow, error) {
+ rows, err := q.db.QueryContext(ctx, listSessionsByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []ListSessionsByDIDRow{}
+ for rows.Next() {
+ var i ListSessionsByDIDRow
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.CredentialID,
+ &i.SessionID,
+ &i.DeviceInfo,
+ &i.IsCurrent,
+ &i.LastActivity,
+ &i.ExpiresAt,
+ &i.CreatedAt,
+ &i.DeviceName,
+ &i.Authenticator,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listSyncCheckpoints = `-- name: ListSyncCheckpoints :many
+SELECT id, did_id, resource_type, last_block, last_tx_hash, last_synced FROM sync_checkpoints WHERE did_id = ?
+`
+
+func (q *Queries) ListSyncCheckpoints(ctx context.Context, didID int64) ([]SyncCheckpoint, error) {
+ rows, err := q.db.QueryContext(ctx, listSyncCheckpoints, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []SyncCheckpoint{}
+ for rows.Next() {
+ var i SyncCheckpoint
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.ResourceType,
+ &i.LastBlock,
+ &i.LastTxHash,
+ &i.LastSynced,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listUCANsByAudience = `-- name: ListUCANsByAudience :many
+SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens
+WHERE audience = ? AND is_revoked = 0 AND expires_at > datetime('now')
+ORDER BY created_at DESC
+`
+
+func (q *Queries) ListUCANsByAudience(ctx context.Context, audience string) ([]UcanToken, error) {
+ rows, err := q.db.QueryContext(ctx, listUCANsByAudience, audience)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []UcanToken{}
+ for rows.Next() {
+ var i UcanToken
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.Cid,
+ &i.Issuer,
+ &i.Audience,
+ &i.Subject,
+ &i.Capabilities,
+ &i.ProofChain,
+ &i.NotBefore,
+ &i.ExpiresAt,
+ &i.Nonce,
+ &i.Facts,
+ &i.Signature,
+ &i.RawToken,
+ &i.IsRevoked,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listUCANsByDID = `-- name: ListUCANsByDID :many
+
+SELECT id, did_id, cid, issuer, audience, subject, capabilities, proof_chain, not_before, expires_at, nonce, facts, signature, raw_token, is_revoked, created_at FROM ucan_tokens
+WHERE did_id = ? AND is_revoked = 0 AND expires_at > datetime('now')
+ORDER BY created_at DESC
+`
+
+// =============================================================================
+// UCAN TOKEN QUERIES
+// =============================================================================
+func (q *Queries) ListUCANsByDID(ctx context.Context, didID int64) ([]UcanToken, error) {
+ rows, err := q.db.QueryContext(ctx, listUCANsByDID, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []UcanToken{}
+ for rows.Next() {
+ var i UcanToken
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.Cid,
+ &i.Issuer,
+ &i.Audience,
+ &i.Subject,
+ &i.Capabilities,
+ &i.ProofChain,
+ &i.NotBefore,
+ &i.ExpiresAt,
+ &i.Nonce,
+ &i.Facts,
+ &i.Signature,
+ &i.RawToken,
+ &i.IsRevoked,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listVerificationMethods = `-- name: ListVerificationMethods :many
+
+SELECT id, did_id, method_id, method_type, controller, public_key, purpose, created_at FROM verification_methods WHERE did_id = ? ORDER BY created_at
+`
+
+// =============================================================================
+// VERIFICATION METHOD QUERIES
+// =============================================================================
+func (q *Queries) ListVerificationMethods(ctx context.Context, didID int64) ([]VerificationMethod, error) {
+ rows, err := q.db.QueryContext(ctx, listVerificationMethods, didID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []VerificationMethod{}
+ for rows.Next() {
+ var i VerificationMethod
+ if err := rows.Scan(
+ &i.ID,
+ &i.DidID,
+ &i.MethodID,
+ &i.MethodType,
+ &i.Controller,
+ &i.PublicKey,
+ &i.Purpose,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const listVerifiedServices = `-- name: ListVerifiedServices :many
+SELECT id, origin, name, description, logo_url, did, is_verified, metadata, created_at FROM services WHERE is_verified = 1 ORDER BY name
+`
+
+func (q *Queries) ListVerifiedServices(ctx context.Context) ([]Service, error) {
+ rows, err := q.db.QueryContext(ctx, listVerifiedServices)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []Service{}
+ for rows.Next() {
+ var i Service
+ if err := rows.Scan(
+ &i.ID,
+ &i.Origin,
+ &i.Name,
+ &i.Description,
+ &i.LogoUrl,
+ &i.Did,
+ &i.IsVerified,
+ &i.Metadata,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const reactivateGrant = `-- name: ReactivateGrant :exec
+UPDATE grants SET status = 'active' WHERE id = ? AND status = 'suspended'
+`
+
+func (q *Queries) ReactivateGrant(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, reactivateGrant, id)
+ return err
+}
+
+const renameCredential = `-- name: RenameCredential :exec
+UPDATE credentials SET device_name = ? WHERE id = ?
+`
+
+type RenameCredentialParams struct {
+ DeviceName string `json:"device_name"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) RenameCredential(ctx context.Context, arg RenameCredentialParams) error {
+ _, err := q.db.ExecContext(ctx, renameCredential, arg.DeviceName, arg.ID)
+ return err
+}
+
+const revokeDelegation = `-- name: RevokeDelegation :exec
+UPDATE delegations SET status = 'revoked' WHERE id = ?
+`
+
+func (q *Queries) RevokeDelegation(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, revokeDelegation, id)
+ return err
+}
+
+const revokeDelegationChain = `-- name: RevokeDelegationChain :exec
+UPDATE delegations SET status = 'revoked' WHERE id = ? OR parent_id = ?
+`
+
+type RevokeDelegationChainParams struct {
+ ID int64 `json:"id"`
+ ParentID *int64 `json:"parent_id"`
+}
+
+func (q *Queries) RevokeDelegationChain(ctx context.Context, arg RevokeDelegationChainParams) error {
+ _, err := q.db.ExecContext(ctx, revokeDelegationChain, arg.ID, arg.ParentID)
+ return err
+}
+
+const revokeGrant = `-- name: RevokeGrant :exec
+UPDATE grants SET status = 'revoked' WHERE id = ?
+`
+
+func (q *Queries) RevokeGrant(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, revokeGrant, id)
+ return err
+}
+
+const revokeUCAN = `-- name: RevokeUCAN :exec
+UPDATE ucan_tokens SET is_revoked = 1 WHERE cid = ?
+`
+
+func (q *Queries) RevokeUCAN(ctx context.Context, cid string) error {
+ _, err := q.db.ExecContext(ctx, revokeUCAN, cid)
+ return err
+}
+
+const rotateKeyShare = `-- name: RotateKeyShare :exec
+UPDATE key_shares
+SET status = 'rotating', rotated_at = datetime('now')
+WHERE id = ?
+`
+
+func (q *Queries) RotateKeyShare(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, rotateKeyShare, id)
+ return err
+}
+
+const setCurrentSession = `-- name: SetCurrentSession :exec
+UPDATE sessions
+SET is_current = CASE WHEN id = ? THEN 1 ELSE 0 END
+WHERE did_id = ?
+`
+
+type SetCurrentSessionParams struct {
+ ID int64 `json:"id"`
+ DidID int64 `json:"did_id"`
+}
+
+func (q *Queries) SetCurrentSession(ctx context.Context, arg SetCurrentSessionParams) error {
+ _, err := q.db.ExecContext(ctx, setCurrentSession, arg.ID, arg.DidID)
+ return err
+}
+
+const setDefaultAccount = `-- name: SetDefaultAccount :exec
+UPDATE accounts
+SET is_default = CASE WHEN id = ? THEN 1 ELSE 0 END
+WHERE did_id = ? AND chain_id = ?
+`
+
+type SetDefaultAccountParams struct {
+ ID int64 `json:"id"`
+ DidID int64 `json:"did_id"`
+ ChainID string `json:"chain_id"`
+}
+
+func (q *Queries) SetDefaultAccount(ctx context.Context, arg SetDefaultAccountParams) error {
+ _, err := q.db.ExecContext(ctx, setDefaultAccount, arg.ID, arg.DidID, arg.ChainID)
+ return err
+}
+
+const suspendGrant = `-- name: SuspendGrant :exec
+UPDATE grants SET status = 'suspended' WHERE id = ?
+`
+
+func (q *Queries) SuspendGrant(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, suspendGrant, id)
+ return err
+}
+
+const updateAccountLabel = `-- name: UpdateAccountLabel :exec
+UPDATE accounts SET label = ? WHERE id = ?
+`
+
+type UpdateAccountLabelParams struct {
+ Label *string `json:"label"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateAccountLabel(ctx context.Context, arg UpdateAccountLabelParams) error {
+ _, err := q.db.ExecContext(ctx, updateAccountLabel, arg.Label, arg.ID)
+ return err
+}
+
+const updateCredentialCounter = `-- name: UpdateCredentialCounter :exec
+UPDATE credentials
+SET sign_count = ?, last_used = datetime('now')
+WHERE id = ?
+`
+
+type UpdateCredentialCounterParams struct {
+ SignCount int64 `json:"sign_count"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateCredentialCounter(ctx context.Context, arg UpdateCredentialCounterParams) error {
+ _, err := q.db.ExecContext(ctx, updateCredentialCounter, arg.SignCount, arg.ID)
+ return err
+}
+
+const updateDIDDocument = `-- name: UpdateDIDDocument :exec
+UPDATE did_documents
+SET document = ?, sequence = ?, last_synced = datetime('now')
+WHERE id = ?
+`
+
+type UpdateDIDDocumentParams struct {
+ Document json.RawMessage `json:"document"`
+ Sequence int64 `json:"sequence"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateDIDDocument(ctx context.Context, arg UpdateDIDDocumentParams) error {
+ _, err := q.db.ExecContext(ctx, updateDIDDocument, arg.Document, arg.Sequence, arg.ID)
+ return err
+}
+
+const updateGrantLastUsed = `-- name: UpdateGrantLastUsed :exec
+UPDATE grants SET last_used = datetime('now') WHERE id = ?
+`
+
+func (q *Queries) UpdateGrantLastUsed(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, updateGrantLastUsed, id)
+ return err
+}
+
+const updateGrantScopes = `-- name: UpdateGrantScopes :exec
+UPDATE grants SET scopes = ?, accounts = ? WHERE id = ?
+`
+
+type UpdateGrantScopesParams struct {
+ Scopes json.RawMessage `json:"scopes"`
+ Accounts json.RawMessage `json:"accounts"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateGrantScopes(ctx context.Context, arg UpdateGrantScopesParams) error {
+ _, err := q.db.ExecContext(ctx, updateGrantScopes, arg.Scopes, arg.Accounts, arg.ID)
+ return err
+}
+
+const updateService = `-- name: UpdateService :exec
+UPDATE services
+SET name = ?, description = ?, logo_url = ?, metadata = ?
+WHERE id = ?
+`
+
+type UpdateServiceParams struct {
+ Name string `json:"name"`
+ Description *string `json:"description"`
+ LogoUrl *string `json:"logo_url"`
+ Metadata json.RawMessage `json:"metadata"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateService(ctx context.Context, arg UpdateServiceParams) error {
+ _, err := q.db.ExecContext(ctx, updateService,
+ arg.Name,
+ arg.Description,
+ arg.LogoUrl,
+ arg.Metadata,
+ arg.ID,
+ )
+ return err
+}
+
+const updateSessionActivity = `-- name: UpdateSessionActivity :exec
+UPDATE sessions SET last_activity = datetime('now') WHERE id = ?
+`
+
+func (q *Queries) UpdateSessionActivity(ctx context.Context, id int64) error {
+ _, err := q.db.ExecContext(ctx, updateSessionActivity, id)
+ return err
+}
+
+const upsertSyncCheckpoint = `-- name: UpsertSyncCheckpoint :exec
+INSERT INTO sync_checkpoints (did_id, resource_type, last_block, last_tx_hash)
+VALUES (?, ?, ?, ?)
+ON CONFLICT(did_id, resource_type) DO UPDATE SET
+ last_block = excluded.last_block,
+ last_tx_hash = excluded.last_tx_hash,
+ last_synced = datetime('now')
+`
+
+type UpsertSyncCheckpointParams struct {
+ DidID int64 `json:"did_id"`
+ ResourceType string `json:"resource_type"`
+ LastBlock int64 `json:"last_block"`
+ LastTxHash *string `json:"last_tx_hash"`
+}
+
+func (q *Queries) UpsertSyncCheckpoint(ctx context.Context, arg UpsertSyncCheckpointParams) error {
+ _, err := q.db.ExecContext(ctx, upsertSyncCheckpoint,
+ arg.DidID,
+ arg.ResourceType,
+ arg.LastBlock,
+ arg.LastTxHash,
+ )
+ return err
+}
diff --git a/internal/migrations/embed.go b/internal/migrations/embed.go
new file mode 100644
index 0000000..682eb47
--- /dev/null
+++ b/internal/migrations/embed.go
@@ -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
diff --git a/db/query.sql b/internal/migrations/query.sql
similarity index 96%
rename from db/query.sql
rename to internal/migrations/query.sql
index a149774..43caf05 100644
--- a/db/query.sql
+++ b/internal/migrations/query.sql
@@ -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
diff --git a/db/schema.sql b/internal/migrations/schema.sql
similarity index 100%
rename from db/schema.sql
rename to internal/migrations/schema.sql
diff --git a/internal/state/state.go b/internal/state/state.go
new file mode 100644
index 0000000..98b4c94
--- /dev/null
+++ b/internal/state/state.go
@@ -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
+}
diff --git a/internal/types/exec.go b/internal/types/exec.go
new file mode 100644
index 0000000..5136279
--- /dev/null
+++ b/internal/types/exec.go
@@ -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"`
+}
diff --git a/internal/types/filters.go b/internal/types/filters.go
new file mode 100644
index 0000000..4315ef0
--- /dev/null
+++ b/internal/types/filters.go
@@ -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
+}
diff --git a/internal/types/generate.go b/internal/types/generate.go
new file mode 100644
index 0000000..9cd550c
--- /dev/null
+++ b/internal/types/generate.go
@@ -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"`
+}
diff --git a/internal/types/load.go b/internal/types/load.go
new file mode 100644
index 0000000..dabe148
--- /dev/null
+++ b/internal/types/load.go
@@ -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"`
+}
diff --git a/internal/types/ping.go b/internal/types/ping.go
new file mode 100644
index 0000000..fd3fca2
--- /dev/null
+++ b/internal/types/ping.go
@@ -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"`
+}
diff --git a/internal/types/query.go b/internal/types/query.go
new file mode 100644
index 0000000..00b0f42
--- /dev/null
+++ b/internal/types/query.go
@@ -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"`
+}
diff --git a/main.go b/main.go
index 220fd6b..3fe569e 100644
--- a/main.go
+++ b/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",
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2a7aaa6
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/sqlc.yaml b/sqlc.yaml
index 1265ca6..1fc6744 100644
--- a/sqlc.yaml
+++ b/sqlc.yaml
@@ -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
diff --git a/src/enclave.ts b/src/enclave.ts
new file mode 100644
index 0000000..5a3c9dc
--- /dev/null
+++ b/src/enclave.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ this.log('reset: clearing plugin state');
+ await this.plugin.reset();
+ }
+
+ /**
+ * Close and cleanup plugin resources
+ */
+ async close(): Promise {
+ 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 {
+ return Enclave.create(wasm, options);
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..9760a0e
--- /dev/null
+++ b/src/index.ts
@@ -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';
diff --git a/src/tsconfig.json b/src/tsconfig.json
new file mode 100644
index 0000000..02fe294
--- /dev/null
+++ b/src/tsconfig.json
@@ -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"]
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..cd4b300
--- /dev/null
+++ b/src/types.ts
@@ -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;
+ /** 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;
+}