From 76fb6c27cbd2a9d239ed3d6049d70ec02a7d9e5b Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 9 Jan 2026 08:19:21 -0500 Subject: [PATCH] init(enclave): Setup enclave package for wasm actions --- cmd/enclave/main.go | 1020 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1020 insertions(+) create mode 100644 cmd/enclave/main.go diff --git a/cmd/enclave/main.go b/cmd/enclave/main.go new file mode 100644 index 0000000..3e75062 --- /dev/null +++ b/cmd/enclave/main.go @@ -0,0 +1,1020 @@ +//go:build wasip1 + +package main + +import ( + "context" + "encoding/base64" + "encoding/hex" + "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 + } + + result, err := initializeDatabase(credentialBytes, input.KeyShare) + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to initialize database: %w", err)) + return 1 + } + + state.SetInitialized(true) + state.SetDID(result.DID) + + dbBytes, err := serializeDatabase() + if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to serialize database: %w", err)) + return 1 + } + + output := types.GenerateOutput{ + DID: result.DID, + Database: dbBytes, + KeyShareID: result.KeyShareID, + Account: result.Account, + } + + 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", result.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 +} + +type initResult struct { + DID string + KeyShareID string + Account *types.AccountInfo +} + +func initializeDatabase(credentialBytes []byte, keyShareInput *types.KeyShareInput) (*initResult, error) { + kb, err := keybase.Open() + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + ctx := context.Background() + did, err := kb.Initialize(ctx, credentialBytes) + if err != nil { + return nil, fmt.Errorf("initialize: %w", err) + } + + result := &initResult{DID: did} + + if keyShareInput != nil { + keyShareID, account, err := createInitialKeyShare(ctx, keyShareInput) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("initializeDatabase: failed to create keyshare: %s", err)) + } else { + result.KeyShareID = keyShareID + result.Account = account + pdk.Log(pdk.LogInfo, fmt.Sprintf("initializeDatabase: created keyshare %s", keyShareID)) + } + } + + pdk.Log(pdk.LogDebug, "initializeDatabase: created schema and initial records") + return result, nil +} + +func createInitialKeyShare(ctx context.Context, input *types.KeyShareInput) (string, *types.AccountInfo, error) { + am, err := keybase.NewActionManager() + if err != nil { + return "", nil, fmt.Errorf("action manager: %w", err) + } + + ks, err := am.CreateKeyShare(ctx, keybase.NewKeyShareInput{ + KeyID: input.KeyID, + PartyIndex: input.PartyIndex, + Threshold: input.Threshold, + TotalParties: input.TotalParties, + Curve: input.Curve, + ShareData: input.ShareData, + PublicKey: input.PublicKey, + ChainCode: input.ChainCode, + DerivationPath: input.DerivationPath, + }) + if err != nil { + return "", nil, fmt.Errorf("create keyshare: %w", err) + } + + account, err := createInitialAccount(ctx, am, ks.ID, input.PublicKey) + if err != nil { + pdk.Log(pdk.LogWarn, fmt.Sprintf("createInitialKeyShare: failed to create account: %s", err)) + return ks.ShareID, nil, nil + } + + return ks.ShareID, account, nil +} + +func createInitialAccount(ctx context.Context, am *keybase.ActionManager, keyShareID int64, publicKey string) (*types.AccountInfo, error) { + address := deriveCosmosAddress(publicKey) + if address == "" { + return nil, fmt.Errorf("failed to derive address from public key") + } + + acc, err := am.CreateAccount(ctx, keybase.NewAccountInput{ + KeyShareID: keyShareID, + Address: address, + ChainID: "sonr-testnet-1", + CoinType: 118, + AccountIndex: 0, + AddressIndex: 0, + Label: "Default Account", + }) + if err != nil { + return nil, fmt.Errorf("create account: %w", err) + } + + return &types.AccountInfo{ + Address: acc.Address, + ChainID: acc.ChainID, + CoinType: acc.CoinType, + }, nil +} + +func deriveCosmosAddress(publicKeyHex string) string { + if publicKeyHex == "" { + return "" + } + pubBytes, err := hex.DecodeString(publicKeyHex) + if err != nil || len(pubBytes) < 20 { + return "" + } + return fmt.Sprintf("snr1%x", pubBytes[:20]) +} + +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) + } +}