@lamalibre/create-lamaste 4.1.0 → 4.2.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 (49) hide show
  1. package/package.json +2 -2
  2. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  3. package/vendor/docs/00-introduction/quickstart.md +5 -5
  4. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  5. package/vendor/docs/01-concepts/certificates.md +3 -3
  6. package/vendor/docs/01-concepts/mtls.md +2 -2
  7. package/vendor/docs/01-concepts/security-model.md +1 -1
  8. package/vendor/docs/01-concepts/tickets.md +2 -2
  9. package/vendor/docs/01-concepts/tunneling.md +4 -4
  10. package/vendor/docs/02-guides/agent-setup.md +3 -3
  11. package/vendor/docs/02-guides/certificate-management.md +2 -2
  12. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  13. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  14. package/vendor/docs/02-guides/installation.md +3 -3
  15. package/vendor/docs/02-guides/onboarding.md +2 -2
  16. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  17. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  18. package/vendor/docs/03-architecture/installer.md +4 -4
  19. package/vendor/docs/03-architecture/management-flow.md +1 -1
  20. package/vendor/docs/03-architecture/overview.md +5 -5
  21. package/vendor/docs/03-architecture/panel-client.md +6 -6
  22. package/vendor/docs/03-architecture/panel-server.md +2 -2
  23. package/vendor/docs/03-architecture/state-management.md +2 -2
  24. package/vendor/docs/03-architecture/system-overview.md +1 -1
  25. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  26. package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -1
  27. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  28. package/vendor/docs/05-operations/uninstalling.md +1 -1
  29. package/vendor/docs/05-operations/upgrades.md +5 -5
  30. package/vendor/docs/06-reference/config-files.md +1 -1
  31. package/vendor/docs/06-reference/glossary.md +3 -3
  32. package/vendor/docs/06-reference/installer-flags.md +13 -13
  33. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  34. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  35. package/vendor/docs/gatekeeper-design.md +8 -8
  36. package/vendor/docs/identity-directory-design.md +2 -2
  37. package/vendor/docs/package.json +1 -1
  38. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  39. package/vendor/gatekeeper/package.json +1 -1
  40. package/vendor/server/package.json +2 -2
  41. package/vendor/server/src/commands/plugins.js +123 -168
  42. package/vendor/server/src/panel-api.js +4 -2
  43. package/vendor/server-ui/dist/assets/{index-B0nWuUyL.js → index-B5Wp8lu-.js} +26 -26
  44. package/vendor/server-ui/dist/index.html +1 -1
  45. package/vendor/serverd/package.json +2 -2
  46. package/vendor/serverd/src/lib/migrations/state/0005_plugin_hosting.sql +3 -0
  47. package/vendor/serverd/src/lib/plugin-oidc.js +33 -16
  48. package/vendor/serverd/src/lib/plugins.js +11 -3
  49. package/vendor/serverd/src/routes/management/storage.js +10 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/lamaste-gatekeeper",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Authorization layer for Lamaste — groups, grants, and tunnel access control",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/lamaste-server",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Server operations CLI for Lamaste",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -17,7 +17,7 @@
17
17
  "test": "node --test test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
- "@lamalibre/lamaste": "^4.1.0",
20
+ "@lamalibre/lamaste": "^4.2.0",
21
21
  "chalk": "^5.3.0",
22
22
  "execa": "^9.0.0"
23
23
  },
@@ -2,7 +2,7 @@
2
2
  * lamaste-server plugins — Manage server-side plugins.
3
3
  *
4
4
  * Subcommands:
5
- * list List installed plugins with status
5
+ * list List installed plugins with status and hosting
6
6
  * install <package> Install a plugin by npm package name
7
7
  * enable <name> Enable a plugin
8
8
  * disable <name> Disable a plugin
@@ -11,25 +11,24 @@
11
11
  * rotate-secret <name> <clientId>
12
12
  * New secret for a confidential OIDC client (shown once)
13
13
  *
14
- * The OIDC subcommands go through the running panel: writing a client to
15
- * Authelia is the panel's job.
14
+ * Every subcommand goes through the running panel (`/api/plugins/*`, admin
15
+ * certificate): the panel owns the plugin registry (SQLite `state.db`), runs
16
+ * npm as the `lamaste` account, and writes the plugins' OIDC clients to
17
+ * Authelia. The CLI keeps no plugin state of its own, so a plugin it installs
18
+ * is the plugin the panel serves. The panel must be running — a plugin that
19
+ * fails to load is logged and skipped by the plugin host, so the panel stays
20
+ * reachable to disable it.
16
21
  */
