@supacloud/elysia 0.8.0 → 0.9.0

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
@@ -26,7 +26,8 @@ Runtime adapter that turns `@supacloud/compiler` output into a production-ready
26
26
  performed.
27
27
  - **Public error mapping**: transforms framework / application errors via
28
28
  `errorMapper` with standard `ApplicationError` envelope support, preserving
29
- Elysia's default behavior (422) for schema validation errors.
29
+ HTTP 422 for request validation and HTTP 500 / `RESPONSE_VALIDATION_ERROR`
30
+ for invalid handler output, without exposing payloads or schema internals.
30
31
 
31
32
  ## Installation
32
33
 
@@ -83,8 +84,16 @@ sandbox.reset();
83
84
 
84
85
  Route `body`, `params`, `query`, and `response` schemas are enforced by
85
86
  Elysia before and after the handler. Invalid input returns the standard `422`
86
- validation response; invalid handler output is rejected before it reaches the
87
- client.
87
+ validation response; invalid structured handler output returns HTTP 500 with
88
+ `RESPONSE_VALIDATION_ERROR`. A response validation failure can occur **after a
89
+ command has committed**; it does not imply rollback and must not trigger a blind
90
+ write retry. Confirm the outcome using the application's durable receipt or
91
+ read-back protocol. A custom `errorMapper` can override this public envelope.
92
+
93
+ Native `Response` objects are passed through by Elysia, including JSON responses.
94
+ Use `validatedJsonResponse` to opt into validation when constructing a native
95
+ JSON response. Otherwise handlers must validate their JSON payload themselves.
96
+ The adapter does not consume or parse binary/streaming responses.
88
97
 
89
98
  Jobs are executed explicitly with `executeJob(compiledModule, services, job,
90
99
  input, requestContext)`. The asynchronous compiler-generated job scope is
@@ -93,6 +102,39 @@ fails partway through.
93
102
 
94
103
  ## API
95
104
 
105
+ ### `validatedJsonResponse(validate, value, init?): Response`
106
+
107
+ Constructs a native JSON response after a synchronous, caller-owned type guard
108
+ validates the actual serialized JSON snapshot. The value type is inferred from
109
+ the guard; compatible extra fields are preserved. For a TypeBox contract, the
110
+ guard can delegate to `Value.Check(schema, value)` or a compiled validator.
111
+ No additional schema dependency is required by the adapter.
112
+
113
+ ```ts
114
+ import { validatedJsonResponse } from "@supacloud/elysia";
115
+ import { isReportReceipt } from "./contracts";
116
+
117
+ return validatedJsonResponse(isReportReceipt, receipt, {
118
+ status: 201,
119
+ headers: { "x-request-id": requestId },
120
+ });
121
+ ```
122
+
123
+ The helper serializes once, validates that wire snapshot, and sends those same
124
+ bytes. Validation cannot mutate the outgoing body; it is not a transform or
125
+ coercion hook. Guards must be synchronous and side-effect free. Serialization
126
+ failures and invalid receipts throw a sanitized `ApplicationError` with HTTP 500
127
+ and `RESPONSE_VALIDATION_ERROR`, without retaining payloads or validator causes.
128
+ The application's `errorMapper` can map this to its outcome-confirmation
129
+ protocol. The helper never retries a command or implies rollback.
130
+
131
+ `init` uses native `Response` options. The helper explicitly rejects null-body
132
+ statuses 204, 205 and 304, including on runtimes that otherwise accept a body
133
+ with those statuses. The default content type is
134
+ `application/json`, and explicitly supplied headers are preserved. This helper
135
+ is for bounded JSON payloads, not files or streams. Existing native `Response`
136
+ passthrough is unchanged.
137
+
96
138
  ### `createApplication(options: ApplicationOptions): Elysia`
97
139
 
98
140
  Creates the root Elysia application from compiled modules.
package/dist/index.d.ts CHANGED
@@ -144,6 +144,13 @@ export declare class ApplicationError extends Error implements PublicApplication
144
144
  readonly details?: unknown;
