@supacloud/elysia 0.7.0 → 0.8.1

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,15 @@ 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
+ Handlers returning a native `Response` must validate their JSON payload before
95
+ constructing it. The adapter does not consume or parse binary/streaming responses.
88
96
 
89
97
  Jobs are executed explicitly with `executeJob(compiledModule, services, job,
90
98
  input, requestContext)`. The asynchronous compiler-generated job scope is
@@ -116,6 +124,15 @@ once is rejected.
116
124
  Executes a compiler-emitted Job descriptor with its static aspect list and
117
125
  compiler-generated job scope.
118
126
 
127
+ ### `assertFeatureTransition(spec, state, event)`
128
+
129
+ Checks a declared feature transition against an authoritative state and returns
130
+ the destination state. Unknown events, stale/illegal states and inherited object
131
+ members fail with HTTP 409 / `FEATURE_TRANSITION_CONFLICT`; malformed destinations
132
+ fail with `FEATURE_SPEC_INVALID`. The helper is a matrix assertion, not a workflow
133
+ engine, persistence or authorization layer. Call it inside the application's
134
+ transaction and persist with a row lock or expected-version check.
135
+
119
136
  ### `ApplicationError`
120
137
 
121
138
  Lightweight error class carrying HTTP `status`, machine-readable `code`, and
@@ -134,3 +151,10 @@ adapters. The memory harness is limited to deterministic HTTP, key-value
134
151
  transaction and object-storage contract tests.
135
152
  `policy` supplies explicit permission grants/revocations and idempotency claims;
136
153
  `storage.failNext()` makes storage failure paths deterministic.
154
+
155
+ Set `memoryGovernance: true` to enable test-only authorization, transaction,
156
+ idempotency and audit adapters; permissions still require explicit grants.
157
+ HTTP receipt fingerprints include route, body, params, query and business
158
+ headers, not mutable request scopes/services or identity/tracing transport.
159
+ Authorization runs again on a replay. Use durable application-owned adapters
160
+ for production receipts, transactions and audits.
@@ -0,0 +1,15 @@
1
+ /** Structural contract: the runtime does not import the compiler or discover metadata. */
2
+ export interface FeatureTransitionSpec {
3
+ name: string;
4
+ states: readonly string[];
5
+ transitions: Readonly<Record<string, {
6
+ from: string;
7
+ to: string;
8
+ }>>;
9
+ }
10
+ /**
11
+ * Check a declared transition against an authoritative state read by the caller.
12
+ * This is a matrix assertion, not persistence, authorization, or an FSM engine.
13
+ * Call inside the application's transaction and persist with a version check.
14
+ */
15
+ export declare function assertFeatureTransition<Spec extends FeatureTransitionSpec>(spec: Spec, state: string, event: string): Spec["states"][number];
package/dist/index.d.ts CHANGED
@@ -90,6 +90,8 @@ export interface CommandInvocation {
90
90
  services: Record<string, unknown>;
91
91
  }
92
92
  export type CommandExecutor = (invocation: CommandInvocation, next: () => unknown | Promise<unknown>) => unknown | Promise<unknown>;
93
+ export { assertFeatureTransition } from "./feature";
94
+ export type { FeatureTransitionSpec } from "./feature";
93
95
  export interface ApplicationAspectContext {
94
96
  kind: "route" | "command" | "job";
95
97
  name: string;
package/dist/index.js CHANGED
@@ -1,6 +1,22 @@
1
1
  // src/index.ts
2
2
  import { Elysia } from "elysia";
3
3
 
4
+ // src/feature.ts
5
+ function assertFeatureTransition(spec, state, event) {
6
+ const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
7
+ if (!transition || !spec.states.includes(state) || transition.from !== state) {
8
+ throw new ApplicationError("Feature transition is not allowed", {
9
+ status: 409,
10
+ code: "FEATURE_TRANSITION_CONFLICT"
11
+ });
12
+ }
13
+ if (!spec.states.includes(transition.to)) {
14
+ throw new ApplicationError("Feature transition references an undeclared state", {
15
+ code: "FEATURE_SPEC_INVALID"
16
+ });
17
+ }
18
+ return transition.to;
19
+ }
4
20
  // src/memory.ts
5
21
  import { AsyncLocalStorage } from "node:async_hooks";
6
22
 
@@ -240,7 +256,7 @@ function createMemorySandbox(options = {}) {
240
256
  }
241
257
  },
