@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency hooks for @beignet/core/server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AppError, httpErrors } from "../../errors/index.js";
|
|
6
|
+
import {
|
|
7
|
+
createIdempotencyFingerprint,
|
|
8
|
+
IdempotencyConflictError,
|
|
9
|
+
IdempotencyInProgressError,
|
|
10
|
+
type IdempotencyMeta,
|
|
11
|
+
type IdempotencyPort,
|
|
12
|
+
type IdempotencyScope,
|
|
13
|
+
} from "../../idempotency/index.js";
|
|
14
|
+
import type { ActivityActor, ActivityTenant } from "../../ports/index.js";
|
|
15
|
+
import type {
|
|
16
|
+
HttpRequestLike,
|
|
17
|
+
HttpResponseLike,
|
|
18
|
+
ServerHook,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ports required by idempotency hooks.
|
|
23
|
+
*/
|
|
24
|
+
export type IdempotencyPorts = {
|
|
25
|
+
idempotency: IdempotencyPort;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimal context shape required for actor- and tenant-scoped idempotency.
|
|
30
|
+
*/
|
|
31
|
+
export type CtxWithIdempotency = {
|
|
32
|
+
ports: IdempotencyPorts;
|
|
33
|
+
actor?: ActivityActor;
|
|
34
|
+
tenant?: ActivityTenant;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for `createIdempotencyHooks(...)`.
|
|
39
|
+
*/
|
|
40
|
+
export interface IdempotencyHooksOptions<Ctx> {
|
|
41
|
+
/**
|
|
42
|
+
* Build the idempotency namespace for a contract.
|
|
43
|
+
*
|
|
44
|
+
* Defaults to `http.<contract name>` so HTTP reservations never collide with
|
|
45
|
+
* use-case `runIdempotently(...)` namespaces.
|
|
46
|
+
*/
|
|
47
|
+
namespace?: (args: { contract: { name: string } }) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Build the idempotency scope after context exists.
|
|
50
|
+
*
|
|
51
|
+
* Defaults to a scope derived from `meta.scope`: `"global"` stays global,
|
|
52
|
+
* `"actor"` scopes by `ctx.actor?.id`, `"tenant"` scopes by `ctx.tenant?.id`,
|
|
53
|
+
* and `"actor-tenant"` scopes by both.
|
|
54
|
+
*/
|
|
55
|
+
scope?: (args: {
|
|
56
|
+
ctx: Ctx;
|
|
57
|
+
req: HttpRequestLike;
|
|
58
|
+
meta: IdempotencyMeta;
|
|
59
|
+
}) => IdempotencyScope;
|
|
60
|
+
/**
|
|
61
|
+
* Build the fingerprint input from the parsed request.
|
|
62
|
+
*
|
|
63
|
+
* Defaults to `{ path, query, body }`.
|
|
64
|
+
*/
|
|
65
|
+
fingerprintInput?: (args: {
|
|
66
|
+
path: unknown;
|
|
67
|
+
query: unknown;
|
|
68
|
+
body: unknown;
|
|
69
|
+
}) => unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Header set on replayed responses.
|
|
74
|
+
*/
|
|
75
|
+
const IDEMPOTENCY_REPLAYED_HEADER = "idempotency-replayed";
|
|
76
|
+
|
|
77
|
+
type PendingReservation = {
|
|
78
|
+
port: IdempotencyPort;
|
|
79
|
+
namespace: string;
|
|
80
|
+
key: string;
|
|
81
|
+
scope: IdempotencyScope;
|
|
82
|
+
fingerprint: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function defaultIdempotencyScope(
|
|
86
|
+
ctx: CtxWithIdempotency,
|
|
87
|
+
meta: IdempotencyMeta,
|
|
88
|
+
): IdempotencyScope {
|
|
89
|
+
const mode = meta.scope ?? "global";
|
|
90
|
+
|
|
91
|
+
switch (mode) {
|
|
92
|
+
case "global":
|
|
93
|
+
return "global";
|
|
94
|
+
case "actor":
|
|
95
|
+
return { actorId: ctx.actor?.id };
|
|
96
|
+
case "tenant":
|
|
97
|
+
return { tenantId: ctx.tenant?.id };
|
|
98
|
+
case "actor-tenant":
|
|
99
|
+
return { actorId: ctx.actor?.id, tenantId: ctx.tenant?.id };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isReplayableHttpResponse(value: unknown): value is HttpResponseLike {
|
|
104
|
+
if (typeof value !== "object" || value === null) return false;
|
|
105
|
+
|
|
106
|
+
const candidate = value as { status?: unknown; headers?: unknown };
|
|
107
|
+
if (typeof candidate.status !== "number") return false;
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
candidate.headers !== undefined &&
|
|
111
|
+
(typeof candidate.headers !== "object" ||
|
|
112
|
+
candidate.headers === null ||
|
|
113
|
+
Array.isArray(candidate.headers))
|
|
114
|
+
) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create metadata-driven idempotency hooks.
|
|
123
|
+
*
|
|
124
|
+
* The hook reads `contract.metadata.idempotency` and enforces it with
|
|
125
|
+
* `ctx.ports.idempotency`. In `beforeHandle` it reserves the client key after
|
|
126
|
+
* request parsing, replays completed matching responses with an
|
|
127
|
+
* `idempotency-replayed: true` header, and rejects in-progress or conflicting
|
|
128
|
+
* keys with the framework `IdempotencyInProgress`/`IdempotencyConflict` catalog
|
|
129
|
+
* errors. In `beforeSend` it stores 2xx framework-neutral responses for replay
|
|
130
|
+
* and releases the reservation for errors, non-2xx responses, and native
|
|
131
|
+
* `Response` results, which are not replayable.
|
|
132
|
+
*
|
|
133
|
+
* Use `runIdempotently(...)` from `@beignet/core/idempotency` for non-HTTP
|
|
134
|
+
* workflows such as jobs, listeners, webhooks, and schedules.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Optional namespace, scope, and fingerprint-input builders.
|
|
137
|
+
* @returns A server hook backed by `ctx.ports.idempotency`.
|
|
138
|
+
*/
|
|
139
|
+
export function createIdempotencyHooks<Ctx extends CtxWithIdempotency>(
|
|
140
|
+
options: IdempotencyHooksOptions<Ctx> = {},
|
|
141
|
+
): ServerHook<Ctx, IdempotencyPorts> {
|
|
142
|
+
const pending = new WeakMap<HttpRequestLike, PendingReservation>();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name: "idempotency",
|
|
146
|
+
beforeHandle: async ({ ctx, contract, req, path, query, body }) => {
|
|
147
|
+
const meta = contract.metadata?.idempotency;
|
|
148
|
+
if (!meta) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const header = (meta.header ?? "idempotency-key").toLowerCase();
|
|
153
|
+
const key = req.headers.get(header);
|
|
154
|
+
|
|
155
|
+
if (!key) {
|
|
156
|
+
if (meta.required) {
|
|
157
|
+
throw new AppError(
|
|
158
|
+
httpErrors.BadRequest,
|
|
159
|
+
{
|
|
160
|
+
contract: contract.name,
|
|
161
|
+
header,
|
|
162
|
+
},
|
|
163
|
+
`Missing required idempotency key header "${header}"`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const namespace =
|
|
170
|
+
options.namespace?.({ contract: { name: contract.name } }) ??
|
|
171
|
+
`http.${contract.name}`;
|
|
172
|
+
const scope =
|
|
173
|
+
options.scope?.({ ctx, req, meta }) ??
|
|
174
|
+
defaultIdempotencyScope(ctx, meta);
|
|
175
|
+
const fingerprint = await createIdempotencyFingerprint(
|
|
176
|
+
options.fingerprintInput?.({ path, query, body }) ?? {
|
|
177
|
+
path,
|
|
178
|
+
query,
|
|
179
|
+
body,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const reservation = await ctx.ports.idempotency.reserve({
|
|
184
|
+
namespace,
|
|
185
|
+
key,
|
|
186
|
+
scope,
|
|
187
|
+
fingerprint,
|
|
188
|
+
ttlSec: meta.ttlSec,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
switch (reservation.status) {
|
|
192
|
+
case "replay": {
|
|
193
|
+
if (!isReplayableHttpResponse(reservation.result)) {
|
|
194
|
+
throw new AppError(
|
|
195
|
+
httpErrors.InternalServerError,
|
|
196
|
+
{ namespace, key },
|
|
197
|
+
`Stored idempotency result for "${namespace}" key "${key}" is not a replayable HTTP response`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
status: reservation.result.status,
|
|
203
|
+
headers: {
|
|
204
|
+
...(reservation.result.headers ?? {}),
|
|
205
|
+
[IDEMPOTENCY_REPLAYED_HEADER]: "true",
|
|
206
|
+
},
|
|
207
|
+
body: reservation.result.body,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "inProgress": {
|
|
211
|
+
throw new IdempotencyInProgressError(reservation);
|
|
212
|
+
}
|
|
213
|
+
case "conflict": {
|
|
214
|
+
throw new IdempotencyConflictError(reservation);
|
|
215
|
+
}
|
|
216
|
+
case "reserved": {
|
|
217
|
+
pending.set(req, {
|
|
218
|
+
port: ctx.ports.idempotency,
|
|
219
|
+
namespace,
|
|
220
|
+
key,
|
|
221
|
+
scope,
|
|
222
|
+
fingerprint,
|
|
223
|
+
});
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
beforeSend: async ({ req, response, error, native }) => {
|
|
229
|
+
const reservation = pending.get(req);
|
|
230
|
+
if (!reservation) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
pending.delete(req);
|
|
234
|
+
|
|
235
|
+
const { port, namespace, key, scope, fingerprint } = reservation;
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
!native &&
|
|
239
|
+
!error &&
|
|
240
|
+
response.status >= 200 &&
|
|
241
|
+
response.status < 300
|
|
242
|
+
) {
|
|
243
|
+
await port.complete({
|
|
244
|
+
namespace,
|
|
245
|
+
key,
|
|
246
|
+
scope,
|
|
247
|
+
fingerprint,
|
|
248
|
+
result: {
|
|
249
|
+
status: response.status,
|
|
250
|
+
headers: response.headers,
|
|
251
|
+
body: response.body,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Errors, non-2xx responses, and native `Response` results release the
|
|
258
|
+
// reservation. Streams are not replayable.
|
|
259
|
+
await port.fail({ namespace, key, scope, fingerprint, error });
|
|
260
|
+
return undefined;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -2,39 +2,42 @@
|
|
|
2
2
|
* Hook utilities for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { AnyPorts } from "../../ports";
|
|
6
|
-
import type { ServerHook } from "../http";
|
|
5
|
+
import type { AnyPorts } from "../../ports/index.js";
|
|
6
|
+
import type { ServerHook } from "../http.js";
|
|
7
7
|
|
|
8
8
|
export {
|
|
9
9
|
type AuthHookArgs,
|
|
10
|
-
type AuthHookAssignArgs,
|
|
11
|
-
type AuthHookMode,
|
|
12
|
-
type AuthHookModeInput,
|
|
13
10
|
type AuthHooksOptions,
|
|
14
|
-
type
|
|
15
|
-
type CtxWithAuthPort,
|
|
11
|
+
type AuthRouteHooks,
|
|
16
12
|
createAuthHooks,
|
|
17
|
-
} from "./auth";
|
|
13
|
+
} from "./auth.js";
|
|
18
14
|
export {
|
|
19
15
|
applyCorsHeaders,
|
|
20
16
|
type CorsConfig,
|
|
21
17
|
createCorsHooks,
|
|
22
|
-
} from "./cors";
|
|
18
|
+
} from "./cors.js";
|
|
23
19
|
export {
|
|
24
20
|
defaultMapErrorToResponse,
|
|
25
21
|
type ErrorMappingConfig,
|
|
26
22
|
type ErrorMappingResult,
|
|
27
|
-
} from "./errors";
|
|
23
|
+
} from "./errors.js";
|
|
24
|
+
export {
|
|
25
|
+
type CtxWithIdempotency,
|
|
26
|
+
createIdempotencyHooks,
|
|
27
|
+
type IdempotencyHooksOptions,
|
|
28
|
+
type IdempotencyPorts,
|
|
29
|
+
} from "./idempotency.js";
|
|
28
30
|
export {
|
|
29
31
|
createLoggingHooks,
|
|
30
32
|
type Logger,
|
|
31
33
|
type LoggingConfig,
|
|
32
|
-
} from "./logging";
|
|
34
|
+
} from "./logging.js";
|
|
33
35
|
export {
|
|
34
36
|
type CtxWithRateLimit,
|
|
35
37
|
createRateLimitHooks,
|
|
38
|
+
type RateLimitIpSource,
|
|
36
39
|
type RateLimitOptions,
|
|
37
|
-
} from "./rate-limit";
|
|
40
|
+
} from "./rate-limit.js";
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Flatten hook arrays into a single hook list.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Logging hook utilities for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { HttpContractConfig } from "../../contracts";
|
|
6
|
-
import type { HttpRequestLike, ServerHook } from "../types";
|
|
7
|
-
import { getRequestIdFromContext } from "./utils";
|
|
5
|
+
import type { HttpContractConfig } from "../../contracts/index.js";
|
|
6
|
+
import type { HttpRequestLike, ServerHook } from "../types.js";
|
|
7
|
+
import { getRequestIdFromContext } from "./utils.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Minimal logger shape accepted by `createLoggingHooks(...)`.
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Rate limit hooks for @beignet/core/server
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { RateLimitScope } from "../../contracts";
|
|
6
|
-
import { AppError, httpErrors } from "../../errors";
|
|
7
|
-
import type { ActivityActor, RateLimitPort } from "../../ports";
|
|
8
|
-
import
|
|
5
|
+
import type { RateLimitScope } from "../../contracts/index.js";
|
|
6
|
+
import { AppError, httpErrors } from "../../errors/index.js";
|
|
7
|
+
import type { ActivityActor, RateLimitPort } from "../../ports/index.js";
|
|
8
|
+
import {
|
|
9
|
+
createProviderInstrumentation,
|
|
10
|
+
type ProviderInstrumentationTarget,
|
|
11
|
+
} from "../../providers/index.js";
|
|
12
|
+
import type { HttpRequestLike, ServerHook } from "../types.js";
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Ports required by rate-limit hooks.
|
|
@@ -24,6 +28,23 @@ export type CtxWithRateLimit = {
|
|
|
24
28
|
|
|
25
29
|
type EarlyRateLimitScope = Exclude<RateLimitScope, "user">;
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Strategy for resolving the client IP used by `ip`-scoped limits.
|
|
33
|
+
*
|
|
34
|
+
* - `"x-forwarded-for-last"` (default): the last `x-forwarded-for` entry.
|
|
35
|
+
* This is the address appended by the platform's trusted reverse proxy and
|
|
36
|
+
* cannot be chosen by the client.
|
|
37
|
+
* - `"x-forwarded-for-first"`: the first `x-forwarded-for` entry. This value
|
|
38
|
+
* is client-controlled, so only use it when a trusted edge normalizes the
|
|
39
|
+
* header before it reaches the app.
|
|
40
|
+
* - A function receives the raw request and returns the client IP, for
|
|
41
|
+
* platform-specific headers such as `cf-connecting-ip`.
|
|
42
|
+
*/
|
|
43
|
+
export type RateLimitIpSource =
|
|
44
|
+
| "x-forwarded-for-last"
|
|
45
|
+
| "x-forwarded-for-first"
|
|
46
|
+
| ((req: HttpRequestLike) => string | undefined);
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Options for `createRateLimitHooks(...)`.
|
|
29
50
|
*/
|
|
@@ -48,14 +69,38 @@ export interface RateLimitOptions<Ctx> {
|
|
|
48
69
|
scope: EarlyRateLimitScope;
|
|
49
70
|
}) => string;
|
|
50
71
|
/**
|
|
51
|
-
* Resolve
|
|
72
|
+
* Resolve the client IP for `ip`-scoped limits.
|
|
73
|
+
*
|
|
74
|
+
* Defaults to `"x-forwarded-for-last"`, the entry appended by the
|
|
75
|
+
* platform's trusted proxy. Pass a function for platform-specific headers.
|
|
52
76
|
*/
|
|
53
|
-
|
|
77
|
+
ipSource?: RateLimitIpSource;
|
|
54
78
|
}
|
|
55
79
|
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
return
|
|
80
|
+
function parseForwardedFor(req: HttpRequestLike): string[] {
|
|
81
|
+
const header = req.headers.get("x-forwarded-for") ?? "";
|
|
82
|
+
return header
|
|
83
|
+
.split(",")
|
|
84
|
+
.map((entry) => entry.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveClientIp(
|
|
89
|
+
req: HttpRequestLike,
|
|
90
|
+
ipSource: RateLimitIpSource,
|
|
91
|
+
): string | undefined {
|
|
92
|
+
if (typeof ipSource === "function") {
|
|
93
|
+
return ipSource(req) || undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const entries = parseForwardedFor(req);
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return ipSource === "x-forwarded-for-first"
|
|
102
|
+
? entries[0]
|
|
103
|
+
: entries[entries.length - 1];
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
function emitUserKey(userId: string): string {
|
|
@@ -104,7 +149,7 @@ function defaultEarlyRateLimitKey(
|
|
|
104
149
|
}
|
|
105
150
|
|
|
106
151
|
async function enforceRateLimit(
|
|
107
|
-
|
|
152
|
+
ports: RateLimitPorts,
|
|
108
153
|
args: {
|
|
109
154
|
key: string;
|
|
110
155
|
limit: number;
|
|
@@ -112,7 +157,7 @@ async function enforceRateLimit(
|
|
|
112
157
|
scope: RateLimitScope;
|
|
113
158
|
},
|
|
114
159
|
): Promise<void> {
|
|
115
|
-
const result = await rateLimit.hit({
|
|
160
|
+
const result = await ports.rateLimit.hit({
|
|
116
161
|
key: args.key,
|
|
117
162
|
limit: args.limit,
|
|
118
163
|
windowSec: args.windowSec,
|
|
@@ -122,10 +167,30 @@ async function enforceRateLimit(
|
|
|
122
167
|
return;
|
|
123
168
|
}
|
|
124
169
|
|
|
170
|
+
// App ports may carry an `instrumentation` or `devtools` sink alongside the
|
|
171
|
+
// typed rate-limit port; the helper resolves them at runtime.
|
|
172
|
+
const instrumentation = createProviderInstrumentation(
|
|
173
|
+
ports as ProviderInstrumentationTarget,
|
|
174
|
+
{
|
|
175
|
+
providerName: "rate-limit",
|
|
176
|
+
watcher: "rateLimit",
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
instrumentation.custom({
|
|
180
|
+
name: "rateLimit.denied",
|
|
181
|
+
label: "Rate limit denied",
|
|
182
|
+
summary: `Rate limit denied for ${args.key}`,
|
|
183
|
+
details: {
|
|
184
|
+
key: args.key,
|
|
185
|
+
scope: args.scope,
|
|
186
|
+
limit: args.limit,
|
|
187
|
+
windowSec: args.windowSec,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
125
191
|
throw new AppError(
|
|
126
192
|
httpErrors.TooManyRequests,
|
|
127
193
|
{
|
|
128
|
-
key: args.key,
|
|
129
194
|
scope: args.scope,
|
|
130
195
|
retryAfterSeconds: result.retryAfterSeconds,
|
|
131
196
|
resetAt: result.resetAt?.toISOString() ?? null,
|
|
@@ -140,15 +205,18 @@ async function enforceRateLimit(
|
|
|
140
205
|
* The hook reads `contract.metadata.rateLimit`. Global and IP-scoped limits run
|
|
141
206
|
* in `onRequest` before context creation; user-scoped limits run in
|
|
142
207
|
* `beforeHandle` after `ctx.actor` is available. Exceeded limits throw the
|
|
143
|
-
* framework `TooManyRequests` app error
|
|
208
|
+
* framework `TooManyRequests` app error with `scope`, `retryAfterSeconds`, and
|
|
209
|
+
* `resetAt` details. The bucket key is never sent to clients; denials emit a
|
|
210
|
+
* `rateLimit.denied` instrumentation event that carries the key for operators.
|
|
144
211
|
*
|
|
145
|
-
* @param options - Optional key builders and client-IP
|
|
212
|
+
* @param options - Optional key builders and client-IP source.
|
|
146
213
|
* @returns A server hook backed by `ctx.ports.rateLimit`.
|
|
147
214
|
*/
|
|
148
215
|
export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
149
216
|
options: RateLimitOptions<Ctx> = {},
|
|
150
217
|
): ServerHook<Ctx, RateLimitPorts> {
|
|
151
|
-
const
|
|
218
|
+
const ipSource = options.ipSource ?? "x-forwarded-for-last";
|
|
219
|
+
const getClientIp = (req: HttpRequestLike) => resolveClientIp(req, ipSource);
|
|
152
220
|
|
|
153
221
|
return {
|
|
154
222
|
name: "rate-limit",
|
|
@@ -167,7 +235,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
167
235
|
options.earlyKey?.({ req, scope }) ??
|
|
168
236
|
defaultEarlyRateLimitKey({ req, scope }, getClientIp);
|
|
169
237
|
|
|
170
|
-
await enforceRateLimit(ports
|
|
238
|
+
await enforceRateLimit(ports, {
|
|
171
239
|
key,
|
|
172
240
|
limit: rlMeta.max,
|
|
173
241
|
windowSec: rlMeta.windowSec,
|
|
@@ -191,7 +259,7 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
191
259
|
options.key?.({ ctx, req, scope }) ??
|
|
192
260
|
defaultRateLimitKey({ ctx, req, scope }, getClientIp);
|
|
193
261
|
|
|
194
|
-
await enforceRateLimit(ctx.ports
|
|
262
|
+
await enforceRateLimit(ctx.ports, {
|
|
195
263
|
key,
|
|
196
264
|
limit: rlMeta.max,
|
|
197
265
|
windowSec: rlMeta.windowSec,
|
package/src/server/hooks.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./hooks/index";
|
|
1
|
+
export * from "./hooks/index.js";
|
package/src/server/http.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type {
|
|
|
3
3
|
InferHeaderSchemaOutput,
|
|
4
4
|
InferOutput,
|
|
5
5
|
StandardSchema,
|
|
6
|
-
} from "../contracts";
|
|
7
|
-
import type { AnyPorts } from "../ports";
|
|
6
|
+
} from "../contracts/index.js";
|
|
7
|
+
import type { AnyPorts } from "../ports/index.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Framework-neutral request shape consumed by Beignet server adapters.
|
|
@@ -84,6 +84,50 @@ export interface HttpResponseLike {
|
|
|
84
84
|
*/
|
|
85
85
|
export type HttpResponse = HttpResponseLike | Response;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Framework-neutral Beignet API handler consumed by HTTP adapters.
|
|
89
|
+
*/
|
|
90
|
+
export type HttpAdapterApiHandler = (
|
|
91
|
+
req: HttpRequestLike,
|
|
92
|
+
) => Promise<HttpResponse>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Native handler shape produced by an HTTP adapter.
|
|
96
|
+
*/
|
|
97
|
+
export type HttpAdapterHandler<NativeRequest, NativeResponse> = (
|
|
98
|
+
req: NativeRequest,
|
|
99
|
+
) => Promise<NativeResponse>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Contract implemented by packages that adapt Beignet's framework-neutral
|
|
103
|
+
* server runtime to a platform HTTP API.
|
|
104
|
+
*
|
|
105
|
+
* Core owns request parsing, hooks, route matching, validation, error mapping,
|
|
106
|
+
* response ownership, and provider lifecycle. Adapters own only the conversion
|
|
107
|
+
* between the platform request/response types and Beignet's `HttpRequestLike`
|
|
108
|
+
* / `HttpResponse` boundary.
|
|
109
|
+
*/
|
|
110
|
+
export interface HttpAdapter<NativeRequest, NativeResponse> {
|
|
111
|
+
/**
|
|
112
|
+
* Human-readable adapter name for diagnostics and documentation.
|
|
113
|
+
*/
|
|
114
|
+
name: string;
|
|
115
|
+
/**
|
|
116
|
+
* Convert a platform request into Beignet's framework-neutral request shape.
|
|
117
|
+
*/
|
|
118
|
+
toRequestLike(req: NativeRequest): HttpRequestLike;
|
|
119
|
+
/**
|
|
120
|
+
* Convert a Beignet response into the platform response type.
|
|
121
|
+
*/
|
|
122
|
+
toNativeResponse(res: HttpResponse): NativeResponse | Promise<NativeResponse>;
|
|
123
|
+
/**
|
|
124
|
+
* Wrap a Beignet API handler in the platform's native handler shape.
|
|
125
|
+
*/
|
|
126
|
+
createHandler(
|
|
127
|
+
handler: HttpAdapterApiHandler,
|
|
128
|
+
): HttpAdapterHandler<NativeRequest, NativeResponse>;
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
type InferSchemaOrFallback<
|
|
88
132
|
T extends StandardSchema | null,
|
|
89
133
|
Fallback,
|
|
@@ -130,7 +174,7 @@ export interface HandlerArgs<Ctx, C extends HttpContractConfig> {
|
|
|
130
174
|
*/
|
|
131
175
|
req: HttpRequestLike;
|
|
132
176
|
/**
|
|
133
|
-
* Application context
|
|
177
|
+
* Application context assembled by the server context blueprint.
|
|
134
178
|
*/
|
|
135
179
|
ctx: Ctx;
|
|
136
180
|
/**
|
|
@@ -168,6 +212,59 @@ export type Handler<Ctx, C extends HttpContractConfig> = (
|
|
|
168
212
|
*/
|
|
169
213
|
export type MaybePromise<T> = T | Promise<T>;
|
|
170
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Arguments passed to a route-scoped hook after request parsing and context
|
|
217
|
+
* creation.
|
|
218
|
+
*/
|
|
219
|
+
export type RouteHookArgs<
|
|
220
|
+
Ctx,
|
|
221
|
+
C extends HttpContractConfig = HttpContractConfig,
|
|
222
|
+
> = HandlerArgs<Ctx, C>;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Hook that runs only for the route or route group where it is attached.
|
|
226
|
+
*
|
|
227
|
+
* Route hooks are for scoped policy and context enrichment such as
|
|
228
|
+
* authentication, tenant resolution, feature gates, and idempotency. They add
|
|
229
|
+
* fields to the handler context instead of replacing the app context.
|
|
230
|
+
*
|
|
231
|
+
* Hook additions must not include `gate`: the server re-attaches the gate
|
|
232
|
+
* declared by the context blueprint after every hook, so identity changes are
|
|
233
|
+
* picked up automatically.
|
|
234
|
+
*/
|
|
235
|
+
export interface RouteHook<
|
|
236
|
+
Ctx,
|
|
237
|
+
AddedCtx extends object & { gate?: never } = Record<string, never>,
|
|
238
|
+
> {
|
|
239
|
+
/**
|
|
240
|
+
* Optional name used in diagnostics and devtools.
|
|
241
|
+
*/
|
|
242
|
+
name?: string;
|
|
243
|
+
/**
|
|
244
|
+
* Resolve additional context for this route or throw to stop handling.
|
|
245
|
+
*/
|
|
246
|
+
resolve: (args: RouteHookArgs<Ctx>) => MaybePromise<AddedCtx | undefined>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type AddedCtxFromHook<Hook> =
|
|
250
|
+
Hook extends RouteHook<infer _Ctx, infer AddedCtx> ? AddedCtx : unknown;
|
|
251
|
+
|
|
252
|
+
type UnionToIntersection<Union> = (
|
|
253
|
+
Union extends unknown
|
|
254
|
+
? (value: Union) => void
|
|
255
|
+
: never
|
|
256
|
+
) extends (value: infer Intersection) => void
|
|
257
|
+
? Intersection
|
|
258
|
+
: never;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Intersection of the context fields added by a route hook list.
|
|
262
|
+
*/
|
|
263
|
+
export type AddedCtxFromHooks<Hooks extends readonly unknown[]> =
|
|
264
|
+
Hooks extends readonly []
|
|
265
|
+
? unknown
|
|
266
|
+
: UnionToIntersection<AddedCtxFromHook<Hooks[number]>>;
|
|
267
|
+
|
|
171
268
|
/**
|
|
172
269
|
* Hook that runs after a route is matched but before request parsing and
|
|
173
270
|
* context creation.
|
|
@@ -215,10 +312,11 @@ export type BeforeHandleHook<
|
|
|
215
312
|
}) => MaybePromise<BeforeHandleResult<Ctx>>;
|
|
216
313
|
|
|
217
314
|
/**
|
|
218
|
-
* Hook that runs before
|
|
315
|
+
* Hook that runs before the response is returned.
|
|
219
316
|
*
|
|
220
317
|
* Return a response to replace or decorate the outgoing response. Hooks run in
|
|
221
|
-
* declaration order.
|
|
318
|
+
* declaration order. For native web `Response` results the hook receives a
|
|
319
|
+
* headers-only view and only header changes are applied.
|
|
222
320
|
*/
|
|
223
321
|
export type BeforeSendHook<
|
|
224
322
|
Ctx,
|
|
@@ -233,6 +331,13 @@ export type BeforeSendHook<
|
|
|
233
331
|
body?: InferBody<C>;
|
|
234
332
|
response: HttpResponseLike;
|
|
235
333
|
error?: unknown;
|
|
334
|
+
/**
|
|
335
|
+
* True when the route returned a native web Response. The response argument
|
|
336
|
+
* is a headers-only view ({ status, headers }); the body is not readable and
|
|
337
|
+
* returned body/status changes are ignored. Header changes are merged onto
|
|
338
|
+
* the native Response.
|
|
339
|
+
*/
|
|
340
|
+
native?: boolean;
|
|
236
341
|
}) => MaybePromise<HttpResponseLike | undefined>;
|
|
237
342
|
|
|
238
343
|
/**
|
|
@@ -316,7 +421,8 @@ export interface ServerHook<Ctx, Ports extends AnyPorts = AnyPorts> {
|
|
|
316
421
|
*/
|
|
317
422
|
beforeHandle?: BeforeHandleHook<Ctx>;
|
|
318
423
|
/**
|
|
319
|
-
* Runs before
|
|
424
|
+
* Runs before the response is returned. Native web `Response` results get a
|
|
425
|
+
* headers-only view with `native: true`.
|
|
320
426
|
*/
|
|
321
427
|
beforeSend?: BeforeSendHook<Ctx>;
|
|
322
428
|
/**
|