17
22
 
18
23
  import chalk from 'chalk';
19
24
  import { execa } from 'execa';
20
- import {
21
- readPlugins,
22
- installPlugin,
23
- enablePlugin,
24
- disablePlugin,
25
- uninstallPlugin,
26
- } from '@lamalibre/lamaste/server';
27
- import { STATE_DIR, PANEL_SERVICE } from '../config.js';
28
- import { exec } from '../exec.js';
25
+ import { PANEL_SERVICE } from '../config.js';
29
26
  import { panelRequest } from '../panel-api.js';
30
- import { createLogger } from '../logger.js';
31
27
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
32
28
 
29
+ /** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
30
+ const NPM_CALL_TIMEOUT_MS = 180_000;
31
+
33
32
  /**
34
33
  * @param {string[]} args Subcommand args: ['list'] | ['install', '<pkg>'] | ...
35
34
  * @param {{ json: boolean }} options
@@ -37,12 +36,6 @@ import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
37
36
  export async function runPlugins(args, { json }) {
38
37
  const sub = args[0];
39
38
 
40
- if (MUTATING.has(sub) && process.getuid?.() === 0) {
41
- await runAsLamaste(args, { json });
42
- await restartPanel(json);
43
- return;
44
- }
45
-
46
39
  switch (sub) {
47
40
  case 'list':
48
41
  return listPlugins({ json });
@@ -71,7 +64,7 @@ function printPluginUsage() {
71
64
  ${b('Usage:')} lamaste-server plugins <subcommand>
72
65
 
73
66
  ${b('Subcommands:')}
74
- ${c('list')} List installed plugins with status
67
+ ${c('list')} Installed plugins: status, hosting (server or agent)
75
68
  ${c('install <package>')} Install a plugin (e.g. @lamalibre/herd-server)
76
69
  ${c('enable <name>')} Enable a plugin
77
70
  ${c('disable <name>')} Disable a plugin
@@ -83,11 +76,13 @@ ${b('Subcommands:')}
83
76
  }
84
77
 
85
78
  /**
79
+ * The plugins the running panel serves, from the panel itself: its registry
80
+ * (SQLite `state.db`) is the one the plugin host and the OIDC sync read.
81
+ *
86
82
  * @param {{ json: boolean }} options
87
83
  */
