930 lines
24 KiB
Go
930 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"enclave/internal/keybase"
|
|
"enclave/internal/state"
|
|
"enclave/internal/types"
|
|
|
|
"github.com/extism/go-pdk"
|
|
)
|
|
|
|
func main() { state.Default() }
|
|
|
|
//go:wasmexport ping
|
|
func ping() int32 {
|
|
pdk.Log(pdk.LogInfo, "ping: received request")
|
|
|
|
var input types.PingInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
output := types.PingOutput{
|
|
Success: false,
|
|
Message: fmt.Sprintf("failed to parse input: %s", err),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 0
|
|
}
|
|
|
|
output := types.PingOutput{
|
|
Success: true,
|
|
Message: "pong",
|
|
Echo: input.Message,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.Log(pdk.LogError, fmt.Sprintf("ping: failed to output: %s", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("ping: responded with echo=%s", input.Message))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport generate
|
|
func generate() int32 {
|
|
pdk.Log(pdk.LogInfo, "generate: starting database initialization")
|
|
|
|
var input types.GenerateInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.Credential == "" {
|
|
pdk.SetError(errors.New("generate: credential is required"))
|
|
return 1
|
|
}
|
|
|
|
credentialBytes, err := base64.StdEncoding.DecodeString(input.Credential)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: invalid base64 credential: %w", err))
|
|
return 1
|
|
}
|
|
|
|
did, err := initializeDatabase(credentialBytes)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err))
|
|
return 1
|
|
}
|
|
|
|
state.SetInitialized(true)
|
|
state.SetDID(did)
|
|
|
|
dbBytes, err := serializeDatabase()
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err))
|
|
return 1
|
|
}
|
|
|
|
output := types.GenerateOutput{
|
|
DID: did,
|
|
Database: dbBytes,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("generate: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("generate: created DID %s", did))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport load
|
|
func load() int32 {
|
|
pdk.Log(pdk.LogInfo, "load: loading database from buffer")
|
|
|
|
var input types.LoadInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("load: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if len(input.Database) == 0 {
|
|
pdk.SetError(errors.New("load: database buffer is required"))
|
|
return 1
|
|
}
|
|
|
|
did, err := loadDatabase(input.Database)
|
|
if err != nil {
|
|
output := types.LoadOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
|
|
state.SetInitialized(true)
|
|
state.SetDID(did)
|
|
|
|
output := types.LoadOutput{
|
|
Success: true,
|
|
DID: did,
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("load: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("load: loaded database for DID %s", did))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport exec
|
|
func exec() int32 {
|
|
pdk.Log(pdk.LogInfo, "exec: executing action")
|
|
|
|
if !state.IsInitialized() {
|
|
output := types.ExecOutput{Success: false, Error: "database not initialized, call generate or load first"}
|
|
pdk.OutputJSON(output)
|
|
return 0
|
|
}
|
|
|
|
var input types.ExecInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
output := types.ExecOutput{Success: false, Error: fmt.Sprintf("failed to parse input: %s", err)}
|
|
pdk.OutputJSON(output)
|
|
return 0
|
|
}
|
|
|
|
if input.Filter == "" {
|
|
output := types.ExecOutput{Success: false, Error: "filter is required"}
|
|
pdk.OutputJSON(output)
|
|
return 0
|
|
}
|
|
|
|
params, err := parseFilter(input.Filter)
|
|
if err != nil {
|
|
output := types.ExecOutput{Success: false, Error: fmt.Sprintf("invalid filter: %s", err)}
|
|
pdk.OutputJSON(output)
|
|
return 0
|
|
}
|
|
|
|
if input.Token != "" {
|
|
if err := validateUCAN(input.Token, params); err != nil {
|
|
output := types.ExecOutput{
|
|
Success: false,
|
|
Error: fmt.Sprintf("authorization failed: %s", err.Error()),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
}
|
|
|
|
result, err := executeAction(params)
|
|
if err != nil {
|
|
output := types.ExecOutput{
|
|
Success: false,
|
|
Error: err.Error(),
|
|
}
|
|
pdk.OutputJSON(output)
|
|
return 1
|
|
}
|
|
|
|
output := types.ExecOutput{
|
|
Success: true,
|
|
Result: result,
|
|
}
|
|
|
|
pdk.OutputJSON(output)
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("exec: completed %s on %s", params.Action, params.Resource))
|
|
return 0
|
|
}
|
|
|
|
//go:wasmexport query
|
|
func query() int32 {
|
|
pdk.Log(pdk.LogInfo, "query: resolving DID document")
|
|
|
|
if !state.IsInitialized() {
|
|
pdk.SetError(errors.New("database not initialized, call generate or load first"))
|
|
return 1
|
|
}
|
|
|
|
var input types.QueryInput
|
|
if err := pdk.InputJSON(&input); err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to parse input: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if input.DID == "" {
|
|
input.DID = state.GetDID()
|
|
}
|
|
|
|
if !strings.HasPrefix(input.DID, "did:") {
|
|
pdk.SetError(errors.New("query: invalid DID format"))
|
|
return 1
|
|
}
|
|
|
|
output, err := resolveDID(input.DID)
|
|
if err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to resolve DID: %w", err))
|
|
return 1
|
|
}
|
|
|
|
if err := pdk.OutputJSON(output); err != nil {
|
|
pdk.SetError(fmt.Errorf("query: failed to output result: %w", err))
|
|
return 1
|
|
}
|
|
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("query: resolved DID %s", input.DID))
|
|
return 0
|
|
}
|
|
|
|
func initializeDatabase(credentialBytes []byte) (string, error) {
|
|
kb, err := keybase.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("open database: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
did, err := kb.Initialize(ctx, credentialBytes)
|
|
if err != nil {
|
|
return "", fmt.Errorf("initialize: %w", err)
|
|
}
|
|
|
|
pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records")
|
|
return did, nil
|
|
}
|
|
|
|
func serializeDatabase() ([]byte, error) {
|
|
kb := keybase.Get()
|
|
if kb == nil {
|
|
return nil, errors.New("database not initialized")
|
|
}
|
|
return kb.Serialize()
|
|
}
|
|
|
|
func loadDatabase(data []byte) (string, error) {
|
|
if len(data) < 10 {
|
|
return "", errors.New("invalid database format")
|
|
}
|
|
|
|
kb, err := keybase.Open()
|
|
if err != nil {
|
|
return "", fmt.Errorf("open database: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
did, err := kb.Load(ctx, data)
|
|
if err != nil {
|
|
return "", fmt.Errorf("load DID: %w", err)
|
|
}
|
|
|
|
pdk.Log(pdk.LogDebug, "loadDatabase: database loaded successfully")
|
|
return did, nil
|
|
}
|
|
|
|
func parseFilter(filter string) (*types.FilterParams, error) {
|
|
params := &types.FilterParams{}
|
|
parts := strings.FieldsSeq(filter)
|
|
|
|
for part := range parts {
|
|
kv := strings.SplitN(part, ":", 2)
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
|
|
key, value := kv[0], kv[1]
|
|
switch key {
|
|
case "resource":
|
|
params.Resource = value
|
|
case "action":
|
|
params.Action = value
|
|
case "subject":
|
|
params.Subject = value
|
|
}
|
|
}
|
|
|
|
if params.Resource == "" {
|
|
return nil, errors.New("resource is required")
|
|
}
|
|
if params.Action == "" {
|
|
return nil, errors.New("action is required")
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
func validateUCAN(token string, params *types.FilterParams) error {
|
|
if token == "" {
|
|
return errors.New("token is required")
|
|
}
|
|
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 3 {
|
|
return errors.New("invalid token format: expected JWT with 3 parts")
|
|
}
|
|
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid token payload: %w", err)
|
|
}
|
|
|
|
var claims map[string]any
|
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
|
return fmt.Errorf("invalid token claims: %w", err)
|
|
}
|
|
|
|
if exp, ok := claims["exp"].(float64); ok {
|
|
if int64(exp) < currentUnixTime() {
|
|
return errors.New("token has expired")
|
|
}
|
|
}
|
|
|
|
if nbf, ok := claims["nbf"].(float64); ok {
|
|
if int64(nbf) > currentUnixTime() {
|
|
return errors.New("token is not yet valid")
|
|
}
|
|
}
|
|
|
|
if aud, ok := claims["aud"].(string); ok {
|
|
currentDID := state.GetDID()
|
|
if currentDID != "" && aud != currentDID {
|
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: audience mismatch, expected %s got %s", currentDID, aud))
|
|
}
|
|
}
|
|
|
|
if att, ok := claims["att"].([]any); ok {
|
|
if !checkAttenuations(att, params.Resource, params.Action) {
|
|
return fmt.Errorf("token does not grant capability for %s:%s", params.Resource, params.Action)
|
|
}
|
|
}
|
|
|
|
am, err := keybase.NewActionManager()
|
|
if err == nil {
|
|
if cid, ok := claims["cid"].(string); ok {
|
|
ctx := context.Background()
|
|
revoked, err := am.IsDelegationRevoked(ctx, cid)
|
|
if err == nil && revoked {
|
|
return errors.New("token has been revoked")
|
|
}
|
|
}
|
|
}
|
|
|
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("validateUCAN: validated token for %s:%s", params.Resource, params.Action))
|
|
return nil
|
|
}
|
|
|
|
func currentUnixTime() int64 {
|
|
return 0
|
|
}
|
|
|
|
func checkAttenuations(attenuations []any, resource, action string) bool {
|
|
for _, att := range attenuations {
|
|
attMap, ok := att.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
with, ok := attMap["with"].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if !matchResource(with, resource) {
|
|
continue
|
|
}
|
|
|
|
can := attMap["can"]
|
|
if canStr, ok := can.(string); ok {
|
|
if canStr == "*" || canStr == action {
|
|
return true
|
|
}
|
|
} else if canSlice, ok := can.([]any); ok {
|
|
for _, c := range canSlice {
|
|
if cStr, ok := c.(string); ok {
|
|
if cStr == "*" || cStr == action {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchResource(pattern, resource string) bool {
|
|
if pattern == resource {
|
|
return true
|
|
}
|
|
|
|
if strings.HasSuffix(pattern, "/*") {
|
|
prefix := strings.TrimSuffix(pattern, "/*")
|
|
return strings.HasPrefix(resource, prefix)
|
|
}
|
|
|
|
if strings.Contains(pattern, "://") {
|
|
parts := strings.SplitN(pattern, "://", 2)
|
|
if len(parts) == 2 && parts[1] == resource {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func executeAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
switch params.Resource {
|
|
case "accounts":
|
|
return executeAccountAction(params)
|
|
case "credentials":
|
|
return executeCredentialAction(params)
|
|
case "sessions":
|
|
return executeSessionAction(params)
|
|
case "grants":
|
|
return executeGrantAction(params)
|
|
case "key_shares":
|
|
return executeKeyShareAction(params)
|
|
case "ucans":
|
|
return executeUCANAction(params)
|
|
case "delegations":
|
|
return executeDelegationAction(params)
|
|
case "verification_methods":
|
|
return executeVerificationMethodAction(params)
|
|
case "services":
|
|
return executeServiceAction(params)
|
|
default:
|
|
return nil, fmt.Errorf("unknown resource: %s", params.Resource)
|
|
}
|
|
}
|
|
|
|
func executeAccountAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
accounts, err := am.ListAccounts(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list accounts: %w", err)
|
|
}
|
|
return json.Marshal(accounts)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (address) required for get action")
|
|
}
|
|
account, err := am.GetAccountByAddress(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get account: %w", err)
|
|
}
|
|
return json.Marshal(account)
|
|
case "balances":
|
|
return fetchAccountBalances(params.Subject)
|
|
case "sign":
|
|
return json.Marshal(map[string]string{"signature": "placeholder"})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for accounts: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func fetchAccountBalances(address string) (json.RawMessage, error) {
|
|
if address == "" {
|
|
address = state.GetDID()
|
|
}
|
|
|
|
apiBase, ok := state.GetConfig("api_endpoint")
|
|
if !ok {
|
|
apiBase = "https://api.sonr.io"
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s", apiBase, address)
|
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("fetchAccountBalances: GET %s", url))
|
|
|
|
req := pdk.NewHTTPRequest(pdk.MethodGet, url)
|
|
req.SetHeader("Accept", "application/json")
|
|
|
|
res := req.Send()
|
|
status := res.Status()
|
|
|
|
if status < 200 || status >= 300 {
|
|
pdk.Log(pdk.LogError, fmt.Sprintf("fetchAccountBalances: HTTP %d", status))
|
|
return json.Marshal(map[string]any{
|
|
"error": "failed to fetch balances",
|
|
"status": status,
|
|
"address": address,
|
|
})
|
|
}
|
|
|
|
body := res.Body()
|
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("fetchAccountBalances: received %d bytes", len(body)))
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func executeCredentialAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
credentials, err := am.ListCredentials(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list credentials: %w", err)
|
|
}
|
|
return json.Marshal(credentials)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (credential_id) required for get action")
|
|
}
|
|
credential, err := am.GetCredentialByID(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get credential: %w", err)
|
|
}
|
|
return json.Marshal(credential)
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for credentials: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeSessionAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
sessions, err := am.ListSessions(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list sessions: %w", err)
|
|
}
|
|
return json.Marshal(sessions)
|
|
case "revoke":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (session_id) required for revoke action")
|
|
}
|
|
if err := am.RevokeSession(ctx, params.Subject); err != nil {
|
|
return nil, fmt.Errorf("revoke session: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for sessions: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeGrantAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
grants, err := am.ListGrants(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list grants: %w", err)
|
|
}
|
|
return json.Marshal(grants)
|
|
case "revoke":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (grant_id) required for revoke action")
|
|
}
|
|
var grantID int64
|
|
if _, err := fmt.Sscanf(params.Subject, "%d", &grantID); err != nil {
|
|
return nil, fmt.Errorf("invalid grant_id: %w", err)
|
|
}
|
|
if err := am.RevokeGrant(ctx, grantID); err != nil {
|
|
return nil, fmt.Errorf("revoke grant: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for grants: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeKeyShareAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
shares, err := am.ListKeyShares(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list key shares: %w", err)
|
|
}
|
|
return json.Marshal(shares)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (share_id) required for get action")
|
|
}
|
|
share, err := am.GetKeyShareByID(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get key share: %w", err)
|
|
}
|
|
return json.Marshal(share)
|
|
case "rotate":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (share_id) required for rotate action")
|
|
}
|
|
if err := am.RotateKeyShare(ctx, params.Subject); err != nil {
|
|
return nil, fmt.Errorf("rotate key share: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"rotated": true})
|
|
case "archive":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (share_id) required for archive action")
|
|
}
|
|
if err := am.ArchiveKeyShare(ctx, params.Subject); err != nil {
|
|
return nil, fmt.Errorf("archive key share: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"archived": true})
|
|
case "delete":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (share_id) required for delete action")
|
|
}
|
|
if err := am.DeleteKeyShare(ctx, params.Subject); err != nil {
|
|
return nil, fmt.Errorf("delete key share: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"deleted": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for key_shares: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeUCANAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
delegations, err := am.ListDelegations(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list delegations: %w", err)
|
|
}
|
|
return json.Marshal(delegations)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for get action")
|
|
}
|
|
delegation, err := am.GetDelegationByCID(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get delegation: %w", err)
|
|
}
|
|
return json.Marshal(delegation)
|
|
case "revoke":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for revoke action")
|
|
}
|
|
if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{
|
|
DelegationCID: params.Subject,
|
|
RevokedBy: state.GetDID(),
|
|
Reason: "user revoked",
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("revoke delegation: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
case "verify":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for verify action")
|
|
}
|
|
revoked, err := am.IsDelegationRevoked(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("check delegation: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked})
|
|
case "cleanup":
|
|
if err := am.CleanExpiredDelegations(ctx); err != nil {
|
|
return nil, fmt.Errorf("cleanup delegations: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"cleaned": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for ucans: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeDelegationAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (issuer DID) required for list action")
|
|
}
|
|
delegations, err := am.ListDelegationsByIssuer(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list delegations: %w", err)
|
|
}
|
|
return json.Marshal(delegations)
|
|
case "list_received":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (audience DID) required for list_received action")
|
|
}
|
|
delegations, err := am.ListDelegationsByAudience(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list received delegations: %w", err)
|
|
}
|
|
return json.Marshal(delegations)
|
|
case "list_command":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (command) required for list_command action")
|
|
}
|
|
delegations, err := am.ListDelegationsForCommand(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list delegations for command: %w", err)
|
|
}
|
|
return json.Marshal(delegations)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for get action")
|
|
}
|
|
delegation, err := am.GetDelegationByCID(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get delegation: %w", err)
|
|
}
|
|
return json.Marshal(delegation)
|
|
case "revoke":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for revoke action")
|
|
}
|
|
if err := am.RevokeDelegation(ctx, keybase.RevokeDelegationParams{
|
|
DelegationCID: params.Subject,
|
|
RevokedBy: state.GetDID(),
|
|
Reason: "user revoked",
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("revoke delegation: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"revoked": true})
|
|
case "verify":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (cid) required for verify action")
|
|
}
|
|
revoked, err := am.IsDelegationRevoked(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("check delegation: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"valid": !revoked, "revoked": revoked})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for delegations: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func resolveDID(did string) (*types.QueryOutput, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
doc, err := am.ResolveDID(ctx, did)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve DID: %w", err)
|
|
}
|
|
|
|
vms := make([]types.VerificationMethod, len(doc.VerificationMethods))
|
|
for i, vm := range doc.VerificationMethods {
|
|
vms[i] = types.VerificationMethod{
|
|
ID: vm.ID,
|
|
Type: vm.Type,
|
|
Controller: vm.Controller,
|
|
PublicKey: vm.PublicKey,
|
|
Purpose: vm.Purpose,
|
|
}
|
|
}
|
|
|
|
accounts := make([]types.Account, len(doc.Accounts))
|
|
for i, acc := range doc.Accounts {
|
|
accounts[i] = types.Account{
|
|
Address: acc.Address,
|
|
ChainID: acc.ChainID,
|
|
CoinType: int(acc.CoinType),
|
|
AccountIndex: int(acc.AccountIndex),
|
|
AddressIndex: int(acc.AddressIndex),
|
|
Label: acc.Label,
|
|
IsDefault: acc.IsDefault,
|
|
}
|
|
}
|
|
|
|
credentials := make([]types.Credential, len(doc.Credentials))
|
|
for i, cred := range doc.Credentials {
|
|
credentials[i] = types.Credential{
|
|
CredentialID: cred.CredentialID,
|
|
DeviceName: cred.DeviceName,
|
|
DeviceType: cred.DeviceType,
|
|
Authenticator: cred.Authenticator,
|
|
Transports: cred.Transports,
|
|
CreatedAt: cred.CreatedAt,
|
|
LastUsed: cred.LastUsed,
|
|
}
|
|
}
|
|
|
|
return &types.QueryOutput{
|
|
DID: doc.DID,
|
|
Controller: doc.Controller,
|
|
VerificationMethods: vms,
|
|
Accounts: accounts,
|
|
Credentials: credentials,
|
|
}, nil
|
|
}
|
|
|
|
func executeVerificationMethodAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
vms, err := am.ListVerificationMethodsFull(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list verification methods: %w", err)
|
|
}
|
|
return json.Marshal(vms)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (method_id) required for get action")
|
|
}
|
|
vm, err := am.GetVerificationMethod(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get verification method: %w", err)
|
|
}
|
|
return json.Marshal(vm)
|
|
case "delete":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (method_id) required for delete action")
|
|
}
|
|
if err := am.DeleteVerificationMethod(ctx, params.Subject); err != nil {
|
|
return nil, fmt.Errorf("delete verification method: %w", err)
|
|
}
|
|
return json.Marshal(map[string]bool{"deleted": true})
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for verification_methods: %s", params.Action)
|
|
}
|
|
}
|
|
|
|
func executeServiceAction(params *types.FilterParams) (json.RawMessage, error) {
|
|
am, err := keybase.NewActionManager()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("action manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
switch params.Action {
|
|
case "list":
|
|
services, err := am.ListVerifiedServices(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list verified services: %w", err)
|
|
}
|
|
return json.Marshal(services)
|
|
case "get":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (origin) required for get action")
|
|
}
|
|
svc, err := am.GetServiceByOrigin(ctx, params.Subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get service: %w", err)
|
|
}
|
|
return json.Marshal(svc)
|
|
case "get_by_id":
|
|
if params.Subject == "" {
|
|
return nil, errors.New("subject (service_id) required for get_by_id action")
|
|
}
|
|
var serviceID int64
|
|
if _, err := fmt.Sscanf(params.Subject, "%d", &serviceID); err != nil {
|
|
return nil, fmt.Errorf("invalid service_id: %w", err)
|
|
}
|
|
svc, err := am.GetServiceByID(ctx, serviceID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get service by ID: %w", err)
|
|
}
|
|
return json.Marshal(svc)
|
|
default:
|
|
return nil, fmt.Errorf("unknown action for services: %s", params.Action)
|
|
}
|
|
}
|