chore(go): audit root package
Share one request, decode, and error path across the client, public endpoints, and export download; align types with the Backend contract and split types_api.go. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
1 parent
f5341ed557
commit
27b720ca69
11 files changed
+528
-663
No files matched your search
+1
-2
@@ -2,8 +2,7 @@ package elydora
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ListAdminEvents retrieves recent administrative events for the organization.
|
||||
// An optional limit (1–100) may be provided; pass 0 to use the server default (20).
|
||||
// ListAdminEvents retrieves recent administrative events; limit 0 uses the server default.
|
||||
func (c *Client) ListAdminEvents(limit int) (*ListAdminEventsResponse, error) {
|
||||
path := "/v1/admin/events"
|
||||
if limit > 0 {
|
||||
|
||||
@@ -1,51 +1,30 @@
|
||||
package elydora
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
// RegisterOption is a functional option for the Register function.
|
||||
// RegisterOption configures a registration request.
|
||||
type RegisterOption func(*AuthRegisterRequest)
|
||||
|
||||
// WithDisplayName sets the display_name on a registration request.
|
||||
func WithDisplayName(name string) RegisterOption {
|
||||
return func(req *AuthRegisterRequest) {
|
||||
req.DisplayName = name
|
||||
}
|
||||
return func(req *AuthRegisterRequest) { req.DisplayName = name }
|
||||
}
|
||||
|
||||
// WithOrgName sets the org_name on a registration request.
|
||||
func WithOrgName(name string) RegisterOption {
|
||||
return func(req *AuthRegisterRequest) {
|
||||
req.OrgName = name
|
||||
}
|
||||
return func(req *AuthRegisterRequest) { req.OrgName = name }
|
||||
}
|
||||
|
||||
// Register creates a new user and organization.
|
||||
//
|
||||
// Deprecated: Use Better Auth endpoints directly. See docs.
|
||||
// Deprecated: use the Console sign-up flow; password registration is being phased out.
|
||||
func Register(baseURL, email, password string, opts ...RegisterOption) (*AuthRegisterResponse, error) {
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
|
||||
req := &AuthRegisterRequest{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}
|
||||
req := &AuthRegisterRequest{Email: email, Password: password}
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
}
|
||||
|
||||
var result AuthRegisterResponse
|
||||
if err := authPost(baseURL, "/v1/auth/register", req, &result); err != nil {
|
||||
if err := publicRequest(http.MethodPost, normalizeBaseURL(baseURL)+"/v1/auth/register", req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
@@ -53,80 +32,16 @@ func Register(baseURL, email, password string, opts ...RegisterOption) (*AuthReg
|
||||
|
||||
// Login authenticates a user and returns a session token.
|
||||
//
|
||||
// Deprecated: Use Better Auth endpoints directly. See docs.
|
||||
// Deprecated: use the Console sign-in flow; password login is being phased out.
|
||||
func Login(baseURL, email, password string) (*AuthLoginResponse, error) {
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
|
||||
req := &AuthLoginRequest{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
req := &AuthLoginRequest{Email: email, Password: password}
|
||||
var result AuthLoginResponse
|
||||
if err := authPost(baseURL, "/v1/auth/login", req, &result); err != nil {
|
||||
if err := publicRequest(http.MethodPost, normalizeBaseURL(baseURL)+"/v1/auth/login", req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// authPost is a helper for unauthenticated POST requests used by Register and Login.
|
||||
func authPost(baseURL, path string, body interface{}, result interface{}) error {
|
||||
u := baseURL + path
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("elydora: marshal request body: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("elydora: create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 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 fmt.Errorf("elydora: read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
if result != nil && len(respBody) > 0 {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
return fmt.Errorf("elydora: unmarshal response: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errResp errorResponse
|
||||
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error.Code != "" {
|
||||
return &ElydoraError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: errResp.Error.Code,
|
||||
Message: errResp.Error.Message,
|
||||
RequestID: errResp.Error.RequestID,
|
||||
Details: errResp.Error.Details,
|
||||
}
|
||||
}
|
||||
|
||||
return &ElydoraError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: ErrorCodeInternalError,
|
||||
Message: string(respBody),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMe retrieves the current authenticated user's profile.
|
||||
func (c *Client) GetMe() (*GetMeResponse, error) {
|
||||
var result GetMeResponse
|
||||
@@ -145,8 +60,7 @@ func (c *Client) IssueApiToken(req *IssueApiTokenRequest) (*IssueApiTokenRespons
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RotateApiToken rotates the current API token and leaves the old token
|
||||
// valid for a 24h grace window.
|
||||
// RotateApiToken rotates the current API token; the old token stays valid for 24h.
|
||||
func (c *Client) RotateApiToken() (*RotateApiTokenResponse, error) {
|
||||
var result RotateApiTokenResponse
|
||||
if err := c.doPost("/v1/auth/rotate", map[string]interface{}{}, &result); err != nil {
|
||||
|
||||
@@ -14,6 +14,8 @@ const (
|
||||
defaultBaseURL = "https://api.elydora.com"
|
||||
defaultTTLMs = 30000
|
||||
defaultMaxRetries = 3
|
||||
maxResponseBytes = 10 << 20
|
||||
requestTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// Config holds the configuration for creating a new Client.
|
||||
@@ -44,135 +46,180 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("elydora: config must not be nil")
|
||||
}
|
||||
|
||||
baseURL := cfg.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
|
||||
ttlMs := cfg.TTLMs
|
||||
if ttlMs <= 0 {
|
||||
ttlMs = defaultTTLMs
|
||||
}
|
||||
|
||||
maxRetries := cfg.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = defaultMaxRetries
|
||||
}
|
||||
|
||||
return &Client{
|
||||
orgID: cfg.OrgID,
|
||||
agentID: cfg.AgentID,
|
||||
privateKey: cfg.PrivateKey,
|
||||
baseURL: baseURL,
|
||||
baseURL: normalizeBaseURL(cfg.BaseURL),
|
||||
ttlMs: ttlMs,
|
||||
maxRetries: maxRetries,
|
||||
token: cfg.Token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
httpClient: &http.Client{Timeout: requestTimeout},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// doRequest executes an HTTP request with retries and error handling.
|
||||
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
|
||||
u := c.baseURL + path
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("elydora: marshal request body: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
// Reset body reader for retry
|
||||
if body != nil {
|
||||
data, _ := json.Marshal(body)
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
time.Sleep(time.Duration(attempt*attempt) * 100 * time.Millisecond)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("elydora: create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if c.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("elydora: http request: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("elydora: read response body: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
if result != nil && len(respBody) > 0 {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
return fmt.Errorf("elydora: unmarshal response: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
var errResp errorResponse
|
||||
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error.Code != "" {
|
||||
apiErr := &ElydoraError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: errResp.Error.Code,
|
||||
Message: errResp.Error.Message,
|
||||
RequestID: errResp.Error.RequestID,
|
||||
Details: errResp.Error.Details,
|
||||
}
|
||||
// Don't retry 4xx errors (except 429)
|
||||
if resp.StatusCode < 500 && resp.StatusCode != 429 {
|
||||
return apiErr
|
||||
}
|
||||
lastErr = apiErr
|
||||
continue
|
||||
}
|
||||
|
||||
lastErr = &ElydoraError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Code: ErrorCodeInternalError,
|
||||
Message: string(respBody),
|
||||
}
|
||||
if resp.StatusCode < 500 && resp.StatusCode != 429 {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// doGet performs a GET request.
|
||||
func (c *Client) doGet(path string, result interface{}) error {
|
||||
return c.doRequest(http.MethodGet, path, nil, result)
|
||||
}
|
||||
|
||||
// doPost performs a POST request.
|
||||
func (c *Client) doPost(path string, body interface{}, result interface{}) error {
|
||||
return c.doRequest(http.MethodPost, path, body, result)
|
||||
}
|
||||
|
||||
// SetToken sets the API token used for authenticated API requests.
|
||||
func (c *Client) SetToken(token string) {
|
||||
c.token = token
|
||||
}
|
||||
|
||||
func normalizeBaseURL(baseURL string) string {
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
return strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
|
||||
func newRequest(method, url string, body []byte, token, accept string) (*http.Request, error) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, url, reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elydora: create request: %w", err)
|
||||
}
|
||||
if accept != "" {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func marshalBody(body interface{}) ([]byte, error) {
|
||||
if body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elydora: marshal request body: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readBody(resp *http.Response, limit int64) ([]byte, error) {
|
||||
defer resp.Body.Close()
|
||||
var reader io.Reader = resp.Body
|
||||
if limit > 0 {
|
||||
reader = io.LimitReader(resp.Body, limit)
|
||||
}
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elydora: read response body: %w", err)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func decodeSuccess(body []byte, result interface{}) error {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(body, result); err != nil {
|
||||
return fmt.Errorf("elydora: unmarshal response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiError(status int, body []byte) *ElydoraError {
|
||||
var errResp errorResponse
|
||||
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Error.Code != "" {
|
||||
return &ElydoraError{
|
||||
StatusCode: status,
|
||||
Code: errResp.Error.Code,
|
||||
Message: errResp.Error.Message,
|
||||
RequestID: errResp.Error.RequestID,
|
||||
Details: errResp.Error.Details,
|
||||
}
|
||||
}
|
||||
return &ElydoraError{StatusCode: status, Code: ErrorCodeInternalError, Message: string(body)}
|
||||
}
|
||||
|
||||
func retryable(status int) bool {
|
||||
return status >= 500 || status == http.StatusTooManyRequests
|
||||
}
|
||||
|
||||
// doRequest executes an authenticated request, retrying transport and 5xx failures.
|
||||
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
|
||||
data, err := marshalBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt*attempt) * 100 * time.Millisecond)
|
||||
}
|
||||
req, err := newRequest(method, c.baseURL+path, data, c.token, "application/json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("elydora: http request: %w", err)
|
||||
continue
|
||||
}
|
||||
respBody, err := readBody(resp, maxResponseBytes)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return decodeSuccess(respBody, result)
|
||||
}
|
||||
lastErr = apiError(resp.StatusCode, respBody)
|
||||
if !retryable(resp.StatusCode) {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *Client) doGet(path string, result interface{}) error {
|
||||
return c.doRequest(http.MethodGet, path, nil, result)
|
||||
}
|
||||
|
||||
func (c *Client) doPost(path string, body interface{}, result interface{}) error {
|
||||
return c.doRequest(http.MethodPost, path, body, result)
|
||||
}
|
||||
|
||||
// publicRequest sends one unauthenticated request and decodes accepted statuses.
|
||||
func publicRequest(method, url string, body interface{}, result interface{}, accepted ...int) error {
|
||||
data, err := marshalBody(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := newRequest(method, url, data, "", "application/json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := (&http.Client{Timeout: requestTimeout}).Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("elydora: http request: %w", err)
|
||||
}
|
||||
respBody, err := readBody(resp, maxResponseBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, status := range accepted {
|
||||
if resp.StatusCode == status {
|
||||
return decodeSuccess(respBody, result)
|
||||
}
|
||||
}
|
||||
if len(accepted) == 0 && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
return decodeSuccess(respBody, result)
|
||||
}
|
||||
return apiError(resp.StatusCode, respBody)
|
||||
}
|
||||
@@ -11,12 +11,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sha256Base64url computes SHA-256 of the input string and returns base64url-encoded result.
|
||||
func sha256Base64url(data string) string {
|
||||
h := sha256.Sum256([]byte(data))
|
||||
return base64urlEncode(h[:])
|
||||
}
|
||||
|
||||
// sha256BytesBase64url computes SHA-256 of raw bytes and returns base64url-encoded result.
|
||||
func sha256BytesBase64url(data []byte) string {
|
||||
h := sha256.Sum256(data)
|
||||
@@ -32,8 +26,7 @@ func computePayloadHash(payload interface{}) (string, error) {
|
||||
return sha256BytesBase64url([]byte(canonical)), nil
|
||||
}
|
||||
|
||||
// signEd25519 signs data with an Ed25519 private key seed (base64url-encoded 32 bytes).
|
||||
// Returns the signature as base64url.
|
||||
// signEd25519 signs data with a base64url Ed25519 seed and returns a base64url signature.
|
||||
func signEd25519(privateKeyBase64url string, data []byte) (string, error) {
|
||||
seed, err := base64urlDecode(privateKeyBase64url)
|
||||
if err != nil {
|
||||
@@ -47,13 +40,7 @@ func signEd25519(privateKeyBase64url string, data []byte) (string, error) {
|
||||
return base64urlEncode(sig), nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JCS Canonicalization (RFC 8785)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// jcsCanonicalise produces a JCS-canonicalized JSON string from a Go value.
|
||||
// Object keys are sorted lexicographically, no whitespace is used, and numbers
|
||||
// follow ES2015 serialization rules.
|
||||
// jcsCanonicalise renders a value as RFC 8785 canonical JSON.
|
||||
func jcsCanonicalise(value interface{}) (string, error) {
|
||||
var b strings.Builder
|
||||
if err := jcsWrite(&b, value); err != nil {
|
||||
@@ -113,10 +100,6 @@ func jcsWrite(b *strings.Builder, value interface{}) error {
|
||||
first := true
|
||||
for _, k := range keys {
|
||||
val := v[k]
|
||||
if val == nil {
|
||||
// JCS includes null values but skips undefined; in Go there is no undefined
|
||||
// so we include null.
|
||||
}
|
||||
if !first {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
@@ -145,7 +128,7 @@ func jcsWrite(b *strings.Builder, value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// jcsWriteString writes a JSON string with minimal escaping per the JSON spec.
|
||||
// jcsWriteString writes a JSON string with minimal escaping.
|
||||
func jcsWriteString(b *strings.Builder, s string) {
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
@@ -175,7 +158,7 @@ func jcsWriteString(b *strings.Builder, s string) {
|
||||
b.WriteByte('"')
|
||||
}
|
||||
|
||||
// jcsFormatNumber formats a float64 according to ES2015 Number serialization (JCS/RFC 8785).
|
||||
// jcsFormatNumber formats a float64 as ES2015 Number serialization.
|
||||
func jcsFormatNumber(f float64) string {
|
||||
if math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return "null"
|
||||
@@ -183,32 +166,28 @@ func jcsFormatNumber(f float64) string {
|
||||
if f == 0 {
|
||||
return "0"
|
||||
}
|
||||
// If the number is an integer that fits in int64, format without decimal point
|
||||
if f == math.Trunc(f) && !math.IsInf(f, 0) && math.Abs(f) < 1e20 {
|
||||
return strconv.FormatInt(int64(f), 10)
|
||||
}
|
||||
// Use the shortest representation that round-trips
|
||||
return strconv.FormatFloat(f, 'G', -1, 64)
|
||||
}
|
||||
|
||||
// signEOR signs an EOR struct. It computes the canonical representation (all fields except signature),
|
||||
// hashes it, and produces an Ed25519 signature.
|
||||
// signEOR signs the canonical EOR fields except the signature.
|
||||
func signEOR(eor *EOR, privateKeyBase64url string) (string, error) {
|
||||
// Build canonical EOR without signature field
|
||||
canonical := map[string]interface{}{
|
||||
"op_version": eor.OpVersion,
|
||||
"operation_id": eor.OperationID,
|
||||
"org_id": eor.OrgID,
|
||||
"agent_id": eor.AgentID,
|
||||
"issued_at": eor.IssuedAt,
|
||||
"ttl_ms": eor.TTLMs,
|
||||
"nonce": eor.Nonce,
|
||||
"operation_type": eor.OperationType,
|
||||
"subject": eor.Subject,
|
||||
"action": eor.Action,
|
||||
"payload": eor.Payload,
|
||||
"payload_hash": eor.PayloadHash,
|
||||
"prev_chain_hash": eor.PrevChainHash,
|
||||
"op_version": eor.OpVersion,
|
||||
"operation_id": eor.OperationID,
|
||||
"org_id": eor.OrgID,
|
||||
"agent_id": eor.AgentID,
|
||||
"issued_at": eor.IssuedAt,
|
||||
"ttl_ms": eor.TTLMs,
|
||||
"nonce": eor.Nonce,
|
||||
"operation_type": eor.OperationType,
|
||||
"subject": eor.Subject,
|
||||
"action": eor.Action,
|
||||
"payload": eor.Payload,
|
||||
"payload_hash": eor.PayloadHash,
|
||||
"prev_chain_hash": eor.PrevChainHash,
|
||||
"agent_pubkey_kid": eor.AgentPubkeyKID,
|
||||
}
|
||||
canonicalStr, err := jcsCanonicalise(canonical)
|
||||
|
||||
+5
-31
@@ -1,9 +1,7 @@
|
||||
package elydora
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -36,44 +34,20 @@ func (c *Client) GetExport(exportID string) (*GetExportResponse, error) {
|
||||
|
||||
// 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)
|
||||
req, err := newRequest(http.MethodGet, c.baseURL+fmt.Sprintf("/v1/exports/%s/download", exportID), nil, c.token, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elydora: create request: %w", err)
|
||||
return nil, 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)
|
||||
body, err := readBody(resp, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elydora: read response body: %w", err)
|
||||
return nil, 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 nil, apiError(resp.StatusCode, body)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -1,116 +1,29 @@
|
||||
package elydora
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
// Health checks the health of the Elydora API. This is a public endpoint
|
||||
// that does not require authentication.
|
||||
// Health checks the public health endpoint.
|
||||
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),
|
||||
var result HealthResponse
|
||||
if err := publicRequest(http.MethodGet, normalizeBaseURL(baseURL)+"/v1/health", nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeepHealth checks the deep health of the Elydora API, including its backend
|
||||
// dependencies (D1, R2, KV). This is a public endpoint that does not require
|
||||
// authentication.
|
||||
// DeepHealth checks backend dependencies; 503 reports a degraded API.
|
||||
func DeepHealth(baseURL string) (*DeepHealthResponse, 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/deep", nil)
|
||||
var result DeepHealthResponse
|
||||
err := publicRequest(
|
||||
http.MethodGet,
|
||||
normalizeBaseURL(baseURL)+"/v1/health/deep",
|
||||
nil,
|
||||
&result,
|
||||
http.StatusOK,
|
||||
http.StatusServiceUnavailable,
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
// Accept both 200 (healthy) and 503 (degraded) as valid deep-health responses.
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusServiceUnavailable {
|
||||
var result DeepHealthResponse
|
||||
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),
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
+2
-5
@@ -2,13 +2,10 @@ package elydora
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GenesisChainHash is the initial chain hash for an agent's first operation.
|
||||
// It is the base64url encoding of 32 zero bytes, matching the backend.
|
||||
// GenesisChainHash is the base64url encoding of 32 zero bytes.
|
||||
const GenesisChainHash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
// CreateOperation builds a signed EOR from the given parameters.
|
||||
// It generates a UUIDv7 operation ID, a nonce, computes the payload hash and chain hash,
|
||||
// and signs the entire record with the client's private key.
|
||||
// CreateOperation builds and signs an EOR locally.
|
||||
func (c *Client) CreateOperation(params *CreateOperationParams) (*EOR, error) {
|
||||
if params == nil {
|
||||
return nil, fmt.Errorf("elydora: params must not be nil")
|
||||
|
||||
@@ -81,8 +81,8 @@ func TestSDKCompatibilityAgainstLatestAPI(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("api token auth failed: %v", err)
|
||||
}
|
||||
if meWithAPI.User.OrgID != login.User.OrgID {
|
||||
t.Fatalf("api token org mismatch: got %s want %s", meWithAPI.User.OrgID, login.User.OrgID)
|
||||
if orgID(meWithAPI.User.OrgID) != login.User.OrgID {
|
||||
t.Fatalf("api token org mismatch: got %s want %s", orgID(meWithAPI.User.OrgID), login.User.OrgID)
|
||||
}
|
||||
|
||||
rotateResp, err := client.RotateApiToken()
|
||||
@@ -98,8 +98,8 @@ func TestSDKCompatibilityAgainstLatestAPI(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("rotated token auth failed: %v", err)
|
||||
}
|
||||
if meWithRotated.User.OrgID != login.User.OrgID {
|
||||
t.Fatalf("rotated token org mismatch: got %s want %s", meWithRotated.User.OrgID, login.User.OrgID)
|
||||
if orgID(meWithRotated.User.OrgID) != login.User.OrgID {
|
||||
t.Fatalf("rotated token org mismatch: got %s want %s", orgID(meWithRotated.User.OrgID), login.User.OrgID)
|
||||
}
|
||||
|
||||
_, err = client.RegisterAgent(&elydora.RegisterAgentRequest{
|
||||
@@ -154,3 +154,10 @@ func TestSDKCompatibilityAgainstLatestAPI(t *testing.T) {
|
||||
t.Fatalf("verify operation invalid: %+v", verify)
|
||||
}
|
||||
}
|
||||
|
||||
func orgID(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package elydora
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type AgentStatus string
|
||||
|
||||
const (
|
||||
@@ -69,7 +65,9 @@ const (
|
||||
ErrorCodeInvalidSignature ErrorCode = "INVALID_SIGNATURE"
|
||||
ErrorCodeUnknownAgent ErrorCode = "UNKNOWN_AGENT"
|
||||
ErrorCodeKeyRevoked ErrorCode = "KEY_REVOKED"
|
||||
ErrorCodeKeyRetired ErrorCode = "KEY_RETIRED"
|
||||
ErrorCodeAgentFrozen ErrorCode = "AGENT_FROZEN"
|
||||
ErrorCodeAgentRevoked ErrorCode = "AGENT_REVOKED"
|
||||
ErrorCodeTTLExpired ErrorCode = "TTL_EXPIRED"
|
||||
ErrorCodeReplayDetected ErrorCode = "REPLAY_DETECTED"
|
||||
ErrorCodePrevHashMismatch ErrorCode = "PREV_HASH_MISMATCH"
|
||||
@@ -82,6 +80,36 @@ const (
|
||||
ErrorCodeValidationError ErrorCode = "VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
type AdminAction string
|
||||
|
||||
const (
|
||||
AdminActionAgentRegister AdminAction = "agent.register"
|
||||
AdminActionAgentUpdate AdminAction = "agent.update"
|
||||
AdminActionAgentFreeze AdminAction = "agent.freeze"
|
||||
AdminActionAgentUnfreeze AdminAction = "agent.unfreeze"
|
||||
AdminActionAgentRevoke AdminAction = "agent.revoke"
|
||||
AdminActionAgentDelete AdminAction = "agent.delete"
|
||||
AdminActionKeyRevoke AdminAction = "key.revoke"
|
||||
AdminActionExportCreate AdminAction = "export.create"
|
||||
AdminActionOrgCreate AdminAction = "org.create"
|
||||
AdminActionOrgUpdate AdminAction = "org.update"
|
||||
AdminActionMemberInvite AdminAction = "member.invite"
|
||||
AdminActionInvitationCancel AdminAction = "invitation.cancel"
|
||||
AdminActionMemberRemove AdminAction = "member.remove"
|
||||
AdminActionMemberRoleChange AdminAction = "member.role_change"
|
||||
AdminActionAgentAssign AdminAction = "agent.assign"
|
||||
AdminActionAgentUnassign AdminAction = "agent.unassign"
|
||||
AdminActionWebhookRegister AdminAction = "webhook.register"
|
||||
AdminActionWebhookDelete AdminAction = "webhook.delete"
|
||||
)
|
||||
|
||||
type WebhookStatus string
|
||||
|
||||
const (
|
||||
WebhookStatusActive WebhookStatus = "active"
|
||||
WebhookStatusDisabled WebhookStatus = "disabled"
|
||||
)
|
||||
|
||||
type ExportFormat string
|
||||
|
||||
const (
|
||||
@@ -89,10 +117,6 @@ const (
|
||||
ExportFormatPDF ExportFormat = "pdf"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entity types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Agent struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
@@ -155,7 +179,7 @@ type Epoch struct {
|
||||
type Organization struct {
|
||||
OrgID string `json:"org_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Description *string `json:"description"`
|
||||
BAOrgID *string `json:"ba_org_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
@@ -182,11 +206,7 @@ type Export struct {
|
||||
CompletedAt *int64 `json:"completed_at"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Protocol types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// EOR is the Elydora Operation Record — the fundamental unit of auditable activity.
|
||||
// EOR is the Elydora Operation Record.
|
||||
type EOR struct {
|
||||
OpVersion string `json:"op_version"`
|
||||
OperationID string `json:"operation_id"`
|
||||
@@ -205,7 +225,7 @@ type EOR struct {
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// EAR is the Elydora Acknowledgment Receipt — server-issued receipt confirming acceptance.
|
||||
// EAR is the Elydora Acknowledgment Receipt.
|
||||
type EAR struct {
|
||||
ReceiptVersion string `json:"receipt_version"`
|
||||
ReceiptID string `json:"receipt_id"`
|
||||
@@ -221,210 +241,6 @@ type EAR struct {
|
||||
ElydoraSignature string `json:"elydora_signature"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API request/response types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type RegisterAgentKeyInput struct {
|
||||
KID string `json:"kid"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
type RegisterAgentRequest struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
ResponsibleEntity string `json:"responsible_entity,omitempty"`
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
Keys []RegisterAgentKeyInput `json:"keys"`
|
||||
}
|
||||
|
||||
type RegisterAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
Keys []AgentKey `json:"keys"`
|
||||
}
|
||||
|
||||
type GetAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
Keys []AgentKey `json:"keys"`
|
||||
}
|
||||
|
||||
type FreezeAgentRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type FreezeAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
PreviousStatus AgentStatus `json:"previous_status"`
|
||||
}
|
||||
|
||||
type RevokeAgentRequest struct {
|
||||
KID string `json:"kid"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UnfreezeAgentRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UnfreezeAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
PreviousStatus AgentStatus `json:"previous_status"`
|
||||
}
|
||||
|
||||
type UpdateAgentRequest struct {
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
}
|
||||
|
||||
type UpdateAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
}
|
||||
|
||||
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"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
type RotateApiTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type GetOperationResponse struct {
|
||||
Operation Operation `json:"operation"`
|
||||
Receipt *Receipt `json:"receipt,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyOperationChecks struct {
|
||||
Signature bool `json:"signature"`
|
||||
Chain bool `json:"chain"`
|
||||
Receipt bool `json:"receipt"`
|
||||
Merkle *bool `json:"merkle,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyOperationResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Checks VerifyOperationChecks `json:"checks"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type AuditQueryRequest struct {
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
OperationType string `json:"operation_type,omitempty"`
|
||||
StartTime *int64 `json:"start_time,omitempty"`
|
||||
EndTime *int64 `json:"end_time,omitempty"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
type AuditQueryResponse struct {
|
||||
Operations []Operation `json:"operations"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
type GetEpochResponse struct {
|
||||
Epoch Epoch `json:"epoch"`
|
||||
Anchor *EpochAnchor `json:"anchor,omitempty"`
|
||||
}
|
||||
|
||||
type EpochAnchor struct {
|
||||
TSAToken string `json:"tsa_token,omitempty"`
|
||||
TSAUrl string `json:"tsa_url,omitempty"`
|
||||
AnchoredAt *int64 `json:"anchored_at,omitempty"`
|
||||
}
|
||||
|
||||
type ListEpochsResponse struct {
|
||||
Epochs []Epoch `json:"epochs"`
|
||||
}
|
||||
|
||||
type CreateExportRequest struct {
|
||||
StartTime int64 `json:"start_time"`
|
||||
EndTime int64 `json:"end_time"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
OperationType string `json:"operation_type,omitempty"`
|
||||
Format ExportFormat `json:"format"`
|
||||
}
|
||||
|
||||
type CreateExportResponse struct {
|
||||
Export Export `json:"export"`
|
||||
}
|
||||
|
||||
type GetExportResponse struct {
|
||||
Export Export `json:"export"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
type ListExportsResponse struct {
|
||||
Exports []Export `json:"exports"`
|
||||
}
|
||||
|
||||
type JWK struct {
|
||||
KTY string `json:"kty"`
|
||||
CRV string `json:"crv,omitempty"`
|
||||
X string `json:"x,omitempty"`
|
||||
KID string `json:"kid"`
|
||||
Use string `json:"use"`
|
||||
Alg string `json:"alg"`
|
||||
}
|
||||
|
||||
type JWKSResponse struct {
|
||||
Keys []JWK `json:"keys"`
|
||||
}
|
||||
|
||||
type AuthRegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
OrgName string `json:"org_name,omitempty"`
|
||||
}
|
||||
|
||||
type AuthRegisterResponse struct {
|
||||
User User `json:"user"`
|
||||
Organization Organization `json:"organization"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type AuthLoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AuthLoginResponse struct {
|
||||
User User `json:"user"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// AdminEvent represents an administrative event log entry.
|
||||
type AdminEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
@@ -449,27 +265,13 @@ type AgentAssignment struct {
|
||||
|
||||
// Webhook represents a registered webhook endpoint.
|
||||
type Webhook struct {
|
||||
WebhookID string `json:"webhook_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
EndpointURL string `json:"endpoint_url"`
|
||||
Events []string `json:"events"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListWebhooksResponse struct {
|
||||
Webhooks []Webhook `json:"webhooks"`
|
||||
}
|
||||
|
||||
type RegisterWebhookRequest struct {
|
||||
EndpointURL string `json:"endpoint_url"`
|
||||
Events []string `json:"events"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type RegisterWebhookResponse struct {
|
||||
Webhook Webhook `json:"webhook"`
|
||||
WebhookID string `json:"webhook_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
EndpointURL string `json:"endpoint_url"`
|
||||
Events []string `json:"events"`
|
||||
Status WebhookStatus `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Member represents a console user returned from the members list endpoint.
|
||||
@@ -482,40 +284,42 @@ type Member struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
type ListMembersResponse struct {
|
||||
Members []Member `json:"members"`
|
||||
// AuthMeUser is the profile returned by GET /v1/auth/me.
|
||||
type AuthMeUser struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrgID *string `json:"org_id"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Role RbacRole `json:"role"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
OnboardingCompleted bool `json:"onboarding_completed"`
|
||||
}
|
||||
|
||||
type ListAdminEventsResponse struct {
|
||||
Events []AdminEvent `json:"events"`
|
||||
// CurrentOrganization is the active organization of the authenticated user.
|
||||
type CurrentOrganization struct {
|
||||
OrgID *string `json:"org_id"`
|
||||
BAOrgID *string `json:"ba_org_id"`
|
||||
Role RbacRole `json:"role"`
|
||||
}
|
||||
|
||||
// DependencyHealth holds the health status of a single backend dependency.
|
||||
// DependencyHealth is the health of one backend dependency.
|
||||
type DependencyHealth struct {
|
||||
Status string `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
Status string `json:"status"`
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
ContractPhase string `json:"contract_phase,omitempty"`
|
||||
}
|
||||
|
||||
// DeepHealthDependencies lists the health of all backend dependencies.
|
||||
// DeepHealthDependencies lists every backend dependency.
|
||||
type DeepHealthDependencies struct {
|
||||
D1 DependencyHealth `json:"d1"`
|
||||
R2 DependencyHealth `json:"r2"`
|
||||
KV DependencyHealth `json:"kv"`
|
||||
D1 DependencyHealth `json:"d1"`
|
||||
AuthStorage DependencyHealth `json:"auth_storage"`
|
||||
R2 DependencyHealth `json:"r2"`
|
||||
KV DependencyHealth `json:"kv"`
|
||||
}
|
||||
|
||||
// DeepHealthResponse is returned by GET /v1/health/deep.
|
||||
type DeepHealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Dependencies DeepHealthDependencies `json:"dependencies"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CreateOperation params (SDK-specific)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// CreateOperationParams are the inputs for CreateOperation.
|
||||
type CreateOperationParams struct {
|
||||
OperationType string
|
||||
Subject map[string]interface{}
|
||||
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package elydora
|
||||
|
||||
type RegisterAgentKeyInput struct {
|
||||
KID string `json:"kid"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
type RegisterAgentRequest struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
ResponsibleEntity string `json:"responsible_entity,omitempty"`
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
Keys []RegisterAgentKeyInput `json:"keys"`
|
||||
}
|
||||
|
||||
type RegisterAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
Keys []AgentKey `json:"keys"`
|
||||
}
|
||||
|
||||
type GetAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
Keys []AgentKey `json:"keys"`
|
||||
}
|
||||
|
||||
type FreezeAgentRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type FreezeAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
PreviousStatus AgentStatus `json:"previous_status"`
|
||||
}
|
||||
|
||||
type RevokeAgentRequest struct {
|
||||
KID string `json:"kid"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UnfreezeAgentRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type UnfreezeAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
PreviousStatus AgentStatus `json:"previous_status"`
|
||||
}
|
||||
|
||||
type UpdateAgentRequest struct {
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
}
|
||||
|
||||
type UpdateAgentResponse struct {
|
||||
Agent Agent `json:"agent"`
|
||||
}
|
||||
|
||||
type ListAgentsResponse struct {
|
||||
Agents []Agent `json:"agents"`
|
||||
}
|
||||
|
||||
type DeleteAgentResponse struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
type GetMeResponse struct {
|
||||
User AuthMeUser `json:"user"`
|
||||
CurrentOrganization CurrentOrganization `json:"current_organization"`
|
||||
}
|
||||
|
||||
type IssueApiTokenRequest struct {
|
||||
TTLSeconds *int `json:"ttl_seconds"`
|
||||
}
|
||||
|
||||
type IssueApiTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
type RotateApiTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt *int64 `json:"expires_at"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
Capabilities map[string]string `json:"capabilities"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type SubmitOperationResponse struct {
|
||||
Receipt EAR `json:"receipt"`
|
||||
}
|
||||
|
||||
type GetOperationResponse struct {
|
||||
Operation Operation `json:"operation"`
|
||||
Receipt *Receipt `json:"receipt,omitempty"`
|
||||
Payload interface{} `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyOperationChecks struct {
|
||||
Signature bool `json:"signature"`
|
||||
Chain bool `json:"chain"`
|
||||
Receipt bool `json:"receipt"`
|
||||
Merkle *bool `json:"merkle,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyOperationResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Checks VerifyOperationChecks `json:"checks"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type AuditQueryRequest struct {
|
||||
OrgID string `json:"org_id,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
OperationType string `json:"operation_type,omitempty"`
|
||||
StartTime *int64 `json:"start_time,omitempty"`
|
||||
EndTime *int64 `json:"end_time,omitempty"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
type AuditQueryResponse struct {
|
||||
Operations []Operation `json:"operations"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
type GetEpochResponse struct {
|
||||
Epoch Epoch `json:"epoch"`
|
||||
Anchor *EpochAnchor `json:"anchor,omitempty"`
|
||||
}
|
||||
|
||||
type EpochAnchor struct {
|
||||
TSAToken string `json:"tsa_token,omitempty"`
|
||||
TSAUrl string `json:"tsa_url,omitempty"`
|
||||
AnchoredAt *int64 `json:"anchored_at,omitempty"`
|
||||
}
|
||||
|
||||
type ListEpochsResponse struct {
|
||||
Epochs []Epoch `json:"epochs"`
|
||||
}
|
||||
|
||||
type CreateExportRequest struct {
|
||||
StartTime int64 `json:"start_time"`
|
||||
EndTime int64 `json:"end_time"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
OperationType string `json:"operation_type,omitempty"`
|
||||
Format ExportFormat `json:"format"`
|
||||
}
|
||||
|
||||
type CreateExportResponse struct {
|
||||
Export Export `json:"export"`
|
||||
}
|
||||
|
||||
type GetExportResponse struct {
|
||||
Export Export `json:"export"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
type ListExportsResponse struct {
|
||||
Exports []Export `json:"exports"`
|
||||
}
|
||||
|
||||
type JWK struct {
|
||||
KTY string `json:"kty"`
|
||||
CRV string `json:"crv,omitempty"`
|
||||
X string `json:"x,omitempty"`
|
||||
KID string `json:"kid"`
|
||||
Use string `json:"use"`
|
||||
Alg string `json:"alg"`
|
||||
}
|
||||
|
||||
type JWKSResponse struct {
|
||||
Keys []JWK `json:"keys"`
|
||||
}
|
||||
|
||||
type AuthRegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
OrgName string `json:"org_name,omitempty"`
|
||||
}
|
||||
|
||||
type AuthRegisterResponse struct {
|
||||
User User `json:"user"`
|
||||
Organization Organization `json:"organization"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type AuthLoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AuthLoginResponse struct {
|
||||
User User `json:"user"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ListWebhooksResponse struct {
|
||||
Webhooks []Webhook `json:"webhooks"`
|
||||
}
|
||||
|
||||
type RegisterWebhookRequest struct {
|
||||
EndpointURL string `json:"endpoint_url"`
|
||||
Events []string `json:"events"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type RegisterWebhookResponse struct {
|
||||
Webhook Webhook `json:"webhook"`
|
||||
}
|
||||
|
||||
type ListMembersResponse struct {
|
||||
Members []Member `json:"members"`
|
||||
}
|
||||
|
||||
type ListAdminEventsResponse struct {
|
||||
Events []AdminEvent `json:"events"`
|
||||
}
|
||||
|
||||
type DeepHealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
Capabilities map[string]string `json:"capabilities"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Dependencies DeepHealthDependencies `json:"dependencies"`
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// base64urlEncode encodes bytes to base64url without padding (RFC 4648 section 5).
|
||||
// base64urlEncode encodes bytes as unpadded base64url.
|
||||
func base64urlEncode(data []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(data)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func base64urlDecode(s string) ([]byte, error) {
|
||||
return base64.RawURLEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// generateNonce generates a 16-byte cryptographically random nonce, base64url encoded.
|
||||
// generateNonce returns 16 random bytes as base64url.
|
||||
func generateNonce() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
@@ -34,19 +34,15 @@ func generateUUIDv7() (string, error) {
|
||||
|
||||
var uuid [16]byte
|
||||
|
||||
// Timestamp: 48 bits in big-endian
|
||||
binary.BigEndian.PutUint16(uuid[0:2], uint16(ms>>32))
|
||||
binary.BigEndian.PutUint16(uuid[2:4], uint16(ms>>16))
|
||||
binary.BigEndian.PutUint16(uuid[4:6], uint16(ms))
|
||||
|
||||
// Fill the rest with random data
|
||||
if _, err := io.ReadFull(rand.Reader, uuid[6:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Set version 7 (bits 48-51)
|
||||
uuid[6] = (uuid[6] & 0x0F) | 0x70
|
||||
// Set variant 10 (bits 64-65)
|
||||
uuid[8] = (uuid[8] & 0x3F) | 0x80
|
||||
|
||||
return formatUUID(uuid), nil
|
||||
|
||||
Reference in new issue
Block a user