260 lines
8.5 KiB
Go
260 lines
8.5 KiB
Go
package ucan
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"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/token/invocation"
|
|
"github.com/ipfs/go-cid"
|
|
)
|
|
|
|
// InvocationBuilder provides a fluent API for creating UCAN invocations.
|
|
// An invocation exercises a delegated capability to perform an action.
|
|
type InvocationBuilder struct {
|
|
issuer did.DID
|
|
subject did.DID
|
|
cmd command.Command
|
|
proofs []cid.Cid
|
|
opts []invocation.Option
|
|
err error
|
|
}
|
|
|
|
// NewInvocationBuilder creates a builder for constructing UCAN invocations.
|
|
func NewInvocationBuilder() *InvocationBuilder {
|
|
return &InvocationBuilder{
|
|
proofs: make([]cid.Cid, 0),
|
|
opts: make([]invocation.Option, 0),
|
|
}
|
|
}
|
|
|
|
// Issuer sets the invocation issuer (the entity invoking the capability).
|
|
func (b *InvocationBuilder) Issuer(iss did.DID) *InvocationBuilder {
|
|
b.issuer = iss
|
|
return b
|
|
}
|
|
|
|
// IssuerString parses and sets the issuer from a DID string.
|
|
func (b *InvocationBuilder) IssuerString(issStr string) *InvocationBuilder {
|
|
iss, err := did.Parse(issStr)
|
|
if err != nil {
|
|
b.err = fmt.Errorf("invalid issuer DID: %w", err)
|
|
return b
|
|
}
|
|
b.issuer = iss
|
|
return b
|
|
}
|
|
|
|
// Subject sets the invocation subject (the resource being acted upon).
|
|
func (b *InvocationBuilder) Subject(sub did.DID) *InvocationBuilder {
|
|
b.subject = sub
|
|
return b
|
|
}
|
|
|
|
// SubjectString parses and sets the subject from a DID string.
|
|
func (b *InvocationBuilder) SubjectString(subStr string) *InvocationBuilder {
|
|
sub, err := did.Parse(subStr)
|
|
if err != nil {
|
|
b.err = fmt.Errorf("invalid subject DID: %w", err)
|
|
return b
|
|
}
|
|
b.subject = sub
|
|
return b
|
|
}
|
|
|
|
// Command sets the command being invoked.
|
|
func (b *InvocationBuilder) Command(cmd command.Command) *InvocationBuilder {
|
|
b.cmd = cmd
|
|
return b
|
|
}
|
|
|
|
// CommandString parses and sets the command from a string.
|
|
func (b *InvocationBuilder) CommandString(cmdStr string) *InvocationBuilder {
|
|
cmd, err := command.Parse(cmdStr)
|
|
if err != nil {
|
|
b.err = fmt.Errorf("invalid command: %w", err)
|
|
return b
|
|
}
|
|
b.cmd = cmd
|
|
return b
|
|
}
|
|
|
|
// Proof adds a delegation CID to the proof chain.
|
|
// Proofs should be ordered from leaf (matching invocation's issuer as audience)
|
|
// to root delegation.
|
|
func (b *InvocationBuilder) Proof(delegationCID cid.Cid) *InvocationBuilder {
|
|
b.proofs = append(b.proofs, delegationCID)
|
|
return b
|
|
}
|
|
|
|
// ProofString parses and adds a delegation CID from a string.
|
|
func (b *InvocationBuilder) ProofString(cidStr string) *InvocationBuilder {
|
|
c, err := cid.Parse(cidStr)
|
|
if err != nil {
|
|
b.err = fmt.Errorf("invalid proof CID: %w", err)
|
|
return b
|
|
}
|
|
b.proofs = append(b.proofs, c)
|
|
return b
|
|
}
|
|
|
|
// Proofs sets all proofs at once, replacing any previously added.
|
|
func (b *InvocationBuilder) Proofs(cids []cid.Cid) *InvocationBuilder {
|
|
b.proofs = cids
|
|
return b
|
|
}
|
|
|
|
// Arg adds an argument to the invocation.
|
|
func (b *InvocationBuilder) Arg(key string, value any) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithArgument(key, value))
|
|
return b
|
|
}
|
|
|
|
// Audience sets the invocation's audience (executor if different from subject).
|
|
func (b *InvocationBuilder) Audience(aud did.DID) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithAudience(aud))
|
|
return b
|
|
}
|
|
|
|
// AudienceString parses and sets the audience from a DID string.
|
|
func (b *InvocationBuilder) AudienceString(audStr string) *InvocationBuilder {
|
|
aud, err := did.Parse(audStr)
|
|
if err != nil {
|
|
b.err = fmt.Errorf("invalid audience DID: %w", err)
|
|
return b
|
|
}
|
|
b.opts = append(b.opts, invocation.WithAudience(aud))
|
|
return b
|
|
}
|
|
|
|
// ExpiresAt sets when the invocation expires.
|
|
func (b *InvocationBuilder) ExpiresAt(exp time.Time) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithExpiration(exp))
|
|
return b
|
|
}
|
|
|
|
// ExpiresIn sets the invocation to expire after a duration from now.
|
|
func (b *InvocationBuilder) ExpiresIn(d time.Duration) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithExpirationIn(d))
|
|
return b
|
|
}
|
|
|
|
// Meta adds metadata to the invocation.
|
|
func (b *InvocationBuilder) Meta(key string, value any) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithMeta(key, value))
|
|
return b
|
|
}
|
|
|
|
// Nonce sets a custom nonce. If not called, a random 12-byte nonce is generated.
|
|
func (b *InvocationBuilder) Nonce(nonce []byte) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithNonce(nonce))
|
|
return b
|
|
}
|
|
|
|
// EmptyNonce sets an empty nonce for idempotent operations.
|
|
func (b *InvocationBuilder) EmptyNonce() *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithEmptyNonce())
|
|
return b
|
|
}
|
|
|
|
// Cause sets the receipt CID that enqueued this invocation.
|
|
func (b *InvocationBuilder) Cause(receiptCID cid.Cid) *InvocationBuilder {
|
|
b.opts = append(b.opts, invocation.WithCause(&receiptCID))
|
|
return b
|
|
}
|
|
|
|
// Build creates the invocation token.
|
|
func (b *InvocationBuilder) Build() (*invocation.Token, error) {
|
|
if b.err != nil {
|
|
return nil, b.err
|
|
}
|
|
|
|
if b.issuer == nil {
|
|
return nil, fmt.Errorf("issuer is required")
|
|
}
|
|
if b.subject == nil {
|
|
return nil, fmt.Errorf("subject is required")
|
|
}
|
|
if b.cmd == "" {
|
|
return nil, fmt.Errorf("command is required")
|
|
}
|
|
|
|
return invocation.New(b.issuer, b.cmd, b.subject, b.proofs, b.opts...)
|
|
}
|
|
|
|
// BuildSealed creates and signs the invocation, returning DAG-CBOR bytes and CID.
|
|
func (b *InvocationBuilder) 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 Invocation Builders ---
|
|
|
|
// VaultReadInvocation creates an invocation to read from a vault.
|
|
func VaultReadInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{invocation.WithArgument("vault", vaultCID)}, opts...)
|
|
return invocation.New(issuer, VaultRead, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// VaultWriteInvocation creates an invocation to write to a vault.
|
|
func VaultWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, data any, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{
|
|
invocation.WithArgument("vault", vaultCID),
|
|
invocation.WithArgument("data", data),
|
|
}, opts...)
|
|
return invocation.New(issuer, VaultWrite, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// VaultSignInvocation creates an invocation to sign with a key in a vault.
|
|
func VaultSignInvocation(issuer, subject did.DID, proofs []cid.Cid, vaultCID string, keyID string, payload []byte, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{
|
|
invocation.WithArgument("vault", vaultCID),
|
|
invocation.WithArgument("key_id", keyID),
|
|
invocation.WithArgument("payload", payload),
|
|
}, opts...)
|
|
return invocation.New(issuer, VaultSign, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// DIDUpdateInvocation creates an invocation to update a DID document.
|
|
func DIDUpdateInvocation(issuer, subject did.DID, proofs []cid.Cid, targetDID string, updates map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{
|
|
invocation.WithArgument("did", targetDID),
|
|
invocation.WithArgument("updates", updates),
|
|
}, opts...)
|
|
return invocation.New(issuer, DIDUpdate, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// DWNRecordsWriteInvocation creates an invocation to write a DWN record.
|
|
func DWNRecordsWriteInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, data any, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{
|
|
invocation.WithArgument("record_type", recordType),
|
|
invocation.WithArgument("data", data),
|
|
}, opts...)
|
|
return invocation.New(issuer, DWNRecordsWrite, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// DWNRecordsReadInvocation creates an invocation to read DWN records.
|
|
func DWNRecordsReadInvocation(issuer, subject did.DID, proofs []cid.Cid, recordType string, filter map[string]any, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := []invocation.Option{
|
|
invocation.WithArgument("record_type", recordType),
|
|
}
|
|
if filter != nil {
|
|
allOpts = append(allOpts, invocation.WithArgument("filter", filter))
|
|
}
|
|
allOpts = append(allOpts, opts...)
|
|
return invocation.New(issuer, DWNRecordsRead, subject, proofs, allOpts...)
|
|
}
|
|
|
|
// RevocationInvocation creates an invocation to revoke a delegation.
|
|
func RevocationInvocation(issuer, subject did.DID, proofs []cid.Cid, delegationCID cid.Cid, opts ...invocation.Option) (*invocation.Token, error) {
|
|
allOpts := append([]invocation.Option{
|
|
invocation.WithArgument("ucan", delegationCID),
|
|
}, opts...)
|
|
return invocation.New(issuer, UCANRevoke, subject, proofs, allOpts...)
|
|
}
|