@beignet/core 0.0.3 → 0.0.5
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 +159 -0
- package/README.md +792 -50
- 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 +12 -14
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +13 -13
- 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 +15 -6
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +60 -16
- 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 +45 -4
- 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 +49 -10
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +77 -37
- 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 +61 -35
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +1 -20
- package/dist/server/http.js.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 +105 -33
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +434 -118
- 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 +607 -5
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +426 -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 +1 -1
- package/dist/uploads/client.d.ts.map +1 -1
- package/dist/uploads/index.d.ts +2 -2
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +1 -1
- package/dist/uploads/index.js.map +1 -1
- package/package.json +24 -2
- 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 +14 -24
- 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 +84 -19
- 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 +86 -7
- 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 +141 -51
- 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 +14 -7
- 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 +78 -51
- package/src/server/index.ts +62 -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 +886 -238
- 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 +1142 -6
- package/src/tracing/index.ts +176 -0
- package/src/uploads/client.ts +1 -1
- package/src/uploads/index.ts +7 -3
- 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,9 +1,15 @@
|
|
|
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";
|
|
8
14
|
/**
|
|
9
15
|
* Define one route registration with hook-aware handler typing.
|
|
@@ -13,7 +19,10 @@ const ROUTE_GROUP_KIND = "beignet.route-group";
|
|
|
13
19
|
* added fields.
|
|
14
20
|
*/
|
|
15
21
|
export function defineRoute() {
|
|
16
|
-
|
|
22
|
+
function define(route) {
|
|
23
|
+
return route;
|
|
24
|
+
}
|
|
25
|
+
return define;
|
|
17
26
|
}
|
|
18
27
|
class PathDecodeError extends Error {
|
|
19
28
|
constructor() {
|
|
@@ -65,6 +74,35 @@ function errorResponse(status, code, message, details) {
|
|
|
65
74
|
body: createErrorResponseBody({ code, message, details }),
|
|
66
75
|
};
|
|
67
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
|
+
}
|
|
68
106
|
function normalizeResponse(res) {
|
|
69
107
|
return {
|
|
70
108
|
status: res.status,
|
|
@@ -136,6 +174,42 @@ function responseForHooks(res) {
|
|
|
136
174
|
headers: headersToRecord(res.headers),
|
|
137
175
|
};
|
|
138
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
|
+
}
|
|
139
213
|
function isHttpResponseLike(value) {
|
|
140
214
|
return (!isWebResponse(value) &&
|
|
141
215
|
typeof value === "object" &&
|
|
@@ -153,6 +227,25 @@ class ResponseContractViolationError extends Error {
|
|
|
153
227
|
this.details = args.details;
|
|
154
228
|
}
|
|
155
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
|
+
}
|
|
156
249
|
function getDeclaredCatalogErrorsForStatus(contract, status) {
|
|
157
250
|
const errors = contract.metadata?.errors;
|
|
158
251
|
if (typeof errors !== "object" || errors === null)
|
|
@@ -175,16 +268,15 @@ async function validateCatalogErrorResponse(contract, res) {
|
|
|
175
268
|
if (!matchingError) {
|
|
176
269
|
throw new ResponseContractViolationError({
|
|
177
270
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
178
|
-
message:
|
|
179
|
-
|
|
180
|
-
details: {
|
|
271
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
272
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
181
273
|
issues: [
|
|
182
274
|
{
|
|
183
275
|
message: `Error response code "${body.code}" is not declared for status ${res.status}. ` +
|
|
184
276
|
`Expected one of: ${declaredErrors.map((error) => error.code).join(", ")}.`,
|
|
185
277
|
},
|
|
186
278
|
],
|
|
187
|
-
},
|
|
279
|
+
}),
|
|
188
280
|
});
|
|
189
281
|
}
|
|
190
282
|
if (matchingError.details && body.details !== undefined) {
|
|
@@ -195,16 +287,17 @@ async function validateCatalogErrorResponse(contract, res) {
|
|
|
195
287
|
if (error instanceof SchemaValidationError) {
|
|
196
288
|
throw new ResponseContractViolationError({
|
|
197
289
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
198
|
-
message:
|
|
199
|
-
|
|
200
|
-
|
|
290
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
291
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
292
|
+
issues: error.issues,
|
|
293
|
+
}),
|
|
201
294
|
});
|
|
202
295
|
}
|
|
203
296
|
throw error;
|
|
204
297
|
}
|
|
205
298
|
}
|
|
206
299
|
}
|
|
207
|
-
async function validateResponseAgainstContract(contract, res) {
|
|
300
|
+
async function validateResponseAgainstContract(contract, res, responseValidationExemptStatus) {
|
|
208
301
|
const statusKey = String(res.status);
|
|
209
302
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
210
303
|
if (!hasDeclaredStatus) {
|
|
@@ -214,10 +307,9 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
214
307
|
code: "UNDECLARED_RESPONSE_STATUS",
|
|
215
308
|
message: `Handler returned undeclared status ${res.status} for ` +
|
|
216
309
|
`${contract.method} ${contract.path} (contract: ${contract.name})`,
|
|
217
|
-
details: {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
},
|
|
310
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
311
|
+
returnedStatus: res.status,
|
|
312
|
+
}),
|
|
221
313
|
});
|
|
222
314
|
}
|
|
223
315
|
const responseSchema = contract.responses[res.status];
|
|
@@ -225,21 +317,25 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
225
317
|
if (res.body !== undefined && res.body !== null) {
|
|
226
318
|
throw new ResponseContractViolationError({
|
|
227
319
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
228
|
-
message:
|
|
229
|
-
|
|
230
|
-
details: {
|
|
320
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
321
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
231
322
|
issues: [
|
|
232
323
|
{
|
|
233
324
|
message: "Response body must be empty for a null response schema.",
|
|
234
325
|
},
|
|
235
326
|
],
|
|
236
|
-
},
|
|
327
|
+
}),
|
|
237
328
|
});
|
|
238
329
|
}
|
|
239
330
|
return;
|
|
240
331
|
}
|
|
241
332
|
if (!responseSchema)
|
|
242
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;
|
|
243
339
|
try {
|
|
244
340
|
await parseStandardSchema(responseSchema, res.body);
|
|
245
341
|
await validateCatalogErrorResponse(contract, res);
|
|
@@ -248,17 +344,18 @@ async function validateResponseAgainstContract(contract, res) {
|
|
|
248
344
|
if (error instanceof SchemaValidationError) {
|
|
249
345
|
throw new ResponseContractViolationError({
|
|
250
346
|
code: "RESPONSE_VALIDATION_ERROR",
|
|
251
|
-
message:
|
|
252
|
-
|
|
253
|
-
|
|
347
|
+
message: responseContractViolationMessage(contract, res.status),
|
|
348
|
+
details: responseContractViolationDetails(contract, res.status, {
|
|
349
|
+
issues: error.issues,
|
|
350
|
+
}),
|
|
254
351
|
});
|
|
255
352
|
}
|
|
256
353
|
throw error;
|
|
257
354
|
}
|
|
258
355
|
}
|
|
259
|
-
async function finalizeResponse(contract, res) {
|
|
356
|
+
async function finalizeResponse(contract, res, responseValidationExemptStatus) {
|
|
260
357
|
const normalized = normalizeResponse(res);
|
|
261
|
-
await validateResponseAgainstContract(contract, normalized);
|
|
358
|
+
await validateResponseAgainstContract(contract, normalized, responseValidationExemptStatus);
|
|
262
359
|
return normalized;
|
|
263
360
|
}
|
|
264
361
|
function toContractViolationResponse(error) {
|
|
@@ -311,11 +408,19 @@ async function parseBody(req) {
|
|
|
311
408
|
return undefined;
|
|
312
409
|
}
|
|
313
410
|
}
|
|
314
|
-
|
|
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(
|
|
315
419
|
// biome-ignore lint/suspicious/noExplicitAny: Options are generic and need to work with any routes
|
|
316
|
-
options, finalPorts,
|
|
317
|
-
const
|
|
318
|
-
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;
|
|
319
424
|
let baseCtx;
|
|
320
425
|
let pathValue;
|
|
321
426
|
let queryValue;
|
|
@@ -382,6 +487,36 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
382
487
|
owner: "framework",
|
|
383
488
|
};
|
|
384
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
|
+
}
|
|
385
520
|
if (currentError instanceof GateAuthorizationError) {
|
|
386
521
|
return {
|
|
387
522
|
ctx,
|
|
@@ -460,8 +595,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
460
595
|
matchedParams = preMatchedParams;
|
|
461
596
|
}
|
|
462
597
|
else {
|
|
463
|
-
const
|
|
464
|
-
|
|
598
|
+
const compiled = target.compiled;
|
|
599
|
+
const match = compiled ? compiled.pattern.exec(url.pathname) : null;
|
|
600
|
+
if (!compiled ||
|
|
601
|
+
!match ||
|
|
465
602
|
contract.method.toUpperCase() !== req.method.toUpperCase()) {
|
|
466
603
|
return errorResponse(404, "NOT_FOUND", "Not found");
|
|
467
604
|
}
|
|
@@ -476,11 +613,49 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
476
613
|
}
|
|
477
614
|
}
|
|
478
615
|
const rawHeaders = requestHeadersToRecord(req.headers);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
}
|
|
482
648
|
}
|
|
649
|
+
return mergeNativeResponseHeaders(nativeResponse, originalHeaders, transformed.headers ?? {});
|
|
650
|
+
};
|
|
651
|
+
const applyTransformHooks = async (initialResult, allowRetry) => {
|
|
483
652
|
try {
|
|
653
|
+
if (isWebResponse(initialResult.response)) {
|
|
654
|
+
return {
|
|
655
|
+
...initialResult,
|
|
656
|
+
response: await runNativeBeforeSend(initialResult, initialResult.response),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
484
659
|
let transformed = normalizeResponse(initialResult.response);
|
|
485
660
|
for (const hook of hooks) {
|
|
486
661
|
if (!hook.beforeSend)
|
|
@@ -542,11 +717,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
542
717
|
if (optionsOverrides?.skipRoutePreparation) {
|
|
543
718
|
let createdCtx;
|
|
544
719
|
try {
|
|
545
|
-
createdCtx = await
|
|
546
|
-
req,
|
|
547
|
-
ports: finalPorts,
|
|
548
|
-
contract,
|
|
549
|
-
});
|
|
720
|
+
createdCtx = await contextRuntime.createRequestContext(req, contract);
|
|
550
721
|
baseCtx = createdCtx;
|
|
551
722
|
}
|
|
552
723
|
catch (error) {
|
|
@@ -586,16 +757,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
586
757
|
query = await parseStandardSchema(contract.query, query);
|
|
587
758
|
}
|
|
588
759
|
catch (error) {
|
|
589
|
-
result =
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
owner: "framework",
|
|
594
|
-
}
|
|
595
|
-
: {
|
|
596
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid query parameters", error.message),
|
|
597
|
-
owner: "framework",
|
|
598
|
-
};
|
|
760
|
+
result = {
|
|
761
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid query parameters", "query", error),
|
|
762
|
+
owner: "framework",
|
|
763
|
+
};
|
|
599
764
|
}
|
|
600
765
|
}
|
|
601
766
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -605,16 +770,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
605
770
|
path = await parseStandardSchema(contract.pathParams, matchedParams);
|
|
606
771
|
}
|
|
607
772
|
catch (error) {
|
|
608
|
-
result =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
owner: "framework",
|
|
613
|
-
}
|
|
614
|
-
: {
|
|
615
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid path parameters", error.message),
|
|
616
|
-
owner: "framework",
|
|
617
|
-
};
|
|
773
|
+
result = {
|
|
774
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid path parameters", "path", error),
|
|
775
|
+
owner: "framework",
|
|
776
|
+
};
|
|
618
777
|
}
|
|
619
778
|
}
|
|
620
779
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -625,16 +784,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
625
784
|
headers = await parseHeaderSchemas(headerSchemas, rawHeaders);
|
|
626
785
|
}
|
|
627
786
|
catch (error) {
|
|
628
|
-
result =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
owner: "framework",
|
|
633
|
-
}
|
|
634
|
-
: {
|
|
635
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request headers", error.message),
|
|
636
|
-
owner: "framework",
|
|
637
|
-
};
|
|
787
|
+
result = {
|
|
788
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request headers", "headers", error),
|
|
789
|
+
owner: "framework",
|
|
790
|
+
};
|
|
638
791
|
}
|
|
639
792
|
}
|
|
640
793
|
// biome-ignore lint/suspicious/noExplicitAny: Type will be narrowed by schema validation
|
|
@@ -643,9 +796,9 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
643
796
|
try {
|
|
644
797
|
body = await parseBody(req);
|
|
645
798
|
}
|
|
646
|
-
catch {
|
|
799
|
+
catch (error) {
|
|
647
800
|
result = {
|
|
648
|
-
response:
|
|
801
|
+
response: requestValidationError(contract, 400, "INVALID_BODY", "Malformed JSON", "body", error),
|
|
649
802
|
owner: "framework",
|
|
650
803
|
};
|
|
651
804
|
}
|
|
@@ -658,21 +811,15 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
658
811
|
if (body === undefined &&
|
|
659
812
|
error instanceof SchemaValidationError) {
|
|
660
813
|
result = {
|
|
661
|
-
response:
|
|
814
|
+
response: requestValidationError(contract, 400, "MISSING_BODY", "Request body is required", "body", error),
|
|
662
815
|
owner: "framework",
|
|
663
816
|
};
|
|
664
817
|
}
|
|
665
818
|
else {
|
|
666
|
-
result =
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
owner: "framework",
|
|
671
|
-
}
|
|
672
|
-
: {
|
|
673
|
-
response: errorResponse(422, "VALIDATION_ERROR", "Invalid request body", error.message),
|
|
674
|
-
owner: "framework",
|
|
675
|
-
};
|
|
819
|
+
result = {
|
|
820
|
+
response: requestValidationError(contract, 422, "VALIDATION_ERROR", "Invalid request body", "body", error),
|
|
821
|
+
owner: "framework",
|
|
822
|
+
};
|
|
676
823
|
}
|
|
677
824
|
}
|
|
678
825
|
}
|
|
@@ -683,11 +830,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
683
830
|
bodyValue = body;
|
|
684
831
|
let createdCtx;
|
|
685
832
|
try {
|
|
686
|
-
createdCtx = await
|
|
687
|
-
req,
|
|
688
|
-
ports: finalPorts,
|
|
689
|
-
contract,
|
|
690
|
-
});
|
|
833
|
+
createdCtx = await contextRuntime.createRequestContext(req, contract);
|
|
691
834
|
baseCtx = createdCtx;
|
|
692
835
|
}
|
|
693
836
|
catch (error) {
|
|
@@ -734,7 +877,7 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
734
877
|
break;
|
|
735
878
|
}
|
|
736
879
|
if (hookResult?.ctx !== undefined) {
|
|
737
|
-
currentCtx = hookResult.ctx;
|
|
880
|
+
currentCtx = contextRuntime.finalizeContext(hookResult.ctx);
|
|
738
881
|
}
|
|
739
882
|
if (hookResult?.response) {
|
|
740
883
|
const response = normalizeHttpResponse(hookResult.response);
|
|
@@ -764,10 +907,10 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
764
907
|
body,
|
|
765
908
|
});
|
|
766
909
|
if (additions && typeof additions === "object") {
|
|
767
|
-
currentCtx = {
|
|
910
|
+
currentCtx = contextRuntime.finalizeContext({
|
|
768
911
|
...currentCtx,
|
|
769
912
|
...additions,
|
|
770
|
-
};
|
|
913
|
+
});
|
|
771
914
|
}
|
|
772
915
|
}
|
|
773
916
|
catch (error) {
|
|
@@ -777,6 +920,14 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
777
920
|
}
|
|
778
921
|
}
|
|
779
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
|
+
});
|
|
780
931
|
try {
|
|
781
932
|
result = {
|
|
782
933
|
ctx: currentCtx,
|
|
@@ -795,9 +946,11 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
795
946
|
let finalResponse = normalizeHttpResponse(result.response);
|
|
796
947
|
let finalError = result.error;
|
|
797
948
|
let finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
798
|
-
if (finalOwner === "route" &&
|
|
949
|
+
if (finalOwner === "route" &&
|
|
950
|
+
!isWebResponse(finalResponse) &&
|
|
951
|
+
(options.validateResponses ?? true)) {
|
|
799
952
|
try {
|
|
800
|
-
finalResponse = await finalizeResponse(contract, finalResponse);
|
|
953
|
+
finalResponse = await finalizeResponse(contract, finalResponse, target.responseValidationExemptStatus);
|
|
801
954
|
}
|
|
802
955
|
catch (error) {
|
|
803
956
|
if (error instanceof ResponseContractViolationError) {
|
|
@@ -855,6 +1008,18 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
855
1008
|
}
|
|
856
1009
|
};
|
|
857
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
|
+
}
|
|
858
1023
|
/**
|
|
859
1024
|
* Create a Beignet server instance.
|
|
860
1025
|
*
|
|
@@ -863,19 +1028,80 @@ options, finalPorts, contract, userHandler, hooks, routeHooks = [], optionsOverr
|
|
|
863
1028
|
* Use adapter packages such as `@beignet/next` to expose `server.api` to a
|
|
864
1029
|
* specific runtime.
|
|
865
1030
|
*
|
|
866
|
-
* @param options - Ports, providers, routes, hooks, context
|
|
867
|
-
* mapping hooks for the server.
|
|
1031
|
+
* @param options - Ports, providers, routes, hooks, context blueprint, and
|
|
1032
|
+
* error mapping hooks for the server.
|
|
868
1033
|
* @returns A started server instance with final ports and a catch-all handler.
|
|
869
1034
|
*/
|
|
870
1035
|
export async function createServer(options) {
|
|
871
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;
|
|
872
1041
|
const providers = (options.providers ?? []);
|
|
873
1042
|
const env = options.providerEnv ?? process.env;
|
|
874
1043
|
const overrides = options.providerConfig ?? {};
|
|
875
1044
|
const providerResults = [];
|
|
876
1045
|
const finalPorts = { ...options.ports };
|
|
877
|
-
const
|
|
1046
|
+
const instrumentation = createServerInstrumentation(options.instrumentation);
|
|
1047
|
+
const hooks = [
|
|
1048
|
+
...(instrumentation.hook
|
|
1049
|
+
? [instrumentation.hook]
|
|
1050
|
+
: []),
|
|
1051
|
+
...(options.hooks ?? []),
|
|
1052
|
+
];
|
|
878
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
|
+
};
|
|
879
1105
|
let stopped = false;
|
|
880
1106
|
const stop = async () => {
|
|
881
1107
|
if (stopped)
|
|
@@ -887,6 +1113,7 @@ export async function createServer(options) {
|
|
|
887
1113
|
try {
|
|
888
1114
|
await result?.stop?.({
|
|
889
1115
|
ports: finalPorts,
|
|
1116
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
890
1117
|
});
|
|
891
1118
|
}
|
|
892
1119
|
catch (err) {
|
|
@@ -899,7 +1126,8 @@ export async function createServer(options) {
|
|
|
899
1126
|
};
|
|
900
1127
|
const registeredPaths = new Set();
|
|
901
1128
|
const registeredShapes = new Map();
|
|
902
|
-
const
|
|
1129
|
+
const registeredNames = new Map();
|
|
1130
|
+
const registerRoute = (contract, handler, routeHooks = [], responseValidationExemptStatus) => {
|
|
903
1131
|
if (contract.body && !methodSupportsRequestBody(contract.method)) {
|
|
904
1132
|
throw new Error(`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`);
|
|
905
1133
|
}
|
|
@@ -914,12 +1142,31 @@ export async function createServer(options) {
|
|
|
914
1142
|
if (conflictingRoute) {
|
|
915
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.`);
|
|
916
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
|
+
}
|
|
917
1162
|
registeredPaths.add(routeKey);
|
|
918
1163
|
registeredShapes.set(shapeRouteKey, routeKey);
|
|
919
|
-
|
|
1164
|
+
registeredNames.set(contract.name, routeKey);
|
|
1165
|
+
const builtHandler = buildHandler(options, finalPorts, contextRuntime, contract, handler, hooks, routeHooks, undefined, responseValidationExemptStatus);
|
|
920
1166
|
registry.push({
|
|
921
1167
|
contract,
|
|
922
1168
|
compiled,
|
|
1169
|
+
method: contract.method.toUpperCase(),
|
|
923
1170
|
handler: builtHandler,
|
|
924
1171
|
match: (method, pathname) => {
|
|
925
1172
|
if (contract.method.toUpperCase() !== method.toUpperCase()) {
|
|
@@ -931,11 +1178,11 @@ export async function createServer(options) {
|
|
|
931
1178
|
return { matched: true };
|
|
932
1179
|
},
|
|
933
1180
|
});
|
|
934
|
-
|
|
1181
|
+
registryNeedsSort = true;
|
|
935
1182
|
};
|
|
936
1183
|
const createBuilder = (contract, shouldRegister) => ({
|
|
937
1184
|
handle: (fn) => {
|
|
938
|
-
const wrapped = buildHandler(options, finalPorts, contract, fn, hooks);
|
|
1185
|
+
const wrapped = buildHandler(options, finalPorts, contextRuntime, contract, fn, hooks);
|
|
939
1186
|
if (shouldRegister)
|
|
940
1187
|
registerRoute(contract, fn);
|
|
941
1188
|
return wrapped;
|
|
@@ -945,7 +1192,21 @@ export async function createServer(options) {
|
|
|
945
1192
|
try {
|
|
946
1193
|
for (const route of options.routes) {
|
|
947
1194
|
const contract = resolveContract(route.contract);
|
|
948
|
-
|
|
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
|
+
}
|
|
949
1210
|
}
|
|
950
1211
|
}
|
|
951
1212
|
catch (error) {
|
|
@@ -964,6 +1225,7 @@ export async function createServer(options) {
|
|
|
964
1225
|
const result = await provider.setup({
|
|
965
1226
|
ports: finalPorts,
|
|
966
1227
|
config: cfg,
|
|
1228
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
967
1229
|
});
|
|
968
1230
|
if (result.ports) {
|
|
969
1231
|
Object.assign(finalPorts, result.ports);
|
|
@@ -975,8 +1237,25 @@ export async function createServer(options) {
|
|
|
975
1237
|
continue;
|
|
976
1238
|
await result.start({
|
|
977
1239
|
ports: finalPorts,
|
|
1240
|
+
createServiceContext: lifecycleCreateServiceContext,
|
|
978
1241
|
});
|
|
979
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;
|
|
980
1259
|
}
|
|
981
1260
|
catch (error) {
|
|
982
1261
|
try {
|
|
@@ -987,27 +1266,62 @@ export async function createServer(options) {
|
|
|
987
1266
|
}
|
|
988
1267
|
throw error;
|
|
989
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");
|
|
990
1287
|
const api = async (req) => {
|
|
1288
|
+
if (registryNeedsSort) {
|
|
1289
|
+
registry.sort((a, b) => compareRouteSpecificity(a.compiled, b.compiled));
|
|
1290
|
+
registryNeedsSort = false;
|
|
1291
|
+
}
|
|
991
1292
|
const url = new URL(req.url);
|
|
1293
|
+
const method = req.method.toUpperCase();
|
|
1294
|
+
let pathMatchedMethods;
|
|
992
1295
|
for (const entry of registry) {
|
|
993
|
-
|
|
994
|
-
|
|
1296
|
+
if (!entry.compiled.pattern.test(url.pathname))
|
|
1297
|
+
continue;
|
|
1298
|
+
if (entry.method === method) {
|
|
995
1299
|
return await entry.handler(req);
|
|
996
1300
|
}
|
|
1301
|
+
if (!pathMatchedMethods) {
|
|
1302
|
+
pathMatchedMethods = new Set();
|
|
1303
|
+
}
|
|
1304
|
+
pathMatchedMethods.add(entry.method);
|
|
997
1305
|
}
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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, {});
|
|
1011
1325
|
};
|
|
1012
1326
|
return {
|
|
1013
1327
|
api,
|
|
@@ -1015,6 +1329,8 @@ export async function createServer(options) {
|
|
|
1015
1329
|
const contract = resolveContract(contractLike);
|
|
1016
1330
|
return createBuilder(contract, true);
|
|
1017
1331
|
},
|
|
1332
|
+
createRequestContext: (req) => createRequestContext(req),
|
|
1333
|
+
createServiceContext,
|
|
1018
1334
|
contracts,
|
|
1019
1335
|
stop,
|
|
1020
1336
|
ports: finalPorts,
|