551 lines
15 KiB
Go
551 lines
15 KiB
Go
// 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.PublicKeyHex,
|
|
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
|
|
}
|
|
|
|
var deviceInfo json.RawMessage
|
|
if sess.DeviceInfo != nil {
|
|
deviceInfo = json.RawMessage(*sess.DeviceInfo)
|
|
} else {
|
|
deviceInfo = json.RawMessage(`{}`)
|
|
}
|
|
|
|
results[i] = SessionResult{
|
|
ID: sess.ID,
|
|
SessionID: sess.SessionID,
|
|
DeviceName: sess.DeviceName,
|
|
Authenticator: authenticator,
|
|
DeviceInfo: 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: json.RawMessage(g.Scopes),
|
|
Accounts: json.RawMessage(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.PublicKeyHex,
|
|
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)
|
|
}
|