@webiny/event-handler-core 6.6.0-alpha.0 → 6.6.0-alpha.2
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/exports/api.d.ts +2 -0
- package/exports/api.js +1 -0
- package/features/events/ChildContainerFactory.d.ts +11 -0
- package/features/events/ChildContainerFactory.js +25 -0
- package/features/events/ChildContainerFactory.js.map +1 -0
- package/features/events/HandlerApp.d.ts +27 -0
- package/features/events/HandlerApp.js +53 -0
- package/features/events/HandlerApp.js.map +1 -0
- package/features/events/RootContainerFactory.d.ts +12 -0
- package/features/events/RootContainerFactory.js +24 -0
- package/features/events/RootContainerFactory.js.map +1 -0
- package/features/events/abstractions.d.ts +60 -0
- package/features/events/abstractions.js +7 -0
- package/features/events/abstractions.js.map +1 -0
- package/features/events/index.d.ts +4 -4
- package/features/events/index.js +4 -4
- package/features/http/HttpResponseBuilder.d.ts +36 -0
- package/features/http/HttpResponseBuilder.js +133 -0
- package/features/http/HttpResponseBuilder.js.map +1 -0
- package/features/http/HttpRouter.d.ts +17 -3
- package/features/http/HttpRouter.js +24 -17
- package/features/http/HttpRouter.js.map +1 -1
- package/features/http/HttpStreamBody.d.ts +34 -0
- package/features/http/HttpStreamBody.js +44 -0
- package/features/http/HttpStreamBody.js.map +1 -0
- package/features/http/abstractions.d.ts +154 -3
- package/features/http/abstractions.js +3 -2
- package/features/http/abstractions.js.map +1 -1
- package/features/http/createHttpRouteDefinition.d.ts +17 -0
- package/features/http/createHttpRouteDefinition.js +18 -0
- package/features/http/createHttpRouteDefinition.js.map +1 -0
- package/features/http/decorators/CompressionDecorator.d.ts +28 -0
- package/features/http/decorators/CompressionDecorator.js +66 -0
- package/features/http/decorators/CompressionDecorator.js.map +1 -0
- package/features/http/decorators/SecureHeadersDecorator.js +1 -1
- package/features/http/decorators/SecureHeadersDecorator.js.map +1 -1
- package/features/http/decorators/index.d.ts +1 -0
- package/features/http/decorators/index.js +1 -0
- package/features/http/feature.js +2 -2
- package/features/http/feature.js.map +1 -1
- package/features/http/index.d.ts +5 -0
- package/features/http/index.js +5 -0
- package/features/http/invokeHttpRoute.d.ts +17 -0
- package/features/http/invokeHttpRoute.js +32 -0
- package/features/http/invokeHttpRoute.js.map +1 -0
- package/features/http/toSseFrame.d.ts +10 -0
- package/features/http/toSseFrame.js +6 -0
- package/features/http/toSseFrame.js.map +1 -0
- package/features/testing/createTestHttpHandler.d.ts +1 -1
- package/features/testing/createTestHttpHandler.js +4 -4
- package/features/testing/createTestHttpHandler.js.map +1 -1
- package/features/testing/index.d.ts +1 -0
- package/features/testing/index.js +1 -0
- package/features/testing/registerHttpRouteInstance.d.ts +16 -0
- package/features/testing/registerHttpRouteInstance.js +27 -0
- package/features/testing/registerHttpRouteInstance.js.map +1 -0
- package/package.json +6 -6
- package/features/events/RequestContextInitializer.d.ts +0 -26
- package/features/events/RequestContextInitializer.js +0 -5
- package/features/events/RequestContextInitializer.js.map +0 -1
- package/features/events/RequestInitializer.d.ts +0 -22
- package/features/events/RequestInitializer.js +0 -5
- package/features/events/RequestInitializer.js.map +0 -1
- package/features/events/createHandler.d.ts +0 -20
- package/features/events/createHandler.js +0 -41
- package/features/events/createHandler.js.map +0 -1
- package/features/events/runRequestContextInitializers.d.ts +0 -20
- package/features/events/runRequestContextInitializers.js +0 -16
- package/features/events/runRequestContextInitializers.js.map +0 -1
- package/features/http/decorators/RequestContextInitializerDecorator.d.ts +0 -20
- package/features/http/decorators/RequestContextInitializerDecorator.js +0 -22
- package/features/http/decorators/RequestContextInitializerDecorator.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type HttpStreamChunk = Uint8Array | string;
|
|
2
|
+
export type HttpStreamSource = AsyncIterable<HttpStreamChunk>;
|
|
3
|
+
/**
|
|
4
|
+
* Marker for a streaming HTTP response body.
|
|
5
|
+
*
|
|
6
|
+
* `IHttpResponse.body` is untyped, and buffered bodies already carry meaning by their runtime type
|
|
7
|
+
* (string, Buffer/Uint8Array, plain object). Streaming has to be distinguishable from those WITHOUT
|
|
8
|
+
* duck-typing `Symbol.asyncIterator`: a plain object body could accidentally satisfy it, and
|
|
9
|
+
* `ReadableStream`'s async-iterator support (present in Node at runtime) isn't declared in the DOM
|
|
10
|
+
* types. So a route opts into streaming explicitly, by wrapping its source in this class.
|
|
11
|
+
*
|
|
12
|
+
* Transports that can stream (the Node HTTP server; AWS Lambda response streaming via a Function
|
|
13
|
+
* URL) write chunks as they are produced. Transports that cannot (API Gateway buffers the entire
|
|
14
|
+
* Lambda response no matter how it was produced) call {@link collect} and send one buffered body —
|
|
15
|
+
* so the same route still works there, just without incremental delivery.
|
|
16
|
+
*/
|
|
17
|
+
export declare class HttpStreamBody {
|
|
18
|
+
readonly source: HttpStreamSource;
|
|
19
|
+
constructor(source: HttpStreamSource);
|
|
20
|
+
static is(value: unknown): value is HttpStreamBody;
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a web `ReadableStream` — what `fetch` and the AI SDK's `toUIMessageStreamResponse()`
|
|
23
|
+
* hand back. The reader is driven explicitly rather than relying on async iteration, because
|
|
24
|
+
* the DOM types don't declare it even though Node implements it.
|
|
25
|
+
*/
|
|
26
|
+
static fromWebStream(stream: ReadableStream<Uint8Array>): HttpStreamBody;
|
|
27
|
+
/**
|
|
28
|
+
* Drain the whole stream into a single byte array, for transports that can't stream.
|
|
29
|
+
*
|
|
30
|
+
* Consumes the source — a stream can only be read once, so this must not be combined with
|
|
31
|
+
* writing the same body incrementally.
|
|
32
|
+
*/
|
|
33
|
+
collect(): Promise<Uint8Array>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class HttpStreamBody {
|
|
2
|
+
constructor(source){
|
|
3
|
+
this.source = source;
|
|
4
|
+
}
|
|
5
|
+
static is(value) {
|
|
6
|
+
return value instanceof HttpStreamBody;
|
|
7
|
+
}
|
|
8
|
+
static fromWebStream(stream) {
|
|
9
|
+
return new HttpStreamBody({
|
|
10
|
+
async *[Symbol.asyncIterator] () {
|
|
11
|
+
const reader = stream.getReader();
|
|
12
|
+
try {
|
|
13
|
+
while(true){
|
|
14
|
+
const { done, value } = await reader.read();
|
|
15
|
+
if (done) break;
|
|
16
|
+
if (void 0 !== value) yield value;
|
|
17
|
+
}
|
|
18
|
+
} finally{
|
|
19
|
+
reader.releaseLock();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async collect() {
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
const chunks = [];
|
|
27
|
+
let total = 0;
|
|
28
|
+
for await (const chunk of this.source){
|
|
29
|
+
const bytes = "string" == typeof chunk ? encoder.encode(chunk) : chunk;
|
|
30
|
+
chunks.push(bytes);
|
|
31
|
+
total += bytes.byteLength;
|
|
32
|
+
}
|
|
33
|
+
const result = new Uint8Array(total);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks){
|
|
36
|
+
result.set(chunk, offset);
|
|
37
|
+
offset += chunk.byteLength;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export { HttpStreamBody };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=HttpStreamBody.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/http/HttpStreamBody.js","sources":["../../../src/features/http/HttpStreamBody.ts"],"sourcesContent":["export type HttpStreamChunk = Uint8Array | string;\n\nexport type HttpStreamSource = AsyncIterable<HttpStreamChunk>;\n\n/**\n * Marker for a streaming HTTP response body.\n *\n * `IHttpResponse.body` is untyped, and buffered bodies already carry meaning by their runtime type\n * (string, Buffer/Uint8Array, plain object). Streaming has to be distinguishable from those WITHOUT\n * duck-typing `Symbol.asyncIterator`: a plain object body could accidentally satisfy it, and\n * `ReadableStream`'s async-iterator support (present in Node at runtime) isn't declared in the DOM\n * types. So a route opts into streaming explicitly, by wrapping its source in this class.\n *\n * Transports that can stream (the Node HTTP server; AWS Lambda response streaming via a Function\n * URL) write chunks as they are produced. Transports that cannot (API Gateway buffers the entire\n * Lambda response no matter how it was produced) call {@link collect} and send one buffered body —\n * so the same route still works there, just without incremental delivery.\n */\nexport class HttpStreamBody {\n constructor(readonly source: HttpStreamSource) {}\n\n static is(value: unknown): value is HttpStreamBody {\n return value instanceof HttpStreamBody;\n }\n\n /**\n * Wrap a web `ReadableStream` — what `fetch` and the AI SDK's `toUIMessageStreamResponse()`\n * hand back. The reader is driven explicitly rather than relying on async iteration, because\n * the DOM types don't declare it even though Node implements it.\n */\n static fromWebStream(stream: ReadableStream<Uint8Array>): HttpStreamBody {\n return new HttpStreamBody({\n async *[Symbol.asyncIterator]() {\n const reader = stream.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (value !== undefined) {\n yield value;\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n });\n }\n\n /**\n * Drain the whole stream into a single byte array, for transports that can't stream.\n *\n * Consumes the source — a stream can only be read once, so this must not be combined with\n * writing the same body incrementally.\n */\n async collect(): Promise<Uint8Array> {\n const encoder = new TextEncoder();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for await (const chunk of this.source) {\n const bytes = typeof chunk === \"string\" ? encoder.encode(chunk) : chunk;\n chunks.push(bytes);\n total += bytes.byteLength;\n }\n\n const result = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n result.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return result;\n }\n}\n"],"names":["HttpStreamBody","source","value","stream","Symbol","reader","done","undefined","encoder","TextEncoder","chunks","total","chunk","bytes","result","Uint8Array","offset"],"mappings":"AAkBO,MAAMA;IACT,YAAqBC,MAAwB,CAAE;aAA1BA,MAAM,GAANA;IAA2B;IAEhD,OAAO,GAAGC,KAAc,EAA2B;QAC/C,OAAOA,iBAAiBF;IAC5B;IAOA,OAAO,cAAcG,MAAkC,EAAkB;QACrE,OAAO,IAAIH,eAAe;YACtB,OAAO,CAACI,OAAO,aAAa,CAAC;gBACzB,MAAMC,SAASF,OAAO,SAAS;gBAC/B,IAAI;oBACA,MAAO,KAAM;wBACT,MAAM,EAAEG,IAAI,EAAEJ,KAAK,EAAE,GAAG,MAAMG,OAAO,IAAI;wBACzC,IAAIC,MACA;wBAEJ,IAAIJ,AAAUK,WAAVL,OACA,MAAMA;oBAEd;gBACJ,SAAU;oBACNG,OAAO,WAAW;gBACtB;YACJ;QACJ;IACJ;IAQA,MAAM,UAA+B;QACjC,MAAMG,UAAU,IAAIC;QACpB,MAAMC,SAAuB,EAAE;QAC/B,IAAIC,QAAQ;QAEZ,WAAW,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAE;YACnC,MAAMC,QAAQ,AAAiB,YAAjB,OAAOD,QAAqBJ,QAAQ,MAAM,CAACI,SAASA;YAClEF,OAAO,IAAI,CAACG;YACZF,SAASE,MAAM,UAAU;QAC7B;QAEA,MAAMC,SAAS,IAAIC,WAAWJ;QAC9B,IAAIK,SAAS;QACb,KAAK,MAAMJ,SAASF,OAAQ;YACxBI,OAAO,GAAG,CAACF,OAAOI;YAClBA,UAAUJ,MAAM,UAAU;QAC9B;QAEA,OAAOE;IACX;AACJ"}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { Abstraction } from "@webiny/di";
|
|
2
|
+
import type { Constructor } from "@webiny/di";
|
|
3
|
+
import type { HttpStreamSource } from "./HttpStreamBody.js";
|
|
4
|
+
/**
|
|
5
|
+
* Which route the router matched, as seen from inside `handle()` — the `req.route` of Express-style
|
|
6
|
+
* handlers. It is the definition without its `handler`, so a route (or anything wrapping one) can
|
|
7
|
+
* tell WHICH route is running and act only on the one it cares about.
|
|
8
|
+
*/
|
|
9
|
+
export interface IMatchedRouteDefinition {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly method: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
2
14
|
export interface IHttpRequest {
|
|
3
15
|
method: string;
|
|
4
16
|
path: string;
|
|
@@ -6,24 +18,163 @@ export interface IHttpRequest {
|
|
|
6
18
|
query: Record<string, string>;
|
|
7
19
|
pathParameters: Record<string, string>;
|
|
8
20
|
body: any;
|
|
21
|
+
/**
|
|
22
|
+
* Set by `HttpRouter` once a route matches, so it is absent on the request a transport builds
|
|
23
|
+
* and always present by the time `handle()` runs. Handlers should take
|
|
24
|
+
* {@link HttpRouteHandler.Request}, where it is required.
|
|
25
|
+
*/
|
|
26
|
+
route?: IMatchedRouteDefinition;
|
|
9
27
|
}
|
|
10
28
|
export interface IHttpResponse {
|
|
11
29
|
statusCode: number;
|
|
12
30
|
headers?: Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Serialized `Set-Cookie` values. A separate field (not a header) because `Set-Cookie` is the
|
|
33
|
+
* one response header that can legally repeat, which `headers: Record<string, string>` cannot
|
|
34
|
+
* express. Transports map it to whatever their protocol needs — API Gateway's
|
|
35
|
+
* `multiValueHeaders` / `cookies`, or a repeated header on a Node `ServerResponse`.
|
|
36
|
+
*/
|
|
37
|
+
cookies?: string[];
|
|
13
38
|
body?: any;
|
|
14
39
|
}
|
|
40
|
+
export interface CookieOptions {
|
|
41
|
+
domain?: string;
|
|
42
|
+
path?: string;
|
|
43
|
+
expires?: Date;
|
|
44
|
+
/**
|
|
45
|
+
* Cookie lifetime in SECONDS — the `Max-Age` attribute value as the HTTP spec defines it.
|
|
46
|
+
* Note this differs from Express, whose `maxAge` is in milliseconds.
|
|
47
|
+
*/
|
|
48
|
+
maxAge?: number;
|
|
49
|
+
httpOnly?: boolean;
|
|
50
|
+
secure?: boolean;
|
|
51
|
+
partitioned?: boolean;
|
|
52
|
+
sameSite?: "strict" | "lax" | "none";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The mutable response handed to every route as the second argument to `handle()` — the `res` of
|
|
56
|
+
* Express-style handlers. Every method returns `this`, so a route can chain and then return the
|
|
57
|
+
* builder directly:
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* async handle(request: IHttpRequest, response: IHttpResponseBuilder) {
|
|
61
|
+
* return response.status(201).cookie("sid", id, { httpOnly: true }).json({ id });
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Returning it is optional: a route that mutates it and returns nothing gets the same response.
|
|
66
|
+
* Routes returning a plain {@link IHttpResponse} keep working — anything set on the builder
|
|
67
|
+
* (headers, cookies) is merged underneath the returned object, which wins on conflicts.
|
|
68
|
+
*/
|
|
69
|
+
export interface IHttpResponseBuilder {
|
|
70
|
+
/** Set the status code. Defaults to 200. */
|
|
71
|
+
status(statusCode: number): this;
|
|
72
|
+
/** Set a single response header (header names are stored lowercase). */
|
|
73
|
+
header(name: string, value: string): this;
|
|
74
|
+
/** Merge multiple response headers at once. */
|
|
75
|
+
setHeaders(headers: Record<string, string>): this;
|
|
76
|
+
getHeader(name: string): string | undefined;
|
|
77
|
+
removeHeader(name: string): this;
|
|
78
|
+
/**
|
|
79
|
+
* Set `Content-Type`. Accepts a full media type (`"image/png"`) or one of the shorthands
|
|
80
|
+
* `json`, `text`, `html`, `xml`, `bin`.
|
|
81
|
+
*/
|
|
82
|
+
type(contentType: string): this;
|
|
83
|
+
/** Append a `Set-Cookie` value. Can be called multiple times for multiple cookies. */
|
|
84
|
+
cookie(name: string, value: string, options?: CookieOptions): this;
|
|
85
|
+
/** Append a `Set-Cookie` value that expires the cookie immediately. */
|
|
86
|
+
clearCookie(name: string, options?: Omit<CookieOptions, "expires" | "maxAge">): this;
|
|
87
|
+
/** JSON-serialize the body and set `Content-Type: application/json` (unless already set). */
|
|
88
|
+
json(body?: any): this;
|
|
89
|
+
/**
|
|
90
|
+
* Set the body, inferring `Content-Type` when it hasn't been set: `text/html` for a string,
|
|
91
|
+
* `application/octet-stream` for a Buffer/Uint8Array, `application/json` for anything else.
|
|
92
|
+
*/
|
|
93
|
+
send(body?: any): this;
|
|
94
|
+
/** Set the body with `Content-Type: text/plain`. */
|
|
95
|
+
text(body: string): this;
|
|
96
|
+
/** Set the body with `Content-Type: text/html`. */
|
|
97
|
+
html(body: string): this;
|
|
98
|
+
/** Set `Location` and a redirect status code (302 by default). */
|
|
99
|
+
redirect(url: string, statusCode?: number): this;
|
|
100
|
+
/** Set the body verbatim, without touching `Content-Type`. */
|
|
101
|
+
end(body?: any): this;
|
|
102
|
+
/**
|
|
103
|
+
* Stream the response as server-sent events: sets the SSE headers and wraps `source` in an
|
|
104
|
+
* {@link HttpStreamBody} so transports that can stream write each chunk as it is produced.
|
|
105
|
+
*
|
|
106
|
+
* Two of the headers are not obvious and are easy to omit, and omitting either yields a response
|
|
107
|
+
* that looks correct yet never arrives incrementally: `no-transform` stops CloudFront (and other
|
|
108
|
+
* proxies) compressing the body, which buffers chunks, and `x-accel-buffering: no` opts out of
|
|
109
|
+
* nginx-family response buffering.
|
|
110
|
+
*
|
|
111
|
+
* Framing is the caller's job — `source` should yield complete `data: ...\n\n` records.
|
|
112
|
+
*/
|
|
113
|
+
sse(source: HttpStreamSource): this;
|
|
114
|
+
/** Materialize the transport-agnostic response. Called by the router; routes rarely need it. */
|
|
115
|
+
toResponse(): IHttpResponse;
|
|
116
|
+
}
|
|
15
117
|
export interface IHttpRoute {
|
|
118
|
+
/**
|
|
119
|
+
* Handle the request. Either return a response (a plain {@link IHttpResponse}, or the
|
|
120
|
+
* {@link IHttpResponseBuilder} passed in as `response`), or mutate `response` and return
|
|
121
|
+
* nothing.
|
|
122
|
+
*/
|
|
123
|
+
handle(request: IHttpRequest, response: IHttpResponseBuilder): Promise<IHttpResponse | IHttpResponseBuilder | void>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a route IS, separated from what it DOES.
|
|
127
|
+
*
|
|
128
|
+
* `method` and `path` are plain data, so the router matches a request without building anything.
|
|
129
|
+
* `handler` is the route's implementation class — declared with
|
|
130
|
+
* `HttpRouteHandler.createImplementation`, so it carries its own dependency metadata — and the
|
|
131
|
+
* router builds it only once this definition matches.
|
|
132
|
+
*
|
|
133
|
+
* Resolving every route just to read its path used to drag in each one's whole dependency graph, so
|
|
134
|
+
* a request for a static asset built the entire GraphQL engine, every contextual schema and the AI
|
|
135
|
+
* provider before finding the route it wanted.
|
|
136
|
+
*/
|
|
137
|
+
export interface IHttpRouteDefinition {
|
|
138
|
+
/**
|
|
139
|
+
* Identifies this route so a decorator can pick it out.
|
|
140
|
+
*
|
|
141
|
+
* Decorators DO reach `HttpRouteDefinition` (unlike `HttpRouteHandler`, which the router builds
|
|
142
|
+
* directly), so decorating it hands you every route in turn — `name` is how you tell which one
|
|
143
|
+
* you have, and whether to wrap its handler or pass it through.
|
|
144
|
+
*/
|
|
145
|
+
readonly name: string;
|
|
16
146
|
readonly method: string;
|
|
17
147
|
readonly path: string;
|
|
18
|
-
|
|
148
|
+
readonly handler: Constructor<IHttpRoute>;
|
|
19
149
|
}
|
|
20
150
|
export interface IHttpRouter {
|
|
21
151
|
route(request: IHttpRequest): Promise<IHttpResponse>;
|
|
22
152
|
}
|
|
23
|
-
export declare const
|
|
153
|
+
export declare const HttpRouteDefinition: Abstraction<IHttpRouteDefinition>;
|
|
154
|
+
/**
|
|
155
|
+
* Declares a route's request handling. One shared abstraction: the router never resolves it from the
|
|
156
|
+
* container (that would build every route), it builds the ONE class a matched definition points at.
|
|
157
|
+
* Declaring implementations through it is what attaches their dependency metadata.
|
|
158
|
+
*/
|
|
159
|
+
export declare const HttpRouteHandler: Abstraction<IHttpRoute>;
|
|
24
160
|
export declare const HttpRouter: Abstraction<IHttpRouter>;
|
|
25
|
-
export declare namespace
|
|
161
|
+
export declare namespace HttpRouteDefinition {
|
|
162
|
+
type Interface = IHttpRouteDefinition;
|
|
163
|
+
}
|
|
164
|
+
export declare namespace HttpRouteHandler {
|
|
26
165
|
type Interface = IHttpRoute;
|
|
166
|
+
/**
|
|
167
|
+
* The request handed to `handle()`: an {@link IHttpRequest} that has been through the router,
|
|
168
|
+
* so {@link IMatchedRouteDefinition} is guaranteed rather than optional.
|
|
169
|
+
*/
|
|
170
|
+
type Request = IHttpRequest & {
|
|
171
|
+
route: IMatchedRouteDefinition;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The response handed to `handle()` — the mutable builder, not the materialized
|
|
175
|
+
* {@link IHttpResponse}. Shorthand for {@link IHttpResponseBuilder}.
|
|
176
|
+
*/
|
|
177
|
+
type Response = IHttpResponseBuilder;
|
|
27
178
|
}
|
|
28
179
|
export declare namespace HttpRouter {
|
|
29
180
|
type Interface = IHttpRouter;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Abstraction } from "@webiny/di";
|
|
2
|
-
const
|
|
2
|
+
const HttpRouteDefinition = new Abstraction("HttpRouteDefinition");
|
|
3
|
+
const HttpRouteHandler = new Abstraction("HttpRouteHandler");
|
|
3
4
|
const HttpRouter = new Abstraction("HttpRouter");
|
|
4
5
|
class RouteNotFoundError extends Error {
|
|
5
6
|
constructor(method, path){
|
|
6
7
|
super(`Route not found: ${method} ${path}`), this.code = "ROUTE_NOT_FOUND";
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
|
-
export {
|
|
10
|
+
export { HttpRouteDefinition, HttpRouteHandler, HttpRouter, RouteNotFoundError };
|
|
10
11
|
|
|
11
12
|
//# sourceMappingURL=abstractions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/http/abstractions.js","sources":["../../../src/features/http/abstractions.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\n\nexport interface IHttpRequest {\n method: string;\n path: string;\n headers: Record<string, string>;\n query: Record<string, string>;\n pathParameters: Record<string, string>;\n body: any;\n}\n\nexport interface IHttpResponse {\n statusCode: number;\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface
|
|
1
|
+
{"version":3,"file":"features/http/abstractions.js","sources":["../../../src/features/http/abstractions.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\nimport type { Constructor } from \"@webiny/di\";\nimport type { HttpStreamSource } from \"./HttpStreamBody.js\";\n\n/**\n * Which route the router matched, as seen from inside `handle()` — the `req.route` of Express-style\n * handlers. It is the definition without its `handler`, so a route (or anything wrapping one) can\n * tell WHICH route is running and act only on the one it cares about.\n */\nexport interface IMatchedRouteDefinition {\n readonly name: string;\n readonly method: string;\n readonly path: string;\n}\n\nexport interface IHttpRequest {\n method: string;\n path: string;\n headers: Record<string, string>;\n query: Record<string, string>;\n pathParameters: Record<string, string>;\n body: any;\n /**\n * Set by `HttpRouter` once a route matches, so it is absent on the request a transport builds\n * and always present by the time `handle()` runs. Handlers should take\n * {@link HttpRouteHandler.Request}, where it is required.\n */\n route?: IMatchedRouteDefinition;\n}\n\nexport interface IHttpResponse {\n statusCode: number;\n headers?: Record<string, string>;\n /**\n * Serialized `Set-Cookie` values. A separate field (not a header) because `Set-Cookie` is the\n * one response header that can legally repeat, which `headers: Record<string, string>` cannot\n * express. Transports map it to whatever their protocol needs — API Gateway's\n * `multiValueHeaders` / `cookies`, or a repeated header on a Node `ServerResponse`.\n */\n cookies?: string[];\n body?: any;\n}\n\nexport interface CookieOptions {\n domain?: string;\n path?: string;\n expires?: Date;\n /**\n * Cookie lifetime in SECONDS — the `Max-Age` attribute value as the HTTP spec defines it.\n * Note this differs from Express, whose `maxAge` is in milliseconds.\n */\n maxAge?: number;\n httpOnly?: boolean;\n secure?: boolean;\n partitioned?: boolean;\n sameSite?: \"strict\" | \"lax\" | \"none\";\n}\n\n/**\n * The mutable response handed to every route as the second argument to `handle()` — the `res` of\n * Express-style handlers. Every method returns `this`, so a route can chain and then return the\n * builder directly:\n *\n * ```ts\n * async handle(request: IHttpRequest, response: IHttpResponseBuilder) {\n * return response.status(201).cookie(\"sid\", id, { httpOnly: true }).json({ id });\n * }\n * ```\n *\n * Returning it is optional: a route that mutates it and returns nothing gets the same response.\n * Routes returning a plain {@link IHttpResponse} keep working — anything set on the builder\n * (headers, cookies) is merged underneath the returned object, which wins on conflicts.\n */\nexport interface IHttpResponseBuilder {\n /** Set the status code. Defaults to 200. */\n status(statusCode: number): this;\n /** Set a single response header (header names are stored lowercase). */\n header(name: string, value: string): this;\n /** Merge multiple response headers at once. */\n setHeaders(headers: Record<string, string>): this;\n getHeader(name: string): string | undefined;\n removeHeader(name: string): this;\n /**\n * Set `Content-Type`. Accepts a full media type (`\"image/png\"`) or one of the shorthands\n * `json`, `text`, `html`, `xml`, `bin`.\n */\n type(contentType: string): this;\n /** Append a `Set-Cookie` value. Can be called multiple times for multiple cookies. */\n cookie(name: string, value: string, options?: CookieOptions): this;\n /** Append a `Set-Cookie` value that expires the cookie immediately. */\n clearCookie(name: string, options?: Omit<CookieOptions, \"expires\" | \"maxAge\">): this;\n /** JSON-serialize the body and set `Content-Type: application/json` (unless already set). */\n json(body?: any): this;\n /**\n * Set the body, inferring `Content-Type` when it hasn't been set: `text/html` for a string,\n * `application/octet-stream` for a Buffer/Uint8Array, `application/json` for anything else.\n */\n send(body?: any): this;\n /** Set the body with `Content-Type: text/plain`. */\n text(body: string): this;\n /** Set the body with `Content-Type: text/html`. */\n html(body: string): this;\n /** Set `Location` and a redirect status code (302 by default). */\n redirect(url: string, statusCode?: number): this;\n /** Set the body verbatim, without touching `Content-Type`. */\n end(body?: any): this;\n /**\n * Stream the response as server-sent events: sets the SSE headers and wraps `source` in an\n * {@link HttpStreamBody} so transports that can stream write each chunk as it is produced.\n *\n * Two of the headers are not obvious and are easy to omit, and omitting either yields a response\n * that looks correct yet never arrives incrementally: `no-transform` stops CloudFront (and other\n * proxies) compressing the body, which buffers chunks, and `x-accel-buffering: no` opts out of\n * nginx-family response buffering.\n *\n * Framing is the caller's job — `source` should yield complete `data: ...\\n\\n` records.\n */\n sse(source: HttpStreamSource): this;\n /** Materialize the transport-agnostic response. Called by the router; routes rarely need it. */\n toResponse(): IHttpResponse;\n}\n\nexport interface IHttpRoute {\n /**\n * Handle the request. Either return a response (a plain {@link IHttpResponse}, or the\n * {@link IHttpResponseBuilder} passed in as `response`), or mutate `response` and return\n * nothing.\n */\n handle(\n request: IHttpRequest,\n response: IHttpResponseBuilder\n ): Promise<IHttpResponse | IHttpResponseBuilder | void>;\n}\n\n/**\n * What a route IS, separated from what it DOES.\n *\n * `method` and `path` are plain data, so the router matches a request without building anything.\n * `handler` is the route's implementation class — declared with\n * `HttpRouteHandler.createImplementation`, so it carries its own dependency metadata — and the\n * router builds it only once this definition matches.\n *\n * Resolving every route just to read its path used to drag in each one's whole dependency graph, so\n * a request for a static asset built the entire GraphQL engine, every contextual schema and the AI\n * provider before finding the route it wanted.\n */\nexport interface IHttpRouteDefinition {\n /**\n * Identifies this route so a decorator can pick it out.\n *\n * Decorators DO reach `HttpRouteDefinition` (unlike `HttpRouteHandler`, which the router builds\n * directly), so decorating it hands you every route in turn — `name` is how you tell which one\n * you have, and whether to wrap its handler or pass it through.\n */\n readonly name: string;\n readonly method: string;\n readonly path: string;\n readonly handler: Constructor<IHttpRoute>;\n}\n\nexport interface IHttpRouter {\n route(request: IHttpRequest): Promise<IHttpResponse>;\n}\n\nexport const HttpRouteDefinition = new Abstraction<IHttpRouteDefinition>(\"HttpRouteDefinition\");\n/**\n * Declares a route's request handling. One shared abstraction: the router never resolves it from the\n * container (that would build every route), it builds the ONE class a matched definition points at.\n * Declaring implementations through it is what attaches their dependency metadata.\n */\nexport const HttpRouteHandler = new Abstraction<IHttpRoute>(\"HttpRouteHandler\");\nexport const HttpRouter = new Abstraction<IHttpRouter>(\"HttpRouter\");\n\nexport namespace HttpRouteDefinition {\n export type Interface = IHttpRouteDefinition;\n}\n\nexport namespace HttpRouteHandler {\n export type Interface = IHttpRoute;\n /**\n * The request handed to `handle()`: an {@link IHttpRequest} that has been through the router,\n * so {@link IMatchedRouteDefinition} is guaranteed rather than optional.\n */\n export type Request = IHttpRequest & { route: IMatchedRouteDefinition };\n /**\n * The response handed to `handle()` — the mutable builder, not the materialized\n * {@link IHttpResponse}. Shorthand for {@link IHttpResponseBuilder}.\n */\n export type Response = IHttpResponseBuilder;\n}\n\nexport namespace HttpRouter {\n export type Interface = IHttpRouter;\n}\n\nexport class RouteNotFoundError extends Error {\n readonly code = \"ROUTE_NOT_FOUND\" as const;\n\n constructor(method: string, path: string) {\n super(`Route not found: ${method} ${path}`);\n }\n}\n"],"names":["HttpRouteDefinition","Abstraction","HttpRouteHandler","HttpRouter","RouteNotFoundError","Error","method","path"],"mappings":";AAoKO,MAAMA,sBAAsB,IAAIC,YAAkC;AAMlE,MAAMC,mBAAmB,IAAID,YAAwB;AACrD,MAAME,aAAa,IAAIF,YAAyB;AAwBhD,MAAMG,2BAA2BC;IAGpC,YAAYC,MAAc,EAAEC,IAAY,CAAE;QACtC,KAAK,CAAC,CAAC,iBAAiB,EAAED,OAAO,CAAC,EAAEC,MAAM,QAHrC,IAAI,GAAG;IAIhB;AACJ"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Constructor } from "@webiny/di";
|
|
2
|
+
import type { IHttpRoute, IHttpRouteDefinition } from "../../features/http/abstractions.js";
|
|
3
|
+
export interface ICreateHttpRouteDefinitionParams {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly method: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly handler: Constructor<IHttpRoute>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds an `HttpRouteDefinition` implementation from plain values.
|
|
11
|
+
*
|
|
12
|
+
* For callers that only learn a route's method and path at build time — the `Api.Route` extension
|
|
13
|
+
* generates its registration from the props in `webiny.config.tsx`, so the handler file never
|
|
14
|
+
* restates them and the two can't drift. Hand-written routes should declare the definition class
|
|
15
|
+
* directly instead; it reads better and keeps the values next to the handler.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createHttpRouteDefinition(params: ICreateHttpRouteDefinitionParams): Constructor<IHttpRouteDefinition>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HttpRouteDefinition } from "./abstractions.js";
|
|
2
|
+
function createHttpRouteDefinition(params) {
|
|
3
|
+
class GeneratedHttpRouteDefinition {
|
|
4
|
+
constructor(){
|
|
5
|
+
this.name = params.name;
|
|
6
|
+
this.method = params.method;
|
|
7
|
+
this.path = params.path;
|
|
8
|
+
this.handler = params.handler;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return HttpRouteDefinition.createImplementation({
|
|
12
|
+
implementation: GeneratedHttpRouteDefinition,
|
|
13
|
+
dependencies: []
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export { createHttpRouteDefinition };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=createHttpRouteDefinition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/http/createHttpRouteDefinition.js","sources":["../../../src/features/http/createHttpRouteDefinition.ts"],"sourcesContent":["import type { Constructor } from \"@webiny/di\";\nimport { HttpRouteDefinition } from \"~/features/http/abstractions.js\";\nimport type { IHttpRoute, IHttpRouteDefinition } from \"~/features/http/abstractions.js\";\n\nexport interface ICreateHttpRouteDefinitionParams {\n readonly name: string;\n readonly method: string;\n readonly path: string;\n readonly handler: Constructor<IHttpRoute>;\n}\n\n/**\n * Builds an `HttpRouteDefinition` implementation from plain values.\n *\n * For callers that only learn a route's method and path at build time — the `Api.Route` extension\n * generates its registration from the props in `webiny.config.tsx`, so the handler file never\n * restates them and the two can't drift. Hand-written routes should declare the definition class\n * directly instead; it reads better and keeps the values next to the handler.\n */\nexport function createHttpRouteDefinition(\n params: ICreateHttpRouteDefinitionParams\n): Constructor<IHttpRouteDefinition> {\n class GeneratedHttpRouteDefinition implements HttpRouteDefinition.Interface {\n readonly name = params.name;\n readonly method = params.method;\n readonly path = params.path;\n readonly handler = params.handler;\n }\n\n return HttpRouteDefinition.createImplementation({\n implementation: GeneratedHttpRouteDefinition,\n dependencies: []\n });\n}\n"],"names":["createHttpRouteDefinition","params","GeneratedHttpRouteDefinition","HttpRouteDefinition"],"mappings":";AAmBO,SAASA,0BACZC,MAAwC;IAExC,MAAMC;;iBACO,IAAI,GAAGD,OAAO,IAAI;iBAClB,MAAM,GAAGA,OAAO,MAAM;iBACtB,IAAI,GAAGA,OAAO,IAAI;iBAClB,OAAO,GAAGA,OAAO,OAAO;;IACrC;IAEA,OAAOE,oBAAoB,oBAAoB,CAAC;QAC5C,gBAAgBD;QAChB,cAAc,EAAE;IACpB;AACJ"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IHttpRouter, IHttpRequest, IHttpResponse } from "../../../features/http/abstractions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compresses HTTP response bodies with brotli/gzip when the client accepts it.
|
|
4
|
+
*
|
|
5
|
+
* Why: large command-model / entry GraphQL responses were hitting the API Gateway response-size
|
|
6
|
+
* limit and failing. The previous Fastify stack compressed responses (@fastify/compress); that was
|
|
7
|
+
* lost in the move to the event-handler / httpRoute architecture. This restores it transport-
|
|
8
|
+
* agnostically — the compressed body is emitted as a `Buffer`, which both terminal translators
|
|
9
|
+
* already handle (API Gateway base64-encodes it with `isBase64Encoded: true`; the Node server writes
|
|
10
|
+
* the raw bytes), so no translator changes are needed.
|
|
11
|
+
*
|
|
12
|
+
* Registered as the OUTERMOST HTTP decorator so it compresses the fully-formed response (after CORS
|
|
13
|
+
* and other headers are set).
|
|
14
|
+
*/
|
|
15
|
+
declare class CompressionDecoratorImpl implements IHttpRouter {
|
|
16
|
+
private decoratee;
|
|
17
|
+
private static readonly THRESHOLD_BYTES;
|
|
18
|
+
constructor(decoratee: IHttpRouter);
|
|
19
|
+
route(request: IHttpRequest): Promise<IHttpResponse>;
|
|
20
|
+
private static getHeader;
|
|
21
|
+
private static serializeBody;
|
|
22
|
+
private static negotiateEncoding;
|
|
23
|
+
private static mergeVary;
|
|
24
|
+
}
|
|
25
|
+
export declare const CompressionDecorator: typeof CompressionDecoratorImpl & {
|
|
26
|
+
__abstraction: import("@webiny/di").Abstraction<IHttpRouter>;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import node_zlib from "node:zlib";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { HttpRouter } from "../abstractions.js";
|
|
4
|
+
import { HttpStreamBody } from "../HttpStreamBody.js";
|
|
5
|
+
const gzip = promisify(node_zlib.gzip);
|
|
6
|
+
const brotli = promisify(node_zlib.brotliCompress);
|
|
7
|
+
class CompressionDecoratorImpl {
|
|
8
|
+
static{
|
|
9
|
+
this.THRESHOLD_BYTES = 1024;
|
|
10
|
+
}
|
|
11
|
+
constructor(decoratee){
|
|
12
|
+
this.decoratee = decoratee;
|
|
13
|
+
}
|
|
14
|
+
async route(request) {
|
|
15
|
+
const response = await this.decoratee.route(request);
|
|
16
|
+
if (CompressionDecoratorImpl.getHeader(response.headers, "content-encoding")) return response;
|
|
17
|
+
const acceptEncoding = CompressionDecoratorImpl.getHeader(request.headers, "accept-encoding") || "";
|
|
18
|
+
const encoding = CompressionDecoratorImpl.negotiateEncoding(acceptEncoding);
|
|
19
|
+
if (!encoding) return response;
|
|
20
|
+
const serialized = CompressionDecoratorImpl.serializeBody(response.body);
|
|
21
|
+
if (!serialized || serialized.length < CompressionDecoratorImpl.THRESHOLD_BYTES) return response;
|
|
22
|
+
const compressed = "br" === encoding ? await brotli(serialized) : await gzip(serialized);
|
|
23
|
+
return {
|
|
24
|
+
...response,
|
|
25
|
+
body: compressed,
|
|
26
|
+
headers: {
|
|
27
|
+
...response.headers,
|
|
28
|
+
"content-encoding": encoding,
|
|
29
|
+
"content-length": String(compressed.length),
|
|
30
|
+
vary: CompressionDecoratorImpl.mergeVary(CompressionDecoratorImpl.getHeader(response.headers, "vary"))
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
static getHeader(headers, name) {
|
|
35
|
+
if (!headers) return;
|
|
36
|
+
const lower = name.toLowerCase();
|
|
37
|
+
for(const key in headers)if (key.toLowerCase() === lower) return headers[key];
|
|
38
|
+
}
|
|
39
|
+
static serializeBody(body) {
|
|
40
|
+
if (null == body) return null;
|
|
41
|
+
if (HttpStreamBody.is(body)) return null;
|
|
42
|
+
if ("string" == typeof body) return Buffer.from(body, "utf8");
|
|
43
|
+
if (Buffer.isBuffer(body) || body instanceof Uint8Array) return null;
|
|
44
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
45
|
+
}
|
|
46
|
+
static negotiateEncoding(acceptEncoding) {
|
|
47
|
+
const value = acceptEncoding.toLowerCase();
|
|
48
|
+
if (value.includes("br")) return "br";
|
|
49
|
+
if (value.includes("gzip")) return "gzip";
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
static mergeVary(existing) {
|
|
53
|
+
const parts = new Set((existing || "").split(",").map((part)=>part.trim().toLowerCase()).filter(Boolean));
|
|
54
|
+
parts.add("accept-encoding");
|
|
55
|
+
return [
|
|
56
|
+
...parts
|
|
57
|
+
].join(", ");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const CompressionDecorator = HttpRouter.createDecorator({
|
|
61
|
+
decorator: CompressionDecoratorImpl,
|
|
62
|
+
dependencies: []
|
|
63
|
+
});
|
|
64
|
+
export { CompressionDecorator };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=CompressionDecorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/http/decorators/CompressionDecorator.js","sources":["../../../../src/features/http/decorators/CompressionDecorator.ts"],"sourcesContent":["import zlib from \"node:zlib\";\nimport { promisify } from \"node:util\";\nimport { HttpRouter } from \"~/features/http/abstractions.js\";\nimport type { IHttpRouter, IHttpRequest, IHttpResponse } from \"~/features/http/abstractions.js\";\nimport { HttpStreamBody } from \"~/features/http/HttpStreamBody.js\";\n\nconst gzip = promisify(zlib.gzip);\nconst brotli = promisify(zlib.brotliCompress);\n\n/**\n * Compresses HTTP response bodies with brotli/gzip when the client accepts it.\n *\n * Why: large command-model / entry GraphQL responses were hitting the API Gateway response-size\n * limit and failing. The previous Fastify stack compressed responses (@fastify/compress); that was\n * lost in the move to the event-handler / httpRoute architecture. This restores it transport-\n * agnostically — the compressed body is emitted as a `Buffer`, which both terminal translators\n * already handle (API Gateway base64-encodes it with `isBase64Encoded: true`; the Node server writes\n * the raw bytes), so no translator changes are needed.\n *\n * Registered as the OUTERMOST HTTP decorator so it compresses the fully-formed response (after CORS\n * and other headers are set).\n */\nclass CompressionDecoratorImpl implements IHttpRouter {\n // Don't compress tiny payloads — the gzip/br framing overhead can make them LARGER, and the CPU\n // cost isn't worth it. Matches the threshold used by the old Fastify (@fastify/compress) setup.\n private static readonly THRESHOLD_BYTES = 1024;\n\n constructor(private decoratee: IHttpRouter) {}\n\n async route(request: IHttpRequest): Promise<IHttpResponse> {\n const response = await this.decoratee.route(request);\n\n // Never double-encode a body a route already compressed itself.\n if (CompressionDecoratorImpl.getHeader(response.headers, \"content-encoding\")) {\n return response;\n }\n\n const acceptEncoding =\n CompressionDecoratorImpl.getHeader(request.headers, \"accept-encoding\") || \"\";\n const encoding = CompressionDecoratorImpl.negotiateEncoding(acceptEncoding);\n if (!encoding) {\n return response;\n }\n\n const serialized = CompressionDecoratorImpl.serializeBody(response.body);\n if (!serialized || serialized.length < CompressionDecoratorImpl.THRESHOLD_BYTES) {\n return response;\n }\n\n const compressed = encoding === \"br\" ? await brotli(serialized) : await gzip(serialized);\n\n return {\n ...response,\n body: compressed,\n headers: {\n ...response.headers,\n \"content-encoding\": encoding,\n \"content-length\": String(compressed.length),\n vary: CompressionDecoratorImpl.mergeVary(\n CompressionDecoratorImpl.getHeader(response.headers, \"vary\")\n )\n }\n };\n }\n\n // Case-insensitive header lookup. Header casing isn't guaranteed across transports (Node\n // lowercases them, API Gateway payload formats vary), so we can't index by a fixed key.\n private static getHeader(\n headers: Record<string, string> | undefined,\n name: string\n ): string | undefined {\n if (!headers) {\n return undefined;\n }\n const lower = name.toLowerCase();\n for (const key in headers) {\n if (key.toLowerCase() === lower) {\n return headers[key];\n }\n }\n return undefined;\n }\n\n // Serialize the response body to the exact bytes the terminal transport would have written, so\n // the size check and the compressed output match what the client receives. Binary bodies (Buffer\n // / Uint8Array — e.g. asset delivery images) are returned as `null`: they're already in a\n // compressed media format, so gzipping them wastes CPU and can even enlarge them.\n private static serializeBody(body: any): Buffer | null {\n if (body === undefined || body === null) {\n return null;\n }\n // A streaming body must never be touched. Compressing it would mean draining it here, which\n // defeats incremental delivery, and it must not fall through to the JSON branch below: that\n // would REPLACE the body with a gzipped `{\"source\":...}` and silently lose the stream. A\n // generator source stringifies to `{}` and so survives on size alone, but an object source\n // with enumerable data does not.\n if (HttpStreamBody.is(body)) {\n return null;\n }\n if (typeof body === \"string\") {\n return Buffer.from(body, \"utf8\");\n }\n if (Buffer.isBuffer(body) || body instanceof Uint8Array) {\n return null;\n }\n return Buffer.from(JSON.stringify(body), \"utf8\");\n }\n\n // Pick the best encoding the client accepts. Prefer brotli (better ratio → more headroom under\n // the API Gateway response-size limit) and fall back to gzip. Only encodings the client\n // explicitly lists are used, so non-browser clients that send `accept-encoding: gzip` still get a\n // body they can read.\n private static negotiateEncoding(acceptEncoding: string): \"br\" | \"gzip\" | null {\n const value = acceptEncoding.toLowerCase();\n if (value.includes(\"br\")) {\n return \"br\";\n }\n if (value.includes(\"gzip\")) {\n return \"gzip\";\n }\n return null;\n }\n\n private static mergeVary(existing: string | undefined): string {\n const parts = new Set(\n (existing || \"\")\n .split(\",\")\n .map(part => part.trim().toLowerCase())\n .filter(Boolean)\n );\n parts.add(\"accept-encoding\");\n return [...parts].join(\", \");\n }\n}\n\nexport const CompressionDecorator = HttpRouter.createDecorator({\n decorator: CompressionDecoratorImpl,\n dependencies: []\n});\n"],"names":["gzip","promisify","zlib","brotli","CompressionDecoratorImpl","decoratee","request","response","acceptEncoding","encoding","serialized","compressed","String","headers","name","lower","key","body","HttpStreamBody","Buffer","Uint8Array","JSON","value","existing","parts","Set","part","Boolean","CompressionDecorator","HttpRouter"],"mappings":";;;;AAMA,MAAMA,OAAOC,UAAUC,UAAAA,IAAS;AAChC,MAAMC,SAASF,UAAUC,UAAAA,cAAmB;AAe5C,MAAME;;aAGsB,eAAe,GAAG;;IAE1C,YAAoBC,SAAsB,CAAE;aAAxBA,SAAS,GAATA;IAAyB;IAE7C,MAAM,MAAMC,OAAqB,EAA0B;QACvD,MAAMC,WAAW,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAACD;QAG5C,IAAIF,yBAAyB,SAAS,CAACG,SAAS,OAAO,EAAE,qBACrD,OAAOA;QAGX,MAAMC,iBACFJ,yBAAyB,SAAS,CAACE,QAAQ,OAAO,EAAE,sBAAsB;QAC9E,MAAMG,WAAWL,yBAAyB,iBAAiB,CAACI;QAC5D,IAAI,CAACC,UACD,OAAOF;QAGX,MAAMG,aAAaN,yBAAyB,aAAa,CAACG,SAAS,IAAI;QACvE,IAAI,CAACG,cAAcA,WAAW,MAAM,GAAGN,yBAAyB,eAAe,EAC3E,OAAOG;QAGX,MAAMI,aAAaF,AAAa,SAAbA,WAAoB,MAAMN,OAAOO,cAAc,MAAMV,KAAKU;QAE7E,OAAO;YACH,GAAGH,QAAQ;YACX,MAAMI;YACN,SAAS;gBACL,GAAGJ,SAAS,OAAO;gBACnB,oBAAoBE;gBACpB,kBAAkBG,OAAOD,WAAW,MAAM;gBAC1C,MAAMP,yBAAyB,SAAS,CACpCA,yBAAyB,SAAS,CAACG,SAAS,OAAO,EAAE;YAE7D;QACJ;IACJ;IAIA,OAAe,UACXM,OAA2C,EAC3CC,IAAY,EACM;QAClB,IAAI,CAACD,SACD;QAEJ,MAAME,QAAQD,KAAK,WAAW;QAC9B,IAAK,MAAME,OAAOH,QACd,IAAIG,IAAI,WAAW,OAAOD,OACtB,OAAOF,OAAO,CAACG,IAAI;IAI/B;IAMA,OAAe,cAAcC,IAAS,EAAiB;QACnD,IAAIA,QAAAA,MACA,OAAO;QAOX,IAAIC,eAAe,EAAE,CAACD,OAClB,OAAO;QAEX,IAAI,AAAgB,YAAhB,OAAOA,MACP,OAAOE,OAAO,IAAI,CAACF,MAAM;QAE7B,IAAIE,OAAO,QAAQ,CAACF,SAASA,gBAAgBG,YACzC,OAAO;QAEX,OAAOD,OAAO,IAAI,CAACE,KAAK,SAAS,CAACJ,OAAO;IAC7C;IAMA,OAAe,kBAAkBT,cAAsB,EAAwB;QAC3E,MAAMc,QAAQd,eAAe,WAAW;QACxC,IAAIc,MAAM,QAAQ,CAAC,OACf,OAAO;QAEX,IAAIA,MAAM,QAAQ,CAAC,SACf,OAAO;QAEX,OAAO;IACX;IAEA,OAAe,UAAUC,QAA4B,EAAU;QAC3D,MAAMC,QAAQ,IAAIC,IACbF,AAAAA,CAAAA,YAAY,EAAC,EACT,KAAK,CAAC,KACN,GAAG,CAACG,CAAAA,OAAQA,KAAK,IAAI,GAAG,WAAW,IACnC,MAAM,CAACC;QAEhBH,MAAM,GAAG,CAAC;QACV,OAAO;eAAIA;SAAM,CAAC,IAAI,CAAC;IAC3B;AACJ;AAEO,MAAMI,uBAAuBC,WAAW,eAAe,CAAC;IAC3D,WAAWzB;IACX,cAAc,EAAE;AACpB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpRouter } from "../abstractions.js";
|
|
2
|
-
const ALLOWED_HEADERS = "accept, authorization, cache-control, content-type, x-i18n-locale, x-tenant";
|
|
2
|
+
const ALLOWED_HEADERS = "accept, authorization, cache-control, content-type, x-i18n-locale, x-tenant, x-webiny-authorization, x-amz-content-sha256";
|
|
3
3
|
class SecureHeadersDecoratorImpl {
|
|
4
4
|
constructor(decoratee){
|
|
5
5
|
this.decoratee = decoratee;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/http/decorators/SecureHeadersDecorator.js","sources":["../../../../src/features/http/decorators/SecureHeadersDecorator.ts"],"sourcesContent":["import { HttpRouter } from \"~/features/http/abstractions.js\";\nimport type { IHttpRouter, IHttpRequest, IHttpResponse } from \"~/features/http/abstractions.js\";\n\nconst ALLOWED_HEADERS = [\n \"accept\",\n \"authorization\",\n \"cache-control\",\n \"content-type\",\n \"x-i18n-locale\",\n \"x-tenant\"\n].join(\", \");\n\nclass SecureHeadersDecoratorImpl implements IHttpRouter {\n constructor(private decoratee: IHttpRouter) {}\n\n async route(request: IHttpRequest): Promise<IHttpResponse> {\n if (request.method === \"OPTIONS\") {\n return this.optionsResponse(request);\n }\n\n const response = await this.decoratee.route(request);\n\n return {\n ...response,\n headers: {\n ...response.headers,\n \"access-control-allow-origin\": request.headers[\"origin\"] || \"*\",\n \"access-control-allow-credentials\": \"true\",\n vary: \"origin\"\n }\n };\n }\n\n private optionsResponse(request: IHttpRequest): IHttpResponse {\n return {\n statusCode: 204,\n headers: {\n \"access-control-allow-origin\": request.headers[\"origin\"] || \"*\",\n \"access-control-allow-credentials\": \"true\",\n \"access-control-allow-methods\": \"OPTIONS,POST,GET,DELETE,PUT,PATCH\",\n \"access-control-allow-headers\": ALLOWED_HEADERS,\n \"access-control-max-age\": \"86400\",\n \"cache-control\": \"public, max-age=86400\"\n }\n };\n }\n}\n\nexport const SecureHeadersDecorator = HttpRouter.createDecorator({\n decorator: SecureHeadersDecoratorImpl,\n dependencies: []\n});\n"],"names":["ALLOWED_HEADERS","SecureHeadersDecoratorImpl","decoratee","request","response","SecureHeadersDecorator","HttpRouter"],"mappings":";AAGA,MAAMA,kBAAkB;
|
|
1
|
+
{"version":3,"file":"features/http/decorators/SecureHeadersDecorator.js","sources":["../../../../src/features/http/decorators/SecureHeadersDecorator.ts"],"sourcesContent":["import { HttpRouter } from \"~/features/http/abstractions.js\";\nimport type { IHttpRouter, IHttpRequest, IHttpResponse } from \"~/features/http/abstractions.js\";\n\nconst ALLOWED_HEADERS = [\n \"accept\",\n \"authorization\",\n \"cache-control\",\n \"content-type\",\n \"x-i18n-locale\",\n \"x-tenant\",\n // Streaming clients send the auth token here rather than in `Authorization`, which SigV4 occupies\n // when a Lambda Function URL sits behind CloudFront with Origin Access Control. Omitting it makes\n // the browser fail the preflight CORS check and never send the actual request.\n \"x-webiny-authorization\",\n // The SigV4 payload hash a streaming client sends when its request carries a body, because OAC\n // signs the request without hashing the body itself. Same consequence as above if omitted: the\n // browser fails the preflight and the real request is never sent.\n \"x-amz-content-sha256\"\n].join(\", \");\n\nclass SecureHeadersDecoratorImpl implements IHttpRouter {\n constructor(private decoratee: IHttpRouter) {}\n\n async route(request: IHttpRequest): Promise<IHttpResponse> {\n if (request.method === \"OPTIONS\") {\n return this.optionsResponse(request);\n }\n\n const response = await this.decoratee.route(request);\n\n return {\n ...response,\n headers: {\n ...response.headers,\n \"access-control-allow-origin\": request.headers[\"origin\"] || \"*\",\n \"access-control-allow-credentials\": \"true\",\n vary: \"origin\"\n }\n };\n }\n\n private optionsResponse(request: IHttpRequest): IHttpResponse {\n return {\n statusCode: 204,\n headers: {\n \"access-control-allow-origin\": request.headers[\"origin\"] || \"*\",\n \"access-control-allow-credentials\": \"true\",\n \"access-control-allow-methods\": \"OPTIONS,POST,GET,DELETE,PUT,PATCH\",\n \"access-control-allow-headers\": ALLOWED_HEADERS,\n \"access-control-max-age\": \"86400\",\n \"cache-control\": \"public, max-age=86400\"\n }\n };\n }\n}\n\nexport const SecureHeadersDecorator = HttpRouter.createDecorator({\n decorator: SecureHeadersDecoratorImpl,\n dependencies: []\n});\n"],"names":["ALLOWED_HEADERS","SecureHeadersDecoratorImpl","decoratee","request","response","SecureHeadersDecorator","HttpRouter"],"mappings":";AAGA,MAAMA,kBAAkB;AAiBxB,MAAMC;IACF,YAAoBC,SAAsB,CAAE;aAAxBA,SAAS,GAATA;IAAyB;IAE7C,MAAM,MAAMC,OAAqB,EAA0B;QACvD,IAAIA,AAAmB,cAAnBA,QAAQ,MAAM,EACd,OAAO,IAAI,CAAC,eAAe,CAACA;QAGhC,MAAMC,WAAW,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAACD;QAE5C,OAAO;YACH,GAAGC,QAAQ;YACX,SAAS;gBACL,GAAGA,SAAS,OAAO;gBACnB,+BAA+BD,QAAQ,OAAO,CAAC,SAAS,IAAI;gBAC5D,oCAAoC;gBACpC,MAAM;YACV;QACJ;IACJ;IAEQ,gBAAgBA,OAAqB,EAAiB;QAC1D,OAAO;YACH,YAAY;YACZ,SAAS;gBACL,+BAA+BA,QAAQ,OAAO,CAAC,SAAS,IAAI;gBAC5D,oCAAoC;gBACpC,gCAAgC;gBAChC,gCAAgCH;gBAChC,0BAA0B;gBAC1B,iBAAiB;YACrB;QACJ;IACJ;AACJ;AAEO,MAAMK,yBAAyBC,WAAW,eAAe,CAAC;IAC7D,WAAWL;IACX,cAAc,EAAE;AACpB"}
|
package/features/http/feature.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createFeature } from "@webiny/feature/api";
|
|
2
2
|
import { HttpRouterImpl } from "./HttpRouter.js";
|
|
3
3
|
import { SecureHeadersDecorator } from "./decorators/SecureHeadersDecorator.js";
|
|
4
|
-
import {
|
|
4
|
+
import { CompressionDecorator } from "./decorators/CompressionDecorator.js";
|
|
5
5
|
const HttpFeature = createFeature({
|
|
6
6
|
name: "Http",
|
|
7
7
|
register (container) {
|
|
8
8
|
container.register(HttpRouterImpl);
|
|
9
|
-
container.registerDecorator(RequestContextInitializerDecorator);
|
|
10
9
|
container.registerDecorator(SecureHeadersDecorator);
|
|
10
|
+
container.registerDecorator(CompressionDecorator);
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
export { HttpFeature };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/http/feature.js","sources":["../../../src/features/http/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { HttpRouterImpl } from \"~/features/http/HttpRouter.js\";\nimport { SecureHeadersDecorator } from \"~/features/http/decorators/SecureHeadersDecorator.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"features/http/feature.js","sources":["../../../src/features/http/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { HttpRouterImpl } from \"~/features/http/HttpRouter.js\";\nimport { SecureHeadersDecorator } from \"~/features/http/decorators/SecureHeadersDecorator.js\";\nimport { CompressionDecorator } from \"~/features/http/decorators/CompressionDecorator.js\";\n\nexport const HttpFeature = createFeature({\n name: \"Http\",\n register(container) {\n // Transient so per-request routes (e.g. GraphQLRoute) resolve from child container\n container.register(HttpRouterImpl);\n container.registerDecorator(SecureHeadersDecorator);\n // Registered last so it's the OUTERMOST decorator: it compresses the fully-formed response\n // (after CORS/other headers are set). Emits a gzip/br Buffer body — both terminal transports\n // already handle Buffers (API Gateway base64 + isBase64Encoded; Node server raw bytes).\n container.registerDecorator(CompressionDecorator);\n }\n});\n"],"names":["HttpFeature","createFeature","container","HttpRouterImpl","SecureHeadersDecorator","CompressionDecorator"],"mappings":";;;;AAKO,MAAMA,cAAcC,cAAc;IACrC,MAAM;IACN,UAASC,SAAS;QAEdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,iBAAiB,CAACE;QAI5BF,UAAU,iBAAiB,CAACG;IAChC;AACJ"}
|
package/features/http/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export * from "./abstractions.js";
|
|
2
2
|
export * from "./HttpRouter.js";
|
|
3
|
+
export * from "./HttpStreamBody.js";
|
|
4
|
+
export * from "./toSseFrame.js";
|
|
5
|
+
export * from "./HttpResponseBuilder.js";
|
|
6
|
+
export * from "./invokeHttpRoute.js";
|
|
3
7
|
export * from "./decorators/index.js";
|
|
4
8
|
export * from "./feature.js";
|
|
9
|
+
export * from "./createHttpRouteDefinition.js";
|
package/features/http/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export * from "./abstractions.js";
|
|
2
2
|
export * from "./HttpRouter.js";
|
|
3
|
+
export * from "./HttpStreamBody.js";
|
|
4
|
+
export * from "./toSseFrame.js";
|
|
5
|
+
export * from "./HttpResponseBuilder.js";
|
|
6
|
+
export * from "./invokeHttpRoute.js";
|
|
3
7
|
export * from "./decorators/index.js";
|
|
4
8
|
export * from "./feature.js";
|
|
9
|
+
export * from "./createHttpRouteDefinition.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HttpResponseBuilder } from "./HttpResponseBuilder.js";
|
|
2
|
+
import type { IHttpRequest, IHttpResponse, IHttpRoute } from "./abstractions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize whatever a route's `handle()` returned into an IHttpResponse.
|
|
5
|
+
*
|
|
6
|
+
* A route can (a) return the response builder it was handed, (b) mutate it and return nothing, or
|
|
7
|
+
* (c) return a plain IHttpResponse object — the pre-builder style. In case (c) anything set on the
|
|
8
|
+
* builder is merged UNDERNEATH the returned object, so a route can mix the two (e.g.
|
|
9
|
+
* `response.cookie(...)` plus a literal return) and the explicit return wins on conflicts.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toHttpResponse(result: unknown, builder: HttpResponseBuilder): IHttpResponse;
|
|
12
|
+
/**
|
|
13
|
+
* Run a single route and get its IHttpResponse — the same call + normalization `HttpRouter` does,
|
|
14
|
+
* minus the path matching. Use it to exercise a route directly (typically in tests) instead of
|
|
15
|
+
* calling `route.handle()` by hand, which would leave the response builder undefined.
|
|
16
|
+
*/
|
|
17
|
+
export declare function invokeHttpRoute(route: IHttpRoute, request: IHttpRequest): Promise<IHttpResponse>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HttpResponseBuilder } from "./HttpResponseBuilder.js";
|
|
2
|
+
function isResponseBuilder(value) {
|
|
3
|
+
return "object" == typeof value && null !== value && "function" == typeof value.toResponse;
|
|
4
|
+
}
|
|
5
|
+
function toHttpResponse(result, builder) {
|
|
6
|
+
if (null == result) return builder.toResponse();
|
|
7
|
+
if (isResponseBuilder(result)) return result.toResponse();
|
|
8
|
+
const response = result;
|
|
9
|
+
if (!builder.isModified()) return response;
|
|
10
|
+
const built = builder.toResponse();
|
|
11
|
+
const cookies = [
|
|
12
|
+
...built.cookies ?? [],
|
|
13
|
+
...response.cookies ?? []
|
|
14
|
+
];
|
|
15
|
+
return {
|
|
16
|
+
...response,
|
|
17
|
+
headers: {
|
|
18
|
+
...built.headers,
|
|
19
|
+
...response.headers
|
|
20
|
+
},
|
|
21
|
+
...cookies.length > 0 ? {
|
|
22
|
+
cookies
|
|
23
|
+
} : {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function invokeHttpRoute(route, request) {
|
|
27
|
+
const response = new HttpResponseBuilder();
|
|
28
|
+
return toHttpResponse(await route.handle(request, response), response);
|
|
29
|
+
}
|
|
30
|
+
export { invokeHttpRoute, toHttpResponse };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=invokeHttpRoute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/http/invokeHttpRoute.js","sources":["../../../src/features/http/invokeHttpRoute.ts"],"sourcesContent":["import { HttpResponseBuilder } from \"./HttpResponseBuilder.js\";\nimport type {\n IHttpRequest,\n IHttpResponse,\n IHttpResponseBuilder,\n IHttpRoute\n} from \"./abstractions.js\";\n\nfunction isResponseBuilder(value: unknown): value is IHttpResponseBuilder {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as IHttpResponseBuilder).toResponse === \"function\"\n );\n}\n\n/**\n * Normalize whatever a route's `handle()` returned into an IHttpResponse.\n *\n * A route can (a) return the response builder it was handed, (b) mutate it and return nothing, or\n * (c) return a plain IHttpResponse object — the pre-builder style. In case (c) anything set on the\n * builder is merged UNDERNEATH the returned object, so a route can mix the two (e.g.\n * `response.cookie(...)` plus a literal return) and the explicit return wins on conflicts.\n */\nexport function toHttpResponse(result: unknown, builder: HttpResponseBuilder): IHttpResponse {\n if (result === undefined || result === null) {\n return builder.toResponse();\n }\n\n if (isResponseBuilder(result)) {\n return result.toResponse();\n }\n\n const response = result as IHttpResponse;\n\n if (!builder.isModified()) {\n return response;\n }\n\n const built = builder.toResponse();\n const cookies = [...(built.cookies ?? []), ...(response.cookies ?? [])];\n\n return {\n ...response,\n headers: { ...built.headers, ...response.headers },\n ...(cookies.length > 0 ? { cookies } : {})\n };\n}\n\n/**\n * Run a single route and get its IHttpResponse — the same call + normalization `HttpRouter` does,\n * minus the path matching. Use it to exercise a route directly (typically in tests) instead of\n * calling `route.handle()` by hand, which would leave the response builder undefined.\n */\nexport async function invokeHttpRoute(\n route: IHttpRoute,\n request: IHttpRequest\n): Promise<IHttpResponse> {\n const response = new HttpResponseBuilder();\n return toHttpResponse(await route.handle(request, response), response);\n}\n"],"names":["isResponseBuilder","value","toHttpResponse","result","builder","response","built","cookies","invokeHttpRoute","route","request","HttpResponseBuilder"],"mappings":";AAQA,SAASA,kBAAkBC,KAAc;IACrC,OACI,AAAiB,YAAjB,OAAOA,SACPA,AAAU,SAAVA,SACA,AAAsD,cAAtD,OAAQA,MAA+B,UAAU;AAEzD;AAUO,SAASC,eAAeC,MAAe,EAAEC,OAA4B;IACxE,IAAID,QAAAA,QACA,OAAOC,QAAQ,UAAU;IAG7B,IAAIJ,kBAAkBG,SAClB,OAAOA,OAAO,UAAU;IAG5B,MAAME,WAAWF;IAEjB,IAAI,CAACC,QAAQ,UAAU,IACnB,OAAOC;IAGX,MAAMC,QAAQF,QAAQ,UAAU;IAChC,MAAMG,UAAU;WAAKD,MAAM,OAAO,IAAI,EAAE;WAAOD,SAAS,OAAO,IAAI,EAAE;KAAE;IAEvE,OAAO;QACH,GAAGA,QAAQ;QACX,SAAS;YAAE,GAAGC,MAAM,OAAO;YAAE,GAAGD,SAAS,OAAO;QAAC;QACjD,GAAIE,QAAQ,MAAM,GAAG,IAAI;YAAEA;QAAQ,IAAI,CAAC,CAAC;IAC7C;AACJ;AAOO,eAAeC,gBAClBC,KAAiB,EACjBC,OAAqB;IAErB,MAAML,WAAW,IAAIM;IACrB,OAAOT,eAAe,MAAMO,MAAM,MAAM,CAACC,SAASL,WAAWA;AACjE"}
|