@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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +21 -2
- package/src/lib/oidc-issuer-key.js +124 -0
- package/src/lib/relay-services.js +4 -0
- package/src/lib/service-config.js +16 -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 +45 -1
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +1 -1
- 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 +3 -3
- 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/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +6 -5
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- 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 +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +111 -105
- package/vendor/docs/04-api-reference/registry-and-oidc.md +207 -0
- 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 +6 -5
- package/vendor/docs/06-reference/config-files.md +103 -45
- 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 +5 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +443 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/package.json +1 -1
- package/vendor/server/bin/lamaste-server.js +10 -1
- package/vendor/server/package.json +4 -3
- package/vendor/server/src/commands/plugins.js +200 -158
- package/vendor/server/src/commands/registry-credential.js +142 -0
- package/vendor/server/src/panel-api.js +86 -39
- package/vendor/server-ui/dist/assets/{index-B0nWuUyL.js → index-B5Wp8lu-.js} +26 -26
- package/vendor/server-ui/dist/index.html +1 -1
- package/vendor/serverd/package.json +5 -2
- package/vendor/serverd/src/index.js +11 -0
- package/vendor/serverd/src/lib/authelia.js +11 -0
- package/vendor/serverd/src/lib/migrations/state/0005_plugin_hosting.sql +3 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +233 -0
- package/vendor/serverd/src/lib/plugins.js +37 -5
- package/vendor/serverd/src/lib/registry-credential.js +145 -0
- package/vendor/serverd/src/routes/management/agents.js +81 -0
- package/vendor/serverd/src/routes/management/plugins.js +65 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
- package/vendor/serverd/src/routes/management/storage.js +10 -2
- package/vendor/serverd/src/routes/management/users.js +31 -0
- 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
|
-
|
|
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')}
|
|
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
|
|
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
|
-
|
|
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', `
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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)
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
273
|
+
async function oidcClientsCmd(name, { json }) {
|
|
222
274
|
if (!name) {
|
|
223
|
-
const msg = 'Usage: lamaste-server plugins
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
*
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
339
|
+
console.log(` ${chalk.bold('New secret (shown once):')} ${result.secret}\n`);
|
|
297
340
|
}
|
|
298
341
|
|
|
299
342
|
/**
|
|
300
|
-
* Restart the panel
|
|
301
|
-
*
|
|
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
|
+
}
|