feat(keybase): add action manager for concurrent database operations
This commit is contained in:
543
internal/keybase/actions.go
Normal file
543
internal/keybase/actions.go
Normal file
@@ -0,0 +1,543 @@
|
||||
// Package keybase provides action handlers for concurrent database operations.
|
||||
package keybase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ActionManager provides thread-safe database operations with concurrent read support.
|
||||
type ActionManager struct {
|
||||
kb *Keybase
|
||||
}
|
||||
|
||||
// NewActionManager creates an ActionManager from the current keybase instance.
|
||||
func NewActionManager() (*ActionManager, error) {
|
||||
kb := Get()
|
||||
if kb == nil {
|
||||
return nil, fmt.Errorf("keybase not initialized")
|
||||
}
|
||||
return &ActionManager{kb: kb}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ACCOUNT ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
// AccountResult represents an account in API responses.
|
||||
type AccountResult struct {
|
||||
ID int64 `json:"id"`
|
||||
Address string `json:"address"`
|
||||
ChainID string `json:"chain_id"`
|
||||
CoinType int64 `json:"coin_type"`
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
AddressIndex int64 `json:"address_index"`
|
||||
Label string `json:"label"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
Curve string `json:"curve,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListAccounts returns all accounts for the current DID.
|
||||
func (am *ActionManager) ListAccounts(ctx context.Context) ([]AccountResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []AccountResult{}, nil
|
||||
}
|
||||
|
||||
rows, err := am.kb.queries.ListAccountsByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
|
||||
results := make([]AccountResult, len(rows))
|
||||
for i, row := range rows {
|
||||
label := ""
|
||||
if row.Label != nil {
|
||||
label = *row.Label
|
||||
}
|
||||
results[i] = AccountResult{
|
||||
ID: row.ID,
|
||||
Address: row.Address,
|
||||
ChainID: row.ChainID,
|
||||
CoinType: row.CoinType,
|
||||
AccountIndex: row.AccountIndex,
|
||||
AddressIndex: row.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: row.IsDefault == 1,
|
||||
PublicKey: row.PublicKey,
|
||||
Curve: row.Curve,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetAccountByAddress retrieves an account by its blockchain address.
|
||||
func (am *ActionManager) GetAccountByAddress(ctx context.Context, address string) (*AccountResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
acc, err := am.kb.queries.GetAccountByAddress(ctx, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get account: %w", err)
|
||||
}
|
||||
|
||||
label := ""
|
||||
if acc.Label != nil {
|
||||
label = *acc.Label
|
||||
}
|
||||
|
||||
return &AccountResult{
|
||||
ID: acc.ID,
|
||||
Address: acc.Address,
|
||||
ChainID: acc.ChainID,
|
||||
CoinType: acc.CoinType,
|
||||
AccountIndex: acc.AccountIndex,
|
||||
AddressIndex: acc.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: acc.IsDefault == 1,
|
||||
CreatedAt: acc.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CREDENTIAL ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
// CredentialResult represents a credential in API responses.
|
||||
type CredentialResult struct {
|
||||
ID int64 `json:"id"`
|
||||
CredentialID string `json:"credential_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
Authenticator string `json:"authenticator"`
|
||||
Transports []string `json:"transports"`
|
||||
SignCount int64 `json:"sign_count"`
|
||||
IsDiscoverable bool `json:"is_discoverable"`
|
||||
BackedUp bool `json:"backed_up"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
LastUsed string `json:"last_used"`
|
||||
}
|
||||
|
||||
// ListCredentials returns all credentials for the current DID.
|
||||
func (am *ActionManager) ListCredentials(ctx context.Context) ([]CredentialResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []CredentialResult{}, nil
|
||||
}
|
||||
|
||||
creds, err := am.kb.queries.ListCredentialsByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list credentials: %w", err)
|
||||
}
|
||||
|
||||
results := make([]CredentialResult, len(creds))
|
||||
for i, cred := range creds {
|
||||
var transports []string
|
||||
if err := json.Unmarshal(cred.Transports, &transports); err != nil {
|
||||
transports = []string{}
|
||||
}
|
||||
|
||||
authenticator := ""
|
||||
if cred.Authenticator != nil {
|
||||
authenticator = *cred.Authenticator
|
||||
}
|
||||
|
||||
results[i] = CredentialResult{
|
||||
ID: cred.ID,
|
||||
CredentialID: cred.CredentialID,
|
||||
DeviceName: cred.DeviceName,
|
||||
DeviceType: cred.DeviceType,
|
||||
Authenticator: authenticator,
|
||||
Transports: transports,
|
||||
SignCount: cred.SignCount,
|
||||
IsDiscoverable: cred.IsDiscoverable == 1,
|
||||
BackedUp: cred.BackedUp == 1,
|
||||
CreatedAt: cred.CreatedAt,
|
||||
LastUsed: cred.LastUsed,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetCredentialByID retrieves a credential by its WebAuthn credential ID.
|
||||
func (am *ActionManager) GetCredentialByID(ctx context.Context, credentialID string) (*CredentialResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
cred, err := am.kb.queries.GetCredentialByID(ctx, credentialID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get credential: %w", err)
|
||||
}
|
||||
|
||||
var transports []string
|
||||
if err := json.Unmarshal(cred.Transports, &transports); err != nil {
|
||||
transports = []string{}
|
||||
}
|
||||
|
||||
authenticator := ""
|
||||
if cred.Authenticator != nil {
|
||||
authenticator = *cred.Authenticator
|
||||
}
|
||||
|
||||
return &CredentialResult{
|
||||
ID: cred.ID,
|
||||
CredentialID: cred.CredentialID,
|
||||
DeviceName: cred.DeviceName,
|
||||
DeviceType: cred.DeviceType,
|
||||
Authenticator: authenticator,
|
||||
Transports: transports,
|
||||
SignCount: cred.SignCount,
|
||||
IsDiscoverable: cred.IsDiscoverable == 1,
|
||||
BackedUp: cred.BackedUp == 1,
|
||||
CreatedAt: cred.CreatedAt,
|
||||
LastUsed: cred.LastUsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SESSION ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
// SessionResult represents a session in API responses.
|
||||
type SessionResult struct {
|
||||
ID int64 `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
Authenticator string `json:"authenticator"`
|
||||
DeviceInfo json.RawMessage `json:"device_info"`
|
||||
IsCurrent bool `json:"is_current"`
|
||||
LastActivity string `json:"last_activity"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListSessions returns all active sessions for the current DID.
|
||||
func (am *ActionManager) ListSessions(ctx context.Context) ([]SessionResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []SessionResult{}, nil
|
||||
}
|
||||
|
||||
sessions, err := am.kb.queries.ListSessionsByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sessions: %w", err)
|
||||
}
|
||||
|
||||
results := make([]SessionResult, len(sessions))
|
||||
for i, sess := range sessions {
|
||||
authenticator := ""
|
||||
if sess.Authenticator != nil {
|
||||
authenticator = *sess.Authenticator
|
||||
}
|
||||
|
||||
results[i] = SessionResult{
|
||||
ID: sess.ID,
|
||||
SessionID: sess.SessionID,
|
||||
DeviceName: sess.DeviceName,
|
||||
Authenticator: authenticator,
|
||||
DeviceInfo: sess.DeviceInfo,
|
||||
IsCurrent: sess.IsCurrent == 1,
|
||||
LastActivity: sess.LastActivity,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
CreatedAt: sess.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// NewSessionParams contains parameters for creating a session.
|
||||
type NewSessionParams struct {
|
||||
CredentialID int64 `json:"credential_id"`
|
||||
DeviceInfo json.RawMessage `json:"device_info"`
|
||||
ExpiresIn time.Duration `json:"expires_in"`
|
||||
}
|
||||
|
||||
// CreateSession creates a new session for the current DID.
|
||||
func (am *ActionManager) CreateSession(ctx context.Context, params NewSessionParams) (*SessionResult, error) {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return nil, fmt.Errorf("DID not initialized")
|
||||
}
|
||||
|
||||
sessionID := generateSessionID()
|
||||
expiresAt := time.Now().Add(params.ExpiresIn).UTC().Format(time.RFC3339)
|
||||
|
||||
deviceInfo := params.DeviceInfo
|
||||
if deviceInfo == nil {
|
||||
deviceInfo = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
sess, err := am.kb.queries.CreateSession(ctx, CreateSessionParams{
|
||||
DidID: am.kb.didID,
|
||||
CredentialID: params.CredentialID,
|
||||
SessionID: sessionID,
|
||||
DeviceInfo: deviceInfo,
|
||||
IsCurrent: 1,
|
||||
ExpiresAt: expiresAt,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create session: %w", err)
|
||||
}
|
||||
|
||||
return &SessionResult{
|
||||
ID: sess.ID,
|
||||
SessionID: sess.SessionID,
|
||||
DeviceInfo: sess.DeviceInfo,
|
||||
IsCurrent: sess.IsCurrent == 1,
|
||||
LastActivity: sess.LastActivity,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
CreatedAt: sess.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateSessionID creates a random session identifier.
|
||||
func generateSessionID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return "sess_" + hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// RevokeSession deletes a session by its ID.
|
||||
func (am *ActionManager) RevokeSession(ctx context.Context, sessionID string) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
sess, err := am.kb.queries.GetSessionByID(ctx, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get session: %w", err)
|
||||
}
|
||||
|
||||
if err := am.kb.queries.DeleteSession(ctx, sess.ID); err != nil {
|
||||
return fmt.Errorf("delete session: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GRANT ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
// GrantResult represents a grant in API responses.
|
||||
type GrantResult struct {
|
||||
ID int64 `json:"id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
ServiceOrigin string `json:"service_origin"`
|
||||
ServiceLogo string `json:"service_logo,omitempty"`
|
||||
Scopes json.RawMessage `json:"scopes"`
|
||||
Accounts json.RawMessage `json:"accounts"`
|
||||
Status string `json:"status"`
|
||||
GrantedAt string `json:"granted_at"`
|
||||
LastUsed string `json:"last_used,omitempty"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
}
|
||||
|
||||
// ListGrants returns all active grants for the current DID.
|
||||
func (am *ActionManager) ListGrants(ctx context.Context) ([]GrantResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
if am.kb.didID == 0 {
|
||||
return []GrantResult{}, nil
|
||||
}
|
||||
|
||||
grants, err := am.kb.queries.ListGrantsByDID(ctx, am.kb.didID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list grants: %w", err)
|
||||
}
|
||||
|
||||
results := make([]GrantResult, len(grants))
|
||||
for i, g := range grants {
|
||||
serviceLogo := ""
|
||||
if g.ServiceLogo != nil {
|
||||
serviceLogo = *g.ServiceLogo
|
||||
}
|
||||
|
||||
lastUsed := ""
|
||||
if g.LastUsed != nil {
|
||||
lastUsed = *g.LastUsed
|
||||
}
|
||||
|
||||
expiresAt := ""
|
||||
if g.ExpiresAt != nil {
|
||||
expiresAt = *g.ExpiresAt
|
||||
}
|
||||
|
||||
results[i] = GrantResult{
|
||||
ID: g.ID,
|
||||
ServiceName: g.ServiceName,
|
||||
ServiceOrigin: g.ServiceOrigin,
|
||||
ServiceLogo: serviceLogo,
|
||||
Scopes: g.Scopes,
|
||||
Accounts: g.Accounts,
|
||||
Status: g.Status,
|
||||
GrantedAt: g.GrantedAt,
|
||||
LastUsed: lastUsed,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// RevokeGrant revokes a grant by its ID.
|
||||
func (am *ActionManager) RevokeGrant(ctx context.Context, grantID int64) error {
|
||||
am.kb.mu.Lock()
|
||||
defer am.kb.mu.Unlock()
|
||||
|
||||
if err := am.kb.queries.RevokeGrant(ctx, grantID); err != nil {
|
||||
return fmt.Errorf("revoke grant: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DID RESOLUTION
|
||||
// =============================================================================
|
||||
|
||||
// VerificationMethodResult represents a verification method in API responses.
|
||||
type VerificationMethodResult struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Controller string `json:"controller"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Purpose string `json:"purpose"`
|
||||
}
|
||||
|
||||
// DIDDocumentResult represents a resolved DID document.
|
||||
type DIDDocumentResult struct {
|
||||
DID string `json:"did"`
|
||||
Controller string `json:"controller"`
|
||||
VerificationMethods []VerificationMethodResult `json:"verification_methods"`
|
||||
Accounts []AccountResult `json:"accounts"`
|
||||
Credentials []CredentialResult `json:"credentials"`
|
||||
}
|
||||
|
||||
// ResolveDID resolves a DID document with all associated data.
|
||||
func (am *ActionManager) ResolveDID(ctx context.Context, did string) (*DIDDocumentResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
defer am.kb.mu.RUnlock()
|
||||
|
||||
// Get DID document
|
||||
doc, err := am.kb.queries.GetDIDByDID(ctx, did)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get DID document: %w", err)
|
||||
}
|
||||
|
||||
// Get verification methods
|
||||
vms, err := am.kb.queries.ListVerificationMethods(ctx, doc.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list verification methods: %w", err)
|
||||
}
|
||||
|
||||
vmResults := make([]VerificationMethodResult, len(vms))
|
||||
for i, vm := range vms {
|
||||
vmResults[i] = VerificationMethodResult{
|
||||
ID: vm.MethodID,
|
||||
Type: vm.MethodType,
|
||||
Controller: vm.Controller,
|
||||
PublicKey: vm.PublicKey,
|
||||
Purpose: vm.Purpose,
|
||||
}
|
||||
}
|
||||
|
||||
// Get accounts
|
||||
accountRows, err := am.kb.queries.ListAccountsByDID(ctx, doc.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
|
||||
accountResults := make([]AccountResult, len(accountRows))
|
||||
for i, row := range accountRows {
|
||||
label := ""
|
||||
if row.Label != nil {
|
||||
label = *row.Label
|
||||
}
|
||||
accountResults[i] = AccountResult{
|
||||
ID: row.ID,
|
||||
Address: row.Address,
|
||||
ChainID: row.ChainID,
|
||||
CoinType: row.CoinType,
|
||||
AccountIndex: row.AccountIndex,
|
||||
AddressIndex: row.AddressIndex,
|
||||
Label: label,
|
||||
IsDefault: row.IsDefault == 1,
|
||||
PublicKey: row.PublicKey,
|
||||
Curve: row.Curve,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Get credentials
|
||||
creds, err := am.kb.queries.ListCredentialsByDID(ctx, doc.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list credentials: %w", err)
|
||||
}
|
||||
|
||||
credResults := make([]CredentialResult, len(creds))
|
||||
for i, cred := range creds {
|
||||
var transports []string
|
||||
if err := json.Unmarshal(cred.Transports, &transports); err != nil {
|
||||
transports = []string{}
|
||||
}
|
||||
|
||||
authenticator := ""
|
||||
if cred.Authenticator != nil {
|
||||
authenticator = *cred.Authenticator
|
||||
}
|
||||
|
||||
credResults[i] = CredentialResult{
|
||||
ID: cred.ID,
|
||||
CredentialID: cred.CredentialID,
|
||||
DeviceName: cred.DeviceName,
|
||||
DeviceType: cred.DeviceType,
|
||||
Authenticator: authenticator,
|
||||
Transports: transports,
|
||||
SignCount: cred.SignCount,
|
||||
IsDiscoverable: cred.IsDiscoverable == 1,
|
||||
BackedUp: cred.BackedUp == 1,
|
||||
CreatedAt: cred.CreatedAt,
|
||||
LastUsed: cred.LastUsed,
|
||||
}
|
||||
}
|
||||
|
||||
return &DIDDocumentResult{
|
||||
DID: doc.Did,
|
||||
Controller: doc.Controller,
|
||||
VerificationMethods: vmResults,
|
||||
Accounts: accountResults,
|
||||
Credentials: credResults,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResolveCurrentDID resolves the current DID document.
|
||||
func (am *ActionManager) ResolveCurrentDID(ctx context.Context) (*DIDDocumentResult, error) {
|
||||
am.kb.mu.RLock()
|
||||
did := am.kb.did
|
||||
am.kb.mu.RUnlock()
|
||||
|
||||
if did == "" {
|
||||
return nil, fmt.Errorf("no DID initialized")
|
||||
}
|
||||
|
||||
return am.ResolveDID(ctx, did)
|
||||
}
|
||||
Reference in New Issue
Block a user