@lamalibre/create-lamaste 4.4.1 → 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 +234 -18
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- 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 +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- 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 +70 -33
- 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 +113 -25
- 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 +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +87 -47
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +24 -6
- package/vendor/docs/06-reference/config-files.md +100 -202
- 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 +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- 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 +4 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +4 -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 +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- 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 +49 -56
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
- 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 +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +156 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/sites.js +15 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +186 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- 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 +34 -5
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +546 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +95 -7
- package/vendor/serverd/src/lib/registry-credential.js +9 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +36 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +754 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -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/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +109 -22
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +6 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* a version that turns a secret setting into a plain one always waits for
|
|
19
19
|
* an administrator's approval, its digest approved or not, and whatever
|
|
20
20
|
* auto-approve says (`pendingUpdate.approvalRequired` says why an update
|
|
21
|
-
* waits).
|
|
21
|
+
* waits). The identity block is inside the digest; a version that adds or
|
|
22
|
+
* removes a role or widens the attributes its users' records carry waits
|
|
23
|
+
* for an administrator too, whatever auto-approve says
|
|
24
|
+
* (`identity.changed`; `pendingUpdate.identity` shows the change), and
|
|
25
|
+
* after the update the audiences are recomputed and the agent half told.
|
|
22
26
|
*/
|
|
23
27
|
|
|
24
28
|
import {
|
|
@@ -27,26 +31,30 @@ import {
|
|
|
27
31
|
derivePluginRoute,
|
|
28
32
|
hasAgentHalf,
|
|
29
33
|
hookEntrySha256,
|
|
34
|
+
identityChangeNeedsApproval,
|
|
35
|
+
identityDiff,
|
|
30
36
|
relayHookSetNeedsEntry,
|
|
31
37
|
relayHookSetOf,
|
|
32
38
|
relayHookSetPreview,
|
|
33
39
|
settingsSchemaDiff,
|
|
34
40
|
validateManifest,
|
|
35
41
|
} from '@lamalibre/lamaste';
|
|
36
|
-
import { pluginHostingPhase } from '@lamalibre/lamaste/server';
|
|
42
|
+
import { pluginHostingPhase, webTreeManifestOfDirectory } from '@lamalibre/lamaste/server';
|
|
37
43
|
import { httpError } from './app-error.js';
|
|
38
44
|
import { setPluginCapabilities } from './mtls.js';
|
|
39
45
|
import {
|
|
40
46
|
findPlugin,
|
|
41
47
|
getPluginCapabilities,
|
|
48
|
+
identityOf,
|
|
42
49
|
manifestPolicyError,
|
|
43
50
|
replacePluginPackage,
|
|
44
51
|
uninstallPlugin,
|
|
45
52
|
writePluginRow,
|
|
46
53
|
} from './plugins.js';
|
|
47
|
-
import { pinAgentHalf, readRegistryPackage } from './plugin-packages.js';
|
|
54
|
+
import { installedWebTreeDir, pinAgentHalf, readRegistryPackage } from './plugin-packages.js';
|
|
48
55
|
import {
|
|
49
56
|
DIGEST_UNAPPROVED,
|
|
57
|
+
IDENTITY_CHANGED,
|
|
50
58
|
SETTINGS_SECRET_TO_PLAIN,
|
|
51
59
|
digestUnapprovedReason,
|
|
52
60
|
installRunnerModule,
|
|
@@ -82,6 +90,8 @@ import { getConfig } from './config.js';
|
|
|
82
90
|
import { recordAudit, recordAuditBestEffort } from './audit.js';
|
|
83
91
|
import { notifyAdminsBestEffort } from './notifications.js';
|
|
84
92
|
import { refreshPluginSettingsBestEffort, settingsBlockOf } from './plugin-settings.js';
|
|
93
|
+
import { refreshPluginIdentity } from './identity-sync.js';
|
|
94
|
+
import { refreshUserSurfaceVhosts } from './user-surface.js';
|
|
85
95
|
|
|
86
96
|
/** Replaces the OIDC client sync (tests: there is no Authelia to write to). */
|
|
87
97
|
let oidcSyncOverride = null;
|
|
@@ -163,6 +173,16 @@ export async function uninstallPluginWithHosting(
|
|
|
163
173
|
}
|
|
164
174
|
await op.step('remove the package', () => uninstallPlugin(name, logger));
|
|
165
175
|
setPluginCapabilities(await getPluginCapabilities());
|
|
176
|
+
// With purge nothing of the plugin is kept — not who held its roles.
|
|
177
|
+
if (purge) {
|
|
178
|
+
const { revokePluginRoleGrants } = await import('./entitlements.js');
|
|
179
|
+
const revoked = await revokePluginRoleGrants(plugin, { actor, logger });
|
|
180
|
+
if (!revoked.ok) {
|
|
181
|
+
op.warn(
|
|
182
|
+
`The plugin's role grants were not revoked (${revoked.error}) — revoke them in Gatekeeper`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
166
186
|
await recordAuditBestEffort(
|
|
167
187
|
{ actor, action: 'plugin.uninstalled', plugin: name, details: { operation: op.id } },
|
|
168
188
|
logger,
|
|
@@ -261,6 +281,32 @@ export function settingsApprovalRequired(diff) {
|
|
|
261
281
|
];
|
|
262
282
|
}
|
|
263
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Why a version waits for an administrator because of how it changes the
|
|
286
|
+
* plugin's identity block (`identity.changed`): it adds or removes a role —
|
|
287
|
+
* entitlements name roles, and a new role is a new power over its users —
|
|
288
|
+
* or widens the attributes it receives about them. Narrowing the
|
|
289
|
+
* attributes and editing labels only change the digest. Auto-approve never
|
|
290
|
+
* covers it.
|
|
291
|
+
*
|
|
292
|
+
* @param {(import('@lamalibre/lamaste').IdentityDiff & { before: object | null, after: object | null }) | null} identity
|
|
293
|
+
* @returns {Array<{ code: string, reason: string }>}
|
|
294
|
+
*/
|
|
295
|
+
function identityApprovalRequired(identity) {
|
|
296
|
+
if (!identity || !identityChangeNeedsApproval(identity)) return [];
|
|
297
|
+
const changes = [];
|
|
298
|
+
if (identity.rolesAdded.length > 0) {
|
|
299
|
+
changes.push(`adds roles (${identity.rolesAdded.join(', ')})`);
|
|
300
|
+
}
|
|
301
|
+
if (identity.rolesRemoved.length > 0) {
|
|
302
|
+
changes.push(`removes roles (${identity.rolesRemoved.join(', ')})`);
|
|
303
|
+
}
|
|
304
|
+
if (identity.attributesAdded.length > 0) {
|
|
305
|
+
changes.push(`receives more about its users (${identity.attributesAdded.join(', ')})`);
|
|
306
|
+
}
|
|
307
|
+
return [{ code: IDENTITY_CHANGED, reason: `it ${changes.join(' and ')}` }];
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
/**
|
|
265
311
|
* Why a staged version waits for an administrator's approval (`[{ code,
|
|
266
312
|
* reason }]`; empty: it needs none and runs at once) — what the approval of
|
|
@@ -268,21 +314,32 @@ export function settingsApprovalRequired(diff) {
|
|
|
268
314
|
* in for an administrator on the first reason alone:
|
|
269
315
|
*
|
|
270
316
|
* - `digest.unapproved` — what it brings (relay hooks, the agent half
|
|
271
|
-
* Lamaste installs) is not what was
|
|
272
|
-
*
|
|
317
|
+
* Lamaste installs, its identity block, its web tree) is not what was
|
|
318
|
+
* approved;
|
|
319
|
+
* - `settings.secret-to-plain` — {@link settingsApprovalRequired};
|
|
320
|
+
* - `identity.changed` — {@link identityApprovalRequired}.
|
|
273
321
|
*
|
|
274
|
-
* @param {{ approvalRequired: boolean, digest: string | null, relaySet: object | null, agentHalf: object | null }} staged
|
|
322
|
+
* @param {{ approvalRequired: boolean, digest: string | null, relaySet: object | null, agentHalf: object | null, identity: object | null }} staged
|
|
275
323
|
* @param {{ approvedDigest: string | null } | null} state
|
|
276
324
|
* @param {import('@lamalibre/lamaste').SettingsSchemaDiff | null} settings
|
|
325
|
+
* @param {(import('@lamalibre/lamaste').IdentityDiff & { before: object | null, after: object | null }) | null} identity
|
|
277
326
|
* @returns {Array<{ code: string, reason: string }>}
|
|
278
327
|
*/
|
|
279
|
-
function approvalRequiredFor(staged, state, settings) {
|
|
328
|
+
function approvalRequiredFor(staged, state, settings, identity) {
|
|
280
329
|
const digestApproved = !staged.approvalRequired || state?.approvedDigest === staged.digest;
|
|
281
330
|
return [
|
|
282
331
|
...(digestApproved
|
|
283
332
|
? []
|
|
284
|
-
: [
|
|
333
|
+
: [
|
|
334
|
+
digestUnapprovedReason({
|
|
335
|
+
relay: staged.relaySet,
|
|
336
|
+
agentHalf: staged.agentHalf,
|
|
337
|
+
identity: staged.identity,
|
|
338
|
+
web: staged.webTree,
|
|
339
|
+
}),
|
|
340
|
+
]),
|
|
285
341
|
...settingsApprovalRequired(settings),
|
|
342
|
+
...identityApprovalRequired(identity),
|
|
286
343
|
];
|
|
287
344
|
}
|
|
288
345
|
|
|
@@ -391,6 +448,10 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
391
448
|
}
|
|
392
449
|
entrySha256 = hookEntrySha256(moduleBytes);
|
|
393
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;
|
|
394
455
|
// The staged version as a registry entry, so it is judged exactly as the
|
|
395
456
|
// installed one will be.
|
|
396
457
|
const entry = {
|
|
@@ -401,6 +462,9 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
401
462
|
agent: manifest.agent,
|
|
402
463
|
agentHalfPin,
|
|
403
464
|
settings: manifest.settings,
|
|
465
|
+
identity: identityOf(manifest),
|
|
466
|
+
userSurface: manifest.userSurface,
|
|
467
|
+
webTreeSha256: webTree?.sha256 ?? null,
|
|
404
468
|
};
|
|
405
469
|
const approval = installedApproval(entry);
|
|
406
470
|
return {
|
|
@@ -412,6 +476,10 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
412
476
|
entrySha256,
|
|
413
477
|
agentHalfPin,
|
|
414
478
|
agentHalf: approval.agentHalf,
|
|
479
|
+
// The staged identity block, normalized (null: it declares none).
|
|
480
|
+
identity: approval.identity,
|
|
481
|
+
// The staged web tree's manifest (null: it declares no web surface).
|
|
482
|
+
webTree,
|
|
415
483
|
// The relay hook set's own digest — what the runner holds.
|
|
416
484
|
relayDigest: approval.relay?.digest ?? null,
|
|
417
485
|
approvalRequired: approval.required,
|
|
@@ -439,6 +507,121 @@ function settingsPreview(plugin, manifest) {
|
|
|
439
507
|
return settingsSchemaDiff(installed, staged);
|
|
440
508
|
}
|
|
441
509
|
|
|
510
|
+
/**
|
|
511
|
+
* How a staged version changes the identity block, for the update's
|
|
512
|
+
* preview: both blocks, normalized (null: that version declares none), and
|
|
513
|
+
* core's `identityDiff` — roles added and removed, attributes added and
|
|
514
|
+
* removed, roles whose label or description changed. Null when neither
|
|
515
|
+
* version declares one.
|
|
516
|
+
*
|
|
517
|
+
* @param {object} plugin the installed registry entry
|
|
518
|
+
* @param {object} manifest the staged version's manifest
|
|
519
|
+
*/
|
|
520
|
+
function identityPreview(plugin, manifest) {
|
|
521
|
+
const before = plugin.identity ?? null;
|
|
522
|
+
const after = identityOf(manifest) ?? null;
|
|
523
|
+
if (!before && !after) return null;
|
|
524
|
+
return { before, after, ...identityDiff(before, after) };
|
|
525
|
+
}
|
|
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
|
+
|
|
442
625
|
/**
|
|
443
626
|
* Update plugin `name` to `requestedVersion` (an exact version or a
|
|
444
627
|
* dist-tag; default `latest`). Returns `{ upToDate }`, `{ pendingUpdate }`
|
|
@@ -489,7 +672,8 @@ export async function updatePlugin(
|
|
|
489
672
|
|
|
490
673
|
const state = await readHookState(name);
|
|
491
674
|
const settings = settingsPreview(plugin, staged.manifest);
|
|
492
|
-
const
|
|
675
|
+
const identity = identityPreview(plugin, staged.manifest);
|
|
676
|
+
const approvalRequired = approvalRequiredFor(staged, state, settings, identity);
|
|
493
677
|
let approvedBy = null;
|
|
494
678
|
let autoApproved = false;
|
|
495
679
|
if (approvedUpdate !== null) {
|
|
@@ -497,13 +681,14 @@ export async function updatePlugin(
|
|
|
497
681
|
approvedBy = actor;
|
|
498
682
|
} else if (approvalRequired.length > 0) {
|
|
499
683
|
// Auto-approve stands in for an administrator on a digest that changed,
|
|
500
|
-
// when the agent half changes nothing that runs as root on the agent
|
|
501
|
-
// and on nothing else: every other reason (a secret setting turned
|
|
502
|
-
// plain) withholds it.
|
|
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
|
|
686
|
+
// plain, a role added or removed, an attribute widened) withholds it.
|
|
503
687
|
const digestChanged = approvalRequired.some((reason) => reason.code === DIGEST_UNAPPROVED);
|
|
504
688
|
const withheldReasons = state?.autoApprove
|
|
505
689
|
? [
|
|
506
690
|
...(digestChanged ? agentHalfWithheld(installedAgentHalf(plugin), staged.agentHalf) : []),
|
|
691
|
+
...(digestChanged ? webTreeWithheld(plugin, staged) : []),
|
|
507
692
|
...approvalRequired
|
|
508
693
|
.filter((reason) => reason.code !== DIGEST_UNAPPROVED)
|
|
509
694
|
.map(({ code, reason }) => ({ code, message: reason })),
|
|
@@ -514,6 +699,8 @@ export async function updatePlugin(
|
|
|
514
699
|
plugin,
|
|
515
700
|
staged,
|
|
516
701
|
settings,
|
|
702
|
+
identity,
|
|
703
|
+
web: await webPreview(plugin, staged),
|
|
517
704
|
approvalRequired,
|
|
518
705
|
withheldReasons,
|
|
519
706
|
actor,
|
|
@@ -592,6 +779,8 @@ async function awaitApproval({
|
|
|
592
779
|
plugin,
|
|
593
780
|
staged,
|
|
594
781
|
settings,
|
|
782
|
+
identity,
|
|
783
|
+
web,
|
|
595
784
|
approvalRequired,
|
|
596
785
|
withheldReasons,
|
|
597
786
|
actor,
|
|
@@ -609,6 +798,8 @@ async function awaitApproval({
|
|
|
609
798
|
? agentHalfPreview(derivePluginRoute(plugin.name), staged.agentHalf)
|
|
610
799
|
: null,
|
|
611
800
|
settings,
|
|
801
|
+
identity,
|
|
802
|
+
web,
|
|
612
803
|
approvalRequired,
|
|
613
804
|
...(withheld.length > 0 ? { autoApproveWithheld: withheld } : {}),
|
|
614
805
|
};
|
|
@@ -654,7 +845,10 @@ async function awaitApproval({
|
|
|
654
845
|
},
|
|
655
846
|
logger,
|
|
656
847
|
);
|
|
657
|
-
|
|
848
|
+
const { withIdentityGrantImpact } = await import('./entitlements.js');
|
|
849
|
+
return {
|
|
850
|
+
pendingUpdate: await withIdentityGrantImpact(pendingUpdateView(pendingUpdate), plugin.route),
|
|
851
|
+
};
|
|
658
852
|
}
|
|
659
853
|
|
|
660
854
|
async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
@@ -704,6 +898,9 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
704
898
|
hooks: installed.manifest.hooks,
|
|
705
899
|
hooksEntrySha256: installed.hooksEntrySha256,
|
|
706
900
|
agent: installed.manifest.agent,
|
|
901
|
+
identity: identityOf(installed.manifest),
|
|
902
|
+
userSurface: installed.manifest.userSurface,
|
|
903
|
+
webTreeSha256: installed.webTreeSha256,
|
|
707
904
|
};
|
|
708
905
|
let approval;
|
|
709
906
|
try {
|
|
@@ -719,6 +916,7 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
719
916
|
installed.manifest,
|
|
720
917
|
installed.hooksEntrySha256,
|
|
721
918
|
staged.agentHalfPin,
|
|
919
|
+
installed.webTreeSha256,
|
|
722
920
|
);
|
|
723
921
|
if (approval.required) {
|
|
724
922
|
await recordApprovedSet(installedEntry, approval.digest, staged.version, approvedBy);
|
|
@@ -726,6 +924,13 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
726
924
|
setPluginCapabilities(await getPluginCapabilities());
|
|
727
925
|
createdSecrets = await syncOidcBestEffort(logger);
|
|
728
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
|
+
},
|
|
729
934
|
revert: async ({ applied }) => {
|
|
730
935
|
if (applied) {
|
|
731
936
|
const restored = await replacePluginPackage(
|
|
@@ -739,6 +944,7 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
739
944
|
restored.manifest,
|
|
740
945
|
restored.hooksEntrySha256,
|
|
741
946
|
plugin.agentHalfPin ?? null,
|
|
947
|
+
restored.webTreeSha256,
|
|
742
948
|
);
|
|
743
949
|
await restoreHookState(name, priorHookState);
|
|
744
950
|
setPluginCapabilities(await getPluginCapabilities());
|
|
@@ -760,6 +966,14 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
760
966
|
// The agent half may have dropped values the new version retyped or no
|
|
761
967
|
// longer knows (and keeps that after a revert): the relay re-reads them.
|
|
762
968
|
if (hosted) await refreshPluginSettingsBestEffort(name, logger);
|
|
969
|
+
// The roles and attributes may have changed with the version (or back,
|
|
970
|
+
// after a revert); the agent half of the installed version must answer
|
|
971
|
+
// protocol 1 again before its users reach it.
|
|
972
|
+
await refreshPluginIdentity(name, logger);
|
|
973
|
+
// So may whether it declares identity at all: the hosts its user surface
|
|
974
|
+
// is bound to serve it from the version that does — a plugin's
|
|
975
|
+
// switch-over to the relay's user surface is this update.
|
|
976
|
+
await refreshUserSurfaceVhosts(plugin.route, logger);
|
|
763
977
|
|
|
764
978
|
// Hooks the new version no longer has: the old runner goes once the new
|
|
765
979
|
// version is in place.
|
|
@@ -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 });
|