@lamalibre/create-lamaste 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +56 -5
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- 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/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- 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/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- 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 +80 -12
- 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 +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
- 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/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- package/vendor/docs/04-api-reference/users.md +41 -29
- 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/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- 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/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.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 +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- 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 +325 -55
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- 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-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +50 -19
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +576 -131
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- 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(
|
|
@@ -816,12 +938,14 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
816
938
|
plugin.packageName,
|
|
817
939
|
plugin.version,
|
|
818
940
|
logger,
|
|
941
|
+
{ restoring: true },
|
|
819
942
|
);
|
|
820
943
|
await writePluginRow(
|
|
821
944
|
name,
|
|
822
945
|
restored.manifest,
|
|
823
946
|
restored.hooksEntrySha256,
|
|
824
947
|
plugin.agentHalfPin ?? null,
|
|
948
|
+
restored.webTreeSha256,
|
|
825
949
|
);
|
|
826
950
|
await restoreHookState(name, priorHookState);
|
|
827
951
|
setPluginCapabilities(await getPluginCapabilities());
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
* `identity_providers.oidc` section is written; Authelia is restarted only
|
|
20
20
|
* when the section actually changed.
|
|
21
21
|
*
|
|
22
|
+
* Every client belongs to a domain — its plugin's (the panel's own: the
|
|
23
|
+
* registry credential's issuer domain): `{domain}` in its audience and
|
|
24
|
+
* redirect URIs is that domain, and a user-facing client has its own
|
|
25
|
+
* authorization policy (core `renderOidcClientPolicies`): that domain's
|
|
26
|
+
* members, and its external members granted the client (a Gatekeeper grant
|
|
27
|
+
* `resourceType: 'oidc-client'`, rendered by lib/domains.js into users.yml's
|
|
28
|
+
* owned groups). The provider is written under lib/domains.js
|
|
29
|
+
* `withDomainRendering` — the one writer of what members may do — with the
|
|
30
|
+
* domains read under it.
|
|
31
|
+
*
|
|
22
32
|
* Best effort by design where a plugin lifecycle calls it: a failure here
|
|
23
33
|
* must not fail the plugin operation — the caller logs it and the next
|
|
24
34
|
* lifecycle change retries.
|
|
@@ -26,9 +36,11 @@
|
|
|
26
36
|
|
|
27
37
|
import { readFile } from 'node:fs/promises';
|
|
28
38
|
import { createRequire } from 'node:module';
|
|
29
|
-
import {
|
|
39
|
+
import { PromiseChainMutex, validateInstalledManifest } from '@lamalibre/lamaste';
|
|
30
40
|
import {
|
|
31
41
|
LAMASTE_REGISTRY_OIDC_SCOPES,
|
|
42
|
+
LAMASTE_SERVER_OIDC_CLIENT_ID,
|
|
43
|
+
registryIssuerDomain,
|
|
32
44
|
lamasteServerOidcClient,
|
|
33
45
|
listOidcClientSecrets,
|
|
34
46
|
reconcileOidcClientSecrets,
|
|
@@ -36,32 +48,43 @@ import {
|
|
|
36
48
|
rotateOidcClientSecret,
|
|
37
49
|
} from '@lamalibre/lamaste/server';
|
|
38
50
|
|
|
39
|
-
import { getConfig } from './config.js';
|
|
40
51
|
import { updateOidcProvider } from './authelia.js';
|
|
52
|
+
import { listGrantsOfType } from './gatekeeper-client.js';
|
|
53
|
+
import { OIDC_CLIENT_RESOURCE, isUserFacingOidcClient } from './owned-groups.js';
|
|
54
|
+
import { pluginDomainOr, primaryDomainName, withDomainRendering } from './domains.js';
|
|
41
55
|
import { readPlugins } from './plugins.js';
|
|
42
56
|
import { readRegistryCredentialConfig } from './registry-credential.js';
|
|
43
57
|
|
|
44
58
|
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
45
59
|
|
|
60
|
+
/** The domains a rendering read, as the OIDC provider's policies name them: name and group key. */
|
|
61
|
+
const keyed = (domains) => domains.map(({ name, groupKey }) => ({ name, groupKey }));
|
|
62
|
+
|
|
46
63
|
/**
|
|
47
64
|
* The OIDC clients every installed plugin declares, tagged with the plugin
|
|
48
|
-
* name and whether the plugin is enabled. Manifests are re-read
|
|
49
|
-
* installed packages — the plugin registry does not store them.
|
|
65
|
+
* name, its domain and whether the plugin is enabled. Manifests are re-read
|
|
66
|
+
* from the installed packages — the plugin registry does not store them.
|
|
50
67
|
* @returns {Promise<Array<import('@lamalibre/lamaste/server').AutheliaOidcClient & { enabled: boolean }>>}
|
|
51
68
|
*/
|
|
52
69
|
export async function collectPluginOidcClients() {
|
|
53
70
|
const require = createRequire(`${STATE_DIR}/`);
|
|
71
|
+
const primary = await primaryDomainName();
|
|
54
72
|
const clients = [];
|
|
55
73
|
for (const plugin of (await readPlugins()).plugins) {
|
|
56
74
|
let manifest;
|
|
57
75
|
try {
|
|
58
76
|
const manifestPath = require.resolve(`${plugin.packageName}/lamaste-plugin.json`);
|
|
59
|
-
manifest =
|
|
77
|
+
manifest = validateInstalledManifest(JSON.parse(await readFile(manifestPath, 'utf-8')));
|
|
60
78
|
} catch {
|
|
61
79
|
continue; // an unreadable manifest contributes no clients
|
|
62
80
|
}
|
|
63
81
|
for (const client of manifest.oidcClients) {
|
|
64
|
-
clients.push({
|
|
82
|
+
clients.push({
|
|
83
|
+
...client,
|
|
84
|
+
plugin: manifest.name,
|
|
85
|
+
domain: pluginDomainOr(plugin, primary),
|
|
86
|
+
enabled: plugin.status === 'enabled',
|
|
87
|
+
});
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
return clients;
|
|
@@ -69,31 +92,77 @@ export async function collectPluginOidcClients() {
|
|
|
69
92
|
|
|
70
93
|
/**
|
|
71
94
|
* The panel's own client, when the registry credential (stored, or the
|
|
72
|
-
* candidate being verified) uses it.
|
|
73
|
-
* @param {{ registryUrl: string } | null | undefined} registryAuth
|
|
95
|
+
* candidate being verified) uses it — of the credential's issuer domain.
|
|
96
|
+
* @param {{ registryUrl: string, issuerDomain?: string } | null | undefined} registryAuth
|
|
74
97
|
*/
|
|
75
|
-
function lamasteOwnClient(registryAuth) {
|
|
98
|
+
async function lamasteOwnClient(registryAuth) {
|
|
76
99
|
if (!registryAuth) return [];
|
|
77
|
-
|
|
100
|
+
const domain = registryIssuerDomain(
|
|
101
|
+
{ kind: 'oidc', clientId: LAMASTE_SERVER_OIDC_CLIENT_ID, ...registryAuth },
|
|
102
|
+
await primaryDomainName(),
|
|
103
|
+
);
|
|
104
|
+
return [{ ...lamasteServerOidcClient(registryAuth.registryUrl, domain), enabled: true }];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A registry credential being set (`PUT /api/registry-credential`): its
|
|
109
|
+
* `registryAuth` while the panel's client is synced, the chain verified and
|
|
110
|
+
* the credential stored. Every sync that names no override serves it — the
|
|
111
|
+
* identity tick's every-minute sync included, which otherwise reads the
|
|
112
|
+
* stored credential, removes the client the candidate needs and restarts
|
|
113
|
+
* Authelia under the verification's token request. Every sync reads it under
|
|
114
|
+
* the one writer ({@link syncOidcClients}), so none can write a plan made
|
|
115
|
+
* before it was set.
|
|
116
|
+
* @type {{ registryUrl: string, issuerDomain?: string } | undefined}
|
|
117
|
+
*/
|
|
118
|
+
let registryCandidate;
|
|
119
|
+
const registryCandidateMutex = new PromiseChainMutex();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run `fn` with `registryAuth` as the candidate registry credential
|
|
123
|
+
* (see {@link registryCandidate}); one candidate at a time. Without one
|
|
124
|
+
* (a pasted token), `fn` just runs.
|
|
125
|
+
* @template T
|
|
126
|
+
* @param {{ registryUrl: string, issuerDomain?: string } | undefined} registryAuth
|
|
127
|
+
* @param {() => Promise<T>} fn
|
|
128
|
+
* @returns {Promise<T>}
|
|
129
|
+
*/
|
|
130
|
+
export function withRegistryCandidate(registryAuth, fn) {
|
|
131
|
+
if (registryAuth === undefined) return fn();
|
|
132
|
+
return registryCandidateMutex.run(async () => {
|
|
133
|
+
registryCandidate = registryAuth;
|
|
134
|
+
try {
|
|
135
|
+
return await fn();
|
|
136
|
+
} finally {
|
|
137
|
+
registryCandidate = undefined;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
78
140
|
}
|
|
79
141
|
|
|
80
142
|
/**
|
|
81
143
|
* Everything Authelia should know, from the stored state.
|
|
82
|
-
* @param {{ registryAuth?: { registryUrl: string } | null }} [overrides]
|
|
144
|
+
* @param {{ registryAuth?: { registryUrl: string, issuerDomain?: string } | null }} [overrides]
|
|
83
145
|
* `registryAuth` replaces the stored registry credential's say in the
|
|
84
|
-
* matter: `{ registryUrl }` while a candidate oidc
|
|
85
|
-
* verified, `null` to leave the panel's client out.
|
|
146
|
+
* matter: `{ registryUrl, issuerDomain }` while a candidate oidc
|
|
147
|
+
* credential is being verified, `null` to leave the panel's client out.
|
|
86
148
|
*/
|
|
87
149
|
async function collectAllClients(overrides = {}) {
|
|
88
150
|
let registryAuth;
|
|
89
151
|
if (overrides.registryAuth !== undefined) {
|
|
90
152
|
registryAuth = overrides.registryAuth;
|
|
153
|
+
} else if (registryCandidate !== undefined) {
|
|
154
|
+
registryAuth = registryCandidate;
|
|
91
155
|
} else {
|
|
92
156
|
const stored = await readRegistryCredentialConfig().catch(() => null);
|
|
93
157
|
registryAuth =
|
|
94
|
-
stored && stored.auth.kind === 'oidc'
|
|
158
|
+
stored && stored.auth.kind === 'oidc'
|
|
159
|
+
? {
|
|
160
|
+
registryUrl: stored.registryUrl,
|
|
161
|
+
...(stored.auth.issuerDomain ? { issuerDomain: stored.auth.issuerDomain } : {}),
|
|
162
|
+
}
|
|
163
|
+
: null;
|
|
95
164
|
}
|
|
96
|
-
return [...(await collectPluginOidcClients()), ...lamasteOwnClient(registryAuth)];
|
|
165
|
+
return [...(await collectPluginOidcClients()), ...(await lamasteOwnClient(registryAuth))];
|
|
97
166
|
}
|
|
98
167
|
|
|
99
168
|
/**
|
|
@@ -103,7 +172,7 @@ async function collectAllClients(overrides = {}) {
|
|
|
103
172
|
* enabled plugins served, and the panel's own scopes when its client is.
|
|
104
173
|
* Pure with respect to Authelia — nothing is written.
|
|
105
174
|
*
|
|
106
|
-
* @param {{ registryAuth?: { registryUrl: string } | null }} [overrides] see collectAllClients
|
|
175
|
+
* @param {{ registryAuth?: { registryUrl: string, issuerDomain?: string } | null }} [overrides] see collectAllClients
|
|
107
176
|
* @returns {Promise<{
|
|
108
177
|
* confidential: Array<{ id: string, plugin: string }>,
|
|
109
178
|
* served: Array<import('@lamalibre/lamaste/server').AutheliaOidcClient>,
|
|
@@ -123,37 +192,41 @@ export async function planOidcClients(overrides = {}) {
|
|
|
123
192
|
/**
|
|
124
193
|
* Sync Authelia's OIDC clients and the secret store with the plugins and
|
|
125
194
|
* the registry credential. Returns what was done, or `null` when the panel
|
|
126
|
-
* has no domain yet (onboarding).
|
|
195
|
+
* has no domain yet (onboarding). Plan, secrets and write all happen under
|
|
196
|
+
* the one writer: a sync that waited for it plans from what is true then,
|
|
197
|
+
* never from before another sync's change (a registry credential being
|
|
198
|
+
* set, a plugin moved).
|
|
127
199
|
*
|
|
128
200
|
* @param {import('pino').Logger} logger
|
|
129
|
-
* @param {{ registryAuth?: { registryUrl: string } | null }} [overrides] see collectAllClients
|
|
130
201
|
* @returns {Promise<{ changed: boolean, clients: string[], createdSecrets: Array<{ id: string, plugin: string, secret: string }> } | null>}
|
|
131
202
|
*/
|
|
132
|
-
export async function syncOidcClients(logger
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
domain = getConfig().domain;
|
|
136
|
-
} catch {
|
|
137
|
-
domain = null;
|
|
138
|
-
}
|
|
139
|
-
if (!domain) {
|
|
203
|
+
export async function syncOidcClients(logger) {
|
|
204
|
+
if (!(await primaryDomainName())) {
|
|
140
205
|
logger.debug('OIDC client sync skipped — no domain configured yet');
|
|
141
206
|
return null;
|
|
142
207
|
}
|
|
143
208
|
|
|
144
|
-
const {
|
|
145
|
-
|
|
209
|
+
const { served, reconciled, result } = await withDomainRendering(
|
|
210
|
+
async (domains) => {
|
|
211
|
+
const plan = await planOidcClients();
|
|
212
|
+
const secrets = await reconcileOidcClientSecrets(STATE_DIR, plan.confidential);
|
|
213
|
+
return {
|
|
214
|
+
served: plan.served,
|
|
215
|
+
reconciled: secrets,
|
|
216
|
+
result: await updateOidcProvider(keyed(domains), plan.served, {
|
|
217
|
+
secretDigests: secrets.digests,
|
|
218
|
+
scopes: plan.scopes,
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
{ logger },
|
|
223
|
+
);
|
|
146
224
|
if (reconciled.created.length > 0 || reconciled.removed.length > 0) {
|
|
147
225
|
logger.info(
|
|
148
226
|
{ created: reconciled.created.map((c) => c.id), removed: reconciled.removed },
|
|
149
227
|
'OIDC client secrets reconciled',
|
|
150
228
|
);
|
|
151
229
|
}
|
|
152
|
-
|
|
153
|
-
const result = await updateOidcProvider(domain, served, {
|
|
154
|
-
secretDigests: reconciled.digests,
|
|
155
|
-
scopes,
|
|
156
|
-
});
|
|
157
230
|
if (result.changed) {
|
|
158
231
|
logger.info({ clients: served.map((c) => c.id) }, 'Authelia OIDC provider updated');
|
|
159
232
|
}
|
|
@@ -166,16 +239,37 @@ export async function syncOidcClients(logger, overrides = {}) {
|
|
|
166
239
|
|
|
167
240
|
/**
|
|
168
241
|
* Metadata of a plugin's OIDC clients: what the manifest declares, whether
|
|
169
|
-
* it is served,
|
|
170
|
-
*
|
|
242
|
+
* it is served, whether it signs users in (`userFacing`) and — for those —
|
|
243
|
+
* the Gatekeeper grants on it (`grants`: who among the external members of
|
|
244
|
+
* its domain may sign in with it; members of the domain always may; null
|
|
245
|
+
* when Gatekeeper cannot be read), and — for confidential clients — when
|
|
246
|
+
* the secret was created and last rotated. Never the secret.
|
|
171
247
|
* @param {string} pluginName
|
|
172
248
|
*/
|
|
173
249
|
export async function listPluginOidcClients(pluginName) {
|
|
174
250
|
const declared = (await collectPluginOidcClients()).filter((c) => c.plugin === pluginName);
|
|
175
251
|
const secrets = new Map((await listOidcClientSecrets(STATE_DIR)).map((m) => [m.id, m]));
|
|
252
|
+
const grants = declared.some(isUserFacingOidcClient)
|
|
253
|
+
? await listGrantsOfType(OIDC_CLIENT_RESOURCE)
|
|
254
|
+
: null;
|
|
176
255
|
return declared.map((client) => {
|
|
177
256
|
const secret = client.confidential ? (secrets.get(client.id) ?? null) : null;
|
|
178
257
|
return {
|
|
258
|
+
userFacing: isUserFacingOidcClient(client),
|
|
259
|
+
...(isUserFacingOidcClient(client)
|
|
260
|
+
? {
|
|
261
|
+
grants: grants?.ok
|
|
262
|
+
? grants.grants
|
|
263
|
+
.filter((g) => g.resourceId === client.id)
|
|
264
|
+
.map((g) => ({
|
|
265
|
+
grantId: g.grantId,
|
|
266
|
+
principalType: g.principalType,
|
|
267
|
+
principalId: g.principalId,
|
|
268
|
+
createdAt: g.createdAt,
|
|
269
|
+
}))
|
|
270
|
+
: null,
|
|
271
|
+
}
|
|
272
|
+
: {}),
|
|
179
273
|
id: client.id,
|
|
180
274
|
name: client.name ?? null,
|
|
181
275
|
confidential: client.confidential,
|
|
@@ -216,13 +310,21 @@ export async function rotatePluginOidcClientSecret(pluginName, clientId, logger)
|
|
|
216
310
|
// The store is reconciled first so a client that was never synced (an
|
|
217
311
|
// earlier Authelia failure) gets its record now.
|
|
218
312
|
await syncOidcClients(logger);
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
313
|
+
// Rotate, plan and write under the one writer (see syncOidcClients).
|
|
314
|
+
const rotated = await withDomainRendering(
|
|
315
|
+
async (domains) => {
|
|
316
|
+
const secret = await rotateOidcClientSecret(STATE_DIR, clientId);
|
|
317
|
+
if (client.enabled) {
|
|
318
|
+
const { served, scopes } = await planOidcClients();
|
|
319
|
+
await updateOidcProvider(keyed(domains), served, {
|
|
320
|
+
secretDigests: await readOidcClientSecretDigests(STATE_DIR),
|
|
321
|
+
scopes,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return secret;
|
|
325
|
+
},
|
|
326
|
+
{ logger },
|
|
327
|
+
);
|
|
226
328
|
logger.info({ plugin: pluginName, clientId }, 'OIDC client secret rotated');
|
|
227
329
|
return {
|
|
228
330
|
id: rotated.id,
|
|
@@ -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 });
|