@nimbus-sh/worker 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 (95) hide show
  1. package/README.md +46 -43
  2. package/dist/_shared/preview-host.d.ts +33 -2
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +73 -8
  5. package/dist/_shared/session-router.d.ts +43 -0
  6. package/dist/_shared/session-router.d.ts.map +1 -1
  7. package/dist/_shared/session-router.js +59 -12
  8. package/dist/facets/durable-images.d.ts +59 -0
  9. package/dist/facets/durable-images.d.ts.map +1 -0
  10. package/dist/facets/durable-images.js +94 -0
  11. package/dist/facets/durable-slots.d.ts +45 -0
  12. package/dist/facets/durable-slots.d.ts.map +1 -0
  13. package/dist/facets/durable-slots.js +95 -0
  14. package/dist/facets/esbuild-transform.d.ts +10 -0
  15. package/dist/facets/esbuild-transform.d.ts.map +1 -0
  16. package/dist/facets/esbuild-transform.js +36 -0
  17. package/dist/facets/manager.d.ts +244 -6
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +901 -117
  20. package/dist/facets/resident-identity.d.ts +23 -0
  21. package/dist/facets/resident-identity.d.ts.map +1 -0
  22. package/dist/facets/resident-identity.js +31 -0
  23. package/dist/git/commands.d.ts +22 -1
  24. package/dist/git/commands.d.ts.map +1 -1
  25. package/dist/git/commands.js +429 -425
  26. package/dist/git-bundle.generated.d.ts +2 -2
  27. package/dist/git-bundle.generated.d.ts.map +1 -1
  28. package/dist/git-bundle.generated.js +3 -3
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/node-shims-artifact.generated.js +3 -3
  33. package/dist/router/index.d.ts.map +1 -1
  34. package/dist/router/index.js +97 -3
  35. package/dist/router/public-directory-do.d.ts +17 -0
  36. package/dist/router/public-directory-do.d.ts.map +1 -0
  37. package/dist/router/public-directory-do.js +16 -0
  38. package/dist/router/public-directory.d.ts +79 -0
  39. package/dist/router/public-directory.d.ts.map +1 -0
  40. package/dist/router/public-directory.js +122 -0
  41. package/dist/router/remote-api.d.ts +27 -1
  42. package/dist/router/remote-api.d.ts.map +1 -1
  43. package/dist/router/remote-api.js +48 -2
  44. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  45. package/dist/runtime/cpython-resident.js +3 -1
  46. package/dist/runtime/node-shims.d.ts +4 -1
  47. package/dist/runtime/node-shims.d.ts.map +1 -1
  48. package/dist/runtime/node-shims.js +324 -6
  49. package/dist/runtime/package-manager.d.ts +44 -1
  50. package/dist/runtime/package-manager.d.ts.map +1 -1
  51. package/dist/runtime/package-manager.js +168 -3
  52. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  53. package/dist/runtime/ruby-resident.js +2 -0
  54. package/dist/session/agent.d.ts +11 -0
  55. package/dist/session/agent.d.ts.map +1 -1
  56. package/dist/session/agent.js +28 -1
  57. package/dist/session/helpers.js +1 -1
  58. package/dist/session/init-phases.d.ts +17 -23
  59. package/dist/session/init-phases.d.ts.map +1 -1
  60. package/dist/session/init-phases.js +17 -41
  61. package/dist/session/init.d.ts.map +1 -1
  62. package/dist/session/init.js +28 -15
  63. package/dist/session/keys.d.ts +12 -0
  64. package/dist/session/keys.d.ts.map +1 -1
  65. package/dist/session/keys.js +12 -0
  66. package/dist/session/nimbus-session.d.ts +63 -5
  67. package/dist/session/nimbus-session.d.ts.map +1 -1
  68. package/dist/session/nimbus-session.js +131 -17
  69. package/dist/session/port-capability.d.ts +141 -8
  70. package/dist/session/port-capability.d.ts.map +1 -1
  71. package/dist/session/port-capability.js +265 -8
  72. package/dist/session/programmatic.d.ts +145 -3
  73. package/dist/session/programmatic.d.ts.map +1 -1
  74. package/dist/session/programmatic.js +434 -12
  75. package/dist/session/routes.d.ts +26 -0
  76. package/dist/session/routes.d.ts.map +1 -1
  77. package/dist/session/routes.js +117 -25
  78. package/dist/session/rpc.d.ts +24 -1
  79. package/dist/session/rpc.d.ts.map +1 -1
  80. package/dist/session/rpc.js +65 -98
  81. package/dist/session/shell-socket.d.ts +76 -0
  82. package/dist/session/shell-socket.d.ts.map +1 -0
  83. package/dist/session/shell-socket.js +172 -0
  84. package/dist/session/start-real-vite.js +1 -1
  85. package/dist/session/supervisor-op.d.ts +42 -0
  86. package/dist/session/supervisor-op.d.ts.map +1 -0
  87. package/dist/session/supervisor-op.js +96 -0
  88. package/dist/session/supervisor-rpc.d.ts +6 -10
  89. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  90. package/dist/session/supervisor-rpc.js +91 -71
  91. package/dist/session/ws.d.ts.map +1 -1
  92. package/dist/session/ws.js +19 -4
  93. package/package.json +4 -4
  94. package/public/_assets/runtime/{node-shims-fea906843f6d594e.js → node-shims-9e1380d9ee430fcc.js} +320 -5
  95. package/public/s/index.html +101 -5
