@nimbus-sh/core 0.4.0 → 0.6.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.
Files changed (156) hide show
  1. package/README.md +35 -7
  2. package/dist/_shared/cache-stats.d.ts +23 -1
  3. package/dist/_shared/cache-stats.d.ts.map +1 -1
  4. package/dist/_shared/cache-stats.js +13 -1
  5. package/dist/_shared/error-text.d.ts +15 -0
  6. package/dist/_shared/error-text.d.ts.map +1 -0
  7. package/dist/_shared/error-text.js +21 -0
  8. package/dist/_shared/exports-resolver.d.ts +19 -7
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  10. package/dist/_shared/exports-resolver.js +1 -1
  11. package/dist/_shared/retry.d.ts.map +1 -1
  12. package/dist/_shared/retry.js +4 -2
  13. package/dist/constants.d.ts +5 -11
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/constants.js +23 -49
  16. package/dist/runtime/assets-loader.js +1 -1
  17. package/dist/runtime/esbuild-service.d.ts +4 -3
  18. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  19. package/dist/runtime/esbuild-service.js +9 -4
  20. package/dist/runtime/installed-runtimes.d.ts +4 -3
  21. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  22. package/dist/runtime/long-running-handle.d.ts +3 -3
  23. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  24. package/dist/runtime/os-contracts.d.ts +7 -5
  25. package/dist/runtime/os-contracts.d.ts.map +1 -1
  26. package/dist/runtime/port-registry.d.ts +28 -0
  27. package/dist/runtime/port-registry.d.ts.map +1 -1
  28. package/dist/runtime/port-registry.js +79 -9
  29. package/dist/runtime/process-logs.d.ts +9 -1
  30. package/dist/runtime/process-logs.d.ts.map +1 -1
  31. package/dist/runtime/project-detect.d.ts +6 -1
  32. package/dist/runtime/project-detect.d.ts.map +1 -1
  33. package/dist/runtime/require-resolver.d.ts.map +1 -1
  34. package/dist/runtime/require-resolver.js +3 -4
  35. package/dist/runtime/runtime-registry.d.ts +4 -3
  36. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  37. package/dist/runtime/runtime-registry.js +2 -1
  38. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  39. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts +2 -2
  41. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  42. package/dist/runtime/virtual-socket-kernel.generated.js +3 -3
  43. package/dist/runtime/virtual-socket-kernel.js +10 -5
  44. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  45. package/dist/runtime/wasm-runner.js +22 -14
  46. package/dist/shell/features.d.ts.map +1 -1
  47. package/dist/shell/features.js +5 -4
  48. package/dist/shell/unix-commands.d.ts +14 -1
  49. package/dist/shell/unix-commands.d.ts.map +1 -1
  50. package/dist/shell/unix-commands.js +115 -69
  51. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  52. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  53. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  54. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  55. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  56. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  57. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  58. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  59. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  60. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  61. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  62. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  63. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  64. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  65. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  66. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  67. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  68. package/dist/substrate/lifo/node-compat/index.js +3 -2
  69. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  70. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  71. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  72. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  73. package/dist/substrate/lifo/node-compat/process.js +6 -3
  74. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  75. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  76. package/dist/substrate/lifo/shell/Shell.js +86 -75
  77. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  78. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  79. package/dist/substrate/lifo/shell/lexer.js +40 -0
  80. package/dist/vfs/seed-project.d.ts.map +1 -1
  81. package/dist/vfs/seed-project.js +9 -3
  82. package/dist/vfs/sqlite-vfs.d.ts +63 -31
  83. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  84. package/dist/vfs/sqlite-vfs.js +206 -41
  85. package/package.json +3 -2
  86. package/src/_shared/cache-stats.ts +27 -1
  87. package/src/_shared/error-text.ts +21 -0
  88. package/src/_shared/exports-resolver.ts +24 -4
  89. package/src/_shared/retry.ts +5 -3
  90. package/src/constants.ts +37 -53
  91. package/src/runtime/assets-loader.ts +1 -1
  92. package/src/runtime/esbuild-service.ts +16 -11
  93. package/src/runtime/installed-runtimes.ts +4 -3
  94. package/src/runtime/long-running-handle.ts +3 -3
  95. package/src/runtime/os-contracts.ts +7 -5
  96. package/src/runtime/port-registry.ts +97 -9
  97. package/src/runtime/process-logs.ts +12 -3
  98. package/src/runtime/project-detect.ts +7 -1
  99. package/src/runtime/require-resolver.ts +7 -7
  100. package/src/runtime/runtime-registry.ts +9 -7
  101. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  102. package/src/runtime/virtual-socket-kernel.generated.ts +3 -3
  103. package/src/runtime/virtual-socket-kernel.ts +10 -5
  104. package/src/runtime/wasm-runner.ts +55 -29
  105. package/src/shell/features.ts +5 -4
  106. package/src/shell/unix-commands.ts +273 -128
  107. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  108. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  109. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  110. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  111. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  112. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  113. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  114. package/src/substrate/lifo/node-compat/index.ts +3 -2
  115. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  116. package/src/substrate/lifo/node-compat/process.ts +5 -2
  117. package/src/substrate/lifo/shell/Shell.ts +92 -70
  118. package/src/substrate/lifo/shell/lexer.ts +38 -0
  119. package/src/vfs/seed-project.ts +13 -6
  120. package/src/vfs/sqlite-vfs.ts +233 -77
  121. package/dist/_shared/install-phase.d.ts +0 -47
  122. package/dist/_shared/install-phase.d.ts.map +0 -1
  123. package/dist/_shared/install-phase.js +0 -1
  124. package/dist/_shared/rpc-dispose.d.ts +0 -4
  125. package/dist/_shared/rpc-dispose.d.ts.map +0 -1
  126. package/dist/_shared/rpc-dispose.js +0 -30
  127. package/dist/_shared/w7-frame.d.ts +0 -79
  128. package/dist/_shared/w7-frame.d.ts.map +0 -1
  129. package/dist/_shared/w7-frame.js +0 -839
  130. package/dist/_shared/weighted-credit-pool.d.ts +0 -61
  131. package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
  132. package/dist/_shared/weighted-credit-pool.js +0 -196
  133. package/dist/observability/diag-counters.d.ts +0 -236
  134. package/dist/observability/diag-counters.d.ts.map +0 -1
  135. package/dist/observability/diag-counters.js +0 -195
  136. package/dist/observability/heap-estimate.d.ts +0 -225
  137. package/dist/observability/heap-estimate.d.ts.map +0 -1
  138. package/dist/observability/heap-estimate.js +0 -185
  139. package/dist/observability/heavy-alloc-coord.d.ts +0 -93
  140. package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
  141. package/dist/observability/heavy-alloc-coord.js +0 -181
  142. package/dist/observability/oom-classify.d.ts +0 -138
  143. package/dist/observability/oom-classify.d.ts.map +0 -1
  144. package/dist/observability/oom-classify.js +0 -261
  145. package/dist/observability/oom-discriminator.d.ts +0 -176
  146. package/dist/observability/oom-discriminator.d.ts.map +0 -1
  147. package/dist/observability/oom-discriminator.js +0 -272
  148. package/src/_shared/install-phase.ts +0 -56
  149. package/src/_shared/rpc-dispose.ts +0 -31
  150. package/src/_shared/w7-frame.ts +0 -1012
  151. package/src/_shared/weighted-credit-pool.ts +0 -244
  152. package/src/observability/diag-counters.ts +0 -343
  153. package/src/observability/heap-estimate.ts +0 -331
  154. package/src/observability/heavy-alloc-coord.ts +0 -241
  155. package/src/observability/oom-classify.ts +0 -253
  156. package/src/observability/oom-discriminator.ts +0 -405
