@boo-labs/server 0.0.0-stage → 1.0.0
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/README.md +229 -2
- package/index.mjs +23 -0
- package/package.json +71 -4
- package/source/application/Kernel/index.mjs +1137 -0
- package/source/application/Server/index.mjs +1201 -0
- package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
- package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
- package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
- package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
- package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
- package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
- package/source/application/resolvers/TransportResolver/index.mjs +119 -0
- package/source/application/use-cases/CloseServer/index.mjs +85 -0
- package/source/application/use-cases/ListenServer/index.mjs +98 -0
- package/source/core/EventSystem/index.mjs +110 -0
- package/source/core/Exchange/ExchangeContext.mjs +291 -0
- package/source/core/Exchange/Telemetry.mjs +193 -0
- package/source/core/Exchange/index.mjs +604 -0
- package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
- package/source/core/IPCMain/IPCMain.mjs +507 -0
- package/source/core/IPCMain/IPCProtocol.mjs +441 -0
- package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
- package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
- package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
- package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
- package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
- package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
- package/source/core/Request/RequestHeaders.mjs +177 -0
- package/source/core/Request/index.mjs +805 -0
- package/source/core/Response/ResponseHeaders.mjs +402 -0
- package/source/core/Response/index.mjs +1370 -0
- package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
- package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
- package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
- package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
- package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
- package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
- package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
- package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
- package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
- package/source/core/routing/ExchangeLayer/index.mjs +254 -0
- package/source/core/routing/Pipeline/index.mjs +157 -0
- package/source/core/routing/Router/Route.mjs +184 -0
- package/source/core/routing/Router/index.mjs +523 -0
- package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
- package/source/core/routing/RouterRegister/index.mjs +150 -0
- package/source/core/routing/RouterSystem/index.mjs +225 -0
- package/source/core/value-objects/Address/index.mjs +281 -0
- package/source/core/value-objects/Endpoint/index.mjs +293 -0
- package/source/index.mjs +62 -0
- package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
- package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
- package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
- package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
- package/source/shared/Identifier/index.mjs +671 -0
- package/source/shared/constants/symbols/brands/index.mjs +45 -0
- package/source/shared/domain/UUIDV7/index.mjs +346 -0
- package/source/shared/errors/BaseError/index.mjs +157 -0
- package/source/shared/errors/IPCError/index.mjs +97 -0
- package/source/shared/errors/MiddlewareError/index.mjs +106 -0
- package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
- package/source/shared/errors/RequestError/index.mjs +116 -0
- package/source/shared/errors/ResponseError/index.mjs +140 -0
- package/source/shared/errors/RouterError/index.mjs +90 -0
- package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
- package/source/shared/errors/ServerError/index.mjs +127 -0
- package/source/shared/errors/WebSocketError/index.mjs +58 -0
- package/source/shared/outcome/ErrorSchema.mjs +361 -0
- package/source/shared/outcome/Result/index.mjs +239 -0
- package/source/shared/outcome/Validation/index.mjs +208 -0
- package/source/shared/validations/Middleware/index.mjs +160 -0
- package/source/shared/validations/PathnamePattern/index.mjs +256 -0
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
- package/source/shared/validations/Server/Router/index.mjs +68 -0
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
- package/types/index.d.ts +719 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Orchestrates IPC request processing for one WebSocket connection.
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* IPCServerChannel binds one established WebSocket connection to the IPCMain
|
|
7
|
+
* dispatch capability. It owns request decoding, response serialization,
|
|
8
|
+
* connection-local state, and transport writes.
|
|
9
|
+
*
|
|
10
|
+
* IPCServerChannel does not own IPC handlers, event listeners, or application
|
|
11
|
+
* dispatch rules. Those responsibilities remain with IPCMain.
|
|
12
|
+
*
|
|
13
|
+
* @module IPCServerChannel
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
// » IMPORT LOCAL MODULES
|
|
18
|
+
import IPCProtocol from './IPCProtocol.mjs';
|
|
19
|
+
|
|
20
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
21
|
+
/**
|
|
22
|
+
* Represents an established WebSocket connection contract.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {import('#adapters/TransportAdapter').WebSocketConnection} WebSocketConnection
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents a decoded IPC request.
|
|
29
|
+
*
|
|
30
|
+
* @typedef {import('./IPCProtocol.mjs').IPCRequest} IPCRequest
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Represents the normalized IPC wire error.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {import('./IPCProtocol.mjs').IPCError} IPCError
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Represents a successful response payload supplied by the IPC host.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {object} IPCSuccessPayload
|
|
43
|
+
* @property {unknown} result - Handler result returned to the IPC caller.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Represents an error response payload supplied by the IPC host.
|
|
48
|
+
*
|
|
49
|
+
* @typedef {object} IPCErrorPayload
|
|
50
|
+
* @property {unknown} error - Error value produced while handling the request.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Represents a response payload supplied by the IPC host.
|
|
55
|
+
*
|
|
56
|
+
* @typedef {IPCSuccessPayload|IPCErrorPayload} IPCResponsePayload
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dispatches a decoded IPC request and provides a callback for its response.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {(request: IPCRequest, respond: (payload: IPCResponsePayload) => void) => Promise<void>} DispatchFunction
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Represents the internal capability supplied by IPCMain to dispatch IPC requests.
|
|
67
|
+
*
|
|
68
|
+
* @typedef {object} IPCChannelHost
|
|
69
|
+
* @property {DispatchFunction} dispatch - Dispatches an IPC request and reports its response through the supplied callback.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
/**
|
|
74
|
+
* Determines whether a value is a non-null plain object.
|
|
75
|
+
*
|
|
76
|
+
* @private
|
|
77
|
+
* @param {unknown} value - Value to inspect.
|
|
78
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
79
|
+
*/
|
|
80
|
+
function isPlainObject(value) {
|
|
81
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
85
|
+
/**
|
|
86
|
+
* Orchestrates IPC request processing for one WebSocket connection.
|
|
87
|
+
*
|
|
88
|
+
* @classdesc IPCServerChannel binds one established WebSocket connection to the
|
|
89
|
+
* IPCMain dispatch capability. It owns request decoding, response
|
|
90
|
+
* serialization, connection-local state, and transport writes.
|
|
91
|
+
*
|
|
92
|
+
* IPCServerChannel does not own IPC handlers, event listeners, or application
|
|
93
|
+
* dispatch rules. Those responsibilities remain with IPCMain.
|
|
94
|
+
*/
|
|
95
|
+
class IPCServerChannel {
|
|
96
|
+
/**
|
|
97
|
+
* Established WebSocket connection owned by this IPC channel.
|
|
98
|
+
*
|
|
99
|
+
* @type {WebSocketConnection}
|
|
100
|
+
*/
|
|
101
|
+
#connection;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Internal IPC host used to dispatch requests.
|
|
105
|
+
*
|
|
106
|
+
* @type {IPCChannelHost}
|
|
107
|
+
*/
|
|
108
|
+
#host;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* IPC wire protocol used to decode requests and serialize responses.
|
|
112
|
+
*
|
|
113
|
+
* @type {IPCProtocol}
|
|
114
|
+
*/
|
|
115
|
+
#protocol = new IPCProtocol();
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Indicates whether this IPC channel is closed.
|
|
119
|
+
*
|
|
120
|
+
* @type {boolean}
|
|
121
|
+
*/
|
|
122
|
+
#closed = false;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates an IPC channel bound to a WebSocket connection and an IPC host.
|
|
126
|
+
*
|
|
127
|
+
* Registers message, close, and error handlers on the connection. The
|
|
128
|
+
* channel is considered closed once the connection emits either `close`
|
|
129
|
+
* or `error`.
|
|
130
|
+
*
|
|
131
|
+
* @param {WebSocketConnection} connection - Established WebSocket connection.
|
|
132
|
+
* @param {IPCChannelHost} host - IPC host providing the dispatch capability.
|
|
133
|
+
* @example
|
|
134
|
+
* ```js
|
|
135
|
+
* const channel = new IPCServerChannel(connection, {
|
|
136
|
+
* dispatch: async (request, respond) => {
|
|
137
|
+
* respond({ result: { ok: true } });
|
|
138
|
+
* },
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
constructor(connection, host) {
|
|
143
|
+
this.#connection = connection;
|
|
144
|
+
this.#host = host;
|
|
145
|
+
|
|
146
|
+
connection.on('message', raw => {
|
|
147
|
+
void this.#onMessage(raw);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
connection.on('close', () => {
|
|
151
|
+
this.#closed = true;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
connection.on('error', () => {
|
|
155
|
+
this.#closed = true;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Decodes and dispatches one incoming WebSocket message.
|
|
161
|
+
*
|
|
162
|
+
* @param {string|Buffer} raw - Raw WebSocket message received from the connection.
|
|
163
|
+
* @returns {Promise<void>} Resolves once the message has been dispatched.
|
|
164
|
+
*/
|
|
165
|
+
async #onMessage(raw) {
|
|
166
|
+
if (!this.#closed) {
|
|
167
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf8');
|
|
168
|
+
const request = this.#decodeOrHandleFailure(text);
|
|
169
|
+
|
|
170
|
+
if (request !== null) {
|
|
171
|
+
await this.#dispatchRequest(request);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Decodes an IPC request and handles decoding failures.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} text - Raw IPC payload to decode.
|
|
180
|
+
* @returns {IPCRequest|null} Decoded IPC request, or null when decoding fails.
|
|
181
|
+
*/
|
|
182
|
+
#decodeOrHandleFailure(text) {
|
|
183
|
+
try {
|
|
184
|
+
return this.#protocol.decode(text);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.#handleDecodeFailure(text, error);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Dispatches a decoded IPC request to the IPC host.
|
|
193
|
+
*
|
|
194
|
+
* @param {IPCRequest} request - Decoded IPC request to dispatch.
|
|
195
|
+
* @returns {Promise<void>} Resolves once the request has been dispatched.
|
|
196
|
+
*/
|
|
197
|
+
async #dispatchRequest(request) {
|
|
198
|
+
if (request.type === 'event') {
|
|
199
|
+
try {
|
|
200
|
+
await this.#host.dispatch(request, () => {});
|
|
201
|
+
} catch {
|
|
202
|
+
// Event dispatch errors are intentionally isolated from the connection.
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const id = request.id;
|
|
206
|
+
let responded = false;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Responds to the current invocation once.
|
|
210
|
+
*
|
|
211
|
+
* Subsequent invocations are ignored to preserve the one-response
|
|
212
|
+
* contract of IPC invocations.
|
|
213
|
+
*
|
|
214
|
+
* @param {IPCResponsePayload} payload - Response payload supplied by the IPC host.
|
|
215
|
+
*/
|
|
216
|
+
const respond = payload => {
|
|
217
|
+
if (!responded) {
|
|
218
|
+
responded = true;
|
|
219
|
+
this.#sendResponse(id, payload);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await this.#host.dispatch(request, respond);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
respond({ error });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Handles a message that could not be decoded as a valid IPC request.
|
|
233
|
+
*
|
|
234
|
+
* A second lightweight parse is intentionally used to recover an invocation
|
|
235
|
+
* identifier when possible, allowing the peer to correlate the protocol
|
|
236
|
+
* error with its original request.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} text - Raw IPC payload that failed to decode.
|
|
239
|
+
* @param {unknown} error - Error raised while decoding the payload.
|
|
240
|
+
*/
|
|
241
|
+
#handleDecodeFailure(text, error) {
|
|
242
|
+
const id = this.#peekInvokeId(text);
|
|
243
|
+
|
|
244
|
+
if (id !== null) {
|
|
245
|
+
const reason = error instanceof Error ? error.message : 'Invalid IPC message';
|
|
246
|
+
|
|
247
|
+
this.#sendRaw(this.#protocol.badMessage(id, reason));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Attempts to recover an invocation identifier without validating the full
|
|
253
|
+
* IPC request a second time.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} text - Raw IPC payload to inspect.
|
|
256
|
+
* @returns {string|null} Recovered invocation identifier, or null when absent.
|
|
257
|
+
*/
|
|
258
|
+
#peekInvokeId(text) {
|
|
259
|
+
try {
|
|
260
|
+
const value = JSON.parse(text);
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
isPlainObject(value) &&
|
|
264
|
+
value.type === 'invoke' &&
|
|
265
|
+
typeof value.id === 'string' &&
|
|
266
|
+
value.id.length > 0
|
|
267
|
+
) {
|
|
268
|
+
return value.id;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Ignore recovery failures.
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Encodes and sends an IPC response.
|
|
279
|
+
*
|
|
280
|
+
* Raw errors are deliberately forwarded to IPCProtocol, which owns their
|
|
281
|
+
* normalization into the IPC wire representation.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} id - Invocation identifier associated with the request.
|
|
284
|
+
* @param {IPCResponsePayload} payload - Response payload supplied by the IPC host.
|
|
285
|
+
*/
|
|
286
|
+
#sendResponse(id, payload) {
|
|
287
|
+
if (!this.#closed) {
|
|
288
|
+
const encoded =
|
|
289
|
+
'error' in payload
|
|
290
|
+
? this.#protocol.error(id, payload.error)
|
|
291
|
+
: this.#protocol.success(id, payload.result);
|
|
292
|
+
|
|
293
|
+
this.#sendRaw(encoded);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Sends an already encoded IPC message through the WebSocket connection.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} encoded - Encoded IPC message to send.
|
|
301
|
+
*/
|
|
302
|
+
#sendRaw(encoded) {
|
|
303
|
+
if (!this.#closed) {
|
|
304
|
+
try {
|
|
305
|
+
this.#connection.send(encoded);
|
|
306
|
+
} catch {
|
|
307
|
+
this.#forceClose();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Marks the channel closed and attempts to close its transport connection.
|
|
314
|
+
*
|
|
315
|
+
* Invoked when the transport reports a failure that prevents further
|
|
316
|
+
* writes. Errors thrown while closing the connection are ignored, since
|
|
317
|
+
* the channel is already considered closed.
|
|
318
|
+
*/
|
|
319
|
+
#forceClose() {
|
|
320
|
+
this.#closed = true;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
this.#connection.close();
|
|
324
|
+
} catch {
|
|
325
|
+
// Ignore transport close failures.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
331
|
+
export default IPCServerChannel;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Adapts transport-level WebSocket upgrades to connection listeners.
|
|
4
|
+
*
|
|
5
|
+
* @description WebSocketAdapter bridges the TransportAdapter WebSocket upgrade
|
|
6
|
+
* capability with consumers that need established WebSocket connections.
|
|
7
|
+
*
|
|
8
|
+
* The adapter does not own active connections. It only owns the subscription
|
|
9
|
+
* to the transport upgrade source and the current connection listener.
|
|
10
|
+
*
|
|
11
|
+
* @module WebSocketAdapter
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
/**
|
|
16
|
+
* Represents a permitted transport identity.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {import('#adapters/TransportAdapter').TransportIdentity} TransportIdentity
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a transport adapter capable of managing a permitted transport.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {import('#adapters/TransportAdapter').default<TransportIdentity>} TransportAdapter
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents an established WebSocket connection contract.
|
|
29
|
+
*
|
|
30
|
+
* @typedef {import('#adapters/TransportAdapter').WebSocketConnection} WebSocketConnection
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Transport-level request used to accept or reject a WebSocket handshake.
|
|
35
|
+
*
|
|
36
|
+
* @typedef {import('#adapters/TransportAdapter').WebSocketOpenRequest} WebSocketOpenRequest
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Receives an established WebSocket connection.
|
|
41
|
+
*
|
|
42
|
+
* @callback WebSocketConnectionListener
|
|
43
|
+
* @param {WebSocketConnection} connection - Established WebSocket connection.
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for WebSocketAdapter.
|
|
49
|
+
*
|
|
50
|
+
* @typedef {object} WebSocketAdapterOptions
|
|
51
|
+
* @property {(url: string) => boolean} [accept] - Optional predicate used to accept or reject WebSocket upgrade URLs.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
55
|
+
class WebSocketAdapter {
|
|
56
|
+
/**
|
|
57
|
+
* Underlying transport adapter responsible for WebSocket upgrade handling.
|
|
58
|
+
*
|
|
59
|
+
* @type {TransportAdapter}
|
|
60
|
+
*/
|
|
61
|
+
#transport;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optional predicate used to determine whether a WebSocket URL is accepted.
|
|
65
|
+
*
|
|
66
|
+
* @type {((url: string) => boolean)|null}
|
|
67
|
+
*/
|
|
68
|
+
#accept;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Current listener that receives accepted WebSocket connections.
|
|
72
|
+
*
|
|
73
|
+
* @type {WebSocketConnectionListener|null}
|
|
74
|
+
*/
|
|
75
|
+
#listener = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Indicates whether the adapter has subscribed to transport WebSocket upgrades.
|
|
79
|
+
*
|
|
80
|
+
* @type {boolean}
|
|
81
|
+
*/
|
|
82
|
+
#subscribed = false;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initializes the WebSocket adapter with a transport adapter.
|
|
86
|
+
*
|
|
87
|
+
* @param {TransportAdapter} transport - Transport adapter responsible for handling WebSocket upgrade requests.
|
|
88
|
+
* @param {WebSocketAdapterOptions} [options] - Optional WebSocket adapter configuration.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```js
|
|
92
|
+
* const adapter = new WebSocketAdapter(transport, {
|
|
93
|
+
* accept: url => url.startsWith('/ws/')
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
constructor(transport, options = {}) {
|
|
98
|
+
this.#transport = transport;
|
|
99
|
+
this.#accept = typeof options.accept === 'function' ? options.accept : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Registers the current listener that receives accepted WebSocket connections.
|
|
104
|
+
*
|
|
105
|
+
* Calling this method again replaces the previously registered listener.
|
|
106
|
+
* The underlying transport is subscribed only once, regardless of how many
|
|
107
|
+
* times this method is called.
|
|
108
|
+
*
|
|
109
|
+
* @param {WebSocketConnectionListener} listener - Callback invoked when a WebSocket connection is established.
|
|
110
|
+
* @throws {TypeError} If listener is not a function.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```js
|
|
114
|
+
* adapter.onConnection(connection => {
|
|
115
|
+
* connection.on('message', data => {
|
|
116
|
+
* connection.send(`Echo: ${data}`);
|
|
117
|
+
* });
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
onConnection(listener) {
|
|
122
|
+
if (typeof listener !== 'function') {
|
|
123
|
+
throw new TypeError('listener must be a function');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.#listener = listener;
|
|
127
|
+
|
|
128
|
+
if (!this.#subscribed) {
|
|
129
|
+
this.#subscribed = true;
|
|
130
|
+
|
|
131
|
+
this.#transport.onWebSocket(openRequest => {
|
|
132
|
+
this.#handleOpen(openRequest);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Stops delivering newly accepted connections to the current listener.
|
|
139
|
+
*
|
|
140
|
+
* After `close()` is called, incoming WebSocket upgrade requests are
|
|
141
|
+
* rejected with `503 WebSocket unavailable`. The adapter cannot be
|
|
142
|
+
* reopened once closed.
|
|
143
|
+
*
|
|
144
|
+
* Existing connections remain owned by their consumer.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```js
|
|
148
|
+
* adapter.close();
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
close() {
|
|
152
|
+
this.#listener = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Processes one transport-level WebSocket upgrade request.
|
|
157
|
+
*
|
|
158
|
+
* @param {WebSocketOpenRequest} openRequest - Transport-level request used to accept or reject the WebSocket handshake.
|
|
159
|
+
*/
|
|
160
|
+
#handleOpen(openRequest) {
|
|
161
|
+
if (this.#listener === null) {
|
|
162
|
+
openRequest.reject(503, 'WebSocket unavailable');
|
|
163
|
+
} else if (this.#accept && !this.#accept(openRequest.url)) {
|
|
164
|
+
openRequest.reject(404, 'Not Found');
|
|
165
|
+
} else {
|
|
166
|
+
Promise.resolve()
|
|
167
|
+
.then(() => openRequest.accept())
|
|
168
|
+
.then(
|
|
169
|
+
connection => {
|
|
170
|
+
if (this.#listener !== null) {
|
|
171
|
+
this.#listener(connection);
|
|
172
|
+
} else {
|
|
173
|
+
try {
|
|
174
|
+
connection.close();
|
|
175
|
+
} catch {
|
|
176
|
+
// Ignore transport close failures during cleanup.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
() => {
|
|
181
|
+
openRequest.reject(500, 'WebSocket accept failed');
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
189
|
+
export default WebSocketAdapter;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Defines the base abstraction for parsing incoming request bodies.
|
|
4
|
+
*
|
|
5
|
+
* @description Provides the common functionality required by concrete body
|
|
6
|
+
* parsers to consume an incoming request stream while enforcing the
|
|
7
|
+
* configured maximum body size.
|
|
8
|
+
*
|
|
9
|
+
* When the body limit is exceeded, the underlying request is destroyed so
|
|
10
|
+
* that the socket is not left consuming bytes. As a consequence, the
|
|
11
|
+
* connection may be closed abruptly instead of producing a normal `413`
|
|
12
|
+
* response.
|
|
13
|
+
*
|
|
14
|
+
* @module BodyParser
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
18
|
+
// » IMPORT LOCAL MODULES
|
|
19
|
+
import RequestError from '#shared/errors/RequestError';
|
|
20
|
+
|
|
21
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
/**
|
|
23
|
+
* Represents an incoming HTTP/1.1 or HTTPS/1.1 request message from Node.js.
|
|
24
|
+
*
|
|
25
|
+
* @typedef {import('node:http').IncomingMessage} HttpIncomingMessage
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Represents an incoming HTTP/2 request message from Node.js.
|
|
30
|
+
*
|
|
31
|
+
* @typedef {import('node:http2').Http2ServerRequest} Http2IncomingMessage
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents an incoming request message supported by the server.
|
|
36
|
+
*
|
|
37
|
+
* @typedef {HttpIncomingMessage|Http2IncomingMessage} IncomingRequest
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
41
|
+
/**
|
|
42
|
+
* Provides the base abstraction for parsing incoming request bodies.
|
|
43
|
+
*
|
|
44
|
+
* @classdesc BodyParser provides common functionality for safely consuming
|
|
45
|
+
* an incoming request stream while enforcing the configured maximum body
|
|
46
|
+
* size. Concrete parsers implement `parse()` to transform the request body
|
|
47
|
+
* into the appropriate representation.
|
|
48
|
+
*/
|
|
49
|
+
class BodyParser {
|
|
50
|
+
/**
|
|
51
|
+
* Underlying Node.js incoming request message.
|
|
52
|
+
*
|
|
53
|
+
* @type {IncomingRequest}
|
|
54
|
+
*/
|
|
55
|
+
#incomingMessage;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Maximum number of bytes allowed for the request body.
|
|
59
|
+
*
|
|
60
|
+
* @type {number}
|
|
61
|
+
*/
|
|
62
|
+
#bodyLimit;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a body parser.
|
|
66
|
+
*
|
|
67
|
+
* @param {IncomingRequest} incomingMessage - Incoming HTTP request message.
|
|
68
|
+
* @param {number} bodyLimit - Maximum number of bytes allowed for the body.
|
|
69
|
+
* @throws {Error} Thrown when attempting to instantiate `BodyParser` directly.
|
|
70
|
+
*/
|
|
71
|
+
constructor(incomingMessage, bodyLimit) {
|
|
72
|
+
if (new.target === BodyParser) {
|
|
73
|
+
throw new Error('BodyParser is abstract and cannot be instantiated directly');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.#incomingMessage = incomingMessage;
|
|
77
|
+
this.#bodyLimit = bodyLimit;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the underlying Node.js incoming request message.
|
|
82
|
+
*
|
|
83
|
+
* Concrete parsers use this stream to consume the request body.
|
|
84
|
+
*
|
|
85
|
+
* @type {IncomingRequest}
|
|
86
|
+
*/
|
|
87
|
+
get incomingMessage() {
|
|
88
|
+
return this.#incomingMessage;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parses the incoming request body.
|
|
93
|
+
*
|
|
94
|
+
* Concrete parsers must implement this method and return the parsed
|
|
95
|
+
* representation appropriate for their media type.
|
|
96
|
+
*
|
|
97
|
+
* @abstract
|
|
98
|
+
* @returns {Promise<unknown>} Parsed request body.
|
|
99
|
+
* @throws {Error} If the method is not implemented.
|
|
100
|
+
*/
|
|
101
|
+
async parse() {
|
|
102
|
+
throw new Error('BodyParser.parse() must be implemented.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reads the complete request body from the underlying stream.
|
|
107
|
+
*
|
|
108
|
+
* When `Content-Length` is present, its value is validated before the
|
|
109
|
+
* stream is consumed. The actual number of bytes received is tracked to
|
|
110
|
+
* enforce the configured body size limit. When the limit is exceeded, the
|
|
111
|
+
* underlying request is destroyed so the socket is not left consuming
|
|
112
|
+
* further bytes.
|
|
113
|
+
*
|
|
114
|
+
* Stream-level failures (such as client disconnections) are normalized
|
|
115
|
+
* into `RequestError` instances. `RequestError` instances produced by
|
|
116
|
+
* this method are preserved as-is.
|
|
117
|
+
*
|
|
118
|
+
* @returns {Promise<Buffer>} The complete request body as a Buffer.
|
|
119
|
+
* @throws {RequestError} If `Content-Length` is invalid.
|
|
120
|
+
* @throws {RequestError} If the request body exceeds the configured limit.
|
|
121
|
+
* @throws {RequestError} If the underlying stream fails while being read.
|
|
122
|
+
* @example
|
|
123
|
+
* ```js
|
|
124
|
+
* const buffer = await bodyParser.readBody();
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
async readBody() {
|
|
128
|
+
const contentLength = this.#incomingMessage.headers['content-length'];
|
|
129
|
+
|
|
130
|
+
if (contentLength !== undefined) {
|
|
131
|
+
const length = Number(contentLength);
|
|
132
|
+
|
|
133
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
134
|
+
throw new RequestError('INVALID_CONTENT_LENGTH');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (length > this.#bodyLimit) {
|
|
138
|
+
this.#incomingMessage.destroy();
|
|
139
|
+
throw new RequestError('BODY_LIMIT_EXCEEDED', {
|
|
140
|
+
values: {
|
|
141
|
+
bodySize: length,
|
|
142
|
+
bodyLimit: this.#bodyLimit,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let bytesRead = 0;
|
|
149
|
+
const chunks = [];
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
for await (const chunk of this.#incomingMessage) {
|
|
153
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
154
|
+
bytesRead += buffer.length;
|
|
155
|
+
|
|
156
|
+
if (bytesRead > this.#bodyLimit) {
|
|
157
|
+
this.#incomingMessage.destroy();
|
|
158
|
+
throw new RequestError('BODY_LIMIT_EXCEEDED', {
|
|
159
|
+
values: {
|
|
160
|
+
bodySize: bytesRead,
|
|
161
|
+
bodyLimit: this.#bodyLimit,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
chunks.push(buffer);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Buffer.concat(chunks);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw this.#normalizeStreamError(error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalizes a thrown value from a stream read operation.
|
|
177
|
+
*
|
|
178
|
+
* Preserves existing `RequestError` instances (for example
|
|
179
|
+
* `BODY_LIMIT_EXCEEDED`) and wraps everything else, including native
|
|
180
|
+
* stream errors such as `ECONNRESET`, in a `RequestError`.
|
|
181
|
+
*
|
|
182
|
+
* @param {unknown} error - The value thrown by the stream.
|
|
183
|
+
* @returns {RequestError} The normalized error.
|
|
184
|
+
*/
|
|
185
|
+
#normalizeStreamError(error) {
|
|
186
|
+
if (error instanceof RequestError) {
|
|
187
|
+
return error;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new RequestError('BODY_STREAM_ERROR', {
|
|
191
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
197
|
+
export default BodyParser;
|