1 | "use strict";
|
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
4 | };
|
5 | Object.defineProperty(exports, "__esModule", { value: true });
|
6 | exports.fromRequest = exports.redirect = exports.RedirectError = exports.httpError = exports.HttpError = exports.route = exports.router = exports.notFound = exports.send = exports.json = exports.text = exports.buffer = exports.getHeader = exports.serve = void 0;
|
7 | const content_type_1 = __importDefault(require("content-type"));
|
8 | const path_to_regexp_1 = require("path-to-regexp");
|
9 | const raw_body_1 = __importDefault(require("raw-body"));
|
10 | const stream_1 = require("stream");
|
11 | const url_1 = require("url");
|
12 | const IS_DEV = process.env.NODE_ENV === 'development';
|
13 | function serve(handler, options = {}) {
|
14 | return async function (req, res) {
|
15 | const serverRequest = requestFromHTTP(req, options);
|
16 | const serverResponse = responseFromHTTP(res);
|
17 | try {
|
18 | await handler(serverRequest, serverResponse);
|
19 | }
|
20 | catch (error) {
|
21 | if (res.writableEnded)
|
22 | throw error;
|
23 | if (error instanceof RedirectError) {
|
24 | res.statusCode = error.statusCode;
|
25 | res.setHeader('Location', error.location);
|
26 | res.end();
|
27 | return;
|
28 | }
|
29 | const errorHandler = options.errorHandler ?? ((_, res, error) => sendError(res, error));
|
30 | errorHandler(serverRequest, serverResponse, error);
|
31 | }
|
32 | };
|
33 | }
|
34 | exports.serve = serve;
|
35 |
|
36 | const protocolFromRequest = fromRequest((req, options) => {
|
37 | const socketProtocol = Boolean(req.socket.encrypted) ? 'https' : 'http';
|
38 | if (!options.trustProxy)
|
39 | return socketProtocol;
|
40 | const headerProtocol = getHeader(req, 'x-forwarded-proto') ?? socketProtocol;
|
41 | const commaIndex = headerProtocol.indexOf(',');
|
42 | return commaIndex === -1 ? headerProtocol.trim() : headerProtocol.substring(0, commaIndex).trim();
|
43 | });
|
44 | const queryFromRequest = fromRequest((req) => {
|
45 | return Object.fromEntries(req.parsedURL.searchParams);
|
46 | });
|
47 | const urlFromRequest = fromRequest((req) => {
|
48 | return new url_1.URL(req.url, `${req.protocol}://${req.headers.host}`);
|
49 | });
|
50 | function requestFromHTTP(req, options) {
|
51 | const serverRequest = Object.defineProperties(req, {
|
52 | protocol: { get: () => protocolFromRequest(serverRequest, options), enumerable: true },
|
53 | query: { get: () => queryFromRequest(serverRequest), enumerable: true },
|
54 | parsedURL: { get: () => urlFromRequest(serverRequest), enumerable: true },
|
55 | });
|
56 | return serverRequest;
|
57 | }
|
58 | function getHeader(req, header) {
|
59 | const value = req.headers[header];
|
60 | return Array.isArray(value) ? value[0] : value;
|
61 | }
|
62 | exports.getHeader = getHeader;
|
63 | const requestBodyMap = new WeakMap();
|
64 | async function buffer(req, { limit = '1mb', encoding } = {}) {
|
65 | const type = req.headers['content-type'] ?? 'text/plain';
|
66 | const length = req.headers['content-length'];
|
67 | if (encoding === undefined) {
|
68 | encoding = content_type_1.default.parse(type).parameters.charset;
|
69 | }
|
70 | const existingBody = requestBodyMap.get(req);
|
71 | if (existingBody)
|
72 | return existingBody;
|
73 | try {
|
74 | const body = Buffer.from(await (0, raw_body_1.default)(req, { limit, length, encoding }));
|
75 | requestBodyMap.set(req, body);
|
76 | return body;
|
77 | }
|
78 | catch (error) {
|
79 | if (error.type === 'entity.too.large') {
|
80 | throw httpError(413, `Body exceeded ${limit} limit`, error);
|
81 | }
|
82 | throw httpError(400, 'Invalid body', error);
|
83 | }
|
84 | }
|
85 | exports.buffer = buffer;
|
86 | async function text(req, options = {}) {
|
87 | return await buffer(req, options).then((body) => body.toString());
|
88 | }
|
89 | exports.text = text;
|
90 | async function json(req, options = {}) {
|
91 | return await text(req, options).then((body) => {
|
92 | try {
|
93 | return JSON.parse(body);
|
94 | }
|
95 | catch (error) {
|
96 | throw httpError(400, 'Invalid JSON', error);
|
97 | }
|
98 | });
|
99 | }
|
100 | exports.json = json;
|
101 |
|
102 | function responseFromHTTP(res) {
|
103 | const serverResponse = Object.defineProperties(res, {});
|
104 | return serverResponse;
|
105 | }
|
106 | function send(res, code, body = null) {
|
107 | res.statusCode = code;
|
108 | if (body === null || body === undefined) {
|
109 | res.end();
|
110 | return;
|
111 | }
|
112 |
|
113 | if (body instanceof Error) {
|
114 | throw body;
|
115 | }
|
116 | if (body instanceof stream_1.Stream || isReadableStream(body)) {
|
117 | if (!res.getHeader('Content-Type')) {
|
118 | res.setHeader('Content-Type', 'application/octet-stream');
|
119 | }
|
120 | body.pipe(res);
|
121 | return;
|
122 | }
|
123 | if (Buffer.isBuffer(body)) {
|
124 | if (!res.getHeader('Content-Type')) {
|
125 | res.setHeader('Content-Type', 'application/octet-stream');
|
126 | }
|
127 | res.setHeader('Content-Length', body.length);
|
128 | res.end(body);
|
129 | return;
|
130 | }
|
131 | let stringifiedBody;
|
132 | if (typeof body === 'object' || typeof body === 'number') {
|
133 | stringifiedBody = JSON.stringify(body);
|
134 | if (!res.getHeader('Content-Type')) {
|
135 | res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
136 | }
|
137 | }
|
138 | else {
|
139 | stringifiedBody = body;
|
140 | }
|
141 | res.setHeader('Content-Length', Buffer.byteLength(stringifiedBody));
|
142 | res.end(stringifiedBody);
|
143 | }
|
144 | exports.send = send;
|
145 | function sendError(res, error) {
|
146 | if (error instanceof HttpError) {
|
147 | send(res, error.statusCode, error.message);
|
148 | }
|
149 | else if (error instanceof Error) {
|
150 | send(res, 500, IS_DEV ? error.stack : error.message);
|
151 | }
|
152 | else {
|
153 | send(res, 500, `${error}`);
|
154 | }
|
155 | }
|
156 | function notFound() {
|
157 | return httpError(404, 'Not Found');
|
158 | }
|
159 | exports.notFound = notFound;
|
160 |
|
161 | function router(...handlers) {
|
162 | return async function (req, res) {
|
163 | for (const current of handlers) {
|
164 | if (req.method !== current.method)
|
165 | continue;
|
166 | const match = current.matchPath(req.parsedURL.pathname);
|
167 | if (!match)
|
168 | continue;
|
169 | req.params = match.params;
|
170 | return await current(req, res);
|
171 | }
|
172 | return send(res, 404, 'Not Found');
|
173 | };
|
174 | }
|
175 | exports.router = router;
|
176 |
|
177 | function route(method, path, handler) {
|
178 | const routeHandler = async (req, res) => {
|
179 | const responseBody = await Promise.resolve(handler(req, res));
|
180 | if (responseBody === null)
|
181 | return send(res, 204, null);
|
182 | if (responseBody === undefined)
|
183 | return;
|
184 | send(res, res.statusCode ?? 200, responseBody);
|
185 | };
|
186 | return Object.assign(routeHandler, { method, route: path, compilePath: (0, path_to_regexp_1.compile)(path), matchPath: (0, path_to_regexp_1.match)(path) });
|
187 | }
|
188 | exports.route = route;
|
189 |
|
190 | class HttpError extends Error {
|
191 | constructor(statusCode, message, metadata) {
|
192 | super(message);
|
193 | this.statusCode = statusCode;
|
194 | this.metadata = metadata;
|
195 | if (Error.captureStackTrace)
|
196 | Error.captureStackTrace(this, RedirectError);
|
197 | }
|
198 | }
|
199 | exports.HttpError = HttpError;
|
200 | function httpError(code, message, metadata) {
|
201 | return new HttpError(code, message, metadata);
|
202 | }
|
203 | exports.httpError = httpError;
|
204 |
|
205 | class RedirectError extends Error {
|
206 | constructor(statusCode, location) {
|
207 | super(`Redirect to ${location}, status code ${statusCode}`);
|
208 | this.statusCode = statusCode;
|
209 | this.location = location;
|
210 | if (Error.captureStackTrace)
|
211 | Error.captureStackTrace(this, RedirectError);
|
212 | }
|
213 | }
|
214 | exports.RedirectError = RedirectError;
|
215 | function redirect(location, statusCode = 303) {
|
216 | return new RedirectError(statusCode, location);
|
217 | }
|
218 | exports.redirect = redirect;
|
219 |
|
220 | function isStream(val) {
|
221 | return val !== null && typeof val === 'object' && typeof val.pipe === 'object';
|
222 | }
|
223 | function isReadableStream(val) {
|
224 | return (isStream(val) &&
|
225 | val.readable !== false &&
|
226 | typeof val._read === 'function' &&
|
227 | typeof val._readableState === 'object');
|
228 | }
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 | function fromRequest(fn) {
|
237 | const cache = new WeakMap();
|
238 | const errorCache = new WeakMap();
|
239 | const cachedFn = (req, ...rest) => {
|
240 | if (errorCache.has(req))
|
241 | throw errorCache.get(req);
|
242 | if (cache.has(req))
|
243 | return cache.get(req);
|
244 | try {
|
245 | const value = fn(req, ...rest);
|
246 | cache.set(req, value);
|
247 | return value;
|
248 | }
|
249 | catch (error) {
|
250 | errorCache.set(req, error);
|
251 | throw error;
|
252 | }
|
253 | };
|
254 | return cachedFn;
|
255 | }
|
256 | exports.fromRequest = fromRequest;
|
257 |
|
\ | No newline at end of file |