package/src/constants.ts CHANGED
@@ -1,7 +1,32 @@
1
1
  /**
2
2
  * constants.ts — Single source of truth for all Nimbus configuration.
3
+ *
4
+ * Nimbus POLICY only. The measured platform limits these policies are
5
+ * derived from (tx bounds, the RPC envelope, supervisor budgets) live in
6
+ * `@nimbus-sh/platform/limits.js`.
3
7
  */
4
8
 
9
+ import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
10
+
11
+ // Published-surface forwarding: @nimbus-sh/core@0.5.0 exports these from
12
+ // this module and real consumers import them here (Proteus merge-back.ts
13
+ // pulls CHUNK_SIZE and the MAX_TX_* set). The platform split moved the
14
+ // definitions to @nimbus-sh/platform/limits.js, which stays the single
15
+ // source of truth; core's own modules import platform directly.
16
+ export {
17
+ CHUNK_SIZE,
18
+ MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES,
19
+ MAX_RPC_SAFE_PAYLOAD_BYTES,
20
+ MAX_TX_BLOB_BYTES,
21
+ MAX_TX_LOGICAL_ROWS,
22
+ MAX_TX_SQL_EXECS,
23
+ PRE_BUNDLE_CONCURRENCY,
24
+ PRE_BUNDLE_SLICE_CAP_BYTES,
25
+ SUPERVISOR_HEAP_CEILING_BYTES,
26
+ SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES,
27
+ SUPERVISOR_READ_RESERVE_BYTES,
28
+ } from '@nimbus-sh/platform/limits.js';
29
+
5
30
  // ── Versions ────────────────────────────────────────────────────────────
