@criterionx/server 0.3.1 → 0.3.2

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/dist/index.d.ts CHANGED
@@ -130,6 +130,39 @@ interface DecisionInfo {
130
130
  description?: string;
131
131
  meta?: Record<string, unknown>;
132
132
  }
133
+ /**
134
+ * Error codes for structured error responses
135
+ */
136
+ type ErrorCode = "DECISION_NOT_FOUND" | "INVALID_JSON" | "MISSING_INPUT" | "MISSING_PROFILE" | "VALIDATION_ERROR" | "EVALUATION_ERROR" | "INTERNAL_ERROR";
137
+ /**
138
+ * Structured error response
139
+ */
140
+ interface ErrorResponse {
141
+ error: {
142
+ code: ErrorCode;
143
+ message: string;
144
+ details?: Record<string, unknown>;
145
+ };
146
+ requestId?: string;
147
+ timestamp: string;
148
+ }
149
+ /**
150
+ * Health check status
151
+ */
152
+ type HealthStatus = "healthy" | "degraded" | "unhealthy";
153
+ /**
154
+ * Health check response
155
+ */
156
+ interface HealthResponse {
157
+ status: HealthStatus;
158
+ version: string;
159
+ uptime: number;
160
+ timestamp: string;
161
+ checks?: Record<string, {
162
+ status: HealthStatus;
163
+ message?: string;
164
+ }>;
165
+ }
133
166
 
134
167
  /**
135
168
  * Generate OpenAPI-compatible schema for a decision endpoint
@@ -269,6 +302,7 @@ declare class CriterionServer {
269
302
  private metricsOptions;
270
303
  private openApiOptions;
271
304
  private openApiSpec;
305
+ private startTime;
272
306
  constructor(options: ServerOptions);
273
307
  private setupRoutes;
274
308
  private generateDocsHtml;
@@ -290,4 +324,4 @@ declare class CriterionServer {
290
324
  */
291
325
  declare function createServer(options: ServerOptions): CriterionServer;
292
326
 
293
- export { type AfterEvaluateHook, type BeforeEvaluateHook, CriterionServer, type DecisionInfo, type EvaluateRequest, type HookContext, type Hooks, METRIC_EVALUATIONS_TOTAL, METRIC_EVALUATION_DURATION_SECONDS, METRIC_RULE_MATCHES_TOTAL, MetricsCollector, type MetricsOptions$1 as MetricsOptions, type OnErrorHook, type OpenAPIInfo, type OpenAPIOptions, type OpenAPISpec, type ServerOptions, createServer, generateEndpointSchema, generateOpenAPISpec, generateSwaggerUIHtml };
327
+ export { type AfterEvaluateHook, type BeforeEvaluateHook, CriterionServer, type DecisionInfo, type ErrorCode, type ErrorResponse, type EvaluateRequest, type HealthResponse, type HealthStatus, type HookContext, type Hooks, METRIC_EVALUATIONS_TOTAL, METRIC_EVALUATION_DURATION_SECONDS, METRIC_RULE_MATCHES_TOTAL, MetricsCollector, type MetricsOptions$1 as MetricsOptions, type OnErrorHook, type OpenAPIInfo, type OpenAPIOptions, type OpenAPISpec, type ServerOptions, createServer, generateEndpointSchema, generateOpenAPISpec, generateSwaggerUIHtml };
package/dist/index.js CHANGED
@@ -457,9 +457,21 @@ import { Hono } from "hono";
457
457
  import { cors } from "hono/cors";
458
458
  import { serve } from "@hono/node-server";
459
459
  import { Engine } from "@criterionx/core";
460
+ var SERVER_VERSION = "0.3.2";
460
461
  function generateRequestId() {
461
462
  return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
462
463
  }
