Initial commit: Elydora Open Source — self-hosted audit platform for AI agents

Full self-hosted port of the Elydora tamper-evident audit protocol:
- Hono API server with PostgreSQL, Redis, MinIO adapters
- Ed25519 signed operations, chain hashing, Merkle epoch rollups
- PBKDF2 password hashing, HMAC-SHA256 JWT, 5-role RBAC
- Next.js management console
- Node.js, Python, and Go SDKs
- Docker Compose for local deployment
- Helm chart for Kubernetes
- Comprehensive security hardening and audit fixes applied

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-02 13:41:42 -05:00
1 parent 15547a8c35
commit e390e68594
207 files changed
+32790 -63

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
import type {
Agent,
RegisterAgentRequest,
RegisterAgentResponse,
GetAgentResponse,
ListAgentsResponse,
FreezeAgentRequest,
UnfreezeAgentRequest,
RevokeAgentRequest,
UpdateAgentRequest,
SubmitOperationRequest,
SubmitOperationResponse,
GetOperationResponse,
VerifyOperationResponse,
AuditQueryRequest,
AuditQueryResponse,
GetEpochResponse,
CreateExportRequest,
CreateExportResponse,
GetExportResponse,
AuthLoginResponse,
AuthRegisterResponse,
AuthMeResponse,
AuthRefreshResponse,
IssueTokenResponse,
JWKSResponse,
ErrorResponse,
} from '@elydora/shared';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
class ApiError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
public readonly requestId: string,
message: string,
public readonly details?: Record<string, unknown>,
) {
super(message);
this.name = 'ApiError';
}
}
function getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('elydora_token');
}
async function request<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers as Record<string, string> ?? {}),
};
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
headers,
});
if (!response.ok) {
let errorBody: ErrorResponse | null = null;
try {
errorBody = await response.json() as ErrorResponse;
} catch {
// Response body is not JSON
}
if (errorBody?.error) {
throw new ApiError(
response.status,
errorBody.error.code,
errorBody.error.request_id,
errorBody.error.message,
errorBody.error.details,
);
}
throw new ApiError(
response.status,
'INTERNAL_ERROR',
'unknown',
`Request failed with status ${response.status}`,
);
}
return response.json() as Promise<T>;
}
export const api = {
agents: {
list(): Promise<ListAgentsResponse> {
return request<ListAgentsResponse>('/v1/agents');
},
register(body: RegisterAgentRequest): Promise<RegisterAgentResponse> {
return request<RegisterAgentResponse>('/v1/agents/register', {
method: 'POST',
body: JSON.stringify(body),
});
},
get(agentId: string): Promise<GetAgentResponse> {
return request<GetAgentResponse>(`/v1/agents/${encodeURIComponent(agentId)}`);
},
update(agentId: string, body: UpdateAgentRequest): Promise<{ agent: Agent }> {
return request<{ agent: Agent }>(`/v1/agents/${encodeURIComponent(agentId)}`, {
method: 'PATCH',
body: JSON.stringify(body),
});
},
freeze(agentId: string, reason: string): Promise<void> {
const body: FreezeAgentRequest = { reason };
return request<void>(`/v1/agents/${encodeURIComponent(agentId)}/freeze`, {
method: 'POST',
body: JSON.stringify(body),
});
},
unfreeze(agentId: string, reason: string): Promise<void> {
const body: UnfreezeAgentRequest = { reason };
return request<void>(`/v1/agents/${encodeURIComponent(agentId)}/unfreeze`, {
method: 'POST',
body: JSON.stringify(body),
});
},
revokeKey(agentId: string, kid: string, reason: string): Promise<void> {
const body: RevokeAgentRequest = { kid, reason };
return request<void>(`/v1/agents/${encodeURIComponent(agentId)}/revoke`, {
method: 'POST',
body: JSON.stringify(body),
});
},
delete(agentId: string): Promise<void> {
return request<void>(`/v1/agents/${encodeURIComponent(agentId)}`, {
method: 'DELETE',
});
},
},
operations: {
submit(eor: SubmitOperationRequest): Promise<SubmitOperationResponse> {
return request<SubmitOperationResponse>('/v1/operations', {
method: 'POST',
body: JSON.stringify(eor),
});
},
get(operationId: string): Promise<GetOperationResponse> {
return request<GetOperationResponse>(
`/v1/operations/${encodeURIComponent(operationId)}`,
);
},
verify(operationId: string): Promise<VerifyOperationResponse> {
return request<VerifyOperationResponse>(
`/v1/operations/${encodeURIComponent(operationId)}/verify`,
{ method: 'POST' },
);
},
},
audit: {
query(params: AuditQueryRequest): Promise<AuditQueryResponse> {
return request<AuditQueryResponse>(`/v1/audit/query`, {
method: 'POST',
body: JSON.stringify(params),
});
},
},
epochs: {
list(): Promise<{ epochs: Array<{ epoch_id: string; org_id: string; start_time: number; end_time: number; root_hash: string; leaf_count: number; r2_epoch_key: string; created_at: number }> }> {
return request(`/v1/epochs`);
},
get(epochId: string): Promise<GetEpochResponse> {
return request<GetEpochResponse>(`/v1/epochs/${encodeURIComponent(epochId)}`);
},
},
exports: {
create(body: CreateExportRequest): Promise<CreateExportResponse> {
return request<CreateExportResponse>('/v1/exports', {
method: 'POST',
body: JSON.stringify(body),
});
},
get(exportId: string): Promise<GetExportResponse> {
return request<GetExportResponse>(`/v1/exports/${encodeURIComponent(exportId)}`);
},
list(): Promise<{ exports: Array<import('@elydora/shared').Export> }> {
return request(`/v1/exports`);
},
async download(exportId: string): Promise<Blob> {
const token = getAuthToken();
const response = await fetch(
`${API_BASE_URL}/v1/exports/${encodeURIComponent(exportId)}/download`,
{
headers: token ? { Authorization: `Bearer ${token}` } : {},
},
);
if (!response.ok) {
throw new ApiError(response.status, 'DOWNLOAD_ERROR', 'unknown', `Download failed with status ${response.status}`);
}
return response.blob();
},
},
jwks: {
get(): Promise<JWKSResponse> {
return request<JWKSResponse>('/.well-known/elydora/jwks.json');
},
},
auth: {
login(email: string, password: string): Promise<AuthLoginResponse> {
return request<AuthLoginResponse>('/v1/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
},
register(email: string, password: string, displayName: string, orgName: string): Promise<AuthRegisterResponse> {
return request<AuthRegisterResponse>('/v1/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, display_name: displayName, org_name: orgName }),
});
},
me(): Promise<AuthMeResponse> {
return request<AuthMeResponse>('/v1/auth/me');
},
refresh(): Promise<AuthRefreshResponse> {
return request<AuthRefreshResponse>('/v1/auth/refresh', {
method: 'POST',
});
},
issueToken(ttlSeconds: number | null): Promise<IssueTokenResponse> {
return request<IssueTokenResponse>('/v1/auth/token', {
method: 'POST',
body: JSON.stringify({ ttl_seconds: ttlSeconds }),
});
},
},
} as const;
export { ApiError };
export type { ErrorResponse };
+108
View File
@@ -0,0 +1,108 @@
'use client';
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react';
import type { RbacRole } from '@elydora/shared';
interface AuthUser {
sub: string;
org_id: string;
role: RbacRole;
display_name?: string;
email?: string;
}
interface AuthContextValue {
user: AuthUser | null;
token: string | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (token: string, user?: { display_name?: string; email?: string }) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
function decodeJWTPayload(token: string): AuthUser | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
// base64url decode
let base64 = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4 !== 0) base64 += '=';
const json = atob(base64);
const payload = JSON.parse(json);
// Check expiration
const nowSeconds = Math.floor(Date.now() / 1000);
if (typeof payload.exp === 'number' && payload.exp < nowSeconds) return null;
if (!payload.sub || !payload.org_id || !payload.role) return null;
return { sub: payload.sub, org_id: payload.org_id, role: payload.role };
} catch {
return null;
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const stored = localStorage.getItem('elydora_token');
if (stored) {
const decoded = decodeJWTPayload(stored);
if (decoded) {
setToken(stored);
// Try to fetch full user profile from localStorage
const displayName = localStorage.getItem('elydora_display_name');
const email = localStorage.getItem('elydora_email');
if (displayName) decoded.display_name = displayName;
if (email) decoded.email = email;
setUser({ ...decoded });
} else {
// Token expired or invalid, clear it
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
}
}
setIsLoading(false);
}, []);
const login = useCallback((newToken: string, userInfo?: { display_name?: string; email?: string }) => {
localStorage.setItem('elydora_token', newToken);
const decoded = decodeJWTPayload(newToken);
if (decoded) {
if (userInfo?.display_name) {
decoded.display_name = userInfo.display_name;
localStorage.setItem('elydora_display_name', userInfo.display_name);
}
if (userInfo?.email) {
decoded.email = userInfo.email;
localStorage.setItem('elydora_email', userInfo.email);
}
setUser(decoded);
}
setToken(newToken);
}, []);
const logout = useCallback(() => {
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
setUser(null);
setToken(null);
}, []);
return (
<AuthContext.Provider value={{ user, token, isAuthenticated: !!user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
}
+177
View File
@@ -0,0 +1,177 @@
'use client';
import useSWR from 'swr';
import { useEffect, useRef, useState, useCallback } from 'react';
import { api } from './api';
import type {
AuditQueryRequest,
GetAgentResponse,
ListAgentsResponse,
GetOperationResponse,
GetEpochResponse,
GetExportResponse,
AuditQueryResponse,
JWKSResponse,
} from '@elydora/shared';
export function useAgent(agentId: string | undefined) {
return useSWR<GetAgentResponse>(
agentId ? `/agents/${agentId}` : null,
agentId ? () => api.agents.get(agentId) : null,
{ revalidateOnFocus: false },
);
}
export function useAgentsList() {
return useSWR<ListAgentsResponse>(
'/agents',
() => api.agents.list(),
{ revalidateOnFocus: false },
);
}
export function useOperation(operationId: string | undefined) {
return useSWR<GetOperationResponse>(
operationId ? `/operations/${operationId}` : null,
operationId ? () => api.operations.get(operationId) : null,
{ revalidateOnFocus: false },
);
}
export function useAudit(params: AuditQueryRequest) {
return useSWR<AuditQueryResponse>(
['/audit', params],
() => api.audit.query(params),
{ revalidateOnFocus: false },
);
}
export function useEpoch(epochId: string | undefined) {
return useSWR<GetEpochResponse>(
epochId ? `/epochs/${epochId}` : null,
epochId ? () => api.epochs.get(epochId) : null,
{ revalidateOnFocus: false },
);
}
export function useEpochs() {
return useSWR<{ epochs: Array<{ epoch_id: string; org_id: string; start_time: number; end_time: number; root_hash: string; leaf_count: number; r2_epoch_key: string; created_at: number }> }>(
'/epochs',
() => api.epochs.list(),
{ revalidateOnFocus: false },
);
}
export function useExport(exportId: string | undefined) {
return useSWR<GetExportResponse>(
exportId ? `/exports/${exportId}` : null,
exportId ? () => api.exports.get(exportId) : null,
{
revalidateOnFocus: false,
refreshInterval: (data) => {
if (data?.export?.status === 'queued' || data?.export?.status === 'running') {
return 3000;
}
return 0;
},
},
);
}
export function useExports() {
return useSWR<{ exports: Array<import('@elydora/shared').Export> }>(
'/exports',
() => api.exports.list(),
{ revalidateOnFocus: false },
);
}
export function useJWKS() {
return useSWR<JWKSResponse>(
'/jwks',
() => api.jwks.get(),
{ revalidateOnFocus: false },
);
}
export function useFadeIn() {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry?.isIntersecting) {
setIsVisible(true);
observer.unobserve(element);
}
},
{ threshold: 0.1 },
);
observer.observe(element);
return () => observer.disconnect();
}, []);
return { ref, isVisible };
}
export function useClipboard() {
const [copied, setCopied] = useState(false);
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}, []);
return { copied, copy };
}
export function formatTimestamp(ms: number): string {
return new Date(ms).toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
export function formatRelativeTime(ms: number): string {
const now = Date.now();
const diff = now - ms;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return `${seconds}s ago`;
}
export function truncateHash(hash: string, length = 8): string {
if (hash.length <= length * 2) return hash;
return `${hash.slice(0, length)}...${hash.slice(-length)}`;
}