From 69b0eca088307ed060b0eaee0343efd1d8f89633 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 8 Jan 2026 16:42:16 -0500 Subject: [PATCH] feat(keybase): add invocation actions for UCAN v1.0.0-rc.1 --- internal/keybase/actions_invocation.go | 257 +++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 internal/keybase/actions_invocation.go diff --git a/internal/keybase/actions_invocation.go b/internal/keybase/actions_invocation.go new file mode 100644 index 0000000..8d096fa --- /dev/null +++ b/internal/keybase/actions_invocation.go @@ -0,0 +1,257 @@ +package keybase + +import ( + "context" + "fmt" +) + +// ============================================================================= +// INVOCATION ACTIONS (UCAN v1.0.0-rc.1) +// ============================================================================= + +// InvocationResult represents an invocation in API responses. +type InvocationResult struct { + ID int64 `json:"id"` + CID string `json:"cid"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience string `json:"aud,omitempty"` + Command string `json:"cmd"` + Proofs string `json:"prf"` + Expiration string `json:"exp,omitempty"` + IssuedAt string `json:"iat,omitempty"` + ExecutedAt string `json:"executed_at,omitempty"` + ResultCID string `json:"result_cid,omitempty"` + CreatedAt string `json:"created_at"` +} + +// StoreInvocationParams contains parameters for storing an invocation. +type StoreInvocationParams struct { + CID string `json:"cid"` + Envelope []byte `json:"envelope"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience string `json:"aud,omitempty"` + Command string `json:"cmd"` + Proofs string `json:"prf"` + Expiration string `json:"exp,omitempty"` + IssuedAt string `json:"iat,omitempty"` +} + +// StoreInvocation stores a new UCAN invocation envelope. +func (am *ActionManager) StoreInvocation(ctx context.Context, params StoreInvocationParams) (*InvocationResult, error) { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if am.kb.didID == 0 { + return nil, fmt.Errorf("DID not initialized") + } + + var aud, exp, iat *string + if params.Audience != "" { + aud = ¶ms.Audience + } + if params.Expiration != "" { + exp = ¶ms.Expiration + } + if params.IssuedAt != "" { + iat = ¶ms.IssuedAt + } + + inv, err := am.kb.queries.CreateInvocation(ctx, CreateInvocationParams{ + DidID: am.kb.didID, + Cid: params.CID, + Envelope: params.Envelope, + Iss: params.Issuer, + Sub: params.Subject, + Aud: aud, + Cmd: params.Command, + Prf: params.Proofs, + Exp: exp, + Iat: iat, + }) + if err != nil { + return nil, fmt.Errorf("create invocation: %w", err) + } + + return invocationToResult(inv), nil +} + +// GetInvocationByCID retrieves an invocation by its CID. +func (am *ActionManager) GetInvocationByCID(ctx context.Context, cid string) (*InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + inv, err := am.kb.queries.GetInvocationByCID(ctx, cid) + if err != nil { + return nil, fmt.Errorf("get invocation: %w", err) + } + + return invocationToResult(inv), nil +} + +// GetInvocationEnvelope retrieves the raw CBOR envelope for an invocation. +func (am *ActionManager) GetInvocationEnvelope(ctx context.Context, cid string) ([]byte, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + envelope, err := am.kb.queries.GetInvocationEnvelopeByCID(ctx, cid) + if err != nil { + return nil, fmt.Errorf("get invocation envelope: %w", err) + } + + return envelope, nil +} + +// ListInvocations returns recent invocations for the current DID. +func (am *ActionManager) ListInvocations(ctx context.Context, limit int64) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + if limit <= 0 { + limit = 50 + } + + invocations, err := am.kb.queries.ListInvocationsByDID(ctx, ListInvocationsByDIDParams{ + DidID: am.kb.didID, + Limit: limit, + }) + if err != nil { + return nil, fmt.Errorf("list invocations: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// ListInvocationsByCommand returns invocations for a specific command. +func (am *ActionManager) ListInvocationsByCommand(ctx context.Context, cmd string, limit int64) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + if limit <= 0 { + limit = 50 + } + + invocations, err := am.kb.queries.ListInvocationsForCommand(ctx, ListInvocationsForCommandParams{ + DidID: am.kb.didID, + Cmd: cmd, + Limit: limit, + }) + if err != nil { + return nil, fmt.Errorf("list invocations for command: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// ListPendingInvocations returns invocations that haven't been executed yet. +func (am *ActionManager) ListPendingInvocations(ctx context.Context) ([]InvocationResult, error) { + am.kb.mu.RLock() + defer am.kb.mu.RUnlock() + + if am.kb.didID == 0 { + return []InvocationResult{}, nil + } + + invocations, err := am.kb.queries.ListPendingInvocations(ctx, am.kb.didID) + if err != nil { + return nil, fmt.Errorf("list pending invocations: %w", err) + } + + results := make([]InvocationResult, len(invocations)) + for i, inv := range invocations { + results[i] = *invocationToResult(inv) + } + + return results, nil +} + +// MarkInvocationExecuted marks an invocation as executed with an optional result CID. +func (am *ActionManager) MarkInvocationExecuted(ctx context.Context, cid string, resultCID string) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + var result *string + if resultCID != "" { + result = &resultCID + } + + err := am.kb.queries.MarkInvocationExecuted(ctx, MarkInvocationExecutedParams{ + ResultCid: result, + Cid: cid, + }) + if err != nil { + return fmt.Errorf("mark invocation executed: %w", err) + } + + return nil +} + +// CleanOldInvocations removes invocations older than 90 days. +func (am *ActionManager) CleanOldInvocations(ctx context.Context) error { + am.kb.mu.Lock() + defer am.kb.mu.Unlock() + + if err := am.kb.queries.CleanOldInvocations(ctx); err != nil { + return fmt.Errorf("clean old invocations: %w", err) + } + + return nil +} + +// invocationToResult converts a UcanInvocation to InvocationResult. +func invocationToResult(inv UcanInvocation) *InvocationResult { + audience := "" + if inv.Aud != nil { + audience = *inv.Aud + } + expiration := "" + if inv.Exp != nil { + expiration = *inv.Exp + } + issuedAt := "" + if inv.Iat != nil { + issuedAt = *inv.Iat + } + executedAt := "" + if inv.ExecutedAt != nil { + executedAt = *inv.ExecutedAt + } + resultCID := "" + if inv.ResultCid != nil { + resultCID = *inv.ResultCid + } + + return &InvocationResult{ + ID: inv.ID, + CID: inv.Cid, + Issuer: inv.Iss, + Subject: inv.Sub, + Audience: audience, + Command: inv.Cmd, + Proofs: inv.Prf, + Expiration: expiration, + IssuedAt: issuedAt, + ExecutedAt: executedAt, + ResultCID: resultCID, + CreatedAt: inv.CreatedAt, + } +}