A community based topic aggregation platform built on atproto

Merge feat/communities-v2-pds-managed-keys into main

V2.0 Communities Architecture: PDS-Managed Keys & Password Encryption

This major refactor simplifies community provisioning by delegating all
cryptographic operations to the PDS, enabling faster shipping and better
atProto compliance.

Key Changes:
- Password encryption (not hashing) for session recovery
- PDS-managed DID and key generation
- Removed Coves-side DID generator
- Local PLC directory for E2E testing
- Comprehensive integration tests

Architecture Benefits:
- Simpler codebase (less cryptography to maintain)
- Faster community creation
- Standard atProto migration support
- Better separation of concerns

Migration Path:
- V2.0 (current): PDS-managed keys, Coves-to-Coves migration
- V2.1 (future): Optional Coves rotation key for external migration

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

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

+25 -8
.env.dev
···
PDS_HOSTNAME=localhost
PDS_PORT=3001
-
# DID PLC Directory (use Bluesky's for development)
-
PDS_DID_PLC_URL=https://plc.directory
# JWT Secret (for signing tokens - change in production!)
PDS_JWT_SECRET=local-dev-jwt-secret-change-in-production
···
# =============================================================================
# Identity Resolution Configuration
# =============================================================================
-
# PLC Directory URL for DID resolution
-
IDENTITY_PLC_URL=https://plc.directory
# Cache TTL for resolved identities (Go duration format: 24h, 1h30m, etc.)
IDENTITY_CACHE_TTL=24h
···
# Environment
ENV=development
NODE_ENV=development
IS_DEV_ENV=true
# Logging
···
# PLC Directory Configuration
# =============================================================================
# URL for PLC (Public Ledger of Credentials) directory
-
# Only used when IS_DEV_ENV=false (production)
#
-
# When IS_DEV_ENV=true: Generate did:plc:xxx locally WITHOUT registering (no PLC needed)
-
# When IS_DEV_ENV=false: Generate did:plc:xxx AND register with PLC_DIRECTORY_URL
#
# Production: https://plc.directory (currently Bluesky's, will transfer to third party)
-
PLC_DIRECTORY_URL=https://plc.directory
# =============================================================================
# Notes
···
PDS_HOSTNAME=localhost
PDS_PORT=3001
+
# PDS Service Endpoint for DIDs
+
# This is the URL that goes in DID documents' atproto_pds service endpoint
+
# Must match what the PDS thinks its public URL is (internal port 3000)
+
# Development: http://localhost:3000 (PDS's internal view)
+
# Production: https://pds.coves.social
+
PDS_SERVICE_ENDPOINT=http://localhost:3000
+
+
# DID PLC Directory for PDS
+
# For local E2E testing: Use local PLC (requires --profile plc)
+
# Note: Use container hostname for PDS to reach PLC within Docker network
+
PDS_DID_PLC_URL=http://plc-directory:3000
# JWT Secret (for signing tokens - change in production!)
PDS_JWT_SECRET=local-dev-jwt-secret-change-in-production
···
# =============================================================================
# Identity Resolution Configuration
# =============================================================================
+
# IMPORTANT: In dev mode (IS_DEV_ENV=true), identity resolution automatically
+
# uses PLC_DIRECTORY_URL to ensure E2E tests stay local. In production, you
+
# can optionally set IDENTITY_PLC_URL to use a different URL for read operations.
+
#
+
# For local dev: Leave IDENTITY_PLC_URL unset (uses PLC_DIRECTORY_URL)
+
# For production: Optionally set IDENTITY_PLC_URL=https://plc.directory
# Cache TTL for resolved identities (Go duration format: 24h, 1h30m, etc.)
IDENTITY_CACHE_TTL=24h
···
# Environment
ENV=development
NODE_ENV=development
+
# Always true for local development (use PLC_DIRECTORY_URL to control registration)
IS_DEV_ENV=true
# Logging
···
# PLC Directory Configuration
# =============================================================================
# URL for PLC (Public Ledger of Credentials) directory
#
+
# For local E2E testing with registration: http://localhost:3002 (requires --profile plc)
+
# - Registers DIDs with local PLC directory
+
# - Safe for testing, won't pollute production plc.directory
+
# - PDS must also be configured to use local PLC (see PDS_DID_PLC_URL)
#
# Production: https://plc.directory (currently Bluesky's, will transfer to third party)
+
# - DO NOT use production PLC for testing!
+
#
+
PLC_DIRECTORY_URL=http://localhost:3002
# =============================================================================
# Notes
+91 -65
CLAUDE.md
···
-
# [CLAUDE-BUILD.md](http://claude-build.md/)
-
Project: Coves Builder You are a distinguished developer actively building Coves, a forum-like atProto social media platform. Your goal is to ship working features quickly while maintaining quality and security.
-
## Builder Mindset
-
- Ship working code today, refactor tomorrow
-
- Security is built-in, not bolted-on
-
- Test-driven: write the test, then make it pass
-
- When stuck, check Context7 for patterns and examples
-
- ASK QUESTIONS if you need context surrounding the product DONT ASSUME
-
#### Human & LLM Readability Guidelines:
-
-
- Descriptive Naming: Use full words over abbreviations (e.g., CommunityGovernance not CommGov)
-
-
## atProto Essentials for Coves
-
-
### Architecture
-
-
- **PDS is Self-Contained**: Uses internal SQLite + CAR files (in Docker volume)
-
- **PostgreSQL for AppView Only**: One database for Coves AppView indexing
-
- **Don't Touch PDS Internals**: PDS manages its own storage, we just read from firehose
-
- **Data Flow**: Client → PDS → Firehose → AppView → PostgreSQL
-
-
### Always Consider:
-
-
- [ ]  **Identity**: Every action needs DID verification
-
- [ ]  **Record Types**: Define custom lexicons (e.g., `social.coves.post`, `social.coves.community`)
-
- [ ]  **Is it federated-friendly?** (Can other PDSs interact with it?)
-
- [ ]  **Does the Lexicon make sense?** (Would it work for other forums?)
-
- [ ]  **AppView only indexes**: We don't write to CAR files, only read from firehose
-
## Security-First Building
-
### Every Feature MUST:
-
- [ ]  **Validate all inputs** at the handler level
-
- [ ]  **Use parameterized queries** (never string concatenation)
-
- [ ]  **Check authorization** before any operation
-
- [ ]  **Limit resource access** (pagination, rate limits)
-
- [ ]  **Log security events** (failed auth, invalid inputs)
-
- [ ]  **Never log sensitive data** (passwords, tokens, PII)
-
### Red Flags to Avoid:
-
- `fmt.Sprintf` in SQL queries → Use parameterized queries
-
- Missing `context.Context` → Need it for timeouts/cancellation
-
- No input validation → Add it immediately
-
- Error messages with internal details → Wrap errors properly
-
- Unbounded queries → Add limits/pagination
-
### "How should I structure this?"
-
1. One domain, one package
-
2. Interfaces for testability
-
3. Services coordinate repos
-
4. Handlers only handle XRPC
-
## Pre-Production Advantages
-
Since we're pre-production:
-
- **Break things**: Delete and rebuild rather than complex migrations
-
- **Experiment**: Try approaches, keep what works
-
- **Simplify**: Remove unused code aggressively
-
- **But never compromise security basics**
-
## Success Metrics
-
Your code is ready when:
-
- [ ]  Tests pass (including security tests)
-
- [ ]  Follows atProto patterns
-
- [ ]  Handles errors gracefully
-
- [ ]  Works end-to-end with auth
-
## Quick Checks Before Committing
-
1. **Will it work?** (Integration test proves it)
-
2. **Is it secure?** (Auth, validation, parameterized queries)
-
3. **Is it simple?** (Could you explain to a junior?)
-
4. **Is it complete?** (Test, implementation, documentation)
-
Remember: We're building a working product. Perfect is the enemy of shipped.
···
+
Project: Coves PR Reviewer
+
You are a distinguished senior architect conducting a thorough code review for Coves, a forum-like atProto social media platform.
+
## Review Mindset
+
- Be constructive but thorough - catch issues before they reach production
+
- Question assumptions and look for edge cases
+
- Prioritize security, performance, and maintainability concerns
+
- Suggest alternatives when identifying problems
+
- Ensure there is proper test coverage
+
## Special Attention Areas for Coves
+
- **atProto architecture**: Ensure architecture follows atProto recommendations with WRITE FORWARD ARCHITECTURE (Appview -> PDS -> Relay -> Appview -> App DB (if necessary))
+
- **Federation**: Check for proper DID resolution and identity verification
+
## Review Checklist
+
### 1. Architecture Compliance
+
**MUST VERIFY:**
+
- [ ] NO SQL queries in handlers (automatic rejection if found)
+
- [ ] Proper layer separation: Handler → Service → Repository → Database
+
- [ ] Services use repository interfaces, not concrete implementations
+
- [ ] Dependencies injected via constructors, not globals
+
- [ ] No database packages imported in handlers
+
### 2. Security Review
+
**CHECK FOR:**
+
- SQL injection vulnerabilities (even with prepared statements, verify)
+
- Proper input validation and sanitization
+
- Authentication/authorization checks on all protected endpoints
+
- No sensitive data in logs or error messages
+
- Rate limiting on public endpoints
+
- CSRF protection where applicable
+
- Proper atProto identity verification
+
### 3. Error Handling Audit
+
**VERIFY:**
+
- All errors are handled, not ignored
+
- Error wrapping provides context: `fmt.Errorf("service: %w", err)`
+
- Domain errors defined in core/errors/
+
- HTTP status codes correctly map to error types
+
- No internal error details exposed to API consumers
+
- Nil pointer checks before dereferencing
+
### 4. Performance Considerations
+
**LOOK FOR:**
+
- N+1 query problems
+
- Missing database indexes for frequently queried fields
+
- Unnecessary database round trips
+
- Large unbounded queries without pagination
+
- Memory leaks in goroutines
+
- Proper connection pool usage
+
- Efficient atProto federation calls
+
### 5. Testing Coverage
+
**REQUIRE:**
+
- Unit tests for all new service methods
+
- Integration tests for new API endpoints
+
- Edge case coverage (empty inputs, max values, special characters)
+
- Error path testing
+
- Mock verification in unit tests
+
- No flaky tests (check for time dependencies, random values)
+
### 6. Code Quality
+
**ASSESS:**
+
- Naming follows conventions (full words, not abbreviations)
+
- Functions do one thing well
+
- No code duplication (DRY principle)
+
- Consistent error handling patterns
+
- Proper use of Go idioms
+
- No commented-out code
+
### 7. Breaking Changes
+
**IDENTIFY:**
+
- API contract changes
+
- Database schema modifications affecting existing data
+
- Changes to core interfaces
+
- Modified error codes or response formats
+
### 8. Documentation
+
**ENSURE:**
+
- API endpoints have example requests/responses
+
- Complex business logic is explained
+
- Database migrations include rollback scripts
+
- README updated if setup process changes
+
- Swagger/OpenAPI specs updated if applicable
+
## Review Process
+
1. **First Pass - Automatic Rejections**
+
- SQL in handlers
+
- Missing tests
+
- Security vulnerabilities
+
- Broken layer separation
+
2. **Second Pass - Deep Dive**
+
- Business logic correctness
+
- Edge case handling
+
- Performance implications
+
- Code maintainability
+
3. **Third Pass - Suggestions**
+
- Better patterns or approaches
+
- Refactoring opportunities
+
- Future considerations
+
Then provide detailed feedback organized by: 1. 🚨 **Critical Issues** (must fix) 2. ⚠️ **Important Issues** (should fix) 3. 💡 **Suggestions** (consider for improvement) 4. ✅ **Good Practices Observed** (reinforce positive patterns)
+
Remember: The goal is to ship quality code quickly. Perfection is not required, but safety and maintainability are non-negotiable.
+6 -4
Makefile
···
##@ Local Development (All-in-One)
-
dev-up: ## Start PDS + PostgreSQL + Jetstream for local development
@echo "$(GREEN)Starting Coves development stack...$(RESET)"
-
@docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile jetstream up -d postgres pds jetstream
@echo ""
@echo "$(GREEN)✓ Development stack started!$(RESET)"
@echo ""
@echo "Services available at:"
-
@echo " - PostgreSQL: localhost:5433"
@echo " - PDS (XRPC): http://localhost:3001"
@echo " - PDS Firehose: ws://localhost:3001/xrpc/com.atproto.sync.subscribeRepos"
@echo " - Jetstream: ws://localhost:6008/subscribe $(CYAN)(Read-Forward)$(RESET)"
@echo " - Jetstream Metrics: http://localhost:6009/metrics"
@echo ""
@echo "$(CYAN)Next steps:$(RESET)"
@echo " 1. Run: make run (starts AppView)"
@echo " 2. AppView will auto-index users from Jetstream"
@echo ""
@echo "Run 'make dev-logs' to view logs"
dev-down: ## Stop all development services
···
@echo "$(CYAN)========================================$(RESET)"
@echo ""
@echo "$(CYAN)Prerequisites:$(RESET)"
-
@echo " 1. Run 'make dev-up' (if not already running)"
@echo " 2. Run 'make run' in another terminal (AppView must be running)"
@echo ""
@echo "$(GREEN)Running E2E tests...$(RESET)"
···
##@ Local Development (All-in-One)
+
dev-up: ## Start PDS + PostgreSQL + Jetstream + PLC Directory for local development
@echo "$(GREEN)Starting Coves development stack...$(RESET)"
+
@docker-compose -f docker-compose.dev.yml --env-file .env.dev --profile jetstream --profile plc up -d postgres postgres-plc plc-directory pds jetstream
@echo ""
@echo "$(GREEN)✓ Development stack started!$(RESET)"
@echo ""
@echo "Services available at:"
+
@echo " - PostgreSQL: localhost:5435"
@echo " - PDS (XRPC): http://localhost:3001"
@echo " - PDS Firehose: ws://localhost:3001/xrpc/com.atproto.sync.subscribeRepos"
@echo " - Jetstream: ws://localhost:6008/subscribe $(CYAN)(Read-Forward)$(RESET)"
@echo " - Jetstream Metrics: http://localhost:6009/metrics"
+
@echo " - PLC Directory: http://localhost:3002 $(CYAN)(Local DID registry)$(RESET)"
@echo ""
@echo "$(CYAN)Next steps:$(RESET)"
@echo " 1. Run: make run (starts AppView)"
@echo " 2. AppView will auto-index users from Jetstream"
@echo ""
+
@echo "$(CYAN)Note:$(RESET) Using local PLC directory - DIDs registered locally (won't pollute plc.directory)"
@echo "Run 'make dev-logs' to view logs"
dev-down: ## Stop all development services
···
@echo "$(CYAN)========================================$(RESET)"
@echo ""
@echo "$(CYAN)Prerequisites:$(RESET)"
+
@echo " 1. Run 'make dev-up' (starts PDS + Jetstream)"
@echo " 2. Run 'make run' in another terminal (AppView must be running)"
@echo ""
@echo "$(GREEN)Running E2E tests...$(RESET)"
+35 -18
cmd/server/main.go
···
"Coves/internal/api/handlers/oauth"
"Coves/internal/api/middleware"
"Coves/internal/api/routes"
-
"Coves/internal/atproto/did"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
r.Use(rateLimiter.Middleware)
// Initialize identity resolver
identityConfig := identity.DefaultConfig()
-
// Override from environment if set
-
if plcURL := os.Getenv("IDENTITY_PLC_URL"); plcURL != "" {
-
identityConfig.PLCURL = plcURL
}
if cacheTTL := os.Getenv("IDENTITY_CACHE_TTL"); cacheTTL != "" {
if duration, parseErr := time.ParseDuration(cacheTTL); parseErr == nil {
identityConfig.CacheTTL = duration
···
}
identityResolver := identity.NewResolver(db, identityConfig)
-
log.Println("Identity resolver initialized with PLC:", identityConfig.PLCURL)
// Initialize OAuth session store
sessionStore := oauthCore.NewPostgresSessionStore(db)
···
communityRepo := postgresRepo.NewCommunityRepository(db)
-
// Initialize DID generator for communities
-
// IS_DEV_ENV=true: Generate did:plc:xxx without registering to PLC directory
-
// IS_DEV_ENV=false: Generate did:plc:xxx and register with PLC_DIRECTORY_URL
-
isDevEnv := os.Getenv("IS_DEV_ENV") == "true"
-
plcDirectoryURL := os.Getenv("PLC_DIRECTORY_URL")
-
if plcDirectoryURL == "" {
-
plcDirectoryURL = "https://plc.directory" // Default to Bluesky's PLC
-
}
-
didGenerator := did.NewGenerator(isDevEnv, plcDirectoryURL)
-
log.Printf("DID generator initialized (dev_mode=%v, plc_url=%s)", isDevEnv, plcDirectoryURL)
instanceDID := os.Getenv("INSTANCE_DID")
if instanceDID == "" {
···
log.Printf("Instance domain: %s (extracted from DID: %s)", instanceDomain, instanceDID)
-
// V2: Initialize PDS account provisioner for communities
-
provisioner := communities.NewPDSAccountProvisioner(userService, instanceDomain, defaultPDS)
-
communityService := communities.NewCommunityService(communityRepo, didGenerator, defaultPDS, instanceDID, instanceDomain, provisioner)
// Authenticate Coves instance with PDS to enable community record writes
// The instance needs a PDS account to write community records it owns
···
"Coves/internal/api/handlers/oauth"
"Coves/internal/api/middleware"
"Coves/internal/api/routes"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
r.Use(rateLimiter.Middleware)
// Initialize identity resolver
+
// IMPORTANT: In dev mode, identity resolution MUST use the same local PLC
+
// directory as DID registration to ensure E2E tests work without hitting
+
// the production plc.directory
identityConfig := identity.DefaultConfig()
+
+
isDevEnv := os.Getenv("IS_DEV_ENV") == "true"
+
plcDirectoryURL := os.Getenv("PLC_DIRECTORY_URL")
+
if plcDirectoryURL == "" {
+
plcDirectoryURL = "https://plc.directory" // Default to production PLC
+
}
+
+
// In dev mode, use PLC_DIRECTORY_URL for identity resolution
+
// In prod mode, use IDENTITY_PLC_URL if set, otherwise PLC_DIRECTORY_URL
+
if isDevEnv {
+
identityConfig.PLCURL = plcDirectoryURL
+
log.Printf("🧪 DEV MODE: Identity resolver will use local PLC: %s", plcDirectoryURL)
+
} else {
+
// Production: Allow separate IDENTITY_PLC_URL for read operations
+
if identityPLCURL := os.Getenv("IDENTITY_PLC_URL"); identityPLCURL != "" {
+
identityConfig.PLCURL = identityPLCURL
+
} else {
+
identityConfig.PLCURL = plcDirectoryURL
+
}
+
log.Printf("✅ PRODUCTION MODE: Identity resolver using PLC: %s", identityConfig.PLCURL)
}
+
if cacheTTL := os.Getenv("IDENTITY_CACHE_TTL"); cacheTTL != "" {
if duration, parseErr := time.ParseDuration(cacheTTL); parseErr == nil {
identityConfig.CacheTTL = duration
···
}
identityResolver := identity.NewResolver(db, identityConfig)
// Initialize OAuth session store
sessionStore := oauthCore.NewPostgresSessionStore(db)
···
communityRepo := postgresRepo.NewCommunityRepository(db)
+
// V2.0: PDS-managed DID generation
+
// Community DIDs and keys are generated entirely by the PDS
+
// No Coves-side DID generator needed (reserved for future V2.1 hybrid approach)
instanceDID := os.Getenv("INSTANCE_DID")
if instanceDID == "" {
···
log.Printf("Instance domain: %s (extracted from DID: %s)", instanceDomain, instanceDID)
+
// V2.0: Initialize PDS account provisioner for communities (simplified)
+
// PDS handles all DID and key generation - no Coves-side cryptography needed
+
provisioner := communities.NewPDSAccountProvisioner(instanceDomain, defaultPDS)
+
log.Printf("✅ Community provisioner initialized (PDS-managed keys)")
+
log.Printf(" - Communities will be created at: %s", defaultPDS)
+
log.Printf(" - PDS will generate and manage all DIDs and keys")
+
// Initialize community service (no longer needs didGenerator directly)
+
communityService := communities.NewCommunityService(communityRepo, defaultPDS, instanceDID, instanceDomain, provisioner)
// Authenticate Coves instance with PDS to enable community record writes
// The instance needs a PDS account to write community records it owns
+86
docker-compose.dev.yml
···
profiles:
- jetstream
# Indigo Relay (BigSky) - OPTIONAL for local dev
# WARNING: BigSky is designed to crawl the entire atProto network!
# For local dev, consider using direct PDS firehose instead (see AppView config below)
···
name: coves-dev-postgres-data
postgres-test-data:
name: coves-test-postgres-data
pds-data:
name: coves-dev-pds-data
jetstream-data:
name: coves-dev-jetstream-data
···
profiles:
- jetstream
+
# PostgreSQL Database for PLC Directory (Port 5436)
+
# Separate database for local PLC directory to avoid conflicts
+
postgres-plc:
+
image: postgres:15
+
container_name: coves-dev-postgres-plc
+
ports:
+
- "5436:5432"
+
environment:
+
POSTGRES_DB: plc_dev
+
POSTGRES_USER: plc_user
+
POSTGRES_PASSWORD: plc_password
+
volumes:
+
- postgres-plc-data:/var/lib/postgresql/data
+
networks:
+
- coves-dev
+
healthcheck:
+
test: ["CMD-SHELL", "pg_isready -U plc_user -d plc_dev"]
+
interval: 5s
+
timeout: 5s
+
retries: 5
+
profiles:
+
- plc
+
+
# Local PLC Directory - For E2E testing without polluting production plc.directory
+
# This allows dev mode DID registration for testing community provisioning
+
#
+
# Usage:
+
# docker-compose --profile plc up postgres-plc plc-directory
+
# Or with all services: docker-compose --profile jetstream --profile plc up
+
#
+
# Configuration in your tests:
+
# PLC_DIRECTORY_URL=http://localhost:3002
+
# IS_DEV_ENV=false # Use production mode but point to local PLC
+
plc-directory:
+
image: node:18-alpine
+
container_name: coves-dev-plc
+
ports:
+
- "3002:3000" # PLC directory API
+
working_dir: /app
+
command: >
+
sh -c "
+
if [ ! -d '/app/.git' ]; then
+
echo 'First run: Installing PLC directory...' &&
+
apk add --no-cache git python3 make g++ yarn &&
+
git clone https://github.com/did-method-plc/did-method-plc.git . &&
+
yarn install --frozen-lockfile &&
+
yarn build &&
+
echo 'PLC directory installed successfully!'
+
fi &&
+
cd packages/server &&
+
yarn start
+
"
+
environment:
+
# Point to dedicated PLC PostgreSQL database
+
DATABASE_URL: postgresql://plc_user:plc_password@postgres-plc:5432/plc_dev?sslmode=disable
+
+
# Development settings
+
DEBUG_MODE: "1"
+
LOG_ENABLED: "true"
+
LOG_LEVEL: debug
+
LOG_DESTINATION: "1"
+
NODE_ENV: development
+
+
# API configuration
+
PORT: 3000
+
volumes:
+
# Persist the PLC repo so we don't rebuild every time
+
- plc-app-data:/app
+
networks:
+
- coves-dev
+
depends_on:
+
postgres-plc:
+
condition: service_healthy
+
healthcheck:
+
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"]
+
interval: 10s
+
timeout: 5s
+
retries: 10
+
start_period: 120s
+
profiles:
+
- plc
+
# Indigo Relay (BigSky) - OPTIONAL for local dev
# WARNING: BigSky is designed to crawl the entire atProto network!
# For local dev, consider using direct PDS firehose instead (see AppView config below)
···
name: coves-dev-postgres-data
postgres-test-data:
name: coves-test-postgres-data
+
postgres-plc-data:
+
name: coves-dev-postgres-plc-data
pds-data:
name: coves-dev-pds-data
jetstream-data:
name: coves-dev-jetstream-data
+
plc-app-data:
+
name: coves-dev-plc-app-data
+7 -36
go.mod
···
go 1.24
require (
-
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e
github.com/go-chi/chi/v5 v5.2.1
github.com/gorilla/sessions v1.4.0
-
github.com/ipfs/go-cid v0.4.1
-
github.com/ipfs/go-ipld-cbor v0.1.0
-
github.com/ipfs/go-ipld-format v0.6.0
-
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4
github.com/lestrrat-go/jwx/v2 v2.0.12
github.com/lib/pq v1.10.9
github.com/pressly/goose/v3 v3.22.1
-
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
-
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
-
github.com/gocql/gocql v1.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
-
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
-
github.com/gorilla/websocket v1.5.3 // indirect
-
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
-
github.com/ipfs/go-blockservice v0.5.2 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
-
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
-
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
-
github.com/ipfs/go-libipfs v0.7.0 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
-
github.com/ipfs/go-merkledag v0.11.0 // indirect
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
-
github.com/ipfs/go-verifcid v0.0.3 // indirect
-
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
-
github.com/ipld/go-ipld-prime v0.21.0 // indirect
-
github.com/jackc/pgpassfile v1.0.0 // indirect
-
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
-
github.com/jackc/pgx/v5 v5.7.1 // indirect
-
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
-
github.com/jinzhu/inflection v1.0.0 // indirect
-
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
···
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
-
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
···
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
-
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
-
github.com/urfave/cli/v2 v2.25.7 // indirect
-
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
···
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
-
golang.org/x/crypto v0.31.0 // indirect
-
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
-
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.3.0 // indirect
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.33.0 // indirect
-
gopkg.in/inf.v0 v0.9.1 // indirect
-
gorm.io/driver/postgres v1.6.0 // indirect
-
gorm.io/gorm v1.30.0 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
···
go 1.24
require (
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe
github.com/go-chi/chi/v5 v5.2.1
github.com/gorilla/sessions v1.4.0
+
github.com/gorilla/websocket v1.5.3
github.com/lestrrat-go/jwx/v2 v2.0.12
github.com/lib/pq v1.10.9
github.com/pressly/goose/v3 v3.22.1
+
golang.org/x/crypto v0.31.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
+
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
+
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
+
github.com/ipfs/go-ipld-format v0.6.0 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
···
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
···
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
···
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.33.0 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
+4 -160
go.sum
···
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
-
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
-
github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b h1:QniihTdfvYFr8oJZgltN0VyWSWa28v/0DiIVFHy6nfg=
-
github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b/go.mod h1:8FlFpF5cIq3DQG0kEHqyTkPV/5MDQoaWLcVwza5ZPJU=
-
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU=
-
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8=
-
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
-
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-
github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0=
-
github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
···
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
-
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
···
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-
github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
-
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
-
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
···
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
-
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
-
github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
-
github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
-
github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
-
github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk=
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
-
github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8=
-
github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
-
github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ=
-
github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
-
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
-
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
-
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s=
-
github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E=
-
github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA=
-
github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s=
-
github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY=
-
github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY=
-
github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc=
-
github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo=
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
-
github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk=
-
github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM=
-
github.com/ipfs/go-libipfs v0.7.0 h1:Mi54WJTODaOL2/ZSm5loi3SwI3jI2OuFWUrQIkJ5cpM=
-
github.com/ipfs/go-libipfs v0.7.0/go.mod h1:KsIf/03CqhICzyRGyGo68tooiBE2iFbI/rXW7FhAYr0=
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
-
github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY=
-
github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4=
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
-
github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU=
-
github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM=
-
github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs=
-
github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw=
-
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI=
-
github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA=
-
github.com/ipld/go-car/v2 v2.13.1 h1:KnlrKvEPEzr5IZHKTXLAEub+tPrzeAFQVRlSQvuxBO4=
-
github.com/ipld/go-car/v2 v2.13.1/go.mod h1:QkdjjFNGit2GIkpQ953KBwowuoukoM75nP/JI1iDJdo=
-
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
-
github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
-
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
-
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
-
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
-
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
-
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
-
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
-
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
-
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
-
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
-
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
-
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
-
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
-
github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8=
-
github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
···
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
-
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
-
github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c=
-
github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic=
-
github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw=
-
github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4=
-
github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw=
-
github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI=
-
github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0=
-
github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk=
-
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
-
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
-
github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU=
-
github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY=
-
github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg=
-
github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM=
-
github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE=
-
github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI=
-
github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo=
-
github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
-
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
-
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
-
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
-
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
···
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
-
github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc=
-
github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs=
-
github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A=
-
github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk=
-
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
-
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
-
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
-
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
-
github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o=
-
github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
-
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk=
-
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
···
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
-
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
···
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
-
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
-
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
···
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-
github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
-
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
-
github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s=
-
github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
-
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0=
-
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
-
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
-
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
···
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
-
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
···
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
···
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
···
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
-
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
···
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
-
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
-
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
-
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
-
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
-
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
-
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
-
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
···
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe h1:VBhaqE5ewQgXbY5SfSWFZC/AwHFo7cHxZKFYi2ce9Yo=
+
github.com/bluesky-social/indigo v0.0.0-20251009212240-20524de167fe/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
···
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
···
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
···
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
···
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
···
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
···
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
···
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
···
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
···
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
···
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
···
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
···
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
···
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
···
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
···
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
···
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
···
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
-80
internal/atproto/did/generator.go
···
-
package did
-
-
import (
-
"crypto/rand"
-
"encoding/base32"
-
"fmt"
-
"strings"
-
)
-
-
// Generator creates DIDs for Coves entities
-
type Generator struct {
-
plcDirectoryURL string
-
isDevEnv bool
-
}
-
-
// NewGenerator creates a new DID generator
-
// isDevEnv: true for local development (no PLC registration), false for production (register with PLC)
-
// plcDirectoryURL: URL for PLC directory (e.g., "https://plc.directory")
-
func NewGenerator(isDevEnv bool, plcDirectoryURL string) *Generator {
-
return &Generator{
-
isDevEnv: isDevEnv,
-
plcDirectoryURL: plcDirectoryURL,
-
}
-
}
-
-
// GenerateCommunityDID creates a new random DID for a community
-
// Format: did:plc:{base32-random}
-
//
-
// Dev mode (isDevEnv=true): Generates did:plc:xxx without registering to PLC
-
// Prod mode (isDevEnv=false): Generates did:plc:xxx AND registers with PLC directory
-
//
-
// See: https://github.com/bluesky-social/did-method-plc
-
func (g *Generator) GenerateCommunityDID() (string, error) {
-
// Generate 16 random bytes for the DID identifier
-
randomBytes := make([]byte, 16)
-
if _, err := rand.Read(randomBytes); err != nil {
-
return "", fmt.Errorf("failed to generate random DID: %w", err)
-
}
-
-
// Encode as base32 (lowercase, no padding) - matches PLC format
-
encoded := base32.StdEncoding.EncodeToString(randomBytes)
-
encoded = strings.ToLower(strings.TrimRight(encoded, "="))
-
-
did := fmt.Sprintf("did:plc:%s", encoded)
-
-
// TODO: In production (isDevEnv=false), register this DID with PLC directory
-
// This would involve:
-
// 1. Generate signing keypair for the DID
-
// 2. Create DID document with service endpoints
-
// 3. POST to plcDirectoryURL to register
-
// 4. Store keypair securely for future DID updates
-
//
-
// For now, we just generate the identifier (works fine for local dev)
-
// Production PLC registration is not yet implemented - DIDs are generated
-
// locally but not registered with the PLC directory. This is acceptable
-
// for development and private instances, but production deployments should
-
// implement full PLC registration to ensure DIDs are globally resolvable.
-
_ = g.isDevEnv // Acknowledge that isDevEnv will be used when PLC registration is implemented
-
-
return did, nil
-
}
-
-
// ValidateDID checks if a DID string is properly formatted
-
// Supports did:plc, did:web (for instances)
-
func ValidateDID(did string) bool {
-
if !strings.HasPrefix(did, "did:") {
-
return false
-
}
-
-
parts := strings.Split(did, ":")
-
if len(parts) < 3 {
-
return false
-
}
-
-
method := parts[1]
-
identifier := parts[2]
-
-
// Basic validation: method and identifier must not be empty
-
return method != "" && identifier != ""
-
}
···
-127
internal/atproto/did/generator_test.go
···
-
package did
-
-
import (
-
"strings"
-
"testing"
-
)
-
-
func TestGenerateCommunityDID(t *testing.T) {
-
tests := []struct {
-
name string
-
plcDirectoryURL string
-
want string
-
isDevEnv bool
-
}{
-
{
-
name: "generates did:plc in dev mode",
-
isDevEnv: true,
-
plcDirectoryURL: "https://plc.directory",
-
want: "did:plc:",
-
},
-
{
-
name: "generates did:plc in prod mode",
-
isDevEnv: false,
-
plcDirectoryURL: "https://plc.directory",
-
want: "did:plc:",
-
},
-
}
-
-
for _, tt := range tests {
-
t.Run(tt.name, func(t *testing.T) {
-
g := NewGenerator(tt.isDevEnv, tt.plcDirectoryURL)
-
did, err := g.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("GenerateCommunityDID() error = %v", err)
-
}
-
-
if !strings.HasPrefix(did, tt.want) {
-
t.Errorf("GenerateCommunityDID() = %v, want prefix %v", did, tt.want)
-
}
-
-
// Verify it's a valid DID
-
if !ValidateDID(did) {
-
t.Errorf("Generated DID failed validation: %v", did)
-
}
-
})
-
}
-
}
-
-
func TestGenerateCommunityDID_Uniqueness(t *testing.T) {
-
g := NewGenerator(true, "https://plc.directory")
-
-
// Generate 100 DIDs and ensure they're all unique
-
dids := make(map[string]bool)
-
for i := 0; i < 100; i++ {
-
did, err := g.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("GenerateCommunityDID() error = %v", err)
-
}
-
-
if dids[did] {
-
t.Errorf("Duplicate DID generated: %v", did)
-
}
-
dids[did] = true
-
}
-
}
-
-
func TestValidateDID(t *testing.T) {
-
tests := []struct {
-
name string
-
did string
-
want bool
-
}{
-
{
-
name: "valid did:plc",
-
did: "did:plc:z72i7hdynmk6r22z27h6tvur",
-
want: true,
-
},
-
{
-
name: "valid did:plc with base32",
-
did: "did:plc:abc123xyz",
-
want: true,
-
},
-
{
-
name: "valid did:web",
-
did: "did:web:coves.social",
-
want: true,
-
},
-
{
-
name: "valid did:web with path",
-
did: "did:web:coves.social:community:gaming",
-
want: true,
-
},
-
{
-
name: "invalid: missing prefix",
-
did: "plc:abc123",
-
want: false,
-
},
-
{
-
name: "invalid: missing method",
-
did: "did::abc123",
-
want: false,
-
},
-
{
-
name: "invalid: missing identifier",
-
did: "did:plc:",
-
want: false,
-
},
-
{
-
name: "invalid: only did",
-
did: "did:",
-
want: false,
-
},
-
{
-
name: "invalid: empty string",
-
did: "",
-
want: false,
-
},
-
}
-
-
for _, tt := range tests {
-
t.Run(tt.name, func(t *testing.T) {
-
if got := ValidateDID(tt.did); got != tt.want {
-
t.Errorf("ValidateDID(%v) = %v, want %v", tt.did, got, tt.want)
-
}
-
})
-
}
-
}
···
+9 -7
internal/core/communities/community.go
···
type Community struct {
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
-
PDSAccessToken string `json:"-" db:"pds_access_token"`
-
FederatedID string `json:"federatedId,omitempty" db:"federated_id"`
DisplayName string `json:"displayName" db:"display_name"`
Description string `json:"description" db:"description"`
PDSURL string `json:"-" db:"pds_url"`
···
CreatedByDID string `json:"createdByDid" db:"created_by_did"`
HostedByDID string `json:"hostedByDid" db:"hosted_by_did"`
PDSEmail string `json:"-" db:"pds_email"`
-
PDSPasswordHash string `json:"-" db:"pds_password_hash"`
Name string `json:"name" db:"name"`
RecordCID string `json:"recordCid,omitempty" db:"record_cid"`
-
RecordURI string `json:"recordUri,omitempty" db:"record_uri"`
-
Visibility string `json:"visibility" db:"visibility"`
-
DID string `json:"did" db:"did"`
ModerationType string `json:"moderationType,omitempty" db:"moderation_type"`
Handle string `json:"handle" db:"handle"`
PDSRefreshToken string `json:"-" db:"pds_refresh_token"`
-
FederatedFrom string `json:"federatedFrom,omitempty" db:"federated_from"`
ContentWarnings []string `json:"contentWarnings,omitempty" db:"content_warnings"`
DescriptionFacets []byte `json:"descriptionFacets,omitempty" db:"description_facets"`
PostCount int `json:"postCount" db:"post_count"`
···
type Community struct {
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
+
RecordURI string `json:"recordUri,omitempty" db:"record_uri"`
+
FederatedFrom string `json:"federatedFrom,omitempty" db:"federated_from"`
DisplayName string `json:"displayName" db:"display_name"`
Description string `json:"description" db:"description"`
PDSURL string `json:"-" db:"pds_url"`
···
CreatedByDID string `json:"createdByDid" db:"created_by_did"`
HostedByDID string `json:"hostedByDid" db:"hosted_by_did"`
PDSEmail string `json:"-" db:"pds_email"`
+
PDSPassword string `json:"-" db:"pds_password_encrypted"`
Name string `json:"name" db:"name"`
RecordCID string `json:"recordCid,omitempty" db:"record_cid"`
+
FederatedID string `json:"federatedId,omitempty" db:"federated_id"`
+
PDSAccessToken string `json:"-" db:"pds_access_token"`
+
SigningKeyPEM string `json:"-" db:"signing_key_encrypted"`
ModerationType string `json:"moderationType,omitempty" db:"moderation_type"`
Handle string `json:"handle" db:"handle"`
PDSRefreshToken string `json:"-" db:"pds_refresh_token"`
+
Visibility string `json:"visibility" db:"visibility"`
+
RotationKeyPEM string `json:"-" db:"rotation_key_encrypted"`
+
DID string `json:"did" db:"did"`
ContentWarnings []string `json:"contentWarnings,omitempty" db:"content_warnings"`
DescriptionFacets []byte `json:"descriptionFacets,omitempty" db:"description_facets"`
PostCount int `json:"postCount" db:"post_count"`
+91 -59
internal/core/communities/pds_provisioning.go
···
package communities
import (
-
"Coves/internal/core/users"
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
-
"golang.org/x/crypto/bcrypt"
)
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
-
DID string // Community's DID (owns the repository)
-
Handle string // Community's handle (e.g., gaming.coves.social)
-
Email string // System email for PDS account
-
PasswordHash string // bcrypt hash of generated password
-
AccessToken string // JWT for making API calls as the community
-
RefreshToken string // For refreshing sessions
-
PDSURL string // PDS hosting this community
}
-
// PDSAccountProvisioner creates PDS accounts for communities
type PDSAccountProvisioner struct {
-
userService users.UserService
instanceDomain string
-
pdsURL string
}
-
// NewPDSAccountProvisioner creates a new provisioner
-
func NewPDSAccountProvisioner(userService users.UserService, instanceDomain, pdsURL string) *PDSAccountProvisioner {
return &PDSAccountProvisioner{
-
userService: userService,
instanceDomain: instanceDomain,
pdsURL: pdsURL,
}
}
-
// ProvisionCommunityAccount creates a real PDS account for a community
//
-
// This function:
-
// 1. Generates a unique handle (e.g., gaming.coves.social)
-
// 2. Generates a system email (e.g., community-gaming@system.coves.social)
-
// 3. Generates a secure random password
-
// 4. Calls com.atproto.server.createAccount via the PDS
-
// 5. The PDS automatically generates and stores the signing keypair
-
// 6. Returns credentials for Coves to act on behalf of the community
//
-
// V2 Architecture:
-
// - Community DID owns its own repository (at://community_did/...)
-
// - PDS manages signing keys (we never see them)
-
// - We store credentials to authenticate as the community
-
// - Future: Add rotation key management for true portability (V2.1)
func (p *PDSAccountProvisioner) ProvisionCommunityAccount(
ctx context.Context,
communityName string,
···
return nil, fmt.Errorf("community name is required")
}
-
// 1. Generate unique handle for the community using subdomain
-
// This makes it immediately clear these are communities, not user accounts
// Format: {name}.communities.{instance-domain}
handle := fmt.Sprintf("%s.communities.%s", strings.ToLower(communityName), p.instanceDomain)
-
// Example: "gaming.communities.coves.social" (much cleaner!)
// 2. Generate system email for PDS account management
// This email is used for account operations, not for user communication
email := fmt.Sprintf("community-%s@communities.%s", strings.ToLower(communityName), p.instanceDomain)
-
// Example: "community-gaming@communities.coves.social"
// 3. Generate secure random password (32 characters)
// This password is never shown to users - it's for Coves to authenticate as the community
···
return nil, fmt.Errorf("failed to generate password: %w", err)
}
-
// 4. Call PDS com.atproto.server.createAccount
// The PDS will:
-
// - Generate a signing keypair (we never see the private key)
-
// - Create a DID (did:plc:xxx)
-
// - Store the private signing key securely
-
// - Return DID, handle, and authentication tokens
-
//
-
// Note: No inviteCode needed for our local PDS (configure PDS with invites disabled)
-
resp, err := p.userService.RegisterAccount(ctx, users.RegisterAccountRequest{
Handle: handle,
-
Email: email,
-
Password: password,
-
// InviteCode: "", // Not needed if PDS has open registration or we're admin
-
})
-
if err != nil {
-
return nil, fmt.Errorf("PDS account creation failed for community %s: %w", communityName, err)
}
-
// 5. Hash the password for storage
-
// We need to store the password hash so we can re-authenticate if tokens expire
-
// This is secure - bcrypt is industry standard
-
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
-
return nil, fmt.Errorf("failed to hash password: %w", err)
}
-
// 6. Return account credentials
return &CommunityPDSAccount{
-
DID: resp.DID, // The community's DID - it owns its own repository!
-
Handle: resp.Handle, // e.g., gaming.coves.social
-
Email: email, // community-gaming@system.coves.social
-
PasswordHash: string(passwordHash), // bcrypt hash for re-authentication
-
AccessToken: resp.AccessJwt, // JWT for making API calls as the community
-
RefreshToken: resp.RefreshJwt, // For refreshing sessions when access token expires
-
PDSURL: resp.PDSURL, // PDS hosting this community's repository
}, nil
}
···
return password, nil
}
···
package communities
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
+
"github.com/bluesky-social/indigo/api/atproto"
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/xrpc"
)
// CommunityPDSAccount represents PDS account credentials for a community
type CommunityPDSAccount struct {
+
DID string // Community's DID (owns the repository)
+
Handle string // Community's handle (e.g., gaming.communities.coves.social)
+
Email string // System email for PDS account
+
Password string // Cleartext password (MUST be encrypted before database storage)
+
AccessToken string // JWT for making API calls as the community
+
RefreshToken string // For refreshing sessions
+
PDSURL string // PDS hosting this community
+
RotationKeyPEM string // PEM-encoded rotation key (for portability)
+
SigningKeyPEM string // PEM-encoded signing key (for atproto operations)
}
+
// PDSAccountProvisioner creates PDS accounts for communities with PDS-managed DIDs
type PDSAccountProvisioner struct {
instanceDomain string
+
pdsURL string // URL to call PDS (e.g., http://localhost:3001)
}
+
// NewPDSAccountProvisioner creates a new provisioner for V2.0 (PDS-managed keys)
+
func NewPDSAccountProvisioner(instanceDomain, pdsURL string) *PDSAccountProvisioner {
return &PDSAccountProvisioner{
instanceDomain: instanceDomain,
pdsURL: pdsURL,
}
}
+
// ProvisionCommunityAccount creates a real PDS account for a community with PDS-managed keys
+
//
+
// V2.0 Architecture (PDS-Managed Keys):
+
// 1. Generates community handle and credentials
+
// 2. Calls com.atproto.server.createAccount (PDS generates DID and keys)
+
// 3. Returns credentials for storage
+
//
+
// V2.0 Design Philosophy:
+
// - PDS manages ALL cryptographic keys (signing + rotation)
+
// - Communities can migrate between Coves-controlled PDSs using standard atProto migration
+
// - Simpler, faster, ships immediately
+
// - Migration uses com.atproto.server.getServiceAuth + standard migration endpoints
//
+
// Future V2.1 (Optional Portability Enhancement):
+
// - Add Coves-controlled rotation key alongside PDS rotation key
+
// - Enables migration to non-Coves PDSs
+
// - Implement when actual external migration is needed
//
+
// SECURITY: The returned credentials MUST be encrypted before database storage
func (p *PDSAccountProvisioner) ProvisionCommunityAccount(
ctx context.Context,
communityName string,
···
return nil, fmt.Errorf("community name is required")
}
+
// 1. Generate unique handle for the community
// Format: {name}.communities.{instance-domain}
+
// Example: "gaming.communities.coves.social"
handle := fmt.Sprintf("%s.communities.%s", strings.ToLower(communityName), p.instanceDomain)
// 2. Generate system email for PDS account management
// This email is used for account operations, not for user communication
email := fmt.Sprintf("community-%s@communities.%s", strings.ToLower(communityName), p.instanceDomain)
// 3. Generate secure random password (32 characters)
// This password is never shown to users - it's for Coves to authenticate as the community
···
return nil, fmt.Errorf("failed to generate password: %w", err)
}
+
// 4. Create PDS account - let PDS generate DID and all keys
// The PDS will:
+
// 1. Generate a signing keypair (stored in PDS, never exported)
+
// 2. Generate rotation keys (stored in PDS)
+
// 3. Create a DID (did:plc:xxx)
+
// 4. Register DID with PLC directory
+
// 5. Return credentials (DID, handle, tokens)
+
client := &xrpc.Client{
+
Host: p.pdsURL,
+
}
+
+
emailStr := email
+
passwordStr := password
+
+
input := &atproto.ServerCreateAccount_Input{
Handle: handle,
+
Email: &emailStr,
+
Password: &passwordStr,
+
// No Did parameter - let PDS generate it
+
// No RecoveryKey - PDS manages rotation keys
}
+
output, err := atproto.ServerCreateAccount(ctx, client, input)
if err != nil {
+
return nil, fmt.Errorf("PDS account creation failed for community %s: %w", communityName, err)
}
+
// 5. Return account credentials with cleartext password
+
// CRITICAL: The password MUST be encrypted (not hashed) before database storage
+
// We need to recover the plaintext password to call com.atproto.server.createSession
+
// when access/refresh tokens expire (90-day window on refresh tokens)
+
// The repository layer handles encryption using pgp_sym_encrypt()
return &CommunityPDSAccount{
+
DID: output.Did, // The community's DID (PDS-generated)
+
Handle: output.Handle, // e.g., gaming.communities.coves.social
+
Email: email, // community-gaming@communities.coves.social
+
Password: password, // Cleartext - will be encrypted by repository
+
AccessToken: output.AccessJwt, // JWT for making API calls
+
RefreshToken: output.RefreshJwt, // For refreshing sessions
+
PDSURL: p.pdsURL, // PDS hosting this community
+
RotationKeyPEM: "", // Empty - PDS manages keys (V2.1: add Coves rotation key)
+
SigningKeyPEM: "", // Empty - PDS manages keys
}, nil
}
···
return password, nil
}
+
+
+
// FetchPDSDID queries the PDS to get its DID via com.atproto.server.describeServer
+
// This is the proper way to get the PDS DID rather than hardcoding it
+
// Works in both development (did:web:localhost) and production (did:web:pds.example.com)
+
func FetchPDSDID(ctx context.Context, pdsURL string) (string, error) {
+
client := &xrpc.Client{
+
Host: pdsURL,
+
}
+
+
resp, err := comatproto.ServerDescribeServer(ctx, client)
+
if err != nil {
+
return "", fmt.Errorf("failed to describe server at %s: %w", pdsURL, err)
+
}
+
+
if resp.Did == "" {
+
return "", fmt.Errorf("PDS at %s did not return a DID", pdsURL)
+
}
+
+
return resp.Did, nil
+
}
+
+13 -9
internal/core/communities/service.go
···
package communities
import (
-
"Coves/internal/atproto/did"
"bytes"
"context"
"encoding/json"
···
type communityService struct {
repo Repository
-
didGen *did.Generator
provisioner *PDSAccountProvisioner
pdsURL string
instanceDID string
···
}
// NewCommunityService creates a new community service
-
func NewCommunityService(repo Repository, didGen *did.Generator, pdsURL, instanceDID, instanceDomain string, provisioner *PDSAccountProvisioner) Service {
return &communityService{
repo: repo,
-
didGen: didGen,
pdsURL: pdsURL,
instanceDID: instanceDID,
instanceDomain: instanceDomain,
···
return nil, fmt.Errorf("failed to create community profile record: %w", err)
}
-
// Build Community object with PDS credentials
community := &Community{
DID: pdsAccount.DID, // Community's DID (owns the repo!)
Handle: pdsAccount.Handle, // atProto handle (e.g., gaming.communities.coves.social)
···
CreatedByDID: req.CreatedByDID,
HostedByDID: req.HostedByDID,
PDSEmail: pdsAccount.Email,
-
PDSPasswordHash: pdsAccount.PasswordHash,
PDSAccessToken: pdsAccount.AccessToken,
PDSRefreshToken: pdsAccount.RefreshToken,
PDSURL: pdsAccount.PDSURL,
···
UpdatedAt: time.Now(),
RecordURI: recordURI,
RecordCID: recordCID,
}
// CRITICAL: Persist PDS credentials immediately to database
···
return NewValidationError("name", "required")
}
-
if len(req.Name) > 64 {
-
return NewValidationError("name", "must be 64 characters or less")
}
// Name can only contain alphanumeric and hyphens
-
nameRegex := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?$`)
if !nameRegex.MatchString(req.Name) {
return NewValidationError("name", "must contain only alphanumeric characters and hyphens")
}
···
package communities
import (
"bytes"
"context"
"encoding/json"
···
type communityService struct {
repo Repository
provisioner *PDSAccountProvisioner
pdsURL string
instanceDID string
···
}
// NewCommunityService creates a new community service
+
func NewCommunityService(repo Repository, pdsURL, instanceDID, instanceDomain string, provisioner *PDSAccountProvisioner) Service {
return &communityService{
repo: repo,
pdsURL: pdsURL,
instanceDID: instanceDID,
instanceDomain: instanceDomain,
···
return nil, fmt.Errorf("failed to create community profile record: %w", err)
}
+
// Build Community object with PDS credentials AND cryptographic keys
community := &Community{
DID: pdsAccount.DID, // Community's DID (owns the repo!)
Handle: pdsAccount.Handle, // atProto handle (e.g., gaming.communities.coves.social)
···
CreatedByDID: req.CreatedByDID,
HostedByDID: req.HostedByDID,
PDSEmail: pdsAccount.Email,
+
PDSPassword: pdsAccount.Password,
PDSAccessToken: pdsAccount.AccessToken,
PDSRefreshToken: pdsAccount.RefreshToken,
PDSURL: pdsAccount.PDSURL,
···
UpdatedAt: time.Now(),
RecordURI: recordURI,
RecordCID: recordCID,
+
// V2: Cryptographic keys for portability (will be encrypted by repository)
+
RotationKeyPEM: pdsAccount.RotationKeyPEM, // CRITICAL: Enables DID migration
+
SigningKeyPEM: pdsAccount.SigningKeyPEM, // For atproto operations
}
// CRITICAL: Persist PDS credentials immediately to database
···
return NewValidationError("name", "required")
}
+
// DNS label limit: 63 characters per label
+
// Community handle format: {name}.communities.{instanceDomain}
+
// The first label is just req.Name, so it must be <= 63 chars
+
if len(req.Name) > 63 {
+
return NewValidationError("name", "must be 63 characters or less (DNS label limit)")
}
// Name can only contain alphanumeric and hyphens
+
// Must start and end with alphanumeric (not hyphen)
+
nameRegex := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
if !nameRegex.MatchString(req.Name) {
return NewValidationError("name", "must contain only alphanumeric characters and hyphens")
}
+35
internal/db/migrations/007_add_password_encryption.sql
···
···
+
-- +goose Up
+
-- +goose StatementBegin
+
-- V2.0: Add encrypted password column for PDS account recovery
+
-- CRITICAL FIX: Password must be encrypted (not hashed) for session recovery
+
-- When access/refresh tokens expire (90-day window), we need the plaintext password
+
-- to call com.atproto.server.createSession - bcrypt hashing prevents this
+
+
-- Add encrypted password column
+
ALTER TABLE communities ADD COLUMN pds_password_encrypted BYTEA;
+
+
-- Drop legacy plaintext token columns (we now use *_encrypted versions from migration 006)
+
ALTER TABLE communities DROP COLUMN IF EXISTS pds_access_token;
+
ALTER TABLE communities DROP COLUMN IF EXISTS pds_refresh_token;
+
+
-- Drop legacy password_hash column from migration 005 (never used in production)
+
ALTER TABLE communities DROP COLUMN IF EXISTS pds_password_hash;
+
+
-- Add comment
+
COMMENT ON COLUMN communities.pds_password_encrypted IS 'Encrypted community PDS password (pgp_sym_encrypt) - required for session recovery when tokens expire';
+
+
-- +goose StatementEnd
+
+
-- +goose Down
+
-- +goose StatementBegin
+
-- Restore legacy columns (for rollback compatibility)
+
ALTER TABLE communities ADD COLUMN pds_access_token TEXT;
+
ALTER TABLE communities ADD COLUMN pds_refresh_token TEXT;
+
ALTER TABLE communities ADD COLUMN pds_password_hash TEXT;
+
+
-- Drop encrypted password
+
ALTER TABLE communities DROP COLUMN IF EXISTS pds_password_encrypted;
+
+
-- Restore old comment
+
COMMENT ON COLUMN communities.pds_password_hash IS 'bcrypt hash of community PDS password (DEPRECATED - cannot recover plaintext)';
+
-- +goose StatementEnd
+33 -13
internal/db/postgres/community_repo.go
···
INSERT INTO communities (
did, handle, name, display_name, description, description_facets,
avatar_cid, banner_cid, owner_did, created_by_did, hosted_by_did,
-
pds_email, pds_password_hash,
pds_access_token_encrypted, pds_refresh_token_encrypted, pds_url,
visibility, allow_external_discovery, moderation_type, content_warnings,
member_count, subscriber_count, post_count,
···
record_uri, record_cid
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
-
$12, $13,
CASE WHEN $14 != '' THEN pgp_sym_encrypt($14, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)) ELSE NULL END,
CASE WHEN $15 != '' THEN pgp_sym_encrypt($15, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)) ELSE NULL END,
$16,
···
community.OwnerDID,
community.CreatedByDID,
community.HostedByDID,
-
// V2: PDS credentials for community account
nullString(community.PDSEmail),
-
nullString(community.PDSPasswordHash),
-
nullString(community.PDSAccessToken),
-
nullString(community.PDSRefreshToken),
nullString(community.PDSURL),
community.Visibility,
community.AllowExternalDiscovery,
nullString(community.ModerationType),
···
// GetByDID retrieves a community by its DID
// Note: PDS credentials are included (for internal service use only)
// Handlers MUST use json:"-" tags to prevent credential exposure in APIs
func (r *postgresCommunityRepo) GetByDID(ctx context.Context, did string) (*communities.Community, error) {
community := &communities.Community{}
query := `
SELECT id, did, handle, name, display_name, description, description_facets,
avatar_cid, banner_cid, owner_did, created_by_did, hosted_by_did,
-
pds_email, pds_password_hash,
-
COALESCE(pgp_sym_decrypt(pds_access_token_encrypted, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)), '') as pds_access_token,
-
COALESCE(pgp_sym_decrypt(pds_refresh_token_encrypted, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)), '') as pds_refresh_token,
pds_url,
visibility, allow_external_discovery, moderation_type, content_warnings,
member_count, subscriber_count, post_count,
···
var displayName, description, avatarCID, bannerCID, moderationType sql.NullString
var federatedFrom, federatedID, recordURI, recordCID sql.NullString
-
var pdsEmail, pdsPasswordHash, pdsAccessToken, pdsRefreshToken, pdsURL sql.NullString
var descFacets []byte
var contentWarnings []string
···
&displayName, &description, &descFacets,
&avatarCID, &bannerCID,
&community.OwnerDID, &community.CreatedByDID, &community.HostedByDID,
-
// V2: PDS credentials
-
&pdsEmail, &pdsPasswordHash, &pdsAccessToken, &pdsRefreshToken, &pdsURL,
&community.Visibility, &community.AllowExternalDiscovery,
&moderationType, pq.Array(&contentWarnings),
&community.MemberCount, &community.SubscriberCount, &community.PostCount,
···
community.AvatarCID = avatarCID.String
community.BannerCID = bannerCID.String
community.PDSEmail = pdsEmail.String
-
community.PDSPasswordHash = pdsPasswordHash.String
community.PDSAccessToken = pdsAccessToken.String
community.PDSRefreshToken = pdsRefreshToken.String
community.PDSURL = pdsURL.String
community.ModerationType = moderationType.String
community.ContentWarnings = contentWarnings
community.FederatedFrom = federatedFrom.String
···
INSERT INTO communities (
did, handle, name, display_name, description, description_facets,
avatar_cid, banner_cid, owner_did, created_by_did, hosted_by_did,
+
pds_email, pds_password_encrypted,
pds_access_token_encrypted, pds_refresh_token_encrypted, pds_url,
visibility, allow_external_discovery, moderation_type, content_warnings,
member_count, subscriber_count, post_count,
···
record_uri, record_cid
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
+
$12,
+
CASE WHEN $13 != '' THEN pgp_sym_encrypt($13, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)) ELSE NULL END,
CASE WHEN $14 != '' THEN pgp_sym_encrypt($14, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)) ELSE NULL END,
CASE WHEN $15 != '' THEN pgp_sym_encrypt($15, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1)) ELSE NULL END,
$16,
···
community.OwnerDID,
community.CreatedByDID,
community.HostedByDID,
+
// V2.0: PDS credentials for community account (encrypted at rest)
nullString(community.PDSEmail),
+
nullString(community.PDSPassword), // Encrypted by pgp_sym_encrypt
+
nullString(community.PDSAccessToken), // Encrypted by pgp_sym_encrypt
+
nullString(community.PDSRefreshToken), // Encrypted by pgp_sym_encrypt
nullString(community.PDSURL),
+
// V2.0: No key columns - PDS manages all keys
community.Visibility,
community.AllowExternalDiscovery,
nullString(community.ModerationType),
···
// GetByDID retrieves a community by its DID
// Note: PDS credentials are included (for internal service use only)
// Handlers MUST use json:"-" tags to prevent credential exposure in APIs
+
//
+
// V2.0: Key columns not included - PDS manages all keys
func (r *postgresCommunityRepo) GetByDID(ctx context.Context, did string) (*communities.Community, error) {
community := &communities.Community{}
query := `
SELECT id, did, handle, name, display_name, description, description_facets,
avatar_cid, banner_cid, owner_did, created_by_did, hosted_by_did,
+
pds_email,
+
CASE
+
WHEN pds_password_encrypted IS NOT NULL
+
THEN pgp_sym_decrypt(pds_password_encrypted, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1))
+
ELSE NULL
+
END as pds_password,
+
CASE
+
WHEN pds_access_token_encrypted IS NOT NULL
+
THEN pgp_sym_decrypt(pds_access_token_encrypted, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1))
+
ELSE NULL
+
END as pds_access_token,
+
CASE
+
WHEN pds_refresh_token_encrypted IS NOT NULL
+
THEN pgp_sym_decrypt(pds_refresh_token_encrypted, (SELECT encode(key_data, 'hex') FROM encryption_keys WHERE id = 1))
+
ELSE NULL
+
END as pds_refresh_token,
pds_url,
visibility, allow_external_discovery, moderation_type, content_warnings,
member_count, subscriber_count, post_count,
···
var displayName, description, avatarCID, bannerCID, moderationType sql.NullString
var federatedFrom, federatedID, recordURI, recordCID sql.NullString
+
var pdsEmail, pdsPassword, pdsAccessToken, pdsRefreshToken, pdsURL sql.NullString
var descFacets []byte
var contentWarnings []string
···
&displayName, &description, &descFacets,
&avatarCID, &bannerCID,
&community.OwnerDID, &community.CreatedByDID, &community.HostedByDID,
+
// V2.0: PDS credentials (decrypted from pgp_sym_encrypt)
+
&pdsEmail, &pdsPassword, &pdsAccessToken, &pdsRefreshToken, &pdsURL,
&community.Visibility, &community.AllowExternalDiscovery,
&moderationType, pq.Array(&contentWarnings),
&community.MemberCount, &community.SubscriberCount, &community.PostCount,
···
community.AvatarCID = avatarCID.String
community.BannerCID = bannerCID.String
community.PDSEmail = pdsEmail.String
+
community.PDSPassword = pdsPassword.String
community.PDSAccessToken = pdsAccessToken.String
community.PDSRefreshToken = pdsRefreshToken.String
community.PDSURL = pdsURL.String
+
// V2.0: No key fields - PDS manages all keys
+
community.RotationKeyPEM = "" // Empty - PDS-managed
+
community.SigningKeyPEM = "" // Empty - PDS-managed
community.ModerationType = moderationType.String
community.ContentWarnings = contentWarnings
community.FederatedFrom = federatedFrom.String
+4 -19
tests/integration/community_consumer_test.go
···
package integration
import (
-
"Coves/internal/atproto/did"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
···
repo := postgres.NewCommunityRepository(db)
consumer := jetstream.NewCommunityEventConsumer(repo)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("creates community from firehose event", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
// Simulate a Jetstream commit event
event := &jetstream.JetstreamEvent{
···
})
t.Run("updates existing community", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
handle := fmt.Sprintf("!update-test-%s@coves.local", uniqueSuffix)
// Create initial community
···
})
t.Run("deletes community", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
// Create community to delete
community := &communities.Community{
···
repo := postgres.NewCommunityRepository(db)
consumer := jetstream.NewCommunityEventConsumer(repo)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("creates subscription from event", func(t *testing.T) {
// Create a community first
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
···
package integration
import (
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
···
repo := postgres.NewCommunityRepository(db)
consumer := jetstream.NewCommunityEventConsumer(repo)
ctx := context.Background()
t.Run("creates community from firehose event", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
// Simulate a Jetstream commit event
event := &jetstream.JetstreamEvent{
···
})
t.Run("updates existing community", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
handle := fmt.Sprintf("!update-test-%s@coves.local", uniqueSuffix)
// Create initial community
···
})
t.Run("deletes community", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
// Create community to delete
community := &communities.Community{
···
repo := postgres.NewCommunityRepository(db)
consumer := jetstream.NewCommunityEventConsumer(repo)
ctx := context.Background()
t.Run("creates subscription from event", func(t *testing.T) {
// Create a community first
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
+9 -28
tests/integration/community_credentials_test.go
···
package integration
import (
-
"Coves/internal/atproto/did"
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
"context"
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("persists PDS credentials on create", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
···
Visibility: "public",
// V2: PDS credentials
PDSEmail: "community-test@communities.coves.local",
-
PDSPasswordHash: "$2a$10$abcdefghijklmnopqrstuv", // Mock bcrypt hash
PDSAccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token",
PDSRefreshToken: "refresh_token_xyz123",
PDSURL: "http://localhost:2583",
···
if retrieved.PDSEmail != community.PDSEmail {
t.Errorf("Expected PDSEmail %s, got %s", community.PDSEmail, retrieved.PDSEmail)
}
-
if retrieved.PDSPasswordHash != community.PDSPasswordHash {
-
t.Errorf("Expected PDSPasswordHash to be persisted")
}
if retrieved.PDSAccessToken != community.PDSAccessToken {
t.Errorf("Expected PDSAccessToken to be persisted and decrypted correctly")
···
})
t.Run("handles empty credentials gracefully", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
// Community without PDS credentials (e.g., from Jetstream consumer)
community := &communities.Community{
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("credentials are encrypted in database", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
accessToken := "sensitive_access_token_xyz123"
refreshToken := "sensitive_refresh_token_abc456"
···
HostedByDID: "did:web:coves.local",
Visibility: "public",
PDSEmail: "encrypted@communities.coves.local",
-
PDSPasswordHash: "$2a$10$encrypted",
PDSAccessToken: accessToken,
PDSRefreshToken: refreshToken,
PDSURL: "http://localhost:2583",
···
})
t.Run("encryption handles special characters", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
// Token with special characters
specialToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2NvdmVzLnNvY2lhbCIsInN1YiI6ImRpZDpwbGM6YWJjMTIzIiwiaWF0IjoxNzA5MjQwMDAwfQ.special/chars+here=="
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("V2 communities are self-owned", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
···
package integration
import (
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
"context"
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("persists PDS credentials on create", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
···
Visibility: "public",
// V2: PDS credentials
PDSEmail: "community-test@communities.coves.local",
+
PDSPassword: "cleartext-password-encrypted-by-repo", // V2: Cleartext (encrypted by repository)
PDSAccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test.token",
PDSRefreshToken: "refresh_token_xyz123",
PDSURL: "http://localhost:2583",
···
if retrieved.PDSEmail != community.PDSEmail {
t.Errorf("Expected PDSEmail %s, got %s", community.PDSEmail, retrieved.PDSEmail)
}
+
if retrieved.PDSPassword != community.PDSPassword {
+
t.Errorf("Expected PDSPassword to be persisted and encrypted/decrypted")
}
if retrieved.PDSAccessToken != community.PDSAccessToken {
t.Errorf("Expected PDSAccessToken to be persisted and decrypted correctly")
···
})
t.Run("handles empty credentials gracefully", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
// Community without PDS credentials (e.g., from Jetstream consumer)
community := &communities.Community{
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("credentials are encrypted in database", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
accessToken := "sensitive_access_token_xyz123"
refreshToken := "sensitive_refresh_token_abc456"
···
HostedByDID: "did:web:coves.local",
Visibility: "public",
PDSEmail: "encrypted@communities.coves.local",
+
PDSPassword: "cleartext-password-for-encryption", // V2: Cleartext (encrypted by repository)
PDSAccessToken: accessToken,
PDSRefreshToken: refreshToken,
PDSURL: "http://localhost:2583",
···
})
t.Run("encryption handles special characters", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
// Token with special characters
specialToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2NvdmVzLnNvY2lhbCIsInN1YiI6ImRpZDpwbGM6YWJjMTIzIiwiaWF0IjoxNzA5MjQwMDAwfQ.special/chars+here=="
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("V2 communities are self-owned", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
+16 -10
tests/integration/community_e2e_test.go
···
import (
"Coves/internal/api/routes"
-
"Coves/internal/atproto/did"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
// Setup dependencies
communityRepo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
// Get instance credentials
instanceHandle := os.Getenv("PDS_INSTANCE_HANDLE")
···
t.Logf("✅ Authenticated - Instance DID: %s", instanceDID)
-
// V2: Extract instance domain for community provisioning
var instanceDomain string
if strings.HasPrefix(instanceDID, "did:web:") {
instanceDomain = strings.TrimPrefix(instanceDID, "did:web:")
···
instanceDomain = "coves.social"
}
-
// V2: Create user service for PDS account provisioning
userRepo := postgres.NewUserRepository(db)
-
identityResolver := &communityTestIdentityResolver{} // Simple mock for test
-
userService := users.NewUserService(userRepo, identityResolver, pdsURL)
-
// V2: Initialize PDS account provisioner
-
provisioner := communities.NewPDSAccountProvisioner(userService, instanceDomain, pdsURL)
-
// Create service and consumer
-
communityService := communities.NewCommunityService(communityRepo, didGen, pdsURL, instanceDID, instanceDomain, provisioner)
if svc, ok := communityService.(interface{ SetPDSAccessToken(string) }); ok {
svc.SetPDSAccessToken(accessToken)
}
···
import (
"Coves/internal/api/routes"
"Coves/internal/atproto/identity"
"Coves/internal/atproto/jetstream"
"Coves/internal/core/communities"
···
// Setup dependencies
communityRepo := postgres.NewCommunityRepository(db)
// Get instance credentials
instanceHandle := os.Getenv("PDS_INSTANCE_HANDLE")
···
t.Logf("✅ Authenticated - Instance DID: %s", instanceDID)
+
// V2.0: Extract instance domain for community provisioning
var instanceDomain string
if strings.HasPrefix(instanceDID, "did:web:") {
instanceDomain = strings.TrimPrefix(instanceDID, "did:web:")
···
instanceDomain = "coves.social"
}
+
// V2.0: Create user service with REAL identity resolution using local PLC
+
plcURL := os.Getenv("PLC_DIRECTORY_URL")
+
if plcURL == "" {
+
plcURL = "http://localhost:3002" // Local PLC directory
+
}
userRepo := postgres.NewUserRepository(db)
+
identityConfig := identity.DefaultConfig()
+
identityConfig.PLCURL = plcURL // Use local PLC for identity resolution
+
identityResolver := identity.NewResolver(db, identityConfig)
+
_ = users.NewUserService(userRepo, identityResolver, pdsURL) // Keep for potential future use
+
t.Logf("✅ Identity resolver configured with local PLC: %s", plcURL)
+
// V2.0: Initialize PDS account provisioner (simplified - no DID generator needed!)
+
// PDS handles all DID generation and registration automatically
+
provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL)
+
// Create service (no longer needs didGen directly - provisioner owns it)
+
communityService := communities.NewCommunityService(communityRepo, pdsURL, instanceDID, instanceDomain, provisioner)
if svc, ok := communityService.(interface{ SetPDSAccessToken(string) }); ok {
svc.SetPDSAccessToken(accessToken)
}
+873
tests/integration/community_provisioning_test.go
···
···
+
package integration
+
+
import (
+
"Coves/internal/core/communities"
+
"Coves/internal/db/postgres"
+
"context"
+
"fmt"
+
"strings"
+
"testing"
+
"time"
+
)
+
+
// TestCommunityRepository_PasswordEncryption verifies P0 fix:
+
// Password must be encrypted (not hashed) so we can recover it for session renewal
+
func TestCommunityRepository_PasswordEncryption(t *testing.T) {
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
ctx := context.Background()
+
+
t.Run("encrypts and decrypts password correctly", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
testPassword := "test-password-12345678901234567890"
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("test-encryption-%s.communities.test.local", uniqueSuffix),
+
Name: "test-encryption",
+
DisplayName: "Test Encryption",
+
Description: "Testing password encryption",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: "test@test.local",
+
PDSPassword: testPassword, // Cleartext password
+
PDSAccessToken: "test-access-token",
+
PDSRefreshToken: "test-refresh-token",
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
// Create community with password
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
// CRITICAL: Query database directly to verify password is ENCRYPTED at rest
+
var encryptedPassword []byte
+
query := `
+
SELECT pds_password_encrypted
+
FROM communities
+
WHERE did = $1
+
`
+
if err := db.QueryRowContext(ctx, query, created.DID).Scan(&encryptedPassword); err != nil {
+
t.Fatalf("Failed to query encrypted password: %v", err)
+
}
+
+
// Verify password is NOT stored as plaintext
+
if string(encryptedPassword) == testPassword {
+
t.Error("CRITICAL: Password is stored as plaintext in database! Must be encrypted.")
+
}
+
+
// Verify password is NOT stored as bcrypt hash (would start with $2a$, $2b$, or $2y$)
+
if strings.HasPrefix(string(encryptedPassword), "$2") {
+
t.Error("Password appears to be bcrypt hashed instead of pgcrypto encrypted!")
+
}
+
+
// Verify encrypted data is not empty
+
if len(encryptedPassword) == 0 {
+
t.Error("Expected encrypted password to have data")
+
}
+
+
t.Logf("✅ Password is encrypted in database (not plaintext or bcrypt)")
+
+
// Retrieve community - password should be decrypted by repository
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
// Verify password roundtrip (encrypted → decrypted)
+
if retrieved.PDSPassword != testPassword {
+
t.Errorf("Password roundtrip failed: expected %q, got %q", testPassword, retrieved.PDSPassword)
+
}
+
+
t.Logf("✅ Password decrypted correctly on retrieval: %d chars", len(retrieved.PDSPassword))
+
})
+
+
t.Run("handles empty password gracefully", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()+1)
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("test-empty-pass-%s.communities.test.local", uniqueSuffix),
+
Name: "test-empty-pass",
+
DisplayName: "Test Empty Password",
+
Description: "Testing empty password handling",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: "test2@test.local",
+
PDSPassword: "", // Empty password
+
PDSAccessToken: "test-access-token",
+
PDSRefreshToken: "test-refresh-token",
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community with empty password: %v", err)
+
}
+
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
if retrieved.PDSPassword != "" {
+
t.Errorf("Expected empty password, got: %q", retrieved.PDSPassword)
+
}
+
})
+
}
+
+
// TestCommunityService_NameValidation verifies P1 fix:
+
// Community names must respect DNS label limits (63 chars max)
+
func TestCommunityService_NameValidation(t *testing.T) {
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
provisioner := communities.NewPDSAccountProvisioner("test.local", "http://localhost:3001")
+
service := communities.NewCommunityService(
+
repo,
+
"http://localhost:3001", // pdsURL
+
"did:web:test.local", // instanceDID
+
"test.local", // instanceDomain
+
provisioner,
+
)
+
ctx := context.Background()
+
+
t.Run("rejects empty name", func(t *testing.T) {
+
req := communities.CreateCommunityRequest{
+
Name: "", // Empty!
+
DisplayName: "Empty Name Test",
+
Description: "This should fail",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
AllowExternalDiscovery: true,
+
}
+
+
_, err := service.CreateCommunity(ctx, req)
+
if err == nil {
+
t.Error("Expected error for empty name, got nil")
+
}
+
+
if !strings.Contains(err.Error(), "name") {
+
t.Errorf("Expected 'name' error, got: %v", err)
+
}
+
})
+
+
t.Run("rejects 64-char name (exceeds DNS limit)", func(t *testing.T) {
+
// DNS label limit is 63 characters
+
longName := strings.Repeat("a", 64)
+
+
req := communities.CreateCommunityRequest{
+
Name: longName,
+
DisplayName: "Long Name Test",
+
Description: "This should fail - name too long for DNS",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
AllowExternalDiscovery: true,
+
}
+
+
_, err := service.CreateCommunity(ctx, req)
+
if err == nil {
+
t.Error("Expected error for 64-char name, got nil")
+
}
+
+
if !strings.Contains(err.Error(), "63") || !strings.Contains(err.Error(), "name") {
+
t.Errorf("Expected '63 characters' name error, got: %v", err)
+
}
+
+
t.Logf("✅ Correctly rejected 64-char name: %v", err)
+
})
+
+
t.Run("accepts 63-char name (exactly at DNS limit)", func(t *testing.T) {
+
// This should be accepted - exactly 63 chars
+
maxName := strings.Repeat("a", 63)
+
+
req := communities.CreateCommunityRequest{
+
Name: maxName,
+
DisplayName: "Max Name Test",
+
Description: "This should succeed - exactly at DNS limit",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
AllowExternalDiscovery: true,
+
}
+
+
// This will fail at PDS provisioning (no mock PDS), but should pass validation
+
_, err := service.CreateCommunity(ctx, req)
+
+
// We expect PDS provisioning to fail, but NOT validation
+
if err != nil && strings.Contains(err.Error(), "63 characters") {
+
t.Errorf("Name validation should pass for 63-char name, got: %v", err)
+
}
+
+
t.Logf("✅ 63-char name passed validation (may fail at PDS provisioning)")
+
})
+
+
t.Run("rejects special characters in name", func(t *testing.T) {
+
testCases := []struct {
+
name string
+
errorDesc string
+
}{
+
{"test!community", "exclamation mark"},
+
{"test@space", "at symbol"},
+
{"test community", "space"},
+
{"test.community", "period/dot"},
+
{"test_community", "underscore"},
+
{"test#tag", "hash"},
+
{"-testcommunity", "leading hyphen"},
+
{"testcommunity-", "trailing hyphen"},
+
}
+
+
for _, tc := range testCases {
+
t.Run(tc.errorDesc, func(t *testing.T) {
+
req := communities.CreateCommunityRequest{
+
Name: tc.name,
+
DisplayName: "Special Char Test",
+
Description: "Testing special character rejection",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
AllowExternalDiscovery: true,
+
}
+
+
_, err := service.CreateCommunity(ctx, req)
+
if err == nil {
+
t.Errorf("Expected error for name with %s: %q", tc.errorDesc, tc.name)
+
}
+
+
if !strings.Contains(err.Error(), "name") {
+
t.Errorf("Expected 'name' error for %q, got: %v", tc.name, err)
+
}
+
})
+
}
+
})
+
+
t.Run("accepts valid names", func(t *testing.T) {
+
validNames := []string{
+
"gaming",
+
"tech-news",
+
"Web3Dev",
+
"community123",
+
"a", // Single character is valid
+
"ab", // Two characters is valid
+
}
+
+
for _, name := range validNames {
+
t.Run(name, func(t *testing.T) {
+
req := communities.CreateCommunityRequest{
+
Name: name,
+
DisplayName: "Valid Name Test",
+
Description: "Testing valid name acceptance",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
AllowExternalDiscovery: true,
+
}
+
+
// This will fail at PDS provisioning (no mock PDS), but should pass validation
+
_, err := service.CreateCommunity(ctx, req)
+
+
// We expect PDS provisioning to fail, but NOT name validation
+
if err != nil && strings.Contains(strings.ToLower(err.Error()), "name") && strings.Contains(err.Error(), "alphanumeric") {
+
t.Errorf("Name validation should pass for %q, got: %v", name, err)
+
}
+
})
+
}
+
})
+
}
+
+
// TestPasswordSecurity verifies password generation security properties
+
// Critical for P0: Passwords must be unpredictable and have sufficient entropy
+
func TestPasswordSecurity(t *testing.T) {
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
ctx := context.Background()
+
+
t.Run("generates unique passwords", func(t *testing.T) {
+
// Create 100 communities and verify each gets a unique password
+
// We test this by storing passwords in the DB (encrypted) and verifying uniqueness
+
passwords := make(map[string]bool)
+
const numCommunities = 100
+
+
// Use a unique base timestamp for this test run to avoid collisions
+
baseTimestamp := time.Now().UnixNano()
+
+
for i := 0; i < numCommunities; i++ {
+
uniqueSuffix := fmt.Sprintf("%d-%d", baseTimestamp, i)
+
+
// Generate a unique password for this test (simulating what provisioner does)
+
// In production, provisioner generates the password, but we can't intercept it
+
// So we generate our own unique passwords and verify they're stored uniquely
+
testPassword := fmt.Sprintf("unique-password-%s", uniqueSuffix)
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("pwd-unique-%s.communities.test.local", uniqueSuffix),
+
Name: fmt.Sprintf("pwd-unique-%s", uniqueSuffix),
+
DisplayName: fmt.Sprintf("Password Unique Test %d", i),
+
Description: "Testing password uniqueness",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("pwd-unique-%s@test.local", uniqueSuffix),
+
PDSPassword: testPassword,
+
PDSAccessToken: fmt.Sprintf("access-token-%s", uniqueSuffix),
+
PDSRefreshToken: fmt.Sprintf("refresh-token-%s", uniqueSuffix),
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community %d: %v", i, err)
+
}
+
+
// Retrieve and verify password
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community %d: %v", i, err)
+
}
+
+
// Verify password was decrypted correctly
+
if retrieved.PDSPassword != testPassword {
+
t.Errorf("Community %d: password mismatch after encryption/decryption", i)
+
}
+
+
// Track password uniqueness
+
if passwords[retrieved.PDSPassword] {
+
t.Errorf("Community %d: duplicate password detected: %s", i, retrieved.PDSPassword)
+
}
+
passwords[retrieved.PDSPassword] = true
+
}
+
+
// Verify all passwords are unique
+
if len(passwords) != numCommunities {
+
t.Errorf("Expected %d unique passwords, got %d", numCommunities, len(passwords))
+
}
+
+
t.Logf("✅ All %d communities have unique passwords", numCommunities)
+
})
+
+
t.Run("password has sufficient length", func(t *testing.T) {
+
// The implementation uses 32-character passwords
+
// We can verify this indirectly through the database
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
+
// Create a community with a known password
+
testPassword := "test-password-with-32-chars--"
+
if len(testPassword) < 32 {
+
testPassword = testPassword + strings.Repeat("x", 32-len(testPassword))
+
}
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("test-pwd-len-%s.communities.test.local", uniqueSuffix),
+
Name: "test-pwd-len",
+
DisplayName: "Test Password Length",
+
Description: "Testing password length requirements",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("test-pwd-len-%s@test.local", uniqueSuffix),
+
PDSPassword: testPassword,
+
PDSAccessToken: "test-access-token",
+
PDSRefreshToken: "test-refresh-token",
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
// Verify password is stored correctly and has sufficient length
+
if len(retrieved.PDSPassword) < 32 {
+
t.Errorf("Password too short: expected >= 32 characters, got %d", len(retrieved.PDSPassword))
+
}
+
+
t.Logf("✅ Password length verified: %d characters", len(retrieved.PDSPassword))
+
})
+
}
+
+
// TestConcurrentProvisioning verifies thread-safety during community creation
+
// Critical: Prevents race conditions that could create duplicate communities
+
func TestConcurrentProvisioning(t *testing.T) {
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
ctx := context.Background()
+
+
t.Run("prevents duplicate community creation", func(t *testing.T) {
+
// Try to create the same community concurrently
+
const numGoroutines = 10
+
sameName := fmt.Sprintf("concurrent-test-%d", time.Now().UnixNano())
+
+
// Channel to collect results
+
type result struct {
+
community *communities.Community
+
err error
+
}
+
results := make(chan result, numGoroutines)
+
+
// Launch concurrent creation attempts
+
for i := 0; i < numGoroutines; i++ {
+
go func(idx int) {
+
uniqueSuffix := fmt.Sprintf("%d-%d", time.Now().UnixNano(), idx)
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("%s.communities.test.local", sameName),
+
Name: sameName,
+
DisplayName: "Concurrent Test",
+
Description: "Testing concurrent creation",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("%s-%s@test.local", sameName, uniqueSuffix),
+
PDSPassword: "test-password-concurrent",
+
PDSAccessToken: fmt.Sprintf("access-token-%d", idx),
+
PDSRefreshToken: fmt.Sprintf("refresh-token-%d", idx),
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
results <- result{community: created, err: err}
+
}(i)
+
}
+
+
// Collect results
+
successCount := 0
+
duplicateErrorCount := 0
+
+
for i := 0; i < numGoroutines; i++ {
+
res := <-results
+
if res.err == nil {
+
successCount++
+
} else if strings.Contains(res.err.Error(), "duplicate") ||
+
strings.Contains(res.err.Error(), "unique") ||
+
strings.Contains(res.err.Error(), "already exists") {
+
duplicateErrorCount++
+
} else {
+
t.Logf("Unexpected error: %v", res.err)
+
}
+
}
+
+
// We expect exactly one success and the rest to fail with duplicate errors
+
// OR all to succeed with unique DIDs (depending on implementation)
+
t.Logf("Results: %d successful, %d duplicate errors", successCount, duplicateErrorCount)
+
+
// At minimum, we should have some creations succeed
+
if successCount == 0 {
+
t.Error("Expected at least one successful community creation")
+
}
+
+
// If we have duplicate errors, that's good - it means uniqueness is enforced
+
if duplicateErrorCount > 0 {
+
t.Logf("✅ Database correctly prevents duplicate handles: %d duplicate errors", duplicateErrorCount)
+
}
+
})
+
+
t.Run("handles concurrent reads safely", func(t *testing.T) {
+
// Create a test community
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("read-test-%s.communities.test.local", uniqueSuffix),
+
Name: "read-test",
+
DisplayName: "Read Test",
+
Description: "Testing concurrent reads",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("read-test-%s@test.local", uniqueSuffix),
+
PDSPassword: "test-password-reads",
+
PDSAccessToken: "access-token",
+
PDSRefreshToken: "refresh-token",
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create test community: %v", err)
+
}
+
+
// Now read it concurrently
+
const numReaders = 20
+
results := make(chan error, numReaders)
+
+
for i := 0; i < numReaders; i++ {
+
go func() {
+
_, err := repo.GetByDID(ctx, created.DID)
+
results <- err
+
}()
+
}
+
+
// All reads should succeed
+
failCount := 0
+
for i := 0; i < numReaders; i++ {
+
if err := <-results; err != nil {
+
failCount++
+
t.Logf("Read %d failed: %v", i, err)
+
}
+
}
+
+
if failCount > 0 {
+
t.Errorf("Expected all concurrent reads to succeed, but %d failed", failCount)
+
} else {
+
t.Logf("✅ All %d concurrent reads succeeded", numReaders)
+
}
+
})
+
}
+
+
// TestPDSNetworkFailures verifies graceful handling of PDS network issues
+
// Critical: Ensures service doesn't crash or leak resources on PDS failures
+
func TestPDSNetworkFailures(t *testing.T) {
+
ctx := context.Background()
+
+
t.Run("handles invalid PDS URL", func(t *testing.T) {
+
// Invalid URL should fail gracefully
+
invalidURLs := []string{
+
"not-a-url",
+
"ftp://invalid-protocol.com",
+
"http://",
+
"://missing-scheme",
+
"",
+
}
+
+
for _, invalidURL := range invalidURLs {
+
provisioner := communities.NewPDSAccountProvisioner("test.local", invalidURL)
+
_, err := provisioner.ProvisionCommunityAccount(ctx, "testcommunity")
+
+
if err == nil {
+
t.Errorf("Expected error for invalid PDS URL %q, got nil", invalidURL)
+
}
+
+
// Should get a clear error about PDS failure
+
if !strings.Contains(err.Error(), "PDS") && !strings.Contains(err.Error(), "failed") {
+
t.Logf("Error message could be clearer for URL %q: %v", invalidURL, err)
+
}
+
+
t.Logf("✅ Invalid URL %q correctly rejected: %v", invalidURL, err)
+
}
+
})
+
+
t.Run("handles unreachable PDS server", func(t *testing.T) {
+
// Use a port that's guaranteed to be unreachable
+
unreachablePDS := "http://localhost:9999"
+
provisioner := communities.NewPDSAccountProvisioner("test.local", unreachablePDS)
+
+
_, err := provisioner.ProvisionCommunityAccount(ctx, "testcommunity")
+
+
if err == nil {
+
t.Error("Expected error for unreachable PDS, got nil")
+
}
+
+
// Should get connection error
+
if !strings.Contains(err.Error(), "PDS account creation failed") {
+
t.Logf("Error for unreachable PDS: %v", err)
+
}
+
+
t.Logf("✅ Unreachable PDS handled gracefully: %v", err)
+
})
+
+
t.Run("handles timeout scenarios", func(t *testing.T) {
+
// Create a context with a very short timeout
+
timeoutCtx, cancel := context.WithTimeout(ctx, 1)
+
defer cancel()
+
+
provisioner := communities.NewPDSAccountProvisioner("test.local", "http://localhost:3001")
+
_, err := provisioner.ProvisionCommunityAccount(timeoutCtx, "testcommunity")
+
+
// Should either timeout or fail to connect (since PDS isn't running)
+
if err == nil {
+
t.Error("Expected timeout or connection error, got nil")
+
}
+
+
t.Logf("✅ Timeout handled: %v", err)
+
})
+
+
t.Run("FetchPDSDID handles invalid URLs", func(t *testing.T) {
+
invalidURLs := []string{
+
"not-a-url",
+
"http://",
+
"",
+
}
+
+
for _, invalidURL := range invalidURLs {
+
_, err := communities.FetchPDSDID(ctx, invalidURL)
+
+
if err == nil {
+
t.Errorf("FetchPDSDID should fail for invalid URL %q", invalidURL)
+
}
+
+
t.Logf("✅ FetchPDSDID rejected invalid URL %q: %v", invalidURL, err)
+
}
+
})
+
+
t.Run("FetchPDSDID handles unreachable server", func(t *testing.T) {
+
unreachablePDS := "http://localhost:9998"
+
_, err := communities.FetchPDSDID(ctx, unreachablePDS)
+
+
if err == nil {
+
t.Error("Expected error for unreachable PDS")
+
}
+
+
if !strings.Contains(err.Error(), "failed to describe server") {
+
t.Errorf("Expected 'failed to describe server' error, got: %v", err)
+
}
+
+
t.Logf("✅ FetchPDSDID handles unreachable server: %v", err)
+
})
+
+
t.Run("FetchPDSDID handles timeout", func(t *testing.T) {
+
timeoutCtx, cancel := context.WithTimeout(ctx, 1)
+
defer cancel()
+
+
_, err := communities.FetchPDSDID(timeoutCtx, "http://localhost:3001")
+
+
// Should timeout or fail to connect
+
if err == nil {
+
t.Error("Expected timeout or connection error")
+
}
+
+
t.Logf("✅ FetchPDSDID timeout handled: %v", err)
+
})
+
}
+
+
// TestTokenValidation verifies that PDS-returned tokens meet requirements
+
// Critical for P0: Tokens must be valid JWTs that can be used for authentication
+
func TestTokenValidation(t *testing.T) {
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
repo := postgres.NewCommunityRepository(db)
+
ctx := context.Background()
+
+
t.Run("validates access token storage", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
+
// Create a community with realistic-looking tokens
+
// Real atProto JWTs are typically 200+ characters
+
accessToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOnRlc3QiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
+
refreshToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOnRlc3QiLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTUxNjIzOTAyMn0.different_signature_here"
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("token-test-%s.communities.test.local", uniqueSuffix),
+
Name: "token-test",
+
DisplayName: "Token Test",
+
Description: "Testing token storage",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("token-test-%s@test.local", uniqueSuffix),
+
PDSPassword: "test-password-tokens",
+
PDSAccessToken: accessToken,
+
PDSRefreshToken: refreshToken,
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
// Retrieve and verify tokens
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
// Verify access token stored correctly
+
if retrieved.PDSAccessToken != accessToken {
+
t.Errorf("Access token mismatch: expected %q, got %q", accessToken, retrieved.PDSAccessToken)
+
}
+
+
// Verify refresh token stored correctly
+
if retrieved.PDSRefreshToken != refreshToken {
+
t.Errorf("Refresh token mismatch: expected %q, got %q", refreshToken, retrieved.PDSRefreshToken)
+
}
+
+
// Verify tokens are not empty
+
if retrieved.PDSAccessToken == "" {
+
t.Error("Access token should not be empty")
+
}
+
if retrieved.PDSRefreshToken == "" {
+
t.Error("Refresh token should not be empty")
+
}
+
+
// Verify tokens have reasonable length (JWTs are typically 100+ chars)
+
if len(retrieved.PDSAccessToken) < 50 {
+
t.Errorf("Access token seems too short: %d characters", len(retrieved.PDSAccessToken))
+
}
+
if len(retrieved.PDSRefreshToken) < 50 {
+
t.Errorf("Refresh token seems too short: %d characters", len(retrieved.PDSRefreshToken))
+
}
+
+
t.Logf("✅ Tokens stored and retrieved correctly:")
+
t.Logf(" Access token: %d chars", len(retrieved.PDSAccessToken))
+
t.Logf(" Refresh token: %d chars", len(retrieved.PDSRefreshToken))
+
})
+
+
t.Run("handles empty tokens gracefully", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()+1)
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("empty-token-%s.communities.test.local", uniqueSuffix),
+
Name: "empty-token",
+
DisplayName: "Empty Token Test",
+
Description: "Testing empty token handling",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("empty-token-%s@test.local", uniqueSuffix),
+
PDSPassword: "test-password",
+
PDSAccessToken: "", // Empty
+
PDSRefreshToken: "", // Empty
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community with empty tokens: %v", err)
+
}
+
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
// Empty tokens should be preserved
+
if retrieved.PDSAccessToken != "" {
+
t.Errorf("Expected empty access token, got: %q", retrieved.PDSAccessToken)
+
}
+
if retrieved.PDSRefreshToken != "" {
+
t.Errorf("Expected empty refresh token, got: %q", retrieved.PDSRefreshToken)
+
}
+
+
t.Logf("✅ Empty tokens handled correctly (NULL/empty string)")
+
})
+
+
t.Run("validates token encryption in database", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()+2)
+
+
// Use distinct tokens so we can verify they're encrypted separately
+
accessToken := "access-token-should-be-encrypted-" + uniqueSuffix
+
refreshToken := "refresh-token-should-be-encrypted-" + uniqueSuffix
+
+
community := &communities.Community{
+
DID: generateTestDID(uniqueSuffix),
+
Handle: fmt.Sprintf("encrypted-token-%s.communities.test.local", uniqueSuffix),
+
Name: "encrypted-token",
+
DisplayName: "Encrypted Token Test",
+
Description: "Testing token encryption",
+
OwnerDID: "did:web:test.local",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:test.local",
+
PDSEmail: fmt.Sprintf("encrypted-token-%s@test.local", uniqueSuffix),
+
PDSPassword: "test-password",
+
PDSAccessToken: accessToken,
+
PDSRefreshToken: refreshToken,
+
PDSURL: "http://localhost:3001",
+
Visibility: "public",
+
AllowExternalDiscovery: true,
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
created, err := repo.Create(ctx, community)
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
retrieved, err := repo.GetByDID(ctx, created.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
// Verify tokens are decrypted correctly
+
if retrieved.PDSAccessToken != accessToken {
+
t.Errorf("Access token decryption failed: expected %q, got %q", accessToken, retrieved.PDSAccessToken)
+
}
+
if retrieved.PDSRefreshToken != refreshToken {
+
t.Errorf("Refresh token decryption failed: expected %q, got %q", refreshToken, retrieved.PDSRefreshToken)
+
}
+
+
t.Logf("✅ Tokens encrypted/decrypted correctly")
+
})
+
}
+17 -55
tests/integration/community_repo_test.go
···
package integration
import (
-
"Coves/internal/atproto/did"
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
"context"
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("creates community successfully", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
-
// Generate unique handle using timestamp to avoid collisions
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!test-gaming-%s@coves.local", uniqueSuffix),
···
})
t.Run("returns error for duplicate DID", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!duplicate-test-%s@coves.local", uniqueSuffix),
···
}
// Try to create again with same DID
-
if _, err = repo.Create(ctx, community); err != communities.ErrCommunityAlreadyExists {
t.Errorf("Expected ErrCommunityAlreadyExists, got: %v", err)
}
})
···
handle := fmt.Sprintf("!unique-handle-%s@coves.local", uniqueSuffix)
// First community
-
did1, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate first community DID: %v", err)
-
}
community1 := &communities.Community{
DID: did1,
Handle: handle,
···
}
// Second community with different DID but same handle
-
did2, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate second community DID: %v", err)
-
}
community2 := &communities.Community{
DID: did2,
Handle: handle, // Same handle!
···
UpdatedAt: time.Now(),
}
-
if _, err = repo.Create(ctx, community2); err != communities.ErrHandleTaken {
t.Errorf("Expected ErrHandleTaken, got: %v", err)
}
})
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("retrieves existing community", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!getbyid-test-%s@coves.local", uniqueSuffix),
···
})
t.Run("returns error for non-existent community", func(t *testing.T) {
-
fakeDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate fake DID: %v", err)
-
}
if _, err := repo.GetByDID(ctx, fakeDID); err != communities.ErrCommunityNotFound {
t.Errorf("Expected ErrCommunityNotFound, got: %v", err)
}
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("retrieves community by handle", func(t *testing.T) {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
handle := fmt.Sprintf("!handle-lookup-%s@coves.local", uniqueSuffix)
community := &communities.Community{
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
// Create a community for subscription tests
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!subscription-test-%s@coves.local", uniqueSuffix),
···
}
// Try to subscribe again
-
_, err = repo.Subscribe(ctx, sub)
if err != communities.ErrSubscriptionAlreadyExists {
t.Errorf("Expected ErrSubscriptionAlreadyExists, got: %v", err)
}
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("lists communities with pagination", func(t *testing.T) {
// Create multiple communities
baseSuffix := time.Now().UnixNano()
for i := 0; i < 5; i++ {
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!list-test-%d-%d@coves.local", baseSuffix, i),
···
t.Run("filters by visibility", func(t *testing.T) {
// Create an unlisted community
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!unlisted-test-%s@coves.local", uniqueSuffix),
···
}()
repo := postgres.NewCommunityRepository(db)
-
didGen := did.NewGenerator(true, "https://plc.directory")
ctx := context.Background()
t.Run("searches communities by name", func(t *testing.T) {
// Create a community with searchable name
-
communityDID, err := didGen.GenerateCommunityDID()
-
if err != nil {
-
t.Fatalf("Failed to generate community DID: %v", err)
-
}
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!golang-search-%s@coves.local", uniqueSuffix),
···
package integration
import (
"Coves/internal/core/communities"
"Coves/internal/db/postgres"
"context"
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("creates community successfully", func(t *testing.T) {
+
// Generate unique handle and DID using timestamp to avoid collisions
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!test-gaming-%s@coves.local", uniqueSuffix),
···
})
t.Run("returns error for duplicate DID", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!duplicate-test-%s@coves.local", uniqueSuffix),
···
}
// Try to create again with same DID
+
if _, err := repo.Create(ctx, community); err != communities.ErrCommunityAlreadyExists {
t.Errorf("Expected ErrCommunityAlreadyExists, got: %v", err)
}
})
···
handle := fmt.Sprintf("!unique-handle-%s@coves.local", uniqueSuffix)
// First community
+
did1 := generateTestDID(uniqueSuffix + "1")
community1 := &communities.Community{
DID: did1,
Handle: handle,
···
}
// Second community with different DID but same handle
+
did2 := generateTestDID(uniqueSuffix + "2")
community2 := &communities.Community{
DID: did2,
Handle: handle, // Same handle!
···
UpdatedAt: time.Now(),
}
+
if _, err := repo.Create(ctx, community2); err != communities.ErrHandleTaken {
t.Errorf("Expected ErrHandleTaken, got: %v", err)
}
})
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("retrieves existing community", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!getbyid-test-%s@coves.local", uniqueSuffix),
···
})
t.Run("returns error for non-existent community", func(t *testing.T) {
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
fakeDID := generateTestDID(uniqueSuffix)
if _, err := repo.GetByDID(ctx, fakeDID); err != communities.ErrCommunityNotFound {
t.Errorf("Expected ErrCommunityNotFound, got: %v", err)
}
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("retrieves community by handle", func(t *testing.T) {
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
handle := fmt.Sprintf("!handle-lookup-%s@coves.local", uniqueSuffix)
community := &communities.Community{
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
// Create a community for subscription tests
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!subscription-test-%s@coves.local", uniqueSuffix),
···
}
// Try to subscribe again
+
_, err := repo.Subscribe(ctx, sub)
if err != communities.ErrSubscriptionAlreadyExists {
t.Errorf("Expected ErrSubscriptionAlreadyExists, got: %v", err)
}
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("lists communities with pagination", func(t *testing.T) {
// Create multiple communities
baseSuffix := time.Now().UnixNano()
for i := 0; i < 5; i++ {
+
uniqueSuffix := fmt.Sprintf("%d%d", baseSuffix, i)
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!list-test-%d-%d@coves.local", baseSuffix, i),
···
t.Run("filters by visibility", func(t *testing.T) {
// Create an unlisted community
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!unlisted-test-%s@coves.local", uniqueSuffix),
···
}()
repo := postgres.NewCommunityRepository(db)
ctx := context.Background()
t.Run("searches communities by name", func(t *testing.T) {
// Create a community with searchable name
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
community := &communities.Community{
DID: communityDID,
Handle: fmt.Sprintf("!golang-search-%s@coves.local", uniqueSuffix),
+603
tests/integration/community_service_integration_test.go
···
···
+
package integration
+
+
import (
+
"Coves/internal/core/communities"
+
"Coves/internal/db/postgres"
+
"bytes"
+
"context"
+
"encoding/json"
+
"fmt"
+
"io"
+
"net/http"
+
"strings"
+
"testing"
+
"time"
+
)
+
+
// TestCommunityService_CreateWithRealPDS tests the complete service layer flow
+
// using a REAL local PDS. This verifies:
+
// - Password generation happens in provisioner (not hardcoded test passwords)
+
// - PDS account creation works (real com.atproto.server.createAccount)
+
// - Write-forward to community's own repository succeeds
+
// - Credentials flow correctly: PDS → service → repository
+
// - Complete atProto write-forward architecture
+
//
+
// This test fills the gap between:
+
// - Unit tests (direct DB writes, bypass PDS)
+
// - E2E tests (full HTTP + Jetstream flow)
+
func TestCommunityService_CreateWithRealPDS(t *testing.T) {
+
if testing.Short() {
+
t.Skip("Skipping integration test in short mode - requires PDS")
+
}
+
+
// Check if PDS is running
+
pdsURL := "http://localhost:3001"
+
healthResp, err := http.Get(pdsURL + "/xrpc/_health")
+
if err != nil {
+
t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err)
+
}
+
defer func() {
+
if closeErr := healthResp.Body.Close(); closeErr != nil {
+
t.Logf("Failed to close health response: %v", closeErr)
+
}
+
}()
+
+
// Setup test database
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
ctx := context.Background()
+
repo := postgres.NewCommunityRepository(db)
+
+
t.Run("creates community with real PDS provisioning", func(t *testing.T) {
+
// Create provisioner and service (production code path)
+
// Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as .communities.coves.social)
+
provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL)
+
service := communities.NewCommunityService(
+
repo,
+
pdsURL,
+
"did:web:coves.social",
+
"coves.social",
+
provisioner,
+
)
+
+
// Generate unique community name (keep short for DNS label limit)
+
// Must start with letter, can contain alphanumeric and hyphens
+
uniqueName := fmt.Sprintf("svc%d", time.Now().UnixNano()%1000000)
+
+
// Create community via service (FULL PRODUCTION CODE PATH)
+
t.Logf("Creating community via service.CreateCommunity()...")
+
community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: uniqueName,
+
DisplayName: "Test Community",
+
Description: "Integration test community with real PDS",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser123",
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
t.Logf("✅ Community created: %s", community.DID)
+
+
// CRITICAL: Verify password was generated by provisioner (not hardcoded)
+
if len(community.PDSPassword) < 32 {
+
t.Errorf("Password too short: expected >= 32 chars from provisioner, got %d", len(community.PDSPassword))
+
}
+
+
// Verify password is not empty
+
if community.PDSPassword == "" {
+
t.Error("Password should not be empty")
+
}
+
+
// Verify password is not a known test password
+
testPasswords := []string{"test-password", "password123", "admin", ""}
+
for _, testPwd := range testPasswords {
+
if community.PDSPassword == testPwd {
+
t.Errorf("Password appears to be hardcoded test password: %s", testPwd)
+
}
+
}
+
+
t.Logf("✅ Password generated by provisioner: %d chars", len(community.PDSPassword))
+
+
// Verify DID is real (did:plc:xxx from PDS)
+
if !strings.HasPrefix(community.DID, "did:plc:") {
+
t.Errorf("Expected real PLC DID from PDS, got: %s", community.DID)
+
}
+
+
t.Logf("✅ Real DID generated: %s", community.DID)
+
+
// Verify handle format
+
expectedHandle := fmt.Sprintf("%s.communities.coves.social", uniqueName)
+
if community.Handle != expectedHandle {
+
t.Errorf("Expected handle %s, got %s", expectedHandle, community.Handle)
+
}
+
+
t.Logf("✅ Handle generated correctly: %s", community.Handle)
+
+
// Verify tokens are present (from PDS)
+
if community.PDSAccessToken == "" {
+
t.Error("Access token should not be empty")
+
}
+
if community.PDSRefreshToken == "" {
+
t.Error("Refresh token should not be empty")
+
}
+
+
// Verify tokens are JWT format (3 parts separated by dots)
+
accessParts := strings.Split(community.PDSAccessToken, ".")
+
if len(accessParts) != 3 {
+
t.Errorf("Access token should be JWT format (3 parts), got %d parts", len(accessParts))
+
}
+
+
t.Logf("✅ JWT tokens received from PDS")
+
+
// Verify record URI points to community's own repository (V2 architecture)
+
expectedURIPrefix := fmt.Sprintf("at://%s/social.coves.community.profile/self", community.DID)
+
if community.RecordURI != expectedURIPrefix {
+
t.Errorf("Expected record URI %s, got %s", expectedURIPrefix, community.RecordURI)
+
}
+
+
t.Logf("✅ Record URI points to community's own repo: %s", community.RecordURI)
+
+
// Verify V2 ownership model (community owns itself)
+
if community.OwnerDID != community.DID {
+
t.Errorf("V2: community should own itself. Expected OwnerDID=%s, got %s", community.DID, community.OwnerDID)
+
}
+
+
t.Logf("✅ V2 ownership: community owns itself")
+
+
// CRITICAL: Verify credentials were persisted to database WITH ENCRYPTION
+
retrieved, err := repo.GetByDID(ctx, community.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community from DB: %v", err)
+
}
+
+
// Verify password roundtrip (encrypted → decrypted)
+
if retrieved.PDSPassword != community.PDSPassword {
+
t.Error("Password not persisted correctly (encryption/decryption failed)")
+
}
+
+
// Verify tokens roundtrip
+
if retrieved.PDSAccessToken != community.PDSAccessToken {
+
t.Error("Access token not persisted correctly")
+
}
+
if retrieved.PDSRefreshToken != community.PDSRefreshToken {
+
t.Error("Refresh token not persisted correctly")
+
}
+
+
t.Logf("✅ Credentials persisted to DB with encryption")
+
+
// Verify password is encrypted at rest in database
+
var encryptedPassword []byte
+
query := `
+
SELECT pds_password_encrypted
+
FROM communities
+
WHERE did = $1
+
`
+
if err := db.QueryRowContext(ctx, query, community.DID).Scan(&encryptedPassword); err != nil {
+
t.Fatalf("Failed to query encrypted password: %v", err)
+
}
+
+
// Verify NOT stored as plaintext
+
if string(encryptedPassword) == community.PDSPassword {
+
t.Error("CRITICAL: Password stored as plaintext in database!")
+
}
+
+
// Verify encrypted data exists
+
if len(encryptedPassword) == 0 {
+
t.Error("Encrypted password should have data")
+
}
+
+
t.Logf("✅ Password encrypted at rest in database")
+
+
t.Logf("✅ COMPLETE TEST PASSED: Full write-forward architecture verified")
+
})
+
+
t.Run("handles PDS errors gracefully", func(t *testing.T) {
+
provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL)
+
service := communities.NewCommunityService(
+
repo,
+
pdsURL,
+
"did:web:coves.social",
+
"coves.social",
+
provisioner,
+
)
+
+
// Try to create community with invalid name (should fail validation before PDS)
+
_, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: "", // Empty name
+
DisplayName: "Invalid Community",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser123",
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err == nil {
+
t.Error("Expected validation error for empty name")
+
}
+
+
if !strings.Contains(err.Error(), "name") {
+
t.Errorf("Expected 'name' error, got: %v", err)
+
}
+
+
t.Logf("✅ Validation errors handled correctly")
+
})
+
+
t.Run("validates DNS label limits", func(t *testing.T) {
+
provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL)
+
service := communities.NewCommunityService(
+
repo,
+
pdsURL,
+
"did:web:coves.social",
+
"coves.social",
+
provisioner,
+
)
+
+
// Try 64-char name (exceeds DNS limit of 63)
+
longName := strings.Repeat("a", 64)
+
+
_, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: longName,
+
DisplayName: "Long Name Test",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser123",
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err == nil {
+
t.Error("Expected error for 64-char name (DNS limit is 63)")
+
}
+
+
if !strings.Contains(err.Error(), "63") {
+
t.Errorf("Expected DNS limit error mentioning '63', got: %v", err)
+
}
+
+
t.Logf("✅ DNS label limits enforced")
+
})
+
}
+
+
// TestCommunityService_UpdateWithRealPDS tests the V2 update flow
+
// This is CRITICAL - currently has ZERO test coverage in unit tests!
+
//
+
// Verifies:
+
// - Updates use community's OWN credentials (not instance credentials)
+
// - Writes to community's repository (at://community_did/...)
+
// - Authorization checks (only creator can update)
+
// - Record rkey is always "self" for V2
+
func TestCommunityService_UpdateWithRealPDS(t *testing.T) {
+
if testing.Short() {
+
t.Skip("Skipping integration test in short mode - requires PDS")
+
}
+
+
// Check if PDS is running
+
pdsURL := "http://localhost:3001"
+
healthResp, err := http.Get(pdsURL + "/xrpc/_health")
+
if err != nil {
+
t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err)
+
}
+
defer func() {
+
if closeErr := healthResp.Body.Close(); closeErr != nil {
+
t.Logf("Failed to close health response: %v", closeErr)
+
}
+
}()
+
+
// Setup test database
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
ctx := context.Background()
+
repo := postgres.NewCommunityRepository(db)
+
+
provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL)
+
service := communities.NewCommunityService(
+
repo,
+
pdsURL,
+
"did:web:coves.social",
+
"coves.social",
+
provisioner,
+
)
+
+
t.Run("updates community with real PDS", func(t *testing.T) {
+
// First, create a community
+
uniqueName := fmt.Sprintf("upd%d", time.Now().UnixNano()%1000000)
+
creatorDID := "did:plc:updatetestuser"
+
+
t.Logf("Creating community to update...")
+
community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: uniqueName,
+
DisplayName: "Original Display Name",
+
Description: "Original description",
+
Visibility: "public",
+
CreatedByDID: creatorDID,
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
t.Logf("✅ Community created: %s", community.DID)
+
+
// Now update it
+
newDisplayName := "Updated Display Name"
+
newDescription := "Updated description via V2 write-forward"
+
newVisibility := "unlisted"
+
+
t.Logf("Updating community via service.UpdateCommunity()...")
+
updated, err := service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{
+
CommunityDID: community.DID,
+
UpdatedByDID: creatorDID, // Same as creator - should be authorized
+
DisplayName: &newDisplayName,
+
Description: &newDescription,
+
Visibility: &newVisibility,
+
AllowExternalDiscovery: nil, // Don't change
+
})
+
+
if err != nil {
+
t.Fatalf("Failed to update community: %v", err)
+
}
+
+
t.Logf("✅ Community updated via PDS")
+
+
// Verify updates were applied
+
if updated.DisplayName != newDisplayName {
+
t.Errorf("Expected display name %s, got %s", newDisplayName, updated.DisplayName)
+
}
+
if updated.Description != newDescription {
+
t.Errorf("Expected description %s, got %s", newDescription, updated.Description)
+
}
+
if updated.Visibility != newVisibility {
+
t.Errorf("Expected visibility %s, got %s", newVisibility, updated.Visibility)
+
}
+
+
t.Logf("✅ Updates applied correctly")
+
+
// Verify record URI still points to community's own repo with rkey "self"
+
expectedURIPrefix := fmt.Sprintf("at://%s/social.coves.community.profile/self", community.DID)
+
if updated.RecordURI != expectedURIPrefix {
+
t.Errorf("Expected record URI %s, got %s", expectedURIPrefix, updated.RecordURI)
+
}
+
+
t.Logf("✅ Record URI correct (uses community's repo)")
+
+
// Verify record CID changed (new version)
+
if updated.RecordCID == community.RecordCID {
+
t.Error("Expected record CID to change after update")
+
}
+
+
t.Logf("✅ Record CID updated (new version)")
+
})
+
+
t.Run("rejects unauthorized updates", func(t *testing.T) {
+
// Create a community
+
uniqueName := fmt.Sprintf("auth%d", time.Now().UnixNano()%1000000)
+
creatorDID := "did:plc:creator123"
+
+
community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: uniqueName,
+
DisplayName: "Auth Test Community",
+
Visibility: "public",
+
CreatedByDID: creatorDID,
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
// Try to update as different user
+
differentUserDID := "did:plc:nottheowner"
+
newDisplayName := "Hacked Display Name"
+
+
_, err = service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{
+
CommunityDID: community.DID,
+
UpdatedByDID: differentUserDID, // NOT the creator
+
DisplayName: &newDisplayName,
+
})
+
+
if err == nil {
+
t.Error("Expected authorization error for non-creator update")
+
}
+
+
if !strings.Contains(strings.ToLower(err.Error()), "unauthorized") {
+
t.Errorf("Expected 'unauthorized' error, got: %v", err)
+
}
+
+
t.Logf("✅ Unauthorized updates rejected")
+
})
+
+
t.Run("handles missing PDS credentials", func(t *testing.T) {
+
// Create a community manually in DB without PDS credentials
+
// (simulating a federated community indexed from another instance)
+
uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
+
communityDID := generateTestDID(uniqueSuffix)
+
+
federatedCommunity := &communities.Community{
+
DID: communityDID,
+
Handle: fmt.Sprintf("federated-%s.external.social", uniqueSuffix),
+
Name: "federated-test",
+
OwnerDID: communityDID,
+
CreatedByDID: "did:plc:externaluser",
+
HostedByDID: "did:web:external.social",
+
Visibility: "public",
+
// No PDS credentials - this is a federated community
+
CreatedAt: time.Now(),
+
UpdatedAt: time.Now(),
+
}
+
+
_, err := repo.Create(ctx, federatedCommunity)
+
if err != nil {
+
t.Fatalf("Failed to create federated community: %v", err)
+
}
+
+
// Try to update it - should fail because we don't have credentials
+
newDisplayName := "Cannot Update This"
+
_, err = service.UpdateCommunity(ctx, communities.UpdateCommunityRequest{
+
CommunityDID: communityDID,
+
UpdatedByDID: "did:plc:externaluser",
+
DisplayName: &newDisplayName,
+
})
+
+
if err == nil {
+
t.Error("Expected error when updating community without PDS credentials")
+
}
+
+
if !strings.Contains(err.Error(), "missing PDS credentials") {
+
t.Logf("Error message: %v", err)
+
}
+
+
t.Logf("✅ Missing credentials handled gracefully")
+
})
+
}
+
+
// TestPasswordAuthentication verifies that generated passwords work for PDS authentication
+
// This is CRITICAL for P0: passwords must be recoverable for session renewal
+
func TestPasswordAuthentication(t *testing.T) {
+
if testing.Short() {
+
t.Skip("Skipping integration test in short mode - requires PDS")
+
}
+
+
// Check if PDS is running
+
pdsURL := "http://localhost:3001"
+
healthResp, err := http.Get(pdsURL + "/xrpc/_health")
+
if err != nil {
+
t.Skipf("PDS not running at %s: %v. Run 'make dev-up' to start PDS.", pdsURL, err)
+
}
+
defer func() {
+
if closeErr := healthResp.Body.Close(); closeErr != nil {
+
t.Logf("Failed to close health response: %v", closeErr)
+
}
+
}()
+
+
// Setup test database
+
db := setupTestDB(t)
+
defer func() {
+
if err := db.Close(); err != nil {
+
t.Logf("Failed to close database: %v", err)
+
}
+
}()
+
+
ctx := context.Background()
+
repo := postgres.NewCommunityRepository(db)
+
+
provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL)
+
service := communities.NewCommunityService(
+
repo,
+
pdsURL,
+
"did:web:coves.social",
+
"coves.social",
+
provisioner,
+
)
+
+
t.Run("generated password works for session creation", func(t *testing.T) {
+
// Create a community with PDS-generated password
+
uniqueName := fmt.Sprintf("pwd%d", time.Now().UnixNano()%1000000)
+
+
t.Logf("Creating community with generated password...")
+
community, err := service.CreateCommunity(ctx, communities.CreateCommunityRequest{
+
Name: uniqueName,
+
DisplayName: "Password Auth Test",
+
Visibility: "public",
+
CreatedByDID: "did:plc:testuser",
+
HostedByDID: "did:web:coves.social",
+
AllowExternalDiscovery: true,
+
})
+
+
if err != nil {
+
t.Fatalf("Failed to create community: %v", err)
+
}
+
+
t.Logf("✅ Community created with password: %d chars", len(community.PDSPassword))
+
+
// Retrieve from DB to get decrypted password
+
retrieved, err := repo.GetByDID(ctx, community.DID)
+
if err != nil {
+
t.Fatalf("Failed to retrieve community: %v", err)
+
}
+
+
t.Logf("✅ Password retrieved from DB (decrypted): %d chars", len(retrieved.PDSPassword))
+
+
// Now try to authenticate with the password via com.atproto.server.createSession
+
// This simulates what we'd do for token renewal
+
sessionPayload := map[string]interface{}{
+
"identifier": retrieved.Handle, // Use handle for login
+
"password": retrieved.PDSPassword,
+
}
+
+
payloadBytes, err := json.Marshal(sessionPayload)
+
if err != nil {
+
t.Fatalf("Failed to marshal session payload: %v", err)
+
}
+
+
sessionReq, err := http.NewRequestWithContext(ctx, "POST",
+
pdsURL+"/xrpc/com.atproto.server.createSession",
+
bytes.NewReader(payloadBytes))
+
if err != nil {
+
t.Fatalf("Failed to create session request: %v", err)
+
}
+
sessionReq.Header.Set("Content-Type", "application/json")
+
+
client := &http.Client{Timeout: 10 * time.Second}
+
resp, err := client.Do(sessionReq)
+
if err != nil {
+
t.Fatalf("Failed to create session: %v", err)
+
}
+
defer func() {
+
if closeErr := resp.Body.Close(); closeErr != nil {
+
t.Logf("Failed to close response body: %v", closeErr)
+
}
+
}()
+
+
body, err := io.ReadAll(resp.Body)
+
if err != nil {
+
t.Fatalf("Failed to read response body: %v", err)
+
}
+
+
if resp.StatusCode != http.StatusOK {
+
t.Fatalf("Session creation failed with status %d: %s", resp.StatusCode, string(body))
+
}
+
+
// Verify we got new tokens
+
var sessionResp struct {
+
AccessJwt string `json:"accessJwt"`
+
RefreshJwt string `json:"refreshJwt"`
+
DID string `json:"did"`
+
}
+
+
if err := json.Unmarshal(body, &sessionResp); err != nil {
+
t.Fatalf("Failed to parse session response: %v", err)
+
}
+
+
if sessionResp.AccessJwt == "" {
+
t.Error("Expected new access token from session")
+
}
+
if sessionResp.RefreshJwt == "" {
+
t.Error("Expected new refresh token from session")
+
}
+
if sessionResp.DID != community.DID {
+
t.Errorf("Expected session DID %s, got %s", community.DID, sessionResp.DID)
+
}
+
+
t.Logf("✅ Password authentication successful!")
+
t.Logf(" - New access token: %d chars", len(sessionResp.AccessJwt))
+
t.Logf(" - New refresh token: %d chars", len(sessionResp.RefreshJwt))
+
t.Logf(" - Session DID: %s", sessionResp.DID)
+
+
t.Logf("✅ CRITICAL TEST PASSED: Password encryption enables session renewal")
+
})
+
}
+8
tests/integration/user_test.go
···
return db
}
func TestUserCreationAndRetrieval(t *testing.T) {
db := setupTestDB(t)
defer func() {
···
return db
}
+
// generateTestDID generates a unique test DID for integration tests
+
// V2.0: No longer uses DID generator - just creates valid did:plc strings
+
func generateTestDID(suffix string) string {
+
// Use a deterministic base + suffix for reproducible test DIDs
+
// Format matches did:plc but doesn't need PLC registration for unit/repo tests
+
return fmt.Sprintf("did:plc:test%s", suffix)
+
}
+
func TestUserCreationAndRetrieval(t *testing.T) {
db := setupTestDB(t)
defer func() {
+2 -3
tests/unit/community_service_test.go
···
package unit
import (
-
"Coves/internal/atproto/did"
"Coves/internal/core/communities"
"context"
"fmt"
···
defer slowPDS.Close()
_ = newMockCommunityRepo()
-
_ = did.NewGenerator(true, "https://plc.directory")
// Note: We can't easily test the actual service without mocking more dependencies
// This test verifies the concept - in practice, a 15s operation should NOT timeout
···
CreatedByDID: "did:plc:creator",
HostedByDID: "did:web:coves.social",
PDSEmail: "community-test@communities.coves.social",
-
PDSPasswordHash: "$2a$10$hash",
PDSAccessToken: "test_access_token",
PDSRefreshToken: "test_refresh_token",
PDSURL: "http://localhost:2583",
···
package unit
import (
"Coves/internal/core/communities"
"context"
"fmt"
···
defer slowPDS.Close()
_ = newMockCommunityRepo()
+
// V2.0: DID generator no longer needed - PDS generates DIDs
// Note: We can't easily test the actual service without mocking more dependencies
// This test verifies the concept - in practice, a 15s operation should NOT timeout
···
CreatedByDID: "did:plc:creator",
HostedByDID: "did:web:coves.social",
PDSEmail: "community-test@communities.coves.social",
+
PDSPassword: "cleartext-password-will-be-encrypted", // V2: Cleartext (encrypted by repository)
PDSAccessToken: "test_access_token",
PDSRefreshToken: "test_refresh_token",
PDSURL: "http://localhost:2583",