@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
|
@@ -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,35 +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
10
|
type AuthHooksOptions,
|
|
11
11
|
type AuthRouteHooks,
|
|
12
12
|
createAuthHooks,
|
|
13
|
-
} from "./auth";
|
|
13
|
+
} from "./auth.js";
|
|
14
14
|
export {
|
|
15
15
|
applyCorsHeaders,
|
|
16
16
|
type CorsConfig,
|
|
17
17
|
createCorsHooks,
|
|
18
|
-
} from "./cors";
|
|
18
|
+
} from "./cors.js";
|
|
19
19
|
export {
|
|
20
20
|
defaultMapErrorToResponse,
|
|
21
21
|
type ErrorMappingConfig,
|
|
22
22
|
type ErrorMappingResult,
|
|
23
|
-
} from "./errors";
|
|
23
|
+
} from "./errors.js";
|
|
24
|
+
export {
|
|
25
|
+
type CtxWithIdempotency,
|
|
26
|
+
createIdempotencyHooks,
|
|
27
|
+
type IdempotencyHooksOptions,
|
|
28
|
+
type IdempotencyPorts,
|
|
29
|
+
} from "./idempotency.js";
|
|
24
30
|
export {
|
|
25
31
|
createLoggingHooks,
|
|
26
32
|
type Logger,
|
|
27
33
|
type LoggingConfig,
|
|
28
|
-
} from "./logging";
|
|
34
|
+
} from "./logging.js";
|
|
29
35
|
export {
|
|
30
36
|
type CtxWithRateLimit,
|
|
31
37
|
createRateLimitHooks,
|
|
38
|
+
type RateLimitIpSource,
|
|
32
39
|
type RateLimitOptions,
|
|
33
|
-
} from "./rate-limit";
|
|
40
|
+
} from "./rate-limit.js";
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
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
|
/**
|
|
@@ -183,10 +227,14 @@ export type RouteHookArgs<
|
|
|
183
227
|
* Route hooks are for scoped policy and context enrichment such as
|
|
184
228
|
* authentication, tenant resolution, feature gates, and idempotency. They add
|
|
185
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.
|
|
186
234
|
*/
|
|
187
235
|
export interface RouteHook<
|
|
188
236
|
Ctx,
|
|
189
|
-
AddedCtx extends object = Record<string, never>,
|
|
237
|
+
AddedCtx extends object & { gate?: never } = Record<string, never>,
|
|
190
238
|
> {
|
|
191
239
|
/**
|
|
192
240
|
* Optional name used in diagnostics and devtools.
|
|
@@ -198,54 +246,24 @@ export interface RouteHook<
|
|
|
198
246
|
resolve: (args: RouteHookArgs<Ctx>) => MaybePromise<AddedCtx | undefined>;
|
|
199
247
|
}
|
|
200
248
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*/
|
|
204
|
-
export type RouteHookBuilder<Ctx> = {
|
|
205
|
-
/**
|
|
206
|
-
* Assign a diagnostic name to the hook.
|
|
207
|
-
*/
|
|
208
|
-
name: (name: string) => RouteHookNamedBuilder<Ctx>;
|
|
209
|
-
/**
|
|
210
|
-
* Define the hook resolver.
|
|
211
|
-
*/
|
|
212
|
-
resolve: <AddedCtx extends object>(
|
|
213
|
-
resolve: RouteHook<Ctx, AddedCtx>["resolve"],
|
|
214
|
-
) => RouteHook<Ctx, AddedCtx>;
|
|
215
|
-
};
|
|
249
|
+
type AddedCtxFromHook<Hook> =
|
|
250
|
+
Hook extends RouteHook<infer _Ctx, infer AddedCtx> ? AddedCtx : unknown;
|
|
216
251
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
resolve: <AddedCtx extends object>(
|
|
225
|
-
resolve: RouteHook<Ctx, AddedCtx>["resolve"],
|
|
226
|
-
) => RouteHook<Ctx, AddedCtx>;
|
|
227
|
-
};
|
|
252
|
+
type UnionToIntersection<Union> = (
|
|
253
|
+
Union extends unknown
|
|
254
|
+
? (value: Union) => void
|
|
255
|
+
: never
|
|
256
|
+
) extends (value: infer Intersection) => void
|
|
257
|
+
? Intersection
|
|
258
|
+
: never;
|
|
228
259
|
|
|
229
260
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* Route hooks enrich handler context for one route or route group. They should
|
|
233
|
-
* throw application/framework errors for denials instead of returning HTTP
|
|
234
|
-
* responses directly.
|
|
261
|
+
* Intersection of the context fields added by a route hook list.
|
|
235
262
|
*/
|
|
236
|
-
export
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
name,
|
|
241
|
-
resolve,
|
|
242
|
-
}),
|
|
243
|
-
}),
|
|
244
|
-
resolve: (resolve) => ({
|
|
245
|
-
resolve,
|
|
246
|
-
}),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
263
|
+
export type AddedCtxFromHooks<Hooks extends readonly unknown[]> =
|
|
264
|
+
Hooks extends readonly []
|
|
265
|
+
? unknown
|
|
266
|
+
: UnionToIntersection<AddedCtxFromHook<Hooks[number]>>;
|
|
249
267
|
|
|
250
268
|
/**
|
|
251
269
|
* Hook that runs after a route is matched but before request parsing and
|
|
@@ -294,10 +312,11 @@ export type BeforeHandleHook<
|
|
|
294
312
|
}) => MaybePromise<BeforeHandleResult<Ctx>>;
|
|
295
313
|
|
|
296
314
|
/**
|
|
297
|
-
* Hook that runs before
|
|
315
|
+
* Hook that runs before the response is returned.
|
|
298
316
|
*
|
|
299
317
|
* Return a response to replace or decorate the outgoing response. Hooks run in
|
|
300
|
-
* declaration order.
|
|
318
|
+
* declaration order. For native web `Response` results the hook receives a
|
|
319
|
+
* headers-only view and only header changes are applied.
|
|
301
320
|
*/
|
|
302
321
|
export type BeforeSendHook<
|
|
303
322
|
Ctx,
|
|
@@ -312,6 +331,13 @@ export type BeforeSendHook<
|
|
|
312
331
|
body?: InferBody<C>;
|
|
313
332
|
response: HttpResponseLike;
|
|
314
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;
|
|
315
341
|
}) => MaybePromise<HttpResponseLike | undefined>;
|
|
316
342
|
|
|
317
343
|
/**
|
|
@@ -395,7 +421,8 @@ export interface ServerHook<Ctx, Ports extends AnyPorts = AnyPorts> {
|
|
|
395
421
|
*/
|
|
396
422
|
beforeHandle?: BeforeHandleHook<Ctx>;
|
|
397
423
|
/**
|
|
398
|
-
* Runs before
|
|
424
|
+
* Runs before the response is returned. Native web `Response` results get a
|
|
425
|
+
* headers-only view with `native: true`.
|
|
399
426
|
*/
|
|
400
427
|
beforeSend?: BeforeSendHook<Ctx>;
|
|
401
428
|
/**
|