@secure-exec/nodejs 0.2.0-rc.1 → 0.2.0-rc.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.
@@ -3,6 +3,81 @@
3
3
  // Cap in-sandbox request/response buffering to prevent host memory exhaustion
4
4
  const MAX_HTTP_BODY_BYTES = 50 * 1024 * 1024; // 50 MB
5
5
  import { exposeCustomGlobal } from "@secure-exec/core/internal/shared/global-exposure";
6
+ let _fetchHandleCounter = 0;
7
+ function encodeFetchBody(body, bodyEncoding) {
8
+ if (bodyEncoding === "base64" && typeof Buffer !== "undefined") {
9
+ return new Uint8Array(Buffer.from(body, "base64"));
10
+ }
11
+ if (typeof TextEncoder !== "undefined") {
12
+ return new TextEncoder().encode(body);
13
+ }
14
+ const bytes = new Uint8Array(body.length);
15
+ for (let index = 0; index < body.length; index += 1) {
16
+ bytes[index] = body.charCodeAt(index) & 0xff;
17
+ }
18
+ return bytes;
19
+ }
20
+ function createFetchBodyStream(body, bodyEncoding) {
21
+ const ReadableStreamCtor = globalThis.ReadableStream;
22
+ if (typeof ReadableStreamCtor !== "function") {
23
+ return null;
24
+ }
25
+ const bytes = encodeFetchBody(body, bodyEncoding);
26
+ const handleId = typeof _registerHandle === "function"
27
+ ? `fetch-body:${++_fetchHandleCounter}`
28
+ : null;
29
+ let released = false;
30
+ let delivered = false;
31
+ const release = () => {
32
+ if (released || !handleId) {
33
+ return;
34
+ }
35
+ released = true;
36
+ _unregisterHandle?.(handleId);
37
+ };
38
+ if (handleId) {
39
+ _registerHandle?.(handleId, "fetch response body");
40
+ }
41
+ return new ReadableStreamCtor({
42
+ pull(controller) {
43
+ if (delivered) {
44
+ release();
45
+ controller.close();
46
+ return;
47
+ }
48
+ delivered = true;
49
+ controller.enqueue(bytes);
50
+ controller.close();
51
+ release();
52
+ },
53
+ cancel() {
54
+ release();
55
+ },
56
+ });
57
+ }
58
+ function serializeFetchHeaders(headers) {
59
+ if (!headers) {
60
+ return {};
61
+ }
62
+ if (headers instanceof Headers) {
63
+ return Object.fromEntries(headers.entries());
64
+ }
65
+ if (isFlatHeaderList(headers)) {
66
+ const normalized = {};
67
+ for (let index = 0; index < headers.length; index += 2) {
68
+ const key = headers[index];
69
+ const value = headers[index + 1];
70
+ if (key !== undefined && value !== undefined) {
71
+ normalized[key] = value;
72
+ }
73
+ }
74
+ return normalized;
75
+ }
76
+ return Object.fromEntries(new Headers(headers).entries());
77
+ }
78
+ function createFetchHeaders(headers) {
79
+ return new Headers(serializeFetchHeaders(headers));
80
+ }
6
81
  // Fetch polyfill