88
84
  async function listPlugins({ json }) {
89
- const registry = await readPlugins(STATE_DIR);
90
- const plugins = registry.plugins;
85
+ const { plugins = [] } = await panelRequest('GET', '/api/plugins');
91
86
 
92
87
  if (json) {
93
88
  emit({ plugins });
@@ -111,63 +106,95 @@ async function listPlugins({ json }) {
111
106
 
112
107
  for (const p of plugins) {
113
108
  const status = p.status === 'enabled' ? g('enabled') : r('disabled');
114
- console.log(` ${c(p.name)} ${d(p.version)} ${status} ${d(p.packageName)}`);
109
+ const hosting = p.hosting === 'agent' ? chalk.yellow('agent-hosted') : d('server-hosted');
110
+ console.log(` ${c(p.name)} ${d(p.version)} ${status} ${hosting} ${d(p.packageName)}`);
115
111
  }
116
112
  console.log('');
117
113
  }
118
114
 
115
+ /**
116
+ * Print the secrets of confidential OIDC clients an install or enable created.
117
+ * They are shown once — the panel stores only their digests.
118
+ *
119
+ * @param {Array<{ id: string, secret: string }> | undefined} clients
120
+ */
121
+ function printCreatedSecrets(clients) {
122
+ if (!clients || clients.length === 0) return;
123
+ console.log(` ${chalk.bold('Confidential OIDC client secrets (shown once):')}`);
124
+ for (const c of clients) console.log(` ${chalk.cyan(c.id)} ${c.secret}`);
125
+ console.log('');
126
+ }
127
+
128
+ /**
129
+ * Exit with a usage or panel error, as text or NDJSON.
130
+ *
131
+ * @param {string} message
132
+ * @param {boolean} json
133
+ * @param {string} [step] NDJSON step that failed
134
+ * @returns {never}
135
+ */
136
+ function fail(message, json, step) {
137
+ if (json) {
138
+ if (step) emitStep(step, 'failed', message);
139
+ emitError(message);
140
+ } else {
141
+ console.error(`\n ${chalk.red(message)}\n`);
142
+ }
143
+ process.exit(1);
144
+ }
145
+
119
146
  /**
120
147
  * @param {string | undefined} packageName
121
148
  * @param {{ json: boolean }} options
122
149
  */
123
150
  async function installPluginCmd(packageName, { json }) {
124
- if (!packageName) {
125
- const msg = 'Usage: lamaste-server plugins install <package>';
126
- if (json) emitError(msg);
127
- else console.error(`\n ${msg}\n`);
128
- process.exit(1);
129
- }
130
-
131
- const logger = createLogger({ json });
151
+ if (!packageName) fail('Usage: lamaste-server plugins install <package>', json);
132
152
 
133
153
  if (json) {
134
154
  emitStep('install', 'running', `Installing ${packageName}`);
135
- emitLog('info', `Fetching ${packageName} from the npm registry — this may take a moment.`);
155
+ emitLog('info', `The panel is fetching ${packageName} from the npm registry.`);
136
156
  } else {
137
157
  process.stderr.write(` Installing ${chalk.cyan(packageName)}...`);
138
158
  }
139
159
 
160
+ let result;
140
161
  try {
141
- const entry = await installPlugin({
142
- packageName,
143
- stateDir: STATE_DIR,
144
- exec,
145
- logger,
146
- });
147
-
148
- if (json) {
149
- emitStep('install', 'complete');
150
- emitComplete({ plugin: entry });
151
- } else {
152
- console.log(` ${chalk.green('ok')}`);
153
- console.log(` Plugin ${chalk.cyan(entry.name)} installed (${chalk.dim('disabled')})`);
154
- console.log(
155
- ` Run ${chalk.cyan(`lamaste-server plugins enable ${entry.name}`)} to activate.`,
156
- );
157
- console.log('');
158
- }
159
-
160
- await restartPanel(json);
162
+ result = await panelRequest(
163
+ 'POST',
164
+ '/api/plugins/install',
165
+ { packageName },
166
+ { timeoutMs: NPM_CALL_TIMEOUT_MS },
167
+ );
161
168
  } catch (err) {
162
- if (json) {
163
- emitStep('install', 'failed', err.message);
164
- emitError(err.message);
165
- } else {
166
- console.log(` ${chalk.red('failed')}`);
167
- console.error(` ${chalk.red(err.message)}`);
168
- }
169
- process.exit(1);
169
+ if (!json) console.log(` ${chalk.red('failed')}`);
170
+ fail(err.message, json, 'install');
171
+ }
172
+
173
+ const { plugin, oidcClients } = result;
174
+ if (json) {
175
+ emitStep('install', 'complete');
176
+ emitComplete({ plugin, oidcClients });
177
+ return;
170
178
  }
179
+ const hosting = plugin.hosting === 'agent' ? ', agent-hosted' : '';
180
+ console.log(` ${chalk.green('ok')}`);
181
+ console.log(` Plugin ${chalk.cyan(plugin.name)} installed (${chalk.dim(`disabled${hosting}`)})`);
182
+ console.log(` Run ${chalk.cyan(`lamaste-server plugins enable ${plugin.name}`)} to activate.`);
183
+ console.log('');
184
+ printCreatedSecrets(oidcClients);
185
+ }
186
+
187
+ /**
188
+ * The panel mounts a server-hosted plugin's routes at startup, so enabling or
189
+ * disabling one takes effect after a restart; an agent-hosted plugin has
190
+ * nothing mounted and its OIDC clients are already applied.
191
+ *
192
+ * @param {string} name
193
+ * @returns {Promise<boolean>}
194
+ */
195
+ async function needsRestart(name) {
196
+ const { plugin } = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}`);
197
+ return plugin.hosting !== 'agent';
171
198
  }
172
199
 
173
200
  /**
@@ -175,28 +202,21 @@ async function installPluginCmd(packageName, { json }) {
175
202
  * @param {{ json: boolean }} options
176
203
  */
177
204
  async function enablePluginCmd(name, { json }) {
178
- if (!name) {
179
- const msg = 'Usage: lamaste-server plugins enable <name>';
180
- if (json) emitError(msg);
181
- else console.error(`\n ${msg}\n`);
182
- process.exit(1);
183
- }
184
-
185
- const logger = createLogger({ json });
205
+ if (!name) fail('Usage: lamaste-server plugins enable <name>', json);
186
206
 
207
+ let result;
187
208
  try {
188
- await enablePlugin({ name, stateDir: STATE_DIR, logger });
189
- if (json) {
190
- emitComplete({ name, status: 'enabled' });
191
- } else {
192
- console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.green('enabled')}.`);
193
- }
194
- await restartPanel(json);
209
+ result = await panelRequest('POST', `/api/plugins/${encodeURIComponent(name)}/enable`);
195
210
  } catch (err) {
196
- if (json) emitError(err.message);
197
- else console.error(`\n ${chalk.red(err.message)}\n`);
198
- process.exit(1);
211
+ fail(err.message, json);
199
212
  }
