@lamalibre/create-lamaste 4.2.0 → 4.3.1

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 (128) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +13 -8
  3. package/scripts/lamaste-priv +205 -2
  4. package/src/index.js +8 -4
  5. package/src/lib/node-version.js +114 -0
  6. package/src/lib/panel-pin.js +41 -0
  7. package/src/lib/relay-services.js +15 -1
  8. package/src/lib/service-config.js +113 -0
  9. package/src/tasks/gatekeeper.js +8 -3
  10. package/src/tasks/node.js +34 -75
  11. package/src/tasks/redeploy.js +36 -0
  12. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  13. package/vendor/docs/00-introduction/quickstart.md +5 -5
  14. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  15. package/vendor/docs/01-concepts/authentication.md +6 -3
  16. package/vendor/docs/01-concepts/authorization.md +29 -17
  17. package/vendor/docs/01-concepts/certificates.md +3 -3
  18. package/vendor/docs/01-concepts/mtls.md +2 -2
  19. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
  20. package/vendor/docs/01-concepts/security-model.md +110 -35
  21. package/vendor/docs/01-concepts/tickets.md +2 -2
  22. package/vendor/docs/01-concepts/tunneling.md +4 -4
  23. package/vendor/docs/02-guides/agent-setup.md +34 -16
  24. package/vendor/docs/02-guides/certificate-management.md +2 -2
  25. package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
  26. package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
  27. package/vendor/docs/02-guides/installation.md +3 -3
  28. package/vendor/docs/02-guides/onboarding.md +2 -2
  29. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  30. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  31. package/vendor/docs/03-architecture/installer.md +4 -4
  32. package/vendor/docs/03-architecture/management-flow.md +1 -1
  33. package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
  34. package/vendor/docs/03-architecture/overview.md +8 -8
  35. package/vendor/docs/03-architecture/panel-client.md +6 -6
  36. package/vendor/docs/03-architecture/panel-server.md +2 -2
  37. package/vendor/docs/03-architecture/state-management.md +2 -2
  38. package/vendor/docs/03-architecture/system-overview.md +1 -1
  39. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
  40. package/vendor/docs/04-api-reference/app-host.md +360 -0
  41. package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
  42. package/vendor/docs/04-api-reference/certificates.md +95 -38
  43. package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
  44. package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
  45. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  46. package/vendor/docs/04-api-reference/overview.md +137 -111
  47. package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
  48. package/vendor/docs/04-api-reference/tunnels.md +15 -13
  49. package/vendor/docs/05-operations/backup-and-restore.md +3 -1
  50. package/vendor/docs/05-operations/uninstalling.md +1 -1
  51. package/vendor/docs/05-operations/upgrades.md +25 -20
  52. package/vendor/docs/06-reference/config-files.md +195 -97
  53. package/vendor/docs/06-reference/glossary.md +3 -3
  54. package/vendor/docs/06-reference/installer-flags.md +13 -13
  55. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  56. package/vendor/docs/06-reference/troubleshooting.md +33 -8
  57. package/vendor/docs/_index.json +20 -0
  58. package/vendor/docs/gatekeeper-design.md +22 -19
  59. package/vendor/docs/identity-directory-design.md +57 -56
  60. package/vendor/docs/package.json +1 -1
  61. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  62. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  63. package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
  64. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  65. package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
  66. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/server/index.js +47 -8
  68. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  69. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
  71. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  72. package/vendor/gatekeeper/package.json +4 -3
  73. package/vendor/server/bin/lamaste-server.js +25 -1
  74. package/vendor/server/package.json +2 -2
  75. package/vendor/server/src/commands/audit.js +60 -0
  76. package/vendor/server/src/commands/panel-pin.js +40 -0
  77. package/vendor/server/src/commands/plugins.js +963 -12
  78. package/vendor/server/src/commands/push-install.js +317 -0
  79. package/vendor/server/src/commands/registry-credential.js +2 -1
  80. package/vendor/server/src/commands/uninstall.js +28 -1
  81. package/vendor/server/src/ndjson.js +4 -2
  82. package/vendor/server/src/panel-api.js +277 -34
  83. package/vendor/server/src/panel-identity.js +165 -0
  84. package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
  85. package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
  86. package/vendor/server-ui/dist/index.html +2 -2
  87. package/vendor/serverd/package.json +2 -2
  88. package/vendor/serverd/src/index.js +56 -4
  89. package/vendor/serverd/src/lib/actor.js +24 -0
  90. package/vendor/serverd/src/lib/agent-jobs.js +330 -0
  91. package/vendor/serverd/src/lib/app-error.js +10 -0
  92. package/vendor/serverd/src/lib/audit.js +145 -0
  93. package/vendor/serverd/src/lib/hook-effects.js +123 -0
  94. package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
  95. package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
  96. package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
  97. package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
  98. package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
  99. package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
  100. package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
  101. package/vendor/serverd/src/lib/mtls.js +89 -8
  102. package/vendor/serverd/src/lib/nginx.js +298 -44
  103. package/vendor/serverd/src/lib/notifications.js +107 -0
  104. package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
  105. package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
  106. package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
  107. package/vendor/serverd/src/lib/plugin-operations.js +295 -0
  108. package/vendor/serverd/src/lib/plugin-packages.js +249 -0
  109. package/vendor/serverd/src/lib/plugins.js +298 -68
  110. package/vendor/serverd/src/lib/push-install.js +88 -2
  111. package/vendor/serverd/src/lib/relay-hooks.js +100 -0
  112. package/vendor/serverd/src/lib/relay-signing.js +43 -0
  113. package/vendor/serverd/src/lib/request-hooks.js +79 -0
  114. package/vendor/serverd/src/lib/retained-data.js +99 -0
  115. package/vendor/serverd/src/lib/trust-proxy.js +34 -0
  116. package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
  117. package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
  118. package/vendor/serverd/src/middleware/twofa-session.js +11 -1
  119. package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
  120. package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
  121. package/vendor/serverd/src/routes/management/agents.js +262 -1
  122. package/vendor/serverd/src/routes/management/audit.js +81 -0
  123. package/vendor/serverd/src/routes/management/certs.js +1 -15
  124. package/vendor/serverd/src/routes/management/plugins.js +569 -54
  125. package/vendor/serverd/src/routes/management/tunnels.js +26 -0
  126. package/vendor/serverd/src/routes/management.js +4 -0
  127. package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
  128. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
