Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub de934e9384 chore(deps): bump google.golang.org/grpc from 1.75.0 to 1.77.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.75.0 to 1.77.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.75.0...v1.77.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.77.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:06:07 +00:00
Prad NukalaandGitHub 6c6cec6326 ci: migrate workflows to tenki (#1309)
👋 Hey there, reviewer!

Experience the fastest GitHub Actions with
[Tenki](https://www.tenki.cloud/), built for performance and cost
efficiency.

## What does this PR do?

This pull request was automatically generated by Tenki's Migration
Wizard.

Here's what's new once you merge:

- ** Faster Runners:** Your workflows now use Tenki's high-performance
bare-metal infrastructure ~35% faster than
default GitHub-hosted runners. 
[Learn more about Tenki's instance types
→](https://www.tenki.cloud/docs/runner-offering)

- **💵 Cheaper Runners:** as you now use Tenki's high-performance
bare-metal infrastructure and WE manage the costs,
that means for you: 90% cheaper than default GitHub-hosted runners. 
[Learn more about Tenki's pricing
→](https://www.tenki.cloud/docs/runner-billing)

## Any questions ? 
**- Is this free?** 
Yes, your first 3,000 minutes per month are on us.

**- Do I need to complete any other steps to migrate to Tenki?**
Nope. Hit Merge, and you are good to go 🎊

**- Who's can I contact if I have any other questions?**
Reach out at [hello@tenki.cloud,](mailto:hello@tenki.cloud,) we will
answer you within 24hrs.
2025-11-19 17:45:53 -05:00
tenki-cloud[bot]andGitHub f4c7b72b3e chore: migrate runner to tenki 2025-11-19 22:44:40 +00:00
tenki-cloud[bot]andGitHub 5dbcc455e8 chore: migrate runner to tenki 2025-11-19 22:44:40 +00:00
181b10b6b2 Feat/noble ibc testnet (#1307)
* feat(dex): integrate Noble testnet for USDC cross-chain operations

This commit integrates the Noble testnet (grand-1) with Sonr's x/dex module,
enabling native USDC trading and liquidity operations across IBC-enabled chains.

Changes:
- Add default params configuration with Noble testnet in allowed connections
- Create Noble-specific helper types and functions for USDC operations
- Implement USDC conversion utilities (base units <-> USDC decimals)
- Add NobleSwapParams and NobleLiquidityParams for structured operations
- Update genesis state to use default params with validation
- Add comprehensive Noble integration documentation
- Create unit tests for params and Noble helpers

Noble Configuration:
- Chain ID: noble-grand-1 (testnet)
- USDC Denom: uusdc (6 decimals)
- RPC: https://noble-testnet-rpc.polkachu.com:443
- gRPC: noble-testnet-grpc.polkachu.com:21590

Module Parameters:
- Max accounts per DID: 5
- Default ICA timeout: 600 seconds
- Min swap amount: 1000 base units
- Max daily volume: 1T base units
- Rate limits: 10 ops/block, 100 ops/DID/day
- Fees: 0.3% swap, 0.2% liquidity, 0.1% orders

This integration enables Sonr users to:
- Register ICA accounts on Noble testnet
- Execute cross-chain swaps with USDC
- Provide/remove liquidity in USDC pairs
- Trade with slippage protection
- Route multi-hop swaps through USDC

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore(ci): migrate scopes to toml format

* feat(dex): add support for noble usdc swaps

##

feat(dex): add support for noble usdc swaps

* feat(test): add e2e tests for usdc swap with did

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-18 12:28:30 -05:00
Prad Nukala 4ddcd54558 Merge branch 'master' into feat/noble-ibc-testnet 2025-11-18 12:27:30 -05:00
Prad Nukala f71193674b feat(cmd): add ASCII art logo to CLI output 2025-11-18 12:27:01 -05:00
Prad Nukala ad69f0ac80 refactor(cmd): remove unnecessary logo print statement from main function 2025-11-18 12:20:40 -05:00
Prad Nukala 28eba5a3f3 feat(cmd): add ASCII art logo to snrd cli tool 2025-11-18 12:19:21 -05:00
Prad Nukala 99167120dc chore(.gitignore): add .claude to ignore list 2025-11-05 15:03:16 -05:00
Prad Nukala 93a5d8826d > Ran tool: github_create_or_update_file
>
> *Failed*:
> ```
> failed to create/update file: PUT https://api.github.com/repos/sonr-io/sonr-io.github.io/contents/docs/guides/decentralized-authentication.mdx: 401 Bad credentials []
> ```

> Ran tool: `github_create_or_update_file`
>
> *Failed*:
> ```
> failed to create/update file: PUT https://api.github.com/repos/sonr-io/sonr-io.github.io/contents/docs/guides/decentralized-authentication.mdx: 401 Bad credentials []
> ```

docs: Remove decentralized authentication guide
2025-10-28 10:39:59 -04:00
Prad NukalaandGitHub ae16bee0c1 Merge branch 'master' into feat/noble-ibc-testnet 2025-10-26 16:49:38 -04:00
Prad Nukala 6f93b63e4b feat(test): add e2e tests for usdc swap with did 2025-10-26 16:22:10 -04:00
Prad Nukala 3c682390a6 feat(dex): add support for noble usdc swaps
##

feat(dex): add support for noble usdc swaps
2025-10-26 16:22:07 -04:00
Prad Nukala 85304fd8b4 chore(ci): migrate scopes to toml format 2025-10-26 16:22:04 -04:00
Prad Nukala be4e4b8367 docs(src): add README file for sonrctl cli 2025-10-25 14:43:47 -04:00
Prad Nukala 88ca486108 chore(devbox): add dasel package and update devbox config 2025-10-25 14:43:46 -04:00
Prad Nukala 30635ba397 chore(ci): migrate scopes to toml format 2025-10-25 14:43:44 -04:00
Claude 9db77a2bf4 feat(dex): integrate Noble testnet for USDC cross-chain operations
This commit integrates the Noble testnet (grand-1) with Sonr's x/dex module,
enabling native USDC trading and liquidity operations across IBC-enabled chains.

Changes:
- Add default params configuration with Noble testnet in allowed connections
- Create Noble-specific helper types and functions for USDC operations
- Implement USDC conversion utilities (base units <-> USDC decimals)
- Add NobleSwapParams and NobleLiquidityParams for structured operations
- Update genesis state to use default params with validation
- Add comprehensive Noble integration documentation
- Create unit tests for params and Noble helpers

Noble Configuration:
- Chain ID: noble-grand-1 (testnet)
- USDC Denom: uusdc (6 decimals)
- RPC: https://noble-testnet-rpc.polkachu.com:443
- gRPC: noble-testnet-grpc.polkachu.com:21590

Module Parameters:
- Max accounts per DID: 5
- Default ICA timeout: 600 seconds
- Min swap amount: 1000 base units
- Max daily volume: 1T base units
- Rate limits: 10 ops/block, 100 ops/DID/day
- Fees: 0.3% swap, 0.2% liquidity, 0.1% orders

This integration enables Sonr users to:
- Register ICA accounts on Noble testnet
- Execute cross-chain swaps with USDC
- Provide/remove liquidity in USDC pairs
- Trade with slippage protection
- Route multi-hop swaps through USDC

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 18:32:48 +00:00
Prad Nukala 5fbcd85420 chore(ci): update scopes.json to reflect new repository structure 2025-10-23 15:03:46 -04:00
Prad Nukala 9438cb2fff docs: remove swagger generation config and docs 2025-10-23 10:18:27 -04:00
Prad Nukala ab44cf15eb chore(.gitignore): update gitignore to reflect removal of Highway and Vault components 2025-10-21 12:30:43 -04:00
Prad Nukala 3769d11f9f docs: remove hway documentation 2025-10-21 12:30:42 -04:00
Prad Nukala c49fead1d8 docs(dwn): clarify vault operations and remove motor client references 2025-10-21 12:30:40 -04:00
Prad Nukala 2241ac470b chore(docs): update README with new build commands and architecture information 2025-10-21 12:30:38 -04:00
Prad Nukala cf1712c2d8 docs(client): add OpenAPI/Swagger documentation for Sonr blockchain API 2025-10-21 12:04:31 -04:00
Prad Nukala 53a34171e8 style(proto): ignore tmp-swagger-gen/**/*.swagger.json 2025-10-21 12:04:29 -04:00
Prad Nukala e3b492f3b9 refactor(svc): re-order import sections in protobuf generated files 2025-10-21 12:04:27 -04:00
Prad Nukala e3e6b4c918 feat(dwn): update enclave wasm plugin for secure operations 2025-10-21 12:04:26 -04:00
Prad Nukala 782a93bae0 refactor(did): organize protobuf imports according to best practices 2025-10-21 12:04:25 -04:00
Prad Nukala 1c908fca36 refactor(dex): reorganize protobuf imports for consistency 2025-10-21 12:04:24 -04:00
Prad Nukala 30f6902e5f ci: update Makefile for consistent localnet and dockernet behavior 2025-10-21 12:04:23 -04:00
Prad Nukala d4b45bb9d7 chore(ci): remove mods.yml and scopes.yml files 2025-10-20 11:15:58 -04:00
Prad Nukala 3442afd407 No commit suggestions generated 2025-10-16 09:00:48 -04:00
Prad Nukala 5e113a8066 No commit suggestions generated 2025-10-15 15:31:10 -04:00
Prad Nukala 81a33f4541 Falling back to manual conventional commit...
fix(gitignore files):
2025-10-14 09:27:20 -04:00
Prad Nukala 6bd50128fb Falling back to manual conventional commit...
fix(Update gitignore):
2025-10-13 16:46:37 -04:00
Prad Nukala 6b4b1c42ed No commit suggestions generated 2025-10-12 21:10:32 -04:00
Prad NukalaandGitHub ebfa2b062a Feat/add networks (#1303)
* No commit suggestions generated

* No commit suggestions generated

* No commit suggestions generated
2025-10-12 11:23:20 -04:00
40eadc995e Feat/1285 es ucan formatting (#1302)
* feat: Add Enclave Usage Examples

* feat(es/ucan): Add comprehensive integration tests

- Create integration.test.ts with full UCAN token lifecycle testing
- Cover end-to-end token creation, parsing, and validation
- Test capability attenuation and delegation chains
- Validate multi-algorithm support and timestamp scenarios
- Implement error recovery and performance test scenarios

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* chore: Remove migrated components and add migration documentation

Removed all code and references for components that have been moved to separate repositories:

**Moved to sonr-io/hway:**
- bridge/ - HTTP service with OAuth2/OIDC/WebAuthn handlers
- cmd/hway/ - Highway service binary
- internal/migrations/ - PostgreSQL schema migrations

**Moved to sonr-io/motr:**
- cmd/motr/ - Motor worker service (WASM vault operations)
- cmd/vault/ - Vault CLI tool
- crypto/ - Comprehensive cryptographic library
- packages/ - TypeScript SDK packages (es, sdk, ui, com, pkl)
- web/auth/ - Authentication web application
- web/dash/ - Dashboard web application

**Updated Configuration:**
- Makefile: Removed build/test/release targets for moved components
- CLAUDE.md: Simplified to focus on core blockchain components
- devbox.json: Removed scripts for moved services
- docker-compose.yml: Removed hway, postgres, redis, auth, dash services
- .github/scopes.yml: Removed CI scopes for migrated components
- .goreleaser.yml: Updated release configuration

**Added Migration Documentation:**
- MIGRATE_HWAY.md: Comprehensive Highway service architecture and migration guide
- MIGRATE_MOTR.md: Comprehensive Motor/Worker/Vault architecture and migration guide

These migration documents provide complete context for setting up the new repositories including architecture diagrams, component breakdowns, API documentation, and migration checklists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* chore: Remove contracts references and documentation

Removed all references to the contracts directory that was migrated to a separate repository.

**Changes:**
- .gitignore: Removed contract-specific ignore patterns for DAO and wSNR contracts
- .gitignore: Removed hway and motr binary references (already migrated)
- .rgignore: Removed contracts, chains, and crypto directory references
- docs/reference/contracts/: Removed DAO.mdx and wSNR.mdx documentation files

This completes the cleanup of migrated components from the repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add crypto library migration documentation

Added comprehensive migration documentation for the crypto library that was
moved to sonr-io/crypto repository. This documentation provides complete context
for understanding the cryptographic primitives and protocols used throughout
the Sonr ecosystem.

## Key Documentation Added

### MIGRATE_CRYPTO.md
Complete documentation of the crypto library covering:

**Core Cryptographic Primitives**
- Elliptic curve implementations (Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta)
- Native curve arithmetic with optimized field operations
- Pairing-friendly curves for BLS signatures

**Multi-Party Computation (MPC)**
- MPC enclave for vault key generation and management
- Threshold cryptography (TECDSA, TED25519 with FROST protocol)
- Distributed Key Generation (DKG) via Gennaro and FROST protocols
- Secret sharing schemes (Shamir, Feldman VSS, Pedersen VSS)

**Digital Signature Schemes**
- BLS signatures with aggregation support
- BBS+ signatures for selective disclosure
- Schnorr signatures (standard and Mina/NEM variants)
- ECDSA with deterministic nonce generation

**Zero-Knowledge Proofs**
- Bulletproofs for range proofs
- Inner Product Arguments (IPA)
- Batch verification support

**Advanced Cryptographic Protocols**
- Cryptographic accumulators for set membership proofs
- Paillier homomorphic encryption
- Oblivious Transfer (OT) protocols
- Verifiable Random Functions (VRF)

**Key Management & Identity**
- DID key management with multi-chain support
- Multi-algorithm public key handling
- Wallet address derivation (Bitcoin, Ethereum, Cosmos, Solana, etc.)

**UCAN Integration**
- User-Controlled Authorization Networks
- Capability delegation and attenuation
- JWT-based capability tokens
- MPC-enabled UCAN signing

**Security Utilities**
- AEAD encryption (AES-GCM, AES-SIV)
- Argon2 key derivation
- ECIES encryption
- Secure memory handling

### MIGRATE_MOTR.md Updates
Updated Motor migration documentation to clarify that the crypto library
is now a separate external dependency at github.com/sonr-io/crypto v1.0.1

## Repository Context

The crypto library has been successfully migrated to its own repository
and is published as a Go module. It serves as the foundational cryptographic
layer for:
- Sonr blockchain (snrd) - DID signatures, vault operations
- Highway service (hway) - UCAN token signing, WebAuthn
- Motor/Worker (motr) - MPC vault operations, threshold signatures

## Integration Impact

All Sonr ecosystem components now depend on the external crypto library:
```go
require github.com/sonr-io/crypto v1.0.1
```

The migration enables independent versioning and maintenance of cryptographic
primitives while maintaining security and compatibility across the ecosystem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* No commit suggestions generated

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-10 11:47:18 -04:00
Prad Nukala 7570d67787 No commit suggestions generated 2025-10-05 12:44:08 -04:00
Prad Nukala d19e0b676b bump: version 0.0.1 → 0.0.2 2025-10-04 11:33:27 -04:00
Prad Nukala 08d4af2dc0 bump: version 0.0.2 → 0.0.3 2025-10-04 11:27:30 -04:00
Prad Nukala 7f8034ae44 bump: version 0.0.2 → 0.0.3 2025-10-04 11:27:21 -04:00
Prad Nukala 19ae52dd12 bump: version 0.0.2 → 0.0.3 2025-10-04 11:24:19 -04:00
Prad Nukala ee2ba1745e fix: License 2025-10-04 11:24:13 -04:00
Prad NukalaandGitHub 0bfaf26357 Create LICENSE
Signed-off-by: Prad Nukala <prad@sonr.io>
2025-10-04 11:21:29 -04:00
Prad Nukala 8ed37f02f3 bump: version 0.0.1 → 0.0.2 2025-10-04 08:58:48 -04:00
Prad Nukala c3dbb04f21 bump: version 0.0.1 → 0.0.2 2025-10-04 08:58:44 -04:00
Prad Nukala 5bc00c5481 bump: version 0.0.1 → 0.0.2 2025-10-04 08:58:16 -04:00
1494 changed files with 10414 additions and 332704 deletions
View File
+11 -209
View File
@@ -106,10 +106,8 @@ make testnet-stop
### Building from Source
```bash
# Build all binaries
make build # snrd blockchain node
make build-hway # Highway service
make build-vault # Vault WASM plugin
# Build snrd blockchain node
make build
# Build Docker image
make docker
@@ -162,38 +160,11 @@ make ipfs-status # Check IPFS connectivity
## 🏗️ Architecture
Sonr consists of three primary components and three custom modules:
### Service Architecture
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Caddy │────▶│ snrd │ │ IPFS │
└─────────┘ └─────────┘ └─────────┘
│ │ │
│ ▼ ▼
│ ┌─────────┐ ┌─────────┐
└─────────▶│ Highway │────▶│ Redis │
└─────────┘ └─────────┘
```
- **Caddy**: Reverse proxy with gRPC-Web support (port 80)
- **Redis**: Task queue backend for Highway (port 6379)
- **Highway**: UCAN-based task processor (port 8090)
- **IPFS**: Distributed storage (API: 5001, Gateway: 8080)
- **snrd**: Blockchain node (gRPC: 9090, REST: 1317)
### Cross-Platform Support
The `localnet` target now automatically detects and uses the best available method:
1. Checks for local binary (built with `make install`)
2. Falls back to Docker if available
3. Handles permission issues on systems like Arch Linux
4. Supports systemd service installation (see `etc/systemd/`)
Sonr is a Cosmos SDK-based blockchain with integrated IPFS storage and three custom modules:
### Core Components
#### 1. **Blockchain Node (`snrd`)**
#### **Blockchain Node (`snrd`)**
The main blockchain daemon built with Cosmos SDK v0.50.14, providing:
@@ -201,22 +172,7 @@ The main blockchain daemon built with Cosmos SDK v0.50.14, providing:
- EVM compatibility via Evmos integration
- IBC for cross-chain communication
- CosmWasm smart contract support
#### 2. **Highway Service (`hway`)**
An Asynq-based task processing service for vault operations:
- Redis-backed job queue with priority levels
- Actor-based concurrency using Proto.Actor framework
- Processes cryptographic operations through WebAssembly enclaves
#### 3. **Motor Plugin (`motr`)**
WebAssembly-based vault system providing:
- Secure execution environment for sensitive operations
- Hardware-backed key management
- Multi-party computation capabilities
- IPFS integration for decentralized storage
## 📖 Module Documentation
@@ -291,7 +247,7 @@ snrd tx svc register-service my-service example.com \
### Environment Variables
Environment variables can be configured via Docker Compose:
Environment variables can be configured for local development:
```bash
# Chain configuration
@@ -301,45 +257,12 @@ export BLOCK_TIME="1000ms"
# Network selection for Starship
export NETWORK="devnet" # or "testnet"
# Redis configuration
REDIS_URL=redis://redis:6379
REDIS_DB=0
# Highway service
HIGHWAY_PORT=8090
LOG_LEVEL=debug
# IPFS configuration
IPFS_API_URL=http://ipfs:5001
# Caddy configuration
CADDY_DOMAIN=localhost
IPFS_API_URL=http://127.0.0.1:5001
```
Environment variables can be set directly or via a `.env` file in the project root.
### Docker Compose Services
The project includes a comprehensive Docker Compose setup for running all backend services:
```bash
# Start all services (Redis, Highway, Caddy, IPFS)
make docker-up
# Stop all services
make docker-down
# View service logs
make docker-logs # All services
make docker-logs-redis # Specific service
# Check service health
make docker-status
# Clean up Docker resources
make docker-clean
```
### Starship Configuration
Edit `starship.yml` to configure multi-node testnets:
@@ -353,147 +276,26 @@ chains:
# ... additional configuration
```
#### Troubleshooting
**Services not starting:**
```bash
# Check service status
make docker-status
# View detailed logs
make docker-logs
# Ensure Docker network exists
docker network ls | grep sonr-network
```
**Redis connection issues:**
```bash
# Test Redis connectivity
docker exec redis redis-cli ping
# Check Redis logs
make docker-logs-redis
```
**IPFS not accessible:**
```bash
# Verify IPFS is running
curl http://127.0.0.1:5001/api/v0/version
# Check IPFS logs
docker compose -f etc/stack/compose.yml logs ipfs
```
**Port conflicts:**
- Caddy: 80 (HTTP)
- Redis: 6379
- Highway: 8090
- IPFS API: 5001
- IPFS Gateway: 8080
Stop conflicting services or modify ports in `etc/stack/compose.yml`.
## 🏗️ Project Structure
Sonr uses a modern monorepo architecture with pnpm workspaces for JavaScript/TypeScript packages alongside the Go blockchain implementation:
### Workspace Organization
```
sonr/
├── app/ # Application setup and module wiring
├── cmd/ # Binary entry points
── snrd/ # Blockchain node
│ ├── hway/ # Highway service
│ └── motr/ # Motor WASM plugin
── snrd/ # Blockchain node
├── x/ # Custom chain modules
│ ├── did/ # W3C DID implementation
│ ├── dwn/ # Decentralized Web Nodes
│ └── svc/ # Service management
├── types/ # Internal packages
│ ├── coins/ # Task processing
│ └── ipfs/ # Authorization networks
│ ├── coins/ # Coin utilities
│ └── ipfs/ # IPFS integration
├── proto/ # Protobuf definitions
├── scripts/ # Utility scripts
├── test/ # Integration tests
── docs/ # Documentation site
# Monorepo Packages (pnpm workspaces)
├── packages/ # Core JavaScript/TypeScript packages
│ ├── es/ # @sonr.io/es - ES client library
│ ├── sdk/ # @sonr.io/sdk - SDK package
│ └── ui/ # @sonr.io/ui - Shared UI components
├── cli/ # CLI tools
│ ├── install/ # @sonr.io/install - Installation CLI
│ └── join-testnet/# @sonr.io/join-testnet - Testnet CLI
└── web/ # Web applications
├── auth/ # Authentication app (Next.js)
└── dash/ # Dashboard app (Next.js)
── docs/ # Documentation site
```
### Working with the Monorepo
#### Installation
```bash
# Install all dependencies for the monorepo
pnpm install
# Build all packages
pnpm build
# Run development mode for all packages
pnpm dev
```
#### Package Management
```bash
# Run commands in specific packages
pnpm --filter @sonr.io/es build
pnpm --filter @sonr.io/sdk test
# Add dependencies to specific packages
pnpm --filter @sonr.io/ui add react
# Run commands in all packages
pnpm -r build # Build all packages
pnpm -r test # Test all packages
```
#### Versioning & Publishing
The monorepo uses [Changesets](https://github.com/changesets/changesets) for package versioning:
```bash
# Add a changeset for your changes
pnpm changeset
# Version packages based on changesets
pnpm changeset version
# Publish packages to npm
pnpm changeset publish
```
### Key Technologies
- **pnpm workspaces**: Efficient dependency management with single lockfile
- **TypeScript project references**: Incremental builds and better IDE performance
- **Turbo**: Build orchestration and caching for faster builds
- **Changesets**: Automated versioning and changelog generation
- **Biome**: Fast, unified linting and formatting (replaces ESLint/Prettier)
### Package Dependencies
- `@sonr.io/es`: Core ES client library with protobuf types
- `@sonr.io/sdk`: High-level SDK (depends on @sonr.io/es)
- `@sonr.io/ui`: Shared UI components for web applications
- `@sonr.io/install`: CLI tool for installing Sonr
- `@sonr.io/join-testnet`: CLI tool for joining testnet
- Web apps use all three core packages (@sonr.io/es, @sonr.io/sdk, @sonr.io/ui)
## 🤝 Community & Support
- [GitHub Discussions](https://github.com/sonr-io/sonr/discussions) - Community forum
+18
View File
@@ -0,0 +1,18 @@
[scopes]
ante = ["app/ante"]
api = ["api/dex", "api/did", "api/dwn", "api/svc", "api"]
app = ["app/context", "app/decorators", "app/params", "app/upgrades", "app"]
chains = ["chains"]
ci = ["Makefile", ".github"]
cli = ["app/commands"]
cmd = ["cmd/snrd", "cmd"]
config = ["networks/localnet", "networks/testnet", "networks"]
deps = ["go.sum", "go.mod"]
dex = ["proto/dex", "x/dex"]
did = ["proto/did", "x/did"]
docs = ["docs"]
dwn = ["proto/dwn", "x/dwn"]
proto = ["proto"]
scripts = ["scripts"]
svc = ["proto/svc", "x/svc"]
test = ["test/e2e", "test/integration", "test/oauth", "test"]
+3 -78
View File
@@ -1,6 +1,6 @@
version: 2
updates:
# Go modules
# Go modules (main)
- package-ecosystem: gomod
directory: "/"
schedule:
@@ -14,88 +14,13 @@ updates:
prefix: chore
include: scope
# Go modules in subdirectories
- package-ecosystem: gomod
directory: "/cmd/hway"
schedule:
interval: weekly
day: monday
time: "02:00"
labels:
- dependencies
- hway
commit-message:
prefix: "chore(hway)"
- package-ecosystem: gomod
directory: "/cmd/motr"
schedule:
interval: weekly
day: monday
time: "02:00"
labels:
- dependencies
- motr
commit-message:
prefix: "chore(motr)"
- package-ecosystem: gomod
directory: "/cmd/vault"
schedule:
interval: weekly
day: monday
time: "02:00"
labels:
- dependencies
- vault
commit-message:
prefix: "chore(vault)"
- package-ecosystem: gomod
directory: "/client"
schedule:
interval: weekly
day: monday
time: "02:00"
labels:
- dependencies
- client
commit-message:
prefix: "chore(client)"
- package-ecosystem: gomod
directory: "/crypto"
schedule:
interval: weekly
day: monday
time: "02:00"
labels:
- dependencies
- crypto
commit-message:
prefix: "chore(crypto)"
# JavaScript/TypeScript packages
- package-ecosystem: npm
# GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
time: "03:00"
labels:
- dependencies
- javascript
commit-message:
prefix: chore
include: scope
# GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
time: "04:00"
labels:
- dependencies
- ci
-583
View File
@@ -1,583 +0,0 @@
default-api: groq
default-model: qwen3
format-text:
markdown: "Format the response as markdown without enclosing backticks."
json: "Format the response as json without enclosing backticks."
mcp-servers:
github:
command: docker
args:
- run
- "-i"
- "--rm"
- "-e"
- GITHUB_PERSONAL_ACCESS_TOKEN
- "ghcr.io/github/github-mcp-server"
ref:
command: npx
args:
- ref-tools-mcp@latest
mcp-timeout: 15s
roles:
"default": []
"pr-writer":
- You are a PR description writer
- Create clear, structured PR descriptions
- Include all relevant context from commits and changes
- Format using markdown with proper sections
- Reference issues and milestones appropriately
- Be concise but comprehensive
- Include a testing checklist if relevant
"commit-writer":
- You are a conventional commit message generator following commitizen format
- Analyze the provided git diff and scope information to create a proper commit
- Use the format type(scope) description
- Valid types feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
- The scope should be one of the affected scopes provided
- Description should be concise, present tense, no period at the end
- If there are breaking changes, add BREAKING CHANGE in the footer
- Focus on WHAT changed and WHY, not HOW (the diff shows how)
- Group related changes logically
- Maximum 72 characters for the header line
- Output ONLY the commit message, no explanations
"commit-analyzer":
- Analyze git diffs to determine the primary type of change
- Identify if changes are features, fixes, refactoring, etc
- Summarize the key changes in a structured format
- Output a JSON object with type, scope, description, and body fields
"branch-namer":
- you are a branch namer
- you do not explain anything
- you read the provided issue and body then create a title for the branch
- you do not provide any explanation whatsoever, ONLY the title
- titles must be no more than 3 words (example = feature/add-new-feature)
- ONLY use the following format - feat/<feature-name>
"issue-creator":
- you are an advanced issue creator for github in the sonr-io org
- you are provided an input in the format of - <repo-name>:<issue-context>
- you do not explain anything
- you read the short provided context for a new issue and then create an appropriate issue body
- ALWAYS find relevant context by using the `mcp::ref::ref_search_documentation` tool
- ALWAYS link relevant issues by using the `mcp::github::search_issues` tool
- ALWAYS create a new issue in the sonr-io/<repo-name> repo
- ALWAYS assign the issue to @prnk28
- |
ONLY use the following format as this example:
Title: Implement x/dwn vaults plugin
Labels: x/dwn, feature
## Requirements
- Must include DWN Bytes in Protobuf
- Must Spawn DWN Plugins using Genesis Params
- Must have Gas Free Query Methods for Spawn and Verify
## Context
### Affected Files
- @x/dwn/keeper/keeper.go
- @x/dwn/client/wasm/main.go
- @x/dwn/types/genesis.go
### Relevant Documentation
- [Example Doc 1](https://example.com)
- [Example Doc 2](https://example.com)
## Acceptance Criteria
- [ ] Tests for Spawning Plugins from the x/dwn Genesis
- [ ] Tests for Resolving Vault Secret Data from IPFS
- [ ] Test for full-round Sign/Verify/Refresh on-chain
- [ ] Grpc Gateway Compatibility for HTTP Requests
- ALWAYS create issues using the `mcp::github::create_issue` tool
- ALWAYS return the issue number after creating a new issue
format: false
role: "branch-namer"
raw: true
quiet: true
temp: 1.0
topp: 1.0
topk: 50
no-limit: true
word-wrap: 80
include-prompt-args: false
include-prompt: 0
max-retries: 5
fanciness: 10
status-text: Generating
theme: base16
max-input-chars: 12250
max-completion-tokens: 100
apis:
openai:
base-url: https://api.openai.com/v1
api-key:
api-key-env: OPENAI_API_KEY
# api-key-cmd: rbw get -f OPENAI_API_KEY chat.openai.com
models: # https://platform.openai.com/docs/models
gpt-4.5-preview: #128k https://platform.openai.com/docs/models/gpt-4.5-preview
aliases: ["gpt-4.5", "gpt4.5"]
max-input-chars: 392000
fallback: gpt-4
gpt-4.5-preview-2025-02-27:
max-input-chars: 392000
fallback: gpt-4
gpt-4o-mini:
aliases: ["4o-mini"]
max-input-chars: 392000
fallback: gpt-4o
gpt-4o:
aliases: ["4o"]
max-input-chars: 392000
fallback: gpt-4
gpt-4:
aliases: ["4"]
max-input-chars: 24500
fallback: gpt-3.5-turbo
gpt-4-1106-preview:
aliases: ["128k"]
max-input-chars: 392000
fallback: gpt-4
gpt-4-32k:
aliases: ["32k"]
max-input-chars: 98000
fallback: gpt-4
gpt-3.5-turbo:
aliases: ["35t"]
max-input-chars: 12250
fallback: gpt-3.5
gpt-3.5-turbo-1106:
aliases: ["35t-1106"]
max-input-chars: 12250
fallback: gpt-3.5-turbo
gpt-3.5-turbo-16k:
aliases: ["35t16k"]
max-input-chars: 44500
fallback: gpt-3.5
gpt-3.5:
aliases: ["35"]
max-input-chars: 12250
fallback:
o1:
aliases: ["o1"]
max-input-chars: 200000
o1-preview:
aliases: ["o1-preview"]
max-input-chars: 128000
o1-mini:
aliases: ["o1-mini"]
max-input-chars: 128000
o3-mini:
aliases: ["o3m", "o3-mini"]
max-input-chars: 200000
copilot:
base-url: https://api.githubcopilot.com
models:
gpt-4o-2024-05-13:
aliases: ["4o-2024", "4o", "gpt-4o"]
max-input-chars: 392000
gpt-4:
aliases: ["4"]
max-input-chars: 24500
gpt-3.5-turbo:
aliases: ["35t"]
max-input-chars: 12250
o1-preview-2024-09-12:
aliases: ["o1-preview", "o1p"]
max-input-chars: 128000
claude-3.5-sonnet:
aliases: ["claude3.5-sonnet", "sonnet-3.5", "claude-3-5-sonnet"]
max-input-chars: 680000
o1-preview:
aliases: ["o1-preview"]
max-input-chars: 128000
o1-mini:
aliases: ["o1-mini", "o1m", "o1-mini-2024-09-12"]
max-input-chars: 128000
o3-mini:
aliases: ["o3m", "o3-mini"]
max-input-chars: 128000
gemini-2.0-flash-001:
aliases: ["gm2f", "flash-2", "gemini-2-flash"]
max-input-chars: 4194304
anthropic:
base-url: https://api.anthropic.com/v1
api-key:
api-key-env: ANTHROPIC_API_KEY
models: # https://docs.anthropic.com/en/docs/about-claude/models
claude-sonnet-4-20250514:
aliases: ["claude-sonnet-4", "sonnet-4"]
max-input-chars: 680000
claude-3-7-sonnet-latest:
aliases: ["claude3.7-sonnet", "claude-3-7-sonnet", "sonnet-3.7"]
max-input-chars: 680000
claude-3-7-sonnet-20250219:
max-input-chars: 680000
claude-3-5-sonnet-latest:
aliases: ["claude3.5-sonnet", "claude-3-5-sonnet", "sonnet-3.5"]
max-input-chars: 680000
claude-3-5-sonnet-20241022:
max-input-chars: 680000
claude-3-5-sonnet-20240620:
max-input-chars: 680000
claude-3-opus-20240229:
aliases: ["claude3-opus", "opus"]
max-input-chars: 680000
cohere:
base-url: https://api.cohere.com/v1
models:
command-r-plus:
max-input-chars: 128000
command-r:
max-input-chars: 128000
google:
models: # https://ai.google.dev/gemini-api/docs/models/gemini
gemini-1.5-pro-latest:
aliases: ["gmp", "gemini", "gemini-1.5-pro"]
max-input-chars: 392000
gemini-1.5-flash-latest:
aliases: ["gmf", "flash", "gemini-1.5-flash"]
max-input-chars: 392000
gemini-2.0-flash-001:
aliases: ["gm2f", "flash-2", "gemini-2-flash"]
max-input-chars: 4194304
gemini-2.0-flash-lite:
aliases: ["gm2fl", "flash-2-lite", "gemini-2-flash-lite"]
max-input-chars: 4194304
ollama:
base-url: http://localhost:11434
models: # https://ollama.com/library
"llama3.2:3b":
aliases: ["llama3.2"]
max-input-chars: 650000
"llama3.2:1b":
aliases: ["llama3.2_1b"]
max-input-chars: 650000
"llama3:70b":
aliases: ["llama3"]
max-input-chars: 650000
perplexity:
base-url: https://api.perplexity.ai
api-key:
api-key-env: PERPLEXITY_API_KEY
models: # https://docs.perplexity.ai/guides/model-cards
llama-3.1-sonar-small-128k-online:
aliases: ["llam31-small"]
max-input-chars: 127072
llama-3.1-sonar-large-128k-online:
aliases: ["llam31-large"]
max-input-chars: 127072
llama-3.1-sonar-huge-128k-online:
aliases: ["llam31-huge"]
max-input-chars: 127072
groq:
base-url: https://api.groq.com/openai/v1
api-key:
api-key-env: GROQ_API_KEY
models: # https://console.groq.com/docs/models
# Production models
gemma2-9b-it:
aliases: ["gemma2", "gemma"]
max-input-chars: 24500 # 8,192
llama-3.3-70b-versatile:
aliases: ["llama3.3", "llama3.3-70b", "llama3.3-versatile"]
max-input-chars: 392000 # 128K
max-completion-tokens: 98000 # 32,768
llama-3.1-8b-instant:
aliases: ["llama3.1-8b", "llama3.1-instant"]
max-input-chars: 392000 # 128K
max-completion-tokens: 24500 # 8,192
llama-guard-3-8b:
aliases: ["llama-guard"]
max-input-chars: 24500 # 8,192
llama3-70b-8192:
aliases: ["llama3", "llama3-70b"]
max-input-chars: 24500 # 8,192
fallback: llama3-8b-8192
llama3-8b-8192:
aliases: ["llama3-8b"]
max-input-chars: 24500 # 8,192
mixtral-8x7b-32768:
aliases: ["mixtral"]
max-input-chars: 98000 # 32,768
meta-llama/llama-4-scout-17b-16e-instruct:
aliases: ["llama4-scout"]
max-input-chars: 392000 # 128K
meta-llama/llama-4-maverick-17b-128e-instruct:
aliases: ["llama4", "llama4-maverick"]
max-input-chars: 392000 # 128K
# Preview models
mistral-saba-24b:
aliases: ["saba", "mistral-saba", "saba-24b"]
max-input-chars: 98000 # 32K
qwen-2.5-coder-32b:
aliases: ["qwen-coder", "qwen2.5-coder", "qwen-2.5-coder"]
max-input-chars: 392000 # 128K
qwen/qwen3-32b:
aliases: ["qwen3", "qwen3-32b"]
max-input-chars: 392000 # 128K
deepseek-r1-distill-qwen-32b:
aliases: ["deepseek-r1", "r1-qwen", "deepseek-qwen"]
max-input-chars: 392000 # 128K
max-completion-tokens: 49152 # 16,384
deepseek-r1-distill-llama-70b-specdec:
aliases: ["deepseek-r1-specdec", "r1-llama-specdec"]
max-input-chars: 392000 # 128K
max-completion-tokens: 49152 # 16,384
deepseek-r1-distill-llama-70b:
aliases: ["deepseek-r1-llama", "r1-llama"]
max-input-chars: 392000 # 128K
llama-3.3-70b-specdec:
aliases: ["llama3.3-specdec"]
max-input-chars: 24500 # 8,192
llama-3.2-1b-preview:
aliases: ["llama3.2-1b"]
max-input-chars: 392000 # 128K
max-completion-tokens: 24500 # 8,192
llama-3.2-3b-preview:
aliases: ["llama3.2-3b"]
max-input-chars: 392000 # 128K
max-completion-tokens: 24500 # 8,192
llama-3.2-11b-vision-preview:
aliases: ["llama3.2-vision", "llama3.2-11b-vision"]
max-input-chars: 392000 # 128K
max-completion-tokens: 24500 # 8,192
llama-3.2-90b-vision-preview:
aliases: ["llama3.2-90b-vision"]
max-input-chars: 392000 # 128K
max-completion-tokens: 24500 # 8,192
cerebras:
base-url: https://api.cerebras.ai/v1
api-key:
api-key-env: CEREBRAS_API_KEY
models: # https://inference-docs.cerebras.ai/introduction
llama3.1-8b:
aliases: ["llama3.1-8b-cerebras"]
max-input-chars: 24500
llama3.1-70b:
aliases: ["llama3.1-cerebras", "llama3.1-70b-cerebras"]
max-input-chars: 24500
sambanova:
base-url: https://api.sambanova.ai/v1
api-key:
api-key-env: SAMBANOVA_API_KEY
models: # https://docs.sambanova.ai/cloud/docs/get-started/supported-models
# Preview models
DeepSeek-R1:
aliases: ["deepseek-r1-sambanova", "deepseek-r1-preview"]
max-input-chars: 24500 # 8k tokens
# Production models
DeepSeek-R1-Distill-Llama-70B:
aliases: ["deepseek-r1-llama-sambanova", "deepseek-r1-distill"]
max-input-chars: 98000 # 32k tokens
Llama-3.1-Tulu-3-405B:
aliases: ["llama3.1-tulu", "tulu-405b"]
max-input-chars: 49000 # 16k tokens
Meta-Llama-3.3-70B-Instruct:
aliases: ["llama3.3-sambanova", "llama3.3-70b-sambanova"]
max-input-chars: 392000 # 128k tokens
Meta-Llama-3.2-3B-Instruct:
aliases: ["llama3.2-3b-sambanova"]
max-input-chars: 24500 # 8k tokens
Meta-Llama-3.2-1B-Instruct:
aliases: ["llama3.2-1b-sambanova"]
max-input-chars: 49000 # 16k tokens
Meta-Llama-3.1-405B-Instruct:
aliases: ["llama3.1-405b-sambanova"]
max-input-chars: 49000 # 16k tokens
Meta-Llama-3.1-70B-Instruct:
aliases: ["llama3.1-70b-sambanova"]
max-input-chars: 392000 # 128k tokens
Meta-Llama-3.1-8B-Instruct:
aliases: ["llama3.1-8b-sambanova"]
max-input-chars: 49000 # 16k tokens
Meta-Llama-Guard-3-8B:
aliases: ["llama-guard-sambanova"]
max-input-chars: 24500 # 8k tokens
Llama-3.2-90B-Vision-Instruct:
aliases: ["llama3.2-vision-90b", "llama3.2-90b-vision-sambanova"]
max-input-chars: 12250 # 4k tokens
Llama-3.2-11B-Vision-Instruct:
aliases: ["llama3.2-vision-11b", "llama3.2-11b-vision-sambanova"]
max-input-chars: 12250 # 4k tokens
Qwen2.5-72B-Instruct:
aliases: ["qwen2.5-sambanova", "qwen2.5-72b"]
max-input-chars: 49000 # 16k tokens
Qwen2.5-Coder-32B-Instruct:
aliases: ["qwen2.5-coder-sambanova", "qwen-coder-sambanova"]
max-input-chars: 49000 # 16k tokens
QwQ-32B-Preview:
aliases: ["qwq-sambanova", "qwq-32b"]
max-input-chars: 49000 # 16k tokens
localai:
# LocalAI setup instructions: https://github.com/go-skynet/LocalAI#example-use-gpt4all-j-model
base-url: http://localhost:8080
models:
ggml-gpt4all-j:
aliases: ["local", "4all"]
max-input-chars: 12250
fallback:
azure:
# Set to 'azure-ad' to use Active Directory
# Azure OpenAI setup: https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource
base-url: https://YOUR_RESOURCE_NAME.openai.azure.com
api-key:
api-key-env: AZURE_OPENAI_KEY
models:
gpt-4:
aliases: ["az4"]
max-input-chars: 24500
fallback: gpt-35-turbo
gpt-35-turbo:
aliases: ["az35t"]
max-input-chars: 12250
fallback: gpt-35
gpt-35:
aliases: ["az35"]
max-input-chars: 12250
fallback:
o1-preview:
aliases: ["o1-preview"]
max-input-chars: 128000
o1-mini:
aliases: ["o1-mini"]
max-input-chars: 128000
runpod:
# https://docs.runpod.io/serverless/workers/vllm/openai-compatibility
base-url: https://api.runpod.ai/v2/${YOUR_ENDPOINT}/openai/v1
api-key:
api-key-env: RUNPOD_API_KEY
models:
openchat/openchat-3.5-1210:
aliases: ["openchat"]
max-input-chars: 8192
mistral:
base-url: https://api.mistral.ai/v1
api-key:
api-key-env: MISTRAL_API_KEY
models: # https://docs.mistral.ai/getting-started/models/
mistral-large-latest:
aliases: ["mistral-large"]
max-input-chars: 384000
open-mistral-nemo:
aliases: ["mistral-nemo"]
max-input-chars: 384000
deepseek:
base-url: https://api.deepseek.com/
api-key:
api-key-env: DEEPSEEK_API_KEY
models:
deepseek-chat:
aliases: ["chat"]
max-input-chars: 384000
deepseek-reasoner:
aliases: ["r1"]
max-input-chars: 384000
github-models:
base-url: https://models.github.ai/inference
api-key:
api-key-env: GITHUB_PERSONAL_ACCESS_TOKEN
models:
openai/gpt-4.1:
max-input-chars: 392000
openai/o3-mini:
max-input-chars: 392000
openai/o4-mini:
max-input-chars: 392000
openai/text-embedding-3-large:
max-input-chars: 392000
openai/text-embedding-3-small:
max-input-chars: 392000
ai21-labs/AI21-Jamba-1.5-Large:
max-input-chars: 392000
ai21-labs/AI21-Jamba-1.5-Mini:
max-input-chars: 392000
cohere/cohere-command-a:
max-input-chars: 392000
cohere/Cohere-command-r:
max-input-chars: 392000
cohere/Cohere-command-r-08-2024:
max-input-chars: 392000
cohere/Cohere-command-r-plus:
max-input-chars: 392000
cohere/Cohere-command-r-plus-08-2024:
max-input-chars: 392000
cohere/Cohere-embed-v3-english:
max-input-chars: 392000
cohere/Cohere-embed-v3-multilingual:
max-input-chars: 392000
core42/jais-30b-chat:
max-input-chars: 392000
deepseek/DeepSeek-R1:
max-input-chars: 392000
deepseek/DeepSeek-V3-0324:
max-input-chars: 392000
meta/Llama-3.2-11B-Vision-Instruct:
max-input-chars: 392000
meta/Llama-3.2-90B-Vision-Instruct:
max-input-chars: 392000
meta/Llama-3.3-70B-Instruct:
max-input-chars: 392000
meta/Llama-4-Maverick-17B-128E-Instruct-FP8:
max-input-chars: 392000
meta/Llama-4-Scout-17B-16E-Instruct:
max-input-chars: 392000
meta/Meta-Llama-3.1-405B-Instruct:
max-input-chars: 392000
meta/Meta-Llama-3.1-70B-Instruct:
max-input-chars: 392000
meta/Meta-Llama-3.1-8B-Instruct:
max-input-chars: 392000
meta/Meta-Llama-3-70B-Instruct:
max-input-chars: 392000
meta/Meta-Llama-3-8B-Instruct:
max-input-chars: 392000
mistral-ai/Codestral-2501:
max-input-chars: 392000
mistral-ai/Ministral-3B:
max-input-chars: 392000
mistral-ai/Mistral-Large-2411:
max-input-chars: 392000
mistral-ai/mistral-medium-2505:
max-input-chars: 392000
mistral-ai/Mistral-Nemo:
max-input-chars: 392000
mistral-ai/mistral-small-2503:
max-input-chars: 392000
xai/grok-3:
max-input-chars: 392000
xai/grok-3-mini:
max-input-chars: 392000
microsoft/MAI-DS-R1:
max-input-chars: 392000
microsoft/Phi-3.5-mini-instruct:
max-input-chars: 392000
microsoft/Phi-3.5-MoE-instruct:
max-input-chars: 392000
microsoft/Phi-3.5-vision-instruct:
max-input-chars: 392000
microsoft/Phi-3-medium-128k-instruct:
max-input-chars: 392000
microsoft/Phi-3-medium-4k-instruct:
max-input-chars: 392000
microsoft/Phi-3-mini-128k-instruct:
max-input-chars: 392000
microsoft/Phi-3-mini-4k-instruct:
max-input-chars: 392000
microsoft/Phi-3-small-128k-instruct:
max-input-chars: 392000
microsoft/Phi-3-small-8k-instruct:
max-input-chars: 392000
microsoft/Phi-4:
max-input-chars: 392000
microsoft/Phi-4-mini-instruct:
max-input-chars: 392000
microsoft/Phi-4-mini-reasoning:
max-input-chars: 392000
microsoft/Phi-4-multimodal-instruct:
max-input-chars: 392000
microsoft/Phi-4-reasoning:
max-input-chars: 392000
@@ -0,0 +1,58 @@
[
{
"scope": "deps",
"path": "go.sum"
},
{
"scope": "deps",
"path": "go.mod"
},
{
"scope": "mdx",
"path": "docs"
},
{
"scope": "ci",
"path": "Makefile"
},
{
"scope": "ci",
"path": ".github"
},
{
"scope": "dex",
"path": "x/dex"
},
{
"scope": "dex",
"path": "proto/dex"
},
{
"scope": "did",
"path": "x/did"
},
{
"scope": "did",
"path": "proto/did"
},
{
"scope": "dwn",
"path": "x/dwn"
},
{
"scope": "dwn",
"path": "proto/dwn"
},
{
"scope": "svc",
"path": "x/svc"
},
{
"scope": "svc",
"path": "proto/svc"
},
{
"scope": "config",
"path": "networks"
}
]
-73
View File
@@ -1,73 +0,0 @@
- name: client
include:
- client/**
- name: snrd
include:
- cmd/snrd/**
- app/**
- go.mod
- go.sum
- name: crypto
include:
- crypto/**
- name: devops
include:
- .github/**
- .devcontainer/**
- scripts/**
- etc/**
- Dockerfile
- "*-compose.yml"
- README.md
- name: docs
include:
- docs/**
- name: hway
include:
- cmd/hway/**
- bridge/**
- types/**
- name: dex
include:
- proto/dex/**
- x/dex/**
- name: did
include:
- proto/dex/**
- x/did/**
- name: dwn
include:
- proto/dex/**
- x/dwn/**
- name: svc
include:
- proto/dex/**
- x/svc/**
- name: motr
include:
- cmd/motr/**
- name: vault
include:
- cmd/vault/**
- name: com
include:
- packages/com/**
- name: es
include:
- packages/es/**
- name: pkl
include:
- packages/pkl/**
- name: sdk
include:
- packages/sdk/**
- name: ui
include:
- packages/ui/**
- name: auth
include:
- web/auth/**
- name: dash
include:
- web/dash/**
+1 -1
View File
@@ -7,7 +7,7 @@ on:
jobs:
report:
name: Report CI Status
runs-on: builder
runs-on: tenki-standard-autoscale
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: Report Failure
+4 -5
View File
@@ -13,12 +13,12 @@ env:
jobs:
validate:
name: Validate & Test
runs-on: builder # Self-hosted runner
runs-on: tenki-standard-autoscale
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for github-env.sh comparisons
fetch-depth: 0 # Required for github-env.sh comparisons
# Cache can be local on self-hosted runner for better performance
- name: Cache Dependencies
uses: actions/cache@v4
@@ -26,9 +26,8 @@ jobs:
path: |
~/go/pkg/mod
~/.cache/go-build
~/.local/share/pnpm/store
~/.devbox
key: deps-builder-${{ hashFiles('go.sum', 'pnpm-lock.yaml', 'devbox.json') }}
key: deps-builder-${{ hashFiles('go.sum', 'devbox.json') }}
restore-keys: deps-builder-
- name: Install Dependencies
@@ -58,4 +57,4 @@ jobs:
# Clean build artifacts to prevent disk fill
make clean || true
# Keep caches but remove build outputs
rm -rf dist/ build/ || true
rm -rf dist/ build/ || true
+22 -37
View File
@@ -11,6 +11,9 @@ Thumbs.db
.logs/
.venv/
services/
Taskfile*
mprocs*
.claude*
# ===== IDE & Editor Files =====
.vscode/
@@ -31,30 +34,22 @@ out/
# Go binaries (but allow docs references)
snrd
hway
motr
!cmd/motr/
!cmd/hway/
!cmd/snrd/
!docs/**/motr
!docs/**/hway
# Allow specific CLI binaries
!cli/join-testnet/bin/
!cli/install/bin/
# ===== Node.js & Frontend =====
node_modules/
.pnpm-debug.log*
pnpm-debug.log*
package-lock.json
yarn.lock
*.tsbuildinfo
# Next.js
.next/
.vercel/
**/.open-next
# ===== Removed: Node.js & Frontend (no longer needed for blockchain-only repo) =====
# node_modules/
# .pnpm-debug.log*
# pnpm-debug.log*
# package-lock.json
# yarn.lock
# *.tsbuildinfo
# .next/
# .vercel/
# **/.open-next
# ===== Go Development =====
pkg/mod/
@@ -73,30 +68,13 @@ artifacts/
**/target/
**/Cargo.lock
# Contract-specific
contracts/DAO/deployment_ids.env
contracts/DAO/deployment_report_*.md
contracts/DAO/relayer.pid
contracts/DAO/*.mnemonic
contracts/DAO/tx_*.json
contracts/DAO/signed_*.json
contracts/DAO/init_*.json
contracts/DAO/MAINNET_DEPLOYMENT_INSTRUCTIONS.md
contracts/DAO/backups/
contracts/wSNR/remappings.txt
# ===== Blockchain & Cosmos SDK =====
.snrd/
.hway/
keyring-test/
mytestnet/
docker/testnet/testnet-data
# ===== Monorepo & Build Tools =====
.turbo/
.gitwork/
.changeset/.cli-state
.biome/
# ===== Build Tools =====
.devbox/
.task*
Taskfile.yml
@@ -111,6 +89,7 @@ logs/
configs/logs.json
tmp/
tmp*
tmp-openapi-gen/
# ===== Environment & Secrets =====
.env
@@ -137,4 +116,10 @@ COMPLETION_SUMMARY.md
data/
compose/
heighliner*
**/*.swagger.yaml
# Ignore individual module OpenAPI files (generated in tmp directory)
tmp-openapi-gen/**/*.json
# Allow the final combined openapi.yml
!docs/static/openapi.yml
.mcp.json
node_modules
.claude
+326 -2
View File
@@ -1,5 +1,329 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
---
version: 2
monorepo:
tag_prefix: v
dist: dist/snrd
project_name: snrd
before:
hooks:
- go mod download
builds:
# Darwin AMD64 Build
- id: snrd-darwin-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- amd64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Darwin ARM64 Build
- id: snrd-darwin-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=oa64-clang
- CXX=oa64-clang++
- CGO_LDFLAGS=-lm
goos:
- darwin
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux AMD64 Build
- id: snrd-linux-amd64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- amd64
goamd64:
- v1
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
# Linux ARM64 Build
- id: snrd-linux-arm64
main: ./cmd/snrd
binary: snrd
mod_timestamp: "{{ .CommitTimestamp }}"
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
- CGO_LDFLAGS=-lm
goos:
- linux
goarch:
- arm64
flags:
- -mod=readonly
- -trimpath
ldflags:
- -X github.com/cosmos/cosmos-sdk/version.Name=sonr
- -X github.com/cosmos/cosmos-sdk/version.AppName=snrd
- -X github.com/cosmos/cosmos-sdk/version.Version={{.Version}}
- -X github.com/cosmos/cosmos-sdk/version.Commit={{.Commit}}
- -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger"
- -s -w
tags:
- netgo
- ledger
aur_sources:
- name: snrd
disable: true
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
maintainers:
- "Sonr <support@sonr.io>"
license: "GPL-3.0"
private_key: "{{ .Env.AUR_KEY }}"
git_url: "ssh://[email protected]/snrd.git"
skip_upload: auto
provides:
- snrd
conflicts:
- snrd-bin
depends:
- glibc
makedepends:
- go
- git
- make
commit_msg_template: "Update to {{ .Tag }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
prepare: |-
cd "${pkgname}_${pkgver}"
go mod download
build: |-
cd "${pkgname}_${pkgver}"
export CGO_ENABLED=1
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw"
go build \
-ldflags="-w -s -buildid='' -linkmode=external \
-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
-X github.com/cosmos/cosmos-sdk/version.AppName=snrd \
-X github.com/cosmos/cosmos-sdk/version.Version=${pkgver} \
-X 'github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger'" \
-tags "netgo,ledger" \
-o snrd ./cmd/snrd
chmod +x ./snrd
package: |-
cd "${pkgname}_${pkgver}"
# bin
install -Dm755 "./snrd" "${pkgdir}/usr/bin/snrd"
# license
if [ -f "./LICENSE" ]; then
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/snrd/LICENSE"
fi
# readme
if [ -f "./README.md" ]; then
install -Dm644 "./README.md" "${pkgdir}/usr/share/doc/snrd/README.md"
fi
# config directory
install -dm755 "${pkgdir}/etc/snrd"
install -dm755 "${pkgdir}/var/lib/snrd"
backup:
- /etc/snrd/config.toml
- /etc/snrd/app.toml
nix:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity network"
license: "gpl3"
path: pkgs/snrd/default.nix
commit_msg_template: "snrd: {{ .Tag }}"
dependencies:
- stdenv
- glibc
extra_install: |-
wrapProgram $out/bin/snrd --prefix PATH : ${lib.makeBinPath [ glibc stdenv.cc.cc.lib ]}
repository:
owner: sonr-io
name: nur
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
archives:
- id: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
- snrd-darwin-amd64
- snrd-darwin-arm64
name_template: >-
snrd_{{ .Os }}_{{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }}
formats: ["tar.gz"]
files:
- src: README*
wrap_in_directory: false
homebrew_casks:
- name: snrd
ids:
- snrd
homepage: "https://sonr.io"
description: "Sonr blockchain daemon - decentralized identity and data storage network"
commit_msg_template: "Brew cask update for {{ .ProjectName }} version {{ .Tag }}"
directory: Casks
repository:
owner: sonr-io
name: homebrew-tap
branch: main
token: "{{ .Env.GITHUB_TOKEN }}"
commit_author:
name: goreleaserbot
email: "prad@sonr.io"
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/snrd"]
end
nfpms:
- id: snrd
package_name: snrd
ids:
- snrd-linux-amd64
- snrd-linux-arm64
file_name_template: "snrd_{{ .Os }}_{{ .Arch }}{{ .ConventionalExtension }}"
vendor: Sonr
homepage: "https://sonr.io"
maintainer: "Sonr <support@sonr.io>"
description: "Sonr is a decentralized, permissionless, and censorship-resistant identity network."
license: "GPL-3.0"
formats:
- rpm
- deb
- apk
- archlinux
contents:
- src: README*
dst: /usr/share/doc/snrd
bindir: /usr/bin
section: net
priority: optional
blobs:
- provider: s3
endpoint: https://eb37925850388bca807b7fab964c12bb.r2.cloudflarestorage.com
bucket: releases
region: auto
directory: "snrd/{{ .Tag }}"
ids:
- snrd
release:
disable: false
github:
owner: sonr-io
name: sonr
name_template: "{{ .Tag }}"
draft: false
replace_existing_draft: false # Don't replace drafts
replace_existing_artifacts: false # Append, don't replace
mode: append # Explicitly set to append mode
checksum:
name_template: "snrd_checksums.txt"
npms:
- name: "@sonr.io/snrd"
ids:
- snrd
description: "Sonr blockchain daemon - decentralized identity and data storage network"
homepage: "https://sonr.io"
license: "GPL-3.0"
author: "Sonr <support@sonr.io>"
repository: "https://github.com/sonr-io/sonr"
bugs: "https://github.com/sonr-io/sonr/issues"
keywords:
- blockchain
- cosmos
- did
- identity
- web3
- sonr
access: public
format: tar.gz
snapshot:
version_template: "{{ .Branch }}-{{ .ShortCommit }}-dev"
# Changelog configuration
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
-3
View File
@@ -1,9 +1,6 @@
.dockerignore
.goreleaser.yml
api
contracts
chains
crypto
proto
test
**/_test.go
-93
View File
@@ -1,93 +0,0 @@
# Caddy reverse proxy configuration for Sonr services
{
# Global options
admin off
auto_https off
# Enable debug mode for development
debug
}
# Health check endpoint
:80/health {
respond "OK" 200
}
# REST API proxy (Cosmos SDK)
:80/api/* {
reverse_proxy snrd:1317
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
}
# gRPC-Web proxy
:80/grpc/* {
reverse_proxy h2c://snrd:9090
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, x-grpc-web, x-user-agent"
}
# JSON-RPC proxy (EVM)
:80/rpc {
reverse_proxy snrd:8545
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type"
}
# WebSocket JSON-RPC proxy (EVM)
:80/ws {
reverse_proxy snrd:8546
header Access-Control-Allow-Origin *
}
# Highway service proxy
:80/highway/* {
reverse_proxy highway:8090
header Access-Control-Allow-Origin *
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
}
# Production configuration with TLS (when CADDY_DOMAIN is set)
{$CADDY_DOMAIN:localhost} {
# TLS configuration for production
@production {
not host localhost
}
tls @production {
# Automatic HTTPS with Let's Encrypt
}
# Same proxy rules as above
handle /health {
respond "OK" 200
}
handle /api/* {
reverse_proxy snrd:1317
}
handle /grpc/* {
reverse_proxy h2c://snrd:9090
}
handle /rpc {
reverse_proxy snrd:8545
}
handle /ws {
reverse_proxy snrd:8546
}
handle /highway/* {
reverse_proxy highway:8090
}
# Default handler
handle {
respond "Sonr Network Gateway" 200
}
}
+10 -86
View File
@@ -1,5 +1,5 @@
# Use build argument for base image to allow using pre-cached base
ARG BASE_IMAGE=golang:1.24.7-alpine3.22
ARG BASE_IMAGE=golang:1.25-alpine3.22
FROM ${BASE_IMAGE} AS builder
SHELL ["/bin/sh", "-ecuxo", "pipefail"]
@@ -16,7 +16,7 @@ RUN apk add --no-cache \
WORKDIR /code
# Copy entire source code (including crypto module)
# Copy entire source code
COPY . .
# Fix git ownership issue
@@ -26,7 +26,7 @@ RUN git config --global --add safe.directory /code
RUN --mount=type=cache,target=/tmp/wasmvm \
set -eux; \
export ARCH=$(uname -m); \
WASM_VERSION=$(go list -m all | grep github.com/CosmWasm/wasmvm | head -1 || echo ""); \
WASM_VERSION=$(GOTOOLCHAIN=auto go list -m all | grep github.com/CosmWasm/wasmvm | head -1 || echo ""); \
if [ ! -z "${WASM_VERSION}" ]; then \
WASMVM_REPO=$(echo $WASM_VERSION | awk '{print $1}'); \
WASMVM_VERS=$(echo $WASM_VERSION | awk '{print $2}'); \
@@ -40,7 +40,7 @@ RUN --mount=type=cache,target=/tmp/wasmvm \
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
GOTOOLCHAIN=auto go mod download
# Build binary with optimizations
RUN --mount=type=cache,target=/go/pkg/mod \
@@ -50,7 +50,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
COMMIT=$(git log -1 --format='%H' 2>/dev/null || echo "unknown"); \
LEDGER_ENABLED=false BUILD_TAGS=muslc LINK_STATICALLY=true \
CGO_ENABLED=1 GOOS=linux \
go build \
GOTOOLCHAIN=auto go build \
-mod=readonly \
-tags "netgo,ledger,muslc" \
-ldflags "-X github.com/cosmos/cosmos-sdk/version.Name=sonr \
@@ -67,85 +67,6 @@ RUN --mount=type=cache,target=/go/pkg/mod \
echo "Ensuring binary is statically linked ..."; \
(file /code/build/snrd | grep "statically linked")
# --------------------------------------------------------
# Highway service build stage
FROM ${BASE_IMAGE} AS highway-builder
SHELL ["/bin/sh", "-ecuxo", "pipefail"]
# Install build dependencies
RUN apk add --no-cache \
ca-certificates \
build-base \
git \
linux-headers \
bash
WORKDIR /code
# Copy entire source code
COPY . .
# Fix git ownership issue
RUN git config --global --add safe.directory /code
# Download Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
# Build Highway binary with optimizations
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev"); \
COMMIT=$(git log -1 --format='%H' 2>/dev/null || echo "unknown"); \
CGO_ENABLED=1 GOOS=linux \
go build \
-mod=readonly \
-tags "netgo" \
-ldflags "-X main.Version=${VERSION} \
-X main.Commit=${COMMIT} \
-w -s -linkmode=external -extldflags '-static'" \
-buildvcs=false \
-trimpath \
-o /code/build/hway \
./cmd/hway; \
file /code/build/hway; \
echo "Ensuring binary is statically linked ..."; \
(file /code/build/hway | grep "statically linked")
# --------------------------------------------------------
# Highway runtime image
FROM alpine:3.17 AS highway
LABEL org.opencontainers.image.title="Sonr Highway Service"
LABEL org.opencontainers.image.source="https://github.com/sonr-io/sonr"
# Copy binary from builder
COPY --from=highway-builder /code/build/hway /usr/bin/hway
# Install runtime dependencies
RUN apk add --no-cache ca-certificates wget
# Create non-root user
RUN adduser -D -u 1000 highway
# Set working directory
WORKDIR /home/highway
# Switch to non-root user
USER highway
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --spider -q http://localhost:8090/health || exit 1
# Expose Highway port
EXPOSE 8090
# Set default command
ENTRYPOINT ["/usr/bin/hway"]
# --------------------------------------------------------
# Final minimal runtime image (default target for snrd)
FROM alpine:3.17
@@ -156,8 +77,11 @@ LABEL org.opencontainers.image.source="https://github.com/sonr-io/sonr"
# Copy binary from build stage
COPY --from=builder /code/build/snrd /usr/bin
COPY --from=builder /lib/libwasmvm_muslc.a /lib/libwasmvm_muslc.a
COPY --from=builder /code/scripts/test_node.sh /usr/bin/testnet
RUN chmod +x /usr/bin/testnet
# Copy runtime scripts and make them executable
COPY --from=builder /code/scripts/test_node.sh /usr/bin/devnet
COPY --from=builder /code/scripts/testnet-setup.sh /usr/bin/testnet
COPY --from=builder /code/scripts/lib/ /usr/local/lib/sonr-scripts/
# Set up dependencies
ENV PACKAGES="curl make bash jq sed"
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+46 -107
View File
@@ -104,7 +104,7 @@ stop:
########################################
### Tools & dependencies
########################################
format: go-format ts-format
format: go-format
go-format:
@gum log --level info "Formatting Go code with gofumpt and goimports..."
@if command -v gofumpt > /dev/null; then \
@@ -119,12 +119,7 @@ go-format:
fi
@gum log --level info "✅ Code formatted"
ts-format:
@gum log --level info "Formatting all web applications..."
@pnpm biome format --config-path=$(PWD)/biome.json .
lint: go-lint ts-lint
lint: go-lint
go-lint:
@gum log --level info "Running golangci-lint..."
@@ -137,11 +132,7 @@ go-lint:
golangci-lint run --timeout=10m; \
fi
ts-lint:
@gum log --level info "Running Biome linter with auto-fix for TypeScript projects..."
@pnpm biome lint --config-path=./biome.json --write .
.PHONY: lint go-lint ts-lint format
.PHONY: lint go-lint format
go-mod-cache: go.sum
@gum log --level info "Download go modules to local cache"
@@ -152,10 +143,6 @@ go.sum: go.mod
@go mod tidy
@go mod verify
pnpm-install:
@gum log --level info "Installing pnpm dependencies"
@pnpm install
draw-deps:
@# requires brew install graphviz or apt-get install graphviz
go install github.com/RobotsAndPencils/goviz@latest
@@ -165,18 +152,11 @@ draw-deps:
tidy:
@go mod tidy
@make -C client tidy
@make -C crypto tidy
@make -C cmd/hway tidy
@make -C cmd/motr tidy
@make -C cmd/vault tidy
clean: tidy
@gum log --level info "Cleaning build artifacts..."
rm -rf snapcraft-local.yaml build/ dist/
@$(MAKE) -C cmd/snrd clean
@$(MAKE) -C cmd/hway clean
@$(MAKE) -C cmd/vault clean
@$(MAKE) -C cmd/motr clean
clean-docker:
@gum log --level info "Removing all Docker volumes and networks..."
@@ -197,28 +177,19 @@ build: go.sum
build-snrd: build
build-client: go.sum build-vault build-motr
build-client: go.sum
@$(MAKE) -C client build
@cd /tmp && go mod init test || true
@cd /tmp && go get github.com/sonr-io/sonr/client@main || true
@cd /tmp && gum log --level info "Client SDK import successful"
build-hway: go.sum build-vault build-motr
@$(MAKE) -C cmd/hway build
build-vault:
@$(MAKE) -C cmd/vault build
build-motr: ## Build Motor WASM service worker
@$(MAKE) -C cmd/motr build
# Build all components in parallel
build-all: go.sum
@gum log --level info "Building all components in parallel..."
@$(MAKE) -j5 build build-hway build-client build-motr build-vault
@$(MAKE) -j2 build build-client
@gum log --level info "✅ All components built successfully"
.PHONY: install build build-client build-hway build-snrd build-vault build-motr build-all
.PHONY: install build build-client build-snrd build-all
########################################
### Docker & Services
@@ -226,17 +197,20 @@ build-all: go.sum
docker:
@gum log --level info "Building Docker images..."
@bash scripts/containers.sh build-all
@docker buildx build -t onsonr/snrd:latest -f Dockerfile .
localnet: ## Cross-platform localnet (auto-detects best method for your system)
@bash scripts/cross_platform_localnet.sh
localnet: ## Start single-node localnet with Docker (cleans HOME_DIR first)
@gum log --level info "Cleaning HOME_DIR (~/.sonr)..."
@chmod -R u+rwX $(HOME)/.sonr 2>/dev/null || true
@rm -rf $(HOME)/.sonr 2>/dev/null || true
@$(MAKE) dockernet
dockernet:
@gum log --level info "Starting network with Docker in detached mode..."
@docker stop sonr-testnode 2>/dev/null || true
@docker rm sonr-testnode 2>/dev/null || true
@sleep 3
@CHAIN_ID="sonrtest_1-1" BLOCK_TIME="1000ms" CLEAN=true FORCE_DOCKER=true DOCKER_DETACHED=true bash scripts/test_node.sh
@HOME_DIR=$(HOME)/.sonr CHAIN_ID="sonrtest_1-1" BLOCK_TIME="1000ms" CLEAN=true FORCE_DOCKER=true DOCKER_DETACHED=true bash scripts/test_node.sh
.PHONY: docker localnet dockernet
@@ -245,18 +219,12 @@ dockernet:
########################################
# Smart component release detection and automation
release:
@$(MAKE) -C cmd/hway release
@$(MAKE) -C cmd/motr release
@$(MAKE) -C cmd/snrd release
@$(MAKE) -C cmd/vault release
# Snapshot builds for development
snapshot:
@gum log --level info "📦 Preparing component snapshot..."
@$(MAKE) -C cmd/vault snapshot
@$(MAKE) -C cmd/motr snapshot
@$(MAKE) -C cmd/snrd snapshot
@$(MAKE) -C cmd/hway snapshot
.PHONY: release snapshot
@@ -285,36 +253,40 @@ test-e2e-all:
@gum log --level info "Running all e2e tests"
@cd test/e2e && go test -race -v ./tests/...
e2e-usdc-swap-did:
@gum log --level info "Running USDC Swap E2E tests with DID integration"
@gum log --level warn "Note: These tests require manual setup. See test/e2e/usdc-swap-did/README.md"
@cd test/e2e/usdc-swap-did && make test
e2e-usdc-swap-did-setup:
@gum log --level info "Running USDC Swap E2E test setup..."
@cd test/e2e/usdc-swap-did && bash setup.sh
e2e-usdc-swap-did-verbose:
@gum log --level info "Running USDC Swap E2E tests (verbose mode)"
@cd test/e2e/usdc-swap-did && make test-verbose
test-build-snrd: build
@ls -la build/snrd
@chmod +x build/snrd
@./build/snrd version
test-build-hway: build-hway
@ls -la build/hway
test-tdd:
go test -json ./... 2>&1 | tdd-guard-go -project-root ${GIT_ROOT}
test-app:
@VERSION=$(VERSION) go test -C . -mod=readonly -tags='ledger test_ledger_mock test' github.com/sonr-io/sonr/app/... github.com/sonr-io/sonr/x/... github.com/sonr-io/sonr/types/... github.com/sonr-io/sonr/internal/...
@VERSION=$(VERSION) CGO_LDFLAGS="-lm" go test -C . -mod=readonly -tags='ledger test_ledger_mock test' github.com/sonr-io/sonr/app/... github.com/sonr-io/sonr/x/... github.com/sonr-io/common/... github.com/sonr-io/sonr/internal/...
test-devops:
@echo "No devops tests"
test-client:
@$(MAKE) -C cmd/vault build
@$(MAKE) -C client test
test-crypto:
@$(MAKE) -C crypto test
test-dwn-ci:
@$(MAKE) -C cmd/vault build
@go test -mod=readonly -tags='ledger test_ledger_mock test' -run='!IPFS' ./x/dwn/...
test-internal:
@$(MAKE) -C cmd/vault build
@VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock test' ./internal/...
# Module testing - Simplified
@@ -340,28 +312,14 @@ test-module:
VERSION=$(VERSION) go test -mod=readonly -tags='ledger test_ledger_mock test' ./x/$(MODULE)/...; \
fi \
fi
# Specialized tests
test-hway:
@gum log --level info "Testing Highway service..."
@VERSION=$(VERSION) go test -C . -mod=readonly -v github.com/sonr-io/sonr/bridge/...
test-proto:
@$(MAKE) -C proto lint
@$(MAKE) -C proto check-breaking
test-motr:
@gum log --level info "Testing Motor WASM service worker..."
@$(MAKE) -C cmd/motr test
test-vault:
@gum log --level info "Testing Vault WASM plugin..."
@$(MAKE) -C cmd/vault test
test-benchmark:
@go test -mod=readonly -bench=. ./...
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-hway test-motr test-vault test-benchmark
.PHONY: test test-all test-unit test-race test-cover test-tdd test-module test-benchmark test-e2e test-e2e-all e2e-usdc-swap-did e2e-usdc-swap-did-setup e2e-usdc-swap-did-verbose
###############################################################################
### Protobuf ###
@@ -373,41 +331,20 @@ climd-gen:
proto-gen:
@gum log --level info "Generating Go protobuf files..."
@$(MAKE) -C proto gen
@gum log --level info "Generating TypeScript protobuf files..."
@if [ -d "packages/es" ]; then \
cd packages/es && pnpm gen:protobufs || { gum log --level error "TypeScript protobuf generation failed"; exit 1; }; \
else \
gum log --level warn "Skipping TypeScript generation: packages/es not found"; \
fi
@gum log --level info "Auto-formatting generated protobuf files..."
@$(MAKE) format
swagger-gen:
@$(MAKE) -C proto swagger-gen
@gum log --level info "Moving and renaming generated files..."
@find docs/static/openapi -type f \( -name "query.swagger.yaml" -o -name "tx.swagger.yaml" \) | while read -r filepath; do \
\
parent_dir=$$(dirname "$$filepath"); \
grandparent_dir=$$(dirname "$$parent_dir"); \
module=$$(basename "$$grandparent_dir"); \
filename=$$(basename "$$filepath"); \
\
new_filename="$$module.$$filename"; \
destination="docs/static/openapi/$$new_filename"; \
\
gum log --level debug "Moving $$filepath to $$destination"; \
mv "$$filepath" "$$destination"; \
done
@gum log --level info "Cleaning up empty source directories..."
@find docs/static/openapi -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
@gum log --level info "Converting Swagger 2.0 to OpenAPI 3.0..."
@pnpm run convert-swagger
@gum log --level info "✅ API documentation processing complete."
openapi-gen:
@$(MAKE) -C proto openapi-gen
@gum log --level info "✅ OpenAPI generation complete. Output: docs/static/openapi.yml"
# Backwards compatibility alias
swagger-gen: openapi-gen
templ-gen:
@docker run --rm -v `pwd`:/code -w=/code --user $(shell id -u):$(shell id -g) ghcr.io/a-h/templ:latest generate
.PHONY: proto-gen proto-swagger-gen swagger-gen proto-lint proto-check-breaking proto-publish
.PHONY: proto-gen proto-openapi-gen openapi-gen swagger-gen proto-lint proto-check-breaking proto-publish
###############################################################################
### Network Operations ###
@@ -444,14 +381,15 @@ help:
@gum log --level info ""
@gum log --level info "🛠️ Build & Install:"
@gum log --level info " install Install snrd binary"
@gum log --level info " build Build snrd binary with vault WASM"
@gum log --level info " build Build snrd binary"
@gum log --level info " build-all Build all components in parallel"
@gum log --level info " build-hway Build Highway service"
@gum log --level info " build-vault Build vault WASM module"
@gum log --level info " build-motr Build Motor WASM service worker"
@gum log --level info " build-client Build client SDK"
@gum log --level info " docker Build Docker images"
@gum log --level info ""
@gum log --level info "📦 Release & Distribution:"
@gum log --level info " release Create production release with GoReleaser"
@gum log --level info " snapshot Create development snapshot builds"
@gum log --level info ""
@gum log --level info "🚀 Local Development:"
@gum log --level info " localnet Start single-node testnet"
@gum log --level info " start Start backend services"
@@ -461,7 +399,7 @@ help:
@gum log --level info ""
@gum log --level info "📦 Code Generation:"
@gum log --level info " proto-gen Generate protobuf code"
@gum log --level info " swagger-gen Generate OpenAPI docs"
@gum log --level info " openapi-gen Generate OpenAPI documentation"
@gum log --level info ""
@gum log --level info "🔧 Development Tools:"
@gum log --level info " format Format code (Go + TypeScript)"
@@ -476,10 +414,11 @@ help:
@gum log --level info " test-e2e Run e2e tests"
@gum log --level info " test-e2e-all Run all e2e tests"
@gum log --level info " test-module Test specific module (MODULE=did|dwn|svc)"
@gum log --level info " test-packages Test all packages (lint + build)"
@gum log --level info " test-ipfs Test vault with IPFS export/import"
@gum log --level info " test-vault Test vault operations"
@gum log --level info " test-web Test all web apps (lint + build)"
@gum log --level info ""
@gum log --level info "🔄 E2E Test Suites:"
@gum log --level info " e2e-usdc-swap-did Run USDC swap E2E tests"
@gum log --level info " e2e-usdc-swap-did-setup Interactive setup for USDC swap tests"
@gum log --level info " e2e-usdc-swap-did-verbose Run USDC swap tests with verbose output"
@gum log --level info ""
@gum log --level info "📚 Module Testing Examples:"
@gum log --level info " make test-module MODULE=did # Test DID module"
+28 -190
View File
@@ -106,10 +106,8 @@ make testnet-stop
### Building from Source
```bash
# Build all binaries
make build # snrd blockchain node
make build-hway # Highway service
make build-vault # Vault WASM plugin
# Build blockchain node
make build
# Build Docker image
make docker
@@ -162,38 +160,11 @@ make ipfs-status # Check IPFS connectivity
## 🏗️ Architecture
Sonr consists of three primary components and three custom modules:
### Service Architecture
```
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Caddy │────▶│ snrd │ │ IPFS │
└─────────┘ └─────────┘ └─────────┘
│ │ │
│ ▼ ▼
│ ┌─────────┐ ┌─────────┐
└─────────▶│ Highway │────▶│ Redis │
└─────────┘ └─────────┘
```
- **Caddy**: Reverse proxy with gRPC-Web support (port 80)
- **Redis**: Task queue backend for Highway (port 6379)
- **Highway**: UCAN-based task processor (port 8090)
- **IPFS**: Distributed storage (API: 5001, Gateway: 8080)
- **snrd**: Blockchain node (gRPC: 9090, REST: 1317)
### Cross-Platform Support
The `localnet` target now automatically detects and uses the best available method:
1. Checks for local binary (built with `make install`)
2. Falls back to Docker if available
3. Handles permission issues on systems like Arch Linux
4. Supports systemd service installation (see `etc/systemd/`)
Sonr is a Cosmos SDK-based blockchain with integrated IPFS storage and three custom modules:
### Core Components
#### 1. **Blockchain Node (`snrd`)**
#### **Blockchain Node (`snrd`)**
The main blockchain daemon built with Cosmos SDK v0.50.14, providing:
@@ -201,22 +172,15 @@ The main blockchain daemon built with Cosmos SDK v0.50.14, providing:
- EVM compatibility via Evmos integration
- IBC for cross-chain communication
- CosmWasm smart contract support
- IPFS integration for decentralized storage
#### 2. **Highway Service (`hway`)**
### Cross-Platform Support
An Asynq-based task processing service for vault operations:
- Redis-backed job queue with priority levels
- Actor-based concurrency using Proto.Actor framework
- Processes cryptographic operations through WebAssembly enclaves
#### 3. **Motor Plugin (`motr`)**
WebAssembly-based vault system providing:
- Secure execution environment for sensitive operations
- Hardware-backed key management
- Multi-party computation capabilities
The `localnet` target automatically detects and uses the best available method:
1. Checks for local binary (built with `make install`)
2. Falls back to Docker if available
3. Handles permission issues on systems like Arch Linux
4. Supports systemd service installation (see `etc/systemd/`)
## 📖 Module Documentation
@@ -301,45 +265,12 @@ export BLOCK_TIME="1000ms"
# Network selection for Starship
export NETWORK="devnet" # or "testnet"
# Redis configuration
REDIS_URL=redis://redis:6379
REDIS_DB=0
# Highway service
HIGHWAY_PORT=8090
LOG_LEVEL=debug
# IPFS configuration
IPFS_API_URL=http://ipfs:5001
# Caddy configuration
CADDY_DOMAIN=localhost
```
Environment variables can be set directly or via a `.env` file in the project root.
### Docker Compose Services
The project includes a comprehensive Docker Compose setup for running all backend services:
```bash
# Start all services (Redis, Highway, Caddy, IPFS)
make docker-up
# Stop all services
make docker-down
# View service logs
make docker-logs # All services
make docker-logs-redis # Specific service
# Check service health
make docker-status
# Clean up Docker resources
make docker-clean
```
### Starship Configuration
Edit `starship.yml` to configure multi-node testnets:
@@ -353,146 +284,53 @@ chains:
# ... additional configuration
```
#### Troubleshooting
**Services not starting:**
```bash
# Check service status
make docker-status
# View detailed logs
make docker-logs
# Ensure Docker network exists
docker network ls | grep sonr-network
```
**Redis connection issues:**
```bash
# Test Redis connectivity
docker exec redis redis-cli ping
# Check Redis logs
make docker-logs-redis
```
### Troubleshooting
**IPFS not accessible:**
```bash
# Verify IPFS is running
curl http://127.0.0.1:5001/api/v0/version
# Check IPFS logs
docker compose -f etc/stack/compose.yml logs ipfs
# Check IPFS status
make ipfs-status
```
**Port conflicts:**
- Caddy: 80 (HTTP)
- Redis: 6379
- Highway: 8090
- IPFS API: 5001
- IPFS Gateway: 8080
- Node gRPC: 9090
- Node REST API: 1317
Stop conflicting services or modify ports in `etc/stack/compose.yml`.
Stop conflicting services or modify ports in configuration files.
## 🏗️ Project Structure
Sonr uses a modern monorepo architecture with pnpm workspaces for JavaScript/TypeScript packages alongside the Go blockchain implementation:
### Workspace Organization
Sonr is a focused Cosmos SDK blockchain implementation:
```
sonr/
├── app/ # Application setup and module wiring
├── cmd/ # Binary entry points
── snrd/ # Blockchain node
│ ├── hway/ # Highway service
│ └── motr/ # Motor WASM plugin
├── x/ # Custom chain modules
── snrd/ # Blockchain node daemon
├── x/ # Custom Cosmos SDK modules
│ ├── did/ # W3C DID implementation
│ ├── dwn/ # Decentralized Web Nodes
│ └── svc/ # Service management
├── types/ # Internal packages
│ ├── coins/ # Task processing
│ └── ipfs/ # Authorization networks
├── types/ # Internal Go packages
├── proto/ # Protobuf definitions
├── scripts/ # Utility scripts
├── test/ # Integration tests
├── docs/ # Documentation site
# Monorepo Packages (pnpm workspaces)
├── packages/ # Core JavaScript/TypeScript packages
│ ├── es/ # @sonr.io/es - ES client library
│ ├── sdk/ # @sonr.io/sdk - SDK package
│ └── ui/ # @sonr.io/ui - Shared UI components
├── cli/ # CLI tools
│ ├── install/ # @sonr.io/install - Installation CLI
│ └── join-testnet/# @sonr.io/join-testnet - Testnet CLI
└── web/ # Web applications
├── auth/ # Authentication app (Next.js)
└── dash/ # Dashboard app (Next.js)
```
### Working with the Monorepo
#### Installation
```bash
# Install all dependencies for the monorepo
pnpm install
# Build all packages
pnpm build
# Run development mode for all packages
pnpm dev
```
#### Package Management
```bash
# Run commands in specific packages
pnpm --filter @sonr.io/es build
pnpm --filter @sonr.io/sdk test
# Add dependencies to specific packages
pnpm --filter @sonr.io/ui add react
# Run commands in all packages
pnpm -r build # Build all packages
pnpm -r test # Test all packages
```
#### Versioning & Publishing
The monorepo uses [Changesets](https://github.com/changesets/changesets) for package versioning:
```bash
# Add a changeset for your changes
pnpm changeset
# Version packages based on changesets
pnpm changeset version
# Publish packages to npm
pnpm changeset publish
├── docs/ # Documentation
└── client/ # Client libraries and tooling
```
### Key Technologies
- **pnpm workspaces**: Efficient dependency management with single lockfile
- **TypeScript project references**: Incremental builds and better IDE performance
- **Turbo**: Build orchestration and caching for faster builds
- **Changesets**: Automated versioning and changelog generation
- **Biome**: Fast, unified linting and formatting (replaces ESLint/Prettier)
### Package Dependencies
- `@sonr.io/es`: Core ES client library with protobuf types
- `@sonr.io/sdk`: High-level SDK (depends on @sonr.io/es)
- `@sonr.io/ui`: Shared UI components for web applications
- `@sonr.io/install`: CLI tool for installing Sonr
- `@sonr.io/join-testnet`: CLI tool for joining testnet
- Web apps use all three core packages (@sonr.io/es, @sonr.io/sdk, @sonr.io/ui)
- **Cosmos SDK v0.50.14**: Blockchain framework
- **CometBFT v0.38.17**: Byzantine fault-tolerant consensus
- **IBC v8.7.0**: Inter-blockchain communication
- **CosmWasm v1.5.8**: Smart contract support
- **IPFS**: Decentralized storage integration
## 🤝 Community & Support
+4 -5
View File
@@ -2,16 +2,15 @@
package modulev1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/app/v1alpha1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+4 -5
View File
@@ -2,17 +2,16 @@
package dexv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+3 -4
View File
@@ -3,15 +3,14 @@ package dexv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_GenesisState_3_list)(nil)
+4 -5
View File
@@ -2,12 +2,8 @@
package dexv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
@@ -15,6 +11,9 @@ import (
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_InterchainDEXAccount_7_list)(nil)
+4 -5
View File
@@ -2,19 +2,18 @@
package dexv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/query/v1beta1"
v1beta11 "cosmossdk.io/api/cosmos/base/v1beta1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package dexv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+4 -5
View File
@@ -2,13 +2,9 @@
package dexv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
_ "cosmossdk.io/api/cosmos/msg/v1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
@@ -16,6 +12,9 @@ import (
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_MsgRegisterDEXAccount_3_list)(nil)
-1
View File
@@ -8,7 +8,6 @@ package dexv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+4 -5
View File
@@ -2,16 +2,15 @@
package modulev1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/app/v1alpha1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+3 -4
View File
@@ -3,16 +3,15 @@ package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_EventDIDCreated_3_list)(nil)
+4 -5
View File
@@ -2,17 +2,16 @@
package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/amino"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+5 -6
View File
@@ -2,19 +2,18 @@
package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/query/v1beta1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package didv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
-1
View File
@@ -4,7 +4,6 @@ package didv1
import (
context "context"
ormlist "cosmossdk.io/orm/model/ormlist"
ormtable "cosmossdk.io/orm/model/ormtable"
ormerrors "cosmossdk.io/orm/types/ormerrors"
+4 -5
View File
@@ -2,16 +2,15 @@
package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/orm/v1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+4 -5
View File
@@ -2,18 +2,17 @@
package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/msg/v1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package didv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+5 -6
View File
@@ -2,18 +2,17 @@
package didv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
_ "cosmossdk.io/api/amino"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
)
var (
+4 -5
View File
@@ -2,16 +2,15 @@
package modulev1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/app/v1alpha1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+3 -4
View File
@@ -3,16 +3,15 @@ package dwnv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
io "io"
reflect "reflect"
sync "sync"
)
var (
+4 -5
View File
@@ -2,17 +2,16 @@
package dwnv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/amino"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_GenesisState_2_list)(nil)
+4 -5
View File
@@ -2,18 +2,17 @@
package dwnv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
v1beta1 "cosmossdk.io/api/cosmos/base/query/v1beta1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package dwnv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
-1
View File
@@ -4,7 +4,6 @@ package dwnv1
import (
context "context"
ormlist "cosmossdk.io/orm/model/ormlist"
ormtable "cosmossdk.io/orm/model/ormtable"
ormerrors "cosmossdk.io/orm/types/ormerrors"
+4 -5
View File
@@ -2,16 +2,15 @@
package dwnv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/orm/v1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_EncryptionMetadata_6_list)(nil)
+4 -5
View File
@@ -2,18 +2,17 @@
package dwnv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/msg/v1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package dwnv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+4 -5
View File
@@ -2,16 +2,15 @@
package modulev1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/app/v1alpha1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
+3 -4
View File
@@ -3,16 +3,15 @@ package svcv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
io "io"
reflect "reflect"
sync "sync"
)
var (
+4 -5
View File
@@ -2,19 +2,18 @@
package svcv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/amino"
v1beta1 "cosmossdk.io/api/cosmos/base/v1beta1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var _ protoreflect.List = (*_GenesisState_2_list)(nil)
+4 -5
View File
@@ -3,16 +3,15 @@ package svcv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package svcv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
-1
View File
@@ -4,7 +4,6 @@ package svcv1
import (
context "context"
ormlist "cosmossdk.io/orm/model/ormlist"
ormtable "cosmossdk.io/orm/model/ormtable"
ormerrors "cosmossdk.io/orm/types/ormerrors"
+5 -6
View File
@@ -2,17 +2,16 @@
package svcv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
_ "cosmossdk.io/api/cosmos/orm/v1"
fmt "fmt"
runtime "github.com/cosmos/cosmos-proto/runtime"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sort "sort"
sync "sync"
)
var _ protoreflect.List = (*_Service_5_list)(nil)
+4 -5
View File
@@ -2,18 +2,17 @@
package svcv1
import (
fmt "fmt"
io "io"
reflect "reflect"
sync "sync"
_ "cosmossdk.io/api/cosmos/msg/v1"
fmt "fmt"
_ "github.com/cosmos/cosmos-proto"
runtime "github.com/cosmos/cosmos-proto/runtime"
_ "github.com/cosmos/gogoproto/gogoproto"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoiface "google.golang.org/protobuf/runtime/protoiface"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
io "io"
reflect "reflect"
sync "sync"
)
var (
-1
View File
@@ -8,7 +8,6 @@ package svcv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+2 -2
View File
@@ -6,8 +6,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/crypto/keys"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/crypto/keys"
"github.com/sonr-io/crypto/ucan"
)
// UCANDecorator validates UCAN tokens in transactions
+1 -1
View File
@@ -9,7 +9,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/crypto/ucan"
)
// TestUCANDecorator tests the UCAN decorator functionality
+1 -1
View File
@@ -13,7 +13,7 @@ import (
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
"github.com/spf13/cobra"
)
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"path/filepath"
"github.com/sonr-io/sonr/app"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
"github.com/spf13/cobra"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/client"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
)
// SonrContext manages node-specific state and configuration
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"cosmossdk.io/log"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/crypto/vrf"
"github.com/sonr-io/crypto/vrf"
)
// TestSonrContextInitialization tests SonrContext initialization with VRF keys
-103
View File
@@ -1,103 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"useExhaustiveDependencies": "warn"
},
"style": {
"noNonNullAssertion": "warn",
"useConst": "error"
},
"suspicious": {
"noExplicitAny": "warn",
"noArrayIndexKey": "warn"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
},
"overrides": [
{
"formatter": {
"enabled": false
},
"linter": {
"enabled": false
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
},
"complexity": {
"noStaticOnlyClass": "off"
}
}
}
},
{
"linter": {
"rules": {
"style": {
"useTemplate": "warn",
"noUnusedTemplateLiteral": "warn"
}
}
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
}
}
}
},
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedVariables": "warn"
}
}
}
}
]
}
-118
View File
@@ -1,118 +0,0 @@
// Package bridge provides the HTTP bridge server for the Highway service.
package bridge
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/bridge/server"
)
// HighwayService encapsulates the entire Highway service setup and lifecycle
type HighwayService struct {
config *Config
client *asynq.Client
httpServer *server.Server
queueManager *QueueManager
// Internal channels for coordination
ctx context.Context
cancel context.CancelFunc
sigCh chan os.Signal
}
// NewHighwayService creates a new Highway service with all components initialized
func NewHighwayService() *HighwayService {
// Setup graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
// Handle shutdown signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
// Initialize configuration
config := NewConfig()
// Initialize Redis-based task queue client
client := asynq.NewClient(asynq.RedisClientOpt{Addr: config.RedisAddr})
log.Printf("Asynq client connected to Redis at %s", config.RedisAddr)
// Create server configuration for bridge proxy
serverConfig := &server.Config{
HTTPAddr: fmt.Sprintf(":%d", config.HTTPPort),
JWTSecret: config.JWTSecret,
IPFSClient: config.IPFSClient,
}
// Create HTTP bridge server
httpServer := server.NewServer(serverConfig)
// Initialize UCAN task processing server with queue manager
queueManager := NewQueueManager(config)
return &HighwayService{
config: config,
client: client,
httpServer: httpServer,
queueManager: queueManager,
ctx: ctx,
cancel: cancel,
sigCh: sigCh,
}
}
// Start begins the Highway service with HTTP server and queue processing
func (hs *HighwayService) Start() error {
log.Println("Starting Highway Service - UCAN-based MPC Task Processor")
// Create and start HTTP bridge server in a goroutine
go func() {
log.Printf("Starting HTTP bridge server on port %d", hs.config.HTTPPort)
if err := hs.httpServer.Start(hs.client); err != nil {
log.Fatalf("HTTP bridge server failed: %v", err)
}
}()
// Start queue server with graceful shutdown support
go func() {
if err := hs.queueManager.Run(); err != nil {
log.Printf("Asynq server error: %v", err)
hs.cancel()
}
}()
// Wait for shutdown signal
select {
case <-hs.sigCh:
log.Println("Received shutdown signal, initiating graceful shutdown...")
case <-hs.ctx.Done():
log.Println("Context cancelled, shutting down...")
}
return nil
}
// Shutdown gracefully stops all service components
func (hs *HighwayService) Shutdown() {
log.Println("Shutting down Highway service...")
// Close Asynq client
if hs.client != nil {
hs.client.Close()
}
// Shutdown queue manager
if hs.queueManager != nil {
hs.queueManager.Shutdown()
}
// Cancel context to signal shutdown to all components
hs.cancel()
log.Println("Highway service stopped")
}
-31
View File
@@ -1,31 +0,0 @@
package bridge
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewHighwayService(t *testing.T) {
service := NewHighwayService()
require.NotNil(t, service)
assert.NotNil(t, service.config)
assert.NotNil(t, service.client)
assert.NotNil(t, service.httpServer)
assert.NotNil(t, service.queueManager)
assert.NotNil(t, service.ctx)
assert.NotNil(t, service.cancel)
assert.NotNil(t, service.sigCh)
}
func TestHighwayServiceComponents(t *testing.T) {
service := NewHighwayService()
defer service.Shutdown()
// Test that all components are properly initialized
assert.NotNil(t, service.config.RedisAddr)
assert.NotNil(t, service.config.JWTSecret)
assert.NotNil(t, service.config.AsynqConfig)
}
-202
View File
@@ -1,202 +0,0 @@
package bridge
import (
"log"
"os"
"strconv"
"time"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/types/ipfs"
)
const (
DefaultRedisAddr = "127.0.0.1:6379"
DefaultJWTSecret = "highway-ucan-secret-key"
DefaultHTTPPort = 8090
ShutdownTimeout = 30 * time.Second
)
// OIDCProviderConfig contains OIDC provider configuration
type OIDCProviderConfig struct {
Issuer string
PublicURL string
SigningKeyPath string
EncryptionKeyPath string
AuthorizationCodeTTL time.Duration
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
IDTokenTTL time.Duration
EnablePKCE bool
EnableRefreshTokens bool
EnableSIOP bool
SupportedScopes []string
SupportedResponseTypes []string
SupportedGrantTypes []string
AllowedRedirectURIs []string
WebAuthnRPID string
WebAuthnRPName string
WebAuthnTimeout int
AutoCreateVault bool
}
type Config struct {
RedisAddr string
HTTPPort int
JWTSecret []byte
IPFSClient ipfs.IPFSClient
ShutdownTimeout time.Duration
AsynqConfig asynq.Config
OIDC OIDCProviderConfig
}
func NewConfig() *Config {
jwtSecret := initializeJWTSecret()
redisAddr := getRedisAddr()
httpPort := getHTTPPort()
ipfsClient := initializeIPFS()
oidcConfig := initializeOIDCConfig()
return &Config{
RedisAddr: redisAddr,
HTTPPort: httpPort,
JWTSecret: jwtSecret,
IPFSClient: ipfsClient,
ShutdownTimeout: ShutdownTimeout,
AsynqConfig: asynq.Config{
Concurrency: 10,
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
ShutdownTimeout: ShutdownTimeout,
// Enhanced error handling and retry configuration
RetryDelayFunc: asynq.DefaultRetryDelayFunc,
IsFailure: func(err error) bool {
return err != nil
},
},
OIDC: oidcConfig,
}
}
func initializeJWTSecret() []byte {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
secret = DefaultJWTSecret
log.Printf("Warning: Using default JWT secret for UCAN operations")
log.Printf("Set JWT_SECRET environment variable for production deployment")
} else {
log.Println("JWT secret loaded from environment")
}
return []byte(secret)
}
func getRedisAddr() string {
// Check for REDIS_URL first (Docker Compose style)
if url := os.Getenv("REDIS_URL"); url != "" {
// Parse redis://host:port format
if len(url) > 8 && url[:8] == "redis://" {
return url[8:]
}
return url
}
// Fall back to REDIS_ADDR
if addr := os.Getenv("REDIS_ADDR"); addr != "" {
return addr
}
log.Printf("Using default Redis address: %s", DefaultRedisAddr)
return DefaultRedisAddr
}
func initializeIPFS() ipfs.IPFSClient {
ipfsClient, err := ipfs.GetClient()
if err != nil {
log.Printf("Warning: IPFS client initialization failed: %v", err)
log.Println("Enclave data will be handled directly without IPFS storage")
return nil
}
log.Println("IPFS client initialized successfully")
return ipfsClient
}
func initializeOIDCConfig() OIDCProviderConfig {
issuer := os.Getenv("OIDC_ISSUER")
if issuer == "" {
issuer = "https://localhost:8080"
}
publicURL := os.Getenv("OIDC_PUBLIC_URL")
if publicURL == "" {
publicURL = issuer
}
rpID := os.Getenv("WEBAUTHN_RP_ID")
if rpID == "" {
rpID = "localhost"
}
rpName := os.Getenv("WEBAUTHN_RP_NAME")
if rpName == "" {
rpName = "Sonr Identity Platform"
}
return OIDCProviderConfig{
Issuer: issuer,
PublicURL: publicURL,
SigningKeyPath: os.Getenv("OIDC_SIGNING_KEY_PATH"),
EncryptionKeyPath: os.Getenv("OIDC_ENCRYPTION_KEY_PATH"),
AuthorizationCodeTTL: 10 * time.Minute,
AccessTokenTTL: 1 * time.Hour,
RefreshTokenTTL: 7 * 24 * time.Hour,
IDTokenTTL: 1 * time.Hour,
EnablePKCE: true,
EnableRefreshTokens: true,
EnableSIOP: true,
SupportedScopes: []string{
"openid", "profile", "email", "did", "vault", "offline_access",
},
SupportedResponseTypes: []string{
"code", "id_token", "code id_token",
},
SupportedGrantTypes: []string{
"authorization_code", "refresh_token", "client_credentials",
},
AllowedRedirectURIs: getRedirectURIs(),
WebAuthnRPID: rpID,
WebAuthnRPName: rpName,
WebAuthnTimeout: 60000,
AutoCreateVault: true,
}
}
// getRedirectURIs returns the allowed redirect URIs for OIDC
func getRedirectURIs() []string {
// Default URIs for development
uris := []string{
"http://localhost:3000/callback",
"http://localhost:3001/callback",
"https://localhost:3000/callback",
"https://localhost:3001/callback",
}
// Add additional URIs from environment if specified
if envURIs := os.Getenv("OIDC_ALLOWED_REDIRECT_URIS"); envURIs != "" {
// Parse comma-separated URIs
// This could be enhanced with proper validation
log.Printf("Additional redirect URIs configured from environment")
}
return uris
}
// getHTTPPort returns the HTTP port for the Highway service
func getHTTPPort() int {
if port := os.Getenv("HIGHWAY_PORT"); port != "" {
if p, err := strconv.Atoi(port); err == nil {
return p
}
}
return DefaultHTTPPort
}
-265
View File
@@ -1,265 +0,0 @@
// Package handlers provides HTTP handlers for the highway server
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// CustomClaims defines custom JWT claims for vault operations
type CustomClaims struct {
UserID string `json:"user_id"`
Permissions []string `json:"permissions"`
jwt.RegisteredClaims
}
// LoginRequest represents the login request payload
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// LoginResponse represents the login response payload
type LoginResponse struct {
Token string `json:"token"`
}
// LoginHandler generates JWT tokens for authentication
// This now supports both traditional login and OIDC-based authentication
func LoginHandler(jwtSecret []byte) echo.HandlerFunc {
return func(c echo.Context) error {
// Check if this is an OIDC callback
code := c.QueryParam("code")
if code != "" {
return handleOIDCCallback(c, code, jwtSecret)
}
// Check if user has WebAuthn credentials
authHeader := c.Request().Header.Get("X-WebAuthn-Assertion")
if authHeader != "" {
return handleWebAuthnLogin(c, authHeader, jwtSecret)
}
// Traditional login flow
var req LoginRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
// Simple authentication - in production, validate against a proper user database
if req.Username == "" || req.Password == "" {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Username and password are required"},
)
}
// For demo purposes, accept any non-empty credentials
// In production, verify credentials against database/directory
if req.Username == "vault-user" && req.Password == "vault-pass" {
// Create custom claims
claims := &CustomClaims{
UserID: req.Username,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), // 24 hours
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault",
Subject: req.Username,
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign token with secret
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to generate token"},
)
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
}
// handleOIDCCallback processes OIDC authorization code callback
func handleOIDCCallback(c echo.Context, code string, jwtSecret []byte) error {
// Exchange code for tokens using OIDC token endpoint
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: c.QueryParam("redirect_uri"),
ClientID: c.QueryParam("client_id"),
}
// Call internal OIDC token handler
// In production, this would make an HTTP call to the OIDC provider
c.Set("oidc_token_request", tokenReq)
if err := handleAuthorizationCodeGrant(c, tokenReq); err != nil {
return err
}
// Get the OIDC session from context
session, ok := c.Get("oidc_session").(*OIDCSession)
if !ok {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to establish OIDC session",
})
}
// Create JWT token from OIDC session
claims := &CustomClaims{
UserID: session.UserDID,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(session.ExpiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault-oidc",
Subject: session.UserDID,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate token",
})
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
// handleWebAuthnLogin processes WebAuthn-based login
func handleWebAuthnLogin(c echo.Context, assertion string, jwtSecret []byte) error {
// Verify WebAuthn assertion
// This would integrate with the WebAuthn handlers
// For now, extract username from assertion (simplified)
username := c.Request().Header.Get("X-WebAuthn-Username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "WebAuthn username required",
})
}
// Verify the assertion matches a stored credential
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
if !exists || len(credentials) == 0 {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "No WebAuthn credentials found for user",
})
}
// Create JWT token for WebAuthn authenticated user
claims := &CustomClaims{
UserID: username,
Permissions: []string{
"vault:generate",
"vault:sign",
"vault:verify",
"vault:export",
"vault:import",
"vault:refresh",
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "highway-vault-webauthn",
Subject: username,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate token",
})
}
return c.JSON(http.StatusOK, LoginResponse{Token: tokenString})
}
// OIDCLoginHandler initiates OIDC login flow
func OIDCLoginHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Build authorization URL
authURL := buildOIDCAuthorizationURL(
c.QueryParam("client_id"),
c.QueryParam("redirect_uri"),
c.QueryParam("scope"),
c.QueryParam("state"),
)
// Redirect to OIDC authorization endpoint
return c.Redirect(http.StatusFound, authURL)
}
}
// buildOIDCAuthorizationURL constructs the OIDC authorization URL
func buildOIDCAuthorizationURL(clientID, redirectURI, scope, state string) string {
// Default values if not provided
if clientID == "" {
clientID = "highway-vault-client"
}
if redirectURI == "" {
redirectURI = "http://localhost:8080/auth/callback"
}
if scope == "" {
scope = "openid profile did vault"
}
if state == "" {
state = generateState()
}
// Build authorization URL
return fmt.Sprintf(
"https://localhost:8080/oidc/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
clientID,
redirectURI,
scope,
state,
)
}
// generateState generates a random state parameter for OIDC
func generateState() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "default-state"
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
-60
View File
@@ -1,60 +0,0 @@
package handlers
import (
"encoding/json"
"testing"
"github.com/sonr-io/sonr/crypto/mpc"
)
// getQueueFromPriority returns the queue name based on priority
func getQueueFromPriority(priority string) string {
return GetQueueFromPriority(priority)
}
// BenchmarkJSONMarshaling measures JSON encoding/decoding performance
func BenchmarkJSONMarshaling(b *testing.B) {
payload := map[string]any{
"message": []byte("benchmark test message for JSON marshaling performance"),
"enclave": &mpc.EnclaveData{},
"priority": "critical",
}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Marshal
data, err := json.Marshal(payload)
if err != nil {
b.Error(err)
}
// Unmarshal
var decoded map[string]any
err = json.Unmarshal(data, &decoded)
if err != nil {
b.Error(err)
}
}
})
}
// BenchmarkQueuePrioritySelection measures queue selection performance
func BenchmarkQueuePrioritySelection(b *testing.B) {
priorities := []string{"critical", "high", "default", "low", "", "unknown"}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
priority := priorities[i%len(priorities)]
i++
queue := getQueueFromPriority(priority)
_ = queue // Avoid compiler optimization
}
})
}
-333
View File
@@ -1,333 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
)
// BroadcastService handles blockchain broadcasting operations
type BroadcastService struct {
// TODO: Add cosmos SDK client for actual broadcasting
}
var broadcastService = &BroadcastService{}
// HandleBroadcast handles generic message broadcasting to blockchain
func HandleBroadcast(c echo.Context) error {
var req BroadcastRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid broadcast request",
})
}
// Validate request
if req.Message == nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Message is required",
})
}
// Process based on gasless flag
if req.Gasless {
return handleGaslessBroadcast(c, &req)
}
return handleStandardBroadcast(c, &req)
}
// handleGaslessBroadcast handles gasless transaction broadcasting
func handleGaslessBroadcast(c echo.Context, req *BroadcastRequest) error {
// Validate gasless eligibility
if !isGaslessEligible(req.Message) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Message not eligible for gasless broadcast",
})
}
// TODO: Implement actual blockchain broadcast
// For now, simulate successful broadcast
response := &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12345,
Code: 0,
RawLog: "Transaction broadcast successfully",
Success: true,
}
return c.JSON(http.StatusOK, response)
}
// handleStandardBroadcast handles regular transaction broadcasting
func handleStandardBroadcast(c echo.Context, req *BroadcastRequest) error {
// Get sender address
fromAddress := req.FromAddress
if fromAddress == "" {
// Try to get from context
userDID := c.Get("user_did")
if userDID != nil {
fromAddress = deriveAddressFromDID(userDID.(string))
}
}
if fromAddress == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "From address is required for standard broadcast",
})
}
// TODO: Implement actual blockchain broadcast
// For now, simulate successful broadcast
response := &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12346,
Code: 0,
RawLog: "Transaction broadcast successfully",
Success: true,
}
return c.JSON(http.StatusOK, response)
}
// BroadcastWebAuthnRegistration broadcasts WebAuthn registration to blockchain
func BroadcastWebAuthnRegistration(
credential *WebAuthnCredential,
gasless bool,
) (*BroadcastResponse, error) {
// Create MsgRegisterWebAuthnCredential
msg := map[string]any{
"@type": "/sonr.did.v1.MsgRegisterWebAuthnCredential",
"username": credential.Username,
"credential_id": credential.CredentialID,
"public_key": credential.PublicKey,
"attestation_object": credential.AttestationObject,
"client_data_json": credential.ClientDataJSON,
"origin": credential.Origin,
"algorithm": credential.Algorithm,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: gasless,
AutoSign: true,
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12347,
Code: 0,
RawLog: "WebAuthn credential registered successfully",
Success: true,
}, nil
}
// BroadcastVaultCreation broadcasts vault creation to blockchain
func BroadcastVaultCreation(
userDID string,
vaultConfig map[string]any,
) (*BroadcastResponse, error) {
// Create MsgCreateVault
msg := map[string]any{
"@type": "/sonr.vault.v1.MsgCreateVault",
"creator": userDID,
"config": vaultConfig,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: true, // Vault creation is gasless for new users
AutoSign: true,
FromAddress: deriveAddressFromDID(userDID),
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12348,
Code: 0,
RawLog: "Vault created successfully",
Success: true,
}, nil
}
// BroadcastDIDDocument broadcasts DID document to blockchain
func BroadcastDIDDocument(didDoc map[string]any, gasless bool) (*BroadcastResponse, error) {
// Create MsgCreateDIDDocument or MsgUpdateDIDDocument
msg := map[string]any{
"@type": "/sonr.did.v1.MsgCreateDIDDocument",
"did_document": didDoc,
}
// Create broadcast request (for future use with actual broadcast)
_ = &BroadcastRequest{
Message: msg,
Gasless: gasless,
AutoSign: true,
}
// Simulate broadcast (TODO: Implement actual broadcast)
return &BroadcastResponse{
TxHash: generateTxHash(),
Height: 12349,
Code: 0,
RawLog: "DID document created successfully",
Success: true,
}, nil
}
// HandleTransactionStatus checks transaction status
func HandleTransactionStatus(c echo.Context) error {
txHash := c.Param("hash")
if txHash == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Transaction hash is required",
})
}
// TODO: Query actual blockchain for transaction status
// For now, return simulated status
status := map[string]any{
"tx_hash": txHash,
"height": 12350,
"status": "confirmed",
"code": 0,
"gas_used": 50000,
"gas_wanted": 100000,
"timestamp": time.Now().Unix(),
}
return c.JSON(http.StatusOK, status)
}
// HandleEstimateGas estimates gas for a transaction
func HandleEstimateGas(c echo.Context) error {
var req BroadcastRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request",
})
}
// Check if gasless eligible
if isGaslessEligible(req.Message) {
return c.JSON(http.StatusOK, map[string]any{
"gas_estimate": 0,
"gasless": true,
"fee": "0usnr",
})
}
// TODO: Implement actual gas estimation
// For now, return default estimate
return c.JSON(http.StatusOK, map[string]any{
"gas_estimate": 100000,
"gasless": false,
"fee": "100usnr",
})
}
// Helper functions
// isGaslessEligible checks if a message is eligible for gasless broadcasting
func isGaslessEligible(message any) bool {
// Check message type
msgMap, ok := message.(map[string]any)
if !ok {
return false
}
msgType, ok := msgMap["@type"].(string)
if !ok {
return false
}
// WebAuthn registration and vault creation are gasless
gaslessTypes := []string{
"/sonr.did.v1.MsgRegisterWebAuthnCredential",
"/sonr.vault.v1.MsgCreateVault",
"/sonr.did.v1.MsgCreateDIDDocument",
}
for _, t := range gaslessTypes {
if msgType == t {
return true
}
}
return false
}
// deriveAddressFromDID derives a blockchain address from a DID
func deriveAddressFromDID(did string) string {
// TODO: Implement actual address derivation
// For now, return a placeholder address
return "sonr1placeholder" + did[len(did)-10:]
}
// generateTxHash generates a mock transaction hash
func generateTxHash() string {
// TODO: Replace with actual tx hash from broadcast
return fmt.Sprintf("%X", time.Now().UnixNano())
}
// CreateWebAuthnBroadcastHandler creates a handler that broadcasts WebAuthn credentials
func CreateWebAuthnBroadcastHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Get WebAuthn credential from context
credential, ok := c.Get("webauthn_credential").(*WebAuthnCredential)
if !ok {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No WebAuthn credential found",
})
}
// Broadcast to blockchain
response, err := BroadcastWebAuthnRegistration(credential, true)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, response)
}
}
// CreateVaultBroadcastHandler creates a handler that broadcasts vault creation
func CreateVaultBroadcastHandler() echo.HandlerFunc {
return func(c echo.Context) error {
// Get user DID from context
userDID, ok := c.Get("user_did").(string)
if !ok {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "User DID not found",
})
}
// Get vault config from request
var vaultConfig map[string]any
if err := c.Bind(&vaultConfig); err != nil {
// Use default config
vaultConfig = map[string]any{
"type": "standard",
"encryption": "AES256",
}
}
// Broadcast vault creation
response, err := BroadcastVaultCreation(userDID, vaultConfig)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, response)
}
}
-33
View File
@@ -1,33 +0,0 @@
package handlers
import "errors"
// Common errors for OAuth2 and UCAN handling
var (
// Client errors
ErrClientNotFound = errors.New("client not found")
ErrInvalidClientCredentials = errors.New("invalid client credentials")
// Token errors
ErrTokenNotFound = errors.New("token not found")
ErrTokenExpired = errors.New("token has expired")
ErrInvalidToken = errors.New("invalid token")
ErrTokenRevoked = errors.New("token has been revoked")
// Authorization errors
ErrUnauthorized = errors.New("unauthorized")
ErrInsufficientScope = errors.New("insufficient scope")
ErrInvalidScope = errors.New("invalid scope")
ErrScopeNotAllowed = errors.New("scope not allowed for client")
// UCAN errors
ErrInvalidAttenuation = errors.New("invalid attenuation")
ErrInvalidDelegation = errors.New("invalid delegation")
ErrBrokenChain = errors.New("broken delegation chain")
ErrPrivilegeEscalation = errors.New("privilege escalation attempted")
// OIDC errors
ErrInvalidRedirectURI = errors.New("invalid redirect URI")
ErrInvalidResponseType = errors.New("invalid response type")
ErrInvalidGrantType = errors.New("invalid grant type")
)
-27
View File
@@ -1,27 +0,0 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetQueueFromPriority(t *testing.T) {
tests := []struct {
priority string
expected string
}{
{"critical", "critical"},
{"high", "critical"},
{"low", "low"},
{"", "default"},
{"unknown", "default"},
}
for _, tt := range tests {
t.Run(tt.priority, func(t *testing.T) {
result := GetQueueFromPriority(tt.priority)
assert.Equal(t, tt.expected, result)
})
}
}
-201
View File
@@ -1,201 +0,0 @@
package handlers
import (
"context"
"net/http"
"sync"
"time"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/types/ipfs"
)
// HealthStatus represents the health status of the service
type HealthStatus struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Uptime string `json:"uptime"`
Dependencies map[string]string `json:"dependencies"`
}
// HealthChecker manages health and readiness checks
type HealthChecker struct {
startTime time.Time
redisClient *asynq.Client
ipfsClient ipfs.IPFSClient
ready bool
readyMu sync.RWMutex
redisHealthy bool
ipfsHealthy bool
}
// NewHealthChecker creates a new health checker
func NewHealthChecker(redisClient *asynq.Client, ipfsClient ipfs.IPFSClient) *HealthChecker {
hc := &HealthChecker{
startTime: time.Now(),
redisClient: redisClient,
ipfsClient: ipfsClient,
ready: false,
}
// Start background health checks
go hc.startHealthChecks()
return hc
}
// startHealthChecks runs periodic health checks
func (hc *HealthChecker) startHealthChecks() {
// Initial startup delay
time.Sleep(3 * time.Second)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
// Run initial check
hc.checkDependencies()
for range ticker.C {
hc.checkDependencies()
}
}
// checkDependencies checks all service dependencies
func (hc *HealthChecker) checkDependencies() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Check Redis connectivity
hc.redisHealthy = hc.checkRedis(ctx)
// Check IPFS connectivity (optional)
hc.ipfsHealthy = hc.checkIPFS(ctx)
// Update readiness based on critical dependencies
hc.readyMu.Lock()
hc.ready = hc.redisHealthy // Redis is required
hc.readyMu.Unlock()
}
// checkRedis verifies Redis connectivity
func (hc *HealthChecker) checkRedis(ctx context.Context) bool {
if hc.redisClient == nil {
return false
}
// Try to ping Redis by enqueuing a test task
testTask := asynq.NewTask("health:check", nil)
_, err := hc.redisClient.EnqueueContext(ctx, testTask,
asynq.Queue("health"),
asynq.MaxRetry(0),
asynq.Retention(1*time.Second)) // Auto-delete after 1 second
if err != nil {
return false
}
return true
}
// checkIPFS verifies IPFS connectivity
func (hc *HealthChecker) checkIPFS(ctx context.Context) bool {
if hc.ipfsClient == nil {
return true // IPFS is optional
}
// Check IPFS connectivity by getting version
ch := make(chan bool, 1)
go func() {
// Try a simple operation to check connectivity
if hc.ipfsClient != nil {
// IPFS client exists, assume healthy for now
// Real check would depend on actual IPFS client implementation
ch <- true
} else {
ch <- false
}
}()
select {
case result := <-ch:
return result
case <-ctx.Done():
return false
}
}
// IsReady returns whether the service is ready to handle requests
func (hc *HealthChecker) IsReady() bool {
hc.readyMu.RLock()
defer hc.readyMu.RUnlock()
return hc.ready
}
// GetStatus returns the current health status
func (hc *HealthChecker) GetStatus() HealthStatus {
uptime := time.Since(hc.startTime)
deps := make(map[string]string)
if hc.redisHealthy {
deps["redis"] = "healthy"
} else {
deps["redis"] = "unhealthy"
}
if hc.ipfsClient != nil {
if hc.ipfsHealthy {
deps["ipfs"] = "healthy"
} else {
deps["ipfs"] = "unhealthy"
}
} else {
deps["ipfs"] = "not_configured"
}
status := "healthy"
if !hc.ready {
status = "unhealthy"
}
return HealthStatus{
Status: status,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Uptime: uptime.String(),
Dependencies: deps,
}
}
// Global health checker instance
var healthChecker *HealthChecker
// InitHealthChecker initializes the global health checker
func InitHealthChecker(redisClient *asynq.Client, ipfsClient ipfs.IPFSClient) {
healthChecker = NewHealthChecker(redisClient, ipfsClient)
}
// HealthCheckHandler returns health status (liveness probe)
func HealthCheckHandler(c echo.Context) error {
if healthChecker == nil {
return c.JSON(http.StatusOK, map[string]string{"status": "starting"})
}
status := healthChecker.GetStatus()
if status.Status == "healthy" {
return c.JSON(http.StatusOK, status)
}
return c.JSON(http.StatusServiceUnavailable, status)
}
// ReadinessHandler returns readiness status (readiness probe)
func ReadinessHandler(c echo.Context) error {
if healthChecker == nil || !healthChecker.IsReady() {
return c.JSON(http.StatusServiceUnavailable, map[string]string{
"ready": "false",
"reason": "service not ready",
})
}
return c.JSON(http.StatusOK, map[string]string{
"ready": "true",
})
}
-422
View File
@@ -1,422 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/url"
"strings"
"sync"
"time"
)
const (
// ClientTypePublic represents a public OAuth2 client
ClientTypePublic = "public"
// ClientTypeConfidential represents a confidential OAuth2 client
ClientTypeConfidential = "confidential"
)
// ClientRegistry manages OAuth2 client registrations
type ClientRegistry struct {
mu sync.RWMutex
clients map[string]*OAuth2Client
}
// NewClientRegistry creates a new client registry
func NewClientRegistry() *ClientRegistry {
registry := &ClientRegistry{
clients: make(map[string]*OAuth2Client),
}
// Initialize with default clients for development
registry.initializeDefaultClients()
return registry
}
// initializeDefaultClients adds default clients for development/testing
func (r *ClientRegistry) initializeDefaultClients() {
// Development public client (e.g., SPA)
_ = r.RegisterClient(&OAuth2Client{
ClientID: "sonr-web-app",
ClientType: ClientTypePublic,
RedirectURIs: []string{
"http://localhost:3000/callback",
"http://localhost:3001/callback",
},
AllowedScopes: []string{
"openid",
"profile",
"vault:read",
"vault:write",
"service:manage",
},
AllowedGrants: []string{"authorization_code", "refresh_token"},
TokenLifetime: time.Hour,
RequirePKCE: true,
TrustedClient: true,
RequiresConsent: false,
Metadata: map[string]string{
"name": "Sonr Web Application",
"description": "Official Sonr web application",
"logo_uri": "https://sonr.io/logo.png",
"client_uri": "https://app.sonr.io",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
// Development confidential client (e.g., backend service)
_ = r.RegisterClient(&OAuth2Client{
ClientID: "sonr-backend-service",
ClientSecret: "development-secret-change-in-production",
ClientType: ClientTypeConfidential,
RedirectURIs: []string{"http://localhost:8081/callback"},
AllowedScopes: []string{"openid", "profile", "vault:admin", "service:manage"},
AllowedGrants: []string{"authorization_code", "refresh_token", "client_credentials"},
TokenLifetime: time.Hour * 2,
RequirePKCE: false,
TrustedClient: true,
RequiresConsent: false,
Metadata: map[string]string{
"name": "Sonr Backend Service",
"description": "Backend service for Sonr ecosystem",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
// Example third-party client
_ = r.RegisterClient(&OAuth2Client{
ClientID: "example-third-party",
ClientSecret: "third-party-secret",
ClientType: ClientTypeConfidential,
RedirectURIs: []string{"https://example.com/oauth/callback"},
AllowedScopes: []string{"openid", "profile", "vault:read"},
AllowedGrants: []string{"authorization_code", "refresh_token"},
TokenLifetime: time.Hour,
RequirePKCE: true,
TrustedClient: false,
RequiresConsent: true,
Metadata: map[string]string{
"name": "Example Third Party App",
"description": "Example integration partner",
"logo_uri": "https://example.com/logo.png",
"client_uri": "https://example.com",
"policy_uri": "https://example.com/privacy",
"tos_uri": "https://example.com/terms",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
}
// RegisterClient registers a new OAuth2 client
func (r *ClientRegistry) RegisterClient(client *OAuth2Client) error {
r.mu.Lock()
defer r.mu.Unlock()
// Validate client
if err := r.validateClient(client); err != nil {
return err
}
// Generate client ID if not provided
if client.ClientID == "" {
client.ClientID = generateOAuth2ClientID()
}
// Generate client secret for confidential clients
if client.ClientType == ClientTypeConfidential && client.ClientSecret == "" {
client.ClientSecret = generateClientSecret()
}
// Set timestamps
if client.CreatedAt.IsZero() {
client.CreatedAt = time.Now()
}
client.UpdatedAt = time.Now()
// Store client
r.clients[client.ClientID] = client
return nil
}
// GetClient retrieves a client by ID
func (r *ClientRegistry) GetClient(clientID string) (*OAuth2Client, error) {
r.mu.RLock()
defer r.mu.RUnlock()
client, exists := r.clients[clientID]
if !exists {
return nil, fmt.Errorf("client not found: %s", clientID)
}
return client, nil
}
// UpdateClient updates an existing client
func (r *ClientRegistry) UpdateClient(clientID string, updates *OAuth2Client) error {
r.mu.Lock()
defer r.mu.Unlock()
client, exists := r.clients[clientID]
if !exists {
return fmt.Errorf("client not found: %s", clientID)
}
// Update allowed fields
if len(updates.RedirectURIs) > 0 {
client.RedirectURIs = updates.RedirectURIs
}
if len(updates.AllowedScopes) > 0 {
client.AllowedScopes = updates.AllowedScopes
}
if len(updates.AllowedGrants) > 0 {
client.AllowedGrants = updates.AllowedGrants
}
if updates.TokenLifetime > 0 {
client.TokenLifetime = updates.TokenLifetime
}
if updates.Metadata != nil {
client.Metadata = updates.Metadata
}
client.RequirePKCE = updates.RequirePKCE
client.RequiresConsent = updates.RequiresConsent
client.UpdatedAt = time.Now()
return nil
}
// DeleteClient removes a client from the registry
func (r *ClientRegistry) DeleteClient(clientID string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clients[clientID]; !exists {
return fmt.Errorf("client not found: %s", clientID)
}
delete(r.clients, clientID)
return nil
}
// ListClients returns all registered clients
func (r *ClientRegistry) ListClients() []*OAuth2Client {
r.mu.RLock()
defer r.mu.RUnlock()
clients := make([]*OAuth2Client, 0, len(r.clients))
for _, client := range r.clients {
clients = append(clients, client)
}
return clients
}
// ValidateRedirectURI checks if a redirect URI is valid for the client
func (c *OAuth2Client) ValidateRedirectURI(redirectURI string) bool {
if redirectURI == "" {
return false
}
// Parse the redirect URI
parsedURI, err := url.Parse(redirectURI)
if err != nil {
return false
}
// Check against registered redirect URIs
for _, registeredURI := range c.RedirectURIs {
registeredParsed, err := url.Parse(registeredURI)
if err != nil {
continue
}
// For public clients, allow localhost with any port
if c.ClientType == ClientTypePublic && registeredParsed.Hostname() == "localhost" &&
parsedURI.Hostname() == "localhost" {
if registeredParsed.Path == parsedURI.Path {
return true
}
}
// Exact match for other cases
if registeredURI == redirectURI {
return true
}
// Allow subdomain matching for trusted clients
if c.TrustedClient && matchesWithSubdomain(registeredParsed, parsedURI) {
return true
}
}
return false
}
// ValidateScopes checks if the requested scopes are allowed for the client
func (c *OAuth2Client) ValidateScopes(requestedScopes []string) bool {
if len(requestedScopes) == 0 {
return true // No scopes requested is valid
}
for _, scope := range requestedScopes {
if !c.hasScope(scope) {
return false
}
}
return true
}
// hasScope checks if a client has a specific scope
func (c *OAuth2Client) hasScope(scope string) bool {
for _, allowedScope := range c.AllowedScopes {
if allowedScope == scope {
return true
}
// Check for hierarchical scopes (e.g., vault:admin includes vault:read)
if isHierarchicalScope(allowedScope, scope) {
return true
}
}
return false
}
// HasGrantType checks if a client supports a specific grant type
func (c *OAuth2Client) HasGrantType(grantType string) bool {
for _, allowed := range c.AllowedGrants {
if allowed == grantType {
return true
}
}
return false
}
// validateClient validates client configuration
func (r *ClientRegistry) validateClient(client *OAuth2Client) error {
// Validate client type
if client.ClientType != ClientTypePublic && client.ClientType != ClientTypeConfidential {
return fmt.Errorf("invalid client type: %s", client.ClientType)
}
// Validate redirect URIs
if len(client.RedirectURIs) == 0 {
return fmt.Errorf("at least one redirect URI is required")
}
for _, uri := range client.RedirectURIs {
if _, err := url.Parse(uri); err != nil {
return fmt.Errorf("invalid redirect URI: %s", uri)
}
}
// Validate grant types
if len(client.AllowedGrants) == 0 {
client.AllowedGrants = []string{"authorization_code"}
}
validGrants := map[string]bool{
"authorization_code": true,
"implicit": true,
"refresh_token": true,
"client_credentials": true,
"password": true,
"urn:ietf:params:oauth:grant-type:device_code": true,
}
for _, grant := range client.AllowedGrants {
if !validGrants[grant] {
return fmt.Errorf("invalid grant type: %s", grant)
}
}
// Client credentials grant requires confidential client
if contains(client.AllowedGrants, "client_credentials") &&
client.ClientType != ClientTypeConfidential {
return fmt.Errorf("client_credentials grant requires confidential client")
}
// Public clients should use PKCE
if client.ClientType == ClientTypePublic && !client.RequirePKCE {
// Log warning but don't fail
fmt.Printf("Warning: Public client %s should use PKCE\n", client.ClientID)
}
return nil
}
// Helper functions
func generateOAuth2ClientID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return fmt.Sprintf("client_%s", base64.RawURLEncoding.EncodeToString(bytes))
}
func generateClientSecret() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func matchesWithSubdomain(registered, requested *url.URL) bool {
if registered.Scheme != requested.Scheme {
return false
}
if registered.Path != requested.Path {
return false
}
// Check if requested hostname is a subdomain of registered
registeredHost := registered.Hostname()
requestedHost := requested.Hostname()
if registeredHost == requestedHost {
return true
}
// Check subdomain match (e.g., *.example.com matches sub.example.com)
if strings.HasPrefix(registeredHost, "*.") {
domain := strings.TrimPrefix(registeredHost, "*.")
return strings.HasSuffix(requestedHost, domain)
}
return false
}
func isHierarchicalScope(allowed, requested string) bool {
// Define scope hierarchy
hierarchy := map[string][]string{
"vault:admin": {"vault:write", "vault:read", "vault:sign"},
"vault:write": {"vault:read"},
"service:manage": {"service:read", "service:write"},
"did:write": {"did:read"},
}
childScopes, exists := hierarchy[allowed]
if !exists {
return false
}
for _, child := range childScopes {
if child == requested {
return true
}
// Recursive check for nested hierarchies
if isHierarchicalScope(child, requested) {
return true
}
}
return false
}
-323
View File
@@ -1,323 +0,0 @@
package handlers
import (
"encoding/json"
"fmt"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// UCANDelegator handles UCAN token generation for OAuth flows
type UCANDelegator struct {
scopeMapper *ScopeMapper
signer UCANSigner
}
// UCANSigner interface for signing UCAN tokens
type UCANSigner interface {
Sign(token *ucan.Token) (string, error)
GetIssuerDID() string
}
// NewUCANDelegator creates a new UCAN delegator
func NewUCANDelegator(signer UCANSigner) *UCANDelegator {
if signer == nil {
// Use blockchain signer by default
signer, _ = NewBlockchainUCANSigner(nil, "did:sonr:oauth-provider")
}
return &UCANDelegator{
scopeMapper: NewScopeMapper(),
signer: signer,
}
}
// CreateDelegation creates a UCAN token for user-to-client delegation
func (d *UCANDelegator) CreateDelegation(
userDID string,
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Build resource context for the user
resourceContext := d.buildResourceContext(userDID)
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, userDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: userDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "user_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateServiceDelegation creates a UCAN token for service-to-service delegation
func (d *UCANDelegator) CreateServiceDelegation(
clientID string,
scopes []string,
expiresAt time.Time,
) (*ucan.Token, error) {
// Service delegations use the OAuth provider as issuer
issuerDID := d.signer.GetIssuerDID()
// Build resource context for service
resourceContext := map[string]string{
"service_id": clientID,
"type": "service",
}
// Map OAuth scopes to UCAN attenuations
attenuations := d.scopeMapper.MapToUCAN(scopes, issuerDID, clientID, resourceContext)
if len(attenuations) == 0 {
return nil, fmt.Errorf("no valid attenuations for scopes: %v", scopes)
}
// Create UCAN token
token := &ucan.Token{
Issuer: issuerDID,
Audience: clientID,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, "service_delegation"),
},
},
}
// Sign the token
signedToken, err := d.signToken(token)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
token.Raw = signedToken
return token, nil
}
// CreateDelegationChain creates a chain of UCAN tokens for complex delegations
func (d *UCANDelegator) CreateDelegationChain(
userDID string,
intermediaries []string,
finalAudience string,
scopes []string,
expiresAt time.Time,
) ([]*ucan.Token, error) {
chain := make([]*ucan.Token, 0, len(intermediaries)+1)
currentIssuer := userDID
proofs := []ucan.Proof{}
// Create delegation for each intermediary
for _, intermediary := range intermediaries {
token, err := d.createIntermediateDelegation(
currentIssuer,
intermediary,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create intermediate delegation: %w", err)
}
chain = append(chain, token)
proofs = append(proofs, ucan.Proof(token.Raw))
currentIssuer = intermediary
}
// Create final delegation
finalToken, err := d.createFinalDelegation(
currentIssuer,
finalAudience,
scopes,
expiresAt,
proofs,
)
if err != nil {
return nil, fmt.Errorf("failed to create final delegation: %w", err)
}
chain = append(chain, finalToken)
return chain, nil
}
// RevokeDelegation revokes a UCAN delegation
func (d *UCANDelegator) RevokeDelegation(tokenID string) error {
// TODO: Implement delegation revocation
// This would typically involve:
// 1. Adding the token to a revocation list
// 2. Publishing revocation to a public ledger or database
// 3. Notifying relevant parties
return fmt.Errorf("delegation revocation not yet implemented")
}
// ValidateDelegation validates a UCAN token delegation
func (d *UCANDelegator) ValidateDelegation(token *ucan.Token, requiredScopes []string) error {
// Check expiration
if time.Now().Unix() > token.ExpiresAt {
return fmt.Errorf("token expired")
}
// Check not before
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
return fmt.Errorf("token not yet valid")
}
// Verify the token has required capabilities
for _, scope := range requiredScopes {
if !d.tokenGrantsScope(token, scope) {
return fmt.Errorf("token does not grant required scope: %s", scope)
}
}
// TODO: Verify signature
// TODO: Verify delegation chain if proofs exist
return nil
}
// Private helper methods
func (d *UCANDelegator) buildResourceContext(userDID string) map[string]string {
// TODO: Fetch actual resource information for the user
return map[string]string{
"vault_address": fmt.Sprintf("vault:%s", userDID),
"enclave_cid": fmt.Sprintf("cid:%s", userDID),
"dwn_id": fmt.Sprintf("dwn:%s", userDID),
"service_id": fmt.Sprintf("service:%s", userDID),
}
}
func (d *UCANDelegator) createOAuthFact(scopes []string, delegationType string) json.RawMessage {
fact := map[string]any{
"oauth_scopes": scopes,
"delegation_type": delegationType,
"issued_at": time.Now().Unix(),
"oauth_version": "2.0",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
func (d *UCANDelegator) signToken(token *ucan.Token) (string, error) {
if d.signer == nil {
return "", fmt.Errorf("no signer configured")
}
return d.signer.Sign(token)
}
func (d *UCANDelegator) createIntermediateDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"intermediate_delegation",
)
}
func (d *UCANDelegator) createFinalDelegation(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
) (*ucan.Token, error) {
return d.createDelegationWithType(
issuer,
audience,
scopes,
expiresAt,
proofs,
"final_delegation",
)
}
// createDelegationWithType creates a delegation token with a specific type
func (d *UCANDelegator) createDelegationWithType(
issuer, audience string,
scopes []string,
expiresAt time.Time,
proofs []ucan.Proof,
delegationType string,
) (*ucan.Token, error) {
resourceContext := d.buildResourceContext(issuer)
attenuations := d.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
token := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: expiresAt.Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: d.createOAuthFact(scopes, delegationType),
},
},
}
signedToken, err := d.signToken(token)
if err != nil {
return nil, err
}
token.Raw = signedToken
return token, nil
}
func (d *UCANDelegator) tokenGrantsScope(token *ucan.Token, scope string) bool {
// Get the scope definition
scopeDef, exists := d.scopeMapper.GetScope(scope)
if !exists {
return false
}
// Check if any attenuation grants the required capabilities
for _, attenuation := range token.Attenuations {
if attenuation.Capability.Grants(scopeDef.UCANActions) {
// Also check resource type matches
if d.resourceMatches(attenuation.Resource, scopeDef.ResourceType) {
return true
}
}
}
return false
}
func (d *UCANDelegator) resourceMatches(resource ucan.Resource, resourceType string) bool {
// Simple scheme matching for now
return resource.GetScheme() == resourceType || resource.GetScheme() == "*"
}
-923
View File
@@ -1,923 +0,0 @@
package handlers
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/crypto/ucan"
)
// OAuth2Provider extends OIDCProvider with full OAuth2 capabilities
type OAuth2Provider struct {
*OIDCProvider
clientRegistry *ClientRegistry
scopeMapper *ScopeMapper
ucanDelegator *UCANDelegator
authCodeStore *AuthCodeStore
accessTokenStore *AccessTokenStore
refreshTokenStore *RefreshTokenStore
consentStore *ConsentStore
config *OAuth2Config
}
// AuthCodeStore manages authorization codes
type AuthCodeStore struct {
mu sync.RWMutex
codes map[string]*OAuth2AuthorizationCode
}
// AccessTokenStore manages access tokens
type AccessTokenStore struct {
mu sync.RWMutex
tokens map[string]*OAuth2AccessToken
}
// RefreshTokenStore manages refresh tokens
type RefreshTokenStore struct {
mu sync.RWMutex
tokens map[string]*OAuth2RefreshToken
}
// ConsentStore manages user consent records
type ConsentStore struct {
mu sync.RWMutex
consents map[string]*UserConsent // key: userDID:clientID
}
// UserConsent represents stored user consent
type UserConsent struct {
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
ApprovedScopes []string `json:"approved_scopes"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
}
var oauth2Provider *OAuth2Provider
// InitializeOAuth2Provider initializes the OAuth2 provider
func InitializeOAuth2Provider() {
oauth2Provider = &OAuth2Provider{
OIDCProvider: oidcProvider,
clientRegistry: NewClientRegistry(),
scopeMapper: NewScopeMapper(),
ucanDelegator: NewUCANDelegator(nil),
authCodeStore: &AuthCodeStore{codes: make(map[string]*OAuth2AuthorizationCode)},
accessTokenStore: &AccessTokenStore{tokens: make(map[string]*OAuth2AccessToken)},
refreshTokenStore: &RefreshTokenStore{tokens: make(map[string]*OAuth2RefreshToken)},
consentStore: &ConsentStore{consents: make(map[string]*UserConsent)},
config: getDefaultOAuth2Config(),
}
// Start cleanup goroutine for expired tokens
go oauth2Provider.cleanupExpiredTokens()
}
// GetOAuth2Discovery returns OAuth2 discovery configuration
func GetOAuth2Discovery(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
return c.JSON(http.StatusOK, oauth2Provider.config)
}
// HandleOAuth2Authorize handles OAuth2 authorization requests
func HandleOAuth2Authorize(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
req := &OAuth2AuthorizationRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
State: c.QueryParam("state"),
CodeChallenge: c.QueryParam("code_challenge"),
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
Nonce: c.QueryParam("nonce"),
Prompt: c.QueryParam("prompt"),
LoginHint: c.QueryParam("login_hint"),
}
// Validate client
client, err := oauth2Provider.clientRegistry.GetClient(req.ClientID)
if err != nil {
return oauth2Error(c, "invalid_client", "Unknown client", req.State)
}
// Validate redirect URI
if !client.ValidateRedirectURI(req.RedirectURI) {
return oauth2Error(c, "invalid_request", "Invalid redirect URI", req.State)
}
// Validate response type
if !isValidResponseType(req.ResponseType) {
return redirectError(
c,
req.RedirectURI,
"unsupported_response_type",
"Response type not supported",
req.State,
)
}
// Validate scopes
requestedScopes := parseScopes(req.Scope)
if !client.ValidateScopes(requestedScopes) {
return redirectError(
c,
req.RedirectURI,
"invalid_scope",
"Requested scope not allowed",
req.State,
)
}
// Validate PKCE for public clients
if client.ClientType == "public" && client.RequirePKCE {
if req.CodeChallenge == "" {
return redirectError(
c,
req.RedirectURI,
"invalid_request",
"PKCE required for public clients",
req.State,
)
}
if req.CodeChallengeMethod != PKCEMethodS256 {
return redirectError(
c,
req.RedirectURI,
"invalid_request",
"Only S256 PKCE method supported",
req.State,
)
}
}
// Check authentication
userDID := c.Get("user_did")
if userDID == nil {
// Store authorization request and redirect to authentication
sessionID := generateSessionID()
// TODO: Store auth request in session store
authURL := fmt.Sprintf(
"/auth/login?session_id=%s&return_to=%s",
sessionID,
c.Request().URL.String(),
)
return c.Redirect(http.StatusFound, authURL)
}
// Check consent
if client.RequiresConsent &&
!oauth2Provider.hasValidConsent(userDID.(string), req.ClientID, requestedScopes) {
// Render consent page
return renderOAuth2ConsentPage(c, req, client)
}
// Generate authorization code
code := generateSecureToken(32)
authCode := &OAuth2AuthorizationCode{
Code: code,
ClientID: req.ClientID,
UserDID: userDID.(string),
RedirectURI: req.RedirectURI,
Scopes: requestedScopes,
State: req.State,
Nonce: req.Nonce,
CodeChallenge: req.CodeChallenge,
CodeChallengeMethod: req.CodeChallengeMethod,
ExpiresAt: time.Now().Add(10 * time.Minute),
UCANContext: oauth2Provider.buildUCANContext(userDID.(string)),
}
// Store authorization code
oauth2Provider.authCodeStore.Store(authCode)
// Build redirect URL
redirectURL := buildAuthorizationRedirect(req.RedirectURI, code, req.State)
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleOAuth2Token handles OAuth2 token requests
func HandleOAuth2Token(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2TokenRequest
if err := c.Bind(&req); err != nil {
return oauth2TokenError(c, "invalid_request", "Invalid token request")
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &req)
if err != nil {
return oauth2TokenError(c, "invalid_client", "Client authentication failed")
}
// Handle grant type
switch req.GrantType {
case "authorization_code":
return oauth2Provider.handleAuthorizationCodeGrant(c, client, &req)
case "refresh_token":
return oauth2Provider.handleRefreshTokenGrant(c, client, &req)
case "client_credentials":
return oauth2Provider.handleClientCredentialsGrant(c, client, &req)
default:
return oauth2TokenError(c, "unsupported_grant_type", "Grant type not supported")
}
}
// HandleOAuth2Introspection handles token introspection requests
func HandleOAuth2Introspection(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2IntrospectionRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, &OAuth2IntrospectionResponse{Active: false})
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
})
if err != nil {
return c.JSON(http.StatusUnauthorized, &OAuth2IntrospectionResponse{Active: false})
}
// Introspect token
response := oauth2Provider.introspectToken(req.Token, req.TokenTypeHint, client)
return c.JSON(http.StatusOK, response)
}
// HandleOAuth2Revocation handles token revocation requests
func HandleOAuth2Revocation(c echo.Context) error {
if oauth2Provider == nil {
InitializeOAuth2Provider()
}
var req OAuth2RevocationRequest
if err := c.Bind(&req); err != nil {
return c.NoContent(http.StatusBadRequest)
}
// Authenticate client
client, err := oauth2Provider.authenticateClient(c, &OAuth2TokenRequest{
ClientID: req.ClientID,
ClientSecret: req.ClientSecret,
})
if err != nil {
return c.NoContent(http.StatusUnauthorized)
}
// Revoke token
oauth2Provider.revokeToken(req.Token, req.TokenTypeHint, client)
return c.NoContent(http.StatusOK)
}
// Private methods
func (p *OAuth2Provider) handleAuthorizationCodeGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Retrieve authorization code
authCode := p.authCodeStore.Exchange(req.Code)
if authCode == nil {
return oauth2TokenError(c, "invalid_grant", "Invalid authorization code")
}
// Validate code hasn't expired
if time.Now().After(authCode.ExpiresAt) {
return oauth2TokenError(c, "invalid_grant", "Authorization code expired")
}
// Validate client
if authCode.ClientID != client.ClientID {
return oauth2TokenError(c, "invalid_grant", "Code was issued to different client")
}
// Validate redirect URI
if authCode.RedirectURI != req.RedirectURI {
return oauth2TokenError(c, "invalid_grant", "Redirect URI mismatch")
}
// Validate PKCE if present
if authCode.CodeChallenge != "" {
if !p.validatePKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
return oauth2TokenError(c, "invalid_grant", "Invalid PKCE verifier")
}
}
// Create UCAN delegation
ucanToken, err := p.ucanDelegator.CreateDelegation(
authCode.UserDID,
client.ClientID,
authCode.Scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate tokens
accessToken := p.generateAccessToken(authCode, ucanToken)
refreshToken := p.generateRefreshToken(authCode)
// Store tokens
p.accessTokenStore.Store(accessToken)
p.refreshTokenStore.Store(refreshToken)
// Generate ID token if openid scope present
var idToken string
if contains(authCode.Scopes, "openid") {
idToken, _ = generateIDToken(authCode.UserDID, client.ClientID, authCode.Nonce)
}
// Return token response
response := &OAuth2TokenResponse{
AccessToken: accessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: refreshToken.Token,
Scope: strings.Join(authCode.Scopes, " "),
IDToken: idToken,
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) handleRefreshTokenGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Retrieve refresh token
oldRefreshToken := p.refreshTokenStore.Get(req.RefreshToken)
if oldRefreshToken == nil {
return oauth2TokenError(c, "invalid_grant", "Invalid refresh token")
}
// Validate client
if oldRefreshToken.ClientID != client.ClientID {
return oauth2TokenError(c, "invalid_grant", "Token was issued to different client")
}
// Validate expiration
if time.Now().After(oldRefreshToken.ExpiresAt) {
return oauth2TokenError(c, "invalid_grant", "Refresh token expired")
}
// Rotate refresh token
p.refreshTokenStore.Revoke(req.RefreshToken)
// Create new UCAN delegation
ucanToken, err := p.ucanDelegator.CreateDelegation(
oldRefreshToken.UserDID,
client.ClientID,
oldRefreshToken.Scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate new tokens
newAccessToken := &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: oldRefreshToken.UserDID,
ClientID: client.ClientID,
Scopes: oldRefreshToken.Scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
}
newRefreshToken := &OAuth2RefreshToken{
Token: generateSecureToken(32),
AccessToken: newAccessToken.Token,
ClientID: client.ClientID,
UserDID: oldRefreshToken.UserDID,
Scopes: oldRefreshToken.Scopes,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
IssuedAt: time.Now(),
RotationCount: oldRefreshToken.RotationCount + 1,
}
// Store new tokens
p.accessTokenStore.Store(newAccessToken)
p.refreshTokenStore.Store(newRefreshToken)
// Return response
response := &OAuth2TokenResponse{
AccessToken: newAccessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: newRefreshToken.Token,
Scope: strings.Join(newAccessToken.Scopes, " "),
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) handleClientCredentialsGrant(
c echo.Context,
client *OAuth2Client,
req *OAuth2TokenRequest,
) error {
// Client credentials grant is only for confidential clients
if client.ClientType != "confidential" {
return oauth2TokenError(
c,
"unauthorized_client",
"Client type not authorized for this grant",
)
}
// Parse requested scopes
scopes := parseScopes(req.Scope)
if !client.ValidateScopes(scopes) {
return oauth2TokenError(c, "invalid_scope", "Requested scope not allowed")
}
// Create service-to-service UCAN token
ucanToken, err := p.ucanDelegator.CreateServiceDelegation(
client.ClientID,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return oauth2TokenError(c, "server_error", "Failed to create delegation")
}
// Generate access token
accessToken := &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: "", // No user for client credentials
ClientID: client.ClientID,
Scopes: scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
TokenType: "client_credentials",
}
// Store token
p.accessTokenStore.Store(accessToken)
// Return response
response := &OAuth2TokenResponse{
AccessToken: accessToken.Token,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
UCANToken: ucanToken.Raw,
}
return c.JSON(http.StatusOK, response)
}
func (p *OAuth2Provider) authenticateClient(
c echo.Context,
req *OAuth2TokenRequest,
) (*OAuth2Client, error) {
// Try Basic Auth first
if username, password, ok := c.Request().BasicAuth(); ok {
client, err := p.clientRegistry.GetClient(username)
if err != nil {
return nil, err
}
if client.ClientType == "confidential" &&
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(password)) == 1 {
return client, nil
}
return nil, fmt.Errorf("invalid client credentials")
}
// Try client_secret_post
if req.ClientID != "" && req.ClientSecret != "" {
client, err := p.clientRegistry.GetClient(req.ClientID)
if err != nil {
return nil, err
}
if client.ClientType == "confidential" &&
subtle.ConstantTimeCompare([]byte(client.ClientSecret), []byte(req.ClientSecret)) == 1 {
return client, nil
}
return nil, fmt.Errorf("invalid client credentials")
}
// Try client_assertion (JWT)
if req.ClientAssertion != "" &&
req.ClientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
// TODO: Implement JWT client assertion validation
return nil, fmt.Errorf("JWT client assertion not yet implemented")
}
// Public client (no authentication)
if req.ClientID != "" {
client, err := p.clientRegistry.GetClient(req.ClientID)
if err != nil {
return nil, err
}
if client.ClientType == "public" {
return client, nil
}
}
return nil, fmt.Errorf("client authentication required")
}
func (p *OAuth2Provider) validatePKCE(verifier, challenge, method string) bool {
if method == "" {
method = PKCEMethodPlain
}
computed := computePKCEChallenge(verifier, method)
return subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) == 1
}
func (p *OAuth2Provider) hasValidConsent(userDID, clientID string, scopes []string) bool {
p.consentStore.mu.RLock()
defer p.consentStore.mu.RUnlock()
key := fmt.Sprintf("%s:%s", userDID, clientID)
consent, exists := p.consentStore.consents[key]
if !exists {
return false
}
// Check expiration
if time.Now().After(consent.ExpiresAt) {
return false
}
// Check all requested scopes are approved
for _, scope := range scopes {
if !contains(consent.ApprovedScopes, scope) {
return false
}
}
return true
}
func (p *OAuth2Provider) buildUCANContext(userDID string) *UCANAuthContext {
// TODO: Fetch actual vault and DID document data
return &UCANAuthContext{
VaultAddress: fmt.Sprintf("vault_%s", userDID),
EnclaveDataCID: fmt.Sprintf("cid_%s", userDID),
Capabilities: []string{"read", "write", "sign"},
}
}
func (p *OAuth2Provider) generateAccessToken(
authCode *OAuth2AuthorizationCode,
ucanToken *ucan.Token,
) *OAuth2AccessToken {
return &OAuth2AccessToken{
Token: generateSecureToken(32),
UserDID: authCode.UserDID,
ClientID: authCode.ClientID,
Scopes: authCode.Scopes,
ExpiresAt: time.Now().Add(time.Hour),
IssuedAt: time.Now(),
UCANToken: ucanToken,
SessionID: generateSessionID(),
TokenType: "authorization_code",
}
}
func (p *OAuth2Provider) generateRefreshToken(
authCode *OAuth2AuthorizationCode,
) *OAuth2RefreshToken {
return &OAuth2RefreshToken{
Token: generateSecureToken(32),
ClientID: authCode.ClientID,
UserDID: authCode.UserDID,
Scopes: authCode.Scopes,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour),
IssuedAt: time.Now(),
RotationCount: 0,
}
}
func (p *OAuth2Provider) introspectToken(
token, tokenTypeHint string,
client *OAuth2Client,
) *OAuth2IntrospectionResponse {
// Try access token first
if accessToken := p.accessTokenStore.Get(token); accessToken != nil {
if accessToken.ClientID != client.ClientID {
return &OAuth2IntrospectionResponse{Active: false}
}
return &OAuth2IntrospectionResponse{
Active: time.Now().Before(accessToken.ExpiresAt),
Scope: strings.Join(accessToken.Scopes, " "),
ClientID: accessToken.ClientID,
Username: accessToken.UserDID,
TokenType: "Bearer",
ExpiresAt: accessToken.ExpiresAt.Unix(),
IssuedAt: accessToken.IssuedAt.Unix(),
Subject: accessToken.UserDID,
UCANToken: accessToken.UCANToken.Raw,
}
}
// Try refresh token
if refreshToken := p.refreshTokenStore.Get(token); refreshToken != nil {
if refreshToken.ClientID != client.ClientID {
return &OAuth2IntrospectionResponse{Active: false}
}
return &OAuth2IntrospectionResponse{
Active: time.Now().Before(refreshToken.ExpiresAt),
Scope: strings.Join(refreshToken.Scopes, " "),
ClientID: refreshToken.ClientID,
Username: refreshToken.UserDID,
TokenType: "refresh_token",
ExpiresAt: refreshToken.ExpiresAt.Unix(),
IssuedAt: refreshToken.IssuedAt.Unix(),
Subject: refreshToken.UserDID,
}
}
return &OAuth2IntrospectionResponse{Active: false}
}
func (p *OAuth2Provider) revokeToken(token, tokenTypeHint string, client *OAuth2Client) {
// Try to revoke as access token
if p.accessTokenStore.Revoke(token) {
return
}
// Try to revoke as refresh token
p.refreshTokenStore.Revoke(token)
}
func (p *OAuth2Provider) cleanupExpiredTokens() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
// Cleanup expired authorization codes
p.authCodeStore.CleanupExpired()
// Cleanup expired access tokens
p.accessTokenStore.CleanupExpired()
// Cleanup expired refresh tokens
p.refreshTokenStore.CleanupExpired()
}
}
// Store methods for token stores
func (s *AuthCodeStore) Store(code *OAuth2AuthorizationCode) {
s.mu.Lock()
defer s.mu.Unlock()
s.codes[code.Code] = code
}
func (s *AuthCodeStore) Exchange(code string) *OAuth2AuthorizationCode {
s.mu.Lock()
defer s.mu.Unlock()
authCode, exists := s.codes[code]
if !exists || authCode.Used {
return nil
}
authCode.Used = true
return authCode
}
func (s *AuthCodeStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for code, authCode := range s.codes {
if now.After(authCode.ExpiresAt) {
delete(s.codes, code)
}
}
}
func (s *AccessTokenStore) Store(token *OAuth2AccessToken) {
s.mu.Lock()
defer s.mu.Unlock()
s.tokens[token.Token] = token
}
func (s *AccessTokenStore) Get(token string) *OAuth2AccessToken {
s.mu.RLock()
defer s.mu.RUnlock()
return s.tokens[token]
}
func (s *AccessTokenStore) Revoke(token string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tokens[token]; exists {
delete(s.tokens, token)
return true
}
return false
}
func (s *AccessTokenStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for token, accessToken := range s.tokens {
if now.After(accessToken.ExpiresAt) {
delete(s.tokens, token)
}
}
}
func (s *RefreshTokenStore) Store(token *OAuth2RefreshToken) {
s.mu.Lock()
defer s.mu.Unlock()
s.tokens[token.Token] = token
}
func (s *RefreshTokenStore) Get(token string) *OAuth2RefreshToken {
s.mu.RLock()
defer s.mu.RUnlock()
return s.tokens[token]
}
func (s *RefreshTokenStore) Revoke(token string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tokens[token]; exists {
delete(s.tokens, token)
return true
}
return false
}
func (s *RefreshTokenStore) CleanupExpired() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for token, refreshToken := range s.tokens {
if now.After(refreshToken.ExpiresAt) {
delete(s.tokens, token)
}
}
}
// Helper functions
func generateSecureToken(bytes int) string {
b := make([]byte, bytes)
if _, err := rand.Read(b); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(b)
}
func parseScopes(scope string) []string {
if scope == "" {
return []string{}
}
return strings.Split(scope, " ")
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
func isValidResponseType(responseType string) bool {
validTypes := []string{
"code",
"token",
"id_token",
"code id_token",
"code token",
"id_token token",
"code id_token token",
}
return contains(validTypes, responseType)
}
func oauth2Error(c echo.Context, error, description, state string) error {
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
Error: error,
ErrorDescription: description,
State: state,
})
}
func oauth2TokenError(c echo.Context, error, description string) error {
return c.JSON(http.StatusBadRequest, &OAuth2ErrorResponse{
Error: error,
ErrorDescription: description,
})
}
func redirectError(c echo.Context, redirectURI, error, description, state string) error {
url := fmt.Sprintf("%s?error=%s&error_description=%s&state=%s",
redirectURI, error, description, state)
return c.Redirect(http.StatusFound, url)
}
func buildAuthorizationRedirect(redirectURI, code, state string) string {
if strings.Contains(redirectURI, "?") {
return fmt.Sprintf("%s&code=%s&state=%s", redirectURI, code, state)
}
return fmt.Sprintf("%s?code=%s&state=%s", redirectURI, code, state)
}
func renderOAuth2ConsentPage(
c echo.Context,
req *OAuth2AuthorizationRequest,
client *OAuth2Client,
) error {
// TODO: Render actual consent page
return c.JSON(http.StatusOK, map[string]any{
"client": client,
"scopes": parseScopes(req.Scope),
"state": req.State,
"client_id": req.ClientID,
})
}
func getDefaultOAuth2Config() *OAuth2Config {
baseURL := "https://localhost:8080"
return &OAuth2Config{
Issuer: baseURL,
AuthorizationEndpoint: baseURL + "/oauth2/authorize",
TokenEndpoint: baseURL + "/oauth2/token",
UserInfoEndpoint: baseURL + "/oauth2/userinfo",
JWKSEndpoint: baseURL + "/oauth2/jwks",
RegistrationEndpoint: baseURL + "/oauth2/register",
IntrospectionEndpoint: baseURL + "/oauth2/introspect",
RevocationEndpoint: baseURL + "/oauth2/revoke",
ScopesSupported: []string{
"openid", "profile", "email", "offline_access",
"vault:read", "vault:write", "vault:sign", "vault:admin",
"service:manage", "did:read", "did:write",
},
ResponseTypesSupported: []string{
"code", "token", "id_token",
"code id_token", "code token",
"id_token token", "code id_token token",
},
ResponseModesSupported: []string{
"query", "fragment", "form_post",
},
GrantTypesSupported: []string{
"authorization_code", "implicit", "refresh_token",
"client_credentials", "urn:ietf:params:oauth:grant-type:device_code",
},
SubjectTypesSupported: []string{
"public", "pairwise",
},
IDTokenSigningAlgValuesSupported: []string{
"ES256", "RS256", "HS256",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic", "client_secret_post",
"client_secret_jwt", "private_key_jwt", "none",
},
ClaimsSupported: []string{
"sub", "iss", "aud", "exp", "iat", "auth_time", "nonce",
"name", "given_name", "family_name", "middle_name", "nickname",
"preferred_username", "profile", "picture", "website", "email",
"email_verified", "did", "vault_id", "ucan_capabilities",
},
CodeChallengeMethodsSupported: []string{
PKCEMethodS256, PKCEMethodPlain,
},
ServiceDocumentation: baseURL + "/docs/oauth2",
UILocalesSupported: []string{"en-US"},
UCANSupported: true,
}
}
-563
View File
@@ -1,563 +0,0 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// RefreshTokenHandler handles OAuth2 refresh token flows with UCAN chains
type RefreshTokenHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// RefreshTokenRequest represents an OAuth2 refresh token request
type RefreshTokenRequest struct {
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
}
// RefreshTokenResponse represents an OAuth2 refresh token response
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
UCANToken string `json:"ucan_token,omitempty"`
}
// UCANRefreshMetadata stores metadata for UCAN refresh chains
type UCANRefreshMetadata struct {
OriginalIssuer string `json:"original_issuer"`
DelegationChain []string `json:"delegation_chain"`
RefreshCount int `json:"refresh_count"`
MaxRefreshCount int `json:"max_refresh_count"`
CreatedAt time.Time `json:"created_at"`
LastRefreshedAt time.Time `json:"last_refreshed_at"`
AttenuationPath []Attenuation `json:"attenuation_path"`
}
// Attenuation represents scope reduction in the delegation chain
type Attenuation struct {
FromScopes []string `json:"from_scopes"`
ToScopes []string `json:"to_scopes"`
Timestamp time.Time `json:"timestamp"`
Reason string `json:"reason,omitempty"`
}
// NewRefreshTokenHandler creates a new refresh token handler
func NewRefreshTokenHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *RefreshTokenHandler {
return &RefreshTokenHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleRefreshToken handles OAuth2 refresh token requests
func (h *RefreshTokenHandler) HandleRefreshToken(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request (handle both JSON and form-encoded)
var req RefreshTokenRequest
contentType := r.Header.Get("Content-Type")
if strings.Contains(contentType, "application/json") {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse JSON request body")
return
}
} else {
// Parse form data
if err := r.ParseForm(); err != nil {
h.sendError(w, "invalid_request", "Failed to parse form data")
return
}
req.GrantType = r.FormValue("grant_type")
req.RefreshToken = r.FormValue("refresh_token")
req.Scope = r.FormValue("scope")
req.ClientID = r.FormValue("client_id")
req.ClientSecret = r.FormValue("client_secret")
}
// Validate grant type
if req.GrantType != "refresh_token" {
h.sendError(w, "unsupported_grant_type", "Only refresh_token grant type is supported")
return
}
// Validate refresh token
if req.RefreshToken == "" {
h.sendError(w, "invalid_request", "Missing refresh_token parameter")
return
}
// Authenticate client
clientID, clientSecret := h.extractClientCredentials(r, &req)
ctx := r.Context()
if err := h.clientStore.ValidateClientCredentials(ctx, clientID, clientSecret); err != nil {
h.sendError(w, "invalid_client", "Client authentication failed")
return
}
// Get client information
client, err := h.clientStore.GetClient(ctx, clientID)
if err != nil {
h.sendError(w, "invalid_client", "Client not found")
return
}
// Process refresh token
response, err := h.processRefreshToken(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_grant", err.Error())
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
json.NewEncoder(w).Encode(response)
}
// processRefreshToken processes the refresh token and returns new tokens
func (h *RefreshTokenHandler) processRefreshToken(
ctx context.Context,
req *RefreshTokenRequest,
client *OAuth2Client,
) (*RefreshTokenResponse, error) {
// Retrieve stored refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.RefreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate token type
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Validate client binding
if storedToken.ClientID != client.ClientID {
return nil, fmt.Errorf("refresh token was issued to a different client")
}
// Check if refresh token has expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("refresh token has expired")
}
// Get refresh metadata
metadata, err := h.getRefreshMetadata(ctx, req.RefreshToken)
if err != nil {
// Initialize metadata for first refresh
metadata = &UCANRefreshMetadata{
OriginalIssuer: storedToken.UserDID,
DelegationChain: []string{},
RefreshCount: 0,
MaxRefreshCount: 10, // Default max refresh count
CreatedAt: time.Now(),
AttenuationPath: []Attenuation{},
}
}
// Check refresh count limit
if metadata.RefreshCount >= metadata.MaxRefreshCount {
return nil, fmt.Errorf("refresh token has reached maximum refresh count")
}
// Parse requested scopes
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate scope reduction (attenuate permissions)
if !h.validateScopeAttenuation(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed refresh token scopes")
}
// Record attenuation
if !h.scopesEqual(requestedScopes, storedToken.Scopes) {
metadata.AttenuationPath = append(metadata.AttenuationPath, Attenuation{
FromScopes: storedToken.Scopes,
ToScopes: requestedScopes,
Timestamp: time.Now(),
Reason: "Client requested scope reduction",
})
}
}
// Create new UCAN token with delegation chain
newUCANToken, err := h.createRefreshedUCANToken(
ctx,
metadata,
storedToken,
client,
requestedScopes,
)
if err != nil {
return nil, fmt.Errorf("failed to create refreshed UCAN token: %w", err)
}
// Generate new access token
accessTokenID := h.generateTokenID()
accessToken := &StoredToken{
TokenID: accessTokenID,
TokenType: "access_token",
AccessToken: accessTokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
UCANToken: newUCANToken,
}
if err := h.tokenStore.StoreToken(ctx, accessToken); err != nil {
return nil, fmt.Errorf("failed to store new access token: %w", err)
}
// Update refresh metadata
metadata.RefreshCount++
metadata.LastRefreshedAt = time.Now()
metadata.DelegationChain = append(metadata.DelegationChain, newUCANToken)
// Optionally rotate refresh token
newRefreshTokenID := ""
if h.shouldRotateRefreshToken(metadata) {
newRefreshTokenID = h.generateTokenID()
newRefreshToken := &StoredToken{
TokenID: newRefreshTokenID,
TokenType: "refresh_token",
RefreshToken: newRefreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: requestedScopes,
ClientID: client.ClientID,
UserDID: storedToken.UserDID,
}
if err := h.tokenStore.StoreToken(ctx, newRefreshToken); err != nil {
// Non-fatal, continue with existing refresh token
newRefreshTokenID = ""
} else {
// Revoke old refresh token
h.tokenStore.RevokeToken(ctx, req.RefreshToken)
// Store metadata for new refresh token
h.storeRefreshMetadata(ctx, newRefreshTokenID, metadata)
}
} else {
// Update metadata for existing refresh token
h.storeRefreshMetadata(ctx, req.RefreshToken, metadata)
}
// Build response
response := &RefreshTokenResponse{
AccessToken: accessTokenID,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(requestedScopes, " "),
UCANToken: newUCANToken,
}
if newRefreshTokenID != "" {
response.RefreshToken = newRefreshTokenID
}
return response, nil
}
// createRefreshedUCANToken creates a new UCAN token with proper delegation chain
func (h *RefreshTokenHandler) createRefreshedUCANToken(
ctx context.Context,
metadata *UCANRefreshMetadata,
storedToken *StoredToken,
client *OAuth2Client,
scopes []string,
) (string, error) {
// Build proof chain from previous delegations
proofs := make([]ucan.Proof, 0, len(metadata.DelegationChain))
for _, tokenStr := range metadata.DelegationChain {
proofs = append(proofs, ucan.Proof(tokenStr))
}
// Add original token as proof if exists
if storedToken.UCANToken != "" {
proofs = append([]ucan.Proof{ucan.Proof(storedToken.UCANToken)}, proofs...)
}
// Determine issuer and audience
issuer := metadata.OriginalIssuer
if issuer == "" {
issuer = storedToken.UserDID
}
audience := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
audience = did
}
// Create resource context with refresh metadata
resourceContext := map[string]string{
"refresh_count": fmt.Sprintf("%d", metadata.RefreshCount),
"original_issuer": metadata.OriginalIssuer,
"delegation_type": "refresh_token",
"client_id": client.ClientID,
}
// Map OAuth scopes to UCAN attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with delegation chain
ucanToken := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: h.createRefreshFact(metadata, scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return "", fmt.Errorf("failed to sign UCAN token: %w", err)
}
// Validate the delegation chain
if len(metadata.DelegationChain) > 0 {
allTokens := append([]string{storedToken.UCANToken}, metadata.DelegationChain...)
allTokens = append(allTokens, signedToken)
if err := h.signer.ValidateDelegationChain(allTokens); err != nil {
return "", fmt.Errorf("invalid delegation chain: %w", err)
}
}
return signedToken, nil
}
// validateScopeAttenuation validates that requested scopes are properly attenuated
func (h *RefreshTokenHandler) validateScopeAttenuation(requested, allowed []string) bool {
// Build allowed scope map
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
// Check each requested scope
for _, scope := range requested {
if !allowedMap[scope] {
// Check if a parent scope allows this
found := false
for _, allowedScope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(allowedScope, scope) {
found = true
break
}
}
if !found {
return false
}
}
}
return true
}
// shouldRotateRefreshToken determines if refresh token should be rotated
func (h *RefreshTokenHandler) shouldRotateRefreshToken(metadata *UCANRefreshMetadata) bool {
// Rotate on every use for maximum security
// Could be configured based on policy
return true
}
// extractClientCredentials extracts client credentials from request
func (h *RefreshTokenHandler) extractClientCredentials(
r *http.Request,
req *RefreshTokenRequest,
) (string, string) {
// Try Basic Auth first
if clientID, clientSecret, ok := r.BasicAuth(); ok {
return clientID, clientSecret
}
// Fall back to request body
return req.ClientID, req.ClientSecret
}
// getRefreshMetadata retrieves refresh metadata from storage
func (h *RefreshTokenHandler) getRefreshMetadata(
ctx context.Context,
refreshTokenID string,
) (*UCANRefreshMetadata, error) {
// In production, this would retrieve from persistent storage
// For now, return error to initialize new metadata
return nil, fmt.Errorf("metadata not found")
}
// storeRefreshMetadata stores refresh metadata
func (h *RefreshTokenHandler) storeRefreshMetadata(
ctx context.Context,
refreshTokenID string,
metadata *UCANRefreshMetadata,
) error {
// In production, this would persist to storage
// For now, just return success
return nil
}
// createRefreshFact creates a fact for refresh token
func (h *RefreshTokenHandler) createRefreshFact(
metadata *UCANRefreshMetadata,
scopes []string,
) json.RawMessage {
fact := map[string]any{
"type": "refresh_token",
"refresh_count": metadata.RefreshCount,
"original_issuer": metadata.OriginalIssuer,
"scopes": scopes,
"refreshed_at": time.Now().Unix(),
"delegation_length": len(metadata.DelegationChain),
}
// Add attenuation info if present
if len(metadata.AttenuationPath) > 0 {
fact["attenuations"] = len(metadata.AttenuationPath)
lastAttenuation := metadata.AttenuationPath[len(metadata.AttenuationPath)-1]
fact["last_attenuation"] = map[string]any{
"from": strings.Join(lastAttenuation.FromScopes, " "),
"to": strings.Join(lastAttenuation.ToScopes, " "),
"at": lastAttenuation.Timestamp.Unix(),
}
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// scopesEqual checks if two scope slices are equal
func (h *RefreshTokenHandler) scopesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
aMap := make(map[string]bool)
for _, scope := range a {
aMap[scope] = true
}
for _, scope := range b {
if !aMap[scope] {
return false
}
}
return true
}
// generateTokenID generates a unique token identifier
func (h *RefreshTokenHandler) generateTokenID() string {
// In production, use a proper UUID or secure random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string
func (h *RefreshTokenHandler) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(result)
}
// sendError sends an OAuth error response
func (h *RefreshTokenHandler) sendError(w http.ResponseWriter, errorCode, errorDescription string) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusBadRequest)
response := map[string]string{
"error": errorCode,
"error_description": errorDescription,
}
json.NewEncoder(w).Encode(response)
}
// HandleUCANRefresh handles UCAN-specific refresh requests
func (h *RefreshTokenHandler) HandleUCANRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse UCAN token from Authorization header
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
h.sendError(w, "invalid_request", "Missing or invalid Authorization header")
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(tokenString)
if err != nil {
h.sendError(w, "invalid_grant", "Invalid UCAN token")
return
}
// Check if token can be refreshed (not expired beyond grace period)
gracePeriod := int64(300) // 5 minutes grace period
if time.Now().Unix() > ucanToken.ExpiresAt+gracePeriod {
h.sendError(w, "invalid_grant", "Token expired beyond grace period")
return
}
// Create refreshed token with extended expiration
newToken, err := h.signer.RefreshToken(tokenString, time.Hour)
if err != nil {
h.sendError(w, "server_error", "Failed to refresh token")
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(map[string]any{
"ucan_token": newToken,
"token_type": "UCAN",
"expires_in": 3600,
})
}
-383
View File
@@ -1,383 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
)
// DynamicClientRegistrationRequest represents a client registration request per RFC 7591
type DynamicClientRegistrationRequest struct {
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
ApplicationType string `json:"application_type,omitempty"`
Contacts []string `json:"contacts,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
SoftwareID string `json:"software_id,omitempty"`
SoftwareVersion string `json:"software_version,omitempty"`
}
// DynamicClientRegistrationResponse represents the response for client registration
type DynamicClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
Scope string `json:"scope"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
ApplicationType string `json:"application_type"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TosURI string `json:"tos_uri,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"`
}
// HandleDynamicClientRegistration handles dynamic client registration per RFC 7591
func (s *OAuth2Provider) HandleDynamicClientRegistration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse registration request
var req DynamicClientRegistrationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid registration request", http.StatusBadRequest)
return
}
// Validate required fields
if req.ClientName == "" {
http.Error(w, "client_name is required", http.StatusBadRequest)
return
}
if len(req.RedirectURIs) == 0 {
http.Error(w, "redirect_uris is required", http.StatusBadRequest)
return
}
// Validate redirect URIs
for _, uri := range req.RedirectURIs {
if !isValidRedirectURI(uri) {
http.Error(w, "Invalid redirect URI: "+uri, http.StatusBadRequest)
return
}
}
// Set defaults if not provided
if len(req.GrantTypes) == 0 {
req.GrantTypes = []string{"authorization_code"}
}
if len(req.ResponseTypes) == 0 {
req.ResponseTypes = []string{"code"}
}
if req.TokenEndpointAuthMethod == "" {
// Default based on application type
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
req.TokenEndpointAuthMethod = "none" // Public client
} else {
req.TokenEndpointAuthMethod = "client_secret_basic"
}
}
if req.ApplicationType == "" {
req.ApplicationType = "web"
}
// Validate grant types and response types
if !validateGrantTypes(req.GrantTypes) {
http.Error(w, "Invalid grant types", http.StatusBadRequest)
return
}
if !validateResponseTypes(req.ResponseTypes) {
http.Error(w, "Invalid response types", http.StatusBadRequest)
return
}
// Validate scopes if scope mapper is available
if req.Scope != "" && s.scopeMapper != nil {
scopes := strings.Split(req.Scope, " ")
for _, scope := range scopes {
// Check if scope is valid using the scope mapper
if _, exists := s.scopeMapper.GetScope(scope); !exists {
http.Error(w, "Invalid scope: "+scope, http.StatusBadRequest)
return
}
}
}
// Generate client credentials
clientID := generateDynamicClientID()
var clientSecret string
var clientSecretExpiresAt int64
// Only generate secret for confidential clients
if req.TokenEndpointAuthMethod != "none" {
clientSecret = generateDynamicClientSecret()
// Client secrets expire in 1 year by default
clientSecretExpiresAt = time.Now().Add(365 * 24 * time.Hour).Unix()
}
// Create OAuth2 client
client := &OAuth2Client{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURIs: req.RedirectURIs,
AllowedScopes: strings.Split(req.Scope, " "),
AllowedGrants: req.GrantTypes,
TokenLifetime: time.Hour, // Default 1 hour
RequirePKCE: false,
TrustedClient: false,
RequiresConsent: true,
Metadata: map[string]string{
"client_name": req.ClientName,
"application_type": req.ApplicationType,
"logo_uri": req.LogoURI,
"client_uri": req.ClientURI,
"policy_uri": req.PolicyURI,
"tos_uri": req.TosURI,
"jwks_uri": req.JwksURI,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set client type based on auth method
if req.TokenEndpointAuthMethod == "none" {
client.ClientType = "public"
} else {
client.ClientType = "confidential"
}
// Determine if client requires PKCE
if req.ApplicationType == "native" || req.ApplicationType == "browser" {
client.RequirePKCE = true
}
// Store client in the registry
if s.clientRegistry != nil {
if err := s.clientRegistry.RegisterClient(client); err != nil {
http.Error(w, "Failed to register client", http.StatusInternalServerError)
return
}
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: clientID,
ClientSecret: clientSecret,
ClientName: req.ClientName,
RedirectURIs: req.RedirectURIs,
GrantTypes: req.GrantTypes,
ResponseTypes: req.ResponseTypes,
Scope: req.Scope,
TokenEndpointAuthMethod: req.TokenEndpointAuthMethod,
ApplicationType: req.ApplicationType,
ClientIDIssuedAt: time.Now().Unix(),
ClientSecretExpiresAt: clientSecretExpiresAt,
LogoURI: req.LogoURI,
ClientURI: req.ClientURI,
PolicyURI: req.PolicyURI,
TosURI: req.TosURI,
JwksURI: req.JwksURI,
}
// Return registration response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(resp)
}
// HandleClientConfiguration handles client configuration retrieval
func (s *OAuth2Provider) HandleClientConfiguration(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract client ID from path or query
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
// Try to extract from path (e.g., /register/{client_id})
parts := strings.Split(r.URL.Path, "/")
if len(parts) > 2 {
clientID = parts[len(parts)-1]
}
}
if clientID == "" {
http.Error(w, "client_id is required", http.StatusBadRequest)
return
}
// Validate access token for client management
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", `Bearer realm="client_configuration"`)
http.Error(w, "Access token required", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
// Validate token in the access token store
if s.accessTokenStore == nil {
http.Error(w, "Token store not available", http.StatusInternalServerError)
return
}
// Check token validity (simplified for now)
// In production, this should validate the token properly
if token == "" {
http.Error(w, "Invalid or expired access token", http.StatusUnauthorized)
return
}
// Get client from registry
if s.clientRegistry == nil {
http.Error(w, "Client registry not available", http.StatusInternalServerError)
return
}
client, err := s.clientRegistry.GetClient(clientID)
if err != nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
}
// Build response
resp := DynamicClientRegistrationResponse{
ClientID: client.ClientID,
ClientName: client.Metadata["client_name"],
RedirectURIs: client.RedirectURIs,
GrantTypes: client.AllowedGrants,
ResponseTypes: []string{"code", "token"}, // Default response types
Scope: strings.Join(client.AllowedScopes, " "),
TokenEndpointAuthMethod: getTokenEndpointAuthMethod(client),
ApplicationType: client.Metadata["application_type"],
ClientIDIssuedAt: client.CreatedAt.Unix(),
LogoURI: client.Metadata["logo_uri"],
ClientURI: client.Metadata["client_uri"],
PolicyURI: client.Metadata["policy_uri"],
TosURI: client.Metadata["tos_uri"],
JwksURI: client.Metadata["jwks_uri"],
}
// Return client configuration
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(resp)
}
// getTokenEndpointAuthMethod determines the auth method from client type
func getTokenEndpointAuthMethod(client *OAuth2Client) string {
if client.ClientType == "public" {
return "none"
}
return "client_secret_basic"
}
// Helper functions for dynamic registration
func generateDynamicClientID() string {
// Generate a random client ID for dynamic registration
b := make([]byte, 16)
_, _ = rand.Read(b)
return "dyn_client_" + base64.RawURLEncoding.EncodeToString(b)
}
func generateDynamicClientSecret() string {
// Generate a secure random secret for dynamic registration
b := make([]byte, 32)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func isValidRedirectURI(uri string) bool {
// Basic validation - in production, this should be more comprehensive
if uri == "" {
return false
}
// Allow localhost for development
if strings.HasPrefix(uri, "http://localhost") || strings.HasPrefix(uri, "http://127.0.0.1") {
return true
}
// Require HTTPS for production URIs
if !strings.HasPrefix(uri, "https://") {
// Allow custom schemes for native apps
if strings.Contains(uri, "://") {
return true
}
return false
}
return true
}
func validateGrantTypes(grantTypes []string) bool {
validGrants := map[string]bool{
"authorization_code": true,
"implicit": true,
"refresh_token": true,
"client_credentials": true,
"password": true,
}
for _, grant := range grantTypes {
if !validGrants[grant] {
return false
}
}
return true
}
func validateResponseTypes(responseTypes []string) bool {
validTypes := map[string]bool{
"code": true,
"token": true,
"id_token": true,
}
for _, respType := range responseTypes {
// Handle composite types like "code id_token"
parts := strings.Split(respType, " ")
for _, part := range parts {
if !validTypes[part] {
return false
}
}
}
return true
}
func hasScope(scopes []string, requiredScope string) bool {
for _, scope := range scopes {
if scope == requiredScope {
return true
}
}
return false
}
-412
View File
@@ -1,412 +0,0 @@
package handlers
import (
"fmt"
"strings"
"github.com/sonr-io/sonr/crypto/ucan"
)
// ScopeMapper manages OAuth scope to UCAN capability mapping
type ScopeMapper struct {
scopeDefinitions map[string]*OAuth2ScopeDefinition
ucanTemplates map[string]*ucan.Attenuation
}
// NewScopeMapper creates a new scope mapper with standard scopes
func NewScopeMapper() *ScopeMapper {
mapper := &ScopeMapper{
scopeDefinitions: make(map[string]*OAuth2ScopeDefinition),
ucanTemplates: make(map[string]*ucan.Attenuation),
}
mapper.initializeStandardScopes()
return mapper
}
// initializeStandardScopes defines the standard OAuth scopes and their UCAN mappings
func (m *ScopeMapper) initializeStandardScopes() {
// OpenID Connect scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "openid",
Description: "OpenID Connect authentication",
UCANActions: []string{"authenticate"},
ResourceType: "identity",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "profile",
Description: "Access to user profile information",
UCANActions: []string{"read"},
ResourceType: "did",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "email",
Description: "Access to user email",
UCANActions: []string{"read"},
ResourceType: "contact",
RequiresAuth: true,
Sensitive: true,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "offline_access",
Description: "Maintain access when user is not present",
UCANActions: []string{"refresh"},
ResourceType: "session",
RequiresAuth: true,
Sensitive: true,
})
// Vault scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:read",
Description: "Read access to vault data",
UCANActions: []string{"read"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: false,
ParentScope: "",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:write",
Description: "Write access to vault data",
UCANActions: []string{"read", "write"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:sign",
Description: "Signing operations with vault keys",
UCANActions: []string{"read", "sign"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "vault:admin",
Description: "Full administrative access to vault",
UCANActions: []string{"read", "write", "sign", "export", "import", "delete", "admin"},
ResourceType: "vault",
RequiresAuth: true,
Sensitive: true,
ParentScope: "vault:write",
ChildScopes: []string{"vault:read", "vault:write", "vault:sign"},
})
// Service scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:read",
Description: "Read service information",
UCANActions: []string{"read"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:write",
Description: "Create and update services",
UCANActions: []string{"read", "write"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: true,
ParentScope: "service:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "service:manage",
Description: "Full service management capabilities",
UCANActions: []string{"read", "write", "delete", "admin"},
ResourceType: "service",
RequiresAuth: true,
Sensitive: true,
ParentScope: "service:write",
ChildScopes: []string{"service:read", "service:write"},
})
// DID scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "did:read",
Description: "Read DID documents",
UCANActions: []string{"read"},
ResourceType: "did",
RequiresAuth: false,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "did:write",
Description: "Update DID documents",
UCANActions: []string{"read", "write"},
ResourceType: "did",
RequiresAuth: true,
Sensitive: true,
ParentScope: "did:read",
})
// DWN (Decentralized Web Node) scopes
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:read",
Description: "Read data from DWN",
UCANActions: []string{"read"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: false,
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:write",
Description: "Write data to DWN",
UCANActions: []string{"read", "write"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: true,
ParentScope: "dwn:read",
})
_ = m.RegisterScope(&OAuth2ScopeDefinition{
Name: "dwn:admin",
Description: "Full administrative access to DWN",
UCANActions: []string{"read", "write", "admin", "protocols-configure"},
ResourceType: "dwn",
RequiresAuth: true,
Sensitive: true,
ParentScope: "dwn:write",
ChildScopes: []string{"dwn:read", "dwn:write"},
})
}
// RegisterScope registers a new scope definition
func (m *ScopeMapper) RegisterScope(scope *OAuth2ScopeDefinition) error {
if scope.Name == "" {
return fmt.Errorf("scope name is required")
}
m.scopeDefinitions[scope.Name] = scope
// Create UCAN template for this scope
m.createUCANTemplate(scope)
return nil
}
// GetScope retrieves a scope definition
func (m *ScopeMapper) GetScope(name string) (*OAuth2ScopeDefinition, bool) {
scope, exists := m.scopeDefinitions[name]
return scope, exists
}
// MapToUCAN maps OAuth scopes to UCAN attenuations
func (m *ScopeMapper) MapToUCAN(
scopes []string,
userDID string,
clientID string,
resourceContext map[string]string,
) []ucan.Attenuation {
attenuations := []ucan.Attenuation{}
for _, scopeName := range scopes {
scope, exists := m.scopeDefinitions[scopeName]
if !exists {
continue
}
// Create attenuation for this scope
attenuation := m.createAttenuation(scope, userDID, clientID, resourceContext)
attenuations = append(attenuations, attenuation)
// Add child scope attenuations if this is a parent scope
for _, childScope := range scope.ChildScopes {
if childDef, exists := m.scopeDefinitions[childScope]; exists {
childAttenuation := m.createAttenuation(
childDef,
userDID,
clientID,
resourceContext,
)
attenuations = append(attenuations, childAttenuation)
}
}
}
return attenuations
}
// ValidateScopes validates that the requested scopes are valid
func (m *ScopeMapper) ValidateScopes(scopes []string) error {
for _, scope := range scopes {
if _, exists := m.scopeDefinitions[scope]; !exists {
return fmt.Errorf("invalid scope: %s", scope)
}
}
return nil
}
// GetScopeDescriptions returns human-readable descriptions for scopes
func (m *ScopeMapper) GetScopeDescriptions(scopes []string) map[string]string {
descriptions := make(map[string]string)
for _, scope := range scopes {
if def, exists := m.scopeDefinitions[scope]; exists {
descriptions[scope] = def.Description
}
}
return descriptions
}
// GetSensitiveScopes returns only the sensitive scopes from a list
func (m *ScopeMapper) GetSensitiveScopes(scopes []string) []string {
sensitive := []string{}
for _, scope := range scopes {
if def, exists := m.scopeDefinitions[scope]; exists && def.Sensitive {
sensitive = append(sensitive, scope)
}
}
return sensitive
}
// IsHierarchicalScope checks if one scope includes another
func (m *ScopeMapper) IsHierarchicalScope(parentScope, childScope string) bool {
parent, exists := m.scopeDefinitions[parentScope]
if !exists {
return false
}
// Check direct children
for _, child := range parent.ChildScopes {
if child == childScope {
return true
}
// Recursive check
if m.IsHierarchicalScope(child, childScope) {
return true
}
}
return false
}
// Private helper methods
func (m *ScopeMapper) createUCANTemplate(scope *OAuth2ScopeDefinition) {
// Create a template attenuation for this scope
capability := &ucan.SimpleCapability{
Action: strings.Join(scope.UCANActions, ","),
}
resource := &SimpleResource{
Scheme: scope.ResourceType,
Value: "{resource_id}",
}
m.ucanTemplates[scope.Name] = &ucan.Attenuation{
Capability: capability,
Resource: resource,
}
}
func (m *ScopeMapper) createAttenuation(
scope *OAuth2ScopeDefinition,
userDID string,
clientID string,
resourceContext map[string]string,
) ucan.Attenuation {
// Create capability based on scope actions
var capability ucan.Capability
if len(scope.UCANActions) == 1 {
capability = &ucan.SimpleCapability{
Action: scope.UCANActions[0],
}
} else {
capability = &ucan.MultiCapability{
Actions: scope.UCANActions,
}
}
// Create resource based on scope type and context
resourceValue := m.resolveResourceValue(scope, userDID, resourceContext)
resource := &SimpleResource{
Scheme: scope.ResourceType,
Value: resourceValue,
}
return ucan.Attenuation{
Capability: capability,
Resource: resource,
}
}
func (m *ScopeMapper) resolveResourceValue(
scope *OAuth2ScopeDefinition,
userDID string,
context map[string]string,
) string {
switch scope.ResourceType {
case "did":
return fmt.Sprintf("did:sonr:%s", userDID)
case "vault":
if vaultAddr, exists := context["vault_address"]; exists {
return vaultAddr
}
return fmt.Sprintf("vault:%s", userDID)
case "service":
if serviceID, exists := context["service_id"]; exists {
return serviceID
}
return "service:*"
case "dwn":
if dwnID, exists := context["dwn_id"]; exists {
return dwnID
}
return fmt.Sprintf("dwn:%s", userDID)
default:
return "*"
}
}
// SimpleResource implements the Resource interface for OAuth scopes
type SimpleResource struct {
Scheme string
Value string
}
func (r *SimpleResource) GetScheme() string {
return r.Scheme
}
func (r *SimpleResource) GetValue() string {
return r.Value
}
func (r *SimpleResource) GetURI() string {
return fmt.Sprintf("%s://%s", r.Scheme, r.Value)
}
func (r *SimpleResource) Matches(other ucan.Resource) bool {
if r.Scheme != other.GetScheme() {
return false
}
// Wildcard matching
if r.Value == "*" || other.GetValue() == "*" {
return true
}
// Exact match
return r.Value == other.GetValue()
}
-449
View File
@@ -1,449 +0,0 @@
package handlers
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
// PKCE challenge methods
PKCEMethodS256 = "S256"
PKCEMethodPlain = "plain"
)
// PKCEValidator handles PKCE validation
type PKCEValidator struct{}
// NewPKCEValidator creates a new PKCE validator
func NewPKCEValidator() *PKCEValidator {
return &PKCEValidator{}
}
// GeneratePKCEPair generates a PKCE verifier and challenge pair
func (p *PKCEValidator) GeneratePKCEPair() (verifier, challenge string, err error) {
// Generate cryptographically secure verifier (43-128 characters)
verifierBytes := make([]byte, 32)
if _, err := rand.Read(verifierBytes); err != nil {
return "", "", fmt.Errorf("failed to generate PKCE verifier: %w", err)
}
verifier = base64.RawURLEncoding.EncodeToString(verifierBytes)
// Create S256 challenge
challenge = p.CreateChallenge(verifier, PKCEMethodS256)
return verifier, challenge, nil
}
// CreateChallenge creates a PKCE challenge from a verifier
func (p *PKCEValidator) CreateChallenge(verifier, method string) string {
switch method {
case PKCEMethodS256:
h := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h[:])
case PKCEMethodPlain:
return verifier
default:
// Default to S256 for security
h := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(h[:])
}
}
// Validate validates a PKCE verifier against a challenge
func (p *PKCEValidator) Validate(verifier, challenge, method string) bool {
if method == "" {
method = PKCEMethodPlain
}
computed := p.CreateChallenge(verifier, method)
return subtle.ConstantTimeCompare([]byte(computed), []byte(challenge)) == 1
}
// computePKCEChallenge computes a PKCE challenge (helper function)
func computePKCEChallenge(verifier, method string) string {
validator := NewPKCEValidator()
return validator.CreateChallenge(verifier, method)
}
// CSRFProtector handles CSRF protection
type CSRFProtector struct {
secret []byte
}
// NewCSRFProtector creates a new CSRF protector
func NewCSRFProtector(secret string) *CSRFProtector {
return &CSRFProtector{
secret: []byte(secret),
}
}
// GenerateToken generates a CSRF token
func (c *CSRFProtector) GenerateToken(sessionID string) (string, error) {
// Create a unique token tied to the session
h := hmac.New(sha256.New, c.secret)
h.Write([]byte(sessionID))
h.Write([]byte(time.Now().Format(time.RFC3339)))
tokenBytes := h.Sum(nil)
return base64.RawURLEncoding.EncodeToString(tokenBytes), nil
}
// ValidateToken validates a CSRF token
func (c *CSRFProtector) ValidateToken(token, sessionID string) bool {
// Decode the token
tokenBytes, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return false
}
// Recreate the expected token
h := hmac.New(sha256.New, c.secret)
h.Write([]byte(sessionID))
// Note: In production, you'd want to include time validation
// and possibly store tokens with expiration
expectedBytes := h.Sum(nil)[:len(tokenBytes)]
return hmac.Equal(tokenBytes, expectedBytes)
}
// StateValidator validates OAuth state parameters
type StateValidator struct {
states map[string]*StateEntry
}
// StateEntry represents a stored state parameter
type StateEntry struct {
Value string
ClientID string
ExpiresAt time.Time
Used bool
}
// NewStateValidator creates a new state validator
func NewStateValidator() *StateValidator {
validator := &StateValidator{
states: make(map[string]*StateEntry),
}
// Start cleanup routine
go validator.cleanup()
return validator
}
// GenerateState generates a secure state parameter
func (s *StateValidator) GenerateState() (string, error) {
stateBytes := make([]byte, 32)
if _, err := rand.Read(stateBytes); err != nil {
return "", fmt.Errorf("failed to generate state: %w", err)
}
return base64.RawURLEncoding.EncodeToString(stateBytes), nil
}
// StoreState stores a state parameter for validation
func (s *StateValidator) StoreState(state, clientID string) {
s.states[state] = &StateEntry{
Value: state,
ClientID: clientID,
ExpiresAt: time.Now().Add(10 * time.Minute),
Used: false,
}
}
// ValidateState validates and consumes a state parameter
func (s *StateValidator) ValidateState(state, clientID string) bool {
entry, exists := s.states[state]
if !exists {
return false
}
// Check if expired
if time.Now().After(entry.ExpiresAt) {
delete(s.states, state)
return false
}
// Check if already used
if entry.Used {
return false
}
// Check client ID matches
if entry.ClientID != clientID {
return false
}
// Mark as used
entry.Used = true
return true
}
// cleanup removes expired states
func (s *StateValidator) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
for state, entry := range s.states {
if now.After(entry.ExpiresAt) {
delete(s.states, state)
}
}
}
}
// JWTClientAuthenticator handles JWT client authentication
type JWTClientAuthenticator struct {
clientRegistry *ClientRegistry
}
// NewJWTClientAuthenticator creates a new JWT client authenticator
func NewJWTClientAuthenticator(registry *ClientRegistry) *JWTClientAuthenticator {
return &JWTClientAuthenticator{
clientRegistry: registry,
}
}
// ValidateClientAssertion validates a JWT client assertion
func (j *JWTClientAuthenticator) ValidateClientAssertion(
assertion, expectedAudience string,
) (*OAuth2Client, error) {
// Parse the JWT without verification first to get the claims
token, _, err := jwt.NewParser().ParseUnverified(assertion, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse client assertion: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims format")
}
// Extract client ID from issuer and subject
clientID, ok := claims["iss"].(string)
if !ok {
return nil, fmt.Errorf("missing issuer claim")
}
subject, ok := claims["sub"].(string)
if !ok || subject != clientID {
return nil, fmt.Errorf("issuer and subject must match")
}
// Validate audience
audience, ok := claims["aud"].(string)
if !ok || audience != expectedAudience {
return nil, fmt.Errorf("invalid audience")
}
// Validate expiration
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("assertion expired")
}
} else {
return nil, fmt.Errorf("missing expiration")
}
// Validate not before
if nbf, ok := claims["nbf"].(float64); ok {
if time.Now().Unix() < int64(nbf) {
return nil, fmt.Errorf("assertion not yet valid")
}
}
// Validate issued at
if iat, ok := claims["iat"].(float64); ok {
// Check that the assertion is not too old (5 minutes max)
if time.Now().Unix()-int64(iat) > 300 {
return nil, fmt.Errorf("assertion too old")
}
}
// Validate JTI for replay protection
if jti, ok := claims["jti"].(string); !ok || jti == "" {
return nil, fmt.Errorf("missing jti claim")
}
// TODO: Store and check JTI to prevent replay attacks
// Get the client
client, err := j.clientRegistry.GetClient(clientID)
if err != nil {
return nil, fmt.Errorf("unknown client: %w", err)
}
// TODO: Verify the JWT signature using the client's registered public key
// This requires storing client public keys in the registry
return client, nil
}
// SecureTokenGenerator generates cryptographically secure tokens
type SecureTokenGenerator struct {
entropy int // bits of entropy
}
// NewSecureTokenGenerator creates a new secure token generator
func NewSecureTokenGenerator(entropyBits int) *SecureTokenGenerator {
if entropyBits < 128 {
entropyBits = 256 // Default to 256 bits for security
}
return &SecureTokenGenerator{
entropy: entropyBits,
}
}
// GenerateToken generates a secure random token
func (g *SecureTokenGenerator) GenerateToken() (string, error) {
bytes := make([]byte, g.entropy/8)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate secure token: %w", err)
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
// GenerateHexToken generates a secure random token in hex format
func (g *SecureTokenGenerator) GenerateHexToken() (string, error) {
bytes := make([]byte, g.entropy/8)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate secure token: %w", err)
}
return hex.EncodeToString(bytes), nil
}
// RateLimiter implements rate limiting for OAuth endpoints
type RateLimiter struct {
attempts map[string][]time.Time
maxAttempts int
window time.Duration
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(maxAttempts int, window time.Duration) *RateLimiter {
limiter := &RateLimiter{
attempts: make(map[string][]time.Time),
maxAttempts: maxAttempts,
window: window,
}
// Start cleanup routine
go limiter.cleanup()
return limiter
}
// Allow checks if a request should be allowed
func (r *RateLimiter) Allow(key string) bool {
now := time.Now()
windowStart := now.Add(-r.window)
// Get attempts for this key
attempts := r.attempts[key]
// Filter out attempts outside the window
validAttempts := []time.Time{}
for _, attempt := range attempts {
if attempt.After(windowStart) {
validAttempts = append(validAttempts, attempt)
}
}
// Check if under limit
if len(validAttempts) >= r.maxAttempts {
return false
}
// Add this attempt
validAttempts = append(validAttempts, now)
r.attempts[key] = validAttempts
return true
}
// cleanup removes old entries
func (r *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
windowStart := now.Add(-r.window)
for key, attempts := range r.attempts {
validAttempts := []time.Time{}
for _, attempt := range attempts {
if attempt.After(windowStart) {
validAttempts = append(validAttempts, attempt)
}
}
if len(validAttempts) == 0 {
delete(r.attempts, key)
} else {
r.attempts[key] = validAttempts
}
}
}
}
// OriginValidator validates request origins for CORS
type OriginValidator struct {
allowedOrigins map[string]bool
allowSubdomains bool
}
// NewOriginValidator creates a new origin validator
func NewOriginValidator(origins []string, allowSubdomains bool) *OriginValidator {
validator := &OriginValidator{
allowedOrigins: make(map[string]bool),
allowSubdomains: allowSubdomains,
}
for _, origin := range origins {
validator.allowedOrigins[origin] = true
}
return validator
}
// IsAllowed checks if an origin is allowed
func (o *OriginValidator) IsAllowed(origin string) bool {
// Direct match
if o.allowedOrigins[origin] {
return true
}
// Check subdomain matching if enabled
if o.allowSubdomains {
for allowed := range o.allowedOrigins {
if o.isSubdomainOf(origin, allowed) {
return true
}
}
}
return false
}
// isSubdomainOf checks if origin is a subdomain of allowed
func (o *OriginValidator) isSubdomainOf(origin, allowed string) bool {
// Simple subdomain check
// In production, use proper URL parsing
if strings.HasPrefix(allowed, "*.") {
domain := strings.TrimPrefix(allowed, "*")
return strings.HasSuffix(origin, domain)
}
return false
}
-650
View File
@@ -1,650 +0,0 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// TokenExchangeHandler implements RFC 8693 OAuth 2.0 Token Exchange
type TokenExchangeHandler struct {
delegator *UCANDelegator
signer *BlockchainUCANSigner
tokenStore TokenStore
clientStore ClientStore
}
// TokenStore interface for token persistence
type TokenStore interface {
GetToken(ctx context.Context, tokenID string) (*StoredToken, error)
StoreToken(ctx context.Context, token *StoredToken) error
RevokeToken(ctx context.Context, tokenID string) error
}
// ClientStore interface for OAuth client information
type ClientStore interface {
GetClient(ctx context.Context, clientID string) (*OAuth2Client, error)
ValidateClientCredentials(ctx context.Context, clientID, clientSecret string) error
}
// StoredToken represents a stored OAuth token
type StoredToken struct {
TokenID string `json:"token_id"`
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Scopes []string `json:"scopes"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did,omitempty"`
UCANToken string `json:"ucan_token"`
}
// TokenExchangeRequest represents an RFC 8693 token exchange request
type TokenExchangeRequest struct {
GrantType string `json:"grant_type"`
Resource string `json:"resource,omitempty"`
Audience string `json:"audience,omitempty"`
Scope string `json:"scope,omitempty"`
RequestedTokenType string `json:"requested_token_type,omitempty"`
SubjectToken string `json:"subject_token"`
SubjectTokenType string `json:"subject_token_type"`
ActorToken string `json:"actor_token,omitempty"`
ActorTokenType string `json:"actor_token_type,omitempty"`
}
// TokenExchangeResponse represents an RFC 8693 token exchange response
type TokenExchangeResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in,omitempty"`
Scope string `json:"scope,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Token type identifiers from RFC 8693
const (
TokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token"
TokenTypeRefreshToken = "urn:ietf:params:oauth:token-type:refresh_token"
TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token"
TokenTypeSAML1 = "urn:ietf:params:oauth:token-type:saml1"
TokenTypeSAML2 = "urn:ietf:params:oauth:token-type:saml2"
TokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt"
TokenTypeUCAN = "urn:x-oath:params:oauth:token-type:ucan"
)
// NewTokenExchangeHandler creates a new token exchange handler
func NewTokenExchangeHandler(
delegator *UCANDelegator,
signer *BlockchainUCANSigner,
tokenStore TokenStore,
clientStore ClientStore,
) *TokenExchangeHandler {
return &TokenExchangeHandler{
delegator: delegator,
signer: signer,
tokenStore: tokenStore,
clientStore: clientStore,
}
}
// HandleTokenExchange handles RFC 8693 token exchange requests
func (h *TokenExchangeHandler) HandleTokenExchange(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse request
var req TokenExchangeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.sendError(w, "invalid_request", "Failed to parse request body")
return
}
// Validate grant type
if req.GrantType != "urn:ietf:params:oauth:grant-type:token-exchange" {
h.sendError(w, "unsupported_grant_type", "Only token-exchange grant type is supported")
return
}
// Validate required parameters
if req.SubjectToken == "" || req.SubjectTokenType == "" {
h.sendError(w, "invalid_request", "Missing required parameters")
return
}
// Authenticate client
clientID, clientSecret, ok := r.BasicAuth()
if !ok {
h.sendError(w, "invalid_client", "Client authentication required")
return
}
ctx := r.Context()
if err := h.clientStore.ValidateClientCredentials(ctx, clientID, clientSecret); err != nil {
h.sendError(w, "invalid_client", "Client authentication failed")
return
}
// Get client information
client, err := h.clientStore.GetClient(ctx, clientID)
if err != nil {
h.sendError(w, "invalid_client", "Client not found")
return
}
// Process token exchange based on token types
response, err := h.processTokenExchange(ctx, &req, client)
if err != nil {
h.sendError(w, "invalid_request", err.Error())
return
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
json.NewEncoder(w).Encode(response)
}
// processTokenExchange processes the token exchange based on token types
func (h *TokenExchangeHandler) processTokenExchange(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
) (*TokenExchangeResponse, error) {
// Determine requested token type (default to access token)
requestedType := req.RequestedTokenType
if requestedType == "" {
requestedType = TokenTypeAccessToken
}
// Handle different subject token types
switch req.SubjectTokenType {
case TokenTypeAccessToken:
return h.exchangeAccessToken(ctx, req, client, requestedType)
case TokenTypeRefreshToken:
return h.exchangeRefreshToken(ctx, req, client, requestedType)
case TokenTypeJWT:
return h.exchangeJWT(ctx, req, client, requestedType)
case TokenTypeUCAN:
return h.exchangeUCAN(ctx, req, client, requestedType)
default:
return nil, fmt.Errorf("unsupported subject token type: %s", req.SubjectTokenType)
}
}
// exchangeAccessToken exchanges an access token for a new token
func (h *TokenExchangeHandler) exchangeAccessToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the subject token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid subject token")
}
// Validate token hasn't expired
if time.Now().After(storedToken.ExpiresAt) {
return nil, fmt.Errorf("subject token has expired")
}
// Parse requested scopes (default to original scopes)
requestedScopes := storedToken.Scopes
if req.Scope != "" {
requestedScopes = strings.Split(req.Scope, " ")
// Validate requested scopes are subset of original
if !h.isScopeSubset(requestedScopes, storedToken.Scopes) {
return nil, fmt.Errorf("requested scopes exceed original token scopes")
}
}
// Determine audience (default to requested audience or client ID)
audience := req.Audience
if audience == "" {
// Try to extract DID from client metadata
if clientDID, ok := client.Metadata["client_did"]; ok {
audience = clientDID
} else {
audience = client.ClientID
}
}
// Create new UCAN delegation based on requested type
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(
ctx,
storedToken.UserDID,
audience,
requestedScopes,
storedToken.UCANToken,
)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, storedToken.UserDID, audience, requestedScopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeRefreshToken exchanges a refresh token for new tokens
func (h *TokenExchangeHandler) exchangeRefreshToken(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Retrieve the refresh token
storedToken, err := h.tokenStore.GetToken(ctx, req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token")
}
// Validate it's actually a refresh token
if storedToken.TokenType != "refresh_token" {
return nil, fmt.Errorf("token is not a refresh token")
}
// Create new tokens with same scopes
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
return h.createAccessTokenResponse(ctx, storedToken.UserDID, clientDID, storedToken.Scopes)
}
// exchangeJWT exchanges a JWT for a UCAN token
func (h *TokenExchangeHandler) exchangeJWT(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the JWT
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid JWT: %w", err)
}
// Extract scopes from JWT claims
scopes := h.extractScopesFromUCAN(ucanToken)
// Create response based on requested type
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
switch requestedType {
case TokenTypeUCAN:
return h.createUCANResponse(ctx, ucanToken.Issuer, clientDID, scopes, req.SubjectToken)
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, ucanToken.Issuer, clientDID, scopes)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// exchangeUCAN exchanges a UCAN token for another token type
func (h *TokenExchangeHandler) exchangeUCAN(
ctx context.Context,
req *TokenExchangeRequest,
client *OAuth2Client,
requestedType string,
) (*TokenExchangeResponse, error) {
// Verify the UCAN token
ucanToken, err := h.signer.VerifySignature(req.SubjectToken)
if err != nil {
return nil, fmt.Errorf("invalid UCAN token: %w", err)
}
// Validate delegation chain if actor token is provided
if req.ActorToken != "" {
actorToken, err := h.signer.VerifySignature(req.ActorToken)
if err != nil {
return nil, fmt.Errorf("invalid actor token: %w", err)
}
// Validate actor can act on behalf of subject
if actorToken.Audience != ucanToken.Issuer {
return nil, fmt.Errorf("actor token audience doesn't match subject issuer")
}
}
// Extract scopes from UCAN
scopes := h.extractScopesFromUCAN(ucanToken)
// Handle impersonation/delegation if actor token is present
issuer := ucanToken.Issuer
clientDID := client.ClientID
if did, ok := client.Metadata["client_did"]; ok {
clientDID = did
}
if req.ActorToken != "" {
// Actor is performing action on behalf of subject
issuer = clientDID // Actor becomes the new issuer
}
// Create response based on requested type
switch requestedType {
case TokenTypeAccessToken:
return h.createAccessTokenResponse(ctx, issuer, clientDID, scopes)
case TokenTypeUCAN:
// Create delegated UCAN with proof chain
proofs := []ucan.Proof{ucan.Proof(req.SubjectToken)}
if req.ActorToken != "" {
proofs = append(proofs, ucan.Proof(req.ActorToken))
}
return h.createDelegatedUCANResponse(ctx, issuer, clientDID, scopes, proofs)
default:
return nil, fmt.Errorf("unsupported requested token type: %s", requestedType)
}
}
// createUCANResponse creates a UCAN token response
func (h *TokenExchangeHandler) createUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proof string,
) (*TokenExchangeResponse, error) {
// Create UCAN token with delegation
ucanToken, err := h.delegator.CreateDelegation(
issuer,
audience,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create UCAN token: %w", err)
}
// Add proof if provided
if proof != "" {
ucanToken.Proofs = []ucan.Proof{ucan.Proof(proof)}
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createDelegatedUCANResponse creates a delegated UCAN token with proof chain
func (h *TokenExchangeHandler) createDelegatedUCANResponse(
ctx context.Context,
issuer, audience string,
scopes []string,
proofs []ucan.Proof,
) (*TokenExchangeResponse, error) {
// Build resource context
resourceContext := map[string]string{
"delegation_type": "token_exchange",
"issued_at": fmt.Sprintf("%d", time.Now().Unix()),
}
// Map scopes to attenuations
attenuations := h.delegator.scopeMapper.MapToUCAN(scopes, issuer, audience, resourceContext)
// Create UCAN token with proof chain
ucanToken := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(time.Hour).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
Facts: []ucan.Fact{
{
Data: h.createTokenExchangeFact(scopes),
},
},
}
// Sign the token
signedToken, err := h.signer.Sign(ucanToken)
if err != nil {
return nil, fmt.Errorf("failed to sign delegated UCAN token: %w", err)
}
return &TokenExchangeResponse{
AccessToken: signedToken,
IssuedTokenType: TokenTypeUCAN,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}, nil
}
// createAccessTokenResponse creates a standard OAuth access token response
func (h *TokenExchangeHandler) createAccessTokenResponse(
ctx context.Context,
userDID, clientID string,
scopes []string,
) (*TokenExchangeResponse, error) {
// Create UCAN-backed access token
ucanToken, err := h.delegator.CreateDelegation(
userDID,
clientID,
scopes,
time.Now().Add(time.Hour),
)
if err != nil {
return nil, fmt.Errorf("failed to create access token: %w", err)
}
// Generate token ID
tokenID := h.generateTokenID()
// Store token
storedToken := &StoredToken{
TokenID: tokenID,
TokenType: "access_token",
AccessToken: tokenID,
ExpiresAt: time.Now().Add(time.Hour),
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
UCANToken: ucanToken.Raw,
}
if err := h.tokenStore.StoreToken(ctx, storedToken); err != nil {
return nil, fmt.Errorf("failed to store token: %w", err)
}
// Generate refresh token
refreshTokenID := h.generateTokenID()
refreshToken := &StoredToken{
TokenID: refreshTokenID,
TokenType: "refresh_token",
RefreshToken: refreshTokenID,
ExpiresAt: time.Now().Add(30 * 24 * time.Hour), // 30 days
Scopes: scopes,
ClientID: clientID,
UserDID: userDID,
}
if err := h.tokenStore.StoreToken(ctx, refreshToken); err != nil {
// Non-fatal, continue without refresh token
refreshTokenID = ""
}
response := &TokenExchangeResponse{
AccessToken: tokenID,
IssuedTokenType: TokenTypeAccessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
Scope: strings.Join(scopes, " "),
}
if refreshTokenID != "" {
response.RefreshToken = refreshTokenID
}
return response, nil
}
// extractScopesFromUCAN extracts OAuth scopes from UCAN attenuations
func (h *TokenExchangeHandler) extractScopesFromUCAN(token *ucan.Token) []string {
scopeMap := make(map[string]bool)
for _, att := range token.Attenuations {
scheme := att.Resource.GetScheme()
actions := att.Capability.GetActions()
// Map UCAN capabilities back to OAuth scopes
for _, action := range actions {
scope := h.mapUCANToScope(scheme, action)
if scope != "" {
scopeMap[scope] = true
}
}
}
scopes := make([]string, 0, len(scopeMap))
for scope := range scopeMap {
scopes = append(scopes, scope)
}
return scopes
}
// mapUCANToScope maps UCAN capability to OAuth scope
func (h *TokenExchangeHandler) mapUCANToScope(scheme, action string) string {
// Reverse mapping from UCAN to OAuth scopes
switch scheme {
case "vault":
switch action {
case "read":
return "vault:read"
case "write":
return "vault:write"
case "sign":
return "vault:sign"
case "*", "admin":
return "vault:admin"
}
case "service", "svc":
switch action {
case "read":
return "service:read"
case "write":
return "service:write"
case "*", "admin":
return "service:manage"
}
case "did":
switch action {
case "read":
return "did:read"
case "write", "update":
return "did:write"
}
case "dwn":
switch action {
case "read":
return "dwn:read"
case "write":
return "dwn:write"
}
}
// Default mapping
return fmt.Sprintf("%s:%s", scheme, action)
}
// isScopeSubset checks if requested scopes are subset of allowed scopes
func (h *TokenExchangeHandler) isScopeSubset(requested, allowed []string) bool {
allowedMap := make(map[string]bool)
for _, scope := range allowed {
allowedMap[scope] = true
}
for _, scope := range requested {
if !allowedMap[scope] {
// Check if parent scope is allowed
if !h.isParentScopeAllowed(scope, allowed) {
return false
}
}
}
return true
}
// isParentScopeAllowed checks if a parent scope grants the requested scope
func (h *TokenExchangeHandler) isParentScopeAllowed(requested string, allowed []string) bool {
for _, scope := range allowed {
if h.delegator.scopeMapper.IsHierarchicalScope(scope, requested) {
return true
}
}
return false
}
// createTokenExchangeFact creates a fact for token exchange
func (h *TokenExchangeHandler) createTokenExchangeFact(scopes []string) json.RawMessage {
fact := map[string]any{
"type": "token_exchange",
"scopes": scopes,
"issued_at": time.Now().Unix(),
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
}
data, _ := json.Marshal(fact)
return json.RawMessage(data)
}
// generateTokenID generates a unique token identifier
func (h *TokenExchangeHandler) generateTokenID() string {
// In production, use a proper UUID or random generator
return fmt.Sprintf("tok_%d_%s", time.Now().UnixNano(), h.randomString(16))
}
// randomString generates a random string of specified length
func (h *TokenExchangeHandler) randomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(result)
}
// sendError sends an OAuth error response
func (h *TokenExchangeHandler) sendError(
w http.ResponseWriter,
errorCode, errorDescription string,
) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
w.WriteHeader(http.StatusBadRequest)
response := map[string]string{
"error": errorCode,
"error_description": errorDescription,
}
json.NewEncoder(w).Encode(response)
}
-269
View File
@@ -1,269 +0,0 @@
package handlers
import (
"encoding/json"
"time"
"github.com/sonr-io/sonr/crypto/ucan"
)
// OAuth2Client represents a registered OAuth2 client application
type OAuth2Client struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientType string `json:"client_type"` // "confidential" or "public"
RedirectURIs []string `json:"redirect_uris"`
AllowedScopes []string `json:"allowed_scopes"`
AllowedGrants []string `json:"allowed_grants"`
TokenLifetime time.Duration `json:"token_lifetime"`
RequirePKCE bool `json:"require_pkce"`
TrustedClient bool `json:"trusted_client"`
RequiresConsent bool `json:"requires_consent"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuth2AuthorizationRequest represents an OAuth2 authorization request
type OAuth2AuthorizationRequest struct {
ResponseType string `json:"response_type" form:"response_type" query:"response_type"`
ClientID string `json:"client_id" form:"client_id" query:"client_id"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri" query:"redirect_uri"`
Scope string `json:"scope" form:"scope" query:"scope"`
State string `json:"state" form:"state" query:"state"`
CodeChallenge string `json:"code_challenge" form:"code_challenge" query:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method" form:"code_challenge_method" query:"code_challenge_method"`
Nonce string `json:"nonce" form:"nonce" query:"nonce"`
Prompt string `json:"prompt" form:"prompt" query:"prompt"`
MaxAge int `json:"max_age" form:"max_age" query:"max_age"`
LoginHint string `json:"login_hint" form:"login_hint" query:"login_hint"`
}
// OAuth2TokenRequest represents an OAuth2 token exchange request
type OAuth2TokenRequest struct {
GrantType string `json:"grant_type" form:"grant_type"`
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
RefreshToken string `json:"refresh_token" form:"refresh_token"`
Scope string `json:"scope" form:"scope"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Assertion string `json:"assertion" form:"assertion"`
ClientAssertion string `json:"client_assertion" form:"client_assertion"`
ClientAssertionType string `json:"client_assertion_type" form:"client_assertion_type"`
}
// OAuth2TokenResponse represents an OAuth2 token response
type OAuth2TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
IDToken string `json:"id_token,omitempty"`
UCANToken string `json:"ucan_token,omitempty"` // UCAN delegation token
}
// OAuth2ErrorResponse represents an OAuth2 error response
type OAuth2ErrorResponse struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
State string `json:"state,omitempty"`
}
// OAuth2AuthorizationCode represents an authorization code with UCAN context
type OAuth2AuthorizationCode struct {
Code string `json:"code"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did"`
RedirectURI string `json:"redirect_uri"`
Scopes []string `json:"scopes"`
State string `json:"state"`
Nonce string `json:"nonce"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
UCANContext *UCANAuthContext `json:"ucan_context"`
}
// UCANAuthContext holds UCAN-related data for authorization
type UCANAuthContext struct {
VaultAddress string `json:"vault_address"`
EnclaveDataCID string `json:"enclave_data_cid"`
DIDDocument json.RawMessage `json:"did_document"`
Capabilities []string `json:"capabilities"`
}
// OAuth2AccessToken represents an access token with embedded UCAN
type OAuth2AccessToken struct {
Token string `json:"token"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
ExpiresAt time.Time `json:"expires_at"`
IssuedAt time.Time `json:"issued_at"`
UCANToken *ucan.Token `json:"ucan_token"`
SessionID string `json:"session_id"`
TokenType string `json:"token_type"`
}
// OAuth2RefreshToken represents a refresh token
type OAuth2RefreshToken struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
ClientID string `json:"client_id"`
UserDID string `json:"user_did"`
Scopes []string `json:"scopes"`
ExpiresAt time.Time `json:"expires_at"`
IssuedAt time.Time `json:"issued_at"`
RotationCount int `json:"rotation_count"`
}
// OAuth2Session extends OIDCSession with OAuth2-specific fields
type OAuth2Session struct {
SessionID string `json:"session_id"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
IDToken string `json:"id_token,omitempty"`
UCANToken *ucan.Token `json:"ucan_token"`
ConsentGiven bool `json:"consent_given"`
ConsentScopes []string `json:"consent_scopes"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuth2ConsentRequest represents a consent request for a client
type OAuth2ConsentRequest struct {
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
RequestedScopes []string `json:"requested_scopes"`
ConsentID string `json:"consent_id"`
CreatedAt time.Time `json:"created_at"`
}
// OAuth2ConsentResponse represents a user's consent response
type OAuth2ConsentResponse struct {
ConsentID string `json:"consent_id"`
Approved bool `json:"approved"`
ApprovedScopes []string `json:"approved_scopes"`
RememberChoice bool `json:"remember_choice"`
}
// OAuth2ScopeDefinition defines an OAuth scope and its UCAN mapping
type OAuth2ScopeDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
UCANActions []string `json:"ucan_actions"`
ResourceType string `json:"resource_type"`
RequiresAuth bool `json:"requires_auth"`
Sensitive bool `json:"sensitive"`
ParentScope string `json:"parent_scope,omitempty"`
ChildScopes []string `json:"child_scopes,omitempty"`
Metadata map[string]any `json:"metadata"`
}
// OAuth2ClientRegistrationRequest represents a dynamic client registration request
type OAuth2ClientRegistrationRequest struct {
ClientName string `json:"client_name"`
ClientType string `json:"client_type"` // "confidential" or "public"
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scopes string `json:"scopes,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
ClientURI string `json:"client_uri,omitempty"`
PolicyURI string `json:"policy_uri,omitempty"`
TOSUri string `json:"tos_uri,omitempty"`
Contacts []string `json:"contacts,omitempty"`
}
// OAuth2ClientRegistrationResponse represents a successful client registration
type OAuth2ClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at,omitempty"`
RegistrationAccessToken string `json:"registration_access_token,omitempty"`
RegistrationClientURI string `json:"registration_client_uri,omitempty"`
}
// OAuth2IntrospectionRequest represents a token introspection request
type OAuth2IntrospectionRequest struct {
Token string `json:"token" form:"token"`
TokenTypeHint string `json:"token_type_hint" form:"token_type_hint"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
}
// OAuth2IntrospectionResponse represents a token introspection response
type OAuth2IntrospectionResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
Username string `json:"username,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
Audience []string `json:"aud,omitempty"`
Issuer string `json:"iss,omitempty"`
JWTID string `json:"jti,omitempty"`
UCANToken string `json:"ucan_token,omitempty"`
}
// OAuth2RevocationRequest represents a token revocation request
type OAuth2RevocationRequest struct {
Token string `json:"token" form:"token"`
TokenTypeHint string `json:"token_type_hint" form:"token_type_hint"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
}
// OAuth2Config extends OIDCConfig with OAuth2-specific endpoints
type OAuth2Config struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
JWKSEndpoint string `json:"jwks_uri"`
RegistrationEndpoint string `json:"registration_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
ServiceDocumentation string `json:"service_documentation,omitempty"`
UILocalesSupported []string `json:"ui_locales_supported,omitempty"`
OpPolicyURI string `json:"op_policy_uri,omitempty"`
OpTosURI string `json:"op_tos_uri,omitempty"`
UCANSupported bool `json:"ucan_supported"` // Custom field for UCAN support
}
// TokenValidationResult represents the result of token validation
type TokenValidationResult struct {
Valid bool `json:"valid"`
UserDID string `json:"user_did"`
ClientID string `json:"client_id"`
Scopes []string `json:"scopes"`
UCANToken *ucan.Token `json:"ucan_token,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
Error string `json:"error,omitempty"`
}
-466
View File
@@ -1,466 +0,0 @@
package handlers
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// OIDCProvider manages OIDC operations
type OIDCProvider struct {
mu sync.RWMutex
codes map[string]*AuthorizationCode
sessions map[string]*OIDCSession
config any // TODO: Use bridge.OIDCProviderConfig
}
var (
oidcProvider = &OIDCProvider{
codes: make(map[string]*AuthorizationCode),
sessions: make(map[string]*OIDCSession),
}
codeExpiration = 10 * time.Minute
)
// SetOIDCConfig sets the OIDC provider configuration
func SetOIDCConfig(config any) {
oidcProvider.mu.Lock()
defer oidcProvider.mu.Unlock()
oidcProvider.config = config
}
// GetOIDCDiscovery returns OIDC discovery configuration
func GetOIDCDiscovery(c echo.Context) error {
config := &OIDCConfig{
Issuer: "https://localhost:8080",
AuthorizationEndpoint: "https://localhost:8080/oidc/authorize",
TokenEndpoint: "https://localhost:8080/oidc/token",
UserInfoEndpoint: "https://localhost:8080/oidc/userinfo",
JWKSEndpoint: "https://localhost:8080/oidc/jwks",
RevocationEndpoint: "https://localhost:8080/oidc/revoke",
IntrospectionEndpoint: "https://localhost:8080/oidc/introspect",
ScopesSupported: []string{
"openid", "profile", "email", "did", "vault", "offline_access",
},
ResponseTypesSupported: []string{
"code", "id_token", "code id_token",
},
GrantTypesSupported: []string{
"authorization_code", "refresh_token", "client_credentials",
},
SubjectTypesSupported: []string{
"public", "pairwise",
},
IDTokenSigningAlgValuesSupported: []string{
"ES256", "RS256",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_post", "client_secret_basic", "none",
},
ClaimsSupported: []string{
"sub", "name", "preferred_username", "email", "email_verified",
"did", "vault_id", "updated_at",
},
CodeChallengeMethodsSupported: []string{
"S256", "plain",
},
}
return c.JSON(http.StatusOK, config)
}
// HandleOIDCAuthorization handles OIDC authorization requests
func HandleOIDCAuthorization(c echo.Context) error {
// Parse request parameters from query string for GET or form for POST
req := OIDCAuthorizationRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
State: c.QueryParam("state"),
Nonce: c.QueryParam("nonce"),
CodeChallenge: c.QueryParam("code_challenge"),
CodeChallengeMethod: c.QueryParam("code_challenge_method"),
}
// If no query params, try to bind from body (for POST requests)
if req.ResponseType == "" {
_ = c.Bind(&req) // Ignore bind errors and continue
}
// Validate required parameters
if req.ResponseType == "" || req.ClientID == "" || req.RedirectURI == "" || req.Scope == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Validate response type
if req.ResponseType != "code" && req.ResponseType != "id_token" &&
req.ResponseType != "code id_token" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_response_type",
"error_description": "Response type not supported",
})
}
// Validate scope includes openid
if !strings.Contains(req.Scope, "openid") {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_scope",
"error_description": "Scope must include 'openid'",
})
}
// TODO: Validate client_id and redirect_uri against registered clients
// For now, assume user is authenticated (in production, redirect to login)
userDID := c.Get("user_did")
if userDID == nil {
// Redirect to WebAuthn authentication
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "authentication_required",
"error_description": "User authentication required",
})
}
// Generate authorization code
code := generateAuthorizationCode()
// Store authorization code
authCode := &AuthorizationCode{
Code: code,
ClientID: req.ClientID,
RedirectURI: req.RedirectURI,
UserDID: userDID.(string),
Scope: req.Scope,
Nonce: req.Nonce,
CodeChallenge: req.CodeChallenge,
CodeChallengeMethod: req.CodeChallengeMethod,
ExpiresAt: time.Now().Add(codeExpiration),
Used: false,
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
// Build redirect URL
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", req.RedirectURI, code, req.State)
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleOIDCToken handles OIDC token requests
func HandleOIDCToken(c echo.Context) error {
var req OIDCTokenRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Invalid token request",
})
}
// Handle different grant types
switch req.GrantType {
case "authorization_code":
return handleAuthorizationCodeGrant(c, &req)
case "refresh_token":
return handleRefreshTokenGrant(c, &req)
case "client_credentials":
return handleClientCredentialsGrant(c, &req)
default:
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Grant type not supported",
})
}
}
// handleAuthorizationCodeGrant processes authorization code grant
func handleAuthorizationCodeGrant(c echo.Context, req *OIDCTokenRequest) error {
if req.Code == "" || req.RedirectURI == "" || req.ClientID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Retrieve and validate authorization code
oidcProvider.mu.Lock()
authCode, exists := oidcProvider.codes[req.Code]
if exists {
delete(oidcProvider.codes, req.Code) // Single use
}
oidcProvider.mu.Unlock()
if !exists {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid authorization code",
})
}
// Validate code hasn't expired
if time.Now().After(authCode.ExpiresAt) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Authorization code expired",
})
}
// Validate code hasn't been used
if authCode.Used {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Authorization code already used",
})
}
// Validate client and redirect URI
if authCode.ClientID != req.ClientID || authCode.RedirectURI != req.RedirectURI {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid client or redirect URI",
})
}
// Validate PKCE if present
if authCode.CodeChallenge != "" {
if req.CodeVerifier == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Code verifier required",
})
}
if !verifyPKCE(req.CodeVerifier, authCode.CodeChallenge, authCode.CodeChallengeMethod) {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_grant",
"error_description": "Invalid code verifier",
})
}
}
// Mark code as used
authCode.Used = true
// Generate tokens
accessToken := generateAccessToken(authCode.UserDID)
refreshToken := generateRefreshToken()
idToken, err := generateIDToken(authCode.UserDID, authCode.ClientID, authCode.Nonce)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to generate ID token",
})
}
// Create session
session := &OIDCSession{
SessionID: generateSessionID(),
UserDID: authCode.UserDID,
ClientID: authCode.ClientID,
Scope: authCode.Scope,
Nonce: authCode.Nonce,
AccessToken: accessToken,
RefreshToken: refreshToken,
IDToken: idToken,
ExpiresAt: time.Now().Add(time.Hour),
CreatedAt: time.Now(),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[accessToken] = session
oidcProvider.mu.Unlock()
// Return tokens
response := &OIDCTokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: 3600,
RefreshToken: refreshToken,
IDToken: idToken,
Scope: authCode.Scope,
}
return c.JSON(http.StatusOK, response)
}
// handleRefreshTokenGrant processes refresh token grant
func handleRefreshTokenGrant(c echo.Context, req *OIDCTokenRequest) error {
// TODO: Implement refresh token grant
return c.JSON(http.StatusNotImplemented, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Refresh token grant not yet implemented",
})
}
// handleClientCredentialsGrant processes client credentials grant
func handleClientCredentialsGrant(c echo.Context, req *OIDCTokenRequest) error {
// TODO: Implement client credentials grant
return c.JSON(http.StatusNotImplemented, map[string]string{
"error": "unsupported_grant_type",
"error_description": "Client credentials grant not yet implemented",
})
}
// HandleOIDCUserInfo handles OIDC userinfo requests
func HandleOIDCUserInfo(c echo.Context) error {
// Extract access token from Authorization header
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Invalid access token",
})
}
accessToken := strings.TrimPrefix(authHeader, "Bearer ")
// Validate access token
oidcProvider.mu.RLock()
session, exists := oidcProvider.sessions[accessToken]
oidcProvider.mu.RUnlock()
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Invalid or expired access token",
})
}
// Check if token is expired
if time.Now().After(session.ExpiresAt) {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"error_description": "Access token expired",
})
}
// TODO: Fetch actual user info from DID document or database
userInfo := &OIDCUserInfo{
Subject: session.UserDID,
PreferredUsername: "user", // TODO: Get from DID document
DID: session.UserDID,
UpdatedAt: time.Now().Unix(),
}
// Add additional claims based on scope
if strings.Contains(session.Scope, "profile") {
userInfo.Name = "User Name" // TODO: Get from DID document
}
if strings.Contains(session.Scope, "email") {
userInfo.Email = "user@example.com" // TODO: Get from DID document
userInfo.EmailVerified = true
}
if strings.Contains(session.Scope, "vault") {
userInfo.VaultID = "vault_" + session.UserDID // TODO: Get actual vault ID
}
return c.JSON(http.StatusOK, userInfo)
}
// HandleOIDCJWKS handles JWKS endpoint
func HandleOIDCJWKS(c echo.Context) error {
// TODO: Return actual public keys used for signing
jwks := &JWKSet{
Keys: []JWK{
{
KeyType: "EC",
Use: "sig",
KeyID: "1",
Algorithm: "ES256",
Curve: "P-256",
// TODO: Add actual public key coordinates
},
},
}
return c.JSON(http.StatusOK, jwks)
}
// Helper functions
func generateAuthorizationCode() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateAccessToken(userDID string) string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateRefreshToken() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateSessionID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func generateIDToken(userDID, clientID, nonce string) (string, error) {
claims := &DIDAuthClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: userDID,
Issuer: "https://localhost:8080",
Audience: []string{clientID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
DID: userDID,
}
if nonce != "" {
claims.Extra = map[string]any{
"nonce": nonce,
}
}
// TODO: Sign with actual signing key
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-secret-key"))
}
func verifyPKCE(verifier, challenge, method string) bool {
var computed string
switch method {
case "S256":
h := sha256.Sum256([]byte(verifier))
computed = base64.RawURLEncoding.EncodeToString(h[:])
case "plain":
computed = verifier
default:
return false
}
return computed == challenge
}
-360
View File
@@ -1,360 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestOIDCDiscovery tests the OIDC discovery endpoint
func TestOIDCDiscovery(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/.well-known/openid-configuration", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := GetOIDCDiscovery(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var config OIDCConfig
err = json.Unmarshal(rec.Body.Bytes(), &config)
assert.NoError(t, err)
// Verify required fields
assert.NotEmpty(t, config.Issuer)
assert.NotEmpty(t, config.AuthorizationEndpoint)
assert.NotEmpty(t, config.TokenEndpoint)
assert.NotEmpty(t, config.UserInfoEndpoint)
assert.NotEmpty(t, config.JWKSEndpoint)
assert.Contains(t, config.ScopesSupported, "openid")
assert.Contains(t, config.ResponseTypesSupported, "code")
assert.Contains(t, config.GrantTypesSupported, "authorization_code")
}
// TestOIDCAuthorizationFlow tests the authorization code flow
func TestOIDCAuthorizationFlow(t *testing.T) {
e := echo.New()
t.Run("ValidAuthorizationRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid profile")
q.Set("state", "test-state")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Set authenticated user context
c.Set("user_did", "did:sonr:testuser")
c.Set("authenticated", true)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
// Should redirect with authorization code
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "code=")
assert.Contains(t, location, "state=test-state")
})
t.Run("MissingRequiredParameters", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
})
t.Run("InvalidResponseType", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "invalid")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
})
}
// TestOIDCTokenExchange tests the token endpoint
func TestOIDCTokenExchange(t *testing.T) {
e := echo.New()
// Setup: Create an authorization code
code := "test-auth-code"
authCode := &AuthorizationCode{
Code: code,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
Scope: "openid profile",
ExpiresAt: time.Now().Add(10 * time.Minute),
CodeChallenge: "test-challenge",
CodeChallengeMethod: "S256",
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
t.Run("ValidTokenExchange", func(t *testing.T) {
body := strings.NewReader("grant_type=authorization_code&code=" + code +
"&redirect_uri=http://localhost:3000/callback&client_id=test-client" +
"&code_verifier=test-verifier")
req := httptest.NewRequest(http.MethodPost, "/oidc/token", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
CodeVerifier: "test-verifier",
}
err := handleAuthorizationCodeGrant(c, tokenReq)
assert.NoError(t, err)
if rec.Code == http.StatusOK {
var tokenResp OIDCTokenResponse
err = json.Unmarshal(rec.Body.Bytes(), &tokenResp)
assert.NoError(t, err)
assert.NotEmpty(t, tokenResp.AccessToken)
assert.NotEmpty(t, tokenResp.IDToken)
assert.Equal(t, "Bearer", tokenResp.TokenType)
}
})
t.Run("ExpiredAuthorizationCode", func(t *testing.T) {
expiredCode := "expired-code"
expiredAuthCode := &AuthorizationCode{
Code: expiredCode,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired
}
oidcProvider.mu.Lock()
oidcProvider.codes[expiredCode] = expiredAuthCode
oidcProvider.mu.Unlock()
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: expiredCode,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleAuthorizationCodeGrant(c, tokenReq)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
})
}
// TestOIDCUserInfo tests the userinfo endpoint
func TestOIDCUserInfo(t *testing.T) {
e := echo.New()
// Setup: Create a session
accessToken := "test-access-token"
session := &OIDCSession{
SessionID: "test-session",
UserDID: "did:sonr:testuser",
ClientID: "test-client",
Scope: "openid profile email",
AccessToken: accessToken,
RefreshToken: "test-refresh-token",
ExpiresAt: time.Now().Add(1 * time.Hour),
CreatedAt: time.Now(),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[accessToken] = session
oidcProvider.mu.Unlock()
t.Run("ValidUserInfoRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var userInfo map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &userInfo)
assert.NoError(t, err)
assert.Equal(t, "did:sonr:testuser", userInfo["sub"])
})
t.Run("InvalidAccessToken", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
t.Run("MissingAuthorizationHeader", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/userinfo", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleOIDCUserInfo(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
})
}
// TestPKCEFlow tests PKCE (Proof Key for Code Exchange) implementation
func TestPKCEFlow(t *testing.T) {
e := echo.New()
// Generate PKCE parameters
codeVerifier := "test-code-verifier-string-that-is-long-enough"
codeChallenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" // SHA256 of verifier
t.Run("AuthorizationWithPKCE", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/oidc/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("code_challenge", codeChallenge)
q.Set("code_challenge_method", "S256")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
c.Set("authenticated", true)
err := HandleOIDCAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, rec.Code)
})
t.Run("TokenExchangeWithPKCE", func(t *testing.T) {
// Create auth code with PKCE
code := "pkce-auth-code"
authCode := &AuthorizationCode{
Code: code,
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
UserDID: "did:sonr:testuser",
CodeChallenge: codeChallenge,
CodeChallengeMethod: "S256",
ExpiresAt: time.Now().Add(10 * time.Minute),
}
oidcProvider.mu.Lock()
oidcProvider.codes[code] = authCode
oidcProvider.mu.Unlock()
tokenReq := &OIDCTokenRequest{
GrantType: "authorization_code",
Code: code,
RedirectURI: "http://localhost:3000/callback",
ClientID: "test-client",
CodeVerifier: codeVerifier,
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleAuthorizationCodeGrant(c, tokenReq)
require.NoError(t, err)
// With correct verifier, should succeed
if rec.Code != http.StatusOK {
t.Logf("Response: %s", rec.Body.String())
}
})
}
// TestRefreshTokenFlow tests refresh token functionality
func TestRefreshTokenFlow(t *testing.T) {
e := echo.New()
// Create initial session with refresh token
refreshToken := "test-refresh-token"
session := &OIDCSession{
SessionID: "test-session",
UserDID: "did:sonr:testuser",
ClientID: "test-client",
Scope: "openid profile offline_access",
AccessToken: "old-access-token",
RefreshToken: refreshToken,
ExpiresAt: time.Now().Add(-1 * time.Hour), // Expired access token
CreatedAt: time.Now().Add(-2 * time.Hour),
}
oidcProvider.mu.Lock()
oidcProvider.sessions[refreshToken] = session
oidcProvider.mu.Unlock()
t.Run("ValidRefreshToken", func(t *testing.T) {
tokenReq := &OIDCTokenRequest{
GrantType: "refresh_token",
RefreshToken: refreshToken,
ClientID: "test-client",
}
req := httptest.NewRequest(http.MethodPost, "/oidc/token", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := handleRefreshTokenGrant(c, tokenReq)
assert.NoError(t, err)
if rec.Code == http.StatusOK {
var tokenResp OIDCTokenResponse
err = json.Unmarshal(rec.Body.Bytes(), &tokenResp)
assert.NoError(t, err)
assert.NotEmpty(t, tokenResp.AccessToken)
assert.NotEqual(t, "old-access-token", tokenResp.AccessToken)
}
})
}
-384
View File
@@ -1,384 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
// SIOPProvider manages Self-Issued OpenID Provider operations
type SIOPProvider struct {
// No persistent state needed for SIOP as it's self-issued
}
var siopProvider = &SIOPProvider{}
// HandleSIOPAuthorization handles SIOP authorization requests
func HandleSIOPAuthorization(c echo.Context) error {
var req SIOPRequest
// Check if request was already parsed by HandleSIOPRequest
if parsedReq := c.Get("siop_request"); parsedReq != nil {
req = *(parsedReq.(*SIOPRequest))
} else {
// Parse request parameters from query string for GET or form for POST
req = SIOPRequest{
ResponseType: c.QueryParam("response_type"),
ClientID: c.QueryParam("client_id"),
RedirectURI: c.QueryParam("redirect_uri"),
Scope: c.QueryParam("scope"),
Nonce: c.QueryParam("nonce"),
State: c.QueryParam("state"),
}
// Parse claims if provided
if claimsStr := c.QueryParam("claims"); claimsStr != "" {
_ = json.Unmarshal([]byte(claimsStr), &req.Claims) // Ignore claims parsing errors
}
// If no query params, try to bind from body (for POST requests)
if req.ResponseType == "" {
_ = c.Bind(&req) // Ignore bind errors and continue with empty request
}
}
// Validate required parameters
if req.ResponseType == "" || req.ClientID == "" || req.RedirectURI == "" ||
req.Scope == "" || req.Nonce == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing required parameters",
})
}
// Validate response type (SIOP v2 supports id_token)
if req.ResponseType != "id_token" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "unsupported_response_type",
"error_description": "SIOP only supports id_token response type",
})
}
// Validate scope includes openid
if !strings.Contains(req.Scope, "openid") {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_scope",
"error_description": "Scope must include 'openid'",
})
}
// Get user DID from context (assumes user is authenticated via WebAuthn)
userDID := c.Get("user_did")
if userDID == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "authentication_required",
"error_description": "User authentication required for SIOP",
})
}
// Generate Self-Issued ID Token
idToken, err := generateSelfIssuedIDToken(
userDID.(string),
req.ClientID,
req.Nonce,
req.RedirectURI,
req.Claims,
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to generate self-issued ID token",
})
}
// Build response
response := &SIOPResponse{
IDToken: idToken,
State: req.State,
}
// If VP token is requested, include it
if req.Claims.VPToken != nil {
vpToken, err := generateVPToken(userDID.(string), req.Claims.VPToken)
if err == nil {
response.VPToken = vpToken
}
}
// Redirect back to client with response
redirectURL, err := buildSIOPRedirectURL(req.RedirectURI, response)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "server_error",
"error_description": "Failed to build redirect URL",
})
}
return c.Redirect(http.StatusFound, redirectURL)
}
// HandleSIOPRegistration handles dynamic client registration for SIOP
func HandleSIOPRegistration(c echo.Context) error {
var registration SIOPRegistration
if err := c.Bind(&registration); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Invalid registration request",
})
}
// Validate registration parameters
if registration.ClientName == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Client name is required",
})
}
// Generate client ID for the registration
clientID := generateClientID()
// Store registration (in production, persist this)
// For now, return the registration confirmation
response := map[string]any{
"client_id": clientID,
"client_name": registration.ClientName,
"client_purpose": registration.ClientPurpose,
"logo_uri": registration.LogoURI,
"subject_syntax_types_supported": []string{"did"},
"vp_formats": map[string]any{
"jwt_vp": map[string]any{
"alg": []string{"ES256", "EdDSA"},
},
"ldp_vp": map[string]any{
"proof_type": []string{"Ed25519Signature2018"},
},
},
}
return c.JSON(http.StatusOK, response)
}
// HandleSIOPMetadata returns SIOP provider metadata
func HandleSIOPMetadata(c echo.Context) error {
metadata := map[string]any{
"issuer": "https://self-issued.me/v2",
"authorization_endpoint": "openid://",
"response_types_supported": []string{"id_token"},
"scopes_supported": []string{
"openid",
"profile",
"email",
"did",
},
"subject_types_supported": []string{"pairwise"},
"id_token_signing_alg_values_supported": []string{"ES256", "EdDSA"},
"request_object_signing_alg_values_supported": []string{"ES256", "EdDSA"},
"subject_syntax_types_supported": []string{
"did:key",
"did:web",
"did:ion",
},
"vp_formats_supported": map[string]any{
"jwt_vp": map[string]any{
"alg_values_supported": []string{"ES256", "EdDSA"},
},
"ldp_vp": map[string]any{
"proof_type_values_supported": []string{"Ed25519Signature2018"},
},
},
}
return c.JSON(http.StatusOK, metadata)
}
// generateSelfIssuedIDToken generates a self-issued ID token
func generateSelfIssuedIDToken(
userDID, clientID, nonce, redirectURI string,
requestedClaims SIOPClaims,
) (string, error) {
// Create base claims
claims := jwt.MapClaims{
"iss": "https://self-issued.me/v2",
"sub": userDID,
"aud": clientID,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"nonce": nonce,
"_sd_alg": "sha-256", // Selective disclosure algorithm
"sub_jwk": map[string]any{
// TODO: Include actual public key from DID document
"kty": "EC",
"crv": "P-256",
"x": "placeholder_x",
"y": "placeholder_y",
},
}
// Add requested claims from ID token
if requestedClaims.IDToken != nil {
for claimName := range requestedClaims.IDToken {
// Add claim based on request
switch claimName {
case "email":
claims["email"] = "user@example.com" // TODO: Get from DID document
claims["email_verified"] = true
case "name":
claims["name"] = "User Name" // TODO: Get from DID document
case "did":
claims["did"] = userDID
}
}
}
// Sign with user's DID key (simplified for now)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-siop-key"))
}
// generateVPToken generates a Verifiable Presentation token
func generateVPToken(userDID string, vpClaims map[string]ClaimRequest) (string, error) {
// Create VP structure
vp := map[string]any{
"@context": []string{
"https://www.w3.org/2018/credentials/v1",
},
"type": []string{"VerifiablePresentation"},
"holder": userDID,
"verifiableCredential": []any{
// TODO: Include actual verifiable credentials
},
}
// Convert to JWT
vpJSON, err := json.Marshal(vp)
if err != nil {
return "", err
}
// Sign VP (simplified for now)
claims := jwt.MapClaims{
"vp": string(vpJSON),
"iss": userDID,
"aud": "verifier",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"nonce": generateNonce(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("temporary-vp-key"))
}
// buildSIOPRedirectURL builds the redirect URL with SIOP response
func buildSIOPRedirectURL(redirectURI string, response *SIOPResponse) (string, error) {
u, err := url.Parse(redirectURI)
if err != nil {
return "", err
}
// Add response parameters
q := u.Query()
q.Set("id_token", response.IDToken)
if response.VPToken != "" {
q.Set("vp_token", response.VPToken)
}
if response.State != "" {
q.Set("state", response.State)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// generateClientID generates a unique client ID
func generateClientID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return fmt.Sprintf("siop_client_%s", base64.RawURLEncoding.EncodeToString(bytes))
}
// generateNonce generates a nonce for tokens
func generateNonce() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return ""
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
// ValidateSIOPRequest validates an incoming SIOP request
func ValidateSIOPRequest(requestURI string) (*SIOPRequest, error) {
// Parse the request URI
u, err := url.Parse(requestURI)
if err != nil {
return nil, fmt.Errorf("invalid request URI: %w", err)
}
// Validate that it's a proper URI with scheme and host
if u.Scheme == "" && u.Host == "" {
return nil, fmt.Errorf("invalid request URI: missing scheme or host")
}
// Extract parameters
q := u.Query()
req := &SIOPRequest{
ResponseType: q.Get("response_type"),
ClientID: q.Get("client_id"),
RedirectURI: q.Get("redirect_uri"),
Scope: q.Get("scope"),
Nonce: q.Get("nonce"),
State: q.Get("state"),
}
// Parse claims if present
if claimsStr := q.Get("claims"); claimsStr != "" {
if err := json.Unmarshal([]byte(claimsStr), &req.Claims); err != nil {
return nil, fmt.Errorf("invalid claims parameter: %w", err)
}
}
// Parse registration if present
if regStr := q.Get("registration"); regStr != "" {
if err := json.Unmarshal([]byte(regStr), &req.Registration); err != nil {
return nil, fmt.Errorf("invalid registration parameter: %w", err)
}
}
return req, nil
}
// HandleSIOPRequest processes a complete SIOP request flow
func HandleSIOPRequest(c echo.Context) error {
// Get request URI from query parameter
requestURI := c.QueryParam("request_uri")
if requestURI == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": "Missing request_uri parameter",
})
}
// Validate and parse the request
req, err := ValidateSIOPRequest(requestURI)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid_request",
"error_description": err.Error(),
})
}
// Process as regular SIOP authorization
c.Set("siop_request", req)
return HandleSIOPAuthorization(c)
}
-353
View File
@@ -1,353 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
// TestSIOPAuthorization tests Self-Issued OpenID Provider authorization
func TestSIOPAuthorization(t *testing.T) {
e := echo.New()
t.Run("ValidSIOPRequest", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid did")
q.Set("nonce", "test-nonce")
q.Set("state", "test-state")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Set authenticated user DID
c.Set("user_did", "did:sonr:testuser")
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
// Should redirect with ID token
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "id_token=")
assert.Contains(t, location, "state=test-state")
})
t.Run("InvalidResponseType", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "code") // SIOP only supports id_token
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "unsupported_response_type", errorResp["error"])
})
t.Run("MissingScopeOpenID", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "profile email") // Missing 'openid'
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_scope", errorResp["error"])
})
t.Run("UnauthenticatedUser", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// No user_did set - user not authenticated
err := HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "authentication_required", errorResp["error"])
})
}
// TestSIOPRegistration tests dynamic client registration for SIOP
func TestSIOPRegistration(t *testing.T) {
e := echo.New()
t.Run("ValidRegistration", func(t *testing.T) {
registration := map[string]any{
"client_name": "Test SIOP Client",
"client_purpose": "Testing SIOP functionality",
"logo_uri": "https://example.com/logo.png",
}
body, _ := json.Marshal(registration)
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRegistration(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var response map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response["client_id"])
assert.Equal(t, "Test SIOP Client", response["client_name"])
assert.Contains(t, response["subject_syntax_types_supported"], "did")
})
t.Run("MissingClientName", func(t *testing.T) {
registration := map[string]any{
"client_purpose": "Testing",
}
body, _ := json.Marshal(registration)
req := httptest.NewRequest(http.MethodPost, "/siop/register", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRegistration(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
assert.Contains(t, errorResp["error_description"], "Client name is required")
})
}
// TestSIOPMetadata tests SIOP metadata endpoint
func TestSIOPMetadata(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/siop/metadata", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPMetadata(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
var metadata map[string]any
err = json.Unmarshal(rec.Body.Bytes(), &metadata)
assert.NoError(t, err)
// Verify required SIOP metadata fields
assert.Equal(t, "https://self-issued.me/v2", metadata["issuer"])
assert.Equal(t, "openid://", metadata["authorization_endpoint"])
assert.Contains(t, metadata["response_types_supported"], "id_token")
assert.Contains(t, metadata["scopes_supported"], "openid")
assert.Contains(t, metadata["subject_types_supported"], "pairwise")
assert.NotNil(t, metadata["vp_formats_supported"])
}
// TestValidateSIOPRequest tests SIOP request validation
func TestValidateSIOPRequest(t *testing.T) {
t.Run("ValidRequest", func(t *testing.T) {
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid+did" +
"&nonce=test-nonce" +
"&state=test-state"
req, err := ValidateSIOPRequest(requestURI)
assert.NoError(t, err)
assert.NotNil(t, req)
assert.Equal(t, "id_token", req.ResponseType)
assert.Equal(t, "test-client", req.ClientID)
assert.Equal(t, "openid did", req.Scope)
assert.Equal(t, "test-nonce", req.Nonce)
})
t.Run("InvalidURI", func(t *testing.T) {
req, err := ValidateSIOPRequest("not-a-valid-uri")
assert.Error(t, err)
assert.Nil(t, req)
})
t.Run("WithClaims", func(t *testing.T) {
claims := map[string]any{
"id_token": map[string]any{
"email": map[string]any{
"essential": true,
},
"name": nil,
},
}
claimsJSON, _ := json.Marshal(claims)
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid" +
"&nonce=test-nonce" +
"&claims=" + url.QueryEscape(string(claimsJSON))
req, err := ValidateSIOPRequest(requestURI)
assert.NoError(t, err)
assert.NotNil(t, req)
assert.NotNil(t, req.Claims.IDToken)
})
}
// TestSIOPWithVPToken tests SIOP with Verifiable Presentation
func TestSIOPWithVPToken(t *testing.T) {
e := echo.New()
vpClaims := map[string]any{
"vp_token": map[string]any{
"presentation_definition": map[string]any{
"id": "test-presentation",
"input_descriptors": []map[string]any{
{
"id": "id_credential",
"constraints": map[string]any{
"fields": []map[string]any{
{
"path": []string{"$.type"},
"filter": map[string]any{
"type": "string",
"const": "IdentityCredential",
},
},
},
},
},
},
},
},
}
claimsJSON, _ := json.Marshal(vpClaims)
req := httptest.NewRequest(http.MethodPost, "/siop/authorize", nil)
q := req.URL.Query()
q.Set("response_type", "id_token")
q.Set("client_id", "test-client")
q.Set("redirect_uri", "http://localhost:3000/callback")
q.Set("scope", "openid")
q.Set("nonce", "test-nonce")
q.Set("claims", string(claimsJSON))
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
// Parse request to set claims
siopReq := &SIOPRequest{
ResponseType: "id_token",
ClientID: "test-client",
RedirectURI: "http://localhost:3000/callback",
Scope: "openid",
Nonce: "test-nonce",
}
err := json.Unmarshal(claimsJSON, &siopReq.Claims)
assert.NoError(t, err)
c.Set("siop_request", siopReq)
err = HandleSIOPAuthorization(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusFound, rec.Code)
location := rec.Header().Get("Location")
assert.Contains(t, location, "id_token=")
// VP token generation is optional, so we don't assert its presence
}
// TestHandleSIOPRequest tests complete SIOP request flow
func TestHandleSIOPRequest(t *testing.T) {
e := echo.New()
t.Run("ValidRequestURI", func(t *testing.T) {
requestURI := "openid://?" +
"response_type=id_token" +
"&client_id=test-client" +
"&redirect_uri=" + url.QueryEscape("http://localhost:3000/callback") +
"&scope=openid" +
"&nonce=test-nonce"
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
q := req.URL.Query()
q.Set("request_uri", requestURI)
req.URL.RawQuery = q.Encode()
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.Set("user_did", "did:sonr:testuser")
err := HandleSIOPRequest(c)
assert.NoError(t, err)
// Should process as authorization request
assert.Equal(t, http.StatusFound, rec.Code)
})
t.Run("MissingRequestURI", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/siop/request", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
err := HandleSIOPRequest(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, rec.Code)
var errorResp map[string]string
err = json.Unmarshal(rec.Body.Bytes(), &errorResp)
assert.NoError(t, err)
assert.Equal(t, "invalid_request", errorResp["error"])
assert.Contains(t, errorResp["error_description"], "Missing request_uri")
})
}
-265
View File
@@ -1,265 +0,0 @@
package handlers
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
// WebAuthnCredential represents a WebAuthn credential
type WebAuthnCredential struct {
CredentialID string `json:"credential_id"`
RawID string `json:"raw_id"`
ClientDataJSON string `json:"client_data_json"`
AttestationObject string `json:"attestation_object"`
Username string `json:"username"`
Origin string `json:"origin"`
PublicKey []byte `json:"public_key"`
Algorithm int32 `json:"algorithm"`
CreatedAt time.Time `json:"created_at"`
}
// WebAuthnRegistrationRequest represents a registration request
type WebAuthnRegistrationRequest struct {
Username string `json:"username"`
Challenge string `json:"challenge,omitempty"`
AutoCreateVault bool `json:"auto_create_vault"`
BroadcastToChain bool `json:"broadcast_to_chain"`
}
// WebAuthnAuthenticationRequest represents an authentication request
type WebAuthnAuthenticationRequest struct {
Username string `json:"username"`
Challenge string `json:"challenge,omitempty"`
}
// WebAuthnRegistrationResponse contains registration ceremony data
type WebAuthnRegistrationResponse struct {
Challenge string `json:"challenge"`
RP WebAuthnRPEntity `json:"rp"`
User WebAuthnUserEntity `json:"user"`
PubKeyCredParams []WebAuthnCredParam `json:"pubKeyCredParams"`
AuthenticatorSelection WebAuthnAuthenticatorSelection `json:"authenticatorSelection"`
Timeout int `json:"timeout"`
Attestation string `json:"attestation"`
}
// WebAuthnAuthenticationResponse contains authentication ceremony data
type WebAuthnAuthenticationResponse struct {
Challenge string `json:"challenge"`
Timeout int `json:"timeout"`
RPID string `json:"rpId"`
AllowCredentials []WebAuthnAllowedCred `json:"allowCredentials"`
UserVerification string `json:"userVerification"`
}
// WebAuthnRPEntity represents relying party info
type WebAuthnRPEntity struct {
ID string `json:"id"`
Name string `json:"name"`
}
// WebAuthnUserEntity represents user info
type WebAuthnUserEntity struct {
ID string `json:"id"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
}
// WebAuthnCredParam represents credential parameters
type WebAuthnCredParam struct {
Type string `json:"type"`
Alg int `json:"alg"`
}
// WebAuthnAuthenticatorSelection represents authenticator requirements
type WebAuthnAuthenticatorSelection struct {
AuthenticatorAttachment string `json:"authenticatorAttachment,omitempty"`
UserVerification string `json:"userVerification"`
ResidentKey string `json:"residentKey,omitempty"`
}
// WebAuthnAllowedCred represents allowed credentials for authentication
type WebAuthnAllowedCred struct {
Type string `json:"type"`
ID string `json:"id"`
}
// OIDCConfig represents OpenID Connect configuration
type OIDCConfig struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
JWKSEndpoint string `json:"jwks_uri"`
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// OIDCAuthorizationRequest represents an authorization request
type OIDCAuthorizationRequest struct {
ResponseType string `json:"response_type"`
ClientID string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
State string `json:"state"`
Nonce string `json:"nonce,omitempty"`
CodeChallenge string `json:"code_challenge,omitempty"`
CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
}
// OIDCTokenRequest represents a token request
type OIDCTokenRequest struct {
GrantType string `json:"grant_type"`
Code string `json:"code,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
CodeVerifier string `json:"code_verifier,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// OIDCTokenResponse represents a token response
type OIDCTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
Scope string `json:"scope,omitempty"`
}
// OIDCUserInfo represents user information
type OIDCUserInfo struct {
Subject string `json:"sub"`
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
DID string `json:"did,omitempty"`
VaultID string `json:"vault_id,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Claims map[string]any `json:"claims,omitempty"`
}
// JWKSet represents a JSON Web Key Set
type JWKSet struct {
Keys []JWK `json:"keys"`
}
// JWK represents a JSON Web Key
type JWK struct {
KeyType string `json:"kty"`
Use string `json:"use,omitempty"`
KeyID string `json:"kid"`
Algorithm string `json:"alg,omitempty"`
N string `json:"n,omitempty"` // RSA modulus
E string `json:"e,omitempty"` // RSA exponent
X string `json:"x,omitempty"` // EC x coordinate
Y string `json:"y,omitempty"` // EC y coordinate
Curve string `json:"crv,omitempty"` // EC curve
}
// SIOPRequest represents a Self-Issued OpenID Provider request
type SIOPRequest struct {
ResponseType string `json:"response_type"`
ClientID string `json:"client_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
Nonce string `json:"nonce"`
State string `json:"state,omitempty"`
Claims SIOPClaims `json:"claims,omitempty"`
Registration SIOPRegistration `json:"registration,omitempty"`
}
// SIOPClaims represents claims requested in SIOP
type SIOPClaims struct {
IDToken map[string]ClaimRequest `json:"id_token,omitempty"`
VPToken map[string]ClaimRequest `json:"vp_token,omitempty"`
}
// ClaimRequest represents a claim request
type ClaimRequest struct {
Essential bool `json:"essential,omitempty"`
Value string `json:"value,omitempty"`
Values []string `json:"values,omitempty"`
}
// SIOPRegistration represents client registration in SIOP
type SIOPRegistration struct {
ClientName string `json:"client_name,omitempty"`
ClientPurpose string `json:"client_purpose,omitempty"`
LogoURI string `json:"logo_uri,omitempty"`
SubjectSyntaxTypesSupported []string `json:"subject_syntax_types_supported,omitempty"`
VPFormats map[string]any `json:"vp_formats,omitempty"`
}
// SIOPResponse represents a Self-Issued OpenID Provider response
type SIOPResponse struct {
IDToken string `json:"id_token"`
VPToken string `json:"vp_token,omitempty"`
State string `json:"state,omitempty"`
}
// DIDAuthClaims represents DID-based authentication claims
type DIDAuthClaims struct {
jwt.RegisteredClaims
DID string `json:"did"`
Challenge string `json:"challenge,omitempty"`
WebAuthn *WebAuthnCredential `json:"webauthn,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// AuthorizationCode represents an OAuth2 authorization code
type AuthorizationCode struct {
Code string
ClientID string
RedirectURI string
UserDID string
Scope string
Nonce string
CodeChallenge string
CodeChallengeMethod string
ExpiresAt time.Time
Used bool
}
// OIDCSession represents an active OIDC session
type OIDCSession struct {
SessionID string
UserDID string
ClientID string
Scope string
Nonce string
AccessToken string
RefreshToken string
IDToken string
ExpiresAt time.Time
CreatedAt time.Time
}
// BroadcastRequest represents a blockchain broadcast request
type BroadcastRequest struct {
Message any `json:"message"`
Gasless bool `json:"gasless"`
AutoSign bool `json:"auto_sign"`
FromAddress string `json:"from_address,omitempty"`
}
// BroadcastResponse represents a blockchain broadcast response
type BroadcastResponse struct {
TxHash string `json:"tx_hash"`
Height int64 `json:"height,omitempty"`
Code uint32 `json:"code,omitempty"`
RawLog string `json:"raw_log,omitempty"`
Success bool `json:"success"`
}
-602
View File
@@ -1,602 +0,0 @@
package handlers
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/golang-jwt/jwt/v5"
"github.com/sonr-io/sonr/crypto/ucan"
"github.com/sonr-io/sonr/x/did/types"
)
// BlockchainUCANSigner implements UCAN signing with blockchain keys
type BlockchainUCANSigner struct {
didKeeper DIDKeeperInterface
issuerDID string
signingMethod jwt.SigningMethod
privateKey any
}
// DIDKeeperInterface defines the minimal interface needed from DID keeper
type DIDKeeperInterface interface {
GetDIDDocument(ctx context.Context, did string) (*types.DIDDocument, error)
GetVerificationMethod(
ctx context.Context,
did string,
methodID string,
) (*types.VerificationMethod, error)
}
// NewBlockchainUCANSigner creates a new blockchain-integrated UCAN signer
func NewBlockchainUCANSigner(
didKeeper DIDKeeperInterface,
issuerDID string,
) (*BlockchainUCANSigner, error) {
return &BlockchainUCANSigner{
didKeeper: didKeeper,
issuerDID: issuerDID,
signingMethod: jwt.SigningMethodES256, // Default to ES256
}, nil
}
// Sign signs a UCAN token with blockchain keys
func (s *BlockchainUCANSigner) Sign(token *ucan.Token) (string, error) {
// Build JWT claims from UCAN token
claims := s.buildClaims(token)
// Get signing key
signingKey, method, err := s.getSigningKey(context.Background())
if err != nil {
return "", fmt.Errorf("failed to get signing key: %w", err)
}
// Create JWT token
jwtToken := jwt.NewWithClaims(method, claims)
// Sign token
signedToken, err := jwtToken.SignedString(signingKey)
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}
return signedToken, nil
}
// GetIssuerDID returns the issuer DID
func (s *BlockchainUCANSigner) GetIssuerDID() string {
if s.issuerDID == "" {
return "did:sonr:oauth-provider"
}
return s.issuerDID
}
// buildClaims builds JWT claims from UCAN token
func (s *BlockchainUCANSigner) buildClaims(token *ucan.Token) jwt.MapClaims {
claims := jwt.MapClaims{
"iss": token.Issuer,
"aud": token.Audience,
"exp": token.ExpiresAt,
"iat": time.Now().Unix(),
"nbf": token.NotBefore,
}
// Add attenuations
if len(token.Attenuations) > 0 {
attClaims := make([]map[string]any, len(token.Attenuations))
for i, att := range token.Attenuations {
attClaims[i] = s.serializeAttenuation(att)
}
claims["att"] = attClaims
}
// Add proofs if present
if len(token.Proofs) > 0 {
proofClaims := make([]string, len(token.Proofs))
for i, proof := range token.Proofs {
proofClaims[i] = string(proof)
}
claims["prf"] = proofClaims
}
// Add facts if present
if len(token.Facts) > 0 {
factClaims := make([]json.RawMessage, len(token.Facts))
for i, fact := range token.Facts {
factClaims[i] = fact.Data
}
claims["fct"] = factClaims
}
// Add UCAN version
claims["ucv"] = "0.10.0"
return claims
}
// serializeAttenuation serializes an attenuation for JWT claims
func (s *BlockchainUCANSigner) serializeAttenuation(att ucan.Attenuation) map[string]any {
result := map[string]any{
"with": att.Resource.GetURI(),
}
// Handle capability serialization
actions := att.Capability.GetActions()
if len(actions) == 1 {
result["can"] = actions[0]
} else {
result["can"] = actions
}
// Add resource-specific metadata
scheme := att.Resource.GetScheme()
switch scheme {
case "vault":
result["type"] = "vault_operation"
case "service", "svc":
result["type"] = "service_operation"
case "did":
result["type"] = "identity_operation"
case "dwn":
result["type"] = "data_operation"
case "dex", "pool":
result["type"] = "trading_operation"
}
return result
}
// getSigningKey retrieves the signing key from blockchain
func (s *BlockchainUCANSigner) getSigningKey(ctx context.Context) (any, jwt.SigningMethod, error) {
// If we have a cached private key, use it
if s.privateKey != nil {
return s.privateKey, s.signingMethod, nil
}
// For OAuth provider, use a service key
if s.issuerDID == "did:sonr:oauth-provider" || s.issuerDID == "" {
// Generate or retrieve service key
privateKey, publicKey, err := s.generateServiceKey()
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodEdDSA // Update to EdDSA for Ed25519 keys
_ = publicKey // Store public key if needed
return privateKey, jwt.SigningMethodEdDSA, nil
}
// For DID-based signing, retrieve from DID document
if s.didKeeper != nil {
didDoc, err := s.didKeeper.GetDIDDocument(ctx, s.issuerDID)
if err != nil {
return nil, nil, fmt.Errorf("failed to get DID document: %w", err)
}
// Use the first verification method for signing
if len(didDoc.VerificationMethod) > 0 {
vm := didDoc.VerificationMethod[0]
// Extract key based on verification method kind
switch vm.VerificationMethodKind {
case "Ed25519VerificationKey2020":
// Extract Ed25519 key
privateKey, err := s.extractEd25519Key(vm)
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodEdDSA
return privateKey, jwt.SigningMethodEdDSA, nil
case "EcdsaSecp256k1VerificationKey2019":
// Extract Secp256k1 key
privateKey, err := s.extractSecp256k1Key(vm)
if err != nil {
return nil, nil, err
}
s.privateKey = privateKey
s.signingMethod = jwt.SigningMethodES256
return privateKey, jwt.SigningMethodES256, nil
default:
return nil, nil, fmt.Errorf(
"unsupported verification method type: %s",
vm.VerificationMethodKind,
)
}
}
}
return nil, nil, fmt.Errorf("no signing key available")
}
// generateServiceKey generates a new service key pair
func (s *BlockchainUCANSigner) generateServiceKey() (ed25519.PrivateKey, ed25519.PublicKey, error) {
// In production, this should retrieve from secure storage
// For now, generate a new key pair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate key pair: %w", err)
}
return privateKey, publicKey, nil
}
// extractEd25519Key extracts Ed25519 private key from verification method
func (s *BlockchainUCANSigner) extractEd25519Key(
vm *types.VerificationMethod,
) (ed25519.PrivateKey, error) {
// In production, this would retrieve the private key from secure storage
// based on the public key in the verification method
// For now, return error as we don't have access to private keys
return nil, fmt.Errorf("private key retrieval not implemented")
}
// extractSecp256k1Key extracts Secp256k1 private key from verification method
func (s *BlockchainUCANSigner) extractSecp256k1Key(
vm *types.VerificationMethod,
) (*secp256k1.PrivKey, error) {
// In production, this would retrieve the private key from secure storage
// based on the public key in the verification method
// For now, return error as we don't have access to private keys
return nil, fmt.Errorf("private key retrieval not implemented")
}
// VerifySignature verifies a UCAN token signature
func (s *BlockchainUCANSigner) VerifySignature(tokenString string) (*ucan.Token, error) {
// Parse token without verification first to get issuer
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims format")
}
issuer, ok := claims["iss"].(string)
if !ok {
return nil, fmt.Errorf("no issuer in token")
}
// Get public key for issuer
publicKey, err := s.getPublicKey(context.Background(), issuer)
if err != nil {
return nil, fmt.Errorf("failed to get public key: %w", err)
}
// Verify token with public key
parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
return publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("token verification failed: %w", err)
}
if !parsedToken.Valid {
return nil, fmt.Errorf("invalid token")
}
// Convert JWT claims to UCAN token
return s.claimsToUCAN(claims, tokenString)
}
// getPublicKey retrieves public key for a DID
func (s *BlockchainUCANSigner) getPublicKey(ctx context.Context, did string) (any, error) {
if did == "did:sonr:oauth-provider" {
// Return service public key
// In production, this would be retrieved from configuration
return []byte("oauth-provider-public-key"), nil
}
if s.didKeeper != nil {
didDoc, err := s.didKeeper.GetDIDDocument(ctx, did)
if err != nil {
return nil, fmt.Errorf("failed to get DID document: %w", err)
}
if len(didDoc.VerificationMethod) > 0 {
vm := didDoc.VerificationMethod[0]
// Extract public key based on verification method kind
switch vm.VerificationMethodKind {
case "Ed25519VerificationKey2020":
// Decode base64 public key
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
if err != nil {
return nil, err
}
return ed25519.PublicKey(publicKeyBytes), nil
case "EcdsaSecp256k1VerificationKey2019":
// Decode and return Secp256k1 public key
publicKeyBytes, err := base64.StdEncoding.DecodeString(vm.PublicKeyMultibase)
if err != nil {
return nil, err
}
return publicKeyBytes, nil
default:
return nil, fmt.Errorf(
"unsupported verification method type: %s",
vm.VerificationMethodKind,
)
}
}
}
return nil, fmt.Errorf("no public key found for DID: %s", did)
}
// claimsToUCAN converts JWT claims to UCAN token
func (s *BlockchainUCANSigner) claimsToUCAN(
claims jwt.MapClaims,
rawToken string,
) (*ucan.Token, error) {
token := &ucan.Token{
Raw: rawToken,
}
// Extract standard claims
if iss, ok := claims["iss"].(string); ok {
token.Issuer = iss
}
if aud, ok := claims["aud"].(string); ok {
token.Audience = aud
}
if exp, ok := claims["exp"].(float64); ok {
token.ExpiresAt = int64(exp)
}
if nbf, ok := claims["nbf"].(float64); ok {
token.NotBefore = int64(nbf)
}
// Extract attenuations
if attClaims, ok := claims["att"].([]any); ok {
attenuations := make([]ucan.Attenuation, 0, len(attClaims))
for _, attItem := range attClaims {
if attMap, ok := attItem.(map[string]any); ok {
att, err := s.parseAttenuation(attMap)
if err != nil {
return nil, fmt.Errorf("failed to parse attenuation: %w", err)
}
attenuations = append(attenuations, att)
}
}
token.Attenuations = attenuations
}
// Extract proofs
if proofClaims, ok := claims["prf"].([]any); ok {
proofs := make([]ucan.Proof, 0, len(proofClaims))
for _, proofItem := range proofClaims {
if proofStr, ok := proofItem.(string); ok {
proofs = append(proofs, ucan.Proof(proofStr))
}
}
token.Proofs = proofs
}
// Extract facts
if factClaims, ok := claims["fct"].([]any); ok {
facts := make([]ucan.Fact, 0, len(factClaims))
for _, factItem := range factClaims {
factData, _ := json.Marshal(factItem)
facts = append(facts, ucan.Fact{
Data: json.RawMessage(factData),
})
}
token.Facts = facts
}
return token, nil
}
// parseAttenuation parses an attenuation from JWT claims
func (s *BlockchainUCANSigner) parseAttenuation(attMap map[string]any) (ucan.Attenuation, error) {
// Extract resource URI
resourceURI, ok := attMap["with"].(string)
if !ok {
return ucan.Attenuation{}, fmt.Errorf("missing resource URI")
}
// Parse resource
resource := &SimpleResource{
Scheme: "generic",
Value: resourceURI,
}
// Extract scheme from URI if possible
if len(resourceURI) > 0 {
for _, scheme := range []string{"vault:", "service:", "did:", "dwn:", "dex:", "pool:"} {
if len(resourceURI) >= len(scheme) && resourceURI[:len(scheme)] == scheme {
resource.Scheme = scheme[:len(scheme)-1]
resource.Value = resourceURI[len(scheme):]
break
}
}
}
// Extract capability
var capability ucan.Capability
switch can := attMap["can"].(type) {
case string:
capability = &ucan.SimpleCapability{Action: can}
case []any:
actions := make([]string, 0, len(can))
for _, action := range can {
if actionStr, ok := action.(string); ok {
actions = append(actions, actionStr)
}
}
capability = &ucan.MultiCapability{Actions: actions}
default:
return ucan.Attenuation{}, fmt.Errorf("invalid capability format")
}
return ucan.Attenuation{
Capability: capability,
Resource: resource,
}, nil
}
// SetPrivateKey sets a private key for signing (for testing)
func (s *BlockchainUCANSigner) SetPrivateKey(privateKey any, method jwt.SigningMethod) {
s.privateKey = privateKey
s.signingMethod = method
}
// CreateDelegationToken creates a UCAN token for delegation
func (s *BlockchainUCANSigner) CreateDelegationToken(
issuer, audience string,
attenuations []ucan.Attenuation,
proofs []ucan.Proof,
expiresIn time.Duration,
) (string, error) {
token := &ucan.Token{
Issuer: issuer,
Audience: audience,
ExpiresAt: time.Now().Add(expiresIn).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: attenuations,
Proofs: proofs,
}
return s.Sign(token)
}
// ValidateDelegationChain validates a chain of UCAN delegations
func (s *BlockchainUCANSigner) ValidateDelegationChain(tokens []string) error {
if len(tokens) == 0 {
return fmt.Errorf("empty delegation chain")
}
var previousToken *ucan.Token
for i, tokenString := range tokens {
// Verify current token
token, err := s.VerifySignature(tokenString)
if err != nil {
return fmt.Errorf("failed to verify token %d: %w", i, err)
}
// Check expiration
if time.Now().Unix() > token.ExpiresAt {
return fmt.Errorf("token %d has expired", i)
}
// Check not before
if token.NotBefore > 0 && time.Now().Unix() < token.NotBefore {
return fmt.Errorf("token %d not yet valid", i)
}
// Validate delegation chain
if previousToken != nil {
// Check that previous token's audience matches current issuer
if previousToken.Audience != token.Issuer {
return fmt.Errorf("broken delegation chain at token %d", i)
}
// Check that capabilities are properly attenuated
if !s.isProperlyAttenuated(previousToken.Attenuations, token.Attenuations) {
return fmt.Errorf("improper attenuation at token %d", i)
}
}
previousToken = token
}
return nil
}
// isProperlyAttenuated checks if child attenuations are properly attenuated from parent
func (s *BlockchainUCANSigner) isProperlyAttenuated(parent, child []ucan.Attenuation) bool {
// Child cannot have more permissions than parent
for _, childAtt := range child {
found := false
for _, parentAtt := range parent {
// Check if child attenuation is covered by parent
if s.attenuationCovers(parentAtt, childAtt) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// attenuationCovers checks if parent attenuation covers child
func (s *BlockchainUCANSigner) attenuationCovers(parent, child ucan.Attenuation) bool {
// Check resource match
if parent.Resource.GetScheme() != child.Resource.GetScheme() {
// Wildcard scheme matches all
if parent.Resource.GetScheme() != "*" {
return false
}
}
// Check capability coverage
parentActions := parent.Capability.GetActions()
childActions := child.Capability.GetActions()
// If parent has wildcard, it covers everything
for _, action := range parentActions {
if action == "*" {
return true
}
}
// Check each child action is in parent
for _, childAction := range childActions {
found := false
for _, parentAction := range parentActions {
if parentAction == childAction {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// RefreshToken creates a new token from an existing one with updated expiration
func (s *BlockchainUCANSigner) RefreshToken(
tokenString string,
newExpiration time.Duration,
) (string, error) {
// Verify existing token
token, err := s.VerifySignature(tokenString)
if err != nil {
return "", fmt.Errorf("failed to verify token: %w", err)
}
// Create new token with same claims but new expiration
newToken := &ucan.Token{
Issuer: token.Issuer,
Audience: token.Audience,
ExpiresAt: time.Now().Add(newExpiration).Unix(),
NotBefore: time.Now().Unix(),
Attenuations: token.Attenuations,
Proofs: append(token.Proofs, ucan.Proof(tokenString)), // Add original as proof
Facts: token.Facts,
}
return s.Sign(newToken)
}
-535
View File
@@ -1,535 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/bridge/tasks"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/types/ipfs"
)
// VaultHandlers holds all vault-related handlers and their dependencies
type VaultHandlers struct {
IPFSClient ipfs.IPFSClient
ConnectionManager *ConnectionManager
SSEManager *SSEManager
}
// NewVaultHandlers creates a new VaultHandlers instance
func NewVaultHandlers(
ipfsClient ipfs.IPFSClient,
connManager *ConnectionManager,
sseManager *SSEManager,
) *VaultHandlers {
return &VaultHandlers{
IPFSClient: ipfsClient,
ConnectionManager: connManager,
SSEManager: sseManager,
}
}
// GetQueueFromPriority determines the appropriate queue based on priority
func GetQueueFromPriority(priority string) string {
switch priority {
case "critical", "high":
return "critical"
case "low":
return "low"
default:
return "default"
}
}
// GenerateHandler handles vault generation requests
func (vh *VaultHandlers) GenerateHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
// Extract user info from JWT token
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
userID := claims["user_id"].(string)
var req struct {
UserID int `json:"user_id"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
task, err := tasks.NewUCANDIDTask(req.UserID)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
// Broadcast task status to WebSocket and SSE connections
go func() {
message := TaskStatusMessage{
TaskID: info.ID,
Status: "enqueued",
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, message)
vh.SSEManager.BroadcastToSSE(info.ID, message)
// Simulate task progression for demonstration
// In a real implementation, this would be triggered by the actual task processors
time.Sleep(1 * time.Second)
processingMessage := TaskStatusMessage{
TaskID: info.ID,
Status: "processing",
Progress: 50,
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, processingMessage)
vh.SSEManager.BroadcastToSSE(info.ID, processingMessage)
// Simulate completion
time.Sleep(2 * time.Second)
completedMessage := TaskStatusMessage{
TaskID: info.ID,
Status: "completed",
Progress: 100,
Time: time.Now(),
}
vh.ConnectionManager.BroadcastToTask(info.ID, completedMessage)
vh.SSEManager.BroadcastToSSE(info.ID, completedMessage)
}()
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
"user_id": userID,
})
}
}
// SignHandler handles vault signing requests
func (vh *VaultHandlers) SignHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Message []byte `json:"message"`
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data (fallback)
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password,omitempty"` // Decryption password for IPFS stored data
UCANToken string `json:"ucan_token,omitempty"` // UCAN token for authorization
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.Message) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Message is required"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
task, err := tasks.NewUCANSignTask(
0,
req.Message,
) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// VerifyHandler handles vault verification requests
func (vh *VaultHandlers) VerifyHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
PublicKey []byte `json:"public_key"`
Message []byte `json:"message"`
Signature []byte `json:"signature"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.PublicKey) == 0 || len(req.Message) == 0 || len(req.Signature) == 0 {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "PublicKey, message, and signature are required"},
)
}
task, err := tasks.NewUCANVerifyTask(
0,
req.Message,
req.Signature,
) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// ExportHandler handles vault export requests
func (vh *VaultHandlers) ExportHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password"` // For encryption/decryption
StoreIPFS bool `json:"store_ipfs,omitempty"` // Whether to store result in IPFS
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if len(req.Password) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Password is required"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
// If store_ipfs is true, encrypt and store the enclave in IPFS first
if req.StoreIPFS && vh.IPFSClient != nil {
encryptedData, err := enclave.Encrypt(req.Password)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to encrypt enclave data"},
)
}
cid, err := vh.IPFSClient.Add(encryptedData)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to store enclave in IPFS"},
)
}
// Return the CID for future reference
return c.JSON(http.StatusOK, map[string]any{
"cid": cid,
"status": "stored",
"message": "Enclave data encrypted and stored in IPFS",
})
}
// Export functionality replaced with UCAN token creation for data access
// Convert export operation to UCAN token generation with export permissions
attenuations := []map[string]any{
{
"can": []string{"export", "read"},
"with": "vault://exported-data",
},
}
task, err := tasks.NewUCANTokenTask(
0,
"did:sonr:export-recipient",
attenuations,
time.Now().Add(24*time.Hour).Unix(),
)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// ImportHandler handles vault import requests
func (vh *VaultHandlers) ImportHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
CID string `json:"cid"`
Password []byte `json:"password"`
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
if req.CID == "" || len(req.Password) == 0 {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "CID and password are required"},
)
}
// Import functionality replaced with UCAN token creation for data import
// Convert import operation to UCAN token generation with import permissions
attenuations := []map[string]any{
{
"can": []string{"import", "write"},
"with": fmt.Sprintf("ipfs://%s", req.CID),
},
}
task, err := tasks.NewUCANTokenTask(
0,
"did:sonr:import-recipient",
attenuations,
time.Now().Add(1*time.Hour).Unix(),
)
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
// RefreshHandler handles vault refresh requests
func (vh *VaultHandlers) RefreshHandler(client *asynq.Client) echo.HandlerFunc {
return func(c echo.Context) error {
var req struct {
Enclave *mpc.EnclaveData `json:"enclave,omitempty"` // Direct enclave data
EnclaveCID string `json:"enclave_cid,omitempty"` // IPFS CID reference
Password []byte `json:"password,omitempty"` // Decryption password for IPFS stored data
Priority string `json:"priority,omitempty"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid JSON payload"})
}
// Handle enclave data - either direct or via IPFS CID
var enclave *mpc.EnclaveData
if req.EnclaveCID != "" {
// Retrieve enclave from IPFS
if vh.IPFSClient == nil {
return c.JSON(
http.StatusServiceUnavailable,
map[string]string{"error": "IPFS client not available"},
)
}
encryptedData, err := vh.IPFSClient.Get(req.EnclaveCID)
if err != nil {
return c.JSON(
http.StatusNotFound,
map[string]string{"error": "Failed to retrieve enclave from IPFS"},
)
}
// Create temporary enclave for decryption
tempEnclave := &mpc.EnclaveData{}
decryptedData, err := tempEnclave.Decrypt(req.Password, encryptedData)
if err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to decrypt enclave data"},
)
}
// Unmarshal decrypted data
enclave = &mpc.EnclaveData{}
if err := enclave.Unmarshal(decryptedData); err != nil {
return c.JSON(
http.StatusBadRequest,
map[string]string{"error": "Failed to parse enclave data"},
)
}
} else if req.Enclave != nil {
// Use directly provided enclave data
enclave = req.Enclave
} else {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Either enclave or enclave_cid is required"})
}
// Refresh functionality replaced with UCAN DID generation for new identity
// Convert refresh operation to DID generation which includes key refresh
task, err := tasks.NewUCANDIDTask(0) // TODO: Extract userID from JWT or context
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to create task"},
)
}
queue := GetQueueFromPriority(req.Priority)
info, err := client.Enqueue(task, asynq.Queue(queue))
if err != nil {
return c.JSON(
http.StatusInternalServerError,
map[string]string{"error": "Failed to enqueue task"},
)
}
return c.JSON(http.StatusOK, map[string]any{
"task_id": info.ID,
"queue": queue,
"status": "enqueued",
})
}
}
-558
View File
@@ -1,558 +0,0 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"sync"
"time"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/types/webauthn"
"github.com/sonr-io/sonr/types/webauthn/webauthncbor"
)
// WebAuthnStore manages WebAuthn sessions and credentials
type WebAuthnStore struct {
mu sync.RWMutex
sessions map[string]*WebAuthnSession
credentials map[string][]*WebAuthnCredential
}
// WebAuthnSession holds session data for WebAuthn ceremonies
type WebAuthnSession struct {
Challenge string
Username string
CreatedAt time.Time
SessionType string // "registration" or "authentication"
}
var (
webAuthnStore = &WebAuthnStore{
sessions: make(map[string]*WebAuthnSession),
credentials: make(map[string][]*WebAuthnCredential),
}
sessionTimeout = 5 * time.Minute
)
// BeginWebAuthnRegistration starts WebAuthn registration ceremony
func BeginWebAuthnRegistration(c echo.Context) error {
var req WebAuthnRegistrationRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
if req.Username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Store the request in context for later use in FinishWebAuthnRegistration
c.Set("webauthn_registration_request", &req)
// Generate challenge
challenge, err := generateChallenge()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate challenge",
})
}
// Store session
session := &WebAuthnSession{
Challenge: challenge,
Username: req.Username,
CreatedAt: time.Now(),
SessionType: "registration",
}
webAuthnStore.mu.Lock()
webAuthnStore.sessions[req.Username] = session
webAuthnStore.mu.Unlock()
// Create registration response
response := WebAuthnRegistrationResponse{
Challenge: challenge,
RP: WebAuthnRPEntity{
ID: "localhost", // TODO: Get from config
Name: "Sonr Identity Platform",
},
User: WebAuthnUserEntity{
ID: base64.URLEncoding.EncodeToString([]byte(req.Username)),
Name: req.Username,
DisplayName: req.Username,
},
PubKeyCredParams: []WebAuthnCredParam{
{Type: "public-key", Alg: -7}, // ES256
{Type: "public-key", Alg: -257}, // RS256
},
AuthenticatorSelection: WebAuthnAuthenticatorSelection{
AuthenticatorAttachment: "platform",
UserVerification: "required",
ResidentKey: "preferred",
},
Timeout: 60000,
Attestation: "direct",
}
return c.JSON(http.StatusOK, response)
}
// FinishWebAuthnRegistration completes WebAuthn registration ceremony
func FinishWebAuthnRegistration(c echo.Context) error {
username := c.QueryParam("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Parse registration response
var regResponse map[string]any
if err := c.Bind(&regResponse); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid registration response",
})
}
// Get stored session
webAuthnStore.mu.RLock()
session, exists := webAuthnStore.sessions[username]
webAuthnStore.mu.RUnlock()
if !exists || session.SessionType != "registration" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No registration session found",
})
}
// Check session timeout
if time.Since(session.CreatedAt) > sessionTimeout {
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Registration session expired",
})
}
// Extract credential data
credentialID, _ := regResponse["id"].(string)
rawID, _ := regResponse["rawId"].(string)
response, _ := regResponse["response"].(map[string]any)
clientDataJSON, _ := response["clientDataJSON"].(string)
attestationObject, _ := response["attestationObject"].(string)
// Verify client data
if err := verifyClientData(clientDataJSON, session.Challenge, "webauthn.create"); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("Client data verification failed: %v", err),
})
}
// Extract public key from attestation
publicKey, algorithm, err := extractPublicKeyFromAttestation(attestationObject)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": fmt.Sprintf("Failed to extract public key: %v", err),
})
}
// Create credential
credential := &WebAuthnCredential{
CredentialID: credentialID,
RawID: rawID,
ClientDataJSON: clientDataJSON,
AttestationObject: attestationObject,
Username: username,
Origin: "localhost", // TODO: Extract from client data
PublicKey: publicKey,
Algorithm: algorithm,
CreatedAt: time.Now(),
}
// Store credential
webAuthnStore.mu.Lock()
webAuthnStore.credentials[username] = append(webAuthnStore.credentials[username], credential)
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
// Check if we should broadcast to blockchain
broadcastReq, ok := c.Get("broadcast_to_chain").(bool)
if !ok {
// Check from original request stored in context
if origReq, exists := c.Get("webauthn_registration_request").(*WebAuthnRegistrationRequest); exists {
broadcastReq = origReq.BroadcastToChain
}
}
var broadcastResult *BroadcastResponse
if broadcastReq {
// Broadcast WebAuthn credential to blockchain as gasless transaction
result, err := BroadcastWebAuthnRegistration(credential, true)
if err != nil {
// Log error but don't fail registration
c.Logger().Error("Failed to broadcast WebAuthn credential:", err)
} else {
broadcastResult = result
}
}
// Check if we should create a vault
autoCreateVault, ok := c.Get("auto_create_vault").(bool)
if !ok {
// Check from original request
if origReq, exists := c.Get("webauthn_registration_request").(*WebAuthnRegistrationRequest); exists {
autoCreateVault = origReq.AutoCreateVault
}
}
var vaultResult *BroadcastResponse
if autoCreateVault {
// Create vault for the user
vaultConfig := map[string]any{
"type": "standard",
"encryption": "AES256",
"owner": username,
}
userDID := fmt.Sprintf("did:sonr:%s", username)
result, err := BroadcastVaultCreation(userDID, vaultConfig)
if err != nil {
// Log error but don't fail registration
c.Logger().Error("Failed to create vault:", err)
} else {
vaultResult = result
}
}
finalResponse := map[string]any{
"success": true,
"message": "Registration completed successfully",
"credentialId": credentialID,
}
if broadcastResult != nil {
finalResponse["broadcast"] = broadcastResult
}
if vaultResult != nil {
finalResponse["vault"] = vaultResult
}
return c.JSON(http.StatusOK, finalResponse)
}
// BeginWebAuthnAuthentication starts WebAuthn authentication ceremony
func BeginWebAuthnAuthentication(c echo.Context) error {
var req WebAuthnAuthenticationRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
if req.Username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Check if user has credentials
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[req.Username]
webAuthnStore.mu.RUnlock()
if !exists || len(credentials) == 0 {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "No credentials found for user",
})
}
// Generate challenge
challenge, err := generateChallenge()
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to generate challenge",
})
}
// Store session
session := &WebAuthnSession{
Challenge: challenge,
Username: req.Username,
CreatedAt: time.Now(),
SessionType: "authentication",
}
webAuthnStore.mu.Lock()
webAuthnStore.sessions[req.Username] = session
webAuthnStore.mu.Unlock()
// Build allowed credentials
allowCredentials := make([]WebAuthnAllowedCred, len(credentials))
for i, cred := range credentials {
allowCredentials[i] = WebAuthnAllowedCred{
Type: "public-key",
ID: cred.CredentialID,
}
}
// Create authentication response
response := WebAuthnAuthenticationResponse{
Challenge: challenge,
Timeout: 60000,
RPID: "localhost", // TODO: Get from config
AllowCredentials: allowCredentials,
UserVerification: "required",
}
return c.JSON(http.StatusOK, response)
}
// FinishWebAuthnAuthentication completes WebAuthn authentication ceremony
func FinishWebAuthnAuthentication(c echo.Context) error {
username := c.QueryParam("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
// Parse authentication response
var authResponse map[string]any
if err := c.Bind(&authResponse); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid authentication response",
})
}
// Get stored session
webAuthnStore.mu.RLock()
session, exists := webAuthnStore.sessions[username]
webAuthnStore.mu.RUnlock()
if !exists || session.SessionType != "authentication" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No authentication session found",
})
}
// Check session timeout
if time.Since(session.CreatedAt) > sessionTimeout {
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Authentication session expired",
})
}
// Extract response data
credentialID, _ := authResponse["id"].(string)
response, _ := authResponse["response"].(map[string]any)
clientDataJSON, _ := response["clientDataJSON"].(string)
authenticatorData, _ := response["authenticatorData"].(string)
signature, _ := response["signature"].(string)
userHandle, _ := response["userHandle"].(string)
// Verify client data
if err := verifyClientData(clientDataJSON, session.Challenge, "webauthn.get"); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("Client data verification failed: %v", err),
})
}
// Find matching credential
webAuthnStore.mu.RLock()
credentials := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
var matchedCredential *WebAuthnCredential
for _, cred := range credentials {
if cred.CredentialID == credentialID {
matchedCredential = cred
break
}
}
if matchedCredential == nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credential",
})
}
// TODO: Verify signature using the stored public key
// This would require implementing proper WebAuthn signature verification
// Clean up session
webAuthnStore.mu.Lock()
delete(webAuthnStore.sessions, username)
webAuthnStore.mu.Unlock()
// Create authenticated session
userDID := fmt.Sprintf("did:sonr:%s", username)
authSession := &OIDCSession{
SessionID: generateSessionID(),
UserDID: userDID,
ClientID: "webauthn-client",
Scope: "openid profile did vault",
AccessToken: generateAccessToken(userDID),
RefreshToken: generateRefreshToken(),
ExpiresAt: time.Now().Add(time.Hour),
CreatedAt: time.Now(),
}
// Store session for OIDC compatibility
oidcProvider.mu.Lock()
oidcProvider.sessions[authSession.AccessToken] = authSession
oidcProvider.mu.Unlock()
// Set user context for downstream handlers
c.Set("user_did", userDID)
c.Set("authenticated", true)
c.Set("auth_method", "webauthn")
c.Set("credential_id", credentialID)
return c.JSON(http.StatusOK, map[string]any{
"success": true,
"message": "Authentication successful",
"credentialId": credentialID,
"accessToken": authSession.AccessToken,
"expiresIn": 3600,
"userDID": userDID,
"sessionId": authSession.SessionID,
"authenticatorData": authenticatorData,
"signature": signature,
"userHandle": userHandle,
})
}
// generateChallenge creates a cryptographically secure challenge
func generateChallenge() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(bytes), nil
}
// verifyClientData verifies client data JSON
func verifyClientData(clientDataJSON, expectedChallenge, expectedType string) error {
clientData, err := webauthn.ValidateClientDataJSONFormat(clientDataJSON)
if err != nil {
return fmt.Errorf("failed to parse client data: %w", err)
}
if clientData.Challenge != expectedChallenge {
return fmt.Errorf("challenge mismatch")
}
if clientData.Type != expectedType {
return fmt.Errorf(
"invalid client data type: expected %s, got %s",
expectedType,
clientData.Type,
)
}
// TODO: Verify origin from config
expectedOrigins := []string{
"http://localhost",
"http://localhost:8080",
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:8083",
"http://localhost:8084",
}
validOrigin := false
for _, origin := range expectedOrigins {
if clientData.Origin == origin {
validOrigin = true
break
}
}
if !validOrigin {
return fmt.Errorf("invalid origin: %s", clientData.Origin)
}
return nil
}
// extractPublicKeyFromAttestation extracts public key from attestation object
func extractPublicKeyFromAttestation(attestationObjectB64 string) ([]byte, int32, error) {
// Validate format
if err := webauthn.ValidateAttestationObjectFormat(attestationObjectB64); err != nil {
return nil, 0, fmt.Errorf("invalid attestation format: %w", err)
}
// Decode attestation object
attestationBytes, err := base64.RawURLEncoding.DecodeString(attestationObjectB64)
if err != nil {
return nil, 0, fmt.Errorf("failed to decode attestation: %w", err)
}
// Parse CBOR
var attestationObj webauthn.AttestationObject
if err := webauthncbor.Unmarshal(attestationBytes, &attestationObj); err != nil {
return nil, 0, fmt.Errorf("failed to unmarshal attestation: %w", err)
}
// Unmarshal authenticator data
if err := attestationObj.AuthData.Unmarshal(attestationObj.RawAuthData); err != nil {
return nil, 0, fmt.Errorf("failed to unmarshal auth data: %w", err)
}
// Check for attested credential data
if !attestationObj.AuthData.Flags.HasAttestedCredentialData() {
return nil, 0, fmt.Errorf("no attested credential data")
}
publicKey := attestationObj.AuthData.AttData.CredentialPublicKey
if len(publicKey) == 0 {
return nil, 0, fmt.Errorf("no public key found")
}
// Default to ES256 algorithm
algorithm := int32(-7)
return publicKey, algorithm, nil
}
// GetWebAuthnCredentials retrieves credentials for a user
func GetWebAuthnCredentials(c echo.Context) error {
username := c.Param("username")
if username == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username is required",
})
}
webAuthnStore.mu.RLock()
credentials, exists := webAuthnStore.credentials[username]
webAuthnStore.mu.RUnlock()
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "No credentials found for user",
})
}
// Return sanitized credentials (without sensitive data)
sanitized := make([]map[string]any, len(credentials))
for i, cred := range credentials {
sanitized[i] = map[string]any{
"credentialId": cred.CredentialID,
"createdAt": cred.CreatedAt,
"algorithm": cred.Algorithm,
}
}
return c.JSON(http.StatusOK, sanitized)
}
-284
View File
@@ -1,284 +0,0 @@
package handlers
import (
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
)
// TaskStatusMessage represents a WebSocket message for task status updates
type TaskStatusMessage struct {
TaskID string `json:"task_id"`
Status string `json:"status"`
Progress int `json:"progress,omitempty"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Time time.Time `json:"timestamp"`
}
// ConnectionManager manages WebSocket connections for task status broadcasting
type ConnectionManager struct {
connections map[string]map[*websocket.Conn]bool // taskID -> connections
mutex sync.RWMutex
}
// NewConnectionManager creates a new connection manager
func NewConnectionManager() *ConnectionManager {
return &ConnectionManager{
connections: make(map[string]map[*websocket.Conn]bool),
mutex: sync.RWMutex{},
}
}
// AddConnection adds a WebSocket connection for a specific task ID
func (cm *ConnectionManager) AddConnection(taskID string, conn *websocket.Conn) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.connections[taskID] == nil {
cm.connections[taskID] = make(map[*websocket.Conn]bool)
}
cm.connections[taskID][conn] = true
log.Printf("WebSocket connection added for task: %s", taskID)
}
// RemoveConnection removes a WebSocket connection
func (cm *ConnectionManager) RemoveConnection(taskID string, conn *websocket.Conn) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if connections, exists := cm.connections[taskID]; exists {
delete(connections, conn)
if len(connections) == 0 {
delete(cm.connections, taskID)
}
}
log.Printf("WebSocket connection removed for task: %s", taskID)
}
// BroadcastToTask broadcasts a message to all connections listening to a specific task
func (cm *ConnectionManager) BroadcastToTask(taskID string, message TaskStatusMessage) {
cm.mutex.RLock()
connections, exists := cm.connections[taskID]
cm.mutex.RUnlock()
if !exists {
return
}
for conn := range connections {
if err := conn.WriteJSON(message); err != nil {
log.Printf("Error broadcasting to WebSocket: %v", err)
cm.RemoveConnection(taskID, conn)
conn.Close()
}
}
}
// SSEManager manages Server-Sent Event connections for task status streaming
type SSEManager struct {
connections map[string]map[chan TaskStatusMessage]bool // taskID -> channels
mutex sync.RWMutex
}
// NewSSEManager creates a new SSE manager
func NewSSEManager() *SSEManager {
return &SSEManager{
connections: make(map[string]map[chan TaskStatusMessage]bool),
mutex: sync.RWMutex{},
}
}
// AddSSEConnection adds an SSE channel for a specific task ID
func (sm *SSEManager) AddSSEConnection(taskID string, ch chan TaskStatusMessage) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if sm.connections[taskID] == nil {
sm.connections[taskID] = make(map[chan TaskStatusMessage]bool)
}
sm.connections[taskID][ch] = true
log.Printf("SSE connection added for task: %s", taskID)
}
// RemoveSSEConnection removes an SSE channel
func (sm *SSEManager) RemoveSSEConnection(taskID string, ch chan TaskStatusMessage) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if channels, exists := sm.connections[taskID]; exists {
delete(channels, ch)
if len(channels) == 0 {
delete(sm.connections, taskID)
}
}
close(ch)
log.Printf("SSE connection removed for task: %s", taskID)
}
// BroadcastToSSE broadcasts a message to all SSE connections listening to a specific task
func (sm *SSEManager) BroadcastToSSE(taskID string, message TaskStatusMessage) {
sm.mutex.RLock()
channels, exists := sm.connections[taskID]
sm.mutex.RUnlock()
if !exists {
return
}
for ch := range channels {
select {
case ch <- message:
// Message sent successfully
default:
// Channel is blocked, remove it
log.Printf("SSE channel blocked, removing for task: %s", taskID)
sm.RemoveSSEConnection(taskID, ch)
}
}
}
// WebSocketHandler handles WebSocket connections for real-time task status updates
func WebSocketHandler(
upgrader *websocket.Upgrader,
connectionManager *ConnectionManager,
) echo.HandlerFunc {
return func(c echo.Context) error {
taskID := c.Param("task_id")
if taskID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Task ID is required"})
}
// Upgrade HTTP connection to WebSocket
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return err
}
defer ws.Close()
// Add connection to manager
connectionManager.AddConnection(taskID, ws)
defer connectionManager.RemoveConnection(taskID, ws)
// Send initial connection confirmation
initialMessage := TaskStatusMessage{
TaskID: taskID,
Status: "connected",
Time: time.Now(),
}
if err := ws.WriteJSON(initialMessage); err != nil {
log.Printf("Error sending initial message: %v", err)
return err
}
// Keep connection alive and handle incoming messages
for {
// Read message from client (ping/pong for keepalive)
_, _, err := ws.ReadMessage()
if err != nil {
log.Printf("WebSocket read error: %v", err)
break
}
// Echo back a pong message to keep connection alive
pongMessage := TaskStatusMessage{
TaskID: taskID,
Status: "pong",
Time: time.Now(),
}
if err := ws.WriteJSON(pongMessage); err != nil {
log.Printf("Error sending pong: %v", err)
break
}
}
return nil
}
}
// SSEHandler handles Server-Sent Events for streaming task status updates
func SSEHandler(sseManager *SSEManager) echo.HandlerFunc {
return func(c echo.Context) error {
taskID := c.Param("task_id")
if taskID == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Task ID is required"})
}
// Set SSE headers
c.Response().Header().Set("Content-Type", "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
c.Response().Header().Set("Access-Control-Allow-Headers", "Cache-Control")
// Create a channel for this SSE connection
messageCh := make(chan TaskStatusMessage, 10)
sseManager.AddSSEConnection(taskID, messageCh)
defer sseManager.RemoveSSEConnection(taskID, messageCh)
// Send initial connection message
initialMessage := TaskStatusMessage{
TaskID: taskID,
Status: "connected",
Time: time.Now(),
}
fmt.Fprintf(
c.Response().Writer,
"data: {\"task_id\":\"%s\",\"status\":\"connected\",\"timestamp\":\"%s\"}\n\n",
taskID,
initialMessage.Time.Format(time.RFC3339),
)
c.Response().Flush()
// Keep connection alive and send messages
for {
select {
case message, ok := <-messageCh:
if !ok {
return nil
}
// Format message as SSE data
sseData := fmt.Sprintf(
"data: {\"task_id\":\"%s\",\"status\":\"%s\",\"progress\":%d,\"timestamp\":\"%s\"}",
message.TaskID,
message.Status,
message.Progress,
message.Time.Format(time.RFC3339),
)
if message.Error != "" {
sseData = fmt.Sprintf(
"data: {\"task_id\":\"%s\",\"status\":\"%s\",\"error\":\"%s\",\"timestamp\":\"%s\"}",
message.TaskID,
message.Status,
message.Error,
message.Time.Format(time.RFC3339),
)
}
fmt.Fprintf(c.Response().Writer, "%s\n\n", sseData)
c.Response().Flush()
case <-c.Request().Context().Done():
// Client disconnected
return nil
case <-time.After(30 * time.Second):
// Send keepalive message every 30 seconds
fmt.Fprintf(
c.Response().Writer,
"data: {\"task_id\":\"%s\",\"status\":\"keepalive\",\"timestamp\":\"%s\"}\n\n",
taskID,
time.Now().Format(time.RFC3339),
)
c.Response().Flush()
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
package bridge
import (
"log"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/bridge/tasks"
)
// QueueManager handles Asynq server setup and task registration
type QueueManager struct {
server *asynq.Server
mux *asynq.ServeMux
config *Config
}
// NewQueueManager creates a new queue manager with the given configuration
func NewQueueManager(config *Config) *QueueManager {
// Initialize UCAN task processing server with optimized queue configuration
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: config.RedisAddr},
config.AsynqConfig,
)
// Register UCAN-based task handlers
mux := asynq.NewServeMux()
registerTaskHandlers(mux)
return &QueueManager{
server: srv,
mux: mux,
config: config,
}
}
// registerTaskHandlers registers all UCAN-based task handlers
func registerTaskHandlers(mux *asynq.ServeMux) {
// Core UCAN token operations
mux.Handle(tasks.TypeUCANToken, tasks.NewUCANProcessor())
mux.Handle(tasks.TypeUCANAttenuation, tasks.NewUCANAttenuationProcessor())
// MPC-based cryptographic operations
mux.Handle(tasks.TypeUCANSign, tasks.NewUCANSignProcessor())
mux.Handle(tasks.TypeUCANVerify, tasks.NewUCANVerifyProcessor())
// DID operations (replaces both TypeVaultGenerate and TypeVaultRefresh)
// Serves as proxy for future x/did module integration
mux.Handle(tasks.TypeUCANDIDGeneration, tasks.NewUCANDIDProcessor())
log.Printf("UCAN task handlers registered successfully")
}
// Run starts the Asynq server with the registered task handlers
func (qm *QueueManager) Run() error {
log.Printf("Starting Asynq task server with Redis at %s", qm.config.RedisAddr)
return qm.server.Run(qm.mux)
}
// Shutdown gracefully shuts down the Asynq server
func (qm *QueueManager) Shutdown() {
qm.server.Shutdown()
}
-223
View File
@@ -1,223 +0,0 @@
package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
"github.com/hibiken/asynq"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/crypto/mpc"
)
// MockAsynqClient provides a test double for asynq.Client
type MockAsynqClient struct {
enqueuedTasks []MockTask
}
type MockTask struct {
Type string
Payload []byte
Queue string
}
func (m *MockAsynqClient) Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) {
mockTask := MockTask{
Type: task.Type(),
Payload: task.Payload(),
Queue: "default",
}
m.enqueuedTasks = append(m.enqueuedTasks, mockTask)
return &asynq.TaskInfo{
ID: "test-task-id",
Type: task.Type(),
Queue: mockTask.Queue,
}, nil
}
func (m *MockAsynqClient) Close() error {
return nil
}
// setupTestEcho creates a test Echo server and mock client for benchmarking
func setupTestEcho() (*echo.Echo, *MockAsynqClient) {
mockClient := &MockAsynqClient{}
config := &Config{
JWTSecret: []byte("test-secret"),
IPFSClient: &MockIPFSClient{},
}
s := NewServer(config)
return s.Echo(), mockClient
}
// BenchmarkHealthCheckHandler measures the performance of the health check endpoint
func BenchmarkHealthCheckHandler(b *testing.B) {
e, _ := setupTestEcho()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("GET", "/health", nil)
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
}
// BenchmarkGenerateHandler measures the performance of the generate endpoint
func BenchmarkGenerateHandler(b *testing.B) {
e, client := setupTestEcho()
payload := map[string]any{
"user_id": 123,
"priority": "default",
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
b.StopTimer()
b.Logf("Tasks enqueued: %d", len(client.enqueuedTasks))
}
// BenchmarkSignHandler measures the performance of the sign endpoint
func BenchmarkSignHandler(b *testing.B) {
e, client := setupTestEcho()
payload := map[string]any{
"message": []byte("benchmark test message"),
"enclave": &mpc.EnclaveData{},
"priority": "default",
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("POST", "/vault/sign", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
})
b.StopTimer()
b.Logf("Tasks enqueued: %d", len(client.enqueuedTasks))
}
// BenchmarkMemoryAllocation measures memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
e, _ := setupTestEcho()
payload := map[string]any{
"user_id": 123,
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
b.ReportAllocs()
for b.Loop() {
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
}
b.StopTimer()
runtime.GC()
runtime.ReadMemStats(&m2)
b.Logf("Memory allocated per operation: %d bytes", (m2.TotalAlloc-m1.TotalAlloc)/uint64(b.N))
b.Logf("Total allocations: %d", m2.Mallocs-m1.Mallocs)
}
// BenchmarkLatencyMeasurement measures end-to-end latency
func BenchmarkLatencyMeasurement(b *testing.B) {
e, _ := setupTestEcho()
payload := map[string]any{
"user_id": 123,
}
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(payload)
requestBody := buf.Bytes()
var totalLatency time.Duration
minLatency := time.Hour
var maxLatency time.Duration
for b.Loop() {
start := time.Now()
req := httptest.NewRequest("POST", "/vault/generate", bytes.NewReader(requestBody))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
latency := time.Since(start)
totalLatency += latency
if latency < minLatency {
minLatency = latency
}
if latency > maxLatency {
maxLatency = latency
}
if rr.Code != http.StatusOK {
b.Errorf("Expected status 200, got %d", rr.Code)
}
}
b.StopTimer()
avgLatency := totalLatency / time.Duration(b.N)
b.Logf("Average latency: %v", avgLatency)
b.Logf("Min latency: %v", minLatency)
b.Logf("Max latency: %v", maxLatency)
}
-118
View File
@@ -1,118 +0,0 @@
// Package server provides the HTTP server for the highway server
package server
import (
"net/http"
"github.com/gorilla/websocket"
"github.com/hibiken/asynq"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/sonr-io/sonr/bridge/handlers"
"github.com/sonr-io/sonr/types/ipfs"
)
const (
DefaultHTTPAddr = ":8080"
)
// Config holds server configuration
type Config struct {
HTTPAddr string
JWTSecret []byte
IPFSClient ipfs.IPFSClient
}
// Server represents the HTTP server
type Server struct {
config *Config
echo *echo.Echo
upgrader *websocket.Upgrader
connectionManager *handlers.ConnectionManager
sseManager *handlers.SSEManager
vaultHandlers *handlers.VaultHandlers
}
// NewServer creates a new server instance
func NewServer(config *Config) *Server {
// WebSocket upgrader with CORS settings
upgrader := &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins in development
},
}
// Create connection managers
connectionManager := handlers.NewConnectionManager()
sseManager := handlers.NewSSEManager()
// Create vault handlers
vaultHandlers := handlers.NewVaultHandlers(config.IPFSClient, connectionManager, sseManager)
return &Server{
config: config,
echo: echo.New(),
upgrader: upgrader,
connectionManager: connectionManager,
sseManager: sseManager,
vaultHandlers: vaultHandlers,
}
}
// Echo returns the underlying Echo instance for testing
func (s *Server) Echo() *echo.Echo {
return s.echo
}
// Start starts the HTTP server
func (s *Server) Start(client *asynq.Client) error {
s.setupMiddleware()
s.setupRoutes(client)
addr := s.config.HTTPAddr
if addr == "" {
addr = DefaultHTTPAddr
}
return s.echo.Start(addr)
}
// setupMiddleware configures Echo middleware
func (s *Server) setupMiddleware() {
s.echo.Use(middleware.Logger())
s.echo.Use(middleware.Recover())
s.echo.Use(middleware.CORS())
}
// setupRoutes configures all routes
func (s *Server) setupRoutes(client *asynq.Client) {
// Initialize health checker
handlers.InitHealthChecker(client, s.config.IPFSClient)
// Public endpoints (no authentication required)
s.echo.GET("/health", handlers.HealthCheckHandler) // Liveness probe
s.echo.GET("/ready", handlers.ReadinessHandler) // Readiness probe
s.echo.POST("/auth/login", handlers.LoginHandler(s.config.JWTSecret))
// JWT middleware configuration
jwtConfig := echojwt.Config{
SigningKey: s.config.JWTSecret,
SigningMethod: "HS256",
}
// Protected vault endpoints group with JWT middleware
vault := s.echo.Group("/vault")
vault.Use(echojwt.WithConfig(jwtConfig))
vault.POST("/generate", s.vaultHandlers.GenerateHandler(client))
vault.POST("/sign", s.vaultHandlers.SignHandler(client))
vault.POST("/verify", s.vaultHandlers.VerifyHandler(client))
vault.POST("/export", s.vaultHandlers.ExportHandler(client))
vault.POST("/import", s.vaultHandlers.ImportHandler(client))
vault.POST("/refresh", s.vaultHandlers.RefreshHandler(client))
// WebSocket endpoint for real-time task status updates
vault.GET("/ws/:task_id", handlers.WebSocketHandler(s.upgrader, s.connectionManager))
// Server-Sent Events endpoint for task progress streaming
vault.GET("/events/:task_id", handlers.SSEHandler(s.sseManager))
}
-110
View File
@@ -1,110 +0,0 @@
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/sonr-io/sonr/bridge/handlers"
"github.com/sonr-io/sonr/types/ipfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// MockIPFSClient provides a test implementation of IPFSClient for server tests
type MockIPFSClient struct{}
func (m *MockIPFSClient) Add(data []byte) (string, error) { return "mock-cid", nil }
func (m *MockIPFSClient) AddFile(
file ipfs.File,
) (string, error) {
return "mock-file-cid", nil
}
func (m *MockIPFSClient) AddFolder(
folder ipfs.Folder,
) (string, error) {
return "mock-folder-cid", nil
}
func (m *MockIPFSClient) Get(
cid string,
) ([]byte, error) {
return []byte("mock-ipfs-data"), nil
}
func (m *MockIPFSClient) GetFile(cid string) (ipfs.File, error) { return nil, nil }
func (m *MockIPFSClient) GetFolder(cid string) (ipfs.Folder, error) { return nil, nil }
func (m *MockIPFSClient) Pin(cid string, name string) error { return nil }
func (m *MockIPFSClient) Unpin(cid string) error { return nil }
func (m *MockIPFSClient) Exists(cid string) (bool, error) { return true, nil }
func (m *MockIPFSClient) IsPinned(ipns string) (bool, error) { return true, nil }
func (m *MockIPFSClient) Ls(cid string) ([]string, error) {
return []string{"mock-file1", "mock-file2"}, nil
}
func (m *MockIPFSClient) NodeStatus() (*ipfs.NodeStatus, error) {
return &ipfs.NodeStatus{
PeerID: "mock-peer-id",
Version: "mock-version",
PeerType: "kubo",
ConnectedPeers: 3,
}, nil
}
func setupTestServer() *Server {
config := &Config{
JWTSecret: []byte("test-secret"),
IPFSClient: &MockIPFSClient{},
}
s := NewServer(config)
// Setup routes manually for testing since we can't call Start() which would block
e := s.Echo()
// Setup middleware
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return next(c)
}
})
// Setup routes manually (mimicking server.setupRoutes)
e.GET("/health", handlers.HealthCheckHandler)
e.POST("/auth/login", handlers.LoginHandler(config.JWTSecret))
return s
}
func TestHealthCheckHandler(t *testing.T) {
s := setupTestServer()
e := s.Echo()
req, err := http.NewRequest("GET", "/health", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
e.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
var response map[string]string
err = json.Unmarshal(rr.Body.Bytes(), &response)
require.NoError(t, err)
// When health checker is not initialized, it returns "starting" status
// This is expected behavior in test environment
assert.Equal(t, "starting", response["status"])
}
func TestVaultHandlersCreation(t *testing.T) {
// Test the vault handlers creation
vaultHandlers := handlers.NewVaultHandlers(
&MockIPFSClient{},
handlers.NewConnectionManager(),
handlers.NewSSEManager(),
)
assert.NotNil(t, vaultHandlers)
}
-156
View File
@@ -1,156 +0,0 @@
// Package tasks provides UCAN-based task processing for token attenuation operations.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANAttenuationProcessor implements asynq.Handler interface for UCAN attenuation operations.
type UCANAttenuationProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANAttenuationProcessor creates a new UCANAttenuationProcessor for the specified actor system.
func NewUCANAttenuationProcessor() *UCANAttenuationProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANAttenuation)
return &UCANAttenuationProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANAttenuationPayload contains parameters for UCAN token attenuation task.
type UCANAttenuationPayload struct {
UserID int `json:"user_id"`
ParentToken string `json:"parent_token"`
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
// NewUCANAttenuationTask creates a new UCAN token attenuation task.
func NewUCANAttenuationTask(
userID int,
parentToken string,
audienceDID string,
attenuations []map[string]any,
expiresAt int64,
) (*asynq.Task, error) {
payload, err := json.Marshal(UCANAttenuationPayload{
UserID: userID,
ParentToken: parentToken,
AudienceDID: audienceDID,
Attenuations: attenuations,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANAttenuation, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN token attenuation task.
func (processor *UCANAttenuationProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANAttenuationPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create attenuated token request for the actor
request := &plugin.NewAttenuatedTokenRequest{
ParentToken: p.ParentToken,
AudienceDID: p.AudienceDID,
Attenuations: p.Attenuations,
ExpiresAt: p.ExpiresAt,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.UCANTokenResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN token attenuation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ DID Generation Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANDIDProcessor implements asynq.Handler interface for DID generation operations.
type UCANDIDProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANDIDProcessor creates a new UCANDIDProcessor for the specified actor system.
func NewUCANDIDProcessor() *UCANDIDProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANDIDGeneration)
return &UCANDIDProcessor{
pid: pid,
}
}
// UCANDIDPayload contains parameters for DID generation task.
type UCANDIDPayload struct {
UserID int `json:"user_id"`
}
// NewUCANDIDTask creates a new DID generation task.
func NewUCANDIDTask(userID int) (*asynq.Task, error) {
payload, err := json.Marshal(UCANDIDPayload{
UserID: userID,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANDIDGeneration, payload), nil
}
// ProcessTask processes the DID generation task.
func (processor *UCANDIDProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANDIDPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Request DID generation from the actor
resp, err := system.Root.RequestFuture(processor.pid, &plugin.GetIssuerDIDResponse{}, KRequestTimeout).
Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.GetIssuerDIDResponse:
if resp.Error != "" {
return fmt.Errorf("DID generation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-96
View File
@@ -1,96 +0,0 @@
// Package tasks provides UCAN-based task processing for the refactored Motor plugin.
// This package handles asynchronous UCAN token creation, signing operations,
// and DID management tasks using the MPC-based plugin architecture.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANProcessor implements asynq.Handler interface for UCAN operations.
type UCANProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANProcessor creates a new UCANProcessor for the specified actor system.
func NewUCANProcessor() *UCANProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANToken)
return &UCANProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANTokenPayload contains parameters for UCAN token creation task.
type UCANTokenPayload struct {
UserID int `json:"user_id"`
AudienceDID string `json:"audience_did"`
Attenuations []map[string]any `json:"attenuations,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
}
// NewUCANTokenTask creates a new UCAN token creation task.
func NewUCANTokenTask(
userID int,
audienceDID string,
attenuations []map[string]any,
expiresAt int64,
) (*asynq.Task, error) {
payload, err := json.Marshal(UCANTokenPayload{
UserID: userID,
AudienceDID: audienceDID,
Attenuations: attenuations,
ExpiresAt: expiresAt,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANToken, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN token creation task.
func (processor *UCANProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANTokenPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create UCAN token request for the actor
request := &plugin.NewOriginTokenRequest{
AudienceDID: p.AudienceDID,
Attenuations: p.Attenuations,
ExpiresAt: p.ExpiresAt,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.UCANTokenResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN token creation failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-149
View File
@@ -1,149 +0,0 @@
// Package tasks provides UCAN-based task processing for MPC signing operations.
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ╭─────────────────────────────────────────────────────────╮
// │ Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANSignProcessor implements asynq.Handler interface for UCAN signing operations.
type UCANSignProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANSignProcessor creates a new UCANSignProcessor for the specified actor system.
func NewUCANSignProcessor() *UCANSignProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANSign)
return &UCANSignProcessor{
pid: pid,
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Payload │
// ╰─────────────────────────────────────────────────────────╯
// UCANSignPayload contains parameters for UCAN signing task.
type UCANSignPayload struct {
UserID int `json:"user_id"`
Data []byte `json:"data"`
}
// NewUCANSignTask creates a new UCAN signing task.
func NewUCANSignTask(userID int, data []byte) (*asynq.Task, error) {
payload, err := json.Marshal(UCANSignPayload{
UserID: userID,
Data: data,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANSign, payload), nil
}
// ╭───────────────────────────────────────────────────────╮
// │ Handler │
// ╰───────────────────────────────────────────────────────╯
// ProcessTask processes the UCAN signing task.
func (processor *UCANSignProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANSignPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create signing request for the actor
request := &plugin.SignDataRequest{
Data: p.Data,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.SignDataResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN signing failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
// ╭─────────────────────────────────────────────────────────╮
// │ Verification Processor │
// ╰─────────────────────────────────────────────────────────╯
// UCANVerifyProcessor implements asynq.Handler interface for UCAN verification operations.
type UCANVerifyProcessor struct {
asynq.Handler
pid *actor.PID
}
// NewUCANVerifyProcessor creates a new UCANVerifyProcessor for the specified actor system.
func NewUCANVerifyProcessor() *UCANVerifyProcessor {
pid := system.Root.SpawnPrefix(plugin.Props(), TypeUCANVerify)
return &UCANVerifyProcessor{
pid: pid,
}
}
// UCANVerifyPayload contains parameters for UCAN verification task.
type UCANVerifyPayload struct {
UserID int `json:"user_id"`
Data []byte `json:"data"`
Signature []byte `json:"signature"`
}
// NewUCANVerifyTask creates a new UCAN verification task.
func NewUCANVerifyTask(userID int, data, signature []byte) (*asynq.Task, error) {
payload, err := json.Marshal(UCANVerifyPayload{
UserID: userID,
Data: data,
Signature: signature,
})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeUCANVerify, payload), nil
}
// ProcessTask processes the UCAN verification task.
func (processor *UCANVerifyProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p UCANVerifyPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
// Create verification request for the actor
request := &plugin.VerifyDataRequest{
Data: p.Data,
Signature: p.Signature,
}
resp, err := system.Root.RequestFuture(processor.pid, request, KRequestTimeout).Result()
if err != nil {
return err
}
switch resp := resp.(type) {
case *plugin.VerifyDataResponse:
if resp.Error != "" {
return fmt.Errorf("UCAN verification failed: %s", resp.Error)
}
return nil
default:
return fmt.Errorf("invalid response type: %T", resp)
}
}
-425
View File
@@ -1,425 +0,0 @@
package tasks
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/asynkron/protoactor-go/actor"
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// generateTestEnclaveData creates a test enclave data for testing purposes
func generateTestEnclaveData(t *testing.T) *mpc.EnclaveData {
t.Helper()
// Generate a new enclave for testing
enclave, err := mpc.NewEnclave()
require.NoError(t, err, "failed to generate test enclave")
return enclave.GetData()
}
// MockUCANActor for testing UCAN task processors
type MockUCANActor struct {
responses map[string]any
}
func NewMockUCANActor() *MockUCANActor {
return &MockUCANActor{
responses: make(map[string]any),
}
}
// SlowMockUCANActor simulates timeout scenarios
type SlowMockUCANActor struct{}
func (s *SlowMockUCANActor) Receive(c actor.Context) {
switch c.Message().(type) {
case *plugin.NewOriginTokenRequest:
// Simulate slow response (longer than KRequestTimeout)
time.Sleep(KRequestTimeout + time.Second)
c.Respond(&plugin.UCANTokenResponse{})
}
}
func (m *MockUCANActor) Receive(c actor.Context) {
switch c.Message().(type) {
case *actor.Started:
// Actor started
case *plugin.NewOriginTokenRequest:
c.Respond(&plugin.UCANTokenResponse{
Token: "mock-ucan-token",
Issuer: "did:sonr:mock-issuer",
Address: "mock-address",
})
case *plugin.SignDataRequest:
c.Respond(&plugin.SignDataResponse{
Signature: []byte("mock-signature"),
})
case *plugin.VerifyDataRequest:
c.Respond(&plugin.VerifyDataResponse{
Valid: true,
})
case *plugin.NewAttenuatedTokenRequest:
c.Respond(&plugin.UCANTokenResponse{
Token: "mock-attenuated-token",
Issuer: "did:sonr:mock-issuer",
Address: "mock-address",
})
case *plugin.GetIssuerDIDResponse:
c.Respond(&plugin.GetIssuerDIDResponse{
IssuerDID: "did:sonr:mock-issuer",
Address: "mock-address",
ChainCode: "mock-chain-code",
})
default:
c.Respond(&actor.DeadLetterResponse{})
}
}
func TestUCANTokenTask(t *testing.T) {
tests := []struct {
name string
userID int
audienceDID string
attenuations []map[string]any
expiresAt int64
}{
{
name: "valid UCAN token request",
userID: 123,
audienceDID: "did:sonr:audience",
attenuations: []map[string]any{
{"can": []string{"sign"}, "with": "vault://example"},
},
expiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
{
name: "zero user ID",
userID: 0,
audienceDID: "did:sonr:audience",
attenuations: []map[string]any{},
expiresAt: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANTokenTask(tt.userID, tt.audienceDID, tt.attenuations, tt.expiresAt)
require.NoError(t, err)
assert.Equal(t, TypeUCANToken, task.Type())
var payload UCANTokenPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.audienceDID, payload.AudienceDID)
// Handle JSON unmarshaling type conversion ([]string becomes []any)
assert.Equal(t, len(tt.attenuations), len(payload.Attenuations))
for i, expectedAtt := range tt.attenuations {
actualAtt := payload.Attenuations[i]
for key, expectedVal := range expectedAtt {
actualVal, exists := actualAtt[key]
assert.True(t, exists, "key %s should exist", key)
// Handle []string to []any conversion
if expectedSlice, ok := expectedVal.([]string); ok {
actualSlice, ok := actualVal.([]any)
assert.True(t, ok, "expected []any for key %s", key)
assert.Equal(t, len(expectedSlice), len(actualSlice))
for j, expectedItem := range expectedSlice {
assert.Equal(t, expectedItem, actualSlice[j])
}
} else {
assert.Equal(t, expectedVal, actualVal)
}
}
}
assert.Equal(t, tt.expiresAt, payload.ExpiresAt)
})
}
}
func TestUCANSignTask(t *testing.T) {
tests := []struct {
name string
userID int
data []byte
}{
{
name: "valid sign request",
userID: 123,
data: []byte("test data to sign"),
},
{
name: "empty data",
userID: 456,
data: []byte{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANSignTask(tt.userID, tt.data)
require.NoError(t, err)
assert.Equal(t, TypeUCANSign, task.Type())
var payload UCANSignPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.data, payload.Data)
})
}
}
func TestUCANVerifyTask(t *testing.T) {
tests := []struct {
name string
userID int
data []byte
signature []byte
}{
{
name: "valid verify request",
userID: 123,
data: []byte("test data to verify"),
signature: []byte("test-signature"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANVerifyTask(tt.userID, tt.data, tt.signature)
require.NoError(t, err)
assert.Equal(t, TypeUCANVerify, task.Type())
var payload UCANVerifyPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.data, payload.Data)
assert.Equal(t, tt.signature, payload.Signature)
})
}
}
func TestUCANAttenuationTask(t *testing.T) {
tests := []struct {
name string
userID int
parentToken string
audienceDID string
attenuations []map[string]any
expiresAt int64
}{
{
name: "valid attenuation request",
userID: 123,
parentToken: "parent-ucan-token",
audienceDID: "did:sonr:delegated",
attenuations: []map[string]any{
{"can": []string{"read"}, "with": "vault://example"},
},
expiresAt: time.Now().Add(1 * time.Hour).Unix(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANAttenuationTask(
tt.userID,
tt.parentToken,
tt.audienceDID,
tt.attenuations,
tt.expiresAt,
)
require.NoError(t, err)
assert.Equal(t, TypeUCANAttenuation, task.Type())
var payload UCANAttenuationPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
assert.Equal(t, tt.parentToken, payload.ParentToken)
assert.Equal(t, tt.audienceDID, payload.AudienceDID)
// Handle JSON unmarshaling type conversion ([]string becomes []any)
assert.Equal(t, len(tt.attenuations), len(payload.Attenuations))
for i, expectedAtt := range tt.attenuations {
actualAtt := payload.Attenuations[i]
for key, expectedVal := range expectedAtt {
actualVal, exists := actualAtt[key]
assert.True(t, exists, "key %s should exist", key)
// Handle []string to []any conversion
if expectedSlice, ok := expectedVal.([]string); ok {
actualSlice, ok := actualVal.([]any)
assert.True(t, ok, "expected []any for key %s", key)
assert.Equal(t, len(expectedSlice), len(actualSlice))
for j, expectedItem := range expectedSlice {
assert.Equal(t, expectedItem, actualSlice[j])
}
} else {
assert.Equal(t, expectedVal, actualVal)
}
}
}
assert.Equal(t, tt.expiresAt, payload.ExpiresAt)
})
}
}
func TestUCANDIDTask(t *testing.T) {
tests := []struct {
name string
userID int
}{
{"valid DID request", 123},
{"zero user ID", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task, err := NewUCANDIDTask(tt.userID)
require.NoError(t, err)
assert.Equal(t, TypeUCANDIDGeneration, task.Type())
var payload UCANDIDPayload
err = json.Unmarshal(task.Payload(), &payload)
require.NoError(t, err)
assert.Equal(t, tt.userID, payload.UserID)
})
}
}
// Test processor functionality with mock actors
func TestUCANTokenProcessor(t *testing.T) {
// Create actor system
system := actor.NewActorSystem()
defer system.Shutdown()
// Create mock actor - use the mock instead of real plugin
mockActor := system.Root.Spawn(actor.PropsFromProducer(func() actor.Actor {
return NewMockUCANActor()
}))
// Create processor with mock actor
processor := &UCANProcessor{pid: mockActor}
// Create test task
task, err := NewUCANTokenTask(123, "did:sonr:audience", []map[string]any{
{"can": []string{"sign"}, "with": "vault://example"},
}, time.Now().Add(24*time.Hour).Unix())
require.NoError(t, err)
// Process the task - the error is expected because the mock actor returns a dead letter
// The processor should handle this gracefully
err = processor.ProcessTask(context.Background(), task)
// For now, we expect this to fail with the dead letter error
// In a real scenario, the actor would be properly initialized with enclave data
assert.Error(t, err, "expected error due to mock actor limitations")
assert.Contains(t, err.Error(), "dead letter", "should get dead letter error from mock")
}
// TestUCANActorWithRealEnclave tests the real UCAN actor with generated enclave data
func TestUCANActorWithRealEnclave(t *testing.T) {
// Skip this test if we're not in integration test mode
if testing.Short() {
t.Skip("skipping integration test")
}
// Skip this test in CI environments where WASM plugin is not available
if os.Getenv("CI") == "true" || os.Getenv("GITHUB_ACTIONS") == "true" {
t.Skip("skipping WASM enclave test in CI environment")
}
// Generate test enclave data
enclaveData := generateTestEnclaveData(t)
// Create enclave config with test data
config := plugin.DefaultEnclaveConfig()
config.EnclaveData = enclaveData
// Create actor system
system := actor.NewActorSystem()
defer system.Shutdown()
// Create real UCAN actor with the enclave configuration
realActor := system.Root.Spawn(plugin.PropsWithConfig(config))
// Wait for actor to initialize (longer wait for CI environments)
time.Sleep(500 * time.Millisecond)
// Test creating a UCAN token through the actor
req := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:test-audience",
Attenuations: []map[string]any{
{"can": []string{"read"}, "with": "vault://test"},
},
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
// Send request to actor with enhanced error handling and longer timeout for CI
result := system.Root.RequestFuture(realActor, req, 10*time.Second)
response, err := result.Result()
if err != nil {
t.Logf("Actor request failed: %v", err)
// If it's a timeout or WASM-related error, that's expected in test environments
if err.Error() == "future: timeout" || strings.Contains(err.Error(), "wasm") {
t.Skip("skipping test due to WASM enclave not available in test environment")
}
}
// The response could be an error if the actor didn't initialize properly
// For now, just verify we got some response (could be error or success) - but only if not a timeout
if err == nil || err.Error() != "future: timeout" {
assert.NotNil(t, response, "should receive some response from actor")
}
// Check if this is a WASM-related error (expected when WASM plugin is not available)
if err == nil && response != nil {
// The response could be a string or an error
responseStr := ""
if errResponse, ok := response.(error); ok {
responseStr = errResponse.Error()
} else if strResponse, ok := response.(string); ok {
responseStr = strResponse
} else {
responseStr = fmt.Sprintf("%+v", response)
}
if responseStr != "" && (strings.Contains(responseStr, "wasm error: unreachable") ||
strings.Contains(responseStr, "wasm not available") ||
strings.Contains(responseStr, "runtime.notInitialized")) {
t.Log("WASM plugin not available (expected in test environment)")
} else {
t.Logf("Actor successfully processed request: %+v", response)
}
} else {
t.Logf("Actor response: %+v, error: %v", response, err)
}
}
func TestInvalidJSONPayload(t *testing.T) {
// Create processor
processor := NewUCANProcessor()
// Create task with invalid JSON
task := asynq.NewTask(TypeUCANToken, []byte("invalid json"))
// Process the task - should fail with skip retry
err := processor.ProcessTask(context.Background(), task)
assert.Error(t, err)
assert.Contains(t, err.Error(), "json.Unmarshal failed")
}
-22
View File
@@ -1,22 +0,0 @@
// Package tasks provides UCAN-based tasks for the MPC enclave service.
package tasks
import (
"time"
"github.com/asynkron/protoactor-go/actor"
)
var system = actor.NewActorSystem()
const KRequestTimeout = 20 * time.Second
// A list of UCAN-based task types.
const (
TypeUCANToken = "ucan:token" // Create UCAN origin tokens
TypeUCANAttenuation = "ucan:attenuation" // Create attenuated UCAN tokens
TypeUCANSign = "ucan:sign" // MPC-based data signing
TypeUCANVerify = "ucan:verify" // MPC-based signature verification
TypeUCANDIDGeneration = "ucan:did:generation" // DID generation from MPC enclave
TypeUCANTokenValidation = "ucan:token:validation" // UCAN token validation
)
-101
View File
@@ -1,101 +0,0 @@
package bridge
import (
"github.com/hibiken/asynq"
"github.com/sonr-io/sonr/types/ipfs"
)
// MockIPFSClient provides a test implementation of IPFSClient
type MockIPFSClient struct{}
func (m *MockIPFSClient) Add(data []byte) (string, error) {
return "mock-cid", nil
}
func (m *MockIPFSClient) AddFile(file ipfs.File) (string, error) {
return "mock-file-cid", nil
}
func (m *MockIPFSClient) AddFolder(folder ipfs.Folder) (string, error) {
return "mock-folder-cid", nil
}
func (m *MockIPFSClient) Get(cid string) ([]byte, error) {
return []byte("mock-ipfs-data"), nil
}
func (m *MockIPFSClient) GetFile(cid string) (ipfs.File, error) {
return nil, nil
}
func (m *MockIPFSClient) GetFolder(cid string) (ipfs.Folder, error) {
return nil, nil
}
func (m *MockIPFSClient) Pin(cid string, name string) error {
return nil
}
func (m *MockIPFSClient) Unpin(cid string) error {
return nil
}
func (m *MockIPFSClient) Exists(cid string) (bool, error) {
return true, nil
}
func (m *MockIPFSClient) IsPinned(ipns string) (bool, error) {
return true, nil
}
func (m *MockIPFSClient) Ls(cid string) ([]string, error) {
return []string{"mock-file1", "mock-file2"}, nil
}
func (m *MockIPFSClient) NodeStatus() (*ipfs.NodeStatus, error) {
return &ipfs.NodeStatus{
PeerID: "mock-peer-id",
Version: "mock-version",
PeerType: "kubo",
ConnectedPeers: 5,
}, nil
}
// AsynqClientInterface defines the interface we need for testing
type AsynqClientInterface interface {
Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error)
Close() error
}
// MockAsynqClient provides a test double for asynq.Client
type MockAsynqClient struct {
enqueuedTasks []MockTask
}
type MockTask struct {
Type string
Payload []byte
Queue string
}
func (m *MockAsynqClient) Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error) {
mockTask := MockTask{
Type: task.Type(),
Payload: task.Payload(),
Queue: "default", // Default queue
}
// For testing purposes, we'll just use the default queue
// In real implementation, options would be parsed properly
m.enqueuedTasks = append(m.enqueuedTasks, mockTask)
return &asynq.TaskInfo{
ID: "test-task-id",
Type: task.Type(),
Queue: mockTask.Queue,
}, nil
}
func (m *MockAsynqClient) Close() error {
return nil
}
-120
View File
@@ -1,120 +0,0 @@
#!/usr/bin/make -f
DOCKER := $(shell which docker)
PROJECT_NAME = sonr-client-sdk
# golangci-lint Docker image version
GOLANGCI_VERSION := v2.3.1
###############################################################################
### Build ###
###############################################################################
build:
@gum log --level info "Building Go client SDK..."
@go build ./...
###############################################################################
### Formatting ###
###############################################################################
format fmt:
@gum log --level info "Formatting Go code with golangci-lint Docker..."
@$(DOCKER) run --rm -t -v $$(pwd):/app -w /app \
--user $$(id -u):$$(id -g) \
-v $$(go env GOCACHE):/.cache/go-build -e GOCACHE=/.cache/go-build \
-v $$(go env GOMODCACHE):/.cache/mod -e GOMODCACHE=/.cache/mod \
-v ~/.cache/golangci-lint:/.cache/golangci-lint -e GOLANGCI_LINT_CACHE=/.cache/golangci-lint \
golangci/golangci-lint:$(GOLANGCI_VERSION) golangci-lint run --fix --issues-exit-code=0
###############################################################################
### Linting ###
###############################################################################
lint:
@gum log --level info "Running golangci-lint Docker..."
@$(DOCKER) run --rm -t -v $$(pwd):/app -w /app \
--user $$(id -u):$$(id -g) \
-v $$(go env GOCACHE):/.cache/go-build -e GOCACHE=/.cache/go-build \
-v $$(go env GOMODCACHE):/.cache/mod -e GOMODCACHE=/.cache/mod \
-v ~/.cache/golangci-lint:/.cache/golangci-lint -e GOLANGCI_LINT_CACHE=/.cache/golangci-lint \
golangci/golangci-lint:$(GOLANGCI_VERSION) golangci-lint run --timeout=10m
###############################################################################
### Testing ###
###############################################################################
test:
@gum log --level info "Running all tests..."
@go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic ./...
test-unit:
@gum log --level info "Running unit tests..."
@go test -mod=readonly -short -race ./...
test-integration:
@gum log --level info "Running integration tests..."
@go test -mod=readonly -race -tags=integration ./tests/integration/...
test-verbose:
@gum log --level info "Running tests with verbose output..."
@go test -mod=readonly -v -race ./...
test-cover:
@gum log --level info "Running tests with coverage..."
@go test -mod=readonly -race -coverprofile=coverage.txt -covermode=atomic ./...
@go tool cover -html=coverage.txt -o coverage.html
@gum log --level info "Coverage report generated: coverage.html"
benchmark:
@gum log --level info "Running benchmarks..."
@go test -mod=readonly -bench=. -benchmem ./...
###############################################################################
### Dependencies ###
###############################################################################
deps:
@gum log --level info "Installing dependencies..."
@go mod download
tidy:
@gum log --level info "Tidying dependencies..."
@go mod tidy
verify:
@gum log --level info "Verifying dependencies..."
@go mod verify
###############################################################################
### Clean ###
###############################################################################
clean:
@gum log --level info "Cleaning build artifacts..."
@rm -f coverage.txt coverage.html
@go clean -cache
###############################################################################
### Help ###
###############################################################################
help:
@gum log --level info "Available targets:"
@gum log --level info " build - Build the Go client SDK"
@gum log --level info " format/fmt - Format Go code using golangci-lint"
@gum log --level info " lint - Run golangci-lint"
@gum log --level info " test - Run all tests with coverage"
@gum log --level info " test-unit - Run unit tests only"
@gum log --level info " test-integration - Run integration tests"
@gum log --level info " test-verbose - Run tests with verbose output"
@gum log --level info " test-cover - Run tests and generate HTML coverage report"
@gum log --level info " benchmark - Run benchmarks"
@gum log --level info " deps - Download dependencies"
@gum log --level info " tidy - Tidy and verify dependencies"
@gum log --level info " verify - Verify dependencies"
@gum log --level info " clean - Clean build artifacts"
@gum log --level info " help - Show this help message"
.DEFAULT_GOAL := help
.PHONY: build format fmt lint test test-unit test-integration test-verbose test-cover benchmark deps tidy verify clean help
-169
View File
@@ -1,169 +0,0 @@
# Sonr Go Client SDK
The official Go client SDK for the Sonr blockchain, providing idiomatic Go interfaces for interacting with Sonr's decentralized identity, data storage, and service management features.
## Features
- **Blockchain Interaction**: Query chain state and broadcast transactions
- **Module Support**: Comprehensive support for DID, DWN, SVC, and UCAN modules
- **WebAuthn Integration**: Passwordless authentication with hardware-backed keys
- **Transaction Building**: Simplified transaction construction and broadcasting
- **Key Management**: Secure keyring integration with multiple backends
- **Network Configuration**: Pre-configured endpoints for testnet and mainnet
## Installation
```bash
go get github.com/sonr-io/sonr/client
```
## Quick Start
### Basic SDK Setup
```go
package main
import (
"context"
"log"
client "github.com/sonr-io/sonr/client"
)
func main() {
// Initialize SDK with testnet configuration
sdk, err := client.NewWithNetwork("testnet")
if err != nil {
log.Fatal(err)
}
defer sdk.Close()
// Or create with custom configuration
cfg := client.DefaultConfig()
cfg.Network.GRPCEndpoint = "localhost:9090"
sdk, err = client.New(cfg)
if err != nil {
log.Fatal(err)
}
// Check connection
if !sdk.IsConnected() {
log.Fatal("Failed to connect to network")
}
// Access the underlying Sonr client
sonrClient := sdk.Client()
// Query chain info
ctx := context.Background()
info, err := sonrClient.Query().GetNodeInfo(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("Connected to chain: %s", info.Network)
log.Printf("SDK Version: %s", client.Version())
}
```
### Transaction Broadcasting
```go
// Get the client from SDK
sonrClient := sdk.Client()
// Create and broadcast a transaction
tx := sonrClient.Transaction().
WithChainID("sonrtest_1-1").
WithGasPrice(0.001, "usnr").
WithMemo("Hello Sonr!")
// Add messages to transaction
tx.AddMessage(/* your message */)
// Sign and broadcast
result, err := tx.SignAndBroadcast(ctx, keyring)
if err != nil {
log.Fatal(err)
}
log.Printf("Transaction hash: %s", result.TxHash)
```
### Module-Specific Operations
```go
// Get the client from SDK
sonrClient := sdk.Client()
// DID operations
didClient := sonrClient.DID()
did, err := didClient.CreateDID(ctx, didOpts)
// DWN operations
dwnClient := sonrClient.DWN()
record, err := dwnClient.CreateRecord(ctx, recordData)
// Service operations
svcClient := sonrClient.SVC()
service, err := svcClient.RegisterService(ctx, serviceConfig)
// UCAN operations
ucanClient := sonrClient.UCAN()
capability, err := ucanClient.CreateCapability(ctx, capabilitySpec)
```
## API Overview
### Core Client (`sonr.Client`)
The main client provides access to:
- **Query operations**: Read blockchain state
- **Transaction building**: Create and broadcast transactions
- **Module clients**: Access to specialized functionality
- **Connection management**: Handle multiple endpoint types
### Module Clients
- **DID Client**: W3C DID operations with WebAuthn support
- **DWN Client**: Decentralized Web Node data management
- **SVC Client**: Service registration and management
- **UCAN Client**: User-Controlled Authorization Networks
### Key Management
- **Keyring integration**: Support for multiple keyring backends
- **Hardware keys**: WebAuthn and hardware wallet support
- **Multi-signature**: Threshold signature schemes
### Network Configuration
Pre-configured networks:
- **Testnet**: `sonrtest_1-1` with development endpoints
- **Mainnet**: Production network configuration (coming soon)
## Examples
See the `examples/` directory for complete working examples:
- **CLI Tool**: Example command-line application
- **Web Integration**: HTTP API server
- **Key Management**: Keyring and signing examples
- **Module Operations**: Comprehensive module usage
## Documentation
- [API Reference](https://pkg.go.dev/github.com/sonr-io/sonr/client)
- [Sonr Documentation](https://sonr.dev)
- [Blockchain Guide](https://sonr.dev/blockchain)
## Contributing
This SDK is part of the main Sonr repository. Please see the main project's contributing guidelines.
## License
This project is licensed under the Apache 2.0 License.
-363
View File
@@ -1,363 +0,0 @@
// Package auth provides gasless transaction support for WebAuthn operations.
package auth
import (
"context"
"encoding/base64"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/sonr-io/sonr/client/config"
"github.com/sonr-io/sonr/client/errors"
"github.com/sonr-io/sonr/client/tx"
didtypes "github.com/sonr-io/sonr/x/did/types"
)
// GaslessTransactionManager handles gasless transactions for WebAuthn.
type GaslessTransactionManager interface {
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
CreateGaslessRegistration(ctx context.Context, credential *WebAuthnCredential, opts *GaslessRegistrationOptions) (*GaslessTransaction, error)
// BroadcastGasless broadcasts a gasless transaction.
BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error)
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
IsEligibleForGasless(msgs []sdk.Msg) bool
// EstimateGaslessGas estimates gas for a gasless transaction.
EstimateGaslessGas(msgType string) uint64
}
// GaslessRegistrationOptions configures gasless WebAuthn registration.
type GaslessRegistrationOptions struct {
Username string `json:"username"`
AutoCreateVault bool `json:"auto_create_vault"`
WebAuthnChallenge []byte `json:"webauthn_challenge"`
DIDDocument map[string]any `json:"did_document,omitempty"`
}
// GaslessTransaction represents a gasless transaction.
type GaslessTransaction struct {
Messages []sdk.Msg `json:"messages"`
Memo string `json:"memo"`
GasLimit uint64 `json:"gas_limit"`
SignerAddress string `json:"signer_address"`
SignMode signing.SignMode `json:"sign_mode"`
TxBytes []byte `json:"tx_bytes,omitempty"`
}
// BroadcastResult contains the result of broadcasting a transaction.
type BroadcastResult struct {
TxHash string `json:"tx_hash"`
Height int64 `json:"height"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasUsed int64 `json:"gas_used"`
GasWanted int64 `json:"gas_wanted"`
}
// gaslessManager implements GaslessTransactionManager.
type gaslessManager struct {
txBuilder tx.TxBuilder
broadcaster tx.Broadcaster
config *config.NetworkConfig
}
// NewGaslessTransactionManager creates a new gasless transaction manager.
func NewGaslessTransactionManager(
txBuilder tx.TxBuilder,
broadcaster tx.Broadcaster,
cfg *config.NetworkConfig,
) GaslessTransactionManager {
return &gaslessManager{
txBuilder: txBuilder,
broadcaster: broadcaster,
config: cfg,
}
}
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
func (gm *gaslessManager) CreateGaslessRegistration(
ctx context.Context,
credential *WebAuthnCredential,
opts *GaslessRegistrationOptions,
) (*GaslessTransaction, error) {
// Generate deterministic address from WebAuthn credential
signerAddr := gm.generateAddressFromWebAuthn(credential)
// Convert credential ID to base64 string
credentialID := base64.RawURLEncoding.EncodeToString(credential.RawID)
// Create WebAuthn credential for the message
webauthnCred := didtypes.WebAuthnCredential{
CredentialId: credentialID,
PublicKey: credential.PublicKey,
AttestationType: credential.AttestationType,
Origin: "http://localhost", // Default origin
Algorithm: -7, // ES256 algorithm
CreatedAt: time.Now().Unix(),
RpId: "localhost",
RpName: "Sonr Local",
Transports: credential.Transports,
UserVerified: false, // Default to false for gasless
}
// Set user verification if flags are available
if credential.Flags != nil {
webauthnCred.UserVerified = credential.Flags.UserVerified
}
// Create the registration message
msg := &didtypes.MsgRegisterWebAuthnCredential{
Controller: signerAddr.String(),
Username: opts.Username,
WebauthnCredential: webauthnCred,
AutoCreateVault: opts.AutoCreateVault,
}
// Create gasless transaction
gaslessTx := &GaslessTransaction{
Messages: []sdk.Msg{msg},
Memo: "WebAuthn Gasless Registration",
GasLimit: 200000, // Fixed gas limit for WebAuthn registration
SignerAddress: signerAddr.String(),
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
}
// Build the transaction using fluent interface
gm.txBuilder = gm.txBuilder.
ClearMessages().
AddMessage(msg).
WithMemo(gaslessTx.Memo).
WithGasLimit(gaslessTx.GasLimit).
WithFee(sdk.NewCoins()) // Zero fees for gasless
// Build unsigned transaction
unsignedTx, err := gm.txBuilder.Build()
if err != nil {
return nil, errors.WrapError(err, errors.ErrInvalidTransaction, "failed to build gasless transaction")
}
// Store transaction bytes
gaslessTx.TxBytes = unsignedTx.SignBytes
return gaslessTx, nil
}
// BroadcastGasless broadcasts a gasless transaction.
func (gm *gaslessManager) BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error) {
// Broadcast the transaction using sync mode
resp, err := gm.broadcaster.BroadcastSync(ctx, tx.TxBytes)
if err != nil {
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast gasless transaction")
}
result := &BroadcastResult{
TxHash: resp.TxHash,
Height: resp.Height,
Code: resp.Code,
RawLog: resp.Log,
GasUsed: resp.GasUsed,
GasWanted: resp.GasWanted,
}
// Check for errors
if resp.Code != 0 {
return result, fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.Log)
}
return result, nil
}
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
func (gm *gaslessManager) IsEligibleForGasless(msgs []sdk.Msg) bool {
// Only single message transactions are eligible
if len(msgs) != 1 {
return false
}
// Check message type
msgType := sdk.MsgTypeURL(msgs[0])
// WebAuthn registration is gasless
if msgType == "/did.v1.MsgRegisterWebAuthnCredential" {
return true
}
// Future: Add other gasless message types here
return false
}
// EstimateGaslessGas estimates gas for a gasless transaction.
func (gm *gaslessManager) EstimateGaslessGas(msgType string) uint64 {
switch msgType {
case "/did.v1.MsgRegisterWebAuthnCredential":
return 200000 // Fixed gas for WebAuthn registration
default:
return 100000 // Default gas estimate
}
}
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential.
func (gm *gaslessManager) generateAddressFromWebAuthn(credential *WebAuthnCredential) sdk.AccAddress {
// Use the credential ID as seed for address generation
// This ensures the same credential always generates the same address
// In production, this would use a proper derivation scheme
// For now, we'll use the first 20 bytes of the credential ID
addrBytes := make([]byte, 20)
copy(addrBytes, credential.RawID[:min(20, len(credential.RawID))])
return sdk.AccAddress(addrBytes)
}
// Helper function for min
func min(a, b int) int {
if a < b {
return a
}
return b
}
// WebAuthnGaslessClient provides a high-level interface for gasless WebAuthn operations.
type WebAuthnGaslessClient struct {
webauthnClient WebAuthnClient
gaslessManager GaslessTransactionManager
config *config.NetworkConfig
}
// NewWebAuthnGaslessClient creates a new WebAuthn gasless client.
func NewWebAuthnGaslessClient(
webauthnClient WebAuthnClient,
gaslessManager GaslessTransactionManager,
cfg *config.NetworkConfig,
) *WebAuthnGaslessClient {
return &WebAuthnGaslessClient{
webauthnClient: webauthnClient,
gaslessManager: gaslessManager,
config: cfg,
}
}
// RegisterGasless performs gasless WebAuthn registration.
func (wgc *WebAuthnGaslessClient) RegisterGasless(
ctx context.Context,
username string,
displayName string,
) (*GaslessRegistrationResult, error) {
// Create registration options
regOpts := &RegistrationOptions{
Username: username,
DisplayName: displayName,
Timeout: 60000,
UserVerification: "preferred",
AttestationType: "none",
}
// Begin WebAuthn registration
challenge, err := wgc.webauthnClient.BeginRegistration(ctx, regOpts)
if err != nil {
return nil, err
}
// Return challenge for browser to complete
// The actual credential will be created by the browser
result := &GaslessRegistrationResult{
Challenge: challenge,
GaslessEligible: true,
EstimatedGas: wgc.gaslessManager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential"),
}
return result, nil
}
// CompleteGaslessRegistration completes the gasless registration after browser response.
func (wgc *WebAuthnGaslessClient) CompleteGaslessRegistration(
ctx context.Context,
challenge *RegistrationChallenge,
response *AuthenticatorAttestationResponse,
username string,
autoCreateVault bool,
) (*BroadcastResult, error) {
// Complete WebAuthn registration to get credential
credential, err := wgc.webauthnClient.CompleteRegistration(ctx, challenge, response)
if err != nil {
// For now, create a mock credential since CompleteRegistration is not fully implemented
// In production, this would properly parse the attestation response
credential = &WebAuthnCredential{
ID: base64.URLEncoding.EncodeToString(response.AttestationObject[:32]),
RawID: response.AttestationObject[:32],
PublicKey: response.AttestationObject[32:64], // Mock public key
AttestationType: "none",
Flags: &AuthenticatorFlags{
UserPresent: true,
UserVerified: true,
},
Authenticator: &AuthenticatorData{
RPIDHash: challenge.Challenge,
},
UserID: string(challenge.User.ID),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
}
}
// Create gasless registration options
gaslessOpts := &GaslessRegistrationOptions{
Username: username,
AutoCreateVault: autoCreateVault,
WebAuthnChallenge: challenge.Challenge,
}
// Create gasless transaction
gaslessTx, err := wgc.gaslessManager.CreateGaslessRegistration(ctx, credential, gaslessOpts)
if err != nil {
return nil, err
}
// Broadcast gasless transaction
return wgc.gaslessManager.BroadcastGasless(ctx, gaslessTx)
}
// GaslessRegistrationResult contains the result of initiating gasless registration.
type GaslessRegistrationResult struct {
Challenge *RegistrationChallenge `json:"challenge"`
GaslessEligible bool `json:"gasless_eligible"`
EstimatedGas uint64 `json:"estimated_gas"`
}
// ValidateGaslessEligibility validates if a user is eligible for gasless transactions.
func ValidateGaslessEligibility(credential *WebAuthnCredential) error {
// Validate credential is not nil
if credential == nil {
return fmt.Errorf("credential cannot be nil")
}
// Validate credential has required fields
if len(credential.RawID) == 0 {
return fmt.Errorf("credential ID cannot be empty")
}
if len(credential.PublicKey) == 0 {
return fmt.Errorf("public key cannot be empty")
}
// Validate user presence and verification
if credential.Flags != nil {
if !credential.Flags.UserPresent {
return fmt.Errorf("user presence is required for gasless transactions")
}
}
return nil
}
// GetGaslessEndpoint returns the gasless transaction endpoint for the network.
func GetGaslessEndpoint(cfg *config.NetworkConfig) string {
// For now, gasless transactions use the same RPC endpoint
// In the future, this could be a separate endpoint
return cfg.RPC
}

Some files were not shown because too many files have changed in this diff Show More