@nimbus-sh/worker 0.5.0 → 0.7.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 (164) hide show
  1. package/README.md +2 -2
  2. package/dist/_shared/preview-host.d.ts +26 -8
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +51 -11
  5. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  6. package/dist/cirrus-npm-cjs.generated.js +1 -1
  7. package/dist/facets/cirrus-real.d.ts.map +1 -1
  8. package/dist/facets/cirrus-real.js +6 -5
  9. package/dist/facets/compose.d.ts +148 -0
  10. package/dist/facets/compose.d.ts.map +1 -0
  11. package/dist/facets/compose.js +146 -0
  12. package/dist/facets/durable-images.d.ts +18 -5
  13. package/dist/facets/durable-images.d.ts.map +1 -1
  14. package/dist/facets/durable-images.js +36 -7
  15. package/dist/facets/durable-slots.d.ts +1 -1
  16. package/dist/facets/durable-slots.d.ts.map +1 -1
  17. package/dist/facets/durable-slots.js +2 -1
  18. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  19. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  20. package/dist/facets/esbuild-bundle-pool.js +122 -0
  21. package/dist/facets/exec-telemetry.d.ts +8 -0
  22. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  23. package/dist/facets/manager.d.ts +337 -123
  24. package/dist/facets/manager.d.ts.map +1 -1
  25. package/dist/facets/manager.js +1100 -449
  26. package/dist/facets/process.d.ts +20 -16
  27. package/dist/facets/process.d.ts.map +1 -1
  28. package/dist/facets/process.js +59 -28
  29. package/dist/facets/real-vite-fs-shim.js +2 -2
  30. package/dist/facets/resident-identity.d.ts +23 -0
  31. package/dist/facets/resident-identity.d.ts.map +1 -0
  32. package/dist/facets/resident-identity.js +31 -0
  33. package/dist/facets/vite-dev-server.d.ts +25 -47
  34. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  35. package/dist/facets/vite-dev-server.js +185 -219
  36. package/dist/facets/wasm-image-digest.d.ts +25 -0
  37. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  38. package/dist/facets/wasm-image-digest.js +25 -0
  39. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  40. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  41. package/dist/facets/wasm-swap-registry.js +68 -227
  42. package/dist/git/commands.d.ts +0 -1
  43. package/dist/git/commands.d.ts.map +1 -1
  44. package/dist/git/commands.js +0 -3
  45. package/dist/git/network-facet.d.ts.map +1 -1
  46. package/dist/git/network-facet.js +6 -5
  47. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  48. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  49. package/dist/loaders/child-process/spawn-pool.js +3 -2
  50. package/dist/loaders/generated-workers.d.ts +2 -2
  51. package/dist/loaders/generated-workers.d.ts.map +1 -1
  52. package/dist/loaders/generated-workers.js +3 -3
  53. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  54. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  55. package/dist/loaders/npm-resolve-preamble.js +34 -108
  56. package/dist/node-shims-artifact.generated.js +3 -3
  57. package/dist/npm/install-batch-facet.d.ts +3 -2
  58. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  59. package/dist/npm/install-batch-facet.js +45 -15
  60. package/dist/npm/installer.d.ts +22 -17
  61. package/dist/npm/installer.d.ts.map +1 -1
  62. package/dist/npm/installer.js +209 -193
  63. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  64. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  65. package/dist/npm/pre-bundle-facet.js +1 -1
  66. package/dist/npm/resolve-facet.d.ts +6 -0
  67. package/dist/npm/resolve-facet.d.ts.map +1 -1
  68. package/dist/npm/resolve-one-facet.d.ts +21 -20
  69. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  70. package/dist/npm/resolve-one-facet.js +85 -84
  71. package/dist/npm/semver.d.ts +59 -0
  72. package/dist/npm/semver.d.ts.map +1 -0
  73. package/dist/npm/semver.js +226 -0
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +47 -10
  76. package/dist/router/public-directory.d.ts +5 -1
  77. package/dist/router/public-directory.d.ts.map +1 -1
  78. package/dist/router/public-directory.js +9 -2
  79. package/dist/router/remote-api.d.ts +18 -0
  80. package/dist/router/remote-api.d.ts.map +1 -1
  81. package/dist/router/remote-api.js +64 -5
  82. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  83. package/dist/runtime/cpython-resident.js +3 -1
  84. package/dist/runtime/html-entrypoint.d.ts +7 -0
  85. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  86. package/dist/runtime/html-entrypoint.js +18 -2
  87. package/dist/runtime/node-runner.d.ts +3 -0
  88. package/dist/runtime/node-runner.d.ts.map +1 -1
  89. package/dist/runtime/node-runner.js +7 -0
  90. package/dist/runtime/node-shims.d.ts.map +1 -1
  91. package/dist/runtime/node-shims.js +305 -29
  92. package/dist/runtime/opencode-facet-runner.js +2 -2
  93. package/dist/runtime/package-manager.d.ts +44 -1
  94. package/dist/runtime/package-manager.d.ts.map +1 -1
  95. package/dist/runtime/package-manager.js +168 -3
  96. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  97. package/dist/runtime/ruby-resident.js +2 -0
  98. package/dist/session/agent.d.ts.map +1 -1
  99. package/dist/session/agent.js +28 -1
  100. package/dist/session/helpers.d.ts +38 -0
  101. package/dist/session/helpers.d.ts.map +1 -1
  102. package/dist/session/helpers.js +76 -0
  103. package/dist/session/init-phases.d.ts +1 -1
  104. package/dist/session/init-phases.d.ts.map +1 -1
  105. package/dist/session/init-phases.js +2 -12
  106. package/dist/session/init.d.ts +15 -1
  107. package/dist/session/init.d.ts.map +1 -1
  108. package/dist/session/init.js +91 -603
  109. package/dist/session/keys.d.ts +4 -0
  110. package/dist/session/keys.d.ts.map +1 -1
  111. package/dist/session/keys.js +4 -0
  112. package/dist/session/nimbus-session.d.ts +90 -23
  113. package/dist/session/nimbus-session.d.ts.map +1 -1
  114. package/dist/session/nimbus-session.js +200 -110
  115. package/dist/session/npm-install-port.d.ts +16 -0
  116. package/dist/session/npm-install-port.d.ts.map +1 -0
  117. package/dist/session/npm-install-port.js +37 -0
  118. package/dist/session/port-capability.d.ts +90 -0
  119. package/dist/session/port-capability.d.ts.map +1 -1
  120. package/dist/session/port-capability.js +219 -9
  121. package/dist/session/programmatic.d.ts +125 -5
  122. package/dist/session/programmatic.d.ts.map +1 -1
  123. package/dist/session/programmatic.js +439 -89
  124. package/dist/session/routes.d.ts +39 -17
  125. package/dist/session/routes.d.ts.map +1 -1
  126. package/dist/session/routes.js +67 -131
  127. package/dist/session/rpc.d.ts +39 -16
  128. package/dist/session/rpc.d.ts.map +1 -1
  129. package/dist/session/rpc.js +137 -130
  130. package/dist/session/serving-port.d.ts +27 -0
  131. package/dist/session/serving-port.d.ts.map +1 -0
  132. package/dist/session/serving-port.js +27 -0
  133. package/dist/session/start-real-vite.d.ts +12 -0
  134. package/dist/session/start-real-vite.d.ts.map +1 -1
  135. package/dist/session/start-real-vite.js +4 -4
  136. package/dist/session/supervisor-op.d.ts +41 -4
  137. package/dist/session/supervisor-op.d.ts.map +1 -1
  138. package/dist/session/supervisor-op.js +101 -73
  139. package/dist/session/supervisor-rpc.d.ts +7 -7
  140. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  141. package/dist/session/supervisor-rpc.js +6 -3
  142. package/dist/session/vite-command.d.ts +18 -0
  143. package/dist/session/vite-command.d.ts.map +1 -0
  144. package/dist/session/vite-command.js +609 -0
  145. package/dist/session/ws.d.ts +61 -0
  146. package/dist/session/ws.d.ts.map +1 -1
  147. package/dist/session/ws.js +122 -12
  148. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  149. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  150. package/dist/shell/npm-bin-entrypoints.js +12 -85
  151. package/package.json +24 -4
  152. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  153. package/public/index.html +1 -1
  154. package/scripts/bundle-facet-workers.mjs +1 -1
  155. package/scripts/bundle-npm-cjs.mjs +2 -2
  156. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  157. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  158. package/dist/facets/on-demand-bundle-gate.js +0 -125
  159. package/dist/npm/install-args.d.ts +0 -21
  160. package/dist/npm/install-args.d.ts.map +0 -1
  161. package/dist/npm/install-args.js +0 -70
  162. package/dist/npm/npm-log.d.ts +0 -31
  163. package/dist/npm/npm-log.d.ts.map +0 -1
  164. package/dist/npm/npm-log.js +0 -51
