@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 +27 -3
- package/dist/feature.d.ts +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -10
- package/package.json +1 -1
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
|
-
|
|
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
|
|
87
|
-
|
|
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
|
|
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