7
82
  export async function fetch(input, options = {}) {
8
83
  if (typeof _networkFetchRaw === 'undefined') {
@@ -15,7 +90,7 @@ export async function fetch(input, options = {}) {
15
90
  resolvedUrl = input.url;
16
91
  options = {
17
92
  method: input.method,
18
- headers: Object.fromEntries(input.headers.entries()),
93
+ headers: serializeFetchHeaders(input.headers),
19
94
  body: input.body,
20
95
  ...options,
21
96
  };
@@ -25,39 +100,66 @@ export async function fetch(input, options = {}) {
25
100
  }
26
101
  const optionsJson = JSON.stringify({
27
102
  method: options.method || "GET",
28
- headers: options.headers || {},
103
+ headers: serializeFetchHeaders(options.headers),
29
104
  body: options.body || null,
30
105
  });
31
- const responseJson = await _networkFetchRaw.apply(undefined, [resolvedUrl, optionsJson], {
32
- result: { promise: true },
33
- });
34
- const response = JSON.parse(responseJson);
35
- // Create Response-like object
36
- return {
37
- ok: response.ok,
38
- status: response.status,
39
- statusText: response.statusText,
40
- headers: new Map(Object.entries(response.headers || {})),
41
- url: response.url || resolvedUrl,
42
- redirected: response.redirected || false,
43
- type: "basic",
44
- async text() {
45
- return response.body || "";
46
- },
47
- async json() {
48
- return JSON.parse(response.body || "{}");
49
- },
50
- async arrayBuffer() {
51
- // Not fully supported - return empty buffer
52
- return new ArrayBuffer(0);
53
- },
54
- async blob() {
55
- throw new Error("Blob not supported in sandbox");
56
- },
57
- clone() {
58
- return { ...this };
59
- },
60
- };
106
+ const handleId = typeof _registerHandle === "function"
107
+ ? `fetch:${++_fetchHandleCounter}`
108
+ : null;
109
+ if (handleId) {
110
+ _registerHandle?.(handleId, `fetch ${resolvedUrl}`);
111
+ }
112
+ try {
113
+ const responseJson = await _networkFetchRaw.apply(undefined, [resolvedUrl, optionsJson], {
114
+ result: { promise: true },
115
+ });
116
+ const response = JSON.parse(responseJson);
117
+ const bodyEncoding = response.headers?.["x-body-encoding"] ?? null;
118
+ const responseBody = response.body ?? "";
119
+ let bodyStream;
120
+ // Create Response-like object
121
+ return {
122
+ ok: response.ok,
123
+ status: response.status,
124
+ statusText: response.statusText,
125
+ headers: new Map(Object.entries(response.headers || {})),
126
+ url: response.url || resolvedUrl,
127
+ redirected: response.redirected || false,
128
+ type: "basic",
129
+ get body() {
130
+ if (bodyStream === undefined) {
131
+ bodyStream = createFetchBodyStream(responseBody, bodyEncoding);
132
+ }
133
+ return bodyStream;
134
+ },
135
+ async text() {
136
+ if (bodyEncoding === "base64" && typeof Buffer !== "undefined") {
137
+ return Buffer.from(responseBody, "base64").toString("utf8");
138
+ }
139
+ return responseBody;
140
+ },
141
+ async json() {
142
+ const textBody = bodyEncoding === "base64" && typeof Buffer !== "undefined"
143
+ ? Buffer.from(responseBody, "base64").toString("utf8")
144
+ : responseBody;
145
+ return JSON.parse(textBody || "{}");
146
+ },
147
+ async arrayBuffer() {
148
+ return Uint8Array.from(encodeFetchBody(responseBody, bodyEncoding)).buffer;
149
+ },
150
+ async blob() {
151
+ throw new Error("Blob not supported in sandbox");
152
+ },
153
+ clone() {
154
+ return { ...this };
155
+ },
156
+ };
157
+ }
158
+ finally {
159
+ if (handleId) {
160
+ _unregisterHandle?.(handleId);
161
+ }
162
+ }
61
163
  }
62
164
  // Headers class
63
165
  export class Headers {
@@ -131,7 +233,7 @@ export class Request {
131
233
  constructor(input, init = {}) {
132
234
  this.url = typeof input === "string" ? input : input.url;
133
235
  this.method = init.method || (typeof input !== "string" ? input.method : undefined) || "GET";
134
- this.headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined));
236
+ this.headers = createFetchHeaders(init.headers || (typeof input !== "string" ? input.headers : undefined));
135
237
  this.body = init.body || null;
136
238
  this.mode = init.mode || "cors";
137
239
  this.credentials = init.credentials || "same-origin";
@@ -777,80 +879,14 @@ export class ClientRequest {
777
879
  }
778
880
  const normalizedHeaders = normalizeRequestHeaders(this._options.headers);
779
881
  const requestMethod = String(this._options.method || "GET").toUpperCase();
780
- const loopbackServerByPort = findLoopbackServerByPort(this._options);
781
- const directLoopbackConnectServer = requestMethod === "CONNECT"
782
- ? loopbackServerByPort
783
- : null;
784
- const directLoopbackUpgradeServer = requestMethod !== "CONNECT" &&
785
- hasUpgradeRequestHeaders(normalizedHeaders) &&
786
- loopbackServerByPort?.listenerCount("upgrade")
787
- ? loopbackServerByPort
788
- : null;
789
- if (directLoopbackConnectServer) {
790
- const response = await dispatchLoopbackConnectRequest(directLoopbackConnectServer, this._options);
791
- this.finished = true;
792
- this.socket = response.socket;
793
- response.response.socket = response.socket;
794
- response.socket.once("close", () => {
795
- this._emit("close");
796
- });
797
- this._emit("connect", response.response, response.socket, response.head);
798
- process.nextTick(() => {
799
- this._finalizeSocket(socket, false);
800
- });
801
- return;
802
- }
803
- if (directLoopbackUpgradeServer) {
804
- const response = await dispatchLoopbackUpgradeRequest(directLoopbackUpgradeServer, this._options, this._body);
805
- this.finished = true;
806
- this.socket = response.socket;
807
- response.response.socket = response.socket;
808
- response.socket.once("close", () => {
809
- this._emit("close");
810
- });
811
- this._emit("upgrade", response.response, response.socket, response.head);
812
- process.nextTick(() => {
813
- this._finalizeSocket(socket, false);
814
- });
815
- return;
816
- }
817
- const directLoopbackServer = requestMethod !== "CONNECT" &&
818
- hasUpgradeRequestHeaders(normalizedHeaders) &&
819
- !directLoopbackUpgradeServer
820
- ? loopbackServerByPort
821
- : findLoopbackServerForRequest(this._options);
822
- const directLoopbackHttp2CompatServer = !directLoopbackServer &&
823
- requestMethod !== "CONNECT" &&
824
- !hasUpgradeRequestHeaders(normalizedHeaders)
825
- ? findLoopbackHttp2CompatibilityServer(this._options)
826
- : null;
827
- const serializedRequest = JSON.stringify({
828
- method: requestMethod,
829
- url: this._options.path || "/",
830
- headers: normalizedHeaders,
831
- rawHeaders: flattenRawHeaders(normalizedHeaders),
832
- bodyBase64: this._body
833
- ? Buffer.from(this._body).toString("base64")
834
- : undefined,
882
+ const responseJson = await _networkHttpRequestRaw.apply(undefined, [url, JSON.stringify({
883
+ method: this._options.method || "GET",
884
+ headers: normalizedHeaders,
885
+ body: this._body || null,
886
+ ...tls,
887
+ })], {
888
+ result: { promise: true },
835
889
  });
836
- const loopbackResponse = directLoopbackServer
837
- ? await dispatchLoopbackServerRequest(directLoopbackServer._bridgeServerId, serializedRequest)
838
- : directLoopbackHttp2CompatServer
839
- ? await dispatchLoopbackHttp2CompatibilityRequest(directLoopbackHttp2CompatServer, serializedRequest)
840
- : null;
841
- if (loopbackResponse) {
842
- this._loopbackAbort = loopbackResponse.abortRequest;
843
- }
844
- const responseJson = loopbackResponse
845
- ? loopbackResponse.responseJson
846
- : await _networkHttpRequestRaw.apply(undefined, [url, JSON.stringify({
847
- method: this._options.method || "GET",
848
- headers: normalizedHeaders,
849
- body: this._body || null,
850
- ...tls,
851
- })], {
852
- result: { promise: true },
853
- });
854
890
  const response = JSON.parse(responseJson);
855
891
  this.finished = true;
856
892
  this._clearTimeout();
@@ -906,14 +942,6 @@ export class ClientRequest {
906
942
  });
907
943
  return;
908
944
  }
909
- if (response.connectionReset) {
910
- const error = createConnResetError();
911
- this._emit("error", error);
912
- process.nextTick(() => {
913
- this._finalizeSocket(socket, false);
914
- });
915
- return;
916
- }
917
945
  for (const informational of response.informational || []) {
918
946
  this._emit("information", new IncomingMessage({
919
947
  headers: Object.fromEntries(informational.headers || []),
@@ -928,9 +956,6 @@ export class ClientRequest {
928
956
  res.once("end", () => {
929
957
  process.nextTick(() => {
930
958
  this._finalizeSocket(socket, this._agent?.keepAlive === true && !this.aborted);
931
- if (response.connectionEnded) {
932
- queueMicrotask(() => socket.end?.());
933
- }
934
959
  });
935
960
  });
936
961
  if (this._callback) {
@@ -4361,8 +4386,13 @@ class Http2SocketProxy extends Http2EventEmitter {
4361
4386
  remoteFamily = "IPv4";
4362
4387
  servername;
4363
4388
  alpnProtocol = false;
4389
+ readable = true;
4390
+ writable = true;
4364
4391
  destroyed = false;
4392
+ _bridgeReadPollTimer = null;
4393
+ _loopbackServer = null;
4365
4394
  _onDestroy;
4395
+ _destroyCallbackInvoked = false;
4366
4396
  constructor(state, onDestroy) {
4367
4397
  super();
4368
4398
  this._onDestroy = onDestroy;
@@ -4382,8 +4412,28 @@ class Http2SocketProxy extends Http2EventEmitter {
4382
4412
  this.servername = state.servername;
4383
4413
  this.alpnProtocol = state.alpnProtocol ?? this.alpnProtocol;
4384
4414
  }
4415
+ _clearTimeoutTimer() {
4416
+ // Borrowed net.Socket destroy paths call into this hook.
4417
+ }
4418
+ _emitNet(event, error) {
4419
+ if (event === "error" && error) {
4420
+ this.emit("error", error);
4421
+ return;
4422
+ }
4423
+ if (event === "close") {
4424
+ if (!this._destroyCallbackInvoked) {
4425
+ this._destroyCallbackInvoked = true;
4426
+ queueMicrotask(() => {
4427
+ this._onDestroy?.();
4428
+ });
4429
+ }
4430
+ this.emit("close");
4431
+ }
4432
+ }
4385
4433
  end() {
4386
4434
  this.destroyed = true;
4435
+ this.readable = false;
4436
+ this.writable = false;
4387
4437
  this.emit("close");
4388
4438
  return this;
4389
4439
  }
@@ -4392,8 +4442,9 @@ class Http2SocketProxy extends Http2EventEmitter {
4392
4442
  return this;
4393
4443
  }
4394
4444
  this.destroyed = true;
4395
- this._onDestroy?.();
4396
- this.emit("close");
4445
+ this.readable = false;
4446
+ this.writable = false;
4447
+ this._emitNet("close");
4397
4448
  return this;
4398
4449
  }
4399
4450
  }
@@ -4413,6 +4464,163 @@ function createHttp2SettingTypeError(setting, value) {
4413
4464
  error.code = "ERR_HTTP2_INVALID_SETTING_VALUE";
4414
4465
  return error;
4415
4466
  }
4467
+ const HTTP2_INTERNAL_BINDING_CONSTANTS = {
4468
+ NGHTTP2_NO_ERROR: 0,
4469
+ NGHTTP2_PROTOCOL_ERROR: 1,
4470
+ NGHTTP2_INTERNAL_ERROR: 2,
4471
+ NGHTTP2_FLOW_CONTROL_ERROR: 3,
4472
+ NGHTTP2_SETTINGS_TIMEOUT: 4,
4473
+ NGHTTP2_STREAM_CLOSED: 5,
4474
+ NGHTTP2_FRAME_SIZE_ERROR: 6,
4475
+ NGHTTP2_REFUSED_STREAM: 7,
4476
+ NGHTTP2_CANCEL: 8,
4477
+ NGHTTP2_COMPRESSION_ERROR: 9,
4478
+ NGHTTP2_CONNECT_ERROR: 10,
4479
+ NGHTTP2_ENHANCE_YOUR_CALM: 11,
4480
+ NGHTTP2_INADEQUATE_SECURITY: 12,
4481
+ NGHTTP2_HTTP_1_1_REQUIRED: 13,
4482
+ NGHTTP2_NV_FLAG_NONE: 0,
4483
+ NGHTTP2_NV_FLAG_NO_INDEX: 1,
4484
+ NGHTTP2_ERR_DEFERRED: -508,
4485
+ NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: -509,
4486
+ NGHTTP2_ERR_STREAM_CLOSED: -510,
4487
+ NGHTTP2_ERR_INVALID_ARGUMENT: -501,
4488
+ NGHTTP2_ERR_FRAME_SIZE_ERROR: -522,
4489
+ NGHTTP2_ERR_NOMEM: -901,
4490
+ NGHTTP2_FLAG_NONE: 0,
4491
+ NGHTTP2_FLAG_END_STREAM: 1,
4492
+ NGHTTP2_FLAG_END_HEADERS: 4,
4493
+ NGHTTP2_FLAG_ACK: 1,
4494
+ NGHTTP2_FLAG_PADDED: 8,
4495
+ NGHTTP2_FLAG_PRIORITY: 32,
4496
+ NGHTTP2_DEFAULT_WEIGHT: 16,
4497
+ NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1,
4498
+ NGHTTP2_SETTINGS_ENABLE_PUSH: 2,
4499
+ NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3,
4500
+ NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4,
4501
+ NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5,
4502
+ NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6,
4503
+ NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8,
4504
+ };
4505
+ const HTTP2_NGHTTP2_ERROR_MESSAGES = {
4506
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_DEFERRED]: "Data deferred",
4507
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE]: "Stream ID is not available",
4508
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_STREAM_CLOSED]: "Stream was already closed or invalid",
4509
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_INVALID_ARGUMENT]: "Invalid argument",
4510
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_FRAME_SIZE_ERROR]: "Frame size error",
4511
+ [HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_ERR_NOMEM]: "Out of memory",
4512
+ };
4513
+ class NghttpError extends Error {
4514
+ code = "ERR_HTTP2_ERROR";
4515
+ constructor(message) {
4516
+ super(message);
4517
+ this.name = "Error";
4518
+ }
4519
+ }
4520
+ function nghttp2ErrorString(code) {
4521
+ return HTTP2_NGHTTP2_ERROR_MESSAGES[code] ?? `HTTP/2 error (${String(code)})`;
4522
+ }
4523
+ function createHttp2InvalidArgValueError(property, value) {
4524
+ return createTypeErrorWithCode(`The property 'options.${property}' is invalid. Received ${formatHttp2InvalidValue(value)}`, "ERR_INVALID_ARG_VALUE");
4525
+ }
4526
+ function formatHttp2InvalidValue(value) {
4527
+ if (typeof value === "function") {
4528
+ return `[Function${value.name ? `: ${value.name}` : ": function"}]`;
4529
+ }
4530
+ if (typeof value === "symbol") {
4531
+ return value.toString();
4532
+ }
4533
+ if (Array.isArray(value)) {
4534
+ return "[]";
4535
+ }
4536
+ if (value === null) {
4537
+ return "null";
4538
+ }
4539
+ if (typeof value === "object") {
4540
+ return "{}";
4541
+ }
4542
+ return String(value);
4543
+ }
4544
+ function createHttp2PayloadForbiddenError(statusCode) {
4545
+ return createHttp2Error("ERR_HTTP2_PAYLOAD_FORBIDDEN", `Responses with ${String(statusCode)} status must not have a payload`);
4546
+ }
4547
+ const S_IFMT = 0o170000;
4548
+ const S_IFDIR = 0o040000;
4549
+ const S_IFREG = 0o100000;
4550
+ const S_IFIFO = 0o010000;
4551
+ const S_IFSOCK = 0o140000;
4552
+ const S_IFLNK = 0o120000;
4553
+ function createHttp2BridgeStat(stat) {
4554
+ const atimeMs = stat.atimeMs ?? 0;
4555
+ const mtimeMs = stat.mtimeMs ?? atimeMs;
4556
+ const ctimeMs = stat.ctimeMs ?? mtimeMs;
4557
+ const birthtimeMs = stat.birthtimeMs ?? ctimeMs;
4558
+ const fileType = stat.mode & S_IFMT;
4559
+ return {
4560
+ size: stat.size,
4561
+ mode: stat.mode,
4562
+ atimeMs,
4563
+ mtimeMs,
4564
+ ctimeMs,
4565
+ birthtimeMs,
4566
+ atime: new Date(atimeMs),
4567
+ mtime: new Date(mtimeMs),
4568
+ ctime: new Date(ctimeMs),
4569
+ birthtime: new Date(birthtimeMs),
4570
+ isFile: () => fileType === S_IFREG,
4571
+ isDirectory: () => fileType === S_IFDIR,
4572
+ isFIFO: () => fileType === S_IFIFO,
4573
+ isSocket: () => fileType === S_IFSOCK,
4574
+ isSymbolicLink: () => fileType === S_IFLNK,
4575
+ };
4576
+ }
4577
+ function normalizeHttp2FileResponseOptions(options) {
4578
+ const normalized = options ?? {};
4579
+ const offset = normalized.offset;
4580
+ if (offset !== undefined && (typeof offset !== "number" || !Number.isFinite(offset))) {
4581
+ throw createHttp2InvalidArgValueError("offset", offset);
4582
+ }
4583
+ const length = normalized.length;
4584
+ if (length !== undefined && (typeof length !== "number" || !Number.isFinite(length))) {
4585
+ throw createHttp2InvalidArgValueError("length", length);
4586
+ }
4587
+ const statCheck = normalized.statCheck;
4588
+ if (statCheck !== undefined && typeof statCheck !== "function") {
4589
+ throw createHttp2InvalidArgValueError("statCheck", statCheck);
4590
+ }
4591
+ const onError = normalized.onError;
4592
+ return {
4593
+ offset: offset === undefined ? 0 : Math.max(0, Math.trunc(offset)),
4594
+ length: typeof length === "number"
4595
+ ? Math.trunc(length)
4596
+ : undefined,
4597
+ statCheck: typeof statCheck === "function" ? statCheck : undefined,
4598
+ onError: typeof onError === "function" ? onError : undefined,
4599
+ };
4600
+ }
4601
+ function sliceHttp2FileBody(body, offset, length) {
4602
+ const safeOffset = Math.max(0, Math.min(offset, body.length));
4603
+ if (length === undefined || length < 0) {
4604
+ return body.subarray(safeOffset);
4605
+ }
4606
+ return body.subarray(safeOffset, Math.min(body.length, safeOffset + length));
4607
+ }
4608
+ class Http2Stream {
4609
+ _streamId;
4610
+ constructor(_streamId) {
4611
+ this._streamId = _streamId;
4612
+ }
4613
+ respond(headers) {
4614
+ if (typeof _networkHttp2StreamRespondRaw === "undefined") {
4615
+ throw new Error("http2 server stream respond bridge is not available");
4616
+ }
4617
+ _networkHttp2StreamRespondRaw.applySync(undefined, [
4618
+ this._streamId,
4619
+ serializeHttp2Headers(headers),
4620
+ ]);
4621
+ return 0;
4622
+ }
4623
+ }
4416
4624
  const DEFAULT_HTTP2_SETTINGS = {
4417
4625
  headerTableSize: 4096,
4418
4626
  enablePush: true,
@@ -4911,7 +5119,10 @@ function getCompleteUtf8PrefixLength(buffer) {
4911
5119
  }
4912
5120
  class ServerHttp2Stream extends Http2EventEmitter {
4913
5121
  _streamId;
5122
+ _binding;
4914
5123
  _responded = false;
5124
+ _endQueued = false;
5125
+ _pendingSyntheticErrorSuppressions = 0;
4915
5126
  _requestHeaders;
4916
5127
  _isPushStream;
4917
5128
  session;
@@ -4924,6 +5135,7 @@ class ServerHttp2Stream extends Http2EventEmitter {
4924
5135
  constructor(streamId, session, requestHeaders, isPushStream = false) {
4925
5136
  super();
4926
5137
  this._streamId = streamId;
5138
+ this._binding = new Http2Stream(streamId);
4927
5139
  this.session = session;
4928
5140
  this._requestHeaders = requestHeaders;
4929
5141
  this._isPushStream = isPushStream;
@@ -4936,12 +5148,53 @@ class ServerHttp2Stream extends Http2EventEmitter {
4936
5148
  ended: requestHeaders?.[":method"] === "HEAD",
4937
5149
  };
4938
5150
  }
4939
- respond(headers) {
4940
- if (typeof _networkHttp2StreamRespondRaw === "undefined") {
4941
- throw new Error("http2 server stream respond bridge is not available");
5151
+ _closeWithCode(code) {
5152
+ this.rstCode = code;
5153
+ _networkHttp2StreamCloseRaw?.applySync(undefined, [this._streamId, code]);
5154
+ }
5155
+ _markSyntheticClose() {
5156
+ this.destroyed = true;
5157
+ this.readable = false;
5158
+ this.writable = false;
5159
+ }
5160
+ _shouldSuppressHostError() {
5161
+ if (this._pendingSyntheticErrorSuppressions <= 0) {
5162
+ return false;
4942
5163
  }
5164
+ this._pendingSyntheticErrorSuppressions -= 1;
5165
+ return true;
5166
+ }
5167
+ _emitNghttp2Error(errorCode) {
5168
+ const error = new NghttpError(nghttp2ErrorString(errorCode));
5169
+ this._pendingSyntheticErrorSuppressions += 1;
5170
+ this._markSyntheticClose();
5171
+ this.emit("error", error);
5172
+ this._closeWithCode(HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_INTERNAL_ERROR);
5173
+ }
5174
+ _emitInternalStreamError() {
5175
+ const error = createHttp2Error("ERR_HTTP2_STREAM_ERROR", "Stream closed with error code NGHTTP2_INTERNAL_ERROR");
5176
+ this._pendingSyntheticErrorSuppressions += 1;
5177
+ this._markSyntheticClose();
5178
+ this.emit("error", error);
5179
+ this._closeWithCode(HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_INTERNAL_ERROR);
5180
+ }
5181
+ _submitResponse(headers) {
4943
5182
  this._responded = true;
4944
- _networkHttp2StreamRespondRaw.applySync(undefined, [this._streamId, serializeHttp2Headers(headers)]);
5183
+ const ngError = this._binding.respond(headers);
5184
+ if (typeof ngError === "number" && ngError !== 0) {
5185
+ this._emitNghttp2Error(ngError);
5186
+ return false;
5187
+ }
5188
+ return true;
5189
+ }
5190
+ respond(headers) {
5191
+ if (this.destroyed) {
5192
+ throw createHttp2Error("ERR_HTTP2_INVALID_STREAM", "The stream has been destroyed");
5193
+ }
5194
+ if (this._responded) {
5195
+ throw createHttp2Error("ERR_HTTP2_HEADERS_SENT", "Response has already been initiated.");
5196
+ }
5197
+ this._submitResponse(headers);
4945
5198
  }
4946
5199
  pushStream(headers, optionsOrCallback, maybeCallback) {
4947
5200
  if (this._isPushStream) {
@@ -4959,21 +5212,19 @@ class ServerHttp2Stream extends Http2EventEmitter {
4959
5212
  const options = optionsOrCallback && typeof optionsOrCallback === "object" && !Array.isArray(optionsOrCallback)
4960
5213
  ? optionsOrCallback
4961
5214
  : {};
4962
- const resultJson = _networkHttp2StreamPushStreamRaw.applySyncPromise(undefined, [
5215
+ const resultJson = _networkHttp2StreamPushStreamRaw.applySync(undefined, [
4963
5216
  this._streamId,
4964
5217
  serializeHttp2Headers(normalizeHttp2Headers(headers)),
4965
5218
  JSON.stringify(options ?? {}),
4966
5219
  ]);
4967
5220
  const result = JSON.parse(resultJson);
4968
- queueMicrotask(() => {
4969
- if (result.error) {
4970
- callback(parseHttp2ErrorPayload(result.error));
4971
- return;
4972
- }
4973
- const pushStream = new ServerHttp2Stream(Number(result.streamId), this.session, parseHttp2Headers(result.headers), true);
4974
- http2Streams.set(Number(result.streamId), pushStream);
4975
- callback(null, pushStream, parseHttp2Headers(result.headers));
4976
- });
5221
+ if (result.error) {
5222
+ callback(parseHttp2ErrorPayload(result.error));
5223
+ return;
5224
+ }
5225
+ const pushStream = new ServerHttp2Stream(Number(result.streamId), this.session, parseHttp2Headers(result.headers), true);
5226
+ http2Streams.set(Number(result.streamId), pushStream);
5227
+ callback(null, pushStream, parseHttp2Headers(result.headers));
4977
5228
  }
4978
5229
  write(data) {
4979
5230
  if (this._writableState.ended) {
@@ -4994,7 +5245,12 @@ class ServerHttp2Stream extends Http2EventEmitter {
4994
5245
  }
4995
5246
  end(data) {
4996
5247
  if (!this._responded) {
4997
- this.respond({ ":status": 200 });
5248
+ if (!this._submitResponse({ ":status": 200 })) {
5249
+ return;
5250
+ }
5251
+ }
5252
+ if (this._endQueued) {
5253
+ return;
4998
5254
  }
4999
5255
  if (typeof _networkHttp2StreamEndRaw === "undefined") {
5000
5256
  throw new Error("http2 server stream end bridge is not available");
@@ -5009,8 +5265,14 @@ class ServerHttp2Stream extends Http2EventEmitter {
5009
5265
  : Buffer.from(data);
5010
5266
  encoded = buffer.toString("base64");
5011
5267
  }
5012
- _networkHttp2StreamEndRaw.applySync(undefined, [this._streamId, encoded]);
5013
- this._writableState.ended = true;
5268
+ this._endQueued = true;
5269
+ queueMicrotask(() => {
5270
+ if (!this._endQueued || this.destroyed) {
5271
+ return;
5272
+ }
5273
+ this._endQueued = false;
5274
+ _networkHttp2StreamEndRaw.applySync(undefined, [this._streamId, encoded]);
5275
+ });
5014
5276
  }
5015
5277
  pause() {
5016
5278
  this._readableState.flowing = false;
@@ -5023,18 +5285,39 @@ class ServerHttp2Stream extends Http2EventEmitter {
5023
5285
  return this;
5024
5286
  }
5025
5287
  respondWithFile(path, headers, options) {
5288
+ if (this.destroyed) {
5289
+ throw createHttp2Error("ERR_HTTP2_INVALID_STREAM", "The stream has been destroyed");
5290
+ }
5291
+ if (this._responded) {
5292
+ throw createHttp2Error("ERR_HTTP2_HEADERS_SENT", "Response has already been initiated.");
5293
+ }
5294
+ const normalizedOptions = normalizeHttp2FileResponseOptions(options);
5295
+ const responseHeaders = { ...(headers ?? {}) };
5296
+ const statusCode = responseHeaders[":status"];
5297
+ if (statusCode === 204 || statusCode === 205 || statusCode === 304) {
5298
+ throw createHttp2PayloadForbiddenError(Number(statusCode));
5299
+ }
5026
5300
  try {
5301
+ const statJson = _fs.stat.applySyncPromise(undefined, [path]);
5027
5302
  const bodyBase64 = _fs.readFileBinary.applySyncPromise(undefined, [path]);
5303
+ const stat = createHttp2BridgeStat(JSON.parse(statJson));
5304
+ const callbackOptions = {
5305
+ offset: normalizedOptions.offset,
5306
+ length: normalizedOptions.length ?? Math.max(0, stat.size - normalizedOptions.offset),
5307
+ };
5308
+ normalizedOptions.statCheck?.(stat, responseHeaders, callbackOptions);
5028
5309
  const body = Buffer.from(bodyBase64, "base64");
5029
- this._responded = true;
5030
- this.respond({
5310
+ const slicedBody = sliceHttp2FileBody(body, normalizedOptions.offset, normalizedOptions.length);
5311
+ if (responseHeaders["content-length"] === undefined) {
5312
+ responseHeaders["content-length"] = slicedBody.byteLength;
5313
+ }
5314
+ if (!this._submitResponse({
5031
5315
  ":status": 200,
5032
- ...(headers ?? {}),
5033
- });
5034
- this.end(body);
5035
- queueMicrotask(() => {
5036
- this.session.close();
5037
- });
5316
+ ...responseHeaders,
5317
+ })) {
5318
+ return;
5319
+ }
5320
+ this.end(slicedBody);
5038
5321
  return;
5039
5322
  }
5040
5323
  catch {
@@ -5059,10 +5342,22 @@ class ServerHttp2Stream extends Http2EventEmitter {
5059
5342
  : NaN;
5060
5343
  const path = Number.isFinite(fd) ? _fdGetPath.applySync(undefined, [fd]) : null;
5061
5344
  if (!path) {
5062
- throw new Error("Invalid file descriptor for respondWithFD");
5345
+ this._emitInternalStreamError();
5346
+ return;
5063
5347
  }
5064
5348
  this.respondWithFile(path, headers, options);
5065
5349
  }
5350
+ destroy(error) {
5351
+ if (this.destroyed) {
5352
+ return this;
5353
+ }
5354
+ this.destroyed = true;
5355
+ if (error) {
5356
+ this.emit("error", error);
5357
+ }
5358
+ this._closeWithCode(HTTP2_INTERNAL_BINDING_CONSTANTS.NGHTTP2_CANCEL);
5359
+ return this;
5360
+ }
5066
5361
  _emitData(dataBase64) {
5067
5362
  if (!dataBase64) {
5068
5363
  return;
@@ -5266,7 +5561,11 @@ class Http2Session extends Http2EventEmitter {
5266
5561
  constructor(sessionId, socketState) {
5267
5562
  super();
5268
5563
  this._sessionId = sessionId;
5269
- this.socket = new Http2SocketProxy(socketState, () => this.destroy());
5564
+ this.socket = new Http2SocketProxy(socketState, () => {
5565
+ setTimeout(() => {
5566
+ this.destroy();
5567
+ }, 0);
5568
+ });
5270
5569
  this[HTTP2_K_SOCKET] = this.socket;
5271
5570
  }
5272
5571
  _retain() {
@@ -5832,6 +6131,10 @@ function http2Dispatch(kind, id, data, extra, extraNumber, extraHeaders, flags)
5832
6131
  const stream = http2Streams.get(id);
5833
6132
  if (!stream)
5834
6133
  return;
6134
+ if (typeof stream._shouldSuppressHostError === "function" &&
6135
+ stream._shouldSuppressHostError()) {
6136
+ return;
6137
+ }
5835
6138
  stream.emit("error", parseHttp2ErrorPayload(data));
5836
6139
  return;
5837
6140
  }
@@ -5936,6 +6239,9 @@ function onHttp2Dispatch(_eventType, payload) {
5936
6239
  export const http2 = {
5937
6240
  Http2ServerRequest,
5938
6241
  Http2ServerResponse,
6242
+ Http2Stream,
6243
+ NghttpError,
6244
+ nghttp2ErrorString,
5939
6245
  constants: {
5940
6246
  HTTP2_HEADER_METHOD: ":method",
5941
6247
  HTTP2_HEADER_PATH: ":path",
@@ -5944,19 +6250,14 @@ export const http2 = {
5944
6250
  HTTP2_HEADER_STATUS: ":status",
5945
6251
  HTTP2_HEADER_CONTENT_TYPE: "content-type",
5946
6252
  HTTP2_HEADER_CONTENT_LENGTH: "content-length",
6253
+ HTTP2_HEADER_LAST_MODIFIED: "last-modified",
5947
6254
  HTTP2_HEADER_ACCEPT: "accept",
5948
6255
  HTTP2_HEADER_ACCEPT_ENCODING: "accept-encoding",
5949
6256
  HTTP2_METHOD_GET: "GET",
5950
6257
  HTTP2_METHOD_POST: "POST",
5951
6258
  HTTP2_METHOD_PUT: "PUT",
5952
6259
  HTTP2_METHOD_DELETE: "DELETE",
5953
- NGHTTP2_NO_ERROR: 0,
5954
- NGHTTP2_PROTOCOL_ERROR: 1,
5955
- NGHTTP2_INTERNAL_ERROR: 2,
5956
- NGHTTP2_FRAME_SIZE_ERROR: 6,
5957
- NGHTTP2_FLOW_CONTROL_ERROR: 3,
5958
- NGHTTP2_REFUSED_STREAM: 7,
5959
- NGHTTP2_CANCEL: 8,
6260
+ ...HTTP2_INTERNAL_BINDING_CONSTANTS,
5960
6261
  DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: 65535,
5961
6262
  },
5962
6263
  getDefaultSettings() {
@@ -6023,6 +6324,23 @@ if (typeof globalThis.Blob === "undefined") {
6023
6324
  exposeCustomGlobal("Blob", class BlobStub {
6024
6325
  });
6025
6326
  }
6327
+ if (typeof globalThis.File === "undefined") {
6328
+ class FileStub extends Blob {
6329
+ name;
6330
+ lastModified;
6331
+ webkitRelativePath;
6332
+ constructor(parts = [], name = "", options = {}) {
6333
+ super(parts, options);
6334
+ this.name = String(name);
6335
+ this.lastModified =
6336
+ typeof options.lastModified === "number"
6337
+ ? options.lastModified
6338
+ : Date.now();
6339
+ this.webkitRelativePath = "";
6340
+ }
6341
+ }
6342
+ exposeCustomGlobal("File", FileStub);
6343
+ }
6026
6344
  if (typeof globalThis.FormData === "undefined") {
6027
6345
  // Minimal FormData stub — server frameworks check `instanceof FormData`.
6028
6346
  class FormDataStub {