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

- - - -
+
+

ping(message)

+ +
+ + +
-

load()

-

Load database from serialized buffer

- - - - -
+
+

generate(credential)

+ +
+
+ + +
+
-

exec()

-

Execute action with GitHub-style filter syntax

- - - - -
- - - - - -
-
+
+

load(database)

+ +
+ + +
-

query()

-

Resolve DID to document with resources

- - - -
+
+

exec(filter)

+ +
+ +
+ + + + +
+
-

Console Log

- -
+
+

query(did)

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