@elsium-ai/app 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,37 +8,522 @@ App bootstrap, HTTP server, and API routes for [ElsiumAI](https://github.com/els
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install @elsium-ai/app @elsium-ai/core
11
+ npm install @elsium-ai/app
12
12
  ```
13
13
 
14
14
  ## What's Inside
15
15
 
16
- - **HTTP Server** Built-in server with route definitions
17
- - **CORS** Configurable cross-origin resource sharing
18
- - **Auth** Authentication middleware
19
- - **Rate Limiting** Request rate limiting per client
20
- - **RBAC** Role-based access control with inheritance and wildcard matching
16
+ | Category | Export | Kind |
17
+ | --- | --- | --- |
18
+ | **App** | `createApp` | Function |
19
+ | | `ElsiumApp` | Interface |
20
+ | **Types** | `AppConfig` | Interface |
21
+ | | `ServerConfig` | Interface |
22
+ | | `CorsConfig` | Interface |
23
+ | | `AuthConfig` | Interface |
24
+ | | `RateLimitConfig` | Interface |
25
+ | | `ChatRequest` | Interface |
26
+ | | `ChatResponse` | Interface |
27
+ | | `CompleteRequest` | Interface |
28
+ | | `HealthResponse` | Interface |
29
+ | | `MetricsResponse` | Interface |
30
+ | **Middleware** | `corsMiddleware` | Function |
31
+ | | `authMiddleware` | Function |
32
+ | | `rateLimitMiddleware` | Function |
33
+ | **Routes** | `createRoutes` | Function |
34
+ | | `RoutesDeps` | Interface |
35
+ | **RBAC** | `createRBAC` | Function |
36
+ | | `Permission` | Type |
37
+ | | `Role` | Interface |
38
+ | | `RBACConfig` | Interface |
39
+ | | `RBAC` | Interface |
21
40
 
22
- ## Usage
41
+ ---
23
42
 
24
- ```typescript
25
- import { createApp, createRBAC } from '@elsium-ai/app'
43
+ ## App
44
+
45
+ ### `createApp`
46
+
47
+ Creates and returns a fully configured ElsiumAI application with a gateway, tracer, middleware stack, agent registry, and HTTP routes.
48
+
49
+ ```ts
50
+ function createApp(config: AppConfig): ElsiumApp
51
+ ```
52
+
53
+ | Parameter | Type | Description |
54
+ | --- | --- | --- |
55
+ | `config` | `AppConfig` | Full application configuration including gateway, agents, observability, and server settings. |
56
+
57
+ **Returns** `ElsiumApp` -- the application handle exposing the Hono instance, gateway, tracer, and a `listen` method to start the HTTP server.
58
+
59
+ ```ts
60
+ import { createApp } from '@elsium-ai/app'
61
+
62
+ const app = createApp({
63
+ gateway: {
64
+ providers: {
65
+ openai: { apiKey: process.env.OPENAI_API_KEY! },
66
+ },
67
+ defaultModel: 'gpt-4o',
68
+ },
69
+ server: {
70
+ port: 3000,
71
+ cors: { origin: ['http://localhost:5173'], credentials: true },
72
+ auth: { type: 'bearer', token: process.env.API_TOKEN! },
73
+ rateLimit: { windowMs: 60_000, maxRequests: 100 },
74
+ },
75
+ })
76
+
77
+ const { port, stop } = app.listen()
78
+ console.log(`Listening on port ${port}`)
79
+ ```
80
+
81
+ ### `ElsiumApp`
82
+
83
+ The object returned by `createApp`. Provides access to the underlying Hono app, gateway, tracer, and a method to start the HTTP server.
84
+
85
+ ```ts
86
+ interface ElsiumApp {
87
+ readonly hono: Hono
88
+ readonly gateway: Gateway
89
+ readonly tracer: Tracer
90
+ listen(port?: number): { port: number; stop: () => void }
91
+ }
92
+ ```
93
+
94
+ | Property / Method | Type | Description |
95
+ | --- | --- | --- |
96
+ | `hono` | `Hono` | The underlying Hono application instance. Use it to add custom routes or middleware. |
97
+ | `gateway` | `Gateway` | The configured LLM gateway. |
98
+ | `tracer` | `Tracer` | The observability tracer for cost and latency tracking. |
99
+ | `listen(port?)` | `(port?: number) => { port: number; stop: () => void }` | Starts the HTTP server. Falls back to `server.port` from config, then `3000`. Returns the resolved port and a `stop` function to shut down the server. |
100
+
101
+ ---
102
+
103
+ ## Types
104
+
105
+ ### `AppConfig`
106
+
107
+ Top-level configuration object passed to `createApp`.
108
+
109
+ ```ts
110
+ interface AppConfig {
111
+ gateway: {
112
+ providers: Record<string, { apiKey: string; baseUrl?: string }>
113
+ defaultModel?: string
114
+ }
115
+ agents?: Agent[]
116
+ rag?: RAGPipeline
117
+ observe?: {
118
+ tracing?: boolean
119
+ costTracking?: boolean
120
+ export?: string
121
+ }
122
+ server?: ServerConfig
123
+ }
124
+ ```
125
+
126
+ ### `ServerConfig`
127
+
128
+ HTTP server and middleware configuration.
129
+
130
+ ```ts
131
+ interface ServerConfig {
132
+ port?: number
133
+ hostname?: string
134
+ cors?: boolean | CorsConfig
135
+ auth?: AuthConfig
136
+ rateLimit?: RateLimitConfig
137
+ }
138
+ ```
139
+
140
+ ### `CorsConfig`
141
+
142
+ Fine-grained CORS settings. When `cors` in `ServerConfig` is set to `true`, sensible defaults are used.
143
+
144
+ ```ts
145
+ interface CorsConfig {
146
+ origin?: string | string[]
147
+ methods?: string[]
148
+ headers?: string[]
149
+ credentials?: boolean
150
+ }
151
+ ```
152
+
153
+ ### `AuthConfig`
154
+
155
+ Bearer-token authentication configuration. The middleware uses timing-safe comparison to validate tokens.
156
+
157
+ ```ts
158
+ interface AuthConfig {
159
+ type: 'bearer'
160
+ token: string
161
+ }
162
+ ```
163
+
164
+ ### `RateLimitConfig`
165
+
166
+ Per-client sliding-window rate limiting configuration.
167
+
168
+ ```ts
169
+ interface RateLimitConfig {
170
+ windowMs: number
171
+ maxRequests: number
172
+ }
173
+ ```
174
+
175
+ ### `ChatRequest`
176
+
177
+ Request body for the `POST /chat` endpoint.
178
+
179
+ ```ts
180
+ interface ChatRequest {
181
+ message: string
182
+ agent?: string
183
+ stream?: boolean
184
+ }
185
+ ```
186
+
187
+ ### `ChatResponse`
188
+
189
+ Response body from the `POST /chat` endpoint.
190
+
191
+ ```ts
192
+ interface ChatResponse {
193
+ message: string
194
+ usage: {
195
+ inputTokens: number
196
+ outputTokens: number
197
+ totalTokens: number
198
+ cost: number
199
+ }
200
+ model: string
201
+ traceId: string
202
+ }
203
+ ```
204
+
205
+ ### `CompleteRequest`
206
+
207
+ Request body for the `POST /complete` endpoint.
208
+
209
+ ```ts
210
+ interface CompleteRequest {
211
+ messages: Array<{ role: string; content: string }>
212
+ model?: string
213
+ system?: string
214
+ maxTokens?: number
215
+ temperature?: number
216
+ stream?: boolean
217
+ }
218
+ ```
219
+
220
+ ### `HealthResponse`
221
+
222
+ Response body from the `GET /health` endpoint.
223
+
224
+ ```ts
225
+ interface HealthResponse {
226
+ status: 'ok' | 'degraded'
227
+ version: string
228
+ uptime: number
229
+ providers: string[]
230
+ }
231
+ ```
232
+
233
+ ### `MetricsResponse`
234
+
235
+ Response body from the `GET /metrics` endpoint.
236
+
237
+ ```ts
238
+ interface MetricsResponse {
239
+ uptime: number
240
+ totalRequests: number
241
+ totalTokens: number
242
+ totalCost: number
243
+ byModel: Record<string, { requests: number; tokens: number; cost: number }>
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Middleware
250
+
251
+ All middleware functions return a Hono-compatible handler `(c: Context, next: Next) => Promise<...>`. They are applied automatically when the corresponding `ServerConfig` field is set, but they can also be used standalone on any Hono app.
252
+
253
+ ### `corsMiddleware`
254
+
255
+ Returns a Hono middleware that sets CORS headers and handles preflight `OPTIONS` requests.
256
+
257
+ ```ts
258
+ function corsMiddleware(config?: CorsConfig | boolean): (c: Context, next: Next) => Promise<Response | void>
259
+ ```
260
+
261
+ | Parameter | Type | Default | Description |
262
+ | --- | --- | --- | --- |
263
+ | `config` | `CorsConfig \| boolean` | `true` | When `true`, uses default methods `['GET', 'POST', 'OPTIONS']` and an empty origin list. Pass a `CorsConfig` object for fine-grained control. |
264
+
265
+ ```ts
266
+ import { corsMiddleware } from '@elsium-ai/app'
267
+ import { Hono } from 'hono'
268
+
269
+ const app = new Hono()
270
+
271
+ app.use('*', corsMiddleware({
272
+ origin: ['https://myapp.com'],
273
+ methods: ['GET', 'POST'],
274
+ credentials: true,
275
+ }))
276
+ ```
277
+
278
+ ### `authMiddleware`
279
+
280
+ Returns a Hono middleware that validates `Authorization: Bearer <token>` headers using timing-safe comparison. The `/health` endpoint is always excluded from auth checks.
281
+
282
+ ```ts
283
+ function authMiddleware(config: AuthConfig): (c: Context, next: Next) => Promise<Response | void>
284
+ ```
285
+
286
+ | Parameter | Type | Description |
287
+ | --- | --- | --- |
288
+ | `config` | `AuthConfig` | Must specify `type: 'bearer'` and the expected `token` string. |
289
+
290
+ **Responses on failure:**
291
+ - `401` with `{ error: 'Missing Authorization header' }` when the header is absent.
292
+ - `401` with `{ error: 'Invalid token' }` when the token does not match.
293
+
294
+ ```ts
295
+ import { authMiddleware } from '@elsium-ai/app'
296
+ import { Hono } from 'hono'
297
+
298
+ const app = new Hono()
299
+
300
+ app.use('*', authMiddleware({
301
+ type: 'bearer',
302
+ token: process.env.API_TOKEN!,
303
+ }))
304
+ ```
305
+
306
+ ### `rateLimitMiddleware`
307
+
308
+ Returns a Hono middleware that enforces per-client rate limiting using an in-memory sliding window. Client identity is determined from the `CF-Connecting-IP` header, then `X-Real-IP`, falling back to `'anonymous'`. Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` response headers.
309
+
310
+ ```ts
311
+ function rateLimitMiddleware(config: RateLimitConfig): (c: Context, next: Next) => Promise<Response | void>
312
+ ```
313
+
314
+ | Parameter | Type | Description |
315
+ | --- | --- | --- |
316
+ | `config` | `RateLimitConfig` | `windowMs` is the time window in milliseconds; `maxRequests` is the maximum number of requests allowed per window. |
317
+
318
+ **Responses on failure:**
319
+ - `429` with `{ error: 'Too many requests', retryAfterMs: number }` when the limit is exceeded.
320
+
321
+ ```ts
322
+ import { rateLimitMiddleware } from '@elsium-ai/app'
323
+ import { Hono } from 'hono'
324
+
325
+ const app = new Hono()
326
+
327
+ app.use('*', rateLimitMiddleware({
328
+ windowMs: 60_000,
329
+ maxRequests: 100,
330
+ }))
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Routes
336
+
337
+ ### `createRoutes`
338
+
339
+ Creates a Hono sub-application with all built-in API routes: `GET /health`, `GET /metrics`, `POST /chat`, `POST /complete`, and `GET /agents`.
340
+
341
+ ```ts
342
+ function createRoutes(deps: RoutesDeps): Hono
343
+ ```
344
+
345
+ | Parameter | Type | Description |
346
+ | --- | --- | --- |
347
+ | `deps` | `RoutesDeps` | Dependencies injected into route handlers, including the gateway, agent registry, tracer, and server metadata. |
348
+
349
+ **Returns** a `Hono` instance with the following routes:
350
+
351
+ | Method | Path | Description |
352
+ | --- | --- | --- |
353
+ | `GET` | `/health` | Returns a `HealthResponse` with status, version, uptime, and provider list. |
354
+ | `GET` | `/metrics` | Returns a `MetricsResponse` with request counts, token usage, and cost breakdowns. |
355
+ | `POST` | `/chat` | Accepts a `ChatRequest`, dispatches to the specified (or default) agent, and returns a `ChatResponse`. |
356
+ | `POST` | `/complete` | Accepts a `CompleteRequest`, forwards to the gateway, and returns the completion result. |
357
+ | `GET` | `/agents` | Lists all registered agents with their names, models, and tool names. |
358
+
359
+ ```ts
360
+ import { createRoutes } from '@elsium-ai/app'
361
+ import { Hono } from 'hono'
362
+
363
+ const routes = createRoutes({
364
+ gateway: myGateway,
365
+ agents: new Map([['assistant', myAgent]]),
366
+ defaultAgent: myAgent,
367
+ tracer: myTracer,
368
+ startTime: Date.now(),
369
+ version: '1.0.0',
370
+ providers: ['openai'],
371
+ })
372
+
373
+ const app = new Hono()
374
+ app.route('/', routes)
375
+ ```
376
+
377
+ ### `RoutesDeps`
378
+
379
+ Dependency injection interface for `createRoutes`.
380
+
381
+ ```ts
382
+ interface RoutesDeps {
383
+ gateway: Gateway
384
+ agents: Map<string, Agent>
385
+ defaultAgent?: Agent
386
+ tracer?: Tracer
387
+ startTime: number
388
+ version: string
389
+ providers: string[]
390
+ }
391
+ ```
392
+
393
+ | Field | Type | Description |
394
+ | --- | --- | --- |
395
+ | `gateway` | `Gateway` | The LLM gateway used by the `/complete` endpoint. |
396
+ | `agents` | `Map<string, Agent>` | Registry of named agents used by the `/chat` endpoint. |
397
+ | `defaultAgent` | `Agent` (optional) | The agent used when no `agent` field is specified in a chat request. |
398
+ | `tracer` | `Tracer` (optional) | Observability tracer for tracking LLM calls. |
399
+ | `startTime` | `number` | Timestamp (ms) when the server started, used to calculate uptime. |
400
+ | `version` | `string` | Application version string returned by `/health`. |
401
+ | `providers` | `string[]` | List of configured provider names returned by `/health`. |
402
+
403
+ ---
404
+
405
+ ## RBAC
406
+
407
+ ### `createRBAC`
408
+
409
+ Creates a role-based access control system with permission checking, role inheritance, wildcard matching, and Hono middleware generation. Includes four built-in roles (`admin`, `operator`, `user`, `viewer`) that can be overridden by user-defined roles.
410
+
411
+ ```ts
412
+ function createRBAC(config: RBACConfig): RBAC
413
+ ```
414
+
415
+ | Parameter | Type | Description |
416
+ | --- | --- | --- |
417
+ | `config` | `RBACConfig` | Defines custom roles, a default role, and how the role is extracted from each request. |
418
+
419
+ **Returns** an `RBAC` object with methods for permission checking and middleware creation.
420
+
421
+ **Built-in roles:**
422
+
423
+ | Role | Permissions |
424
+ | --- | --- |
425
+ | `admin` | `model:use:*`, `agent:execute:*`, `tool:call:*`, `config:read`, `config:write`, `audit:read`, `audit:write` |
426
+ | `operator` | `model:use:*`, `agent:execute:*`, `tool:call:*`, `config:read`, `audit:read` |
427
+ | `user` | `model:use`, `agent:execute`, `tool:call` |
428
+ | `viewer` | `config:read`, `audit:read` |
429
+
430
+ ```ts
431
+ import { createRBAC } from '@elsium-ai/app'
26
432
 
27
433
  const rbac = createRBAC({
28
434
  roles: [
29
- { name: 'viewer', permissions: ['model:read:*'] },
30
- { name: 'analyst', permissions: ['model:use:gpt-4o-mini'], inherits: ['viewer'] },
31
- { name: 'admin', permissions: ['*'], inherits: ['analyst'] },
435
+ {
436
+ name: 'analyst',
437
+ permissions: ['model:use:gpt-4o-mini'],
438
+ inherits: ['viewer'],
439
+ },
32
440
  ],
441
+ defaultRole: 'viewer',
33
442
  })
34
443
 
35
- const app = createApp({
36
- port: 3000,
37
- cors: { origins: ['http://localhost:5173'] },
38
- rateLimit: { windowMs: 60_000, max: 100 },
39
- })
444
+ // Check a permission
445
+ rbac.hasPermission('analyst', 'model:use:gpt-4o-mini') // true
446
+ rbac.hasPermission('analyst', 'config:read') // true (inherited from viewer)
447
+
448
+ // Use as Hono middleware
449
+ app.post('/chat', rbac.middleware('model:use'), handler)
450
+ ```
451
+
452
+ ### `Permission`
453
+
454
+ A union type representing all recognized permissions. Supports resource-specific and wildcard variants.
455
+
456
+ ```ts
457
+ type Permission =
458
+ | 'model:use'
459
+ | 'model:use:*'
460
+ | `model:use:${string}`
461
+ | 'agent:execute'
462
+ | 'agent:execute:*'
463
+ | `agent:execute:${string}`
464
+ | 'tool:call'
465
+ | 'tool:call:*'
466
+ | `tool:call:${string}`
467
+ | 'config:read'
468
+ | 'config:write'
469
+ | 'audit:read'
470
+ | 'audit:write'
471
+ ```
472
+
473
+ Wildcard permissions (e.g., `model:use:*`) grant access to all resource-specific permissions under that namespace (e.g., `model:use:gpt-4o`) as well as the base permission (`model:use`).
474
+
475
+ ### `Role`
476
+
477
+ Defines a named role with a set of permissions and optional inheritance from other roles.
478
+
479
+ ```ts
480
+ interface Role {
481
+ name: string
482
+ permissions: Permission[]
483
+ inherits?: string[]
484
+ }
485
+ ```
486
+
487
+ ### `RBACConfig`
488
+
489
+ Configuration for `createRBAC`.
490
+
491
+ ```ts
492
+ interface RBACConfig {
493
+ roles: Role[]
494
+ defaultRole?: string
495
+ roleExtractor?: (c: Context) => string | undefined
496
+ trustRoleHeader?: boolean
497
+ }
40
498
  ```
41
499
 
500
+ | Field | Type | Description |
501
+ | --- | --- | --- |
502
+ | `roles` | `Role[]` | Custom role definitions. These override built-in roles with the same name. |
503
+ | `defaultRole` | `string` (optional) | The role assigned when no role can be determined from the request. Defaults to `'viewer'`. |
504
+ | `roleExtractor` | `(c: Context) => string \| undefined` (optional) | Custom function to extract the role name from a Hono request context. |
505
+ | `trustRoleHeader` | `boolean` (optional) | When `true`, reads the role from the `X-Role` request header. **Warning:** only enable this in development or behind a trusted reverse proxy, as any client can self-assign roles. |
506
+
507
+ ### `RBAC`
508
+
509
+ The object returned by `createRBAC`.
510
+
511
+ ```ts
512
+ interface RBAC {
513
+ hasPermission(role: string, permission: Permission): boolean
514
+ middleware(required: Permission): (c: Context, next: Next) => Promise<Response | undefined>
515
+ getRolePermissions(role: string): Permission[]
516
+ }
517
+ ```
518
+
519
+ | Method | Description |
520
+ | --- | --- |
521
+ | `hasPermission(role, permission)` | Returns `true` if the given role (including inherited permissions) grants the specified permission. |
522
+ | `middleware(required)` | Returns a Hono middleware that rejects requests with `403` if the caller's role lacks the required permission. |
523
+ | `getRolePermissions(role)` | Returns the deduplicated list of all permissions for a role, including those inherited from parent roles. |
524
+
525
+ ---
526
+
42
527
  ## Part of ElsiumAI
43
528
 
44
529
  This package is the app layer of the [ElsiumAI](https://github.com/elsium-ai/elsium-ai) framework. See the [full documentation](https://github.com/elsium-ai/elsium-ai) for guides and examples.
package/dist/app.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface ElsiumApp {
8
8
  readonly tracer: Tracer;
9
9
  listen(port?: number): {
10
10
  port: number;
11
- stop: () => void;
11
+ stop: () => Promise<void>;
12
12
  };
13
13
  }
14
14
  export declare function createApp(config: AppConfig): ElsiumApp;
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,KAAK,MAAM,EAAW,MAAM,oBAAoB,CAAA;AAIzD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CACzD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CA8FtD"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,KAAK,MAAM,EAAW,MAAM,oBAAoB,CAAA;AAIzD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAA;CAClE;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAgItD"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { createApp } from './app';
2
2
  export type { ElsiumApp } from './app';
3
3
  export type { AppConfig, ServerConfig, CorsConfig, AuthConfig, RateLimitConfig, ChatRequest, ChatResponse, CompleteRequest, HealthResponse, MetricsResponse, } from './types';
4
- export { corsMiddleware, authMiddleware, rateLimitMiddleware } from './middleware';
4
+ export { corsMiddleware, authMiddleware, rateLimitMiddleware, requestIdMiddleware, requestLoggerMiddleware, } from './middleware';
5
5
  export { createRoutes } from './routes';
6
6
  export type { RoutesDeps } from './routes';
7
7
  export { createRBAC } from './rbac';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,YAAY,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,EACd,eAAe,GACf,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGlF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACjC,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,YAAY,EACX,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,EACd,eAAe,GACf,MAAM,SAAS,CAAA;AAGhB,OAAO,EACN,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,GACvB,MAAM,cAAc,CAAA;AAGrB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AACvC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA"}
package/dist/index.js CHANGED
@@ -389,6 +389,101 @@ function createLogger(options = {}) {
389
389
  }
390
390
  };
391
391
  }
392
+ // ../core/src/shutdown.ts
393
+ function createShutdownManager(config) {
394
+ const drainTimeoutMs = config?.drainTimeoutMs ?? 30000;
395
+ const signals = config?.signals ?? ["SIGTERM", "SIGINT"];
396
+ if (drainTimeoutMs < 0 || !Number.isFinite(drainTimeoutMs)) {
397
+ throw new ElsiumError({
398
+ code: "CONFIG_ERROR",
399
+ message: "drainTimeoutMs must be >= 0 and finite",
400
+ retryable: false
401
+ });
402
+ }
403
+ let shuttingDown = false;
404
+ let inFlightCount = 0;
405
+ let drainResolve = null;
406
+ let shutdownPromise = null;
407
+ const signalHandlers = [];
408
+ function checkDrained() {
409
+ if (inFlightCount === 0 && drainResolve) {
410
+ drainResolve();
411
+ drainResolve = null;
412
+ }
413
+ }
414
+ async function shutdown() {
415
+ if (shutdownPromise)
416
+ return shutdownPromise;
417
+ shuttingDown = true;
418
+ shutdownPromise = (async () => {
419
+ config?.onDrainStart?.();
420
+ if (inFlightCount === 0) {
421
+ config?.onDrainComplete?.();
422
+ return;
423
+ }
424
+ const drainPromise = new Promise((resolve) => {
425
+ drainResolve = resolve;
426
+ });
427
+ let drainTimer;
428
+ const timeoutPromise = new Promise((resolve) => {
429
+ drainTimer = setTimeout(() => resolve("timeout"), drainTimeoutMs);
430
+ });
431
+ const result = await Promise.race([
432
+ drainPromise.then(() => "drained"),
433
+ timeoutPromise
434
+ ]);
435
+ if (drainTimer !== undefined)
436
+ clearTimeout(drainTimer);
437
+ if (result === "timeout") {
438
+ config?.onForceShutdown?.();
439
+ } else {
440
+ config?.onDrainComplete?.();
441
+ }
442
+ })();
443
+ return shutdownPromise;
444
+ }
445
+ const manager = {
446
+ get inFlight() {
447
+ return inFlightCount;
448
+ },
449
+ get isShuttingDown() {
450
+ return shuttingDown;
451
+ },
452
+ async trackOperation(fn) {
453
+ if (shuttingDown) {
454
+ throw new ElsiumError({
455
+ code: "VALIDATION_ERROR",
456
+ message: "Server is shutting down, not accepting new operations",
457
+ retryable: true
458
+ });
459
+ }
460
+ inFlightCount++;
461
+ try {
462
+ return await fn();
463
+ } finally {
464
+ inFlightCount--;
465
+ checkDrained();
466
+ }
467
+ },
468
+ shutdown,
469
+ dispose() {
470
+ for (const { signal, handler } of signalHandlers) {
471
+ process.removeListener(signal, handler);
472
+ }
473
+ signalHandlers.length = 0;
474
+ }
475
+ };
476
+ if (typeof process !== "undefined" && process.on) {
477
+ for (const signal of signals) {
478
+ const handler = () => {
479
+ manager.shutdown();
480
+ };
481
+ signalHandlers.push({ signal, handler });
482
+ process.on(signal, handler);
483
+ }
484
+ }
485
+ return manager;
486
+ }
392
487
  // ../gateway/src/provider.ts
393
488
  var providerRegistry = new Map;
394
489
  var metadataRegistry = new Map;
@@ -978,7 +1073,19 @@ function createGoogleProvider(config) {
978
1073
  return { role, parts };
979
1074
  }
980
1075
  function formatGeminiMultipartContent(msg, role) {
981
- const parts = msg.content.filter((p) => p.type === "text").map((p) => ({ text: p.text }));
1076
+ const parts = [];
1077
+ for (const p of msg.content) {
1078
+ if (p.type === "text") {
1079
+ parts.push({ text: p.text });
1080
+ } else if (p.type === "image") {
1081
+ const img = p;
1082
+ if (img.source.type === "base64") {
1083
+ parts.push({ inlineData: { mimeType: img.source.mediaType, data: img.source.data } });
1084
+ } else {
1085
+ parts.push({ fileData: { mimeType: "image/jpeg", fileUri: img.source.url } });
1086
+ }
1087
+ }
1088
+ }
982
1089
  return { role, parts };
983
1090
  }
984
1091
  function formatMessages(messages) {
@@ -1153,7 +1260,8 @@ async function handleGoogleErrorResponse(response) {
1153
1260
  throw ElsiumError.authError("google");
1154
1261
  }
1155
1262
  if (response.status === 429) {
1156
- throw ElsiumError.rateLimit("google");
1263
+ const retryAfter = response.headers.get("retry-after");
1264
+ throw ElsiumError.rateLimit("google", retryAfter ? Number.parseInt(retryAfter) * 1000 : undefined);
1157
1265
  }
1158
1266
  throw ElsiumError.providerError(`Google API error ${response.status}: ${errorBody}`, {
1159
1267
  provider: "google",
@@ -1339,6 +1447,24 @@ function createOpenAIProvider(config) {
1339
1447
  }
1340
1448
  return openaiMsg;
1341
1449
  }
1450
+ function formatUserContent(msg) {
1451
+ if (typeof msg.content === "string")
1452
+ return msg.content;
1453
+ const parts = [];
1454
+ for (const part of msg.content) {
1455
+ if (part.type === "text") {
1456
+ parts.push({ type: "text", text: part.text });
1457
+ } else if (part.type === "image") {
1458
+ if (part.source.type === "base64") {
1459
+ const url = `data:${part.source.mediaType};base64,${part.source.data}`;
1460
+ parts.push({ type: "image_url", image_url: { url } });
1461
+ } else {
1462
+ parts.push({ type: "image_url", image_url: { url: part.source.url } });
1463
+ }
1464
+ }
1465
+ }
1466
+ return parts;
1467
+ }
1342
1468
  function formatMessages(messages) {
1343
1469
  const formatted = [];
1344
1470
  for (const msg of messages) {
@@ -1354,7 +1480,7 @@ function createOpenAIProvider(config) {
1354
1480
  formatted.push(formatAssistantMessage(msg));
1355
1481
  continue;
1356
1482
  }
1357
- formatted.push({ role: "user", content: extractTextContent(msg) });
1483
+ formatted.push({ role: "user", content: formatUserContent(msg) });
1358
1484
  }
1359
1485
  return formatted;
1360
1486
  }
@@ -1590,7 +1716,7 @@ var PROVIDER_FACTORIES = {
1590
1716
  openai: createOpenAIProvider,
1591
1717
  google: createGoogleProvider
1592
1718
  };
1593
- function gateway(config) {
1719
+ function validateGatewayConfig(config) {
1594
1720
  const factory = PROVIDER_FACTORIES[config.provider];
1595
1721
  if (!factory) {
1596
1722
  throw new ElsiumError({
@@ -1599,21 +1725,92 @@ function gateway(config) {
1599
1725
  retryable: false
1600
1726
  });
1601
1727
  }
1728
+ if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
1729
+ throw new ElsiumError({
1730
+ code: "CONFIG_ERROR",
1731
+ message: "apiKey must be a non-empty string",
1732
+ retryable: false
1733
+ });
1734
+ }
1735
+ if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
1736
+ throw new ElsiumError({
1737
+ code: "CONFIG_ERROR",
1738
+ message: "timeout must be a positive finite number",
1739
+ retryable: false
1740
+ });
1741
+ }
1742
+ if (config.maxRetries !== undefined && (!Number.isFinite(config.maxRetries) || !Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
1743
+ throw new ElsiumError({
1744
+ code: "CONFIG_ERROR",
1745
+ message: "maxRetries must be a non-negative finite integer",
1746
+ retryable: false
1747
+ });
1748
+ }
1749
+ return factory;
1750
+ }
1751
+ function autoRegisterProvider(provider) {
1752
+ if (!provider.metadata)
1753
+ return;
1754
+ registerProviderMetadata(provider.name, provider.metadata);
1755
+ if (!provider.metadata.pricing)
1756
+ return;
1757
+ for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
1758
+ registerPricing(model, pricing);
1759
+ }
1760
+ }
1761
+ function validateRequestLimits(request, maxMessages, maxInputTokens) {
1762
+ if (request.messages.length > maxMessages) {
1763
+ throw ElsiumError.validation(`Message count ${request.messages.length} exceeds limit of ${maxMessages}`);
1764
+ }
1765
+ let estimatedTokens = 0;
1766
+ for (const msg of request.messages) {
1767
+ const text = typeof msg.content === "string" ? msg.content : msg.content.map((p) => p.type === "text" ? p.text : "").join("");
1768
+ estimatedTokens += Math.ceil(text.length / 4);
1769
+ }
1770
+ if (estimatedTokens > maxInputTokens) {
1771
+ throw ElsiumError.validation(`Estimated input tokens (~${estimatedTokens}) exceeds limit of ${maxInputTokens}`);
1772
+ }
1773
+ }
1774
+ function buildMiddlewareContext(req, providerName, defaultModel, metadata) {
1775
+ return {
1776
+ request: req,
1777
+ provider: providerName,
1778
+ model: req.model ?? defaultModel,
1779
+ traceId: generateTraceId(),
1780
+ startTime: performance.now(),
1781
+ metadata
1782
+ };
1783
+ }
1784
+ async function accumulateStreamEvents(stream, emit) {
1785
+ let textContent = "";
1786
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
1787
+ let stopReason = "end_turn";
1788
+ let id = "";
1789
+ for await (const event of stream) {
1790
+ emit(event);
1791
+ if (event.type === "text_delta") {
1792
+ textContent += event.text;
1793
+ } else if (event.type === "message_end") {
1794
+ usage = event.usage;
1795
+ stopReason = event.stopReason;
1796
+ } else if (event.type === "message_start") {
1797
+ id = event.id;
1798
+ }
1799
+ }
1800
+ return { textContent, usage, stopReason, id };
1801
+ }
1802
+ function gateway(config) {
1803
+ const factory = validateGatewayConfig(config);
1602
1804
  const provider = factory({
1603
1805
  apiKey: config.apiKey,
1604
1806
  baseUrl: config.baseUrl,
1605
1807
  timeout: config.timeout,
1606
1808
  maxRetries: config.maxRetries
1607
1809
  });
1608
- if (provider.metadata) {
1609
- registerProviderMetadata(provider.name, provider.metadata);
1610
- if (provider.metadata.pricing) {
1611
- for (const [model, pricing] of Object.entries(provider.metadata.pricing)) {
1612
- registerPricing(model, pricing);
1613
- }
1614
- }
1615
- }
1810
+ autoRegisterProvider(provider);
1616
1811
  const defaultModel = config.model ?? provider.defaultModel;
1812
+ const maxMessages = config.maxMessages ?? 1000;
1813
+ const maxInputTokens = config.maxInputTokens ?? 1e6;
1617
1814
  let xrayStore = null;
1618
1815
  const allMiddleware = [...config.middleware ?? []];
1619
1816
  if (config.xray) {
@@ -1628,14 +1825,7 @@ function gateway(config) {
1628
1825
  if (!composedMiddleware) {
1629
1826
  return provider.complete(req);
1630
1827
  }
1631
- const ctx = {
1632
- request: req,
1633
- provider: provider.name,
1634
- model: req.model ?? defaultModel,
1635
- traceId: generateTraceId(),
1636
- startTime: performance.now(),
1637
- metadata: request.metadata ?? {}
1638
- };
1828
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1639
1829
  return composedMiddleware(ctx, async (c) => provider.complete(c.request));
1640
1830
  }
1641
1831
  return {
@@ -1647,34 +1837,27 @@ function gateway(config) {
1647
1837
  return xrayStore?.callHistory(limit) ?? [];
1648
1838
  },
1649
1839
  async complete(request) {
1840
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1650
1841
  return executeWithMiddleware(request);
1651
1842
  },
1652
1843
  stream(request) {
1844
+ validateRequestLimits(request, maxMessages, maxInputTokens);
1653
1845
  const req = { ...request, model: request.model ?? defaultModel };
1654
1846
  if (composedMiddleware) {
1655
- const ctx = {
1656
- request: req,
1657
- provider: provider.name,
1658
- model: req.model ?? defaultModel,
1659
- traceId: generateTraceId(),
1660
- startTime: performance.now(),
1661
- metadata: request.metadata ?? {}
1662
- };
1847
+ const ctx = buildMiddlewareContext(req, provider.name, defaultModel, request.metadata ?? {});
1663
1848
  return createStream(async (emit) => {
1664
1849
  await composedMiddleware(ctx, async (c) => {
1665
- const stream = provider.stream(c.request);
1666
- for await (const event of stream) {
1667
- emit(event);
1668
- }
1850
+ const result = await accumulateStreamEvents(provider.stream(c.request), emit);
1851
+ const latencyMs = Math.round(performance.now() - ctx.startTime);
1669
1852
  return {
1670
- id: "",
1671
- message: { role: "assistant", content: "" },
1672
- usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
1673
- cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: "USD" },
1853
+ id: result.id,
1854
+ message: { role: "assistant", content: result.textContent },
1855
+ usage: result.usage,
1856
+ cost: calculateCost(c.model, result.usage),
1674
1857
  model: c.model,
1675
1858
  provider: provider.name,
1676
- stopReason: "end_turn",
1677
- latencyMs: 0,
1859
+ stopReason: result.stopReason,
1860
+ latencyMs,
1678
1861
  traceId: ctx.traceId
1679
1862
  };
1680
1863
  });
@@ -1854,6 +2037,7 @@ function createSpan(name, options = {}) {
1854
2037
  return span;
1855
2038
  }
1856
2039
  // ../observe/src/tracer.ts
2040
+ import { writeFileSync } from "node:fs";
1857
2041
  var log2 = createLogger();
1858
2042
  function observe(config = {}) {
1859
2043
  const {
@@ -1869,7 +2053,21 @@ function observe(config = {}) {
1869
2053
  for (const out of output) {
1870
2054
  if (out === "console") {
1871
2055
  handlers.push(consoleHandler);
1872
- } else if (out === "json-file") {} else {
2056
+ } else if (out === "json-file") {
2057
+ exporters.push({
2058
+ name: "json-file",
2059
+ export(spansToExport) {
2060
+ const filename = `.elsium/traces-${Date.now()}.json`;
2061
+ try {
2062
+ writeFileSync(filename, JSON.stringify(spansToExport, null, 2));
2063
+ } catch (err2) {
2064
+ log2.error("Failed to write trace file", {
2065
+ error: err2 instanceof Error ? err2.message : String(err2)
2066
+ });
2067
+ }
2068
+ }
2069
+ });
2070
+ } else {
1873
2071
  exporters.push(out);
1874
2072
  }
1875
2073
  }
@@ -4049,7 +4247,7 @@ var Hono2 = class extends Hono {
4049
4247
  // src/middleware.ts
4050
4248
  import { timingSafeEqual } from "node:crypto";
4051
4249
  function corsMiddleware(config = true) {
4052
- const opts = typeof config === "boolean" ? { origin: [], methods: ["GET", "POST", "OPTIONS"] } : config;
4250
+ const opts = typeof config === "boolean" ? { origin: "*", methods: ["GET", "POST", "OPTIONS"] } : config;
4053
4251
  return async (c, next) => {
4054
4252
  const requestOrigin = c.req.header("Origin") ?? "";
4055
4253
  let allowedOrigin;
@@ -4126,8 +4324,51 @@ function rateLimitMiddleware(config) {
4126
4324
  await next();
4127
4325
  };
4128
4326
  }
4327
+ function requestIdMiddleware() {
4328
+ return async (c, next) => {
4329
+ const raw2 = c.req.header("X-Request-ID");
4330
+ const id = raw2 && /^[\w\-.:]{1,128}$/.test(raw2) ? raw2 : generateId("req");
4331
+ c.set("requestId", id);
4332
+ await next();
4333
+ c.res.headers.set("X-Request-ID", id);
4334
+ };
4335
+ }
4336
+ function requestLoggerMiddleware(logger) {
4337
+ const log4 = logger ?? createLogger();
4338
+ return async (c, next) => {
4339
+ const start = Date.now();
4340
+ await next();
4341
+ const duration = Date.now() - start;
4342
+ log4.info(`${c.req.method} ${c.req.path}`, {
4343
+ method: c.req.method,
4344
+ path: c.req.path,
4345
+ status: c.res.status,
4346
+ durationMs: duration,
4347
+ requestId: c.get("requestId")
4348
+ });
4349
+ };
4350
+ }
4129
4351
 
4130
4352
  // src/routes.ts
4353
+ function parseJsonBody(raw2) {
4354
+ try {
4355
+ return { ok: true, data: JSON.parse(raw2) };
4356
+ } catch {
4357
+ return { ok: false };
4358
+ }
4359
+ }
4360
+ function elsiumErrorResponse(c, err2, fallbackMessage) {
4361
+ if (err2 instanceof ElsiumError) {
4362
+ return c.json({ error: err2.message, code: err2.code }, err2.statusCode ?? 500);
4363
+ }
4364
+ return c.json({ error: fallbackMessage }, 500);
4365
+ }
4366
+ function resolveAgent(name, agents, defaultAgent) {
4367
+ const agent = name ? agents.get(name) : defaultAgent;
4368
+ if (agent)
4369
+ return { agent };
4370
+ return { error: name ? `Agent "${name}" not found` : "No default agent configured" };
4371
+ }
4131
4372
  function createRoutes(deps) {
4132
4373
  const app = new Hono2;
4133
4374
  let totalRequests = 0;
@@ -4168,17 +4409,24 @@ function createRoutes(deps) {
4168
4409
  if (rawText.length > MAX_BODY_SIZE) {
4169
4410
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
4170
4411
  }
4171
- const body = JSON.parse(rawText);
4412
+ const parsed = parseJsonBody(rawText);
4413
+ if (!parsed.ok) {
4414
+ return c.json({ error: "Invalid JSON in request body" }, 400);
4415
+ }
4416
+ const body = parsed.data;
4172
4417
  if (!body.message) {
4173
4418
  return c.json({ error: "message is required" }, 400);
4174
4419
  }
4175
- const agent = body.agent ? deps.agents.get(body.agent) : deps.defaultAgent;
4176
- if (!agent) {
4177
- return c.json({
4178
- error: body.agent ? `Agent "${body.agent}" not found` : "No default agent configured"
4179
- }, 404);
4420
+ const resolved = resolveAgent(body.agent, deps.agents, deps.defaultAgent);
4421
+ if ("error" in resolved) {
4422
+ return c.json({ error: resolved.error }, 404);
4423
+ }
4424
+ let result;
4425
+ try {
4426
+ result = await resolved.agent.run(body.message);
4427
+ } catch (err2) {
4428
+ return elsiumErrorResponse(c, err2, "Agent execution failed");
4180
4429
  }
4181
- const result = await agent.run(body.message);
4182
4430
  deps.tracer?.trackLLMCall({
4183
4431
  model: "unknown",
4184
4432
  inputTokens: result.usage.totalInputTokens,
@@ -4195,7 +4443,7 @@ function createRoutes(deps) {
4195
4443
  totalTokens: result.usage.totalTokens,
4196
4444
  cost: result.usage.totalCost
4197
4445
  },
4198
- model: agent.config.model ?? "default",
4446
+ model: resolved.agent.config.model ?? "default",
4199
4447
  traceId: result.traceId
4200
4448
  };
4201
4449
  return c.json(response);
@@ -4207,7 +4455,11 @@ function createRoutes(deps) {
4207
4455
  if (rawText.length > MAX_BODY_SIZE) {
4208
4456
  return c.json({ error: "Request body too large (max 1MB)" }, 413);
4209
4457
  }
4210
- const body = JSON.parse(rawText);
4458
+ const parsed = parseJsonBody(rawText);
4459
+ if (!parsed.ok) {
4460
+ return c.json({ error: "Invalid JSON in request body" }, 400);
4461
+ }
4462
+ const body = parsed.data;
4211
4463
  if (!body.messages?.length) {
4212
4464
  return c.json({ error: "messages array is required" }, 400);
4213
4465
  }
@@ -4215,13 +4467,18 @@ function createRoutes(deps) {
4215
4467
  role: m.role,
4216
4468
  content: m.content
4217
4469
  }));
4218
- const response = await deps.gateway.complete({
4219
- messages,
4220
- model: body.model,
4221
- system: body.system,
4222
- maxTokens: body.maxTokens,
4223
- temperature: body.temperature
4224
- });
4470
+ let response;
4471
+ try {
4472
+ response = await deps.gateway.complete({
4473
+ messages,
4474
+ model: body.model,
4475
+ system: body.system,
4476
+ maxTokens: body.maxTokens,
4477
+ temperature: body.temperature
4478
+ });
4479
+ } catch (err2) {
4480
+ return elsiumErrorResponse(c, err2, "Completion failed");
4481
+ }
4225
4482
  deps.tracer?.trackLLMCall({
4226
4483
  model: response.model,
4227
4484
  inputTokens: response.usage.inputTokens,
@@ -4253,6 +4510,15 @@ function createRoutes(deps) {
4253
4510
  var log4 = createLogger();
4254
4511
  function createApp(config) {
4255
4512
  const app = new Hono2;
4513
+ app.onError((err2, c) => {
4514
+ const statusCode = err2 instanceof ElsiumError ? err2.statusCode ?? 500 : 500;
4515
+ const code = err2 instanceof ElsiumError ? err2.code : "UNKNOWN";
4516
+ log4.error("Unhandled error", { error: err2.message, code, path: c.req.path });
4517
+ return c.json({ error: err2.message, code }, statusCode);
4518
+ });
4519
+ app.notFound((c) => {
4520
+ return c.json({ error: "Not found" }, 404);
4521
+ });
4256
4522
  const providerNames = Object.keys(config.gateway.providers);
4257
4523
  const primaryProvider = providerNames[0];
4258
4524
  const primaryConfig = config.gateway.providers[primaryProvider];
@@ -4267,6 +4533,8 @@ function createApp(config) {
4267
4533
  costTracking: config.observe?.costTracking ?? true
4268
4534
  });
4269
4535
  const serverConfig = config.server ?? {};
4536
+ app.use("*", requestIdMiddleware());
4537
+ app.use("*", requestLoggerMiddleware(log4));
4270
4538
  if (serverConfig.cors) {
4271
4539
  app.use("*", corsMiddleware(serverConfig.cors));
4272
4540
  }
@@ -4289,7 +4557,7 @@ function createApp(config) {
4289
4557
  defaultAgent,
4290
4558
  tracer,
4291
4559
  startTime: Date.now(),
4292
- version: "0.1.0",
4560
+ version: config.version ?? "0.2.2",
4293
4561
  providers: providerNames
4294
4562
  });
4295
4563
  app.route("/", routes);
@@ -4305,13 +4573,25 @@ function createApp(config) {
4305
4573
  port: listenPort,
4306
4574
  hostname
4307
4575
  });
4576
+ let shutdownManager;
4577
+ if (serverConfig.gracefulShutdown) {
4578
+ const drainTimeoutMs = typeof serverConfig.gracefulShutdown === "object" ? serverConfig.gracefulShutdown.drainTimeoutMs : undefined;
4579
+ shutdownManager = createShutdownManager({
4580
+ drainTimeoutMs,
4581
+ onDrainStart: () => log4.info("Draining connections..."),
4582
+ onDrainComplete: () => log4.info("Drain complete")
4583
+ });
4584
+ }
4308
4585
  log4.info("ElsiumAI server started", {
4309
4586
  url: `http://${hostname}:${listenPort}`,
4310
4587
  routes: ["POST /chat", "POST /complete", "GET /health", "GET /metrics", "GET /agents"]
4311
4588
  });