@@ -0,0 +1,317 @@
1
+ /**
2
+ * lamaste-server push-install — Push install: what lets the panel install,
3
+ * update and remove what Lamaste installs on an agent (the agent half of an
4
+ * app plugin) while that agent's window is open, within its policy.
5
+ *
6
+ * Subcommands:
7
+ * status The switch, the default policy, the policies, every
8
+ * machine agent's window (the default subcommand)
9
+ * on | off Turn push install on or off for this relay
10
+ * default-policy <id> The policy a window gets when none is named
11
+ * open <agent> [--minutes <n>] [--policy <id>]
12
+ * Open an agent's window for n minutes (default 30,
13
+ * within the bounds the panel reports)
14
+ * close <agent> Close an agent's window
15
+ *
16
+ * Every subcommand goes through the running panel (`/api/plugins/push-install/*`
17
+ * and `/api/certs/agent`, admin certificate). The panel checks a push's
18
+ * source address against the agent's policy: pushes this CLI starts come
19
+ * from 127.0.0.1.
20
+ */
21
+
22
+ import chalk from 'chalk';
23
+ import { panelRequest } from '../panel-api.js';
24
+ import { emit, emitComplete, emitError } from '../ndjson.js';
25
+
26
+ /** How long a window opens for when `--minutes` is not given. */
27
+ export const DEFAULT_WINDOW_MINUTES = 30;
28
+
29
+ /**
30
+ * The arguments of `open <agent> [--minutes <n>] [--policy <id>]`, checked
31
+ * against the window bounds the panel reports. Pure, exported for tests.
32
+ *
33
+ * @param {string[]} args what follows `open`
34
+ * @param {{ min: number, max: number }} bounds
35
+ * @returns {{ ok: true, label: string, minutes: number, policyId: string | null } | { ok: false, error: string }}
36
+ */
37
+ export function parseOpenArgs(args, bounds) {
38
+ const usage = 'Usage: lamaste-server push-install open <agent> [--minutes <n>] [--policy <id>]';
39
+ let label = null;
40
+ let minutesText = null;
41
+ let policyId = null;
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg === '--minutes' || arg === '--policy') {
45
+ const value = args[i + 1];
46
+ if (value === undefined || value.startsWith('--')) {
47
+ return { ok: false, error: `${arg} needs a value — ${usage}` };
48
+ }
49
+ if (arg === '--minutes') minutesText = value;
50
+ else policyId = value;
51
+ i++;
52
+ } else if (arg.startsWith('--')) {
53
+ return { ok: false, error: `Unknown option ${arg} — ${usage}` };
54
+ } else if (label === null) {
55
+ label = arg;
56
+ } else {
57
+ return { ok: false, error: `Unexpected argument "${arg}" — ${usage}` };
58
+ }
59
+ }
60
+ if (label === null) return { ok: false, error: usage };
61
+ const minutes = minutesText === null ? DEFAULT_WINDOW_MINUTES : Number(minutesText);
62
+ if (
63
+ (minutesText !== null && !/^\d+$/.test(minutesText)) ||
64
+ minutes < bounds.min ||
65
+ minutes > bounds.max
66
+ ) {
67
+ return {
68
+ ok: false,
69
+ error: `A push-install window is open ${bounds.min} to ${bounds.max} minutes (whole minutes)`,
70
+ };
71
+ }
72
+ return { ok: true, label, minutes, policyId };
73
+ }
74
+
75
+ /**
76
+ * Each machine agent's push-install window, as `status` lists it: open
77
+ * until a time under a policy, or closed. Revoked agents and plugin-agent
78
+ * certificates have none. Pure, exported for tests.
79
+ *
80
+ * @param {Array<Record<string, unknown>>} agents GET /api/certs/agent
81
+ * @param {number} now
82
+ */
83
+ export function agentWindows(agents, now) {
84
+ return agents
85
+ .filter(
86
+ (a) => !a.revoked && typeof a.label === 'string' && !a.label.startsWith('plugin-agent:'),
87
+ )
88
+ .map((a) => {
89
+ const until =
90
+ typeof a.pushInstallEnabledUntil === 'string' ? a.pushInstallEnabledUntil : null;
91
+ const open = until !== null && Date.parse(until) > now;
92
+ return {
93
+ label: a.label,
94
+ open,
95
+ until: open ? until : null,
96
+ policy: open ? (a.pushInstallPolicy ?? null) : null,
97
+ };
98
+ })
99
+ .sort((a, b) => a.label.localeCompare(b.label));
100
+ }
101
+
102
+ /**
103
+ * @param {string[]} args Subcommand args: [] | ['status'] | ['on'] | ['open', '<agent>', …] | …
104
+ * @param {{ json: boolean }} options
105
+ */
106
+ export async function runPushInstall(args, { json }) {
107
+ const sub = args[0] ?? 'status';
108
+ switch (sub) {
109
+ case 'status':
110
+ return statusCmd({ json });
111
+ case 'on':
112
+ case 'off':
113
+ return switchCmd(sub === 'on', { json });
114
+ case 'default-policy':
115
+ return defaultPolicyCmd(args[1], { json });
116
+ case 'open':
117
+ return openCmd(args.slice(1), { json });
118
+ case 'close':
119
+ return closeCmd(args[1], { json });
120
+ default:
121
+ printUsage();
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ function printUsage() {
127
+ const b = chalk.bold;
128
+ const c = chalk.cyan;
129
+ console.log(`
130
+ ${b('Usage:')} lamaste-server push-install <subcommand>
131
+
132
+ ${b('Subcommands:')}
133
+ ${c('status')} The switch, default policy, policies and every agent's window
134
+ ${c('on')} | ${c('off')} Turn push install on or off for this relay
135
+ ${c('default-policy <id>')} The policy a window gets when none is named
136
+ ${c('open <agent> [--minutes <n>] [--policy <id>]')}
137
+ Open an agent's window (default ${DEFAULT_WINDOW_MINUTES} minutes)
138
+ ${c('close <agent>')} Close an agent's window
139
+ `);
140
+ }
141
+
142
+ /**
143
+ * Exit with a usage or panel error, as text or NDJSON.
144
+ *
145
+ * @param {string} message
146
+ * @param {boolean} json
147
+ * @returns {never}
148
+ */
149
+ function fail(message, json) {
150
+ if (json) emitError(message);
151
+ else console.error(`\n ${chalk.red(message)}\n`);
152
+ process.exit(1);
153
+ }
154
+
155
+ /** The panel's push-install config, or exit with its error. */
156
+ async function readConfig(json) {
157
+ try {
158
+ return await panelRequest('GET', '/api/plugins/push-install/config');
159
+ } catch (err) {
160
+ fail(err.message, json);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * @param {{ json: boolean }} options
166
+ */
167
+ async function statusCmd({ json }) {
168
+ const config = await readConfig(json);
169
+ let agents;
170
+ try {
171
+ ({ agents = [] } = await panelRequest('GET', '/api/certs/agent'));
172
+ } catch (err) {
173
+ fail(err.message, json);
174
+ }
175
+ const windows = agentWindows(agents, Date.now());
176
+ if (json) {
177
+ emit({
178
+ enabled: config.enabled,
179
+ defaultPolicy: config.defaultPolicy,
180
+ windowMinutes: config.windowMinutes,
181
+ policies: config.policies,
182
+ agents: windows,
183
+ });
184
+ return;
185
+ }
186
+
187
+ const b = chalk.bold;
188
+ const c = chalk.cyan;
189
+ const d = chalk.dim;
190
+ console.log('');
191
+ console.log(b(' Push install'));
192
+ console.log(d(' ' + '─'.repeat(40)));
193
+ console.log(
194
+ ` switch: ${config.enabled ? chalk.green('on') : chalk.red('off')}${config.enabled ? '' : d(' — every push is refused')}`,
195
+ );
196
+ console.log(` default policy: ${c(config.defaultPolicy)}`);
197
+ console.log(
198
+ ` window: ${config.windowMinutes.min} to ${config.windowMinutes.max} minutes`,
199
+ );
200
+
201
+ console.log(b('\n Policies'));
202
+ for (const p of config.policies) {
203
+ const all = d('(any)');
204
+ console.log(` ${c(p.id)} ${p.name}${p.id === config.defaultPolicy ? d(' (default)') : ''}`);
205
+ console.log(` actions: ${p.allowedActions?.length ? p.allowedActions.join(', ') : all}`);
206
+ console.log(` packages: ${p.allowedPlugins?.length ? p.allowedPlugins.join(', ') : all}`);
207
+ console.log(` from: ${p.allowedIps?.length ? p.allowedIps.join(', ') : all}`);
208
+ if (p.deniedIps?.length) console.log(` not from: ${p.deniedIps.join(', ')}`);
209
+ }
210
+
211
+ console.log(b('\n Agent windows'));
212
+ if (windows.length === 0) console.log(d(' No enrolled machine agent.'));
213
+ const width = Math.max(5, ...windows.map((w) => w.label.length));
214
+ for (const w of windows) {
215
+ console.log(
216
+ ` ${c(w.label.padEnd(width))} ${w.open ? chalk.green(`open until ${w.until}`) + d(` (policy ${w.policy})`) : d('closed')}`,
217
+ );
218
+ }
219
+ console.log('');
220
+ }
221
+
222
+ /**
223
+ * @param {boolean} enabled
224
+ * @param {{ json: boolean }} options
225
+ */
226
+ async function switchCmd(enabled, { json }) {
227
+ let config;
228
+ try {
229
+ config = await panelRequest('PATCH', '/api/plugins/push-install/config', { enabled });
230
+ } catch (err) {
231
+ fail(err.message, json);
232
+ }
233
+ if (json) {
234
+ emitComplete({ enabled: config.enabled });
235
+ return;
236
+ }
237
+ console.log(
238
+ `\n Push install is ${config.enabled ? chalk.green('on') : chalk.red('off')}${
239
+ config.enabled
240
+ ? ` — open an agent's window with ${chalk.cyan('lamaste-server push-install open <agent>')}`
241
+ : ': every push is refused (open windows stay recorded but let nothing through)'
242
+ }.\n`,
243
+ );
244
+ }
245
+
246
+ /**
247
+ * @param {string | undefined} policyId
248
+ * @param {{ json: boolean }} options
249
+ */
250
+ async function defaultPolicyCmd(policyId, { json }) {
251
+ if (!policyId) fail('Usage: lamaste-server push-install default-policy <id>', json);
252
+ let config;
253
+ try {
254
+ config = await panelRequest('PATCH', '/api/plugins/push-install/config', {
255
+ defaultPolicy: policyId,
256
+ });
257
+ } catch (err) {
258
+ fail(err.message, json);
259
+ }
260
+ if (json) emitComplete({ defaultPolicy: config.defaultPolicy });
261
+ else console.log(`\n Default push-install policy: ${chalk.cyan(config.defaultPolicy)}\n`);
262
+ }
263
+
264
+ /**
265
+ * @param {string[]} args
266
+ * @param {{ json: boolean }} options
267
+ */
268
+ async function openCmd(args, { json }) {
269
+ const config = await readConfig(json);
270
+ const parsed = parseOpenArgs(args, config.windowMinutes);
271
+ if (!parsed.ok) fail(parsed.error, json);
272
+ if (!config.enabled) {
273
+ fail(
274
+ 'Push install is off for this relay — turn it on first: lamaste-server push-install on',
275
+ json,
276
+ );
277
+ }
278
+ let result;
279
+ try {
280
+ result = await panelRequest(
281
+ 'POST',
282
+ `/api/plugins/push-install/enable/${encodeURIComponent(parsed.label)}`,
283
+ {
284
+ durationMinutes: parsed.minutes,
285
+ ...(parsed.policyId ? { policyId: parsed.policyId } : {}),
286
+ },
287
+ );
288
+ } catch (err) {
289
+ fail(err.message, json);
290
+ }
291
+ if (json) {
292
+ emitComplete({
293
+ label: result.label,
294
+ pushInstallEnabledUntil: result.pushInstallEnabledUntil,
295
+ pushInstallPolicy: result.pushInstallPolicy,
296
+ });
297
+ return;
298
+ }
299
+ console.log(
300
+ `\n Push install open for ${chalk.cyan(result.label)} until ${result.pushInstallEnabledUntil} (policy ${chalk.cyan(result.pushInstallPolicy)}).\n`,
301
+ );
302
+ }
303
+
304
+ /**
305
+ * @param {string | undefined} label
306
+ * @param {{ json: boolean }} options
307
+ */
308
+ async function closeCmd(label, { json }) {
309
+ if (!label) fail('Usage: lamaste-server push-install close <agent>', json);
310
+ try {
311
+ await panelRequest('DELETE', `/api/plugins/push-install/enable/${encodeURIComponent(label)}`);
312
+ } catch (err) {
313
+ fail(err.message, json);
314
+ }
315
+ if (json) emitComplete({ label, closed: true });
316
+ else console.log(`\n Push install closed for ${chalk.cyan(label)}.\n`);
317
+ }
@@ -7,7 +7,8 @@
7
7
  * registry-credential set --url <registry> --oidc
8
8
  * The panel authenticates as its own `lamaste-server` OIDC client on
9
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.
10
+ * to paste; Feria's auth.json must grant the relay's lamaste-server client
11
+ * through a "workloads" rule on its groups issuer for the relay Authelia.
11
12
  * registry-credential set --url <registry> [--token-stdin]
12
13
  * A long-lived Feria token, read from LAMASTE_REGISTRY_TOKEN or from
13
14
  * stdin with --token-stdin. Never accepted as a command-line argument.
@@ -16,7 +16,7 @@
16
16
  * Requires root access. Interactive confirmation unless --force is passed.
17
17
  */
18
18
 
19
- import { readFile } from 'node:fs/promises';
19
+ import { readFile, readdir } from 'node:fs/promises';
20
20
  import { createInterface } from 'node:readline';
21
21
  import chalk from 'chalk';
22
22
  import { execa } from 'execa';
@@ -117,7 +117,32 @@ export async function runUninstall(args, { json }) {
117
117
  }
118
118
  }
119
119
 
120
+ /** Relay hook runner instances (`lamalibre-lamaste-hooks@<route>`), one per plugin with relay hooks. */
121
+ const HOOKS_UNIT_PREFIX = 'lamalibre-lamaste-hooks';
122
+
123
+ async function stopHookRunners() {
124
+ let enabled = [];
125
+ try {
126
+ enabled = (await readdir('/etc/systemd/system/sockets.target.wants')).filter((name) =>
127
+ /^lamalibre-lamaste-hooks@[a-z0-9-]+\.socket$/.test(name),
128
+ );
129
+ } catch {
130
+ // No socket units enabled
131
+ }
132
+ for (const socket of enabled) {
133
+ await execa('systemctl', ['disable', '--now', socket], { timeout: 15000 }).catch(() => {});
134
+ }
135
+ await execa(
136
+ 'systemctl',
137
+ ['stop', `${HOOKS_UNIT_PREFIX}@*.service`, `${HOOKS_UNIT_PREFIX}@*.socket`],
138
+ {
139
+ timeout: 15000,
140
+ },
141
+ ).catch(() => {});
142
+ }
143
+
120
144
  async function stopServices() {
145
+ await stopHookRunners();
121
146
  for (const service of [PANEL_SERVICE, GATEKEEPER_SERVICE, 'chisel']) {
122
147
  try {
123
148
  await execa('systemctl', ['stop', service], { timeout: 15000 });
@@ -138,6 +163,8 @@ async function removeServices() {
138
163
  `/etc/systemd/system/${GATEKEEPER_SERVICE}.service`,
139
164
  '/etc/systemd/system/chisel.service',
140
165
  '/etc/systemd/system/authelia.service',
166
+ `/etc/systemd/system/${HOOKS_UNIT_PREFIX}@.socket`,
167
+ `/etc/systemd/system/${HOOKS_UNIT_PREFIX}@.service`,
141
168
  ];
142
169
 
143
170
  for (const file of serviceFiles) {
@@ -31,9 +31,11 @@ export function emitStep(step, status, detail) {
31
31
  * Emit an error event.
32
32
  * @param {string} message
33
33
  * @param {boolean} [recoverable]
34
+ * @param {Record<string, unknown>} [details] what a consumer acts on beyond
35
+ * the message — a refused plugin operation's `code` and `reasons`
34
36
  */
35
- export function emitError(message, recoverable = false) {
36
- emit({ event: 'error', message, recoverable });
37
+ export function emitError(message, recoverable = false, details = {}) {
38
+ emit({ event: 'error', message, recoverable, ...details });
37
39
  }
38
40
 
39
41
  /**