diff --git a/AGENTS.md b/AGENTS.md index a5e70e9..cf229bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,16 +170,22 @@ if err != nil { ``` motr-enclave/ -├── main.go # Plugin entry point, exported functions -├── db/ -│ ├── schema.sql # Database schema -│ ├── query.sql # SQLC query definitions -│ └── *.go # Generated SQLC code -├── sqlc.yaml # SQLC configuration -├── Makefile # Build commands -└── go.mod # Go module +├── cmd/ +│ └── enclave/ +│ └── main.go # Plugin entry point (WASM-only, go-pdk imports) +├── internal/ +│ ├── keybase/ # Database access layer +│ ├── crypto/ # Cryptographic operations +│ ├── state/ # Plugin state management (WASM-only) +│ ├── types/ # Input/output type definitions +│ └── migrations/ # Database migrations +├── sqlc.yaml # SQLC configuration +├── Makefile # Build commands +└── go.mod # Go module ``` +Note: Files with `//go:build wasip1` constraint (cmd/enclave/, internal/state/) only compile for WASM target. + ## Dependencies Install with `make deps`: diff --git a/Makefile b/Makefile index 609b830..0be29eb 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ deps: build: @echo "Building WASM plugin..." - @GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) . + @GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o $(BUILD_DIR)/$(BINARY) ./cmd/enclave @echo "Built $(BUILD_DIR)/$(BINARY)" sdk: diff --git a/internal/state/state.go b/internal/state/state.go index 98b4c94..7dab026 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -1,3 +1,5 @@ +//go:build wasip1 + // Package state contains the state of the enclave. package state diff --git a/main.go b/main.go deleted file mode 100644 index 8cf697e..0000000 --- a/main.go +++ /dev/null @@ -1,1018 +0,0 @@ -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) - } -}