@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.
Files changed (121) hide show
  1. package/README.md +35 -7
  2. package/dist/_shared/error-text.d.ts +15 -0
  3. package/dist/_shared/error-text.d.ts.map +1 -0
  4. package/dist/_shared/error-text.js +21 -0
  5. package/dist/_shared/exports-resolver.d.ts +19 -7
  6. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  7. package/dist/_shared/exports-resolver.js +1 -1
  8. package/dist/_shared/retry.d.ts.map +1 -1
  9. package/dist/_shared/retry.js +3 -1
  10. package/dist/_shared/w7-frame.js +2 -1
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +30 -16
  13. package/dist/observability/oom-classify.d.ts +1 -1
  14. package/dist/observability/oom-classify.d.ts.map +1 -1
  15. package/dist/observability/oom-classify.js +7 -0
  16. package/dist/observability/oom-discriminator.d.ts +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 +1 -1
  41. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  42. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  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 +8 -2
  82. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  83. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  84. package/dist/vfs/sqlite-vfs.js +199 -35
  85. package/package.json +1 -1
  86. package/src/_shared/error-text.ts +21 -0
  87. package/src/_shared/exports-resolver.ts +24 -4
  88. package/src/_shared/retry.ts +4 -2
  89. package/src/_shared/w7-frame.ts +2 -1
  90. package/src/constants.ts +30 -16
  91. package/src/observability/oom-classify.ts +7 -0
  92. package/src/observability/oom-discriminator.ts +1 -1
  93. package/src/runtime/esbuild-service.ts +16 -11
  94. package/src/runtime/installed-runtimes.ts +4 -3
  95. package/src/runtime/long-running-handle.ts +3 -3
  96. package/src/runtime/os-contracts.ts +7 -5
  97. package/src/runtime/port-registry.ts +97 -9
  98. package/src/runtime/process-logs.ts +12 -3
  99. package/src/runtime/project-detect.ts +7 -1
  100. package/src/runtime/require-resolver.ts +7 -7
  101. package/src/runtime/runtime-registry.ts +9 -7
  102. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  103. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  104. package/src/runtime/virtual-socket-kernel.ts +10 -5
  105. package/src/runtime/wasm-runner.ts +55 -29
  106. package/src/shell/features.ts +5 -4
  107. package/src/shell/unix-commands.ts +273 -128
  108. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  109. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  110. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  111. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  112. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  113. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  114. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  115. package/src/substrate/lifo/node-compat/index.ts +3 -2
  116. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  117. package/src/substrate/lifo/node-compat/process.ts +5 -2
  118. package/src/substrate/lifo/shell/Shell.ts +92 -70
  119. package/src/substrate/lifo/shell/lexer.ts +38 -0
  120. package/src/vfs/seed-project.ts +10 -3
  121. package/src/vfs/sqlite-vfs.ts +209 -36
@@ -1,4 +1,4 @@
1
- import type { Command } from '../types.js';
1
+ import type { Command, CommandContext } from '../types.js';
2
2
  import type { NetworkStack } from '../../kernel/network/index.js';
3
3
  import type { Kernel } from '../../kernel/index.js';
4
4
  import { VETHPair } from '../../kernel/network/tunnel/VETHPair.js';
@@ -65,7 +65,7 @@ Examples:
65
65
  // ═══════════════════════════════════════════════════════════════
66
66
 
67
67
  async function handleLink(
68
- ctx: any,
68
+ ctx: CommandContext,
69
69
  command: string,
70
70
  args: string[],
71
71
  networkStack: NetworkStack
@@ -108,7 +108,7 @@ Examples:
108
108
  }
109
109
  }
110
110
 
