@agentcomms/events-daemon 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -5
- package/dist/{b3-contract-8w39oOi0.mjs → b3-contract-ByO1DTgr.mjs} +320 -67
- package/dist/cli.mjs +3200 -121
- package/dist/index.d.mts +152 -47
- package/dist/index.mjs +1 -1
- package/dist/laya-worker.d.mts +1 -0
- package/dist/laya-worker.mjs +356 -0
- package/dist/{records-DsUxYBwb.mjs → records-D15LoVeb.mjs} +100 -3
- package/dist/records-qtNEBi7g.mjs +2 -0
- package/dist/{webhook-target-DyDTwWhV.mjs → webhook-target-EzLIFpIY.mjs} +551 -17
- package/dist/worker-boundary-probe-CFN6EsyK.mjs +850 -0
- package/laya-enablement-disclosures.json +17 -0
- package/laya-manifest.json +52 -0
- package/laya-runtime.json +4 -0
- package/package.json +12 -9
- package/dist/records-DjE5a4_H.mjs +0 -2
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { Server, Socket, connect, createServer } from "node:net";
|
|
4
|
+
import { join, sep } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { Socket as Socket$1, createSocket } from "node:dgram";
|
|
7
|
+
//#region src/network/error-sink.ts
|
|
8
|
+
/**
|
|
9
|
+
* Keeps an owned network resource from turning a later transport error into an uncaught exception.
|
|
10
|
+
* Operation-specific listeners still translate errors into their own result while they are active.
|
|
11
|
+
*/
|
|
12
|
+
function installErrorSink(resource) {
|
|
13
|
+
resource.on("error", () => void 0);
|
|
14
|
+
return resource;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/runtime/laya/templates.ts
|
|
18
|
+
/**
|
|
19
|
+
* Laya's two immutable prompt templates (E2-2, D11). The reviewed manifest lists which of them a release offers; their
|
|
20
|
+
* content lives here, in the daemon's worker source, so a semantic change is a new id and a new manifest entry, never
|
|
21
|
+
* an edit. Each has a stable SHA-256 over its canonical content, which the D11 gate subject binds.
|
|
22
|
+
*
|
|
23
|
+
* This module is loaded inside the isolated worker as well as by the daemon: it imports Node built-ins only.
|
|
24
|
+
*/
|
|
25
|
+
const LAYA_NOUL_TEMPLATE_ID = "laya-noul-v1";
|
|
26
|
+
const LAYA_CHOICE2_TEMPLATE_ID = "laya-choice2-v1";
|
|
27
|
+
/**
|
|
28
|
+
* `laya-noul-v1` asks one `noul` question; the runtime renders a `noul` question's two options itself, as
|
|
29
|
+
* `false: no, the statement does not hold` and `true: yes, the statement holds`, and its answer is P(true).
|
|
30
|
+
* `laya-choice2-v1` asks the same question as a two-option `choice` with neutral keys, the model card's workaround for
|
|
31
|
+
* issue #156, and its value is the probability of the affirmative key.
|
|
32
|
+
*/
|
|
33
|
+
const TEMPLATES = Object.freeze({
|
|
34
|
+
[LAYA_CHOICE2_TEMPLATE_ID]: Object.freeze({
|
|
35
|
+
id: LAYA_CHOICE2_TEMPLATE_ID,
|
|
36
|
+
type: "choice",
|
|
37
|
+
options: Object.freeze([Object.freeze({
|
|
38
|
+
label: "a",
|
|
39
|
+
text: "yes, the statement holds"
|
|
40
|
+
}), Object.freeze({
|
|
41
|
+
label: "b",
|
|
42
|
+
text: "no, the statement does not hold"
|
|
43
|
+
})]),
|
|
44
|
+
affirmative: "a"
|
|
45
|
+
}),
|
|
46
|
+
[LAYA_NOUL_TEMPLATE_ID]: Object.freeze({
|
|
47
|
+
id: LAYA_NOUL_TEMPLATE_ID,
|
|
48
|
+
type: "noul",
|
|
49
|
+
options: Object.freeze([Object.freeze({
|
|
50
|
+
label: "false",
|
|
51
|
+
text: "no, the statement does not hold"
|
|
52
|
+
}), Object.freeze({
|
|
53
|
+
label: "true",
|
|
54
|
+
text: "yes, the statement holds"
|
|
55
|
+
})]),
|
|
56
|
+
affirmative: "true"
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
/** Every template id this worker source implements, in raw UTF-8 byte order (the manifest's order). */
|
|
60
|
+
const LAYA_TEMPLATE_IDS = Object.freeze(Object.keys(TEMPLATES).sort((left, right) => Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"))));
|
|
61
|
+
/**
|
|
62
|
+
* The templates this release runs: those that ask a `noul` question. The worker refuses any other as
|
|
63
|
+
* `template-blocked` before the runtime is asked, so `laya-choice2-v1` stays out until a Laya source record states where
|
|
64
|
+
* a choice's probability comes from (E2 Task 8); B3 control v3 refuses a judge naming it when it is created.
|
|
65
|
+
*/
|
|
66
|
+
const LAYA_RUNNABLE_TEMPLATE_IDS = Object.freeze(LAYA_TEMPLATE_IDS.filter((id) => TEMPLATES[id].type === "noul"));
|
|
67
|
+
function isLayaTemplateId(value) {
|
|
68
|
+
return typeof value === "string" && Object.hasOwn(TEMPLATES, value);
|
|
69
|
+
}
|
|
70
|
+
function layaTemplate(id) {
|
|
71
|
+
const template = TEMPLATES[id];
|
|
72
|
+
if (template === void 0) throw new Error("the Laya prompt template is not one this worker implements");
|
|
73
|
+
return template;
|
|
74
|
+
}
|
|
75
|
+
/** The runtime replaces every literal `[MASK]` with a space before it encodes (npm-receptron-laya-0.1.2-render). */
|
|
76
|
+
function unmasked(text) {
|
|
77
|
+
return text.replaceAll("[MASK]", " ");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The complete, uncut text segments the runtime encodes for one request, as it lays them out: the head
|
|
81
|
+
* `<type> question: <instructions>`, each option as `" " + "<label>: <text>"`, then the state as given. The worker
|
|
82
|
+
* counts these with the bundle's own tokenizer before it runs anything, so the runtime never gets to cut one.
|
|
83
|
+
*/
|
|
84
|
+
function layaLayout(id, input) {
|
|
85
|
+
const template = layaTemplate(id);
|
|
86
|
+
return {
|
|
87
|
+
head: `${template.type} question: ${unmasked(input.instructions)}`,
|
|
88
|
+
options: template.options.map((option) => ` ${unmasked(`${option.label}: ${option.text}`)}`),
|
|
89
|
+
state: unmasked(input.state)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/runtime/laya/worker-protocol.ts
|
|
94
|
+
const WORKER_FRAME_MAX_BYTES = 65536;
|
|
95
|
+
const FRAME_PREFIX_BYTES = 4;
|
|
96
|
+
var LayaWorkerProtocolError = class extends Error {
|
|
97
|
+
code;
|
|
98
|
+
constructor(code) {
|
|
99
|
+
super(code);
|
|
100
|
+
this.name = "LayaWorkerProtocolError";
|
|
101
|
+
this.code = code;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
function frameObject(value) {
|
|
105
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LayaWorkerProtocolError("FRAME_SHAPE");
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function jsonFrame(value) {
|
|
109
|
+
let encoded;
|
|
110
|
+
try {
|
|
111
|
+
encoded = JSON.stringify(frameObject(value));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new LayaWorkerProtocolError("FRAME_JSON");
|
|
114
|
+
}
|
|
115
|
+
if (encoded === void 0) throw new LayaWorkerProtocolError("FRAME_JSON");
|
|
116
|
+
const payload = Buffer.from(encoded, "utf8");
|
|
117
|
+
if (payload.length === 0 || payload.length > 65536) throw new LayaWorkerProtocolError("FRAME_SIZE");
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
/** Encodes one exact big-endian length-prefixed JSON object. */
|
|
121
|
+
function encodeWorkerFrame(value) {
|
|
122
|
+
const payload = jsonFrame(value);
|
|
123
|
+
const frame = Buffer.allocUnsafe(FRAME_PREFIX_BYTES + payload.length);
|
|
124
|
+
frame.writeUInt32BE(payload.length, 0);
|
|
125
|
+
payload.copy(frame, FRAME_PREFIX_BYTES);
|
|
126
|
+
return frame;
|
|
127
|
+
}
|
|
128
|
+
/** Incrementally parses pipes without allocating for an attacker-controlled advertised frame size. */
|
|
129
|
+
var WorkerFrameParser = class {
|
|
130
|
+
#buffer = Buffer.alloc(0);
|
|
131
|
+
push(chunk) {
|
|
132
|
+
if (chunk.length === 0) return [];
|
|
133
|
+
this.#buffer = Buffer.concat([this.#buffer, Buffer.from(chunk)]);
|
|
134
|
+
const frames = [];
|
|
135
|
+
while (this.#buffer.length >= FRAME_PREFIX_BYTES) {
|
|
136
|
+
const length = this.#buffer.readUInt32BE(0);
|
|
137
|
+
if (length === 0 || length > 65536) throw new LayaWorkerProtocolError("FRAME_SIZE");
|
|
138
|
+
const end = FRAME_PREFIX_BYTES + length;
|
|
139
|
+
if (this.#buffer.length < end) break;
|
|
140
|
+
const payload = this.#buffer.subarray(FRAME_PREFIX_BYTES, end);
|
|
141
|
+
this.#buffer = this.#buffer.subarray(end);
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(payload));
|
|
145
|
+
} catch {
|
|
146
|
+
throw new LayaWorkerProtocolError("FRAME_JSON");
|
|
147
|
+
}
|
|
148
|
+
frames.push(frameObject(parsed));
|
|
149
|
+
}
|
|
150
|
+
return frames;
|
|
151
|
+
}
|
|
152
|
+
finish() {
|
|
153
|
+
if (this.#buffer.length !== 0) throw new LayaWorkerProtocolError("FRAME_TRUNCATED");
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function createWorkerRequestId() {
|
|
157
|
+
return randomUUID();
|
|
158
|
+
}
|
|
159
|
+
function createWorkerProbeId() {
|
|
160
|
+
return randomUUID();
|
|
161
|
+
}
|
|
162
|
+
/** Writes one complete bounded frame and honours pipe backpressure. */
|
|
163
|
+
async function writeWorkerFrame(destination, frame) {
|
|
164
|
+
if (destination.write(encodeWorkerFrame(frame))) return;
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
const drained = () => {
|
|
167
|
+
destination.off("error", failed);
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
const failed = (error) => {
|
|
171
|
+
destination.off("drain", drained);
|
|
172
|
+
reject(error);
|
|
173
|
+
};
|
|
174
|
+
destination.once("drain", drained);
|
|
175
|
+
destination.once("error", failed);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/runtime/laya/worker-boundary-probe.ts
|
|
180
|
+
const LAYA_SANDBOX_EXECUTABLE = "/usr/bin/sandbox-exec";
|
|
181
|
+
const LAYA_SANDBOX_PROFILE = "(version 1) (allow default) (deny network*)";
|
|
182
|
+
/** The profile parameter naming the one directory the worker may write in: its own, resolved as the kernel sees it. */
|
|
183
|
+
const LAYA_WORKER_DIRECTORY_PARAMETER = "LAYA_WORKER_DIRECTORY";
|
|
184
|
+
/**
|
|
185
|
+
* What the worker runs under. The binding still names `LAYA_SANDBOX_PROFILE`, the reviewed profile's identity that the
|
|
186
|
+
* worker checks and Task 7's frozen vectors pin; the launch adds the confinement to it, and the launch test pins both.
|
|
187
|
+
*/
|
|
188
|
+
const LAYA_WORKER_SANDBOX_PROFILE = `${LAYA_SANDBOX_PROFILE} ${`(deny process-fork) (deny appleevent-send) (deny mach-lookup) (deny file-write*) (allow file-write* (subpath (param "${LAYA_WORKER_DIRECTORY_PARAMETER}")) (literal "/dev/null"))`}`;
|
|
189
|
+
const BOUNDARY_PROBE_TIMEOUT_MS = 5e3;
|
|
190
|
+
const BOUNDARY_SETTLE_MS = 150;
|
|
191
|
+
const PROBE_SOCKET_RELEASE_MS = 1e3;
|
|
192
|
+
const UNSPAWNED_RELEASE_MS = 1e3;
|
|
193
|
+
const DENIED_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES"]);
|
|
194
|
+
/** macOS keeps 104 bytes for a socket path, its terminating NUL included (B1-G's control socket has the same bound). */
|
|
195
|
+
const UNIX_SOCKET_PATH_LIMIT_BYTES = 103;
|
|
196
|
+
/** The daemon resolves its reviewed worker from its own shipped package, never from a caller-supplied path. */
|
|
197
|
+
const LAYA_WORKER_ENTRY = join(import.meta.dirname, import.meta.dirname.endsWith(`${sep}dist`) ? "laya-worker.mjs" : "worker-main.ts");
|
|
198
|
+
/** The child receives no inherited credential, proxy, Node option or descriptor beyond these three pipes. */
|
|
199
|
+
const LAYA_WORKER_ENV = Object.freeze({
|
|
200
|
+
PATH: "/usr/bin:/bin",
|
|
201
|
+
TZ: "UTC"
|
|
202
|
+
});
|
|
203
|
+
/** The worker's five attempts, in the order it makes and reports them: four connections, then a process start. */
|
|
204
|
+
const PROBE_ATTEMPTS = [
|
|
205
|
+
"tcp",
|
|
206
|
+
"fetch",
|
|
207
|
+
"udp",
|
|
208
|
+
"uds",
|
|
209
|
+
"spawn"
|
|
210
|
+
];
|
|
211
|
+
function listen(server, address, host) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const onError = (error) => {
|
|
214
|
+
server.off("listening", onListening);
|
|
215
|
+
reject(error);
|
|
216
|
+
};
|
|
217
|
+
const onListening = () => {
|
|
218
|
+
server.off("error", onError);
|
|
219
|
+
resolve();
|
|
220
|
+
};
|
|
221
|
+
server.once("error", onError);
|
|
222
|
+
server.once("listening", onListening);
|
|
223
|
+
if (host === void 0) server.listen(address);
|
|
224
|
+
else if (typeof address === "number") server.listen({
|
|
225
|
+
port: address,
|
|
226
|
+
host
|
|
227
|
+
});
|
|
228
|
+
else reject(/* @__PURE__ */ new Error("boundary listener address"));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function closeServer(server) {
|
|
232
|
+
return new Promise((resolve, reject) => server.close((error) => {
|
|
233
|
+
if (error === void 0 || error.code === "ERR_SERVER_NOT_RUNNING") resolve();
|
|
234
|
+
else reject(error);
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
function closeUdp(socket) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
try {
|
|
240
|
+
socket.close(() => resolve());
|
|
241
|
+
} catch {
|
|
242
|
+
resolve();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function bindUdp(socket) {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
socket.once("error", reject);
|
|
249
|
+
socket.bind({
|
|
250
|
+
address: "127.0.0.1",
|
|
251
|
+
port: 0
|
|
252
|
+
}, () => {
|
|
253
|
+
socket.off("error", reject);
|
|
254
|
+
resolve();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function startBoundaryListeners(dataDirectory) {
|
|
259
|
+
await mkdir(dataDirectory, {
|
|
260
|
+
recursive: true,
|
|
261
|
+
mode: 448
|
|
262
|
+
});
|
|
263
|
+
await chmod(dataDirectory, 448);
|
|
264
|
+
const udsDirectory = await mkdtemp(join(dataDirectory, "laya-boundary-"));
|
|
265
|
+
await chmod(udsDirectory, 448);
|
|
266
|
+
const udsPath = join(udsDirectory, "boundary.sock");
|
|
267
|
+
if (Buffer.byteLength(udsPath) > UNIX_SOCKET_PATH_LIMIT_BYTES) {
|
|
268
|
+
await rm(udsDirectory, {
|
|
269
|
+
recursive: true,
|
|
270
|
+
force: true
|
|
271
|
+
});
|
|
272
|
+
throw new Error("the Laya boundary socket path is too long");
|
|
273
|
+
}
|
|
274
|
+
let tcpConnections = 0;
|
|
275
|
+
let udpDatagrams = 0;
|
|
276
|
+
let udsConnections = 0;
|
|
277
|
+
const tcpSockets = /* @__PURE__ */ new Set();
|
|
278
|
+
const udsSockets = /* @__PURE__ */ new Set();
|
|
279
|
+
let reportFailure = () => void 0;
|
|
280
|
+
const failure = new Promise((resolve) => {
|
|
281
|
+
reportFailure = resolve;
|
|
282
|
+
});
|
|
283
|
+
const guardError = (resource) => {
|
|
284
|
+
installErrorSink(resource);
|
|
285
|
+
resource.on("error", reportFailure);
|
|
286
|
+
return resource;
|
|
287
|
+
};
|
|
288
|
+
const track = (sockets, socket) => {
|
|
289
|
+
guardError(socket);
|
|
290
|
+
sockets.add(socket);
|
|
291
|
+
socket.once("close", () => sockets.delete(socket));
|
|
292
|
+
socket.destroy();
|
|
293
|
+
};
|
|
294
|
+
const tcp = guardError(createServer((socket) => {
|
|
295
|
+
tcpConnections += 1;
|
|
296
|
+
track(tcpSockets, socket);
|
|
297
|
+
}));
|
|
298
|
+
const udp = guardError(createSocket("udp4"));
|
|
299
|
+
udp.on("message", () => {
|
|
300
|
+
udpDatagrams += 1;
|
|
301
|
+
});
|
|
302
|
+
const uds = guardError(createServer((socket) => {
|
|
303
|
+
udsConnections += 1;
|
|
304
|
+
track(udsSockets, socket);
|
|
305
|
+
}));
|
|
306
|
+
let closing;
|
|
307
|
+
const close = () => {
|
|
308
|
+
closing ??= (async () => {
|
|
309
|
+
for (const socket of [...tcpSockets, ...udsSockets]) socket.destroy();
|
|
310
|
+
await Promise.allSettled([
|
|
311
|
+
closeServer(tcp),
|
|
312
|
+
closeServer(uds),
|
|
313
|
+
closeUdp(udp)
|
|
314
|
+
]);
|
|
315
|
+
await rm(udsPath, { force: true });
|
|
316
|
+
await rm(udsDirectory, {
|
|
317
|
+
recursive: true,
|
|
318
|
+
force: true
|
|
319
|
+
});
|
|
320
|
+
})();
|
|
321
|
+
return closing;
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
await listen(tcp, 0, "127.0.0.1");
|
|
325
|
+
await bindUdp(udp);
|
|
326
|
+
await listen(uds, udsPath);
|
|
327
|
+
const tcpAddress = tcp.address();
|
|
328
|
+
const udpAddress = udp.address();
|
|
329
|
+
if (tcpAddress === null || typeof tcpAddress === "string" || typeof udpAddress === "string") throw new Error("boundary listener");
|
|
330
|
+
return {
|
|
331
|
+
tcpPort: tcpAddress.port,
|
|
332
|
+
udpPort: udpAddress.port,
|
|
333
|
+
udsPath,
|
|
334
|
+
get observed() {
|
|
335
|
+
return {
|
|
336
|
+
tcpConnections,
|
|
337
|
+
udpDatagrams,
|
|
338
|
+
udsConnections
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
failure,
|
|
342
|
+
close
|
|
343
|
+
};
|
|
344
|
+
} catch (error) {
|
|
345
|
+
await close();
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function workerArguments(entry, workerDirectory) {
|
|
350
|
+
const sandbox = [
|
|
351
|
+
"-D",
|
|
352
|
+
`${LAYA_WORKER_DIRECTORY_PARAMETER}=${workerDirectory}`,
|
|
353
|
+
"-p",
|
|
354
|
+
LAYA_WORKER_SANDBOX_PROFILE
|
|
355
|
+
];
|
|
356
|
+
return entry.endsWith(".ts") ? [
|
|
357
|
+
...sandbox,
|
|
358
|
+
process.execPath,
|
|
359
|
+
"--experimental-strip-types",
|
|
360
|
+
entry
|
|
361
|
+
] : [
|
|
362
|
+
...sandbox,
|
|
363
|
+
process.execPath,
|
|
364
|
+
entry
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
/** The working directory is the owner-only (0700) data directory the proof's listeners were made in. */
|
|
368
|
+
function workerSpawnOptions(workingDirectory) {
|
|
369
|
+
return {
|
|
370
|
+
cwd: workingDirectory,
|
|
371
|
+
env: LAYA_WORKER_ENV,
|
|
372
|
+
shell: false,
|
|
373
|
+
stdio: [
|
|
374
|
+
"pipe",
|
|
375
|
+
"pipe",
|
|
376
|
+
"pipe"
|
|
377
|
+
]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function bindingFrom(value) {
|
|
381
|
+
if (typeof value.nonce !== "string" || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || typeof value.profile !== "string" || typeof value.entry !== "string" || typeof value.pipeGeneration !== "string") return null;
|
|
382
|
+
return {
|
|
383
|
+
nonce: value.nonce,
|
|
384
|
+
pid: value.pid,
|
|
385
|
+
profile: value.profile,
|
|
386
|
+
entry: value.entry,
|
|
387
|
+
pipeGeneration: value.pipeGeneration
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function sameBinding(expected, candidate) {
|
|
391
|
+
return expected.nonce === candidate.nonce && expected.pid === candidate.pid && expected.profile === candidate.profile && expected.entry === candidate.entry && expected.pipeGeneration === candidate.pipeGeneration;
|
|
392
|
+
}
|
|
393
|
+
function isProbeOutcome(value) {
|
|
394
|
+
return value === "denied" || value === "escaped" || value === "failed";
|
|
395
|
+
}
|
|
396
|
+
function isProbeAttempt(value) {
|
|
397
|
+
return PROBE_ATTEMPTS.includes(value);
|
|
398
|
+
}
|
|
399
|
+
function probeReport(frame) {
|
|
400
|
+
const report = {};
|
|
401
|
+
for (const attempt of PROBE_ATTEMPTS) {
|
|
402
|
+
const outcome = frame[attempt];
|
|
403
|
+
if (!isProbeOutcome(outcome)) return null;
|
|
404
|
+
report[attempt] = outcome;
|
|
405
|
+
}
|
|
406
|
+
return report;
|
|
407
|
+
}
|
|
408
|
+
function probeResult(frame, binding) {
|
|
409
|
+
if (frame.type !== "probe-result") return null;
|
|
410
|
+
const candidate = bindingFrom(frame);
|
|
411
|
+
if (candidate === null || !sameBinding(binding, candidate)) return null;
|
|
412
|
+
const report = probeReport(frame);
|
|
413
|
+
return report === null ? null : {
|
|
414
|
+
type: "probe-result",
|
|
415
|
+
...candidate,
|
|
416
|
+
...report
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function probeAttempt(frame, binding) {
|
|
420
|
+
if (frame.type !== "probe-attempt") return null;
|
|
421
|
+
const candidate = bindingFrom(frame);
|
|
422
|
+
if (candidate === null || !sameBinding(binding, candidate)) return null;
|
|
423
|
+
if (!isProbeAttempt(frame.attempt) || !isProbeOutcome(frame.outcome)) return null;
|
|
424
|
+
return {
|
|
425
|
+
attempt: frame.attempt,
|
|
426
|
+
outcome: frame.outcome
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Child stderr can carry untrusted runtime or model text, so the daemon retains none of it. It keeps reading and
|
|
431
|
+
* discarding: a paused pipe would fill and stall a child that writes to stderr, which would look like a hung worker.
|
|
432
|
+
*/
|
|
433
|
+
function discardStderr(child) {
|
|
434
|
+
child.stderr.on("data", () => void 0);
|
|
435
|
+
}
|
|
436
|
+
/** A worker that exits early turns the daemon's next write into EPIPE; that must fail the proof, never the daemon. */
|
|
437
|
+
function sinkWorkerPipes(child) {
|
|
438
|
+
installErrorSink(child);
|
|
439
|
+
installErrorSink(child.stdin);
|
|
440
|
+
installErrorSink(child.stdout);
|
|
441
|
+
installErrorSink(child.stderr);
|
|
442
|
+
}
|
|
443
|
+
function waitForProbe(child, binding, timeoutMs, signal) {
|
|
444
|
+
return new Promise((resolve) => {
|
|
445
|
+
const parser = new WorkerFrameParser();
|
|
446
|
+
const attempts = /* @__PURE__ */ new Set();
|
|
447
|
+
const expectedAttempts = PROBE_ATTEMPTS;
|
|
448
|
+
let done = false;
|
|
449
|
+
let reported;
|
|
450
|
+
let settled;
|
|
451
|
+
const finish = (result) => {
|
|
452
|
+
if (done) return;
|
|
453
|
+
done = true;
|
|
454
|
+
clearTimeout(timeout);
|
|
455
|
+
if (settled !== void 0) clearTimeout(settled);
|
|
456
|
+
child.stdout.off("data", onData);
|
|
457
|
+
child.stdout.pause();
|
|
458
|
+
child.off("error", onEnd);
|
|
459
|
+
child.off("exit", onEnd);
|
|
460
|
+
child.off("close", onEnd);
|
|
461
|
+
signal?.removeEventListener("abort", onAbort);
|
|
462
|
+
resolve(result);
|
|
463
|
+
};
|
|
464
|
+
const onData = (chunk) => {
|
|
465
|
+
try {
|
|
466
|
+
for (const frame of parser.push(chunk)) {
|
|
467
|
+
if (reported !== void 0) {
|
|
468
|
+
finish(null);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (frame.type === "probe-attempt") {
|
|
472
|
+
const attempt = probeAttempt(frame, binding);
|
|
473
|
+
if (attempt === null || attempt.outcome !== "denied" || attempt.attempt !== expectedAttempts[attempts.size] || attempts.has(attempt.attempt)) {
|
|
474
|
+
finish(null);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
attempts.add(attempt.attempt);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const result = probeResult(frame, binding);
|
|
481
|
+
if (result === null || attempts.size !== expectedAttempts.length) {
|
|
482
|
+
finish(null);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
reported = result;
|
|
486
|
+
settled = setTimeout(() => finish(reported ?? null), BOUNDARY_SETTLE_MS);
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
finish(null);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const onEnd = () => finish(null);
|
|
493
|
+
const onAbort = () => finish(null);
|
|
494
|
+
const timeout = setTimeout(() => finish(null), timeoutMs);
|
|
495
|
+
child.stdout.on("data", onData);
|
|
496
|
+
child.once("error", onEnd);
|
|
497
|
+
child.once("exit", onEnd);
|
|
498
|
+
child.once("close", onEnd);
|
|
499
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
500
|
+
if (signal?.aborted) onAbort();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function waitForExit(child, boundMs) {
|
|
504
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
const timer = boundMs === void 0 ? void 0 : setTimeout(() => settled(), boundMs);
|
|
507
|
+
const settled = () => {
|
|
508
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
509
|
+
child.off("exit", settled);
|
|
510
|
+
child.off("close", settled);
|
|
511
|
+
child.off("error", settled);
|
|
512
|
+
resolve();
|
|
513
|
+
};
|
|
514
|
+
child.once("exit", settled);
|
|
515
|
+
child.once("close", settled);
|
|
516
|
+
child.once("error", settled);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function killWorker(child) {
|
|
520
|
+
if (child.pid === void 0) {
|
|
521
|
+
await waitForExit(child, UNSPAWNED_RELEASE_MS);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
525
|
+
await waitForExit(child);
|
|
526
|
+
}
|
|
527
|
+
/** A child report alone is never sufficient: the daemon's own listeners must observe no connection or datagram. */
|
|
528
|
+
function boundaryProbePassed(result, observed) {
|
|
529
|
+
return result.tcp === "denied" && result.fetch === "denied" && result.udp === "denied" && result.uds === "denied" && result.spawn === "denied" && observed.tcpConnections === 0 && observed.udpDatagrams === 0 && observed.udsConnections === 0;
|
|
530
|
+
}
|
|
531
|
+
function isAbortSignal(value) {
|
|
532
|
+
return typeof value === "object" && value !== null && typeof value.aborted === "boolean" && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
|
|
533
|
+
}
|
|
534
|
+
function productionBoundaryOptions(input) {
|
|
535
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return null;
|
|
536
|
+
const candidate = input;
|
|
537
|
+
const keys = Object.keys(candidate).toSorted();
|
|
538
|
+
if (keys.length < 1 || keys.length > 2 || keys[0] !== "dataDirectory" || keys[1] !== void 0 && keys[1] !== "signal") return null;
|
|
539
|
+
if (typeof candidate.dataDirectory !== "string" || candidate.dataDirectory.length === 0) return null;
|
|
540
|
+
if (candidate.signal !== void 0 && !isAbortSignal(candidate.signal)) return null;
|
|
541
|
+
return {
|
|
542
|
+
dataDirectory: candidate.dataDirectory,
|
|
543
|
+
signal: candidate.signal
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
async function disposeFailedBoundary(child, listeners) {
|
|
547
|
+
await Promise.allSettled([...child === void 0 ? [] : [killWorker(child)], ...listeners === void 0 ? [] : [listeners.close()]]);
|
|
548
|
+
}
|
|
549
|
+
/** Starts one child, proves its own OS boundary before load, and never falls back to an unconfined process. */
|
|
550
|
+
async function createLayaWorkerBoundaryInternal(options) {
|
|
551
|
+
if ((options.platform ?? process.platform) !== "darwin") return {
|
|
552
|
+
ok: false,
|
|
553
|
+
code: "LAYA_PLATFORM_UNAVAILABLE",
|
|
554
|
+
loadSent: false
|
|
555
|
+
};
|
|
556
|
+
if (options.signal?.aborted) return {
|
|
557
|
+
ok: false,
|
|
558
|
+
code: "boundary-failed",
|
|
559
|
+
loadSent: false
|
|
560
|
+
};
|
|
561
|
+
let listeners;
|
|
562
|
+
let child;
|
|
563
|
+
try {
|
|
564
|
+
listeners = await (options.startListeners ?? startBoundaryListeners)(options.dataDirectory);
|
|
565
|
+
if (options.signal?.aborted) {
|
|
566
|
+
await disposeFailedBoundary(void 0, listeners);
|
|
567
|
+
listeners = void 0;
|
|
568
|
+
return {
|
|
569
|
+
ok: false,
|
|
570
|
+
code: "boundary-failed",
|
|
571
|
+
loadSent: false
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const workerDirectory = await realpath(options.dataDirectory);
|
|
575
|
+
child = (options.spawn ?? ((command, args, spawnOptions) => spawn(command, args, spawnOptions)))(LAYA_SANDBOX_EXECUTABLE, workerArguments(options.workerEntry, workerDirectory), workerSpawnOptions(options.dataDirectory));
|
|
576
|
+
const started = child;
|
|
577
|
+
installErrorSink(started);
|
|
578
|
+
if (started.pid === void 0) {
|
|
579
|
+
await disposeFailedBoundary(started, listeners);
|
|
580
|
+
listeners = void 0;
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
code: "boundary-failed",
|
|
584
|
+
loadSent: false
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
sinkWorkerPipes(started);
|
|
588
|
+
discardStderr(started);
|
|
589
|
+
if (options.signal?.aborted) {
|
|
590
|
+
await disposeFailedBoundary(started, listeners);
|
|
591
|
+
listeners = void 0;
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
code: "boundary-failed",
|
|
595
|
+
loadSent: false
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const binding = {
|
|
599
|
+
nonce: createWorkerProbeId(),
|
|
600
|
+
pid: started.pid,
|
|
601
|
+
profile: LAYA_SANDBOX_PROFILE,
|
|
602
|
+
entry: options.workerEntry,
|
|
603
|
+
pipeGeneration: createWorkerRequestId()
|
|
604
|
+
};
|
|
605
|
+
await writeWorkerFrame(started.stdin, {
|
|
606
|
+
type: "probe",
|
|
607
|
+
...binding,
|
|
608
|
+
tcpPort: listeners.tcpPort,
|
|
609
|
+
udpPort: listeners.udpPort,
|
|
610
|
+
udsPath: listeners.udsPath
|
|
611
|
+
});
|
|
612
|
+
const result = await Promise.race([waitForProbe(started, binding, options.timeoutMs ?? BOUNDARY_PROBE_TIMEOUT_MS, options.signal), listeners.failure.then(() => null)]);
|
|
613
|
+
const proof = listeners;
|
|
614
|
+
listeners = void 0;
|
|
615
|
+
await proof.close();
|
|
616
|
+
if (result === null || !boundaryProbePassed(result, proof.observed)) {
|
|
617
|
+
await disposeFailedBoundary(started, void 0);
|
|
618
|
+
return {
|
|
619
|
+
ok: false,
|
|
620
|
+
code: "boundary-failed",
|
|
621
|
+
loadSent: false
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
ok: true,
|
|
626
|
+
child: started,
|
|
627
|
+
binding,
|
|
628
|
+
async sendLoad(payload) {
|
|
629
|
+
assertWorkerLoadBinding(binding, {
|
|
630
|
+
type: "load",
|
|
631
|
+
...binding,
|
|
632
|
+
payload
|
|
633
|
+
});
|
|
634
|
+
await writeWorkerFrame(started.stdin, {
|
|
635
|
+
type: "load",
|
|
636
|
+
...binding,
|
|
637
|
+
payload
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
async close() {
|
|
641
|
+
await killWorker(started);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
} catch {
|
|
645
|
+
await disposeFailedBoundary(child, listeners);
|
|
646
|
+
listeners = void 0;
|
|
647
|
+
return {
|
|
648
|
+
ok: false,
|
|
649
|
+
code: "boundary-failed",
|
|
650
|
+
loadSent: false
|
|
651
|
+
};
|
|
652
|
+
} finally {
|
|
653
|
+
if (listeners !== void 0) await disposeFailedBoundary(void 0, listeners);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** Production surface: only the daemon's shipped worker entry can reach the sandbox launcher. */
|
|
657
|
+
async function createLayaWorkerBoundary(options) {
|
|
658
|
+
const accepted = productionBoundaryOptions(options);
|
|
659
|
+
if (accepted === null) return {
|
|
660
|
+
ok: false,
|
|
661
|
+
code: "boundary-failed",
|
|
662
|
+
loadSent: false
|
|
663
|
+
};
|
|
664
|
+
return createLayaWorkerBoundaryInternal({
|
|
665
|
+
...accepted,
|
|
666
|
+
workerEntry: LAYA_WORKER_ENTRY
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
/** The worker's second frame may only use the exact child that completed the probe. */
|
|
670
|
+
function assertWorkerLoadBinding(binding, frame) {
|
|
671
|
+
const candidate = bindingFrom(frame);
|
|
672
|
+
if (frame.type !== "load" || candidate === null || !sameBinding(binding, candidate)) throw new Error("worker load binding");
|
|
673
|
+
}
|
|
674
|
+
/** True only for the operating system's refusal, found on the error, its cause or an aggregate's members. */
|
|
675
|
+
function deniedBySystem(error, depth = 0) {
|
|
676
|
+
if (depth > 4 || error === null || typeof error !== "object") return false;
|
|
677
|
+
const { code, cause, errors } = error;
|
|
678
|
+
if (typeof code === "string" && DENIED_CODES.has(code)) return true;
|
|
679
|
+
if (deniedBySystem(cause, depth + 1)) return true;
|
|
680
|
+
return Array.isArray(errors) && errors.some((member) => deniedBySystem(member, depth + 1));
|
|
681
|
+
}
|
|
682
|
+
function settled(run) {
|
|
683
|
+
return new Promise((resolve) => resolve(run())).then(() => "escaped", (error) => deniedBySystem(error) ? "denied" : "failed");
|
|
684
|
+
}
|
|
685
|
+
/** Runs one attempt with a bound; on expiry its own socket is released before the attempt fails. */
|
|
686
|
+
function bounded(run) {
|
|
687
|
+
return new Promise((resolve, reject) => {
|
|
688
|
+
let done = false;
|
|
689
|
+
let release;
|
|
690
|
+
const timer = setTimeout(() => {
|
|
691
|
+
if (done) return;
|
|
692
|
+
done = true;
|
|
693
|
+
(release?.() ?? Promise.resolve()).finally(() => reject(/* @__PURE__ */ new Error("boundary probe timeout")));
|
|
694
|
+
}, BOUNDARY_PROBE_TIMEOUT_MS);
|
|
695
|
+
release = run(() => {
|
|
696
|
+
if (done) return;
|
|
697
|
+
done = true;
|
|
698
|
+
clearTimeout(timer);
|
|
699
|
+
resolve();
|
|
700
|
+
}, (error) => {
|
|
701
|
+
if (done) return;
|
|
702
|
+
done = true;
|
|
703
|
+
clearTimeout(timer);
|
|
704
|
+
reject(error);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function closedSocket(socket) {
|
|
709
|
+
if (socket.closed) return Promise.resolve();
|
|
710
|
+
return new Promise((resolve) => {
|
|
711
|
+
socket.once("close", () => resolve());
|
|
712
|
+
socket.destroy();
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function closedDatagram(socket) {
|
|
716
|
+
return new Promise((resolve) => {
|
|
717
|
+
try {
|
|
718
|
+
socket.close(() => resolve());
|
|
719
|
+
} catch {
|
|
720
|
+
resolve();
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/** The attempt settles only after its socket has closed, so no probe handle outlives the report. */
|
|
725
|
+
function connectProbe(options) {
|
|
726
|
+
return bounded((resolve, reject) => {
|
|
727
|
+
const socket = "path" in options ? connect(options.path) : connect(options.port, "127.0.0.1");
|
|
728
|
+
socket.on("error", () => void 0);
|
|
729
|
+
const release = () => closedSocket(socket);
|
|
730
|
+
socket.once("connect", () => void release().then(resolve));
|
|
731
|
+
socket.once("error", (error) => void release().then(() => reject(error)));
|
|
732
|
+
return release;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function fetchProbe(port) {
|
|
736
|
+
return bounded((resolve, reject) => {
|
|
737
|
+
const controller = new AbortController();
|
|
738
|
+
globalThis.fetch(`http://127.0.0.1:${port}/laya-boundary-probe`, { signal: controller.signal }).then(async (response) => {
|
|
739
|
+
await response.body?.cancel().catch(() => void 0);
|
|
740
|
+
resolve();
|
|
741
|
+
}, (error) => reject(error instanceof Error ? error : /* @__PURE__ */ new Error("probe request")));
|
|
742
|
+
return async () => controller.abort();
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function udpProbe(port) {
|
|
746
|
+
return bounded((resolve, reject) => {
|
|
747
|
+
const socket = createSocket("udp4");
|
|
748
|
+
socket.on("error", () => void 0);
|
|
749
|
+
const release = () => closedDatagram(socket);
|
|
750
|
+
socket.once("error", (error) => void release().then(() => reject(error)));
|
|
751
|
+
socket.send(Buffer.from([0]), port, "127.0.0.1", (error) => {
|
|
752
|
+
release().then(() => error ? reject(error) : resolve());
|
|
753
|
+
});
|
|
754
|
+
return release;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
/** The fifth attempt's program, which exits at once if the profile ever let it start. */
|
|
758
|
+
const LAYA_PROBE_SPAWN_COMMAND = "/usr/bin/true";
|
|
759
|
+
const PROBE_SPAWN_OPTIONS = Object.freeze({
|
|
760
|
+
cwd: "/",
|
|
761
|
+
env: {},
|
|
762
|
+
shell: false,
|
|
763
|
+
stdio: "ignore"
|
|
764
|
+
});
|
|
765
|
+
/**
|
|
766
|
+
* The fifth attempt: start another process. Under the profile `process-fork` refuses it and Node's spawn fails with
|
|
767
|
+
* EPERM; unconfined it starts, which is an escape. It settles only after a process it started has exited, so none
|
|
768
|
+
* outlives the report. No daemon listener can see a process start, so this attempt rests on the system's refusal.
|
|
769
|
+
*/
|
|
770
|
+
function spawnProbe() {
|
|
771
|
+
return bounded((resolve, reject) => {
|
|
772
|
+
let child;
|
|
773
|
+
try {
|
|
774
|
+
child = spawn(LAYA_PROBE_SPAWN_COMMAND, [], PROBE_SPAWN_OPTIONS);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
reject(error instanceof Error ? error : /* @__PURE__ */ new Error("probe spawn"));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const ended = new Promise((settle) => child.once("close", () => settle()));
|
|
780
|
+
const release = () => {
|
|
781
|
+
if (child.pid !== void 0 && child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
782
|
+
return child.pid === void 0 ? Promise.resolve() : ended;
|
|
783
|
+
};
|
|
784
|
+
child.once("error", (error) => void release().then(() => reject(error)));
|
|
785
|
+
child.once("spawn", () => void ended.then(resolve));
|
|
786
|
+
return release;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/** This is the worker's first built-in-only act, before any runtime/model import. */
|
|
790
|
+
async function runWorkerBoundaryProbe(input, observeAttempt) {
|
|
791
|
+
const attempts = [
|
|
792
|
+
["tcp", () => connectProbe({ port: input.tcpPort })],
|
|
793
|
+
["fetch", () => fetchProbe(input.tcpPort)],
|
|
794
|
+
["udp", () => udpProbe(input.udpPort)],
|
|
795
|
+
["uds", () => connectProbe({ path: input.udsPath })],
|
|
796
|
+
["spawn", () => spawnProbe()]
|
|
797
|
+
];
|
|
798
|
+
const outcomes = {};
|
|
799
|
+
for (const [attempt, run] of attempts) {
|
|
800
|
+
const outcome = await settled(run);
|
|
801
|
+
outcomes[attempt] = outcome;
|
|
802
|
+
await observeAttempt?.(attempt, outcome);
|
|
803
|
+
}
|
|
804
|
+
if (PROBE_ATTEMPTS.some((attempt) => outcomes[attempt] === void 0)) throw new Error("boundary probe");
|
|
805
|
+
return outcomes;
|
|
806
|
+
}
|
|
807
|
+
function sealedNetwork() {
|
|
808
|
+
throw new Error("Laya worker network is sealed");
|
|
809
|
+
}
|
|
810
|
+
function seal(target, name, value) {
|
|
811
|
+
Object.defineProperty(target, name, {
|
|
812
|
+
configurable: false,
|
|
813
|
+
enumerable: false,
|
|
814
|
+
writable: false,
|
|
815
|
+
value
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Defence in depth after the operating-system proof, never a substitute for it: `fetch` and `WebSocket`, every TCP or
|
|
820
|
+
* pathname-socket connection and listener (which `http`, `https` and `tls` build on) and every datagram bind, connect
|
|
821
|
+
* or send throw for the rest of the worker's life.
|
|
822
|
+
*/
|
|
823
|
+
function sealWorkerNetworkAfterProbe() {
|
|
824
|
+
seal(globalThis, "fetch", async () => sealedNetwork());
|
|
825
|
+
if ("WebSocket" in globalThis) seal(globalThis, "WebSocket", class SealedWebSocket {
|
|
826
|
+
constructor() {
|
|
827
|
+
sealedNetwork();
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
seal(Socket.prototype, "connect", sealedNetwork);
|
|
831
|
+
seal(Server.prototype, "listen", sealedNetwork);
|
|
832
|
+
seal(Socket$1.prototype, "bind", sealedNetwork);
|
|
833
|
+
seal(Socket$1.prototype, "connect", sealedNetwork);
|
|
834
|
+
seal(Socket$1.prototype, "send", sealedNetwork);
|
|
835
|
+
}
|
|
836
|
+
/** Node's names for an open TCP or UDP socket, a listener or a resolver request; stdio pipes are not among them. */
|
|
837
|
+
const NETWORK_RESOURCE = /^(?:TCP|UDP|PipeServer|GetAddrInfo|GetNameInfo|Query|TLS)/;
|
|
838
|
+
function networkResourcesRemain(resources) {
|
|
839
|
+
return resources.some((resource) => NETWORK_RESOURCE.test(resource));
|
|
840
|
+
}
|
|
841
|
+
/** The worker reports its proof only once no socket, listener or lookup it may have opened is still alive. */
|
|
842
|
+
async function assertNoProbeSocketsRemain(resources = () => process.getActiveResourcesInfo()) {
|
|
843
|
+
const deadline = Date.now() + PROBE_SOCKET_RELEASE_MS;
|
|
844
|
+
while (networkResourcesRemain(resources())) {
|
|
845
|
+
if (Date.now() >= deadline) throw new Error("a Laya worker network resource outlived its probe");
|
|
846
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
//#endregion
|
|
850
|
+
export { layaTemplate as _, createLayaWorkerBoundary as a, WORKER_FRAME_MAX_BYTES as c, encodeWorkerFrame as d, writeWorkerFrame as f, layaLayout as g, isLayaTemplateId as h, assertWorkerLoadBinding as i, WorkerFrameParser as l, LAYA_RUNNABLE_TEMPLATE_IDS as m, LAYA_SANDBOX_PROFILE as n, runWorkerBoundaryProbe as o, LAYA_NOUL_TEMPLATE_ID as p, assertNoProbeSocketsRemain as r, sealWorkerNetworkAfterProbe as s, LAYA_SANDBOX_EXECUTABLE as t, createWorkerRequestId as u, installErrorSink as v };
|