Complete Better Auth migration, harden security, update all docs

Console:
- Rewrite auth from JWT decoding to Better Auth useSession()
- Add auth-client.ts with createAuthClient
- Remove elydora_token localStorage, dead auth API functions
- Fix login/register pages to use signIn.email/signUp.email

Server:
- Upgrade better-auth ^1.2.0 → ^1.5.5
- CORS: enforce allowlist on all environments
- Sync auth service and middleware with backend

SDKs:
- Deprecate login/register in Node, Python, Go SDKs
- Update READMEs to show direct token initialization

Config:
- Sanitize .env with dev-only placeholder values
- Update helm chart and install scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-25 00:48:57 -04:00
1 parent e8fa5e557e
commit 27a8f0667c
39 files changed
+1462 -641

No files matched your search

+2 -1
View File
@@ -20,7 +20,8 @@ PORT=8787
ENVIRONMENT=production ENVIRONMENT=production
API_VERSION=v1 API_VERSION=v1
PROTOCOL_VERSION=1.0 PROTOCOL_VERSION=1.0
JWT_SECRET=GENERATE_ME BETTER_AUTH_SECRET=GENERATE_ME
BETTER_AUTH_URL=http://localhost:8787
ELYDORA_SIGNING_KEY=GENERATE_ME ELYDORA_SIGNING_KEY=GENERATE_ME
ALLOWED_ORIGINS=http://localhost:3000 ALLOWED_ORIGINS=http://localhost:3000
+22 -27
View File
@@ -94,7 +94,7 @@ cd Elydora-Open-Source
The install script automatically: The install script automatically:
1. Checks prerequisites (Docker, Docker Compose, OpenSSL, curl) 1. Checks prerequisites (Docker, Docker Compose, OpenSSL, curl)
2. Generates a `.env` with fresh cryptographic secrets (Ed25519 signing key, JWT secret, database and object-store passwords) 2. Generates a `.env` with fresh cryptographic secrets (Ed25519 signing key, Better Auth secret, database and object-store passwords)
3. Starts all services via `docker compose up -d` 3. Starts all services via `docker compose up -d`
4. Waits for the API to become healthy 4. Waits for the API to become healthy
@@ -154,14 +154,15 @@ Elydora-Open-Source/
### Registration and Authentication Flow ### Registration and Authentication Flow
``` ```
1. Register organization and first user 1. Register organization and first user (Better Auth)
POST /v1/auth/register → { user, organization, token } POST /api/auth/sign-up/email → { user, session }
2. Log in to retrieve a JWT 2. Log in to retrieve a session token (Better Auth)
POST /v1/auth/login → { user, token } POST /api/auth/sign-in/email → { user, session }
3. Issue a long-lived API token (optional) 3. Issue a long-lived API token for SDK use
POST /v1/auth/token → { token, expires_at } POST /v1/auth/token → { token, expires_at }
Header: Authorization: Bearer <session-token>
4. Register an agent (as integration_engineer) 4. Register an agent (as integration_engineer)
POST /v1/agents/register → { agent, keys } POST /v1/agents/register → { agent, keys }
@@ -220,9 +221,11 @@ Errors are returned as:
### Authentication ### Authentication
#### `POST /v1/auth/register` Authentication is handled by Better Auth. Use the `/api/auth/` endpoints for session management, then issue API tokens for SDK use via `/v1/auth/token`.
Register a new user and create an organization. Returns a JWT for immediate use. #### `POST /api/auth/sign-up/email`
Register a new user. Organization setup is completed during onboarding after registration.
**Request body:** **Request body:**
@@ -230,36 +233,27 @@ Register a new user and create an organization. Returns a JWT for immediate use.
|-------|------|----------|-------------| |-------|------|----------|-------------|
| `email` | string | yes | User email address | | `email` | string | yes | User email address |
| `password` | string | yes | User password | | `password` | string | yes | User password |
| `display_name` | string | no | Human-readable name for the user | | `name` | string | no | Human-readable display name |
| `org_name` | string | no | Name for the new organization |
**Response:** **Response:** `{ "user": { ... }, "session": { ... } }`
```json
{
"user": { "user_id": "...", "email": "...", "role": "org_owner", ... },
"organization": { "org_id": "...", "name": "..." },
"token": "<jwt>"
}
```
--- ---
#### `POST /v1/auth/login` #### `POST /api/auth/sign-in/email`
Authenticate an existing user. Authenticate an existing user and receive a session token.
**Request body:** `{ "email": "...", "password": "..." }` **Request body:** `{ "email": "...", "password": "..." }`
**Response:** `{ "user": { ... }, "token": "<jwt>" }` **Response:** `{ "user": { ... }, "session": { "token": "<session-token>", ... } }`
--- ---
#### `GET /v1/auth/me` #### `GET /api/auth/session`
Return the authenticated user's profile. Requires a valid JWT. Return the current session and user profile. Requires a valid session token or cookie.
**Response:** `{ "user": { "user_id": "...", "email": "...", "role": "...", ... } }` **Response:** `{ "user": { ... }, "session": { ... } }`
--- ---
@@ -865,7 +859,8 @@ Elydora is configured via environment variables. Copy `.env.example` to `.env` a
| `STORAGE_BUCKET` | Bucket name for payloads, receipts, and exports | | `STORAGE_BUCKET` | Bucket name for payloads, receipts, and exports |
| `STORAGE_ACCESS_KEY_ID` | Object storage access key | | `STORAGE_ACCESS_KEY_ID` | Object storage access key |
| `STORAGE_SECRET_ACCESS_KEY` | Object storage secret key | | `STORAGE_SECRET_ACCESS_KEY` | Object storage secret key |
| `JWT_SECRET` | HMAC-SHA256 secret for signing user JWTs | | `BETTER_AUTH_SECRET` | Better Auth secret for session management |
| `BETTER_AUTH_URL` | Better Auth base URL (e.g. `http://localhost:8787`) |
| `SERVER_PRIVATE_KEY` | Base64url-encoded Ed25519 seed for signing EARs and JWKS | | `SERVER_PRIVATE_KEY` | Base64url-encoded Ed25519 seed for signing EARs and JWKS |
### Optional ### Optional
@@ -892,7 +887,7 @@ Elydora is configured via environment variables. Copy `.env.example` to `.env` a
**PBKDF2-SHA256 password hashing** — User passwords are hashed with PBKDF2-SHA256 before storage. **PBKDF2-SHA256 password hashing** — User passwords are hashed with PBKDF2-SHA256 before storage.
**HMAC-SHA256 JWT signing** — User sessions are signed with a server-side HMAC secret. Tokens cannot be forged without the secret. **Better Auth session-based authentication** — User sessions are managed by Better Auth with secure server-side session storage. Session tokens cannot be forged without the secret.
**RFC 3161 TSA anchoring** — Epoch Merkle roots are submitted to a public timestamp authority, providing independent third-party attestation of when records existed. This prevents retroactive alteration of historical data even by the platform operator. **RFC 3161 TSA anchoring** — Epoch Merkle roots are submitted to a public timestamp authority, providing independent third-party attestation of when records existed. This prevents retroactive alteration of historical data even by the platform operator.
+1 -1
View File
@@ -8,7 +8,7 @@ metadata:
type: Opaque type: Opaque
stringData: stringData:
DATABASE_URL: {{ include "elydora.databaseUrl" . | quote }} DATABASE_URL: {{ include "elydora.databaseUrl" . | quote }}
JWT_SECRET: {{ .Values.api.jwtSecret | quote }} BETTER_AUTH_SECRET: {{ .Values.api.betterAuthSecret | quote }}
ELYDORA_SIGNING_KEY: {{ .Values.api.signingKey | quote }} ELYDORA_SIGNING_KEY: {{ .Values.api.signingKey | quote }}
POSTGRES_PASSWORD: {{ .Values.postgres.password | quote }} POSTGRES_PASSWORD: {{ .Values.postgres.password | quote }}
MINIO_SECRET_KEY: {{ .Values.minio.rootPassword | quote }} MINIO_SECRET_KEY: {{ .Values.minio.rootPassword | quote }}
+1 -1
View File
@@ -77,7 +77,7 @@ minio:
api: api:
port: 8787 port: 8787
environment: production environment: production
jwtSecret: change-me-to-a-random-string-at-least-32-chars betterAuthSecret: change-me-to-a-random-string-at-least-32-chars
signingKey: change-me-base64url-ed25519-private-key signingKey: change-me-base64url-ed25519-private-key
allowedOrigins: "https://console.example.com" allowedOrigins: "https://console.example.com"
tsaUrl: "http://timestamp.sectigo.com" tsaUrl: "http://timestamp.sectigo.com"
+363
View File
@@ -8,6 +8,7 @@
"name": "elydora-console", "name": "elydora-console",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"better-auth": "^1.5.5",
"i18next": "^25.8.13", "i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"next": "^16.1.6", "next": "^16.1.6",
@@ -48,6 +49,134 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"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==",
"license": "MIT",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.39.0",
"@standard-schema/spec": "^1.1.0",
"zod": "^4.3.6"
},
"peerDependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21",
"@cloudflare/workers-types": ">=4",
"@opentelemetry/api": "^1.9.0",
"better-call": "1.3.2",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"drizzle-orm": ">=0.41.0"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"kysely": "^0.27.0 || ^0.28.0"
},
"peerDependenciesMeta": {
"kysely": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0"
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@prisma/client": {
"optional": true
},
"prisma": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21"
},
"peerDependencies": {
"@better-auth/core": "1.5.6"
}
},
"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"
},
"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=="
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
@@ -708,6 +837,55 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@noble/ciphers": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"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==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1030,6 +1208,131 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"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==",
"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",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"better-call": "1.3.2",
"defu": "^6.1.4",
"jose": "^6.1.3",
"kysely": "^0.28.12",
"nanostores": "^1.1.1",
"zod": "^4.3.6"
},
"peerDependencies": {
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": ">=0.41.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
"pg": "^8.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@tanstack/solid-start": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"mongodb": {
"optional": true
},
"mysql2": {
"optional": true
},
"next": {
"optional": true
},
"pg": {
"optional": true
},
"prisma": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vitest": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "^0.3.1",
"@better-fetch/fetch": "^1.1.21",
"rou3": "^0.7.12",
"set-cookie-parser": "^3.0.1"
},
"peerDependencies": {
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001774", "version": "1.0.30001774",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz",
@@ -1063,6 +1366,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1162,6 +1471,24 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"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==",
"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==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.31.1", "version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
@@ -1451,6 +1778,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/nanostores": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
"integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/next": { "node_modules/next": {
"version": "16.1.6", "version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
@@ -1615,6 +1957,12 @@
} }
} }
}, },
"node_modules/rou3": {
"version": "0.7.12",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -1634,6 +1982,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"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==",
"license": "MIT"
},
"node_modules/sharp": { "node_modules/sharp": {
"version": "0.34.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -1789,6 +2143,15 @@
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+1
View File
@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"better-auth": "^1.5.5",
"i18next": "^25.8.13", "i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"next": "^16.1.6", "next": "^16.1.6",
+5 -5
View File
@@ -3,14 +3,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { api } from '@/lib/api'; import { signIn } from '@/lib/auth-client';
import { useAuth } from '@/lib/auth';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function LoginPage() { export default function LoginPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { login } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -21,8 +19,10 @@ export default function LoginPage() {
setError(''); setError('');
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const result = await api.auth.login(email, password); const result = await signIn.email({ email, password });
login(result.token, { display_name: result.user.display_name, email: result.user.email }); if (result.error) {
throw new Error(result.error.message ?? t('login.loginFailed'));
}
router.push('/'); router.push('/');
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : t('login.loginFailed')); setError(err instanceof Error ? err.message : t('login.loginFailed'));
+5 -5
View File
@@ -3,14 +3,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { api } from '@/lib/api'; import { signUp } from '@/lib/auth-client';
import { useAuth } from '@/lib/auth';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function RegisterPage() { export default function RegisterPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { login } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState(''); const [displayName, setDisplayName] = useState('');
@@ -23,8 +21,10 @@ export default function RegisterPage() {
setError(''); setError('');
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const result = await api.auth.register(email, password, displayName, orgName); const result = await signUp.email({ email, password, name: displayName });
login(result.token, { display_name: result.user.display_name, email: result.user.email }); if (result.error) {
throw new Error(result.error.message ?? t('register.registrationFailed'));
}
router.push('/'); router.push('/');
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : t('register.registrationFailed')); setError(err instanceof Error ? err.message : t('register.registrationFailed'));
@@ -76,7 +76,7 @@ const INTEGRATIONS: Integration[] = [
export default function AgentRegistrationForm({ onSuccess, onCancel }: AgentRegistrationFormProps) { export default function AgentRegistrationForm({ onSuccess, onCancel }: AgentRegistrationFormProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { user, token: authToken } = useAuth(); const { user } = useAuth();
const [displayName, setDisplayName] = useState(''); const [displayName, setDisplayName] = useState('');
const [responsibleEntity, setResponsibleEntity] = useState(''); const [responsibleEntity, setResponsibleEntity] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -186,9 +186,9 @@ export default function AgentRegistrationForm({ onSuccess, onCancel }: AgentRegi
const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787'; const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:8787';
// CLI install commands for supported integrations // CLI install commands for supported integrations
const buildCliCommands = (integration: Integration) => ({ const buildCliCommands = (integration: Integration) => ({
node: `npx @elydora/sdk install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base_url "${apiBaseUrl}"`, node: `npx @elydora/sdk install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base_url "${apiBaseUrl}"`,
python: `pip install elydora && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base_url "${apiBaseUrl}"`, python: `pip install elydora && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base_url "${apiBaseUrl}"`,
go: `go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? authToken ?? ''}" --base-url "${apiBaseUrl}"`, go: `go install github.com/Elydora-Infrastructure/Elydora-Go-SDK/cmd/elydora@latest && elydora install --agent ${integration.agentFlag} --org_id "${creds.orgId}" --agent_id "${creds.agentId}" --private_key "${creds.privateKey}" --kid "${creds.kid}" --token "${apiToken ?? ''}" --base-url "${apiBaseUrl}"`,
}); });
// SDK step-by-step tutorial (used for unsupported / SDK integrations) // SDK step-by-step tutorial (used for unsupported / SDK integrations)
@@ -401,7 +401,7 @@ export const client = new ElydoraClient({
kid: '${creds.kid}', kid: '${creds.kid}',
baseUrl: '${apiBaseUrl}', baseUrl: '${apiBaseUrl}',
}); });
client.setToken('${apiToken ?? authToken ?? ''}'); client.setToken('${apiToken ?? ''}');
client.prevChainHash = readChainState(); client.prevChainHash = readChainState();
export async function recordOperation({ operationType, subject, action, payload, verify = false }) { export async function recordOperation({ operationType, subject, action, payload, verify = false }) {
@@ -792,7 +792,7 @@ export const client = new ElydoraClient({
kid: '${creds.kid}', kid: '${creds.kid}',
baseUrl: '${apiBaseUrl}', baseUrl: '${apiBaseUrl}',
}); });
client.setToken('${apiToken ?? authToken ?? ''}'); client.setToken('${apiToken ?? ''}');
client.prevChainHash = readChainState(); client.prevChainHash = readChainState();
export async function recordOperation({ operationType, subject, action, payload, verify = false }) { export async function recordOperation({ operationType, subject, action, payload, verify = false }) {
+4 -41
View File
@@ -18,10 +18,6 @@ import type {
CreateExportRequest, CreateExportRequest,
CreateExportResponse, CreateExportResponse,
GetExportResponse, GetExportResponse,
AuthLoginResponse,
AuthRegisterResponse,
AuthMeResponse,
AuthRefreshResponse,
IssueTokenResponse, IssueTokenResponse,
JWKSResponse, JWKSResponse,
ErrorResponse, ErrorResponse,
@@ -42,34 +38,26 @@ class ApiError extends Error {
} }
} }
function getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('elydora_token');
}
async function request<T>( async function request<T>(
path: string, path: string,
options: RequestInit = {}, options: RequestInit = {},
): Promise<T> { ): Promise<T> {
const token = getAuthToken();
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers as Record<string, string> ?? {}), ...(options.headers as Record<string, string> ?? {}),
}; };
const response = await fetch(`${API_BASE_URL}${path}`, { const response = await fetch(`${API_BASE_URL}${path}`, {
...options, ...options,
headers, headers,
credentials: 'include',
}); });
if (!response.ok) { if (!response.ok) {
// On 401, clear stored tokens and redirect to login // On 401, redirect to login (session expired or not authenticated)
if (response.status === 401 && typeof window !== 'undefined') { if (response.status === 401 && typeof window !== 'undefined') {
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
window.location.href = '/login'; window.location.href = '/login';
return undefined as T;
} }
let errorBody: ErrorResponse | null = null; let errorBody: ErrorResponse | null = null;
@@ -213,11 +201,10 @@ export const api = {
}, },
async download(exportId: string): Promise<Blob> { async download(exportId: string): Promise<Blob> {
const token = getAuthToken();
const response = await fetch( const response = await fetch(
`${API_BASE_URL}/v1/exports/${encodeURIComponent(exportId)}/download`, `${API_BASE_URL}/v1/exports/${encodeURIComponent(exportId)}/download`,
{ {
headers: token ? { Authorization: `Bearer ${token}` } : {}, credentials: 'include',
}, },
); );
if (!response.ok) { if (!response.ok) {
@@ -234,30 +221,6 @@ export const api = {
}, },
auth: { auth: {
login(email: string, password: string): Promise<AuthLoginResponse> {
return request<AuthLoginResponse>('/v1/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
},
register(email: string, password: string, displayName: string, orgName: string): Promise<AuthRegisterResponse> {
return request<AuthRegisterResponse>('/v1/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, display_name: displayName, org_name: orgName }),
});
},
me(): Promise<AuthMeResponse> {
return request<AuthMeResponse>('/v1/auth/me');
},
refresh(): Promise<AuthRefreshResponse> {
return request<AuthRefreshResponse>('/v1/auth/refresh', {
method: 'POST',
});
},
issueToken(ttlSeconds: number | null): Promise<IssueTokenResponse> { issueToken(ttlSeconds: number | null): Promise<IssueTokenResponse> {
return request<IssueTokenResponse>('/v1/auth/token', { return request<IssueTokenResponse>('/v1/auth/token', {
method: 'POST', method: 'POST',
+11
View File
@@ -0,0 +1,11 @@
import { createAuthClient } from 'better-auth/react';
import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8787',
plugins: [
organizationClient(),
],
});
export const { signIn, signUp, signOut, useSession } = authClient;
+51 -69
View File
@@ -1,101 +1,83 @@
'use client'; 'use client';
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { createContext, useContext, useMemo, useCallback } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useSession, signOut } from '@/lib/auth-client';
import type { RbacRole } from '@elydora/shared'; import type { RbacRole } from '@elydora/shared';
interface AuthUser { interface AuthUser {
id: string;
sub: string; sub: string;
org_id: string; org_id: string;
role: RbacRole; role: RbacRole;
display_name?: string; display_name?: string;
email?: string; email?: string;
onboarding_completed: boolean;
} }
interface AuthContextValue { interface AuthContextValue {
user: AuthUser | null; user: AuthUser | null;
token: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
login: (token: string, user?: { display_name?: string; email?: string }) => void;
logout: () => void; logout: () => void;
isAdminRole: boolean;
canManageMembers: boolean;
canManageAgents: boolean;
canViewAudit: boolean;
} }
const AuthContext = createContext<AuthContextValue | null>(null); const AuthContext = createContext<AuthContextValue | null>(null);
function decodeJWTPayload(token: string): AuthUser | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
// base64url decode
let base64 = parts[1]!.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4 !== 0) base64 += '=';
const json = atob(base64);
const payload = JSON.parse(json);
// Check expiration
const nowSeconds = Math.floor(Date.now() / 1000);
if (typeof payload.exp === 'number' && payload.exp < nowSeconds) return null;
if (!payload.sub || !payload.org_id || !payload.role) return null;
return { sub: payload.sub, org_id: payload.org_id, role: payload.role };
} catch {
return null;
}
}
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null); const { data: session, isPending } = useSession();
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { const user: AuthUser | null = useMemo(() => {
const stored = localStorage.getItem('elydora_token'); if (!session?.user) return null;
if (stored) {
const decoded = decodeJWTPayload(stored);
if (decoded) {
setToken(stored);
// Try to fetch full user profile from localStorage
const displayName = localStorage.getItem('elydora_display_name');
const email = localStorage.getItem('elydora_email');
if (displayName) decoded.display_name = displayName;
if (email) decoded.email = email;
setUser({ ...decoded });
} else {
// Token expired or invalid, clear it
localStorage.removeItem('elydora_token');
localStorage.removeItem('elydora_display_name');
localStorage.removeItem('elydora_email');
}
}
setIsLoading(false);
}, []);
const login = useCallback((newToken: string, userInfo?: { display_name?: string; email?: string }) => { const sessionUser = session.user as Record<string, unknown>;
localStorage.setItem('elydora_token', newToken); const sessionData = session.session as Record<string, unknown>;
const decoded = decodeJWTPayload(newToken);
if (decoded) {
if (userInfo?.display_name) {
decoded.display_name = userInfo.display_name;
localStorage.setItem('elydora_display_name', userInfo.display_name);
}
if (userInfo?.email) {
decoded.email = userInfo.email;
localStorage.setItem('elydora_email', userInfo.email);
}
setUser(decoded);
}
setToken(newToken);
}, []);
const logout = useCallback(() => { const orgId =
localStorage.removeItem('elydora_token'); (sessionData.activeOrganizationId as string) ??
localStorage.removeItem('elydora_display_name'); (sessionUser.org_id as string) ??
localStorage.removeItem('elydora_email'); '';
setUser(null); const role =
setToken(null); (sessionUser.role as RbacRole) ?? 'readonly_investigator';
return {
id: session.user.id,
sub: session.user.id,
org_id: orgId,
role,
display_name: session.user.name || undefined,
email: session.user.email || undefined,
onboarding_completed: !!(sessionUser.onboarding_completed),
};
}, [session]);
const isAdminRole = user?.role === 'org_owner' || user?.role === 'security_admin';
const canManageMembers = isAdminRole;
const canManageAgents = isAdminRole || user?.role === 'integration_engineer';
const canViewAudit = isAdminRole || user?.role === 'compliance_auditor';
const logout = useCallback(async () => {
await signOut();
window.location.href = '/login';
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ user, token, isAuthenticated: !!user, isLoading, login, logout }}> <AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isLoading: isPending,
logout,
isAdminRole: !!isAdminRole,
canManageMembers: !!canManageMembers,
canManageAgents: !!canManageAgents,
canViewAudit: !!canViewAudit,
}}
>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );
+363
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.0.0", "@aws-sdk/client-s3": "^3.0.0",
"@hono/node-server": "^1.13.0", "@hono/node-server": "^1.13.0",
"better-auth": "^1.5.5",
"bullmq": "^5.0.0", "bullmq": "^5.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"hono": "^4.7.0", "hono": "^4.7.0",
@@ -874,6 +875,134 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"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==",
"license": "MIT",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.39.0",
"@standard-schema/spec": "^1.1.0",
"zod": "^4.3.6"
},
"peerDependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21",
"@cloudflare/workers-types": ">=4",
"@opentelemetry/api": "^1.9.0",
"better-call": "1.3.2",
"jose": "^6.1.0",
"kysely": "^0.28.5",
"nanostores": "^1.0.1"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"drizzle-orm": ">=0.41.0"
},
"peerDependenciesMeta": {
"drizzle-orm": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"kysely": "^0.27.0 || ^0.28.0"
},
"peerDependenciesMeta": {
"kysely": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0"
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"mongodb": "^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"peerDependencies": {
"@better-auth/core": "1.5.6",
"@better-auth/utils": "^0.3.0",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@prisma/client": {
"optional": true
},
"prisma": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "0.3.1",
"@better-fetch/fetch": "1.1.21"
},
"peerDependencies": {
"@better-auth/core": "1.5.6"
}
},
"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"
},
"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=="
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
@@ -1412,6 +1541,49 @@
"win32" "win32"
] ]
}, },
"node_modules/@noble/ciphers": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"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==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@smithy/abort-controller": { "node_modules/@smithy/abort-controller": {
"version": "4.2.10", "version": "4.2.10",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz",
@@ -2144,6 +2316,12 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.35", "version": "20.19.35",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz",
@@ -2173,6 +2351,131 @@
"pg-types": "^2.2.0" "pg-types": "^2.2.0"
} }
}, },
"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==",
"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",
"@noble/ciphers": "^2.1.1",
"@noble/hashes": "^2.0.1",
"better-call": "1.3.2",
"defu": "^6.1.4",
"jose": "^6.1.3",
"kysely": "^0.28.12",
"nanostores": "^1.1.1",
"zod": "^4.3.6"
},
"peerDependencies": {
"@lynx-js/react": "*",
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@tanstack/react-start": "^1.0.0",
"@tanstack/solid-start": "^1.0.0",
"better-sqlite3": "^12.0.0",
"drizzle-kit": ">=0.31.4",
"drizzle-orm": ">=0.41.0",
"mongodb": "^6.0.0 || ^7.0.0",
"mysql2": "^3.0.0",
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
"pg": "^8.0.0",
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"solid-js": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0",
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"@tanstack/react-start": {
"optional": true
},
"@tanstack/solid-start": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"drizzle-kit": {
"optional": true
},
"drizzle-orm": {
"optional": true
},
"mongodb": {
"optional": true
},
"mysql2": {
"optional": true
},
"next": {
"optional": true
},
"pg": {
"optional": true
},
"prisma": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
},
"solid-js": {
"optional": true
},
"svelte": {
"optional": true
},
"vitest": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"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==",
"license": "MIT",
"dependencies": {
"@better-auth/utils": "^0.3.1",
"@better-fetch/fetch": "^1.1.21",
"rou3": "^0.7.12",
"set-cookie-parser": "^3.0.1"
},
"peerDependencies": {
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/bowser": { "node_modules/bowser": {
"version": "2.14.1", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
@@ -2262,6 +2565,12 @@
} }
} }
}, },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/denque": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -2414,6 +2723,24 @@
"url": "https://opencollective.com/ioredis" "url": "https://opencollective.com/ioredis"
} }
}, },
"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==",
"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==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/lodash.defaults": { "node_modules/lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -2472,6 +2799,21 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
} }
}, },
"node_modules/nanostores": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
"integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/node-abort-controller": { "node_modules/node-abort-controller": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
@@ -2673,6 +3015,12 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
} }
}, },
"node_modules/rou3": {
"version": "0.7.12",
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -2685,6 +3033,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"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==",
"license": "MIT"
},
"node_modules/split2": { "node_modules/split2": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -2780,6 +3134,15 @@
"engines": { "engines": {
"node": ">=0.4" "node": ">=0.4"
} }
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+1
View File
@@ -15,6 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.0.0", "@aws-sdk/client-s3": "^3.0.0",
"better-auth": "^1.5.5",
"@hono/node-server": "^1.13.0", "@hono/node-server": "^1.13.0",
"bullmq": "^5.0.0", "bullmq": "^5.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
-36
View File
@@ -89,42 +89,6 @@ const messages: Record<string, Record<Lang, string>> = {
en: 'Empty Bearer token', en: 'Empty Bearer token',
zh: 'Bearer \u4ee4\u724c\u4e3a\u7a7a', zh: 'Bearer \u4ee4\u724c\u4e3a\u7a7a',
}, },
'auth.malformedJwt': {
en: 'Malformed JWT: expected 3 parts',
zh: 'JWT \u683c\u5f0f\u9519\u8bef\uff1a\u5e94\u5305\u542b 3 \u4e2a\u90e8\u5206',
},
'auth.invalidSignature': {
en: 'Invalid JWT signature',
zh: 'JWT \u7b7e\u540d\u65e0\u6548',
},
'auth.malformedPayload': {
en: 'Malformed JWT payload',
zh: 'JWT \u8d1f\u8f7d\u683c\u5f0f\u9519\u8bef',
},
'auth.missingSub': {
en: 'JWT missing required "sub" claim',
zh: 'JWT \u7f3a\u5c11\u5fc5\u9700\u7684 "sub" \u58f0\u660e',
},
'auth.missingOrgId': {
en: 'JWT missing required "org_id" claim',
zh: 'JWT \u7f3a\u5c11\u5fc5\u9700\u7684 "org_id" \u58f0\u660e',
},
'auth.missingRole': {
en: 'JWT missing required "role" claim',
zh: 'JWT \u7f3a\u5c11\u5fc5\u9700\u7684 "role" \u58f0\u660e',
},
'auth.missingExp': {
en: 'JWT missing required "exp" claim',
zh: 'JWT \u7f3a\u5c11\u5fc5\u9700\u7684 "exp" \u58f0\u660e',
},
'auth.expired': {
en: 'JWT has expired',
zh: 'JWT \u5df2\u8fc7\u671f',
},
'auth.unsupportedAlgorithm': {
en: 'Unsupported JWT algorithm. Only HS256 is accepted.',
zh: '\u4e0d\u652f\u6301\u7684 JWT \u7b97\u6cd5\u3002\u4ec5\u63a5\u53d7 HS256\u3002',
},
'auth.ttlMustBePositive': { 'auth.ttlMustBePositive': {
en: 'ttl_seconds must be a positive number', en: 'ttl_seconds must be a positive number',
zh: 'ttl_seconds \u5fc5\u987b\u4e3a\u6b63\u6570', zh: 'ttl_seconds \u5fc5\u987b\u4e3a\u6b63\u6570',
+5 -7
View File
@@ -48,7 +48,7 @@ const queue = new BullMQAdapter({
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const requiredSecrets = [ const requiredSecrets = [
{ name: 'JWT_SECRET', value: process.env.JWT_SECRET }, { name: 'BETTER_AUTH_SECRET', value: process.env.BETTER_AUTH_SECRET },
{ name: 'ELYDORA_SIGNING_KEY', value: process.env.ELYDORA_SIGNING_KEY }, { name: 'ELYDORA_SIGNING_KEY', value: process.env.ELYDORA_SIGNING_KEY },
]; ];
for (const { name, value } of requiredSecrets) { for (const { name, value } of requiredSecrets) {
@@ -70,7 +70,8 @@ const env: Env = {
ENVIRONMENT: process.env.ENVIRONMENT ?? 'production', ENVIRONMENT: process.env.ENVIRONMENT ?? 'production',
API_VERSION: process.env.API_VERSION ?? 'v1', API_VERSION: process.env.API_VERSION ?? 'v1',
PROTOCOL_VERSION: process.env.PROTOCOL_VERSION ?? '1.0', PROTOCOL_VERSION: process.env.PROTOCOL_VERSION ?? '1.0',
JWT_SECRET: process.env.JWT_SECRET!, BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET!,
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? 'http://localhost:8787',
ELYDORA_SIGNING_KEY: process.env.ELYDORA_SIGNING_KEY!, ELYDORA_SIGNING_KEY: process.env.ELYDORA_SIGNING_KEY!,
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS ?? '', ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS ?? '',
TSA_URL: process.env.TSA_URL, TSA_URL: process.env.TSA_URL,
@@ -89,17 +90,14 @@ app.use('/*', async (c, next) => {
await next(); await next();
}); });
// CORS // CORS — always restrict to the configured allowlist
app.use( app.use(
'/*', '/*',
cors({ cors({
origin: (origin, c) => { origin: (origin, c) => {
if (c.env.ENVIRONMENT === 'production') { const raw = c.env.ALLOWED_ORIGINS ?? 'http://localhost:3000,http://localhost:8787';
const raw = c.env.ALLOWED_ORIGINS ?? '';
const allowedOrigins = raw.split(',').map((s: string) => s.trim()).filter(Boolean); const allowedOrigins = raw.split(',').map((s: string) => s.trim()).filter(Boolean);
return allowedOrigins.includes(origin) ? origin : ''; return allowedOrigins.includes(origin) ? origin : '';
}
return origin;
}, },
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: [ allowHeaders: [
+308
View File
@@ -0,0 +1,308 @@
/**
* Better Auth configuration for ElydoraOpenSource.
*
* Creates a Better Auth instance backed by PostgreSQL. The instance is
* configured to:
*
* - Use the existing `users` table via field-mapping (no schema change)
* - Support legacy PBKDF2-SHA256 password hashes alongside Better Auth's
* default scrypt so existing users can sign in without re-hashing
* - Expose bearer-token auth for API clients
* - Provide organization support with Elydora's 5-role RBAC hierarchy
* - Provide admin user-management endpoints
*/
import { betterAuth } from 'better-auth';
import { verifyPassword as verifyScryptPassword } from 'better-auth/crypto';
import { bearer } from 'better-auth/plugins/bearer';
import { organization } from 'better-auth/plugins/organization';
import { admin } from 'better-auth/plugins/admin';
import { createAccessControl } from 'better-auth/plugins/access';
// ---------------------------------------------------------------------------
// Legacy PBKDF2 password verification (matches auth-service.ts format)
// Format: pbkdf2:iterations:salt_hex:hash_hex
// ---------------------------------------------------------------------------
async function verifyLegacyPbkdf2(password: string, stored: string): Promise<boolean> {
const parts = stored.split(':');
if (parts.length !== 4 || parts[0] !== 'pbkdf2') return false;
const iterations = parseInt(parts[1]!, 10);
const saltHex = parts[2]!;
const storedHashHex = parts[3]!;
const salt = new Uint8Array(saltHex.match(/.{2}/g)!.map((b) => parseInt(b, 16)));
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveBits'],
);
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
keyMaterial,
256,
);
const hashHex = Array.from(new Uint8Array(bits))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
// Constant-time comparison
if (hashHex.length !== storedHashHex.length) return false;
let result = 0;
for (let i = 0; i < hashHex.length; i++) {
result |= hashHex.charCodeAt(i) ^ storedHashHex.charCodeAt(i);
}
return result === 0;
}
// ---------------------------------------------------------------------------
// RBAC access control — maps Elydora's 5-role hierarchy to Better Auth
// ---------------------------------------------------------------------------
const elydoraAc = createAccessControl({
organization: ['create', 'read', 'update', 'delete'],
member: ['create', 'read', 'update', 'delete'],
invitation: ['create', 'cancel'],
agent: ['create', 'read', 'update', 'freeze', 'unfreeze', 'revoke', 'delete'],
operation: ['create', 'read'],
audit: ['read', 'export'],
webhook: ['create', 'read', 'update', 'delete'],
} as const);
const orgOwnerRole = elydoraAc.newRole({
organization: ['create', 'read', 'update', 'delete'],
member: ['create', 'read', 'update', 'delete'],
invitation: ['create', 'cancel'],
agent: ['create', 'read', 'update', 'freeze', 'unfreeze', 'revoke', 'delete'],
operation: ['create', 'read'],
audit: ['read', 'export'],
webhook: ['create', 'read', 'update', 'delete'],
});
const securityAdminRole = elydoraAc.newRole({
organization: ['read', 'update'],
member: ['create', 'read', 'update', 'delete'],
invitation: ['create', 'cancel'],
agent: ['create', 'read', 'update', 'freeze', 'unfreeze', 'revoke', 'delete'],
operation: ['create', 'read'],
audit: ['read', 'export'],
webhook: ['create', 'read', 'update', 'delete'],
});
const complianceAuditorRole = elydoraAc.newRole({
organization: ['read'],
member: ['read'],
agent: ['read'],
operation: ['read'],
audit: ['read', 'export'],
webhook: ['read'],
});
const integrationEngineerRole = elydoraAc.newRole({
organization: ['read'],
member: ['read'],
agent: ['create', 'read', 'update'],
operation: ['create', 'read'],
audit: ['read'],
webhook: ['create', 'read', 'update', 'delete'],
});
const readonlyInvestigatorRole = elydoraAc.newRole({
organization: ['read'],
member: ['read'],
agent: ['read'],
operation: ['read'],
audit: ['read'],
webhook: ['read'],
});
// ---------------------------------------------------------------------------
// Singleton — creates a Better Auth instance for the server process
// ---------------------------------------------------------------------------
export function createAuth(databaseUrl: string, secret: string, baseUrl: string, allowedOrigins: string) {
return betterAuth({
database: {
type: 'postgres',
url: databaseUrl,
},
secret,
baseURL: baseUrl,
trustedOrigins: allowedOrigins
? allowedOrigins.split(',').map((s: string) => s.trim()).filter(Boolean)
: [],
emailAndPassword: {
enabled: true,
minPasswordLength: 8,
maxPasswordLength: 256,
password: {
verify: async ({ hash, password }) => {
// Handle legacy PBKDF2 hashes from the existing auth system
if (hash.startsWith('pbkdf2:')) {
return verifyLegacyPbkdf2(password, hash);
}
// For BA-native scrypt hashes (new registrations, password resets)
return verifyScryptPassword({ hash, password });
},
},
},
user: {
modelName: 'users',
fields: {
name: 'display_name',
email: 'email',
emailVerified: 'email_verified',
image: 'image',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
additionalFields: {
org_id: {
type: 'string',
required: false,
},
role: {
type: 'string',
required: true,
defaultValue: 'org_owner',
},
status: {
type: 'string',
required: true,
defaultValue: 'active',
},
password_hash: {
type: 'string',
required: false,
},
onboarding_completed: {
type: 'number',
required: true,
defaultValue: 0,
},
},
},
databaseHooks: {
user: {
create: {
before: async (user, _context) => {
const now = new Date();
return {
data: {
...user,
createdAt: now,
updatedAt: now,
role: 'org_owner',
},
};
},
after: async (_user) => {
// No-op: new users start with org_id = NULL and
// onboarding_completed = 0 (DB default). The onboarding
// flow handles org creation.
},
},
},
},
account: {
modelName: 'ba_accounts',
fields: {
createdAt: 'created_at',
updatedAt: 'updated_at',
userId: 'user_id',
providerId: 'provider_id',
accountId: 'account_id',
accessToken: 'access_token',
refreshToken: 'refresh_token',
idToken: 'id_token',
accessTokenExpiresAt: 'access_token_expires_at',
refreshTokenExpiresAt: 'refresh_token_expires_at',
},
},
verification: {
modelName: 'ba_verifications',
fields: {
createdAt: 'created_at',
updatedAt: 'updated_at',
expiresAt: 'expires_at',
},
},
session: {
modelName: 'ba_sessions',
expiresIn: 604800, // 7 days in seconds
updateAge: 86400, // refresh session if older than 1 day
fields: {
createdAt: 'created_at',
updatedAt: 'updated_at',
userId: 'user_id',
expiresAt: 'expires_at',
ipAddress: 'ip_address',
userAgent: 'user_agent',
},
},
advanced: {
cookiePrefix: 'elydora',
defaultCookieAttributes: {
httpOnly: true,
secure: true,
sameSite: 'lax' as const,
path: '/',
},
},
plugins: [
bearer(),
organization({
ac: elydoraAc,
creatorRole: 'org_owner',
roles: {
org_owner: orgOwnerRole,
security_admin: securityAdminRole,
compliance_auditor: complianceAuditorRole,
integration_engineer: integrationEngineerRole,
readonly_investigator: readonlyInvestigatorRole,
},
schema: {
organization: {
modelName: 'ba_organizations',
fields: {
createdAt: 'created_at',
updatedAt: 'updated_at',
},
},
member: {
modelName: 'ba_members',
fields: {
createdAt: 'created_at',
organizationId: 'organization_id',
userId: 'user_id',
},
},
invitation: {
modelName: 'ba_invitations',
fields: {
createdAt: 'created_at',
organizationId: 'organization_id',
inviterId: 'inviter_id',
expiresAt: 'expires_at',
},
},
},
}),
admin(),
],
});
}
export type ElydoraAuth = ReturnType<typeof createAuth>;
+31 -116
View File
@@ -1,147 +1,62 @@
/** /**
* JWT authentication middleware. * Better Auth authentication middleware.
* *
* Verifies Bearer tokens in the Authorization header using HMAC-SHA256. * Validates requests via Better Auth sessions (cookie or bearer token).
* On success, extracts `org_id`, `role`, and `sub` (actor) claims and * On success, extracts `org_id`, `role`, and user `id` (actor) from the
* stores them in the Hono context variables for downstream handlers. * session and stores them in the Hono context variables for downstream
* handlers.
* *
* Token structure (payload): * Supports:
* { * - Session cookies (browser / console)
* "sub": "user_abc123", // actor identifier * - Bearer tokens via Better Auth's bearer plugin (SDK clients)
* "org_id": "org_acme", // organization scope
* "role": "org_owner", // RBAC role
* "iat": 1700000000, // issued-at (seconds)
* "exp": 1700003600 // expiration (seconds)
* }
*/ */
import type { MiddlewareHandler } from 'hono'; import type { MiddlewareHandler } from 'hono';
import type { RbacRole } from '../shared/index.js'; import type { RbacRole } from '../shared/index.js';
import type { Env, AppVariables } from '../types.js'; import type { Env, AppVariables } from '../types.js';
import { AppError } from './error-handler.js'; import { AppError } from './error-handler.js';
import { base64urlDecode } from '../utils/crypto.js'; import { createAuth } from '../lib/auth.js';
/** Minimal JWT payload shape we require. */
interface JWTPayload {
sub: string;
org_id: string;
role: RbacRole;
iat: number;
exp: number;
}
/**
* Verify a JWT using HMAC-SHA256 and return its payload.
*/
async function verifyJWT(token: string, secret: string): Promise<JWTPayload> {
const parts = token.split('.');
if (parts.length !== 3) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.malformedJwt' });
}
const headerB64 = parts[0]!;
const payloadB64 = parts[1]!;
const signatureB64 = parts[2]!;
// Validate JWT header — only HS256 is accepted
try {
const headerBytes = base64urlDecode(headerB64);
const header = JSON.parse(new TextDecoder().decode(headerBytes)) as { alg?: string; typ?: string };
if (header.alg !== 'HS256') {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.unsupportedAlgorithm' });
}
} catch (e) {
if (e instanceof AppError) throw e;
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.malformedJwt' });
}
// Import the HMAC key
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify'],
);
// Verify signature over "header.payload"
const signedInput = encoder.encode(`${headerB64}.${payloadB64}`);
const signatureBytes = base64urlDecode(signatureB64);
const valid = await crypto.subtle.verify('HMAC', cryptoKey, signatureBytes as unknown as Uint8Array<ArrayBuffer>, signedInput as unknown as Uint8Array<ArrayBuffer>);
if (!valid) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.invalidSignature' });
}
// Decode and validate payload
const payloadBytes = base64urlDecode(payloadB64);
const payloadJson = new TextDecoder().decode(payloadBytes);
let payload: JWTPayload;
try {
payload = JSON.parse(payloadJson);
} catch {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.malformedPayload' });
}
// Validate required claims
if (!payload.sub || typeof payload.sub !== 'string') {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingSub' });
}
if (!payload.org_id || typeof payload.org_id !== 'string') {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingOrgId' });
}
if (!payload.role || typeof payload.role !== 'string') {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingRole' });
}
// Check expiration
const nowSeconds = Math.floor(Date.now() / 1000);
if (typeof payload.exp !== 'number') {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingExp' });
}
if (payload.exp < nowSeconds) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.expired' });
}
return payload;
}
/** /**
* Authentication middleware. * Authentication middleware.
* *
* Extracts the Bearer token, verifies it, and sets org_id, role, and * Uses Better Auth's getSession to validate the request via session
* actor on the context. Should be applied to all routes that require * cookie or bearer token. Sets org_id, role, and actor on the context.
* authentication.
*/ */
export const authMiddleware: MiddlewareHandler<{ export const authMiddleware: MiddlewareHandler<{
Bindings: Env; Bindings: Env;
Variables: AppVariables; Variables: AppVariables;
}> = async (c, next) => { }> = async (c, next) => {
const authHeader = c.req.header('Authorization'); const authHeader = c.req.header('Authorization');
if (!authHeader) { if (!authHeader) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingHeader' }); throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.missingHeader' });
} }
if (!authHeader.startsWith('Bearer ')) { let session: Awaited<ReturnType<ReturnType<typeof createAuth>['api']['getSession']>>;
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.bearerRequired' });
try {
const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
session = await betterAuthInstance.api.getSession({
headers: c.req.raw.headers,
});
} catch {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.invalidSession' });
} }
const token = authHeader.slice(7).trim(); if (!session) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.invalidSession' });
if (!token) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.emptyToken' });
} }
const payload = await verifyJWT(token, c.env.JWT_SECRET); const user = session.user as { id: string; org_id?: string; role?: string };
c.set('org_id', payload.org_id); c.set('org_id', user.org_id ?? '');
c.set('role', payload.role); c.set('role', ((user.role ?? 'readonly_investigator') as RbacRole));
c.set('actor', payload.sub); c.set('actor', user.id);
await next(); await next();
}; };
+144 -33
View File
@@ -2,15 +2,17 @@
* Auth routes — registration, login, profile retrieval, and token refresh. * Auth routes — registration, login, profile retrieval, and token refresh.
* *
* /register and /login are public (no auth required). * /register and /login are public (no auth required).
* /me and /refresh require a valid JWT. * /me, /refresh, and /token require a valid Better Auth session.
*
* Uses Better Auth for session management while preserving the existing
* API surface (endpoint paths and response formats).
*/ */
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { Env, AppVariables } from '../types.js'; import type { Env, AppVariables } from '../types.js';
import { authMiddleware } from '../middleware/auth.js'; import { authMiddleware } from '../middleware/auth.js';
import { registerUser, loginUser, getMe, issueJWT, issueApiToken } from '../services/auth-service.js'; import { getMe } from '../services/auth-service.js';
import type { RbacRole } from '../shared/index.js'; import { createAuth } from '../lib/auth.js';
import { DEFAULT_JWT_TTL_SECONDS } from '../shared/index.js';
import { getMessage } from '../i18n/messages.js'; import { getMessage } from '../i18n/messages.js';
const auth = new Hono<{ Bindings: Env; Variables: AppVariables }>(); const auth = new Hono<{ Bindings: Env; Variables: AppVariables }>();
@@ -22,28 +24,111 @@ auth.post('/register', async (c) => {
const body = await c.req.json(); const body = await c.req.json();
const { email, password, display_name, org_name } = body; const { email, password, display_name, org_name } = body;
const result = await registerUser( const betterAuthInstance = createAuth(
c.env.ELYDORA_DB, process.env.DATABASE_URL!,
c.env.JWT_SECRET, c.env.BETTER_AUTH_SECRET,
email, c.env.BETTER_AUTH_URL,
password, c.env.ALLOWED_ORIGINS,
display_name,
org_name,
); );
return c.json(result, 201); // Use Better Auth to sign up the user
const signUpResult = await betterAuthInstance.api.signUpEmail({
body: {
email,
password,
name: display_name || email.split('@')[0]!,
},
});
if (!signUpResult) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'VALIDATION_ERROR', message: getMessage('auth.emailAlreadyRegistered', lang), request_id: '' } }, 409);
}
const user = signUpResult.user as { id: string; email: string; name: string; org_id?: string; role?: string; status?: string; createdAt?: unknown; updatedAt?: unknown };
// Create the organization via the database adapter (preserving existing schema)
const now = Date.now();
const { generateUUIDv7 } = await import('../utils/uuid.js');
const orgId = generateUUIDv7();
const resolvedOrgName = org_name || `${user.name}'s Organization`;
await c.env.ELYDORA_DB.batch([
c.env.ELYDORA_DB
.prepare(
'INSERT INTO organizations (org_id, name, created_at, updated_at) VALUES (?, ?, ?, ?)',
)
.bind(orgId, resolvedOrgName, now, now),
c.env.ELYDORA_DB
.prepare(
'UPDATE users SET org_id = ?, role = ?, status = ? WHERE user_id = ?',
)
.bind(orgId, 'org_owner', 'active', user.id),
]);
// Return the session token
const token = signUpResult.token ?? '';
return c.json({
user: {
user_id: user.id,
org_id: orgId,
email: user.email,
display_name: user.name,
role: 'org_owner',
status: 'active',
created_at: now,
updated_at: now,
},
organization: {
org_id: orgId,
name: resolvedOrgName,
created_at: now,
updated_at: now,
},
token,
}, 201);
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// POST /v1/auth/login — Authenticate and receive a JWT // POST /v1/auth/login — Authenticate and receive a session token
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
auth.post('/login', async (c) => { auth.post('/login', async (c) => {
const body = await c.req.json(); const body = await c.req.json();
const { email, password } = body; const { email, password } = body;
const result = await loginUser(c.env.ELYDORA_DB, c.env.JWT_SECRET, email, password); const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
return c.json(result, 200); const signInResult = await betterAuthInstance.api.signInEmail({
body: { email, password },
});
if (!signInResult) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidCredentials', lang), request_id: '' } }, 401);
}
const user = signInResult.user as { id: string; email: string; name: string; org_id?: string; role?: string; status?: string; created_at?: number; updated_at?: number };
const token = signInResult.token ?? '';
return c.json({
user: {
user_id: user.id,
org_id: user.org_id ?? '',
email: user.email,
display_name: user.name,
role: user.role ?? 'org_owner',
status: user.status ?? 'active',
created_at: user.created_at ?? 0,
updated_at: user.updated_at ?? 0,
},
token,
}, 200);
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -57,21 +142,29 @@ auth.get('/me', authMiddleware, async (c) => {
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// POST /v1/auth/refresh — Issue a new JWT (auth required) // POST /v1/auth/refresh — Issue a new session token (auth required)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
auth.post('/refresh', authMiddleware, async (c) => { auth.post('/refresh', authMiddleware, async (c) => {
const now = Date.now(); const betterAuthInstance = createAuth(
const token = await issueJWT( process.env.DATABASE_URL!,
{ c.env.BETTER_AUTH_SECRET,
sub: c.get('actor'), c.env.BETTER_AUTH_URL,
org_id: c.get('org_id'), c.env.ALLOWED_ORIGINS,
role: c.get('role') as RbacRole,
iat: Math.floor(now / 1000),
exp: Math.floor(now / 1000) + DEFAULT_JWT_TTL_SECONDS,
},
c.env.JWT_SECRET,
); );
// Get the current session and return its token — Better Auth handles
// session refresh automatically based on the updateAge setting
const session = await betterAuthInstance.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidSession', lang), request_id: '' } }, 401);
}
const token = session.session?.token ?? '';
return c.json({ token }, 200); return c.json({ token }, 200);
}); });
@@ -89,15 +182,33 @@ auth.post('/token', authMiddleware, async (c) => {
} }
} }
const result = await issueApiToken( // Get the current session token — Better Auth sessions have a fixed
c.get('actor'), // lifetime configured in the auth instance (7 days by default).
c.get('org_id'), // The ttl_seconds parameter is accepted for API compatibility but
c.get('role') as RbacRole, // session lifetime is governed by Better Auth's session config.
c.env.JWT_SECRET, const betterAuthInstance = createAuth(
ttl_seconds ?? null, process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
); );
return c.json(result, 200); const session = await betterAuthInstance.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidSession', lang), request_id: '' } }, 401);
}
const sessionData = session.session as { token?: string; expiresAt?: Date };
const token = sessionData.token ?? '';
const expiresAt = sessionData.expiresAt
? Math.floor(new Date(sessionData.expiresAt).getTime() / 1000)
: Math.floor(Date.now() / 1000) + 604800;
return c.json({ token, expires_at: expiresAt }, 200);
}); });
export { auth }; export { auth };
+1 -1
View File
@@ -16,7 +16,7 @@ const operations = new Hono<{ Bindings: Env; Variables: AppVariables }>();
// POST /v1/operations — Submit a signed EOR // POST /v1/operations — Submit a signed EOR
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Operation submission uses the EOR's embedded signature for authentication // Operation submission uses the EOR's embedded signature for authentication
// rather than a Bearer JWT. The agent authenticates via its Ed25519 signature. // rather than a Bearer token. The agent authenticates via its Ed25519 signature.
// We still apply auth middleware to get the org context. // We still apply auth middleware to get the org context.
operations.post( operations.post(
'/', '/',
+4 -211
View File
@@ -1,14 +1,12 @@
/** /**
* Auth service — password hashing, JWT issuance, user registration, and login. * Auth service — password hashing and user profile retrieval.
* *
* Uses PBKDF2-SHA256 via Web Crypto API for password hashing and * Authentication is handled by Better Auth (see lib/auth.ts).
* HMAC-SHA256 for JWT issuance (matching the existing auth middleware). * This service provides password utilities for legacy PBKDF2 hash
* verification and the getMe profile query.
*/ */
import { base64urlEncode } from '../utils/crypto.js';
import { generateUUIDv7 } from '../utils/uuid.js';
import { AppError } from '../middleware/error-handler.js'; import { AppError } from '../middleware/error-handler.js';
import { DEFAULT_JWT_TTL_SECONDS } from '../shared/index.js';
import type { RbacRole } from '../shared/index.js'; import type { RbacRole } from '../shared/index.js';
import type { Database } from '../adapters/interfaces.js'; import type { Database } from '../adapters/interfaces.js';
@@ -34,18 +32,6 @@ export interface OrgRow {
readonly updated_at: number; readonly updated_at: number;
} }
// ---------------------------------------------------------------------------
// JWT payload
// ---------------------------------------------------------------------------
interface JWTPayload {
sub: string;
org_id: string;
role: RbacRole;
iat: number;
exp: number;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Password hashing (PBKDF2-SHA256 via Web Crypto) // Password hashing (PBKDF2-SHA256 via Web Crypto)
// Format: pbkdf2:100000:salt_hex:hash_hex // Format: pbkdf2:100000:salt_hex:hash_hex
@@ -110,199 +96,6 @@ export async function verifyPassword(password: string, stored: string): Promise<
return result === 0; return result === 0;
} }
// ---------------------------------------------------------------------------
// JWT issuance (HMAC-SHA256, matching existing auth middleware verification)
// ---------------------------------------------------------------------------
export async function issueJWT(payload: JWTPayload, secret: string): Promise<string> {
const encoder = new TextEncoder();
const header = { alg: 'HS256', typ: 'JWT' };
const headerB64 = base64urlEncode(encoder.encode(JSON.stringify(header)));
const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
const signingInput = `${headerB64}.${payloadB64}`;
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signingInput));
const signatureB64 = base64urlEncode(new Uint8Array(signature));
return `${headerB64}.${payloadB64}.${signatureB64}`;
}
// ---------------------------------------------------------------------------
// Issue API token with custom TTL
// ---------------------------------------------------------------------------
/** Maximum API token lifetime: 365 days. */
const MAX_API_TOKEN_TTL = 365 * 24 * 60 * 60;
export async function issueApiToken(
sub: string,
orgId: string,
role: RbacRole,
jwtSecret: string,
ttlSeconds: number | null,
): Promise<{ token: string; expires_at: number }> {
const now = Math.floor(Date.now() / 1000);
// Default to 90 days if no TTL specified; cap at 365 days
const effectiveTtl = ttlSeconds === null ? 90 * 24 * 60 * 60 : Math.min(ttlSeconds, MAX_API_TOKEN_TTL);
if (effectiveTtl <= 0) {
throw new AppError(400, 'VALIDATION_ERROR', { key: 'auth.ttlMustBePositive' });
}
const exp = now + effectiveTtl;
const expiresAt = exp;
const token = await issueJWT(
{ sub, org_id: orgId, role, iat: now, exp },
jwtSecret,
);
return { token, expires_at: expiresAt };
}
// ---------------------------------------------------------------------------
// Register user + org
// ---------------------------------------------------------------------------
export async function registerUser(
db: Database,
jwtSecret: string,
email: string,
password: string,
displayName: string,
orgName: string,
): Promise<{ user: UserRow; organization: OrgRow; token: string }> {
// Validate
if (!email || !password || password.length < 8 || password.length > 128) {
throw new AppError(400, 'VALIDATION_ERROR', { key: 'auth.emailPasswordRequired' });
}
// Check email uniqueness
const existing = await db
.prepare('SELECT user_id FROM users WHERE email = ?')
.bind(email)
.first();
if (existing) {
throw new AppError(409, 'VALIDATION_ERROR', { key: 'auth.emailAlreadyRegistered' });
}
const now = Date.now();
const orgId = generateUUIDv7();
const userId = generateUUIDv7();
const hash = await hashPassword(password);
const resolvedDisplayName = displayName || email.split('@')[0]!;
const resolvedOrgName = orgName || `${resolvedDisplayName}'s Organization`;
// Create org + user atomically in a single transaction
await db.batch([
db
.prepare(
'INSERT INTO organizations (org_id, name, created_at, updated_at) VALUES (?, ?, ?, ?)',
)
.bind(orgId, resolvedOrgName, now, now),
db
.prepare(
'INSERT INTO users (user_id, org_id, email, password_hash, display_name, role, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
)
.bind(userId, orgId, email, hash, resolvedDisplayName, 'org_owner', 'active', now, now),
]);
const token = await issueJWT(
{
sub: userId,
org_id: orgId,
role: 'org_owner',
iat: Math.floor(now / 1000),
exp: Math.floor(now / 1000) + DEFAULT_JWT_TTL_SECONDS,
},
jwtSecret,
);
return {
user: {
user_id: userId,
org_id: orgId,
email,
display_name: resolvedDisplayName,
role: 'org_owner' as RbacRole,
status: 'active',
created_at: now,
updated_at: now,
},
organization: {
org_id: orgId,
name: resolvedOrgName,
created_at: now,
updated_at: now,
},
token,
};
}
// ---------------------------------------------------------------------------
// Login user
// ---------------------------------------------------------------------------
export async function loginUser(
db: Database,
jwtSecret: string,
email: string,
password: string,
): Promise<{ user: UserRow; token: string }> {
const row = await db
.prepare(
'SELECT user_id, org_id, email, password_hash, display_name, role, status, created_at, updated_at FROM users WHERE email = ?',
)
.bind(email)
.first<UserRow & { password_hash: string }>();
if (!row) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.invalidCredentials' });
}
if (row.status !== 'active') {
throw new AppError(403, 'FORBIDDEN', { key: 'auth.accountSuspended' });
}
const valid = await verifyPassword(password, row.password_hash);
if (!valid) {
throw new AppError(401, 'UNAUTHORIZED', { key: 'auth.invalidCredentials' });
}
const now = Date.now();
const token = await issueJWT(
{
sub: row.user_id,
org_id: row.org_id,
role: row.role as RbacRole,
iat: Math.floor(now / 1000),
exp: Math.floor(now / 1000) + DEFAULT_JWT_TTL_SECONDS,
},
jwtSecret,
);
return {
user: {
user_id: row.user_id,
org_id: row.org_id,
email: row.email,
display_name: row.display_name,
role: row.role as RbacRole,
status: row.status,
created_at: row.created_at,
updated_at: row.updated_at,
},
token,
};
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Get current user profile // Get current user profile
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -7,5 +7,5 @@ export {
DEFAULT_EPOCH_INTERVAL_MS, DEFAULT_EPOCH_INTERVAL_MS,
MAX_QUERY_LIMIT, MAX_QUERY_LIMIT,
DEFAULT_QUERY_LIMIT, DEFAULT_QUERY_LIMIT,
DEFAULT_JWT_TTL_SECONDS, DEFAULT_SESSION_TTL_SECONDS,
} from './limits.js'; } from './limits.js';
@@ -19,5 +19,5 @@ export const MAX_QUERY_LIMIT = 1000;
/** Default number of results per query */ /** Default number of results per query */
export const DEFAULT_QUERY_LIMIT = 100; export const DEFAULT_QUERY_LIMIT = 100;
/** Default JWT token lifetime in seconds (24 hours) */ /** Default session token lifetime in seconds (24 hours) */
export const DEFAULT_JWT_TTL_SECONDS = 86400; export const DEFAULT_SESSION_TTL_SECONDS = 86400;
+1 -1
View File
@@ -60,5 +60,5 @@ export {
DEFAULT_EPOCH_INTERVAL_MS, DEFAULT_EPOCH_INTERVAL_MS,
MAX_QUERY_LIMIT, MAX_QUERY_LIMIT,
DEFAULT_QUERY_LIMIT, DEFAULT_QUERY_LIMIT,
DEFAULT_JWT_TTL_SECONDS, DEFAULT_SESSION_TTL_SECONDS,
} from './constants/index.js'; } from './constants/index.js';
+8 -5
View File
@@ -30,8 +30,11 @@ export interface Env {
/** Current Elydora protocol version (e.g. "1.0") */ /** Current Elydora protocol version (e.g. "1.0") */
PROTOCOL_VERSION: string; PROTOCOL_VERSION: string;
/** HMAC secret used for JWT verification */ /** Secret used by Better Auth for session signing and encryption */
JWT_SECRET: string; BETTER_AUTH_SECRET: string;
/** Base URL for Better Auth (e.g. "http://localhost:8787") */
BETTER_AUTH_URL: string;
/** Base64url-encoded Ed25519 private key used by the server to sign receipts */ /** Base64url-encoded Ed25519 private key used by the server to sign receipts */
ELYDORA_SIGNING_KEY: string; ELYDORA_SIGNING_KEY: string;
@@ -50,13 +53,13 @@ export interface AppVariables {
/** UUIDv7 request identifier injected by the request-id middleware */ /** UUIDv7 request identifier injected by the request-id middleware */
request_id: string; request_id: string;
/** Organization identifier extracted from the verified JWT */ /** Organization identifier extracted from the verified session */
org_id: string; org_id: string;
/** RBAC role extracted from the verified JWT */ /** RBAC role extracted from the verified session */
role: import('./shared/index.js').RbacRole; role: import('./shared/index.js').RbacRole;
/** Actor identifier (sub claim) extracted from the verified JWT */ /** Actor identifier extracted from the verified session */
actor: string; actor: string;
/** Detected language from Accept-Language header (set by i18n middleware) */ /** Detected language from Accept-Language header (set by i18n middleware) */
+4 -4
View File
@@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# Generates an Ed25519 signing key and a random JWT secret for initial setup. # Generates an Ed25519 signing key and a random Better Auth secret for initial setup.
# Requires: openssl # Requires: openssl
set -e set -e
@@ -14,12 +14,12 @@ SIGNING_KEY=$(openssl genpkey -algorithm ed25519 2>/dev/null \
| tr '+/' '-_' \ | tr '+/' '-_' \
| tr -d '=\n') | tr -d '=\n')
# Generate 32-byte random JWT secret as hex # Generate 32-byte random Better Auth secret as hex
JWT_SECRET=$(openssl rand -hex 32) BETTER_AUTH_SECRET=$(openssl rand -hex 32)
echo "Add these to your .env file:" echo "Add these to your .env file:"
echo "" echo ""
echo "ELYDORA_SIGNING_KEY=${SIGNING_KEY}" echo "ELYDORA_SIGNING_KEY=${SIGNING_KEY}"
echo "JWT_SECRET=${JWT_SECRET}" echo "BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}"
echo "" echo ""
echo "WARNING: Store these values securely. Do not commit them to version control." echo "WARNING: Store these values securely. Do not commit them to version control."
+3 -3
View File
@@ -61,7 +61,7 @@ else
# Generate secrets # Generate secrets
PG_PASS=$(openssl rand -hex 24) PG_PASS=$(openssl rand -hex 24)
MINIO_PASS=$(openssl rand -hex 24) MINIO_PASS=$(openssl rand -hex 24)
JWT=$(openssl rand -hex 32) BETTER_AUTH_SECRET=$(openssl rand -hex 32)
SIGNING_KEY=$(openssl genpkey -algorithm ed25519 2>/dev/null \ SIGNING_KEY=$(openssl genpkey -algorithm ed25519 2>/dev/null \
| openssl pkcs8 -topk8 -nocrypt -outform DER 2>/dev/null \ | openssl pkcs8 -topk8 -nocrypt -outform DER 2>/dev/null \
| tail -c 32 \ | tail -c 32 \
@@ -71,7 +71,7 @@ else
[ -n "$PG_PASS" ] || fail "Failed to generate Postgres password." [ -n "$PG_PASS" ] || fail "Failed to generate Postgres password."
[ -n "$MINIO_PASS" ] || fail "Failed to generate MinIO password." [ -n "$MINIO_PASS" ] || fail "Failed to generate MinIO password."
[ -n "$JWT" ] || fail "Failed to generate JWT secret." [ -n "$BETTER_AUTH_SECRET" ] || fail "Failed to generate Better Auth secret."
[ -n "$SIGNING_KEY" ] || fail "Failed to generate Ed25519 signing key." [ -n "$SIGNING_KEY" ] || fail "Failed to generate Ed25519 signing key."
# Replace placeholders # Replace placeholders
@@ -79,7 +79,7 @@ else
sedi "s|elydora:GENERATE_ME@postgres|elydora:${PG_PASS}@postgres|" .env sedi "s|elydora:GENERATE_ME@postgres|elydora:${PG_PASS}@postgres|" .env
sedi "s|MINIO_ROOT_PASSWORD=GENERATE_ME|MINIO_ROOT_PASSWORD=${MINIO_PASS}|" .env sedi "s|MINIO_ROOT_PASSWORD=GENERATE_ME|MINIO_ROOT_PASSWORD=${MINIO_PASS}|" .env
sedi "s|MINIO_SECRET_KEY=GENERATE_ME|MINIO_SECRET_KEY=${MINIO_PASS}|" .env sedi "s|MINIO_SECRET_KEY=GENERATE_ME|MINIO_SECRET_KEY=${MINIO_PASS}|" .env
sedi "s|JWT_SECRET=GENERATE_ME|JWT_SECRET=${JWT}|" .env sedi "s|BETTER_AUTH_SECRET=GENERATE_ME|BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}|" .env
sedi "s|ELYDORA_SIGNING_KEY=GENERATE_ME|ELYDORA_SIGNING_KEY=${SIGNING_KEY}|" .env sedi "s|ELYDORA_SIGNING_KEY=GENERATE_ME|ELYDORA_SIGNING_KEY=${SIGNING_KEY}|" .env
chmod 600 .env chmod 600 .env
+21 -25
View File
@@ -23,18 +23,15 @@ import (
) )
func main() { func main() {
// Authenticate // Initialize the client with your API token.
auth, err := elydora.Login("https://api.elydora.com", "user@example.com", "password") // Obtain an API token by signing in via the Elydora console or:
if err != nil { // POST /api/auth/sign-in/email -> get session token
log.Fatal(err) // POST /v1/auth/token -> exchange for long-lived API token
}
// Create client
client, err := elydora.NewClient(&elydora.Config{ client, err := elydora.NewClient(&elydora.Config{
OrgID: auth.User.OrgID, OrgID: "org-123",
AgentID: "my-agent-id", AgentID: "my-agent-id",
PrivateKey: "<base64url-encoded-ed25519-seed>", PrivateKey: "<base64url-encoded-ed25519-seed>",
Token: auth.Token, Token: "your-api-token",
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -108,36 +105,35 @@ client, err := elydora.NewClient(&elydora.Config{
BaseURL: "https://...", // API base URL (default: https://api.elydora.com) BaseURL: "https://...", // API base URL (default: https://api.elydora.com)
TTLMs: 30000, // Operation TTL in ms (default: 30000) TTLMs: 30000, // Operation TTL in ms (default: 30000)
MaxRetries: 3, // Max retries on transient failures (default: 3) MaxRetries: 3, // Max retries on transient failures (default: 3)
Token: "<jwt>", // JWT token for authenticated requests Token: "<token>", // API token for authenticated requests
}) })
``` ```
### Client Methods ### Client Methods
```go ```go
// Update the JWT token at runtime // Update the API token at runtime
client.SetToken("new-jwt-token") client.SetToken("new-api-token")
``` ```
### Authentication ### Authentication
Authentication uses Better Auth. Register and sign in via the Elydora console or the Better Auth endpoints, then issue a long-lived API token for SDK use:
```go ```go
// Register a new user and organization // Sign up (Better Auth) — use the console or call directly:
reg, err := elydora.Register(baseURL, email, password, // POST /api/auth/sign-up/email { email, password, name }
elydora.WithDisplayName("Alice"), //
elydora.WithOrgName("Acme Corp"), // Sign in (Better Auth) — get a session token:
) // POST /api/auth/sign-in/email { email, password }
//
// Login and receive a JWT // Issue a long-lived API token from an active session:
auth, err := elydora.Login(baseURL, email, password)
// Get current authenticated user profile
me, err := client.GetMe()
// Issue a new API token (with optional TTL)
tokenResp, err := client.IssueApiToken(&elydora.IssueApiTokenRequest{ tokenResp, err := client.IssueApiToken(&elydora.IssueApiTokenRequest{
TTLSeconds: &ttlSeconds, TTLSeconds: &ttlSeconds,
}) })
// Update the token on an existing client instance at runtime
client.SetToken("new-api-token")
``` ```
### Operations ### Operations
+5 -1
View File
@@ -28,6 +28,8 @@ func WithOrgName(name string) RegisterOption {
} }
// Register creates a new user and organization. // Register creates a new user and organization.
//
// Deprecated: Use Better Auth endpoints directly. See docs.
func Register(baseURL, email, password string, opts ...RegisterOption) (*AuthRegisterResponse, error) { func Register(baseURL, email, password string, opts ...RegisterOption) (*AuthRegisterResponse, error) {
if baseURL == "" { if baseURL == "" {
baseURL = defaultBaseURL baseURL = defaultBaseURL
@@ -49,7 +51,9 @@ func Register(baseURL, email, password string, opts ...RegisterOption) (*AuthReg
return &result, nil return &result, nil
} }
// Login authenticates a user and returns a JWT token. // Login authenticates a user and returns a session token.
//
// Deprecated: Use Better Auth endpoints directly. See docs.
func Login(baseURL, email, password string) (*AuthLoginResponse, error) { func Login(baseURL, email, password string) (*AuthLoginResponse, error) {
if baseURL == "" { if baseURL == "" {
baseURL = defaultBaseURL baseURL = defaultBaseURL
+2 -2
View File
@@ -24,7 +24,7 @@ type Config struct {
BaseURL string // defaults to "https://api.elydora.com" BaseURL string // defaults to "https://api.elydora.com"
TTLMs int // defaults to 30000 TTLMs int // defaults to 30000
MaxRetries int // defaults to 3 MaxRetries int // defaults to 3
Token string // JWT token for authenticated requests Token string // API token for authenticated requests
} }
// Client is the Elydora SDK client. // Client is the Elydora SDK client.
@@ -172,7 +172,7 @@ func (c *Client) doPost(path string, body interface{}, result interface{}) error
return c.doRequest(http.MethodPost, path, body, result) return c.doRequest(http.MethodPost, path, body, result)
} }
// SetToken sets the JWT token used for authenticated API requests. // SetToken sets the API token used for authenticated API requests.
func (c *Client) SetToken(token string) { func (c *Client) SetToken(token string) {
c.token = token c.token = token
} }
+1 -1
View File
@@ -69,7 +69,7 @@ func cmdInstall(args []string) {
agentID := fs.String("agent-id", "", "Agent ID (required)") agentID := fs.String("agent-id", "", "Agent ID (required)")
privateKey := fs.String("private-key", "", "Base64url-encoded Ed25519 private key seed (required)") privateKey := fs.String("private-key", "", "Base64url-encoded Ed25519 private key seed (required)")
kid := fs.String("kid", "", "Key ID (defaults to <agent-id>-key-1)") kid := fs.String("kid", "", "Key ID (defaults to <agent-id>-key-1)")
token := fs.String("token", "", "JWT token for authenticated requests (optional)") token := fs.String("token", "", "API token for authenticated requests (optional)")
baseURL := fs.String("base-url", "https://api.elydora.com", "Elydora API base URL") baseURL := fs.String("base-url", "https://api.elydora.com", "Elydora API base URL")
fs.Parse(args) fs.Parse(args)
+7 -7
View File
@@ -15,17 +15,17 @@ Requires Node.js 18+ (uses built-in `crypto` module with Ed25519 support).
```typescript ```typescript
import { ElydoraClient } from '@elydora/sdk'; import { ElydoraClient } from '@elydora/sdk';
// Authenticate // Initialize the client with your API token.
const auth = await ElydoraClient.login('https://api.elydora.com', 'user@example.com', 'password'); // Obtain an API token by signing in via the Elydora console or:
// POST /api/auth/sign-in/email → get session token
// Create client // POST /v1/auth/token → exchange for long-lived API token
const client = new ElydoraClient({ const client = new ElydoraClient({
orgId: auth.user.org_id, orgId: 'org-123',
agentId: 'my-agent-id', agentId: 'my-agent-id',
privateKey: '<base64url-encoded-32-byte-ed25519-seed>', privateKey: '<base64url-encoded-32-byte-ed25519-seed>',
baseUrl: 'https://api.elydora.com', baseUrl: 'https://api.elydora.com',
token: 'your-api-token',
}); });
client.setToken(auth.token);
// Create and submit an operation // Create and submit an operation
const eor = client.createOperation({ const eor = client.createOperation({
@@ -96,7 +96,7 @@ const client = new ElydoraClient({
// Register a new user and organization // Register a new user and organization
const reg = await ElydoraClient.register(baseUrl, email, password, displayName?, orgName?); const reg = await ElydoraClient.register(baseUrl, email, password, displayName?, orgName?);
// Login and receive a JWT // Login and receive a session token
const auth = await ElydoraClient.login(baseUrl, email, password); const auth = await ElydoraClient.login(baseUrl, email, password);
// Set token on client // Set token on client
+5 -2
View File
@@ -1,13 +1,16 @@
{ {
"name": "@elydora/sdk", "name": "@elydora/sdk",
"version": "1.0.0", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@elydora/sdk", "name": "@elydora/sdk",
"version": "1.0.0", "version": "1.1.0",
"license": "MIT", "license": "MIT",
"bin": {
"elydora": "dist/cli.js"
},
"devDependencies": { "devDependencies": {
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
+1 -1
View File
@@ -41,7 +41,7 @@ function printUsage(): void {
console.log(`Elydora CLI — Tamper-evident audit for AI coding agents console.log(`Elydora CLI — Tamper-evident audit for AI coding agents
Usage: Usage:
elydora install --agent <name> --org_id <id> --agent_id <id> --private_key <key> --kid <kid> [--token <jwt>] [--base_url <url>] elydora install --agent <name> --org_id <id> --agent_id <id> --private_key <key> --kid <kid> [--token <token>] [--base_url <url>]
elydora uninstall --agent <name> [--agent_id <id>] elydora uninstall --agent <name> [--agent_id <id>]
elydora status elydora status
elydora agents elydora agents
+7 -1
View File
@@ -65,7 +65,7 @@ export class ElydoraClient {
} }
/** /**
* Set the JWT token used for authenticated API calls. * Set the session token used for authenticated API calls.
*/ */
setToken(token: string): void { setToken(token: string): void {
this.token = token; this.token = token;
@@ -89,6 +89,9 @@ export class ElydoraClient {
// Auth (static methods — no instance needed) // Auth (static methods — no instance needed)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/**
* @deprecated Use Better Auth endpoints directly. See docs.
*/
static async register( static async register(
baseUrl: string, baseUrl: string,
email: string, email: string,
@@ -110,6 +113,9 @@ export class ElydoraClient {
return handleResponse<AuthRegisterResponse>(res); return handleResponse<AuthRegisterResponse>(res);
} }
/**
* @deprecated Use Better Auth endpoints directly. See docs.
*/
static async login( static async login(
baseUrl: string, baseUrl: string,
email: string, email: string,
+20 -18
View File
@@ -15,16 +15,16 @@ Requires Python 3.9+.
```python ```python
from elydora import ElydoraClient from elydora import ElydoraClient
# Authenticate # Initialize the client with your API token.
auth = ElydoraClient.login("https://api.elydora.com", "user@example.com", "password") # Obtain an API token by signing in via the Elydora console or:
# POST /api/auth/sign-in/email -> get session token
# Create client # POST /v1/auth/token -> exchange for long-lived API token
client = ElydoraClient( client = ElydoraClient(
org_id=auth["user"]["org_id"], org_id="org-123",
agent_id="my-agent-id", agent_id="my-agent-id",
private_key="<base64url-encoded-ed25519-seed>", private_key="<base64url-encoded-ed25519-seed>",
base_url="https://api.elydora.com", base_url="https://api.elydora.com",
token=auth["token"], token="your-api-token",
) )
# Create and submit an operation # Create and submit an operation
@@ -48,7 +48,7 @@ async def main():
org_id="org-123", org_id="org-123",
agent_id="agent-456", agent_id="agent-456",
private_key="<base64url-encoded-ed25519-seed>", private_key="<base64url-encoded-ed25519-seed>",
token="<jwt-token>", token="<api-token>",
) )
eor = client.create_operation( eor = client.create_operation(
@@ -107,24 +107,26 @@ client = ElydoraClient(
base_url="https://...", # API base URL (default: https://api.elydora.com) base_url="https://...", # API base URL (default: https://api.elydora.com)
ttl_ms=30000, # Operation TTL in ms (default: 30000) ttl_ms=30000, # Operation TTL in ms (default: 30000)
max_retries=3, # Max retries on transient failures (default: 3) max_retries=3, # Max retries on transient failures (default: 3)
token="<jwt>", # Optional JWT bearer token token="<api-token>", # Optional API token
) )
``` ```
### Authentication ### Authentication
Authentication uses Better Auth. Register and sign in via the Elydora console or the Better Auth endpoints, then issue a long-lived API token for SDK use:
```python ```python
# Register a new user and organization # Sign up (Better Auth) — use the console or call directly:
reg = ElydoraClient.register(base_url, email, password, display_name=None, org_name=None) # POST /api/auth/sign-up/email { email, password, name }
#
# Login and receive a JWT # Sign in (Better Auth) — get a session token:
auth = ElydoraClient.login(base_url, email, password) # POST /api/auth/sign-in/email { email, password }
#
# Get current authenticated user profile # Issue a long-lived API token from an active session:
me = client.get_me()
# Issue a new API token (with optional TTL in seconds)
token_resp = client.issue_token(ttl_seconds=3600) token_resp = client.issue_token(ttl_seconds=3600)
# Update the token on an existing client instance
client.set_token("new-api-token")
``` ```
### Operations ### Operations
+22 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import time import time
import asyncio import asyncio
import warnings
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
import aiohttp import aiohttp
@@ -50,7 +51,7 @@ class AsyncElydoraClient:
base_url: API base URL. base_url: API base URL.
ttl_ms: Time-to-live for operations in milliseconds. ttl_ms: Time-to-live for operations in milliseconds.
max_retries: Maximum number of retries on transient failures. max_retries: Maximum number of retries on transient failures.
token: Optional JWT bearer token for authenticated endpoints. token: Optional bearer token for authenticated endpoints.
""" """
def __init__( def __init__(
@@ -171,7 +172,16 @@ class AsyncElydoraClient:
display_name: Optional[str] = None, display_name: Optional[str] = None,
org_name: Optional[str] = None, org_name: Optional[str] = None,
) -> AuthRegisterResponse: ) -> AuthRegisterResponse:
"""Register a new user and organization.""" """Register a new user and organization.
.. deprecated::
Use Better Auth endpoints directly. See docs.
"""
warnings.warn(
"AsyncElydoraClient.register() is deprecated. Use Better Auth endpoints directly. See docs.",
DeprecationWarning,
stacklevel=2,
)
url = f"{base_url.rstrip('/')}/v1/auth/register" url = f"{base_url.rstrip('/')}/v1/auth/register"
body: Dict[str, Any] = {"email": email, "password": password} body: Dict[str, Any] = {"email": email, "password": password}
if display_name is not None: if display_name is not None:
@@ -186,7 +196,16 @@ class AsyncElydoraClient:
@staticmethod @staticmethod
async def login(base_url: str, email: str, password: str) -> AuthLoginResponse: async def login(base_url: str, email: str, password: str) -> AuthLoginResponse:
"""Authenticate and receive a JWT.""" """Authenticate and receive a session token.
.. deprecated::
Use Better Auth endpoints directly. See docs.
"""
warnings.warn(
"AsyncElydoraClient.login() is deprecated. Use Better Auth endpoints directly. See docs.",
DeprecationWarning,
stacklevel=2,
)
url = f"{base_url.rstrip('/')}/v1/auth/login" url = f"{base_url.rstrip('/')}/v1/auth/login"
body = {"email": email, "password": password} body = {"email": email, "password": password}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
+1 -1
View File
@@ -172,7 +172,7 @@ def build_parser() -> argparse.ArgumentParser:
install_parser.add_argument("--agent_id", required=True, help="Agent ID") install_parser.add_argument("--agent_id", required=True, help="Agent ID")
install_parser.add_argument("--private_key", required=True, help="Base64url-encoded Ed25519 private key seed") install_parser.add_argument("--private_key", required=True, help="Base64url-encoded Ed25519 private key seed")
install_parser.add_argument("--kid", required=True, help="Key ID") install_parser.add_argument("--kid", required=True, help="Key ID")
install_parser.add_argument("--token", default="", help="JWT bearer token (optional)") install_parser.add_argument("--token", default="", help="API bearer token (optional)")
install_parser.add_argument("--base_url", default="https://api.elydora.com", help="API base URL") install_parser.add_argument("--base_url", default="https://api.elydora.com", help="API base URL")
# uninstall # uninstall
+22 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import time import time
import warnings
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
import requests import requests
@@ -49,7 +50,7 @@ class ElydoraClient:
base_url: API base URL. base_url: API base URL.
ttl_ms: Time-to-live for operations in milliseconds. ttl_ms: Time-to-live for operations in milliseconds.
max_retries: Maximum number of retries on transient failures. max_retries: Maximum number of retries on transient failures.
token: Optional JWT bearer token for authenticated endpoints. token: Optional bearer token for authenticated endpoints.
""" """
def __init__( def __init__(
@@ -168,7 +169,16 @@ class ElydoraClient:
display_name: Optional[str] = None, display_name: Optional[str] = None,
org_name: Optional[str] = None, org_name: Optional[str] = None,
) -> AuthRegisterResponse: ) -> AuthRegisterResponse:
"""Register a new user and organization.""" """Register a new user and organization.
.. deprecated::
Use Better Auth endpoints directly. See docs.
"""
warnings.warn(
"ElydoraClient.register() is deprecated. Use Better Auth endpoints directly. See docs.",
DeprecationWarning,
stacklevel=2,
)
url = f"{base_url.rstrip('/')}/v1/auth/register" url = f"{base_url.rstrip('/')}/v1/auth/register"
body: Dict[str, Any] = {"email": email, "password": password} body: Dict[str, Any] = {"email": email, "password": password}
if display_name is not None: if display_name is not None:
@@ -180,7 +190,16 @@ class ElydoraClient:
@staticmethod @staticmethod
def login(base_url: str, email: str, password: str) -> AuthLoginResponse: def login(base_url: str, email: str, password: str) -> AuthLoginResponse:
"""Authenticate and receive a JWT.""" """Authenticate and receive a session token.
.. deprecated::
Use Better Auth endpoints directly. See docs.
"""
warnings.warn(
"ElydoraClient.login() is deprecated. Use Better Auth endpoints directly. See docs.",
DeprecationWarning,
stacklevel=2,
)
url = f"{base_url.rstrip('/')}/v1/auth/login" url = f"{base_url.rstrip('/')}/v1/auth/login"
body = {"email": email, "password": password} body = {"email": email, "password": password}
resp = requests.post(url, json=body, headers={"Content-Type": "application/json"}, timeout=30) resp = requests.post(url, json=body, headers={"Content-Type": "application/json"}, timeout=30)