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