242
258
  idempotency(invocation, next) {
243
- return policy.runOnce(policy.subject(invocation.requestContext), invocation.command.name, requireIdempotencyKey(invocation), invocation.input, next);
259
+ return policy.runOnce(policy.subject(invocation.requestContext), invocation.command.name, requireIdempotencyKey(invocation), memoryCommandInput(invocation), next);
244
260
  },
245
261
  transaction(_invocation, next) {
246
262
  const count = audit.length;
@@ -297,6 +313,21 @@ function createMemorySandbox(options = {}) {
297
313
  }
298
314
  };
299
315
  }
316
+ function memoryCommandInput(invocation) {
317
+ const input = invocation.input;
318
+ if (typeof input !== "object" || input === null)
319
+ return input;
320
+ const ignoredHeaders = new Set(["authorization", "idempotency-key", "x-request-id", "traceparent", "tracestate"]);
321
+ const headers = Object.fromEntries([...invocation.request.headers.entries()].filter(([name]) => !ignoredHeaders.has(name)));
322
+ return {
323
+ method: invocation.request.method,
324
+ path: new URL(invocation.request.url).pathname,
325
+ body: "body" in input ? input.body : undefined,
326
+ params: "params" in input ? input.params : undefined,
327
+ query: "query" in input ? input.query : undefined,
328
+ headers
329
+ };
330
+ }
300
331
  function storageKey(bucket, key) {
301
332
  return JSON.stringify([bucket, key]);
302
333
  }
@@ -541,6 +572,15 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
541
572
  requestContexts.set(request, requestContext);
542
573
  return { requestContext };
543
574
  });
575
+ plugin.onError(async ({ code, error, request }) => {
576
+ const context = {
577
+ request,
578
+ requestContext: requestContexts.get(request),
579
+ frameworkCode: code
580
+ };
581
+ const mapped = await options.errorMapper?.(error, context);
582
+ return mapped ?? defaultErrorResponse(error, code);
583
+ });
544
584
  for (const controller of compiled.controllers) {
545
585
  for (const route of controller.routes) {
546
586
  const path = joinPaths(controller.path, route.path);
@@ -652,15 +692,6 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
652
692
  }
653
693
  }
654
694
  }
655
- plugin.onError({ as: "global" }, async ({ code, error, request }) => {
656
- const context = {
657
- request,
658
- requestContext: requestContexts.get(request),
659
- frameworkCode: code
660
- };
661
- const mapped = await options.errorMapper?.(error, context);
662
- return mapped ?? defaultErrorResponse(error, code);
663
- });
664
695
  return plugin;
665
696
  }
666
697
  async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor) {
@@ -707,6 +738,13 @@ function defaultErrorResponse(error, frameworkCode) {
707
738
  }, { status: error.status });
708
739
  }
709
740
  if (frameworkCode === "VALIDATION") {
741
+ if (isRecord(error) && error.type === "response") {
742
+ return Response.json({
743
+ ok: false,
744
+ code: "RESPONSE_VALIDATION_ERROR",
745
+ message: "Response validation failed"
746
+ }, { status: 500 });
747
+ }
710
748
  return Response.json({
711
749
  ok: false,
712
750
  code: "VALIDATION_ERROR",
@@ -756,6 +794,7 @@ export {
756
794
  EXECUTION_ID_HEADER,
757
795
  IDEMPOTENCY_KEY_HEADER,
758
796
  VERIFIED_JWT_SUBJECT_HEADER,
797
+ assertFeatureTransition,
759
798
  composeAspects,
760
799
  composeCommandExecutors,
761
800
  createApplication,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",