145
145
  constructor(message: string, options?: ApplicationErrorOptions);
146
146
  }
147
+ export type JsonResponseValidator<T> = (value: unknown) => value is T;
148
+ /**
149
+ * Validate the serialized JSON snapshot before creating a native Response.
150
+ * Validators must be synchronous and side-effect free; response failures do not
151
+ * imply that application writes were rolled back.
152
+ */
153
+ export declare function validatedJsonResponse<T>(validate: JsonResponseValidator<T>, value: NoInfer<T>, init?: ResponseInit): Response;
147
154
  export interface ErrorContext {
148
155
  request: Request;
149
156
  requestContext: unknown;
package/dist/index.js CHANGED
@@ -405,6 +405,28 @@ class ApplicationError extends Error {
405
405
  this.details = options.details;
406
406
  }
407
407
  }
408
+ function validatedJsonResponse(validate, value, init) {
409
+ if (init?.status === 204 || init?.status === 205 || init?.status === 304) {
410
+ throw new TypeError("JSON responses cannot use a null-body status");
411
+ }
412
+ let body;
413
+ try {
414
+ const serialized = JSON.stringify(value);
415
+ if (serialized === undefined || validate(JSON.parse(serialized)) !== true) {
416
+ throw new Error("Invalid JSON response");
417
+ }
418
+ body = serialized;
419
+ } catch {
420
+ throw new ApplicationError("Response validation failed", {
421
+ status: 500,
422
+ code: "RESPONSE_VALIDATION_ERROR"
423
+ });
424
+ }
425
+ const headers = new Headers(init?.headers);
426
+ if (!headers.has("content-type"))
427
+ headers.set("content-type", "application/json");
428
+ return new Response(body, { ...init, headers });
429
+ }
408
430
  function safeHeaderValue(value, maxLength) {
409
431
  if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
410
432
  return;
@@ -572,6 +594,15 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
572
594
  requestContexts.set(request, requestContext);
573
595
  return { requestContext };
574
596
  });
597
+ plugin.onError(async ({ code, error, request }) => {
598
+ const context = {
599
+ request,
600
+ requestContext: requestContexts.get(request),
601
+ frameworkCode: code
602
+ };
603
+ const mapped = await options.errorMapper?.(error, context);
604
+ return mapped ?? defaultErrorResponse(error, code);
605
+ });
575
606
  for (const controller of compiled.controllers) {
576
607
  for (const route of controller.routes) {
577
608
  const path = joinPaths(controller.path, route.path);
@@ -683,15 +714,6 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
683
714
  }
684
715
  }
685
716
  }
686
- plugin.onError({ as: "global" }, async ({ code, error, request }) => {
687
- const context = {
688
- request,
689
- requestContext: requestContexts.get(request),
690
- frameworkCode: code
691
- };
692
- const mapped = await options.errorMapper?.(error, context);
693
- return mapped ?? defaultErrorResponse(error, code);
694
- });
695
717
  return plugin;
696
718
  }
697
719
  async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor) {
@@ -738,6 +760,13 @@ function defaultErrorResponse(error, frameworkCode) {
738
760
  }, { status: error.status });
739
761
  }
740
762
  if (frameworkCode === "VALIDATION") {
763
+ if (isRecord(error) && error.type === "response") {
764
+ return Response.json({
765
+ ok: false,
766
+ code: "RESPONSE_VALIDATION_ERROR",
767
+ message: "Response validation failed"
768
+ }, { status: 500 });
769
+ }
741
770
  return Response.json({
742
771
  ok: false,
743
772
  code: "VALIDATION_ERROR",
@@ -800,5 +829,6 @@ export {
800
829
  defaultErrorResponse,
801
830
  executeJob,
802
831
  requireIdempotencyKey,
803
- requireTrustedIdentity
832
+ requireTrustedIdentity,
833
+ validatedJsonResponse
804
834
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",