@lamalibre/create-lamaste 4.4.1 → 4.6.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 (170) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +234 -18
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/02-guides/static-sites.md +4 -0
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +3 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
  32. package/vendor/docs/03-architecture/overview.md +13 -17
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +3 -2
  35. package/vendor/docs/03-architecture/state-management.md +4 -8
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
  38. package/vendor/docs/04-api-reference/app-host.md +3 -3
  39. package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
  40. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  41. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  42. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  43. package/vendor/docs/04-api-reference/sites.md +4 -0
  44. package/vendor/docs/04-api-reference/tunnels.md +87 -47
  45. package/vendor/docs/04-api-reference/users.md +241 -45
  46. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  47. package/vendor/docs/05-operations/monitoring.md +39 -0
  48. package/vendor/docs/05-operations/uninstalling.md +2 -1
  49. package/vendor/docs/05-operations/upgrades.md +24 -6
  50. package/vendor/docs/06-reference/config-files.md +100 -202
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  54. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  55. package/vendor/docs/gatekeeper-design.md +10 -8
  56. package/vendor/docs/identity-directory-design.md +48 -14
  57. package/vendor/docs/package.json +1 -1
  58. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  59. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  60. package/vendor/gatekeeper/dist/index.d.ts +4 -3
  61. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/index.js +4 -2
  63. package/vendor/gatekeeper/dist/index.js.map +1 -1
  64. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  65. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  67. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  69. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  71. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  72. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  73. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  74. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  75. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  76. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  77. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  79. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  81. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  82. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  83. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  84. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
  85. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  86. package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
  87. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  88. package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
  89. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  90. package/vendor/gatekeeper/dist/server/index.js +49 -56
  91. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  92. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  93. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  94. package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
  95. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  96. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  98. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  100. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  101. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  102. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  103. package/vendor/gatekeeper/package.json +2 -2
  104. package/vendor/server/bin/lamaste-server.js +15 -1
  105. package/vendor/server/package.json +2 -2
  106. package/vendor/server/src/commands/plugin-identity.js +335 -0
  107. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  108. package/vendor/server/src/commands/plugins.js +156 -11
  109. package/vendor/server/src/commands/registry-credential.js +2 -1
  110. package/vendor/server/src/commands/sites.js +15 -1
  111. package/vendor/server/src/commands/tunnels.js +102 -2
  112. package/vendor/server/src/commands/uninstall.js +3 -1
  113. package/vendor/server/src/commands/users.js +358 -0
  114. package/vendor/server/src/plugin-approval.js +186 -5
  115. package/vendor/server/src/plugin-identity.js +382 -0
  116. package/vendor/server/src/plugin-web-surface.js +307 -0
  117. package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
  118. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
  119. package/vendor/server-ui/dist/index.html +2 -2
  120. package/vendor/serverd/package.json +2 -2
  121. package/vendor/serverd/src/index.js +34 -5
  122. package/vendor/serverd/src/lib/audience.js +576 -0
  123. package/vendor/serverd/src/lib/authelia.js +78 -7
  124. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  125. package/vendor/serverd/src/lib/chisel.js +10 -2
  126. package/vendor/serverd/src/lib/config.js +68 -43
  127. package/vendor/serverd/src/lib/directory.js +1113 -0
  128. package/vendor/serverd/src/lib/entitlements.js +517 -0
  129. package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
  130. package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
  131. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  132. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  133. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  134. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  135. package/vendor/serverd/src/lib/mtls.js +6 -2
  136. package/vendor/serverd/src/lib/nginx.js +546 -48
  137. package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
  138. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  139. package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
  140. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  141. package/vendor/serverd/src/lib/plugins.js +95 -7
  142. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  143. package/vendor/serverd/src/lib/request-logging.js +19 -0
  144. package/vendor/serverd/src/lib/retained-data.js +50 -4
  145. package/vendor/serverd/src/lib/state.js +36 -0
  146. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  147. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  148. package/vendor/serverd/src/lib/user-caller.js +67 -0
  149. package/vendor/serverd/src/lib/user-surface.js +754 -0
  150. package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
  151. package/vendor/serverd/src/lib/web-surface.js +766 -0
  152. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  153. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  154. package/vendor/serverd/src/routes/invite.js +53 -12
  155. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  156. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  157. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  158. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  159. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  160. package/vendor/serverd/src/routes/management/plugins.js +2 -0
  161. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  162. package/vendor/serverd/src/routes/management/sites.js +143 -65
  163. package/vendor/serverd/src/routes/management/tunnels.js +109 -22
  164. package/vendor/serverd/src/routes/management/users.js +290 -208
  165. package/vendor/serverd/src/routes/management.js +6 -0
  166. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  167. package/vendor/serverd/src/routes/user-access.js +17 -4
  168. package/vendor/serverd/src/routes/user-surface.js +69 -0
  169. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  170. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -0,0 +1,269 @@
