@lamalibre/create-lamaste 4.0.0 → 4.1.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 (61) 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/tickets.md +2 -2
  13. package/vendor/docs/01-concepts/tunneling.md +4 -4
  14. package/vendor/docs/02-guides/agent-setup.md +3 -3
  15. package/vendor/docs/02-guides/certificate-management.md +2 -2
  16. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  17. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  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 +6 -5
  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 +6 -6
  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/onboarding.md +1 -1
  30. package/vendor/docs/04-api-reference/overview.md +111 -105
  31. package/vendor/docs/04-api-reference/registry-and-oidc.md +182 -0
  32. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  33. package/vendor/docs/05-operations/uninstalling.md +1 -1
  34. package/vendor/docs/05-operations/upgrades.md +6 -5
  35. package/vendor/docs/06-reference/config-files.md +103 -45
  36. package/vendor/docs/06-reference/glossary.md +3 -3
  37. package/vendor/docs/06-reference/installer-flags.md +13 -13
  38. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  39. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  40. package/vendor/docs/_index.json +5 -0
  41. package/vendor/docs/gatekeeper-design.md +8 -8
  42. package/vendor/docs/identity-directory-design.md +443 -0
  43. package/vendor/docs/package.json +1 -1
  44. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  45. package/vendor/gatekeeper/package.json +1 -1
  46. package/vendor/server/bin/lamaste-server.js +10 -1
  47. package/vendor/server/package.json +4 -3
  48. package/vendor/server/src/commands/plugins.js +87 -0
  49. package/vendor/server/src/commands/registry-credential.js +142 -0
  50. package/vendor/server/src/panel-api.js +83 -38
  51. package/vendor/serverd/package.json +5 -2
  52. package/vendor/serverd/src/index.js +11 -0
  53. package/vendor/serverd/src/lib/authelia.js +11 -0
  54. package/vendor/serverd/src/lib/plugin-oidc.js +216 -0
  55. package/vendor/serverd/src/lib/plugins.js +26 -2
  56. package/vendor/serverd/src/lib/registry-credential.js +145 -0
  57. package/vendor/serverd/src/routes/management/agents.js +81 -0
  58. package/vendor/serverd/src/routes/management/plugins.js +65 -2
  59. package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
  60. package/vendor/serverd/src/routes/management/users.js +31 -0
  61. package/vendor/serverd/src/routes/management.js +2 -0
@@ -7,6 +7,12 @@
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
+ * The OIDC subcommands go through the running panel: writing a client to
15
+ * Authelia is the panel's job.
10
16
  */
11
17
 
12
18
  import chalk from 'chalk';
@@ -20,6 +26,7 @@ import {
20
26
  } from '@lamalibre/lamaste/server';
21
27
  import { STATE_DIR, PANEL_SERVICE } from '../config.js';
22
28
  import { exec } from '../exec.js';
29
+ import { panelRequest } from '../panel-api.js';
23
30
  import { createLogger } from '../logger.js';
24
31
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
25
32
 