6
31
  export const NIMBUS_VERSION = '2.0.0';
7
32
  //
@@ -55,19 +80,11 @@ export const SQLJS_VERSION = '1.14.1';
55
80
  export const OPENCODE_VERSION = '1.16.2';
56
81
 
57
82
  // ── VFS Constants ───────────────────────────────────────────────────────
58
- export const CHUNK_SIZE = 65_536; // 64KB per content chunk
83
+ // The chunk size itself (CHUNK_SIZE) and the transaction bounds are
84
+ // platform limits — see @nimbus-sh/platform/limits.js.
59
85
  export const LRU_MAX_ENTRIES = 512; // 512 × 64KB = 32MB hot cache
60
86
  export const BATCH_SIZE = 64; // rows per batch INSERT
61
87
  export const VFS_CAPACITY = 10 * 1024 * 1024 * 1024; // 10 GB
62
- export const MAX_TX_BLOB_BYTES = 1 * 1024 * 1024;
63
- export const MAX_TX_LOGICAL_ROWS = 256;
64
- export const MAX_TX_SQL_EXECS = 64;
65
- export const MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES = 8 * 1024 * 1024;
66
-
67
- // Largest byte payload Nimbus sends through an ordinary Workers RPC value.
68
- // The platform limit is 32 MiB; 28 MiB leaves room for structured-clone
69
- // metadata and matches the proven on-demand facet transfer envelope.
70
- export const MAX_RPC_SAFE_PAYLOAD_BYTES = 28 * 1024 * 1024;
71
88
 
72
89
  // ── Batched filesystem reads ────────────────────────────────────────────
73
90
  // A round trip costs an order of magnitude more than the SQLite lookup
@@ -142,10 +159,13 @@ export const FACET_TIMEOUT_MS = 30_000; // 30s execution timeout
142
159
  // JavaScript, so workerd's per-module text-size limit applies to the
143
160
  // JSON-escaped form (each `\n` / `\"` / `\u` adds bytes, plus the
144
161
  // per-key string-quote overhead).