1
+ /**
2
+ * lamaste-server plugins web-surface — the web surface of an agent-hosted
3
+ * plugin that declares one (its manifest's `userSurface.web`): the site it
4
+ * owns, where the relay serves the static shell its server package ships
5
+ * beside its user surface's API.
6
+ *
7
+ * web-surface <name> What its installed version declares, its site,
8
+ * whether the site serves (and why not), the trees
9
+ * the relay holds, the tunnel still bound as its user
10
+ * surface, and — while it owns no site — the offer
11
+ * web-surface <name> create [--subdomain <label>] [--access-mode authenticated|restricted]
12
+ * Give it its site (default: the subdomain and access
13
+ * mode it declares)
14
+ * web-surface <name> move --tunnel <id | subdomain | fqdn>
15
+ * Hand the host of the tunnel bound as its user
16
+ * surface to its site, in one reload; the tunnel
17
+ * stays, disabled and unbound
18
+ * web-surface <name> delete [--yes]
19
+ * Delete its site, its trees and the grants on it,
20
+ * after typing the site's FQDN (--yes skips it)
21
+ *
22
+ * The plugin is named by its name or its route (as `sites list` shows a
23
+ * site's owner). Every action goes through the running panel
24
+ * (`/api/plugins/:name/web-surface[/move]`, admin certificate). What the
25
+ * panel says is printed only as `terminalText`; `--json` emits it as it
26
+ * came.
27
+ */
28
+
29
+ import chalk from 'chalk';
30
+ import * as prompts from '@clack/prompts';
31
+ import { panelRequest } from '../panel-api.js';
32
+ import { readTunnels } from '../state.js';
33
+ import { emit, emitComplete, emitError } from '../ndjson.js';
34
+ import { findHostTunnel, findPluginByNameOrRoute } from '../plugin-identity.js';
35
+ import { terminalText } from '../plugin-settings.js';
36
+ import {
37
+ createBody,
38
+ parseWebSurfaceArgs,
39
+ webSurfaceRefusalHint,
40
+ webSurfaceServedText,
41
+ webTreeRows,
42
+ } from '../plugin-web-surface.js';
43
+
44
+ /** `/api/plugins/<name>/web-surface[/<rest>]`. */
45
+ function webSurfacePath(name, rest = '') {
46
+ return `/api/plugins/${encodeURIComponent(name)}/web-surface${rest ? `/${rest}` : ''}`;
47
+ }
48
+
49
+ /**
50
+ * Exit with a usage error or a refused request, as text (with what the
51
+ * refusal asks next) or NDJSON (with the refusal's `code`).
52
+ *
53
+ * @param {string} message
54
+ * @param {boolean} json
55
+ * @param {{ code?: string }} [err]
56
+ * @param {string} [name] the plugin, for the hint
57
+ * @returns {never}
58
+ */
59
+ function fail(message, json, err = {}, name = '<name>') {
60
+ const code = typeof err.code === 'string' ? err.code : null;
61
+ if (json) {
62
+ emitError(message, false, code ? { code } : {});
63
+ } else {
64
+ console.error(`\n ${chalk.red(terminalText(message))}`);
65
+ const hint = webSurfaceRefusalHint(code, name);
66
+ if (hint) console.error(` ${chalk.dim(hint)}`);
67
+ console.error('');
68
+ }
69
+ process.exit(1);
70
+ }
71
+
72
+ /**
73
+ * A panel request; a refusal exits through `fail`, with what it asks next
74
+ * of plugin `name`.
75
+ */
76
+ async function request(json, name, method, path, body) {
77
+ try {
78
+ return await panelRequest(method, path, body);
79
+ } catch (err) {
80
+ fail(err.message, json, err, name);
81
+ }
82
+ }
83
+
84
+ /** `label:` padded, then the value. */
85
+ function row(label, value) {
86
+ console.log(` ${chalk.dim(`${label}:`.padEnd(16))}${value}`);
87
+ }
88
+
89
+ /**
90
+ * Print a web surface's status (`webSurface`) for plugin `name`: what it
91
+ * declares, its site, whether it is served, its trees, the bound tunnel and
92
+ * the offer.
93
+ */
94
+ function printWebSurface(name, view) {
95
+ const plugin = terminalText(name);
96
+ console.log('');
97
+ if (!view.declared && !view.site) {
98
+ console.log(` Plugin ${chalk.cyan(plugin)} declares no web surface and owns no site.`);
99
+ console.log('');
100
+ return;
101
+ }
102
+ console.log(chalk.bold(` Web surface of ${plugin}`));
103
+ console.log(chalk.dim(' ' + '─'.repeat(40)));
104
+ row(
105
+ 'declares',
106
+ view.declared
107
+ ? terminalText(
108
+ `${view.declared.defaultSubdomain} (${view.declared.accessMode}), the shell from ${view.declared.root}/`,
109
+ )
110
+ : chalk.yellow('nothing — its installed version declares no web surface'),
111
+ );
112
+ row(
113
+ 'site',
114
+ view.site
115
+ ? `${chalk.cyan(`https://${terminalText(view.site.fqdn)}/`)}${chalk.dim(` ${terminalText(view.site.accessMode ?? 'access mode unknown')} · id ${terminalText(view.site.id)}`)}`
116
+ : chalk.dim('none'),
117
+ );
118
+ const served = webSurfaceServedText(view, name);
119
+ row('served', served.served ? chalk.green(served.text) : chalk.yellow(served.text));
120
+ for (const [label, value] of webTreeRows(view.tree)) row(label, value);
121
+ if (view.boundTunnel) {
122
+ const host = terminalText(view.boundTunnel.fqdn ?? view.boundTunnel.id);
123
+ const others = (view.boundTunnel.routes ?? []).length > 1;
124
+ row(
125
+ 'bound tunnel',
126
+ `${chalk.cyan(host)}${chalk.dim(` id ${terminalText(view.boundTunnel.id)}${others ? ` · also serves ${terminalText(view.boundTunnel.routes.join(', '))}` : ''}`)}`,
127
+ );
128
+ }
129
+ if (view.offer) {
130
+ const offer = `${terminalText(view.offer.subdomain)} (${terminalText(view.offer.accessMode)})`;
131
+ row(
132
+ 'offer',
133
+ view.offer.available
134
+ ? `${offer} ${chalk.green('— free')}`
135
+ : `${offer} ${chalk.yellow('— taken; create with --subdomain <label>')}`,
136
+ );
137
+ }
138
+ if (!view.site && view.declared) {
139
+ console.log('');
140
+ if (view.boundTunnel) {
141
+ console.log(
142
+ chalk.dim(
143
+ ` Move the bound tunnel's host to it: lamaste-server plugins web-surface ${plugin} move --tunnel ${terminalText(view.boundTunnel.fqdn ?? view.boundTunnel.id)}`,
144
+ ),
145
+ );
146
+ } else {
147
+ console.log(
148
+ chalk.dim(
149
+ ` Give it its site: lamaste-server plugins web-surface ${plugin} create [--subdomain <label>] [--access-mode authenticated|restricted]`,
150
+ ),
151
+ );
152
+ }
153
+ }
154
+ console.log('');
155
+ }
156
+
157
+ /**
158
+ * Ask for the site's FQDN before deleting it — or refuse where nobody can
159
+ * answer (no terminal, `--json`) without `--yes`.
160
+ *
161
+ * @param {string} name
162
+ * @param {{ fqdn: string }} site
163
+ * @param {{ yes: boolean, json: boolean }} options
164
+ */
165
+ async function confirmDelete(name, site, { yes, json }) {
166
+ if (yes) return;
167
+ if (json || !process.stdin.isTTY || !process.stdout.isTTY) {
168
+ fail(
169
+ `Deleting the web surface of "${name}" (${site.fqdn}) asks for confirmation — pass --yes`,
170
+ json,
171
+ );
172
+ }
173
+ const fqdn = terminalText(site.fqdn);
174
+ console.log('');
175
+ console.log(chalk.bold.red(` Delete the web surface of ${terminalText(name)}?`));
176
+ console.log(` - ${chalk.cyan(fqdn)} stops answering: its vhost is removed`);
177
+ console.log(' - the web trees the relay holds for it are deleted');
178
+ console.log(' - the grants on the site are revoked (a restricted site loses its users)');
179
+ console.log(' - the plugin stays installed and hosted; create the site again at any time');
180
+ console.log('');
181
+ const answer = await prompts.text({ message: `Type ${fqdn} to delete it` });
182
+ if (prompts.isCancel(answer) || String(answer ?? '').trim() !== site.fqdn) {
183
+ console.log('\n Aborted — nothing was deleted.\n');
184
+ process.exit(0);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * `plugins web-surface <name> [create|move|delete]`.
190
+ *
191
+ * @param {string[]} args what follows the subcommand
192
+ * @param {{ json: boolean }} options
193
+ */
194
+ export async function webSurfaceCmd(args, { json }) {
195
+ const parsed = parseWebSurfaceArgs(args);
196
+ if (!parsed.ok) fail(parsed.error, json);
197
+
198
+ const { plugins = [] } = await request(json, parsed.name, 'GET', '/api/plugins');
199
+ const named = findPluginByNameOrRoute(plugins, parsed.name);
200
+ if (named.error) fail(named.error, json, { code: 'not_found' }, parsed.name);
201
+ const { name } = named.plugin;
202
+ const plugin = terminalText(name);
203
+
204
+ if (parsed.action === 'status') {
205
+ const result = await request(json, name, 'GET', webSurfacePath(name));
206
+ if (json) emit(result);
207
+ else printWebSurface(name, result.webSurface);
208
+ return;
209
+ }
210
+
211
+ if (parsed.action === 'create') {
212
+ const result = await request(json, name, 'POST', webSurfacePath(name), createBody(parsed));
213
+ if (json) {
214
+ emitComplete({ name, ...result });
215
+ return;
216
+ }
217
+ const fqdn = terminalText(result.webSurface?.site?.fqdn ?? '');
218
+ console.log(
219
+ `\n ${chalk.green('✓')} ${plugin} has its site: ${chalk.cyan(`https://${fqdn}/`)}`,
220
+ );
221
+ printWebSurface(name, result.webSurface);
222
+ return;
223
+ }
224
+
225
+ if (parsed.action === 'move') {
226
+ const found = findHostTunnel(await readTunnels(), parsed.tunnel);
227
+ if (found.error) fail(found.error, json);
228
+ const { tunnel } = found;
229
+ const result = await request(json, name, 'POST', webSurfacePath(name, 'move'), {
230
+ tunnelId: tunnel.id,
231
+ });
232
+ if (json) {
233
+ emitComplete({ name, tunnelId: tunnel.id, ...result });
234
+ return;
235
+ }
236
+ const host = terminalText(tunnel.fqdn ?? tunnel.id);
237
+ console.log(
238
+ `\n ${chalk.green('✓')} ${chalk.cyan(host)} is the web surface of ${plugin} now — the shell from its approved tree, the API as before`,
239
+ );
240
+ console.log(
241
+ chalk.dim(
242
+ ` The tunnel ${terminalText(tunnel.id)} stays, disabled and unbound, its vhost unlinked: delete the web surface, then enable the tunnel, to roll back by hand.`,
243
+ ),
244
+ );
245
+ printWebSurface(name, result.webSurface);
246
+ return;
247
+ }
248
+
249
+ // delete
250
+ const current = await request(json, name, 'GET', webSurfacePath(name));
251
+ const site = current.webSurface?.site ?? null;
252
+ if (!site) fail(`Plugin "${name}" owns no site — there is no web surface to delete`, json);
253
+ await confirmDelete(name, site, { yes: parsed.yes, json });
254
+ const result = await request(json, name, 'DELETE', webSurfacePath(name));
255
+ if (json) {
256
+ emitComplete({ name, siteId: site.id, fqdn: site.fqdn, ...result });
257
+ return;
258
+ }
259
+ console.log(
260
+ `\n ${chalk.green('✓')} The web surface of ${plugin} is deleted: ${chalk.cyan(terminalText(site.fqdn))} no longer answers\n`,
261
+ );
262
+ if (Array.isArray(result.grantsNotRevoked) && result.grantsNotRevoked.length > 0) {
263
+ console.log(
264
+ chalk.yellow(
265
+ ` Gatekeeper kept ${result.grantsNotRevoked.length} grant(s) on the deleted site — they name a site that no longer exists; their ids are in the audit log\n`,
266
+ ),
267
+ );
268
+ }
269
+ }
@@ -52,6 +52,17 @@
52
52
  * configure <name> [--set key=value]... [--clear key]... [--secrets-stdin]
53
53
  * Change them (kept by its agent half); in a terminal without
54
54
  * flags, field by field — a secret never on the command line
55
+ * identity <name> The people of an agent-hosted plugin that declares identity:
56
+ * its roles and attributes, approval, readiness, the feed, erasures
57
+ * audience <name> Its members and the users who left, as its agent half is told
58
+ * entitlements <name> [--user <u> | --group <g> --roles a,b] [--revoke --user|--group]
59
+ * Who holds which of its roles; give or take them away
60
+ * identity-sync <name> [--reconcile]
61
+ * Sync its audience to its agent half now
62
+ * web-surface <name> [create [--subdomain <s>] [--access-mode <m>] | move --tunnel <t> | delete [--yes]]
63
+ * The site an agent-hosted plugin owns for its web surface: what it
64
+ * declares, whether it is served, its trees; create, move a bound
65
+ * tunnel's host to it, delete it
55
66
  *
56
67
  * Every subcommand goes through the running panel (`/api/plugins/*`, admin
57
68
  * certificate): the panel owns the plugin registry (SQLite `state.db`), runs
@@ -69,12 +80,18 @@ import { PANEL_SERVICE } from '../config.js';
69
80
  import { panelRequest } from '../panel-api.js';
70
81
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
71
82
  import { configureCmd, settingsCmd } from './plugin-settings.js';
83
+ import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
84
+ import { webSurfaceCmd } from './plugin-web-surface.js';
72
85
  import { settingsDiffLines, terminalText } from '../plugin-settings.js';
73
86
  import {
74
87
  APPROVE_HOOKS_USAGE,
75
88
  approveHooksCommand,
89
+ identityBlockLines,
90
+ identityDiffLines,
76
91
  parseApproveArgs,
77
92
  pendingUpdateReasons,
93
+ webSurfaceBlockLines,
94
+ webTreeDiffLines,
78
95
  } from '../plugin-approval.js';
79
96
 
80
97
  /** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
@@ -134,6 +151,16 @@ export async function runPlugins(args, { json }) {
134
151
  return settingsCmd(args.slice(1), { json });
135
152
  case 'configure':
136
153
  return configureCmd(args.slice(1), { json });
154
+ case 'identity':
155
+ return identityCmd(args[1], { json });
156
+ case 'audience':
157
+ return audienceCmd(args[1], { json });
158
+ case 'entitlements':
159
+ return entitlementsCmd(args.slice(1), { json });
160
+ case 'identity-sync':
161
+ return identitySyncCmd(args.slice(1), { json });
162
+ case 'web-surface':
163
+ return webSurfaceCmd(args.slice(1), { json });
137
164
  default:
138
165
  printPluginUsage();
139
166
  process.exit(sub ? 1 : 0);
@@ -191,6 +218,24 @@ ${b('Subcommands:')}
191
218
  --clear puts a field back to its default or clears a secret,
192
219
  --secrets-stdin reads {"key": "value" | null} from stdin;
193
220
  in a terminal without flags, asked field by field
221
+ ${c('identity <name>')} People of a plugin that declares identity: its roles and attributes,
222
+ approval, whether users reach it (and why not), the feed, erasures
223
+ ${c('audience <name>')} Its members and the users who left, as its agent half is told
224
+ ${c('entitlements <name>')} Who holds which of its roles (Gatekeeper grants with roles)
225
+ ${c('entitlements <name> --user <u> | --group <g> --roles a,b')}
226
+ Give a user or a Lamaste group exactly these roles
227
+ ${c('entitlements <name> --revoke --user <u> | --group <g>')}
228
+ Take every role away
229
+ ${c('identity-sync <name> [--reconcile]')}
230
+ Sync its audience to its agent half now; --reconcile replays all of it
231
+ ${c('web-surface <name>')} The site a plugin owns for its web surface: what it declares, its site,
232
+ whether it is served (and why not), its trees, the bound tunnel, the offer
233
+ ${c('web-surface <name> create [--subdomain <label>] [--access-mode authenticated|restricted]')}
234
+ Give it its site (default: the subdomain and access mode it declares)
235
+ ${c('web-surface <name> move --tunnel <id | subdomain | fqdn>')}
236
+ Hand the host of the tunnel bound as its user surface to its site
237
+ ${c('web-surface <name> delete [--yes]')}
238
+ Delete its site, its trees and the grants on it (typed confirmation)
194
239
  `);
195
240
  }
196
241
 
@@ -725,9 +770,11 @@ function printRelayHookSet(relay, title) {
725
770
  ` request: ${hook.stage} ${who} on ${hook.routes.join(', ')}${failOpen}` +
726
771
  d(` (body ${hook.bodyMaxBytes ?? 0} B, ${hook.timeoutMs} ms, onError ${hook.onError})`),
727
772
  );
728
- if (hook.inactiveRoutes.length > 0) {
773
+ if (hook.userSurfaceRoutes.length > 0) {
729
774
  console.log(
730
- d(` not active until the user surface: ${hook.inactiveRoutes.join(', ')}`),
775
+ d(
776
+ ` runs on user-surface requests (plugins that declare identity): ${hook.userSurfaceRoutes.join(', ')}`,
777
+ ),
731
778
  );
732
779
  }
733
780
  }
@@ -757,15 +804,34 @@ function printAgentHalf(agentHalf, title) {
757
804
  );
758
805
  }
759
806
 
807
+ /**
808
+ * Print an identity block — the roles a plugin's users hold and what it
809
+ * receives about each member.
810
+ */
811
+ function printIdentityBlock(block, title) {
812
+ console.log(chalk.bold(` ${title}`));
813
+ for (const line of identityBlockLines(block)) console.log(` ${line}`);
814
+ }
815
+
816
+ /**
817
+ * Print a web surface — the web tree an approval covers and the site
818
+ * Lamaste offers for it (the hook status's `web`).
819
+ */
820
+ function printWebSurfaceBlock(web, title) {
821
+ console.log(chalk.bold(` ${title}`));
822
+ for (const line of webSurfaceBlockLines(web)) console.log(` ${line}`);
823
+ }
824
+
760
825
  /**
761
826
  * Print what a version brings that needs approval — the approval digest
762
827
  * (what `approve-hooks` takes; none for an update that brings neither
763
- * relay hooks nor an agent half Lamaste installs), its relay hook set and
764
- * the agent half Lamaste installs.
828
+ * relay hooks, nor an agent half Lamaste installs, nor an identity block,
829
+ * nor a web surface), its relay hook set, the agent half Lamaste installs,
830
+ * its identity block and its web tree.
765
831
  */
766
- function printApprovalContents({ digest, relay, agentHalf }) {
832
+ function printApprovalContents({ digest, relay, agentHalf, identity = null, web = null }) {
767
833
  console.log(
768
- ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs')}`,
834
+ ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block, no web surface')}`,
769
835
  );
770
836
  if (relay) {
771
837
  console.log('');
@@ -775,12 +841,66 @@ function printApprovalContents({ digest, relay, agentHalf }) {
775
841
  console.log('');
776
842
  printAgentHalf(agentHalf, 'Agent half — Lamaste installs it on the hosting agent');
777
843
  }
844
+ if (identity) {
845
+ console.log('');
846
+ printIdentityBlock(
847
+ identity,
848
+ 'Identity — Lamaste sends its agent half the users entitled to these roles',
849
+ );
850
+ }
851
+ if (web?.declared) {
852
+ console.log('');
853
+ printWebSurfaceBlock(
854
+ web,
855
+ 'Web surface — the relay serves this tree on the site the plugin owns',
856
+ );
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Print how an update changes the plugin's web tree (part of the digest):
862
+ * the tree before and after, its file count and size.
863
+ */
864
+ function printWebTreeChanges(web) {
865
+ const shown = webTreeDiffLines(web);
866
+ if (!shown) return;
867
+ console.log('');
868
+ console.log(chalk.bold(` Web surface — this version ${shown.headline}`));
869
+ for (const line of shown.lines) {
870
+ console.log(` ${shown.changed ? line : chalk.dim(line)}`);
871
+ }
872
+ }
873
+
874
+ /**
875
+ * Print how an update changes the plugin's identity block (part of the
876
+ * digest): roles added or removed and attributes widened wait for an
877
+ * administrator whatever auto-approve says; the block it brings.
878
+ */
879
+ function printIdentityChanges(diff) {
880
+ const shown = identityDiffLines(diff);
881
+ if (!shown) {
882
+ // Declared before and after, unchanged: still part of what is approved.
883
+ if (diff?.after) {
884
+ console.log('');
885
+ printIdentityBlock(diff.after, 'Identity — unchanged');
886
+ }
887
+ return;
888
+ }
889
+ console.log('');
890
+ console.log(chalk.bold(` Identity — this version ${shown.headline}`));
891
+ for (const line of shown.lines) {
892
+ console.log(` ${line.approval ? chalk.yellow(line.text) : line.text}`);
893
+ }
894
+ if (diff.after) {
895
+ for (const line of identityBlockLines(diff.after)) console.log(chalk.dim(` ${line}`));
896
+ }
778
897
  }
779
898
 
780
899
  /**
781
900
  * Print an update that waits for approval under `title`: why it waits
782
- * (`approvalRequired` — a secret turned plain in red), what it brings
783
- * (its digest, relay hook set and agent half), how it changes the settings
901
+ * (`approvalRequired` — a secret turned plain in red, an identity change
902
+ * with its note), what it brings (its digest, relay hook set and agent
903
+ * half), how it changes the identity block, how it changes the settings
784
904
  * schema (outside the digest) — fields added and removed with their types,
785
905
  * retyped ones, newly required ones, and a warning for secrets that become
786
906
  * plain values — what auto-approve could not cover beyond the reasons, and
@@ -791,10 +911,17 @@ function printPendingUpdate(name, update, title) {
791
911
  console.log(chalk.bold(` ${title}`));
792
912
  const { required, notCovered } = pendingUpdateReasons(update);
793
913
  for (const line of required) {
794
- console.log(line.warning ? chalk.red(` - ${line.text}`) : ` - ${line.text}`);
914
+ const text = line.warning ? chalk.red(` - ${line.text}`) : ` - ${line.text}`;
915
+ console.log(line.note ? `${text}${chalk.dim(` (${line.note})`)}` : text);
795
916
  }
796
917
  console.log('');
797
- printApprovalContents(update);
918
+ printApprovalContents({
919
+ digest: update.digest,
920
+ relay: update.relay,
921
+ agentHalf: update.agentHalf,
922
+ });
923
+ printIdentityChanges(update.identity);
924
+ printWebTreeChanges(update.web);
798
925
  const lines = settingsDiffLines(update.settings);
799
926
  if (lines.length > 0) {
800
927
  console.log('');
@@ -837,7 +964,7 @@ async function hooksCmd(name, { json }) {
837
964
  console.log('');
838
965
  if (!approval.required && !hooks.agent && !hooks.pendingUpdate) {
839
966
  console.log(
840
- ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks and no agent half for Lamaste to install.\n`,
967
+ ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install, no identity and no web surface.\n`,
841
968
  );
842
969
  return;
843
970
  }
@@ -853,6 +980,8 @@ async function hooksCmd(name, { json }) {
853
980
  digest: approval.digest,
854
981
  relay: hooks.relay,
855
982
  agentHalf: hooks.agentHalf,
983
+ identity: hooks.identity?.declared ? hooks.identity.block : null,
984
+ web: hooks.web ?? null,
856
985
  });
857
986
  console.log(
858
987
  `\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
@@ -860,6 +989,20 @@ async function hooksCmd(name, { json }) {
860
989
  if (approval.state !== 'approved') {
861
990
  console.log(`\n Approve with: ${approveCommand(name, { digest: approval.digest })}`);
862
991
  }
992
+ if (hooks.identity?.declared) {
993
+ console.log(
994
+ chalk.dim(
995
+ ` Its people (entitlements, audience, feed): lamaste-server plugins identity ${terminalText(name)}`,
996
+ ),
997
+ );
998
+ }
999
+ if (hooks.web?.declared) {
1000
+ console.log(
1001
+ chalk.dim(
1002
+ ` Its site (served, trees, offer): lamaste-server plugins web-surface ${terminalText(name)}`,
1003
+ ),
1004
+ );
1005
+ }
863
1006
  }
864
1007
  if (hooks.agent) {
865
1008
  console.log(chalk.bold(`\n Agent hooks (run by the agent half; no approval)`));
@@ -1174,6 +1317,8 @@ async function installPluginCmd(packageName, { json }) {
1174
1317
  digest: approval.digest,
1175
1318
  relay: result.hooks.relay,
1176
1319
  agentHalf: result.hooks.agentHalf,
1320
+ identity: result.hooks.identity?.declared ? result.hooks.identity.block : null,
1321
+ web: result.hooks.web ?? null,
1177
1322
  });
1178
1323
  if (approval.state === 'pending') {
1179
1324
  console.log(
@@ -8,7 +8,8 @@
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
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
+ * read and admin through a "workloads" rule on its groups issuer for the
12
+ * relay Authelia.
12
13
  * registry-credential set --url <registry> [--token-stdin]
13
14
  * A long-lived Feria token, read from LAMASTE_REGISTRY_TOKEN or from
14
15
  * stdin with --token-stdin. Never accepted as a command-line argument.
@@ -7,6 +7,10 @@
7
7
  * delete <id> Delete a site by ID
8
8
  * aliases <id> --set <a,b|""> Replace a custom site's redirect aliases
9
9
  *
10
+ * A site a plugin owns (its web surface) is marked in the list and managed
11
+ * through `lamaste-server plugins web-surface <plugin>`; the panel refuses
12
+ * to change or delete it here.
13
+ *
10
14
  * List reads directly from the state file.
11
15
  * Create/delete use the panel REST API (which has the full
12
16
  * nginx/certbot/directory dependency stack).
@@ -16,6 +20,7 @@ import chalk from 'chalk';
16
20
  import { readSites } from '../state.js';
17
21
  import { panelRequest } from '../panel-api.js';
18
22
  import { emit, emitError, emitComplete } from '../ndjson.js';
23
+ import { siteOwnerTag } from '../plugin-web-surface.js';
19
24
 
20
25
  /**
21
26
  * @param {string[]} args
@@ -92,12 +97,21 @@ async function listSites({ json }) {
92
97
  const certStatus = s.certIssued ? g('cert issued') : y('no cert');
93
98
  const spaTag = s.spaMode ? d('[SPA]') : '';
94
99
  const authTag = s.autheliaProtected ? d('[protected]') : '';
100
+ const owner = siteOwnerTag(s);
101
+ const ownerTag = owner ? c(owner) : '';
95
102
 
96
103
  console.log(
97
- ` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag}`.trimEnd(),
104
+ ` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag} ${ownerTag}`.trimEnd(),
98
105
  );
99
106
  console.log(` ${d(`id: ${s.id}`)}`);
100
107
  }
108
+ if (sites.some((s) => siteOwnerTag(s) !== null)) {
109
+ console.log(
110
+ d(
111
+ '\n A [plugin …] site is the web surface of that plugin: manage it with lamaste-server plugins web-surface <plugin>',
112
+ ),
113
+ );
114
+ }
101
115
  console.log('');
102
116
  }
103
117