@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit a command with a usage error or a panel refusal — as text, or as an
|
|
3
|
+
* NDJSON error carrying the refusal's `code` (e.g. `last_admin`,
|
|
4
|
+
* `domain_in_use`). The message is printed only as terminal text.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { emitError } from './ndjson.js';
|
|
9
|
+
import { terminalText } from './plugin-settings.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} message
|
|
13
|
+
* @param {boolean} json
|
|
14
|
+
* @param {{ code?: string }} [err]
|
|
15
|
+
* @returns {never}
|
|
16
|
+
*/
|
|
17
|
+
export function fail(message, json, err = {}) {
|
|
18
|
+
if (json) emitError(message, false, typeof err.code === 'string' ? { code: err.code } : {});
|
|
19
|
+
else console.error(`\n ${chalk.red(terminalText(message))}\n`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grants on a plugin's user-facing OIDC clients as `lamaste-server plugins
|
|
3
|
+
* oidc-clients` takes and prints them — pure helpers, exported for tests
|
|
4
|
+
* (the command is src/commands/plugins.js).
|
|
5
|
+
*
|
|
6
|
+
* A grant is a Gatekeeper grant with `resourceType: 'oidc-client'` and the
|
|
7
|
+
* client id as `resourceId`; members of the plugin's domain sign in without
|
|
8
|
+
* one, an external member only with one (to them or a Lamaste group they
|
|
9
|
+
* belong to).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { terminalText } from './plugin-settings.js';
|
|
13
|
+
|
|
14
|
+
/** The Gatekeeper resource type of a grant on an OIDC client. */
|
|
15
|
+
export const OIDC_CLIENT_RESOURCE = 'oidc-client';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The principal `--user <username>` or `--group <name>` names — exactly one
|
|
19
|
+
* of them — or null.
|
|
20
|
+
*
|
|
21
|
+
* @param {string[]} args
|
|
22
|
+
* @returns {{ principalType: 'user' | 'group', principalId: string } | null}
|
|
23
|
+
*/
|
|
24
|
+
export function parsePrincipal(args) {
|
|
25
|
+
if (args.length !== 2) return null;
|
|
26
|
+
const [flag, id] = args;
|
|
27
|
+
if (!id || id.startsWith('--')) return null;
|
|
28
|
+
if (flag === '--user') return { principalType: 'user', principalId: id };
|
|
29
|
+
if (flag === '--group') return { principalType: 'group', principalId: id };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A grant's principal in words: the username, or `group:<name>`. */
|
|
34
|
+
export function principalText(grant) {
|
|
35
|
+
return terminalText(
|
|
36
|
+
grant.principalType === 'group' ? `group:${grant.principalId}` : grant.principalId,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every grant on `clientId` that names `principal`, among the clients
|
|
42
|
+
* `GET /api/plugins/:name/oidc-clients` lists — Gatekeeper may hold more
|
|
43
|
+
* than one (grants differing only in their context), and revoking means
|
|
44
|
+
* revoking them all — or why there is none to revoke: `no_client` (not a
|
|
45
|
+
* user-facing client of the plugin), `unreadable` (Gatekeeper could not be
|
|
46
|
+
* read), `no_grant`.
|
|
47
|
+
*
|
|
48
|
+
* @param {ReadonlyArray<Record<string, any>>} clients
|
|
49
|
+
* @param {string} clientId
|
|
50
|
+
* @param {{ principalType: string, principalId: string }} principal
|
|
51
|
+
* @returns {{ grantIds: string[] } | { error: 'no_client' | 'unreadable' | 'no_grant' }}
|
|
52
|
+
*/
|
|
53
|
+
export function findClientGrants(clients, clientId, principal) {
|
|
54
|
+
const client = clients.find((c) => c.id === clientId && c.userFacing);
|
|
55
|
+
if (!client) return { error: 'no_client' };
|
|
56
|
+
if (!Array.isArray(client.grants)) return { error: 'unreadable' };
|
|
57
|
+
const grantIds = client.grants
|
|
58
|
+
.filter(
|
|
59
|
+
(g) => g.principalType === principal.principalType && g.principalId === principal.principalId,
|
|
60
|
+
)
|
|
61
|
+
.map((g) => g.grantId);
|
|
62
|
+
return grantIds.length > 0 ? { grantIds } : { error: 'no_grant' };
|
|
63
|
+
}
|
|
@@ -129,6 +129,20 @@ export async function withCurlConfig(content, fn) {
|
|
|
129
129
|
* @param {{ identity: { certPath: string, address: string, port: number }, method: string, path: string, body?: Record<string, unknown> | null, p12Path: string, configPath: string }} request
|
|
130
130
|
* @returns {{ args: string[], input: string | undefined }}
|
|
131
131
|
*/
|
|
132
|
+
/**
|
|
133
|
+
* Refuse a request path with a `.` or `..` segment: curl and `fetch`
|
|
134
|
+
* resolve them (`/api/domains/../activate` → `/api/activate`), so a value
|
|
135
|
+
* put into a path — a domain, a user, a plugin — could name another route.
|
|
136
|
+
* Every path the client sends passes here; values are percent-encoded as
|
|
137
|
+
* usual where the path is built.
|
|
138
|
+
*/
|
|
139
|
+
export function assertPanelPath(path) {
|
|
140
|
+
const route = String(path).split(/[?#]/, 1)[0];
|
|
141
|
+
if (route.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
142
|
+
throw new Error('A panel request path may not have a "." or ".." segment');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
export function buildPanelCurlRequest({
|
|
133
147
|
identity,
|
|
134
148
|
method,
|
|
@@ -137,6 +151,7 @@ export function buildPanelCurlRequest({
|
|
|
137
151
|
p12Path,
|
|
138
152
|
configPath,
|
|
139
153
|
}) {
|
|
154
|
+
assertPanelPath(path);
|
|
140
155
|
const { certPath, address, port } = identity;
|
|
141
156
|
const args = [
|
|
142
157
|
'-q', // No .curlrc — it must be the first argument
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
|
|
3
3
|
* for approval, what an identity block lets a plugin learn and how an
|
|
4
|
-
* update changes it, the
|
|
4
|
+
* update changes it, the web tree a version brings and how an update
|
|
5
|
+
* changes it, the command that approves it, and the arguments that
|
|
5
6
|
* command takes. Pure — no output — so they are tested as they are.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { terminalText } from './plugin-settings.js';
|
|
10
|
+
import { bytesText, shortDigest } from './plugin-web-surface.js';
|
|
9
11
|
|
|
10
12
|
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
11
13
|
const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
|
|
@@ -141,6 +143,83 @@ export function identityDiffLines(diff) {
|
|
|
141
143
|
return { headline, lines };
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* What the installed version's web surface brings (the hook status's
|
|
148
|
+
* `web`: `{ declared, approved, treeSha256, surface }`) as lines — the tree
|
|
149
|
+
* the approval covers, and the site Lamaste offers for it. Empty when it
|
|
150
|
+
* declares none.
|
|
151
|
+
*
|
|
152
|
+
* @param {unknown} web
|
|
153
|
+
* @returns {string[]}
|
|
154
|
+
*/
|
|
155
|
+
export function webSurfaceBlockLines(web) {
|
|
156
|
+
if (web === null || typeof web !== 'object' || !web.declared) return [];
|
|
157
|
+
const surface = web.surface !== null && typeof web.surface === 'object' ? web.surface : null;
|
|
158
|
+
const lines = [terminalText(`tree: ${web.treeSha256 ?? 'unknown'}`)];
|
|
159
|
+
if (surface) {
|
|
160
|
+
lines.push(
|
|
161
|
+
terminalText(
|
|
162
|
+
`site: offered on ${surface.defaultSubdomain} (${surface.accessMode}), the shell from ${surface.root}/ of its server package`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* How an update changes the plugin's web tree (`pendingUpdate.web`:
|
|
171
|
+
* `{ before: sha256 | null, after: { sha256, files, totalBytes } | null }`
|
|
172
|
+
* — part of the digest): a headline and its lines. Null when neither
|
|
173
|
+
* version declares a web surface.
|
|
174
|
+
*
|
|
175
|
+
* @param {unknown} web
|
|
176
|
+
* @returns {{ headline: string, lines: string[], changed: boolean } | null}
|
|
177
|
+
*/
|
|
178
|
+
export function webTreeDiffLines(web) {
|
|
179
|
+
if (web === null || typeof web !== 'object') return null;
|
|
180
|
+
const before = typeof web.before === 'string' ? web.before : null;
|
|
181
|
+
const after =
|
|
182
|
+
web.after !== null && typeof web.after === 'object' && typeof web.after.sha256 === 'string'
|
|
183
|
+
? web.after
|
|
184
|
+
: null;
|
|
185
|
+
if (before === null && after === null) return null;
|
|
186
|
+
const contents = (tree) => `${tree.files} file(s), ${bytesText(tree.totalBytes)}`;
|
|
187
|
+
if (after === null) {
|
|
188
|
+
return {
|
|
189
|
+
headline:
|
|
190
|
+
'no longer declares a web surface — its site answers 503 until a version that does is approved',
|
|
191
|
+
lines: [terminalText(`tree: ${shortDigest(before)} → none`)],
|
|
192
|
+
changed: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (before === null) {
|
|
196
|
+
return {
|
|
197
|
+
headline: 'declares a web surface for the first time',
|
|
198
|
+
lines: [
|
|
199
|
+
terminalText(`tree: ${after.sha256}`),
|
|
200
|
+
terminalText(`contents: ${contents(after)}`),
|
|
201
|
+
],
|
|
202
|
+
changed: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (before === after.sha256) {
|
|
206
|
+
return {
|
|
207
|
+
headline: 'keeps its web tree',
|
|
208
|
+
lines: [terminalText(`tree: ${after.sha256} (${contents(after)})`)],
|
|
209
|
+
changed: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
headline: 'changes its web tree — its site serves the new shell once approved',
|
|
214
|
+
lines: [
|
|
215
|
+
terminalText(`tree: ${shortDigest(before)} → ${shortDigest(after.sha256)}`),
|
|
216
|
+
terminalText(`new tree: ${after.sha256}`),
|
|
217
|
+
terminalText(`contents: ${contents(after)}`),
|
|
218
|
+
],
|
|
219
|
+
changed: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
144
223
|
/**
|
|
145
224
|
* The command that approves what plugin `name` brings: the installed
|
|
146
225
|
* version by its digest (`{ digest }`), or a pending update by its version
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's web surface at the terminal — the arguments of
|
|
3
|
+
* `lamaste-server plugins web-surface <name> [create|move|delete]`, the
|
|
4
|
+
* words its output is made of, and how `sites list` marks a site a plugin
|
|
5
|
+
* owns (what an approval shows about a web tree is in `plugin-approval.js`). Pure — no output, no
|
|
6
|
+
* panel — so they are tested as they are. Every string from the panel goes
|
|
7
|
+
* through `terminalText` before it is printed; `--json` carries the data as
|
|
8
|
+
* it came.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { WEB_SURFACE_ACCESS_MODES, isWebSurfaceSubdomain } from '@lamalibre/lamaste';
|
|
12
|
+
import { terminalText } from './plugin-settings.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Arguments
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** How `plugins web-surface` is called. */
|
|
19
|
+
export const WEB_SURFACE_USAGE =
|
|
20
|
+
'Usage: lamaste-server plugins web-surface <name> [create [--subdomain <label>] [--access-mode authenticated|restricted] | move --tunnel <id | subdomain | fqdn> | delete [--yes]]';
|
|
21
|
+
|
|
22
|
+
/** The flags each action takes, and whether each takes a value. */
|
|
23
|
+
const ACTION_FLAGS = {
|
|
24
|
+
status: {},
|
|
25
|
+
create: { '--subdomain': true, '--access-mode': true },
|
|
26
|
+
move: { '--tunnel': true },
|
|
27
|
+
delete: { '--yes': false },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Every flag that takes a value, whichever action it belongs to. */
|
|
31
|
+
const VALUE_FLAGS = new Set(
|
|
32
|
+
Object.values(ACTION_FLAGS).flatMap((flags) =>
|
|
33
|
+
Object.entries(flags)
|
|
34
|
+
.filter(([, takesValue]) => takesValue)
|
|
35
|
+
.map(([flag]) => flag),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The arguments of `plugins web-surface …` after the subcommand: the
|
|
41
|
+
* plugin, then an action — none (its status), `create` (with `--subdomain`
|
|
42
|
+
* and `--access-mode`, each defaulting to what the plugin declares),
|
|
43
|
+
* `move --tunnel <ref>` (a tunnel's id, subdomain or FQDN), or `delete`
|
|
44
|
+
* (`--yes` skips the typed confirmation).
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} args
|
|
47
|
+
* @returns {{ ok: true, name: string, action: 'status' }
|
|
48
|
+
* | { ok: true, name: string, action: 'create', subdomain: string | null, accessMode: string | null }
|
|
49
|
+
* | { ok: true, name: string, action: 'move', tunnel: string }
|
|
50
|
+
* | { ok: true, name: string, action: 'delete', yes: boolean }
|
|
51
|
+
* | { ok: false, error: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function parseWebSurfaceArgs(args) {
|
|
54
|
+
const usage = WEB_SURFACE_USAGE;
|
|
55
|
+
const positional = [];
|
|
56
|
+
/** @type {Map<string, string | true>} */
|
|
57
|
+
const flags = new Map();
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
if (!arg.startsWith('--')) {
|
|
61
|
+
positional.push(arg);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (flags.has(arg)) return { ok: false, error: `${arg} is given twice — ${usage}` };
|
|
65
|
+
if (!VALUE_FLAGS.has(arg)) {
|
|
66
|
+
flags.set(arg, true);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const value = args[i + 1];
|
|
70
|
+
if (value === undefined || value.startsWith('--')) {
|
|
71
|
+
return { ok: false, error: `${arg} needs a value — ${usage}` };
|
|
72
|
+
}
|
|
73
|
+
flags.set(arg, value);
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
if (positional.length === 0) return { ok: false, error: usage };
|
|
77
|
+
if (positional.length > 2) {
|
|
78
|
+
return { ok: false, error: `Unexpected argument "${positional[2]}" — ${usage}` };
|
|
79
|
+
}
|
|
80
|
+
const [name, action = 'status'] = positional;
|
|
81
|
+
if (positional.length === 2 && !['create', 'move', 'delete'].includes(action)) {
|
|
82
|
+
return { ok: false, error: `Unknown action "${action}" — ${usage}` };
|
|
83
|
+
}
|
|
84
|
+
const allowed = ACTION_FLAGS[action];
|
|
85
|
+
for (const flag of flags.keys()) {
|
|
86
|
+
if (!Object.hasOwn(allowed, flag)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error:
|
|
90
|
+
action === 'status'
|
|
91
|
+
? `Unknown option ${flag} — ${usage}`
|
|
92
|
+
: `${action} takes no ${flag} — ${usage}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (action) {
|
|
98
|
+
case 'create': {
|
|
99
|
+
const subdomain = flags.get('--subdomain') ?? null;
|
|
100
|
+
const accessMode = flags.get('--access-mode') ?? null;
|
|
101
|
+
if (subdomain !== null && !isWebSurfaceSubdomain(subdomain)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: `"${subdomain}" cannot be a web surface's subdomain: a DNS label, not reserved (panel, auth, tunnel, www, mail, ftp, api) and not agent-*`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (accessMode !== null && !WEB_SURFACE_ACCESS_MODES.includes(accessMode)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: `--access-mode is ${WEB_SURFACE_ACCESS_MODES.join(' or ')} — a web surface is never public`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, name, action, subdomain, accessMode };
|
|
114
|
+
}
|
|
115
|
+
case 'move': {
|
|
116
|
+
const tunnel = flags.get('--tunnel');
|
|
117
|
+
if (tunnel === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: `Name the tunnel to move: --tunnel <id | subdomain | fqdn> — ${usage}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, name, action, tunnel };
|
|
124
|
+
}
|
|
125
|
+
case 'delete':
|
|
126
|
+
return { ok: true, name, action, yes: flags.has('--yes') };
|
|
127
|
+
default:
|
|
128
|
+
return { ok: true, name, action: 'status' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The body of `POST /api/plugins/:name/web-surface`: only what was given —
|
|
134
|
+
* the panel falls back to what the plugin declares.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ subdomain: string | null, accessMode: string | null }} parsed
|
|
137
|
+
* @returns {{ subdomain?: string, accessMode?: string }}
|
|
138
|
+
*/
|
|
139
|
+
export function createBody({ subdomain, accessMode }) {
|
|
140
|
+
return {
|
|
141
|
+
...(subdomain !== null ? { subdomain } : {}),
|
|
142
|
+
...(accessMode !== null ? { accessMode } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Output
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** A tree digest as the terminal shows it beside others: its first 12 hex digits. */
|
|
151
|
+
export function shortDigest(sha256) {
|
|
152
|
+
if (typeof sha256 !== 'string' || sha256.length === 0) return '—';
|
|
153
|
+
return terminalText(sha256.slice(0, 12));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A size in bytes, in words (`1.5 MiB`). */
|
|
157
|
+
export function bytesText(bytes) {
|
|
158
|
+
if (!Number.isFinite(bytes) || bytes < 0) return 'an unknown size';
|
|
159
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
160
|
+
const units = ['KiB', 'MiB', 'GiB'];
|
|
161
|
+
let value = bytes / 1024;
|
|
162
|
+
let unit = 0;
|
|
163
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
164
|
+
value /= 1024;
|
|
165
|
+
unit++;
|
|
166
|
+
}
|
|
167
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether the web surface's site serves the shell and the API, and why not
|
|
172
|
+
* — the status's `served`/`reason` (and whether it owns a site at all) in
|
|
173
|
+
* a sentence.
|
|
174
|
+
*
|
|
175
|
+
* @param {{ served?: boolean, reason?: string | null, site?: unknown }} view
|
|
176
|
+
* @param {string} name the plugin
|
|
177
|
+
* @returns {{ served: boolean, text: string }}
|
|
178
|
+
*/
|
|
179
|
+
export function webSurfaceServedText(view, name) {
|
|
180
|
+
const plugin = terminalText(name);
|
|
181
|
+
const hasSite = view?.site !== null && view?.site !== undefined;
|
|
182
|
+
if (view?.served) {
|
|
183
|
+
return hasSite
|
|
184
|
+
? { served: true, text: 'served — its site serves the shell and the API' }
|
|
185
|
+
: {
|
|
186
|
+
served: false,
|
|
187
|
+
text: 'ready to be served, but it owns no site yet — create one, or move a bound tunnel to it',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const where = hasSite ? 'its site answers 503' : 'it owns no site yet';
|
|
191
|
+
const notServed = (why) => ({ served: false, text: `${why} (${where})` });
|
|
192
|
+
switch (view?.reason) {
|
|
193
|
+
case 'not_installed':
|
|
194
|
+
return notServed('not served — the plugin is not installed');
|
|
195
|
+
case 'no_identity':
|
|
196
|
+
return notServed(
|
|
197
|
+
'not served — its installed version declares no identity, which a web surface fronts',
|
|
198
|
+
);
|
|
199
|
+
case 'no_web_surface':
|
|
200
|
+
return notServed('not served — its installed version declares no web surface');
|
|
201
|
+
case 'not_owner':
|
|
202
|
+
return notServed(
|
|
203
|
+
`not served — its site was created for another package under the same route: delete it (lamaste-server plugins web-surface ${plugin} delete --yes), then create one`,
|
|
204
|
+
);
|
|
205
|
+
case 'not_approved':
|
|
206
|
+
return notServed(
|
|
207
|
+
`not served — its installed version waits for approval: lamaste-server plugins hooks ${plugin}`,
|
|
208
|
+
);
|
|
209
|
+
case 'not_hosted':
|
|
210
|
+
return notServed(
|
|
211
|
+
`not served — it is not hosted on an agent, or its hosting is not active: lamaste-server plugins hosting ${plugin}`,
|
|
212
|
+
);
|
|
213
|
+
case 'tree_not_current':
|
|
214
|
+
return notServed(
|
|
215
|
+
'not served yet — the relay does not serve its approved web tree yet; it switches when the approved version is installed',
|
|
216
|
+
);
|
|
217
|
+
default:
|
|
218
|
+
return {
|
|
219
|
+
served: false,
|
|
220
|
+
text: `not served (${terminalText(view?.reason ?? 'unknown')}; ${where})`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The trees the relay holds for a web surface (the status's `tree`:
|
|
227
|
+
* `{ approved, current, previous }`) as `[label, value]` rows — short
|
|
228
|
+
* digests, with whether the one served is the approved one.
|
|
229
|
+
*
|
|
230
|
+
* @param {{ approved?: string | null, current?: string | null, previous?: string | null } | null | undefined} tree
|
|
231
|
+
* @returns {Array<[string, string]>}
|
|
232
|
+
*/
|
|
233
|
+
export function webTreeRows(tree) {
|
|
234
|
+
const approved = tree?.approved ?? null;
|
|
235
|
+
const current = tree?.current ?? null;
|
|
236
|
+
const previous = tree?.previous ?? null;
|
|
237
|
+
const serving =
|
|
238
|
+
current === null
|
|
239
|
+
? 'none'
|
|
240
|
+
: approved !== null && current === approved
|
|
241
|
+
? `${shortDigest(current)} (the approved tree)`
|
|
242
|
+
: `${shortDigest(current)} (not the approved tree)`;
|
|
243
|
+
return [
|
|
244
|
+
['approved tree', approved === null ? 'none' : shortDigest(approved)],
|
|
245
|
+
['serving', serving],
|
|
246
|
+
[
|
|
247
|
+
'previous',
|
|
248
|
+
previous === null ? 'none' : `${shortDigest(previous)} (fallback for pages still open)`,
|
|
249
|
+
],
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** What each refusal of the web-surface routes asks of the administrator, beside its message. */
|
|
254
|
+
const REFUSAL_HINTS = {
|
|
255
|
+
no_web_surface: () =>
|
|
256
|
+
'Its installed version declares no userSurface.web; update to a version that does.',
|
|
257
|
+
no_identity: () =>
|
|
258
|
+
'A web surface is the front of its user surface, which needs an identity block.',
|
|
259
|
+
exists: (name) => `It owns a site already: lamaste-server plugins web-surface ${name}`,
|
|
260
|
+
bound_to_tunnel: (name) =>
|
|
261
|
+
`Move that tunnel's host to it instead: lamaste-server plugins web-surface ${name} move --tunnel <id | fqdn>`,
|
|
262
|
+
hostname_taken: (name) =>
|
|
263
|
+
`Choose another subdomain: lamaste-server plugins web-surface ${name} create --subdomain <label>`,
|
|
264
|
+
not_bound: () =>
|
|
265
|
+
'Only a tunnel bound as its user surface moves (lamaste-server tunnels bind-user-surface).',
|
|
266
|
+
shared_tunnel: () =>
|
|
267
|
+
'Unbind the other plugins first: lamaste-server tunnels unbind-user-surface <tunnel> <plugin>',
|
|
268
|
+
tunnel_disabled: () => 'Enable the tunnel first: lamaste-server tunnels toggle <id> --enable',
|
|
269
|
+
not_hosted: (name) =>
|
|
270
|
+
`The agent carrying the tunnel must host it: lamaste-server plugins hosting ${name}`,
|
|
271
|
+
not_approved: (name) =>
|
|
272
|
+
`Approve its installed version first: lamaste-server plugins hooks ${name}`,
|
|
273
|
+
web_tree: () =>
|
|
274
|
+
"The approved web tree could not be put in place; nothing changed. The panel's log names the file.",
|
|
275
|
+
gatekeeper: () => 'Gatekeeper could not be reached; nothing changed. Try again once it runs.',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The next step a refusal (`code`) of the web-surface routes points to, or
|
|
280
|
+
* null when its message says all there is.
|
|
281
|
+
*
|
|
282
|
+
* @param {unknown} code
|
|
283
|
+
* @param {string} name the plugin
|
|
284
|
+
* @returns {string | null}
|
|
285
|
+
*/
|
|
286
|
+
export function webSurfaceRefusalHint(code, name) {
|
|
287
|
+
if (typeof code !== 'string' || !Object.hasOwn(REFUSAL_HINTS, code)) return null;
|
|
288
|
+
return terminalText(REFUSAL_HINTS[code](terminalText(name)));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Sites
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* How `sites list` marks a site: `[plugin <route>, <access mode>]` for a
|
|
297
|
+
* site a plugin owns as its web surface, null for any other.
|
|
298
|
+
*
|
|
299
|
+
* @param {Record<string, any>} site
|
|
300
|
+
* @returns {string | null}
|
|
301
|
+
*/
|
|
302
|
+
export function siteOwnerTag(site) {
|
|
303
|
+
const owner = site?.owner;
|
|
304
|
+
if (owner === null || typeof owner !== 'object' || typeof owner.plugin !== 'string') return null;
|
|
305
|
+
const mode = typeof site.accessMode === 'string' ? `, ${site.accessMode}` : '';
|
|
306
|
+
return terminalText(`[plugin ${owner.plugin}${mode}]`);
|
|
307
|
+
}
|