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:
1 parent
e8fa5e557e
commit
27a8f0667c
39 files changed
+1462
-641
No files matched your search
+2
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Generated
+363
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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 }) {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+363
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -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();
|
||||||
};
|
};
|
||||||
@@ -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 };
|
||||||
@@ -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(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
@@ -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) */
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Generated
+5
-2
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user