chore(node-sdk): audit src core

Align response types with the Backend (webhooks, me, health, operation payload, error codes, admin actions), shorten comments, drop section banners.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
This commit is contained in:
雷电芽衣 committed 2026-09-02 21:19:01 -04:00
1 parent 2101cb77e3
commit 07f32a48d7
6 files changed
+45 -233

No files matched your search

-18
View File
@@ -50,10 +50,6 @@ const PLUGINS: ReadonlyMap<string, AgentPlugin> = new Map([
['qwen', qwenPlugin], ['qwen', qwenPlugin],
]); ]);
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function die(message: string): never { function die(message: string): never {
console.error(`Error: ${message}`); console.error(`Error: ${message}`);
process.exit(1); process.exit(1);
@@ -137,10 +133,6 @@ Supported agents: ${Array.from(SUPPORTED_AGENTS.keys()).join(', ')}
`); `);
} }
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
async function cmdInstall(args: string[]): Promise<void> { async function cmdInstall(args: string[]): Promise<void> {
const { values } = parseArgs({ const { values } = parseArgs({
args, args,
@@ -178,7 +170,6 @@ async function cmdInstall(args: string[]): Promise<void> {
}); });
const baseUrl = values.base_url ?? 'https://api.elydora.com'; const baseUrl = values.base_url ?? 'https://api.elydora.com';
// Validate private key by deriving public key
let publicKey: string; let publicKey: string;
try { try {
publicKey = derivePublicKey(privateKey); publicKey = derivePublicKey(privateKey);
@@ -255,7 +246,6 @@ async function cmdUninstall(args: string[]): Promise<void> {
let agentDir: string; let agentDir: string;
let agentDirectoryExists: boolean; let agentDirectoryExists: boolean;
// If --agent_id not provided, scan ~/.elydora/*/config.json for matching agent_name
if (agentId) { if (agentId) {
agentDir = resolvePrivateChildDirectory(ELYDORA_DIR, agentId); agentDir = resolvePrivateChildDirectory(ELYDORA_DIR, agentId);
const runtimeRootExists = await requirePhysicalDirectory(ELYDORA_DIR); const runtimeRootExists = await requirePhysicalDirectory(ELYDORA_DIR);
@@ -287,10 +277,8 @@ async function cmdUninstall(args: string[]): Promise<void> {
const plugin = PLUGINS.get(agentName)!; const plugin = PLUGINS.get(agentName)!;
const registryEntry = SUPPORTED_AGENTS.get(agentName)!; const registryEntry = SUPPORTED_AGENTS.get(agentName)!;
// Uninstall agent-specific config
await plugin.uninstall(agentId); await plugin.uninstall(agentId);
// Remove entire agent directory
if (agentDirectoryExists) { if (agentDirectoryExists) {
await fsp.rm(agentDir, { recursive: true }); await fsp.rm(agentDir, { recursive: true });
} }
@@ -303,14 +291,12 @@ async function cmdStatus(): Promise<void> {
let anyInstalled = false; let anyInstalled = false;
// Scan ~/.elydora/*/config.json to discover installed agents
const installedAgents = await discoverInstalledAgents(); const installedAgents = await discoverInstalledAgents();
for (const [name, plugin] of PLUGINS) { for (const [name, plugin] of PLUGINS) {
const st = await plugin.status(); const st = await plugin.status();
const statusIcon = st.installed ? '[installed]' : '[not installed]'; const statusIcon = st.installed ? '[installed]' : '[not installed]';
// Find matching installed agent(s) for this plugin
const matching = installedAgents.filter((a) => a.agentName === name); const matching = installedAgents.filter((a) => a.agentName === name);
console.log(` ${st.displayName} (${name}) ${statusIcon}`); console.log(` ${st.displayName} (${name}) ${statusIcon}`);
@@ -339,10 +325,6 @@ function cmdAgents(): void {
console.log('\nUse "elydora install --agent <name>" to install an audit hook.'); console.log('\nUse "elydora install --agent <name>" to install an audit hook.');
} }
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main(): Promise<void> { async function main(): Promise<void> {
const args = process.argv.slice(2); const args = process.argv.slice(2);
+4 -91
View File
@@ -74,34 +74,19 @@ export class ElydoraClient {
this.prevChainHash = ZERO_CHAIN_HASH; this.prevChainHash = ZERO_CHAIN_HASH;
} }
/**
* Set the API token used for authenticated API calls.
*/
setToken(token: string): void { setToken(token: string): void {
this.token = token; this.token = token;
} }
/**
* Get the current chain hash (useful for debugging/inspection).
*/
getChainHash(): string { getChainHash(): string {
return this.prevChainHash; return this.prevChainHash;
} }
/**
* Get the public key derived from the configured private key.
*/
getPublicKey(): string { getPublicKey(): string {
return derivePublicKey(this.privateKey); return derivePublicKey(this.privateKey);
} }
// ------------------------------------------------------------------------- /** @deprecated Password auth for the SDK CLI path; Console users sign in through Better Auth. */
// Auth (static methods — no instance needed)
// -------------------------------------------------------------------------
/**
* @deprecated Use Better Auth endpoints directly. See docs.
*/
static async register( static async register(
baseUrl: string, baseUrl: string,
email: string, email: string,
@@ -123,9 +108,7 @@ export class ElydoraClient {
return handleResponse<AuthRegisterResponse>(res); return handleResponse<AuthRegisterResponse>(res);
} }
/** /** @deprecated Password auth for the SDK CLI path; Console users sign in through Better Auth. */
* @deprecated Use Better Auth endpoints directly. See docs.
*/
static async login( static async login(
baseUrl: string, baseUrl: string,
email: string, email: string,
@@ -141,10 +124,6 @@ export class ElydoraClient {
return handleResponse<AuthLoginResponse>(res); return handleResponse<AuthLoginResponse>(res);
} }
// -------------------------------------------------------------------------
// Auth (instance methods — require token)
// -------------------------------------------------------------------------
async getMe(): Promise<GetMeResponse> { async getMe(): Promise<GetMeResponse> {
return this.request<GetMeResponse>('GET', '/v1/auth/me'); return this.request<GetMeResponse>('GET', '/v1/auth/me');
} }
@@ -157,10 +136,6 @@ export class ElydoraClient {
return this.request<RotateApiTokenResponse>('POST', '/v1/auth/rotate', {}); return this.request<RotateApiTokenResponse>('POST', '/v1/auth/rotate', {});
} }
// -------------------------------------------------------------------------
// Agent management
// -------------------------------------------------------------------------
async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> { async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> {
assertIntegrationType(request?.integration_type); assertIntegrationType(request?.integration_type);
return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request); return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request);
@@ -195,31 +170,15 @@ export class ElydoraClient {
await this.request<unknown>('POST', `/v1/agents/${encodeURIComponent(agentId)}/revoke`, { kid, reason }); await this.request<unknown>('POST', `/v1/agents/${encodeURIComponent(agentId)}/revoke`, { kid, reason });
} }
// ------------------------------------------------------------------------- // Builds, hashes, chains, and signs an EOR locally, then advances prev_chain_hash.
// Operations (core)
// -------------------------------------------------------------------------
/**
* Construct and sign an Elydora Operation Record (EOR) locally.
*
* 1. Generate UUIDv7 for operation_id
* 2. Generate random nonce (16 bytes, base64url)
* 3. Compute payload_hash (SHA-256 of JCS-canonicalized payload)
* 4. Compute chain_hash from prev_chain_hash
* 5. Construct canonical EOR (minus signature)
* 6. Sign JCS-canonicalized EOR with Ed25519
* 7. Update internal prev_chain_hash
*/
createOperation(params: CreateOperationParams): EOR { createOperation(params: CreateOperationParams): EOR {
const operationId = uuidv7(); const operationId = uuidv7();
const issuedAt = Date.now(); const issuedAt = Date.now();
const nonce = generateNonce(); const nonce = generateNonce();
const payload = params.payload ?? null; const payload = params.payload ?? null;
// Compute payload hash
const payloadHash = computePayloadHash(payload); const payloadHash = computePayloadHash(payload);
// Compute chain hash
const chainHash = computeChainHash( const chainHash = computeChainHash(
this.prevChainHash, this.prevChainHash,
payloadHash, payloadHash,
@@ -227,7 +186,6 @@ export class ElydoraClient {
issuedAt, issuedAt,
); );
// Construct the EOR without signature for signing
const eorWithoutSig: Omit<EOR, 'signature'> = { const eorWithoutSig: Omit<EOR, 'signature'> = {
op_version: '1.0', op_version: '1.0',
operation_id: operationId, operation_id: operationId,
@@ -245,11 +203,9 @@ export class ElydoraClient {
agent_pubkey_kid: this.kid, agent_pubkey_kid: this.kid,
}; };
// Sign the JCS-canonicalized EOR (without signature field)
const canonical = jcsCanonicalise(eorWithoutSig); const canonical = jcsCanonicalise(eorWithoutSig);
const signature = signEd25519(this.privateKey, Buffer.from(canonical, 'utf-8')); const signature = signEd25519(this.privateKey, Buffer.from(canonical, 'utf-8'));
// Update internal chain state
this.prevChainHash = chainHash; this.prevChainHash = chainHash;
return { return {
@@ -270,18 +226,10 @@ export class ElydoraClient {
return this.request<VerifyOperationResponse>('POST', `/v1/operations/${encodeURIComponent(operationId)}/verify`, {}); return this.request<VerifyOperationResponse>('POST', `/v1/operations/${encodeURIComponent(operationId)}/verify`, {});
} }
// -------------------------------------------------------------------------
// Audit
// -------------------------------------------------------------------------
async queryAudit(params: AuditQueryRequest): Promise<AuditQueryResponse> { async queryAudit(params: AuditQueryRequest): Promise<AuditQueryResponse> {
return this.request<AuditQueryResponse>('POST', '/v1/audit/query', params); return this.request<AuditQueryResponse>('POST', '/v1/audit/query', params);
} }
// -------------------------------------------------------------------------
// Epochs
// -------------------------------------------------------------------------
async listEpochs(): Promise<ListEpochsResponse> { async listEpochs(): Promise<ListEpochsResponse> {
return this.request<ListEpochsResponse>('GET', '/v1/epochs'); return this.request<ListEpochsResponse>('GET', '/v1/epochs');
} }
@@ -290,10 +238,6 @@ export class ElydoraClient {
return this.request<GetEpochResponse>('GET', `/v1/epochs/${encodeURIComponent(epochId)}`); return this.request<GetEpochResponse>('GET', `/v1/epochs/${encodeURIComponent(epochId)}`);
} }
// -------------------------------------------------------------------------
// Exports
// -------------------------------------------------------------------------
async createExport(params: CreateExportRequest): Promise<CreateExportResponse> { async createExport(params: CreateExportRequest): Promise<CreateExportResponse> {
return this.request<CreateExportResponse>('POST', '/v1/exports', params); return this.request<CreateExportResponse>('POST', '/v1/exports', params);
} }
@@ -323,10 +267,6 @@ export class ElydoraClient {
return res.arrayBuffer(); return res.arrayBuffer();
} }
// -------------------------------------------------------------------------
// JWKS
// -------------------------------------------------------------------------
async getJWKS(): Promise<JWKSResponse> { async getJWKS(): Promise<JWKSResponse> {
const url = `${this.baseUrl}/.well-known/elydora/jwks.json`; const url = `${this.baseUrl}/.well-known/elydora/jwks.json`;
const res = await fetch(url, { const res = await fetch(url, {
@@ -336,10 +276,6 @@ export class ElydoraClient {
return handleResponse<JWKSResponse>(res); return handleResponse<JWKSResponse>(res);
} }
// -------------------------------------------------------------------------
// Health
// -------------------------------------------------------------------------
async health(): Promise<HealthResponse> { async health(): Promise<HealthResponse> {
const url = `${this.baseUrl}/v1/health`; const url = `${this.baseUrl}/v1/health`;
const res = await fetch(url, { const res = await fetch(url, {
@@ -355,10 +291,6 @@ export class ElydoraClient {
return handleResponse<DeepHealthResponse>(res); return handleResponse<DeepHealthResponse>(res);
} }
// -------------------------------------------------------------------------
// Webhooks
// -------------------------------------------------------------------------
async listWebhooks(): Promise<ListWebhooksResponse> { async listWebhooks(): Promise<ListWebhooksResponse> {
return this.request<ListWebhooksResponse>('GET', '/v1/webhooks'); return this.request<ListWebhooksResponse>('GET', '/v1/webhooks');
} }
@@ -371,27 +303,15 @@ export class ElydoraClient {
await this.request<unknown>('DELETE', `/v1/webhooks/${encodeURIComponent(webhookId)}`); await this.request<unknown>('DELETE', `/v1/webhooks/${encodeURIComponent(webhookId)}`);
} }
// -------------------------------------------------------------------------
// Members
// -------------------------------------------------------------------------
async listMembers(): Promise<ListMembersResponse> { async listMembers(): Promise<ListMembersResponse> {
return this.request<ListMembersResponse>('GET', '/v1/members'); return this.request<ListMembersResponse>('GET', '/v1/members');
} }
// -------------------------------------------------------------------------
// Admin events
// -------------------------------------------------------------------------
async listAdminEvents(limit?: number): Promise<ListAdminEventsResponse> { async listAdminEvents(limit?: number): Promise<ListAdminEventsResponse> {
const query = limit ? `?limit=${limit}` : ''; const query = limit ? `?limit=${limit}` : '';
return this.request<ListAdminEventsResponse>('GET', `/v1/admin/events${query}`); return this.request<ListAdminEventsResponse>('GET', `/v1/admin/events${query}`);
} }
// -------------------------------------------------------------------------
// Internal HTTP helpers
// -------------------------------------------------------------------------
private async request<T>( private async request<T>(
method: string, method: string,
path: string, path: string,
@@ -420,7 +340,6 @@ export class ElydoraClient {
body: body !== undefined ? JSON.stringify(body) : undefined, body: body !== undefined ? JSON.stringify(body) : undefined,
}); });
// Only retry on 429 or 5xx
if (attempt < this.maxRetries && (res.status === 429 || res.status >= 500)) { if (attempt < this.maxRetries && (res.status === 429 || res.status >= 500)) {
const retryAfter = res.headers.get('Retry-After'); const retryAfter = res.headers.get('Retry-After');
const delayMs = retryAfter const delayMs = retryAfter
@@ -434,7 +353,6 @@ export class ElydoraClient {
} catch (err) { } catch (err) {
lastError = err instanceof Error ? err : new Error(String(err)); lastError = err instanceof Error ? err : new Error(String(err));
// Only retry on network errors, not API errors
if (err instanceof ElydoraError) { if (err instanceof ElydoraError) {
throw err; throw err;
} }
@@ -450,13 +368,8 @@ export class ElydoraClient {
} }
} }
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function handleResponse<T>(res: Response): Promise<T> { async function handleResponse<T>(res: Response): Promise<T> {
if (res.ok) { if (res.ok) {
// 204 No Content
if (res.status === 204) { if (res.status === 204) {
return undefined as T; return undefined as T;
} }
@@ -467,7 +380,7 @@ async function handleResponse<T>(res: Response): Promise<T> {
try { try {
errorBody = (await res.json()) as ErrorResponse; errorBody = (await res.json()) as ErrorResponse;
} catch { } catch {
// Response body was not valid JSON // non-JSON error body: fall through to the status-only error
} }
if (errorBody?.error) { if (errorBody?.error) {
+6 -70
View File
@@ -1,18 +1,7 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { base64urlEncode } from './utils.js'; import { base64urlEncode } from './utils.js';
// --------------------------------------------------------------------------- // RFC 8785 JSON Canonicalization Scheme
// JCS Canonicalization (RFC 8785)
// ---------------------------------------------------------------------------
/**
* Canonicalize a JSON value according to JCS (RFC 8785).
*
* - Object keys sorted lexicographically by UTF-16 code units
* - No whitespace
* - Numbers serialized using ES2015 Number serialization
* - Strings serialized with minimal escaping per JSON spec
*/
export function jcsCanonicalise(value: unknown): string { export function jcsCanonicalise(value: unknown): string {
if (value === null || value === undefined) { if (value === null || value === undefined) {
return 'null'; return 'null';
@@ -46,30 +35,13 @@ export function jcsCanonicalise(value: unknown): string {
return JSON.stringify(value); return JSON.stringify(value);
} }
// ---------------------------------------------------------------------------
// SHA-256
// ---------------------------------------------------------------------------
/**
* Compute SHA-256 of a UTF-8 string or buffer and return base64url.
*/
export function sha256Base64url(data: string | Buffer): string { export function sha256Base64url(data: string | Buffer): string {
const input = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data; const input = typeof data === 'string' ? Buffer.from(data, 'utf-8') : data;
const hash = crypto.createHash('sha256').update(input).digest(); const hash = crypto.createHash('sha256').update(input).digest();
return base64urlEncode(hash); return base64urlEncode(hash);
} }
// --------------------------------------------------------------------------- // chain_hash = SHA-256("prev|payload_hash|operation_id|issued_at")
// Chain hash computation
// ---------------------------------------------------------------------------
/**
* Compute the chain hash for an operation.
*
* chain_hash = SHA-256(prev_chain_hash + "|" + payload_hash + "|" + operation_id + "|" + issued_at)
*
* All inputs concatenated as UTF-8 strings separated by '|'.
*/
export function computeChainHash( export function computeChainHash(
prevChainHash: string, prevChainHash: string,
payloadHash: string, payloadHash: string,
@@ -80,13 +52,6 @@ export function computeChainHash(
return sha256Base64url(input); return sha256Base64url(input);
} }
// ---------------------------------------------------------------------------
// Payload hash
// ---------------------------------------------------------------------------
/**
* Compute SHA-256 hash of JCS-canonicalized payload, returned as base64url.
*/
export function computePayloadHash( export function computePayloadHash(
payload: Record<string, unknown> | string | null, payload: Record<string, unknown> | string | null,
): string { ): string {
@@ -94,23 +59,11 @@ export function computePayloadHash(
return sha256Base64url(canonical); return sha256Base64url(canonical);
} }
// --------------------------------------------------------------------------- // PKCS8 envelope: SEQUENCE { INTEGER 0, SEQUENCE { OID 1.3.101.112 }, OCTET STRING { OCTET STRING seed } }
// Ed25519 signing
// ---------------------------------------------------------------------------
// PKCS8 prefix for wrapping a 32-byte Ed25519 seed
const PKCS8_ED25519_PREFIX = Buffer.from([ const PKCS8_ED25519_PREFIX = Buffer.from([
0x30, 0x2e, // SEQUENCE (46 bytes) 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
0x02, 0x01, 0x00, // INTEGER 0
0x30, 0x05, // SEQUENCE (5 bytes)
0x06, 0x03, 0x2b, 0x65, 0x70, // OID 1.3.101.112 (Ed25519)
0x04, 0x22, // OCTET STRING (34 bytes)
0x04, 0x20, // OCTET STRING (32 bytes)
]); ]);
/**
* Import a base64url-encoded 32-byte Ed25519 seed as a Node.js KeyObject.
*/
function importPrivateKey(privateKeyBase64url: string): crypto.KeyObject { function importPrivateKey(privateKeyBase64url: string): crypto.KeyObject {
const seed = Buffer.from(privateKeyBase64url, 'base64url'); const seed = Buffer.from(privateKeyBase64url, 'base64url');
const pkcs8 = Buffer.concat([PKCS8_ED25519_PREFIX, seed]); const pkcs8 = Buffer.concat([PKCS8_ED25519_PREFIX, seed]);
@@ -121,37 +74,20 @@ function importPrivateKey(privateKeyBase64url: string): crypto.KeyObject {
}); });
} }
/**
* Sign data with Ed25519 using a base64url-encoded 32-byte seed.
*
* @returns base64url-encoded 64-byte signature
*/
export function signEd25519(privateKeyBase64url: string, data: Buffer): string { export function signEd25519(privateKeyBase64url: string, data: Buffer): string {
const keyObject = importPrivateKey(privateKeyBase64url); const keyObject = importPrivateKey(privateKeyBase64url);
const signature = crypto.sign(null, data, keyObject); const signature = crypto.sign(null, data, keyObject);
return base64urlEncode(signature); return base64urlEncode(signature);
} }
/**
* Derive the Ed25519 public key from a base64url-encoded 32-byte seed.
*
* @returns base64url-encoded 32-byte public key
*/
export function derivePublicKey(privateKeyBase64url: string): string { export function derivePublicKey(privateKeyBase64url: string): string {
const keyObject = importPrivateKey(privateKeyBase64url); const keyObject = importPrivateKey(privateKeyBase64url);
const publicKeyObject = crypto.createPublicKey(keyObject); const publicKeyObject = crypto.createPublicKey(keyObject);
const rawPublicKey = publicKeyObject.export({ type: 'spki', format: 'der' }); const rawPublicKey = publicKeyObject.export({ type: 'spki', format: 'der' });
// The raw 32-byte public key is the last 32 bytes of the SPKI DER encoding // The raw key is the last 32 bytes of the SPKI DER encoding.
const publicKeyBytes = rawPublicKey.subarray(rawPublicKey.length - 32); const publicKeyBytes = rawPublicKey.subarray(rawPublicKey.length - 32);
return base64urlEncode(publicKeyBytes); return base64urlEncode(publicKeyBytes);
} }
// --------------------------------------------------------------------------- // 32 zero bytes; equals the Backend GENESIS_CHAIN_HASH.
// Zero chain hash (initial prev_chain_hash for the first operation)
// ---------------------------------------------------------------------------
/**
* The initial chain hash value: base64url encoding of 32 zero bytes.
* Must match the backend's GENESIS_CHAIN_HASH constant exactly.
*/
export const ZERO_CHAIN_HASH: string = base64urlEncode(Buffer.alloc(32, 0)); export const ZERO_CHAIN_HASH: string = base64urlEncode(Buffer.alloc(32, 0));
+1 -4
View File
@@ -20,7 +20,6 @@ export {
} from './utils.js'; } from './utils.js';
export type { export type {
// Enums
AgentStatus, AgentStatus,
KeyStatus, KeyStatus,
ExportStatus, ExportStatus,
@@ -29,7 +28,6 @@ export type {
AdminAction, AdminAction,
ErrorCode, ErrorCode,
// Entities
Agent, Agent,
AgentKey, AgentKey,
Operation, Operation,
@@ -41,11 +39,9 @@ export type {
User, User,
Export, Export,
// Protocol
EOR, EOR,
EAR, EAR,
// API request/response
ElydoraClientConfig, ElydoraClientConfig,
CreateOperationParams, CreateOperationParams,
RegisterAgentRequest, RegisterAgentRequest,
@@ -75,6 +71,7 @@ export type {
JWKSResponse, JWKSResponse,
HealthResponse, HealthResponse,
DeepHealthResponse, DeepHealthResponse,
Webhook,
ListWebhooksResponse, ListWebhooksResponse,
RegisterWebhookResponse, RegisterWebhookResponse,
ListMembersResponse, ListMembersResponse,
+33 -31
View File
@@ -1,7 +1,3 @@
// ---------------------------------------------------------------------------
// Enums / status types
// ---------------------------------------------------------------------------
export type AgentStatus = 'active' | 'frozen' | 'revoked'; export type AgentStatus = 'active' | 'frozen' | 'revoked';
export type KeyStatus = 'active' | 'retired' | 'revoked'; export type KeyStatus = 'active' | 'retired' | 'revoked';
export type ExportStatus = 'queued' | 'running' | 'done' | 'failed'; export type ExportStatus = 'queued' | 'running' | 'done' | 'failed';
@@ -37,13 +33,16 @@ export type AdminAction =
| 'agent.register' | 'agent.update' | 'agent.freeze' | 'agent.unfreeze' | 'agent.register' | 'agent.update' | 'agent.freeze' | 'agent.unfreeze'
| 'agent.revoke' | 'agent.delete' | 'key.revoke' | 'export.create' | 'agent.revoke' | 'agent.delete' | 'key.revoke' | 'export.create'
| 'org.create' | 'org.update' | 'org.create' | 'org.update'
| 'member.invite' | 'member.remove' | 'member.role_change' | 'member.invite' | 'invitation.cancel' | 'member.remove' | 'member.role_change'
| 'agent.assign' | 'agent.unassign'; | 'agent.assign' | 'agent.unassign'
| 'webhook.register' | 'webhook.delete';
export type ErrorCode = export type ErrorCode =
| 'INVALID_SIGNATURE' | 'INVALID_SIGNATURE'
| 'UNKNOWN_AGENT' | 'UNKNOWN_AGENT'
| 'KEY_REVOKED' | 'KEY_REVOKED'
| 'KEY_RETIRED'
| 'AGENT_FROZEN' | 'AGENT_FROZEN'
| 'AGENT_REVOKED'
| 'TTL_EXPIRED' | 'TTL_EXPIRED'
| 'REPLAY_DETECTED' | 'REPLAY_DETECTED'
| 'PREV_HASH_MISMATCH' | 'PREV_HASH_MISMATCH'
@@ -55,10 +54,6 @@ export type ErrorCode =
| 'NOT_FOUND' | 'NOT_FOUND'
| 'VALIDATION_ERROR'; | 'VALIDATION_ERROR';
// ---------------------------------------------------------------------------
// Entities
// ---------------------------------------------------------------------------
export interface Agent { export interface Agent {
readonly agent_id: string; readonly agent_id: string;
readonly org_id: string; readonly org_id: string;
@@ -168,10 +163,6 @@ export interface Export {
readonly completed_at: number | null; readonly completed_at: number | null;
} }
// ---------------------------------------------------------------------------
// Protocol types
// ---------------------------------------------------------------------------
export interface EOR { export interface EOR {
readonly op_version: '1.0'; readonly op_version: '1.0';
readonly operation_id: string; readonly operation_id: string;
@@ -205,10 +196,6 @@ export interface EAR {
readonly elydora_signature: string; readonly elydora_signature: string;
} }
// ---------------------------------------------------------------------------
// API request/response types
// ---------------------------------------------------------------------------
export interface RegisterAgentRequest { export interface RegisterAgentRequest {
readonly agent_id: string; readonly agent_id: string;
readonly integration_type: IntegrationType; readonly integration_type: IntegrationType;
@@ -238,6 +225,7 @@ export interface SubmitOperationResponse {
export interface GetOperationResponse { export interface GetOperationResponse {
readonly operation: Operation; readonly operation: Operation;
readonly receipt?: Receipt; readonly receipt?: Receipt;
readonly payload?: Record<string, unknown> | string;
} }
export interface VerifyOperationResponse { export interface VerifyOperationResponse {
@@ -327,15 +315,22 @@ export interface DeleteAgentResponse {
readonly deleted: boolean; readonly deleted: boolean;
} }
export interface Webhook {
readonly webhook_id: string;
readonly org_id: string;
readonly endpoint_url: string;
readonly events: string[];
readonly status: 'active' | 'disabled';
readonly created_at: number;
readonly updated_at: number;
}
export interface ListWebhooksResponse { export interface ListWebhooksResponse {
readonly webhooks: unknown[]; readonly webhooks: Webhook[];
} }
export interface RegisterWebhookResponse { export interface RegisterWebhookResponse {
readonly webhook_id: string; readonly webhook: Webhook;
readonly endpoint_url: string;
readonly events: string[];
readonly created_at: number;
} }
export interface ListMembersResponse { export interface ListMembersResponse {
@@ -357,16 +352,26 @@ export interface DeepHealthResponse {
readonly status: 'healthy' | 'degraded'; readonly status: 'healthy' | 'degraded';
readonly version: string; readonly version: string;
readonly protocol_version: string; readonly protocol_version: string;
readonly capabilities: Record<string, string>;
readonly timestamp: number; readonly timestamp: number;
readonly dependencies: { readonly dependencies: {
readonly d1: { status: 'ok' | 'failed'; latency_ms: number; error?: string }; readonly d1: { status: 'ok' | 'failed'; latency_ms: number };
readonly r2: { status: 'ok' | 'failed'; latency_ms: number; error?: string }; readonly auth_storage: { status: 'ok' | 'failed'; latency_ms: number; contract_phase?: 'expand' | 'contract' };
readonly kv: { status: 'ok' | 'failed'; latency_ms: number; error?: string }; readonly r2: { status: 'ok' | 'failed'; latency_ms: number };
readonly kv: { status: 'ok' | 'failed'; latency_ms: number };
}; };
} }
export interface GetMeResponse { export interface GetMeResponse {
readonly user: User; readonly user: Omit<User, 'org_id'> & {
readonly org_id: string | null;
readonly onboarding_completed: boolean;
};
readonly current_organization: {
readonly org_id: string | null;
readonly ba_org_id: string | null;
readonly role: RbacRole;
};
} }
export interface IssueApiTokenResponse { export interface IssueApiTokenResponse {
@@ -385,6 +390,7 @@ export interface HealthResponse {
readonly status: string; readonly status: string;
readonly version: string; readonly version: string;
readonly protocol_version: string; readonly protocol_version: string;
readonly capabilities: Record<string, string>;
readonly timestamp: number; readonly timestamp: number;
} }
@@ -421,10 +427,6 @@ export interface ErrorResponse {
}; };
} }
// ---------------------------------------------------------------------------
// Client configuration
// ---------------------------------------------------------------------------
export interface ElydoraClientConfig { export interface ElydoraClientConfig {
readonly orgId: string; readonly orgId: string;
readonly agentId: string; readonly agentId: string;
+1 -19
View File
@@ -1,17 +1,10 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
/** // UUIDv7 (RFC 9562): 48-bit ms timestamp, version 7, variant 10, 74 random bits
* Generate a UUIDv7 (time-ordered UUID per RFC 9562).
*
* Layout (128 bits):
* 48-bit Unix timestamp (ms) | 4-bit version (7) | 12-bit rand_a
* 2-bit variant (10) | 62-bit rand_b
*/
export function uuidv7(): string { export function uuidv7(): string {
const now = Date.now(); const now = Date.now();
const random = crypto.randomBytes(10); const random = crypto.randomBytes(10);
// Bytes 0-5: 48-bit timestamp (ms)
const bytes = Buffer.alloc(16); const bytes = Buffer.alloc(16);
bytes[0] = (now / 2 ** 40) & 0xff; bytes[0] = (now / 2 ** 40) & 0xff;
bytes[1] = (now / 2 ** 32) & 0xff; bytes[1] = (now / 2 ** 32) & 0xff;
@@ -20,11 +13,9 @@ export function uuidv7(): string {
bytes[4] = (now / 2 ** 8) & 0xff; bytes[4] = (now / 2 ** 8) & 0xff;
bytes[5] = now & 0xff; bytes[5] = now & 0xff;
// Bytes 6-7: version (0111) + 12 bits random
bytes[6] = 0x70 | (random[0]! & 0x0f); bytes[6] = 0x70 | (random[0]! & 0x0f);
bytes[7] = random[1]!; bytes[7] = random[1]!;
// Bytes 8-15: variant (10) + 62 bits random
bytes[8] = 0x80 | (random[2]! & 0x3f); bytes[8] = 0x80 | (random[2]! & 0x3f);
bytes[9] = random[3]!; bytes[9] = random[3]!;
bytes[10] = random[4]!; bytes[10] = random[4]!;
@@ -44,25 +35,16 @@ export function uuidv7(): string {
); );
} }
/**
* Generate a random nonce: 16 random bytes, base64url encoded.
*/
export function generateNonce(): string { export function generateNonce(): string {
const bytes = crypto.randomBytes(16); const bytes = crypto.randomBytes(16);
return base64urlEncode(bytes); return base64urlEncode(bytes);
} }
/**
* Encode a Buffer or Uint8Array to base64url (no padding, RFC 4648 section 5).
*/
export function base64urlEncode(data: Buffer | Uint8Array): string { export function base64urlEncode(data: Buffer | Uint8Array): string {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
return buf.toString('base64url'); return buf.toString('base64url');
} }
/**
* Decode a base64url string (no padding) to a Buffer.
*/
export function base64urlDecode(input: string): Buffer { export function base64urlDecode(input: string): Buffer {
return Buffer.from(input, 'base64url'); return Buffer.from(input, 'base64url');
} }