Files
2025-12-18 23:23:38 -06:00

146 lines
4.2 KiB
Go

// Package did provides shared DID document types and utilities for ATProto services.
// Both AppView and Hold use this package for did:web document generation.
package did
import (
"encoding/json"
"fmt"
"net/url"
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
// DIDDocument represents a did:web document
type DIDDocument struct {
Context []string `json:"@context"`
ID string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
VerificationMethod []VerificationMethod `json:"verificationMethod"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
Service []Service `json:"service,omitempty"`
}
// VerificationMethod represents a public key in a DID document
type VerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}
// Service represents a service endpoint in a DID document
type Service struct {
ID string `json:"id"`
Type string `json:"type"`
ServiceEndpoint string `json:"serviceEndpoint"`
}
// GenerateDIDFromURL creates a did:web identifier from a public URL
// Example: "https://atcr.io" -> "did:web:atcr.io"
// Example: "http://hold1.example.com:8080" -> "did:web:hold1.example.com:8080"
// Note: Non-standard ports are included in the DID
func GenerateDIDFromURL(publicURL string) string {
u, err := url.Parse(publicURL)
if err != nil {
// Fallback: assume it's just a hostname
return fmt.Sprintf("did:web:%s", publicURL)
}
hostname := u.Hostname()
if hostname == "" {
hostname = "localhost"
}
port := u.Port()
// Include port in DID if it's non-standard (not 80 for http, not 443 for https)
if port != "" && port != "80" && port != "443" {
return fmt.Sprintf("did:web:%s:%s", hostname, port)
}
return fmt.Sprintf("did:web:%s", hostname)
}
// GenerateDIDDocument creates a DID document for a did:web identity
// This is a standalone function that can be used by any ATProto service.
// The services parameter allows customizing which service endpoints to include.
func GenerateDIDDocument(publicURL string, publicKey atcrypto.PublicKey, services []Service) (*DIDDocument, error) {
u, err := url.Parse(publicURL)
if err != nil {
return nil, fmt.Errorf("failed to parse public URL: %w", err)
}
hostname := u.Hostname()
port := u.Port()
// Build host string (include non-standard ports)
host := hostname
if port != "" && port != "80" && port != "443" {
host = fmt.Sprintf("%s:%s", hostname, port)
}
did := fmt.Sprintf("did:web:%s", host)
// Get public key in multibase format
publicKeyMultibase := publicKey.Multibase()
doc := &DIDDocument{
Context: []string{
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1",
},
ID: did,
AlsoKnownAs: []string{
fmt.Sprintf("at://%s", host),
},
VerificationMethod: []VerificationMethod{
{
ID: fmt.Sprintf("%s#atproto", did),
Type: "Multikey",
Controller: did,
PublicKeyMultibase: publicKeyMultibase,
},
},
Authentication: []string{
fmt.Sprintf("%s#atproto", did),
},
Service: services,
}
return doc, nil
}
// MarshalDIDDocument converts a DID document to JSON bytes
func MarshalDIDDocument(doc *DIDDocument) ([]byte, error) {
return json.MarshalIndent(doc, "", " ")
}
// DefaultHoldServices returns the standard service endpoints for a Hold service
func DefaultHoldServices(publicURL string) []Service {
return []Service{
{
ID: "#atproto_pds",
Type: "AtprotoPersonalDataServer",
ServiceEndpoint: publicURL,
},
{
ID: "#atcr_hold",
Type: "AtcrHoldService",
ServiceEndpoint: publicURL,
},
}
}
// DefaultAppViewServices returns the standard service endpoints for AppView
func DefaultAppViewServices(publicURL string) []Service {
return []Service{
{
ID: "#atcr_registry",
Type: "AtcrRegistryService",
ServiceEndpoint: publicURL,
},
}
}