111
- async function linkShow(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
111
+ async function linkShow(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
112
112
  let targetInterface: string | undefined;
113
113
 
114
114
  // Parse "dev NAME"
@@ -154,7 +154,7 @@ async function linkShow(ctx: any, args: string[], networkStack: NetworkStack): P
154
154
  return 0;
155
155
  }
156
156
 
157
- async function linkAdd(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
157
+ async function linkAdd(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
158
158
  if (args.length < 3) {
159
159
  ctx.stderr.write('Usage: ip link add NAME type TYPE [ OPTIONS ]\n');
160
160
  return 1;
@@ -207,7 +207,7 @@ async function linkAdd(ctx: any, args: string[], networkStack: NetworkStack): Pr
207
207
  return 1;
208
208
  }
209
209
 
210
- async function linkDelete(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
210
+ async function linkDelete(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
211
211
  if (args.length < 1) {
212
212
  ctx.stderr.write('Usage: ip link del NAME\n');
213
213
  return 1;
@@ -243,7 +243,7 @@ async function linkDelete(ctx: any, args: string[], networkStack: NetworkStack):
243
243
  return 1;
244
244
  }
245
245
 
246
- async function linkSet(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
246
+ async function linkSet(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
247
247
  if (args.length < 2) {
248
248
  ctx.stderr.write('Usage: ip link set NAME { up | down | netns NETNS }\n');
249
249
  return 1;
@@ -362,7 +362,7 @@ async function linkSet(ctx: any, args: string[], networkStack: NetworkStack): Pr
362
362
  // ═══════════════════════════════════════════════════════════════
363
363
 
364
364
  async function handleAddr(
365
- ctx: any,
365
+ ctx: CommandContext,
366
366
  command: string,
367
367
  _args: string[],
368
368
  _networkStack: NetworkStack
@@ -393,7 +393,7 @@ Examples:
393
393
  // ═══════════════════════════════════════════════════════════════
394
394
 
395
395
  async function handleRoute(
396
- ctx: any,
396
+ ctx: CommandContext,
397
397
  command: string,
398
398
  _args: string[],
399
399
  _networkStack: NetworkStack
@@ -426,7 +426,7 @@ Examples:
426
426
  // ═══════════════════════════════════════════════════════════════
427
427
 
428
428
  async function handleNetns(
429
- ctx: any,
429
+ ctx: CommandContext,
430
430
  command: string,
431
431
  args: string[],
432
432
  networkStack: NetworkStack
@@ -458,7 +458,7 @@ Examples:
458
458
  }
459
459
  }
460
460
 
461
- async function netnsShow(ctx: any, _args: string[], networkStack: NetworkStack): Promise<number> {
461
+ async function netnsShow(ctx: CommandContext, _args: string[], networkStack: NetworkStack): Promise<number> {
462
462
  const namespaces = networkStack.getAllNamespaces();
463
463
 
464
464
  for (const ns of namespaces) {
@@ -468,7 +468,7 @@ async function netnsShow(ctx: any, _args: string[], networkStack: NetworkStack):
468
468
  return 0;
469
469
  }
470
470
 
471
- async function netnsAdd(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
471
+ async function netnsAdd(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
472
472
  if (args.length < 1) {
473
473
  ctx.stderr.write('Usage: ip netns add NAME\n');
474
474
  return 1;
@@ -487,7 +487,7 @@ async function netnsAdd(ctx: any, args: string[], networkStack: NetworkStack): P
487
487
  }
488
488
  }
489
489
 
490
- async function netnsDelete(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
490
+ async function netnsDelete(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
491
491
  if (args.length < 1) {
492
492
  ctx.stderr.write('Usage: ip netns del NAME\n');
493
493
  return 1;
@@ -520,7 +520,7 @@ async function netnsDelete(ctx: any, args: string[], networkStack: NetworkStack)
520
520
  // ═══════════════════════════════════════════════════════════════
521
521
 
522
522
  async function handleBridge(
523
- ctx: any,
523
+ ctx: CommandContext,
524
524
  command: string,
525
525
  args: string[],
526
526
  networkStack: NetworkStack
@@ -547,7 +547,7 @@ Examples:
547
547
  }
548
548
  }
549
549
 
550
- async function bridgeFdb(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
550
+ async function bridgeFdb(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
551
551
  const command = args[0] || 'show';
552
552
 
553
553
  if (command !== 'show') {
@@ -585,7 +585,7 @@ async function bridgeFdb(ctx: any, args: string[], networkStack: NetworkStack):
585
585
  return 0;
586
586
  }
587
587
 
588
- async function bridgeLink(ctx: any, args: string[], networkStack: NetworkStack): Promise<number> {
588
+ async function bridgeLink(ctx: CommandContext, args: string[], networkStack: NetworkStack): Promise<number> {
589
589
  const command = args[0] || 'show';
590
590
 
591
591
  if (command !== 'show') {
@@ -1,5 +1,6 @@
1
1
  import type { Command } from '../types.js';
2
2
  import type { ProcessRegistry } from '../../shell/ProcessRegistry.js';
3
+ import type { JobTable } from '../../shell/jobs.js';
3
4
 
4
5
  export function createPsCommand(processRegistry: ProcessRegistry): Command {
5
6
  return async (ctx) => {
@@ -20,7 +21,7 @@ export function createPsCommand(processRegistry: ProcessRegistry): Command {
20
21
  }
21
22
 
22
23
  // Legacy function for backward compatibility
23
- export function createPsCommandFromJobTable(jobTable: any): Command {
24
+ export function createPsCommandFromJobTable(jobTable: JobTable): Command {
24
25
  return async (ctx) => {
25
26
  ctx.stdout.write(' PID TTY TIME CMD\n');
26
27
 
@@ -191,8 +191,6 @@ export class ServiceManager {
191
191
  }
192
192
 
193
193
  if (svc.exitCode !== null) {
194
- // Already stopped
195
- svc.exitCode = svc.exitCode;
196
194
  return { ok: true, message: '' };
197
195
  }
198
196
 
@@ -11,6 +11,8 @@ import type {
11
11
  RouteEntry,
12
12
  NetworkTunnel,
13
13
  } from './types.js';
14
+ import type { VETHPair } from './tunnel/VETHPair.js';
15
+ import type { Bridge } from './Bridge.js';
14
16
 
15
17
  /** Port binding with concrete Socket type */
16
18
  interface PortBinding {
@@ -21,6 +23,13 @@ interface PortBinding {
21
23
  namespace: string;
22
24
  }
23
25
 
26
+ /**
27
+ * A tunnel as the stack holds it. `NetworkTunnel` declares its interface as the
28
+ * plain device record; namespaces hold the concrete device, which is what every
29
+ * tunnel implementation exposes (see `BaseTunnel`).
30
+ */
31
+ type BoundTunnel = Omit<NetworkTunnel, 'interface'> & { interface: NetworkInterface };
32
+
24
33
  /**
25
34
  * Virtual network stack
26
35
  * Provides Linux-like networking with interfaces, routing, sockets, DNS, and tunneling
@@ -33,9 +42,9 @@ export class NetworkStack {
33
42
  private nextNamespaceId = 1;
34
43
 
35
44
  // Tunnel management
36
- private tunnels = new Map<string, NetworkTunnel>(); // tunnel name -> tunnel
37
- private vethPairs = new Map<string, any>(); // veth pair id -> VETHPair
38
- private bridges = new Map<string, any>(); // bridge name -> Bridge
45
+ private tunnels = new Map<string, BoundTunnel>(); // tunnel name -> tunnel
46
+ private vethPairs = new Map<string, VETHPair>(); // veth pair id -> VETHPair
47
+ private bridges = new Map<string, Bridge>(); // bridge name -> Bridge
39
48
  private nextTunnelId = 0;
40
49
 
41
50
  constructor() {
@@ -396,18 +405,16 @@ export class NetworkStack {
396
405
  /**
397
406
  * Add tunnel to network stack
398
407
  */
399
- addTunnel(name: string, tunnel: NetworkTunnel): void {
408
+ addTunnel(name: string, tunnel: BoundTunnel): void {
400
409
  if (this.tunnels.has(name)) {
401
410
  throw new Error(`Tunnel ${name} already exists`);
402
411
  }
403
412
 
404
413
  this.tunnels.set(name, tunnel);
405
414
 
406
- // Add tunnel interface to namespace
407
- // Cast to NetworkInterface class since tunnel.interface is the concrete class
408
415
  const ns = this.namespaces.get(tunnel.interface.namespace);
409
416
  if (ns) {
410
- ns.addInterface(tunnel.interface as any as NetworkInterface);
417
+ ns.addInterface(tunnel.interface);
411
418
  }
412
419
  }
413
420
 
@@ -435,28 +442,28 @@ export class NetworkStack {
435
442
  /**
436
443
  * Get tunnel by name
437
444
  */
438
- getTunnel(name: string): NetworkTunnel | undefined {
445
+ getTunnel(name: string): BoundTunnel | undefined {
439
446
  return this.tunnels.get(name);
440
447
  }
441
448
 
442
449
  /**
443
450
  * Get all tunnels
444
451
  */
445
- getAllTunnels(): NetworkTunnel[] {
452
+ getAllTunnels(): BoundTunnel[] {
446
453
  return Array.from(this.tunnels.values());
447
454
  }
448
455
 
449
456
  /**
450
457
  * Get tunnels by namespace
451
458
  */
452
- getTunnelsByNamespace(namespace: string): NetworkTunnel[] {
459
+ getTunnelsByNamespace(namespace: string): BoundTunnel[] {
453
460
  return this.getAllTunnels().filter((t) => t.interface.namespace === namespace);
454
461
  }
455
462
 
456
463
  /**
457
464
  * Add VETH pair
458
465
  */
459
- addVETHPair(id: string, vethPair: any): void {
466
+ addVETHPair(id: string, vethPair: VETHPair): void {
460
467
  if (this.vethPairs.has(id)) {
461
468
  throw new Error(`VETH pair ${id} already exists`);
462
469
  }
@@ -496,7 +503,7 @@ export class NetworkStack {
496
503
  /**
497
504
  * Get VETH pair by ID or interface name
498
505
  */
499
- getVETHPair(idOrName: string): any | undefined {
506
+ getVETHPair(idOrName: string): VETHPair | undefined {
500
507
  // Try by ID first
501
508
  const byId = this.vethPairs.get(idOrName);
502
509
  if (byId) return byId;
@@ -514,7 +521,7 @@ export class NetworkStack {
514
521
  /**
515
522
  * Get all VETH pairs
516
523
  */
517
- getAllVETHPairs(): any[] {
524
+ getAllVETHPairs(): VETHPair[] {
518
525
  return Array.from(this.vethPairs.values());
519
526
  }
520
527
 
@@ -532,7 +539,7 @@ export class NetworkStack {
532
539
  /**
533
540
  * Add bridge to network stack
534
541
  */
535
- addBridge(name: string, bridge: any): void {
542
+ addBridge(name: string, bridge: Bridge): void {
536
543
  if (this.bridges.has(name)) {
537
544
  throw new Error(`Bridge ${name} already exists`);
538
545
  }
@@ -570,21 +577,21 @@ export class NetworkStack {
570
577
  /**
571
578
  * Get bridge by name
572
579
  */
573
- getBridge(name: string): any | undefined {
580
+ getBridge(name: string): Bridge | undefined {
574
581
  return this.bridges.get(name);
575
582
  }
576
583
 
577
584
  /**
578
585
  * Get all bridges
579
586
  */
580
- getAllBridges(): any[] {
587
+ getAllBridges(): Bridge[] {
581
588
  return Array.from(this.bridges.values());
582
589
  }
583
590
 
584
591
  /**
585
592
  * Get bridges by namespace
586
593
  */
587
- getBridgesByNamespace(namespace: string): any[] {
594
+ getBridgesByNamespace(namespace: string): Bridge[] {
588
595
  return this.getAllBridges().filter((b) => b.interface.namespace === namespace);
589
596
  }
590
597
  }
@@ -3,6 +3,33 @@ import type { NetworkStack } from '../NetworkStack.js';
3
3
  import { BaseTunnel } from './BaseTunnel.js';
4
4
  import { Buffer } from '../../../node-compat/buffer.js';
5
5
  import type { VirtualResponseWithDone } from '../../../node-compat/http.js';
6
+ import type { VirtualRequestHandler } from '../../index.js';
7
+
8
+ /** Payload a WebSocket `message` event can carry. */
9
+ type WebSocketData = string | ArrayBufferLike | ArrayBufferView | Blob;
10
+
11
+ /** HTTP request forwarded to us by the tunnel server. */
12
+ interface TunnelRequestMessage {
13
+ type: 'request';
14
+ requestId: string;
15
+ method: string;
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ /** base64-encoded */
19
+ body: string;
20
+ }
21
+
22
+ /** HTTP response carried over the tunnel, in either direction. */
23
+ interface TunnelResponseMessage {
24
+ type: 'response';
25
+ requestId: string;
26
+ statusCode: number;
27
+ headers: Record<string, string>;
28
+ /** base64-encoded */
29
+ body: string;
30
+ }
31
+
32
+ type TunnelMessage = TunnelRequestMessage | TunnelResponseMessage;
6
33
 
7
34
  /**
8
35
  * WebSocket Tunnel - Bridge virtual network to external WebSocket server
@@ -21,9 +48,9 @@ export class WebSocketTunnel extends BaseTunnel {
21
48
 
22
49
  private wsUrl: string;
23
50
  private ws: WebSocket | null = null;
24
- private portRegistry?: Map<number, any>;
51
+ private portRegistry?: Map<number, VirtualRequestHandler>;
25
52
  private defaultPort: number | null = null;
26
- private reconnectTimer?: any;
53
+ private reconnectTimer?: ReturnType<typeof setTimeout>;
27
54
  private isReconnecting = false;
28
55
 
29
56
  // Packet queue
@@ -34,7 +61,7 @@ export class WebSocketTunnel extends BaseTunnel {
34
61
  id: string,
35
62
  wsUrl: string,
36
63
  networkStack: NetworkStack,
37
- portRegistry?: Map<number, any>,
64
+ portRegistry?: Map<number, VirtualRequestHandler>,
38
65
  namespace = 'default',
39
66
  defaultPort: number | null = null
40
67
  ) {
@@ -201,9 +228,9 @@ export class WebSocketTunnel extends BaseTunnel {
201
228
  /**
202
229
  * Handle incoming WebSocket message
203
230
  */
204
- private handleMessage(data: any): void {
231
+ private handleMessage(data: WebSocketData): void {
205
232
  try {
206
- const message = JSON.parse(data.toString());
233
+ const message = JSON.parse(data.toString()) as TunnelMessage;
207
234
 
208
235
  if (message.type === 'request') {
209
236
  // Handle HTTP request from tunnel server
@@ -220,7 +247,7 @@ export class WebSocketTunnel extends BaseTunnel {
220
247
  /**
221
248
  * Handle HTTP request from tunnel server
222
249
  */
223
- private async handleHttpRequest(message: any): Promise<void> {
250
+ private async handleHttpRequest(message: TunnelRequestMessage): Promise<void> {
224
251
  const { requestId, method, url, headers, body } = message;
225
252
 
226
253
  let port: number;
@@ -243,15 +270,13 @@ export class WebSocketTunnel extends BaseTunnel {
243
270
  path = match[2] || '/';
244
271
  }
245
272
 
246
- // Check if port exists in registry
247
- if (!this.portRegistry || !this.portRegistry.has(port)) {
273
+ // Check if a server is listening on the port
274
+ const handler = this.portRegistry?.get(port);
275
+ if (!handler) {
248
276
  this.sendError(requestId, 404, `No server listening on port ${port}`);
249
277
  return;
250
278
  }
251
279
 
252
- // Get handler
253
- const handler = this.portRegistry.get(port);
254
-
255
280
  // Create virtual request/response
256
281
  const vReq = {
257
282
  method,
@@ -298,7 +323,7 @@ export class WebSocketTunnel extends BaseTunnel {
298
323
  /**
299
324
  * Handle HTTP response (for client-side requests)
300
325
  */
301
- private handleHttpResponse(_message: any): void {
326
+ private handleHttpResponse(_message: TunnelResponseMessage): void {
302
327
  // For future client-side request support
303
328
  // Not needed for current server-only implementation
304
329
  }
@@ -436,6 +436,40 @@ export class VFS {
436
436
  }
437
437
  }
438
438
 
439
+ /**
440
+ * Type probes, mount-aware through {@link stat}.
441
+ *
442
+ * `CredentialedVfs` declares these and the durable coreutils call them on
443
+ * whatever `ctx.vfs` is, so a view that omitted them was not merely missing
444
+ * a convenience: `touch` on an existing file died with
445
+ * `targetVfs.isDirectory is not a function`, because `&&` short-circuited
446
+ * past the call whenever the file was absent — which is why creating a file
447
+ * worked and touching one did not.
448
+ *
449
+ * A structural miss answers false, the way `fs.existsSync` does. A denial
450
+ * still throws: traverse-x enforcement must not be maskable into a quiet
451
+ * false, which is the rule `SqliteVFS.probeInode` already follows.
452
+ */
453
+ isDirectory(path: string): boolean {
454
+ return this.probeType(path) === 'directory';
455
+ }
456
+
457
+ isFile(path: string): boolean {
458
+ return this.probeType(path) === 'file';
459
+ }
460
+
461
+ private probeType(path: string): string | undefined {
462
+ try {
463
+ return this.stat(path).type;
464
+ } catch (error) {
465
+ if (error !== null && typeof error === 'object' && 'code' in error) {
466
+ const code = error.code;
467
+ if (code === 'ENOENT' || code === 'ENOTDIR') return undefined;
468
+ }
469
+ throw error;
470
+ }
471
+ }
472
+
439
473
  access(path: string, mode: number): void {
440
474
  const vp = this.getProvider(path);
441
475
  if (vp) {
@@ -11,28 +11,36 @@ const ESBUILD_WASM_VERSION = '0.24.2';
11
11
  const ESBUILD_WASM_URL = `https://unpkg.com/esbuild-wasm@${ESBUILD_WASM_VERSION}/esbuild.wasm`;
12
12
  const ESBUILD_ESM_URL = `https://unpkg.com/esbuild-wasm@${ESBUILD_WASM_VERSION}/esm/browser.min.js`;
13
13
 
14
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
- let esbuildModule: any = null;
16
- let initPromise: Promise<void> | null = null;
14
+ /** The esbuild-wasm surface this shim forwards to. */
15
+ interface EsbuildWasm {
16
+ initialize(options: { wasmURL: string }): Promise<void>;
17
+ transform(code: string, options?: unknown): Promise<unknown>;
18
+ build(options?: unknown): Promise<unknown>;
19
+ formatMessages(messages: unknown, options?: unknown): Promise<unknown>;
20
+ analyzeMetafile(metafile: unknown, options?: unknown): Promise<unknown>;
21
+ context(options?: unknown): Promise<unknown>;
22
+ }
23
+
24
+ let esbuildModule: EsbuildWasm | null = null;
25
+ let initPromise: Promise<EsbuildWasm> | null = null;
17
26
 
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- async function ensureInitialized(): Promise<any> {
27
+ async function ensureInitialized(): Promise<EsbuildWasm> {
20
28
  if (esbuildModule) return esbuildModule;
21
29
 
22
30
  if (!initPromise) {
23
31
  initPromise = (async () => {
24
32
  // Use dynamic import from CDN
25
33
  // This works in browsers natively
26
- const mod = await import(/* @vite-ignore */ ESBUILD_ESM_URL);
34
+ const mod: EsbuildWasm = await import(/* @vite-ignore */ ESBUILD_ESM_URL);
27
35
  await mod.initialize({
28
36
  wasmURL: ESBUILD_WASM_URL,
29
37
  });
30
38
  esbuildModule = mod;
39
+ return mod;
31
40
  })();
32
41
  }
33
42
 
34
- await initPromise;
35
- return esbuildModule;
43
+ return await initPromise;
36
44
  }
37
45
 
38
46
  export function createEsbuild(): Record<string, unknown> {
@@ -22,6 +22,7 @@ import { createModuleShim } from './module.js';
22
22
  import * as readlineModule from './readline.js';
23
23
  import { createRimraf } from './rimraf.js';
24
24
  import { createEsbuild } from './esbuild.js';
25
+ import { assertEqualHolds } from './loose-equality.js';
25
26
 
26
27
  export interface NodeContext {
27
28
  vfs: VFS;
@@ -114,9 +115,9 @@ export function createModuleMap(ctx: NodeContext): Record<string, () => unknown>
114
115
  if (!value) throw new Error(message || 'AssertionError');
115
116
  };
116
117
  assert.ok = assert;
117
- assert.equal = (a: unknown, b: unknown, msg?: string) => { if (a != b) throw new Error(msg || `${a} != ${b}`); };
118
+ assert.equal = (a: unknown, b: unknown, msg?: string) => { if (!assertEqualHolds(a, b)) throw new Error(msg || `${a} != ${b}`); };
118
119
  assert.strictEqual = (a: unknown, b: unknown, msg?: string) => { if (a !== b) throw new Error(msg || `${a} !== ${b}`); };
119
- assert.notEqual = (a: unknown, b: unknown, msg?: string) => { if (a == b) throw new Error(msg || `${a} == ${b}`); };
120
+ assert.notEqual = (a: unknown, b: unknown, msg?: string) => { if (assertEqualHolds(a, b)) throw new Error(msg || `${a} == ${b}`); };
120
121
  assert.notStrictEqual = (a: unknown, b: unknown, msg?: string) => { if (a === b) throw new Error(msg || `${a} === ${b}`); };
121
122
  assert.deepStrictEqual = (a: unknown, b: unknown, msg?: string) => {
122
123
  if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'deepStrictEqual failed');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * loose-equality.ts — the `==` coercions, spelled out.
3
+ *
4
+ * `assert.equal`/`assert.notEqual` are specified against the `==` operator —
5
+ * Node's own docs call it "shallow, coercive equality between the actual and
6
+ * expected parameters using the == operator" — and since Node 14 both also
7
+ * treat two NaNs as identical, which `==` on its own does not. Guest code
8
+ * inside the sandbox sees those semantics, so the shim carries them here
9
+ * instead of the strict comparison a reader would expect of new code.
10
+ */
11
+
12
+ /** The spec's Object type: everything callable or keyed, null excluded. */
13
+ function isObject(value: unknown): value is object {
14
+ return value !== null && (typeof value === 'object' || typeof value === 'function');
15
+ }
16
+
17
+ /** ToPrimitive(value) with the default hint. */
18
+ function toPrimitive(value: object): unknown {
19
+ if (Symbol.toPrimitive in value) {
20
+ const exotic = value[Symbol.toPrimitive];
21
+ if (typeof exotic === 'function') return exotic.call(value, 'default');
22
+ }
23
+ for (const method of ['valueOf', 'toString'] as const) {
24
+ if (!(method in value)) continue;
25
+ const convert = value[method];
26
+ if (typeof convert !== 'function') continue;
27
+ const primitive: unknown = convert.call(value);
28
+ if (!isObject(primitive)) return primitive;
29
+ }
30
+ throw new TypeError('Cannot convert object to primitive value');
31
+ }
32
+
33
+ /**
34
+ * A bigint compares against the mathematical value of its counterpart, so a
35
+ * fractional number, a non-finite one, or a string naming no integer can
36
+ * never match one.
37
+ */
38
+ function bigIntLooseEqual(big: bigint, other: unknown): boolean {
39
+ if (typeof other === 'number') return Number.isInteger(other) && BigInt(other) === big;
40
+ if (typeof other !== 'string') return false;
41
+ try {
42
+ return BigInt(other) === big;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /** IsLooselyEqual — what `a == b` answers. */
49
+ export function looseEqual(a: unknown, b: unknown): boolean {
50
+ if (a === null || a === undefined) return b === null || b === undefined;
51
+ if (b === null || b === undefined) return false;
52
+
53
+ // Two objects match only as the same reference; an object against a
54
+ // primitive matches on the object's own primitive value.
55
+ if (isObject(a)) {
56
+ return isObject(b) ? a === b : looseEqual(toPrimitive(a), b);
57
+ }
58
+ if (isObject(b)) return looseEqual(a, toPrimitive(b));
59
+
60
+ const aType = typeof a;
61
+ const bType = typeof b;
62
+ if (aType === bType) return a === b;
63
+ if (aType === 'boolean') return looseEqual(Number(a), b);
64
+ if (bType === 'boolean') return looseEqual(a, Number(b));
65
+ if (aType === 'symbol' || bType === 'symbol') return false;
66
+ if (typeof a === 'bigint') return bigIntLooseEqual(a, b);
67
+ if (typeof b === 'bigint') return bigIntLooseEqual(b, a);
68
+ return Number(a) === Number(b);
69
+ }
70
+
71
+ /**
72
+ * Whether Node's legacy assertions call these equal: `==`, widened by the
73
+ * both-sides-NaN case Node folded into `assert.equal` in v14. Node reaches
74
+ * for the non-coercing `Number.isNaN`, so a string spelling "NaN" is not it.
75
+ */
76
+ export function assertEqualHolds(a: unknown, b: unknown): boolean {
77
+ if (typeof a === 'number' && typeof b === 'number') {
78
+ if (Number.isNaN(a) && Number.isNaN(b)) return true;
79
+ }
80
+ return looseEqual(a, b);
81
+ }
@@ -148,8 +148,11 @@ export function createProcess(opts: ProcessOptions) {
148
148
  },
149
149
  removeListener: (event: string, fn: (...args: unknown[]) => void) => proc.off(event, fn),
150
150
  removeAllListeners: (event?: string) => {
151
- if (event) delete listeners[event];
152
- else Object.keys(listeners).forEach((k) => delete listeners[k]);
151
+ if (event) {
152
+ delete listeners[event];
153
+ } else {
154
+ for (const k of Object.keys(listeners)) delete listeners[k];
155
+ }
153
156
  return proc;
154
157
  },
155
158
  listeners: (event: string) => listeners[event] ? [...listeners[event]] : [],