From c21e9fcfc6969b4bfed6bc9eda944d4a03c4da75 Mon Sep 17 00:00:00 2001 From: Zachary Date: Sun, 1 Mar 2026 15:53:44 -0500 Subject: [PATCH] Add 7 missing SDK methods and fix README type name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix AgentKeyRequest → RegisterAgentKeyInput in README - Add ListAgents, UnfreezeAgent, DeleteAgent to agents.go - Add GetMe, IssueApiToken to auth.go - Add DownloadExport to exports.go - Add Health package-level function in health.go - Add corresponding types and document all exports in README Co-Authored-By: Claude Opus 4.6 --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++- agents.go | 30 +++++++++++++++++++++++++- auth.go | 18 ++++++++++++++++ exports.go | 51 +++++++++++++++++++++++++++++++++++++++++++- health.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 32 ++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 health.go diff --git a/README.md b/README.md index a86cd28..3d54908 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,13 @@ client, err := elydora.NewClient(&elydora.Config{ }) ``` +### Client Methods + +```go +// Update the JWT token at runtime +client.SetToken("new-jwt-token") +``` + ### Authentication ```go @@ -121,6 +128,14 @@ reg, err := elydora.Register(baseURL, email, password, // Login and receive a JWT auth, err := elydora.Login(baseURL, email, password) + +// Get current authenticated user profile +me, err := client.GetMe() + +// Issue a new API token (with optional TTL) +tokenResp, err := client.IssueApiToken(&elydora.IssueApiTokenRequest{ + TTLSeconds: &ttlSeconds, +}) ``` ### Operations @@ -153,7 +168,7 @@ agent, err := client.RegisterAgent(&elydora.RegisterAgentRequest{ AgentID: "my-agent", DisplayName: "My Agent", ResponsibleEntity: "team@example.com", - Keys: []elydora.AgentKeyRequest{ + Keys: []elydora.RegisterAgentKeyInput{ {KID: "key-v1", PublicKey: "", Algorithm: "ed25519"}, }, }) @@ -166,6 +181,15 @@ err := client.FreezeAgent(agentID, "security review") // Revoke a key err := client.RevokeKey(agentID, kid, "key rotation") + +// List all agents in the organization +agents, err := client.ListAgents() + +// Unfreeze a previously frozen agent +err := client.UnfreezeAgent(agentID, "review complete") + +// Delete an agent permanently +deleted, err := client.DeleteAgent(agentID) ``` ### Audit @@ -198,6 +222,9 @@ export, err := client.CreateExport(&elydora.CreateExportRequest{ exports, err := client.ListExports() detail, err := client.GetExport(exportID) + +// Download export file data +data, err := client.DownloadExport(exportID) ``` ### JWKS @@ -206,6 +233,32 @@ detail, err := client.GetExport(exportID) jwks, err := client.GetJWKS() ``` +### Health + +```go +// Check API health (no authentication required, does not need a client) +health, err := elydora.Health("https://api.elydora.com") +// health.Status, health.Version, health.ProtocolVersion, health.Timestamp +``` + +### Constants + +```go +// Genesis chain hash — initial prev_chain_hash for an agent's first operation +elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +``` + +#### Status Enums + +| Type | Constants | +|------|-----------| +| `AgentStatus` | `AgentStatusActive`, `AgentStatusFrozen`, `AgentStatusRevoked` | +| `KeyStatus` | `KeyStatusActive`, `KeyStatusRetired`, `KeyStatusRevoked` | +| `ExportStatus` | `ExportStatusQueued`, `ExportStatusRunning`, `ExportStatusDone`, `ExportStatusFailed` | +| `ExportFormat` | `ExportFormatJSON`, `ExportFormatPDF` | +| `RbacRole` | `RbacRoleOrgOwner`, `RbacRoleSecurityAdmin`, `RbacRoleComplianceAuditor`, `RbacRoleReadonlyInvestigator`, `RbacRoleIntegrationEngineer` | +| `ErrorCode` | `ErrorCodeInvalidSignature`, `ErrorCodeUnknownAgent`, `ErrorCodeKeyRevoked`, `ErrorCodeAgentFrozen`, `ErrorCodeTTLExpired`, `ErrorCodeReplayDetected`, `ErrorCodePrevHashMismatch`, `ErrorCodePayloadTooLarge`, `ErrorCodeRateLimited`, `ErrorCodeInternalError`, `ErrorCodeUnauthorized`, `ErrorCodeForbidden`, `ErrorCodeNotFound`, `ErrorCodeValidationError` | + ## Error Handling ```go diff --git a/agents.go b/agents.go index 4345b29..37719b1 100644 --- a/agents.go +++ b/agents.go @@ -1,6 +1,9 @@ package elydora -import "fmt" +import ( + "fmt" + "net/http" +) // RegisterAgent registers a new AI agent within the organization. func (c *Client) RegisterAgent(req *RegisterAgentRequest) (*RegisterAgentResponse, error) { @@ -34,3 +37,28 @@ func (c *Client) RevokeKey(agentID, kid, reason string) error { Reason: reason, }, nil) } + +// ListAgents retrieves all agents for the organization. +func (c *Client) ListAgents() (*ListAgentsResponse, error) { + var result ListAgentsResponse + if err := c.doGet("/v1/agents", &result); err != nil { + return nil, err + } + return &result, nil +} + +// UnfreezeAgent unfreezes a previously frozen agent. +func (c *Client) UnfreezeAgent(agentID, reason string) error { + return c.doPost(fmt.Sprintf("/v1/agents/%s/unfreeze", agentID), &UnfreezeAgentRequest{ + Reason: reason, + }, nil) +} + +// DeleteAgent permanently deletes an agent and all associated data. +func (c *Client) DeleteAgent(agentID string) (*DeleteAgentResponse, error) { + var result DeleteAgentResponse + if err := c.doRequest(http.MethodDelete, fmt.Sprintf("/v1/agents/%s", agentID), nil, &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/auth.go b/auth.go index 137e427..c48f30f 100644 --- a/auth.go +++ b/auth.go @@ -122,3 +122,21 @@ func authPost(baseURL, path string, body interface{}, result interface{}) error Message: string(respBody), } } + +// GetMe retrieves the current authenticated user's profile. +func (c *Client) GetMe() (*GetMeResponse, error) { + var result GetMeResponse + if err := c.doGet("/v1/auth/me", &result); err != nil { + return nil, err + } + return &result, nil +} + +// IssueApiToken creates a new API token with an optional TTL. +func (c *Client) IssueApiToken(req *IssueApiTokenRequest) (*IssueApiTokenResponse, error) { + var result IssueApiTokenResponse + if err := c.doPost("/v1/auth/token", req, &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/exports.go b/exports.go index e5f8f77..5985db4 100644 --- a/exports.go +++ b/exports.go @@ -1,6 +1,11 @@ package elydora -import "fmt" +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) // CreateExport creates a new compliance export job. func (c *Client) CreateExport(params *CreateExportRequest) (*CreateExportResponse, error) { @@ -28,3 +33,47 @@ func (c *Client) GetExport(exportID string) (*GetExportResponse, error) { } return &result, nil } + +// DownloadExport downloads the raw file data for a completed export. +func (c *Client) DownloadExport(exportID string) ([]byte, error) { + u := c.baseURL + fmt.Sprintf("/v1/exports/%s/download", exportID) + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, fmt.Errorf("elydora: create request: %w", err) + } + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("elydora: http request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("elydora: read response body: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + var errResp errorResponse + if jsonErr := json.Unmarshal(body, &errResp); jsonErr == nil && errResp.Error.Code != "" { + return nil, &ElydoraError{ + StatusCode: resp.StatusCode, + Code: errResp.Error.Code, + Message: errResp.Error.Message, + RequestID: errResp.Error.RequestID, + Details: errResp.Error.Details, + } + } + return nil, &ElydoraError{ + StatusCode: resp.StatusCode, + Code: ErrorCodeInternalError, + Message: string(body), + } + } + + return body, nil +} diff --git a/health.go b/health.go new file mode 100644 index 0000000..102dd6a --- /dev/null +++ b/health.go @@ -0,0 +1,62 @@ +package elydora + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Health checks the health of the Elydora API. This is a public endpoint +// that does not require authentication. +func Health(baseURL string) (*HealthResponse, error) { + if baseURL == "" { + baseURL = defaultBaseURL + } + baseURL = strings.TrimRight(baseURL, "/") + + client := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequest(http.MethodGet, baseURL+"/v1/health", nil) + if err != nil { + return nil, fmt.Errorf("elydora: create request: %w", err) + } + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("elydora: http request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) + if err != nil { + return nil, fmt.Errorf("elydora: read response body: %w", err) + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + var result HealthResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("elydora: unmarshal response: %w", err) + } + return &result, nil + } + + var errResp errorResponse + if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error.Code != "" { + return nil, &ElydoraError{ + StatusCode: resp.StatusCode, + Code: errResp.Error.Code, + Message: errResp.Error.Message, + RequestID: errResp.Error.RequestID, + Details: errResp.Error.Details, + } + } + + return nil, &ElydoraError{ + StatusCode: resp.StatusCode, + Code: ErrorCodeInternalError, + Message: string(respBody), + } +} diff --git a/types.go b/types.go index 3a4b746..098a923 100644 --- a/types.go +++ b/types.go @@ -230,6 +230,38 @@ type RevokeAgentRequest struct { Reason string `json:"reason"` } +type UnfreezeAgentRequest struct { + Reason string `json:"reason"` +} + +type ListAgentsResponse struct { + Agents []Agent `json:"agents"` +} + +type DeleteAgentResponse struct { + Deleted bool `json:"deleted"` +} + +type GetMeResponse struct { + User User `json:"user"` +} + +type IssueApiTokenRequest struct { + TTLSeconds *int `json:"ttl_seconds"` +} + +type IssueApiTokenResponse struct { + Token string `json:"token"` + ExpiresAt *int64 `json:"expires_at"` +} + +type HealthResponse struct { + Status string `json:"status"` + Version string `json:"version"` + ProtocolVersion string `json:"protocol_version"` + Timestamp int64 `json:"timestamp"` +} + type SubmitOperationResponse struct { Receipt EAR `json:"receipt"` }