@@ -47,6 +54,10 @@ export async function runPlugins(args, { json }) {
47
54
  return disablePluginCmd(args[1], { json });
48
55
  case 'uninstall':
49
56
  return uninstallPluginCmd(args[1], { json });
57
+ case 'oidc-clients':
58
+ return oidcClientsCmd(args[1], { json });
59
+ case 'rotate-secret':
60
+ return rotateSecretCmd(args[1], args[2], { json });
50
61
  default:
51
62
  printPluginUsage();
52
63
  process.exit(sub ? 1 : 0);
@@ -65,6 +76,9 @@ ${b('Subcommands:')}
65
76
  ${c('enable <name>')} Enable a plugin
66
77
  ${c('disable <name>')} Disable a plugin
67
78
  ${c('uninstall <name>')} Remove a disabled plugin
79
+ ${c('oidc-clients <name>')} OIDC clients the plugin declares on Authelia (no secrets)
80
+ ${c('rotate-secret <name> <clientId>')}
81
+ Replace a confidential client's secret — printed once
68
82
  `);
69
83
  }
70
84
 
@@ -252,6 +266,79 @@ async function uninstallPluginCmd(name, { json }) {
252
266
  }
253
267
  }
254
268
 
269
+ /**
270
+ * @param {string | undefined} name
271
+ * @param {{ json: boolean }} options
272
+ */
273
+ async function oidcClientsCmd(name, { json }) {
274
+ if (!name) {
275
+ const msg = 'Usage: lamaste-server plugins oidc-clients <name>';
276
+ if (json) emitError(msg);
277
+ else console.error(`\n ${msg}\n`);
278
+ process.exit(1);
279
+ }
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)`);
304
+ }
305
+ if (c.confidential) {
306
+ console.log(
307
+ ` secret: created ${c.secretCreatedAt ?? d('(not yet)')}${
308
+ c.secretRotatedAt ? `, rotated ${c.secretRotatedAt}` : ''
309
+ }`,
310
+ );
311
+ }
312
+ }
313
+ console.log('');
314
+ }
315
+
316
+ /**
317
+ * @param {string | undefined} name
318
+ * @param {string | undefined} clientId
319
+ * @param {{ json: boolean }} options
320
+ */
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}`,
338
+ );
339
+ console.log(` ${chalk.bold('New secret (shown once):')} ${result.secret}\n`);
340
+ }
341
+
255
342
  const MUTATING = new Set(['install', 'enable', 'disable', 'uninstall']);
256
343
 
257
344
  /** Set on the child run as lamaste: the root parent restarts the panel. */
@@ -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
+ }
@@ -13,15 +13,90 @@ import { PKI_DIR } from './config.js';
13
13
 
14
14
  const PANEL_URL = 'https://127.0.0.1:9292';
15
15
 
16
+ /**
17
+ * The curl argument vector for one panel request. Pure, exported for tests.
18
+ *
19
+ * The JSON content type and the body travel together: a `Content-Type:
20
+ * application/json` header on a body-less DELETE made Fastify answer 400
21
+ * ("Body cannot be empty when content-type is set"), so `registry-credential
22
+ * clear`, `tunnels delete` and `sites delete` never reached their routes.
23
+ *
24
+ * @param {{ method: string, path: string, body?: Record<string, unknown> | null, p12Path: string, configPath: string }} request
25
+ * @returns {string[]}
26
+ */
27
+ export function buildPanelCurlArgs({ method, path, body = null, p12Path, configPath }) {
28
+ const args = [
29
+ '-K',
30
+ configPath,
31
+ '--cert-type',
32
+ 'P12',
33
+ '--cert',
34
+ p12Path,
35
+ '-k', // Server uses self-signed cert on localhost
36
+ '-s', // Silent
37
+ '-S', // Show errors
38
+ '-X',
39
+ method,
40
+ '-H',
41
+ 'Accept: application/json',
42
+ ];
43
+ if (body !== null && body !== undefined) {
44
+ args.push('-H', 'Content-Type: application/json', '-d', JSON.stringify(body));
45
+ }
46
+ // Append the HTTP status on its own line so an error response is not
47
+ // mistaken for a result — the panel answers errors with a JSON body.
48
+ args.push('-w', '\n%{http_code}', `${PANEL_URL}${path}`);
49
+ return args;
50
+ }
51
+
52
+ /**
53
+ * Split curl's output (`<payload>\n<status>`) and turn a non-2xx status
54
+ * into an error carrying the panel's `error` (and `details`). Pure,
55
+ * exported for tests.
56
+ *
57
+ * @param {string} stdout
58
+ * @param {string} method
59
+ * @param {string} path
60
+ * @returns {Record<string, unknown>}
61
+ */
62
+ export function parsePanelResponse(stdout, method, path) {
63
+ const split = stdout.lastIndexOf('\n');
64
+ const payload = stdout.slice(0, split).trim();
65
+ const status = Number(stdout.slice(split + 1).trim());
66
+ let parsed = {};
67
+ if (payload) {
68
+ try {
69
+ parsed = JSON.parse(payload);
70
+ } catch {
71
+ parsed = { error: payload };
72
+ }
73
+ }
74
+
75
+ // Anything but a 2xx is an error: 4xx/5xx from the panel, a 3xx curl did
76
+ // not follow, or 0 when curl reported no status at all.
77
+ if (!Number.isInteger(status) || status < 200 || status >= 300) {
78
+ const reason = [parsed.error, parsed.details].filter(Boolean).join(': ');
79
+ const err = new Error(
80
+ `${method} ${path} failed (HTTP ${status})${reason ? `: ${reason}` : ''}`,
81
+ );
82
+ err.statusCode = status;
83
+ throw err;
84
+ }
85
+
86
+ return parsed;
87
+ }
88
+
16
89
  /**
17
90
  * Make an authenticated API request to the local panel server.
18
91
  *
19
92
  * Uses curl with the admin client certificate (mTLS) and a temporary
20
- * config file for the P12 password (never in process args).
93
+ * config file for the P12 password (never in process args). Resolves with
94
+ * the JSON body; rejects with an Error (`statusCode`, the panel's `error`
95
+ * in the message) for any non-2xx answer.
21
96
  *
22
97
  * @param {string} method
23
98
  * @param {string} path e.g. '/api/tunnels'
24
- * @param {Record<string, unknown> | null} [body]
99
+ * @param {Record<string, unknown> | null} [body] sent as JSON when given
25
100
  * @returns {Promise<Record<string, unknown>>}
26
101
  */
27
102
  export async function panelRequest(method, path, body = null) {
@@ -42,43 +117,13 @@ export async function panelRequest(method, path, body = null) {
42
117
  try {
43
118
  await writeFile(tmpConfigPath, `--pass "${p12Password}"\n`, { mode: 0o600 });
44
119
 
45
- const curlArgs = [
46
- '-K',
47
- tmpConfigPath,
48
- '--cert-type',
49
- 'P12',
50
- '--cert',
51
- p12Path,
52
- '-k', // Server uses self-signed cert on localhost
53
- '-s', // Silent
54
- '-S', // Show errors
55
- '-X',
56
- method,
57
- '-H',
58
- 'Content-Type: application/json',
59
- ];
60
-
61
- if (body) {
62
- curlArgs.push('-d', JSON.stringify(body));
63
- }
64
-
65
- // Append the HTTP status on its own line so an error response is not
66
- // mistaken for a result — the panel answers errors with a JSON body.
67
- curlArgs.push('-w', '\n%{http_code}', `${PANEL_URL}${path}`);
68
-
69
- const { stdout } = await execa('curl', curlArgs, { timeout: 30000 });
70
-
71
- const split = stdout.lastIndexOf('\n');
72
- const payload = stdout.slice(0, split).trim();
73
- const status = Number(stdout.slice(split + 1).trim());
74
- const parsed = payload ? JSON.parse(payload) : {};
75
-
76
- if (!Number.isInteger(status) || status >= 400) {
77
- const reason = [parsed.error, parsed.details].filter(Boolean).join(': ');
78
- throw new Error(`${method} ${path} failed (HTTP ${status})${reason ? `: ${reason}` : ''}`);
79
- }
120
+ const { stdout } = await execa(
121
+ 'curl',
122
+ buildPanelCurlArgs({ method, path, body, p12Path, configPath: tmpConfigPath }),
123
+ { timeout: 30000 },
124
+ );
80
125
 
81
- return parsed;
126
+ return parsePanelResponse(stdout, method, path);
82
127
  } finally {
83
128
  await unlink(tmpConfigPath).catch(() => {});
84
129
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamalibre/lamaste-serverd",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Lamaste server daemon — REST API and plugin host",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "publishConfig": {
@@ -8,6 +8,9 @@
8
8
  },
9
9
  "type": "module",
10
10
  "main": "src/index.js",
11
+ "files": [
12
+ "src"
13
+ ],
11
14
  "scripts": {
12
15
  "dev": "node --watch src/index.js | pino-pretty",
13
16
  "build": "echo 'No build step for lamaste-serverd'",
@@ -26,7 +29,7 @@
26
29
  "@fastify/rate-limit": "^10.3.0",
27
30
  "@fastify/static": "^9.1.1",
28
31
  "@fastify/websocket": "^11.0.0",
29
- "@lamalibre/lamaste": "^4.0.0",
32
+ "@lamalibre/lamaste": "^4.1.0",
30
33
  "bcryptjs": "^3.0.3",
31
34
  "execa": "^9.6.1",
32
35
  "fastify": "^5.8.4",
@@ -28,6 +28,7 @@ import userAccessSessionMiddleware from './middleware/user-access-session.js';
28
28
  import { getPluginCapabilities } from './lib/plugins.js';
29
29
  import { setPluginCapabilities } from './lib/mtls.js';
30
30
  import { startChiselReconciler } from './lib/chisel-reconcile.js';
31
+ import { syncOidcClients } from './lib/plugin-oidc.js';
31
32
  import {
32
33
  loadTicketScopeCapabilities,
33
34
  checkInstanceLiveness,
@@ -76,6 +77,16 @@ async function start() {
76
77
  // it can.
77
78
  startChiselReconciler(server.log);
78
79
 
80
+ // Bring Authelia's OIDC clients (plugin manifests, the panel's own
81
+ // registry client) and the client-secret store in line with persisted
82
+ // state. Best effort: a failure is logged and the next lifecycle change
83
+ // retries. Only once onboarding has written Authelia's configuration.
84
+ if (config.onboarding.status === 'COMPLETED') {
85
+ syncOidcClients(server.log).catch((err) => {
86
+ server.log.warn({ err: err.message }, 'Startup OIDC client sync failed');
87
+ });
88
+ }
89
+
79
90
  // --- Plugins ---
80
91
  const ipOrigin = `https://${config.ip}:9292`;
