@lamalibre/create-lamaste 4.0.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 (66) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +21 -2
  3. package/src/lib/oidc-issuer-key.js +124 -0
  4. package/src/lib/relay-services.js +4 -0
  5. package/src/lib/service-config.js +16 -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/authentication.md +45 -1
  10. package/vendor/docs/01-concepts/certificates.md +3 -3
  11. package/vendor/docs/01-concepts/mtls.md +2 -2
  12. package/vendor/docs/01-concepts/security-model.md +1 -1
  13. package/vendor/docs/01-concepts/tickets.md +2 -2
  14. package/vendor/docs/01-concepts/tunneling.md +4 -4
  15. package/vendor/docs/02-guides/agent-setup.md +3 -3
  16. package/vendor/docs/02-guides/certificate-management.md +2 -2
  17. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  18. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  19. package/vendor/docs/02-guides/installation.md +3 -3
  20. package/vendor/docs/02-guides/onboarding.md +2 -2
  21. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  22. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  23. package/vendor/docs/03-architecture/installer.md +6 -5
  24. package/vendor/docs/03-architecture/management-flow.md +1 -1
  25. package/vendor/docs/03-architecture/overview.md +5 -5
  26. package/vendor/docs/03-architecture/panel-client.md +6 -6
  27. package/vendor/docs/03-architecture/panel-server.md +2 -2
  28. package/vendor/docs/03-architecture/state-management.md +2 -2
  29. package/vendor/docs/03-architecture/system-overview.md +1 -1
  30. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  31. package/vendor/docs/04-api-reference/overview.md +111 -105
  32. package/vendor/docs/04-api-reference/registry-and-oidc.md +207 -0
  33. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  34. package/vendor/docs/05-operations/uninstalling.md +1 -1
  35. package/vendor/docs/05-operations/upgrades.md +6 -5
  36. package/vendor/docs/06-reference/config-files.md +103 -45
  37. package/vendor/docs/06-reference/glossary.md +3 -3
  38. package/vendor/docs/06-reference/installer-flags.md +13 -13
  39. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  40. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  41. package/vendor/docs/_index.json +5 -0
  42. package/vendor/docs/gatekeeper-design.md +8 -8
  43. package/vendor/docs/identity-directory-design.md +443 -0
  44. package/vendor/docs/package.json +1 -1
  45. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  46. package/vendor/gatekeeper/package.json +1 -1
  47. package/vendor/server/bin/lamaste-server.js +10 -1
  48. package/vendor/server/package.json +4 -3
  49. package/vendor/server/src/commands/plugins.js +200 -158
  50. package/vendor/server/src/commands/registry-credential.js +142 -0
  51. package/vendor/server/src/panel-api.js +86 -39
  52. package/vendor/server-ui/dist/assets/{index-B0nWuUyL.js → index-B5Wp8lu-.js} +26 -26
  53. package/vendor/server-ui/dist/index.html +1 -1
  54. package/vendor/serverd/package.json +5 -2
  55. package/vendor/serverd/src/index.js +11 -0
  56. package/vendor/serverd/src/lib/authelia.js +11 -0
  57. package/vendor/serverd/src/lib/migrations/state/0005_plugin_hosting.sql +3 -0
  58. package/vendor/serverd/src/lib/plugin-oidc.js +233 -0
  59. package/vendor/serverd/src/lib/plugins.js +37 -5
  60. package/vendor/serverd/src/lib/registry-credential.js +145 -0
  61. package/vendor/serverd/src/routes/management/agents.js +81 -0
  62. package/vendor/serverd/src/routes/management/plugins.js +65 -2
  63. package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
  64. package/vendor/serverd/src/routes/management/storage.js +10 -2
  65. package/vendor/serverd/src/routes/management/users.js +31 -0
  66. package/vendor/serverd/src/routes/management.js +2 -0
@@ -2,27 +2,33 @@
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
9
9
  * uninstall <name> Remove a plugin (must be disabled first)
10
+ * oidc-clients <name> The OIDC clients a plugin declares on Authelia
11
+ * rotate-secret <name> <clientId>
12
+ * New secret for a confidential OIDC client (shown once)
13
+ *
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.
10
21
  */
11
22
 
