@nimbus-sh/core 0.3.0 → 0.5.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/_shared/error-text.d.ts +15 -0
- package/dist/_shared/error-text.d.ts.map +1 -0
- package/dist/_shared/error-text.js +21 -0
- package/dist/_shared/exports-resolver.d.ts +19 -7
- package/dist/_shared/exports-resolver.d.ts.map +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.d.ts.map +1 -1
- package/dist/_shared/retry.js +3 -1
- package/dist/_shared/tarball-stream.d.ts +93 -0
- package/dist/_shared/tarball-stream.d.ts.map +1 -0
- package/dist/_shared/tarball-stream.js +235 -0
- package/dist/_shared/tarball.d.ts +17 -0
- package/dist/_shared/tarball.d.ts.map +1 -0
- package/dist/_shared/tarball.js +39 -0
- package/dist/_shared/w7-frame.js +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +30 -16
- package/dist/observability/oom-classify.d.ts +1 -1
- package/dist/observability/oom-classify.d.ts.map +1 -1
- package/dist/observability/oom-classify.js +7 -0
- package/dist/observability/oom-discriminator.d.ts +1 -1
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +866 -0
- package/dist/runtime/esbuild-service.d.ts +4 -3
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +9 -4
- package/dist/runtime/facet-host.d.ts +12 -0
- package/dist/runtime/facet-host.d.ts.map +1 -1
- package/dist/runtime/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.d.ts.map +1 -1
- package/dist/runtime/local-facet-host.js +29 -9
- package/dist/runtime/long-running-handle.d.ts +3 -3
- package/dist/runtime/long-running-handle.d.ts.map +1 -1
- package/dist/runtime/os-contracts.d.ts +7 -5
- package/dist/runtime/os-contracts.d.ts.map +1 -1
- package/dist/runtime/port-registry.d.ts +28 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +79 -9
- package/dist/runtime/process-logs.d.ts +9 -1
- package/dist/runtime/process-logs.d.ts.map +1 -1
- package/dist/runtime/project-detect.d.ts +6 -1
- package/dist/runtime/project-detect.d.ts.map +1 -1
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +3 -4
- package/dist/runtime/ruby-gems.d.ts +30 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +636 -0
- package/dist/runtime/ruby-runner.d.ts +127 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +1357 -0
- package/dist/runtime/runtime-registry.d.ts +4 -3
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +2 -1
- package/dist/runtime/session-process-supervisor.d.ts +15 -0
- package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
- package/dist/runtime/session-process-supervisor.js +30 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
- package/dist/runtime/virtual-socket-kernel.js +10 -5
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +22 -14
- package/dist/shell/features.d.ts.map +1 -1
- package/dist/shell/features.js +5 -4
- package/dist/shell/unix-commands.d.ts +14 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +115 -69
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/index.js +3 -2
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/process.js +6 -3
- package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/Shell.js +86 -75
- package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/lexer.js +40 -0
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +8 -2
- package/dist/vfs/sqlite-vfs.d.ts +61 -1
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +199 -35
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +9 -2
- package/package.json +4 -2
- package/src/_shared/error-text.ts +21 -0
- package/src/_shared/exports-resolver.ts +24 -4
- package/src/_shared/retry.ts +4 -2
- package/src/_shared/tarball-stream.ts +263 -0
- package/src/_shared/tarball.ts +46 -0
- package/src/_shared/w7-frame.ts +2 -1
- package/src/constants.ts +30 -16
- package/src/observability/oom-classify.ts +7 -0
- package/src/observability/oom-discriminator.ts +1 -1
- package/src/runtime/clang-runner.ts +924 -0
- package/src/runtime/esbuild-service.ts +16 -11
- package/src/runtime/facet-host.ts +12 -0
- package/src/runtime/installed-runtimes.ts +4 -3
- package/src/runtime/local-facet-host.ts +28 -8
- package/src/runtime/long-running-handle.ts +3 -3
- package/src/runtime/os-contracts.ts +7 -5
- package/src/runtime/port-registry.ts +97 -9
- package/src/runtime/process-logs.ts +12 -3
- package/src/runtime/project-detect.ts +7 -1
- package/src/runtime/require-resolver.ts +7 -7
- package/src/runtime/ruby-gems.ts +682 -0
- package/src/runtime/ruby-runner.ts +1484 -0
- package/src/runtime/runtime-registry.ts +9 -7
- package/src/runtime/session-process-supervisor.ts +27 -0
- package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
- package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
- package/src/runtime/virtual-socket-kernel.ts +10 -5
- package/src/runtime/wasm-runner.ts +55 -29
- package/src/shell/features.ts +5 -4
- package/src/shell/unix-commands.ts +273 -128
- package/src/substrate/lifo/commands/net/ip.ts +15 -15
- package/src/substrate/lifo/commands/system/ps.ts +2 -1
- package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
- package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
- package/src/substrate/lifo/node-compat/index.ts +3 -2
- package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
- package/src/substrate/lifo/node-compat/process.ts +5 -2
- package/src/substrate/lifo/shell/Shell.ts +92 -70
- package/src/substrate/lifo/shell/lexer.ts +38 -0
- package/src/vfs/seed-project.ts +10 -3
- package/src/vfs/sqlite-vfs.ts +209 -36
- package/src/workspace/nimbus-workspace.ts +10 -1
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
21
21
|
import { CRED_KERNEL } from './os-contracts.js';
|
|
22
|
-
import { resolvePackageEntry, resolveExports } from '../_shared/exports-resolver.js';
|
|
22
|
+
import { resolvePackageEntry, resolveExports, type ResolvablePackageJson } from '../_shared/exports-resolver.js';
|
|
23
23
|
import { normalizeVfsPath, stripLeadingSlashes } from '../vfs/path.js';
|
|
24
|
+
import { errorText } from '../_shared/error-text.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Bundler version tag. BUMP THIS whenever bundling semantics change —
|
|
@@ -175,7 +176,6 @@ export function hasTopLevelAwait(src: string): boolean {
|
|
|
175
176
|
// inside `(...)`; a `{` only opens a function body when a `function`/
|
|
176
177
|
// `=>` is pending AND we're at parenDepth 0 (past the param list).
|
|
177
178
|
const re = /\b(await|function|class)\b|=>|\{|\}|\(|\)/g;
|
|
178
|
-
let m: RegExpExecArray | null;
|
|
179
179
|
const fnEntryDepths: number[] = [];
|
|
180
180
|
let depth = 0;
|
|
181
181
|
let parenDepth = 0;
|
|
@@ -189,7 +189,7 @@ export function hasTopLevelAwait(src: string): boolean {
|
|
|
189
189
|
// `x => ({ ... })` (object-returning expression body, `{` at parenDepth
|
|
190
190
|
// 1) does not leak a body slot onto a later top-level block.
|
|
191
191
|
let arrowPending = false;
|
|
192
|
-
|
|
192
|
+
for (let m = re.exec(stripped); m !== null; m = re.exec(stripped)) {
|
|
193
193
|
const tok = m[0];
|
|
194
194
|
if (arrowPending && tok !== '{') arrowPending = false;
|
|
195
195
|
if (tok === '(') { parenDepth++; }
|
|
@@ -803,7 +803,7 @@ async function loadEsbuild(): Promise<typeof esbuild> {
|
|
|
803
803
|
export interface TransformResult {
|
|
804
804
|
code: string;
|
|
805
805
|
map: string;
|
|
806
|
-
warnings: { text: string; location?:
|
|
806
|
+
warnings: { text: string; location?: esbuild.Location | null }[];
|
|
807
807
|
}
|
|
808
808
|
|
|
809
809
|
export interface BuildOutputFile {
|
|
@@ -813,8 +813,8 @@ export interface BuildOutputFile {
|
|
|
813
813
|
|
|
814
814
|
export interface BuildResult {
|
|
815
815
|
outputFiles: BuildOutputFile[];
|
|
816
|
-
errors: { text: string; location?:
|
|
817
|
-
warnings: { text: string; location?:
|
|
816
|
+
errors: { text: string; location?: esbuild.Location | null }[];
|
|
817
|
+
warnings: { text: string; location?: esbuild.Location | null }[];
|
|
818
818
|
}
|
|
819
819
|
|
|
820
820
|
// ── EsbuildService ──────────────────────────────────────────────────────
|
|
@@ -876,7 +876,11 @@ export class EsbuildService {
|
|
|
876
876
|
let initTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
877
877
|
await Promise.race([
|
|
878
878
|
esb.initialize({
|
|
879
|
-
|
|
879
|
+
// wrangler resolves this static `.wasm` import to a compiled
|
|
880
|
+
// module at bundle time; the asset stub for a `.wasm` module can
|
|
881
|
+
// only declare its default export as a string, and the guard
|
|
882
|
+
// above is what checks the resolution actually happened.
|
|
883
|
+
wasmModule: esbuildWasmUrl as unknown as WebAssembly.Module,
|
|
880
884
|
worker: false,
|
|
881
885
|
}),
|
|
882
886
|
new Promise<never>((_, reject) => {
|
|
@@ -890,14 +894,15 @@ export class EsbuildService {
|
|
|
890
894
|
}),
|
|
891
895
|
]).finally(() => { if (initTimeout) clearTimeout(initTimeout); });
|
|
892
896
|
this.initialized = true;
|
|
893
|
-
} catch (e
|
|
897
|
+
} catch (e) {
|
|
898
|
+
const message = errorText(e);
|
|
894
899
|
// "Cannot call initialize more than once" means it's already ready
|
|
895
|
-
if (
|
|
900
|
+
if (message.includes('more than once')) {
|
|
896
901
|
this.initialized = true;
|
|
897
902
|
return;
|
|
898
903
|
}
|
|
899
904
|
this.initPromise = null;
|
|
900
|
-
throw new Error('esbuild init failed: ' +
|
|
905
|
+
throw new Error('esbuild init failed: ' + message);
|
|
901
906
|
}
|
|
902
907
|
})();
|
|
903
908
|
|
|
@@ -1315,7 +1320,7 @@ export class EsbuildService {
|
|
|
1315
1320
|
if (vfs.exists(strip(nmDir)) && vfs.isDirectory(strip(nmDir))) {
|
|
1316
1321
|
// Read package.json so we can consult the exports field.
|
|
1317
1322
|
const pkgJsonPath = nmDir + '/package.json';
|
|
1318
|
-
let pkgJson:
|
|
1323
|
+
let pkgJson: ResolvablePackageJson | null = null;
|
|
1319
1324
|
if (vfs.exists(strip(pkgJsonPath))) {
|
|
1320
1325
|
try { pkgJson = JSON.parse(vfs.readFileString(strip(pkgJsonPath))); } catch {}
|
|
1321
1326
|
}
|
|
@@ -79,6 +79,18 @@ export interface FacetSpec {
|
|
|
79
79
|
syscalls?: FacetSyscalls;
|
|
80
80
|
/** Facets the host may keep warm for this spec. Default 1. */
|
|
81
81
|
concurrency?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Who a warm facet may be reused FOR. Default `session`.
|
|
84
|
+
*
|
|
85
|
+
* A scope keeps whatever the last call left in it, so by default it may only
|
|
86
|
+
* ever answer for the session that opened it. `global` is a claim the
|
|
87
|
+
* RUNTIME makes about itself and one only a pure function can make: no
|
|
88
|
+
* syscalls binding, nothing retained between calls, output decided entirely
|
|
89
|
+
* by the arguments. The clang toolchain is that — a compile is the same
|
|
90
|
+
* compile for every tenant — and one warm isolate holding its 31 MiB of
|
|
91
|
+
* compiled compiler serves all of them.
|
|
92
|
+
*/
|
|
93
|
+
reuse?: 'session' | 'global';
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/** The session a facet's syscalls reach, and who they reach it as. */
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
20
|
+
import type { Command } from '../substrate/lifo/commands/types.js';
|
|
20
21
|
import {
|
|
21
22
|
CRED_KERNEL,
|
|
22
23
|
NIMBUS_ABI_TARGET,
|
|
@@ -32,9 +33,9 @@ import {
|
|
|
32
33
|
|
|
33
34
|
/** Minimal shell-registry shape we depend on. */
|
|
34
35
|
export interface MinShellRegistry {
|
|
35
|
-
register(name: string, handler:
|
|
36
|
+
register(name: string, handler: Command): void;
|
|
36
37
|
unregister?(name: string): void;
|
|
37
|
-
resolve?(name: string):
|
|
38
|
+
resolve?(name: string): Promise<Command | null | undefined> | Command | null | undefined;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Runner-factory contract. Each registered runner produces a shell-
|
|
@@ -46,7 +47,7 @@ export type RunnerFactory = (
|
|
|
46
47
|
installRoot: string,
|
|
47
48
|
binName: string,
|
|
48
49
|
binKind: string | undefined,
|
|
49
|
-
) =>
|
|
50
|
+
) => Command;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* How a manifest entrypoint's `runner` key is resolved to code.
|
|
@@ -113,12 +113,21 @@ export function localFacetHost(): FacetHost {
|
|
|
113
113
|
root: string,
|
|
114
114
|
options?: FacetFilesystemOptions,
|
|
115
115
|
): FacetFilesystemSeed | { error: string } {
|
|
116
|
-
|
|
116
|
+
const seeded = snapshotVfs(vfs, root, {
|
|
117
117
|
extraRoots: options?.extraRoots,
|
|
118
118
|
skipSubdirs: [],
|
|
119
119
|
maxBytes: LOCAL_SEED_MAX_BYTES,
|
|
120
120
|
maxFiles: LOCAL_SEED_MAX_FILES,
|
|
121
121
|
});
|
|
122
|
+
if ('error' in seeded) return seeded;
|
|
123
|
+
// The seed is also the WHOLE of what this guest has, which is a stronger
|
|
124
|
+
// claim than the walk's: `snapshotVfs` reports the roots it listed
|
|
125
|
+
// exhaustively, and this says every path outside them is absent too. It
|
|
126
|
+
// is — a miss out there could only be answered by suspending the guest
|
|
127
|
+
// mid-syscall, which this host cannot do, so the alternative to "absent"
|
|
128
|
+
// is not "fetched" but a read the guest can never receive. Ruby's VM
|
|
129
|
+
// startup stats dozens of prefixes it was never given.
|
|
130
|
+
return { ...seeded, snapshot: { ...seeded.snapshot, enumeratedRoots: [''] } };
|
|
122
131
|
},
|
|
123
132
|
open: (spec) => new LocalFacet(spec),
|
|
124
133
|
};
|
|
@@ -152,8 +161,7 @@ class LocalFacet implements Facet {
|
|
|
152
161
|
|
|
153
162
|
private async call<A, R>(fn: FacetFn<A, R>, args: A, options?: FacetSubmitOptions): Promise<R> {
|
|
154
163
|
if (this.disposed) throw new Error(`Nimbus: facet '${this.spec.tag}' is disposed`);
|
|
155
|
-
const scope = await this.scope();
|
|
156
|
-
await this.addModules(options?.wasmModules);
|
|
164
|
+
const scope = await this.scope(options?.wasmModules);
|
|
157
165
|
let scoped = this.scoped.get(fn as FacetFn<never, unknown>);
|
|
158
166
|
if (!scoped) {
|
|
159
167
|
const value = scope(`(${fn.toString()})`);
|
|
@@ -172,17 +180,29 @@ class LocalFacet implements Facet {
|
|
|
172
180
|
* The returned closure's `eval` is a DIRECT eval inside the body the preamble
|
|
173
181
|
* was evaluated in, which is what puts the preamble's top-level declarations
|
|
174
182
|
* in scope for every function submitted afterwards.
|
|
183
|
+
*
|
|
184
|
+
* Both wasm tables are filled BEFORE that body runs, per-call images merged
|
|
185
|
+
* over the spec's. A preamble may boot its runtime as it is evaluated — Ruby
|
|
186
|
+
* instantiates the interpreter right there — so it reads the table at that
|
|
187
|
+
* moment and an image added afterwards would arrive to a facet that had
|
|
188
|
+
* already given up on it. workerd has the same ordering for the same reason:
|
|
189
|
+
* per-call images ride in the module map the inner worker is built from.
|
|
175
190
|
*/
|
|
176
|
-
private async scope(
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
private async scope(
|
|
192
|
+
callModules: Record<string, ArrayBuffer> | undefined,
|
|
193
|
+
): Promise<(source: string) => unknown> {
|
|
194
|
+
const built = this.evaluate;
|
|
195
|
+
if (!built) await this.addModules(this.spec.wasmModules);
|
|
196
|
+
await this.addModules(callModules);
|
|
197
|
+
if (built) return built;
|
|
179
198
|
const globals = { __NIMBUS_WASM: this.wasmTable };
|
|
180
199
|
const build = new AsyncFunction(
|
|
181
200
|
'globalThis',
|
|
182
201
|
`${this.spec.preamble ?? ''}\nreturn (source) => eval(source);`,
|
|
183
202
|
);
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
const evaluate = await build.call(globals, globals) as (source: string) => unknown;
|
|
204
|
+
this.evaluate = evaluate;
|
|
205
|
+
return evaluate;
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
private async addModules(modules: Record<string, ArrayBuffer> | undefined): Promise<void> {
|
|
@@ -107,7 +107,7 @@ export function parsePortFromArgv(argv: string[]): number | null {
|
|
|
107
107
|
*/
|
|
108
108
|
export function resolveLongRunningPort(opts: {
|
|
109
109
|
argv?: string[];
|
|
110
|
-
env?: Record<string,
|
|
110
|
+
env?: Record<string, string> | undefined;
|
|
111
111
|
configPort?: number | undefined;
|
|
112
112
|
fallback: number;
|
|
113
113
|
}): number {
|
|
@@ -152,7 +152,7 @@ export function resolveLongRunningPort(opts: {
|
|
|
152
152
|
*/
|
|
153
153
|
export function expandShellDefaults(
|
|
154
154
|
token: string,
|
|
155
|
-
env: Record<string,
|
|
155
|
+
env: Record<string, string> | undefined,
|
|
156
156
|
): string {
|
|
157
157
|
if (!token || token.indexOf('${') < 0) return token;
|
|
158
158
|
const lookup = (name: string): string | undefined => {
|
|
@@ -179,7 +179,7 @@ export function expandShellDefaults(
|
|
|
179
179
|
*/
|
|
180
180
|
export function expandArgvShellDefaults(
|
|
181
181
|
argv: ReadonlyArray<string>,
|
|
182
|
-
env: Record<string,
|
|
182
|
+
env: Record<string, string> | undefined,
|
|
183
183
|
): string[] {
|
|
184
184
|
return argv.map((t) => expandShellDefaults(t, env));
|
|
185
185
|
}
|
|
@@ -312,11 +312,12 @@ export interface RuntimeTtyOptions {
|
|
|
312
312
|
/**
|
|
313
313
|
* Where a registered port's traffic goes: one process, reached by HTTP.
|
|
314
314
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
315
|
+
* Ordinary HTTP may cross Workers RPC as Request/Response values. A WebSocket
|
|
316
|
+
* upgrade may not: its 101 Response owns a live socket, and RPC's
|
|
317
|
+
* Request/Response transport reconstructs the value rather than handing over
|
|
318
|
+
* the socket. So a target that can serve an upgrade exposes a separate
|
|
319
|
+
* fetch-semantic entrypoint, and every hop of an upgrade stays on the HTTP
|
|
320
|
+
* service-binding path. A target that omits it serves HTTP only.
|
|
320
321
|
*
|
|
321
322
|
* Lives here rather than beside the registry because both ends need it and
|
|
322
323
|
* neither owns it: the port registry stores these, and the process fabric
|
|
@@ -325,6 +326,7 @@ export interface RuntimeTtyOptions {
|
|
|
325
326
|
*/
|
|
326
327
|
export interface RouteableFacetTarget {
|
|
327
328
|
handleHttpRequest(request: Request): Promise<Response>;
|
|
329
|
+
handleWebSocketRequest?(request: Request): Promise<Response>;
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
export interface RuntimePortBridge {
|
|
@@ -40,6 +40,17 @@ export interface PortEntry {
|
|
|
40
40
|
*/
|
|
41
41
|
facetStub: RouteableFacetTarget | null;
|
|
42
42
|
registeredAt: number;
|
|
43
|
+
/**
|
|
44
|
+
* Unguessable token for the lifetime of THIS registration, so an embedder
|
|
45
|
+
* can hand out one port's traffic without handing out the session. A fresh
|
|
46
|
+
* one per `register`, which is what makes an unexposed port stay unexposed.
|
|
47
|
+
*/
|
|
48
|
+
capability: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createPortCapability(): string {
|
|
52
|
+
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
|
53
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export class PortRegistry {
|
|
@@ -64,7 +75,13 @@ export class PortRegistry {
|
|
|
64
75
|
*/
|
|
65
76
|
register(port: number, pid: number): void {
|
|
66
77
|
const target = this.facetStubsByPid.get(pid) ?? null;
|
|
67
|
-
this.ports.set(port, {
|
|
78
|
+
this.ports.set(port, {
|
|
79
|
+
port,
|
|
80
|
+
pid,
|
|
81
|
+
facetStub: target,
|
|
82
|
+
registeredAt: Date.now(),
|
|
83
|
+
capability: createPortCapability(),
|
|
84
|
+
});
|
|
68
85
|
this.notifyPortWaiters(pid);
|
|
69
86
|
}
|
|
70
87
|
|
|
@@ -126,6 +143,24 @@ export class PortRegistry {
|
|
|
126
143
|
return [...this.ports.values()];
|
|
127
144
|
}
|
|
128
145
|
|
|
146
|
+
/** Answer only whether this capability matches, never what is registered. */
|
|
147
|
+
hasCapability(port: number, capability: string): boolean {
|
|
148
|
+
return this.ports.get(port)?.capability === capability;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Re-adopt a capability the embedder was already handed, after the supervisor
|
|
153
|
+
* was rebuilt. A restored dev server is a NEW registration with a new token,
|
|
154
|
+
* which would silently invalidate every preview URL already in circulation
|
|
155
|
+
* across an eviction — so the durable value wins over the fresh one.
|
|
156
|
+
*/
|
|
157
|
+
restoreCapability(port: number, capability: string): boolean {
|
|
158
|
+
const entry = this.ports.get(port);
|
|
159
|
+
if (!entry || !/^[a-f0-9]{24}$/.test(capability)) return false;
|
|
160
|
+
entry.capability = capability;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
129
164
|
/**
|
|
130
165
|
* Forward an HTTP request to the facet owning a port.
|
|
131
166
|
*
|
|
@@ -148,6 +183,36 @@ export class PortRegistry {
|
|
|
148
183
|
* client receives.
|
|
149
184
|
*/
|
|
150
185
|
async routeRequest(port: number, request: Request, pathname: string): Promise<Response | null> {
|
|
186
|
+
return this.routeRequestInternal(port, request, pathname, false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Route a request a trusted embedder has already authenticated against the
|
|
191
|
+
* port's capability and stripped its own credentials from.
|
|
192
|
+
*
|
|
193
|
+
* Unlike the generic route, this one PRESERVES `Authorization`. The generic
|
|
194
|
+
* route strips it because the header it sees is Nimbus's own, and handing a
|
|
195
|
+
* session credential to untrusted code is the thing that must never happen.
|
|
196
|
+
* Here the embedder has removed that credential already and what remains
|
|
197
|
+
* belongs to the guest application, which needs it to authenticate its own
|
|
198
|
+
* users.
|
|
199
|
+
*/
|
|
200
|
+
async routeCapabilityRequest(
|
|
201
|
+
port: number,
|
|
202
|
+
capability: string,
|
|
203
|
+
request: Request,
|
|
204
|
+
pathname: string,
|
|
205
|
+
): Promise<Response | null> {
|
|
206
|
+
if (!this.hasCapability(port, capability)) return null;
|
|
207
|
+
return this.routeRequestInternal(port, request, pathname, true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async routeRequestInternal(
|
|
211
|
+
port: number,
|
|
212
|
+
request: Request,
|
|
213
|
+
pathname: string,
|
|
214
|
+
preserveAuthorization: boolean,
|
|
215
|
+
): Promise<Response | null> {
|
|
151
216
|
const entry = this.ports.get(port);
|
|
152
217
|
// Honour the documented contract: no entry at all → null, so callers
|
|
153
218
|
// report "no process listening". (Pre-fix this fell into the 501 below,
|
|
@@ -189,7 +254,12 @@ export class PortRegistry {
|
|
|
189
254
|
// if a body is supplied on those methods.
|
|
190
255
|
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
191
256
|
const headers = new Headers(request.headers);
|
|
257
|
+
// The strip below removes Nimbus's own credentials. On a capability
|
|
258
|
+
// route the embedder has already removed those, so what is left belongs
|
|
259
|
+
// to the guest application and has to survive the hop.
|
|
260
|
+
const authorization = preserveAuthorization ? headers.get('authorization') : null;
|
|
192
261
|
sanitizeUntrustedHeaders(headers);
|
|
262
|
+
if (authorization) headers.set('authorization', authorization);
|
|
193
263
|
|
|
194
264
|
headers.set('X-Nimbus-Port', String(port));
|
|
195
265
|
// `duplex: 'half'` is required by workerd when body is a
|
|
@@ -205,11 +275,19 @@ export class PortRegistry {
|
|
|
205
275
|
if (hasBody) init.duplex = 'half';
|
|
206
276
|
const forwarded = new Request(innerUrl.toString(), init);
|
|
207
277
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
278
|
+
// HTTP crosses Workers RPC as Request/Response values, streamed with
|
|
279
|
+
// flow control and never materialised. A WebSocket upgrade cannot: its
|
|
280
|
+
// 101 owns a live socket, so it takes the fetch-semantic entrypoint,
|
|
281
|
+
// which every hop preserves. A target with no such entrypoint serves
|
|
282
|
+
// HTTP only, and says so rather than dropping the socket.
|
|
283
|
+
const isWebSocket = request.headers.get('upgrade')?.toLowerCase() === 'websocket';
|
|
284
|
+
const handler = isWebSocket
|
|
285
|
+
? entry.facetStub.handleWebSocketRequest
|
|
286
|
+
: entry.facetStub.handleHttpRequest;
|
|
287
|
+
if (!handler) {
|
|
288
|
+
return new Response('Port target does not expose a WebSocket fetch route', { status: 501 });
|
|
289
|
+
}
|
|
290
|
+
const response: Response = await handler(forwarded);
|
|
213
291
|
|
|
214
292
|
if (!(response instanceof Response)) {
|
|
215
293
|
// Defensive: if a facet ever returns something else (JSON
|
|
@@ -286,10 +364,20 @@ export class PortRegistry {
|
|
|
286
364
|
|
|
287
365
|
function routeableFacetTarget(value: unknown): RouteableFacetTarget | null {
|
|
288
366
|
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) return null;
|
|
289
|
-
|
|
290
|
-
|
|
367
|
+
// A `fetch` target already has fetch semantics, so it serves both.
|
|
368
|
+
const fetchMethod = Reflect.get(value, 'fetch');
|
|
369
|
+
if (typeof fetchMethod === 'function') {
|
|
370
|
+
const fetch = fetchMethod.bind(value) as (request: Request) => Promise<Response>;
|
|
371
|
+
return { handleHttpRequest: fetch, handleWebSocketRequest: fetch };
|
|
372
|
+
}
|
|
373
|
+
const httpMethod = Reflect.get(value, 'handleHttpRequest');
|
|
374
|
+
if (typeof httpMethod !== 'function') return null;
|
|
375
|
+
const webSocketMethod = Reflect.get(value, 'handleWebSocketRequest');
|
|
291
376
|
return {
|
|
292
|
-
handleHttpRequest:
|
|
377
|
+
handleHttpRequest: httpMethod.bind(value),
|
|
378
|
+
...(typeof webSocketMethod === 'function'
|
|
379
|
+
? { handleWebSocketRequest: webSocketMethod.bind(value) }
|
|
380
|
+
: {}),
|
|
293
381
|
};
|
|
294
382
|
}
|
|
295
383
|
|
|
@@ -61,6 +61,15 @@ export interface SequencedLogChunk extends LogChunk {
|
|
|
61
61
|
seq: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* A chunk as a persist adapter hands it back on load. `seq` is the sequence
|
|
66
|
+
* number the adapter stored alongside it; hydration falls back to array
|
|
67
|
+
* position for a row that carries none.
|
|
68
|
+
*/
|
|
69
|
+
export interface PersistedLogChunk extends LogChunk {
|
|
70
|
+
seq?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
export interface ProcessLogReadOptions {
|
|
65
74
|
cursor?: number;
|
|
66
75
|
lines?: number;
|
|
@@ -130,7 +139,7 @@ interface PidState {
|
|
|
130
139
|
* against the SQLite engine); KV is not used by W9.
|
|
131
140
|
*/
|
|
132
141
|
export interface PersistAdapter {
|
|
133
|
-
load(pid: number): { chunks:
|
|
142
|
+
load(pid: number): { chunks: PersistedLogChunk[]; exit: ProcessExitInfo | null } | null;
|
|
134
143
|
persistChunks(pid: number, rows: { seq: number; chunk: LogChunk }[]): void;
|
|
135
144
|
persistExit(pid: number, info: ProcessExitInfo): void;
|
|
136
145
|
dropPid(pid: number): void;
|
|
@@ -710,7 +719,7 @@ export class ProcessLogStore {
|
|
|
710
719
|
if (state.hydrated) return;
|
|
711
720
|
state.hydrated = true;
|
|
712
721
|
if (!this._persist) return;
|
|
713
|
-
let loaded: { chunks:
|
|
722
|
+
let loaded: { chunks: PersistedLogChunk[]; exit: ProcessExitInfo | null } | null;
|
|
714
723
|
try {
|
|
715
724
|
loaded = this._persist.load(pid);
|
|
716
725
|
} catch {
|
|
@@ -736,7 +745,7 @@ export class ProcessLogStore {
|
|
|
736
745
|
let highestSeq = -1;
|
|
737
746
|
for (let i = loaded.chunks.length - 1; i >= 0; i--) {
|
|
738
747
|
const c = loaded.chunks[i];
|
|
739
|
-
const seq =
|
|
748
|
+
const seq = c.seq ?? i;
|
|
740
749
|
if (seq > highestSeq) highestSeq = seq;
|
|
741
750
|
const size = c.data.length;
|
|
742
751
|
if (bytes + size > this.perPidBytes && newest.length > 0) {
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* can add detectVite, detectNext, etc. here.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/** The filesystem reads a project probe needs. */
|
|
14
|
+
export interface ProjectProbeFs {
|
|
15
|
+
exists(path: string): boolean;
|
|
16
|
+
readFileString(path: string): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* W10: detect whether the project at `<root>` is a Cloudflare Workers
|
|
15
21
|
* project. Returns true if any of the standard markers are present:
|
|
@@ -18,7 +24,7 @@
|
|
|
18
24
|
* - <root>/wrangler.toml
|
|
19
25
|
* - <root>/package.json with `wrangler` in deps or devDeps
|
|
20
26
|
*/
|
|
21
|
-
export function detectCloudflareWorkersProject(vfs:
|
|
27
|
+
export function detectCloudflareWorkersProject(vfs: ProjectProbeFs, root: string): boolean {
|
|
22
28
|
const r = String(root).replace(/^\/+/, '').replace(/\/+$/, '');
|
|
23
29
|
for (const f of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
24
30
|
try {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
resolveExports as sharedResolveExports,
|
|
34
34
|
DEFAULT_CJS_CONDITIONS,
|
|
35
35
|
DEFAULT_ESM_CONDITIONS,
|
|
36
|
+
type ResolvablePackageJson,
|
|
36
37
|
} from '../_shared/exports-resolver.js';
|
|
37
38
|
import {
|
|
38
39
|
TYPESCRIPT_INDEX_CANDIDATES,
|
|
@@ -146,7 +147,7 @@ function resolveFile(vfs: CredentialedVfs, base: string, sink?: PkgJsonSink): st
|
|
|
146
147
|
const baseTrim = base.replace(/\/+$/, '');
|
|
147
148
|
const pkgJsonPath = normalizePath(baseTrim + '/package.json');
|
|
148
149
|
if (vfs.exists(pkgJsonPath) && !vfs.isDirectory(pkgJsonPath)) {
|
|
149
|
-
let pkg:
|
|
150
|
+
let pkg: ResolvablePackageJson | null = null;
|
|
150
151
|
try { pkg = JSON.parse(vfs.readFileString(pkgJsonPath)); } catch { /* fall through */ }
|
|
151
152
|
if (pkg && typeof pkg.main === 'string' && pkg.main.length > 0) {
|
|
152
153
|
// Record this package.json so the bundle carries the content the
|
|
@@ -257,7 +258,7 @@ function resolvePkgSubpathEx(vfs: CredentialedVfs, pkgDir: string, subpath: stri
|
|
|
257
258
|
// fallback (consistent behaviour across the no-pkgjson branch).
|
|
258
259
|
return tryLegacyDirectorySubpath(vfs, pkgDir, subpath, sink);
|
|
259
260
|
}
|
|
260
|
-
let pkg:
|
|
261
|
+
let pkg: ResolvablePackageJson;
|
|
261
262
|
try { pkg = JSON.parse(vfs.readFileString(pkgJsonPath)); }
|
|
262
263
|
catch {
|
|
263
264
|
const r = resolveFile(vfs, pkgDir + '/index', sink);
|
|
@@ -477,7 +478,7 @@ function resolveImportsField(
|
|
|
477
478
|
while (true) {
|
|
478
479
|
const pkgJsonPath = (dir ? dir + '/' : '') + 'package.json';
|
|
479
480
|
if (vfs.exists(pkgJsonPath) && !vfs.isDirectory(pkgJsonPath)) {
|
|
480
|
-
let pkg:
|
|
481
|
+
let pkg: ResolvablePackageJson | null = null;
|
|
481
482
|
try { pkg = JSON.parse(vfs.readFileString(pkgJsonPath)); } catch { /* malformed */ }
|
|
482
483
|
// First package.json wins (Node spec), even if no imports field.
|
|
483
484
|
if (pkg && pkg.imports) {
|
|
@@ -618,8 +619,7 @@ export function prefetchForRequire(
|
|
|
618
619
|
const stripped = stripCommentsForImports(code);
|
|
619
620
|
|
|
620
621
|
REQUIRE_RE.lastIndex = 0;
|
|
621
|
-
let match;
|
|
622
|
-
while ((match = REQUIRE_RE.exec(stripped)) !== null) {
|
|
622
|
+
for (let match = REQUIRE_RE.exec(stripped); match !== null; match = REQUIRE_RE.exec(stripped)) {
|
|
623
623
|
const specifier = match[2];
|
|
624
624
|
if (isFacetProvided(specifier)) continue;
|
|
625
625
|
const r = resolveRequireEx(vfs, specifier, fromDir, addPkgJson);
|
|
@@ -635,7 +635,7 @@ export function prefetchForRequire(
|
|
|
635
635
|
// runtime W3.5 Fix B's CJS rewrite calls require('./x') which then
|
|
636
636
|
// fails because `x` was never added.
|
|
637
637
|
IMPORT_RE.lastIndex = 0;
|
|
638
|
-
|
|
638
|
+
for (let match = IMPORT_RE.exec(stripped); match !== null; match = IMPORT_RE.exec(stripped)) {
|
|
639
639
|
const specifier = match[2];
|
|
640
640
|
if (isFacetProvided(specifier)) continue;
|
|
641
641
|
const r = resolveRequireEx(vfs, specifier, fromDir, addPkgJson);
|
|
@@ -647,7 +647,7 @@ export function prefetchForRequire(
|
|
|
647
647
|
// Follow static-string dynamic imports so a CLI entry that defers to
|
|
648
648
|
// import('./dist/index.js') has that subtree's content prefetched.
|
|
649
649
|
DYNIMPORT_RE.lastIndex = 0;
|
|
650
|
-
|
|
650
|
+
for (let match = DYNIMPORT_RE.exec(stripped); match !== null; match = DYNIMPORT_RE.exec(stripped)) {
|
|
651
651
|
const specifier = match[2];
|
|
652
652
|
if (isFacetProvided(specifier)) continue;
|
|
653
653
|
const r = resolveRequireEx(vfs, specifier, fromDir, addPkgJson);
|