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],
|
['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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user