12
23
  import chalk from 'chalk';
13
24
  import { execa } from 'execa';
14
- import {
15
- readPlugins,
16
- installPlugin,
17
- enablePlugin,
18
- disablePlugin,
19
- uninstallPlugin,
20
- } from '@lamalibre/lamaste/server';
21
- import { STATE_DIR, PANEL_SERVICE } from '../config.js';
22
- import { exec } from '../exec.js';
23
- import { createLogger } from '../logger.js';
25
+ import { PANEL_SERVICE } from '../config.js';
26
+ import { panelRequest } from '../panel-api.js';
24
27
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
25
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
+
26
32
  /**
27
33
  * @param {string[]} args Subcommand args: ['list'] | ['install', '<pkg>'] | ...
28
34
  * @param {{ json: boolean }} options
@@ -30,12 +36,6 @@ import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
30
36
  export async function runPlugins(args, { json }) {
31
37
  const sub = args[0];
32
38
 
33
- if (MUTATING.has(sub) && process.getuid?.() === 0) {
34
- await runAsLamaste(args, { json });
35
- await restartPanel(json);
36
- return;
37
- }
38
-
39
39
  switch (sub) {
40
40
  case 'list':
41
41
  return listPlugins({ json });
@@ -47,6 +47,10 @@ export async function runPlugins(args, { json }) {
47
47
  return disablePluginCmd(args[1], { json });
48
48
  case 'uninstall':
49
49
  return uninstallPluginCmd(args[1], { json });
50
+ case 'oidc-clients':
51
+ return oidcClientsCmd(args[1], { json });
52
+ case 'rotate-secret':
53
+ return rotateSecretCmd(args[1], args[2], { json });
50
54
  default:
51
55
  printPluginUsage();
52
56
  process.exit(sub ? 1 : 0);
@@ -60,20 +64,25 @@ function printPluginUsage() {
60
64
  ${b('Usage:')} lamaste-server plugins <subcommand>
61
65
 
62
66
  ${b('Subcommands:')}
63
- ${c('list')} List installed plugins with status
67
+ ${c('list')} Installed plugins: status, hosting (server or agent)
64
68
  ${c('install <package>')} Install a plugin (e.g. @lamalibre/herd-server)
65
69
  ${c('enable <name>')} Enable a plugin
66
70
  ${c('disable <name>')} Disable a plugin
67
71
  ${c('uninstall <name>')} Remove a disabled plugin
72
+ ${c('oidc-clients <name>')} OIDC clients the plugin declares on Authelia (no secrets)
73
+ ${c('rotate-secret <name> <clientId>')}
74
+ Replace a confidential client's secret — printed once
68
75
  `);
69
76
  }
70
77
 
71
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
+ *
72
82
  * @param {{ json: boolean }} options
73
83
  */
74
84
  async function listPlugins({ json }) {
75
- const registry = await readPlugins(STATE_DIR);
76
- const plugins = registry.plugins;
85
+ const { plugins = [] } = await panelRequest('GET', '/api/plugins');
77
86
 
78
87
  if (json) {
79
88
  emit({ plugins });
@@ -97,63 +106,95 @@ async function listPlugins({ json }) {
97
106
 
98
107
  for (const p of plugins) {
99
108
  const status = p.status === 'enabled' ? g('enabled') : r('disabled');
100
- 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)}`);
101
111
  }
102
112
  console.log('');
103
113
  }
104
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
+
105
146
  /**
106
147
  * @param {string | undefined} packageName
107
148
  * @param {{ json: boolean }} options
108
149
  */
109
150
  async function installPluginCmd(packageName, { json }) {
110
- if (!packageName) {
111
- const msg = 'Usage: lamaste-server plugins install <package>';
112
- if (json) emitError(msg);
113
- else console.error(`\n ${msg}\n`);
114
- process.exit(1);
115
- }
116
-
117
- const logger = createLogger({ json });
151
+ if (!packageName) fail('Usage: lamaste-server plugins install <package>', json);
118
152
 
119
153
  if (json) {
120
154
  emitStep('install', 'running', `Installing ${packageName}`);
121
- 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.`);
122
156
  } else {
123
157
  process.stderr.write(` Installing ${chalk.cyan(packageName)}...`);
124
158
  }