4312
4589
  return {
4313
4590
  port: listenPort,
4314
- stop: () => {
4591
+ stop: async () => {
4592
+ if (shutdownManager) {
4593
+ await shutdownManager.shutdown();
4594
+ }
4315
4595
  server.close();
4316
4596
  }
4317
4597
  };
@@ -4414,6 +4694,8 @@ function createRBAC(config) {
4414
4694
  };
4415
4695
  }
4416
4696
  export {
4697
+ requestLoggerMiddleware,
4698
+ requestIdMiddleware,
4417
4699
  rateLimitMiddleware,
4418
4700
  createRoutes,
4419
4701
  createRBAC,
@@ -1,3 +1,4 @@
1
+ import { type Logger } from '@elsium-ai/core';
1
2
  import type { Context, Next } from 'hono';
2
3
  import type { AuthConfig, CorsConfig, RateLimitConfig } from './types';
3
4
  export declare function corsMiddleware(config?: CorsConfig | boolean): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<null, 200, "body">) | undefined>;
@@ -8,4 +9,6 @@ export declare function rateLimitMiddleware(config: RateLimitConfig): (c: Contex
8
9
  error: string;
9
10
  retryAfterMs: number;
10
11
  }, 429, "json">) | undefined>;
12
+ export declare function requestIdMiddleware(): (c: Context, next: Next) => Promise<void>;
13
+ export declare function requestLoggerMiddleware(logger?: Logger): (c: Context, next: Next) => Promise<void>;
11
14
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAItE,wBAAgB,cAAc,CAAC,MAAM,GAAE,UAAU,GAAG,OAAc,IAInD,GAAG,OAAO,EAAE,MAAM,IAAI,uFAiCpC;AAID,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,IAClC,GAAG,OAAO,EAAE,MAAM,IAAI;;kBAwBpC;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,IAG5C,GAAG,OAAO,EAAE,MAAM,IAAI;;;8BAsCpC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAA4B,MAAM,iBAAiB,CAAA;AACvE,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAItE,wBAAgB,cAAc,CAAC,MAAM,GAAE,UAAU,GAAG,OAAc,IAInD,GAAG,OAAO,EAAE,MAAM,IAAI,uFAiCpC;AAID,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,IAClC,GAAG,OAAO,EAAE,MAAM,IAAI;;kBAwBpC;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,IAG5C,GAAG,OAAO,EAAE,MAAM,IAAI;;;8BAsCpC;AAID,wBAAgB,mBAAmB,KACpB,GAAG,OAAO,EAAE,MAAM,IAAI,mBASpC;AAID,wBAAgB,uBAAuB,CAAC,MAAM,CAAC,EAAE,MAAM,IAGxC,GAAG,OAAO,EAAE,MAAM,IAAI,mBAcpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAS3B,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC1B,YAAY,CAAC,EAAE,KAAK,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CA+JnD"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAkC3B,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAC1B,YAAY,CAAC,EAAE,KAAK,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAyKnD"}
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface AppConfig {
16
16
  export?: string;
17
17
  };
