@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.
- package/README.md +46 -43
- package/dist/_shared/preview-host.d.ts +33 -2
- package/dist/_shared/preview-host.d.ts.map +1 -1
- package/dist/_shared/preview-host.js +73 -8
- package/dist/_shared/session-router.d.ts +43 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +59 -12
- package/dist/facets/durable-images.d.ts +59 -0
- package/dist/facets/durable-images.d.ts.map +1 -0
- package/dist/facets/durable-images.js +94 -0
- package/dist/facets/durable-slots.d.ts +45 -0
- package/dist/facets/durable-slots.d.ts.map +1 -0
- package/dist/facets/durable-slots.js +95 -0
- package/dist/facets/esbuild-transform.d.ts +10 -0
- package/dist/facets/esbuild-transform.d.ts.map +1 -0
- package/dist/facets/esbuild-transform.js +36 -0
- package/dist/facets/manager.d.ts +244 -6
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +901 -117
- package/dist/facets/resident-identity.d.ts +23 -0
- package/dist/facets/resident-identity.d.ts.map +1 -0
- package/dist/facets/resident-identity.js +31 -0
- package/dist/git/commands.d.ts +22 -1
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +429 -425
- package/dist/git-bundle.generated.d.ts +2 -2
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +97 -3
- package/dist/router/public-directory-do.d.ts +17 -0
- package/dist/router/public-directory-do.d.ts.map +1 -0
- package/dist/router/public-directory-do.js +16 -0
- package/dist/router/public-directory.d.ts +79 -0
- package/dist/router/public-directory.d.ts.map +1 -0
- package/dist/router/public-directory.js +122 -0
- package/dist/router/remote-api.d.ts +27 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +48 -2
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +3 -1
- package/dist/runtime/node-shims.d.ts +4 -1
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +324 -6
- package/dist/runtime/package-manager.d.ts +44 -1
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +168 -3
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +2 -0
- package/dist/session/agent.d.ts +11 -0
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/agent.js +28 -1
- package/dist/session/helpers.js +1 -1
- package/dist/session/init-phases.d.ts +17 -23
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +17 -41
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +28 -15
- package/dist/session/keys.d.ts +12 -0
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +12 -0
- package/dist/session/nimbus-session.d.ts +63 -5
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +131 -17
- package/dist/session/port-capability.d.ts +141 -8
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +265 -8
- package/dist/session/programmatic.d.ts +145 -3
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +434 -12
- package/dist/session/routes.d.ts +26 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +117 -25
- package/dist/session/rpc.d.ts +24 -1
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +65 -98
- package/dist/session/shell-socket.d.ts +76 -0
- package/dist/session/shell-socket.d.ts.map +1 -0
- package/dist/session/shell-socket.js +172 -0
- package/dist/session/start-real-vite.js +1 -1
- package/dist/session/supervisor-op.d.ts +42 -0
- package/dist/session/supervisor-op.d.ts.map +1 -0
- package/dist/session/supervisor-op.js +96 -0
- package/dist/session/supervisor-rpc.d.ts +6 -10
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +91 -71
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +19 -4
- package/package.json +4 -4
- package/public/_assets/runtime/{node-shims-fea906843f6d594e.js → node-shims-9e1380d9ee430fcc.js} +320 -5
- 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
|
-
//
|
|
173
|
-
// drained
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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:
|
|
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.
|
|
802
|
-
self.runtimeFsBridges = null;
|
|
1224
|
+
self._supervisorOps = null;
|
|
803
1225
|
self._cpRegistry = null;
|
|
804
1226
|
self._viteShimPid = null;
|
|
805
1227
|
self._viteShimPort = null;
|
package/dist/session/routes.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|