@@ -11,15 +11,17 @@ import { PID_GEN_STRIDE } from '@nimbus-sh/core/runtime/process-table.js';
11
11
  import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
12
12
  import { SessionProcessSupervisor } from '@nimbus-sh/core/runtime/session-process-supervisor.js';
13
13
  import { PortRegistry, createPortCapability } from '@nimbus-sh/core/runtime/port-registry.js';
14
- import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
14
+ import { CRED_KERNEL, requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
15
15
  import { endProcessInput, resizeProcess, signalProcess, writeProcessInput, } from '@nimbus-sh/core/runtime/process-input-routing.js';
16
16
  import { z } from 'zod/v4';
17
17
  import { SESSION_DESTROYED_KEY, SHELL_STATE_KEY_PREFIX, VITE_CONFIG_KEY } from './keys.js';
18
- import { clearPortCapability, persistPortCapability, PortRecordSchema, portRecordKey, readPortReservation, reservePort, restorePortCapability, } from './port-capability.js';
19
- import { PORT_CAPABILITY_KEY_PREFIX } from './keys.js';
18
+ import { clearPortCapability, isValidAppName, persistPortCapability, portRecordKey, readPortReservation, readPortReservationByName, readPortReservationByOwner, reservePort, restorePortCapability, rotatePortCapability, } from './port-capability.js';
20
19
  import { bindPublicPortCapability, unbindPublicPortCapability } from '../router/public-directory.js';
20
+ import { buildPreviewHost, buildPublicPreviewHost, isPreviewHostSafeSid, readPreviewHostSuffix, } from '../_shared/preview-host.js';
21
+ import { RESTART_POLICY_ENV } from '../facets/manager.js';
21
22
  import { GENERATION_KEY, assumeGeneration, generation } from '@nimbus-sh/fabric/generation.js';
22
23
  import { HeadlessTerminal, Shell } from '@nimbus-sh/core/substrate/lifo/index.js';
24
+ import { textSink } from '@nimbus-sh/core/_shared/bytes.js';
23
25
  const ShellIdSchema = z.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
24
26
  const ShellStateSchema = z.object({
25
27
  cwd: z.string().startsWith('/'),
@@ -216,14 +218,13 @@ function startShellJob(self, command, options, job, scoped) {
216
218
  catch { /* already settled */ }
217
219
  });
218
220
  const emit = (stream, sink) => (data) => {
219
- const text = String(data);
220
221
  if (job.background) {
221
222
  try {
222
- self.processes.appendOutput(pid, stream, text);
223
+ self.processes.appendOutputBytes(pid, stream, data);
223
224
  }
224
225
  catch { /* ring gone */ }
225
226
  }
226
- sink?.(text);
227
+ sink?.(data);
227
228
  };
228
229
  const run = shell.execute(line, {
229
230
  // A named shell already holds its own cwd and env; passing them again
@@ -259,7 +260,20 @@ function startShellJob(self, command, options, job, scoped) {
259
260
  }
260
261
  catch { } } };
261
262
  }
263
+ /**
264
+ * The shell, process table and VFS all key on absolute POSIX paths. A
265
+ * relative `cwd` arriving here means a caller skipped its own boundary
266
+ * normalization (the SDK resolves it); handed to the shell it degrades to
267
+ * silently wrong cwd semantics and ENOENT writes. Name the field and fail.
268
+ */
269
+ function assertAbsoluteExecCwd(options) {
270
+ const cwd = options.cwd;
271
+ if (cwd !== undefined && (typeof cwd !== 'string' || !cwd.startsWith('/'))) {
272
+ throw new Error(`cwd must be an absolute POSIX path starting with '/', got ${JSON.stringify(cwd)}`);
273
+ }
274
+ }
262
275
  export async function rpcExec(self, command, options = {}) {
276
+ assertAbsoluteExecCwd(options);
263
277
  await ensureProgrammaticReady(self, options);
264
278
  return withShellState(self, options, false, (scoped) => execOnShell(self, command, options, scoped));
265
279
  }
@@ -271,8 +285,8 @@ async function execOnShell(self, command, options, scoped) {
271
285
  let timedOut = false;
272
286
  const job = startShellJob(self, command, options, {
273
287
  background: false,
274
- onStdout: (d) => stdout.push(d),
275
- onStderr: (d) => stderr.push(d),
288
+ onStdout: textSink((d) => stdout.push(d)),
289
+ onStderr: textSink((d) => stderr.push(d)),
276
290
  }, scoped);
277
291
  let result;
278
292
  try {
@@ -351,8 +365,18 @@ function collectJobOutput(self, pid) {
351
365
  * process surface (`listProcesses`, `processLogs`, `killProcess`).
352
366
  */
353
367
  export async function rpcStartProcess(self, command, options = {}) {
368
+ assertAbsoluteExecCwd(options);
354
369
  await ensureProgrammaticReady(self, options);
355
- return withShellState(self, options, true, (scoped) => startOnShell(self, command, options, scoped));
370
+ if (options.restart !== undefined && options.restart !== 'never' && options.restart !== 'on-failure') {
371
+ throw new Error(`startProcess: restart must be 'never' or 'on-failure', got ${String(options.restart)}`);
372
+ }
373
+ // The policy rides the command's environment: the resident the command
374
+ // starts reads it off its env at launch and journals it, so the shell,
375
+ // the SDK and a child_process spawn all carry it the same way.
376
+ const withPolicy = options.restart === 'on-failure'
377
+ ? { ...options, env: { ...(options.env ?? {}), [RESTART_POLICY_ENV]: 'on-failure' } }
378
+ : options;
379
+ return withShellState(self, withPolicy, true, (scoped) => startOnShell(self, command, withPolicy, scoped));
356
380
  }
357
381
  async function startOnShell(self, command, options, scoped) {
358
382
  const job = startShellJob(self, command, options, { background: true }, scoped);
@@ -397,7 +421,15 @@ async function startOnShell(self, command, options, scoped) {
397
421
  * the handoff, the facet is the live process, and it reports its own exit.
398
422
  */
399
423
  function finishBackgroundJob(self, pid, command, exitCode) {
400
- if (exitCode === 0 && self.facetManager?.hasResidentProcess(pid))
424
+ // The shell line returned, but the pid may still be serving: a resident
425
+ // facet the node runner handed off under this pid, or an in-process dev
426
+ // server a builtin (`vite`) adopted this pid for and registered a port
427
+ // under. Either way the process is alive — marking it exited would put a
428
+ // dead pid on a live port, and the app verbs identify a server by the pid
429
+ // serving its port.
430
+ const stillServing = self.facetManager?.hasResidentProcess(pid) === true
431
+ || self.portRegistry.getAll().some((live) => live.pid === pid);
432
+ if (exitCode === 0 && stillServing)
401
433
  return;
402
434
  self.processes.exit(pid, exitCode);
403
435
  if (!self.processes.getExit(pid))
@@ -517,52 +549,380 @@ export async function rpcProcessLogs(self, pid, options = {}) {
517
549
  }
518
550
  export async function rpcListPorts(self) {
519
551
  await ensureProgrammaticReady(self);
520
- const entries = self.portRegistry.getAll();
521
- // Persisted at the moment the embedder is told the value, not at
522
- // registration: a capability nobody has been handed does not need to
523
- // survive anything.
524
- await Promise.all(entries.map((entry) => persistPortCapability(self, entry.port, entry.capability)));
525
- return entries.map(serializePort);
552
+ // The composed manager's copy is the one implementation — persistence
553
+ // at tell-time included.
554
+ return self.ensureFacetManager().apps.listPorts();
526
555
  }
556
+ /**
557
+ * Browser-facing URL for an application, built inside the session: the
558
+ * host form when the deployment carries a preview suffix (name first, port
559
+ * otherwise; the public bearer form when public and a capability exists),
560
+ * else the path form on the origin the session was last reached at. Null
561
+ * when the session has never been reached over HTTP and has no suffix —
562
+ * an embedder builds its own from the port and capability in that case.
563
+ */
564
+ export function appUrl(self, app) {
565
+ const sid = sessionIdOf(self);
566
+ const suffix = readPreviewHostSuffix(self.env);
567
+ if (suffix !== null && sid !== null && isPreviewHostSafeSid(sid)) {
568
+ const label = app.name ?? app.port;
569
+ if (app.visibility === 'public' && typeof app.capability === 'string') {
570
+ return `https://${buildPublicPreviewHost(sid, label, app.capability, suffix)}/`;
571
+ }
572
+ return `https://${buildPreviewHost(sid, label, suffix)}/`;
573
+ }
574
+ const origin = self.sessionOrigin;
575
+ if (!origin || sid === null)
576
+ return null;
577
+ const base = self.sessionBasePath || `/s/${sid}`;
578
+ return app.name ? `${origin}${base}/app/${app.name}/` : `${origin}${base}/port/${app.port}/`;
579
+ }
580
+ /** The sid this DO serves: the last segment of its name, `tn:sub:sid`. */
581
+ function sessionIdOf(self) {
582
+ const name = self.ctx.id?.name;
583
+ if (typeof name === 'string' && name.length > 0) {
584
+ const cut = name.lastIndexOf(':');
585
+ if (cut > 0)
586
+ return name.slice(cut + 1);
587
+ }
588
+ const fromBase = (self.sessionBasePath || '').replace(/^\/s\//, '');
589
+ return fromBase.length > 0 ? fromBase : null;
590
+ }
591
+ /**
592
+ * The port-centric surface, kept for every caller that has a port and
593
+ * nothing else — a dev server or a process outside the
594
+ * resident lifecycle. When the port's occupant carries an identity the
595
+ * exposure is the same lazy reservation `apps.expose` makes; when it does
596
+ * not, this compatibility path writes the row port-only, as before. One implementation:
597
+ * `applyExposure` below.
598
+ */
527
599
  export async function rpcExposePort(self, port, options) {
528
600
  await ensureProgrammaticReady(self);
529
601
  const n = Number(port);
530
602
  const entry = self.portRegistry.get(n);
531
- if (entry)
532
- await persistPortCapability(self, n, entry.capability);
533
- // The stored record decides visibility: a reservation's mark is sticky,
534
- // so 'public' survives persists of unrelated fields — and the caller's
535
- // explicit choice, when given, lands on the row the same way.
536
- if (options?.visibility !== undefined) {
537
- // A visibility choice is a write on the stored row, not a reservation
538
- // claim: owner and capability stay exactly as they were.
539
- await self.ctx.storage.transaction(async (txn) => {
540
- const stored = await readPortReservation({ storage: txn }, n);
541
- if (stored !== null && stored.visibility !== options.visibility) {
542
- await txn.put(portRecordKey(n), {
603
+ const identity = entry === undefined ? null : await self.facetManager.residentIdentity(entry.pid);
604
+ if (identity?.ephemeral) {
605
+ throw new Error(`port ${n} is served by the second instance of its program, which is not the durable one`);
606
+ }
607
+ const exposed = await applyExposure(self, n, identity?.owner ?? null, options ?? {});
608
+ return {
609
+ port: n,
610
+ listening: !!entry,
611
+ pid: entry?.pid ?? null,
612
+ registeredAt: entry?.registeredAt ?? null,
613
+ capability: exposed.capability,
614
+ visibility: exposed.visibility,
615
+ owner: exposed.owner,
616
+ name: exposed.name,
617
+ };
618
+ }
619
+ /**
620
+ * The identity-centric surface: the target names a running application —
621
+ * by port, by pid, or by name — and the exposure is what makes it durable
622
+ * under its identity: the port is reserved for the owner lazily, the
623
+ * capability minted when public and bound in the directory, the name
624
+ * stored on the reservation. Returns the address the caller can reach.
625
+ */
626
+ export async function rpcExposeApp(self, target, options = {}) {
627
+ await ensureProgrammaticReady(self);
628
+ const resolved = await resolveAppTarget(self, target);
629
+ if (resolved.port === null) {
630
+ throw new Error(`${describeTarget(target)} has not bound a port yet — expose it once it is listening`);
631
+ }
632
+ if (resolved.owner === null) {
633
+ // Every running process has an identity, so this is only a port nothing
634
+ // serves and no reservation names.
635
+ throw new Error(`nothing is serving ${describeTarget(target)} — start the server, then expose it`);
636
+ }
637
+ const exposed = await applyExposure(self, resolved.port, resolved.owner, options);
638
+ const live = self.portRegistry.get(resolved.port);
639
+ return {
640
+ owner: resolved.owner,
641
+ name: exposed.name,
642
+ port: resolved.port,
643
+ pid: live?.pid ?? null,
644
+ capability: exposed.capability,
645
+ visibility: exposed.visibility,
646
+ url: appUrl(self, { port: resolved.port, ...exposed }),
647
+ };
648
+ }
649
+ /**
650
+ * The one exposure implementation. With an owner: reserve the port for it
651
+ * (a conflict if another owner holds it), carrying the name and visibility
652
+ * onto the reservation and adopting the live capability so the URL in
653
+ * circulation is the one the reservation re-adopts after a reset; public
654
+ * exposures mint a capability if none is live yet and bind the directory.
655
+ * Without an owner: the row is written port-only, exactly as `exposePort`
656
+ * always did.
657
+ */
658
+ async function applyExposure(self, port, owner, options) {
659
+ if (options.name !== undefined && !isValidAppName(options.name)) {
660
+ throw new Error(`'${options.name}' is not a valid app name: one DNS label, not all digits, not 24 hex`);
661
+ }
662
+ const entry = self.portRegistry.get(port);
663
+ if (owner !== null) {
664
+ await assertServingOwner(self, port, owner);
665
+ const occupied = new Set(self.portRegistry.getAll().map((live) => live.port));
666
+ occupied.delete(port);
667
+ await reservePort(self.ctx, {
668
+ owner,
669
+ kind: 'derived',
670
+ preferredPort: port,
671
+ occupiedPorts: occupied,
672
+ ...(entry !== undefined ? { capability: entry.capability } : {}),
673
+ ...(options.visibility !== undefined ? { visibility: options.visibility } : {}),
674
+ ...(options.name !== undefined ? { name: options.name } : {}),
675
+ });
676
+ }
677
+ else {
678
+ if (entry)
679
+ await persistPortCapability(self, port, entry.capability);
680
+ // A visibility or name choice is a write on the stored row, not a
681
+ // reservation claim: owner and capability stay exactly as they were.
682
+ if (options.visibility !== undefined || options.name !== undefined) {
683
+ await self.ctx.storage.transaction(async (txn) => {
684
+ const stored = await readPortReservation({ storage: txn }, port);
685
+ if (stored === null)
686
+ return;
687
+ const visibility = options.visibility ?? stored.visibility;
688
+ const name = options.name ?? stored.name;
689
+ if (visibility === stored.visibility && name === stored.name)
690
+ return;
691
+ await txn.put(portRecordKey(port), {
543
692
  owner: stored.owner,
693
+ kind: stored.kind,
544
694
  capability: stored.capability,
545
- visibility: options.visibility,
695
+ visibility,
696
+ ...(name !== undefined ? { name } : {}),
546
697
  });
547
- }
698
+ });
699
+ }
700
+ }
701
+ let record = await readPortReservation(self.ctx, port);
702
+ if (record?.visibility === 'public' && record.capability === null && owner !== null) {
703
+ // Public before the application has bound: mint now, so the URL exists
704
+ // and the binding re-adopts it, exactly as ensureDurableApp does.
705
+ await reservePort(self.ctx, {
706
+ owner, preferredPort: port, occupiedPorts: new Set(), capability: createPortCapability(),
548
707
  });
708
+ record = await readPortReservation(self.ctx, port);
549
709
  }
550
- const record = await readPortReservation(self.ctx, n);
551
- if (record?.visibility === 'public' && entry) {
710
+ // The live registration must answer the stored capability, or a URL
711
+ // minted before the bind 404s until the next restore.
712
+ if (record?.capability !== null && record?.capability !== undefined) {
713
+ const capability = record.capability;
714
+ if (record.owner !== null) {
715
+ await assertServingOwner(self, port, record.owner, () => self.portRegistry.restoreCapability(port, capability));
716
+ }
717
+ else {
718
+ self.portRegistry.restoreCapability(port, capability);
719
+ }
720
+ }
721
+ if (record?.visibility === 'public' && record.capability !== null) {
552
722
  // A port that goes public must be resolvable: the capability the URL
553
723
  // carries has to name this session in the directory, and a non-legacy
554
724
  // deployment without the binding is refused loudly — never half-public.
555
- await bindPublicPortCapability(self, entry.capability, n);
725
+ await bindPublicPortCapability(self, record.capability, port, record.name);
556
726
  }
557
727
  return {
558
- port: n,
559
- listening: !!entry,
560
- pid: entry?.pid ?? null,
561
- registeredAt: entry?.registeredAt ?? null,
562
- capability: entry?.capability ?? null,
728
+ owner: record?.owner ?? owner,
729
+ name: record?.name ?? null,
730
+ capability: record?.capability ?? entry?.capability ?? null,
563
731
  visibility: record?.visibility ?? 'scoped',
564
732
  };
565
733
  }
734
+ /**
735
+ * Mint a new capability for the application and rebind the directory:
736
+ * every URL built on the old one stops resolving. The registry adopts the
737
+ * new value at once if the port is live, so the new URL answers without
738
+ * waiting for a restore.
739
+ */
740
+ export async function rpcRotateLink(self, target) {
741
+ await ensureProgrammaticReady(self);
742
+ const resolved = await resolveAppTarget(self, target);
743
+ if (resolved.port === null || resolved.reservation === null) {
744
+ throw new Error(`${describeTarget(target)} is not exposed — nothing to rotate`);
745
+ }
746
+ const previous = resolved.reservation;
747
+ if (previous.owner !== null)
748
+ await assertServingOwner(self, resolved.port, previous.owner);
749
+ if (previous.visibility === 'public' && previous.capability !== null) {
750
+ await unbindPublicPortCapability(self, previous.capability);
751
+ }
752
+ if (previous.owner !== null)
753
+ await assertServingOwner(self, resolved.port, previous.owner);
754
+ const capability = await rotatePortCapability(self, resolved.port, createPortCapability());
755
+ if (capability === null)
756
+ throw new Error(`${describeTarget(target)} is not exposed — nothing to rotate`);
757
+ if (previous.owner !== null) {
758
+ await assertServingOwner(self, resolved.port, previous.owner, () => self.portRegistry.restoreCapability(resolved.port, capability));
759
+ }
760
+ else {
761
+ self.portRegistry.restoreCapability(resolved.port, capability);
762
+ }
763
+ if (previous.visibility === 'public') {
764
+ await bindPublicPortCapability(self, capability, resolved.port, previous.name);
765
+ }
766
+ const live = self.portRegistry.get(resolved.port);
767
+ return {
768
+ owner: resolved.owner ?? previous.owner ?? '',
769
+ name: previous.name ?? null,
770
+ port: resolved.port,
771
+ pid: live?.pid ?? null,
772
+ capability,
773
+ visibility: previous.visibility,
774
+ url: appUrl(self, { port: resolved.port, name: previous.name, capability, visibility: previous.visibility }),
775
+ };
776
+ }
777
+ /** A name or owner identifies a reservation, never whoever happens to occupy its port. */
778
+ async function assertServingOwner(self, port, owner, adopt) {
779
+ for (;;) {
780
+ const live = self.portRegistry.get(port);
781
+ if (live === undefined)
782
+ return;
783
+ const identity = await self.facetManager.residentIdentity(live.pid);
784
+ if (self.portRegistry.get(port)?.pid !== live.pid)
785
+ continue;
786
+ if (identity?.owner !== owner || identity.ephemeral) {
787
+ throw new Error(`port ${port} is served by a different process (owner ${identity?.owner ?? 'none'})`);
788
+ }
789
+ adopt?.();
790
+ return;
791
+ }
792
+ }
793
+ /** Every stamped identity, with the URL each is reachable at. */
794
+ export async function rpcListApps(self) {
795
+ await ensureProgrammaticReady(self);
796
+ const apps = await self.facetManager.listResidentApps();
797
+ return apps.map((app) => ({
798
+ ...app,
799
+ url: app.port === null ? null : appUrl(self, { ...app, port: app.port }),
800
+ }));
801
+ }
802
+ /**
803
+ * End an application: kill its live pids, release the reservation, purge
804
+ * its journal rows, free the durable slot and its storage, unbind the
805
+ * directory — `removeDurableApp`, addressed by any target.
806
+ */
807
+ export async function rpcRemoveApp(self, target) {
808
+ await ensureProgrammaticReady(self);
809
+ const resolved = await resolveAppTarget(self, target);
810
+ if (resolved.owner === null) {
811
+ throw new Error(`${describeTarget(target)} names no application — nothing to remove`);
812
+ }
813
+ const owner = resolved.owner;
814
+ // The manager's removal kills what the journal knows. A server nothing
815
+ // journalled — the dev servers above all — is found the way every verb
816
+ // finds it, by the identity of the pid serving a port, and ended through
817
+ // the session's own kill, which is the one that also stops an in-process
818
+ // dev server rather than only marking its pid dead.
819
+ const fm = self.facetManager;
820
+ let killed = 0;
821
+ for (const live of self.portRegistry.getAll()) {
822
+ const identity = await fm.residentIdentity(live.pid);
823
+ if (identity === null || identity.ephemeral || identity.owner !== owner)
824
+ continue;
825
+ if ((await rpcKillProcess(self, live.pid)).ok)
826
+ killed += 1;
827
+ }
828
+ const durable = await rpcRemoveDurableApp(self, owner);
829
+ return { ...durable, removed: durable.removed || killed > 0 };
830
+ }
831
+ function describeTarget(target) {
832
+ if (typeof target === 'number')
833
+ return `target ${target}`;
834
+ if (typeof target === 'string')
835
+ return `'${target}'`;
836
+ if ('port' in target)
837
+ return `port ${target.port}`;
838
+ if ('pid' in target)
839
+ return `pid ${target.pid}`;
840
+ if ('name' in target)
841
+ return `app '${target.name}'`;
842
+ return `owner '${target.owner}'`;
843
+ }
844
+ /**
845
+ * Resolve an app target to what the verbs need: the owner (from the
846
+ * serving pid's journal row, or the reservation), the port, and the
847
+ * reservation if one exists. A bare number is a port when something
848
+ * listens on it or a reservation names it, else a pid; a string is a name
849
+ * first, then an owner.
850
+ */
851
+ async function resolveAppTarget(self, target) {
852
+ const fm = self.facetManager;
853
+ const byPort = async (port) => {
854
+ const live = self.portRegistry.get(port);
855
+ const reservation = await readPortReservation(self.ctx, port);
856
+ const identity = live === undefined ? null : await fm.residentIdentity(live.pid);
857
+ if (identity?.ephemeral) {
858
+ throw new Error(`port ${port} is served by the second instance of its program, which is not the durable one`);
859
+ }
860
+ return { owner: identity?.owner ?? reservation?.owner ?? null, port, reservation };
861
+ };
862
+ const byPid = async (pid) => {
863
+ const identity = await fm.residentIdentity(pid);
864
+ if (identity === null)
865
+ return null;
866
+ if (identity.ephemeral) {
867
+ throw new Error(`pid ${pid} is the second instance of its program, which is not the durable one`);
868
+ }
869
+ const port = self.portRegistry.getAll().find((live) => live.pid === pid)?.port ?? identity.port ?? null;
870
+ const reservation = port === null ? null : await readPortReservation(self.ctx, port);
871
+ return { owner: identity.owner ?? null, port, reservation };
872
+ };
873
+ const byOwner = async (owner) => {
874
+ const held = await readPortReservationByOwner(self.ctx, owner);
875
+ if (held !== null)
876
+ return { owner, port: held.port, reservation: held.reservation };
877
+ const app = (await fm.listResidentApps()).find((candidate) => candidate.owner === owner);
878
+ return app === undefined ? null : { owner, port: app.port, reservation: null };
879
+ };
880
+ const byName = async (name) => {
881
+ const held = await readPortReservationByName(self.ctx, name);
882
+ return held === null ? null : { owner: held.reservation.owner, port: held.port, reservation: held.reservation };
883
+ };
884
+ if (typeof target === 'number') {
885
+ if (!Number.isInteger(target) || target <= 0)
886
+ throw new Error(`target ${target} is not a port or a pid`);
887
+ if (self.portRegistry.has(target) || (await readPortReservation(self.ctx, target)) !== null) {
888
+ return byPort(target);
889
+ }
890
+ const asPid = await byPid(target);
891
+ if (asPid !== null)
892
+ return asPid;
893
+ if (target <= 65535)
894
+ return byPort(target);
895
+ throw new Error(`nothing listens on port ${target} and no process ${target} exists`);
896
+ }
897
+ if (typeof target === 'string') {
898
+ const asName = await byName(target);
899
+ if (asName !== null)
900
+ return asName;
901
+ const asOwner = await byOwner(target);
902
+ if (asOwner !== null)
903
+ return asOwner;
904
+ throw new Error(`no application is named or owned '${target}'`);
905
+ }
906
+ if ('port' in target)
907
+ return byPort(Number(target.port));
908
+ if ('pid' in target) {
909
+ const asPid = await byPid(Number(target.pid));
910
+ if (asPid === null) {
911
+ throw new Error(`pid ${target.pid} is not running and has no launch record`);
912
+ }
913
+ return asPid;
914
+ }
915
+ if ('name' in target) {
916
+ const asName = await byName(target.name);
917
+ if (asName === null)
918
+ throw new Error(`no application is named '${target.name}'`);
919
+ return asName;
920
+ }
921
+ const asOwner = await byOwner(target.owner);
922
+ if (asOwner === null)
923
+ throw new Error(`no application is owned by '${target.owner}'`);
924
+ return asOwner;
925
+ }
566
926
  /**
567
927
  * The embedder's durable-application seam: reserve (or re-answer) the port
568
928
  * `owner` holds, minting the capability the application's public URL is
@@ -573,32 +933,10 @@ export async function rpcExposePort(self, port, options) {
573
933
  */
574
934
  export async function rpcEnsureDurableApp(self, input) {
575
935
  await ensureProgrammaticReady(self);
576
- const owner = input.owner;
577
- if (typeof owner !== 'string' || owner.length === 0) {
578
- throw new Error('ensureDurableApp: owner must be a non-empty string');
579
- }
580
- const occupied = new Set(self.portRegistry.getAll().map((entry) => entry.port));
581
- const port = await reservePort(self.ctx, {
582
- owner,
583
- preferredPort: input.preferredPort,
584
- occupiedPorts: occupied,
585
- // Minted here, not at boot: the URL is built from this capability, and
586
- // re-drive re-adopts it out of the same row.
587
- capability: createPortCapability(),
588
- ...(input.visibility !== undefined ? { visibility: input.visibility } : {}),
589
- });
590
- const record = await readPortReservation(self.ctx, port);
591
- if (record?.visibility === 'public' && record.capability !== null) {
592
- // The URL is built on this capability before the app has ever run:
593
- // publish it to the directory now, loudly refusing a deployment whose
594
- // binding is missing rather than minting an unroutable URL.
595
- await bindPublicPortCapability(self, record.capability, port);
596
- }
597
- return {
598
- port,
599
- capability: record?.capability ?? null,
600
- visibility: record?.visibility ?? 'scoped',
601
- };
936
+ // The composed manager owns the durable-app verbs — one implementation,
937
+ // capability minted there so a URL handed out before boot is the one the
938
+ // eventual binding re-adopts.
939
+ return self.ensureFacetManager().apps.ensureDurableApp(input);
602
940
  }
603
941
  export async function rpcUnexposePort(self, port) {
604
942
  await ensureProgrammaticReady(self);
@@ -621,26 +959,8 @@ export async function rpcUnexposePort(self, port) {
621
959
  */
622
960
  export async function rpcRemoveDurableApp(self, owner) {
623
961
  await ensureProgrammaticReady(self);
624
- if (typeof owner !== 'string' || owner.length === 0) {
625
- throw new Error('removeDurableApp: owner must be a non-empty string');
626
- }
627
- // Which durable address is being released — the reservation's row names it
628
- // before the manager retires it.
629
- const rows = await self.ctx.storage.list({ prefix: PORT_CAPABILITY_KEY_PREFIX });
630
- let port = null;
631
- for (const [key, record] of rows) {
632
- const parsed = PortRecordSchema.safeParse(record);
633
- if (!parsed.success || parsed.data.owner !== owner)
634
- continue;
635
- const n = Number(key.slice(PORT_CAPABILITY_KEY_PREFIX.length));
636
- if (Number.isInteger(n) && n > 0) {
637
- port = n;
638
- break;
639
- }
640
- }
641
- self.ensureFacetManager();
642
- const removed = await self.facetManager.removeDurableApp(owner);
643
- return { owner, removed, port };
962
+ // The composed manager owns the durable-app verbs — validation included.
963
+ return self.ensureFacetManager().apps.removeDurableApp(owner);
644
964
  }
645
965
  /**
646
966
  * Route an embedder request that carries a port capability. The embedder has
@@ -658,10 +978,34 @@ export async function rpcRouteCapabilityPort(self, port, capability, request, pa
658
978
  const routed = await self.portRegistry.routeCapabilityRequest(Number(port), String(capability), request, pathname);
659
979
  return routed ?? new Response('Not found', { status: 404 });
660
980
  }
661
- export async function rpcDeleteFile(self, path, options = {}) {
981
+ /**
982
+ * `spawnWorker` for a colocated embedder holding the DO stub: boot the
983
+ * embedder's own Worker-class program — its main module, inline modules and
984
+ * content-addressed text/wasm modules — as one of this session's resident
985
+ * processes, and answer with the pid, the runner's boot payload and the
986
+ * process's facet (`fetch`/`connect`, bound to the resident handle; no
987
+ * release — `killProcess(pid)` ends it). Deliberately absent from the remote
988
+ * HTTP dispatcher: the facet is a live handle, and a remote token holder is
989
+ * not the embedder.
990
+ */
991
+ export async function rpcSpawnWorker(self, workerCode, command, cwd, opts = {}) {
992
+ await ensureProgrammaticReady(self);
993
+ if (typeof workerCode !== 'string' || workerCode.length === 0) {
994
+ throw new Error('spawnWorker: workerCode must be a non-empty string');
995
+ }
996
+ self.ensureFacetManager();
997
+ return self.facetManager.spawnWorker(workerCode, String(command), String(cwd), opts);
998
+ }
999
+ /**
1000
+ * `files.delete`. Absent a `cred` this acts as CRED_KERNEL — what it has
1001
+ * always done, and the embedder's trusted surface: only a caller holding the
1002
+ * DO binding reaches it. A `cred` binds the removal to that identity instead,
1003
+ * the same view `SqliteVFS.as(cred)` gives in-process.
1004
+ */
1005
+ export async function rpcDeleteFile(self, path, options = {}, cred) {
662
1006
  await ensureProgrammaticReady(self);
663
1007
  const p = String(path).replace(/^\/+/, '');
664
- const vfs = self.sqliteFs.as(CRED_KERNEL);
1008
+ const vfs = self.sqliteFs.as(cred === undefined ? CRED_KERNEL : requireVfsCred(cred, 'files.delete'));
665
1009
  if (!vfs.exists(p))
666
1010
  return;
667
1011
  if (vfs.isDirectory(p)) {
@@ -887,17 +1231,23 @@ function resetInMemorySessionState(self) {
887
1231
  self.shell = null;
888
1232
  self.shellProcessPid = null;
889
1233
  self.terminal = null;
890
- self.facetManager = null;
1234
+ self.facetManagerComposed = null;
891
1235
  self.facetProcessManager = null;
892
1236
  self.esbuildService = null;
893
1237
  self.viteDevServer = null;
1238
+ // The installer's pre-bundler and the dev server's on-demand path share
1239
+ // this pool; it goes when both of them go.
1240
+ try {
1241
+ self.bundlePool?.dispose();
1242
+ }
1243
+ catch { }
1244
+ self.bundlePool = null;
894
1245
  self.cirrusReal = null;
895
1246
  self._cirrusHmrWsClients = null;
896
1247
  self.nimbusWrangler = null;
897
1248
  self.npmInstaller = null;
898
1249
  self.fetchProxyEntrypoint = null;
899
- self.runtimeFsBridges?.clear();
900
- self.runtimeFsBridges = null;
1250
+ self._supervisorOps = null;
901
1251
  self._cpRegistry = null;
902
1252
  self._viteShimPid = null;
903
1253
  self._viteShimPort = null;