borgmcp-server 0.1.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 (71) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +8 -0
  3. package/README.md +119 -0
  4. package/SECURITY.md +38 -0
  5. package/THIRD_PARTY_NOTICES.md +35 -0
  6. package/dist/bootstrap.d.ts +18 -0
  7. package/dist/bootstrap.js +104 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/cli.d.ts +7 -0
  10. package/dist/cli.js +96 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/coordination-api.d.ts +25 -0
  13. package/dist/coordination-api.js +457 -0
  14. package/dist/coordination-api.js.map +1 -0
  15. package/dist/credentials.d.ts +58 -0
  16. package/dist/credentials.js +244 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/enrollment.d.ts +17 -0
  19. package/dist/enrollment.js +122 -0
  20. package/dist/enrollment.js.map +1 -0
  21. package/dist/https-server.d.ts +94 -0
  22. package/dist/https-server.js +814 -0
  23. package/dist/https-server.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/main.d.ts +3 -0
  28. package/dist/main.js +67 -0
  29. package/dist/main.js.map +1 -0
  30. package/dist/migrations.d.ts +8 -0
  31. package/dist/migrations.js +366 -0
  32. package/dist/migrations.js.map +1 -0
  33. package/dist/network-policy.d.ts +13 -0
  34. package/dist/network-policy.js +49 -0
  35. package/dist/network-policy.js.map +1 -0
  36. package/dist/operator-error.d.ts +3 -0
  37. package/dist/operator-error.js +63 -0
  38. package/dist/operator-error.js.map +1 -0
  39. package/dist/principal.d.ts +32 -0
  40. package/dist/principal.js +64 -0
  41. package/dist/principal.js.map +1 -0
  42. package/dist/protocol-draft.d.ts +2 -0
  43. package/dist/protocol-draft.js +31 -0
  44. package/dist/protocol-draft.js.map +1 -0
  45. package/dist/service.d.ts +61 -0
  46. package/dist/service.js +455 -0
  47. package/dist/service.js.map +1 -0
  48. package/dist/start-options.d.ts +2 -0
  49. package/dist/start-options.js +46 -0
  50. package/dist/start-options.js.map +1 -0
  51. package/dist/store.d.ts +327 -0
  52. package/dist/store.js +1729 -0
  53. package/dist/store.js.map +1 -0
  54. package/npm-shrinkwrap.json +1942 -0
  55. package/package.json +60 -0
  56. package/src/bootstrap.ts +127 -0
  57. package/src/cli.ts +102 -0
  58. package/src/coordination-api.ts +508 -0
  59. package/src/credentials.ts +319 -0
  60. package/src/enrollment.ts +156 -0
  61. package/src/https-server.ts +962 -0
  62. package/src/index.ts +3 -0
  63. package/src/main.ts +73 -0
  64. package/src/migrations.ts +394 -0
  65. package/src/network-policy.ts +65 -0
  66. package/src/operator-error.ts +97 -0
  67. package/src/principal.ts +106 -0
  68. package/src/protocol-draft.ts +32 -0
  69. package/src/service.ts +525 -0
  70. package/src/start-options.ts +46 -0
  71. package/src/store.ts +2316 -0
