fix: enforce atomic agent registration contract
This commit is contained in:
1 parent
47d323c17b
commit
f64d00673c
58 files changed
+2692
-1667
No files matched your search
@@ -31,6 +31,8 @@ yarn-error.log*
|
||||
# Coverage
|
||||
coverage/
|
||||
.nyc_output/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
|
||||
@@ -7,6 +7,7 @@ This repository publishes the open-source server, console, integration catalog,
|
||||
## Sources of Truth
|
||||
|
||||
- `integrations/catalog.json` owns provider IDs, official hook contracts, blocking semantics, release variants, and adapter delivery state.
|
||||
- The server exports that catalog as `INTEGRATION_TYPES`; registration requires an explicit member before any database access.
|
||||
- Official provider documentation owns every external hook API fact.
|
||||
- The standalone SDK repositories own released package behavior. Mirror a reviewed standalone commit here without semantic drift.
|
||||
- Backend schemas own persisted data contracts. Shared types and Console consumers must follow those schemas.
|
||||
@@ -52,6 +53,24 @@ This repository publishes the open-source server, console, integration catalog,
|
||||
|
||||
## Verification
|
||||
|
||||
Server:
|
||||
|
||||
```powershell
|
||||
cd packages/server
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm audit --omit=dev --audit-level=high
|
||||
```
|
||||
|
||||
Console:
|
||||
|
||||
```powershell
|
||||
cd packages/console
|
||||
npm run typecheck
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
Node SDK:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -282,6 +282,7 @@ Register a new agent. Required role: **integration_engineer**.
|
||||
```json
|
||||
{
|
||||
"agent_id": "my-agent-v1",
|
||||
"integration_type": "codex",
|
||||
"display_name": "Customer Support Bot",
|
||||
"responsible_entity": "engineering-team@example.com",
|
||||
"keys": [
|
||||
@@ -318,6 +319,8 @@ Retrieve a specific agent and its keys. Required role: **readonly_investigator**
|
||||
|
||||
Update agent metadata. Required role: **integration_engineer**.
|
||||
|
||||
**Request body:** `{ "integration_type": "codex" }`
|
||||
|
||||
---
|
||||
|
||||
#### `DELETE /v1/agents/:id`
|
||||
@@ -566,6 +569,7 @@ client.setToken(token);
|
||||
// 3. Register the agent with its public key
|
||||
await client.registerAgent({
|
||||
agent_id: 'order-processor-v1',
|
||||
integration_type: 'sdk',
|
||||
display_name: 'Order Processor',
|
||||
keys: [{ kid: 'order-processor-v1-key-v1', public_key: client.getPublicKey(), algorithm: 'ed25519' }],
|
||||
});
|
||||
@@ -627,6 +631,7 @@ client = ElydoraClient(
|
||||
from elydora.crypto import derive_public_key
|
||||
client.register_agent({
|
||||
"agent_id": "order-processor-v1",
|
||||
"integration_type": "sdk",
|
||||
"display_name": "Order Processor",
|
||||
"keys": [{"kid": "order-processor-v1-key-v1",
|
||||
"public_key": derive_public_key(os.environ["AGENT_PRIVATE_KEY"]),
|
||||
|
||||
+23
-1
@@ -121,7 +121,14 @@ services:
|
||||
org_id TEXT NOT NULL REFERENCES organizations (org_id),
|
||||
display_name TEXT NOT NULL,
|
||||
responsible_entity TEXT NOT NULL,
|
||||
integration_type TEXT NOT NULL DEFAULT 'sdk',
|
||||
integration_type TEXT NOT NULL
|
||||
CONSTRAINT agents_integration_type_check
|
||||
CHECK (integration_type IN (
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot',
|
||||
'cursor', 'droid', 'gemini', 'grok', 'kimi', 'kirocli',
|
||||
'kiroide', 'letta', 'opencode', 'qwen', 'enterprise',
|
||||
'gui', 'sdk', 'other'
|
||||
)),
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'frozen', 'revoked')),
|
||||
created_at BIGINT NOT NULL,
|
||||
@@ -130,6 +137,17 @@ services:
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agents_org_id ON agents (org_id);
|
||||
|
||||
ALTER TABLE agents ALTER COLUMN integration_type DROP DEFAULT;
|
||||
ALTER TABLE agents DROP CONSTRAINT IF EXISTS agents_integration_type_check;
|
||||
ALTER TABLE agents
|
||||
ADD CONSTRAINT agents_integration_type_check
|
||||
CHECK (integration_type IN (
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot',
|
||||
'cursor', 'droid', 'gemini', 'grok', 'kimi', 'kirocli',
|
||||
'kiroide', 'letta', 'opencode', 'qwen', 'enterprise',
|
||||
'gui', 'sdk', 'other'
|
||||
));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_keys (
|
||||
kid TEXT NOT NULL PRIMARY KEY,
|
||||
agent_id TEXT NOT NULL REFERENCES agents (agent_id),
|
||||
@@ -216,6 +234,10 @@ services:
|
||||
INSERT INTO schema_versions (version, applied_at, description)
|
||||
VALUES (1, EXTRACT(EPOCH FROM NOW())::BIGINT * 1000, 'Initial schema')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO schema_versions (version, applied_at, description)
|
||||
VALUES (2, EXTRACT(EPOCH FROM NOW())::BIGINT * 1000, 'Require supported agent integration type')
|
||||
ON CONFLICT DO NOTHING;
|
||||
"
|
||||
restart: on-failure
|
||||
|
||||
|
||||
@@ -495,15 +495,15 @@
|
||||
"name": "GUI Agent",
|
||||
"description": "Desktop or browser automation agent"
|
||||
},
|
||||
{
|
||||
"id": "other",
|
||||
"name": "Other",
|
||||
"description": "Custom integration outside the provider catalog"
|
||||
},
|
||||
{
|
||||
"id": "sdk",
|
||||
"name": "SDK",
|
||||
"description": "Direct Elydora SDK integration"
|
||||
},
|
||||
{
|
||||
"id": "other",
|
||||
"name": "Other",
|
||||
"description": "Custom integration outside the provider catalog"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,6 +23,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"validate:integrations": "node --test test/integration-catalog.test.mjs",
|
||||
"test:server": "cd packages/server && npm test",
|
||||
"test:console:e2e": "cd packages/console && npm run test:e2e",
|
||||
"build:server": "cd packages/server && npm run build",
|
||||
"build:console": "cd packages/console && npm run build",
|
||||
"build": "npm run build:server && npm run build:console",
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { buildInstallInstructions, SDK_LANGUAGES } from '@/features/agent-registration/install-instructions';
|
||||
import {
|
||||
INTEGRATION_CATALOG,
|
||||
INTEGRATION_TYPES,
|
||||
} from '@/features/agent-registration/integrations';
|
||||
import {
|
||||
assertRegistrationResponse,
|
||||
RegistrationContractError,
|
||||
} from '@/features/agent-registration/registration-contract';
|
||||
|
||||
const EXPECTED_INTEGRATIONS = [
|
||||
'augment',
|
||||
'claudecode',
|
||||
'cline',
|
||||
'codex',
|
||||
'copilot',
|
||||
'cursor',
|
||||
'droid',
|
||||
'gemini',
|
||||
'grok',
|
||||
'kimi',
|
||||
'kirocli',
|
||||
'kiroide',
|
||||
'letta',
|
||||
'opencode',
|
||||
'qwen',
|
||||
'enterprise',
|
||||
'gui',
|
||||
'sdk',
|
||||
'other',
|
||||
] as const;
|
||||
|
||||
const credentials = {
|
||||
agentId: 'agent-contract',
|
||||
kid: 'agent-contract-key-1',
|
||||
publicKey: 'public-key',
|
||||
privateKey: 'private-key',
|
||||
orgId: 'org-contract',
|
||||
};
|
||||
|
||||
test('keeps the complete integration catalog in canonical order', () => {
|
||||
expect(INTEGRATION_TYPES).toEqual(EXPECTED_INTEGRATIONS);
|
||||
expect(INTEGRATION_CATALOG.map(({ id }) => id)).toEqual(EXPECTED_INTEGRATIONS);
|
||||
expect(new Set(INTEGRATION_CATALOG.map(({ id }) => id)).size).toBe(19);
|
||||
expect(INTEGRATION_CATALOG.filter(({ mode }) => mode === 'adapter')).toHaveLength(15);
|
||||
expect(INTEGRATION_CATALOG.filter(({ mode }) => mode === 'sdk')).toHaveLength(4);
|
||||
});
|
||||
|
||||
test('generates every adapter command with the verified SDK flag contract', () => {
|
||||
for (const integration of INTEGRATION_CATALOG.filter(({ mode }) => mode === 'adapter')) {
|
||||
for (const language of SDK_LANGUAGES) {
|
||||
const instructions = buildInstallInstructions(language, {
|
||||
integration,
|
||||
credentials,
|
||||
token: 'token-contract',
|
||||
baseUrl: 'https://api.example.com',
|
||||
});
|
||||
|
||||
expect(instructions.setup).toContain(`--agent ${integration.id}`);
|
||||
expect(instructions.setup).toContain('"private-key"');
|
||||
expect(instructions.setup).toContain('"token-contract"');
|
||||
expect(instructions.setup).toContain('"https://api.example.com"');
|
||||
|
||||
if (language === 'go') {
|
||||
expect(instructions.setup).toContain('--org-id "org-contract"');
|
||||
expect(instructions.setup).toContain('--agent-id "agent-contract"');
|
||||
expect(instructions.setup).toContain('--private-key "private-key"');
|
||||
expect(instructions.setup).toContain('--base-url "https://api.example.com"');
|
||||
} else {
|
||||
expect(instructions.setup).toContain('--org_id "org-contract"');
|
||||
expect(instructions.setup).toContain('--agent_id "agent-contract"');
|
||||
expect(instructions.setup).toContain('--private_key "private-key"');
|
||||
expect(instructions.setup).toContain('--base_url "https://api.example.com"');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('generates direct SDK setup and operation code for each language', () => {
|
||||
const integration = INTEGRATION_CATALOG.find(({ id }) => id === 'sdk');
|
||||
expect(integration).toBeDefined();
|
||||
|
||||
for (const language of SDK_LANGUAGES) {
|
||||
const instructions = buildInstallInstructions(language, {
|
||||
integration: integration!,
|
||||
credentials,
|
||||
token: 'token-contract',
|
||||
baseUrl: 'https://api.example.com',
|
||||
});
|
||||
expect(instructions.setup).toContain('agent-contract');
|
||||
expect(instructions.setup).toContain('token-contract');
|
||||
expect(instructions.usage).toContain('ai.tool_use');
|
||||
expect(instructions.verify).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('classifies malformed successful responses as blocking contract failures', () => {
|
||||
expect(() => assertRegistrationResponse(
|
||||
null as never,
|
||||
credentials,
|
||||
'grok',
|
||||
)).toThrow(RegistrationContractError);
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:8787';
|
||||
const ORG_ID = 'org-e2e';
|
||||
const TOKEN = 'ely_test_registration_token';
|
||||
|
||||
interface ObservedRequest {
|
||||
readonly method: string;
|
||||
readonly path: string;
|
||||
readonly body: unknown;
|
||||
}
|
||||
|
||||
function json(route: Route, body: unknown, status = 200) {
|
||||
return route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
async function waitForRenderedDialog(page: Page) {
|
||||
await page.getByRole('dialog').evaluate(async (dialog) => {
|
||||
await Promise.all(dialog.getAnimations().map((animation) => animation.finished));
|
||||
});
|
||||
}
|
||||
|
||||
async function mockBackend(
|
||||
page: Page,
|
||||
observed: ObservedRequest[],
|
||||
unexpected: string[],
|
||||
registrationIntegrationOverride?: string,
|
||||
) {
|
||||
await page.route(`${API_BASE_URL}/**`, async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname;
|
||||
const method = request.method();
|
||||
const body = request.postDataJSON() as unknown;
|
||||
observed.push({ method, path, body });
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
await route.fulfill({ status: 204 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && path === '/api/auth/get-session') {
|
||||
await json(route, {
|
||||
user: {
|
||||
id: 'user-e2e',
|
||||
name: 'E2E Owner',
|
||||
email: 'owner@example.com',
|
||||
emailVerified: true,
|
||||
createdAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
onboarding_completed: true,
|
||||
org_id: ORG_ID,
|
||||
role: 'org_owner',
|
||||
},
|
||||
session: {
|
||||
id: 'session-e2e',
|
||||
userId: 'user-e2e',
|
||||
token: 'session-token',
|
||||
activeOrganizationId: ORG_ID,
|
||||
expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
createdAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && path === '/v1/agents') {
|
||||
await json(route, { agents: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'POST' && path === '/v1/agents/register') {
|
||||
const registration = body as {
|
||||
agent_id: string;
|
||||
display_name: string;
|
||||
responsible_entity?: string;
|
||||
integration_type: string;
|
||||
keys: Array<{ kid: string; public_key: string; algorithm: string }>;
|
||||
};
|
||||
await json(route, {
|
||||
agent: {
|
||||
agent_id: registration.agent_id,
|
||||
org_id: ORG_ID,
|
||||
display_name: registration.display_name,
|
||||
responsible_entity: registration.responsible_entity ?? '',
|
||||
integration_type: registrationIntegrationOverride ?? registration.integration_type,
|
||||
status: 'active',
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
},
|
||||
keys: registration.keys.map((key) => ({
|
||||
...key,
|
||||
agent_id: registration.agent_id,
|
||||
status: 'active',
|
||||
created_at: Date.now(),
|
||||
retired_at: null,
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'POST' && path === '/v1/auth/token') {
|
||||
await json(route, { token: TOKEN, expires_at: Date.now() + 86_400_000 });
|
||||
return;
|
||||
}
|
||||
|
||||
unexpected.push(`${method} ${path}`);
|
||||
await json(route, { error: { code: 'UNEXPECTED_E2E_REQUEST' } }, 501);
|
||||
});
|
||||
}
|
||||
|
||||
test('registers Grok atomically and renders verified adapter commands', async ({ page }) => {
|
||||
const observed: ObservedRequest[] = [];
|
||||
const unexpected: string[] = [];
|
||||
const runtimeErrors: string[] = [];
|
||||
page.on('pageerror', (error) => runtimeErrors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') runtimeErrors.push(message.text());
|
||||
});
|
||||
await mockBackend(page, observed, unexpected);
|
||||
|
||||
await page.goto('/agents');
|
||||
await page.getByRole('button', { name: 'Register Agent' }).click();
|
||||
await waitForRenderedDialog(page);
|
||||
await expect(page.getByRole('button', { name: 'Augment Code' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'OpenAI Codex' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Kimi CLI' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Qwen Code' })).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/agent-registration-catalog.png', fullPage: true });
|
||||
await page.getByRole('button', { name: 'Grok Build' }).click();
|
||||
await page.getByLabel('Display Name').fill('Production Grok');
|
||||
await page.getByLabel('Responsible Entity (optional)').fill('Platform Security');
|
||||
await page.locator('form').getByRole('button', { name: 'Register Agent' }).click();
|
||||
|
||||
await expect(page.getByText('Private Key — Save This Now')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Close' })).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByText('Private Key — Save This Now')).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/agent-registration-credentials.png', fullPage: true });
|
||||
await page.getByLabel('Token Expiration').selectOption('custom');
|
||||
await page.getByLabel('days').fill('366');
|
||||
await page.getByRole('button', { name: 'Issue API Token' }).click();
|
||||
await expect(page.getByText('Enter a whole number from 1 to 365.')).toBeVisible();
|
||||
expect(observed.filter(({ path }) => path === '/v1/auth/token')).toHaveLength(0);
|
||||
await page.getByLabel('Token Expiration').selectOption('24hours');
|
||||
await page.getByRole('button', { name: 'Issue API Token' }).click();
|
||||
await expect(page.getByText(TOKEN)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
const setup = page.locator('pre');
|
||||
await expect(setup).toContainText('npx @elydora/sdk install --agent grok');
|
||||
await expect(setup).toContainText(`--org_id "${ORG_ID}"`);
|
||||
await expect(setup).toContainText(`--token "${TOKEN}"`);
|
||||
|
||||
await page.getByRole('tab', { name: 'go' }).click();
|
||||
await expect(setup).toContainText('elydora install --agent grok');
|
||||
await expect(setup).toContainText(`--org-id "${ORG_ID}"`);
|
||||
await expect(setup).toContainText('--agent-id "agent-');
|
||||
|
||||
await page.screenshot({ path: 'test-results/agent-registration-grok.png', fullPage: true });
|
||||
|
||||
const registrationRequests = observed.filter(({ path }) => path === '/v1/agents/register');
|
||||
expect(registrationRequests).toHaveLength(1);
|
||||
expect(registrationRequests[0]?.method).toBe('POST');
|
||||
expect(registrationRequests[0]?.body).toMatchObject({
|
||||
display_name: 'Production Grok',
|
||||
responsible_entity: 'Platform Security',
|
||||
integration_type: 'grok',
|
||||
});
|
||||
expect(observed.filter(({ method }) => method === 'PATCH')).toHaveLength(0);
|
||||
expect(observed.filter(({ path }) => path === '/v1/auth/token')).toHaveLength(1);
|
||||
expect(unexpected).toEqual([]);
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test('blocks the success state when the backend returns a different integration', async ({ page }) => {
|
||||
const observed: ObservedRequest[] = [];
|
||||
const unexpected: string[] = [];
|
||||
await mockBackend(page, observed, unexpected, 'codex');
|
||||
|
||||
await page.goto('/agents');
|
||||
await page.getByRole('button', { name: 'Register Agent' }).click();
|
||||
await page.getByRole('button', { name: 'Grok Build' }).click();
|
||||
await page.getByLabel('Display Name').fill('Mismatched Grok');
|
||||
await page.locator('form').getByRole('button', { name: 'Register Agent' }).click();
|
||||
|
||||
await expect(page.locator('form [role="alert"]')).toContainText('different integration type');
|
||||
await expect(page.getByText('Private Key — Save This Now')).toHaveCount(0);
|
||||
await expect(page.locator('form').getByRole('button', { name: 'Register Agent' })).toBeDisabled();
|
||||
expect(observed.filter(({ path }) => path === '/v1/agents/register')).toHaveLength(1);
|
||||
expect(observed.filter(({ method }) => method === 'PATCH')).toHaveLength(0);
|
||||
expect(unexpected).toEqual([]);
|
||||
});
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "node:path";
|
||||
|
||||
const monorepoRoot = path.join(__dirname, "../..");
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
outputFileTracingRoot: monorepoRoot,
|
||||
turbopack: {
|
||||
root: monorepoRoot,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+224
-180
@@ -8,23 +8,27 @@
|
||||
"name": "elydora-console",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"better-auth": "^1.5.5",
|
||||
"better-auth": "^1.6.23",
|
||||
"i18next": "^25.8.13",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"next": "^16.1.6",
|
||||
"next": "^16.2.10",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.4",
|
||||
"swr": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/node": "25.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.0.0",
|
||||
"postcss": "^8.5.20",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -50,9 +54,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/core": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.5.6.tgz",
|
||||
"integrity": "sha512-Ez9DZdIMFyxHremmoLz1emFPGNQomDC1jqqBPnZ6Ci+6TiGN3R9w/Y03cJn6I8r1ycKgOzeVMZtJ/erOZ27Gsw==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.23.tgz",
|
||||
"integrity": "sha512-beEhOs0uVeOxYOZKUfIEBd/nQV2Bd4/6wyLxZ0OFkn6CMTK2Vi+hXuZLnyPBeB6RdHpebEoJWiHqwHxBIxgPDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.39.0",
|
||||
@@ -60,30 +64,33 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"@better-fetch/fetch": "1.3.1",
|
||||
"@cloudflare/workers-types": ">=4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"better-call": "1.3.2",
|
||||
"better-call": "1.3.7",
|
||||
"jose": "^6.1.0",
|
||||
"kysely": "^0.28.5",
|
||||
"kysely": "^0.28.5 || ^0.29.0",
|
||||
"nanostores": "^1.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cloudflare/workers-types": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/drizzle-adapter": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.5.6.tgz",
|
||||
"integrity": "sha512-VfFFmaoFw3ug12SiSuIwzrMoHyIVmkMGWm9gZ4sXdYYVX4HboCL4m3fjzOhppcmK5OGatRuU+N1UX6wxCITcXw==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.23.tgz",
|
||||
"integrity": "sha512-2+/PTVfIP9E7iz6af8TB3lhnowHUj9ljC66kECmHaFEdUqPgzHoWux9epotKwO7XDg2ui4ttWQ8CMeNFLvQeKQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"drizzle-orm": ">=0.41.0"
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"drizzle-orm": "^0.45.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"drizzle-orm": {
|
||||
@@ -92,14 +99,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/kysely-adapter": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.5.6.tgz",
|
||||
"integrity": "sha512-Fnf+h8WVKtw6lEOmVmiVVzDf3shJtM60AYf9XTnbdCeUd6MxN/KnaJZpkgtYnRs7a+nwtkVB+fg4lGETebGFXQ==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.23.tgz",
|
||||
"integrity": "sha512-zbNJsMbG09exfkGyvFqBLLqWoMPAUWjxCuUnEK5AsjbYoZeIjj/QGZgdf4CapVWryKxjA9Q6Jlr6fbiPpC3VAg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"kysely": "^0.27.0 || ^0.28.0"
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"kysely": "^0.28.17 || ^0.29.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"kysely": {
|
||||
@@ -108,23 +115,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/memory-adapter": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.5.6.tgz",
|
||||
"integrity": "sha512-rS7ZsrIl5uvloUgNN0u9LOZJMMXnsZXVdUZ3MrTBKWM2KpoJjzPr9yN3Szyma5+0V7SltnzSGHPkYj2bEzzmlA==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.23.tgz",
|
||||
"integrity": "sha512-krIiR0pIVkaKlAzm690n5bcMW4NGbqeMg0HQSD9fz/KcQF/eWLqcq9gG/BhHTj2i/y96qH+W5JWPmaSOS5iTgQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/utils": "^0.3.0"
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/mongo-adapter": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.5.6.tgz",
|
||||
"integrity": "sha512-6+M3MS2mor8fTUV3EI1FBLP0cs6QfbN+Ovx9+XxR/GdfKIBoNFzmPEPRbdGt+ft6PvrITsUm+T70+kkHgVSP6w==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.23.tgz",
|
||||
"integrity": "sha512-7+QdevitGlKBbP6JbiSk5SBnzPsKV/mDrQBGBn8hwByQLeJwqpqbuBPw7ZI8vzUlFfAAnyFiqwP3Eb8mxnp7pA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"mongodb": "^6.0.0 || ^7.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -134,13 +141,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/prisma-adapter": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.5.6.tgz",
|
||||
"integrity": "sha512-UxY9vQJs1Tt+O+T2YQnseDMlWmUSQvFZSBb5YiFRg7zcm+TEzujh4iX2/csA0YiZptLheovIuVWTP9nriewEBA==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.23.tgz",
|
||||
"integrity": "sha512-2qSdzidq4tkb1eS5TTqb4Nzg0mdZWm3Qky9SYeXeb8PpVQbC2sxqJhEM5mK7y12uU6I8hc64wO9f7AFVNL+6UQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
@@ -154,28 +161,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/telemetry": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.5.6.tgz",
|
||||
"integrity": "sha512-yXC7NSxnIFlxDkGdpD7KA+J9nqIQAPCJKe77GoaC5bWoe/DALo1MYorZfTgOafS7wrslNtsPT4feV/LJi1ubqQ==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.23.tgz",
|
||||
"integrity": "sha512-/R2Kb+z2BpDOOWwVHqOk+c0VNpuwfCv4Hp5Yr9003WIZPax/zyNraGLB9CFE8qF2gZW8Dsz419k4I8CPrGzpDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.6"
|
||||
"@better-auth/core": "^1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"@better-fetch/fetch": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz",
|
||||
"integrity": "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==",
|
||||
"license": "MIT"
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
|
||||
"integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-fetch/fetch": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
|
||||
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.1.tgz",
|
||||
"integrity": "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
@@ -704,15 +713,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
|
||||
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.10.tgz",
|
||||
"integrity": "sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
|
||||
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.10.tgz",
|
||||
"integrity": "sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -726,9 +735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
|
||||
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.10.tgz",
|
||||
"integrity": "sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -742,12 +751,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.10.tgz",
|
||||
"integrity": "sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -758,12 +770,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.10.tgz",
|
||||
"integrity": "sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -774,12 +789,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.10.tgz",
|
||||
"integrity": "sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -790,12 +808,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.10.tgz",
|
||||
"integrity": "sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -806,9 +827,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.10.tgz",
|
||||
"integrity": "sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -822,9 +843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.10.tgz",
|
||||
"integrity": "sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -861,25 +882,31 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
|
||||
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
|
||||
"integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -1209,26 +1236,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/better-auth": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.6.tgz",
|
||||
"integrity": "sha512-QSpJTqaT1XVfWRQe/fm3PgeuwOIlz1nWX/Dx7nsHStJ382bLzmDbQk2u7IT0IJ6wS5SRxfqEE1Ev9TXontgyAQ==",
|
||||
"version": "1.6.23",
|
||||
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.23.tgz",
|
||||
"integrity": "sha512-4vOaRd9UiKGKm9R+ej0jjU1es3MiJIiNc9Qq3VCnYqOZ4/nb5272QqTxWYoDxyUXl5x6A2x2we5KZKQO9teTQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/core": "1.5.6",
|
||||
"@better-auth/drizzle-adapter": "1.5.6",
|
||||
"@better-auth/kysely-adapter": "1.5.6",
|
||||
"@better-auth/memory-adapter": "1.5.6",
|
||||
"@better-auth/mongo-adapter": "1.5.6",
|
||||
"@better-auth/prisma-adapter": "1.5.6",
|
||||
"@better-auth/telemetry": "1.5.6",
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21",
|
||||
"@better-auth/core": "1.6.23",
|
||||
"@better-auth/drizzle-adapter": "1.6.23",
|
||||
"@better-auth/kysely-adapter": "1.6.23",
|
||||
"@better-auth/memory-adapter": "1.6.23",
|
||||
"@better-auth/mongo-adapter": "1.6.23",
|
||||
"@better-auth/prisma-adapter": "1.6.23",
|
||||
"@better-auth/telemetry": "1.6.23",
|
||||
"@better-auth/utils": "0.4.2",
|
||||
"@better-fetch/fetch": "1.3.1",
|
||||
"@noble/ciphers": "^2.1.1",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"better-call": "1.3.2",
|
||||
"better-call": "1.3.7",
|
||||
"defu": "^6.1.4",
|
||||
"jose": "^6.1.3",
|
||||
"kysely": "^0.28.12",
|
||||
"kysely": "^0.28.17 || ^0.29.0",
|
||||
"nanostores": "^1.1.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
@@ -1240,7 +1267,7 @@
|
||||
"@tanstack/solid-start": "^1.0.0",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"drizzle-kit": ">=0.31.4",
|
||||
"drizzle-orm": ">=0.41.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"mongodb": "^6.0.0 || ^7.0.0",
|
||||
"mysql2": "^3.0.0",
|
||||
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||
@@ -1314,12 +1341,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/better-call": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.2.tgz",
|
||||
"integrity": "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==",
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.7.tgz",
|
||||
"integrity": "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/utils": "^0.3.1",
|
||||
"@better-auth/utils": "^0.4.0",
|
||||
"@better-fetch/fetch": "^1.1.21",
|
||||
"rou3": "^0.7.12",
|
||||
"set-cookie-parser": "^3.0.1"
|
||||
@@ -1367,9 +1394,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
@@ -1405,6 +1432,20 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1472,21 +1513,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/kysely": {
|
||||
"version": "0.28.14",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz",
|
||||
"integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==",
|
||||
"version": "0.29.4",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.4.tgz",
|
||||
"integrity": "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
@@ -1761,9 +1802,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1779,9 +1820,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanostores": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
|
||||
"integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.4.0.tgz",
|
||||
"integrity": "sha512-i0tloweeudshAEuddpDxcg9Ik6pkPfVsHIgKyf143JrgG7/MOh0+q7BypdLXZPoOP7fOYt1eTcwGkyiVmhJFkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1794,14 +1835,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
|
||||
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
|
||||
"version": "16.2.10",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz",
|
||||
"integrity": "sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.1.6",
|
||||
"@next/env": "16.2.10",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
@@ -1813,15 +1854,15 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.1.6",
|
||||
"@next/swc-darwin-x64": "16.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.6",
|
||||
"@next/swc-linux-arm64-musl": "16.1.6",
|
||||
"@next/swc-linux-x64-gnu": "16.1.6",
|
||||
"@next/swc-linux-x64-musl": "16.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.6",
|
||||
"@next/swc-win32-x64-msvc": "16.1.6",
|
||||
"sharp": "^0.34.4"
|
||||
"@next/swc-darwin-arm64": "16.2.10",
|
||||
"@next/swc-darwin-x64": "16.2.10",
|
||||
"@next/swc-linux-arm64-gnu": "16.2.10",
|
||||
"@next/swc-linux-arm64-musl": "16.2.10",
|
||||
"@next/swc-linux-x64-gnu": "16.2.10",
|
||||
"@next/swc-linux-x64-musl": "16.2.10",
|
||||
"@next/swc-win32-arm64-msvc": "16.2.10",
|
||||
"@next/swc-win32-x64-msvc": "16.2.10",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
@@ -1846,45 +1887,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"version": "8.5.20",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
|
||||
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1901,7 +1945,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -1983,9 +2027,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
|
||||
"integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
@@ -2145,9 +2189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -2,29 +2,37 @@
|
||||
"name": "elydora-console",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^1.5.5",
|
||||
"better-auth": "^1.6.23",
|
||||
"i18next": "^25.8.13",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"next": "^16.1.6",
|
||||
"next": "^16.2.10",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-i18next": "^16.5.4",
|
||||
"swr": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/node": "25.3.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.0.0",
|
||||
"postcss": "^8.5.20",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "$postcss"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [['list'], ['html', { open: 'never' }]],
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:3100',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev -- --hostname 127.0.0.1 --port 3100',
|
||||
url: 'http://127.0.0.1:3100/agents',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
@@ -40,6 +40,7 @@ function AgentsContent() {
|
||||
const router = useRouter();
|
||||
const [search, setSearch] = useState('');
|
||||
const [showRegister, setShowRegister] = useState(false);
|
||||
const [registrationDismissible, setRegistrationDismissible] = useState(true);
|
||||
const [deleteTarget, setDeleteTarget] = useState<AgentRow | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
@@ -132,11 +133,27 @@ function AgentsContent() {
|
||||
[router],
|
||||
);
|
||||
|
||||
const handleRegisterSuccess = useCallback(() => {
|
||||
setShowRegister(false);
|
||||
mutate();
|
||||
const refreshAgents = useCallback(() => {
|
||||
void mutate().catch((error: unknown) => {
|
||||
console.error('Failed to refresh agents after registration.', error);
|
||||
});
|
||||
}, [mutate]);
|
||||
|
||||
const handleRegisterSuccess = useCallback(() => {
|
||||
setRegistrationDismissible(true);
|
||||
setShowRegister(false);
|
||||
refreshAgents();
|
||||
}, [refreshAgents]);
|
||||
|
||||
const openRegistration = useCallback(() => {
|
||||
setRegistrationDismissible(true);
|
||||
setShowRegister(true);
|
||||
}, []);
|
||||
|
||||
const closeRegistration = useCallback(() => {
|
||||
if (registrationDismissible) setShowRegister(false);
|
||||
}, [registrationDismissible]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!deleteTarget) return;
|
||||
setDeleting(true);
|
||||
@@ -162,7 +179,7 @@ function AgentsContent() {
|
||||
{ label: t('agents.title') },
|
||||
]}
|
||||
actions={
|
||||
<button className="btn-brutalist" onClick={() => setShowRegister(true)}>
|
||||
<button className="btn-brutalist" onClick={openRegistration}>
|
||||
{t('agents.registerAgent')}
|
||||
</button>
|
||||
}
|
||||
@@ -194,13 +211,15 @@ function AgentsContent() {
|
||||
|
||||
<Modal
|
||||
isOpen={showRegister}
|
||||
onClose={() => setShowRegister(false)}
|
||||
onClose={closeRegistration}
|
||||
title={t('agents.registerAgent')}
|
||||
width="max-w-2xl"
|
||||
dismissible={registrationDismissible}
|
||||
>
|
||||
<AgentRegistrationForm
|
||||
onRegistered={refreshAgents}
|
||||
onDismissibleChange={setRegistrationDismissible}
|
||||
onSuccess={handleRegisterSuccess}
|
||||
onCancel={() => setShowRegister(false)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ import Modal from '@/components/ui/Modal';
|
||||
import Link from 'next/link';
|
||||
import type { AgentKey, Operation } from '@elydora/shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const HOOKS_INTEGRATIONS = new Set([
|
||||
'claudecode', 'cursor', 'gemini', 'kirocli', 'kiroide', 'opencode', 'copilot', 'letta',
|
||||
]);
|
||||
import { ADAPTER_INTEGRATION_IDS } from '@/features/agent-registration/integrations';
|
||||
|
||||
export default function AgentDetailClient({ agentId }: { agentId: string }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,7 +36,9 @@ export default function AgentDetailClient({ agentId }: { agentId: string }) {
|
||||
const [revokeError, setRevokeError] = useState<string | null>(null);
|
||||
|
||||
const agent = agentData?.agent;
|
||||
const supportsFreeze = !agent?.integration_type || HOOKS_INTEGRATIONS.has(agent.integration_type);
|
||||
const supportsFreeze = agent
|
||||
? ADAPTER_INTEGRATION_IDS.has(agent.integration_type)
|
||||
: false;
|
||||
const keys = agentData?.keys ?? [];
|
||||
const operations = opsData?.operations ?? [];
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AgentCredentials } from '@/features/agent-registration/credentials';
|
||||
import type { IntegrationCatalogItem } from '@/features/agent-registration/integrations';
|
||||
import {
|
||||
buildInstallInstructions,
|
||||
SDK_LANGUAGES,
|
||||
type SdkLanguage,
|
||||
} from '@/features/agent-registration/install-instructions';
|
||||
import CopyControl from './CopyControl';
|
||||
|
||||
interface ConnectStepProps {
|
||||
readonly integration: IntegrationCatalogItem;
|
||||
readonly credentials: AgentCredentials;
|
||||
readonly token: string;
|
||||
readonly onBack: () => void;
|
||||
readonly onDone: () => void;
|
||||
}
|
||||
|
||||
export default function ConnectStep({
|
||||
integration,
|
||||
credentials,
|
||||
token,
|
||||
onBack,
|
||||
onDone,
|
||||
}: ConnectStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [language, setLanguage] = useState<SdkLanguage>('node');
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
|
||||
const instructions = useMemo(
|
||||
() => buildInstallInstructions(language, { integration, credentials, token, baseUrl }),
|
||||
[baseUrl, credentials, integration, language, token],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||
<span className="section-label">{t('agentRegistration.integration')}</span>
|
||||
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex border-b border-border" role="tablist" aria-label={t('agentRegistration.sdkLanguage')}>
|
||||
{SDK_LANGUAGES.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={language === item}
|
||||
onClick={() => setLanguage(item)}
|
||||
className={`px-4 py-2 font-mono text-[10px] uppercase tracking-wider border-b-2 -mb-px ${
|
||||
language === item ? 'border-ink text-ink' : 'border-transparent text-ink-dim hover:text-ink'
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="setup-command-label">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 id="setup-command-label" className="section-label">
|
||||
{integration.mode === 'adapter' ? t('agentRegistration.install') : t('agentRegistration.configure')}
|
||||
</h3>
|
||||
<CopyControl value={instructions.setup} />
|
||||
</div>
|
||||
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
||||
{instructions.setup}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
{instructions.usage && (
|
||||
<section aria-labelledby="record-operation-label">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 id="record-operation-label" className="section-label">{t('agentRegistration.record')}</h3>
|
||||
<CopyControl value={instructions.usage} />
|
||||
</div>
|
||||
<pre className="p-4 bg-ink text-[#EAEAE5] font-mono text-[10px] leading-relaxed whitespace-pre-wrap break-all overflow-x-auto">
|
||||
{instructions.usage}
|
||||
</pre>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{instructions.verify && (
|
||||
<section aria-labelledby="verify-command-label">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 id="verify-command-label" className="section-label">{t('agentRegistration.verify')}</h3>
|
||||
<CopyControl value={instructions.verify} />
|
||||
</div>
|
||||
<code className="block px-3 py-2 border-y border-border font-mono text-[11px] text-ink">
|
||||
{instructions.verify}
|
||||
</code>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{instructions.postInstall && (
|
||||
<ol className="border-t border-border" aria-label={t('agentRegistration.postInstall')}>
|
||||
{instructions.postInstall.map((step, index) => (
|
||||
<li key={step} className="grid grid-cols-[2rem_1fr] gap-2 py-2 border-b border-border font-mono text-[11px] text-ink">
|
||||
<span className="text-ink-dim tabular-nums">{index + 1}</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<button type="button" className="btn-ghost" onClick={onBack}>{t('common.back')}</button>
|
||||
<button type="button" className="btn-brutalist" onClick={onDone}>{t('agentRegistration.done')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface CopyControlProps {
|
||||
readonly value: string;
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CopyControl({ value, className = '' }: CopyControlProps) {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle');
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setStatus('copied');
|
||||
} catch {
|
||||
setStatus('failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-2 ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="font-mono text-[10px] uppercase tracking-wider text-ink-dim hover:text-ink"
|
||||
>
|
||||
{status === 'copied' ? t('common.copied') : t('common.copy')}
|
||||
</button>
|
||||
{status === 'failed' && (
|
||||
<span className="font-mono text-[10px] text-red-600" role="alert">
|
||||
{t('agentRegistration.copyFailed')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AgentCredentials } from '@/features/agent-registration/credentials';
|
||||
import CopyControl from './CopyControl';
|
||||
|
||||
const EXPIRATION_OPTIONS = [
|
||||
{ id: '24hours', seconds: 86_400 },
|
||||
{ id: '7days', seconds: 604_800 },
|
||||
{ id: '1month', seconds: 2_592_000 },
|
||||
{ id: '1year', seconds: 31_536_000 },
|
||||
{ id: 'custom', seconds: -1 },
|
||||
{ id: 'neverExpire', seconds: null },
|
||||
] as const;
|
||||
const MAX_CUSTOM_DAYS = 365;
|
||||
|
||||
type ExpirationId = (typeof EXPIRATION_OPTIONS)[number]['id'];
|
||||
|
||||
interface CredentialsStepProps {
|
||||
readonly credentials: AgentCredentials;
|
||||
readonly token?: string;
|
||||
readonly issuingToken: boolean;
|
||||
readonly tokenError: string | null;
|
||||
readonly onIssueToken: (ttlSeconds: number | null) => void;
|
||||
readonly onContinue: () => void;
|
||||
}
|
||||
|
||||
function CredentialRow({ label, value }: { readonly label: string; readonly value: string }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[7rem_minmax(0,1fr)_auto] items-center gap-3 py-2 border-b border-border last:border-b-0">
|
||||
<dt className="section-label">{label}</dt>
|
||||
<dd className="font-mono text-[11px] text-ink truncate" title={value}>{value}</dd>
|
||||
<dd><CopyControl value={value} /></dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function CredentialsStep({
|
||||
credentials,
|
||||
token,
|
||||
issuingToken,
|
||||
tokenError,
|
||||
onIssueToken,
|
||||
onContinue,
|
||||
}: CredentialsStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expiration, setExpiration] = useState<ExpirationId>('24hours');
|
||||
const [customDays, setCustomDays] = useState('');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
const issueToken = () => {
|
||||
const option = EXPIRATION_OPTIONS.find(({ id }) => id === expiration);
|
||||
if (!option) {
|
||||
setValidationError(t('agentRegistration.invalidExpiration'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.seconds === -1) {
|
||||
const days = Number.parseInt(customDays, 10);
|
||||
if (!Number.isSafeInteger(days) || days < 1 || days > MAX_CUSTOM_DAYS) {
|
||||
setValidationError(t('agentRegistration.enterValidDays'));
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
onIssueToken(days * 86_400);
|
||||
return;
|
||||
}
|
||||
|
||||
setValidationError(null);
|
||||
onIssueToken(option.seconds);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<dl>
|
||||
<CredentialRow label={t('agentRegistration.credAgentId')} value={credentials.agentId} />
|
||||
<CredentialRow label={t('agentRegistration.credKeyId')} value={credentials.kid} />
|
||||
<CredentialRow label={t('agentRegistration.credOrgId')} value={credentials.orgId} />
|
||||
</dl>
|
||||
|
||||
<section aria-labelledby="private-key-label">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 id="private-key-label" className="section-label text-red-700">
|
||||
{t('agentRegistration.privateKeySaveNow')}
|
||||
</h3>
|
||||
<CopyControl value={credentials.privateKey} />
|
||||
</div>
|
||||
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
||||
{credentials.privateKey}
|
||||
</code>
|
||||
<p className="font-mono text-[10px] text-ink-dim mt-2">
|
||||
{t('agentRegistration.privateKeyWarning')}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="api-token-label" className="border-t border-border pt-5">
|
||||
<h3 id="api-token-label" className="section-label mb-3">
|
||||
{t('agentRegistration.apiToken')}
|
||||
</h3>
|
||||
|
||||
{token ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-mono text-[10px] text-ink-dim">
|
||||
{t('agentRegistration.tokenIssued')}
|
||||
</span>
|
||||
<CopyControl value={token} />
|
||||
</div>
|
||||
<code className="block p-3 bg-ink text-[#EAEAE5] font-mono text-[10px] break-all select-all">
|
||||
{token}
|
||||
</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row sm:items-end gap-3">
|
||||
<div className="flex-1">
|
||||
<label htmlFor="token-expiration" className="section-label block mb-1.5">
|
||||
{t('agentRegistration.tokenExpiration')}
|
||||
</label>
|
||||
<select
|
||||
id="token-expiration"
|
||||
value={expiration}
|
||||
onChange={(event) => setExpiration(event.target.value as ExpirationId)}
|
||||
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[12px] text-ink focus:outline-none focus:border-ink"
|
||||
disabled={issuingToken}
|
||||
>
|
||||
{EXPIRATION_OPTIONS.map(({ id }) => (
|
||||
<option key={id} value={id}>{t(`tokenExpiration.${id}`)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{expiration === 'custom' && (
|
||||
<div className="sm:w-28">
|
||||
<label htmlFor="custom-token-days" className="section-label block mb-1.5">
|
||||
{t('agentRegistration.days')}
|
||||
</label>
|
||||
<input
|
||||
id="custom-token-days"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={customDays}
|
||||
onChange={(event) => setCustomDays(event.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[12px] text-ink focus:outline-none focus:border-ink"
|
||||
disabled={issuingToken}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button type="button" className="btn-brutalist" onClick={issueToken} disabled={issuingToken}>
|
||||
{issuingToken ? t('agentRegistration.issuing') : t('agentRegistration.issueApiToken')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(validationError || tokenError) && (
|
||||
<p className="font-mono text-[11px] text-red-600 mt-2" role="alert">
|
||||
{validationError ?? tokenError}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end pt-2">
|
||||
<button type="button" className="btn-brutalist" onClick={onContinue} disabled={!token}>
|
||||
{t('common.continue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { IntegrationCatalogItem } from '@/features/agent-registration/integrations';
|
||||
|
||||
export interface AgentDetails {
|
||||
readonly displayName: string;
|
||||
readonly responsibleEntity?: string;
|
||||
}
|
||||
|
||||
interface DetailsStepProps {
|
||||
readonly integration: IntegrationCatalogItem;
|
||||
readonly submitting: boolean;
|
||||
readonly blocked: boolean;
|
||||
readonly error: string | null;
|
||||
readonly onSubmit: (details: AgentDetails) => void;
|
||||
readonly onBack: () => void;
|
||||
}
|
||||
|
||||
export default function DetailsStep({
|
||||
integration,
|
||||
submitting,
|
||||
blocked,
|
||||
error,
|
||||
onSubmit,
|
||||
onBack,
|
||||
}: DetailsStepProps) {
|
||||
const { t } = useTranslation();
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [responsibleEntity, setResponsibleEntity] = useState('');
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
const submit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const normalizedName = displayName.trim();
|
||||
if (!normalizedName) {
|
||||
setValidationError(t('agentRegistration.displayNameRequired'));
|
||||
return;
|
||||
}
|
||||
setValidationError(null);
|
||||
onSubmit({
|
||||
displayName: normalizedName,
|
||||
responsibleEntity: responsibleEntity.trim() || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={submit} className="space-y-5">
|
||||
<div className="flex items-center justify-between border-b border-border pb-3">
|
||||
<span className="section-label">{t('agentRegistration.integration')}</span>
|
||||
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
|
||||
</div>
|
||||
|
||||
{(validationError || error) && (
|
||||
<div className="px-3 py-2 border border-red-300 bg-red-50 text-red-700 font-mono text-[11px]" role="alert">
|
||||
{validationError ?? error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="agent-display-name" className="section-label block mb-1.5">
|
||||
{t('agentRegistration.displayName')}
|
||||
</label>
|
||||
<input
|
||||
id="agent-display-name"
|
||||
value={displayName}
|
||||
onChange={(event) => setDisplayName(event.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[13px] text-ink placeholder:text-ink-dim focus:outline-none focus:border-ink"
|
||||
placeholder={t('agentRegistration.displayNamePlaceholder')}
|
||||
disabled={submitting || blocked}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="agent-responsible-entity" className="section-label block mb-1.5">
|
||||
{t('agentRegistration.responsibleEntity')}
|
||||
</label>
|
||||
<input
|
||||
id="agent-responsible-entity"
|
||||
value={responsibleEntity}
|
||||
onChange={(event) => setResponsibleEntity(event.target.value)}
|
||||
className="w-full px-3 py-2 bg-transparent border border-border font-mono text-[13px] text-ink placeholder:text-ink-dim focus:outline-none focus:border-ink"
|
||||
placeholder={t('agentRegistration.responsibleEntityPlaceholder')}
|
||||
disabled={submitting || blocked}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<button type="button" className="btn-ghost" onClick={onBack} disabled={submitting || blocked}>
|
||||
{t('common.back')}
|
||||
</button>
|
||||
<button type="submit" className="btn-brutalist" disabled={submitting || blocked}>
|
||||
{submitting ? t('agentRegistration.submitting') : t('agentRegistration.submit')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
INTEGRATION_CATALOG,
|
||||
type IntegrationCatalogItem,
|
||||
type IntegrationMode,
|
||||
} from '@/features/agent-registration/integrations';
|
||||
|
||||
interface IntegrationStepProps {
|
||||
readonly onSelect: (integration: IntegrationCatalogItem) => void;
|
||||
}
|
||||
|
||||
function IntegrationGroup({
|
||||
mode,
|
||||
onSelect,
|
||||
}: {
|
||||
readonly mode: IntegrationMode;
|
||||
readonly onSelect: (integration: IntegrationCatalogItem) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const integrations = INTEGRATION_CATALOG.filter((item) => item.mode === mode);
|
||||
|
||||
return (
|
||||
<section aria-labelledby={`integration-${mode}`}>
|
||||
<h3 id={`integration-${mode}`} className="section-label mb-2">
|
||||
{t(`agentRegistration.integrationGroups.${mode}`)}
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 border-t border-border sm:[&>button:nth-child(odd)]:border-r">
|
||||
{integrations.map((integration) => (
|
||||
<button
|
||||
key={integration.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(integration)}
|
||||
className="group flex items-center justify-between gap-3 px-3 py-3 text-left border-b border-border hover:bg-surface focus-visible:bg-surface focus-visible:outline-none"
|
||||
>
|
||||
<span className="font-mono text-[12px] text-ink">{integration.name}</span>
|
||||
<span className="font-mono text-[10px] text-ink-dim opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition-opacity" aria-hidden="true">
|
||||
→
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
export default function IntegrationStep({ onSelect }: IntegrationStepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<IntegrationGroup mode="adapter" onSelect={onSelect} />
|
||||
<IntegrationGroup mode="sdk" onSelect={onSelect} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const STEPS = ['integration', 'details', 'credentials', 'connect'] as const;
|
||||
export type RegistrationStep = (typeof STEPS)[number];
|
||||
|
||||
export default function RegistrationProgress({ current }: { current: RegistrationStep }) {
|
||||
const { t } = useTranslation();
|
||||
const activeIndex = STEPS.indexOf(current);
|
||||
|
||||
return (
|
||||
<ol className="grid grid-cols-4 border-b border-border mb-6" aria-label={t('agentRegistration.progress')}>
|
||||
{STEPS.map((step, index) => (
|
||||
<li
|
||||
key={step}
|
||||
className={`pb-2 font-mono text-[10px] uppercase tracking-wider border-b -mb-px ${
|
||||
index <= activeIndex ? 'border-ink text-ink' : 'border-transparent text-ink-dim'
|
||||
}`}
|
||||
aria-current={index === activeIndex ? 'step' : undefined}
|
||||
>
|
||||
<span className="mr-1.5 tabular-nums">{index + 1}</span>
|
||||
<span className="hidden sm:inline">{t(`agentRegistration.steps.${step}`)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useEffect, useRef, useId } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface ModalProps {
|
||||
@@ -9,28 +9,78 @@ interface ModalProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
width?: string;
|
||||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ isOpen, onClose, title, children, width = 'max-w-lg' }: ModalProps) {
|
||||
export default function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
width = 'max-w-lg',
|
||||
dismissible = true,
|
||||
}: ModalProps) {
|
||||
const titleId = useId();
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const previousFocusRef = useRef<Element | null>(null);
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
// Save previous focus on open; restore on unmount (close)
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
if (!isOpen) return;
|
||||
|
||||
previousFocusRef.current = document.activeElement;
|
||||
document.body.style.overflow = 'hidden';
|
||||
requestAnimationFrame(() => {
|
||||
dialogRef.current?.focus();
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = '';
|
||||
if (previousFocusRef.current instanceof HTMLElement) {
|
||||
previousFocusRef.current.focus();
|
||||
}
|
||||
};
|
||||
}, [isOpen, handleKeyDown]);
|
||||
}, [isOpen]);
|
||||
|
||||
// Keydown listener — uses ref so it never changes identity
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && dismissible) {
|
||||
onCloseRef.current();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Tab' && dialogRef.current) {
|
||||
const focusable = dialogRef.current.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
if (focusable.length === 0) return;
|
||||
|
||||
const first = focusable[0]!;
|
||||
const last = focusable[focusable.length - 1]!;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [dismissible, isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -39,32 +89,40 @@ export default function Modal({ isOpen, onClose, title, children, width = 'max-w
|
||||
ref={overlayRef}
|
||||
className="fixed inset-0 md:left-[260px] z-[100] flex items-center justify-center modal-overlay"
|
||||
onClick={(e) => {
|
||||
if (e.target === overlayRef.current) onClose();
|
||||
if (dismissible && e.target === overlayRef.current) onCloseRef.current();
|
||||
}}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
className={`${width} w-full mx-4 bg-bg border border-ink relative fade-in`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{/* Crosshair corners */}
|
||||
<span className="crosshair ch-tl" />
|
||||
<span className="crosshair ch-tr" />
|
||||
<span className="crosshair ch-bl" />
|
||||
<span className="crosshair ch-br" />
|
||||
<span className="crosshair ch-tl" aria-hidden="true" />
|
||||
<span className="crosshair ch-tr" aria-hidden="true" />
|
||||
<span className="crosshair ch-bl" aria-hidden="true" />
|
||||
<span className="crosshair ch-br" aria-hidden="true" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 sm:px-6 py-4 border-b border-border">
|
||||
<h2 className="font-sans text-lg font-semibold tracking-tight text-ink min-w-0 truncate">
|
||||
<h2 id={titleId} className="font-sans text-lg font-semibold tracking-tight text-ink min-w-0 truncate">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 flex items-center justify-center text-ink-dim hover:text-ink transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M1 1l12 12M13 1L1 13" />
|
||||
</svg>
|
||||
</button>
|
||||
{dismissible && (
|
||||
<button
|
||||
onClick={() => onCloseRef.current()}
|
||||
className="w-8 h-8 flex items-center justify-center text-ink-dim hover:text-ink transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
|
||||
<path d="M1 1l12 12M13 1L1 13" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export interface AgentCredentials {
|
||||
readonly agentId: string;
|
||||
readonly kid: string;
|
||||
readonly publicKey: string;
|
||||
readonly privateKey: string;
|
||||
readonly orgId: string;
|
||||
}
|
||||
|
||||
function requireJwkValue(value: string | undefined, field: 'd' | 'x'): string {
|
||||
if (!value) {
|
||||
throw new Error(`Web Crypto did not export the Ed25519 JWK ${field} value.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function generateAgentCredentials(orgId: string): Promise<AgentCredentials> {
|
||||
const normalizedOrgId = orgId.trim();
|
||||
if (!normalizedOrgId) {
|
||||
throw new Error('An active organization is required to register an agent.');
|
||||
}
|
||||
|
||||
const agentId = `agent-${crypto.randomUUID()}`;
|
||||
const kid = `${agentId}-key-1`;
|
||||
const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
|
||||
const [privateJwk, publicJwk] = await Promise.all([
|
||||
crypto.subtle.exportKey('jwk', keyPair.privateKey),
|
||||
crypto.subtle.exportKey('jwk', keyPair.publicKey),
|
||||
]);
|
||||
|
||||
return {
|
||||
agentId,
|
||||
kid,
|
||||
publicKey: requireJwkValue(publicJwk.x, 'x'),
|
||||
privateKey: requireJwkValue(privateJwk.d, 'd'),
|
||||
orgId: normalizedOrgId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { IntegrationCatalogItem } from './integrations';
|
||||
import type { AgentCredentials } from './credentials';
|
||||
|
||||
export const SDK_LANGUAGES = ['node', 'python', 'go'] as const;
|
||||
export type SdkLanguage = (typeof SDK_LANGUAGES)[number];
|
||||
|
||||
interface InstructionInput {
|
||||
readonly integration: IntegrationCatalogItem;
|
||||
readonly credentials: AgentCredentials;
|
||||
readonly token: string;
|
||||
readonly baseUrl: string;
|
||||
}
|
||||
|
||||
export interface InstallInstructions {
|
||||
readonly setup: string;
|
||||
readonly usage?: string;
|
||||
readonly verify?: string;
|
||||
readonly postInstall?: readonly string[];
|
||||
}
|
||||
|
||||
function quoted(value: string): string {
|
||||
return `"${value.replaceAll('"', '\\"')}"`;
|
||||
}
|
||||
|
||||
function adapterCommand(
|
||||
language: SdkLanguage,
|
||||
{ integration, credentials, token, baseUrl }: InstructionInput,
|
||||
): string {
|
||||
const values = {
|
||||
agent: integration.id,
|
||||
orgId: quoted(credentials.orgId),
|
||||
agentId: quoted(credentials.agentId),
|
||||
privateKey: quoted(credentials.privateKey),
|
||||
kid: quoted(credentials.kid),
|
||||
token: quoted(token),
|
||||
baseUrl: quoted(baseUrl),
|
||||
};
|
||||
|
||||
if (language === 'go') {
|
||||
return [
|
||||
'go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest',
|
||||
`elydora install --agent ${values.agent} --org-id ${values.orgId} --agent-id ${values.agentId} --private-key ${values.privateKey} --kid ${values.kid} --token ${values.token} --base-url ${values.baseUrl}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const executable = language === 'node'
|
||||
? 'npx @elydora/sdk install'
|
||||
: 'python -m pip install elydora\nelydora install';
|
||||
return `${executable} --agent ${values.agent} --org_id ${values.orgId} --agent_id ${values.agentId} --private_key ${values.privateKey} --kid ${values.kid} --token ${values.token} --base_url ${values.baseUrl}`;
|
||||
}
|
||||
|
||||
function postInstallSteps(integration: IntegrationCatalogItem): readonly string[] | undefined {
|
||||
if (integration.postInstall === 'review-hooks') {
|
||||
return ['Open the agent and run /hooks to review the active Elydora hooks.'];
|
||||
}
|
||||
if (integration.postInstall === 'start-kiro') {
|
||||
return [
|
||||
'Kiro CLI v2: kiro-cli --agent elydora-audit',
|
||||
'Kiro CLI v3: kiro-cli --v3',
|
||||
];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sdkSetup(language: SdkLanguage, input: InstructionInput): string {
|
||||
const { credentials, token, baseUrl } = input;
|
||||
if (language === 'python') {
|
||||
return `python -m pip install elydora
|
||||
|
||||
from elydora import ElydoraClient
|
||||
|
||||
client = ElydoraClient(
|
||||
org_id=${JSON.stringify(credentials.orgId)},
|
||||
agent_id=${JSON.stringify(credentials.agentId)},
|
||||
private_key=${JSON.stringify(credentials.privateKey)},
|
||||
base_url=${JSON.stringify(baseUrl)},
|
||||
token=${JSON.stringify(token)},
|
||||
)
|
||||
client.set_kid(${JSON.stringify(credentials.kid)})`;
|
||||
}
|
||||
|
||||
if (language === 'go') {
|
||||
return `go get github.com/Elydora-Infrastructure/Elydora-Go-SDK
|
||||
|
||||
client, err := elydora.NewClient(&elydora.Config{
|
||||
OrgID: ${JSON.stringify(credentials.orgId)},
|
||||
AgentID: ${JSON.stringify(credentials.agentId)},
|
||||
PrivateKey: ${JSON.stringify(credentials.privateKey)},
|
||||
BaseURL: ${JSON.stringify(baseUrl)},
|
||||
Token: ${JSON.stringify(token)},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}`;
|
||||
}
|
||||
|
||||
return `npm install @elydora/sdk
|
||||
|
||||
import { ElydoraClient } from '@elydora/sdk';
|
||||
|
||||
const client = new ElydoraClient({
|
||||
orgId: ${JSON.stringify(credentials.orgId)},
|
||||
agentId: ${JSON.stringify(credentials.agentId)},
|
||||
privateKey: ${JSON.stringify(credentials.privateKey)},
|
||||
kid: ${JSON.stringify(credentials.kid)},
|
||||
baseUrl: ${JSON.stringify(baseUrl)},
|
||||
});
|
||||
client.setToken(${JSON.stringify(token)});`;
|
||||
}
|
||||
|
||||
function sdkUsage(language: SdkLanguage): string {
|
||||
if (language === 'python') {
|
||||
return `operation = client.create_operation(
|
||||
operation_type="ai.tool_use",
|
||||
subject={"session_id": "session-123"},
|
||||
action={"tool": "shell"},
|
||||
payload={"command": "example"},
|
||||
)
|
||||
receipt = client.submit_operation(operation)`;
|
||||
}
|
||||
|
||||
if (language === 'go') {
|
||||
return `operation, err := client.CreateOperation(&elydora.CreateOperationParams{
|
||||
OperationType: "ai.tool_use",
|
||||
Subject: map[string]any{"session_id": "session-123"},
|
||||
Action: map[string]any{"tool": "shell"},
|
||||
Payload: map[string]any{"command": "example"},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = client.SubmitOperation(operation)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}`;
|
||||
}
|
||||
|
||||
return `const operation = client.createOperation({
|
||||
operationType: 'ai.tool_use',
|
||||
subject: { session_id: 'session-123' },
|
||||
action: { tool: 'shell' },
|
||||
payload: { command: 'example' },
|
||||
});
|
||||
await client.submitOperation(operation);`;
|
||||
}
|
||||
|
||||
export function buildInstallInstructions(
|
||||
language: SdkLanguage,
|
||||
input: InstructionInput,
|
||||
): InstallInstructions {
|
||||
if (input.integration.mode === 'adapter') {
|
||||
return {
|
||||
setup: adapterCommand(language, input),
|
||||
verify: language === 'node' ? 'npx @elydora/sdk status' : 'elydora status',
|
||||
postInstall: postInstallSteps(input.integration),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
setup: sdkSetup(language, input),
|
||||
usage: sdkUsage(language),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
INTEGRATION_TYPES,
|
||||
type IntegrationType,
|
||||
} from '../../../../server/src/shared/types/enums';
|
||||
|
||||
export { INTEGRATION_TYPES };
|
||||
|
||||
export type IntegrationMode = 'adapter' | 'sdk';
|
||||
export type PostInstallAction = 'review-hooks' | 'start-kiro';
|
||||
|
||||
export interface IntegrationCatalogItem {
|
||||
readonly id: IntegrationType;
|
||||
readonly name: string;
|
||||
readonly mode: IntegrationMode;
|
||||
readonly postInstall?: PostInstallAction;
|
||||
}
|
||||
|
||||
const INTEGRATION_METADATA: Record<
|
||||
IntegrationType,
|
||||
Omit<IntegrationCatalogItem, 'id'>
|
||||
> = {
|
||||
augment: { name: 'Augment Code', mode: 'adapter' },
|
||||
claudecode: { name: 'Claude Code', mode: 'adapter' },
|
||||
cline: { name: 'Cline', mode: 'adapter' },
|
||||
codex: { name: 'OpenAI Codex', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
copilot: { name: 'GitHub Copilot CLI', mode: 'adapter' },
|
||||
cursor: { name: 'Cursor', mode: 'adapter' },
|
||||
droid: { name: 'Factory Droid', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
gemini: { name: 'Gemini CLI', mode: 'adapter' },
|
||||
grok: { name: 'Grok Build', mode: 'adapter' },
|
||||
kimi: { name: 'Kimi CLI', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
kirocli: { name: 'Kiro CLI', mode: 'adapter', postInstall: 'start-kiro' },
|
||||
kiroide: { name: 'Kiro IDE', mode: 'adapter' },
|
||||
letta: { name: 'Letta Code', mode: 'adapter' },
|
||||
opencode: { name: 'OpenCode', mode: 'adapter' },
|
||||
qwen: { name: 'Qwen Code', mode: 'adapter', postInstall: 'review-hooks' },
|
||||
enterprise: { name: 'Enterprise Agent', mode: 'sdk' },
|
||||
gui: { name: 'GUI Agent', mode: 'sdk' },
|
||||
sdk: { name: 'SDK', mode: 'sdk' },
|
||||
other: { name: 'Other', mode: 'sdk' },
|
||||
};
|
||||
|
||||
export const INTEGRATION_CATALOG: readonly IntegrationCatalogItem[] =
|
||||
INTEGRATION_TYPES.map((id) => ({ id, ...INTEGRATION_METADATA[id] }));
|
||||
|
||||
export const ADAPTER_INTEGRATION_IDS: ReadonlySet<IntegrationType> = new Set(
|
||||
INTEGRATION_CATALOG.filter(({ mode }) => mode === 'adapter').map(({ id }) => id),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IntegrationType, RegisterAgentResponse } from '@elydora/shared';
|
||||
import type { AgentCredentials } from './credentials';
|
||||
|
||||
export class RegistrationContractError extends Error {
|
||||
override readonly name = 'RegistrationContractError';
|
||||
}
|
||||
|
||||
function contractError(message: string): never {
|
||||
throw new RegistrationContractError(message);
|
||||
}
|
||||
|
||||
export function assertRegistrationResponse(
|
||||
response: RegisterAgentResponse,
|
||||
credentials: AgentCredentials,
|
||||
integrationType: IntegrationType,
|
||||
): void {
|
||||
if (response?.agent?.agent_id !== credentials.agentId) {
|
||||
contractError('The registration response returned a different agent ID.');
|
||||
}
|
||||
if (response?.agent?.org_id !== credentials.orgId) {
|
||||
contractError('The registration response returned a different organization ID.');
|
||||
}
|
||||
if (response?.agent?.integration_type !== integrationType) {
|
||||
contractError('The registration response returned a different integration type.');
|
||||
}
|
||||
|
||||
const registeredKey = Array.isArray(response?.keys)
|
||||
? response.keys.find(({ kid }) => kid === credentials.kid)
|
||||
: undefined;
|
||||
if (!registeredKey || registeredKey.public_key !== credentials.publicKey) {
|
||||
contractError('The registration response did not confirm the generated public key.');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"continue": "Continue",
|
||||
"back": "Back",
|
||||
"signOut": "Sign Out",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
@@ -306,34 +307,40 @@
|
||||
"submitting": "Registering...",
|
||||
"submit": "Register Agent",
|
||||
"failedToRegister": "Failed to register agent.",
|
||||
"agentRegistered": "Agent Registered",
|
||||
"privateKeySaveNow": "Private Key — Save This Now",
|
||||
"privateKeyWarning": "This will not be shown again. Store it as an environment variable or in a secrets manager.",
|
||||
"apiToken": "API Token",
|
||||
"apiTokenDesc": "Use this as your ELYDORA_TOKEN environment variable. This is your bearer token for API authentication.",
|
||||
"tokenExpiration": "Token Expiration",
|
||||
"customDays": "Number of days",
|
||||
"days": "days",
|
||||
"enterValidDays": "Enter a valid number of days (minimum 1).",
|
||||
"enterValidDays": "Enter a whole number from 1 to 365.",
|
||||
"issuing": "Issuing...",
|
||||
"issueApiToken": "Issue API Token",
|
||||
"failedToIssueToken": "Failed to issue token.",
|
||||
"credAgentId": "Agent ID",
|
||||
"credKeyId": "Key ID",
|
||||
"credOrgId": "Org ID",
|
||||
"credAlgorithm": "Algorithm",
|
||||
"chooseIntegration": "Choose Integration",
|
||||
"chooseIntegrationDesc": "How will this agent connect to Elydora?",
|
||||
"supported": "Supported (one-command install)",
|
||||
"sdkIntegration": "SDK Integration",
|
||||
"install": "Install",
|
||||
"backToList": "Back",
|
||||
"sdkTutorial": "SDK Tutorial",
|
||||
"aiReadyPrompt": "AI-Ready Prompt",
|
||||
"aiReadyPromptDesc": "Copy this prompt and paste it into your AI agent to set up Elydora automatically.",
|
||||
"copyPrompt": "Copy Prompt",
|
||||
"done": "Done",
|
||||
"step": "Step {{current}} of {{total}}"
|
||||
"progress": "Agent registration progress",
|
||||
"steps": {
|
||||
"integration": "Integration",
|
||||
"details": "Details",
|
||||
"credentials": "Credentials",
|
||||
"connect": "Connect"
|
||||
},
|
||||
"integrationGroups": {
|
||||
"adapter": "CLI adapters",
|
||||
"sdk": "Custom SDK"
|
||||
},
|
||||
"integration": "Integration",
|
||||
"copyFailed": "Copy failed.",
|
||||
"invalidExpiration": "Select a valid expiration.",
|
||||
"tokenIssued": "Issued once. Store it securely.",
|
||||
"configure": "Configure",
|
||||
"record": "Record an operation",
|
||||
"verify": "Verify",
|
||||
"postInstall": "Post-install steps",
|
||||
"sdkLanguage": "SDK language"
|
||||
},
|
||||
"chain": {
|
||||
"title": "Chain Visualization",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"delete": "删除",
|
||||
"download": "下载",
|
||||
"continue": "继续",
|
||||
"back": "返回",
|
||||
"signOut": "退出登录",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
@@ -306,34 +307,40 @@
|
||||
"submitting": "注册中...",
|
||||
"submit": "注册代理",
|
||||
"failedToRegister": "注册代理失败。",
|
||||
"agentRegistered": "代理注册成功",
|
||||
"privateKeySaveNow": "私钥 — 请立即保存",
|
||||
"privateKeyWarning": "此私钥不会再次显示。请将其存储为环境变量或保存在密钥管理器中。",
|
||||
"apiToken": "API 令牌",
|
||||
"apiTokenDesc": "将其用作 ELYDORA_TOKEN 环境变量。这是您的 API 认证令牌。",
|
||||
"tokenExpiration": "令牌有效期",
|
||||
"customDays": "天数",
|
||||
"days": "天",
|
||||
"enterValidDays": "请输入有效的天数(最少 1 天)。",
|
||||
"enterValidDays": "请输入 1 至 365 的整数天数。",
|
||||
"issuing": "签发中...",
|
||||
"issueApiToken": "签发 API 令牌",
|
||||
"failedToIssueToken": "签发令牌失败。",
|
||||
"credAgentId": "代理 ID",
|
||||
"credKeyId": "密钥 ID",
|
||||
"credOrgId": "组织 ID",
|
||||
"credAlgorithm": "算法",
|
||||
"chooseIntegration": "选择集成方式",
|
||||
"chooseIntegrationDesc": "此代理将如何连接到 Elydora?",
|
||||
"supported": "支持的(一键安装)",
|
||||
"sdkIntegration": "SDK 集成",
|
||||
"install": "安装",
|
||||
"backToList": "返回",
|
||||
"sdkTutorial": "SDK 教程",
|
||||
"aiReadyPrompt": "AI 就绪提示",
|
||||
"aiReadyPromptDesc": "复制此提示并粘贴到您的 AI 代理中以自动设置 Elydora。",
|
||||
"copyPrompt": "复制提示",
|
||||
"done": "完成",
|
||||
"step": "第 {{current}} 步,共 {{total}} 步"
|
||||
"progress": "代理注册进度",
|
||||
"steps": {
|
||||
"integration": "集成",
|
||||
"details": "详情",
|
||||
"credentials": "凭据",
|
||||
"connect": "连接"
|
||||
},
|
||||
"integrationGroups": {
|
||||
"adapter": "CLI 适配器",
|
||||
"sdk": "自定义 SDK"
|
||||
},
|
||||
"integration": "集成",
|
||||
"copyFailed": "复制失败。",
|
||||
"invalidExpiration": "请选择有效期限。",
|
||||
"tokenIssued": "仅显示一次,请安全保存。",
|
||||
"configure": "配置",
|
||||
"record": "记录操作",
|
||||
"verify": "验证",
|
||||
"postInstall": "安装后步骤",
|
||||
"sdkLanguage": "SDK 语言"
|
||||
},
|
||||
"chain": {
|
||||
"title": "链可视化",
|
||||
|
||||
@@ -47,7 +47,14 @@ CREATE TABLE IF NOT EXISTS agents (
|
||||
org_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
responsible_entity TEXT NOT NULL,
|
||||
integration_type TEXT NOT NULL DEFAULT 'sdk',
|
||||
integration_type TEXT NOT NULL
|
||||
CONSTRAINT agents_integration_type_check
|
||||
CHECK (integration_type IN (
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot',
|
||||
'cursor', 'droid', 'gemini', 'grok', 'kimi', 'kirocli',
|
||||
'kiroide', 'letta', 'opencode', 'qwen', 'enterprise',
|
||||
'gui', 'sdk', 'other'
|
||||
)),
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Require every agent to declare a supported integration type.
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE agents
|
||||
ALTER COLUMN integration_type DROP DEFAULT;
|
||||
|
||||
ALTER TABLE agents
|
||||
DROP CONSTRAINT IF EXISTS agents_integration_type_check;
|
||||
|
||||
ALTER TABLE agents
|
||||
ADD CONSTRAINT agents_integration_type_check
|
||||
CHECK (integration_type IN (
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot',
|
||||
'cursor', 'droid', 'gemini', 'grok', 'kimi', 'kirocli',
|
||||
'kiroide', 'letta', 'opencode', 'qwen', 'enterprise',
|
||||
'gui', 'sdk', 'other'
|
||||
));
|
||||
|
||||
INSERT INTO schema_versions (version, applied_at, description)
|
||||
VALUES (
|
||||
2,
|
||||
EXTRACT(EPOCH FROM NOW())::BIGINT * 1000,
|
||||
'Require a supported agent integration type'
|
||||
)
|
||||
ON CONFLICT (version) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@@ -14,6 +14,7 @@
|
||||
"start": "node dist/index.js",
|
||||
"start:worker": "node dist/worker.js",
|
||||
"start:scheduler": "node dist/scheduler.js",
|
||||
"test": "npm run build && node --test test/*.test.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -98,17 +98,13 @@ agents.patch(
|
||||
const agentId = c.req.param('agent_id');
|
||||
const body = await c.req.json<UpdateAgentRequest>();
|
||||
|
||||
if (!body || !body.integration_type || typeof body.integration_type !== 'string') {
|
||||
throw new AppError(400, 'VALIDATION_ERROR', { key: 'agent.missingIntegrationType' });
|
||||
}
|
||||
|
||||
const orgId = c.get('org_id');
|
||||
const actor = c.get('actor');
|
||||
|
||||
const result = await agentService.updateAgentIntegrationType(
|
||||
c.env.ELYDORA_DB,
|
||||
agentId,
|
||||
body.integration_type,
|
||||
body?.integration_type,
|
||||
orgId,
|
||||
actor,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
INTEGRATION_TYPES,
|
||||
type AdminAction,
|
||||
type Agent,
|
||||
type IntegrationType,
|
||||
} from '../shared/index.js';
|
||||
import type { Database, PreparedStatement } from '../adapters/interfaces.js';
|
||||
import { AppError } from '../middleware/error-handler.js';
|
||||
import { generateUUIDv7 } from '../utils/uuid.js';
|
||||
|
||||
export function requireIntegrationType(value: unknown): IntegrationType {
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
throw new AppError(400, 'VALIDATION_ERROR', { key: 'agent.missingIntegrationType' });
|
||||
}
|
||||
if (!(INTEGRATION_TYPES as readonly string[]).includes(value)) {
|
||||
throw new AppError(
|
||||
400,
|
||||
'VALIDATION_ERROR',
|
||||
{
|
||||
key: 'agent.invalidIntegrationType',
|
||||
params: { value, valid: INTEGRATION_TYPES.join(', ') },
|
||||
},
|
||||
);
|
||||
}
|
||||
return value as IntegrationType;
|
||||
}
|
||||
|
||||
export async function updateAgentIntegrationType(
|
||||
db: Database,
|
||||
agentId: string,
|
||||
integrationValue: unknown,
|
||||
orgId: string,
|
||||
actor: string,
|
||||
): Promise<{ agent: Agent }> {
|
||||
const integrationType = requireIntegrationType(integrationValue);
|
||||
const now = Date.now();
|
||||
|
||||
const agentRow = await db
|
||||
.prepare('SELECT * FROM agents WHERE agent_id = ? AND org_id = ?')
|
||||
.bind(agentId, orgId)
|
||||
.first<Agent>();
|
||||
|
||||
if (!agentRow) {
|
||||
throw new AppError(404, 'NOT_FOUND', { key: 'agent.notFound', params: { id: agentId } });
|
||||
}
|
||||
|
||||
const statements: PreparedStatement[] = [];
|
||||
statements.push(
|
||||
db
|
||||
.prepare('UPDATE agents SET integration_type = ?, updated_at = ? WHERE agent_id = ? AND org_id = ?')
|
||||
.bind(integrationType, now, agentId, orgId),
|
||||
);
|
||||
|
||||
const eventId = generateUUIDv7();
|
||||
const action: AdminAction = 'agent.update';
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO admin_events (event_id, org_id, actor, action, target_type, target_id, details, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
eventId,
|
||||
orgId,
|
||||
actor,
|
||||
action,
|
||||
'agent',
|
||||
agentId,
|
||||
JSON.stringify({
|
||||
integration_type: integrationType,
|
||||
previous_integration_type: agentRow.integration_type,
|
||||
}),
|
||||
now,
|
||||
),
|
||||
);
|
||||
|
||||
await db.batch(statements);
|
||||
|
||||
return {
|
||||
agent: {
|
||||
...agentRow,
|
||||
integration_type: integrationType,
|
||||
updated_at: now,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,9 @@ import { generateUUIDv7 } from '../utils/uuid.js';
|
||||
import { base64urlDecode } from '../utils/crypto.js';
|
||||
import { AppError } from '../middleware/error-handler.js';
|
||||
import type { Database, PreparedStatement } from '../adapters/interfaces.js';
|
||||
import { requireIntegrationType } from './agent-integration.js';
|
||||
|
||||
export { updateAgentIntegrationType } from './agent-integration.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Register agent
|
||||
@@ -29,6 +32,7 @@ export async function registerAgent(
|
||||
orgId: string,
|
||||
actor: string,
|
||||
): Promise<RegisterAgentResponse> {
|
||||
const integrationType = requireIntegrationType(body.integration_type);
|
||||
const now = Date.now();
|
||||
|
||||
// Check if agent already exists
|
||||
@@ -68,7 +72,7 @@ export async function registerAgent(
|
||||
org_id: orgId,
|
||||
display_name: body.display_name ?? body.agent_id,
|
||||
responsible_entity: body.responsible_entity ?? '',
|
||||
integration_type: body.integration_type ?? 'sdk',
|
||||
integration_type: integrationType,
|
||||
status: 'active',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
@@ -197,81 +201,6 @@ export async function getAgent(
|
||||
return { agent: agentRow, keys };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Update agent integration type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const VALID_INTEGRATION_TYPES = [
|
||||
'sdk', 'claudecode', 'cursor', 'gemini', 'kirocli', 'kiroide',
|
||||
'opencode', 'copilot', 'letta', 'codex', 'kimi',
|
||||
'enterprise', 'gui', 'other',
|
||||
] as const;
|
||||
|
||||
export async function updateAgentIntegrationType(
|
||||
db: Database,
|
||||
agentId: string,
|
||||
integrationValue: string,
|
||||
orgId: string,
|
||||
actor: string,
|
||||
): Promise<{ agent: Agent }> {
|
||||
const now = Date.now();
|
||||
|
||||
const agentRow = await db
|
||||
.prepare('SELECT * FROM agents WHERE agent_id = ? AND org_id = ?')
|
||||
.bind(agentId, orgId)
|
||||
.first<Agent>();
|
||||
|
||||
if (!agentRow) {
|
||||
throw new AppError(404, 'NOT_FOUND', { key: 'agent.notFound', params: { id: agentId } });
|
||||
}
|
||||
|
||||
if (!(VALID_INTEGRATION_TYPES as readonly string[]).includes(integrationValue)) {
|
||||
throw new AppError(
|
||||
400,
|
||||
'VALIDATION_ERROR',
|
||||
{ key: 'agent.invalidIntegrationType', params: { value: integrationValue, valid: VALID_INTEGRATION_TYPES.join(', ') } },
|
||||
);
|
||||
}
|
||||
|
||||
const statements: PreparedStatement[] = [];
|
||||
|
||||
statements.push(
|
||||
db
|
||||
.prepare('UPDATE agents SET integration_type = ?, updated_at = ? WHERE agent_id = ?')
|
||||
.bind(integrationValue, now, agentId),
|
||||
);
|
||||
|
||||
const eventId = generateUUIDv7();
|
||||
const action: AdminAction = 'agent.update';
|
||||
statements.push(
|
||||
db
|
||||
.prepare(
|
||||
`INSERT INTO admin_events (event_id, org_id, actor, action, target_type, target_id, details, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.bind(
|
||||
eventId,
|
||||
orgId,
|
||||
actor,
|
||||
action,
|
||||
'agent',
|
||||
agentId,
|
||||
JSON.stringify({ integration_type: integrationValue, previous_integration_type: agentRow.integration_type }),
|
||||
now,
|
||||
),
|
||||
);
|
||||
|
||||
await db.batch(statements);
|
||||
|
||||
const updatedAgent: Agent = {
|
||||
...agentRow,
|
||||
integration_type: integrationValue,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
return { agent: updatedAgent };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Freeze agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,7 @@ export type {
|
||||
KeyStatus,
|
||||
ExportStatus,
|
||||
AdminAction,
|
||||
IntegrationType,
|
||||
RbacRole,
|
||||
ErrorCode,
|
||||
EOR,
|
||||
@@ -51,6 +52,8 @@ export type {
|
||||
ErrorResponse,
|
||||
} from './types/index.js';
|
||||
|
||||
export { INTEGRATION_TYPES } from './types/index.js';
|
||||
|
||||
export {
|
||||
ERROR_CODES,
|
||||
MAX_PAYLOAD_SIZE,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Agent, AgentKey, Epoch, Export, Operation, Organization, Receipt, User } from './entities.js';
|
||||
import type { ErrorCode } from './enums.js';
|
||||
import type { ErrorCode, IntegrationType } from './enums.js';
|
||||
import type { EAR, EOR } from './protocol.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -10,7 +10,7 @@ export interface RegisterAgentRequest {
|
||||
readonly agent_id: string;
|
||||
readonly display_name?: string;
|
||||
readonly responsible_entity?: string;
|
||||
readonly integration_type?: string;
|
||||
readonly integration_type: IntegrationType;
|
||||
readonly keys: ReadonlyArray<{
|
||||
readonly kid: string;
|
||||
readonly public_key: string;
|
||||
@@ -46,7 +46,7 @@ export interface RevokeAgentRequest {
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
readonly integration_type: string;
|
||||
readonly integration_type: IntegrationType;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentStatus, ExportStatus, KeyStatus, RbacRole } from './enums.js';
|
||||
import type { AgentStatus, ExportStatus, IntegrationType, KeyStatus, RbacRole } from './enums.js';
|
||||
|
||||
/** Registered AI agent within an organization */
|
||||
export interface Agent {
|
||||
@@ -6,7 +6,7 @@ export interface Agent {
|
||||
readonly org_id: string;
|
||||
readonly display_name: string;
|
||||
readonly responsible_entity: string;
|
||||
readonly integration_type: string;
|
||||
readonly integration_type: IntegrationType;
|
||||
readonly status: AgentStatus;
|
||||
readonly created_at: number;
|
||||
readonly updated_at: number;
|
||||
|
||||
@@ -19,10 +19,29 @@ export type AdminAction =
|
||||
| 'export.create';
|
||||
|
||||
/** Supported agent integration types */
|
||||
export type IntegrationType =
|
||||
| 'claudecode' | 'cursor' | 'gemini' | 'kirocli' | 'kiroide'
|
||||
| 'opencode' | 'copilot' | 'letta' | 'codex' | 'kimi'
|
||||
| 'enterprise' | 'gui' | 'sdk' | 'other';
|
||||
export const INTEGRATION_TYPES = [
|
||||
'augment',
|
||||
'claudecode',
|
||||
'cline',
|
||||
'codex',
|
||||
'copilot',
|
||||
'cursor',
|
||||
'droid',
|
||||
'gemini',
|
||||
'grok',
|
||||
'kimi',
|
||||
'kirocli',
|
||||
'kiroide',
|
||||
'letta',
|
||||
'opencode',
|
||||
'qwen',
|
||||
'enterprise',
|
||||
'gui',
|
||||
'sdk',
|
||||
'other',
|
||||
] as const;
|
||||
|
||||
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
|
||||
|
||||
/** RBAC roles for console access */
|
||||
export type RbacRole =
|
||||
|
||||
@@ -3,10 +3,13 @@ export type {
|
||||
KeyStatus,
|
||||
ExportStatus,
|
||||
AdminAction,
|
||||
IntegrationType,
|
||||
RbacRole,
|
||||
ErrorCode,
|
||||
} from './enums.js';
|
||||
|
||||
export { INTEGRATION_TYPES } from './enums.js';
|
||||
|
||||
export type {
|
||||
EOR,
|
||||
ECH,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { INTEGRATION_TYPES } from '../dist/shared/index.js';
|
||||
import {
|
||||
registerAgent,
|
||||
updateAgentIntegrationType,
|
||||
} from '../dist/services/agent-service.js';
|
||||
|
||||
const EXPECTED_INTEGRATION_TYPES = [
|
||||
'augment', 'claudecode', 'cline', 'codex', 'copilot', 'cursor', 'droid',
|
||||
'gemini', 'grok', 'kimi', 'kirocli', 'kiroide', 'letta', 'opencode', 'qwen',
|
||||
'enterprise', 'gui', 'sdk', 'other',
|
||||
];
|
||||
const PUBLIC_KEY = Buffer.alloc(32, 7).toString('base64url');
|
||||
|
||||
class FakeStatement {
|
||||
constructor(database, sql, values = []) {
|
||||
this.database = database;
|
||||
this.sql = sql;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
bind(...values) {
|
||||
return new FakeStatement(this.database, this.sql, values);
|
||||
}
|
||||
|
||||
async first() {
|
||||
return this.database.firstResult;
|
||||
}
|
||||
|
||||
async all() {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
async run() {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDatabase {
|
||||
constructor(firstResult = null) {
|
||||
this.firstResult = firstResult;
|
||||
this.prepareCount = 0;
|
||||
this.batches = [];
|
||||
}
|
||||
|
||||
prepare(sql) {
|
||||
this.prepareCount += 1;
|
||||
return new FakeStatement(this, sql);
|
||||
}
|
||||
|
||||
async batch(statements) {
|
||||
this.batches.push(statements);
|
||||
return statements.map(() => ({ results: [], success: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function registrationRequest(integrationType) {
|
||||
return {
|
||||
agent_id: `agent-${integrationType}`,
|
||||
integration_type: integrationType,
|
||||
display_name: 'Audit Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
keys: [{ kid: 'key-v1', public_key: PUBLIC_KEY, algorithm: 'ed25519' }],
|
||||
};
|
||||
}
|
||||
|
||||
async function expectIntegrationError(operation, messageKey) {
|
||||
await assert.rejects(operation, (error) => {
|
||||
assert.equal(error.name, 'AppError');
|
||||
assert.equal(error.statusCode, 400);
|
||||
assert.equal(error.errorCode, 'VALIDATION_ERROR');
|
||||
assert.equal(error.messageKey, messageKey);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
test('integration types match the public registration contract', () => {
|
||||
assert.deepEqual(INTEGRATION_TYPES, EXPECTED_INTEGRATION_TYPES);
|
||||
});
|
||||
|
||||
test('registration rejects missing and unknown integration types before database access', async () => {
|
||||
for (const [value, messageKey] of [
|
||||
[undefined, 'agent.missingIntegrationType'],
|
||||
['future-cli', 'agent.invalidIntegrationType'],
|
||||
]) {
|
||||
const database = new FakeDatabase();
|
||||
const request = registrationRequest(value);
|
||||
await expectIntegrationError(
|
||||
registerAgent(database, request, 'org-1', 'user-1'),
|
||||
messageKey,
|
||||
);
|
||||
assert.equal(database.prepareCount, 0);
|
||||
assert.equal(database.batches.length, 0);
|
||||
}
|
||||
|
||||
for (const [value, messageKey] of [
|
||||
[undefined, 'agent.missingIntegrationType'],
|
||||
['future-cli', 'agent.invalidIntegrationType'],
|
||||
]) {
|
||||
const database = new FakeDatabase();
|
||||
await expectIntegrationError(
|
||||
updateAgentIntegrationType(database, 'agent-1', value, 'org-1', 'user-1'),
|
||||
messageKey,
|
||||
);
|
||||
assert.equal(database.prepareCount, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('integration updates stay scoped to the authenticated organization', async () => {
|
||||
const existingAgent = {
|
||||
agent_id: 'agent-1',
|
||||
org_id: 'org-1',
|
||||
display_name: 'Audit Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
integration_type: 'sdk',
|
||||
status: 'active',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
};
|
||||
|
||||
for (const integrationType of INTEGRATION_TYPES) {
|
||||
const database = new FakeDatabase(existingAgent);
|
||||
const response = await updateAgentIntegrationType(
|
||||
database,
|
||||
existingAgent.agent_id,
|
||||
integrationType,
|
||||
existingAgent.org_id,
|
||||
'user-1',
|
||||
);
|
||||
|
||||
assert.equal(response.agent.integration_type, integrationType);
|
||||
const update = database.batches[0].find((statement) => (
|
||||
statement.sql.includes('UPDATE agents SET integration_type')
|
||||
));
|
||||
assert.ok(update);
|
||||
assert.match(update.sql, /WHERE agent_id = \? AND org_id = \?/);
|
||||
assert.equal(update.values[0], integrationType);
|
||||
assert.equal(update.values[2], existingAgent.agent_id);
|
||||
assert.equal(update.values[3], existingAgent.org_id);
|
||||
}
|
||||
});
|
||||
|
||||
test('registration persists every supported integration type without substitution', async () => {
|
||||
for (const integrationType of INTEGRATION_TYPES) {
|
||||
const database = new FakeDatabase();
|
||||
const response = await registerAgent(
|
||||
database,
|
||||
registrationRequest(integrationType),
|
||||
'org-1',
|
||||
'user-1',
|
||||
);
|
||||
|
||||
assert.equal(response.agent.integration_type, integrationType);
|
||||
assert.equal(database.batches.length, 1);
|
||||
const agentInsert = database.batches[0].find((statement) => (
|
||||
statement.sql.includes('INSERT INTO agents')
|
||||
));
|
||||
assert.ok(agentInsert);
|
||||
assert.equal(agentInsert.values[4], integrationType);
|
||||
}
|
||||
});
|
||||
@@ -189,6 +189,7 @@ agent, err := client.RegisterAgent(&elydora.RegisterAgentRequest{
|
||||
AgentID: "my-agent",
|
||||
DisplayName: "My Agent",
|
||||
ResponsibleEntity: "team@example.com",
|
||||
IntegrationType: elydora.IntegrationTypeSDK,
|
||||
Keys: []elydora.RegisterAgentKeyInput{
|
||||
{KID: "key-v1", PublicKey: "<base64url>", Algorithm: "ed25519"},
|
||||
},
|
||||
|
||||
@@ -7,6 +7,13 @@ import (
|
||||
|
||||
// RegisterAgent registers a new AI agent within the organization.
|
||||
func (c *Client) RegisterAgent(req *RegisterAgentRequest) (*RegisterAgentResponse, error) {
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("elydora: register agent request must not be nil")
|
||||
}
|
||||
if !req.IntegrationType.IsValid() {
|
||||
return nil, fmt.Errorf("elydora: invalid integration_type %q", req.IntegrationType)
|
||||
}
|
||||
|
||||
var result RegisterAgentResponse
|
||||
if err := c.doPost("/v1/agents/register", req, &result); err != nil {
|
||||
return nil, err
|
||||
|
||||
+124
-70
@@ -20,6 +20,58 @@ const (
|
||||
KeyStatusRevoked KeyStatus = "revoked"
|
||||
)
|
||||
|
||||
type IntegrationType string
|
||||
|
||||
const (
|
||||
IntegrationTypeAugment IntegrationType = "augment"
|
||||
IntegrationTypeClaudecode IntegrationType = "claudecode"
|
||||
IntegrationTypeCline IntegrationType = "cline"
|
||||
IntegrationTypeCodex IntegrationType = "codex"
|
||||
IntegrationTypeCopilot IntegrationType = "copilot"
|
||||
IntegrationTypeCursor IntegrationType = "cursor"
|
||||
IntegrationTypeDroid IntegrationType = "droid"
|
||||
IntegrationTypeGemini IntegrationType = "gemini"
|
||||
IntegrationTypeGrok IntegrationType = "grok"
|
||||
IntegrationTypeKimi IntegrationType = "kimi"
|
||||
IntegrationTypeKiroCLI IntegrationType = "kirocli"
|
||||
IntegrationTypeKiroIDE IntegrationType = "kiroide"
|
||||
IntegrationTypeLetta IntegrationType = "letta"
|
||||
IntegrationTypeOpenCode IntegrationType = "opencode"
|
||||
IntegrationTypeQwen IntegrationType = "qwen"
|
||||
IntegrationTypeEnterprise IntegrationType = "enterprise"
|
||||
IntegrationTypeGUI IntegrationType = "gui"
|
||||
IntegrationTypeSDK IntegrationType = "sdk"
|
||||
IntegrationTypeOther IntegrationType = "other"
|
||||
)
|
||||
|
||||
// IsValid reports whether the integration type belongs to the public registration contract.
|
||||
func (integrationType IntegrationType) IsValid() bool {
|
||||
switch integrationType {
|
||||
case IntegrationTypeAugment,
|
||||
IntegrationTypeClaudecode,
|
||||
IntegrationTypeCline,
|
||||
IntegrationTypeCodex,
|
||||
IntegrationTypeCopilot,
|
||||
IntegrationTypeCursor,
|
||||
IntegrationTypeDroid,
|
||||
IntegrationTypeGemini,
|
||||
IntegrationTypeGrok,
|
||||
IntegrationTypeKimi,
|
||||
IntegrationTypeKiroCLI,
|
||||
IntegrationTypeKiroIDE,
|
||||
IntegrationTypeLetta,
|
||||
IntegrationTypeOpenCode,
|
||||
IntegrationTypeQwen,
|
||||
IntegrationTypeEnterprise,
|
||||
IntegrationTypeGUI,
|
||||
IntegrationTypeSDK,
|
||||
IntegrationTypeOther:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type ExportStatus string
|
||||
|
||||
const (
|
||||
@@ -32,30 +84,30 @@ const (
|
||||
type RbacRole string
|
||||
|
||||
const (
|
||||
RbacRoleOrgOwner RbacRole = "org_owner"
|
||||
RbacRoleSecurityAdmin RbacRole = "security_admin"
|
||||
RbacRoleComplianceAuditor RbacRole = "compliance_auditor"
|
||||
RbacRoleReadonlyInvestigator RbacRole = "readonly_investigator"
|
||||
RbacRoleIntegrationEngineer RbacRole = "integration_engineer"
|
||||
RbacRoleOrgOwner RbacRole = "org_owner"
|
||||
RbacRoleSecurityAdmin RbacRole = "security_admin"
|
||||
RbacRoleComplianceAuditor RbacRole = "compliance_auditor"
|
||||
RbacRoleReadonlyInvestigator RbacRole = "readonly_investigator"
|
||||
RbacRoleIntegrationEngineer RbacRole = "integration_engineer"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorCodeInvalidSignature ErrorCode = "INVALID_SIGNATURE"
|
||||
ErrorCodeUnknownAgent ErrorCode = "UNKNOWN_AGENT"
|
||||
ErrorCodeKeyRevoked ErrorCode = "KEY_REVOKED"
|
||||
ErrorCodeAgentFrozen ErrorCode = "AGENT_FROZEN"
|
||||
ErrorCodeTTLExpired ErrorCode = "TTL_EXPIRED"
|
||||
ErrorCodeReplayDetected ErrorCode = "REPLAY_DETECTED"
|
||||
ErrorCodePrevHashMismatch ErrorCode = "PREV_HASH_MISMATCH"
|
||||
ErrorCodePayloadTooLarge ErrorCode = "PAYLOAD_TOO_LARGE"
|
||||
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||
ErrorCodeInternalError ErrorCode = "INTERNAL_ERROR"
|
||||
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
|
||||
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrorCodeValidationError ErrorCode = "VALIDATION_ERROR"
|
||||
ErrorCodeInvalidSignature ErrorCode = "INVALID_SIGNATURE"
|
||||
ErrorCodeUnknownAgent ErrorCode = "UNKNOWN_AGENT"
|
||||
ErrorCodeKeyRevoked ErrorCode = "KEY_REVOKED"
|
||||
ErrorCodeAgentFrozen ErrorCode = "AGENT_FROZEN"
|
||||
ErrorCodeTTLExpired ErrorCode = "TTL_EXPIRED"
|
||||
ErrorCodeReplayDetected ErrorCode = "REPLAY_DETECTED"
|
||||
ErrorCodePrevHashMismatch ErrorCode = "PREV_HASH_MISMATCH"
|
||||
ErrorCodePayloadTooLarge ErrorCode = "PAYLOAD_TOO_LARGE"
|
||||
ErrorCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||
ErrorCodeInternalError ErrorCode = "INTERNAL_ERROR"
|
||||
ErrorCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrorCodeForbidden ErrorCode = "FORBIDDEN"
|
||||
ErrorCodeNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrorCodeValidationError ErrorCode = "VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
type ExportFormat string
|
||||
@@ -70,13 +122,14 @@ const (
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Agent struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ResponsibleEntity string `json:"responsible_entity"`
|
||||
Status AgentStatus `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
AgentID string `json:"agent_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ResponsibleEntity string `json:"responsible_entity"`
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
Status AgentStatus `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AgentKey struct {
|
||||
@@ -90,23 +143,23 @@ type AgentKey struct {
|
||||
}
|
||||
|
||||
type Operation struct {
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
SeqNo int64 `json:"seq_no"`
|
||||
OperationType string `json:"operation_type"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
TTLMs int64 `json:"ttl_ms"`
|
||||
Nonce string `json:"nonce"`
|
||||
Subject string `json:"subject"`
|
||||
Action string `json:"action"`
|
||||
PayloadHash string `json:"payload_hash"`
|
||||
PrevChainHash string `json:"prev_chain_hash"`
|
||||
ChainHash string `json:"chain_hash"`
|
||||
AgentPubkeyKID string `json:"agent_pubkey_kid"`
|
||||
Signature string `json:"signature"`
|
||||
R2PayloadKey *string `json:"r2_payload_key"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
SeqNo int64 `json:"seq_no"`
|
||||
OperationType string `json:"operation_type"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
TTLMs int64 `json:"ttl_ms"`
|
||||
Nonce string `json:"nonce"`
|
||||
Subject string `json:"subject"`
|
||||
Action string `json:"action"`
|
||||
PayloadHash string `json:"payload_hash"`
|
||||
PrevChainHash string `json:"prev_chain_hash"`
|
||||
ChainHash string `json:"chain_hash"`
|
||||
AgentPubkeyKID string `json:"agent_pubkey_kid"`
|
||||
Signature string `json:"signature"`
|
||||
R2PayloadKey *string `json:"r2_payload_key"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type Receipt struct {
|
||||
@@ -161,36 +214,36 @@ type Export struct {
|
||||
|
||||
// EOR is the Elydora Operation Record — the fundamental unit of auditable activity.
|
||||
type EOR struct {
|
||||
OpVersion string `json:"op_version"`
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
TTLMs int64 `json:"ttl_ms"`
|
||||
Nonce string `json:"nonce"`
|
||||
OperationType string `json:"operation_type"`
|
||||
Subject interface{} `json:"subject"`
|
||||
Action interface{} `json:"action"`
|
||||
Payload interface{} `json:"payload"`
|
||||
PayloadHash string `json:"payload_hash"`
|
||||
PrevChainHash string `json:"prev_chain_hash"`
|
||||
AgentPubkeyKID string `json:"agent_pubkey_kid"`
|
||||
Signature string `json:"signature"`
|
||||
OpVersion string `json:"op_version"`
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
TTLMs int64 `json:"ttl_ms"`
|
||||
Nonce string `json:"nonce"`
|
||||
OperationType string `json:"operation_type"`
|
||||
Subject interface{} `json:"subject"`
|
||||
Action interface{} `json:"action"`
|
||||
Payload interface{} `json:"payload"`
|
||||
PayloadHash string `json:"payload_hash"`
|
||||
PrevChainHash string `json:"prev_chain_hash"`
|
||||
AgentPubkeyKID string `json:"agent_pubkey_kid"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// EAR is the Elydora Acknowledgment Receipt — server-issued receipt confirming acceptance.
|
||||
type EAR struct {
|
||||
ReceiptVersion string `json:"receipt_version"`
|
||||
ReceiptID string `json:"receipt_id"`
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
ServerReceivedAt int64 `json:"server_received_at"`
|
||||
SeqNo int64 `json:"seq_no"`
|
||||
ChainHash string `json:"chain_hash"`
|
||||
QueueMessageID string `json:"queue_message_id"`
|
||||
ReceiptHash string `json:"receipt_hash"`
|
||||
ElydoraKID string `json:"elydora_kid"`
|
||||
ReceiptVersion string `json:"receipt_version"`
|
||||
ReceiptID string `json:"receipt_id"`
|
||||
OperationID string `json:"operation_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
ServerReceivedAt int64 `json:"server_received_at"`
|
||||
SeqNo int64 `json:"seq_no"`
|
||||
ChainHash string `json:"chain_hash"`
|
||||
QueueMessageID string `json:"queue_message_id"`
|
||||
ReceiptHash string `json:"receipt_hash"`
|
||||
ElydoraKID string `json:"elydora_kid"`
|
||||
ElydoraSignature string `json:"elydora_signature"`
|
||||
}
|
||||
|
||||
@@ -208,6 +261,7 @@ type RegisterAgentRequest struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
ResponsibleEntity string `json:"responsible_entity,omitempty"`
|
||||
IntegrationType IntegrationType `json:"integration_type"`
|
||||
Keys []RegisterAgentKeyInput `json:"keys"`
|
||||
}
|
||||
|
||||
@@ -326,7 +380,7 @@ type CreateExportResponse struct {
|
||||
}
|
||||
|
||||
type GetExportResponse struct {
|
||||
Export Export `json:"export"`
|
||||
Export Export `json:"export"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package elydora
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegisterAgentRequestIncludesIntegrationType(t *testing.T) {
|
||||
payload, err := json.Marshal(RegisterAgentRequest{
|
||||
AgentID: "agent-1",
|
||||
IntegrationType: IntegrationTypeSDK,
|
||||
Keys: []RegisterAgentKeyInput{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal register agent request: %v", err)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal register agent request: %v", err)
|
||||
}
|
||||
if got := decoded["integration_type"]; got != "sdk" {
|
||||
t.Fatalf("integration_type = %v, want sdk", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationTypesMatchPublicAPIContract(t *testing.T) {
|
||||
got := []IntegrationType{
|
||||
IntegrationTypeAugment,
|
||||
IntegrationTypeClaudecode,
|
||||
IntegrationTypeCline,
|
||||
IntegrationTypeCodex,
|
||||
IntegrationTypeCopilot,
|
||||
IntegrationTypeCursor,
|
||||
IntegrationTypeDroid,
|
||||
IntegrationTypeGemini,
|
||||
IntegrationTypeGrok,
|
||||
IntegrationTypeKimi,
|
||||
IntegrationTypeKiroCLI,
|
||||
IntegrationTypeKiroIDE,
|
||||
IntegrationTypeLetta,
|
||||
IntegrationTypeOpenCode,
|
||||
IntegrationTypeQwen,
|
||||
IntegrationTypeEnterprise,
|
||||
IntegrationTypeGUI,
|
||||
IntegrationTypeSDK,
|
||||
IntegrationTypeOther,
|
||||
}
|
||||
want := []IntegrationType{
|
||||
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
|
||||
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
|
||||
"enterprise", "gui", "sdk", "other",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("integration types = %v, want %v", got, want)
|
||||
}
|
||||
for _, integrationType := range got {
|
||||
if !integrationType.IsValid() {
|
||||
t.Fatalf("integration type %q must be valid", integrationType)
|
||||
}
|
||||
}
|
||||
if IntegrationType("future-cli").IsValid() {
|
||||
t.Fatal("unknown integration type must be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterAgentRejectsInvalidIntegrationBeforeNetworkAccess(t *testing.T) {
|
||||
requestCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
requestCount++
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := &Client{baseURL: server.URL, httpClient: server.Client()}
|
||||
requests := []*RegisterAgentRequest{
|
||||
nil,
|
||||
{AgentID: "agent-1"},
|
||||
{AgentID: "agent-1", IntegrationType: IntegrationType("future-cli")},
|
||||
}
|
||||
for _, request := range requests {
|
||||
if _, err := client.RegisterAgent(request); err == nil {
|
||||
t.Fatalf("RegisterAgent(%+v) succeeded, want validation error", request)
|
||||
}
|
||||
}
|
||||
if requestCount != 0 {
|
||||
t.Fatalf("network request count = %d, want 0", requestCount)
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,7 @@ const result = await client.verifyOperation(operationId);
|
||||
// Register a new agent
|
||||
const agent = await client.registerAgent({
|
||||
agent_id: 'my-agent',
|
||||
integration_type: 'codex',
|
||||
display_name: 'My Agent',
|
||||
responsible_entity: 'team@example.com',
|
||||
keys: [{ kid: 'key-v1', public_key: '<base64url>', algorithm: 'ed25519' }],
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
ErrorResponse,
|
||||
} from './types.js';
|
||||
import { ElydoraError } from './errors.js';
|
||||
import { assertIntegrationType } from './integration-types.js';
|
||||
import {
|
||||
jcsCanonicalise,
|
||||
computePayloadHash,
|
||||
@@ -148,6 +149,7 @@ export class ElydoraClient {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
async registerAgent(request: RegisterAgentRequest): Promise<RegisterAgentResponse> {
|
||||
assertIntegrationType(request?.integration_type);
|
||||
return this.request<RegisterAgentResponse>('POST', '/v1/agents/register', request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { ElydoraClient } from './client.js';
|
||||
export { ElydoraError } from './errors.js';
|
||||
export { INTEGRATION_TYPES } from './types.js';
|
||||
|
||||
export {
|
||||
jcsCanonicalise,
|
||||
@@ -24,6 +25,7 @@ export type {
|
||||
KeyStatus,
|
||||
ExportStatus,
|
||||
RbacRole,
|
||||
IntegrationType,
|
||||
ErrorCode,
|
||||
|
||||
// Entities
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { INTEGRATION_TYPES, type IntegrationType } from './types.js';
|
||||
|
||||
export function assertIntegrationType(value: unknown): asserts value is IntegrationType {
|
||||
const isSupported = typeof value === 'string'
|
||||
&& INTEGRATION_TYPES.some((integrationType) => integrationType === value);
|
||||
if (!isSupported) {
|
||||
throw new TypeError(
|
||||
`Invalid integration_type "${String(value)}". Expected one of: ${INTEGRATION_TYPES.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,28 @@ export type RbacRole =
|
||||
| 'compliance_auditor'
|
||||
| 'readonly_investigator'
|
||||
| 'integration_engineer';
|
||||
export const INTEGRATION_TYPES = [
|
||||
'augment',
|
||||
'claudecode',
|
||||
'cline',
|
||||
'codex',
|
||||
'copilot',
|
||||
'cursor',
|
||||
'droid',
|
||||
'gemini',
|
||||
'grok',
|
||||
'kimi',
|
||||
'kirocli',
|
||||
'kiroide',
|
||||
'letta',
|
||||
'opencode',
|
||||
'qwen',
|
||||
'enterprise',
|
||||
'gui',
|
||||
'sdk',
|
||||
'other',
|
||||
] as const;
|
||||
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
|
||||
export type ErrorCode =
|
||||
| 'INVALID_SIGNATURE'
|
||||
| 'UNKNOWN_AGENT'
|
||||
@@ -36,6 +58,7 @@ export interface Agent {
|
||||
readonly org_id: string;
|
||||
readonly display_name: string;
|
||||
readonly responsible_entity: string;
|
||||
readonly integration_type: IntegrationType;
|
||||
readonly status: AgentStatus;
|
||||
readonly created_at: number;
|
||||
readonly updated_at: number;
|
||||
@@ -160,6 +183,7 @@ export interface EAR {
|
||||
|
||||
export interface RegisterAgentRequest {
|
||||
readonly agent_id: string;
|
||||
readonly integration_type: IntegrationType;
|
||||
readonly display_name?: string;
|
||||
readonly responsible_entity?: string;
|
||||
readonly keys: ReadonlyArray<{
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ElydoraClient, INTEGRATION_TYPES } from '../dist/index.js';
|
||||
|
||||
const EXPECTED_INTEGRATION_TYPES = [
|
||||
'augment',
|
||||
'claudecode',
|
||||
'cline',
|
||||
'codex',
|
||||
'copilot',
|
||||
'cursor',
|
||||
'droid',
|
||||
'gemini',
|
||||
'grok',
|
||||
'kimi',
|
||||
'kirocli',
|
||||
'kiroide',
|
||||
'letta',
|
||||
'opencode',
|
||||
'qwen',
|
||||
'enterprise',
|
||||
'gui',
|
||||
'sdk',
|
||||
'other',
|
||||
];
|
||||
|
||||
async function createFixture() {
|
||||
const requests = [];
|
||||
const server = http.createServer(async (request, response) => {
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(chunk);
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
requests.push({
|
||||
method: request.method,
|
||||
path: request.url,
|
||||
authorization: request.headers.authorization,
|
||||
body,
|
||||
});
|
||||
response.writeHead(201, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify({
|
||||
agent: {
|
||||
agent_id: body.agent_id,
|
||||
org_id: 'org-1',
|
||||
display_name: body.display_name,
|
||||
responsible_entity: body.responsible_entity,
|
||||
integration_type: body.integration_type,
|
||||
status: 'active',
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
},
|
||||
keys: [],
|
||||
}));
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const address = server.address();
|
||||
assert(address && typeof address === 'object');
|
||||
|
||||
const client = new ElydoraClient({
|
||||
orgId: 'org-1',
|
||||
agentId: 'admin-agent',
|
||||
privateKey: 'unused',
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
maxRetries: 0,
|
||||
});
|
||||
client.setToken('api-token');
|
||||
|
||||
return {
|
||||
client,
|
||||
requests,
|
||||
async close() {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('integration types match the public API contract', () => {
|
||||
assert.deepEqual(INTEGRATION_TYPES, EXPECTED_INTEGRATION_TYPES);
|
||||
});
|
||||
|
||||
test('agent registration sends integration_type in one authenticated request', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await assert.rejects(
|
||||
fixture.client.registerAgent({ agent_id: 'agent-1', keys: [] }),
|
||||
{
|
||||
name: 'TypeError',
|
||||
message: /Invalid integration_type "undefined"/,
|
||||
},
|
||||
);
|
||||
await assert.rejects(
|
||||
fixture.client.registerAgent({
|
||||
agent_id: 'agent-1',
|
||||
integration_type: 'future-cli',
|
||||
keys: [],
|
||||
}),
|
||||
{
|
||||
name: 'TypeError',
|
||||
message: /Invalid integration_type "future-cli"/,
|
||||
},
|
||||
);
|
||||
assert.equal(fixture.requests.length, 0);
|
||||
|
||||
const response = await fixture.client.registerAgent({
|
||||
agent_id: 'agent-1',
|
||||
integration_type: 'grok',
|
||||
display_name: 'Grok Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
keys: [{ kid: 'key-v1', public_key: 'public-key', algorithm: 'ed25519' }],
|
||||
});
|
||||
|
||||
assert.equal(response.agent.integration_type, 'grok');
|
||||
assert.deepEqual(fixture.requests, [{
|
||||
method: 'POST',
|
||||
path: '/v1/agents/register',
|
||||
authorization: 'Bearer api-token',
|
||||
body: {
|
||||
agent_id: 'agent-1',
|
||||
integration_type: 'grok',
|
||||
display_name: 'Grok Agent',
|
||||
responsible_entity: 'platform@example.com',
|
||||
keys: [{ kid: 'key-v1', public_key: 'public-key', algorithm: 'ed25519' }],
|
||||
},
|
||||
}]);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -179,6 +179,7 @@ result = client.verify_operation(operation_id)
|
||||
# Register a new agent
|
||||
agent = client.register_agent({
|
||||
"agent_id": "my-agent",
|
||||
"integration_type": "codex",
|
||||
"display_name": "My Agent",
|
||||
"responsible_entity": "team@example.com",
|
||||
"keys": [{"kid": "key-v1", "public_key": "<base64url>", "algorithm": "ed25519"}],
|
||||
|
||||
@@ -44,6 +44,8 @@ from .types import (
|
||||
GetMeResponse,
|
||||
IssueTokenResponse,
|
||||
HealthResponse,
|
||||
INTEGRATION_TYPES,
|
||||
IntegrationType,
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -93,4 +95,6 @@ __all__ = [
|
||||
"GetMeResponse",
|
||||
"IssueTokenResponse",
|
||||
"HealthResponse",
|
||||
"INTEGRATION_TYPES",
|
||||
"IntegrationType",
|
||||
]
|
||||
@@ -11,6 +11,7 @@ import aiohttp
|
||||
|
||||
from .crypto import compute_chain_hash, compute_payload_hash, sign_eor
|
||||
from .errors import ElydoraError
|
||||
from .integration_types import require_integration_type
|
||||
from .types import (
|
||||
AuditQueryResponse,
|
||||
AuthLoginResponse,
|
||||
@@ -231,6 +232,7 @@ class AsyncElydoraClient:
|
||||
|
||||
async def register_agent(self, request: RegisterAgentRequest) -> RegisterAgentResponse:
|
||||
"""Register a new agent with the organization."""
|
||||
require_integration_type(request.get("integration_type"))
|
||||
return await self._request("POST", "/v1/agents/register", json_body=request)
|
||||
|
||||
async def get_agent(self, agent_id: str) -> GetAgentResponse:
|
||||
|
||||
@@ -10,6 +10,7 @@ import requests
|
||||
|
||||
from .crypto import compute_chain_hash, compute_payload_hash, sign_eor
|
||||
from .errors import ElydoraError
|
||||
from .integration_types import require_integration_type
|
||||
from .types import (
|
||||
AuditQueryResponse,
|
||||
AuthLoginResponse,
|
||||
@@ -222,6 +223,7 @@ class ElydoraClient:
|
||||
|
||||
def register_agent(self, request: RegisterAgentRequest) -> RegisterAgentResponse:
|
||||
"""Register a new agent with the organization."""
|
||||
require_integration_type(request.get("integration_type"))
|
||||
return self._request("POST", "/v1/agents/register", json_body=request)
|
||||
|
||||
def get_agent(self, agent_id: str) -> GetAgentResponse:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Runtime validation for agent integration types."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from .types import INTEGRATION_TYPES, IntegrationType
|
||||
|
||||
|
||||
def require_integration_type(value: object) -> IntegrationType:
|
||||
if not isinstance(value, str) or value not in INTEGRATION_TYPES:
|
||||
expected = ", ".join(INTEGRATION_TYPES)
|
||||
raise ValueError(f"Invalid integration_type {value!r}. Expected one of: {expected}")
|
||||
return cast(IntegrationType, value)
|
||||
@@ -3,15 +3,13 @@
|
||||
All types use TypedDict for structural typing, compatible with Python 3.9+.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Literal, TypedDict
|
||||
from typing import Literal, NotRequired, TypedDict
|
||||
else:
|
||||
from typing_extensions import Literal, TypedDict
|
||||
from typing_extensions import Literal, NotRequired, TypedDict
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -28,6 +26,32 @@ RbacRole = Literal[
|
||||
"readonly_investigator",
|
||||
"integration_engineer",
|
||||
]
|
||||
IntegrationType = Literal[
|
||||
"augment",
|
||||
"claudecode",
|
||||
"cline",
|
||||
"codex",
|
||||
"copilot",
|
||||
"cursor",
|
||||
"droid",
|
||||
"gemini",
|
||||
"grok",
|
||||
"kimi",
|
||||
"kirocli",
|
||||
"kiroide",
|
||||
"letta",
|
||||
"opencode",
|
||||
"qwen",
|
||||
"enterprise",
|
||||
"gui",
|
||||
"sdk",
|
||||
"other",
|
||||
]
|
||||
INTEGRATION_TYPES: Tuple[IntegrationType, ...] = (
|
||||
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
|
||||
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
|
||||
"enterprise", "gui", "sdk", "other",
|
||||
)
|
||||
ErrorCode = Literal[
|
||||
"INVALID_SIGNATURE",
|
||||
"UNKNOWN_AGENT",
|
||||
@@ -55,6 +79,7 @@ class Agent(TypedDict):
|
||||
org_id: str
|
||||
display_name: str
|
||||
responsible_entity: str
|
||||
integration_type: IntegrationType
|
||||
status: AgentStatus
|
||||
created_at: int
|
||||
updated_at: int
|
||||
@@ -187,11 +212,12 @@ class RegisterAgentKeyParam(TypedDict):
|
||||
algorithm: Literal["ed25519"]
|
||||
|
||||
|
||||
class RegisterAgentRequest(TypedDict, total=False):
|
||||
class RegisterAgentRequest(TypedDict):
|
||||
agent_id: str
|
||||
display_name: str
|
||||
responsible_entity: str
|
||||
integration_type: IntegrationType
|
||||
keys: List[RegisterAgentKeyParam]
|
||||
display_name: NotRequired[str]
|
||||
responsible_entity: NotRequired[str]
|
||||
|
||||
|
||||
class AuditQueryParams(TypedDict, total=False):
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import get_args
|
||||
|
||||
import pytest
|
||||
|
||||
from elydora import (
|
||||
AsyncElydoraClient,
|
||||
ElydoraClient,
|
||||
INTEGRATION_TYPES,
|
||||
IntegrationType,
|
||||
RegisterAgentRequest,
|
||||
)
|
||||
|
||||
EXPECTED_INTEGRATION_TYPES = (
|
||||
"augment", "claudecode", "cline", "codex", "copilot", "cursor", "droid",
|
||||
"gemini", "grok", "kimi", "kirocli", "kiroide", "letta", "opencode", "qwen",
|
||||
"enterprise", "gui", "sdk", "other",
|
||||
)
|
||||
|
||||
|
||||
class RegistrationHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None:
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
body = json.loads(self.rfile.read(content_length))
|
||||
self.server.requests.append({
|
||||
"method": self.command,
|
||||
"path": self.path,
|
||||
"authorization": self.headers.get("Authorization"),
|
||||
"body": body,
|
||||
})
|
||||
response = json.dumps({
|
||||
"agent": {
|
||||
"agent_id": body["agent_id"],
|
||||
"org_id": "org-1",
|
||||
"display_name": body.get("display_name", ""),
|
||||
"responsible_entity": body.get("responsible_entity", ""),
|
||||
"integration_type": body["integration_type"],
|
||||
"status": "active",
|
||||
"created_at": 1,
|
||||
"updated_at": 1,
|
||||
},
|
||||
"keys": [],
|
||||
}).encode()
|
||||
self.send_response(201)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(response)))
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
def log_message(self, _format: str, *args: object) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registration_server():
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), RegistrationHandler)
|
||||
server.requests = []
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join()
|
||||
|
||||
|
||||
def registration_request():
|
||||
return {
|
||||
"agent_id": "agent-1",
|
||||
"integration_type": "grok",
|
||||
"display_name": "Grok Agent",
|
||||
"responsible_entity": "platform@example.com",
|
||||
"keys": [{"kid": "key-v1", "public_key": "public-key", "algorithm": "ed25519"}],
|
||||
}
|
||||
|
||||
|
||||
def expected_http_request():
|
||||
return {
|
||||
"method": "POST",
|
||||
"path": "/v1/agents/register",
|
||||
"authorization": "Bearer api-token",
|
||||
"body": registration_request(),
|
||||
}
|
||||
|
||||
|
||||
def test_integration_types_match_public_api_contract() -> None:
|
||||
assert INTEGRATION_TYPES == EXPECTED_INTEGRATION_TYPES
|
||||
assert get_args(IntegrationType) == EXPECTED_INTEGRATION_TYPES
|
||||
assert RegisterAgentRequest.__required_keys__ == frozenset({
|
||||
"agent_id", "integration_type", "keys",
|
||||
})
|
||||
assert RegisterAgentRequest.__optional_keys__ == frozenset({
|
||||
"display_name", "responsible_entity",
|
||||
})
|
||||
|
||||
|
||||
def test_sync_registration_requires_and_sends_integration_type(registration_server) -> None:
|
||||
host, port = registration_server.server_address
|
||||
client = ElydoraClient(
|
||||
"org-1", "admin-agent", "unused",
|
||||
base_url=f"http://{host}:{port}", max_retries=1, token="api-token",
|
||||
)
|
||||
with pytest.raises(ValueError, match="Invalid integration_type None"):
|
||||
client.register_agent({"agent_id": "agent-1", "keys": []})
|
||||
with pytest.raises(ValueError, match="Invalid integration_type 'future-cli'"):
|
||||
client.register_agent({
|
||||
"agent_id": "agent-1", "integration_type": "future-cli", "keys": [],
|
||||
})
|
||||
assert registration_server.requests == []
|
||||
|
||||
response = client.register_agent(registration_request())
|
||||
|
||||
assert response["agent"]["integration_type"] == "grok"
|
||||
assert registration_server.requests == [expected_http_request()]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_registration_requires_and_sends_integration_type(registration_server) -> None:
|
||||
host, port = registration_server.server_address
|
||||
client = AsyncElydoraClient(
|
||||
"org-1", "admin-agent", "unused",
|
||||
base_url=f"http://{host}:{port}", max_retries=1, token="api-token",
|
||||
)
|
||||
try:
|
||||
with pytest.raises(ValueError, match="Invalid integration_type None"):
|
||||
await client.register_agent({"agent_id": "agent-1", "keys": []})
|
||||
assert registration_server.requests == []
|
||||
|
||||
response = await client.register_agent(registration_request())
|
||||
|
||||
assert response["agent"]["integration_type"] == "grok"
|
||||
assert registration_server.requests == [expected_http_request()]
|
||||
finally:
|
||||
await client.close()
|
||||
@@ -22,7 +22,7 @@ const expectedProviderIds = [
|
||||
'qwen',
|
||||
];
|
||||
|
||||
const expectedCustomIntegrationIds = ['enterprise', 'gui', 'other', 'sdk'];
|
||||
const expectedCustomIntegrationIds = ['enterprise', 'gui', 'sdk', 'other'];
|
||||
const expectedAdapterKeys = ['go', 'node', 'python'];
|
||||
const adapterSourcePaths = {
|
||||
go: (id) => `sdks/go/cmd/elydora/plugins/${id}.go`,
|
||||
@@ -48,6 +48,16 @@ async function readJson(path) {
|
||||
return JSON.parse(await readFile(new URL(path, root), 'utf8'));
|
||||
}
|
||||
|
||||
async function readText(path) {
|
||||
return readFile(new URL(path, root), 'utf8');
|
||||
}
|
||||
|
||||
function extractQuotedValues(source, pattern, label) {
|
||||
const match = source.match(pattern);
|
||||
assert.ok(match, `${label} inventory was not found`);
|
||||
return [...match[1].matchAll(/["']([^"']+)["']/g)].map((entry) => entry[1]);
|
||||
}
|
||||
|
||||
async function sourceExists(path) {
|
||||
try {
|
||||
await readFile(new URL(path, root));
|
||||
@@ -80,6 +90,55 @@ test('catalog contains the complete, ordered integration inventory', async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test('server, SDK, and PostgreSQL contracts match the integration catalog', async () => {
|
||||
const catalog = await readJson('integrations/catalog.json');
|
||||
const expected = [
|
||||
...catalog.providers.map(({ id }) => id),
|
||||
...catalog.custom_integrations.map(({ id }) => id),
|
||||
];
|
||||
const enumSource = await readText('packages/server/src/shared/types/enums.ts');
|
||||
assert.deepEqual(
|
||||
extractQuotedValues(
|
||||
enumSource,
|
||||
/export const INTEGRATION_TYPES = \[([\s\S]*?)\] as const;/,
|
||||
'server enum',
|
||||
),
|
||||
expected,
|
||||
);
|
||||
|
||||
for (const [path, pattern] of [
|
||||
[
|
||||
'sdks/node/src/types.ts',
|
||||
/export const INTEGRATION_TYPES = \[([\s\S]*?)\] as const;/,
|
||||
],
|
||||
[
|
||||
'sdks/python/elydora/types.py',
|
||||
/INTEGRATION_TYPES:[^=]+\= \(([\s\S]*?)\)/,
|
||||
],
|
||||
[
|
||||
'sdks/go/types.go',
|
||||
/type IntegrationType string\s+const \(([\s\S]*?)\)/,
|
||||
],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
extractQuotedValues(await readText(path), pattern, path),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
|
||||
const constraintPattern = /CONSTRAINT agents_integration_type_check\s+CHECK\s+\(integration_type IN \(([\s\S]*?)\)\)/;
|
||||
for (const path of [
|
||||
'packages/server/migrations/001_initial.sql',
|
||||
'packages/server/migrations/002_agent_integration_contract.sql',
|
||||
'docker-compose.yml',
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
extractQuotedValues(await readText(path), constraintPattern, path),
|
||||
expected,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every provider exposes a complete, machine-readable hook contract', async () => {
|
||||
const catalog = await readJson('integrations/catalog.json');
|
||||
|
||||
|
||||
Reference in new issue
Block a user