125
159
 
160
+ let result;
126
161
  try {
127
- const entry = await installPlugin({
128
- packageName,
129
- stateDir: STATE_DIR,
130
- exec,
131
- logger,
132
- });
133
-
134
- if (json) {
135
- emitStep('install', 'complete');
136
- emitComplete({ plugin: entry });
137
- } else {
138
- console.log(` ${chalk.green('ok')}`);
139
- console.log(` Plugin ${chalk.cyan(entry.name)} installed (${chalk.dim('disabled')})`);
140
- console.log(
141
- ` Run ${chalk.cyan(`lamaste-server plugins enable ${entry.name}`)} to activate.`,
142
- );
143
- console.log('');
144
- }
145
-
146
- await restartPanel(json);
162
+ result = await panelRequest(
163
+ 'POST',
164
+ '/api/plugins/install',
165
+ { packageName },
166
+ { timeoutMs: NPM_CALL_TIMEOUT_MS },
167
+ );
147
168
  } catch (err) {
148
- if (json) {
149
- emitStep('install', 'failed', err.message);
150
- emitError(err.message);
151
- } else {
152
- console.log(` ${chalk.red('failed')}`);
153
- console.error(` ${chalk.red(err.message)}`);
154
- }
155
- 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;
156
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';
157
198
  }
158
199
 
159
200
  /**
@@ -161,28 +202,21 @@ async function installPluginCmd(packageName, { json }) {
161
202
  * @param {{ json: boolean }} options
162
203
  */
163
204
  async function enablePluginCmd(name, { json }) {
164
- if (!name) {
165
- const msg = 'Usage: lamaste-server plugins enable <name>';
166
- if (json) emitError(msg);
167
- else console.error(`\n ${msg}\n`);
168
- process.exit(1);
169
- }
170
-
171
- const logger = createLogger({ json });
205
+ if (!name) fail('Usage: lamaste-server plugins enable <name>', json);
172
206
 
207
+ let result;
173
208
  try {
174
- await enablePlugin({ name, stateDir: STATE_DIR, logger });
175
- if (json) {
176
- emitComplete({ name, status: 'enabled' });
177
- } else {
178
- console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.green('enabled')}.`);
179
- }
180
- await restartPanel(json);
209
+ result = await panelRequest('POST', `/api/plugins/${encodeURIComponent(name)}/enable`);
181
210
  } catch (err) {
182
- if (json) emitError(err.message);
183
- else console.error(`\n ${chalk.red(err.message)}\n`);
184
- process.exit(1);
211
+ fail(err.message, json);
185
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);
186
220
  }
187
221
 
188
222
  /**
@@ -190,27 +224,45 @@ async function enablePluginCmd(name, { json }) {
190
224
  * @param {{ json: boolean }} options
191
225
  */
192
226
  async function disablePluginCmd(name, { json }) {
193
- if (!name) {
194
- const msg = 'Usage: lamaste-server plugins disable <name>';
195
- if (json) emitError(msg);
196
- else console.error(`\n ${msg}\n`);
197
- process.exit(1);
227
+ if (!name) fail('Usage: lamaste-server plugins disable <name>', json);
228
+
229
+ try {
230
+ await panelRequest('POST', `/api/plugins/${encodeURIComponent(name)}/disable`);
231
+ } catch (err) {
232
+ fail(err.message, json);
198
233
  }
199
234
 
200
- const logger = createLogger({ json });
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);
238
+ }
239
+
240
+ /**
241
+ * @param {string | undefined} name
242
+ * @param {{ json: boolean }} options
243
+ */
244
+ async function uninstallPluginCmd(name, { json }) {
245
+ if (!name) fail('Usage: lamaste-server plugins uninstall <name>', json);
246
+
247
+ if (json) emitStep('uninstall', 'running', `Uninstalling ${name}`);
248
+ else process.stderr.write(` Uninstalling ${chalk.cyan(name)}...`);
201
249
 
202
250
  try {
203
- await disablePlugin({ name, stateDir: STATE_DIR, logger });
204
- if (json) {
205
- emitComplete({ name, status: 'disabled' });
206
- } else {
207
- console.log(`\n Plugin ${chalk.cyan(name)} ${chalk.red('disabled')}.`);
208
- }
209
- 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
+ });
210
256
  } catch (err) {
211
- if (json) emitError(err.message);
212
- else console.error(`\n ${chalk.red(err.message)}\n`);
213
- 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')}`);
214
266
  }