213
+
214
+ if (json) emitComplete({ name, status: 'enabled', oidcClients: result.oidcClients });
215
+ else {
216
+ console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.green('enabled')}.`);
217
+ printCreatedSecrets(result.oidcClients);
218
+ }
219
+ if (await needsRestart(name)) await restartPanel(json);
200
220
  }
201
221
 
202
222
  /**
@@ -204,28 +224,17 @@ async function enablePluginCmd(name, { json }) {
204
224
  * @param {{ json: boolean }} options
205
225
  */
206
226
  async function disablePluginCmd(name, { json }) {
207
- if (!name) {
208
- const msg = 'Usage: lamaste-server plugins disable <name>';
209
- if (json) emitError(msg);
210
- else console.error(`\n ${msg}\n`);
211
- process.exit(1);
212
- }
213
-
214
- const logger = createLogger({ json });
227
+ if (!name) fail('Usage: lamaste-server plugins disable <name>', json);
215
228
 
216
229
  try {
217
- await disablePlugin({ name, stateDir: STATE_DIR, logger });
218
- if (json) {
219
- emitComplete({ name, status: 'disabled' });
220
- } else {
221
- console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.red('disabled')}.`);
222
- }
223
- await restartPanel(json);
230
+ await panelRequest('POST', `/api/plugins/${encodeURIComponent(name)}/disable`);
224
231
  } catch (err) {
225
- if (json) emitError(err.message);
226
- else console.error(`\n ${chalk.red(err.message)}\n`);
227
- process.exit(1);
232
+ fail(err.message, json);
228
233
  }
