starlight-server 0.0.1

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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/demo/index.d.ts +1 -0
  4. package/dist/demo/index.js +27 -0
  5. package/dist/http/body/form-data.d.ts +35 -0
  6. package/dist/http/body/form-data.js +141 -0
  7. package/dist/http/body/index.d.ts +23 -0
  8. package/dist/http/body/index.js +47 -0
  9. package/dist/http/body/receive.d.ts +7 -0
  10. package/dist/http/body/receive.js +39 -0
  11. package/dist/http/http-status.d.ts +9 -0
  12. package/dist/http/http-status.js +64 -0
  13. package/dist/http/index.d.ts +9 -0
  14. package/dist/http/index.js +9 -0
  15. package/dist/http/mime-types.d.ts +14 -0
  16. package/dist/http/mime-types.js +764 -0
  17. package/dist/http/request.d.ts +25 -0
  18. package/dist/http/request.js +40 -0
  19. package/dist/http/response.d.ts +32 -0
  20. package/dist/http/response.js +66 -0
  21. package/dist/http/server.d.ts +31 -0
  22. package/dist/http/server.js +52 -0
  23. package/dist/http/types.d.ts +26 -0
  24. package/dist/http/types.js +12 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +4 -0
  27. package/dist/logging.d.ts +24 -0
  28. package/dist/logging.js +30 -0
  29. package/dist/router/cors.d.ts +24 -0
  30. package/dist/router/cors.js +35 -0
  31. package/dist/router/index.d.ts +38 -0
  32. package/dist/router/index.js +36 -0
  33. package/dist/router/match.d.ts +23 -0
  34. package/dist/router/match.js +172 -0
  35. package/dist/router/parameters.d.ts +51 -0
  36. package/dist/router/parameters.js +118 -0
  37. package/dist/router/router.d.ts +127 -0
  38. package/dist/router/router.js +97 -0
  39. package/dist/swagger/index.d.ts +1 -0
  40. package/dist/swagger/index.js +168 -0
  41. package/dist/swagger/openapi-spec.d.ts +261 -0
  42. package/dist/swagger/openapi-spec.js +5 -0
  43. package/dist/validators/array.d.ts +9 -0
  44. package/dist/validators/array.js +28 -0
  45. package/dist/validators/boolean.d.ts +4 -0
  46. package/dist/validators/boolean.js +28 -0
  47. package/dist/validators/common.d.ts +23 -0
  48. package/dist/validators/common.js +25 -0
  49. package/dist/validators/index.d.ts +20 -0
  50. package/dist/validators/index.js +38 -0
  51. package/dist/validators/number.d.ts +10 -0
  52. package/dist/validators/number.js +30 -0
  53. package/dist/validators/object.d.ts +13 -0
  54. package/dist/validators/object.js +36 -0
  55. package/dist/validators/string.d.ts +11 -0
  56. package/dist/validators/string.js +29 -0
  57. package/package.json +54 -0
  58. package/src/demo/index.ts +33 -0
  59. package/src/http/body/form-data.ts +164 -0
  60. package/src/http/body/index.ts +59 -0
  61. package/src/http/body/receive.ts +49 -0
  62. package/src/http/http-status.ts +65 -0
  63. package/src/http/index.ts +9 -0
  64. package/src/http/mime-types.ts +765 -0
  65. package/src/http/request.ts +44 -0
  66. package/src/http/response.ts +73 -0
  67. package/src/http/server.ts +67 -0
  68. package/src/http/types.ts +31 -0
  69. package/src/index.ts +4 -0
  70. package/src/logging.ts +57 -0
  71. package/src/router/cors.ts +54 -0
  72. package/src/router/index.ts +38 -0
  73. package/src/router/match.ts +194 -0
  74. package/src/router/parameters.ts +172 -0
  75. package/src/router/router.ts +233 -0
  76. package/src/swagger/index.ts +184 -0
  77. package/src/swagger/openapi-spec.ts +312 -0
  78. package/src/validators/array.ts +33 -0
  79. package/src/validators/boolean.ts +23 -0
  80. package/src/validators/common.ts +46 -0
  81. package/src/validators/index.ts +50 -0
  82. package/src/validators/number.ts +36 -0
  83. package/src/validators/object.ts +41 -0
  84. package/src/validators/string.ts +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 安坚实
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # Starlight Server: Simple But Powerful Node.js HTTP Server
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import { getFileDir } from '@anjianshi/utils/env-node/index.js';
3
+ import { getLogger, startHTTPServer, DefaultRouter } from '../index.js';
4
+ // import { registerSwaggerRoute } from '../swagger/index.js'
5
+ const logger = getLogger({
6
+ level: 'debug',
7
+ debugLib: '*',
8
+ file: {
9
+ dir: path.resolve(getFileDir(import.meta), '../../logs'),
10
+ },
11
+ });
12
+ const router = new DefaultRouter();
13
+ router.registerResponseReference('hello', { object: [{ name: 'hello', type: 'string' }] });
14
+ router.register({
15
+ method: 'GET',
16
+ path: '/hello',
17
+ response: { ref: 'hello' },
18
+ handler({ response }) {
19
+ response.json({ hello: 'world' });
20
+ },
21
+ });
22
+ // registerSwaggerRoute(router)
23
+ startHTTPServer({
24
+ handler: router.handle,
25
+ logger: logger.getChild('http'),
26
+ port: 8801,
27
+ });
@@ -0,0 +1,35 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /**
3
+ * 解析后的 FormData(格式参考浏览器环境中的 FormData 对象)
4
+ * Parsed FormData (formatted like the FormData object in a browser environment)
5
+ */
6
+ export interface TextInput {
7
+ type: 'text';
8
+ name: string;
9
+ data: string;
10
+ }
11
+ export interface FileInput {
12
+ type: 'file';
13
+ name: string;
14
+ filename: string;
15
+ mimeType: string;
16
+ data: Buffer;
17
+ }
18
+ export type Input = TextInput | FileInput;
19
+ export declare class FormData {
20
+ readonly inputs: Input[];
21
+ constructor(inputs: Input[]);
22
+ get(name: string): Input | undefined;
23
+ getAll(name: string): Input[];
24
+ getText(name: string): string | undefined;
25
+ getFile(name: string): FileInput | undefined;
26
+ has(name: string): boolean;
27
+ hasText(name: string): boolean;
28
+ hasFile(name: string): boolean;
29
+ }
30
+ /**
31
+ * 从 Content-Type Header 中解析出 form-data boundary
32
+ * Parse form-data boundary from Content-Type header.
33
+ */
34
+ export declare function getBoundary(contentType: string): string | null;
35
+ export declare function parseFormData(body: Buffer, boundary: string): FormData;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Parsing multipart/form-data
3
+ *
4
+ * Referenced this library:https://github.com/nachomazzara/parse-multipart-data
5
+ * (This library has bugs, so I decided to implement it myself.)
6
+ *
7
+ * Introduce for multipart/form-data format:https://www.jianshu.com/p/29e38bcc8a1d
8
+ */
9
+ import { HTTPError } from '../types.js';
10
+ export class FormData {
11
+ inputs;
12
+ constructor(inputs) {
13
+ this.inputs = inputs;
14
+ }
15
+ get(name) {
16
+ return this.inputs.find(v => v.name === name);
17
+ }
18
+ getAll(name) {
19
+ return this.inputs.filter(v => v.name === name);
20
+ }
21
+ getText(name) {
22
+ return this.inputs.find((v) => v.name === name && v.type === 'text')?.data;
23
+ }
24
+ getFile(name) {
25
+ return this.inputs.find((v) => v.name === name && v.type === 'file');
26
+ }
27
+ has(name) {
28
+ return !!this.get(name);
29
+ }
30
+ hasText(name) {
31
+ return this.getText(name) !== undefined;
32
+ }
33
+ hasFile(name) {
34
+ return !!this.getFile(name);
35
+ }
36
+ }
37
+ /**
38
+ * 从 Content-Type Header 中解析出 form-data boundary
39
+ * Parse form-data boundary from Content-Type header.
40
+ */
41
+ export function getBoundary(contentType) {
42
+ const prefix = 'multipart/form-data; boundary=';
43
+ return contentType.startsWith(prefix) ? contentType.slice(prefix.length) : null;
44
+ }
45
+ /**
46
+ * parse form-data
47
+ */
48
+ var State;
49
+ (function (State) {
50
+ State[State["Init"] = 0] = "Init";
51
+ State[State["ReadingHeaders"] = 1] = "ReadingHeaders";
52
+ State[State["ReadingData"] = 2] = "ReadingData";
53
+ })(State || (State = {}));
54
+ export function parseFormData(body, boundary) {
55
+ const inputs = [];
56
+ let rest = body;
57
+ let state = State.Init;
58
+ let partialInput; // 在 ReadingHeaders 状态里会为其赋值 Will be assigned when in the ReadingHeaders state
59
+ // eslint-disable-next-line no-constant-condition
60
+ while (true) {
61
+ if (!rest.byteLength)
62
+ throw new HTTPError(400, 'form-data: not complete');
63
+ if (state === State.Init) {
64
+ const sep = `--${boundary}\r\n`;
65
+ if (!startsWith(rest, sep))
66
+ throw new HTTPError(400, 'form-data: part separator invalid');
67
+ rest = rest.subarray(byteLength(sep));
68
+ state = State.ReadingHeaders;
69
+ }
70
+ else if (state === State.ReadingHeaders) {
71
+ if (!startsWith(rest, 'Content-Disposition: form-data;', true))
72
+ throw new HTTPError(400, 'form-data: headers or format invalid');
73
+ const headerEnds = Buffer.from('\r\n\r\n');
74
+ const headerMax = 10000; // header 部分最多允许这么多字节 Max allowd bytes of header
75
+ const headersEndIndex = rest.subarray(0, headerMax).indexOf(headerEnds);
76
+ if (headersEndIndex === -1)
77
+ throw new HTTPError(400, 'form-data: headers invalid or too long');
78
+ const headers = rest
79
+ .subarray(0, headersEndIndex)
80
+ .toString()
81
+ .split('\r\n')
82
+ .map(item => {
83
+ const sepIndex = item.indexOf(':');
84
+ return sepIndex !== -1
85
+ ? { name: item.slice(0, sepIndex).trim(), value: item.slice(sepIndex + 2).trim() }
86
+ : null;
87
+ })
88
+ .filter((v) => !!v);
89
+ const disposition = headers.find(v => v.name.toLowerCase() === 'Content-Disposition'.toLowerCase())?.value ?? '';
90
+ const name = /(?:;| )name="(.*?)"(?:;|$)/.exec(disposition)?.[1] ?? '';
91
+ const filename = /(?:;| )filename="(.*?)"(?:;|$)/.exec(disposition)?.[1];
92
+ const contentType = headers.find(v => v.name.toLowerCase() === 'Content-Type'.toLowerCase())?.value;
93
+ partialInput =
94
+ filename === undefined
95
+ ? { type: 'text', name, data: '' }
96
+ : { type: 'file', name, filename, mimeType: contentType ?? '', data: Buffer.from('') };
97
+ rest = rest.subarray(headersEndIndex + headerEnds.byteLength);
98
+ state = State.ReadingData;
99
+ }
100
+ else {
101
+ const dataEnds = Buffer.from(`\r\n--${boundary}`);
102
+ const dataEndIndex = rest.indexOf(dataEnds);
103
+ if (dataEndIndex === -1)
104
+ throw new HTTPError(400, 'form-data: data no ends');
105
+ const data = rest.subarray(0, dataEndIndex);
106
+ const input = partialInput;
107
+ if (input.type === 'text')
108
+ input.data = data.toString();
109
+ else
110
+ input.data = data;
111
+ inputs.push(input);
112
+ const afterData = rest.subarray(dataEndIndex + dataEnds.byteLength);
113
+ if (equals(afterData, '--') || equals(afterData, '--\r\n')) {
114
+ // 全部解析结束 Parse whole finished
115
+ break;
116
+ }
117
+ else if (startsWith(afterData, '\r\n')) {
118
+ // 开始解析下一部分 Parsing next part
119
+ rest = rest.subarray(dataEndIndex + byteLength('\r\n'));
120
+ state = State.Init;
121
+ }
122
+ else {
123
+ throw new HTTPError(400, 'form-data: invalid data ends');
124
+ }
125
+ }
126
+ }
127
+ return new FormData(inputs);
128
+ }
129
+ function byteLength(string) {
130
+ return Buffer.byteLength(string);
131
+ }
132
+ function startsWith(buffer, string, caseInsensitive = false) {
133
+ const sliced = buffer.subarray(0, byteLength(string)).toString();
134
+ return caseInsensitive ? sliced.toLowerCase() === string.toLowerCase() : sliced === string;
135
+ }
136
+ function equals(buffer, string) {
137
+ return (
138
+ // 先进行长度比较,以避免 buffer 太大时进行 toString() 影响性能
139
+ // Perform a length comparison first to avoid the performance impact of toString() when the buffer is too large.
140
+ buffer.byteLength === byteLength(string) && buffer.toString() === string);
141
+ }
@@ -0,0 +1,23 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { type NodeRequest } from '../types.js';
3
+ export interface BodyOptions {
4
+ /**
5
+ * Maximum size of body, unit `M`.
6
+ * @default 1000
7
+ */
8
+ limit?: number;
9
+ }
10
+ /**
11
+ * Receive And Parse Request Body
12
+ * Support JSON And multipart/form-data
13
+ */
14
+ export declare class RequestBody {
15
+ readonly nodeRequest: NodeRequest;
16
+ readonly options: BodyOptions;
17
+ constructor(nodeRequest: NodeRequest, options: BodyOptions);
18
+ get contentType(): string;
19
+ protected receiving?: Promise<Buffer | undefined>;
20
+ buffer(): Promise<Buffer | undefined>;
21
+ json(): Promise<unknown>;
22
+ formData(): Promise<import("./form-data.js").FormData | undefined>;
23
+ }
@@ -0,0 +1,47 @@
1
+ import { HTTPError } from '../types.js';
2
+ import { getBoundary, parseFormData } from './form-data.js';
3
+ import { receiveBody } from './receive.js';
4
+ /**
5
+ * Receive And Parse Request Body
6
+ * Support JSON And multipart/form-data
7
+ */
8
+ export class RequestBody {
9
+ nodeRequest;
10
+ options;
11
+ constructor(nodeRequest, options) {
12
+ this.nodeRequest = nodeRequest;
13
+ this.options = options;
14
+ }
15
+ get contentType() {
16
+ return this.nodeRequest.headers['content-type'] ?? '';
17
+ }
18
+ receiving;
19
+ async buffer() {
20
+ if (!this.receiving)
21
+ this.receiving = receiveBody(this.nodeRequest, (this.options.limit ?? 1000) * 1000 * 1000);
22
+ return this.receiving;
23
+ }
24
+ async json() {
25
+ if (this.contentType && !this.contentType.startsWith('application/json')) {
26
+ throw new HTTPError(400, "JSON parse failed, invalid 'Content-Type'.");
27
+ }
28
+ const buffer = await this.buffer();
29
+ if (!buffer)
30
+ return undefined;
31
+ try {
32
+ return JSON.parse(buffer.toString());
33
+ }
34
+ catch (e) {
35
+ throw new HTTPError(400, 'Invalid JSON.');
36
+ }
37
+ }
38
+ async formData() {
39
+ const boundary = getBoundary(this.contentType);
40
+ if (boundary === null)
41
+ throw new HTTPError(400, "form-data parse failed, invalid 'Content-Type'.");
42
+ const buffer = await this.buffer();
43
+ if (!buffer)
44
+ return undefined;
45
+ return parseFormData(buffer, boundary);
46
+ }
47
+ }
@@ -0,0 +1,7 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { type NodeRequest } from '../types.js';
3
+ export declare function receiveBody(nodeRequest: NodeRequest,
4
+ /**
5
+ * Maximum size of body, unit `byte`
6
+ */
7
+ limit: number): Promise<Buffer | undefined>;
@@ -0,0 +1,39 @@
1
+ import { HTTPError } from '../types.js';
2
+ export async function receiveBody(nodeRequest,
3
+ /**
4
+ * Maximum size of body, unit `byte`
5
+ */
6
+ limit) {
7
+ return new Promise(callback.bind(null, nodeRequest, limit));
8
+ }
9
+ function callback(nodeRequest, limit, resolve, reject) {
10
+ // 若客户端提供了 Content-Length,确认其小于限额(若不符合要求,直接跳过接收)
11
+ // If the client provides a Content-Length header, confirm that it is less than the maximum allowed size (if not, skip receiving it).
12
+ let contentLength;
13
+ if ('content-length' in nodeRequest.headers) {
14
+ contentLength = parseInt(nodeRequest.headers['content-length'] ?? '', 10);
15
+ if (!isFinite(contentLength))
16
+ return void reject(new HTTPError(400, 'Invalid Content-Length'));
17
+ if (contentLength > limit)
18
+ return void reject(new HTTPError(413));
19
+ }
20
+ const parts = [];
21
+ let recvLength = 0;
22
+ const handleData = (part) => {
23
+ parts.push(part);
24
+ recvLength += part.byteLength;
25
+ if (recvLength > limit) {
26
+ nodeRequest.off('data', handleData);
27
+ nodeRequest.off('end', handleEnd);
28
+ reject(new HTTPError(413));
29
+ }
30
+ };
31
+ const handleEnd = () => {
32
+ if (contentLength !== undefined && recvLength !== contentLength)
33
+ reject(new HTTPError(400, 'Content-Length mismatch.'));
34
+ else
35
+ resolve(parts.length ? Buffer.concat(parts) : undefined);
36
+ };
37
+ nodeRequest.on('data', handleData);
38
+ nodeRequest.on('end', handleEnd);
39
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * HTTP Status
3
+ * From:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
4
+ */
5
+ export declare const HTTPStatus: string[];
6
+ /**
7
+ * Map(400 => '400 Bad Request')
8
+ */
9
+ export declare const HTTPStatusMap: Map<number, string>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * HTTP Status
3
+ * From:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
4
+ */
5
+ export const HTTPStatus = [
6
+ '100 Continue',
7
+ '101 Switching Protocols',
8
+ '103 Early Hints',
9
+ '200 OK',
10
+ '201 Created',
11
+ '202 Accepted',
12
+ '203 Non-Authoritative Information',
13
+ '204 No Content',
14
+ '205 Reset Content',
15
+ '206 Partial Content',
16
+ '300 Multiple Choices',
17
+ '301 Moved Permanently',
18
+ '302 Found',
19
+ '303 See Other',
20
+ '304 Not Modified',
21
+ '307 Temporary Redirect',
22
+ '308 Permanent Redirect',
23
+ '400 Bad Request',
24
+ '401 Unauthorized',
25
+ '402 Payment Required',
26
+ '403 Forbidden',
27
+ '404 Not Found',
28
+ '405 Method Not Allowed',
29
+ '406 Not Acceptable',
30
+ '407 Proxy Authentication Required',
31
+ '408 Request Timeout',
32
+ '409 Conflict',
33
+ '410 Gone',
34
+ '411 Length Required',
35
+ '412 Precondition Failed',
36
+ '413 Payload Too Large',
37
+ '414 URI Too Long',
38
+ '415 Unsupported Media Type',
39
+ '416 Range Not Satisfiable',
40
+ '417 Expectation Failed',
41
+ "418 I'm a teapot",
42
+ '422 Unprocessable Entity',
43
+ '425 Too Early',
44
+ '426 Upgrade Required',
45
+ '428 Precondition Required',
46
+ '429 Too Many Requests',
47
+ '431 Request Header Fields Too Large',
48
+ '451 Unavailable For Legal Reasons',
49
+ '500 Internal Server Error',
50
+ '501 Not Implemented',
51
+ '502 Bad Gateway',
52
+ '503 Service Unavailable',
53
+ '504 Gateway Timeout',
54
+ '505 HTTP Version Not Supported',
55
+ '506 Variant Also Negotiates',
56
+ '507 Insufficient Storage',
57
+ '508 Loop Detected',
58
+ '510 Not Extended',
59
+ '511 Network Authentication Required',
60
+ ];
61
+ /**
62
+ * Map(400 => '400 Bad Request')
63
+ */
64
+ export const HTTPStatusMap = new Map(HTTPStatus.map(item => [parseInt(item.slice(0, 3), 10), item]));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 在协议层面,实现对 HTTP 请求的处理
3
+ */
4
+ export * from './types.js';
5
+ export * from './request.js';
6
+ export * from './response.js';
7
+ export * from './server.js';
8
+ export * from './http-status.js';
9
+ export * from './mime-types.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 在协议层面,实现对 HTTP 请求的处理
3
+ */
4
+ export * from './types.js';
5
+ export * from './request.js';
6
+ export * from './response.js';
7
+ export * from './server.js';
8
+ export * from './http-status.js';
9
+ export * from './mime-types.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 传入文件路径,返回此文件的 MIME Type。路径不含扩展名或找不到对应类型的,返回 null。
3
+ * Given a file path, return the MIME Type of the file.
4
+ * If the path does not contain an extension or if the type cannot be found, return null.
5
+ */
6
+ export declare function path2MIMEType(path: string): string | null;
7
+ export declare const MIMETypes: {
8
+ type: string;
9
+ extensions: string[];
10
+ }[];
11
+ /**
12
+ * Map(.ext => 'MIME-Type')
13
+ */
14
+ export declare const MIMETypesMap: Map<string, string>;