mirror of
https://github.com/sonr-io/common.git
synced 2026-01-11 20:08:57 +00:00
refactor(common): simplify WebAuthn metadata handling
This commit is contained in:
110
Makefile
Normal file
110
Makefile
Normal file
@@ -0,0 +1,110 @@
|
||||
# Makefile for sonr-io/common
|
||||
# Provides convenient testing and validation commands for ipfs and webauthn modules
|
||||
|
||||
.PHONY: help test test-ipfs test-webauthn test-all coverage coverage-ipfs coverage-webauthn coverage-all lint vet fmt clean
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " make test - Run all tests"
|
||||
@echo " make test-ipfs - Run tests for ipfs module"
|
||||
@echo " make test-webauthn - Run tests for webauthn module"
|
||||
@echo " make test-all - Run all tests with verbose output"
|
||||
@echo " make coverage - Generate coverage report for all modules"
|
||||
@echo " make coverage-ipfs - Generate coverage report for ipfs module"
|
||||
@echo " make coverage-webauthn - Generate coverage report for webauthn module"
|
||||
@echo " make coverage-all - Generate combined coverage report (HTML)"
|
||||
@echo " make lint - Run golangci-lint"
|
||||
@echo " make vet - Run go vet"
|
||||
@echo " make fmt - Format all Go files"
|
||||
@echo " make clean - Clean test cache and coverage files"
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
@echo "Running tests for all modules..."
|
||||
@go test ./ipfs/... ./webauthn/...
|
||||
|
||||
# Run tests for ipfs module
|
||||
test-ipfs:
|
||||
@echo "Running tests for ipfs module..."
|
||||
@go test ./ipfs/... -v
|
||||
|
||||
# Run tests for webauthn module
|
||||
test-webauthn:
|
||||
@echo "Running tests for webauthn module..."
|
||||
@go test ./webauthn/... -v
|
||||
|
||||
# Run all tests with verbose output
|
||||
test-all:
|
||||
@echo "Running all tests with verbose output..."
|
||||
@go test -v ./ipfs/... ./webauthn/...
|
||||
|
||||
# Generate coverage report for all modules
|
||||
coverage:
|
||||
@echo "Generating coverage report for all modules..."
|
||||
@go test ./ipfs/... ./webauthn/... -coverprofile=coverage.out
|
||||
@go tool cover -func=coverage.out
|
||||
|
||||
# Generate coverage report for ipfs module
|
||||
coverage-ipfs:
|
||||
@echo "Generating coverage report for ipfs module..."
|
||||
@go test ./ipfs/... -coverprofile=coverage-ipfs.out
|
||||
@go tool cover -func=coverage-ipfs.out
|
||||
|
||||
# Generate coverage report for webauthn module
|
||||
coverage-webauthn:
|
||||
@echo "Generating coverage report for webauthn module..."
|
||||
@go test ./webauthn/... -coverprofile=coverage-webauthn.out
|
||||
@go tool cover -func=coverage-webauthn.out
|
||||
|
||||
# Generate HTML coverage report for all modules
|
||||
coverage-all:
|
||||
@echo "Generating HTML coverage report for all modules..."
|
||||
@go test ./ipfs/... ./webauthn/... -coverprofile=coverage.out
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "Coverage report generated: coverage.html"
|
||||
|
||||
# Run golangci-lint (requires golangci-lint to be installed)
|
||||
lint:
|
||||
@echo "Running golangci-lint..."
|
||||
@which golangci-lint > /dev/null || (echo "golangci-lint not found. Install from https://golangci-lint.run/usage/install/" && exit 1)
|
||||
@golangci-lint run ./ipfs/... ./webauthn/...
|
||||
|
||||
# Run go vet
|
||||
vet:
|
||||
@echo "Running go vet..."
|
||||
@go vet ./ipfs/...
|
||||
@go vet ./webauthn/...
|
||||
|
||||
# Format all Go files
|
||||
fmt:
|
||||
@echo "Formatting Go files..."
|
||||
@go fmt ./ipfs/...
|
||||
@go fmt ./webauthn/...
|
||||
|
||||
# Clean test cache and coverage files
|
||||
clean:
|
||||
@echo "Cleaning test cache and coverage files..."
|
||||
@go clean -testcache
|
||||
@rm -f coverage.out coverage-ipfs.out coverage-webauthn.out coverage.html
|
||||
@echo "Clean complete"
|
||||
|
||||
# Run tests with race detector
|
||||
test-race:
|
||||
@echo "Running tests with race detector..."
|
||||
@go test -race ./ipfs/... ./webauthn/...
|
||||
|
||||
# Run benchmarks
|
||||
bench:
|
||||
@echo "Running benchmarks..."
|
||||
@go test -bench=. -benchmem ./ipfs/... ./webauthn/...
|
||||
|
||||
# Install test dependencies
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
@go mod download
|
||||
@go mod tidy
|
||||
|
||||
# Quick check: fmt, vet, and test
|
||||
check: fmt vet test
|
||||
@echo "All checks passed!"
|
||||
602
README.md
602
README.md
@@ -0,0 +1,602 @@
|
||||
# Sonr Common Package
|
||||
|
||||
[](https://golang.org)
|
||||
[](LICENSE)
|
||||
|
||||
The `common` package provides high-level, simplified interfaces for IPFS and WebAuthn functionality, designed for easy integration into external libraries and applications within the Sonr network.
|
||||
|
||||
## Features
|
||||
|
||||
- **IPFS Integration**: Simple, high-level API for storing and retrieving data from IPFS
|
||||
- **WebAuthn Support**: Comprehensive WebAuthn credential management and verification
|
||||
- **Helper Functions**: Convenient wrapper functions that abstract complexity
|
||||
- **Well-Tested**: Comprehensive test coverage with unit and integration tests
|
||||
- **Production-Ready**: Battle-tested implementations with proper error handling
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [IPFS Module](#ipfs-module)
|
||||
- [WebAuthn Module](#webauthn-module)
|
||||
- [Helper Functions](#helper-functions)
|
||||
- [Testing](#testing)
|
||||
- [Examples](#examples)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/sonr-io/common
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### IPFS Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sonr-io/common"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check if IPFS daemon is running
|
||||
if !common.IsIPFSDaemonRunning() {
|
||||
panic("IPFS daemon is not running")
|
||||
}
|
||||
|
||||
// Store data
|
||||
data := []byte("Hello, IPFS!")
|
||||
cid, err := common.StoreData(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Stored data with CID: %s\n", cid)
|
||||
|
||||
// Retrieve data
|
||||
retrieved, err := common.RetrieveData(cid)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Retrieved: %s\n", string(retrieved))
|
||||
}
|
||||
```
|
||||
|
||||
### WebAuthn Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sonr-io/common"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Generate a challenge for WebAuthn ceremony
|
||||
challenge, err := common.NewChallenge()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Challenge: %s\n", challenge)
|
||||
|
||||
// Verify origin
|
||||
origin := "https://example.com"
|
||||
allowedOrigins := []string{"https://example.com", "https://app.example.com"}
|
||||
if err := common.VerifyOrigin(origin, allowedOrigins); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("Origin verified successfully!")
|
||||
}
|
||||
```
|
||||
|
||||
## IPFS Module
|
||||
|
||||
The IPFS module (`ipfs/`) provides a high-level interface for interacting with IPFS nodes.
|
||||
|
||||
### Core Functions
|
||||
|
||||
#### Client Management
|
||||
|
||||
```go
|
||||
// Create a new IPFS client
|
||||
client, err := common.NewIPFSClient()
|
||||
|
||||
// Or use the panic version for critical scenarios
|
||||
client := common.MustGetIPFSClient()
|
||||
|
||||
// Check if daemon is running
|
||||
if common.IsIPFSDaemonRunning() {
|
||||
// Daemon is available
|
||||
}
|
||||
```
|
||||
|
||||
#### Data Storage
|
||||
|
||||
```go
|
||||
// Store raw bytes
|
||||
cid, err := common.StoreData([]byte("data"))
|
||||
|
||||
// Store a file with metadata
|
||||
cid, err := common.StoreFile("document.txt", fileData)
|
||||
|
||||
// Store multiple files as a folder
|
||||
files := map[string][]byte{
|
||||
"file1.txt": []byte("content 1"),
|
||||
"file2.txt": []byte("content 2"),
|
||||
}
|
||||
cid, err := common.StoreFolder(files)
|
||||
```
|
||||
|
||||
#### Data Retrieval
|
||||
|
||||
```go
|
||||
// Retrieve data by CID
|
||||
data, err := common.RetrieveData("QmXxx...")
|
||||
|
||||
// Using client directly for advanced operations
|
||||
client, _ := common.NewIPFSClient()
|
||||
exists, err := client.Exists("QmXxx...")
|
||||
entries, err := client.Ls("QmXxx...")
|
||||
```
|
||||
|
||||
### Advanced IPFS Usage
|
||||
|
||||
```go
|
||||
import "github.com/sonr-io/common/ipfs"
|
||||
|
||||
client, _ := ipfs.GetClient()
|
||||
|
||||
// Pin content
|
||||
err := client.Pin("QmXxx...", "my-important-data")
|
||||
|
||||
// Unpin content
|
||||
err := client.Unpin("QmXxx...")
|
||||
|
||||
// Check if pinned
|
||||
pinned, err := client.IsPinned("ipns-name")
|
||||
|
||||
// Get node status
|
||||
status, err := client.NodeStatus()
|
||||
fmt.Printf("Peer ID: %s\n", status.PeerID)
|
||||
fmt.Printf("Connected Peers: %d\n", status.ConnectedPeers)
|
||||
```
|
||||
|
||||
## WebAuthn Module
|
||||
|
||||
The WebAuthn module (`webauthn/`) provides comprehensive support for WebAuthn authentication.
|
||||
|
||||
### Challenge Generation
|
||||
|
||||
```go
|
||||
// Generate a cryptographic challenge (32 bytes, base64url encoded)
|
||||
challenge, err := common.NewChallenge()
|
||||
|
||||
// Get the standard challenge length
|
||||
length := common.ChallengeLength() // Returns 32
|
||||
```
|
||||
|
||||
### Origin Verification
|
||||
|
||||
```go
|
||||
// Verify origin against allowed list
|
||||
allowedOrigins := []string{
|
||||
"https://example.com",
|
||||
"https://app.example.com:8080",
|
||||
}
|
||||
|
||||
err := common.VerifyOrigin("https://example.com", allowedOrigins)
|
||||
```
|
||||
|
||||
### Base64 URL Encoding
|
||||
|
||||
```go
|
||||
// Encode data to URL-safe base64 (no padding)
|
||||
encoded := common.EncodeBase64URL([]byte("data"))
|
||||
|
||||
// Decode URL-safe base64
|
||||
decoded, err := common.DecodeBase64URL(encoded)
|
||||
```
|
||||
|
||||
### Credential Management
|
||||
|
||||
#### Credential Creation (Registration)
|
||||
|
||||
```go
|
||||
// Unmarshal credential creation response from client
|
||||
credData := []byte(`{"id":"...","type":"public-key",...}`)
|
||||
credResponse, err := common.UnmarshalCredentialCreation(credData)
|
||||
|
||||
// Parse and validate credential creation
|
||||
parsedCred, err := common.ParseCredentialCreation(credData)
|
||||
|
||||
// Marshal credential for storage
|
||||
jsonData, err := common.MarshalCredentialCreation(credResponse)
|
||||
```
|
||||
|
||||
#### Credential Assertion (Authentication)
|
||||
|
||||
```go
|
||||
// Unmarshal assertion response from client
|
||||
assertionData := []byte(`{"id":"...","type":"public-key",...}`)
|
||||
assertionResponse, err := common.UnmarshalCredentialAssertion(assertionData)
|
||||
|
||||
// Parse and validate assertion
|
||||
parsedAssertion, err := common.ParseCredentialAssertion(assertionData)
|
||||
|
||||
// Marshal assertion for storage
|
||||
jsonData, err := common.MarshalCredentialAssertion(assertionResponse)
|
||||
```
|
||||
|
||||
### Advanced WebAuthn Usage
|
||||
|
||||
```go
|
||||
import "github.com/sonr-io/common/webauthn"
|
||||
|
||||
// Create registration options
|
||||
options := &webauthn.PublicKeyCredentialCreationOptions{
|
||||
RelyingParty: webauthn.RelyingPartyEntity{
|
||||
Name: "Example Corp",
|
||||
ID: "example.com",
|
||||
},
|
||||
User: webauthn.UserEntity{
|
||||
ID: []byte("user-id"),
|
||||
Name: "user@example.com",
|
||||
DisplayName: "User Name",
|
||||
},
|
||||
Challenge: challenge,
|
||||
Parameters: []webauthn.CredentialParameter{
|
||||
{Type: webauthn.PublicKeyCredentialType, Algorithm: -7}, // ES256
|
||||
},
|
||||
}
|
||||
|
||||
// Verify credential creation
|
||||
clientDataHash, err := parsedCred.Verify(
|
||||
storedChallenge,
|
||||
verifyUser,
|
||||
verifyUserPresence,
|
||||
relyingPartyID,
|
||||
rpOrigins,
|
||||
rpTopOrigins,
|
||||
rpTopOriginsVerify,
|
||||
metadataProvider,
|
||||
credParams,
|
||||
)
|
||||
|
||||
// Verify credential assertion
|
||||
err = parsedAssertion.Verify(
|
||||
storedChallenge,
|
||||
relyingPartyID,
|
||||
rpOrigins,
|
||||
rpTopOrigins,
|
||||
rpTopOriginsVerify,
|
||||
appID,
|
||||
verifyUser,
|
||||
verifyUserPresence,
|
||||
credentialPublicKey,
|
||||
)
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### IPFS Helpers
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `NewIPFSClient()` | Create IPFS client with detailed error messages |
|
||||
| `MustGetIPFSClient()` | Panic version for critical initialization |
|
||||
| `StoreData(data)` | Store raw bytes and return CID |
|
||||
| `RetrieveData(cid)` | Retrieve content by CID |
|
||||
| `IsIPFSDaemonRunning()` | Check if IPFS daemon is accessible |
|
||||
| `StoreFile(name, data)` | Store file with metadata |
|
||||
| `StoreFolder(files)` | Store multiple files as a folder |
|
||||
|
||||
### WebAuthn Helpers
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `NewChallenge()` | Generate 32-byte cryptographic challenge |
|
||||
| `ChallengeLength()` | Get standard challenge length (32) |
|
||||
| `VerifyOrigin(origin, allowed)` | Verify origin against allowed list |
|
||||
| `EncodeBase64URL(data)` | Encode to URL-safe base64 |
|
||||
| `DecodeBase64URL(encoded)` | Decode URL-safe base64 |
|
||||
| `UnmarshalCredentialCreation(data)` | Parse credential creation response |
|
||||
| `MarshalCredentialCreation(ccr)` | Serialize credential creation |
|
||||
| `ParseCredentialCreation(data)` | Parse and validate creation response |
|
||||
| `UnmarshalCredentialAssertion(data)` | Parse assertion response |
|
||||
| `MarshalCredentialAssertion(car)` | Serialize assertion response |
|
||||
| `ParseCredentialAssertion(data)` | Parse and validate assertion |
|
||||
|
||||
## Testing
|
||||
|
||||
The package includes a comprehensive Makefile for testing and development tasks.
|
||||
|
||||
### Available Make Targets
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run tests for specific modules
|
||||
make test-ipfs
|
||||
make test-webauthn
|
||||
|
||||
# Generate coverage reports
|
||||
make coverage # Show coverage summary
|
||||
make coverage-ipfs # IPFS module coverage
|
||||
make coverage-webauthn # WebAuthn module coverage
|
||||
make coverage-all # HTML coverage report
|
||||
|
||||
# Code quality
|
||||
make fmt # Format code
|
||||
make vet # Run go vet
|
||||
make lint # Run golangci-lint
|
||||
|
||||
# Performance
|
||||
make bench # Run benchmarks
|
||||
make test-race # Run tests with race detector
|
||||
|
||||
# Maintenance
|
||||
make clean # Clean test cache and coverage files
|
||||
make deps # Install dependencies
|
||||
|
||||
# Quick check
|
||||
make check # Run fmt, vet, and test
|
||||
```
|
||||
|
||||
### Running Tests Manually
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Run specific tests
|
||||
go test -v -run TestNewChallenge
|
||||
|
||||
# Run benchmarks
|
||||
go test -bench=. -benchmem ./...
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Current test coverage:
|
||||
- **Common package**: 49.4%
|
||||
- **WebAuthn module**: Comprehensive (50+ tests)
|
||||
- **IPFS module**: Core functionality tested
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete IPFS Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"github.com/sonr-io/common"
|
||||
"github.com/sonr-io/common/ipfs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check daemon availability
|
||||
if !common.IsIPFSDaemonRunning() {
|
||||
log.Fatal("IPFS daemon is not running. Start it with: ipfs daemon")
|
||||
}
|
||||
|
||||
// Get client for advanced operations
|
||||
client, err := common.NewIPFSClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Store a file
|
||||
fileData := []byte("This is my important document")
|
||||
cid, err := common.StoreFile("document.txt", fileData)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Stored file with CID: %s\n", cid)
|
||||
|
||||
// Pin the content
|
||||
if err := client.Pin(cid, "important-document"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Content pinned successfully")
|
||||
|
||||
// Check if content exists
|
||||
exists, err := client.Exists(cid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Content exists: %v\n", exists)
|
||||
|
||||
// Get node status
|
||||
status, err := client.NodeStatus()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Node Status:\n")
|
||||
fmt.Printf(" Peer ID: %s\n", status.PeerID)
|
||||
fmt.Printf(" Connected Peers: %d\n", status.ConnectedPeers)
|
||||
fmt.Printf(" Version: %s\n", status.Version)
|
||||
|
||||
// Store multiple files as a folder
|
||||
folder := map[string][]byte{
|
||||
"readme.md": []byte("# Project\nThis is a test project"),
|
||||
"main.go": []byte("package main\n\nfunc main() {}"),
|
||||
"config.yml": []byte("port: 8080\nhost: localhost"),
|
||||
}
|
||||
folderCID, err := common.StoreFolder(folder)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Stored folder with CID: %s\n", folderCID)
|
||||
|
||||
// List folder contents
|
||||
entries, err := client.Ls(folderCID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Folder contents:")
|
||||
for _, entry := range entries {
|
||||
fmt.Printf(" - %s\n", entry)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complete WebAuthn Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"github.com/sonr-io/common"
|
||||
"github.com/sonr-io/common/webauthn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Generate challenge for registration
|
||||
challenge, err := common.NewChallenge()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Registration Challenge: %s\n", challenge)
|
||||
|
||||
// Verify request origin
|
||||
origin := "https://example.com"
|
||||
allowedOrigins := []string{
|
||||
"https://example.com",
|
||||
"https://app.example.com",
|
||||
}
|
||||
|
||||
if err := common.VerifyOrigin(origin, allowedOrigins); err != nil {
|
||||
log.Fatalf("Origin verification failed: %v", err)
|
||||
}
|
||||
fmt.Println("Origin verified!")
|
||||
|
||||
// Simulate receiving credential creation response from client
|
||||
// In a real scenario, this would come from the browser
|
||||
credentialJSON := []byte(`{
|
||||
"id": "base64-credential-id",
|
||||
"type": "public-key",
|
||||
"rawId": "base64-raw-id",
|
||||
"response": {
|
||||
"clientDataJSON": "base64-client-data",
|
||||
"attestationObject": "base64-attestation"
|
||||
}
|
||||
}`)
|
||||
|
||||
// Parse credential creation response
|
||||
credResponse, err := common.UnmarshalCredentialCreation(credentialJSON)
|
||||
if err != nil {
|
||||
log.Printf("Parse error (expected in demo): %v", err)
|
||||
}
|
||||
|
||||
// For authentication, generate a new challenge
|
||||
authChallenge, err := common.NewChallenge()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Authentication Challenge: %s\n", authChallenge)
|
||||
|
||||
// Encode/decode example
|
||||
secretData := []byte("secret-session-data")
|
||||
encoded := common.EncodeBase64URL(secretData)
|
||||
fmt.Printf("Encoded: %s\n", encoded)
|
||||
|
||||
decoded, err := common.DecodeBase64URL(encoded)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Decoded: %s\n", string(decoded))
|
||||
|
||||
// Challenge length validation
|
||||
expectedLength := common.ChallengeLength()
|
||||
fmt.Printf("Expected challenge length: %d bytes\n", expectedLength)
|
||||
}
|
||||
```
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
common/
|
||||
| ||||