2026-01-08 15:21:07 -05:00
|
|
|
package ucan
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-01-08 15:51:17 -05:00
|
|
|
"code.sonr.org/go/did-it"
|
|
|
|
|
"code.sonr.org/go/did-it/crypto"
|
|
|
|
|
"code.sonr.org/go/ucan/pkg/command"
|
|
|
|
|
"code.sonr.org/go/ucan/pkg/policy"
|
|
|
|
|
"code.sonr.org/go/ucan/token/delegation"
|
2026-01-10 16:49:23 -05:00
|
|
|
"github.com/ipfs/go-cid"
|
2026-01-08 15:21:07 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// DelegationBuilder provides a fluent API for creating UCAN delegations.
|
|
|
|
|
// A delegation grants authority from issuer to audience to perform a command on a subject.
|
|
|
|
|
type DelegationBuilder struct {
|
|
|
|
|
issuer did.DID
|
|
|
|
|
audience did.DID
|
|
|
|
|
subject did.DID
|
|
|
|
|
cmd command.Command
|
|
|
|
|
pol policy.Policy
|
|
|
|
|
opts []delegation.Option
|
|
|
|
|
err error
|
|
|
|
|
isPowerline bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDelegationBuilder creates a builder for constructing UCAN delegations.
|
|
|
|
|
func NewDelegationBuilder() *DelegationBuilder {
|
|
|
|
|
return &DelegationBuilder{
|
|
|
|
|
pol: policy.Policy{},
|
|
|
|
|
opts: make([]delegation.Option, 0),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Issuer sets the delegation issuer (the entity granting authority).
|
|
|
|
|
func (b *DelegationBuilder) Issuer(iss did.DID) *DelegationBuilder {
|
|
|
|
|
b.issuer = iss
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IssuerString parses and sets the issuer from a DID string.
|
|
|
|
|
func (b *DelegationBuilder) IssuerString(issStr string) *DelegationBuilder {
|
|
|
|
|
iss, err := did.Parse(issStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.err = fmt.Errorf("invalid issuer DID: %w", err)
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
b.issuer = iss
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Audience sets the delegation audience (the entity receiving authority).
|
|
|
|
|
func (b *DelegationBuilder) Audience(aud did.DID) *DelegationBuilder {
|
|
|
|
|
b.audience = aud
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AudienceString parses and sets the audience from a DID string.
|
|
|
|
|
func (b *DelegationBuilder) AudienceString(audStr string) *DelegationBuilder {
|
|
|
|
|
aud, err := did.Parse(audStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.err = fmt.Errorf("invalid audience DID: %w", err)
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
b.audience = aud
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subject sets the delegation subject (the resource being delegated).
|
|
|
|
|
// For root delegations, subject should equal issuer.
|
|
|
|
|
// For powerline delegations, use Powerline() instead.
|
|
|
|
|
func (b *DelegationBuilder) Subject(sub did.DID) *DelegationBuilder {
|
|
|
|
|
b.subject = sub
|
|
|
|
|
b.isPowerline = false
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SubjectString parses and sets the subject from a DID string.
|
|
|
|
|
func (b *DelegationBuilder) SubjectString(subStr string) *DelegationBuilder {
|
|
|
|
|
sub, err := did.Parse(subStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.err = fmt.Errorf("invalid subject DID: %w", err)
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
b.subject = sub
|
|
|
|
|
b.isPowerline = false
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Powerline creates a powerline delegation (subject = nil).
|
|
|
|
|
// Powerline automatically delegates all future delegations regardless of subject.
|
|
|
|
|
// Use with caution - this is a very powerful pattern.
|
|
|
|
|
func (b *DelegationBuilder) Powerline() *DelegationBuilder {
|
|
|
|
|
b.subject = nil
|
|
|
|
|
b.isPowerline = true
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AsRoot sets the subject equal to the issuer (root delegation).
|
|
|
|
|
// Root delegations are typically used to create the initial grant of authority.
|
|
|
|
|
func (b *DelegationBuilder) AsRoot() *DelegationBuilder {
|
|
|
|
|
b.subject = b.issuer
|
|
|
|
|
b.isPowerline = false
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Command sets the command being delegated.
|
|
|
|
|
func (b *DelegationBuilder) Command(cmd command.Command) *DelegationBuilder {
|
|
|
|
|
b.cmd = cmd
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CommandString parses and sets the command from a string.
|
|
|
|
|
func (b *DelegationBuilder) CommandString(cmdStr string) *DelegationBuilder {
|
|
|
|
|
cmd, err := command.Parse(cmdStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
b.err = fmt.Errorf("invalid command: %w", err)
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
b.cmd = cmd
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Policy sets the policy constraints on the delegation.
|
|
|
|
|
func (b *DelegationBuilder) Policy(pol policy.Policy) *DelegationBuilder {
|
|
|
|
|
b.pol = pol
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExpiresAt sets when the delegation expires.
|
|
|
|
|
func (b *DelegationBuilder) ExpiresAt(exp time.Time) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithExpiration(exp))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExpiresIn sets the delegation to expire after a duration from now.
|
|
|
|
|
func (b *DelegationBuilder) ExpiresIn(d time.Duration) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithExpirationIn(d))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NotBefore sets when the delegation becomes valid.
|
|
|
|
|
func (b *DelegationBuilder) NotBefore(nbf time.Time) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithNotBefore(nbf))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NotBeforeIn sets the delegation to become valid after a duration from now.
|
|
|
|
|
func (b *DelegationBuilder) NotBeforeIn(d time.Duration) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithNotBeforeIn(d))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Meta adds metadata to the delegation.
|
|
|
|
|
func (b *DelegationBuilder) Meta(key string, value any) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithMeta(key, value))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
|
|
|
|
|
func (b *DelegationBuilder) Nonce(nonce []byte) *DelegationBuilder {
|
|
|
|
|
b.opts = append(b.opts, delegation.WithNonce(nonce))
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build creates the delegation token.
|
|
|
|
|
func (b *DelegationBuilder) Build() (*delegation.Token, error) {
|
|
|
|
|
if b.err != nil {
|
|
|
|
|
return nil, b.err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.issuer == nil {
|
|
|
|
|
return nil, fmt.Errorf("issuer is required")
|
|
|
|
|
}
|
|
|
|
|
if b.audience == nil {
|
|
|
|
|
return nil, fmt.Errorf("audience is required")
|
|
|
|
|
}
|
|
|
|
|
if b.cmd == "" {
|
|
|
|
|
return nil, fmt.Errorf("command is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if b.isPowerline {
|
|
|
|
|
return delegation.Powerline(b.issuer, b.audience, b.cmd, b.pol, b.opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If subject not set and not powerline, default to root delegation
|
|
|
|
|
if b.subject == nil {
|
|
|
|
|
b.subject = b.issuer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return delegation.New(b.issuer, b.audience, b.cmd, b.pol, b.subject, b.opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BuildSealed creates and signs the delegation, returning DAG-CBOR bytes and CID.
|
|
|
|
|
func (b *DelegationBuilder) BuildSealed(privKey crypto.PrivateKeySigningBytes) ([]byte, cid.Cid, error) {
|
|
|
|
|
tkn, err := b.Build()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, cid.Cid{}, err
|
|
|
|
|
}
|
|
|
|
|
return tkn.ToSealed(privKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Sonr-specific Delegation Builders ---
|
|
|
|
|
|
|
|
|
|
// NewVaultDelegation creates a delegation for vault operations.
|
|
|
|
|
// cmd should be one of: VaultRead, VaultWrite, VaultSign, VaultExport, VaultImport, VaultDelete, VaultAdmin, or Vault (all).
|
|
|
|
|
func NewVaultDelegation(issuer, audience did.DID, cmd command.Command, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
pol := VaultPolicy(vaultCID)
|
|
|
|
|
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewVaultReadDelegation creates a delegation to read from a specific vault.
|
|
|
|
|
func NewVaultReadDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewVaultDelegation(issuer, audience, VaultRead, vaultCID, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewVaultWriteDelegation creates a delegation to write to a specific vault.
|
|
|
|
|
func NewVaultWriteDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewVaultDelegation(issuer, audience, VaultWrite, vaultCID, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewVaultSignDelegation creates a delegation to sign with keys in a specific vault.
|
|
|
|
|
func NewVaultSignDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewVaultDelegation(issuer, audience, VaultSign, vaultCID, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewVaultFullAccessDelegation creates a delegation for all vault operations on a specific vault.
|
|
|
|
|
func NewVaultFullAccessDelegation(issuer, audience did.DID, vaultCID string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewVaultDelegation(issuer, audience, Vault, vaultCID, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDIDDelegation creates a delegation for DID operations.
|
|
|
|
|
// cmd should be one of: DIDCreate, DIDUpdate, DIDDeactivate, or DID (all).
|
|
|
|
|
func NewDIDDelegation(issuer, audience did.DID, cmd command.Command, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
pol := DIDPolicy(didPattern)
|
|
|
|
|
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDIDUpdateDelegation creates a delegation to update DIDs matching a pattern.
|
|
|
|
|
func NewDIDUpdateDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewDIDDelegation(issuer, audience, DIDUpdate, didPattern, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDIDFullAccessDelegation creates a delegation for all DID operations on DIDs matching a pattern.
|
|
|
|
|
func NewDIDFullAccessDelegation(issuer, audience did.DID, didPattern string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewDIDDelegation(issuer, audience, DID, didPattern, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDWNDelegation creates a delegation for DWN (Decentralized Web Node) operations.
|
|
|
|
|
// cmd should be one of: DWNRecordsWrite, DWNRecordsRead, DWNRecordsDelete, or DWN (all).
|
|
|
|
|
func NewDWNDelegation(issuer, audience did.DID, cmd command.Command, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
pol := RecordTypePolicy(recordType)
|
|
|
|
|
return delegation.Root(issuer, audience, cmd, pol, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDWNReadDelegation creates a delegation to read DWN records of a specific type.
|
|
|
|
|
func NewDWNReadDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewDWNDelegation(issuer, audience, DWNRecordsRead, recordType, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDWNWriteDelegation creates a delegation to write DWN records of a specific type.
|
|
|
|
|
func NewDWNWriteDelegation(issuer, audience did.DID, recordType string, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return NewDWNDelegation(issuer, audience, DWNRecordsWrite, recordType, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewRootCapabilityDelegation creates a root delegation granting all capabilities.
|
|
|
|
|
// Use with extreme caution - this grants full authority.
|
|
|
|
|
func NewRootCapabilityDelegation(issuer, audience did.DID, opts ...delegation.Option) (*delegation.Token, error) {
|
|
|
|
|
return delegation.Root(issuer, audience, Root, EmptyPolicy(), opts...)
|
|
|
|
|
}
|