@lamalibre/create-lamaste 4.3.0 → 4.4.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.
Files changed (79) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +13 -8
  3. package/src/index.js +4 -0
  4. package/src/lib/panel-pin.js +41 -0
  5. package/src/lib/service-config.js +6 -0
  6. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  7. package/vendor/docs/00-introduction/quickstart.md +5 -5
  8. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  9. package/vendor/docs/01-concepts/certificates.md +3 -3
  10. package/vendor/docs/01-concepts/mtls.md +2 -2
  11. package/vendor/docs/01-concepts/security-model.md +50 -2
  12. package/vendor/docs/01-concepts/tickets.md +2 -2
  13. package/vendor/docs/01-concepts/tunneling.md +4 -4
  14. package/vendor/docs/02-guides/agent-setup.md +6 -6
  15. package/vendor/docs/02-guides/certificate-management.md +2 -2
  16. package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
  17. package/vendor/docs/02-guides/desktop-app-setup.md +12 -11
  18. package/vendor/docs/02-guides/installation.md +3 -3
  19. package/vendor/docs/02-guides/onboarding.md +2 -2
  20. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  21. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  22. package/vendor/docs/03-architecture/installer.md +4 -4
  23. package/vendor/docs/03-architecture/management-flow.md +1 -1
  24. package/vendor/docs/03-architecture/overview.md +5 -5
  25. package/vendor/docs/03-architecture/panel-client.md +7 -7
  26. package/vendor/docs/03-architecture/panel-server.md +2 -2
  27. package/vendor/docs/03-architecture/state-management.md +2 -2
  28. package/vendor/docs/03-architecture/system-overview.md +1 -1
  29. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +393 -33
  30. package/vendor/docs/04-api-reference/app-host.md +87 -38
  31. package/vendor/docs/04-api-reference/audit-and-notifications.md +10 -10
  32. package/vendor/docs/04-api-reference/certificates.md +22 -19
  33. package/vendor/docs/04-api-reference/lifecycle-hooks.md +33 -11
  34. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  35. package/vendor/docs/04-api-reference/overview.md +137 -135
  36. package/vendor/docs/04-api-reference/registry-and-oidc.md +1 -1
  37. package/vendor/docs/04-api-reference/tunnels.md +14 -10
  38. package/vendor/docs/05-operations/backup-and-restore.md +4 -1
  39. package/vendor/docs/05-operations/uninstalling.md +1 -1
  40. package/vendor/docs/05-operations/upgrades.md +6 -6
  41. package/vendor/docs/06-reference/config-files.md +94 -84
  42. package/vendor/docs/06-reference/glossary.md +3 -3
  43. package/vendor/docs/06-reference/installer-flags.md +13 -13
  44. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  45. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  46. package/vendor/docs/gatekeeper-design.md +8 -8
  47. package/vendor/docs/identity-directory-design.md +2 -2
  48. package/vendor/docs/package.json +1 -1
  49. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  50. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  51. package/vendor/gatekeeper/package.json +2 -2
  52. package/vendor/server/bin/lamaste-server.js +11 -1
  53. package/vendor/server/package.json +4 -3
  54. package/vendor/server/src/commands/panel-pin.js +40 -0
  55. package/vendor/server/src/commands/plugin-settings.js +541 -0
  56. package/vendor/server/src/commands/plugins.js +94 -27
  57. package/vendor/server/src/panel-api.js +281 -34
  58. package/vendor/server/src/panel-identity.js +165 -0
  59. package/vendor/server/src/plugin-approval.js +78 -0
  60. package/vendor/server/src/plugin-settings.js +699 -0
  61. package/vendor/server-ui/dist/assets/index-25wo_8av.css +1 -0
  62. package/vendor/server-ui/dist/assets/index-CESyyK8V.js +84 -0
  63. package/vendor/server-ui/dist/index.html +2 -2
  64. package/vendor/serverd/package.json +2 -2
  65. package/vendor/serverd/src/index.js +9 -0
  66. package/vendor/serverd/src/lib/agent-presence.js +106 -0
  67. package/vendor/serverd/src/lib/hook-pipelines.js +20 -4
  68. package/vendor/serverd/src/lib/migrations/state/0012_plugin_settings_and_presence.sql +62 -0
  69. package/vendor/serverd/src/lib/mtls.js +44 -4
  70. package/vendor/serverd/src/lib/plugin-hooks.js +120 -20
  71. package/vendor/serverd/src/lib/plugin-hosting.js +35 -14
  72. package/vendor/serverd/src/lib/plugin-lifecycle.js +267 -83
  73. package/vendor/serverd/src/lib/plugin-settings.js +1101 -0
  74. package/vendor/serverd/src/lib/plugins.js +73 -22
  75. package/vendor/serverd/src/lib/tunnel-hooks.js +11 -0
  76. package/vendor/serverd/src/routes/management/plugins.js +166 -19
  77. package/vendor/serverd/src/routes/management/tunnels.js +21 -1
  78. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +0 -1
  79. package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +0 -84
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The panel's IP listener, as `lamaste-server` verifies it.
3
+ *
4
+ * The CLI runs as root on the relay and sends the panel requests that carry
5
+ * the registry token, plugin settings and secrets, and the admin client
6
+ * certificate. It reaches the panel through nginx's IP vhost on port 9292
7
+ * (`lamalibre-lamaste-panel-ip`), which presents the self-signed certificate
8
+ * create-lamaste generated for the server's address
9
+ * (`<PKI_DIR>/self-signed.pem`; its key is 0600). Port 9292 is not a
10
+ * privileged port: whenever nginx does not hold it — panel 2FA switches the
11
+ * IP vhost off, nginx is stopped or failed to start — any local user can
12
+ * listen there. So the CLI never trusts whatever answers on it:
13
+ *
14
+ * - before connecting, it refuses when panel 2FA is on or the IP vhost is not
15
+ * enabled, since nginx then does not listen on 9292 (`assertPanelListener`);
16
+ * - every request verifies the listener against exactly that certificate,
17
+ * read from disk as root (`readPanelIdentity`, curl `--cacert`), never
18
+ * `-k`. The TLS handshake fails before the CLI presents its client
19
+ * certificate or sends a byte of the request (see `buildPanelCurlRequest`).
20
+ */
21
+
22
+ import { X509Certificate, createHash } from 'node:crypto';
23
+ import { lstat, readFile } from 'node:fs/promises';
24
+ import net from 'node:net';
25
+ import path from 'node:path';
26
+ import { PKI_DIR } from './config.js';
27
+
28
+ /** The port of nginx's IP vhost for the panel. */
29
+ export const PANEL_IP_PORT = 9292;
30
+
31
+ /** The certificate the IP vhost presents (create-lamaste, `tasks/nginx.js`). */
32
+ export const PANEL_IP_CERT_PATH = path.join(PKI_DIR, 'self-signed.pem');
33
+
34
+ /** The IP vhost's link in nginx's enabled sites (panel 2FA removes it). */
35
+ export const PANEL_IP_SITE_ENABLED = '/etc/nginx/sites-enabled/lamalibre-lamaste-panel-ip';
36
+
37
+ /** Where the CLI connects: the panel's own machine. */
38
+ export const PANEL_LOOPBACK = '127.0.0.1';
39
+
40
+ /**
41
+ * The IPv4 address a certificate is issued for: its first `IP Address`
42
+ * subject alternative name. create-lamaste (and the restore procedure in the
43
+ * docs) issue the IP vhost's certificate with `subjectAltName=IP:<address>`;
44
+ * curl checks the request's host against it. Pure, exported for tests.
45
+ *
46
+ * @param {X509Certificate} cert
47
+ * @returns {string}
48
+ */
49
+ export function certifiedAddress(cert) {
50
+ const names = (cert.subjectAltName ?? '').split(', ');
51
+ for (const name of names) {
52
+ if (!name.startsWith('IP Address:')) continue;
53
+ const address = name.slice('IP Address:'.length).trim();
54
+ if (net.isIPv4(address)) return address;
55
+ }
56
+ throw new Error(
57
+ 'The panel certificate names no IPv4 address (subjectAltName IP:<address>), so it cannot be verified',
58
+ );
59
+ }
60
+
61
+ /**
62
+ * The certificate's public-key pin: base64 SHA-256 of its SubjectPublicKeyInfo
63
+ * — curl's `--pinnedpubkey sha256//<pin>` form, the one agents and the
64
+ * desktop app store for the panel. Pure, exported for tests.
65
+ *
66
+ * @param {X509Certificate} cert
67
+ * @returns {string}
68
+ */
69
+ export function publicKeyPin(cert) {
70
+ const spki = cert.publicKey.export({ type: 'spki', format: 'der' });
71
+ return createHash('sha256').update(spki).digest('base64');
72
+ }
73
+
74
+ /**
75
+ * Read the IP vhost's certificate: the identity every panel request is
76
+ * verified against. A link or a file others could write is refused — the
77
+ * CLI only trusts a certificate as much as the file that holds it. The file
78
+ * is read on every request, so a certificate regenerated by a full
79
+ * reinstall (or the restore procedure) is picked up as soon as nginx serves
80
+ * it.
81
+ *
82
+ * @param {string} [certPath]
83
+ * @returns {Promise<{ certPath: string, address: string, port: number, pin: string }>}
84
+ */
85
+ export async function readPanelIdentity(certPath = PANEL_IP_CERT_PATH) {
86
+ let st;
87
+ try {
88
+ st = await lstat(certPath);
89
+ } catch (err) {
90
+ throw new Error(
91
+ `Cannot read the panel's TLS certificate ${certPath} (${err.code ?? err.message}); ` +
92
+ 'lamaste-server verifies the panel against it and sends nothing without it',
93
+ );
94
+ }
95
+ if (!st.isFile()) {
96
+ throw new Error(`The panel's TLS certificate ${certPath} is not a regular file`);
97
+ }
98
+ if ((st.mode & 0o022) !== 0) {
99
+ throw new Error(
100
+ `The panel's TLS certificate ${certPath} is writable by group or others; refusing to trust it`,
101
+ );
102
+ }
103
+ let cert;
104
+ try {
105
+ cert = new X509Certificate(await readFile(certPath));
106
+ } catch {
107
+ throw new Error(`${certPath} does not hold a certificate`);
108
+ }
109
+ return {
110
+ certPath,
111
+ address: certifiedAddress(cert),
112
+ port: PANEL_IP_PORT,
113
+ pin: publicKeyPin(cert),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Why nothing verified listens on the IP vhost's port, or null when nginx
119
+ * should hold it. Pure, exported for tests.
120
+ *
121
+ * @param {{ panel2faEnabled: boolean, siteEnabled: boolean }} state
122
+ * @returns {string | null}
123
+ */
124
+ export function panelListenerRefusal({ panel2faEnabled, siteEnabled }) {
125
+ if (panel2faEnabled) {
126
+ return (
127
+ 'Panel 2FA is enabled: it switches off the panel listener on port 9292 that lamaste-server ' +
128
+ 'talks to, and every admin request then needs a 2FA session the CLI cannot hold. Nothing was ' +
129
+ 'sent. Use the panel in the browser, or turn 2FA off in its Settings to use this command.'
130
+ );
131
+ }
132
+ if (!siteEnabled) {
133
+ return (
134
+ 'The panel listener on port 9292 is not enabled (nginx site lamalibre-lamaste-panel-ip), so ' +
135
+ 'nginx does not hold the port and nothing was sent to it. Re-enable the site or re-run ' +
136
+ 'create-lamaste.'
137
+ );
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Refuse before connecting when nginx does not listen on the IP vhost's
144
+ * port: whatever answered there could only be someone else's listener.
145
+ *
146
+ * @param {Record<string, unknown> | null} config panel.json
147
+ * @param {string} [siteEnabledPath]
148
+ * @returns {Promise<void>}
149
+ */
150
+ export async function assertPanelListener(config, siteEnabledPath = PANEL_IP_SITE_ENABLED) {
151
+ let siteEnabled = true;
152
+ try {
153
+ // The link itself, not what it points to: nginx follows it.
154
+ await lstat(siteEnabledPath);
155
+ } catch (err) {
156
+ if (err.code !== 'ENOENT') throw err;
157
+ siteEnabled = false;
158
+ }
159
+ const twofa = config?.panel2fa;
160
+ const refusal = panelListenerRefusal({
161
+ panel2faEnabled: typeof twofa === 'object' && twofa !== null && twofa.enabled === true,
162
+ siteEnabled,
163
+ });
164
+ if (refusal) throw new Error(refusal);
165
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
3
+ * for approval, the command that approves it, and the arguments that
4
+ * command takes. Pure — no output — so they are tested as they are.
5
+ */
6
+
7
+ import { terminalText } from './plugin-settings.js';
8
+
9
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
10
+ const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
11
+ /** The reason an update turns secret settings into plain ones. */
12
+ const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
13
+
14
+ /** How `approve-hooks` is called. */
15
+ export const APPROVE_HOOKS_USAGE =
16
+ 'Usage: lamaste-server plugins approve-hooks <name> --digest <sha256> [--version <v>] (what `hooks <name>` shows)';
17
+
18
+ /**
19
+ * Why a pending update waits, as the terminal shows it: each reason of
20
+ * `approvalRequired` (`[{ code, reason }]`; a secret turned plain is a
21
+ * warning), and the sentences of `autoApproveWithheld` that do not repeat
22
+ * one of them — what auto-approve, when on, could not cover beyond that.
23
+ * Every text as {@link terminalText}.
24
+ *
25
+ * @param {{ approvalRequired?: unknown, autoApproveWithheld?: unknown }} update
26
+ * @returns {{ required: Array<{ text: string, warning: boolean }>, notCovered: string[] }}
27
+ */
28
+ export function pendingUpdateReasons(update) {
29
+ const required = (Array.isArray(update?.approvalRequired) ? update.approvalRequired : [])
30
+ .filter((entry) => entry !== null && typeof entry === 'object')
31
+ .map((entry) => ({
32
+ text: terminalText(entry.reason),
33
+ warning: entry.code === SETTINGS_SECRET_TO_PLAIN,
34
+ }));
35
+ const said = new Set(required.map((line) => line.text));
36
+ const notCovered = (Array.isArray(update?.autoApproveWithheld) ? update.autoApproveWithheld : [])
37
+ .map((reason) => terminalText(reason))
38
+ .filter((text) => !said.has(text));
39
+ return { required, notCovered };
40
+ }
41
+
42
+ /**
43
+ * The command that approves what plugin `name` brings: the installed
44
+ * version by its digest (`{ digest }`), or a pending update by its version
45
+ * and — when it has one — its digest (`{ digest, version }`).
46
+ *
47
+ * @param {string} name
48
+ * @param {{ digest?: string | null, version?: string | null }} target
49
+ */
50
+ export function approveHooksCommand(name, { digest = null, version = null }) {
51
+ return [
52
+ `lamaste-server plugins approve-hooks ${terminalText(name)}`,
53
+ ...(digest ? [`--digest ${terminalText(digest)}`] : []),
54
+ ...(version ? [`--version ${terminalText(version)}`] : []),
55
+ ].join(' ');
56
+ }
57
+
58
+ /**
59
+ * The arguments of `approve-hooks <name>` after the name: `--digest
60
+ * <sha256>` (the installed version's, or a pending update's) and
61
+ * `--version <v>` (a pending update's) — at least one; `--version` alone
62
+ * approves an update that brings neither relay hooks nor an agent half.
63
+ *
64
+ * @param {string[]} rest
65
+ * @returns {{ digest: string | null, version: string | null } | { error: string }}
66
+ */
67
+ export function parseApproveArgs(rest) {
68
+ const valueOf = (flag) => {
69
+ const idx = rest.indexOf(flag);
70
+ return idx === -1 ? null : (rest[idx + 1] ?? '');
71
+ };
72
+ const digest = valueOf('--digest');
73
+ const version = valueOf('--version');
74
+ if (digest === null && version === null) return { error: APPROVE_HOOKS_USAGE };
75
+ if (digest !== null && !DIGEST_RE.test(digest)) return { error: APPROVE_HOOKS_USAGE };
76
+ if (version !== null && !VERSION_RE.test(version)) return { error: APPROVE_HOOKS_USAGE };
77
+ return { digest, version };
78
+ }