@volter/tabnode 0.5.48 → 0.5.50
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/assets/{runtime-worker-B-BP9mj-.js → runtime-worker-Cu2mr9G9.js} +1253 -89
- package/dist/fetch-transport.d.ts +0 -1
- package/dist/index.mjs +1250 -100
- package/dist/node-body.d.ts +9 -0
- package/dist/shims/crypto-cipher.d.ts +203 -0
- package/dist/shims/crypto.d.ts +465 -1
- package/package.json +2 -1
- package/src/fetch-transport.ts +7 -3
- package/src/node-body.ts +88 -0
- package/src/node-response.ts +5 -2
- package/src/runtime.ts +3 -0
- package/src/shims/child_process.ts +49 -6
- package/src/shims/crypto-cipher.ts +354 -0
- package/src/shims/crypto.ts +27 -2
- package/src/shims/shell-jobs.ts +72 -0
package/src/runtime.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { VirtualFS } from './virtual-fs';
|
|
|
9
9
|
import { guestPromise, intrinsicPromise } from './promise-ownership';
|
|
10
10
|
import { guestFetch, rememberRequestBodySource } from './fetch-transport';
|
|
11
11
|
import { installNodeResponse } from './node-response';
|
|
12
|
+
import { withNodeRequestBody } from './node-body';
|
|
12
13
|
import { forGuestRealm, installGuestRealm, takeFromHost, defineOnHost, heldWork } from './host-globals';
|
|
13
14
|
import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
|
|
14
15
|
import type { PackageJson } from './types/package-json';
|
|
@@ -2953,6 +2954,8 @@ forGuestRealm(() => {
|
|
|
2953
2954
|
const kept = new WeakMap<object, Headers>();
|
|
2954
2955
|
class NodeRequest extends Native {
|
|
2955
2956
|
constructor(input: RequestInfo | URL, init?: RequestInit) {
|
|
2957
|
+
// An async-iterable body (a Node stream) is read as undici reads it.
|
|
2958
|
+
init = withNodeRequestBody(init);
|
|
2956
2959
|
super(input as RequestInfo, init);
|
|
2957
2960
|
rememberRequestBodySource(this, input, init);
|
|
2958
2961
|
const asked = new Headers(init?.headers ?? (input instanceof Native ? (input as Request).headers : undefined));
|
|
@@ -268,6 +268,22 @@ export function registerRunStreams(token: ProcessToken, streams: RunStreams): vo
|
|
|
268
268
|
/** Forget a run that has ended. */
|
|
269
269
|
export function releaseRunStreams(token: ProcessToken): void {
|
|
270
270
|
_runStreams.delete(token);
|
|
271
|
+
shellRuns.delete(token);
|
|
272
|
+
hostedNodes.delete(token);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Runs whose program is a shell or another non-node program: a `node` they run is their child. */
|
|
276
|
+
const shellRuns = new Set<ProcessToken>();
|
|
277
|
+
/** Runs that are themselves a hosted `node`: a further `node` under them is a child. */
|
|
278
|
+
const hostedNodes = new Set<ProcessToken>();
|
|
279
|
+
|
|
280
|
+
/** A child identity for a `node` a run's shell runs: its own token and pid under the run's, on the run's streams. */
|
|
281
|
+
function nestedNodeToken(parentToken: ProcessToken, args: readonly string[], cwd: string | undefined): ProcessToken {
|
|
282
|
+
const token: ProcessToken = `child-${__nextChildRun++}`;
|
|
283
|
+
setRunPid(token, mintPid(), runPid(parentToken)?.pid ?? 0, { argv: ['node', ...args], ...(cwd ? { cwd } : {}) });
|
|
284
|
+
const parentStreams = runStreamsFor(parentToken);
|
|
285
|
+
if (parentStreams) registerRunStreams(token, parentStreams);
|
|
286
|
+
return token;
|
|
271
287
|
}
|
|
272
288
|
|
|
273
289
|
/** What the host gave the named run, while it lasts. */
|
|
@@ -429,14 +445,30 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
429
445
|
|
|
430
446
|
// Capture this run's name, streams and cancellation before another Node
|
|
431
447
|
// entry can start. Forks and shell entries reach this same dispatch seam.
|
|
432
|
-
const
|
|
448
|
+
const parentToken = runTokenOf(ctx);
|
|
449
|
+
// A `node` a shell runs is a process of its own unless it is the run
|
|
450
|
+
// itself: the first `node` of a run that is not a shell's child is the
|
|
451
|
+
// run (the CMD `node server.js`, the page's `node -e`), and every other
|
|
452
|
+
// one, a line of a start script, the second command of a `-c` line, a
|
|
453
|
+
// step of a `sh file` child, gets a child token under the run's pid,
|
|
454
|
+
// the run's streams, and its own admission at the host. Admitted under
|
|
455
|
+
// the shell's own token, its exit was read as the shell's: a script
|
|
456
|
+
// ended at its first `node` line with nothing after it (measured
|
|
457
|
+
// 2026-09-28: `sh start.sh` printed its first step and stopped).
|
|
458
|
+
const nested = parentToken !== null && (shellRuns.has(parentToken) || hostedNodes.has(parentToken));
|
|
459
|
+
const runToken = nested ? nestedNodeToken(parentToken!, args, ctx.cwd) : parentToken;
|
|
433
460
|
const streams = runToken === null ? undefined : runStreamsFor(runToken);
|
|
434
461
|
const processHost = nodeProcessHostFor(runToken);
|
|
435
462
|
if (processHost && runToken !== null) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
463
|
+
if (!nested) hostedNodes.add(runToken);
|
|
464
|
+
try {
|
|
465
|
+
return await runHostedNode(processHost, {
|
|
466
|
+
token: runToken, argv: [...args], cwd: ctx.cwd, filesystem: tree, env: guestEnvironmentOf(ctx),
|
|
467
|
+
...(typeof ctx.stdin === 'string' ? { stdin: ctx.stdin } : {}), ...(streams ? { streams } : {}),
|
|
468
|
+
});
|
|
469
|
+
} finally {
|
|
470
|
+
if (nested) releaseRunStreams(runToken);
|
|
471
|
+
}
|
|
440
472
|
}
|
|
441
473
|
|
|
442
474
|
// Node reads its own options before the script: `node --turbo-fast-api-calls
|
|
@@ -1719,6 +1751,9 @@ function existsInTree(path: string): boolean {
|
|
|
1719
1751
|
* a program the engine can run; the engine's own `node`, written to
|
|
1720
1752
|
* `/usr/local/bin/node` when the shim is initialized, is one of those.
|
|
1721
1753
|
*/
|
|
1754
|
+
/** The shells a script or a `-c` line runs under; a child of these goes to the host when there is one. */
|
|
1755
|
+
const SHELL_PROGRAMS = new Set(['sh', 'bash', 'dash', '/bin/sh', '/bin/bash', '/usr/bin/sh', '/usr/bin/bash']);
|
|
1756
|
+
|
|
1722
1757
|
function engineProgramFor(file: string, cwd?: string): string {
|
|
1723
1758
|
const name = __substrateProgramName(file);
|
|
1724
1759
|
if (!name.includes('/')) return name;
|
|
@@ -1794,6 +1829,7 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1794
1829
|
// A host terminal consumes input incrementally. The old string-only
|
|
1795
1830
|
// route dropped every keystroke that arrived after a child was launched.
|
|
1796
1831
|
const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
|
|
1832
|
+
if (!admittedNode) shellRuns.add(token);
|
|
1797
1833
|
const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
|
|
1798
1834
|
let wakeInput: (() => void) | undefined;
|
|
1799
1835
|
/**
|
|
@@ -1904,7 +1940,14 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1904
1940
|
const begin = (): void => {
|
|
1905
1941
|
if (started || finished) return;
|
|
1906
1942
|
started = true;
|
|
1907
|
-
|
|
1943
|
+
// A shell a hosted program spawns (`sh start.sh`, `sh -c '…'`) runs at
|
|
1944
|
+
// the host when there is one, not in this worker's own bash: the host is
|
|
1945
|
+
// where the programs the page registered live (`npx` under /usr/bin),
|
|
1946
|
+
// and where a `node` inside the script is given a named run. Run here,
|
|
1947
|
+
// the script's `node` found no name and was refused, and its `npx` was
|
|
1948
|
+
// not found at all.
|
|
1949
|
+
const shellChild = SHELL_PROGRAMS.has(engineProgramFor(request.file, request.cwd)) && hostExecutor() !== null;
|
|
1950
|
+
const engineFirst = !hostTerminal && !shellChild && programExists(request.file, request.cwd, request.env);
|
|
1908
1951
|
liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
|
|
1909
1952
|
if (liveInput) {
|
|
1910
1953
|
pendingStdin.unshift(...initialStdin);
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node's `createCipheriv` and `createDecipheriv` for AES in GCM, CBC, CTR and
|
|
3
|
+
* ECB modes. A guest calls them synchronously and WebCrypto is async-only, so
|
|
4
|
+
* the modes are built here over one synchronous AES block (`@noble/ciphers`,
|
|
5
|
+
* pinned exactly: its single-block helpers are the library's unsafe surface)
|
|
6
|
+
* and GCM's GHASH. Every mode streams as OpenSSL does: `update` answers the
|
|
7
|
+
* bytes it can, `final` the rest, so a caller that reads `update` alone for a
|
|
8
|
+
* stream mode gets its ciphertext (jose's GCM encrypt: `const ciphertext =
|
|
9
|
+
* cipher.update(plaintext); cipher.final();`, which is how NextAuth seals its
|
|
10
|
+
* session cookie).
|
|
11
|
+
*
|
|
12
|
+
* Not here: the ciphers are not `stream.Transform`s (only `update`/`final`),
|
|
13
|
+
* and no mode but these four, nor ChaCha20-Poly1305, is offered.
|
|
14
|
+
*/
|
|
15
|
+
import { unsafe } from '@noble/ciphers/aes.js';
|
|
16
|
+
import { GHASH } from '@noble/ciphers/_polyval.js';
|
|
17
|
+
|
|
18
|
+
type Mode = 'gcm' | 'cbc' | 'ctr' | 'ecb';
|
|
19
|
+
type Bytes = Uint8Array;
|
|
20
|
+
|
|
21
|
+
export interface CipherDeps {
|
|
22
|
+
/** The guest's Buffer.from over bytes. */
|
|
23
|
+
toBuffer(bytes: Bytes): Uint8Array;
|
|
24
|
+
/** Bytes of a string (in its encoding) or a view, as the rest of crypto reads them. */
|
|
25
|
+
bytesOf(value: unknown, encoding?: string): Bytes;
|
|
26
|
+
/** A secret KeyObject's bytes, or undefined when the value is not a KeyObject. */
|
|
27
|
+
secretKeyBytes(value: unknown): Bytes | undefined;
|
|
28
|
+
/** Bytes in an output encoding, as `digest` answers them. */
|
|
29
|
+
encode(bytes: Bytes, encoding?: string): unknown;
|
|
30
|
+
error(code: string, message: string): Error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CIPHERS = /^aes-(128|192|256)-(gcm|cbc|ctr|ecb)$/u;
|
|
34
|
+
|
|
35
|
+
/** The ciphers `createCipheriv` accepts, as `getCiphers` lists them. */
|
|
36
|
+
export function cipherNames(): string[] {
|
|
37
|
+
const names: string[] = [];
|
|
38
|
+
for (const bits of [128, 192, 256]) for (const mode of ['cbc', 'ctr', 'ecb', 'gcm']) names.push(`aes-${bits}-${mode}`);
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const GCM_TAG_LENGTHS = new Set([4, 8, 12, 13, 14, 15, 16]);
|
|
43
|
+
|
|
44
|
+
export function createCipherClasses(deps: CipherDeps) {
|
|
45
|
+
const { error } = deps;
|
|
46
|
+
|
|
47
|
+
function parse(algorithm: unknown): { keyLength: number; mode: Mode } {
|
|
48
|
+
if (typeof algorithm !== 'string') throw Object.assign(new TypeError('The "cipher" argument must be of type string.'), { code: 'ERR_INVALID_ARG_TYPE' });
|
|
49
|
+
const match = CIPHERS.exec(algorithm.toLowerCase());
|
|
50
|
+
if (!match) throw error('ERR_CRYPTO_UNKNOWN_CIPHER', 'Unknown cipher');
|
|
51
|
+
return { keyLength: Number(match[1]) / 8, mode: match[2] as Mode };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function keyOf(key: unknown, keyLength: number): Bytes {
|
|
55
|
+
const bytes = deps.secretKeyBytes(key) ?? deps.bytesOf(key);
|
|
56
|
+
if (bytes.length !== keyLength) throw Object.assign(new RangeError('Invalid key length'), { code: 'ERR_CRYPTO_INVALID_KEYLEN' });
|
|
57
|
+
return new Uint8Array(bytes);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ivOf(iv: unknown, mode: Mode): Bytes | undefined {
|
|
61
|
+
if (mode === 'ecb') {
|
|
62
|
+
if (iv === null || iv === undefined) return undefined;
|
|
63
|
+
const bytes = deps.bytesOf(iv);
|
|
64
|
+
if (bytes.length !== 0) throw Object.assign(new TypeError('Invalid initialization vector'), { code: 'ERR_CRYPTO_INVALID_IV' });
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (iv === null || iv === undefined) throw Object.assign(new TypeError('Invalid initialization vector'), { code: 'ERR_CRYPTO_INVALID_IV' });
|
|
68
|
+
const bytes = new Uint8Array(deps.bytesOf(iv));
|
|
69
|
+
if (mode === 'gcm' ? bytes.length === 0 : bytes.length !== 16) throw Object.assign(new TypeError('Invalid initialization vector'), { code: 'ERR_CRYPTO_INVALID_IV' });
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One AES block, encrypted in place in a fresh aligned array. */
|
|
74
|
+
function encryptBlock(xk: Uint32Array, block: Bytes): Bytes {
|
|
75
|
+
const out = new Uint8Array(16);
|
|
76
|
+
out.set(block);
|
|
77
|
+
return unsafe.encryptBlock(xk, out) as Bytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decryptBlock(xk: Uint32Array, block: Bytes): Bytes {
|
|
81
|
+
const out = new Uint8Array(16);
|
|
82
|
+
out.set(block);
|
|
83
|
+
return unsafe.decryptBlock(xk, out) as Bytes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function concat(a: Bytes, b: Bytes): Bytes {
|
|
87
|
+
if (a.length === 0) return b;
|
|
88
|
+
if (b.length === 0) return a;
|
|
89
|
+
const out = new Uint8Array(a.length + b.length);
|
|
90
|
+
out.set(a);
|
|
91
|
+
out.set(b, a.length);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A 128-bit big-endian counter, or its low 32 bits alone (GCM's inc32). */
|
|
96
|
+
function increment(counter: Bytes, from: number): void {
|
|
97
|
+
for (let i = 15; i >= from; i--) {
|
|
98
|
+
counter[i] = (counter[i]! + 1) & 0xff;
|
|
99
|
+
if (counter[i] !== 0) return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The subclasses' doors to the tag, which the base keeps private (as KeyObject's keyInfoOf reads its key).
|
|
104
|
+
let authTagOf: (cipher: CipherBase) => Bytes;
|
|
105
|
+
let expectTagOf: (cipher: CipherBase, tag: unknown, encoding?: string) => void;
|
|
106
|
+
|
|
107
|
+
class CipherBase {
|
|
108
|
+
static {
|
|
109
|
+
authTagOf = (cipher) => cipher.#getTag();
|
|
110
|
+
expectTagOf = (cipher, tag, encoding) => cipher.#setTag(tag, encoding);
|
|
111
|
+
}
|
|
112
|
+
readonly #decrypt: boolean;
|
|
113
|
+
readonly #mode: Mode;
|
|
114
|
+
readonly #xk: Uint32Array;
|
|
115
|
+
readonly #xkDec: Uint32Array | undefined;
|
|
116
|
+
#padding = true;
|
|
117
|
+
#finished = false;
|
|
118
|
+
#started = false;
|
|
119
|
+
/** CBC/ECB: bytes not yet a whole block (and, decrypting with padding, the last whole block). */
|
|
120
|
+
#pending: Bytes = new Uint8Array(0);
|
|
121
|
+
/** CBC's chaining block. */
|
|
122
|
+
#chain: Bytes | undefined;
|
|
123
|
+
/** CTR/GCM: the next counter block and what is left of the current keystream block. */
|
|
124
|
+
#counter: Bytes | undefined;
|
|
125
|
+
#keystream: Bytes = new Uint8Array(0);
|
|
126
|
+
/** GCM. */
|
|
127
|
+
#ghash: GHASH | undefined;
|
|
128
|
+
#tagMask: Bytes | undefined;
|
|
129
|
+
#ghashPending: Bytes = new Uint8Array(0);
|
|
130
|
+
#aadLength = 0;
|
|
131
|
+
#dataLength = 0;
|
|
132
|
+
#authTagLength: number | undefined;
|
|
133
|
+
#authTag: Bytes | undefined;
|
|
134
|
+
|
|
135
|
+
constructor(decrypt: boolean, algorithm: unknown, key: unknown, iv: unknown, options?: { authTagLength?: number }) {
|
|
136
|
+
const { keyLength, mode } = parse(algorithm);
|
|
137
|
+
this.#decrypt = decrypt;
|
|
138
|
+
this.#mode = mode;
|
|
139
|
+
const keyBytes = keyOf(key, keyLength);
|
|
140
|
+
const ivBytes = ivOf(iv, mode);
|
|
141
|
+
this.#xk = unsafe.expandKeyLE(keyBytes) as Uint32Array;
|
|
142
|
+
this.#xkDec = decrypt && (mode === 'cbc' || mode === 'ecb') ? unsafe.expandKeyDecLE(keyBytes) as Uint32Array : undefined;
|
|
143
|
+
const tagLength = options?.authTagLength;
|
|
144
|
+
if (tagLength !== undefined) {
|
|
145
|
+
if (mode !== 'gcm') throw Object.assign(new TypeError(`Invalid authentication tag length: ${tagLength}`), { code: 'ERR_CRYPTO_INVALID_AUTH_TAG' });
|
|
146
|
+
if (!GCM_TAG_LENGTHS.has(tagLength)) throw Object.assign(new TypeError(`Invalid authentication tag length: ${tagLength}`), { code: 'ERR_CRYPTO_INVALID_AUTH_TAG' });
|
|
147
|
+
this.#authTagLength = tagLength;
|
|
148
|
+
}
|
|
149
|
+
if (mode === 'cbc') this.#chain = ivBytes;
|
|
150
|
+
if (mode === 'ctr') this.#counter = new Uint8Array(ivBytes!);
|
|
151
|
+
if (mode === 'gcm') {
|
|
152
|
+
// NIST SP 800-38D: H = E_K(0), J0 from a 96-bit IV or GHASH of any other, the tag mask E_K(J0), and the
|
|
153
|
+
// payload counted from inc32(J0).
|
|
154
|
+
const h = encryptBlock(this.#xk, new Uint8Array(16));
|
|
155
|
+
let j0: Bytes;
|
|
156
|
+
if (ivBytes!.length === 12) {
|
|
157
|
+
j0 = new Uint8Array(16);
|
|
158
|
+
j0.set(ivBytes!);
|
|
159
|
+
j0[15] = 1;
|
|
160
|
+
} else {
|
|
161
|
+
const lengths = new Uint8Array(16);
|
|
162
|
+
new DataView(lengths.buffer).setBigUint64(8, BigInt(ivBytes!.length * 8), false);
|
|
163
|
+
const g = new GHASH(h);
|
|
164
|
+
g.update(ivBytes!).update(lengths);
|
|
165
|
+
j0 = g.digest() as Bytes;
|
|
166
|
+
}
|
|
167
|
+
this.#tagMask = encryptBlock(this.#xk, j0);
|
|
168
|
+
this.#counter = new Uint8Array(j0);
|
|
169
|
+
increment(this.#counter, 12);
|
|
170
|
+
this.#ghash = new GHASH(h);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setAutoPadding(autoPadding = true): this {
|
|
175
|
+
if (this.#finished) throw error('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation setAutoPadding');
|
|
176
|
+
this.#padding = Boolean(autoPadding);
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setAAD(buffer: unknown, _options?: { plaintextLength?: number }): this {
|
|
181
|
+
if (this.#mode !== 'gcm' || this.#started || this.#finished) throw error('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation setAAD');
|
|
182
|
+
const aad = deps.bytesOf(buffer);
|
|
183
|
+
// GHASH pads each update to a block; AAD is padded once, as GCM pads it, whatever its length.
|
|
184
|
+
this.#ghash!.update(aad);
|
|
185
|
+
this.#aadLength += aad.length;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#getTag(): Bytes {
|
|
190
|
+
if (this.#mode !== 'gcm' || this.#decrypt || !this.#finished || !this.#authTag) throw error('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation getAuthTag');
|
|
191
|
+
return this.#authTag;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#setTag(tag: unknown, encoding?: string): void {
|
|
195
|
+
if (this.#mode !== 'gcm' || !this.#decrypt || this.#finished) throw error('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation setAuthTag');
|
|
196
|
+
const bytes = new Uint8Array(typeof tag === 'string' ? deps.bytesOf(tag, encoding) : deps.bytesOf(tag));
|
|
197
|
+
const allowed = this.#authTagLength === undefined ? GCM_TAG_LENGTHS.has(bytes.length) : bytes.length === this.#authTagLength;
|
|
198
|
+
if (!allowed) throw Object.assign(new TypeError(`Invalid authentication tag length: ${bytes.length}`), { code: 'ERR_CRYPTO_INVALID_AUTH_TAG' });
|
|
199
|
+
this.#authTag = bytes;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
update(data: unknown, inputEncoding?: string, outputEncoding?: string): unknown {
|
|
203
|
+
if (this.#finished) throw error('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation update');
|
|
204
|
+
this.#started = true;
|
|
205
|
+
const input = typeof data === 'string' ? deps.bytesOf(data, inputEncoding) : deps.bytesOf(data);
|
|
206
|
+
return deps.encode(this.#process(new Uint8Array(input)), outputEncoding);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
final(outputEncoding?: string): unknown {
|
|
210
|
+
if (this.#finished) throw error('ERR_CRYPTO_INVALID_STATE', 'Unsupported state');
|
|
211
|
+
this.#finished = true;
|
|
212
|
+
return deps.encode(this.#finish(), outputEncoding);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#process(input: Bytes): Bytes {
|
|
216
|
+
switch (this.#mode) {
|
|
217
|
+
case 'ctr': return this.#stream(input);
|
|
218
|
+
case 'gcm': {
|
|
219
|
+
if (this.#decrypt) this.#absorb(input);
|
|
220
|
+
const out = this.#stream(input);
|
|
221
|
+
if (!this.#decrypt) this.#absorb(out);
|
|
222
|
+
this.#dataLength += input.length;
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
default: return this.#blocks(input);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** XOR with the counter's keystream, carrying a partial block across calls. */
|
|
230
|
+
#stream(input: Bytes): Bytes {
|
|
231
|
+
const out = new Uint8Array(input.length);
|
|
232
|
+
let at = 0;
|
|
233
|
+
while (at < input.length) {
|
|
234
|
+
if (this.#keystream.length === 0) {
|
|
235
|
+
this.#keystream = encryptBlock(this.#xk, this.#counter!);
|
|
236
|
+
increment(this.#counter!, this.#mode === 'gcm' ? 12 : 0);
|
|
237
|
+
}
|
|
238
|
+
const take = Math.min(this.#keystream.length, input.length - at);
|
|
239
|
+
for (let i = 0; i < take; i++) out[at + i] = input[at + i]! ^ this.#keystream[i]!;
|
|
240
|
+
this.#keystream = this.#keystream.subarray(take);
|
|
241
|
+
at += take;
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** GHASH over the ciphertext, in whole blocks: GHASH pads each update, so a partial block waits for the next. */
|
|
247
|
+
#absorb(ciphertext: Bytes): void {
|
|
248
|
+
const all = concat(this.#ghashPending, ciphertext);
|
|
249
|
+
const whole = all.length - (all.length % 16);
|
|
250
|
+
if (whole > 0) this.#ghash!.update(all.subarray(0, whole));
|
|
251
|
+
this.#ghashPending = all.slice(whole);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** CBC and ECB: every whole block now; decrypting with padding, the last whole block waits for `final`. */
|
|
255
|
+
#blocks(input: Bytes): Bytes {
|
|
256
|
+
const all = concat(this.#pending, input);
|
|
257
|
+
let whole = all.length - (all.length % 16);
|
|
258
|
+
if (this.#decrypt && this.#padding && whole === all.length && whole > 0) whole -= 16;
|
|
259
|
+
const out = new Uint8Array(whole);
|
|
260
|
+
for (let at = 0; at < whole; at += 16) out.set(this.#block(all.subarray(at, at + 16)), at);
|
|
261
|
+
this.#pending = all.slice(whole);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#block(block: Bytes): Bytes {
|
|
266
|
+
if (this.#mode === 'ecb') return this.#decrypt ? decryptBlock(this.#xkDec!, block) : encryptBlock(this.#xk, block);
|
|
267
|
+
if (this.#decrypt) {
|
|
268
|
+
const plain = decryptBlock(this.#xkDec!, block);
|
|
269
|
+
for (let i = 0; i < 16; i++) plain[i] = plain[i]! ^ this.#chain![i]!;
|
|
270
|
+
this.#chain = new Uint8Array(block);
|
|
271
|
+
return plain;
|
|
272
|
+
}
|
|
273
|
+
const mixed = new Uint8Array(16);
|
|
274
|
+
for (let i = 0; i < 16; i++) mixed[i] = block[i]! ^ this.#chain![i]!;
|
|
275
|
+
const cipher = encryptBlock(this.#xk, mixed);
|
|
276
|
+
this.#chain = cipher;
|
|
277
|
+
return cipher;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#finish(): Bytes {
|
|
281
|
+
if (this.#mode === 'ctr') return new Uint8Array(0);
|
|
282
|
+
if (this.#mode === 'gcm') return this.#finishGcm();
|
|
283
|
+
const rest = this.#pending;
|
|
284
|
+
this.#pending = new Uint8Array(0);
|
|
285
|
+
if (!this.#decrypt) {
|
|
286
|
+
if (!this.#padding) {
|
|
287
|
+
if (rest.length !== 0) throw error('ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH', 'error:1C80006B:Provider routines::wrong final block length');
|
|
288
|
+
return rest;
|
|
289
|
+
}
|
|
290
|
+
const pad = 16 - rest.length;
|
|
291
|
+
const block = new Uint8Array(16);
|
|
292
|
+
block.set(rest);
|
|
293
|
+
block.fill(pad, rest.length);
|
|
294
|
+
return this.#block(block);
|
|
295
|
+
}
|
|
296
|
+
if (!this.#padding) {
|
|
297
|
+
if (rest.length !== 0) throw error('ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH', 'error:1C80006B:Provider routines::wrong final block length');
|
|
298
|
+
return rest;
|
|
299
|
+
}
|
|
300
|
+
if (rest.length !== 16) throw error('ERR_OSSL_WRONG_FINAL_BLOCK_LENGTH', 'error:1C80006B:Provider routines::wrong final block length');
|
|
301
|
+
const plain = this.#block(rest);
|
|
302
|
+
const pad = plain[15]!;
|
|
303
|
+
let valid = pad >= 1 && pad <= 16;
|
|
304
|
+
for (let i = 16 - pad; valid && i < 16; i++) valid = plain[i] === pad;
|
|
305
|
+
if (!valid) throw error('ERR_OSSL_BAD_DECRYPT', 'error:1C800064:Provider routines::bad decrypt');
|
|
306
|
+
return plain.slice(0, 16 - pad);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#finishGcm(): Bytes {
|
|
310
|
+
if (this.#ghashPending.length) this.#ghash!.update(this.#ghashPending);
|
|
311
|
+
const lengths = new Uint8Array(16);
|
|
312
|
+
const view = new DataView(lengths.buffer);
|
|
313
|
+
view.setBigUint64(0, BigInt(this.#aadLength * 8), false);
|
|
314
|
+
view.setBigUint64(8, BigInt(this.#dataLength * 8), false);
|
|
315
|
+
const full = this.#ghash!.update(lengths).digest() as Bytes;
|
|
316
|
+
for (let i = 0; i < 16; i++) full[i] = full[i]! ^ this.#tagMask![i]!;
|
|
317
|
+
if (!this.#decrypt) {
|
|
318
|
+
this.#authTag = full.slice(0, this.#authTagLength ?? 16);
|
|
319
|
+
return new Uint8Array(0);
|
|
320
|
+
}
|
|
321
|
+
const expected = this.#authTag;
|
|
322
|
+
let same = expected !== undefined;
|
|
323
|
+
if (expected) for (let i = 0; i < expected.length; i++) same = same && full[i] === expected[i];
|
|
324
|
+
if (!same) throw new Error('Unsupported state or unable to authenticate data');
|
|
325
|
+
return new Uint8Array(0);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
class Cipheriv extends CipherBase {
|
|
330
|
+
constructor(algorithm: unknown, key: unknown, iv: unknown, options?: { authTagLength?: number }) {
|
|
331
|
+
super(false, algorithm, key, iv, options);
|
|
332
|
+
}
|
|
333
|
+
getAuthTag(): Uint8Array {
|
|
334
|
+
return deps.toBuffer(authTagOf(this));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
class Decipheriv extends CipherBase {
|
|
339
|
+
constructor(algorithm: unknown, key: unknown, iv: unknown, options?: { authTagLength?: number }) {
|
|
340
|
+
super(true, algorithm, key, iv, options);
|
|
341
|
+
}
|
|
342
|
+
setAuthTag(tag: unknown, encoding?: string): this {
|
|
343
|
+
expectTagOf(this, tag, encoding);
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
Cipheriv,
|
|
350
|
+
Decipheriv,
|
|
351
|
+
createCipheriv: (algorithm: unknown, key: unknown, iv: unknown, options?: { authTagLength?: number }) => new Cipheriv(algorithm, key, iv, options),
|
|
352
|
+
createDecipheriv: (algorithm: unknown, key: unknown, iv: unknown, options?: { authTagLength?: number }) => new Decipheriv(algorithm, key, iv, options),
|
|
353
|
+
};
|
|
354
|
+
}
|
package/src/shims/crypto.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
validateNumber,
|
|
22
22
|
} from '../node-internals';
|
|
23
23
|
import { cryptoConstants as constants } from './crypto-constants';
|
|
24
|
+
import { cipherNames, createCipherClasses } from './crypto-cipher';
|
|
24
25
|
export { constants };
|
|
25
26
|
|
|
26
27
|
interface DigestState {
|
|
@@ -911,7 +912,8 @@ function timingSafeEqual(a: Buffer | Uint8Array, b: Buffer | Uint8Array): boolea
|
|
|
911
912
|
}
|
|
912
913
|
|
|
913
914
|
function getCiphers(): string[] {
|
|
914
|
-
|
|
915
|
+
// What createCipheriv accepts, no more, as getHashes is createHash's.
|
|
916
|
+
return cipherNames();
|
|
915
917
|
}
|
|
916
918
|
|
|
917
919
|
function getHashes(): string[] {
|
|
@@ -1133,11 +1135,34 @@ async function importKey(
|
|
|
1133
1135
|
throw new Error(`Unsupported key format: ${keyInfo.format}`);
|
|
1134
1136
|
}
|
|
1135
1137
|
|
|
1138
|
+
// ============================================================================
|
|
1139
|
+
// Ciphers
|
|
1140
|
+
// ============================================================================
|
|
1141
|
+
|
|
1142
|
+
const { Cipheriv, Decipheriv, createCipheriv, createDecipheriv } = createCipherClasses({
|
|
1143
|
+
toBuffer: (bytes) => Buffer.from(bytes),
|
|
1144
|
+
bytesOf: (value, encoding) => cryptoBytes(value, encoding, true),
|
|
1145
|
+
secretKeyBytes: (value) => {
|
|
1146
|
+
if (!(value instanceof KeyObject)) return undefined;
|
|
1147
|
+
const info = keyInfoOf(value);
|
|
1148
|
+
if (info.type !== 'secret' || !(info.keyData instanceof Uint8Array)) {
|
|
1149
|
+
throw Object.assign(new TypeError(`Invalid key object type ${info.type}, expected secret.`), { code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE' });
|
|
1150
|
+
}
|
|
1151
|
+
return info.keyData;
|
|
1152
|
+
},
|
|
1153
|
+
encode: (bytes, encoding) => digestResult(bytes, encoding),
|
|
1154
|
+
error: cryptoError,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1136
1157
|
// ============================================================================
|
|
1137
1158
|
// Exports
|
|
1138
1159
|
// ============================================================================
|
|
1139
1160
|
|
|
1140
1161
|
return {
|
|
1162
|
+
Cipheriv,
|
|
1163
|
+
Decipheriv,
|
|
1164
|
+
createCipheriv,
|
|
1165
|
+
createDecipheriv,
|
|
1141
1166
|
unsupported,
|
|
1142
1167
|
randomBytes,
|
|
1143
1168
|
randomFill,
|
|
@@ -1167,6 +1192,6 @@ return {
|
|
|
1167
1192
|
|
|
1168
1193
|
}
|
|
1169
1194
|
const cryptoModule = createCryptoModule();
|
|
1170
|
-
export const { randomBytes, randomFillSync, randomFill, randomUUID, randomInt, getRandomValues, unsupported, createHash, createHmac, hash, pbkdf2Sync, pbkdf2, sign, verify, createSign, createVerify, KeyObject, createSecretKey, createPublicKey, createPrivateKey, generateKeyPair, timingSafeEqual, getCiphers, getHashes } = cryptoModule;
|
|
1195
|
+
export const { Cipheriv, Decipheriv, createCipheriv, createDecipheriv, randomBytes, randomFillSync, randomFill, randomUUID, randomInt, getRandomValues, unsupported, createHash, createHmac, hash, pbkdf2Sync, pbkdf2, sign, verify, createSign, createVerify, KeyObject, createSecretKey, createPublicKey, createPrivateKey, generateKeyPair, timingSafeEqual, getCiphers, getHashes } = cryptoModule;
|
|
1171
1196
|
export type KeyObject = InstanceType<typeof KeyObject>;
|
|
1172
1197
|
export default cryptoModule;
|
package/src/shims/shell-jobs.ts
CHANGED
|
@@ -386,6 +386,78 @@ export function installShellJobs(bash: Bash, host: ShellJobsHost): void {
|
|
|
386
386
|
return { stdout: held.stdout, stderr: held.stderr + stderr, exitCode: status };
|
|
387
387
|
}));
|
|
388
388
|
|
|
389
|
+
// `sh file`, `sh -c line` and `sh` on stdin: the shell's own ran the script
|
|
390
|
+
// with the caller's exported variables alone, and a run's name travels in
|
|
391
|
+
// a variable the shell never exports, so a `node` on any line of a script
|
|
392
|
+
// found no named run and was refused ("Isolated Node execution requires a
|
|
393
|
+
// named run", a start script in browser-substrate's tab, 2026-09-28). The
|
|
394
|
+
// script is run as the shell's own would run it, with the name carried.
|
|
395
|
+
for (const shell of ['sh', 'bash'] as const) {
|
|
396
|
+
bash.registerCommand(defineCommand(shell, async (args, ctx) => {
|
|
397
|
+
const exec = ctx.exec;
|
|
398
|
+
if (!exec) return { stdout: '', stderr: `bash: ${shell}: needs a shell that can run a script\n`, exitCode: 1 };
|
|
399
|
+
let script: string;
|
|
400
|
+
let zero: string;
|
|
401
|
+
let positional: string[];
|
|
402
|
+
let sourced: string | undefined;
|
|
403
|
+
// Options before the script (`sh -e start.sh`, `bash -eu -c …`) are the
|
|
404
|
+
// shell's own; they are passed over rather than read as the file's name.
|
|
405
|
+
let at = 0;
|
|
406
|
+
while (at < args.length && /^[-+][a-bd-zA-Z]+$/u.test(args[at]!)) at++;
|
|
407
|
+
args = args.slice(at);
|
|
408
|
+
if (args[0] === '-c' && args.length >= 2) {
|
|
409
|
+
script = args[1]!;
|
|
410
|
+
zero = args[2] ?? shell;
|
|
411
|
+
positional = args.slice(3);
|
|
412
|
+
} else if (args.length === 0) {
|
|
413
|
+
if (!ctx.stdin?.trim()) return { stdout: '', stderr: '', exitCode: 0 };
|
|
414
|
+
script = ctx.stdin;
|
|
415
|
+
zero = shell;
|
|
416
|
+
positional = [];
|
|
417
|
+
} else {
|
|
418
|
+
const path = ctx.fs.resolvePath(ctx.cwd, args[0]!);
|
|
419
|
+
try { script = String(await ctx.fs.readFile(path)); }
|
|
420
|
+
catch { return { stdout: '', stderr: `${shell}: ${args[0]}: No such file or directory\n`, exitCode: 127 }; }
|
|
421
|
+
zero = args[0]!;
|
|
422
|
+
positional = args.slice(1);
|
|
423
|
+
sourced = path;
|
|
424
|
+
}
|
|
425
|
+
if (script.startsWith('#!')) {
|
|
426
|
+
const newline = script.indexOf('\n');
|
|
427
|
+
if (newline !== -1) script = script.slice(newline + 1);
|
|
428
|
+
}
|
|
429
|
+
const env: Record<string, string> = { ...(ctx.exportedEnv ?? {}), '0': zero, '#': String(positional.length), '@': positional.join(' '), '*': positional.join(' ') };
|
|
430
|
+
positional.forEach((word, index) => { env[String(index + 1)] = word; });
|
|
431
|
+
const token = ctx.env.get(PROCESS_TOKEN_ENV);
|
|
432
|
+
if (token) env[PROCESS_TOKEN_ENV] = token;
|
|
433
|
+
// A file is sourced by one statement of the nested run rather than run
|
|
434
|
+
// as the nested run's own script: a nested script of several lines
|
|
435
|
+
// stopped after its first hosted `node` in a tab (2026-09-28), while
|
|
436
|
+
// the same lines sourced from one statement ran whole. The shebang
|
|
437
|
+
// line is a comment to the shell that sources it.
|
|
438
|
+
if (sourced !== undefined) script = `. ${sourced.replace(/'/gu, "'\\''").replace(/^(.*)$/su, "'$1'")}`;
|
|
439
|
+
const result = await exec(script, { env, cwd: ctx.cwd, stdin: ctx.stdin, signal: (ctx as { signal?: AbortSignal }).signal });
|
|
440
|
+
return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: result.exitCode ?? 0 };
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
// `exec command…` runs the command in the script's place, with the
|
|
444
|
+
// script's environment and its run's name, and answers with its status.
|
|
445
|
+
// The shell's own `exec` ran a builtin but a program the engine defines,
|
|
446
|
+
// `node`, ran nothing: a start script's `exec node server.js` ended with
|
|
447
|
+
// no server and no word. This one hands the line back to the shell, which
|
|
448
|
+
// is where `node`, `npm` and the host's programs are found. The shell is
|
|
449
|
+
// not replaced: a line after `exec` runs once the command ends, where
|
|
450
|
+
// bash would never reach it. A start script's `exec` is its last line, a
|
|
451
|
+
// server that does not end.
|
|
452
|
+
bash.registerCommand(defineCommand('exec', async (args, ctx) => {
|
|
453
|
+
if (args.length === 0) return { stdout: '', stderr: '', exitCode: 0 };
|
|
454
|
+
if (!ctx.exec) return { stdout: '', stderr: 'bash: exec: needs a shell that can run a line\n', exitCode: 1 };
|
|
455
|
+
const line = args.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/u.test(word) ? word : `'${word.replace(/'/gu, "'\\''")}'`).join(' ');
|
|
456
|
+
const env: Record<string, string> = Object.fromEntries(ctx.env);
|
|
457
|
+
const result = await ctx.exec(line, { env, cwd: ctx.cwd, replaceEnv: true });
|
|
458
|
+
return { stdout: result.stdout ?? '', stderr: result.stderr ?? '', exitCode: result.exitCode ?? 0 };
|
|
459
|
+
}));
|
|
460
|
+
|
|
389
461
|
bash.registerCommand(defineCommand('kill', async (args) => {
|
|
390
462
|
let signal = 'SIGTERM';
|
|
391
463
|
let index = 0;
|