215
267
  }
216
268
 
@@ -218,92 +270,82 @@ async function disablePluginCmd(name, { json }) {
218
270
  * @param {string | undefined} name
219
271
  * @param {{ json: boolean }} options
220
272
  */
221
- async function uninstallPluginCmd(name, { json }) {
273
+ async function oidcClientsCmd(name, { json }) {
222
274
  if (!name) {
223
- const msg = 'Usage: lamaste-server plugins uninstall <name>';
275
+ const msg = 'Usage: lamaste-server plugins oidc-clients <name>';
224
276
  if (json) emitError(msg);
225
277
  else console.error(`\n ${msg}\n`);
226
278
  process.exit(1);
227
279
  }
228
-
229
- const logger = createLogger({ json });
230
-
231
- if (json) emitStep('uninstall', 'running', `Uninstalling ${name}`);
232
- else process.stderr.write(` Uninstalling ${chalk.cyan(name)}...`);
233
-
234
- try {
235
- await uninstallPlugin({ name, stateDir: STATE_DIR, exec, logger });
236
- if (json) {
237
- emitStep('uninstall', 'complete');
238
- emitComplete({ name, removed: true });
239
- } else {
240
- console.log(` ${chalk.green('ok')}`);
280
+ const result = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}/oidc-clients`);
281
+ if (json) {
282
+ emit(result);
283
+ return;
284
+ }
285
+ const clients = result.clients ?? [];
286
+ if (clients.length === 0) {
287
+ console.log(`\n Plugin ${chalk.cyan(name)} declares no OIDC clients.\n`);
288
+ return;
289
+ }
290
+ const b = chalk.bold;
291
+ const d = chalk.dim;
292
+ console.log('');
293
+ console.log(b(` OIDC clients of ${name}`));
294
+ console.log(d(' ' + '\u2500'.repeat(40)));
295
+ for (const c of clients) {
296
+ const kind = c.confidential ? chalk.yellow('confidential') : chalk.green('public');
297
+ const served = c.enabled ? '' : d(' (plugin disabled — not served)');
298
+ console.log(` ${chalk.cyan(c.id)} ${kind}${served}`);
299
+ console.log(` grants: ${c.grantTypes.join(', ')}`);
300
+ console.log(` scopes: ${c.scopes.join(', ')}`);
301
+ if (c.audience.length > 0) console.log(` audience: ${c.audience.join(', ')}`);
302
+ if (c.accessTokenClaims.length > 0) {
303
+ console.log(` claims: ${c.accessTokenClaims.join(', ')} (in access tokens)`);
241
304
  }
242
- await restartPanel(json);
243
- } catch (err) {
244
- if (json) {
245
- emitStep('uninstall', 'failed', err.message);
246
- emitError(err.message);
247
- } else {
248
- console.log(` ${chalk.red('failed')}`);
249
- console.error(` ${chalk.red(err.message)}`);
305
+ if (c.confidential) {
306
+ console.log(
307
+ ` secret: created ${c.secretCreatedAt ?? d('(not yet)')}${
308
+ c.secretRotatedAt ? `, rotated ${c.secretRotatedAt}` : ''
309
+ }`,
310
+ );
250
311
  }
251
- process.exit(1);
252
312
  }
313
+ console.log('');
253
314
  }
254
315
 
255
- const MUTATING = new Set(['install', 'enable', 'disable', 'uninstall']);
256
-
257
- /** Set on the child run as lamaste: the root parent restarts the panel. */
258
- const CHILD_ENV = 'LAMALIBRE_LAMASTE_CLI_PLUGINS_CHILD';
259
-
260
316
  /**
261
- * Run a plugin subcommand as the lamaste user and wait for it.
262
- *
263
- * Plugin state (the npm tree, plugins.json, plugin data) lives in the
264
- * lamaste-owned state directory. Running npm or writing files there as root
265
- * would let whatever the panel left in that directory — a `.npmrc`, a
266
- * symlink — steer root. As lamaste, it can only reach what the panel could
267
- * already reach. Stays usable while the panel is down (disabling a plugin
268
- * that keeps it from starting).
269
- *
270
- * @param {string[]} args
317
+ * @param {string | undefined} name
318
+ * @param {string | undefined} clientId
271
319
  * @param {{ json: boolean }} options
272
320
  */
273
- async function runAsLamaste(args, { json }) {
274
- const uid = Number.parseInt((await execa('id', ['-u', 'lamaste'])).stdout.trim(), 10);
275
- const gid = Number.parseInt((await execa('id', ['-g', 'lamaste'])).stdout.trim(), 10);
276
- const { exitCode } = await execa(
277
- process.execPath,
278
- [process.argv[1], 'plugins', ...args, ...(json ? ['--json'] : [])],
279
- {
280
- uid,
281
- gid,
282
- cwd: STATE_DIR,
283
- stdio: 'inherit',
284
- reject: false,
285
- env: {
286
- ...process.env,
287
- [CHILD_ENV]: '1',
288
- HOME: STATE_DIR,
289
- USER: 'lamaste',
290
- LOGNAME: 'lamaste',
291
- npm_config_cache: `${STATE_DIR}/.npm`,
292
- },
293
- extendEnv: false,
294
- },
321
+ async function rotateSecretCmd(name, clientId, { json }) {
322
+ if (!name || !clientId) {
323
+ const msg = 'Usage: lamaste-server plugins rotate-secret <name> <clientId>';
324
+ if (json) emitError(msg);
325
+ else console.error(`\n ${msg}\n`);
326
+ process.exit(1);
327
+ }
328
+ const result = await panelRequest(
329
+ 'POST',
330
+ `/api/plugins/${encodeURIComponent(name)}/oidc-clients/${encodeURIComponent(clientId)}/rotate-secret`,
331
+ );
332
+ if (json) {
333
+ emit(result);
334
+ return;
335
+ }
336
+ console.log(
337
+ `\n ${chalk.green('✓')} Secret of ${chalk.cyan(clientId)} rotated at ${result.rotatedAt}`,
295
338
  );
296
- if (exitCode !== 0) process.exit(exitCode ?? 1);
339
+ console.log(` ${chalk.bold('New secret (shown once):')} ${result.secret}\n`);
297
340
  }
298
341
 
299
342
  /**
300
- * Restart the panel service after plugin changes.
301
- * 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.
302
346
  * @param {boolean} json
303
347
  */
304
348
  async function restartPanel(json) {
305
- // The child run as lamaste cannot; its root parent does.
306
- if (process.env[CHILD_ENV] === '1') return;
307
349
  try {
308
350
  await execa('systemctl', ['restart', PANEL_SERVICE]);
309
351
  if (!json) {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * lamaste-server registry-credential — the panel's service credential on the
3
+ * Lamalibre npm registry (Feria), used to broker short-lived install tokens
4
+ * for agents.
5
+ *
6
+ * registry-credential show
7
+ * registry-credential set --url <registry> --oidc
8
+ * The panel authenticates as its own `lamaste-server` OIDC client on
9
+ * the relay's Authelia (client-credentials grant, Feria login). Nothing
10
+ * to paste; Feria's auth.json must trust the relay as a rules issuer.
11
+ * registry-credential set --url <registry> [--token-stdin]
12
+ * A long-lived Feria token, read from LAMASTE_REGISTRY_TOKEN or from
13
+ * stdin with --token-stdin. Never accepted as a command-line argument.
14
+ * registry-credential clear
15
+ */
16
+
17
+ import chalk from 'chalk';
18
+ import { panelRequest } from '../panel-api.js';
19
+ import { emit } from '../ndjson.js';
20
+
21
+ /**
22
+ * @param {string[]} args
23
+ * @param {{ json: boolean }} options
24
+ */
25
+ export async function runRegistryCredential(args, { json }) {
26
+ const sub = args[0];
27
+ switch (sub) {
28
+ case 'show':
29
+ return show({ json });
30
+ case 'set':
31
+ return set(args.slice(1), { json });
32
+ case 'clear':
33
+ return clear({ json });
34
+ default:
35
+ process.stderr.write(
36
+ `Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc | --token-stdin) | clear>\n`,
37
+ );
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ async function show({ json }) {
43
+ const result = await panelRequest('GET', '/api/registry-credential');
44
+ if (json) {
45
+ emit(result);
46
+ return;
47
+ }
48
+ if (!result.configured) {
49
+ console.log(chalk.dim('No registry credential configured.'));
50
+ return;
51
+ }
52
+ console.log(`${chalk.bold('Registry:')} ${result.registryUrl}`);
53
+ console.log(`${chalk.bold('Scope:')} ${result.scope}`);
54
+ console.log(
55
+ `${chalk.bold('Auth:')} ${
56
+ result.auth === 'oidc'
57
+ ? `Authelia OIDC client ${result.clientId} (client credentials)`
58
+ : 'long-lived registry token'
59
+ }`,
60
+ );
61
+ console.log(`${chalk.bold('Owner:')} ${result.owner ?? chalk.dim('(unknown)')}`);
62
+ console.log(`${chalk.bold('Configured:')} ${result.configuredAt}`);
63
+ }
64
+
65
+ async function set(args, { json }) {
66
+ let url;
67
+ let tokenFromStdin = false;
68
+ let oidc = false;
69
+ for (let i = 0; i < args.length; i += 1) {
70
+ const arg = args[i];
71
+ if (arg === '--url') {
72
+ url = args[++i];
73
+ } else if (arg.startsWith('--url=')) {
74
+ url = arg.slice('--url='.length);
75
+ } else if (arg === '--token-stdin') {
76
+ tokenFromStdin = true;
77
+ } else if (arg === '--oidc') {
78
+ oidc = true;
79
+ } else if (arg === '--token' || arg.startsWith('--token=')) {
80
+ throw new Error(
81
+ 'The token is never accepted on the command line — set LAMASTE_REGISTRY_TOKEN or use --token-stdin',
82
+ );
83
+ } else {
84
+ throw new Error(`Unknown argument: ${arg}`);
85
+ }
86
+ }
87
+ if (!url) throw new Error('registry-credential set: --url <registry> is required');
88
+ if (oidc && tokenFromStdin) {
89
+ throw new Error('registry-credential set: choose --oidc or --token-stdin, not both');
90
+ }
91
+
92
+ let auth;
93
+ if (oidc) {
94
+ auth = { kind: 'oidc' };
95
+ } else {
96
+ let token = tokenFromStdin ? (await readStdin()).trim() : process.env.LAMASTE_REGISTRY_TOKEN;
97
+ if (!token) {
98
+ throw new Error(
99
+ 'No token: use --oidc, set LAMASTE_REGISTRY_TOKEN, or pipe a token in with --token-stdin (e.g. from `feria-server token bootstrap`)',
100
+ );
101
+ }
102
+ token = token.trim();
103
+ auth = { kind: 'token', token };
104
+ }
105
+
106
+ const result = await panelRequest('PUT', '/api/registry-credential', {
107
+ registryUrl: url,
108
+ auth,
109
+ });
110
+ if (result.error) throw new Error(result.error);
111
+ if (json) {
112
+ emit(result);
113
+ return;
114
+ }
115
+ console.log(
116
+ `${chalk.green('✓')} Registry credential configured for ${result.registryUrl} as ${result.owner ?? '(unknown)'}` +
117
+ (result.auth === 'oidc' ? ` (Authelia client ${result.clientId})` : ''),
118
+ );
119
+ }
120
+
121
+ async function clear({ json }) {
122
+ const result = await panelRequest('DELETE', '/api/registry-credential');
123
+ if (json) {
124
+ emit(result);
125
+ return;
126
+ }
127
+ console.log(
128
+ result.removed
129
+ ? `${chalk.green('✓')} Registry credential removed`
130
+ : chalk.dim('Nothing to remove.'),
131
+ );
132
+ }
133
+
134
+ function readStdin() {
135
+ return new Promise((resolve, reject) => {
136
+ let data = '';
137
+ process.stdin.setEncoding('utf-8');
138
+ process.stdin.on('data', (chunk) => (data += chunk));
139
+ process.stdin.on('end', () => resolve(data));
140
+ process.stdin.on('error', reject);
141
+ });
142
+ }