@@ -0,0 +1,814 @@
1
+ import { createServer } from "node:https";
2
+ import { createHash, X509Certificate } from "node:crypto";
3
+ import { resolveBindOptions } from "./network-policy.js";
4
+ export const DEFAULT_SERVICE_LIMITS = {
5
+ maxConnections: 100,
6
+ maxConnectionsPerAddress: 25,
7
+ maxRequestsPerWindow: 120,
8
+ maxRequestsPerAddressWindow: 600,
9
+ maxRequestsGlobalWindow: 5_000,
10
+ rateLimitWindowMs: 60_000,
11
+ maxRateLimitEntries: 1_024,
12
+ maxStreamsPerCredential: 8,
13
+ maxHeaderBytes: 16_384,
14
+ maxRequestBodyBytes: 65_536,
15
+ maxRequestsPerSocket: 100,
16
+ requestTimeoutMs: 15_000,
17
+ tlsHandshakeTimeoutMs: 10_000,
18
+ headersTimeoutMs: 10_000,
19
+ keepAliveTimeoutMs: 5_000,
20
+ handlerTimeoutMs: 5_000,
21
+ };
22
+ export async function startHttpsServer(options) {
23
+ if (options.handleCoordination !== undefined && options.authorizeCoordination === undefined) {
24
+ throw new Error("Coordination routes require server-derived principal authentication.");
25
+ }
26
+ const bind = resolveBindOptions(options.bind ?? {});
27
+ const limits = options.limits ?? DEFAULT_SERVICE_LIMITS;
28
+ validateLimits(limits);
29
+ validateTlsCertificate(options.tls.cert, bind.host, bind.mode, options.tls.ca);
30
+ const handlerContext = createRequestHandlerContext(options);
31
+ const server = createServer({
32
+ key: options.tls.key,
33
+ cert: options.tls.cert,
34
+ minVersion: "TLSv1.3",
35
+ maxHeaderSize: limits.maxHeaderBytes,
36
+ requestTimeout: limits.requestTimeoutMs,
37
+ handshakeTimeout: limits.tlsHandshakeTimeoutMs,
38
+ headersTimeout: limits.headersTimeoutMs,
39
+ keepAliveTimeout: limits.keepAliveTimeoutMs,
40
+ }, createRequestListener(handlerContext, limits, options.testHooks?.identifyRemoteAddress));
41
+ const acceptedSockets = applyServerLimits(server, limits);
42
+ server.on("secureConnection", (socket) => socket.disableRenegotiation());
43
+ server.on("tlsClientError", (_error, socket) => socket.destroy());
44
+ server.on("clientError", (_error, socket) => socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"));
45
+ server.on("checkContinue", (_request, response) => sendEmpty(response, 417, true));
46
+ try {
47
+ await listen(server, bind.port, bind.host);
48
+ }
49
+ catch (error) {
50
+ server.closeAllConnections();
51
+ try {
52
+ server.close();
53
+ }
54
+ catch {
55
+ // Preserve the originating listen failure.
56
+ }
57
+ throw error;
58
+ }
59
+ const address = server.address();
60
+ const displayHost = address.family === "IPv6" ? `[${address.address}]` : address.address;
61
+ let closePromise;
62
+ return {
63
+ origin: `https://${displayHost}:${address.port}`,
64
+ limits,
65
+ close: () => {
66
+ closePromise ??= close(server, acceptedSockets);
67
+ return closePromise;
68
+ },
69
+ };
70
+ }
71
+ export function createRequestHandlerContext(options) {
72
+ return Object.freeze({
73
+ protocolInfo: options.protocolInfo,
74
+ authorizeProtocol: options.authorizeProtocol,
75
+ ...(options.exchangeEnrollment === undefined
76
+ ? {}
77
+ : { exchangeEnrollment: options.exchangeEnrollment }),
78
+ ...(options.authorizeCoordination === undefined
79
+ ? {}
80
+ : { authorizeCoordination: options.authorizeCoordination }),
81
+ ...(options.handleCoordination === undefined
82
+ ? {}
83
+ : { handleCoordination: options.handleCoordination }),
84
+ });
85
+ }
86
+ function createRequestListener(context, limits, identifyRemoteAddress = (socket) => socket.remoteAddress ?? "unknown") {
87
+ const admissionLimiter = new PreAuthAdmissionLimiter(limits);
88
+ const credentialRateLimiter = new RequestRateLimiter(limits, limits.maxRequestsPerWindow);
89
+ const streamQuota = new ConcurrentQuota(limits.maxStreamsPerCredential);
90
+ return (request, response) => {
91
+ const controller = new AbortController();
92
+ response.once("close", () => controller.abort());
93
+ let timer;
94
+ const deadline = new Promise((resolve) => {
95
+ timer = setTimeout(() => {
96
+ controller.abort();
97
+ resolve("deadline");
98
+ }, limits.handlerTimeoutMs);
99
+ timer.unref();
100
+ });
101
+ const handled = handleRequest(request, response, context, limits, admissionLimiter, credentialRateLimiter, streamQuota, identifyRemoteAddress, controller.signal)
102
+ .then(() => "handled");
103
+ void Promise.race([handled, deadline])
104
+ .then((outcome) => {
105
+ if (outcome === "deadline")
106
+ sendEmpty(response, 503, true);
107
+ })
108
+ .catch(() => sendEmpty(response, 500, true))
109
+ .finally(() => {
110
+ if (timer !== undefined)
111
+ clearTimeout(timer);
112
+ });
113
+ };
114
+ }
115
+ async function handleRequest(request, response, context, limits, admissionLimiter, credentialRateLimiter, streamQuota, identifyRemoteAddress, signal) {
116
+ const addressIdentity = `address:${identifyRemoteAddress(request.socket)}`;
117
+ const preAuthRetry = admissionLimiter.consume(addressIdentity);
118
+ if (preAuthRetry !== null) {
119
+ request.resume();
120
+ sendRateLimited(response, preAuthRetry);
121
+ return;
122
+ }
123
+ if (request.headers.origin !== undefined) {
124
+ request.resume();
125
+ sendEmpty(response, 403, true);
126
+ return;
127
+ }
128
+ const path = parseRequestPath(request.url);
129
+ const requestBody = await readRequestBody(request, limits.maxRequestBodyBytes);
130
+ if (requestBody === "oversized") {
131
+ if (isCoordinationPath(path)) {
132
+ sendJson(response, 413, protocolError("CONTENT_TOO_LARGE", "Request body is too large."), true);
133
+ }
134
+ else {
135
+ sendEmpty(response, 413, true);
136
+ }
137
+ return;
138
+ }
139
+ if (path === "/healthz") {
140
+ if (requestBody.length !== 0)
141
+ return sendEmpty(response, 400, true);
142
+ sendEmpty(response, request.method === "GET" ? 204 : 405);
143
+ return;
144
+ }
145
+ if (path === "/api/protocol") {
146
+ if (requestBody.length !== 0)
147
+ return sendEmpty(response, 400, true);
148
+ const authorized = await context.authorizeProtocol(request.headers.authorization, signal);
149
+ if (signal.aborted)
150
+ return;
151
+ if (authorized !== true) {
152
+ const code = authorized === "revoked"
153
+ ? "SESSION_REVOKED"
154
+ : authorized === "missing" || request.headers.authorization === undefined
155
+ ? "AUTH_MISSING"
156
+ : "AUTH_INVALID";
157
+ sendJson(response, 401, protocolError(code, "Authentication failed."));
158
+ return;
159
+ }
160
+ const credentialRetry = consumeAuthenticatedRateLimit(request.headers.authorization, credentialRateLimiter);
161
+ if (credentialRetry !== null)
162
+ return sendRateLimited(response, credentialRetry);
163
+ if (request.method !== "GET") {
164
+ sendEmpty(response, 405);
165
+ return;
166
+ }
167
+ sendJson(response, 200, {
168
+ protocol_version: "1",
169
+ request_id: "protocol-info",
170
+ payload: context.protocolInfo,
171
+ });
172
+ return;
173
+ }
174
+ if (path === "/api/enrollment/exchange") {
175
+ if (request.method !== "POST" || context.exchangeEnrollment === undefined) {
176
+ sendEmpty(response, 405);
177
+ return;
178
+ }
179
+ let decoded;
180
+ if (requestBody.length === 0) {
181
+ decoded = undefined;
182
+ }
183
+ else {
184
+ try {
185
+ decoded = JSON.parse(requestBody.toString("utf8"));
186
+ }
187
+ catch {
188
+ decoded = undefined;
189
+ }
190
+ }
191
+ const result = await context.exchangeEnrollment(decoded);
192
+ if (signal.aborted)
193
+ return;
194
+ if (result.body === undefined)
195
+ sendEmpty(response, result.status, result.status === 400);
196
+ else if (result.status === 400)
197
+ sendJson(response, 400, result.body, true);
198
+ else if (result.status === 401 || result.status === 507)
199
+ sendJson(response, result.status, result.body);
200
+ else
201
+ sendJson(response, 201, result.body);
202
+ return;
203
+ }
204
+ if (isCoordinationPath(path) && context.handleCoordination !== undefined) {
205
+ let decoded;
206
+ if (requestBody.length === 0) {
207
+ decoded = undefined;
208
+ }
209
+ else {
210
+ try {
211
+ decoded = JSON.parse(requestBody.toString("utf8"));
212
+ }
213
+ catch {
214
+ decoded = undefined;
215
+ }
216
+ }
217
+ const authorization = request.headers.authorization;
218
+ const authorizeCoordination = context.authorizeCoordination;
219
+ if (authorizeCoordination === undefined) {
220
+ sendJson(response, 500, protocolError("INTERNAL_ERROR", "Coordination authentication is unavailable."), true);
221
+ return;
222
+ }
223
+ const authentication = await authorizeCoordination(authorization, signal);
224
+ if (signal.aborted)
225
+ return;
226
+ if (typeof authentication === "string") {
227
+ const code = authentication === "revoked"
228
+ ? "SESSION_REVOKED"
229
+ : authentication === "missing" || authorization === undefined ? "AUTH_MISSING" : "AUTH_INVALID";
230
+ sendJson(response, 401, protocolError(code, "Authentication failed."));
231
+ return;
232
+ }
233
+ const clientIdentity = `client:${authentication.kind === "drone-session"
234
+ ? authentication.clientId
235
+ : authentication.id}`;
236
+ const credentialRetry = credentialRateLimiter.consume(clientIdentity);
237
+ if (credentialRetry !== null)
238
+ return sendRateLimited(response, credentialRetry);
239
+ const cursor = parseCursorParameter(request.url, path);
240
+ if (cursor === INVALID_COORDINATION_QUERY) {
241
+ sendJson(response, 400, protocolError("INVALID_INPUT", "Invalid query parameters."), true);
242
+ return;
243
+ }
244
+ const result = await context.handleCoordination({
245
+ method: request.method ?? "",
246
+ path,
247
+ principal: authentication,
248
+ ...(decoded === undefined ? {} : { body: decoded }),
249
+ ...(cursor === undefined ? {} : { cursor }),
250
+ signal,
251
+ });
252
+ if (signal.aborted) {
253
+ if (result.stream !== undefined)
254
+ await closeRejectedStream(result.stream);
255
+ return;
256
+ }
257
+ if (result.stream !== undefined) {
258
+ const release = streamQuota.acquire(credentialIdentity(authorization) ?? addressIdentity);
259
+ if (release === null) {
260
+ await closeRejectedStream(result.stream);
261
+ sendRateLimited(response, 1);
262
+ }
263
+ else
264
+ await startEventStream(response, result.stream, release);
265
+ }
266
+ else if (result.body === undefined) {
267
+ sendEmpty(response, result.status);
268
+ }
269
+ else {
270
+ sendJson(response, result.status, result.body, result.status === 400 || result.status === 413);
271
+ }
272
+ return;
273
+ }
274
+ sendEmpty(response, 404);
275
+ }
276
+ async function closeRejectedStream(stream) {
277
+ const iterator = stream[Symbol.asyncIterator]();
278
+ try {
279
+ await iterator.return?.();
280
+ }
281
+ catch {
282
+ // Quota rejection must not expose stream cleanup failures.
283
+ }
284
+ }
285
+ async function readRequestBody(request, maxBytes) {
286
+ const declaredLength = request.headers["content-length"];
287
+ if (declaredLength !== undefined) {
288
+ if (!/^\d+$/u.test(declaredLength)) {
289
+ request.resume();
290
+ return "oversized";
291
+ }
292
+ if (Number(declaredLength) > maxBytes) {
293
+ request.resume();
294
+ return "oversized";
295
+ }
296
+ }
297
+ let bytes = 0;
298
+ const chunks = [];
299
+ for await (const chunk of request) {
300
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
301
+ bytes += buffer.length;
302
+ if (bytes > maxBytes) {
303
+ request.resume();
304
+ return "oversized";
305
+ }
306
+ chunks.push(buffer);
307
+ }
308
+ return Buffer.concat(chunks, bytes);
309
+ }
310
+ function parseRequestPath(value) {
311
+ if (value === undefined || !value.startsWith("/") || value.startsWith("//"))
312
+ return null;
313
+ try {
314
+ return new URL(value, "https://local.invalid").pathname;
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ function sendEmpty(response, status, closeConnection = false) {
321
+ if (response.headersSent || response.destroyed)
322
+ return;
323
+ response.writeHead(status, {
324
+ "cache-control": "no-store",
325
+ ...(closeConnection ? { connection: "close" } : {}),
326
+ "content-length": "0",
327
+ });
328
+ response.end();
329
+ }
330
+ function sendRateLimited(response, retryAfter) {
331
+ if (response.headersSent || response.destroyed)
332
+ return;
333
+ response.writeHead(429, {
334
+ "cache-control": "no-store",
335
+ connection: "close",
336
+ "content-length": "0",
337
+ "retry-after": retryAfter.toString(),
338
+ });
339
+ response.end();
340
+ }
341
+ function sendJson(response, status, value, closeConnection = false) {
342
+ const body = JSON.stringify(value);
343
+ response.writeHead(status, {
344
+ "cache-control": "no-store",
345
+ ...(closeConnection ? { connection: "close" } : {}),
346
+ "content-length": Buffer.byteLength(body).toString(),
347
+ "content-type": "application/json; charset=utf-8",
348
+ "x-content-type-options": "nosniff",
349
+ });
350
+ response.end(body);
351
+ }
352
+ async function startEventStream(response, stream, releaseQuota) {
353
+ if (response.destroyed || response.writableEnded || response.headersSent) {
354
+ releaseQuota();
355
+ await closeRejectedStream(stream);
356
+ return;
357
+ }
358
+ try {
359
+ response.writeHead(200, {
360
+ "cache-control": "no-store",
361
+ connection: "keep-alive",
362
+ "content-type": "text/event-stream; charset=utf-8",
363
+ "x-accel-buffering": "no",
364
+ "x-content-type-options": "nosniff",
365
+ });
366
+ }
367
+ catch (error) {
368
+ releaseQuota();
369
+ await closeRejectedStream(stream);
370
+ throw error;
371
+ }
372
+ void (async () => {
373
+ try {
374
+ for await (const chunk of stream) {
375
+ if (response.destroyed || response.writableEnded)
376
+ break;
377
+ if (!response.write(chunk))
378
+ await waitForDrain(response);
379
+ }
380
+ }
381
+ catch {
382
+ // Stream failures terminate the connection without exposing internals.
383
+ }
384
+ finally {
385
+ releaseQuota();
386
+ if (!response.destroyed && !response.writableEnded)
387
+ response.end();
388
+ }
389
+ })();
390
+ }
391
+ function waitForDrain(response) {
392
+ return new Promise((resolve) => {
393
+ const finish = () => {
394
+ response.off("drain", finish);
395
+ response.off("close", finish);
396
+ response.off("error", finish);
397
+ resolve();
398
+ };
399
+ response.once("drain", finish);
400
+ response.once("close", finish);
401
+ response.once("error", finish);
402
+ });
403
+ }
404
+ function protocolError(code, message) {
405
+ return { protocol_version: "1", error: { code, message } };
406
+ }
407
+ const INVALID_COORDINATION_QUERY = Symbol("invalid-coordination-query");
408
+ function parseCursorParameter(value, path) {
409
+ if (value === undefined)
410
+ return undefined;
411
+ try {
412
+ const parsed = new URL(value, "https://local.invalid");
413
+ const keys = [...parsed.searchParams.keys()];
414
+ if (!path.endsWith("/stream")) {
415
+ return keys.length === 0 ? undefined : INVALID_COORDINATION_QUERY;
416
+ }
417
+ if (keys.some((key) => key !== "cursor"))
418
+ return INVALID_COORDINATION_QUERY;
419
+ const values = parsed.searchParams.getAll("cursor");
420
+ if (values.length === 0)
421
+ return undefined;
422
+ return values.length === 1 && values[0].length > 0
423
+ ? values[0]
424
+ : INVALID_COORDINATION_QUERY;
425
+ }
426
+ catch {
427
+ return INVALID_COORDINATION_QUERY;
428
+ }
429
+ }
430
+ function isCoordinationPath(path) {
431
+ return path === "/api/client/attach" || path === "/api/cubes" ||
432
+ path?.startsWith("/api/cubes/") === true;
433
+ }
434
+ function applyServerLimits(server, limits) {
435
+ server.maxConnections = limits.maxConnections;
436
+ server.maxRequestsPerSocket = limits.maxRequestsPerSocket;
437
+ server.requestTimeout = limits.requestTimeoutMs;
438
+ server.headersTimeout = limits.headersTimeoutMs;
439
+ server.keepAliveTimeout = limits.keepAliveTimeoutMs;
440
+ server.setTimeout(Math.min(limits.handlerTimeoutMs * 2, 2_147_483_647), (socket) => socket.destroy());
441
+ const addressConnections = new ConcurrentQuota(limits.maxConnectionsPerAddress);
442
+ const acceptedSockets = new Set();
443
+ server.on("connection", (socket) => {
444
+ const tracked = socket;
445
+ acceptedSockets.add(tracked);
446
+ tracked.once("close", () => acceptedSockets.delete(tracked));
447
+ const identity = tracked.remoteAddress ?? "unknown";
448
+ const release = addressConnections.acquire(identity);
449
+ if (release === null) {
450
+ socket.destroy();
451
+ return;
452
+ }
453
+ socket.once("close", release);
454
+ });
455
+ return acceptedSockets;
456
+ }
457
+ function validateLimits(limits) {
458
+ for (const [name, value] of Object.entries(limits)) {
459
+ if (!Number.isSafeInteger(value) || value <= 0) {
460
+ throw new Error(`${name} must be a positive safe integer.`);
461
+ }
462
+ }
463
+ if (limits.headersTimeoutMs > limits.requestTimeoutMs) {
464
+ throw new Error("headersTimeoutMs must not exceed requestTimeoutMs.");
465
+ }
466
+ if (limits.tlsHandshakeTimeoutMs > limits.requestTimeoutMs) {
467
+ throw new Error("tlsHandshakeTimeoutMs must not exceed requestTimeoutMs.");
468
+ }
469
+ if (limits.maxConnectionsPerAddress > limits.maxConnections) {
470
+ throw new Error("maxConnectionsPerAddress must not exceed maxConnections.");
471
+ }
472
+ if (limits.maxStreamsPerCredential > limits.maxConnectionsPerAddress) {
473
+ throw new Error("maxStreamsPerCredential must not exceed maxConnectionsPerAddress.");
474
+ }
475
+ if (limits.maxRequestsPerWindow > limits.maxRequestsPerAddressWindow) {
476
+ throw new Error("maxRequestsPerWindow must not exceed maxRequestsPerAddressWindow.");
477
+ }
478
+ if (limits.maxRequestsPerAddressWindow > limits.maxRequestsGlobalWindow) {
479
+ throw new Error("maxRequestsPerAddressWindow must not exceed maxRequestsGlobalWindow.");
480
+ }
481
+ }
482
+ export function validateTlsCertificate(certificate, host, mode, caCertificate) {
483
+ const [parsed, ...intermediates] = parseCertificateBundle(certificate);
484
+ if (parsed === undefined)
485
+ throw new Error("TLS certificate is missing.");
486
+ const now = Date.now();
487
+ if (now < parsed.validFromDate.getTime() || now > parsed.validToDate.getTime()) {
488
+ throw new Error("TLS certificate is outside its validity period.");
489
+ }
490
+ if (parsed.ca) {
491
+ throw new Error("TLS certificate must be a non-CA leaf certificate.");
492
+ }
493
+ if (parsed.checkIP(host) === undefined) {
494
+ throw new Error("TLS certificate does not cover the bind address.");
495
+ }
496
+ const serverAuthOid = "1.3.6.1.5.5.7.3.1";
497
+ if (parsed.keyUsage.length > 0 && !parsed.keyUsage.includes(serverAuthOid)) {
498
+ throw new Error("TLS certificate does not permit server authentication.");
499
+ }
500
+ if (caCertificate === undefined) {
501
+ if (mode === "lan")
502
+ throw new Error("A private LAN bind requires an explicit TLS trust anchor.");
503
+ return;
504
+ }
505
+ const [trustAnchor, ...additionalIntermediates] = parseCertificateBundle(caCertificate);
506
+ if (trustAnchor === undefined)
507
+ throw new Error("TLS trust anchor is missing.");
508
+ validateCertificateAuthority(trustAnchor, now, "TLS trust anchor");
509
+ if (!trustAnchor.verify(trustAnchor.publicKey)) {
510
+ throw new Error("TLS trust anchor must be self-signed.");
511
+ }
512
+ const chain = [...intermediates, ...additionalIntermediates];
513
+ for (const intermediate of chain)
514
+ validateCertificateAuthority(intermediate, now, "TLS intermediate");
515
+ const path = findCertificatePath(parsed, trustAnchor, chain, new Set(), 0);
516
+ if (path === null) {
517
+ throw new Error("TLS certificate is not signed by the configured trust anchor.");
518
+ }
519
+ validatePathLengthConstraints(path);
520
+ }
521
+ function parseCertificateBundle(value) {
522
+ const text = typeof value === "string" ? value : value.toString("utf8");
523
+ const pem = text.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/gu);
524
+ return pem === null ? [new X509Certificate(value)] : pem.map((certificate) => new X509Certificate(certificate));
525
+ }
526
+ function validateCertificateAuthority(certificate, now, label) {
527
+ if (!certificate.ca)
528
+ throw new Error(`${label} must be a CA certificate.`);
529
+ if (now < certificate.validFromDate.getTime() || now > certificate.validToDate.getTime()) {
530
+ throw new Error(`${label} is outside its validity period.`);
531
+ }
532
+ const constraints = readCaConstraints(certificate);
533
+ if (!constraints.basicConstraintsCritical || !constraints.ca || !constraints.keyCertSign) {
534
+ throw new Error(`${label} lacks required CA constraints or certificate-signing usage.`);
535
+ }
536
+ }
537
+ function findCertificatePath(certificate, trustAnchor, intermediates, visited, depth) {
538
+ if (depth > 8)
539
+ return null;
540
+ if (isIssuedBy(certificate, trustAnchor))
541
+ return [certificate, trustAnchor];
542
+ for (const intermediate of intermediates) {
543
+ if (visited.has(intermediate.fingerprint256) || !isIssuedBy(certificate, intermediate))
544
+ continue;
545
+ visited.add(intermediate.fingerprint256);
546
+ const path = findCertificatePath(intermediate, trustAnchor, intermediates, visited, depth + 1);
547
+ if (path !== null)
548
+ return [certificate, ...path];
549
+ visited.delete(intermediate.fingerprint256);
550
+ }
551
+ return null;
552
+ }
553
+ function isIssuedBy(certificate, issuer) {
554
+ return certificate.checkIssued(issuer) && certificate.verify(issuer.publicKey);
555
+ }
556
+ function validatePathLengthConstraints(path) {
557
+ for (let index = 1; index < path.length; index += 1) {
558
+ const authority = path[index];
559
+ const { pathLength } = readCaConstraints(authority);
560
+ if (pathLength === null)
561
+ continue;
562
+ const subordinateAuthorities = path.slice(1, index)
563
+ .filter((certificate) => certificate.subject !== certificate.issuer).length;
564
+ if (subordinateAuthorities > pathLength) {
565
+ throw new Error("TLS certificate chain exceeds a CA path-length constraint.");
566
+ }
567
+ }
568
+ }
569
+ function readCaConstraints(certificate) {
570
+ const extensions = readCertificateExtensions(certificate.raw);
571
+ const basic = extensions.get("551d13");
572
+ const keyUsage = extensions.get("551d0f");
573
+ if (basic === undefined || keyUsage === undefined) {
574
+ return { basicConstraintsCritical: false, ca: false, pathLength: null, keyCertSign: false };
575
+ }
576
+ const basicSequence = readDerElement(basic.value, 0, basic.value.length);
577
+ if (basicSequence.tag !== 0x30 || basicSequence.end !== basic.value.length)
578
+ throw new Error("Invalid CA constraints.");
579
+ const basicChildren = readDerChildren(basic.value, basicSequence);
580
+ const caElement = basicChildren.find((element) => element.tag === 0x01);
581
+ const pathElement = basicChildren.find((element) => element.tag === 0x02);
582
+ const usageBits = readDerElement(keyUsage.value, 0, keyUsage.value.length);
583
+ if (usageBits.tag !== 0x03 || usageBits.end !== keyUsage.value.length ||
584
+ usageBits.contentStart + 1 >= usageBits.end)
585
+ throw new Error("Invalid CA key usage.");
586
+ return {
587
+ basicConstraintsCritical: basic.critical,
588
+ ca: caElement !== undefined && basic.value[caElement.contentStart] !== 0,
589
+ pathLength: pathElement === undefined ? null : readDerInteger(basic.value, pathElement),
590
+ keyCertSign: (keyUsage.value[usageBits.contentStart + 1] & 0x04) !== 0,
591
+ };
592
+ }
593
+ function readCertificateExtensions(certificate) {
594
+ const outer = readDerElement(certificate, 0, certificate.length);
595
+ const outerChildren = readDerChildren(certificate, outer);
596
+ const tbs = outerChildren[0];
597
+ if (outer.tag !== 0x30 || tbs?.tag !== 0x30)
598
+ throw new Error("Invalid X.509 certificate encoding.");
599
+ const extensionWrapper = readDerChildren(certificate, tbs).find((element) => element.tag === 0xa3);
600
+ if (extensionWrapper === undefined)
601
+ return new Map();
602
+ const wrapperChildren = readDerChildren(certificate, extensionWrapper);
603
+ const extensionSequence = wrapperChildren[0];
604
+ if (extensionSequence?.tag !== 0x30)
605
+ throw new Error("Invalid X.509 extension encoding.");
606
+ const result = new Map();
607
+ for (const extension of readDerChildren(certificate, extensionSequence)) {
608
+ if (extension.tag !== 0x30)
609
+ throw new Error("Invalid X.509 extension encoding.");
610
+ const fields = readDerChildren(certificate, extension);
611
+ const oid = fields[0];
612
+ const hasCriticalField = fields[1]?.tag === 0x01;
613
+ const critical = hasCriticalField && certificate[fields[1].contentStart] !== 0;
614
+ const value = fields[hasCriticalField ? 2 : 1];
615
+ if (oid?.tag !== 0x06 || value?.tag !== 0x04)
616
+ throw new Error("Invalid X.509 extension encoding.");
617
+ result.set(certificate.subarray(oid.contentStart, oid.end).toString("hex"), { critical, value: certificate.subarray(value.contentStart, value.end) });
618
+ }
619
+ return result;
620
+ }
621
+ function readDerElement(data, offset, limit) {
622
+ if (offset + 2 > limit)
623
+ throw new Error("Invalid DER encoding.");
624
+ const tag = data[offset];
625
+ const firstLength = data[offset + 1];
626
+ let length = firstLength;
627
+ let contentStart = offset + 2;
628
+ if ((firstLength & 0x80) !== 0) {
629
+ const lengthBytes = firstLength & 0x7f;
630
+ if (lengthBytes === 0 || lengthBytes > 4 || contentStart + lengthBytes > limit) {
631
+ throw new Error("Invalid DER encoding.");
632
+ }
633
+ length = 0;
634
+ for (let index = 0; index < lengthBytes; index += 1) {
635
+ length = (length * 256) + data[contentStart + index];
636
+ }
637
+ contentStart += lengthBytes;
638
+ }
639
+ const end = contentStart + length;
640
+ if (end > limit || end < contentStart)
641
+ throw new Error("Invalid DER encoding.");
642
+ return { tag, contentStart, end };
643
+ }
644
+ function readDerChildren(data, parent) {
645
+ const children = [];
646
+ let offset = parent.contentStart;
647
+ while (offset < parent.end) {
648
+ const child = readDerElement(data, offset, parent.end);
649
+ children.push(child);
650
+ offset = child.end;
651
+ }
652
+ if (offset !== parent.end)
653
+ throw new Error("Invalid DER encoding.");
654
+ return children;
655
+ }
656
+ function readDerInteger(data, element) {
657
+ if (element.tag !== 0x02 || element.contentStart === element.end ||
658
+ (data[element.contentStart] & 0x80) !== 0)
659
+ throw new Error("Invalid DER integer.");
660
+ let value = 0;
661
+ for (let offset = element.contentStart; offset < element.end; offset += 1) {
662
+ value = (value * 256) + data[offset];
663
+ if (!Number.isSafeInteger(value))
664
+ throw new Error("DER integer exceeds policy.");
665
+ }
666
+ return value;
667
+ }
668
+ export class RequestRateLimiter {
669
+ #limits;
670
+ #clock;
671
+ #maxRequests;
672
+ #buckets = new Map();
673
+ constructor(limits, maxRequests = limits.maxRequestsPerWindow, clock = Date.now) {
674
+ this.#limits = limits;
675
+ this.#maxRequests = maxRequests;
676
+ this.#clock = clock;
677
+ }
678
+ consume(identity) {
679
+ const reservation = this.reserve(identity);
680
+ reservation.commit();
681
+ return reservation.retryAfter;
682
+ }
683
+ reserve(identity) {
684
+ const now = this.#clock();
685
+ const existing = this.#buckets.get(identity);
686
+ if (existing !== undefined && existing.resetAt > now) {
687
+ if (existing.count >= this.#maxRequests) {
688
+ return rejectedReservation(Math.max(1, Math.ceil((existing.resetAt - now) / 1_000)));
689
+ }
690
+ existing.count += 1;
691
+ return acceptedReservation(() => { existing.count -= 1; });
692
+ }
693
+ if (existing !== undefined)
694
+ this.#buckets.delete(identity);
695
+ for (const [key, bucket] of this.#buckets) {
696
+ if (bucket.resetAt <= now)
697
+ this.#buckets.delete(key);
698
+ }
699
+ if (this.#buckets.size >= this.#limits.maxRateLimitEntries) {
700
+ return rejectedReservation(Math.max(1, Math.ceil(this.#limits.rateLimitWindowMs / 1_000)));
701
+ }
702
+ const bucket = { count: 1, resetAt: now + this.#limits.rateLimitWindowMs };
703
+ this.#buckets.set(identity, bucket);
704
+ return acceptedReservation(() => {
705
+ if (this.#buckets.get(identity) === bucket)
706
+ this.#buckets.delete(identity);
707
+ });
708
+ }
709
+ }
710
+ export class PreAuthAdmissionLimiter {
711
+ #global;
712
+ #address;
713
+ constructor(limits, clock = Date.now) {
714
+ this.#global = new RequestRateLimiter(limits, limits.maxRequestsGlobalWindow, clock);
715
+ this.#address = new RequestRateLimiter(limits, limits.maxRequestsPerAddressWindow, clock);
716
+ }
717
+ consume(addressIdentity) {
718
+ const address = this.#address.reserve(addressIdentity);
719
+ if (address.retryAfter !== null)
720
+ return address.retryAfter;
721
+ const global = this.#global.reserve("global");
722
+ if (global.retryAfter !== null) {
723
+ address.rollback();
724
+ return global.retryAfter;
725
+ }
726
+ address.commit();
727
+ global.commit();
728
+ return null;
729
+ }
730
+ }
731
+ function acceptedReservation(undo) {
732
+ let active = true;
733
+ return {
734
+ retryAfter: null,
735
+ commit: () => { active = false; },
736
+ rollback: () => {
737
+ if (!active)
738
+ return;
739
+ active = false;
740
+ undo();
741
+ },
742
+ };
743
+ }
744
+ function rejectedReservation(retryAfter) {
745
+ return { retryAfter, commit: () => undefined, rollback: () => undefined };
746
+ }
747
+ export class ConcurrentQuota {
748
+ #limit;
749
+ #counts = new Map();
750
+ constructor(limit) {
751
+ this.#limit = limit;
752
+ }
753
+ acquire(identity) {
754
+ const count = this.#counts.get(identity) ?? 0;
755
+ if (count >= this.#limit)
756
+ return null;
757
+ this.#counts.set(identity, count + 1);
758
+ let released = false;
759
+ return () => {
760
+ if (released)
761
+ return;
762
+ released = true;
763
+ const remaining = (this.#counts.get(identity) ?? 1) - 1;
764
+ if (remaining <= 0)
765
+ this.#counts.delete(identity);
766
+ else
767
+ this.#counts.set(identity, remaining);
768
+ };
769
+ }
770
+ }
771
+ function credentialIdentity(authorization) {
772
+ if (authorization === undefined)
773
+ return null;
774
+ return `credential:${createHash("sha256").update(authorization).digest("base64url")}`;
775
+ }
776
+ function consumeAuthenticatedRateLimit(authorization, rateLimiter) {
777
+ const identity = credentialIdentity(authorization);
778
+ return identity === null ? null : rateLimiter.consume(identity);
779
+ }
780
+ function listen(server, port, host) {
781
+ return new Promise((resolve, reject) => {
782
+ const onError = (error) => {
783
+ server.off("listening", onListening);
784
+ reject(error);
785
+ };
786
+ const onListening = () => {
787
+ server.off("error", onError);
788
+ resolve();
789
+ };
790
+ server.once("error", onError);
791
+ server.once("listening", onListening);
792
+ server.listen(port, host);
793
+ });
794
+ }
795
+ async function close(server, acceptedSockets) {
796
+ const socketClosures = [...acceptedSockets].map((socket) => new Promise((resolve) => {
797
+ socket.once("close", () => resolve());
798
+ }));
799
+ const listenerClosure = new Promise((resolve, reject) => {
800
+ server.close((error) => {
801
+ if (error !== undefined)
802
+ reject(error);
803
+ else
804
+ resolve();
805
+ });
806
+ });
807
+ for (const socket of acceptedSockets)
808
+ socket.destroy();
809
+ server.closeAllConnections();
810
+ await Promise.all([listenerClosure, ...socketClosures]);
811
+ if (acceptedSockets.size !== 0)
812
+ throw new Error("HTTPS socket closure could not be confirmed.");
813
+ }
814
+ //# sourceMappingURL=https-server.js.map