mirror of
https://github.com/sonr-io/common.git
synced 2026-01-11 20:08:57 +00:00
603 lines
15 KiB
Markdown
603 lines
15 KiB
Markdown
# 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/
|
||
|