145
- // raw → boots, 8 MiB raw fails. Encoded as JSON that's roughly 18-25 MiB
146
- // of module text. We target 22 MiB encoded as the hard ceiling, leaving
147
- // ~2-3 MiB of headroom for the rest of the worker module (shims, runner
148
- // boot code) and any minor drift in the eviction loop's accounting.
162
+ // Measured: 8 MiB raw failed to boot; the passing raw bound was measured
163
+ // too, but the number was lost to comment truncation before it ever
164
+ // reached git, so only the failing side is known. Encoded as JSON that's
165
+ // roughly 18-25 MiB of module text. We target 22 MiB encoded as the hard
166
+ // ceiling, leaving ~2-3 MiB of headroom for the rest of the worker module
167
+ // (shims, runner boot code) and any minor drift in the eviction loop's
168
+ // accounting.
149
169
  //
150
170
  // facet-manager.ts:buildPrefetchBundle uses TextEncoder().encode().length
151
171
  // to measure exact UTF-8 bytes (not JS string .length, which counts UTF-16
@@ -198,11 +218,12 @@ export const PREFETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024;
198
218
  export const CWD_SNAPSHOT_MAX_FILE_BYTES = 2 * 1024 * 1024;
199
219
 
200
220
  // ── npm Constants ───────────────────────────────────────────────────────
221
+ // The pre-bundle admission envelope (PRE_BUNDLE_SLICE_CAP_BYTES,
222
+ // PRE_BUNDLE_CONCURRENCY) is a measured platform envelope — see
223
+ // @nimbus-sh/platform/limits.js.
201
224
  export const NPM_REGISTRY = 'https://registry.npmjs.org';
202
225
  export const NPM_CONCURRENCY = 12;
203
226
  export const NPM_DECOMPRESS_TIMEOUT = 15_000;
204
- export const PRE_BUNDLE_SLICE_CAP_BYTES = MAX_RPC_SAFE_PAYLOAD_BYTES;
205
- export const PRE_BUNDLE_CONCURRENCY = 1;
206
227
 
207
228
  // ── Dev Server Constants ────────────────────────────────────────────────
208
229
  export const DEFAULT_VITE_PORT = 5173;
@@ -222,43 +243,6 @@ export const NIMBUS_AI_GATEWAY_PORT = 8790;
222
243
  // ── Compatibility ───────────────────────────────────────────────────────
223
244
  export const CF_COMPAT_DATE = '2026-04-01';
224
245
 
225
- // ── Supervisor heap budget [C'.1] ───────────────────────────────────────
226
- //
227
- // The supervisor isolate's 128 MiB workerd cap is a HARD platform ceiling
228
- // (per docs/research/cf-primitives-dossier.md §6 invariant I1 — 128 MiB
229
- // per V8 isolate, may be shared across same-class peer DOs co-tenanting in
230
- // one process). Nimbus targets HALF of that as a self-imposed soft ceiling
231
- // so the supervisor always has runway when workerd LRU-evicts neighbours
232
- // or AIR (Asynchronous Isolate Recreation) folds growing isolates.
233
- //
234
- // 64 MiB is a budget, not a measurement, and nothing enforces it.
235
- // src/observability/heap-estimate.ts sums the INSTRUMENTED contributors —
236
- // the supervisor baseline, VFS LRU and in-flight writes, pre-bundle slices,
237
- // and streaming RPC buffers — which is a lower bound, not full coverage.
238
- // The prefetch-bundle path in facets/manager.ts allocates against this
239
- // budget without accounting for it; see HEAP_BLIND_SPOTS for the current
240
- // gap. Read a low percentOfCeiling accordingly.
241
- export const SUPERVISOR_HEAP_CEILING_BYTES = 64 * 1024 * 1024;
242
-
243
- // Shared allowance for transient allocations in the supervisor DO. With the
244
- // VFS LRU shrunk to 8 MiB during an active reservation, 40 MiB of admitted
245
- // payload plus the 9 MiB bundle baseline stays below the 64 MiB soft ceiling
246
- // with 7 MiB left for metadata, structured-clone overhead, and runtime state.
247
- // This is 31.25% of the platform's 128 MiB hard isolate ceiling.
248
- export const SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES = 40 * 1024 * 1024;
249
-
250
- // Reserved slice of that allowance for chunk-sized filesystem reads, so a
251
- // read is never queued behind a multi-megabyte owner for a wait that has
252
- // nothing to do with its own cost. A read only draws on this when the shared
253
- // budget is already contended — which is exactly when the VFS LRU is shrunk
254
- // to 8 MiB — so peak accounting is 40 + 1 admitted, 8 LRU, 9 bundle baseline:
255
- // 58 MiB, still under the 64 MiB soft ceiling.
256
- //
257
- // 1 MiB is 16 concurrent READ_STREAM_CHUNK_BYTES reads. The point is not
258
- // depth, it is that a read loop keeps moving while a heavy owner works
259
- // instead of stopping dead for the duration.
260
- export const SUPERVISOR_READ_RESERVE_BYTES = 1024 * 1024;
261
-
262
246
  // ── OS Defaults ─────────────────────────────────────────────────────────
