@beignet/core 0.0.2 → 0.0.4
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 +173 -0
- package/README.md +821 -30
- package/dist/application/index.d.ts +28 -2
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +140 -12
- package/dist/application/index.js.map +1 -1
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +136 -48
- package/dist/client/client.js.map +1 -1
- package/dist/client/error-messages.d.ts +14 -0
- package/dist/client/error-messages.d.ts.map +1 -0
- package/dist/client/error-messages.js +23 -0
- package/dist/client/error-messages.js.map +1 -0
- package/dist/client/index.d.ts +8 -4
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +35 -5
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client-only.d.ts +8 -0
- package/dist/client-only.d.ts.map +1 -0
- package/dist/client-only.js +8 -0
- package/dist/client-only.js.map +1 -0
- package/dist/config/index.d.ts +5 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/contracts/catalog-errors.d.ts +27 -0
- package/dist/contracts/catalog-errors.d.ts.map +1 -0
- package/dist/contracts/catalog-errors.js +69 -0
- package/dist/contracts/catalog-errors.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +15 -12
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +15 -41
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts +11 -8
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +13 -40
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +1 -1
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +13 -9
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +9 -5
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/openapi-meta.d.ts +48 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -1
- package/dist/contracts/openapi-meta.js +3 -0
- package/dist/contracts/openapi-meta.js.map +1 -1
- package/dist/contracts/path-template.d.ts +1 -1
- package/dist/contracts/path-template.js +2 -2
- package/dist/contracts/path-template.js.map +1 -1
- package/dist/contracts/schema-shape.d.ts +37 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -0
- package/dist/contracts/schema-shape.js +61 -0
- package/dist/contracts/schema-shape.js.map +1 -0
- package/dist/contracts/success-status.d.ts +32 -0
- package/dist/contracts/success-status.d.ts.map +1 -0
- package/dist/contracts/success-status.js +18 -0
- package/dist/contracts/success-status.js.map +1 -0
- package/dist/contracts/types.d.ts +25 -5
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/contracts/utils.d.ts +1 -1
- package/dist/contracts/utils.d.ts.map +1 -1
- package/dist/contracts/utils.js +1 -1
- package/dist/contracts/utils.js.map +1 -1
- package/dist/domain/events.d.ts +1 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +3 -3
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +3 -3
- package/dist/domain/index.js.map +1 -1
- package/dist/errors/catalog.d.ts +9 -1
- package/dist/errors/catalog.d.ts.map +1 -1
- package/dist/errors/catalog.js +7 -1
- package/dist/errors/catalog.js.map +1 -1
- package/dist/errors/http.d.ts +10 -0
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +11 -1
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +4 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +4 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/response.d.ts +4 -1
- package/dist/errors/response.d.ts.map +1 -1
- package/dist/errors/response.js.map +1 -1
- package/dist/events/index.d.ts +10 -12
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +10 -10
- package/dist/events/index.js.map +1 -1
- package/dist/idempotency/index.d.ts +5 -3
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js.map +1 -1
- package/dist/jobs/index.d.ts +148 -16
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +174 -14
- package/dist/jobs/index.js.map +1 -1
- package/dist/notifications/index.d.ts +14 -16
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +14 -14
- package/dist/notifications/index.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -3
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +41 -29
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/schema-introspector.d.ts +37 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -1
- package/dist/openapi/schema-introspector.js +23 -17
- package/dist/openapi/schema-introspector.js.map +1 -1
- package/dist/outbox/index.d.ts +18 -4
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +104 -4
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/audit.d.ts +56 -10
- package/dist/ports/audit.d.ts.map +1 -1
- package/dist/ports/audit.js +71 -3
- package/dist/ports/audit.js.map +1 -1
- package/dist/ports/auth.d.ts +92 -0
- package/dist/ports/auth.d.ts.map +1 -1
- package/dist/ports/auth.js +92 -0
- package/dist/ports/auth.js.map +1 -1
- package/dist/ports/events.d.ts +2 -2
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/index.d.ts +62 -33
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +28 -34
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +32 -3
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +13 -2
- package/dist/ports/policy.js.map +1 -1
- package/dist/ports/testing.d.ts +1030 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +1031 -1
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unbound.d.ts +21 -0
- package/dist/ports/unbound.d.ts.map +1 -0
- package/dist/ports/unbound.js +57 -0
- package/dist/ports/unbound.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +1 -1
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +1 -1
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +46 -5
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +25 -6
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +39 -0
- package/dist/providers/metadata.d.ts.map +1 -0
- package/dist/providers/metadata.js +169 -0
- package/dist/providers/metadata.js.map +1 -0
- package/dist/providers/provider.d.ts +114 -9
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +3 -20
- package/dist/providers/provider.js.map +1 -1
- package/dist/schedules/index.d.ts +94 -13
- package/dist/schedules/index.d.ts.map +1 -1
- package/dist/schedules/index.js +66 -12
- package/dist/schedules/index.js.map +1 -1
- package/dist/server/audit-context.d.ts +29 -0
- package/dist/server/audit-context.d.ts.map +1 -0
- package/dist/server/audit-context.js +44 -0
- package/dist/server/audit-context.js.map +1 -0
- package/dist/server/context.d.ts +141 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +39 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract-like.d.ts +1 -1
- package/dist/server/contract-like.d.ts.map +1 -1
- package/dist/server/contract-like.js +1 -1
- package/dist/server/contract-like.js.map +1 -1
- package/dist/server/health.d.ts +2 -2
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/hooks/auth.d.ts +89 -65
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +84 -55
- package/dist/server/hooks/auth.js.map +1 -1
- package/dist/server/hooks/cors.d.ts +1 -1
- package/dist/server/hooks/cors.d.ts.map +1 -1
- package/dist/server/hooks/errors.d.ts +2 -2
- package/dist/server/hooks/errors.d.ts.map +1 -1
- package/dist/server/hooks/errors.js +2 -2
- package/dist/server/hooks/errors.js.map +1 -1
- package/dist/server/hooks/idempotency.d.ts +78 -0
- package/dist/server/hooks/idempotency.d.ts.map +1 -0
- package/dist/server/hooks/idempotency.js +154 -0
- package/dist/server/hooks/idempotency.js.map +1 -0
- package/dist/server/hooks/index.d.ts +8 -7
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +6 -5
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/hooks/logging.d.ts +2 -2
- package/dist/server/hooks/logging.d.ts.map +1 -1
- package/dist/server/hooks/logging.js +1 -1
- package/dist/server/hooks/logging.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +25 -7
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +47 -12
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/hooks.d.ts +1 -1
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +1 -1
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/http.d.ts +84 -6
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/index.d.ts +36 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +24 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +108 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +297 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/openapi.d.ts +3 -3
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/openapi.js +1 -1
- package/dist/server/openapi.js.map +1 -1
- package/dist/server/providers/index.d.ts +3 -3
- package/dist/server/providers/index.d.ts.map +1 -1
- package/dist/server/providers/index.js +3 -3
- package/dist/server/providers/index.js.map +1 -1
- package/dist/server/providers/loadProviderConfig.d.ts +2 -2
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -1
- package/dist/server/providers/loadProviderConfig.js +2 -2
- package/dist/server/providers/loadProviderConfig.js.map +1 -1
- package/dist/server/request-context.d.ts +67 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +79 -0
- package/dist/server/request-context.js.map +1 -0
- package/dist/server/server-context.d.ts +38 -0
- package/dist/server/server-context.d.ts.map +1 -0
- package/dist/server/server-context.js +38 -0
- package/dist/server/server-context.js.map +1 -0
- package/dist/server/server.d.ts +148 -35
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +482 -145
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +2 -2
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/use-case-route.d.ts +263 -0
- package/dist/server/use-case-route.d.ts.map +1 -0
- package/dist/server/use-case-route.js +77 -0
- package/dist/server/use-case-route.js.map +1 -0
- package/dist/server-only.d.ts +8 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/server-only.js +8 -0
- package/dist/server-only.js.map +1 -0
- package/dist/tasks/index.d.ts +139 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +98 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/testing/index.d.ts +611 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +434 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/tracing/index.d.ts +89 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +101 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/uploads/client.d.ts +278 -0
- package/dist/uploads/client.d.ts.map +1 -0
- package/dist/uploads/client.js +428 -0
- package/dist/uploads/client.js.map +1 -0
- package/dist/uploads/index.d.ts +361 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +543 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +34 -3
- package/src/application/index.ts +193 -10
- package/src/client/client.ts +148 -150
- package/src/client/error-messages.ts +35 -0
- package/src/client/index.ts +12 -4
- package/src/client/types.ts +44 -5
- package/src/client-only.ts +7 -0
- package/src/config/index.ts +6 -6
- package/src/contracts/catalog-errors.ts +115 -0
- package/src/contracts/contract-builder.ts +39 -76
- package/src/contracts/contract-group.ts +33 -68
- package/src/contracts/contract-like.ts +1 -1
- package/src/contracts/index.ts +24 -11
- package/src/contracts/openapi-meta.ts +55 -0
- package/src/contracts/path-template.ts +2 -2
- package/src/contracts/schema-shape.ts +75 -0
- package/src/contracts/success-status.ts +68 -0
- package/src/contracts/types.ts +32 -5
- package/src/contracts/utils.ts +5 -2
- package/src/domain/events.ts +6 -2
- package/src/domain/index.ts +3 -3
- package/src/errors/catalog.ts +9 -1
- package/src/errors/http.ts +11 -1
- package/src/errors/index.ts +4 -4
- package/src/errors/response.ts +4 -1
- package/src/events/index.ts +12 -26
- package/src/idempotency/index.ts +5 -3
- package/src/jobs/index.ts +340 -29
- package/src/notifications/index.ts +17 -27
- package/src/openapi/index.ts +73 -38
- package/src/openapi/schema-introspector.ts +68 -17
- package/src/outbox/index.ts +151 -6
- package/src/ports/audit.ts +120 -11
- package/src/ports/auth.ts +132 -0
- package/src/ports/events.ts +2 -2
- package/src/ports/index.ts +104 -35
- package/src/ports/policy.ts +50 -3
- package/src/ports/testing.ts +2220 -33
- package/src/ports/unbound.ts +64 -0
- package/src/ports/unit-of-work.ts +6 -2
- package/src/providers/index.ts +16 -3
- package/src/providers/instrumentation.ts +93 -8
- package/src/providers/metadata.ts +234 -0
- package/src/providers/provider.ts +168 -9
- package/src/schedules/index.ts +173 -23
- package/src/server/audit-context.ts +45 -0
- package/src/server/context.ts +224 -0
- package/src/server/contract-like.ts +1 -1
- package/src/server/health.ts +2 -2
- package/src/server/hooks/auth.ts +175 -158
- package/src/server/hooks/cors.ts +1 -1
- package/src/server/hooks/errors.ts +7 -4
- package/src/server/hooks/idempotency.ts +263 -0
- package/src/server/hooks/index.ts +15 -12
- package/src/server/hooks/logging.ts +3 -3
- package/src/server/hooks/rate-limit.ts +85 -17
- package/src/server/hooks.ts +1 -1
- package/src/server/http.ts +112 -6
- package/src/server/index.ts +63 -12
- package/src/server/instrumentation.ts +470 -0
- package/src/server/openapi.ts +4 -4
- package/src/server/providers/index.ts +6 -3
- package/src/server/providers/loadProviderConfig.ts +4 -4
- package/src/server/request-context.ts +116 -0
- package/src/server/server-context.ts +44 -0
- package/src/server/server.ts +1045 -229
- package/src/server/types.ts +2 -2
- package/src/server/use-case-route.ts +430 -0
- package/src/server-only.ts +7 -0
- package/src/tasks/index.ts +275 -0
- package/src/testing/index.ts +1153 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +861 -0
- package/src/uploads/index.ts +1071 -0
- package/dist/ports/mailer.d.ts +0 -6
- package/dist/ports/mailer.d.ts.map +0 -1
- package/dist/ports/mailer.js +0 -2
- package/dist/ports/mailer.js.map +0 -1
- package/dist/ports/schedules.d.ts +0 -9
- package/dist/ports/schedules.d.ts.map +0 -1
- package/dist/ports/schedules.js +0 -2
- package/dist/ports/schedules.js.map +0 -1
package/dist/server/server.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { BEIGNET_ERROR_OWNER_HEADER, getContractHeaderSchemas, methodSupportsRequestBody, parsePathTemplate, } from "../contracts/index.js";
|
|
2
|
+
import { comparePathParamsToTemplate, formatPathParamsMismatch, getObjectSchemaShape, } from "../contracts/schema-shape.js";
|
|
3
|
+
import { createErrorResponseBody, httpErrors, isAppError, isErrorResponseBody, toErrorResponseBody, } from "../errors/index.js";
|
|
4
|
+
import { IdempotencyConflictError, IdempotencyInProgressError, } from "../idempotency/index.js";
|
|
5
|
+
import { AuthUnauthorizedError, GateAuthorizationError, isUnboundPort, TenantRequiredError, } from "../ports/index.js";
|
|
6
|
+
import { createContextFinalizer, resolveServerContext } from "./context.js";
|
|
7
|
+
import { resolveContract } from "./contract-like.js";
|
|
8
|
+
import { getRequestIdFromContext } from "./hooks/utils.js";
|
|
9
|
+
import { createServerInstrumentation } from "./instrumentation.js";
|
|
10
|
+
import { loadProviderConfig, parseStandardSchema, SchemaValidationError, } from "./providers/index.js";
|
|
11
|
+
import { enterActiveRequestContext, readContextActor, readContextTenant, setActiveRequestIdentity, } from "./request-context.js";
|
|
12
|
+
import { createUseCaseRouteHandler, isUseCaseRouteDef, } from "./use-case-route.js";
|
|
7
13
|
const ROUTE_GROUP_KIND = "beignet.route-group";
|
|
14
|
+
/**
|
|
15
|
+
* Define one route registration with hook-aware handler typing.
|
|
16
|
+
*
|
|
17
|
+
* Direct route objects are still supported. Use this helper when route-scoped
|
|
18
|
+
* hooks enrich `ctx` for a single handler and you want TypeScript to infer the
|
|
19
|
+
* added fields.
|
|
20
|
+
*/
|
|
21
|
+
export function defineRoute() {
|
|
22
|
+
function define(route) {
|
|
23
|
+
return route;
|
|
24
|
+
}
|
|
25
|
+
return define;
|
|
26
|
+
}
|
|
8
27
|
class PathDecodeError extends Error {
|
|
9
28
|
constructor() {
|
|
10
29
|
super("Malformed URL path");
|
|
@@ -55,6 +74,35 @@ function errorResponse(status, code, message, details) {
|
|
|
55
74
|
body: createErrorResponseBody({ code, message, details }),
|
|
56
75
|
};
|
|
57
76
|
}
|
|
77
|
+
function contractDiagnostics(contract) {
|
|
78
|
+
return {
|
|
79
|
+
contract: contract.name,
|
|
80
|
+
method: contract.method,
|
|
81
|
+
path: contract.path,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function requestValidationDetails(contract, location, error) {
|
|
85
|
+
const details = {
|
|
86
|
+
...contractDiagnostics(contract),
|
|
87
|
+
location,
|
|
88
|
+
};
|
|
89
|
+
if (error instanceof SchemaValidationError) {
|
|
90
|
+
return {
|
|
91
|
+
...details,
|
|
92
|
+
issues: error.issues,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
return {
|
|
97
|
+
...details,
|
|
98
|
+
message: error.message,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return details;
|
|
102
|
+
}
|
|
103
|
+
function requestValidationError(contract, status, code, message, location, error) {
|
|
104
|
+
return errorResponse(status, code, message, requestValidationDetails(contract, location, error));
|
|
105
|
+
}
|
|
58
106
|
function normalizeResponse(res) {
|
|
59
107
|
return {
|
|
60
108
|
status: res.status,
|
|
@@ -126,6 +174,42 @@ function responseForHooks(res) {
|
|
|
126
174
|
headers: headersToRecord(res.headers),
|
|
127
175
|
};
|
|
128
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Merge hook-applied header changes onto a native web Response.
|
|
179
|
+
*
|
|
180
|
+
* Starts from the native response's `Headers` so `set-cookie` multiplicity is
|
|
181
|
+
* preserved, then applies headers the beforeSend chain added or changed
|
|
182
|
+
* relative to the original headers-only view. The body stream passes through
|
|
183
|
+
* untouched; status and statusText are preserved.
|
|
184
|
+
*/
|
|
185
|
+
function mergeNativeResponseHeaders(nativeResponse, originalHeaders, finalHeaders) {
|
|
186
|
+
const originalByLowerKey = new Map();
|
|
187
|
+
for (const [key, value] of Object.entries(originalHeaders)) {
|
|
188
|
+
originalByLowerKey.set(key.toLowerCase(), value);
|
|
189
|
+
}
|
|
190
|
+
let changed = false;
|
|
191
|
+
const merged = new Headers(nativeResponse.headers);
|
|
192
|
+
for (const [key, value] of Object.entries(finalHeaders)) {
|
|
193
|
+
const lowerKey = key.toLowerCase();
|
|
194
|
+
if (originalByLowerKey.get(lowerKey) === value)
|
|
195
|
+
continue;
|
|
196
|
+
changed = true;
|
|
197
|
+
if (lowerKey === "set-cookie") {
|
|
198
|
+
merged.append(lowerKey, value);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
merged.set(key, value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!changed) {
|
|
205
|
+
return nativeResponse;
|
|
206
|
+
}
|
|
207
|
+
return new Response(nativeResponse.body, {
|
|
208
|
+
status: nativeResponse.status,
|
|
209
|
+
statusText: nativeResponse.statusText,
|
|
210
|
+
headers: merged,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
129
213
|
function isHttpResponseLike(value) {
|
|
130
214
|
return (!isWebResponse(value) &&
|
|
131
215
|
typeof value === "object" &&
|
|
@@ -143,6 +227,25 @@ class ResponseContractViolationError extends Error {
|
|
|
143
227
|
this.details = args.details;
|
|
144
228
|
}
|
|
145
229
|
}
|
|
230
|
+
function responseContractViolationMessage(contract, status) {
|
|
231
|
+
return (`Response validation failed for ${contract.method} ${contract.path} ` +
|
|
232
|
+
`(status ${status}, contract: ${contract.name})`);
|
|
233
|
+
}
|
|
234
|
+
function declaredResponseStatuses(contract) {
|
|
235
|
+
return Object.keys(contract.responses)
|
|
236
|
+
.map((status) => Number(status))
|
|
237
|
+
.filter((status) => Number.isFinite(status))
|
|
238
|
+
.sort((a, b) => a - b);
|
|
239
|
+
}
|
|
240
|
+
function responseContractViolationDetails(contract, status, details) {
|
|
241
|
+
return {
|
|
242
|
+
...contractDiagnostics(contract),
|
|
243
|
+
location: "response",
|
|
244
|
+
status,
|
|
245
|
+
declaredStatuses: declaredResponseStatuses(contract),
|
|
246
|
+
...details,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
146
249
|
function getDeclaredCatalogErrorsForStatus(contract, status) {
|
|
147
250
|
const errors = contract.metadata?.errors;
|
|
148
251
|
if (typeof errors !== "object" || errors === null)
|
|
@@ -165,16 +268,15 @@ async function validateCatalogErrorResponse(contract, res) {
|
|
|
165
268
|
if (!matchingError) {
|
|
166
269
|
throw new ResponseContractViolationError({
|
|
167
270
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
168
|
-
message:
|
|
169
|
-
|
|
170
|
-
details: {
|
|
271
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
272
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
171
273
|
issues: [
|
|
172
274
|
{
|
|
173
275
|
message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
|
|
174
276
|
`Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
|
|
175
277
|
},
|
|
176
278
|
],
|
|
177
|
-
},
|
|
279
|
+
}),
|
|
178
280
|
});
|
|
179
281
|
}
|
|
180
282
|
if (matchingError.details && body.details !== undefined) {
|
|
@@ -185,16 +287,17 @@ async function validateCatalogErrorResponse(contract, res) {
|
|
|
185
287
|
if (error instanceof SchemaValidationError) {
|
|
186
288
|
throw new ResponseContractViolationError({
|
|
187
289
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
188
|
-
message:
|
|
189
|
-
|
|
190
|
-
|
|
290
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
291
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
292
|
+
issues: error.issues,
|
|
293
|
+
}),
|
|
191
294
|
});
|
|
192
295
|
}
|
|
193
296
|
throw error;
|
|
194
297
|
}
|
|
195
298
|
}
|
|
196
299
|
}
|
|
197
|
-
async function validateResponseAgainstContract(contract, res) {
|
|
300
|
+
async function validateResponseAgainstContract(contract, res, responseValidationExemptStatus) {
|
|
198
301
|
const statusKey = String(res.status);
|
|
199
302
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
200
303
|
if (!hasDeclaredStatus) {
|
|
@@ -204,10 +307,9 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
204
307
|
code: "UNDECLARED_RESPONSE_STATUS",
|
|
205
308
|
message: `Handler returned undeclared status ${res.status} for ` +
|
|
206
309
|
`${contract.method} ${contract.path} (contract: ${contract.name})`,
|
|
207
|
-
details: {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
},
|
|
310
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
311
|
+
returnedStatus: res.status,
|
|
312
|
+
}),
|
|
211
313
|
});
|
|
212
314
|
}
|
|
213
315
|
const responseSchema = contract.responses[res.status];
|
|
@@ -215,21 +317,25 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
215
317
|
if (res.body !== undefined && res.body !== null) {
|
|
216
318
|
throw new ResponseContractViolationError({
|
|
217
319
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
218
|
-
message:
|
|
219
|
-
|
|
220
|
-
details: {
|
|
320
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
321
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
221
322
|
issues: [
|
|
222
323
|
{
|
|
223
324
|
message: "Response body must be empty for a null response schema.",
|
|
224
325
|
},
|
|
225
326
|
],
|
|
226
|
-
},
|
|
327
|
+
}),
|
|
227
328
|
});
|
|
228
329
|
}
|
|
229
330
|
return;
|
|
230
331
|
}
|
|
231
332
|
if (!responseSchema)
|
|
232
333
|
return;
|
|
334
|
+
// Binder routes whose use case output schema is the same object as the
|
|
335
|
+
// declared success response schema skip the redundant success-status parse.
|
|
336
|
+
// Error statuses and undeclared statuses are validated unchanged.
|
|
337
|
+
if (res.status === responseValidationExemptStatus)
|
|
338
|
+
return;
|
|
233
339
|
try {
|
|
234
340
|
await parseStandardSchema(responseSchema, res.body);
|
|
235
341
|
await validateCatalogErrorResponse(contract, res);
|
|
@@ -238,17 +344,18 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
238
344
|
if (error instanceof SchemaValidationError) {
|
|
239
345
|
throw new ResponseContractViolationError({
|
|
240
346
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
241
|
-
message:
|
|
242
|
-
|
|
243
|
-
|
|
347
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
348
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
349
|
+
issues: error.issues,
|
|
350
|
+
}),
|
|
244
351
|
});
|
|
245
352
|
}
|
|
246
353
|
throw error;
|
|
247
354
|
}
|
|
248
355
|
}
|
|
249
|
-
async function finalizeResponse(contract, res) {
|
|
356
|
+
async function finalizeResponse(contract, res, responseValidationExemptStatus) {
|
|
250
357
|
const normalized = normalizeResponse(res);
|
|
251
|
-
await validateResponseAgainstContract(contract, normalized);
|
|
358
|
+
await validateResponseAgainstContract(contract, normalized, responseValidationExemptStatus);
|
|
252
359
|
return normalized;
|
|
253
360
|
}
|
|
254
361
|
function toContractViolationResponse(error) {
|
|
@@ -301,11 +408,19 @@ async function parseBody(req) {
|
|
|
301
408
|
return undefined;
|
|
302
409
|
}
|
|
303
410
|
}
|
|
304
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Build the per-request execution pipeline once.
|
|
413
|
+
*
|
|
414
|
+
* The returned executor takes the contract, compiled pattern, and user handler
|
|
415
|
+
* per invocation so fallback responses (404/405) can reuse a single pipeline
|
|
416
|
+
* across requests instead of rebuilding it per unmatched request.
|
|
417
|
+
*/
|
|
418
|
+
function createRequestExecutor(
|
|
305
419
|
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
306
|
-
options, finalPorts,
|
|
307
|
-
const
|
|
308
|
-
return async (req, preMatchedParams) => {
|
|
420
|
+
options, finalPorts, contextRuntime, hooks, routeHooks = [], optionsOverrides) {
|
|
421
|
+
const warnedNativeReplacementHooks = new WeakSet();
|
|
422
|
+
return async (target, req, preMatchedParams) => {
|
|
423
|
+
const { contract, handler: userHandler } = target;
|
|
309
424
|
let baseCtx;
|
|
310
425
|
let pathValue;
|
|
311
426
|
let queryValue;
|
|
@@ -372,6 +487,36 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
372
487
|
owner: "framework",
|
|
373
488
|
};
|
|
374
489
|
}
|
|
490
|
+
if (currentError instanceof TenantRequiredError) {
|
|
491
|
+
return {
|
|
492
|
+
ctx,
|
|
493
|
+
response: errorResponse(currentError.status, currentError.code, currentError.message),
|
|
494
|
+
error: currentError,
|
|
495
|
+
owner: "framework",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (currentError instanceof IdempotencyConflictError) {
|
|
499
|
+
return {
|
|
500
|
+
ctx,
|
|
501
|
+
response: errorResponse(httpErrors.IdempotencyConflict.status, httpErrors.IdempotencyConflict.code, currentError.message, {
|
|
502
|
+
namespace: currentError.namespace,
|
|
503
|
+
key: currentError.key,
|
|
504
|
+
}),
|
|
505
|
+
error: currentError,
|
|
506
|
+
owner: "framework",
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (currentError instanceof IdempotencyInProgressError) {
|
|
510
|
+
return {
|
|
511
|
+
ctx,
|
|
512
|
+
response: errorResponse(httpErrors.IdempotencyInProgress.status, httpErrors.IdempotencyInProgress.code, currentError.message, {
|
|
513
|
+
namespace: currentError.namespace,
|
|
514
|
+
key: currentError.key,
|
|
515
|
+
}),
|
|
516
|
+
error: currentError,
|
|
517
|
+
owner: "framework",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
375
520
|
if (currentError instanceof GateAuthorizationError) {
|
|
376
521
|
return {
|
|
377
522
|
ctx,
|
|
@@ -450,8 +595,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
450
595
|
matchedParams = preMatchedParams;
|
|
451
596
|
}
|
|
452
597
|
else {
|
|
453
|
-
const
|
|
454
|
-
|
|
598
|
+
const compiled = target.compiled;
|
|
599
|
+
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
|
|
600
|
+
if (!compiled ||
|
|
601
|
+
!match ||
|
|
455
602
|
contract.method.toUpperCase() !== req.method.toUpperCase()) {
|
|
456
603
|
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
457
604
|
}
|
|
@@ -466,11 +613,49 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
466
613
|
}
|
|
467
614
|
}
|
|
468
615
|
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
616
|
+
const runNativeBeforeSend = async (initialResult, nativeResponse) => {
|
|
617
|
+
const originalView = responseForHooks(nativeResponse);
|
|
618
|
+
const originalHeaders = originalView.headers ?? {};
|
|
619
|
+
let transformed = originalView;
|
|
620
|
+
for (const hook of hooks) {
|
|
621
|
+
if (!hook.beforeSend)
|
|
622
|
+
continue;
|
|
623
|
+
const nextResponse = await hook.beforeSend({
|
|
624
|
+
req,
|
|
625
|
+
ctx: initialResult.ctx,
|
|
626
|
+
contract,
|
|
627
|
+
path: pathValue,
|
|
628
|
+
query: queryValue,
|
|
629
|
+
headers: headersValue,
|
|
630
|
+
body: bodyValue,
|
|
631
|
+
response: transformed,
|
|
632
|
+
error: initialResult.error,
|
|
633
|
+
native: true,
|
|
634
|
+
});
|
|
635
|
+
if (nextResponse) {
|
|
636
|
+
if ((nextResponse.status !== nativeResponse.status ||
|
|
637
|
+
nextResponse.body !== undefined) &&
|
|
638
|
+
!warnedNativeReplacementHooks.has(hook) &&
|
|
639
|
+
process.env.NODE_ENV !== "production") {
|
|
640
|
+
warnedNativeReplacementHooks.add(hook);
|
|
641
|
+
console.warn(`[beignet] beforeSend hook "${hook.name ?? "(anonymous)"}" returned a replacement status or body for a native Response on ${contract.method} ${contract.path}. Native responses are headers-only in beforeSend; status and body changes are ignored.`);
|
|
642
|
+
}
|
|
643
|
+
transformed = {
|
|
644
|
+
status: nativeResponse.status,
|
|
645
|
+
headers: nextResponse.headers,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
472
648
|
}
|
|
649
|
+
return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
|
|
650
|
+
};
|
|
651
|
+
const applyTransformHooks = async (initialResult, allowRetry) => {
|
|
473
652
|
try {
|
|
653
|
+
if (isWebResponse(initialResult.response)) {
|
|
654
|
+
return {
|
|
655
|
+
...initialResult,
|
|
656
|
+
response: await runNativeBeforeSend(initialResult, initialResult.response),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
474
659
|
let transformed = normalizeResponse(initialResult.response);
|
|
475
660
|
for (const hook of hooks) {
|
|
476
661
|
if (!hook.beforeSend)
|
|
@@ -532,11 +717,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
532
717
|
if (optionsOverrides?.skipRoutePreparation) {
|
|
533
718
|
let createdCtx;
|
|
534
719
|
try {
|
|
535
|
-
createdCtx = await
|
|
536
|
-
req,
|
|
537
|
-
ports: finalPorts,
|
|
538
|
-
contract,
|
|
539
|
-
});
|
|
720
|
+
createdCtx = await contextRuntime.createRequestContext(req, contract);
|
|
540
721
|
baseCtx = createdCtx;
|
|
541
722
|
}
|
|
542
723
|
catch (error) {
|
|
@@ -576,16 +757,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
576
757
|
query = await parseStandardSchema(contract.query, query);
|
|
577
758
|
}
|
|
578
759
|
catch (error) {
|
|
579
|
-
result =
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
owner: "framework",
|
|
584
|
-
}
|
|
585
|
-
: {
|
|
586
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", error.message),
|
|
587
|
-
owner: "framework",
|
|
588
|
-
};
|
|
760
|
+
result = {
|
|
761
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid query parameters", "query", error),
|
|
762
|
+
owner: "framework",
|
|
763
|
+
};
|
|
589
764
|
}
|
|
590
765
|
}
|
|
591
766
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -595,16 +770,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
595
770
|
path = await parseStandardSchema(contract.pathParams, matchedParams);
|
|
596
771
|
}
|
|
597
772
|
catch (error) {
|
|
598
|
-
result =
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
owner: "framework",
|
|
603
|
-
}
|
|
604
|
-
: {
|
|
605
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", error.message),
|
|
606
|
-
owner: "framework",
|
|
607
|
-
};
|
|
773
|
+
result = {
|
|
774
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid path parameters", "path", error),
|
|
775
|
+
owner: "framework",
|
|
776
|
+
};
|
|
608
777
|
}
|
|
609
778
|
}
|
|
610
779
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -615,16 +784,10 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
615
784
|
headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
|
|
616
785
|
}
|
|
617
786
|
catch (error) {
|
|
618
|
-
result =
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
owner: "framework",
|
|
623
|
-
}
|
|
624
|
-
: {
|
|
625
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", error.message),
|
|
626
|
-
owner: "framework",
|
|
627
|
-
};
|
|
787
|
+
result = {
|
|
788
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request headers", "headers", error),
|
|
789
|
+
owner: "framework",
|
|
790
|
+
};
|
|
628
791
|
}
|
|
629
792
|
}
|
|
630
793
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -633,9 +796,9 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
633
796
|
try {
|
|
634
797
|
body = await parseBody(req);
|
|
635
798
|
}
|
|
636
|
-
catch {
|
|
799
|
+
catch (error) {
|
|
637
800
|
result = {
|
|
638
|
-
response:
|
|
801
|
+
response: requestValidationError(contract, 400, "INVALID_BODY", "Malformed JSON", "body", error),
|
|
639
802
|
owner: "framework",
|
|
640
803
|
};
|
|
641
804
|
}
|
|
@@ -648,21 +811,15 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
648
811
|
if (body === undefined &&
|
|
649
812
|
error instanceof SchemaValidationError) {
|
|
650
813
|
result = {
|
|
651
|
-
response:
|
|
814
|
+
response: requestValidationError(contract, 400, "MISSING_BODY", "Request body is required", "body", error),
|
|
652
815
|
owner: "framework",
|
|
653
816
|
};
|
|
654
817
|
}
|
|
655
818
|
else {
|
|
656
|
-
result =
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
owner: "framework",
|
|
661
|
-
}
|
|
662
|
-
: {
|
|
663
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", error.message),
|
|
664
|
-
owner: "framework",
|
|
665
|
-
};
|
|
819
|
+
result = {
|
|
820
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request body", "body", error),
|
|
821
|
+
owner: "framework",
|
|
822
|
+
};
|
|
666
823
|
}
|
|
667
824
|
}
|
|
668
825
|
}
|
|
@@ -673,11 +830,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
673
830
|
bodyValue = body;
|
|
674
831
|
let createdCtx;
|
|
675
832
|
try {
|
|
676
|
-
createdCtx = await
|
|
677
|
-
req,
|
|
678
|
-
ports: finalPorts,
|
|
679
|
-
contract,
|
|
680
|
-
});
|
|
833
|
+
createdCtx = await contextRuntime.createRequestContext(req, contract);
|
|
681
834
|
baseCtx = createdCtx;
|
|
682
835
|
}
|
|
683
836
|
catch (error) {
|
|
@@ -724,7 +877,7 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
724
877
|
break;
|
|
725
878
|
}
|
|
726
879
|
if (hookResult?.ctx !== undefined) {
|
|
727
|
-
currentCtx = hookResult.ctx;
|
|
880
|
+
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
728
881
|
}
|
|
729
882
|
if (hookResult?.response) {
|
|
730
883
|
const response = normalizeHttpResponse(hookResult.response);
|
|
@@ -742,6 +895,39 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
742
895
|
}
|
|
743
896
|
}
|
|
744
897
|
if (!result) {
|
|
898
|
+
for (const hook of routeHooks) {
|
|
899
|
+
try {
|
|
900
|
+
const additions = await hook.resolve({
|
|
901
|
+
req,
|
|
902
|
+
ctx: currentCtx,
|
|
903
|
+
contract,
|
|
904
|
+
path,
|
|
905
|
+
query,
|
|
906
|
+
headers,
|
|
907
|
+
body,
|
|
908
|
+
});
|
|
909
|
+
if (additions && typeof additions === "object") {
|
|
910
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
911
|
+
...currentCtx,
|
|
912
|
+
...additions,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
result = await resolveErrorResult(error, currentCtx, pathValue, queryValue, headersValue, bodyValue, { owner: "framework" });
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (!result) {
|
|
923
|
+
// Hooks may have elevated the actor or resolved a tenant.
|
|
924
|
+
// Refresh the ambient request context so record-time
|
|
925
|
+
// consumers such as createAmbientAuditLog see the finalized
|
|
926
|
+
// identity.
|
|
927
|
+
setActiveRequestIdentity({
|
|
928
|
+
actor: readContextActor(currentCtx),
|
|
929
|
+
tenant: readContextTenant(currentCtx),
|
|
930
|
+
});
|
|
745
931
|
try {
|
|
746
932
|
result = {
|
|
747
933
|
ctx: currentCtx,
|
|
@@ -760,9 +946,11 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
760
946
|
let finalResponse = normalizeHttpResponse(result.response);
|
|
761
947
|
let finalError = result.error;
|
|
762
948
|
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
763
|
-
if (finalOwner === "route" &&
|
|
949
|
+
if (finalOwner === "route" &&
|
|
950
|
+
!isWebResponse(finalResponse) &&
|
|
951
|
+
(options.validateResponses ?? true)) {
|
|
764
952
|
try {
|
|
765
|
-
finalResponse = await finalizeResponse(contract, finalResponse);
|
|
953
|
+
finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
|
|
766
954
|
}
|
|
767
955
|
catch (error) {
|
|
768
956
|
if (error instanceof ResponseContractViolationError) {
|
|
@@ -820,6 +1008,18 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
820
1008
|
}
|
|
821
1009
|
};
|
|
822
1010
|
}
|
|
1011
|
+
function buildHandler(
|
|
1012
|
+
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
1013
|
+
options, finalPorts, contextRuntime, contract, userHandler, hooks, routeHooks = [], optionsOverrides, responseValidationExemptStatus) {
|
|
1014
|
+
const execute = createRequestExecutor(options, finalPorts, contextRuntime, hooks, routeHooks, optionsOverrides);
|
|
1015
|
+
const executionTarget = {
|
|
1016
|
+
contract,
|
|
1017
|
+
compiled: compilePath(contract.path),
|
|
1018
|
+
handler: userHandler,
|
|
1019
|
+
responseValidationExemptStatus,
|
|
1020
|
+
};
|
|
1021
|
+
return (req, preMatchedParams) => execute(executionTarget, req, preMatchedParams);
|
|
1022
|
+
}
|
|
823
1023
|
/**
|
|
824
1024
|
* Create a Beignet server instance.
|
|
825
1025
|
*
|
|
@@ -828,19 +1028,80 @@ options, finalPorts, contract, userHandler, hooks, optionsOverrides) {
|
|
|
828
1028
|
* Use adapter packages such as `@beignet/next` to expose `server.api` to a
|
|
829
1029
|
* specific runtime.
|
|
830
1030
|
*
|
|
831
|
-
* @param options - Ports, providers, routes, hooks, context
|
|
832
|
-
* mapping hooks for the server.
|
|
1031
|
+
* @param options - Ports, providers, routes, hooks, context blueprint, and
|
|
1032
|
+
* error mapping hooks for the server.
|
|
833
1033
|
* @returns A started server instance with final ports and a catch-all handler.
|
|
834
1034
|
*/
|
|
835
1035
|
export async function createServer(options) {
|
|
836
1036
|
const registry = [];
|
|
1037
|
+
// Routes can register after startup via server.route(...), so the registry
|
|
1038
|
+
// is re-sorted lazily before the next dispatch instead of on every
|
|
1039
|
+
// registration.
|
|
1040
|
+
let registryNeedsSort = false;
|
|
837
1041
|
const providers = (options.providers ?? []);
|
|
838
1042
|
const env = options.providerEnv ?? process.env;
|
|
839
1043
|
const overrides = options.providerConfig ?? {};
|
|
840
1044
|
const providerResults = [];
|
|
841
1045
|
const finalPorts = { ...options.ports };
|
|
842
|
-
const
|
|
1046
|
+
const instrumentation = createServerInstrumentation(options.instrumentation);
|
|
1047
|
+
const hooks = [
|
|
1048
|
+
...(instrumentation.hook
|
|
1049
|
+
? [instrumentation.hook]
|
|
1050
|
+
: []),
|
|
1051
|
+
...(options.hooks ?? []),
|
|
1052
|
+
];
|
|
843
1053
|
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
1054
|
+
const resolvedContext = resolveServerContext(options.context);
|
|
1055
|
+
const finalizeContext = createContextFinalizer(resolvedContext, () => finalPorts);
|
|
1056
|
+
const createRequestContext = async (req, contract) => {
|
|
1057
|
+
const { requestId, trace } = instrumentation.prepareRequest(req);
|
|
1058
|
+
return finalizeContext(await resolvedContext.request({
|
|
1059
|
+
req,
|
|
1060
|
+
ports: finalPorts,
|
|
1061
|
+
contract,
|
|
1062
|
+
requestId,
|
|
1063
|
+
trace,
|
|
1064
|
+
}));
|
|
1065
|
+
};
|
|
1066
|
+
const createServiceContext = async (...args) => {
|
|
1067
|
+
const serviceFactory = resolvedContext.service;
|
|
1068
|
+
if (!serviceFactory) {
|
|
1069
|
+
throw new Error("Define context.service in createServer(...) to create service contexts.");
|
|
1070
|
+
}
|
|
1071
|
+
const { requestId, trace } = instrumentation.createServiceCorrelation();
|
|
1072
|
+
// Enter the ambient context synchronously (before the factory awaits) so
|
|
1073
|
+
// it propagates to the caller's continuation. Identity fields are filled
|
|
1074
|
+
// onto the same object once the context is finalized, so jobs, listeners,
|
|
1075
|
+
// schedules, and tasks observe the service actor/tenant at record time.
|
|
1076
|
+
const ambient = {
|
|
1077
|
+
requestId,
|
|
1078
|
+
traceId: trace.traceId,
|
|
1079
|
+
spanId: trace.spanId,
|
|
1080
|
+
parentSpanId: trace.parentSpanId,
|
|
1081
|
+
traceparent: trace.traceparent,
|
|
1082
|
+
};
|
|
1083
|
+
enterActiveRequestContext(ambient);
|
|
1084
|
+
const ctx = finalizeContext(await serviceFactory({
|
|
1085
|
+
ports: finalPorts,
|
|
1086
|
+
input: args[0],
|
|
1087
|
+
requestId,
|
|
1088
|
+
trace,
|
|
1089
|
+
}));
|
|
1090
|
+
ambient.actor = readContextActor(ctx);
|
|
1091
|
+
ambient.tenant = readContextTenant(ctx);
|
|
1092
|
+
return ctx;
|
|
1093
|
+
};
|
|
1094
|
+
const contextRuntime = {
|
|
1095
|
+
createRequestContext,
|
|
1096
|
+
finalizeContext,
|
|
1097
|
+
};
|
|
1098
|
+
let serviceContextsAvailable = false;
|
|
1099
|
+
const lifecycleCreateServiceContext = async (input) => {
|
|
1100
|
+
if (!serviceContextsAvailable) {
|
|
1101
|
+
throw new Error("Service contexts are unavailable until providers have started.");
|
|
1102
|
+
}
|
|
1103
|
+
return createServiceContext(...[input]);
|
|
1104
|
+
};
|
|
844
1105
|
let stopped = false;
|
|
845
1106
|
const stop = async () => {
|
|
846
1107
|
if (stopped)
|
|
@@ -852,6 +1113,7 @@ export async function createServer(options) {
|
|
|
852
1113
|
try {
|
|
853
1114
|
await result?.stop?.({
|
|
854
1115
|
ports: finalPorts,
|
|
1116
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
855
1117
|
});
|
|
856
1118
|
}
|
|
857
1119
|
catch (err) {
|
|
@@ -864,7 +1126,8 @@ export async function createServer(options) {
|
|
|
864
1126
|
};
|
|
865
1127
|
const registeredPaths = new Set();
|
|
866
1128
|
const registeredShapes = new Map();
|
|
867
|
-
const
|
|
1129
|
+
const registeredNames = new Map();
|
|
1130
|
+
const registerRoute = (contract, handler, routeHooks = [], responseValidationExemptStatus) => {
|
|
868
1131
|
if (contract.body && !methodSupportsRequestBody(contract.method)) {
|
|
869
1132
|
throw new Error(`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`);
|
|
870
1133
|
}
|
|
@@ -879,12 +1142,31 @@ export async function createServer(options) {
|
|
|
879
1142
|
if (conflictingRoute) {
|
|
880
1143
|
throw new Error(`Ambiguous route: ${routeKey} conflicts with ${conflictingRoute}. Dynamic parameter names are ignored during routing, so each method + path shape must be unique.`);
|
|
881
1144
|
}
|
|
1145
|
+
const conflictingName = registeredNames.get(contract.name);
|
|
1146
|
+
if (conflictingName) {
|
|
1147
|
+
throw new Error(`Duplicate contract name: "${contract.name}" is registered for both ${conflictingName} and ${routeKey}. Contract names must be unique because typed clients, OpenAPI operations, and devtools key on them.`);
|
|
1148
|
+
}
|
|
1149
|
+
if (contract.pathParams) {
|
|
1150
|
+
const shape = getObjectSchemaShape(contract.pathParams);
|
|
1151
|
+
if (shape) {
|
|
1152
|
+
const { missingKeys, extraKeys } = comparePathParamsToTemplate({
|
|
1153
|
+
pathKeys: compiled.keys,
|
|
1154
|
+
shapeKeys: Object.keys(shape),
|
|
1155
|
+
});
|
|
1156
|
+
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
|
1157
|
+
const details = formatPathParamsMismatch({ missingKeys, extraKeys });
|
|
1158
|
+
throw new Error(`Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}). Path templates and pathParams schemas drive routing, clients, and OpenAPI together.`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
882
1162
|
registeredPaths.add(routeKey);
|
|
883
1163
|
registeredShapes.set(shapeRouteKey, routeKey);
|
|
884
|
-
|
|
1164
|
+
registeredNames.set(contract.name, routeKey);
|
|
1165
|
+
const builtHandler = buildHandler(options, finalPorts, contextRuntime, contract, handler, hooks, routeHooks, undefined, responseValidationExemptStatus);
|
|
885
1166
|
registry.push({
|
|
886
1167
|
contract,
|
|
887
1168
|
compiled,
|
|
1169
|
+
method: contract.method.toUpperCase(),
|
|
888
1170
|
handler: builtHandler,
|
|
889
1171
|
match: (method, pathname) => {
|
|
890
1172
|
if (contract.method.toUpperCase() !== method.toUpperCase()) {
|
|
@@ -896,11 +1178,11 @@ export async function createServer(options) {
|
|
|
896
1178
|
return { matched: true };
|
|
897
1179
|
},
|
|
898
1180
|
});
|
|
899
|
-
|
|
1181
|
+
registryNeedsSort = true;
|
|
900
1182
|
};
|
|
901
1183
|
const createBuilder = (contract, shouldRegister) => ({
|
|
902
1184
|
handle: (fn) => {
|
|
903
|
-
const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
|
|
1185
|
+
const wrapped = buildHandler(options, finalPorts, contextRuntime, contract, fn, hooks);
|
|
904
1186
|
if (shouldRegister)
|
|
905
1187
|
registerRoute(contract, fn);
|
|
906
1188
|
return wrapped;
|
|
@@ -910,7 +1192,21 @@ export async function createServer(options) {
|
|
|
910
1192
|
try {
|
|
911
1193
|
for (const route of options.routes) {
|
|
912
1194
|
const contract = resolveContract(route.contract);
|
|
913
|
-
|
|
1195
|
+
const hasHandle = typeof route.handle === "function";
|
|
1196
|
+
const hasUseCase = isUseCaseRouteDef(route);
|
|
1197
|
+
if (hasHandle && hasUseCase) {
|
|
1198
|
+
throw new Error(`Route for contract "${contract.name}" declares both "handle" and "useCase". Bind the contract to exactly one of them.`);
|
|
1199
|
+
}
|
|
1200
|
+
if (!hasHandle && !hasUseCase) {
|
|
1201
|
+
throw new Error(`Route for contract "${contract.name}" declares neither "handle" nor "useCase". Bind the contract to a use case or implement a handler.`);
|
|
1202
|
+
}
|
|
1203
|
+
if (isUseCaseRouteDef(route)) {
|
|
1204
|
+
const { handler, responseValidationExemptStatus } = createUseCaseRouteHandler(contract, route);
|
|
1205
|
+
registerRoute(contract, handler, route.hooks, responseValidationExemptStatus);
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
registerRoute(contract, route.handle, route.hooks);
|
|
1209
|
+
}
|
|
914
1210
|
}
|
|
915
1211
|
}
|
|
916
1212
|
catch (error) {
|
|
@@ -929,6 +1225,7 @@ export async function createServer(options) {
|
|
|
929
1225
|
const result = await provider.setup({
|
|
930
1226
|
ports: finalPorts,
|
|
931
1227
|
config: cfg,
|
|
1228
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
932
1229
|
});
|
|
933
1230
|
if (result.ports) {
|
|
934
1231
|
Object.assign(finalPorts, result.ports);
|
|
@@ -940,8 +1237,25 @@ export async function createServer(options) {
|
|
|
940
1237
|
continue;
|
|
941
1238
|
await result.start({
|
|
942
1239
|
ports: finalPorts,
|
|
1240
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
943
1241
|
});
|
|
944
1242
|
}
|
|
1243
|
+
instrumentation.attachPorts(finalPorts);
|
|
1244
|
+
const onUnboundPorts = options.onUnboundPorts ?? "error";
|
|
1245
|
+
if (onUnboundPorts !== "ignore") {
|
|
1246
|
+
const unboundKeys = Object.keys(finalPorts).filter((key) => isUnboundPort(finalPorts[key]));
|
|
1247
|
+
if (unboundKeys.length > 0) {
|
|
1248
|
+
const message = `Unbound ports after provider startup: ${unboundKeys.join(", ")}. ` +
|
|
1249
|
+
"Each port declared as deferred in definePorts(...) must be contributed " +
|
|
1250
|
+
"by a provider (server/providers.ts) or bound in infra/app-ports.ts. " +
|
|
1251
|
+
'Pass onUnboundPorts: "warn" or "ignore" to change this behavior.';
|
|
1252
|
+
if (onUnboundPorts === "error") {
|
|
1253
|
+
throw new Error(message);
|
|
1254
|
+
}
|
|
1255
|
+
console.warn(`[beignet] ${message}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
serviceContextsAvailable = true;
|
|
945
1259
|
}
|
|
946
1260
|
catch (error) {
|
|
947
1261
|
try {
|
|
@@ -952,27 +1266,62 @@ export async function createServer(options) {
|
|
|
952
1266
|
}
|
|
953
1267
|
throw error;
|
|
954
1268
|
}
|
|
1269
|
+
// The fallback 404/405 pipeline is built once at server creation. Only the
|
|
1270
|
+
// contract surface that hooks observe (method, path, and the Allow set for
|
|
1271
|
+
// 405s) is assembled per unmatched request.
|
|
1272
|
+
const executeFallback = createRequestExecutor(options, finalPorts, contextRuntime, hooks, [], {
|
|
1273
|
+
skipRoutePreparation: true,
|
|
1274
|
+
});
|
|
1275
|
+
const fallbackContract = (name, method, path) => ({
|
|
1276
|
+
kind: "http",
|
|
1277
|
+
name,
|
|
1278
|
+
method: method,
|
|
1279
|
+
path,
|
|
1280
|
+
pathParams: null,
|
|
1281
|
+
query: null,
|
|
1282
|
+
body: null,
|
|
1283
|
+
responses: {},
|
|
1284
|
+
metadata: {},
|
|
1285
|
+
});
|
|
1286
|
+
const notFoundHandler = async () => errorResponse(404, "NOT_FOUND", "Not found");
|
|
955
1287
|
const api = async (req) => {
|
|
1288
|
+
if (registryNeedsSort) {
|
|
1289
|
+
registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
|
|
1290
|
+
registryNeedsSort = false;
|
|
1291
|
+
}
|
|
956
1292
|
const url = new URL(req.url);
|
|
1293
|
+
const method = req.method.toUpperCase();
|
|
1294
|
+
let pathMatchedMethods;
|
|
957
1295
|
for (const entry of registry) {
|
|
958
|
-
|
|
959
|
-
|
|
1296
|
+
if (!entry.compiled.pattern.test(url.pathname))
|
|
1297
|
+
continue;
|
|
1298
|
+
if (entry.method === method) {
|
|
960
1299
|
return await entry.handler(req);
|
|
961
1300
|
}
|
|
1301
|
+
if (!pathMatchedMethods) {
|
|
1302
|
+
pathMatchedMethods = new Set();
|
|
1303
|
+
}
|
|
1304
|
+
pathMatchedMethods.add(entry.method);
|
|
962
1305
|
}
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1306
|
+
const pathname = url.pathname || "/";
|
|
1307
|
+
if (pathMatchedMethods) {
|
|
1308
|
+
const allow = [...pathMatchedMethods].sort().join(", ");
|
|
1309
|
+
return await executeFallback({
|
|
1310
|
+
contract: fallbackContract("methodNotAllowed", method, pathname),
|
|
1311
|
+
handler: async () => ({
|
|
1312
|
+
status: 405,
|
|
1313
|
+
headers: { allow },
|
|
1314
|
+
body: createErrorResponseBody({
|
|
1315
|
+
code: "METHOD_NOT_ALLOWED",
|
|
1316
|
+
message: `Method ${method} is not allowed for ${pathname}`,
|
|
1317
|
+
}),
|
|
1318
|
+
}),
|
|
1319
|
+
}, req, {});
|
|
1320
|
+
}
|
|
1321
|
+
return await executeFallback({
|
|
1322
|
+
contract: fallbackContract("notFound", method, pathname),
|
|
1323
|
+
handler: notFoundHandler,
|
|
1324
|
+
}, req, {});
|
|
976
1325
|
};
|
|
977
1326
|
return {
|
|
978
1327
|
api,
|
|
@@ -980,29 +1329,23 @@ export async function createServer(options) {
|
|
|
980
1329
|
const contract = resolveContract(contractLike);
|
|
981
1330
|
return createBuilder(contract, true);
|
|
982
1331
|
},
|
|
1332
|
+
createRequestContext: (req) => createRequestContext(req),
|
|
1333
|
+
createServiceContext,
|
|
983
1334
|
contracts,
|
|
984
1335
|
stop,
|
|
985
1336
|
ports: finalPorts,
|
|
986
1337
|
};
|
|
987
1338
|
}
|
|
988
|
-
/**
|
|
989
|
-
* Define and flatten route registrations with strong type inference.
|
|
990
|
-
*
|
|
991
|
-
* Pass route definitions and route groups here before `createServer(...)`.
|
|
992
|
-
* Group entries are flattened so downstream tooling receives one route list.
|
|
993
|
-
*
|
|
994
|
-
* @example
|
|
995
|
-
* ```ts
|
|
996
|
-
* const routes = defineRoutes<AppContext>([
|
|
997
|
-
* { contract: listPosts, handle: async ({ ctx }) => ctx.posts.list() },
|
|
998
|
-
* ]);
|
|
999
|
-
* ```
|
|
1000
|
-
*/
|
|
1001
1339
|
export function defineRoutes(routes) {
|
|
1002
1340
|
const flattened = [];
|
|
1003
1341
|
for (const route of routes) {
|
|
1004
1342
|
if (isRouteGroup(route)) {
|
|
1005
|
-
|
|
1343
|
+
for (const groupRoute of route.routes) {
|
|
1344
|
+
flattened.push({
|
|
1345
|
+
...groupRoute,
|
|
1346
|
+
hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1006
1349
|
}
|
|
1007
1350
|
else {
|
|
1008
1351
|
flattened.push(route);
|
|
@@ -1019,26 +1362,20 @@ export function defineRoutes(routes) {
|
|
|
1019
1362
|
export function contractsFromRoutes(routes) {
|
|
1020
1363
|
return routes.map((route) => resolveContract(route.contract));
|
|
1021
1364
|
}
|
|
1022
|
-
/**
|
|
1023
|
-
* Define a named group of related route registrations.
|
|
1024
|
-
*
|
|
1025
|
-
* Route groups are flattened by defineRoutes, so createServer still receives
|
|
1026
|
-
* a regular route list while app code can keep feature route wiring colocated.
|
|
1027
|
-
*
|
|
1028
|
-
* @example
|
|
1029
|
-
* ```ts
|
|
1030
|
-
* const todoRoutes = defineRouteGroup<AppContext>({
|
|
1031
|
-
* name: "todos",
|
|
1032
|
-
* routes: [
|
|
1033
|
-
* { contract: listTodos, handle: async ({ ctx }) => ctx.todos.list() },
|
|
1034
|
-
* ]
|
|
1035
|
-
* });
|
|
1036
|
-
* ```
|
|
1037
|
-
*/
|
|
1038
1365
|
export function defineRouteGroup(group) {
|
|
1366
|
+
const createGroup = (input) => ({
|
|
1367
|
+
kind: ROUTE_GROUP_KIND,
|
|
1368
|
+
name: input.name,
|
|
1369
|
+
hooks: input.hooks,
|
|
1370
|
+
routes: input.routes,
|
|
1371
|
+
});
|
|
1372
|
+
if (!group) {
|
|
1373
|
+
return createGroup;
|
|
1374
|
+
}
|
|
1039
1375
|
return {
|
|
1040
1376
|
kind: ROUTE_GROUP_KIND,
|
|
1041
1377
|
name: group.name,
|
|
1378
|
+
hooks: group.hooks,
|
|
1042
1379
|
routes: group.routes,
|
|
1043
1380
|
};
|
|
1044
1381
|
}
|