package keybase import ( "context" "encoding/json" "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 json.RawMessage `json:"prf"` Args json.RawMessage `json:"args,omitempty"` Meta json.RawMessage `json:"meta,omitempty"` 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 json.RawMessage `json:"prf"` Args json.RawMessage `json:"args,omitempty"` Meta json.RawMessage `json:"meta,omitempty"` 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 } prf := string(params.Proofs) if prf == "" { prf = "[]" } args := string(params.Args) if args == "" { args = "{}" } meta := string(params.Meta) if meta == "" { meta = "{}" } 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: prf, Args: args, Meta: meta, 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, Args: inv.Args, Meta: inv.Meta, Expiration: expiration, IssuedAt: issuedAt, ExecutedAt: executedAt, ResultCID: resultCID, CreatedAt: inv.CreatedAt, } }