3 Commits
Author SHA1 Message Date
雷电芽衣 b085947547 chore(go): audit shared plugin helpers
Drop unused expandHome and standardizeJSONC, share the runtime base URL
validator between the guard and audit templates, and trim comments.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:17:26 -04:00
雷电芽衣 5e08ab20f5 chore(go): audit cmd/elydora root
Drop the tautological secret file identity check and the test-only
guard path wrapper.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 01:09:31 -04:00
雷电芽衣 27b720ca69 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
2026-09-03 01:06:42 -04:00
23 changed files with 574 additions and 795 deletions

No files matched your search

+1 -2
View File
@@ -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 {
+11 -97
View File
@@ -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 {
+157 -110
View File
@@ -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)
}
-3
View File
@@ -119,9 +119,6 @@ func readSecretFileWithOpen(
if err := validateSecretFile(before, path, label); err != nil {
return "", err
}
if !os.SameFile(before, before) {
return "", fmt.Errorf("establish %s file identity at %s", label, path)
}
file, err := openFile(path)
if err != nil {
-25
View File
@@ -57,10 +57,6 @@ Run "elydora <command> -h" for details on each command.
`)
}
// ---------------------------------------------------------------------------
// install
// ---------------------------------------------------------------------------
func cmdInstall(args []string) {
if err := rejectLegacySecretArguments(args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -160,10 +156,6 @@ func cmdInstall(args []string) {
fmt.Printf(" Guard script: %s\n", guardScriptPath)
}
// ---------------------------------------------------------------------------
// uninstall
// ---------------------------------------------------------------------------
func cmdUninstall(args []string) {
fs := flag.NewFlagSet("uninstall", flag.ExitOnError)
agent := fs.String("agent", "", "Agent name (required)")
@@ -209,10 +201,6 @@ func cmdUninstall(args []string) {
}
}
// ---------------------------------------------------------------------------
// status
// ---------------------------------------------------------------------------
func cmdStatus(args []string) {
fs := flag.NewFlagSet("status", flag.ExitOnError)
agent := fs.String("agent", "", "Agent name (optional — omit to show all)")
@@ -256,10 +244,6 @@ func showAgentStatus(name string) {
fmt.Printf(" %-14s %-20s %s\n", name, status.DisplayName, state)
}
// ---------------------------------------------------------------------------
// agents
// ---------------------------------------------------------------------------
func cmdAgents() {
fmt.Println("Supported agents:")
fmt.Println()
@@ -282,12 +266,3 @@ func sortedAgentNames() []string {
sort.Strings(names)
return names
}
// guardScriptPathForAgent returns the path to ~/.elydora/<agentId>/guard.js.
func guardScriptPathForAgent(agentId string) (string, error) {
agentDirectory, err := plugins.ResolveAgentRuntimeDirectory(agentId)
if err != nil {
return "", err
}
return filepath.Join(agentDirectory, "guard.js"), nil
}
+4 -2
View File
@@ -5,6 +5,8 @@ import (
"path/filepath"
"strings"
"testing"
"github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2/cmd/elydora/plugins"
)
func setMainTestHome(t *testing.T) string {
@@ -17,9 +19,9 @@ func setMainTestHome(t *testing.T) string {
func TestGuardScriptPathRejectsUnsafeAgentID(t *testing.T) {
setMainTestHome(t)
_, err := guardScriptPathForAgent("../escape")
_, err := plugins.ResolveAgentRuntimeDirectory("../escape")
if err == nil || !strings.Contains(err.Error(), "invalid agent ID") {
t.Fatalf("guardScriptPathForAgent() error = %v", err)
t.Fatalf("ResolveAgentRuntimeDirectory() error = %v", err)
}
}
@@ -344,3 +344,12 @@ func requireNoDroidStagingFiles(t *testing.T, root string) {
t.Fatalf("walk staging files: %v", err)
}
}
func standardizeJSONC(source []byte, label string, allowTrailingCommas bool) ([]byte, error) {
value, err := parseJSONC(source, label, allowTrailingCommas)
if err != nil {
return nil, err
}
value.Standardize()
return value.Pack(), nil
}
+1 -23
View File
@@ -146,30 +146,8 @@ function writeStatusCache(status) {
});
}
function resolveBaseURL(config) {
const value = config.base_url || 'https://api.elydora.com';
if (typeof value !== 'string' || /[\\\u0000-\u0020]/.test(value)) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
let parsed;
try {
parsed = new URL(value);
} catch (error) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error(
'Agent config base_url must exclude credentials, query parameters, and fragments',
);
}
return value.replace(/\/+$/, '');
}
async function resolveRemoteStatus(config) {
const baseURL = resolveBaseURL(config);
const baseURL = resolveConfigBaseURL(config);
const token = config.token || '';
if (typeof token !== 'string') throw new Error('Agent config token must be a string');
const controller = new AbortController();
+3 -46
View File
@@ -13,10 +13,7 @@ func buildHookScriptWithOutput(
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
// ---------------------------------------------------------------------------
// Self-contained Elydora audit hook for: %s
// Generated by @elydora/sdk — DO NOT EDIT
// ---------------------------------------------------------------------------
// Elydora audit hook for %s. Generated by Elydora SDK.
const crypto = require('node:crypto');
const fs = require('node:fs');
@@ -53,10 +50,6 @@ function parsePrivateKey(raw) {
return value;
}
// ---------------------------------------------------------------------------
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
// ---------------------------------------------------------------------------
function base64urlEncode(data) {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
return buf.toString('base64url');
@@ -158,10 +151,6 @@ function generateNonce() {
return base64urlEncode(crypto.randomBytes(16));
}
// ---------------------------------------------------------------------------
// Chain state persistence (atomic writes)
// ---------------------------------------------------------------------------
function readChainState() {
let raw;
try {
@@ -265,10 +254,6 @@ function advanceChainState(fromHash, toHash) {
});
}
// ---------------------------------------------------------------------------
// Error logging
// ---------------------------------------------------------------------------
function logError(err) {
try {
const ts = new Date().toISOString();
@@ -281,32 +266,6 @@ function logError(err) {
}
}
function resolveAuditBaseURL(config) {
const value = config.base_url || 'https://api.elydora.com';
if (typeof value !== 'string' || /[\\\u0000-\u0020]/.test(value)) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
let parsed;
try {
parsed = new URL(value);
} catch (error) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error(
'Agent config base_url must exclude credentials, query parameters, and fragments',
);
}
return value.replace(/\/+$/, '');
}
// ---------------------------------------------------------------------------
// Main hook logic
// ---------------------------------------------------------------------------
async function postOperation(baseUrl, token, eor, deadline) {
const remaining = deadline - performance.now();
if (remaining <= 0) throw new Error('Audit API retry budget exhausted');
@@ -344,7 +303,6 @@ async function postOperation(baseUrl, token, eor, deadline) {
async function main() {
try {
// Read JSON from stdin
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
@@ -380,7 +338,6 @@ async function main() {
hookData.conversation_id || hookData.session_id || hookData.sessionId ||
hookData.session || hookData.taskId || 'unknown';
// Read agent config and private key
let config;
let privateKey;
try {
@@ -408,7 +365,7 @@ async function main() {
const orgId = config.org_id;
const agentId = config.agent_id;
const kid = config.kid;
const baseUrl = resolveAuditBaseURL(config);
const baseUrl = resolveConfigBaseURL(config);
const token = config.token || '';
if (typeof orgId !== 'string' || !orgId || typeof agentId !== 'string' ||
!agentId || typeof kid !== 'string' || !kid) {
@@ -416,7 +373,7 @@ async function main() {
}
if (typeof token !== 'string') throw new Error('Agent config token must be a string');
// Build and submit, retrying from the server chain head on PREV_HASH_MISMATCH
// Retry from the server chain head on PREV_HASH_MISMATCH.
const payload = NATIVE_PAYLOAD ? hookData : {
tool_name: toolName,
tool_input: toolInput,
-9
View File
@@ -17,15 +17,6 @@ type jsoncEditor struct {
label string
}
func standardizeJSONC(source []byte, label string, allowTrailingCommas bool) ([]byte, error) {
value, err := parseJSONC(source, label, allowTrailingCommas)
if err != nil {
return nil, err
}
value.Standardize()
return value.Pack(), nil
}
func parseJSONC(source []byte, label string, allowTrailingCommas bool) (hujson.Value, error) {
value, err := hujson.Parse(source)
if err != nil {
+3 -16
View File
@@ -5,22 +5,9 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
)
// expandHome replaces a leading ~ with the user's home directory.
func expandHome(p string) (string, error) {
if !strings.HasPrefix(p, "~") {
return p, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, p[1:]), nil
}
// readJSONFile reads a JSON file into a map. Returns an empty map if the file does not exist.
// readJSONFile reads a JSON object; a missing file yields an empty map.
func readJSONFile(path string) (map[string]interface{}, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -53,7 +40,7 @@ func writeJSONFile(path string, data map[string]interface{}) error {
return nil
}
// hookScriptPath returns the default path for the hook script inside ~/.elydora/<agentId>/.
// hookScriptPath returns ~/.elydora/<agentId>/hook.js.
func hookScriptPath(agentId string) (string, error) {
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
if err != nil {
@@ -62,7 +49,7 @@ func hookScriptPath(agentId string) (string, error) {
return filepath.Join(agentDirectory, "hook.js"), nil
}
// guardScriptPath returns the default path for the guard script inside ~/.elydora/<agentId>/.
// guardScriptPath returns ~/.elydora/<agentId>/guard.js.
func guardScriptPath(agentId string) (string, error) {
agentDirectory, err := ResolveAgentRuntimeDirectory(agentId)
if err != nil {
+1 -2
View File
@@ -2,8 +2,7 @@ package plugins
import "strings"
// extractElydoraScriptPath supports adapters that still use the legacy shared
// command-hook shape. Provider-specific adapters should define exact ownership.
// extractElydoraScriptPath reads the legacy shared command-hook shape.
func extractElydoraScriptPath(hookArray any) string {
entries, _ := hookArray.([]any)
for _, entry := range entries {
+1 -2
View File
@@ -26,8 +26,7 @@ var SupportedAgents = map[string]AgentRegistryEntry{
"qwen": {Name: "Qwen Code", ConfigDir: "~/.qwen", ConfigFile: "settings.json"},
}
// NewPlugin creates a plugin instance for the given agent name.
// Returns nil if the agent is not supported.
// NewPlugin returns the plugin for an agent name, or nil when unsupported.
func NewPlugin(agentName string) AgentPlugin {
switch agentName {
case "augment":
+2 -4
View File
@@ -22,8 +22,7 @@ type agentRuntimeConfig struct {
AgentName string `json:"agent_name"`
}
// WriteRuntimeFileAtomic replaces one managed runtime file through a flushed,
// same-directory temporary file.
// WriteRuntimeFileAtomic replaces one managed runtime file atomically.
func WriteRuntimeFileAtomic(
path string,
label string,
@@ -40,8 +39,7 @@ func WriteRuntimeFileAtomic(
return writeChanges([]*fileChange{change}, "write "+label, nil)
}
// GenerateHookScript atomically commits the agent config, private key, and
// self-contained Node.js audit runtime.
// GenerateHookScript commits the config, private key, and audit runtime together.
func GenerateHookScript(destPath string, config InstallConfig) error {
return generateHookScriptWithRename(destPath, config, nil)
}
@@ -142,4 +142,26 @@ function writeProtectedJson(filePath, label, value) {
}
}
}
function resolveConfigBaseURL(config) {
const value = config.base_url || 'https://api.elydora.com';
if (typeof value !== 'string' || /[\\\u0000-\u0020]/.test(value)) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
let parsed;
try {
parsed = new URL(value);
} catch (error) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
throw new Error('Agent config base_url must be an absolute HTTP or HTTPS URL');
}
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
throw new Error(
'Agent config base_url must exclude credentials, query parameters, and fragments',
);
}
return value.replace(/\/+$/, '');
}
`
+18 -39
View File
@@ -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
View File
@@ -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
}
+18 -105
View File
@@ -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
View File
@@ -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")
+11 -4
View File
@@ -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
}
+68 -264
View File
@@ -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
View File
@@ -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"`
}
+2 -6
View File
@@ -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