@beignet/core 0.0.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/CHANGELOG.md +5 -0
- package/README.md +288 -0
- package/dist/application/index.d.ts +260 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +324 -0
- package/dist/application/index.js.map +1 -0
- package/dist/client/client.d.ts +241 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +531 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +139 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +216 -0
- package/dist/config/index.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +121 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -0
- package/dist/contracts/contract-builder.js +346 -0
- package/dist/contracts/contract-builder.js.map +1 -0
- package/dist/contracts/contract-group.d.ts +106 -0
- package/dist/contracts/contract-group.d.ts.map +1 -0
- package/dist/contracts/contract-group.js +240 -0
- package/dist/contracts/contract-group.js.map +1 -0
- package/dist/contracts/contract-like.d.ts +21 -0
- package/dist/contracts/contract-like.d.ts.map +1 -0
- package/dist/contracts/contract-like.js +9 -0
- package/dist/contracts/contract-like.js.map +1 -0
- package/dist/contracts/index.d.ts +15 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +11 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/openapi-meta.d.ts +23 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -0
- package/dist/contracts/openapi-meta.js +2 -0
- package/dist/contracts/openapi-meta.js.map +1 -0
- package/dist/contracts/path-template.d.ts +17 -0
- package/dist/contracts/path-template.d.ts.map +1 -0
- package/dist/contracts/path-template.js +50 -0
- package/dist/contracts/path-template.js.map +1 -0
- package/dist/contracts/rate-limit.d.ts +50 -0
- package/dist/contracts/rate-limit.d.ts.map +1 -0
- package/dist/contracts/rate-limit.js +2 -0
- package/dist/contracts/rate-limit.js.map +1 -0
- package/dist/contracts/types.d.ts +97 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +54 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/contracts/utils.d.ts +3 -0
- package/dist/contracts/utils.d.ts.map +1 -0
- package/dist/contracts/utils.js +44 -0
- package/dist/contracts/utils.js.map +1 -0
- package/dist/domain/entity.d.ts +87 -0
- package/dist/domain/entity.d.ts.map +1 -0
- package/dist/domain/entity.js +155 -0
- package/dist/domain/entity.js.map +1 -0
- package/dist/domain/events.d.ts +41 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +21 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +14 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/value-object.d.ts +60 -0
- package/dist/domain/value-object.d.ts.map +1 -0
- package/dist/domain/value-object.js +87 -0
- package/dist/domain/value-object.js.map +1 -0
- package/dist/errors/catalog.d.ts +71 -0
- package/dist/errors/catalog.d.ts.map +1 -0
- package/dist/errors/catalog.js +71 -0
- package/dist/errors/catalog.js.map +1 -0
- package/dist/errors/http.d.ts +77 -0
- package/dist/errors/http.d.ts.map +1 -0
- package/dist/errors/http.js +74 -0
- package/dist/errors/http.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +14 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/response.d.ts +26 -0
- package/dist/errors/response.d.ts.map +1 -0
- package/dist/errors/response.js +34 -0
- package/dist/errors/response.js.map +1 -0
- package/dist/errors/validation.d.ts +18 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +21 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +102 -0
- package/dist/events/index.js.map +1 -0
- package/dist/jobs/index.d.ts +56 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +89 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/mail/index.d.ts +75 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +84 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/openapi/index.d.ts +207 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +449 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/schema-introspector.d.ts +38 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -0
- package/dist/openapi/schema-introspector.js +67 -0
- package/dist/openapi/schema-introspector.js.map +1 -0
- package/dist/ports/audit.d.ts +58 -0
- package/dist/ports/audit.d.ts.map +1 -0
- package/dist/ports/audit.js +74 -0
- package/dist/ports/audit.js.map +1 -0
- package/dist/ports/auth.d.ts +23 -0
- package/dist/ports/auth.d.ts.map +1 -0
- package/dist/ports/auth.js +31 -0
- package/dist/ports/auth.js.map +1 -0
- package/dist/ports/builder.d.ts +61 -0
- package/dist/ports/builder.d.ts.map +1 -0
- package/dist/ports/builder.js +48 -0
- package/dist/ports/builder.js.map +1 -0
- package/dist/ports/cache.d.ts +15 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +57 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/clock.d.ts +10 -0
- package/dist/ports/clock.d.ts.map +1 -0
- package/dist/ports/clock.js +21 -0
- package/dist/ports/clock.js.map +1 -0
- package/dist/ports/events.d.ts +71 -0
- package/dist/ports/events.d.ts.map +1 -0
- package/dist/ports/events.js +2 -0
- package/dist/ports/events.js.map +1 -0
- package/dist/ports/id-generator.d.ts +12 -0
- package/dist/ports/id-generator.d.ts.map +1 -0
- package/dist/ports/id-generator.js +22 -0
- package/dist/ports/id-generator.js.map +1 -0
- package/dist/ports/index.d.ts +98 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +67 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/logger.d.ts +22 -0
- package/dist/ports/logger.d.ts.map +1 -0
- package/dist/ports/logger.js +34 -0
- package/dist/ports/logger.js.map +1 -0
- package/dist/ports/mailer.d.ts +6 -0
- package/dist/ports/mailer.d.ts.map +1 -0
- package/dist/ports/mailer.js +2 -0
- package/dist/ports/mailer.js.map +1 -0
- package/dist/ports/policy.d.ts +53 -0
- package/dist/ports/policy.d.ts.map +1 -0
- package/dist/ports/policy.js +81 -0
- package/dist/ports/policy.js.map +1 -0
- package/dist/ports/rate-limit.d.ts +41 -0
- package/dist/ports/rate-limit.d.ts.map +1 -0
- package/dist/ports/rate-limit.js +37 -0
- package/dist/ports/rate-limit.js.map +1 -0
- package/dist/ports/redaction.d.ts +26 -0
- package/dist/ports/redaction.d.ts.map +1 -0
- package/dist/ports/redaction.js +126 -0
- package/dist/ports/redaction.js.map +1 -0
- package/dist/ports/schedules.d.ts +9 -0
- package/dist/ports/schedules.d.ts.map +1 -0
- package/dist/ports/schedules.js +2 -0
- package/dist/ports/schedules.js.map +1 -0
- package/dist/ports/storage.d.ts +47 -0
- package/dist/ports/storage.d.ts.map +1 -0
- package/dist/ports/storage.js +185 -0
- package/dist/ports/storage.js.map +1 -0
- package/dist/ports/testing.d.ts +73 -0
- package/dist/ports/testing.d.ts.map +1 -0
- package/dist/ports/testing.js +105 -0
- package/dist/ports/testing.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +56 -0
- package/dist/ports/unit-of-work.d.ts.map +1 -0
- package/dist/ports/unit-of-work.js +64 -0
- package/dist/ports/unit-of-work.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/instrumentation.d.ts +91 -0
- package/dist/providers/instrumentation.d.ts.map +1 -0
- package/dist/providers/instrumentation.js +93 -0
- package/dist/providers/instrumentation.js.map +1 -0
- package/dist/providers/provider.d.ts +146 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +31 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/schedules/index.d.ts +105 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +178 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/server/contract-like.d.ts +5 -0
- package/dist/server/contract-like.d.ts.map +1 -0
- package/dist/server/contract-like.js +5 -0
- package/dist/server/contract-like.js.map +1 -0
- package/dist/server/health.d.ts +41 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +46 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/hooks/auth.d.ts +42 -0
- package/dist/server/hooks/auth.d.ts.map +1 -0
- package/dist/server/hooks/auth.js +61 -0
- package/dist/server/hooks/auth.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +13 -0
- package/dist/server/hooks/cors.d.ts.map +1 -0
- package/dist/server/hooks/cors.js +70 -0
- package/dist/server/hooks/cors.js.map +1 -0
- package/dist/server/hooks/errors.d.ts +66 -0
- package/dist/server/hooks/errors.d.ts.map +1 -0
- package/dist/server/hooks/errors.js +83 -0
- package/dist/server/hooks/errors.js.map +1 -0
- package/dist/server/hooks/index.d.ts +12 -0
- package/dist/server/hooks/index.d.ts.map +1 -0
- package/dist/server/hooks/index.js +12 -0
- package/dist/server/hooks/index.js.map +1 -0
- package/dist/server/hooks/logging.d.ts +33 -0
- package/dist/server/hooks/logging.d.ts.map +1 -0
- package/dist/server/hooks/logging.js +90 -0
- package/dist/server/hooks/logging.js.map +1 -0
- package/dist/server/hooks/rate-limit.d.ts +29 -0
- package/dist/server/hooks/rate-limit.d.ts.map +1 -0
- package/dist/server/hooks/rate-limit.js +93 -0
- package/dist/server/hooks/rate-limit.js.map +1 -0
- package/dist/server/hooks/utils.d.ts +9 -0
- package/dist/server/hooks/utils.d.ts.map +1 -0
- package/dist/server/hooks/utils.js +16 -0
- package/dist/server/hooks/utils.js.map +1 -0
- package/dist/server/hooks.d.ts +2 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +2 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/http.d.ts +124 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +2 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/openapi.d.ts +32 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/openapi.js +43 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/providers/index.d.ts +4 -0
- package/dist/server/providers/index.d.ts.map +1 -0
- package/dist/server/providers/index.js +4 -0
- package/dist/server/providers/index.js.map +1 -0
- package/dist/server/providers/loadProviderConfig.d.ts +7 -0
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
- package/dist/server/providers/loadProviderConfig.js +42 -0
- package/dist/server/providers/loadProviderConfig.js.map +1 -0
- package/dist/server/server.d.ts +86 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1031 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +129 -0
- package/src/application/index.ts +747 -0
- package/src/client/client.ts +1105 -0
- package/src/client/index.ts +45 -0
- package/src/client/types.ts +305 -0
- package/src/config/index.ts +497 -0
- package/src/contracts/contract-builder.ts +583 -0
- package/src/contracts/contract-group.ts +502 -0
- package/src/contracts/contract-like.ts +29 -0
- package/src/contracts/index.ts +53 -0
- package/src/contracts/openapi-meta.ts +22 -0
- package/src/contracts/path-template.ts +91 -0
- package/src/contracts/rate-limit.ts +50 -0
- package/src/contracts/types.ts +207 -0
- package/src/contracts/utils.ts +56 -0
- package/src/domain/entity.ts +256 -0
- package/src/domain/events.ts +52 -0
- package/src/domain/index.ts +18 -0
- package/src/domain/value-object.ts +135 -0
- package/src/errors/catalog.ts +149 -0
- package/src/errors/http.ts +80 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/response.ts +54 -0
- package/src/errors/validation.ts +35 -0
- package/src/events/index.ts +246 -0
- package/src/jobs/index.ts +211 -0
- package/src/mail/index.ts +177 -0
- package/src/openapi/index.ts +865 -0
- package/src/openapi/schema-introspector.ts +107 -0
- package/src/ports/audit.ts +176 -0
- package/src/ports/auth.ts +76 -0
- package/src/ports/builder.ts +97 -0
- package/src/ports/cache.ts +94 -0
- package/src/ports/clock.ts +34 -0
- package/src/ports/events.ts +100 -0
- package/src/ports/id-generator.ts +36 -0
- package/src/ports/index.ts +221 -0
- package/src/ports/logger.ts +67 -0
- package/src/ports/policy.ts +242 -0
- package/src/ports/rate-limit.ts +91 -0
- package/src/ports/redaction.ts +199 -0
- package/src/ports/storage.ts +282 -0
- package/src/ports/testing.ts +234 -0
- package/src/ports/unit-of-work.ts +134 -0
- package/src/providers/index.ts +40 -0
- package/src/providers/instrumentation.ts +248 -0
- package/src/providers/provider.ts +191 -0
- package/src/schedules/index.ts +442 -0
- package/src/server/contract-like.ts +8 -0
- package/src/server/health.ts +82 -0
- package/src/server/hooks/auth.ts +147 -0
- package/src/server/hooks/cors.ts +87 -0
- package/src/server/hooks/errors.ts +126 -0
- package/src/server/hooks/index.ts +43 -0
- package/src/server/hooks/logging.ts +121 -0
- package/src/server/hooks/rate-limit.ts +171 -0
- package/src/server/hooks/utils.ts +16 -0
- package/src/server/hooks.ts +1 -0
- package/src/server/http.ts +189 -0
- package/src/server/index.ts +35 -0
- package/src/server/openapi.ts +72 -0
- package/src/server/providers/index.ts +3 -0
- package/src/server/providers/loadProviderConfig.ts +72 -0
- package/src/server/server.ts +1521 -0
- package/src/server/types.ts +2 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts";
|
|
2
|
+
import { createErrorResponseBody, isAppError, isErrorResponseBody, toErrorResponseBody, } from "../errors";
|
|
3
|
+
import { AuthUnauthorizedError, GateAuthorizationError } from "../ports";
|
|
4
|
+
import { resolveContract } from "./contract-like";
|
|
5
|
+
import { getRequestIdFromContext } from "./hooks/utils";
|
|
6
|
+
import { loadProviderConfig, parseStandardSchema, SchemaValidationError, } from "./providers";
|
|
7
|
+
const ROUTE_GROUP_KIND = "beignet.route-group";
|
|
8
|
+
class PathDecodeError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Malformed URL path");
|
|
11
|
+
this.name = "PathDecodeError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function compilePath(path) {
|
|
15
|
+
const parsed = parsePathTemplate(path);
|
|
16
|
+
const regexParts = parsed.segments.map((segment) => segment.kind === "dynamic"
|
|
17
|
+
? "([^/]+)"
|
|
18
|
+
: segment.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
19
|
+
const pattern = new RegExp(`^/${regexParts.join("/")}$`);
|
|
20
|
+
return { ...parsed, pattern };
|
|
21
|
+
}
|
|
22
|
+
function decodeMatchedParams(keys, match) {
|
|
23
|
+
const params = {};
|
|
24
|
+
try {
|
|
25
|
+
keys.forEach((key, index) => {
|
|
26
|
+
params[key] = decodeURIComponent(match[index + 1]);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof URIError) {
|
|
31
|
+
throw new PathDecodeError();
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return params;
|
|
36
|
+
}
|
|
37
|
+
function compareRouteSpecificity(a, b) {
|
|
38
|
+
const maxLength = Math.max(a.segments.length, b.segments.length);
|
|
39
|
+
for (let index = 0; index < maxLength; index++) {
|
|
40
|
+
const aSegment = a.segments[index];
|
|
41
|
+
const bSegment = b.segments[index];
|
|
42
|
+
if (!aSegment)
|
|
43
|
+
return 1;
|
|
44
|
+
if (!bSegment)
|
|
45
|
+
return -1;
|
|
46
|
+
if (aSegment.kind === bSegment.kind)
|
|
47
|
+
continue;
|
|
48
|
+
return aSegment.kind === "static" ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
function errorResponse(status, code, message, details) {
|
|
53
|
+
return {
|
|
54
|
+
status,
|
|
55
|
+
body: createErrorResponseBody({ code, message, details }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeResponse(res) {
|
|
59
|
+
return {
|
|
60
|
+
status: res.status,
|
|
61
|
+
headers: res.headers,
|
|
62
|
+
body: res.body,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isWebResponse(value) {
|
|
66
|
+
return typeof Response !== "undefined" && value instanceof Response;
|
|
67
|
+
}
|
|
68
|
+
function normalizeHttpResponse(res) {
|
|
69
|
+
return isWebResponse(res) ? res : normalizeResponse(res);
|
|
70
|
+
}
|
|
71
|
+
function withFrameworkErrorOwnerHeader(res, owner) {
|
|
72
|
+
if (owner !== "framework" ||
|
|
73
|
+
res.status < 400 ||
|
|
74
|
+
!isErrorResponseBody(res.body)) {
|
|
75
|
+
return res;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...res,
|
|
79
|
+
headers: {
|
|
80
|
+
...(res.headers ?? {}),
|
|
81
|
+
[BEIGNET_ERROR_OWNER_HEADER]: "framework",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function responseOwnerFor(res, owner) {
|
|
86
|
+
if (isWebResponse(res))
|
|
87
|
+
return "transport";
|
|
88
|
+
return owner ?? "route";
|
|
89
|
+
}
|
|
90
|
+
function headersToRecord(headers) {
|
|
91
|
+
const record = {};
|
|
92
|
+
headers.forEach((value, key) => {
|
|
93
|
+
record[key] = value;
|
|
94
|
+
});
|
|
95
|
+
return record;
|
|
96
|
+
}
|
|
97
|
+
function requestHeadersToRecord(headers) {
|
|
98
|
+
const record = {};
|
|
99
|
+
headers.forEach((value, key) => {
|
|
100
|
+
record[key.toLowerCase()] = value;
|
|
101
|
+
});
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
async function parseHeaderSchemas(schemas, rawHeaders) {
|
|
105
|
+
let parsedHeaders = rawHeaders;
|
|
106
|
+
for (const schema of schemas) {
|
|
107
|
+
const parsed = await parseStandardSchema(schema, rawHeaders);
|
|
108
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
109
|
+
parsedHeaders = {
|
|
110
|
+
...parsedHeaders,
|
|
111
|
+
...parsed,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
parsedHeaders = parsed;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return parsedHeaders;
|
|
119
|
+
}
|
|
120
|
+
function responseForHooks(res) {
|
|
121
|
+
if (!isWebResponse(res)) {
|
|
122
|
+
return normalizeResponse(res);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
status: res.status,
|
|
126
|
+
headers: headersToRecord(res.headers),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isHttpResponseLike(value) {
|
|
130
|
+
return (!isWebResponse(value) &&
|
|
131
|
+
typeof value === "object" &&
|
|
132
|
+
value !== null &&
|
|
133
|
+
"status" in value &&
|
|
134
|
+
typeof value.status === "number");
|
|
135
|
+
}
|
|
136
|
+
class ResponseContractViolationError extends Error {
|
|
137
|
+
code;
|
|
138
|
+
details;
|
|
139
|
+
constructor(args) {
|
|
140
|
+
super(args.message);
|
|
141
|
+
this.name = "ResponseContractViolationError";
|
|
142
|
+
this.code = args.code;
|
|
143
|
+
this.details = args.details;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function getDeclaredCatalogErrorsForStatus(contract, status) {
|
|
147
|
+
const errors = contract.metadata?.errors;
|
|
148
|
+
if (typeof errors !== "object" || errors === null)
|
|
149
|
+
return [];
|
|
150
|
+
return Object.values(errors).filter((error) => typeof error === "object" &&
|
|
151
|
+
error !== null &&
|
|
152
|
+
typeof error.code === "string" &&
|
|
153
|
+
typeof error.status === "number" &&
|
|
154
|
+
typeof error.message === "string" &&
|
|
155
|
+
error.status === status);
|
|
156
|
+
}
|
|
157
|
+
async function validateCatalogErrorResponse(contract, res) {
|
|
158
|
+
const body = res.body;
|
|
159
|
+
if (res.status < 400 || !isErrorResponseBody(body))
|
|
160
|
+
return;
|
|
161
|
+
const declaredErrors = getDeclaredCatalogErrorsForStatus(contract, res.status);
|
|
162
|
+
if (declaredErrors.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
const matchingError = declaredErrors.find((error) => error.code === body.code);
|
|
165
|
+
if (!matchingError) {
|
|
166
|
+
throw new ResponseContractViolationError({
|
|
167
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
168
|
+
message: `Response validation failed for ${contract.method} ${contract.path} ` +
|
|
169
|
+
`(status ${res.status}, contract: ${contract.name})`,
|
|
170
|
+
details: {
|
|
171
|
+
issues: [
|
|
172
|
+
{
|
|
173
|
+
message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
|
|
174
|
+
`Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (matchingError.details && body.details !== undefined) {
|
|
181
|
+
try {
|
|
182
|
+
await parseStandardSchema(matchingError.details, body.details);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error instanceof SchemaValidationError) {
|
|
186
|
+
throw new ResponseContractViolationError({
|
|
187
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
188
|
+
message: `Response validation failed for ${contract.method} ${contract.path} ` +
|
|
189
|
+
`(status ${res.status}, contract: ${contract.name})`,
|
|
190
|
+
details: { issues: error.issues },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function validateResponseAgainstContract(contract, res) {
|
|
198
|
+
const statusKey = String(res.status);
|
|
199
|
+
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
200
|
+
if (!hasDeclaredStatus) {
|
|
201
|
+
if (Object.keys(contract.responses).length === 0)
|
|
202
|
+
return;
|
|
203
|
+
throw new ResponseContractViolationError({
|
|
204
|
+
code: "UNDECLARED_RESPONSE_STATUS",
|
|
205
|
+
message: `Handler returned undeclared status ${res.status} for ` +
|
|
206
|
+
`${contract.method} ${contract.path} (contract: ${contract.name})`,
|
|
207
|
+
details: {
|
|
208
|
+
status: res.status,
|
|
209
|
+
body: res.body,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const responseSchema = contract.responses[res.status];
|
|
214
|
+
if (responseSchema === null) {
|
|
215
|
+
if (res.body !== undefined && res.body !== null) {
|
|
216
|
+
throw new ResponseContractViolationError({
|
|
217
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
218
|
+
message: `Response validation failed for ${contract.method} ${contract.path} ` +
|
|
219
|
+
`(status ${res.status}, contract: ${contract.name})`,
|
|
220
|
+
details: {
|
|
221
|
+
issues: [
|
|
222
|
+
{
|
|
223
|
+
message: "Response body must be empty for a null response schema.",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!responseSchema)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
await parseStandardSchema(responseSchema, res.body);
|
|
235
|
+
await validateCatalogErrorResponse(contract, res);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error instanceof SchemaValidationError) {
|
|
239
|
+
throw new ResponseContractViolationError({
|
|
240
|
+
code: "RESPONSE_VALIDATION_ERROR",
|
|
241
|
+
message: `Response validation failed for ${contract.method} ${contract.path} ` +
|
|
242
|
+
`(status ${res.status}, contract: ${contract.name})`,
|
|
243
|
+
details: { issues: error.issues },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function finalizeResponse(contract, res) {
|
|
250
|
+
const normalized = normalizeResponse(res);
|
|
251
|
+
await validateResponseAgainstContract(contract, normalized);
|
|
252
|
+
return normalized;
|
|
253
|
+
}
|
|
254
|
+
function toContractViolationResponse(error) {
|
|
255
|
+
return {
|
|
256
|
+
status: 500,
|
|
257
|
+
body: createErrorResponseBody({
|
|
258
|
+
code: error.code,
|
|
259
|
+
message: error.message,
|
|
260
|
+
details: error.details,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function defaultErrorResponse(err, ctx) {
|
|
265
|
+
const requestId = getRequestIdFromContext(ctx);
|
|
266
|
+
return {
|
|
267
|
+
status: 500,
|
|
268
|
+
body: createErrorResponseBody({
|
|
269
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
270
|
+
message: "Internal server error",
|
|
271
|
+
requestId,
|
|
272
|
+
details: process.env.NODE_ENV !== "production" && err instanceof Error
|
|
273
|
+
? {
|
|
274
|
+
error: {
|
|
275
|
+
message: err.message,
|
|
276
|
+
stack: err.stack,
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
: undefined,
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function parseBody(req) {
|
|
284
|
+
const method = req.method.toUpperCase();
|
|
285
|
+
if (!methodSupportsRequestBody(method)) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
const bodyReq = req.clone?.() ?? req;
|
|
289
|
+
const contentType = req.headers.get("content-type") || "";
|
|
290
|
+
if (contentType.includes("application/json")) {
|
|
291
|
+
const text = await bodyReq.text();
|
|
292
|
+
if (text === "")
|
|
293
|
+
return undefined;
|
|
294
|
+
return JSON.parse(text);
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const text = await bodyReq.text();
|
|
298
|
+
return text === "" ? undefined : text;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function buildHandler(
|
|
305
|
+
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
306
|
+
options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
307
|
+
const compiled = compilePath(contract.path);
|
|
308
|
+
return async (req, preMatchedParams) => {
|
|
309
|
+
let baseCtx;
|
|
310
|
+
let pathValue;
|
|
311
|
+
let queryValue;
|
|
312
|
+
let headersValue;
|
|
313
|
+
let bodyValue;
|
|
314
|
+
const startedAt = Date.now();
|
|
315
|
+
const resolveErrorResult = async (error, ctx, path, query, headers, body, resultOptions) => {
|
|
316
|
+
let currentError = error;
|
|
317
|
+
const notifyCaughtError = async (caught) => {
|
|
318
|
+
const args = {
|
|
319
|
+
err: caught,
|
|
320
|
+
req,
|
|
321
|
+
ctx,
|
|
322
|
+
contract,
|
|
323
|
+
path,
|
|
324
|
+
query,
|
|
325
|
+
headers,
|
|
326
|
+
body,
|
|
327
|
+
};
|
|
328
|
+
for (const hook of hooks) {
|
|
329
|
+
if (!hook.onCaughtError)
|
|
330
|
+
continue;
|
|
331
|
+
try {
|
|
332
|
+
await hook.onCaughtError(args);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Observers must not change response behavior.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (options.onCaughtError) {
|
|
339
|
+
try {
|
|
340
|
+
await options.onCaughtError(args);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Observers must not change response behavior.
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
await notifyCaughtError(currentError);
|
|
348
|
+
if (currentError instanceof ResponseContractViolationError) {
|
|
349
|
+
return {
|
|
350
|
+
ctx,
|
|
351
|
+
response: toContractViolationResponse(currentError),
|
|
352
|
+
error: currentError,
|
|
353
|
+
owner: "framework",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (isAppError(currentError)) {
|
|
357
|
+
return {
|
|
358
|
+
ctx,
|
|
359
|
+
response: {
|
|
360
|
+
status: currentError.status,
|
|
361
|
+
body: toErrorResponseBody(currentError),
|
|
362
|
+
},
|
|
363
|
+
error: currentError,
|
|
364
|
+
owner: resultOptions?.owner ?? "route",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (currentError instanceof AuthUnauthorizedError) {
|
|
368
|
+
return {
|
|
369
|
+
ctx,
|
|
370
|
+
response: errorResponse(401, currentError.code, currentError.message),
|
|
371
|
+
error: currentError,
|
|
372
|
+
owner: "framework",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (currentError instanceof GateAuthorizationError) {
|
|
376
|
+
return {
|
|
377
|
+
ctx,
|
|
378
|
+
response: errorResponse(currentError.status, currentError.code, currentError.message, currentError.details),
|
|
379
|
+
error: currentError,
|
|
380
|
+
owner: "framework",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
for (const hook of hooks) {
|
|
384
|
+
if (!hook.mapUnhandledError)
|
|
385
|
+
continue;
|
|
386
|
+
try {
|
|
387
|
+
const handled = await hook.mapUnhandledError({
|
|
388
|
+
err: currentError,
|
|
389
|
+
req,
|
|
390
|
+
ctx,
|
|
391
|
+
contract,
|
|
392
|
+
path,
|
|
393
|
+
query,
|
|
394
|
+
headers,
|
|
395
|
+
body,
|
|
396
|
+
});
|
|
397
|
+
if (handled) {
|
|
398
|
+
const response = normalizeHttpResponse(handled);
|
|
399
|
+
return {
|
|
400
|
+
ctx,
|
|
401
|
+
response,
|
|
402
|
+
error: currentError,
|
|
403
|
+
owner: responseOwnerFor(response, "framework"),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch (hookError) {
|
|
408
|
+
currentError = hookError;
|
|
409
|
+
await notifyCaughtError(currentError);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (options.mapUnhandledError) {
|
|
413
|
+
try {
|
|
414
|
+
const handled = await options.mapUnhandledError({
|
|
415
|
+
err: currentError,
|
|
416
|
+
req,
|
|
417
|
+
ctx,
|
|
418
|
+
contract,
|
|
419
|
+
path,
|
|
420
|
+
query,
|
|
421
|
+
headers,
|
|
422
|
+
body,
|
|
423
|
+
});
|
|
424
|
+
if (handled) {
|
|
425
|
+
const response = normalizeHttpResponse(handled);
|
|
426
|
+
return {
|
|
427
|
+
ctx,
|
|
428
|
+
response,
|
|
429
|
+
error: currentError,
|
|
430
|
+
owner: responseOwnerFor(response, "framework"),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (hookError) {
|
|
435
|
+
currentError = hookError;
|
|
436
|
+
await notifyCaughtError(currentError);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
ctx,
|
|
441
|
+
response: defaultErrorResponse(currentError, ctx),
|
|
442
|
+
error: currentError,
|
|
443
|
+
owner: "framework",
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
const url = new URL(req.url);
|
|
448
|
+
let matchedParams;
|
|
449
|
+
if (preMatchedParams) {
|
|
450
|
+
matchedParams = preMatchedParams;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const match = compiled.pattern.exec(url.pathname);
|
|
454
|
+
if (!match ||
|
|
455
|
+
contract.method.toUpperCase() !== req.method.toUpperCase()) {
|
|
456
|
+
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
matchedParams = decodeMatchedParams(compiled.keys, match);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
if (error instanceof PathDecodeError) {
|
|
463
|
+
return errorResponse(400, "INVALID_PATH", "Malformed URL path");
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
469
|
+
const applyTransformHooks = async (initialResult, allowRetry) => {
|
|
470
|
+
if (isWebResponse(initialResult.response)) {
|
|
471
|
+
return initialResult;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
let transformed = normalizeResponse(initialResult.response);
|
|
475
|
+
for (const hook of hooks) {
|
|
476
|
+
if (!hook.beforeSend)
|
|
477
|
+
continue;
|
|
478
|
+
const nextResponse = await hook.beforeSend({
|
|
479
|
+
req,
|
|
480
|
+
ctx: initialResult.ctx,
|
|
481
|
+
contract,
|
|
482
|
+
path: pathValue,
|
|
483
|
+
query: queryValue,
|
|
484
|
+
headers: headersValue,
|
|
485
|
+
body: bodyValue,
|
|
486
|
+
response: transformed,
|
|
487
|
+
error: initialResult.error,
|
|
488
|
+
});
|
|
489
|
+
if (nextResponse) {
|
|
490
|
+
transformed = normalizeResponse(nextResponse);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
...initialResult,
|
|
495
|
+
response: transformed,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const mapped = await resolveErrorResult(error, initialResult.ctx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
500
|
+
if (!allowRetry) {
|
|
501
|
+
return mapped;
|
|
502
|
+
}
|
|
503
|
+
return applyTransformHooks(mapped, false);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
let result;
|
|
507
|
+
for (const hook of hooks) {
|
|
508
|
+
if (!hook.onRequest)
|
|
509
|
+
continue;
|
|
510
|
+
try {
|
|
511
|
+
const hookResult = await hook.onRequest({
|
|
512
|
+
req,
|
|
513
|
+
ports: finalPorts,
|
|
514
|
+
contract,
|
|
515
|
+
params: matchedParams,
|
|
516
|
+
});
|
|
517
|
+
if (hookResult) {
|
|
518
|
+
const response = normalizeHttpResponse(hookResult);
|
|
519
|
+
result = {
|
|
520
|
+
response,
|
|
521
|
+
owner: responseOwnerFor(response, "framework"),
|
|
522
|
+
};
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (!result) {
|
|
532
|
+
if (optionsOverrides?.skipRoutePreparation) {
|
|
533
|
+
let createdCtx;
|
|
534
|
+
try {
|
|
535
|
+
createdCtx = await options.createContext({
|
|
536
|
+
req,
|
|
537
|
+
ports: finalPorts,
|
|
538
|
+
contract,
|
|
539
|
+
});
|
|
540
|
+
baseCtx = createdCtx;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
result = await resolveErrorResult(error, undefined, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
544
|
+
}
|
|
545
|
+
if (!result) {
|
|
546
|
+
try {
|
|
547
|
+
result = {
|
|
548
|
+
ctx: createdCtx,
|
|
549
|
+
response: normalizeHttpResponse(await userHandler({
|
|
550
|
+
req,
|
|
551
|
+
ctx: createdCtx,
|
|
552
|
+
contract,
|
|
553
|
+
path: {},
|
|
554
|
+
query: {},
|
|
555
|
+
headers: rawHeaders,
|
|
556
|
+
body: undefined,
|
|
557
|
+
})),
|
|
558
|
+
owner: "framework",
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
result = await resolveErrorResult(error, createdCtx, undefined, undefined, undefined, undefined, { owner: "framework" });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
const rawQuery = {};
|
|
568
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
569
|
+
const values = url.searchParams.getAll(key);
|
|
570
|
+
rawQuery[key] = values.length === 1 ? values[0] : values;
|
|
571
|
+
}
|
|
572
|
+
// biome-ignore lint/suspicious/noExplicitAny: type is narrowed by schema validation below
|
|
573
|
+
let query = rawQuery;
|
|
574
|
+
if (contract.query) {
|
|
575
|
+
try {
|
|
576
|
+
query = await parseStandardSchema(contract.query, query);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
result =
|
|
580
|
+
error instanceof SchemaValidationError
|
|
581
|
+
? {
|
|
582
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", { issues: error.issues }),
|
|
583
|
+
owner: "framework",
|
|
584
|
+
}
|
|
585
|
+
: {
|
|
586
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", error.message),
|
|
587
|
+
owner: "framework",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
592
|
+
let path = matchedParams;
|
|
593
|
+
if (!result && contract.pathParams) {
|
|
594
|
+
try {
|
|
595
|
+
path = await parseStandardSchema(contract.pathParams, matchedParams);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
result =
|
|
599
|
+
error instanceof SchemaValidationError
|
|
600
|
+
? {
|
|
601
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", { issues: error.issues }),
|
|
602
|
+
owner: "framework",
|
|
603
|
+
}
|
|
604
|
+
: {
|
|
605
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", error.message),
|
|
606
|
+
owner: "framework",
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
611
|
+
let headers = rawHeaders;
|
|
612
|
+
const headerSchemas = getContractHeaderSchemas(contract.headers);
|
|
613
|
+
if (!result && headerSchemas.length > 0) {
|
|
614
|
+
try {
|
|
615
|
+
headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
result =
|
|
619
|
+
error instanceof SchemaValidationError
|
|
620
|
+
? {
|
|
621
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", { issues: error.issues }),
|
|
622
|
+
owner: "framework",
|
|
623
|
+
}
|
|
624
|
+
: {
|
|
625
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", error.message),
|
|
626
|
+
owner: "framework",
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
631
|
+
let body;
|
|
632
|
+
if (!result) {
|
|
633
|
+
try {
|
|
634
|
+
body = await parseBody(req);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
result = {
|
|
638
|
+
response: errorResponse(400, "INVALID_BODY", "Malformed JSON"),
|
|
639
|
+
owner: "framework",
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!result && contract.body) {
|
|
644
|
+
try {
|
|
645
|
+
body = await parseStandardSchema(contract.body, body);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (body === undefined &&
|
|
649
|
+
error instanceof SchemaValidationError) {
|
|
650
|
+
result = {
|
|
651
|
+
response: errorResponse(400, "MISSING_BODY", "Request body is required"),
|
|
652
|
+
owner: "framework",
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
result =
|
|
657
|
+
error instanceof SchemaValidationError
|
|
658
|
+
? {
|
|
659
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", { issues: error.issues }),
|
|
660
|
+
owner: "framework",
|
|
661
|
+
}
|
|
662
|
+
: {
|
|
663
|
+
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", error.message),
|
|
664
|
+
owner: "framework",
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (!result) {
|
|
670
|
+
pathValue = path;
|
|
671
|
+
queryValue = query;
|
|
672
|
+
headersValue = headers;
|
|
673
|
+
bodyValue = body;
|
|
674
|
+
let createdCtx;
|
|
675
|
+
try {
|
|
676
|
+
createdCtx = await options.createContext({
|
|
677
|
+
req,
|
|
678
|
+
ports: finalPorts,
|
|
679
|
+
contract,
|
|
680
|
+
});
|
|
681
|
+
baseCtx = createdCtx;
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
result = await resolveErrorResult(error, undefined, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
685
|
+
}
|
|
686
|
+
if (!result) {
|
|
687
|
+
const baseArgs = {
|
|
688
|
+
req,
|
|
689
|
+
ctx: createdCtx,
|
|
690
|
+
contract,
|
|
691
|
+
path,
|
|
692
|
+
query,
|
|
693
|
+
headers,
|
|
694
|
+
body,
|
|
695
|
+
};
|
|
696
|
+
let currentCtx = createdCtx;
|
|
697
|
+
for (const hook of hooks) {
|
|
698
|
+
if (!hook.beforeHandle)
|
|
699
|
+
continue;
|
|
700
|
+
try {
|
|
701
|
+
const hookResult = await hook.beforeHandle({
|
|
702
|
+
req,
|
|
703
|
+
ctx: currentCtx,
|
|
704
|
+
contract,
|
|
705
|
+
path,
|
|
706
|
+
query,
|
|
707
|
+
headers,
|
|
708
|
+
body,
|
|
709
|
+
});
|
|
710
|
+
if (isWebResponse(hookResult)) {
|
|
711
|
+
result = {
|
|
712
|
+
ctx: currentCtx,
|
|
713
|
+
response: hookResult,
|
|
714
|
+
owner: "transport",
|
|
715
|
+
};
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
if (isHttpResponseLike(hookResult)) {
|
|
719
|
+
result = {
|
|
720
|
+
ctx: currentCtx,
|
|
721
|
+
response: normalizeResponse(hookResult),
|
|
722
|
+
owner: "framework",
|
|
723
|
+
};
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
if (hookResult?.ctx !== undefined) {
|
|
727
|
+
currentCtx = hookResult.ctx;
|
|
728
|
+
}
|
|
729
|
+
if (hookResult?.response) {
|
|
730
|
+
const response = normalizeHttpResponse(hookResult.response);
|
|
731
|
+
result = {
|
|
732
|
+
ctx: currentCtx,
|
|
733
|
+
response,
|
|
734
|
+
owner: responseOwnerFor(response, "framework"),
|
|
735
|
+
};
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (!result) {
|
|
745
|
+
try {
|
|
746
|
+
result = {
|
|
747
|
+
ctx: currentCtx,
|
|
748
|
+
response: normalizeHttpResponse(await userHandler({ ...baseArgs, ctx: currentCtx })),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
result = await applyTransformHooks(result, true);
|
|
760
|
+
let finalResponse = normalizeHttpResponse(result.response);
|
|
761
|
+
let finalError = result.error;
|
|
762
|
+
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
763
|
+
if (finalOwner === "route" && !isWebResponse(finalResponse)) {
|
|
764
|
+
try {
|
|
765
|
+
finalResponse = await finalizeResponse(contract, finalResponse);
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
if (error instanceof ResponseContractViolationError) {
|
|
769
|
+
result = await applyTransformHooks({
|
|
770
|
+
ctx: result.ctx,
|
|
771
|
+
response: toContractViolationResponse(error),
|
|
772
|
+
error,
|
|
773
|
+
owner: "framework",
|
|
774
|
+
}, false);
|
|
775
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
776
|
+
finalError = result.error;
|
|
777
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!isWebResponse(finalResponse)) {
|
|
785
|
+
finalResponse = withFrameworkErrorOwnerHeader(finalResponse, finalOwner);
|
|
786
|
+
}
|
|
787
|
+
const durationMs = Date.now() - startedAt;
|
|
788
|
+
for (const hook of hooks) {
|
|
789
|
+
if (!hook.afterSend)
|
|
790
|
+
continue;
|
|
791
|
+
try {
|
|
792
|
+
await hook.afterSend({
|
|
793
|
+
req,
|
|
794
|
+
ctx: result.ctx,
|
|
795
|
+
contract,
|
|
796
|
+
path: pathValue,
|
|
797
|
+
query: queryValue,
|
|
798
|
+
headers: headersValue,
|
|
799
|
+
body: bodyValue,
|
|
800
|
+
response: responseForHooks(finalResponse),
|
|
801
|
+
error: finalError,
|
|
802
|
+
durationMs,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Ignore after-response hook failures; they should never change the response.
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return finalResponse;
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
const result = await resolveErrorResult(error, baseCtx, pathValue, queryValue, headersValue, bodyValue, {
|
|
813
|
+
owner: "framework",
|
|
814
|
+
});
|
|
815
|
+
const response = normalizeHttpResponse(result.response);
|
|
816
|
+
if (isWebResponse(response)) {
|
|
817
|
+
return response;
|
|
818
|
+
}
|
|
819
|
+
return withFrameworkErrorOwnerHeader(response, responseOwnerFor(response, result.owner));
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
export async function createServer(options) {
|
|
824
|
+
const registry = [];
|
|
825
|
+
const providers = (options.providers ?? []);
|
|
826
|
+
const env = options.providerEnv ?? process.env;
|
|
827
|
+
const overrides = options.providerConfig ?? {};
|
|
828
|
+
const providerResults = [];
|
|
829
|
+
const finalPorts = { ...options.ports };
|
|
830
|
+
const hooks = [...(options.hooks ?? [])];
|
|
831
|
+
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
832
|
+
let stopped = false;
|
|
833
|
+
const stop = async () => {
|
|
834
|
+
if (stopped)
|
|
835
|
+
return;
|
|
836
|
+
stopped = true;
|
|
837
|
+
const errors = [];
|
|
838
|
+
for (let i = providerResults.length - 1; i >= 0; i -= 1) {
|
|
839
|
+
const result = providerResults[i];
|
|
840
|
+
try {
|
|
841
|
+
await result?.stop?.({
|
|
842
|
+
ports: finalPorts,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
errors.push(err);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (errors.length) {
|
|
850
|
+
throw new AggregateError(errors, "Provider shutdown errors");
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
const registeredPaths = new Set();
|
|
854
|
+
const registeredShapes = new Map();
|
|
855
|
+
const registerRoute = (contract, handler) => {
|
|
856
|
+
if (contract.body && !methodSupportsRequestBody(contract.method)) {
|
|
857
|
+
throw new Error(`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`);
|
|
858
|
+
}
|
|
859
|
+
const compiled = compilePath(contract.path);
|
|
860
|
+
const normalizedPath = compiled.normalizedPath;
|
|
861
|
+
const routeKey = `${contract.method.toUpperCase()} ${normalizedPath}`;
|
|
862
|
+
if (registeredPaths.has(routeKey)) {
|
|
863
|
+
throw new Error(`Duplicate route: ${routeKey} is already registered. Each method + path combination must be unique.`);
|
|
864
|
+
}
|
|
865
|
+
const shapeRouteKey = `${contract.method.toUpperCase()} ${compiled.shapeKey}`;
|
|
866
|
+
const conflictingRoute = registeredShapes.get(shapeRouteKey);
|
|
867
|
+
if (conflictingRoute) {
|
|
868
|
+
throw new Error(`Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`);
|
|
869
|
+
}
|
|
870
|
+
registeredPaths.add(routeKey);
|
|
871
|
+
registeredShapes.set(shapeRouteKey, routeKey);
|
|
872
|
+
const builtHandler = buildHandler(options, finalPorts, contract, handler, hooks);
|
|
873
|
+
registry.push({
|
|
874
|
+
contract,
|
|
875
|
+
compiled,
|
|
876
|
+
handler: builtHandler,
|
|
877
|
+
match: (method, pathname) => {
|
|
878
|
+
if (contract.method.toUpperCase() !== method.toUpperCase()) {
|
|
879
|
+
return { matched: false };
|
|
880
|
+
}
|
|
881
|
+
const match = compiled.pattern.exec(pathname);
|
|
882
|
+
if (!match)
|
|
883
|
+
return { matched: false };
|
|
884
|
+
return { matched: true };
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
|
|
888
|
+
};
|
|
889
|
+
const createBuilder = (contract, shouldRegister) => ({
|
|
890
|
+
handle: (fn) => {
|
|
891
|
+
const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
|
|
892
|
+
if (shouldRegister)
|
|
893
|
+
registerRoute(contract, fn);
|
|
894
|
+
return wrapped;
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
if (options.routes) {
|
|
898
|
+
try {
|
|
899
|
+
for (const route of options.routes) {
|
|
900
|
+
const contract = resolveContract(route.contract);
|
|
901
|
+
registerRoute(contract, route.handle);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
try {
|
|
906
|
+
await stop();
|
|
907
|
+
}
|
|
908
|
+
catch (cleanupError) {
|
|
909
|
+
throw new AggregateError([error, cleanupError], "Server initialization failed and provider cleanup failed");
|
|
910
|
+
}
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
for (const provider of providers) {
|
|
916
|
+
const cfg = await loadProviderConfig(provider, env, overrides);
|
|
917
|
+
const result = await provider.setup({
|
|
918
|
+
ports: finalPorts,
|
|
919
|
+
config: cfg,
|
|
920
|
+
});
|
|
921
|
+
if (result.ports) {
|
|
922
|
+
Object.assign(finalPorts, result.ports);
|
|
923
|
+
}
|
|
924
|
+
providerResults.push(result);
|
|
925
|
+
}
|
|
926
|
+
for (const result of providerResults) {
|
|
927
|
+
if (!result.start)
|
|
928
|
+
continue;
|
|
929
|
+
await result.start({
|
|
930
|
+
ports: finalPorts,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
try {
|
|
936
|
+
await stop();
|
|
937
|
+
}
|
|
938
|
+
catch (cleanupError) {
|
|
939
|
+
throw new AggregateError([error, cleanupError], "Server initialization failed and provider cleanup failed");
|
|
940
|
+
}
|
|
941
|
+
throw error;
|
|
942
|
+
}
|
|
943
|
+
const api = async (req) => {
|
|
944
|
+
const url = new URL(req.url);
|
|
945
|
+
for (const entry of registry) {
|
|
946
|
+
const result = entry.match(req.method, url.pathname);
|
|
947
|
+
if (result.matched) {
|
|
948
|
+
return await entry.handler(req);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
const notFoundContract = {
|
|
952
|
+
kind: "http",
|
|
953
|
+
name: "notFound",
|
|
954
|
+
method: req.method.toUpperCase(),
|
|
955
|
+
path: url.pathname || "/",
|
|
956
|
+
pathParams: null,
|
|
957
|
+
query: null,
|
|
958
|
+
body: null,
|
|
959
|
+
responses: {},
|
|
960
|
+
metadata: {},
|
|
961
|
+
};
|
|
962
|
+
const notFoundHandler = buildHandler(options, finalPorts, notFoundContract, async () => errorResponse(404, "NOT_FOUND", "Not found"), hooks, { skipRoutePreparation: true });
|
|
963
|
+
return await notFoundHandler(req);
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
api,
|
|
967
|
+
route: (contractLike) => {
|
|
968
|
+
const contract = resolveContract(contractLike);
|
|
969
|
+
return createBuilder(contract, true);
|
|
970
|
+
},
|
|
971
|
+
contracts,
|
|
972
|
+
stop,
|
|
973
|
+
ports: finalPorts,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Helper function to define routes with proper type inference.
|
|
978
|
+
* Eliminates the need for type assertions when passing routes to createServer.
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* const routes = defineRoutes<AppContext>([
|
|
982
|
+
* { contract: myContract, handle: async ({ query }) => { ... } }
|
|
983
|
+
* ]);
|
|
984
|
+
*/
|
|
985
|
+
export function defineRoutes(routes) {
|
|
986
|
+
const flattened = [];
|
|
987
|
+
for (const route of routes) {
|
|
988
|
+
if (isRouteGroup(route)) {
|
|
989
|
+
flattened.push(...route.routes);
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
flattened.push(route);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return flattened;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Extract contract configs from a route list. Use this to drive OpenAPI from
|
|
999
|
+
* the same route registration list that createServer receives.
|
|
1000
|
+
*/
|
|
1001
|
+
export function contractsFromRoutes(routes) {
|
|
1002
|
+
return routes.map((route) => resolveContract(route.contract));
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Helper function to group related route registrations.
|
|
1006
|
+
*
|
|
1007
|
+
* Route groups are flattened by defineRoutes, so createServer still receives
|
|
1008
|
+
* a regular route list while app code can keep feature route wiring colocated.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* const todoRoutes = defineRouteGroup<AppContext>({
|
|
1012
|
+
* name: "todos",
|
|
1013
|
+
* routes: [
|
|
1014
|
+
* { contract: listTodos, handle: async ({ ctx }) => { ... } }
|
|
1015
|
+
* ]
|
|
1016
|
+
* });
|
|
1017
|
+
*/
|
|
1018
|
+
export function defineRouteGroup(group) {
|
|
1019
|
+
return {
|
|
1020
|
+
kind: ROUTE_GROUP_KIND,
|
|
1021
|
+
name: group.name,
|
|
1022
|
+
routes: group.routes,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function isRouteGroup(route) {
|
|
1026
|
+
return (typeof route === "object" &&
|
|
1027
|
+
route !== null &&
|
|
1028
|
+
"kind" in route &&
|
|
1029
|
+
route.kind === ROUTE_GROUP_KIND);
|
|
1030
|
+
}
|
|
1031
|
+
//# sourceMappingURL=server.js.map
|