@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.
- package/dist/bridge/child-process.js +47 -1
- package/dist/bridge/fs.d.ts +6 -1
- package/dist/bridge/fs.js +12 -3
- package/dist/bridge/index.d.ts +2 -2
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/network.d.ts +35 -1
- package/dist/bridge/network.js +471 -153
- package/dist/bridge/os.js +9 -3
- package/dist/bridge/polyfills.d.ts +6 -9
- package/dist/bridge/polyfills.js +616 -14
- package/dist/bridge/process.d.ts +3 -2
- package/dist/bridge/process.js +288 -61
- package/dist/bridge-contract.d.ts +21 -3
- package/dist/bridge-contract.js +2 -0
- package/dist/bridge-handlers.d.ts +10 -0
- package/dist/bridge-handlers.js +515 -179
- package/dist/bridge.js +1531 -855
- package/dist/builtin-modules.js +6 -0
- package/dist/esm-compiler.d.ts +1 -0
- package/dist/esm-compiler.js +29 -11
- package/dist/execution-driver.js +362 -10
- package/dist/host-network-adapter.js +10 -6
- package/dist/isolate-bootstrap.d.ts +6 -0
- package/dist/kernel-runtime.d.ts +3 -1
- package/dist/kernel-runtime.js +109 -11
- package/dist/module-access.d.ts +3 -0
- package/dist/module-access.js +52 -2
- package/dist/module-resolver.js +3 -3
- package/dist/module-source.d.ts +5 -0
- package/dist/module-source.js +224 -0
- package/dist/polyfills.js +27 -1
- package/package.json +5 -3
package/dist/bridge/network.js
CHANGED
|
@@ -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:
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 =
|
|
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
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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.
|
|
4396
|
-
this.
|
|
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
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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.
|
|
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
|
-
|
|
5013
|
-
|
|
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
|
-
|
|
5030
|
-
|
|
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
|
-
...
|
|
5033
|
-
})
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
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
|
-
|
|
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, () =>
|
|
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
|
-
|
|
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 {
|