18
18
  server?: ServerConfig;
19
+ version?: string;
19
20
  }
20
21
  export interface ServerConfig {
21
22
  port?: number;
@@ -23,6 +24,9 @@ export interface ServerConfig {
23
24
  cors?: boolean | CorsConfig;
24
25
  auth?: AuthConfig;
25
26
  rateLimit?: RateLimitConfig;
27
+ gracefulShutdown?: boolean | {
28
+ drainTimeoutMs?: number;
29
+ };
26
30
  }
27
31
  export interface CorsConfig {
28
32
  origin?: string | string[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@elsium-ai/app",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "App bootstrap, HTTP server, and API routes for ElsiumAI",
5
5
  "license": "MIT",
6
6
  "author": "Eric Utrera <ebutrera9103@gmail.com>",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/elsium-ai/elsium-ai",
9
+ "url": "git+https://github.com/elsium-ai/elsium-ai.git",
10
10
  "directory": "packages/app"
11
11
  },
12
12
  "type": "module",
@@ -26,13 +26,13 @@
26
26
  "dev": "bun --watch src/index.ts"
27
27
  },
28
28
  "dependencies": {
29
- "@elsium-ai/core": "^0.2.1",
30
- "@elsium-ai/gateway": "^0.2.1",
31
- "@elsium-ai/agents": "^0.2.1",
32
- "@elsium-ai/tools": "^0.2.1",
33
- "@elsium-ai/observe": "^0.2.1",
34
- "@elsium-ai/rag": "^0.2.1",
35
- "@elsium-ai/workflows": "^0.2.1",
29
+ "@elsium-ai/core": "^0.2.3",
30
+ "@elsium-ai/gateway": "^0.2.3",
31
+ "@elsium-ai/agents": "^0.2.3",
32
+ "@elsium-ai/tools": "^0.2.3",
33
+ "@elsium-ai/observe": "^0.2.3",
34
+ "@elsium-ai/rag": "^0.2.3",
35
+ "@elsium-ai/workflows": "^0.2.3",
36
36
  "@hono/node-server": "^1.13.0",
37
37
  "hono": "^4.7.0",
38
38
  "zod": "^3.24.0"