@lamalibre/create-lamaste 4.5.0 → 4.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/package.json +2 -2
- package/scripts/lamaste-priv +34 -2
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +2 -2
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +65 -7
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +112 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +19 -1
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +21 -19
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +29 -10
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +103 -22
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +63 -6
- package/vendor/server/src/commands/sites.js +15 -1
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +9 -4
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/gatekeeper-client.js +18 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/nginx.js +269 -2
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +128 -5
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +39 -6
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/user-surface.js +48 -27
- package/vendor/serverd/src/lib/vhost-reconcile.js +5 -3
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +11 -20
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
settingsSchemaDiff,
|
|
40
40
|
validateManifest,
|
|
41
41
|
} from '@lamalibre/lamaste';
|
|
42
|
-
import { pluginHostingPhase } from '@lamalibre/lamaste/server';
|
|
42
|
+
import { pluginHostingPhase, webTreeManifestOfDirectory } from '@lamalibre/lamaste/server';
|
|
43
43
|
import { httpError } from './app-error.js';
|
|
44
44
|
import { setPluginCapabilities } from './mtls.js';
|
|
45
45
|
import {
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
uninstallPlugin,
|
|
52
52
|
writePluginRow,
|
|
53
53
|
} from './plugins.js';
|
|
54
|
-
import { pinAgentHalf, readRegistryPackage } from './plugin-packages.js';
|
|
54
|
+
import { installedWebTreeDir, pinAgentHalf, readRegistryPackage } from './plugin-packages.js';
|
|
55
55
|
import {
|
|
56
56
|
DIGEST_UNAPPROVED,
|
|
57
57
|
IDENTITY_CHANGED,
|
|
@@ -314,7 +314,8 @@ function identityApprovalRequired(identity) {
|
|
|
314
314
|
* in for an administrator on the first reason alone:
|
|
315
315
|
*
|
|
316
316
|
* - `digest.unapproved` — what it brings (relay hooks, the agent half
|
|
317
|
-
* Lamaste installs, its identity block) is not what was
|
|
317
|
+
* Lamaste installs, its identity block, its web tree) is not what was
|
|
318
|
+
* approved;
|
|
318
319
|
* - `settings.secret-to-plain` — {@link settingsApprovalRequired};
|
|
319
320
|
* - `identity.changed` — {@link identityApprovalRequired}.
|
|
320
321
|
*
|
|
@@ -334,6 +335,7 @@ function approvalRequiredFor(staged, state, settings, identity) {
|
|
|
334
335
|
relay: staged.relaySet,
|
|
335
336
|
agentHalf: staged.agentHalf,
|
|
336
337
|
identity: staged.identity,
|
|
338
|
+
web: staged.webTree,
|
|
337
339
|
}),
|
|
338
340
|
]),
|
|
339
341
|
...settingsApprovalRequired(settings),
|
|
@@ -446,6 +448,10 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
446
448
|
}
|
|
447
449
|
entrySha256 = hookEntrySha256(moduleBytes);
|
|
448
450
|
}
|
|
451
|
+
// A web surface's tree, read from the tarball: every rule checked now,
|
|
452
|
+
// its digest approved with the rest.
|
|
453
|
+
const webRoot = manifest.userSurface?.web?.root;
|
|
454
|
+
const webTree = webRoot ? await staged.readWebTree(webRoot) : null;
|
|
449
455
|
// The staged version as a registry entry, so it is judged exactly as the
|
|
450
456
|
// installed one will be.
|
|
451
457
|
const entry = {
|
|
@@ -457,6 +463,8 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
457
463
|
agentHalfPin,
|
|
458
464
|
settings: manifest.settings,
|
|
459
465
|
identity: identityOf(manifest),
|
|
466
|
+
userSurface: manifest.userSurface,
|
|
467
|
+
webTreeSha256: webTree?.sha256 ?? null,
|
|
460
468
|
};
|
|
461
469
|
const approval = installedApproval(entry);
|
|
462
470
|
return {
|
|
@@ -470,6 +478,8 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
470
478
|
agentHalf: approval.agentHalf,
|
|
471
479
|
// The staged identity block, normalized (null: it declares none).
|
|
472
480
|
identity: approval.identity,
|
|
481
|
+
// The staged web tree's manifest (null: it declares no web surface).
|
|
482
|
+
webTree,
|
|
473
483
|
// The relay hook set's own digest — what the runner holds.
|
|
474
484
|
relayDigest: approval.relay?.digest ?? null,
|
|
475
485
|
approvalRequired: approval.required,
|
|
@@ -514,6 +524,104 @@ function identityPreview(plugin, manifest) {
|
|
|
514
524
|
return { before, after, ...identityDiff(before, after) };
|
|
515
525
|
}
|
|
516
526
|
|
|
527
|
+
/**
|
|
528
|
+
* How a staged version changes the web tree, for the update's preview: the
|
|
529
|
+
* installed version's tree digest (`before`), the staged tree (`after`:
|
|
530
|
+
* its digest, file count and size) and what changes by path (`changes`,
|
|
531
|
+
* {@link webTreeChanges}) — null when neither version declares a web
|
|
532
|
+
* surface.
|
|
533
|
+
*
|
|
534
|
+
* @param {object} plugin the installed registry entry
|
|
535
|
+
* @param {{ webTree: import('@lamalibre/lamaste').WebTreeManifest | null }} staged
|
|
536
|
+
*/
|
|
537
|
+
async function webPreview(plugin, staged) {
|
|
538
|
+
const before = installedTreeSha256(plugin);
|
|
539
|
+
const after = staged.webTree
|
|
540
|
+
? {
|
|
541
|
+
sha256: staged.webTree.sha256,
|
|
542
|
+
files: staged.webTree.files.length,
|
|
543
|
+
totalBytes: staged.webTree.totalBytes,
|
|
544
|
+
}
|
|
545
|
+
: null;
|
|
546
|
+
if (before === null && after === null) return null;
|
|
547
|
+
return { before, after, changes: await webTreeChanges(plugin, staged.webTree) };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The approved web tree of the installed version, or null when it declares none. */
|
|
551
|
+
function installedTreeSha256(plugin) {
|
|
552
|
+
return plugin.userSurface?.web ? (plugin.webTreeSha256 ?? null) : null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** At most this many paths per list of a tree change (the counts are exact). */
|
|
556
|
+
const WEB_CHANGE_LIST_MAX = 200;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Files of a web tree an administrator should look at before approving: a
|
|
560
|
+
* document other than the shell's `index.html` (another page served on
|
|
561
|
+
* the relay's domain), an SVG (it runs script when opened), a manifest.
|
|
562
|
+
*/
|
|
563
|
+
function webFileNeedsReview(file) {
|
|
564
|
+
return (file.endsWith('.html') && file !== 'index.html') || /\.(?:svg|webmanifest)$/.test(file);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* What a staged version's web tree changes against the installed one's,
|
|
569
|
+
* by path: added, removed, changed (each `{ count, paths }`, the paths
|
|
570
|
+
* capped), and `review` — the added or changed paths
|
|
571
|
+
* {@link webFileNeedsReview} names. Null when the staged version declares
|
|
572
|
+
* no web tree, or when the installed tree cannot be read (the digests
|
|
573
|
+
* still tell them apart).
|
|
574
|
+
*
|
|
575
|
+
* @param {object} plugin the installed registry entry
|
|
576
|
+
* @param {import('@lamalibre/lamaste').WebTreeManifest | null} stagedTree
|
|
577
|
+
*/
|
|
578
|
+
async function webTreeChanges(plugin, stagedTree) {
|
|
579
|
+
if (!stagedTree) return null;
|
|
580
|
+
let installed = new Map();
|
|
581
|
+
if (installedTreeSha256(plugin) !== null) {
|
|
582
|
+
try {
|
|
583
|
+
const dir = await installedWebTreeDir(plugin.packageName, plugin);
|
|
584
|
+
const tree = await webTreeManifestOfDirectory(dir);
|
|
585
|
+
installed = new Map(tree.files.map((f) => [f.path, f.sha256]));
|
|
586
|
+
} catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const added = [];
|
|
591
|
+
const changed = [];
|
|
592
|
+
for (const file of stagedTree.files) {
|
|
593
|
+
const was = installed.get(file.path);
|
|
594
|
+
if (was === undefined) added.push(file.path);
|
|
595
|
+
else if (was !== file.sha256) changed.push(file.path);
|
|
596
|
+
}
|
|
597
|
+
const staged = new Set(stagedTree.files.map((f) => f.path));
|
|
598
|
+
const removed = [...installed.keys()].filter((p) => !staged.has(p));
|
|
599
|
+
const list = (paths) => ({ count: paths.length, paths: paths.slice(0, WEB_CHANGE_LIST_MAX) });
|
|
600
|
+
return {
|
|
601
|
+
added: list(added),
|
|
602
|
+
removed: list(removed),
|
|
603
|
+
changed: list(changed),
|
|
604
|
+
review: [...added, ...changed].filter(webFileNeedsReview).slice(0, WEB_CHANGE_LIST_MAX),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Why auto-approve may not stand in for an administrator on a staged
|
|
610
|
+
* version's web tree: it changes (or appears) — publisher code served to
|
|
611
|
+
* every user of the plugin on the relay's own domain is approved by a
|
|
612
|
+
* person, as an identity change is.
|
|
613
|
+
*/
|
|
614
|
+
function webTreeWithheld(plugin, staged) {
|
|
615
|
+
const after = staged.webTree?.sha256 ?? null;
|
|
616
|
+
if (after === null || after === installedTreeSha256(plugin)) return [];
|
|
617
|
+
return [
|
|
618
|
+
{
|
|
619
|
+
code: 'web.changed',
|
|
620
|
+
message: "the web tree changes: a shell served to the plugin's users on the relay's domain",
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
|
|
517
625
|
/**
|
|
518
626
|
* Update plugin `name` to `requestedVersion` (an exact version or a
|
|
519
627
|
* dist-tag; default `latest`). Returns `{ upToDate }`, `{ pendingUpdate }`
|
|
@@ -573,13 +681,14 @@ export async function updatePlugin(
|
|
|
573
681
|
approvedBy = actor;
|
|
574
682
|
} else if (approvalRequired.length > 0) {
|
|
575
683
|
// Auto-approve stands in for an administrator on a digest that changed,
|
|
576
|
-
// when the agent half changes nothing that runs as root on the agent
|
|
577
|
-
// and on nothing else: every other reason (a secret setting turned
|
|
684
|
+
// when the agent half changes nothing that runs as root on the agent and
|
|
685
|
+
// the web tree does not change — and on nothing else: every other reason (a secret setting turned
|
|
578
686
|
// plain, a role added or removed, an attribute widened) withholds it.
|
|
579
687
|
const digestChanged = approvalRequired.some((reason) => reason.code === DIGEST_UNAPPROVED);
|
|
580
688
|
const withheldReasons = state?.autoApprove
|
|
581
689
|
? [
|
|
582
690
|
...(digestChanged ? agentHalfWithheld(installedAgentHalf(plugin), staged.agentHalf) : []),
|
|
691
|
+
...(digestChanged ? webTreeWithheld(plugin, staged) : []),
|
|
583
692
|
...approvalRequired
|
|
584
693
|
.filter((reason) => reason.code !== DIGEST_UNAPPROVED)
|
|
585
694
|
.map(({ code, reason }) => ({ code, message: reason })),
|
|
@@ -591,6 +700,7 @@ export async function updatePlugin(
|
|
|
591
700
|
staged,
|
|
592
701
|
settings,
|
|
593
702
|
identity,
|
|
703
|
+
web: await webPreview(plugin, staged),
|
|
594
704
|
approvalRequired,
|
|
595
705
|
withheldReasons,
|
|
596
706
|
actor,
|
|
@@ -670,6 +780,7 @@ async function awaitApproval({
|
|
|
670
780
|
staged,
|
|
671
781
|
settings,
|
|
672
782
|
identity,
|
|
783
|
+
web,
|
|
673
784
|
approvalRequired,
|
|
674
785
|
withheldReasons,
|
|
675
786
|
actor,
|
|
@@ -688,6 +799,7 @@ async function awaitApproval({
|
|
|
688
799
|
: null,
|
|
689
800
|
settings,
|
|
690
801
|
identity,
|
|
802
|
+
web,
|
|
691
803
|
approvalRequired,
|
|
692
804
|
...(withheld.length > 0 ? { autoApproveWithheld: withheld } : {}),
|
|
693
805
|
};
|
|
@@ -787,6 +899,8 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
787
899
|
hooksEntrySha256: installed.hooksEntrySha256,
|
|
788
900
|
agent: installed.manifest.agent,
|
|
789
901
|
identity: identityOf(installed.manifest),
|
|
902
|
+
userSurface: installed.manifest.userSurface,
|
|
903
|
+
webTreeSha256: installed.webTreeSha256,
|
|
790
904
|
};
|
|
791
905
|
let approval;
|
|
792
906
|
try {
|
|
@@ -802,6 +916,7 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
802
916
|
installed.manifest,
|
|
803
917
|
installed.hooksEntrySha256,
|
|
804
918
|
staged.agentHalfPin,
|
|
919
|
+
installed.webTreeSha256,
|
|
805
920
|
);
|
|
806
921
|
if (approval.required) {
|
|
807
922
|
await recordApprovedSet(installedEntry, approval.digest, staged.version, approvedBy);
|
|
@@ -809,6 +924,13 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
809
924
|
setPluginCapabilities(await getPluginCapabilities());
|
|
810
925
|
createdSecrets = await syncOidcBestEffort(logger);
|
|
811
926
|
},
|
|
927
|
+
// The new version's tree served now, as part of the update: users of
|
|
928
|
+
// its site reach the new half without a gap, and a tree that cannot be
|
|
929
|
+
// served reverts the update rather than leave the site at 503.
|
|
930
|
+
serve: async () => {
|
|
931
|
+
const { syncWebSurfaceTree } = await import('./web-surface.js');
|
|
932
|
+
await syncWebSurfaceTree(plugin.route, { logger });
|
|
933
|
+
},
|
|
812
934
|
revert: async ({ applied }) => {
|
|
813
935
|
if (applied) {
|
|
814
936
|
const restored = await replacePluginPackage(
|
|
@@ -822,6 +944,7 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
822
944
|
restored.manifest,
|
|
823
945
|
restored.hooksEntrySha256,
|
|
824
946
|
plugin.agentHalfPin ?? null,
|
|
947
|
+
restored.webTreeSha256,
|
|
825
948
|
);
|
|
826
949
|
await restoreHookState(name, priorHookState);
|
|
827
950
|
setPluginCapabilities(await getPluginCapabilities());
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* manifest, so it must stay inside the package, be a regular file (not a
|
|
9
9
|
* link) and be no larger than {@link HOOK_MODULE_MAX_BYTES}.
|
|
10
10
|
*
|
|
11
|
+
* A web surface's tree (`userSurface.web.root`) is read the same two ways:
|
|
12
|
+
* from the staged tarball, so its digest can be approved, and from the
|
|
13
|
+
* installed package, which must hold the very same tree — every file a
|
|
14
|
+
* regular one, no link on the way (core `webTreeManifestOfTarball`,
|
|
15
|
+
* `webTreeManifestOfDirectory`).
|
|
16
|
+
*
|
|
11
17
|
* An agent half Lamaste installs onto an agent is never fetched here: its
|
|
12
18
|
* tarball's integrity is read from the registry ({@link pinAgentHalf}) and
|
|
13
19
|
* pinned with the version, and the agent fetches the tarball itself and
|
|
@@ -26,8 +32,13 @@ import {
|
|
|
26
32
|
hookEntrySha256,
|
|
27
33
|
relayHookSetNeedsEntry,
|
|
28
34
|
relayHookSetOf,
|
|
35
|
+
WebTreeError,
|
|
29
36
|
} from '@lamalibre/lamaste';
|
|
30
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
readTarballFiles,
|
|
39
|
+
webTreeManifestOfDirectory,
|
|
40
|
+
webTreeManifestOfTarball,
|
|
41
|
+
} from '@lamalibre/lamaste/server';
|
|
31
42
|
import { httpError } from './app-error.js';
|
|
32
43
|
import { withRegistryUserconfig } from './registry-credential.js';
|
|
33
44
|
|
|
@@ -88,6 +99,62 @@ export async function installedHookEntrySha256(packageName, manifest) {
|
|
|
88
99
|
return hookEntrySha256(await readInstalledHookModule(dir, set.entry));
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Run a web tree read, turning a broken rule into a 400 that names the
|
|
104
|
+
* version's tree.
|
|
105
|
+
*
|
|
106
|
+
* @template T
|
|
107
|
+
* @param {string} what — the package (and version) whose tree it is
|
|
108
|
+
* @param {() => T | Promise<T>} read
|
|
109
|
+
* @returns {Promise<T>}
|
|
110
|
+
*/
|
|
111
|
+
async function webTreeOrRefusal(what, read) {
|
|
112
|
+
try {
|
|
113
|
+
return await read();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err instanceof WebTreeError) {
|
|
116
|
+
throw httpError(`The web surface of ${what} cannot be served: ${err.message}`, 400, {
|
|
117
|
+
code: 'invalid_web_tree',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The directory of an installed package's web tree (`userSurface.web.root`,
|
|
126
|
+
* already a relative path of safe segments — core `WebSurfaceBlockSchema`),
|
|
127
|
+
* refused when it leaves the package or reaches it through a link.
|
|
128
|
+
*/
|
|
129
|
+
export async function installedWebTreeDir(packageName, manifest) {
|
|
130
|
+
const root = manifest.userSurface?.web?.root;
|
|
131
|
+
if (!root) return null;
|
|
132
|
+
const packageDir = await installedPackageDir(packageName);
|
|
133
|
+
const resolved = path.resolve(packageDir, root);
|
|
134
|
+
if (!resolved.startsWith(`${packageDir}${path.sep}`)) {
|
|
135
|
+
throw httpError('The web surface lies outside its package', 400, { code: 'invalid_web_tree' });
|
|
136
|
+
}
|
|
137
|
+
const real = await realpath(resolved).catch(() => null);
|
|
138
|
+
if (real !== null && real !== resolved) {
|
|
139
|
+
throw httpError('The web surface path goes through a link', 400, { code: 'invalid_web_tree' });
|
|
140
|
+
}
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* For a validated manifest of an installed package: its web tree's digest
|
|
146
|
+
* (core `webTreeManifest`'s `sha256`), or null when it declares no web
|
|
147
|
+
* surface. Throws 400 when the tree breaks a rule.
|
|
148
|
+
*/
|
|
149
|
+
export async function installedWebTreeSha256(packageName, manifest) {
|
|
150
|
+
const dir = await installedWebTreeDir(packageName, manifest);
|
|
151
|
+
if (dir === null) return null;
|
|
152
|
+
const tree = await webTreeOrRefusal(`${packageName}@${manifest.version}`, () =>
|
|
153
|
+
webTreeManifestOfDirectory(dir),
|
|
154
|
+
);
|
|
155
|
+
return tree.sha256;
|
|
156
|
+
}
|
|
157
|
+
|
|
91
158
|
/**
|
|
92
159
|
* Fetch `<package>@<version>` from the registry the panel installs from
|
|
93
160
|
* (`npm pack` through {@link withRegistryUserconfig}: the configured
|
|
@@ -97,7 +164,7 @@ export async function installedHookEntrySha256(packageName, manifest) {
|
|
|
97
164
|
*
|
|
98
165
|
* @param {string} packageName bare `@lamalibre/<name>`
|
|
99
166
|
* @param {string} version an exact version or a dist-tag (`latest`)
|
|
100
|
-
* @returns {Promise<{ version: string, manifestRaw: unknown, readHookModule: (entry: string) => Buffer | null }>}
|
|
167
|
+
* @returns {Promise<{ version: string, manifestRaw: unknown, readHookModule: (entry: string) => Buffer | null, readWebTree: (root: string) => Promise<import('@lamalibre/lamaste').WebTreeManifest> }>}
|
|
101
168
|
*/
|
|
102
169
|
export async function readRegistryPackage(packageName, version) {
|
|
103
170
|
if (!PACKAGE_RE.test(packageName)) throw httpError('Invalid package name', 400);
|
|
@@ -157,6 +224,11 @@ export async function readRegistryPackage(packageName, version) {
|
|
|
157
224
|
}).get(name) ?? null
|
|
158
225
|
);
|
|
159
226
|
},
|
|
227
|
+
readWebTree(root) {
|
|
228
|
+
return webTreeOrRefusal(`${packageName}@${info.version}`, () =>
|
|
229
|
+
webTreeManifestOfTarball(tgz, root),
|
|
230
|
+
);
|
|
231
|
+
},
|
|
160
232
|
};
|
|
161
233
|
} finally {
|
|
162
234
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -31,6 +31,7 @@ import { RegistryCredentialError } from '@lamalibre/lamaste/server';
|
|
|
31
31
|
import {
|
|
32
32
|
bareLamalibrePackageName,
|
|
33
33
|
installedHookEntrySha256,
|
|
34
|
+
installedWebTreeSha256,
|
|
34
35
|
pinAgentHalf,
|
|
35
36
|
} from './plugin-packages.js';
|
|
36
37
|
|
|
@@ -72,8 +73,9 @@ async function getStmts() {
|
|
|
72
73
|
INSERT INTO plugins
|
|
73
74
|
(name, route, display_name, package_name, version, description, capabilities,
|
|
74
75
|
packages, panel, config, modes, hosting, agent, hooks, hooks_entry_sha256,
|
|
75
|
-
agent_half_pin, settings, identity,
|
|
76
|
-
|
|
76
|
+
agent_half_pin, settings, identity, user_surface, web_tree_sha256, status,
|
|
77
|
+
installed_at, enabled_at)
|
|
78
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
77
79
|
`),
|
|
78
80
|
deleteByName: db.prepare('DELETE FROM plugins WHERE name = ?'),
|
|
79
81
|
// The plugin's audience and the state of its identity feed
|
|
@@ -141,6 +143,8 @@ function rowToPlugin(row) {
|
|
|
141
143
|
if (row.agent_half_pin != null) entry.agentHalfPin = JSON.parse(row.agent_half_pin);
|
|
142
144
|
if (row.settings != null) entry.settings = JSON.parse(row.settings);
|
|
143
145
|
if (row.identity != null) entry.identity = JSON.parse(row.identity);
|
|
146
|
+
if (row.user_surface != null) entry.userSurface = JSON.parse(row.user_surface);
|
|
147
|
+
if (row.web_tree_sha256 != null) entry.webTreeSha256 = row.web_tree_sha256;
|
|
144
148
|
if (row.enabled_at != null) entry.enabledAt = row.enabled_at;
|
|
145
149
|
return entry;
|
|
146
150
|
}
|
|
@@ -224,6 +228,8 @@ export async function writePlugins(data) {
|
|
|
224
228
|
nullableJson(p.agentHalfPin),
|
|
225
229
|
nullableJson(p.settings),
|
|
226
230
|
nullableJson(p.identity),
|
|
231
|
+
nullableJson(p.userSurface),
|
|
232
|
+
nullableString(p.webTreeSha256),
|
|
227
233
|
p.status,
|
|
228
234
|
p.installedAt,
|
|
229
235
|
nullableString(p.enabledAt),
|
|
@@ -457,6 +463,17 @@ export function installPlugin(packageName, logger) {
|
|
|
457
463
|
throw Object.assign(new Error(err.message), { statusCode: err.statusCode || 400 });
|
|
458
464
|
}
|
|
459
465
|
|
|
466
|
+
// A web surface: its tree must be in the package and keep every rule of
|
|
467
|
+
// a web tree; its digest goes into the approval digest, and the relay
|
|
468
|
+
// serves only a copy that matches it.
|
|
469
|
+
let webTreeSha256 = null;
|
|
470
|
+
try {
|
|
471
|
+
webTreeSha256 = await installedWebTreeSha256(packageName, manifest);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
await discardPackage();
|
|
474
|
+
throw Object.assign(new Error(err.message), { statusCode: err.statusCode || 400 });
|
|
475
|
+
}
|
|
476
|
+
|
|
460
477
|
// An agent half Lamaste installs: its tarball's integrity is pinned now,
|
|
461
478
|
// from the registry this install used — the approval covers it and the
|
|
462
479
|
// agent installs only a tarball that matches it.
|
|
@@ -508,6 +525,8 @@ export function installPlugin(packageName, logger) {
|
|
|
508
525
|
agentHalfPin,
|
|
509
526
|
settings: manifest.settings,
|
|
510
527
|
identity: identityOf(manifest),
|
|
528
|
+
userSurface: manifest.userSurface,
|
|
529
|
+
webTreeSha256,
|
|
511
530
|
status: 'disabled',
|
|
512
531
|
installedAt: new Date().toISOString(),
|
|
513
532
|
};
|
|
@@ -536,6 +555,8 @@ export function installPlugin(packageName, logger) {
|
|
|
536
555
|
nullableJson(entry.agentHalfPin),
|
|
537
556
|
nullableJson(entry.settings),
|
|
538
557
|
nullableJson(entry.identity),
|
|
558
|
+
nullableJson(entry.userSurface),
|
|
559
|
+
nullableString(entry.webTreeSha256),
|
|
539
560
|
entry.status,
|
|
540
561
|
entry.installedAt,
|
|
541
562
|
null,
|
|
@@ -684,7 +705,7 @@ const updateRowSql = `
|
|
|
684
705
|
UPDATE plugins SET
|
|
685
706
|
display_name = ?, version = ?, description = ?, capabilities = ?, packages = ?, panel = ?,
|
|
686
707
|
config = ?, modes = ?, hosting = ?, agent = ?, hooks = ?, hooks_entry_sha256 = ?,
|
|
687
|
-
agent_half_pin = ?, settings = ?, identity = ?
|
|
708
|
+
agent_half_pin = ?, settings = ?, identity = ?, user_surface = ?, web_tree_sha256 = ?
|
|
688
709
|
WHERE name = ?`;
|
|
689
710
|
|
|
690
711
|
/**
|
|
@@ -698,8 +719,16 @@ const updateRowSql = `
|
|
|
698
719
|
* beside it, and a secret whose field became a plain value is no longer
|
|
699
720
|
* reported set. The identity block is stored normalized; the update's
|
|
700
721
|
* caller recomputes the audiences once the version is in place.
|
|
722
|
+
* `webTreeSha256` is the digest of the version's web tree, read from the
|
|
723
|
+
* installed package (null when it declares no web surface).
|
|
701
724
|
*/
|
|
702
|
-
export async function writePluginRow(
|
|
725
|
+
export async function writePluginRow(
|
|
726
|
+
name,
|
|
727
|
+
manifest,
|
|
728
|
+
hooksEntrySha256,
|
|
729
|
+
agentHalfPin,
|
|
730
|
+
webTreeSha256 = null,
|
|
731
|
+
) {
|
|
703
732
|
const s = await getStmts();
|
|
704
733
|
const { prunedSettingsCache } = await import('./plugin-settings.js');
|
|
705
734
|
s.begin.run();
|
|
@@ -723,6 +752,8 @@ export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalf
|
|
|
723
752
|
nullableJson(agentHalfPin),
|
|
724
753
|
nullableJson(manifest.settings),
|
|
725
754
|
nullableJson(identityOf(manifest)),
|
|
755
|
+
nullableJson(manifest.userSurface),
|
|
756
|
+
nullableString(webTreeSha256),
|
|
726
757
|
name,
|
|
727
758
|
);
|
|
728
759
|
const pruned = prunedSettingsCache(
|
|
@@ -750,7 +781,8 @@ export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalf
|
|
|
750
781
|
|
|
751
782
|
/**
|
|
752
783
|
* Install `<packageName>@<version>` over the installed copy and read it
|
|
753
|
-
* back: the validated manifest
|
|
784
|
+
* back: the validated manifest, its relay entry module's SHA-256 and its
|
|
785
|
+
* web tree's digest. The
|
|
754
786
|
* row is not touched — the caller compares the result with what was
|
|
755
787
|
* reviewed and then writes it. Refuses a package whose manifest names
|
|
756
788
|
* another plugin.
|
|
@@ -790,7 +822,8 @@ export function replacePluginPackage(name, packageName, version, logger) {
|
|
|
790
822
|
);
|
|
791
823
|
}
|
|
792
824
|
const hooksEntrySha256 = await installedHookEntrySha256(packageName, manifest);
|
|
793
|
-
|
|
825
|
+
const webTreeSha256 = await installedWebTreeSha256(packageName, manifest);
|
|
826
|
+
return { manifest, hooksEntrySha256, webTreeSha256 };
|
|
794
827
|
});
|
|
795
828
|
}
|
|
796
829
|
|
|
@@ -95,6 +95,24 @@ export async function writeSites(sites) {
|
|
|
95
95
|
await rename(tmpPath, filePath);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Serializes every read-modify-write of sites.json in this process. */
|
|
99
|
+
const sitesMutex = new PromiseChainMutex();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Run `fn` holding the sites lock: a read of sites.json, the decisions made
|
|
103
|
+
* on it and the write that follows must not interleave with another. A
|
|
104
|
+
* workflow that gives a site a hostname (creating it, adding an alias,
|
|
105
|
+
* taking a tunnel's host) takes core's tunnel lock first, then this one —
|
|
106
|
+
* always in that order — so a site and a tunnel never take one name at once.
|
|
107
|
+
*
|
|
108
|
+
* @template T
|
|
109
|
+
* @param {() => Promise<T>} fn
|
|
110
|
+
* @returns {Promise<T>}
|
|
111
|
+
*/
|
|
112
|
+
export function withSitesLock(fn) {
|
|
113
|
+
return sitesMutex.run(fn);
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
function invitationsPath() {
|
|
99
117
|
return path.join(STATE_DIR, 'invitations.json');
|
|
100
118
|
}
|
|
@@ -45,8 +45,11 @@ import {
|
|
|
45
45
|
RELAY_PANEL_AUDIENCE,
|
|
46
46
|
RELAY_PANEL_PORT,
|
|
47
47
|
isAgentHostedPlugin,
|
|
48
|
+
siteUserSurfaceRoute,
|
|
48
49
|
tunnelUserSurfaceRoutes,
|
|
50
|
+
userSurfaceHostServing,
|
|
49
51
|
userSurfaceHostingAgent,
|
|
52
|
+
userSurfaceHosts,
|
|
50
53
|
verifyRelayAssertion,
|
|
51
54
|
} from '@lamalibre/lamaste';
|
|
52
55
|
import {
|
|
@@ -62,7 +65,7 @@ import { getConfig } from './config.js';
|
|
|
62
65
|
import { installedVersionApproved } from './plugin-hooks.js';
|
|
63
66
|
import { findPluginByRoute } from './plugins.js';
|
|
64
67
|
import { relaySigner } from './relay-signing.js';
|
|
65
|
-
import { readTunnels } from './state.js';
|
|
68
|
+
import { readSites, readTunnels } from './state.js';
|
|
66
69
|
import { tunnelCertbotDeps, tunnelNginxDeps, tunnelStateDeps } from './tunnel-deps.js';
|
|
67
70
|
|
|
68
71
|
/**
|
|
@@ -102,7 +105,7 @@ export const USER_SURFACE_ROUTE_CONFIG = Object.freeze({
|
|
|
102
105
|
* @param {readonly object[]} tunnels
|
|
103
106
|
* @returns {Promise<string | null>}
|
|
104
107
|
*/
|
|
105
|
-
async function hostingAgentOf(plugin, tunnels) {
|
|
108
|
+
export async function hostingAgentOf(plugin, tunnels) {
|
|
106
109
|
if (!plugin.route) return null;
|
|
107
110
|
const { ownHosting } = await import('./plugin-hosting.js');
|
|
108
111
|
const record = await ownHosting(plugin);
|
|
@@ -160,7 +163,7 @@ async function routeServing(route, agentLabel, tunnels) {
|
|
|
160
163
|
* @param {object} plugin
|
|
161
164
|
* @param {readonly object[]} tunnels the tunnel state
|
|
162
165
|
*/
|
|
163
|
-
async function internalTunnelBodyLimit(plugin, tunnels) {
|
|
166
|
+
export async function internalTunnelBodyLimit(plugin, tunnels) {
|
|
164
167
|
const { ownHosting } = await import('./plugin-hosting.js');
|
|
165
168
|
const record = await ownHosting(plugin);
|
|
166
169
|
if (!record) return TUNNEL_MAX_BODY_MB.default;
|
|
@@ -287,6 +290,14 @@ async function assertBindableRoute(route) {
|
|
|
287
290
|
'INVALID_SETTING',
|
|
288
291
|
);
|
|
289
292
|
}
|
|
293
|
+
// A route is served on one host at most: the plugin's own site, when it
|
|
294
|
+
// has one, is that host.
|
|
295
|
+
if ((await readSites()).some((site) => siteUserSurfaceRoute(site) === route)) {
|
|
296
|
+
throw new TunnelError(
|
|
297
|
+
`Plugin "${plugin.name}" has a web surface — its user surface is served there`,
|
|
298
|
+
'INVALID_SETTING',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
/** Whether an installed agent-hosted plugin holds `route`. */
|
|
@@ -413,7 +424,8 @@ export async function refreshTunnelUserSurfaceVhosts(ids, logger) {
|
|
|
413
424
|
* Whether plugin `route`'s user surface is served may have changed — its
|
|
414
425
|
* version was approved, updated or reverted, it was hosted or unhosted (the
|
|
415
426
|
* body limit is its internal tunnel's): bring the vhosts of the tunnels it
|
|
416
|
-
* is bound to in line
|
|
427
|
+
* is bound to in line, and its web surface — the approved tree served, the
|
|
428
|
+
* vhost of the site it owns (`lib/web-surface.js`). Best effort — logged, never thrown; the panel's
|
|
417
429
|
* start reconciles every vhost again.
|
|
418
430
|
*
|
|
419
431
|
* @param {string | null | undefined} route
|
|
@@ -429,6 +441,9 @@ export async function refreshUserSurfaceVhosts(route, logger) {
|
|
|
429
441
|
'Could not bring the user-surface vhosts of a plugin in line',
|
|
430
442
|
);
|
|
431
443
|
}
|
|
444
|
+
// Its web surface too: the approved tree served, the site's vhost in line.
|
|
445
|
+
const { refreshWebSurface } = await import('./web-surface.js');
|
|
446
|
+
await refreshWebSurface(route, logger);
|
|
432
447
|
}
|
|
433
448
|
|
|
434
449
|
/**
|
|
@@ -491,6 +506,10 @@ export async function releasePluginUserSurface(route, { plugin, logger }) {
|
|
|
491
506
|
'Could not release the user surface of an uninstalled plugin',
|
|
492
507
|
);
|
|
493
508
|
}
|
|
509
|
+
// The site it owns stays, answering 503 until the plugin is back (or an
|
|
510
|
+
// administrator deletes it): its vhost goes in line.
|
|
511
|
+
const { refreshWebSurface } = await import('./web-surface.js');
|
|
512
|
+
await refreshWebSurface(route, logger);
|
|
494
513
|
}
|
|
495
514
|
|
|
496
515
|
// ---------------------------------------------------------------------------
|
|
@@ -560,33 +579,35 @@ export async function verifyUserSurfaceRelay(request) {
|
|
|
560
579
|
}
|
|
561
580
|
|
|
562
581
|
/**
|
|
563
|
-
* The enabled
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
582
|
+
* The enabled host that serves `route`'s user surface, or null — the check
|
|
583
|
+
* of every request, whatever a vhost not yet re-rendered still routes here:
|
|
584
|
+
* a tunnel bound to the route and carried by the agent hosting its plugin,
|
|
585
|
+
* or the site the plugin owns while its web surface is served
|
|
586
|
+
* (`lib/web-surface.js`). Read with core `userSurfaceHosts`, as Gatekeeper
|
|
587
|
+
* reads it, so a route two hosts would serve is served by neither.
|
|
567
588
|
*
|
|
568
589
|
* @param {string} route
|
|
590
|
+
* @returns {Promise<import('@lamalibre/lamaste').UserSurfaceHost | null>}
|
|
569
591
|
*/
|
|
570
|
-
async function
|
|
571
|
-
const tunnels = await readTunnels();
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
) ?? null;
|
|
579
|
-
if (!tunnel) return null;
|
|
592
|
+
async function servedUserSurfaceHost(route) {
|
|
593
|
+
const [tunnels, sites] = await Promise.all([readTunnels(), readSites()]);
|
|
594
|
+
const host = userSurfaceHostServing(userSurfaceHosts(tunnels, sites), route);
|
|
595
|
+
if (!host) return null;
|
|
596
|
+
if (host.kind === 'site') {
|
|
597
|
+
const { webSurfaceServing } = await import('./web-surface.js');
|
|
598
|
+
return (await webSurfaceServing(route, tunnels)).served ? host : null;
|
|
599
|
+
}
|
|
580
600
|
const plugin = await findPluginByRoute(route);
|
|
581
601
|
if (!plugin) return null;
|
|
602
|
+
const tunnel = tunnels.find((t) => t.id === host.id);
|
|
582
603
|
const hosting = await hostingAgentOf(plugin, tunnels);
|
|
583
|
-
return hosting !== null && tunnel
|
|
604
|
+
return hosting !== null && tunnel?.agentLabel === hosting ? host : null;
|
|
584
605
|
}
|
|
585
606
|
|
|
586
607
|
/**
|
|
587
608
|
* The forward target of `route`'s user surface (core `userSurfacePlugin`'s
|
|
588
|
-
* `resolveTarget`): null when no
|
|
589
|
-
*
|
|
609
|
+
* `resolveTarget`): null when no host serves it (see
|
|
610
|
+
* {@link servedUserSurfaceHost}), else what the admin forwarder resolves — the hosting's
|
|
590
611
|
* internal tunnel, or why not (not found, disabled, not hosted) — and
|
|
591
612
|
* `down` while the hosting's health is down: nothing of a user's request
|
|
592
613
|
* reaches the relay-loopback port then (503).
|
|
@@ -594,7 +615,7 @@ async function servedUserSurfaceTunnel(route) {
|
|
|
594
615
|
* @param {string} route
|
|
595
616
|
*/
|
|
596
617
|
export async function resolveUserSurfaceTarget(route) {
|
|
597
|
-
if (!(await
|
|
618
|
+
if (!(await servedUserSurfaceHost(route))) return null;
|
|
598
619
|
const { resolveForwardTarget } = await import('./plugin-hosting.js');
|
|
599
620
|
return resolveForwardTarget(route);
|
|
600
621
|
}
|
|
@@ -602,15 +623,15 @@ export async function resolveUserSurfaceTarget(route) {
|
|
|
602
623
|
/**
|
|
603
624
|
* The origin a request that may change something on `route`'s user surface
|
|
604
625
|
* must come from (core `userSurfacePlugin`'s `originFor`): the bound host,
|
|
605
|
-
* as the
|
|
606
|
-
*
|
|
626
|
+
* as the state names it — never the request's `Host`. Null when no host
|
|
627
|
+
* serves the route.
|
|
607
628
|
*
|
|
608
629
|
* @param {string} route
|
|
609
630
|
* @returns {Promise<string | null>}
|
|
610
631
|
*/
|
|
611
632
|
export async function userSurfaceOrigin(route) {
|
|
612
|
-
const
|
|
613
|
-
return
|
|
633
|
+
const host = await servedUserSurfaceHost(route);
|
|
634
|
+
return host ? `https://${host.fqdn}` : null;
|
|
614
635
|
}
|
|
615
636
|
|
|
616
637
|
// ---------------------------------------------------------------------------
|
|
@@ -702,7 +723,7 @@ export async function sweepOpenUserSurfaceResponses(logger) {
|
|
|
702
723
|
const { route, caller } = response;
|
|
703
724
|
let verdict;
|
|
704
725
|
try {
|
|
705
|
-
if (!served.has(route)) served.set(route, (await
|
|
726
|
+
if (!served.has(route)) served.set(route, (await servedUserSurfaceHost(route)) !== null);
|
|
706
727
|
const key = `${route}\n${caller.username}`;
|
|
707
728
|
if (!callers.has(key)) callers.set(key, await resolveUserCaller(route, caller.username));
|
|
708
729
|
const resolution = callers.get(key);
|