81
92
  await server.register(cors, {
@@ -18,6 +18,7 @@ import {
18
18
  reloadAuthelia as reloadAutheliaCore,
19
19
  isAutheliaRunning as isAutheliaRunningCore,
20
20
  updateAutheliaAccessControl,
21
+ updateAutheliaOidcProvider,
21
22
  createUserFromInvitation as createUserFromInvitationCore,
22
23
  base32Encode,
23
24
  base32Decode,
@@ -73,6 +74,16 @@ export function updateAccessControl(sites) {
73
74
  return updateAutheliaAccessControl(sites, execa);
74
75
  }
75
76
 
77
+ /**
78
+ * Make Authelia's OIDC provider serve exactly these clients.
79
+ * @param {string} domain
80
+ * @param {import('@lamalibre/lamaste/server').AutheliaOidcClient[]} clients
81
+ * @param {import('@lamalibre/lamaste/server').OidcProviderOptions} [options]
82
+ */
83
+ export function updateOidcProvider(domain, clients, options = {}) {
84
+ return updateAutheliaOidcProvider(domain, clients, execa, options);
85
+ }
86
+
76
87
  export function createUserFromInvitation(username, email, groups, hashedPassword) {
77
88
  return createUserFromInvitationCore(username, email, groups, hashedPassword, execa);
78
89
  }