@boo-labs/server 1.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +125 -67
- package/package.json +1 -1
- package/source/core/IPCMain/IPCProtocol.mjs +80 -75
- package/source/shared/domain/UUIDV7/index.mjs +31 -39
- package/source/shared/errors/ServerError/index.mjs +48 -2
- package/source/shared/outcome/Result/index.mjs +77 -130
- package/source/shared/outcome/Validation/index.mjs +56 -114
- package/source/shared/validations/Middleware/index.mjs +32 -23
- package/source/shared/validations/PathnamePattern/index.mjs +38 -39
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +25 -15
- package/source/shared/validations/Server/Router/index.mjs +24 -14
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +24 -14
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +25 -15
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +95 -91
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +96 -96
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +55 -43
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +28 -13
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +101 -111
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +55 -43
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +71 -61
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +44 -31
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +39 -25
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +27 -14
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +85 -84
- package/source/shared/outcome/ErrorSchema.mjs +0 -361
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Beltran Rodriguez Victor
|
|
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
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# @boo-labs/server
|
|
2
2
|
|
|
3
|
-
A lightweight, zero-dependency HTTP server for Node.js, designed around
|
|
4
|
-
modular architecture, explicit configuration, and a predictable request
|
|
5
|
-
lifecycle.
|
|
3
|
+
A lightweight, zero-dependency HTTP server for Node.js, designed around modular architecture, explicit configuration, and a predictable request lifecycle.
|
|
6
4
|
|
|
7
|
-
`@boo-labs/server` provides a small public API for building HTTP applications
|
|
8
|
-
with support for HTTP/1.1, HTTPS, and HTTP/2, along with routing,
|
|
9
|
-
middleware composition, lifecycle events, and graceful resource management.
|
|
5
|
+
`@boo-labs/server` provides a small public API for building HTTP applications with support for HTTP/1.1, HTTPS, and HTTP/2, along with routing, middleware composition, lifecycle events, and graceful resource management.
|
|
10
6
|
|
|
11
7
|
## Features
|
|
12
8
|
|
|
@@ -45,14 +41,14 @@ TypeScript declarations for the public API are included in the package.
|
|
|
45
41
|
```js
|
|
46
42
|
import Server, { HttpServerConfiguration } from '@boo-labs/server';
|
|
47
43
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
44
|
+
const configuration = new HttpServerConfiguration({
|
|
45
|
+
listen: {
|
|
46
|
+
host: '127.0.0.1',
|
|
47
|
+
port: 3000,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const server = new Server(configuration);
|
|
56
52
|
|
|
57
53
|
server.get('/hello', (exchange) => {
|
|
58
54
|
exchange.response.json({ message: 'Hello, World!' });
|
|
@@ -73,12 +69,9 @@ await server.listen();
|
|
|
73
69
|
|
|
74
70
|
### Exchange
|
|
75
71
|
|
|
76
|
-
An `Exchange` represents a single HTTP transaction. It couples the incoming
|
|
77
|
-
request with its outgoing response and exposes shared per-request state,
|
|
78
|
-
correlation identifiers, and a cancellation signal.
|
|
72
|
+
An `Exchange` represents a single HTTP transaction. It couples the incoming request with its outgoing response and exposes shared per-request state, correlation identifiers, and a cancellation signal.
|
|
79
73
|
|
|
80
|
-
Exchanges are created by the runtime and passed to middleware and handlers.
|
|
81
|
-
Consumers do not construct exchanges.
|
|
74
|
+
Exchanges are created by the runtime and passed to middleware and handlers. Consumers do not construct exchanges.
|
|
82
75
|
|
|
83
76
|
```js
|
|
84
77
|
server.get('/hello', async (exchange) => {
|
|
@@ -86,8 +79,17 @@ server.get('/hello', async (exchange) => {
|
|
|
86
79
|
});
|
|
87
80
|
```
|
|
88
81
|
|
|
89
|
-
Key properties:
|
|
90
|
-
|
|
82
|
+
Key properties:
|
|
83
|
+
|
|
84
|
+
| Property | Type | Description |
|
|
85
|
+
|-------------|---------------------------|------------------------------------------|
|
|
86
|
+
| `id` | `string` | Unique identifier for the exchange. |
|
|
87
|
+
| `traceId` | `string` | W3C trace identifier. |
|
|
88
|
+
| `spanId` | `string` | W3C span identifier. |
|
|
89
|
+
| `signal` | `AbortSignal` | Cancellation signal for the exchange. |
|
|
90
|
+
| `request` | `Request` | Incoming request. |
|
|
91
|
+
| `response` | `Response` | Outgoing response. |
|
|
92
|
+
| `state` | `Record<string, unknown>` | Mutable per-request state. |
|
|
91
93
|
|
|
92
94
|
### Request
|
|
93
95
|
|
|
@@ -103,26 +105,31 @@ Supported media types for `body()`:
|
|
|
103
105
|
| `application/x-www-form-urlencoded` | Parsed key/value object |
|
|
104
106
|
| `multipart/form-data` | Parsed multipart representation |
|
|
105
107
|
|
|
106
|
-
Unsupported media types raise `RequestError`. When the body exceeds the
|
|
107
|
-
configured limit, reading fails with `RequestError` and the connection may
|
|
108
|
-
be closed without a normal error response.
|
|
108
|
+
Unsupported media types raise `RequestError`. When the body exceeds the configured limit, reading fails with `RequestError` and the connection may be closed without a normal error response.
|
|
109
109
|
|
|
110
110
|
The `CONNECT` method is not supported.
|
|
111
111
|
|
|
112
112
|
### Response
|
|
113
113
|
|
|
114
|
-
Built progressively by configuring status, headers, and body. Body methods
|
|
115
|
-
do not send anything; only `end()` materializes and sends the response.
|
|
116
|
-
A response can describe at most one body.
|
|
114
|
+
Built progressively by configuring status, headers, and body. Body methods do not send anything; only `end()` materializes and sends the response. A response can describe at most one body.
|
|
117
115
|
|
|
118
|
-
Convenience methods:
|
|
119
|
-
|
|
116
|
+
Convenience methods:
|
|
117
|
+
|
|
118
|
+
| Method | Purpose |
|
|
119
|
+
|--------------|--------------------------------------------------|
|
|
120
|
+
| `json()` | Serializes a value as JSON. |
|
|
121
|
+
| `html()` | Sends an HTML body. |
|
|
122
|
+
| `text()` | Sends a plain-text body. |
|
|
123
|
+
| `send()` | Sends a string or binary body. |
|
|
124
|
+
| `stream()` | Streams a readable body. |
|
|
125
|
+
| `redirect()` | Sends a redirect response. |
|
|
126
|
+
| `cache()` | Sets a public cache lifetime. |
|
|
127
|
+
| `noCache()` | Disables caching. |
|
|
128
|
+
| `vary()` | Adds fields to the `Vary` header. |
|
|
120
129
|
|
|
121
130
|
### Middleware
|
|
122
131
|
|
|
123
|
-
Middleware functions receive `(exchange, next)` and run in registration
|
|
124
|
-
order. They may be registered globally, by pathname, on a route, or on a
|
|
125
|
-
router.
|
|
132
|
+
Middleware functions receive `(exchange, next)` and run in registration order. They may be registered globally, by pathname, on a route, or on a router.
|
|
126
133
|
|
|
127
134
|
```js
|
|
128
135
|
server.use(async (exchange, next) => {
|
|
@@ -141,14 +148,25 @@ server.use(async (exchange, next) => {
|
|
|
141
148
|
|
|
142
149
|
Routes associate an HTTP method and a pathname pattern with middleware.
|
|
143
150
|
|
|
144
|
-
Supported methods:
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
Supported methods:
|
|
152
|
+
|
|
153
|
+
| Method | Server | Router |
|
|
154
|
+
|------------|--------|--------|
|
|
155
|
+
| `get()` | ✅ | ✅ |
|
|
156
|
+
| `post()` | ✅ | ✅ |
|
|
157
|
+
| `put()` | ✅ | ✅ |
|
|
158
|
+
| `patch()` | ✅ | ✅ |
|
|
159
|
+
| `delete()` | ✅ | ✅ |
|
|
160
|
+
| `head()` | ✅ | ✅ |
|
|
161
|
+
| `trace()` | ✅ | ✅ |
|
|
162
|
+
| `query()` | ✅ | ✅ |
|
|
163
|
+
| `all()` | ❌ | ✅ |
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
**Note:** `OPTIONS` is handled automatically and does not require a dedicated registration method.
|
|
166
|
+
|
|
167
|
+
Pathname patterns support literals, named parameters (`:id` or `{id}`), and wildcards (`*` / trailing `/*`).
|
|
168
|
+
|
|
169
|
+
Named parameters and wildcards are used only for matching; captured values are not exposed on the exchange. To read a path segment, parse `exchange.request.pathname` directly.
|
|
152
170
|
|
|
153
171
|
```js
|
|
154
172
|
const api = new Router();
|
|
@@ -168,15 +186,25 @@ Four configuration classes fix the transport:
|
|
|
168
186
|
| `Http2CleartextServerConfiguration` | `http` | `h2c` |
|
|
169
187
|
| `Http2SecureServerConfiguration` | `https` | `h2` |
|
|
170
188
|
|
|
171
|
-
Common top-level options:
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
Common top-level options:
|
|
190
|
+
|
|
191
|
+
| Option | Purpose |
|
|
192
|
+
|-------------------|-----------------------------------------------|
|
|
193
|
+
| `connection` | Transport-specific Node.js server options. |
|
|
194
|
+
| `listen` | Host, port, and backlog for binding. |
|
|
195
|
+
| `request` | Request parsing and proxy trust. |
|
|
196
|
+
| `response` | Response shaping (ETag, charset). |
|
|
197
|
+
| `static` | Static file serving. |
|
|
198
|
+
| `cors` | Cross-Origin Resource Sharing. |
|
|
199
|
+
| `securityHeaders` | HTTP security response headers. |
|
|
200
|
+
| `rateLimit` | Request rate limiting. |
|
|
201
|
+
| `cookie` | Cookie parsing and cookie-backed sessions. |
|
|
202
|
+
| `csrf` | CSRF protection. |
|
|
203
|
+
| `compress` | Response compression. |
|
|
174
204
|
|
|
175
|
-
These options enable server features. When enabled, the corresponding
|
|
176
|
-
behavior is applied before user-registered middleware.
|
|
205
|
+
These options enable server features. When enabled, the corresponding behavior is applied before user-registered middleware.
|
|
177
206
|
|
|
178
|
-
See the [full API reference](
|
|
179
|
-
option shapes and defaults.
|
|
207
|
+
See the [full API reference](https://github.com/vgbr-dev/boo-labs/wiki) for the complete option shapes and defaults.
|
|
180
208
|
|
|
181
209
|
## Lifecycle and Events
|
|
182
210
|
|
|
@@ -186,45 +214,75 @@ await server.listen();
|
|
|
186
214
|
await server.close();
|
|
187
215
|
```
|
|
188
216
|
|
|
189
|
-
States:
|
|
217
|
+
States:
|
|
218
|
+
|
|
219
|
+
| State | Description |
|
|
220
|
+
|-------------|------------------------------------------------------|
|
|
221
|
+
| `idle` | Initial state. The server has not started. |
|
|
222
|
+
| `starting` | The server is starting up. |
|
|
223
|
+
| `listening` | The server is accepting connections. |
|
|
224
|
+
| `stopping` | The server is shutting down. |
|
|
225
|
+
| `closed` | The server has closed. |
|
|
226
|
+
| `error` | The server encountered an error and cannot continue. |
|
|
227
|
+
|
|
228
|
+
Events:
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`
|
|
230
|
+
| Event | Convenience method | Description |
|
|
231
|
+
|------------------|-----------------------|----------------------------------|
|
|
232
|
+
| `listen` | `onListen()` | The server started listening. |
|
|
233
|
+
| `close` | `onClose()` | The server has closed. |
|
|
234
|
+
| `error` | `onError()` | A server error occurred. |
|
|
235
|
+
| `exchange:start` | `onExchangeStart()` | An exchange began processing. |
|
|
236
|
+
| `exchange:end` | `onExchangeEnd()` | An exchange ended. |
|
|
237
|
+
| `exchange:error` | `onExchangeError()` | An exchange failed. |
|
|
194
238
|
|
|
195
|
-
`Server` implements `Symbol.asyncDispose` for `await using` when the
|
|
196
|
-
runtime supports it.
|
|
239
|
+
`Server` implements `Symbol.asyncDispose` for `await using` when the runtime supports it.
|
|
197
240
|
|
|
198
241
|
## IPC
|
|
199
242
|
|
|
200
|
-
An IPC API over WebSocket at `/ipc`. Supports request-response channels
|
|
201
|
-
(`handle` / invoke) and one-way events (`on` / event). Available while the
|
|
243
|
+
An IPC API over WebSocket at `/ipc`. Supports request-response channels (`handle` / invoke) and one-way events (`on` / event). Available while the
|
|
202
244
|
server is listening via `server.ipc`.
|
|
203
245
|
|
|
204
|
-
|
|
205
|
-
|
|
246
|
+
```js
|
|
247
|
+
await server.listen();
|
|
248
|
+
|
|
249
|
+
server.ipc.handle('users:get', async (id) => {
|
|
250
|
+
return usersRepository.findById(id);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
server.ipc.on('analytics:event', (event) => {
|
|
254
|
+
analytics.record(event);
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
See the [full API reference](https://github.com/vgbr-dev/boo-labs/wiki) for the wire protocol and handler details.
|
|
206
259
|
|
|
207
260
|
## Errors
|
|
208
261
|
|
|
209
262
|
Public APIs throw errors that extend `Error` and expose a stable `code`
|
|
210
|
-
property.
|
|
211
|
-
|
|
212
|
-
|
|
263
|
+
property.
|
|
264
|
+
|
|
265
|
+
| Error | Domain |
|
|
266
|
+
|----------------------------|----------------------------------------------|
|
|
267
|
+
| `ServerConfigurationError` | Server configuration validation. |
|
|
268
|
+
| `RequestError` | Incoming request processing. |
|
|
269
|
+
| `ResponseError` | Outgoing response construction. |
|
|
270
|
+
| `RouterError` | Router route registration. |
|
|
271
|
+
| `PathnamePatternError` | Pathname pattern validation. |
|
|
272
|
+
| `MiddlewareError` | Middleware validation. |
|
|
273
|
+
| `ServerError` | Server lifecycle and state transitions. |
|
|
274
|
+
| `IPCError` | IPC operations. |
|
|
213
275
|
|
|
214
|
-
Error codes are documented in the
|
|
215
|
-
[project wiki](https://github.com/vgbr-dev/boo-labs/wiki/Errors).
|
|
276
|
+
Error codes are documented in the [project wiki](https://github.com/vgbr-dev/boo-labs/wiki/Errors).
|
|
216
277
|
|
|
217
278
|
## Documentation
|
|
218
279
|
|
|
219
|
-
- [Full API reference](
|
|
220
|
-
configuration options, response methods, IPC protocol, and more.
|
|
280
|
+
- [Full API reference](https://github.com/vgbr-dev/boo-labs/wiki) — complete public API, configuration options, response methods, IPC protocol, and more.
|
|
221
281
|
|
|
222
282
|
## License
|
|
223
283
|
|
|
224
|
-
Distributed under the MIT License. See the [LICENSE](./LICENSE) file for
|
|
225
|
-
details.
|
|
284
|
+
Distributed under the MIT License. See the [LICENSE](./LICENSE) file for details.
|
|
226
285
|
|
|
227
286
|
## Dedication
|
|
228
287
|
|
|
229
|
-
This project is named after my sister — "Boo". Her nickname gave this
|
|
230
|
-
project its name.
|
|
288
|
+
This project is named after my sister — "Boo". Her nickname gave this project its name.
|
package/package.json
CHANGED
|
@@ -17,6 +17,20 @@
|
|
|
17
17
|
import Result from '#shared/outcome/Result';
|
|
18
18
|
|
|
19
19
|
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
20
|
+
/**
|
|
21
|
+
* A successful {@link Result} instance.
|
|
22
|
+
*
|
|
23
|
+
* @template T
|
|
24
|
+
* @typedef {import('#shared/outcome/Result').SuccessResult<T>} SuccessResult
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A failed {@link Result} instance.
|
|
29
|
+
*
|
|
30
|
+
* @template {Error} E
|
|
31
|
+
* @typedef {import('#shared/outcome/Result').FailureResult<E>} FailureResult
|
|
32
|
+
*/
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* Represents an IPC invocation request.
|
|
22
36
|
*
|
|
@@ -99,6 +113,18 @@ function isPlainObject(value) {
|
|
|
99
113
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
100
114
|
}
|
|
101
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Attaches a machine-readable code to an Error instance.
|
|
118
|
+
*
|
|
119
|
+
* @template {Error} E
|
|
120
|
+
* @param {E} error - Error instance to annotate.
|
|
121
|
+
* @param {string} code - Machine-readable error code.
|
|
122
|
+
* @returns {E & { code: string }} The annotated error.
|
|
123
|
+
*/
|
|
124
|
+
function withCode(error, code) {
|
|
125
|
+
return Object.assign(error, { code });
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
129
|
/**
|
|
104
130
|
* Implements the IPC wire protocol for decoding requests and serializing responses.
|
|
@@ -243,25 +269,19 @@ class IPCProtocol {
|
|
|
243
269
|
* a failure describing the first structural violation encountered.
|
|
244
270
|
*
|
|
245
271
|
* @param {unknown} message - Parsed IPC payload.
|
|
246
|
-
* @returns {
|
|
272
|
+
* @returns {SuccessResult<IPCRequest>|FailureResult<TypeError|ReferenceError>} Successful result containing the normalized request, or a failure describing the validation error.
|
|
247
273
|
*/
|
|
248
274
|
#ensureRequest(message) {
|
|
249
275
|
if (!isPlainObject(message)) {
|
|
250
|
-
return Result.failure(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
code: 'IPC_INVALID_MESSAGE',
|
|
254
|
-
custom: false,
|
|
255
|
-
});
|
|
276
|
+
return Result.failure(
|
|
277
|
+
withCode(new TypeError('IPC message must be a plain object.'), 'IPC_INVALID_MESSAGE'),
|
|
278
|
+
);
|
|
256
279
|
}
|
|
257
280
|
|
|
258
281
|
if (!('type' in message)) {
|
|
259
|
-
return Result.failure(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
code: 'IPC_MISSING_TYPE',
|
|
263
|
-
custom: false,
|
|
264
|
-
});
|
|
282
|
+
return Result.failure(
|
|
283
|
+
withCode(new ReferenceError('IPC message requires a "type" property.'), 'IPC_MISSING_TYPE'),
|
|
284
|
+
);
|
|
265
285
|
}
|
|
266
286
|
|
|
267
287
|
if (message.type === 'invoke') {
|
|
@@ -272,73 +292,61 @@ class IPCProtocol {
|
|
|
272
292
|
return this.#ensureEventRequest(message);
|
|
273
293
|
}
|
|
274
294
|
|
|
275
|
-
return Result.failure(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
code: 'IPC_INVALID_TYPE',
|
|
279
|
-
custom: false,
|
|
280
|
-
});
|
|
295
|
+
return Result.failure(
|
|
296
|
+
withCode(new TypeError('IPC message type must be "invoke" or "event".'), 'IPC_INVALID_TYPE'),
|
|
297
|
+
);
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
/**
|
|
284
301
|
* Ensures a parsed IPC payload is a valid invocation request.
|
|
285
302
|
*
|
|
286
303
|
* @param {Record<string, unknown>} message - Parsed IPC payload.
|
|
287
|
-
* @returns {
|
|
304
|
+
* @returns {SuccessResult<IPCInvokeRequest>|FailureResult<TypeError|ReferenceError|RangeError>} Successful result containing the normalized request, or a failure describing the validation error.
|
|
288
305
|
*/
|
|
289
306
|
#ensureInvokeRequest(message) {
|
|
290
307
|
if (!('channel' in message)) {
|
|
291
|
-
return Result.failure(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
return Result.failure(
|
|
309
|
+
withCode(
|
|
310
|
+
new ReferenceError('IPC message requires a "channel" property.'),
|
|
311
|
+
'IPC_MISSING_CHANNEL',
|
|
312
|
+
),
|
|
313
|
+
);
|
|
297
314
|
}
|
|
298
315
|
|
|
299
316
|
if (typeof message.channel !== 'string') {
|
|
300
|
-
return Result.failure(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
code: 'IPC_INVALID_CHANNEL_TYPE',
|
|
304
|
-
custom: false,
|
|
305
|
-
});
|
|
317
|
+
return Result.failure(
|
|
318
|
+
withCode(new TypeError('IPC channel must be a string.'), 'IPC_INVALID_CHANNEL_TYPE'),
|
|
319
|
+
);
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
if (message.channel.trim().length === 0) {
|
|
309
|
-
return Result.failure(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
return Result.failure(
|
|
324
|
+
withCode(
|
|
325
|
+
new RangeError('IPC channel must be a non-empty string.'),
|
|
326
|
+
'IPC_INVALID_CHANNEL_RANGE',
|
|
327
|
+
),
|
|
328
|
+
);
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
if (!('id' in message)) {
|
|
318
|
-
return Result.failure(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
code: 'IPC_MISSING_ID',
|
|
322
|
-
custom: false,
|
|
323
|
-
});
|
|
332
|
+
return Result.failure(
|
|
333
|
+
withCode(new ReferenceError('IPC invocation requires an "id" property.'), 'IPC_MISSING_ID'),
|
|
334
|
+
);
|
|
324
335
|
}
|
|
325
336
|
|
|
326
337
|
if (typeof message.id !== 'string') {
|
|
327
|
-
return Result.failure(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
code: 'IPC_INVALID_ID_TYPE',
|
|
331
|
-
custom: false,
|
|
332
|
-
});
|
|
338
|
+
return Result.failure(
|
|
339
|
+
withCode(new TypeError('IPC invocation id must be a string.'), 'IPC_INVALID_ID_TYPE'),
|
|
340
|
+
);
|
|
333
341
|
}
|
|
334
342
|
|
|
335
343
|
if (message.id.trim().length === 0) {
|
|
336
|
-
return Result.failure(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
344
|
+
return Result.failure(
|
|
345
|
+
withCode(
|
|
346
|
+
new RangeError('IPC invocation id must be a non-empty string.'),
|
|
347
|
+
'IPC_INVALID_ID_RANGE',
|
|
348
|
+
),
|
|
349
|
+
);
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
return Result.success({
|
|
@@ -353,34 +361,31 @@ class IPCProtocol {
|
|
|
353
361
|
* Ensures a parsed IPC payload is a valid event request.
|
|
354
362
|
*
|
|
355
363
|
* @param {Record<string, unknown>} message - Parsed IPC payload.
|
|
356
|
-
* @returns {
|
|
364
|
+
* @returns {SuccessResult<IPCEventRequest>|FailureResult<TypeError|ReferenceError|RangeError>} Successful result containing the normalized request, or a failure describing the validation error.
|
|
357
365
|
*/
|
|
358
366
|
#ensureEventRequest(message) {
|
|
359
367
|
if (!('channel' in message)) {
|
|
360
|
-
return Result.failure(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
368
|
+
return Result.failure(
|
|
369
|
+
withCode(
|
|
370
|
+
new ReferenceError('IPC message requires a "channel" property.'),
|
|
371
|
+
'IPC_MISSING_CHANNEL',
|
|
372
|
+
),
|
|
373
|
+
);
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
if (typeof message.channel !== 'string') {
|
|
369
|
-
return Result.failure(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
code: 'IPC_INVALID_CHANNEL_TYPE',
|
|
373
|
-
custom: false,
|
|
374
|
-
});
|
|
377
|
+
return Result.failure(
|
|
378
|
+
withCode(new TypeError('IPC channel must be a string.'), 'IPC_INVALID_CHANNEL_TYPE'),
|
|
379
|
+
);
|
|
375
380
|
}
|
|
376
381
|
|
|
377
382
|
if (message.channel.trim().length === 0) {
|
|
378
|
-
return Result.failure(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
return Result.failure(
|
|
384
|
+
withCode(
|
|
385
|
+
new RangeError('IPC channel must be a non-empty string.'),
|
|
386
|
+
'IPC_INVALID_CHANNEL_RANGE',
|
|
387
|
+
),
|
|
388
|
+
);
|
|
384
389
|
}
|
|
385
390
|
|
|
386
391
|
return Result.success({
|
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
* @module UUIDV7
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
/**
|
|
18
|
+
* A successful {@link Validation} instance.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {import('#shared/outcome/Validation').SuccessValidation} SuccessValidation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A failed {@link Validation} instance.
|
|
25
|
+
*
|
|
26
|
+
* @template {Error} E
|
|
27
|
+
* @typedef {import('#shared/outcome/Validation').FailureValidation<E>} FailureValidation
|
|
28
|
+
*/
|
|
29
|
+
|
|
16
30
|
// ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
31
|
// » IMPORT NATIVE NODEJS MODULES
|
|
18
32
|
import { randomBytes } from 'node:crypto';
|
|
@@ -42,6 +56,11 @@ const UUIDV7_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[
|
|
|
42
56
|
* sortable.
|
|
43
57
|
*/
|
|
44
58
|
class UUIDV7 {
|
|
59
|
+
/**
|
|
60
|
+
* The UUID value.
|
|
61
|
+
*
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
45
64
|
#uuid;
|
|
46
65
|
|
|
47
66
|
/**
|
|
@@ -235,8 +254,9 @@ class UUIDV7 {
|
|
|
235
254
|
* timestamp, and a UUID v7 string represents the timestamp encoded in that
|
|
236
255
|
* UUID.
|
|
237
256
|
*
|
|
238
|
-
* @param {
|
|
239
|
-
* @returns {
|
|
257
|
+
* @param {unknown} value - Value to validate.
|
|
258
|
+
* @returns {SuccessValidation|FailureValidation<RangeError|TypeError>} Validation result.
|
|
259
|
+
*
|
|
240
260
|
* @example
|
|
241
261
|
* ```js
|
|
242
262
|
* const result = UUIDV7.validate();
|
|
@@ -249,41 +269,27 @@ class UUIDV7 {
|
|
|
249
269
|
|
|
250
270
|
if (value instanceof Date) {
|
|
251
271
|
if (Number.isNaN(value.getTime())) {
|
|
252
|
-
return Validation.failure(
|
|
253
|
-
error: RangeError,
|
|
254
|
-
message: 'Invalid timestamp.',
|
|
255
|
-
code: 'INVALID_TIMESTAMP',
|
|
256
|
-
custom: false,
|
|
257
|
-
});
|
|
272
|
+
return Validation.failure(new RangeError('Invalid timestamp.'));
|
|
258
273
|
}
|
|
259
274
|
return Validation.success();
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
if (typeof value === 'string') {
|
|
263
278
|
if (!UUIDV7_REGEXP.test(value)) {
|
|
264
|
-
return Validation.failure(
|
|
265
|
-
error: RangeError,
|
|
266
|
-
message: 'Invalid UUID v7.',
|
|
267
|
-
code: 'INVALID_UUID',
|
|
268
|
-
custom: false,
|
|
269
|
-
});
|
|
279
|
+
return Validation.failure(new RangeError('Invalid UUID v7.'));
|
|
270
280
|
}
|
|
271
281
|
return Validation.success();
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
return Validation.failure(
|
|
275
|
-
error: TypeError,
|
|
276
|
-
message: 'Timestamp must be a Date, string, or undefined.',
|
|
277
|
-
code: 'INVALID_TIMESTAMP_SOURCE',
|
|
278
|
-
custom: false,
|
|
279
|
-
});
|
|
284
|
+
return Validation.failure(new TypeError('Timestamp must be a Date, string, or undefined.'));
|
|
280
285
|
}
|
|
281
286
|
|
|
282
287
|
/**
|
|
283
288
|
* Validates that a value is a valid UUID v7 string.
|
|
284
289
|
*
|
|
285
290
|
* @param {unknown} value - Value to validate.
|
|
286
|
-
* @returns {
|
|
291
|
+
* @returns {SuccessValidation|FailureValidation<ReferenceError|RangeError|TypeError>} Validation result.
|
|
292
|
+
*
|
|
287
293
|
* @example
|
|
288
294
|
* ```js
|
|
289
295
|
* const result = UUIDV7.validateUUIDString('019937f2-1234-7abc-8def-123456789abc');
|
|
@@ -291,30 +297,15 @@ class UUIDV7 {
|
|
|
291
297
|
*/
|
|
292
298
|
static validateUUIDString(value) {
|
|
293
299
|
if (value === undefined) {
|
|
294
|
-
return Validation.failure(
|
|
295
|
-
error: ReferenceError,
|
|
296
|
-
message: 'UUID must be a string.',
|
|
297
|
-
custom: false,
|
|
298
|
-
code: 'MISSING_UUID',
|
|
299
|
-
});
|
|
300
|
+
return Validation.failure(new ReferenceError('UUID must be a string.'));
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
if (typeof value !== 'string') {
|
|
303
|
-
return Validation.failure(
|
|
304
|
-
error: TypeError,
|
|
305
|
-
message: 'UUID must be a string.',
|
|
306
|
-
custom: false,
|
|
307
|
-
code: 'INVALID_UUID_TYPE',
|
|
308
|
-
});
|
|
304
|
+
return Validation.failure(new TypeError('UUID must be a string.'));
|
|
309
305
|
}
|
|
310
306
|
|
|
311
307
|
if (!UUIDV7_REGEXP.test(value)) {
|
|
312
|
-
return Validation.failure(
|
|
313
|
-
error: RangeError,
|
|
314
|
-
message: 'Invalid UUID v7 format.',
|
|
315
|
-
custom: false,
|
|
316
|
-
code: 'INVALID_UUID',
|
|
317
|
-
});
|
|
308
|
+
return Validation.failure(new RangeError('Invalid UUID v7 format.'));
|
|
318
309
|
}
|
|
319
310
|
|
|
320
311
|
return Validation.success();
|
|
@@ -328,6 +319,7 @@ class UUIDV7 {
|
|
|
328
319
|
* @param {unknown} value - Value to validate.
|
|
329
320
|
* @throws {TypeError} If the value has an unsupported type.
|
|
330
321
|
* @throws {RangeError} If the value is invalid.
|
|
322
|
+
*
|
|
331
323
|
* @example
|
|
332
324
|
* ```js
|
|
333
325
|
* UUIDV7.assert(new Date());
|