234
+
235
+ if (json) emitComplete({ name, status: 'disabled' });
236
+ else console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.red('disabled')}.`);
237
+ if (await needsRestart(name)) await restartPanel(json);
229
238
  }
230
239
 
231
240
  /**
@@ -233,36 +242,27 @@ async function disablePluginCmd(name, { json }) {
233
242
  * @param {{ json: boolean }} options
234
243
  */
235
244
  async function uninstallPluginCmd(name, { json }) {
236
- if (!name) {
237
- const msg = 'Usage: lamaste-server plugins uninstall <name>';
238
- if (json) emitError(msg);
239
- else console.error(`\n ${msg}\n`);
240
- process.exit(1);
241
- }
242
-
243
- const logger = createLogger({ json });
245
+ if (!name) fail('Usage: lamaste-server plugins uninstall <name>', json);
244
246
 
245
247
  if (json) emitStep('uninstall', 'running', `Uninstalling ${name}`);
246
248
  else process.stderr.write(` Uninstalling ${chalk.cyan(name)}...`);
247
249
 
248
250
  try {
249
- await uninstallPlugin({ name, stateDir: STATE_DIR, exec, logger });
250
- if (json) {
251
- emitStep('uninstall', 'complete');
252
- emitComplete({ name, removed: true });
253
- } else {
254
- console.log(` ${chalk.green('ok')}`);
255
- }
256
- await restartPanel(json);
251
+ // The panel refuses an enabled plugin (409): nothing of it is mounted
252
+ // once it is disabled, so no restart follows.
253
+ await panelRequest('DELETE', `/api/plugins/${encodeURIComponent(name)}`, null, {
254
+ timeoutMs: NPM_CALL_TIMEOUT_MS,
255
+ });
257
256
  } catch (err) {
258
- if (json) {
259
- emitStep('uninstall', 'failed', err.message);
260
- emitError(err.message);
261
- } else {
262
- console.log(` ${chalk.red('failed')}`);
263
- console.error(` ${chalk.red(err.message)}`);
264
- }
265
- process.exit(1);
257
+ if (!json) console.log(` ${chalk.red('failed')}`);
258
+ fail(err.message, json, 'uninstall');
259
+ }
260
+
261
+ if (json) {
262
+ emitStep('uninstall', 'complete');
263
+ emitComplete({ name, removed: true });
264
+ } else {
265
+ console.log(` ${chalk.green('ok')}`);
266
266
  }
267
267
  }
268
268
 
@@ -339,58 +339,13 @@ async function rotateSecretCmd(name, clientId, { json }) {
339
339
  console.log(` ${chalk.bold('New secret (shown once):')} ${result.secret}\n`);
340
340
  }
341
341
 
342
- const MUTATING = new Set(['install', 'enable', 'disable', 'uninstall']);
343
-
344
- /** Set on the child run as lamaste: the root parent restarts the panel. */
345
- const CHILD_ENV = 'LAMALIBRE_LAMASTE_CLI_PLUGINS_CHILD';
346
-
347
- /**
348
- * Run a plugin subcommand as the lamaste user and wait for it.
349
- *
350
- * Plugin state (the npm tree, plugins.json, plugin data) lives in the
351
- * lamaste-owned state directory. Running npm or writing files there as root
352
- * would let whatever the panel left in that directory — a `.npmrc`, a
353
- * symlink — steer root. As lamaste, it can only reach what the panel could
354
- * already reach. Stays usable while the panel is down (disabling a plugin
355
- * that keeps it from starting).
356
- *
357
- * @param {string[]} args
358
- * @param {{ json: boolean }} options
359
- */
360
- async function runAsLamaste(args, { json }) {
361
- const uid = Number.parseInt((await execa('id', ['-u', 'lamaste'])).stdout.trim(), 10);
362
- const gid = Number.parseInt((await execa('id', ['-g', 'lamaste'])).stdout.trim(), 10);
363
- const { exitCode } = await execa(
364
- process.execPath,
365
- [process.argv[1], 'plugins', ...args, ...(json ? ['--json'] : [])],
366
- {
367
- uid,
368
- gid,
369
- cwd: STATE_DIR,
370
- stdio: 'inherit',
371
- reject: false,
372
- env: {
373
- ...process.env,
374
- [CHILD_ENV]: '1',
375
- HOME: STATE_DIR,
376
- USER: 'lamaste',
377
- LOGNAME: 'lamaste',
378
- npm_config_cache: `${STATE_DIR}/.npm`,
379
- },
380
- extendEnv: false,
381
- },
382
- );
383
- if (exitCode !== 0) process.exit(exitCode ?? 1);
384
- }
385
-
386
342
  /**
387
- * Restart the panel service after plugin changes.
388
- * Plugin enable/disable/install/uninstall requires a panel restart to take effect.
343
+ * Restart the panel so it mounts (or unmounts) a server-hosted plugin's
344
+ * routes. Needs root, like every `lamaste-server` command that reads the
345
+ * admin certificate.
389
346
  * @param {boolean} json
390
347
  */
391
348
  async function restartPanel(json) {
392
- // The child run as lamaste cannot; its root parent does.
393
- if (process.env[CHILD_ENV] === '1') return;
394
349
  try {
395
350
  await execa('systemctl', ['restart', PANEL_SERVICE]);
396
351
  if (!json) {
@@ -97,9 +97,11 @@ export function parsePanelResponse(stdout, method, path) {
97
97
  * @param {string} method
98
98
  * @param {string} path e.g. '/api/tunnels'
99
99
  * @param {Record<string, unknown> | null} [body] sent as JSON when given
100
+ * @param {{ timeoutMs?: number }} [options] `timeoutMs` for calls the panel
101
+ * needs longer to answer (a plugin install runs npm); default 30 s
100
102
  * @returns {Promise<Record<string, unknown>>}
101
103
  */
102
- export async function panelRequest(method, path, body = null) {
104
+ export async function panelRequest(method, path, body = null, { timeoutMs = 30000 } = {}) {
103
105
  const p12Path = `${PKI_DIR}/client.p12`;
104
106
 
105
107
  // Read p12 password from the stored file
@@ -120,7 +122,7 @@ export async function panelRequest(method, path, body = null) {
120
122
  const { stdout } = await execa(
121
123
  'curl',
122
124
  buildPanelCurlArgs({ method, path, body, p12Path, configPath: tmpConfigPath }),
123
- { timeout: 30000 },
125
+ { timeout: timeoutMs },
124
126
  );
125
127
 
126
128
  return parsePanelResponse(stdout, method, path);