@@ -10,12 +10,16 @@ import { listInstalledRuntimes, } from '@nimbus-sh/core/runtime/installed-runtim
10
10
  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
- import { PortRegistry } from '@nimbus-sh/core/runtime/port-registry.js';
13
+ import { PortRegistry, createPortCapability } from '@nimbus-sh/core/runtime/port-registry.js';
14
14
  import { CRED_KERNEL } 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, restorePortCapability, } from './port-capability.js';
18
+ import { clearPortCapability, isValidAppName, persistPortCapability, PortRecordSchema, portRecordKey, readPortReservation, readPortReservationByName, readPortReservationByOwner, reservePort, restorePortCapability, rotatePortCapability, } from './port-capability.js';
19
+ import { PORT_CAPABILITY_KEY_PREFIX } from './keys.js';
20
+ import { bindPublicPortCapability, unbindPublicPortCapability } from '../router/public-directory.js';
21
+ import { buildPreviewHost, buildPublicPreviewHost, isPreviewHostSafeSid, readPreviewHostSuffix, } from '../_shared/preview-host.js';
22
+ import { RESTART_POLICY_ENV } from '../facets/manager.js';
19
23
  import { GENERATION_KEY, assumeGeneration, generation } from '@nimbus-sh/fabric/generation.js';
20
24
  import { HeadlessTerminal, Shell } from '@nimbus-sh/core/substrate/lifo/index.js';
21
25
  const ShellIdSchema = z.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