263
247
  export const DEFAULT_HOSTNAME = 'nimbus';
264
248
  export const DEFAULT_HOME = '/home/user';
@@ -35,7 +35,7 @@
35
35
  * same content.
36
36
  */
37
37
 
38
- import { disposeRpcResource } from '../_shared/rpc-dispose.js';
38
+ import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
39
39
 
40
40
  /**
41
41
  * Per-isolate cache. Keys are asset paths (e.g. `/_assets/foo.js`).
@@ -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
- while ((m = re.exec(stripped)) !== null) {
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?: any }[];
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?: any }[];
817
- warnings: { text: string; location?: any }[];
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
- wasmModule: esbuildWasmUrl as any,
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: any) {
897
+ } catch (e) {
898
+ const message = errorText(e);
894
899
  // "Cannot call initialize more than once" means it's already ready
895
- if (e?.message?.includes('more than once')) {
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: ' + (e?.message || e));
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: any = null;
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: (ctx: any) => Promise<number>): void;
36
+ register(name: string, handler: Command): void;
36
37
  unregister?(name: string): void;
37
- resolve?(name: string): any;
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
- ) => (ctx: any) => Promise<number>;
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, any> | undefined;
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, any> | undefined,
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, any> | undefined,
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
- * The whole of what a listening process exposes, deliberately. A resident
316
- * process never receives a WebSocket every inbound socket Nimbus serves
317
- * terminates on the session and reaches the process, if at all, as events on a
318
- * poll so a target that could return a 101 would describe a case no runner
319
- * produces.
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, { port, pid, facetStub: target, registeredAt: Date.now() });
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
- // RPC: the facet receives the Request and returns a Response.
209
- // Both cross the isolate boundary via Workers RPC's native
210
- // Request/Response transport bytes are streamed with
211
- // flow-control, never materialised.
212
- const response: Response = await entry.facetStub.handleHttpRequest(forwarded);
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
- const method = Reflect.get(value, 'fetch') || Reflect.get(value, 'handleHttpRequest');
290
- if (typeof method !== 'function') return null;
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: method.bind(value),
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: LogChunk[]; exit: ProcessExitInfo | null } | null;
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: LogChunk[]; exit: ProcessExitInfo | null } | null;
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 = (c as any).seq ?? i;
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: any, root: string): boolean {
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: any = null;
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: { exports?: any; module?: string; main?: string };
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: any = null;
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
- while ((match = IMPORT_RE.exec(stripped)) !== null) {
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
- while ((match = DYNIMPORT_RE.exec(stripped)) !== null) {
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: any,
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<any> | any;
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
- ): (ctx: any) => Promise<number> {
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: any, args: string[]): Promise<number> {
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: any) {
453
- ctx.stderr.write(`${name}: transform error for ${scriptPath}: ${e?.message}\n`);
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: any): Promise<number> {
493
+ return async function runtimeHandler(ctx: CommandContext): Promise<number> {
492
494
  const args: string[] = ctx.args || [];
493
495
 
494
496
  // ── Subcommand dispatch ──