464
+ function createErrorResponse(code, message, requestId, details) {
465
+ return {
466
+ error: {
467
+ code,
468
+ message,
469
+ ...details && { details }
470
+ },
471
+ ...requestId && { requestId },
472
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
473
+ };
474
+ }
463
475
  var CriterionServer = class {
464
476
  app;
465
477
  engine;
@@ -471,6 +483,7 @@ var CriterionServer = class {
471
483
  metricsOptions;
472
484
  openApiOptions;
473
485
  openApiSpec = null;
486
+ startTime;
474
487
  constructor(options) {
475
488
  this.app = new Hono();
476
489
  this.engine = new Engine();
@@ -479,6 +492,7 @@ var CriterionServer = class {
479
492
  this.hooks = options.hooks ?? {};
480
493
  this.metricsOptions = options.metrics ?? {};
481
494
  this.openApiOptions = options.openapi ?? {};
495
+ this.startTime = /* @__PURE__ */ new Date();
482
496
  if (this.metricsOptions.enabled) {
483
497
  this.metricsCollector = new MetricsCollector(this.metricsOptions);
484
498
  }
@@ -505,11 +519,32 @@ var CriterionServer = class {
505
519
  this.app.get("/", (c) => {
506
520
  return c.json({
507
521
  name: "Criterion Server",
508
- version: "0.1.0",
522
+ version: SERVER_VERSION,
509
523
  decisions: this.decisions.size,
510
- metrics: this.metricsCollector !== null
524
+ docs: "/docs",
525
+ health: "/health"
511
526
  });
512
527
  });
528
+ this.app.get("/health", (c) => {
529
+ const uptime = Math.floor((Date.now() - this.startTime.getTime()) / 1e3);
530
+ const response = {
531
+ status: "healthy",
532
+ version: SERVER_VERSION,
533
+ uptime,
534
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
535
+ checks: {
536
+ decisions: {
537
+ status: this.decisions.size > 0 ? "healthy" : "degraded",
538
+ message: `${this.decisions.size} decision(s) registered`
539
+ },
540
+ engine: {
541
+ status: "healthy",
542
+ message: "Engine operational"
543
+ }
544
+ }
545
+ };
546
+ return c.json(response);
547
+ });
513
548
  if (this.metricsCollector) {
514
549
  const endpoint = this.metricsOptions.endpoint ?? "/metrics";
515
550
  this.app.get(endpoint, (c) => {
@@ -546,7 +581,10 @@ var CriterionServer = class {
546
581
  const id = c.req.param("id");
547
582
  const decision = this.decisions.get(id);
548
583
  if (!decision) {
549
- return c.json({ error: `Decision not found: ${id}` }, 404);
584
+ return c.json(
585
+ createErrorResponse("DECISION_NOT_FOUND", `Decision not found: ${id}`),
586
+ 404
587
+ );
550
588
  }
551
589
  const schema = extractDecisionSchema(decision);
552
590
  return c.json(schema);
@@ -555,34 +593,49 @@ var CriterionServer = class {
555
593
  const id = c.req.param("id");
556
594
  const decision = this.decisions.get(id);
557
595
  if (!decision) {
558
- return c.json({ error: `Decision not found: ${id}` }, 404);
596
+ return c.json(
597
+ createErrorResponse("DECISION_NOT_FOUND", `Decision not found: ${id}`),
598
+ 404
599
+ );
559
600
  }
560
601
  const schema = generateEndpointSchema(decision);
561
602
  return c.json(schema);
562
603
  });
563
604
  this.app.post("/decisions/:id", async (c) => {
564
605
  const id = c.req.param("id");
606
+ const requestId = generateRequestId();
565
607
  const decision = this.decisions.get(id);
566
608
  if (!decision) {
567
- return c.json({ error: `Decision not found: ${id}` }, 404);
609
+ return c.json(
610
+ createErrorResponse("DECISION_NOT_FOUND", `Decision not found: ${id}`, requestId),
611
+ 404
612
+ );
568
613
  }
569
614
  let body;
570
615
  try {
571
616
  body = await c.req.json();
572
617
  } catch {
573
- return c.json({ error: "Invalid JSON body" }, 400);
618
+ return c.json(
619
+ createErrorResponse("INVALID_JSON", "Invalid JSON body", requestId),
620
+ 400
621
+ );
574
622
  }
575
623
  if (body.input === void 0) {
576
- return c.json({ error: "Missing 'input' in request body" }, 400);
624
+ return c.json(
625
+ createErrorResponse("MISSING_INPUT", "Missing 'input' in request body", requestId),
626
+ 400
627
+ );
577
628
  }
578
629
  let profile = body.profile;
579
630
  if (!profile) {
580
631
  profile = this.profiles.get(id);
581
632
  if (!profile) {
582
633
  return c.json(
583
- {
584
- error: `No profile provided and no default profile for decision: ${id}`
585
- },
634
+ createErrorResponse(
635
+ "MISSING_PROFILE",
636
+ `No profile provided and no default profile for decision: ${id}`,
637
+ requestId
638
+ ),
586
639
  400
587
640
  );
588
641
  }
@@ -591,7 +644,7 @@ var CriterionServer = class {
591
644
  decisionId: id,
592
645
  input: body.input,
593
646
  profile,
594
- requestId: generateRequestId(),
647
+ requestId,
595
648
  timestamp: /* @__PURE__ */ new Date()
596
649
  };
597
650
  const startTime = performance.now();
@@ -635,13 +688,19 @@ var CriterionServer = class {
635
688
  status: "ERROR"
636
689
  });
637
690
  }
691
+ const err = error instanceof Error ? error : new Error(String(error));
638
692
  if (this.hooks.onError) {
639
- await this.hooks.onError(
640
- ctx,
641
- error instanceof Error ? error : new Error(String(error))
642
- );
693
+ await this.hooks.onError(ctx, err);
643
694
  }
644
- throw error;
695
+ return c.json(
696
+ createErrorResponse(
697
+ "EVALUATION_ERROR",
698
+ err.message,
699
+ requestId,
700
+ { decisionId: id }
701
+ ),
702
+ 500
703
+ );
645
704
  }
646
705
  });
647
706
  this.app.get("/docs", (c) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@criterionx/server",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "HTTP server for Criterion decisions with auto-generated docs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,7 @@
42
42
  "@hono/node-server": "^1.13.0",
43
43
  "hono": "^4.6.0",
44
44
  "zod-to-json-schema": "^3.24.0",
45
- "@criterionx/core": "0.3.1"
45
+ "@criterionx/core": "0.3.2"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "zod": "^3.22.0"