@@ -169,8 +173,8 @@ function runtimeDeps(self) {
169
173
  export async function ensureProgrammaticReady(self, options = {}) {
170
174
  if (!self.shell) {
171
175
  await self.initSession(makeHeadlessWebSocket());
172
- // Programmatic boot owns no real terminal socket. Mark the session
173
- // drained so a later browser /ws can warm-join instead of 409ing.
176
+ // A programmatic boot owns no terminal socket, which is what
177
+ // 'drained' reports to the diagnostics and the recovery ring.
174
178
  self._b4Phase = 'drained';
175
179
  }
176
180
  else {
@@ -350,7 +354,16 @@ function collectJobOutput(self, pid) {
350
354
  */
351
355
  export async function rpcStartProcess(self, command, options = {}) {
352
356
  await ensureProgrammaticReady(self, options);
353
- return withShellState(self, options, true, (scoped) => startOnShell(self, command, options, scoped));
357
+ if (options.restart !== undefined && options.restart !== 'never' && options.restart !== 'on-failure') {
358
+ throw new Error(`startProcess: restart must be 'never' or 'on-failure', got ${String(options.restart)}`);
359
+ }
360
+ // The policy rides the command's environment: the resident the command
361
+ // starts reads it off its env at launch and journals it, so the shell,
362
+ // the SDK and a child_process spawn all carry it the same way.
363
+ const withPolicy = options.restart === 'on-failure'
364
+ ? { ...options, env: { ...(options.env ?? {}), [RESTART_POLICY_ENV]: 'on-failure' } }
365
+ : options;
366
+ return withShellState(self, withPolicy, true, (scoped) => startOnShell(self, command, withPolicy, scoped));
354
367
  }
355
368
  async function startOnShell(self, command, options, scoped) {
356
369
  const job = startShellJob(self, command, options, { background: true }, scoped);
@@ -522,28 +535,438 @@ export async function rpcListPorts(self) {
522
535
  await Promise.all(entries.map((entry) => persistPortCapability(self, entry.port, entry.capability)));
523
536
  return entries.map(serializePort);
524
537
  }
525
- export async function rpcExposePort(self, port) {
538
+ /**
539
+ * Browser-facing URL for an application, built inside the session: the
540
+ * host form when the deployment carries a preview suffix (name first, port
541
+ * otherwise; the public bearer form when public and a capability exists),
542
+ * else the path form on the origin the session was last reached at. Null
543
+ * when the session has never been reached over HTTP and has no suffix —
544
+ * an embedder builds its own from the port and capability in that case.
545
+ */
546
+ export function appUrl(self, app) {
547
+ const sid = sessionIdOf(self);
548
+ const suffix = readPreviewHostSuffix(self.env);
549
+ if (suffix !== null && sid !== null && isPreviewHostSafeSid(sid)) {
550
+ const label = app.name ?? app.port;
551
+ if (app.visibility === 'public' && typeof app.capability === 'string') {
552
+ return `https://${buildPublicPreviewHost(sid, label, app.capability, suffix)}/`;
553
+ }
554
+ return `https://${buildPreviewHost(sid, label, suffix)}/`;
555
+ }
556
+ const origin = self.sessionOrigin;
557
+ if (!origin || sid === null)
558
+ return null;
559
+ const base = self.sessionBasePath || `/s/${sid}`;
560
+ return app.name ? `${origin}${base}/app/${app.name}/` : `${origin}${base}/port/${app.port}/`;
561
+ }
562
+ /** The sid this DO serves: the last segment of its name, `tn:sub:sid`. */
563
+ function sessionIdOf(self) {
564
+ const name = self.ctx.id?.name;
565
+ if (typeof name === 'string' && name.length > 0) {
566
+ const cut = name.lastIndexOf(':');
567
+ if (cut > 0)
568
+ return name.slice(cut + 1);
569
+ }
570
+ const fromBase = (self.sessionBasePath || '').replace(/^\/s\//, '');
571
+ return fromBase.length > 0 ? fromBase : null;
572
+ }
573
+ /**
574
+ * The port-centric surface, kept for every caller that has a port and
575
+ * nothing else — a dev server or a process outside the
576
+ * resident lifecycle. When the port's occupant carries an identity the
577
+ * exposure is the same lazy reservation `apps.expose` makes; when it does
578
+ * not, this compatibility path writes the row port-only, as before. One implementation:
579
+ * `applyExposure` below.
580
+ */
581
+ export async function rpcExposePort(self, port, options) {
526
582
  await ensureProgrammaticReady(self);
527
583
  const n = Number(port);
528
584
  const entry = self.portRegistry.get(n);
529
- if (entry)
530
- await persistPortCapability(self, n, entry.capability);
585
+ const identity = entry === undefined ? null : await self.facetManager.residentIdentity(entry.pid);
586
+ if (identity?.ephemeral) {
587
+ throw new Error(`port ${n} is served by the second instance of its program, which is not the durable one`);
588
+ }
589
+ const exposed = await applyExposure(self, n, identity?.owner ?? null, options ?? {});
531
590
  return {
532
591
  port: n,
533
592
  listening: !!entry,
534
593
  pid: entry?.pid ?? null,
535
594
  registeredAt: entry?.registeredAt ?? null,
536
- capability: entry?.capability ?? null,
595
+ capability: exposed.capability,
596
+ visibility: exposed.visibility,
597
+ owner: exposed.owner,
598
+ name: exposed.name,
599
+ };
600
+ }
601
+ /**
602
+ * The identity-centric surface: the target names a running application —
603
+ * by port, by pid, or by name — and the exposure is what makes it durable
604
+ * under its identity: the port is reserved for the owner lazily, the
605
+ * capability minted when public and bound in the directory, the name
606
+ * stored on the reservation. Returns the address the caller can reach.
607
+ */
608
+ export async function rpcExposeApp(self, target, options = {}) {
609
+ await ensureProgrammaticReady(self);
610
+ const resolved = await resolveAppTarget(self, target);
611
+ if (resolved.port === null) {
612
+ throw new Error(`${describeTarget(target)} has not bound a port yet — expose it once it is listening`);
613
+ }
614
+ if (resolved.owner === null) {
615
+ throw new Error(`${describeTarget(target)} has no launch record to bind an identity to — `
616
+ + 'only journalled residents carry one; use ports.expose for a bare port');
617
+ }
618
+ const exposed = await applyExposure(self, resolved.port, resolved.owner, options);
619
+ const live = self.portRegistry.get(resolved.port);
620
+ return {
621
+ owner: resolved.owner,
622
+ name: exposed.name,
623
+ port: resolved.port,
624
+ pid: live?.pid ?? null,
625
+ capability: exposed.capability,
626
+ visibility: exposed.visibility,
627
+ url: appUrl(self, { port: resolved.port, ...exposed }),
628
+ };
629
+ }
630
+ /**
631
+ * The one exposure implementation. With an owner: reserve the port for it
632
+ * (a conflict if another owner holds it), carrying the name and visibility
633
+ * onto the reservation and adopting the live capability so the URL in
634
+ * circulation is the one the reservation re-adopts after a reset; public
635
+ * exposures mint a capability if none is live yet and bind the directory.
636
+ * Without an owner: the row is written port-only, exactly as `exposePort`
637
+ * always did.
638
+ */
639
+ async function applyExposure(self, port, owner, options) {
640
+ if (options.name !== undefined && !isValidAppName(options.name)) {
641
+ throw new Error(`'${options.name}' is not a valid app name: one DNS label, not all digits, not 24 hex`);
642
+ }
643
+ const entry = self.portRegistry.get(port);
644
+ if (owner !== null) {
645
+ await assertServingOwner(self, port, owner);
646
+ const occupied = new Set(self.portRegistry.getAll().map((live) => live.port));
647
+ occupied.delete(port);
648
+ await reservePort(self.ctx, {
649
+ owner,
650
+ kind: 'derived',
651
+ preferredPort: port,
652
+ occupiedPorts: occupied,
653
+ ...(entry !== undefined ? { capability: entry.capability } : {}),
654
+ ...(options.visibility !== undefined ? { visibility: options.visibility } : {}),
655
+ ...(options.name !== undefined ? { name: options.name } : {}),
656
+ });
657
+ }
658
+ else {
659
+ if (entry)
660
+ await persistPortCapability(self, port, entry.capability);
661
+ // A visibility or name choice is a write on the stored row, not a
662
+ // reservation claim: owner and capability stay exactly as they were.
663
+ if (options.visibility !== undefined || options.name !== undefined) {
664
+ await self.ctx.storage.transaction(async (txn) => {
665
+ const stored = await readPortReservation({ storage: txn }, port);
666
+ if (stored === null)
667
+ return;
668
+ const visibility = options.visibility ?? stored.visibility;
669
+ const name = options.name ?? stored.name;
670
+ if (visibility === stored.visibility && name === stored.name)
671
+ return;
672
+ await txn.put(portRecordKey(port), {
673
+ owner: stored.owner,
674
+ kind: stored.kind,
675
+ capability: stored.capability,
676
+ visibility,
677
+ ...(name !== undefined ? { name } : {}),
678
+ });
679
+ });
680
+ }
681
+ }
682
+ let record = await readPortReservation(self.ctx, port);
683
+ if (record?.visibility === 'public' && record.capability === null && owner !== null) {
684
+ // Public before the application has bound: mint now, so the URL exists
685
+ // and the binding re-adopts it, exactly as ensureDurableApp does.
686
+ await reservePort(self.ctx, {
687
+ owner, preferredPort: port, occupiedPorts: new Set(), capability: createPortCapability(),
688
+ });
689
+ record = await readPortReservation(self.ctx, port);
690
+ }
691
+ // The live registration must answer the stored capability, or a URL
692
+ // minted before the bind 404s until the next restore.
693
+ if (record?.capability !== null && record?.capability !== undefined) {
694
+ const capability = record.capability;
695
+ if (record.owner !== null) {
696
+ await assertServingOwner(self, port, record.owner, () => self.portRegistry.restoreCapability(port, capability));
697
+ }
698
+ else {
699
+ self.portRegistry.restoreCapability(port, capability);
700
+ }
701
+ }
702
+ if (record?.visibility === 'public' && record.capability !== null) {
703
+ // A port that goes public must be resolvable: the capability the URL
704
+ // carries has to name this session in the directory, and a non-legacy
705
+ // deployment without the binding is refused loudly — never half-public.
706
+ await bindPublicPortCapability(self, record.capability, port, record.name);
707
+ }
708
+ return {
709
+ owner: record?.owner ?? owner,
710
+ name: record?.name ?? null,
711
+ capability: record?.capability ?? entry?.capability ?? null,
712
+ visibility: record?.visibility ?? 'scoped',
713
+ };
714
+ }
715
+ /**
716
+ * Mint a new capability for the application and rebind the directory:
717
+ * every URL built on the old one stops resolving. The registry adopts the
718
+ * new value at once if the port is live, so the new URL answers without
719
+ * waiting for a restore.
720
+ */
721
+ export async function rpcRotateLink(self, target) {
722
+ await ensureProgrammaticReady(self);
723
+ const resolved = await resolveAppTarget(self, target);
724
+ if (resolved.port === null || resolved.reservation === null) {
725
+ throw new Error(`${describeTarget(target)} is not exposed — nothing to rotate`);
726
+ }
727
+ const previous = resolved.reservation;
728
+ if (previous.owner !== null)
729
+ await assertServingOwner(self, resolved.port, previous.owner);
730
+ if (previous.visibility === 'public' && previous.capability !== null) {
731
+ await unbindPublicPortCapability(self, previous.capability);
732
+ }
733
+ if (previous.owner !== null)
734
+ await assertServingOwner(self, resolved.port, previous.owner);
735
+ const capability = await rotatePortCapability(self, resolved.port, createPortCapability());
736
+ if (capability === null)
737
+ throw new Error(`${describeTarget(target)} is not exposed — nothing to rotate`);
738
+ if (previous.owner !== null) {
739
+ await assertServingOwner(self, resolved.port, previous.owner, () => self.portRegistry.restoreCapability(resolved.port, capability));
740
+ }
741
+ else {
742
+ self.portRegistry.restoreCapability(resolved.port, capability);
743
+ }
744
+ if (previous.visibility === 'public') {
745
+ await bindPublicPortCapability(self, capability, resolved.port, previous.name);
746
+ }
747
+ const live = self.portRegistry.get(resolved.port);
748
+ return {
749
+ owner: resolved.owner ?? previous.owner ?? '',
750
+ name: previous.name ?? null,
751
+ port: resolved.port,
752
+ pid: live?.pid ?? null,
753
+ capability,
754
+ visibility: previous.visibility,
755
+ url: appUrl(self, { port: resolved.port, name: previous.name, capability, visibility: previous.visibility }),
756
+ };
757
+ }
758
+ /** A name or owner identifies a reservation, never whoever happens to occupy its port. */
759
+ async function assertServingOwner(self, port, owner, adopt) {
760
+ for (;;) {
761
+ const live = self.portRegistry.get(port);
762
+ if (live === undefined)
763
+ return;
764
+ const identity = await self.facetManager.residentIdentity(live.pid);
765
+ if (self.portRegistry.get(port)?.pid !== live.pid)
766
+ continue;
767
+ if (identity?.owner !== owner || identity.ephemeral) {
768
+ throw new Error(`port ${port} is served by a different process (owner ${identity?.owner ?? 'none'})`);
769
+ }
770
+ adopt?.();
771
+ return;
772
+ }
773
+ }
774
+ /** Every stamped identity, with the URL each is reachable at. */
775
+ export async function rpcListApps(self) {
776
+ await ensureProgrammaticReady(self);
777
+ const apps = await self.facetManager.listResidentApps();
778
+ return apps.map((app) => ({
779
+ ...app,
780
+ url: app.port === null ? null : appUrl(self, { ...app, port: app.port }),
781
+ }));
782
+ }
783
+ /**
784
+ * End an application: kill its live pids, release the reservation, purge
785
+ * its journal rows, free the durable slot and its storage, unbind the
786
+ * directory — `removeDurableApp`, addressed by any target.
787
+ */
788
+ export async function rpcRemoveApp(self, target) {
789
+ await ensureProgrammaticReady(self);
790
+ const resolved = await resolveAppTarget(self, target);
791
+ if (resolved.owner === null) {
792
+ throw new Error(`${describeTarget(target)} names no application — nothing to remove`);
793
+ }
794
+ return rpcRemoveDurableApp(self, resolved.owner);
795
+ }
796
+ function describeTarget(target) {
797
+ if (typeof target === 'number')
798
+ return `target ${target}`;
799
+ if (typeof target === 'string')
800
+ return `'${target}'`;
801
+ if ('port' in target)
802
+ return `port ${target.port}`;
803
+ if ('pid' in target)
804
+ return `pid ${target.pid}`;
805
+ if ('name' in target)
806
+ return `app '${target.name}'`;
807
+ return `owner '${target.owner}'`;
808
+ }
809
+ /**
810
+ * Resolve an app target to what the verbs need: the owner (from the
811
+ * serving pid's journal row, or the reservation), the port, and the
812
+ * reservation if one exists. A bare number is a port when something
813
+ * listens on it or a reservation names it, else a pid; a string is a name
814
+ * first, then an owner.
815
+ */
816
+ async function resolveAppTarget(self, target) {
817
+ const fm = self.facetManager;
818
+ const byPort = async (port) => {
819
+ const live = self.portRegistry.get(port);
820
+ const reservation = await readPortReservation(self.ctx, port);
821
+ const identity = live === undefined ? null : await fm.residentIdentity(live.pid);
822
+ if (identity?.ephemeral) {
823
+ throw new Error(`port ${port} is served by the second instance of its program, which is not the durable one`);
824
+ }
825
+ return { owner: identity?.owner ?? reservation?.owner ?? null, port, reservation };
826
+ };
827
+ const byPid = async (pid) => {
828
+ const identity = await fm.residentIdentity(pid);
829
+ if (identity === null)
830
+ return null;
831
+ if (identity.ephemeral) {
832
+ throw new Error(`pid ${pid} is the second instance of its program, which is not the durable one`);
833
+ }
834
+ const port = self.portRegistry.getAll().find((live) => live.pid === pid)?.port ?? identity.port ?? null;
835
+ const reservation = port === null ? null : await readPortReservation(self.ctx, port);
836
+ return { owner: identity.owner ?? null, port, reservation };
837
+ };
838
+ const byOwner = async (owner) => {
839
+ const held = await readPortReservationByOwner(self.ctx, owner);
840
+ if (held !== null)
841
+ return { owner, port: held.port, reservation: held.reservation };
842
+ const app = (await fm.listResidentApps()).find((candidate) => candidate.owner === owner);
843
+ return app === undefined ? null : { owner, port: app.port, reservation: null };
844
+ };
845
+ const byName = async (name) => {
846
+ const held = await readPortReservationByName(self.ctx, name);
847
+ return held === null ? null : { owner: held.reservation.owner, port: held.port, reservation: held.reservation };
848
+ };
849
+ if (typeof target === 'number') {
850
+ if (!Number.isInteger(target) || target <= 0)
851
+ throw new Error(`target ${target} is not a port or a pid`);
852
+ if (self.portRegistry.has(target) || (await readPortReservation(self.ctx, target)) !== null) {
853
+ return byPort(target);
854
+ }
855
+ const asPid = await byPid(target);
856
+ if (asPid !== null)
857
+ return asPid;
858
+ if (target <= 65535)
859
+ return byPort(target);
860
+ throw new Error(`nothing listens on port ${target} and no process ${target} exists`);
861
+ }
862
+ if (typeof target === 'string') {
863
+ const asName = await byName(target);
864
+ if (asName !== null)
865
+ return asName;
866
+ const asOwner = await byOwner(target);
867
+ if (asOwner !== null)
868
+ return asOwner;
869
+ throw new Error(`no application is named or owned '${target}'`);
870
+ }
871
+ if ('port' in target)
872
+ return byPort(Number(target.port));
873
+ if ('pid' in target) {
874
+ const asPid = await byPid(Number(target.pid));
875
+ if (asPid === null) {
876
+ throw new Error(`pid ${target.pid} has no launch record — only journalled residents carry one`);
877
+ }
878
+ return asPid;
879
+ }
880
+ if ('name' in target) {
881
+ const asName = await byName(target.name);
882
+ if (asName === null)
883
+ throw new Error(`no application is named '${target.name}'`);
884
+ return asName;
885
+ }
886
+ const asOwner = await byOwner(target.owner);
887
+ if (asOwner === null)
888
+ throw new Error(`no application is owned by '${target.owner}'`);
889
+ return asOwner;
890
+ }
891
+ /**
892
+ * The embedder's durable-application seam: reserve (or re-answer) the port
893
+ * `owner` holds, minting the capability the application's public URL is
894
+ * built on — minted HERE, stored on the reservation, so a URL handed out
895
+ * before the application has ever booted is the one its eventual binding
896
+ * re-adopts, and the one a reset re-adopts again. Answers the port, the
897
+ * capability, and the record's visibility.
898
+ */
899
+ export async function rpcEnsureDurableApp(self, input) {
900
+ await ensureProgrammaticReady(self);
901
+ const owner = input.owner;
902
+ if (typeof owner !== 'string' || owner.length === 0) {
903
+ throw new Error('ensureDurableApp: owner must be a non-empty string');
904
+ }
905
+ const occupied = new Set(self.portRegistry.getAll().map((entry) => entry.port));
906
+ const port = await reservePort(self.ctx, {
907
+ owner,
908
+ preferredPort: input.preferredPort,
909
+ occupiedPorts: occupied,
910
+ // Minted here, not at boot: the URL is built from this capability, and
911
+ // re-drive re-adopts it out of the same row.
912
+ capability: createPortCapability(),
913
+ ...(input.visibility !== undefined ? { visibility: input.visibility } : {}),
914
+ });
915
+ const record = await readPortReservation(self.ctx, port);
916
+ if (record?.visibility === 'public' && record.capability !== null) {
917
+ // The URL is built on this capability before the app has ever run:
918
+ // publish it to the directory now, loudly refusing a deployment whose
919
+ // binding is missing rather than minting an unroutable URL.
920
+ await bindPublicPortCapability(self, record.capability, port);
921
+ }
922
+ return {
923
+ port,
924
+ capability: record?.capability ?? null,
925
+ visibility: record?.visibility ?? 'scoped',
537
926
  };
538
927
  }
539
928
  export async function rpcUnexposePort(self, port) {
540
929
  await ensureProgrammaticReady(self);
541
930
  const n = Number(port);
542
931
  // Before the unregister, so a crash between the two leaves a dead token
543
- // rather than a live one.
932
+ // rather than a live one. A public capability also leaves the directory.
933
+ const record = await readPortReservation(self.ctx, n);
544
934
  await clearPortCapability(self, n);
935
+ if (record?.visibility === 'public' && record.capability !== null) {
936
+ await unbindPublicPortCapability(self, record.capability);
937
+ }
545
938
  return { port: n, ok: self.portRegistry.unregister(n) };
546
939
  }
940
+ /**
941
+ * End a durable application's contract. The reservation's port is read first
942
+ * so the caller learns which durable address was released; the manager then
943
+ * kills every launch the owner claims, purges its journal rows, releases the
944
+ * port, and frees the durable slot. `removed` is false only when no durable
945
+ * application held that owner at all.
946
+ */
947
+ export async function rpcRemoveDurableApp(self, owner) {
948
+ await ensureProgrammaticReady(self);
949
+ if (typeof owner !== 'string' || owner.length === 0) {
950
+ throw new Error('removeDurableApp: owner must be a non-empty string');
951
+ }
952
+ // Which durable address is being released — the reservation's row names it
953
+ // before the manager retires it.
954
+ const rows = await self.ctx.storage.list({ prefix: PORT_CAPABILITY_KEY_PREFIX });
955
+ let port = null;
956
+ for (const [key, record] of rows) {
957
+ const parsed = PortRecordSchema.safeParse(record);
958
+ if (!parsed.success || parsed.data.owner !== owner)
959
+ continue;
960
+ const n = Number(key.slice(PORT_CAPABILITY_KEY_PREFIX.length));
961
+ if (Number.isInteger(n) && n > 0) {
962
+ port = n;
963
+ break;
964
+ }
965
+ }
966
+ self.ensureFacetManager();
967
+ const removed = await self.facetManager.removeDurableApp(owner);
968
+ return { owner, removed, port };
969
+ }
547
970
  /**
548
971
  * Route an embedder request that carries a port capability. The embedder has
549
972
  * authenticated the capability at its edge and stripped its own credentials,
@@ -798,8 +1221,7 @@ function resetInMemorySessionState(self) {
798
1221
  self.nimbusWrangler = null;
799
1222
  self.npmInstaller = null;
800
1223
  self.fetchProxyEntrypoint = null;
801
- self.runtimeFsBridges?.clear();
802
- self.runtimeFsBridges = null;
1224
+ self._supervisorOps = null;
803
1225
  self._cpRegistry = null;
804
1226
  self._viteShimPid = null;
805
1227
  self._viteShimPort = null;
@@ -24,8 +24,34 @@
24
24
  * (~30 sites). RoutesHost = any pragmatic deviation, like InitHost in S6.
25
25
  */
26
26
  type RoutesHost = any;
27
+ /**
28
+ * Route a request to whatever is listening on a session port.
29
+ *
30
+ * The one implementation behind every port-addressed surface: `/port/<n>/`,
31
+ * `/preview/?port=N`, and the `<port>--<sid>` preview hostname, which the
32
+ * router forwards as `/port/<n>/`. They differ only in how the port and the
33
+ * inner path are spelled, so they must not differ in what answers.
34
+ *
35
+ * `mountBase` is the public URL prefix the served app is mounted at for THIS
36
+ * request — '' for a root-mounted `<port>--<sid>` host, '/s/<sid>/preview' for
37
+ * the preview path. The in-process Cirrus dev server rewrites base-relative
38
+ * URLs (module URLs, <base href>, BASE_URL, router basename), so it is handed
39
+ * the base directly: the generic port proxy strips the Nimbus base header at
40
+ * the untrusted-code boundary and cannot carry it, and a plain user server on
41
+ * any other port is mounted at root and needs no rewriting.
42
+ */
43
+ export declare function routeToSessionPort(self: RoutesHost, port: number, request: Request, innerPath: string, mountBase: string, capability?: string): Promise<Response>;
27
44
  /** Route a capability-authenticated embedder request to a guest HTTP server. */
28
45
  export declare function routeCapabilityPort(self: RoutesHost, port: number, capability: string, request: Request, innerPath: string): Promise<Response>;
46
+ /**
47
+ * The name-addressed door: `/app/<name>/…` — what the scoped `<name>--<sid>`
48
+ * host is forwarded as, and reachable in path form as `/s/<sid>/app/<name>/`.
49
+ * The name is a reservation alias only this session's records know, so it
50
+ * is resolved here and the request continues exactly as the port form would
51
+ * — same routing, same durable re-drive, same capability gate. A name
52
+ * nothing holds is a 404, never a guess.
53
+ */
54
+ export declare function routeToSessionApp(self: RoutesHost, name: string, request: Request, innerPath: string, capability?: string): Promise<Response>;
29
55
  export declare function handleFetch(self: RoutesHost, request: Request): Promise<Response>;
30
56
  export {};
31
57
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/session/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAqDH,KAAK,UAAU,GAAG,GAAG,CAAC;AA6LtB,gFAAgF;AAChF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC,CAEnB;AAyGD,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8+BvF"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/session/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAyDH,KAAK,UAAU,GAAG,GAAG,CAAC;AA2HtB;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAyDnB;AA2BD,gFAAgF;AAChF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC,CAEnB;AAeD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAMnB;AA4FD,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmhCvF"}