@nimbus-sh/core 0.4.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/README.md +35 -7
- 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/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/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/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- 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/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/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/package.json +1 -1
- 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/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/esbuild-service.ts +16 -11
- package/src/runtime/installed-runtimes.ts +4 -3
- 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/runtime-registry.ts +9 -7
- 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
|
@@ -128,7 +128,7 @@ export interface DiagRecoveryEvent {
|
|
|
128
128
|
toState: SessionState;
|
|
129
129
|
/** Human-readable trigger ('ws-close', 'isolate-evicted', etc.). */
|
|
130
130
|
trigger: string;
|
|
131
|
-
/**
|
|
131
|
+
/** _isolateGen at the time of the event. Lets a probe detect the
|
|
132
132
|
* difference between an in-isolate transition and a cross-isolate one
|
|
133
133
|
* (the latter implies workerd recycled the DO). */
|
|
134
134
|
isolateGen: number;
|
|
@@ -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
|
}
|
|
@@ -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.
|
|
@@ -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);
|
|
@@ -43,6 +43,8 @@ import { CRED_KERNEL, type VfsCred } from './os-contracts.js';
|
|
|
43
43
|
import type { EsbuildService } from './esbuild-service.js';
|
|
44
44
|
import { parseFacetBundleProfile, type FacetBundleProfile } from './bundle-profile.js';
|
|
45
45
|
import { bindImportMetaResolve, importMetaDefines } from './import-meta-transform.js';
|
|
46
|
+
import type { Command, CommandContext } from '../substrate/lifo/commands/types.js';
|
|
47
|
+
import { errorText } from '../_shared/error-text.js';
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* Result shape that runtime-registry expects from a runner. Mirrors
|
|
@@ -130,7 +132,7 @@ export function resolveRuntimeScriptPath(
|
|
|
130
132
|
* a second one.
|
|
131
133
|
*/
|
|
132
134
|
export type RuntimeSubcommand = (
|
|
133
|
-
ctx:
|
|
135
|
+
ctx: CommandContext,
|
|
134
136
|
registry: ShellRegistry,
|
|
135
137
|
runAsRuntime: (args: string[]) => Promise<number>,
|
|
136
138
|
) => Promise<number>;
|
|
@@ -181,7 +183,7 @@ export interface RuntimeSpec {
|
|
|
181
183
|
* shell registry type tree when the runtime path only needs resolve().
|
|
182
184
|
*/
|
|
183
185
|
export interface ShellRegistry {
|
|
184
|
-
resolve(name: string): Promise<
|
|
186
|
+
resolve(name: string): Promise<Command | null | undefined> | Command | null | undefined;
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
/**
|
|
@@ -201,7 +203,7 @@ export function buildRuntimeHandler(
|
|
|
201
203
|
getEsbuild(): EsbuildService | Promise<EsbuildService>;
|
|
202
204
|
registry: ShellRegistry;
|
|
203
205
|
},
|
|
204
|
-
):
|
|
206
|
+
): Command {
|
|
205
207
|
const { vfs, getEsbuild, registry } = ctx0;
|
|
206
208
|
const fs = vfs.as(CRED_KERNEL);
|
|
207
209
|
|
|
@@ -211,7 +213,7 @@ export function buildRuntimeHandler(
|
|
|
211
213
|
* caller has already consumed them — so a verb handler can delegate
|
|
212
214
|
* back in with a rewritten argv without re-triggering itself.
|
|
213
215
|
*/
|
|
214
|
-
async function runtimeInvocation(ctx:
|
|
216
|
+
async function runtimeInvocation(ctx: CommandContext, args: string[]): Promise<number> {
|
|
215
217
|
const name = spec.name;
|
|
216
218
|
const nimbusCtx = ctx as {
|
|
217
219
|
__nimbusCaptureOutput?: unknown;
|
|
@@ -449,8 +451,8 @@ export function buildRuntimeHandler(
|
|
|
449
451
|
define: importMetaDefines(absUrl),
|
|
450
452
|
});
|
|
451
453
|
code = bindImportMetaResolve(transformed.code, absUrl);
|
|
452
|
-
} catch (e
|
|
453
|
-
ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${e
|
|
454
|
+
} catch (e) {
|
|
455
|
+
ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${errorText(e)}\n`);
|
|
454
456
|
return 1;
|
|
455
457
|
}
|
|
456
458
|
}
|
|
@@ -488,7 +490,7 @@ export function buildRuntimeHandler(
|
|
|
488
490
|
return result.exitCode;
|
|
489
491
|
}
|
|
490
492
|
|
|
491
|
-
return async function runtimeHandler(ctx:
|
|
493
|
+
return async function runtimeHandler(ctx: CommandContext): Promise<number> {
|
|
492
494
|
const args: string[] = ctx.args || [];
|
|
493
495
|
|
|
494
496
|
// ── Subcommand dispatch ──
|
|
@@ -455,12 +455,15 @@ function normalizeOpenFlags(flags: RuntimeOpenFlags): RuntimeFileHandle['flags']
|
|
|
455
455
|
};
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
458
|
+
/** An error carrying the fields Node's `fs` puts on a failed syscall. */
|
|
459
|
+
interface FsError extends Error {
|
|
460
|
+
code: string;
|
|
461
|
+
syscall: string;
|
|
462
|
+
path: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function fsError(code: string, syscall: string, path: string): FsError {
|
|
466
|
+
return Object.assign(new Error(`${code}: ${syscall} '${path}'`), { code, syscall, path });
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
function hasErrorCode(error: unknown, code: string): boolean {
|