@supacloud/elysia 0.14.1 → 0.16.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 +89 -0
- package/compatibility.json +9 -0
- package/dist/command-errors.d.ts +1 -0
- package/dist/index.js +64 -4
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# @supacloud/elysia
|
|
2
2
|
|
|
3
|
+
## Compatibility and Acceptance Boundary
|
|
4
|
+
|
|
5
|
+
The dependency range is not a claim that every allowed version has been tested.
|
|
6
|
+
The focused conformance suite was verified with Bun 1.4.2 and Elysia 1.4.30.
|
|
7
|
+
The package declares Elysia `^1.4.30` as a peer and TypeScript `^7.0.2` as a
|
|
8
|
+
development dependency. `compatibility.json` records the exact exercised tuple,
|
|
9
|
+
including the compiler's separate TypeScript 6 semantic API. The contract-upgrade
|
|
10
|
+
gate checks both that semantic API and the TypeScript 7 CLI. These tests do not
|
|
11
|
+
establish a wider version matrix or Node.js runtime compatibility.
|
|
12
|
+
|
|
13
|
+
Run `bun run test:conformance` in this package after building the local
|
|
14
|
+
`@supacloud/contracts` and `@supacloud/app` dependencies and installing this
|
|
15
|
+
package's dependencies. The suite runs through `app.handle(Request)` without a
|
|
16
|
+
network listener. It is included in the normal `bun test` discovery.
|
|
17
|
+
|
|
18
|
+
| Boundary | Acceptance evidence in `src/conformance.test.ts` |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Decoded body, params, query, headers and cookies | Native/adapter response comparison, with explicit decoded-value assertions |
|
|
21
|
+
| Response normalization and declared status maps | Native/adapter comparison, including a 409 response |
|
|
22
|
+
| Native `Response` transport | Status, body, content type, custom header and outgoing cookie preserved |
|
|
23
|
+
| Parent lifecycle hooks | Request, before-handler, handler and after-handler order compared |
|
|
24
|
+
| Parent early return | 403 response compared; controller must not run |
|
|
25
|
+
| Local sibling hooks | Local hook cannot intercept compiled routes |
|
|
26
|
+
| Request schema failure | Native 422 status retained; controller must not run |
|
|
27
|
+
| Malformed JSON | Native 400 status retained for multiple malformed bodies; controller must not run |
|
|
28
|
+
| Error mapper precedence | Custom mapper handles parse failure before request context resolution |
|
|
29
|
+
| Module error isolation | Internal exception redacted; sibling native error handling remains unchanged |
|
|
30
|
+
| Invalid handler output | Intentional 500 response with `RESPONSE_VALIDATION_ERROR` |
|
|
31
|
+
| Unsupported route descriptors | Unsupported methods/native hooks rejected before registration |
|
|
32
|
+
| Duplicate protocol package copies | Known command errors retain their status; unknown codes remain internal |
|
|
33
|
+
|
|
34
|
+
### Intentional Adapter Semantics
|
|
35
|
+
|
|
36
|
+
- Default parse failures return HTTP 400 with `PARSE_ERROR`; request schema
|
|
37
|
+
failures return HTTP 422 with `VALIDATION_ERROR`. Their public messages do not
|
|
38
|
+
include parser details, submitted values or schema internals.
|
|
39
|
+
- Invalid handler output returns HTTP 500, not a client-input error. It may occur
|
|
40
|
+
after business work has completed and must not be interpreted as a rollback.
|
|
41
|
+
- Unknown handler exceptions are redacted. Known `CommandError` instances are
|
|
42
|
+
recognized by their Error identity, name and allowlisted code across separate
|
|
43
|
+
protocol package copies, never by exposing their message. A configured `errorMapper` can
|
|
44
|
+
override these defaults and owns the safety of its response.
|
|
45
|
+
- Cookie input passed to a compiled controller contains decoded values, not
|
|
46
|
+
Elysia's mutable cookie wrappers. Native `Response` headers can carry outgoing
|
|
47
|
+
cookies.
|
|
48
|
+
- Errors before context resolution have no request context. Error mappers must
|
|
49
|
+
not assume identity or request-scoped services are available.
|
|
50
|
+
|
|
51
|
+
### Not Yet Proven by This Suite
|
|
52
|
+
|
|
53
|
+
WebSockets, streaming and disconnect behavior, multipart uploads, signed-cookie
|
|
54
|
+
mutation, arbitrary third-party plugins, alternate runtime/version combinations,
|
|
55
|
+
concurrent tenant isolation, database transaction/idempotency guarantees and
|
|
56
|
+
published-package installation are not covered by the conformance suite alone.
|
|
57
|
+
Runtime safety is covered separately below. This list records
|
|
58
|
+
an evidence gap, not a declaration that all these features are unsupported.
|
|
59
|
+
Do not claim complete Elysia compatibility from this gate.
|
|
60
|
+
|
|
61
|
+
Compiled routes accept only the HTTP methods and schema fields declared by
|
|
62
|
+
`CompiledRoute`, plus compiler-emitted parameter transformation/default and
|
|
63
|
+
descriptive metadata. This metadata does not install native Elysia hooks.
|
|
64
|
+
Other descriptor fields (including browser guards/resolvers and native
|
|
65
|
+
`beforeHandle`) or unsupported methods throw `ROUTE_DESCRIPTOR_UNSUPPORTED`
|
|
66
|
+
at registration. It is not an arbitrary Elysia route-options passthrough.
|
|
67
|
+
TypeScript/decorator inference, compiler migrations and generated client parity
|
|
68
|
+
require their own acceptance gates.
|
|
69
|
+
|
|
70
|
+
### Runtime and Upgrade Gates
|
|
71
|
+
|
|
72
|
+
`bun run test:runtime-safety` requires `SUPACLOUD_COMMAND_TEST_URL` and fails
|
|
73
|
+
instead of skipping when it is missing. Use a dedicated loopback database named
|
|
74
|
+
`supacloud_commands_test`, with PostgreSQL 18 and PGMQ 1.10.0; initialize it using
|
|
75
|
+
`scripts/prepare-command-test-database.ts`. The gate opens real loopback HTTP
|
|
76
|
+
listeners and exercises compiler-generated request-scoped controllers. It proves
|
|
77
|
+
overlapping tenant/actor requests, duplicate-key concurrency, authorization
|
|
78
|
+
revocation, audit rollback, same-key retry and per-request provider teardown.
|
|
79
|
+
Its authentication uses a fixed test token map, not a production JWT provider.
|
|
80
|
+
|
|
81
|
+
`bun run test:contract-upgrade` copies a fixed legacy source fixture, previews and
|
|
82
|
+
applies its versioned migration, compiles factories/client/OpenAPI, checks positive
|
|
83
|
+
and negative types with both TypeScript engines, and calls the generated client
|
|
84
|
+
over real HTTP. It restores the old source checkpoint, regenerates artifacts and
|
|
85
|
+
executes the restored application. Build local contracts, app and compiler
|
|
86
|
+
(including declarations) before installing this package's copied file dependencies.
|
|
87
|
+
|
|
88
|
+
These gates prove the stated scenarios, not a full historical npm upgrade matrix
|
|
89
|
+
or all business-domain isolation. See [framework acceptance](../../docs/framework-acceptance.md)
|
|
90
|
+
for the evidence boundaries and upgrade policy.
|
|
91
|
+
|
|
3
92
|
## Persistent Command Adapters
|
|
4
93
|
|
|
5
94
|
`createPersistentCommandAdapter(command, { identity, input })` binds a
|
package/dist/command-errors.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Elysia as Elysia2 } from "elysia";
|
|
3
|
-
import {
|
|
3
|
+
import { decodeCommandPreview } from "@supacloud/contracts";
|
|
4
4
|
import {
|
|
5
5
|
provideToken,
|
|
6
6
|
runInRequestContext
|
|
@@ -8,6 +8,22 @@ import {
|
|
|
8
8
|
import { REQUEST_CONTEXT } from "@supacloud/app";
|
|
9
9
|
|
|
10
10
|
// src/command-errors.ts
|
|
11
|
+
function commandErrorCode(error) {
|
|
12
|
+
if (!(error instanceof Error) || error.name !== "CommandError" || !("code" in error))
|
|
13
|
+
return;
|
|
14
|
+
switch (error.code) {
|
|
15
|
+
case "COMMAND_INPUT_INVALID":
|
|
16
|
+
case "COMMAND_REJECTED":
|
|
17
|
+
case "COMMAND_IDEMPOTENCY_CONFLICT":
|
|
18
|
+
case "COMMAND_INPUT_EXPIRED":
|
|
19
|
+
case "COMMAND_UNAVAILABLE":
|
|
20
|
+
case "COMMAND_RECEIPT_INVALID":
|
|
21
|
+
case "COMMAND_OUTCOME_UNKNOWN":
|
|
22
|
+
return error.code;
|
|
23
|
+
default:
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
function commandErrorStatus(code) {
|
|
12
28
|
switch (code) {
|
|
13
29
|
case "COMMAND_INPUT_INVALID":
|
|
@@ -1591,6 +1607,42 @@ function assertDeclaredResponseStatus(route, value, configuredStatus) {
|
|
|
1591
1607
|
throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
|
|
1592
1608
|
}
|
|
1593
1609
|
function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
|
|
1610
|
+
const supportedMethods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
1611
|
+
const supportedFields = new Set([
|
|
1612
|
+
"method",
|
|
1613
|
+
"path",
|
|
1614
|
+
"handler",
|
|
1615
|
+
"body",
|
|
1616
|
+
"params",
|
|
1617
|
+
"query",
|
|
1618
|
+
"headers",
|
|
1619
|
+
"cookie",
|
|
1620
|
+
"response",
|
|
1621
|
+
"responses",
|
|
1622
|
+
"contract",
|
|
1623
|
+
"schemaKinds",
|
|
1624
|
+
"nativeResponse",
|
|
1625
|
+
"invoker",
|
|
1626
|
+
"command",
|
|
1627
|
+
"aspects",
|
|
1628
|
+
"paramTransforms",
|
|
1629
|
+
"paramDefaults",
|
|
1630
|
+
"queryTransforms",
|
|
1631
|
+
"queryDefaults",
|
|
1632
|
+
"title",
|
|
1633
|
+
"data",
|
|
1634
|
+
"input",
|
|
1635
|
+
"result",
|
|
1636
|
+
"request"
|
|
1637
|
+
]);
|
|
1638
|
+
for (const controller of compiled.controllers) {
|
|
1639
|
+
for (const route of controller.routes) {
|
|
1640
|
+
const unsupported = Object.keys(route).filter((field) => !supportedFields.has(field));
|
|
1641
|
+
if (!supportedMethods.has(route.method) || unsupported.length > 0) {
|
|
1642
|
+
throw new ApplicationError(`Unsupported compiled route ${route.method} ${controller.path}${route.path}` + (unsupported.length > 0 ? `: ${unsupported.join(", ")}` : ""), { code: "ROUTE_DESCRIPTOR_UNSUPPORTED" });
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1594
1646
|
const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
|
|
1595
1647
|
if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
|
|
1596
1648
|
throw new ApplicationError(`Module "${compiled.name}" has command routes but no commandGovernance`, { code: "COMMAND_GOVERNANCE_UNCONFIGURED" });
|
|
@@ -1810,9 +1862,10 @@ async function executeJob(compiled, services, job, input, requestContext, import
|
|
|
1810
1862
|
}
|
|
1811
1863
|
}
|
|
1812
1864
|
function defaultErrorResponse(error, frameworkCode) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1865
|
+
const protocolCode = commandErrorCode(error);
|
|
1866
|
+
if (protocolCode) {
|
|
1867
|
+
return Response.json({ ok: false, code: protocolCode, message: protocolCode }, {
|
|
1868
|
+
status: commandErrorStatus(protocolCode)
|
|
1816
1869
|
});
|
|
1817
1870
|
}
|
|
1818
1871
|
if (isPublicApplicationError(error)) {
|
|
@@ -1823,6 +1876,13 @@ function defaultErrorResponse(error, frameworkCode) {
|
|
|
1823
1876
|
...error.details === undefined ? {} : { details: error.details }
|
|
1824
1877
|
}, { status: error.status });
|
|
1825
1878
|
}
|
|
1879
|
+
if (frameworkCode === "PARSE") {
|
|
1880
|
+
return Response.json({
|
|
1881
|
+
ok: false,
|
|
1882
|
+
code: "PARSE_ERROR",
|
|
1883
|
+
message: "Request body could not be parsed"
|
|
1884
|
+
}, { status: 400 });
|
|
1885
|
+
}
|
|
1826
1886
|
if (frameworkCode === "VALIDATION") {
|
|
1827
1887
|
if (isRecord(error) && error.type === "response") {
|
|
1828
1888
|
return Response.json({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/elysia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"compatibility.json"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"generate:example": "bun src/generate-webhook-example.ts",
|
|
@@ -25,6 +26,9 @@
|
|
|
25
26
|
"clean": "rm -rf dist",
|
|
26
27
|
"prepublishOnly": "bun run build",
|
|
27
28
|
"test": "bun test",
|
|
29
|
+
"test:conformance": "bun test src/conformance.test.ts",
|
|
30
|
+
"test:runtime-safety": "SUPACLOUD_REQUIRE_RUNTIME_SAFETY=1 bun test src/runtime-safety.test.ts",
|
|
31
|
+
"test:contract-upgrade": "bun test src/contract-upgrade.test.ts",
|
|
28
32
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
33
|
"typecheck:test": "tsc -p tsconfig.test.json --noEmit"
|
|
30
34
|
},
|
|
@@ -54,7 +58,7 @@
|
|
|
54
58
|
"elysia": "^1.4.30"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
|
-
"@supacloud/compiler": "0.
|
|
61
|
+
"@supacloud/compiler": "0.21.0",
|
|
58
62
|
"@supacloud/commands": "0.4.0",
|
|
59
63
|
"@supacloud/db": "0.7.1",
|
|
60
64
|
"@types/bun": "^1.4.2",
|