@lamalibre/create-lamaste 4.5.0 → 4.7.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 (178) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +20 -3
  3. package/scripts/lamaste-priv +56 -5
  4. package/scripts/pin-release.js +98 -0
  5. package/src/index.js +8 -1
  6. package/src/lib/env.js +19 -0
  7. package/src/lib/service-config.js +7 -0
  8. package/src/tasks/redeploy.js +8 -0
  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 +6 -2
  13. package/vendor/docs/01-concepts/authorization.md +4 -0
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
  16. package/vendor/docs/01-concepts/mtls.md +2 -2
  17. package/vendor/docs/01-concepts/security-model.md +28 -3
  18. package/vendor/docs/01-concepts/tickets.md +2 -2
  19. package/vendor/docs/01-concepts/tunneling.md +4 -4
  20. package/vendor/docs/02-guides/agent-setup.md +5 -5
  21. package/vendor/docs/02-guides/certificate-management.md +2 -2
  22. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  23. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  24. package/vendor/docs/02-guides/installation.md +3 -3
  25. package/vendor/docs/02-guides/managing-users.md +5 -11
  26. package/vendor/docs/02-guides/onboarding.md +2 -2
  27. package/vendor/docs/02-guides/second-domain.md +82 -0
  28. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  29. package/vendor/docs/02-guides/static-sites.md +4 -0
  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 +80 -12
  34. package/vendor/docs/03-architecture/overview.md +5 -5
  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 +3 -3
  38. package/vendor/docs/03-architecture/system-overview.md +1 -1
  39. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
  40. package/vendor/docs/04-api-reference/app-host.md +3 -3
  41. package/vendor/docs/04-api-reference/domains.md +192 -0
  42. package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
  43. package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
  44. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  45. package/vendor/docs/04-api-reference/overview.md +9 -0
  46. package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
  47. package/vendor/docs/04-api-reference/sites.md +9 -0
  48. package/vendor/docs/04-api-reference/tunnels.md +35 -28
  49. package/vendor/docs/04-api-reference/users.md +41 -29
  50. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  51. package/vendor/docs/05-operations/uninstalling.md +1 -1
  52. package/vendor/docs/05-operations/upgrades.md +15 -6
  53. package/vendor/docs/06-reference/config-files.md +19 -15
  54. package/vendor/docs/06-reference/glossary.md +3 -3
  55. package/vendor/docs/06-reference/installer-flags.md +13 -13
  56. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  57. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  58. package/vendor/docs/_index.json +105 -19
  59. package/vendor/docs/external-members-design.md +816 -0
  60. package/vendor/docs/gatekeeper-design.md +8 -8
  61. package/vendor/docs/identity-directory-design.md +3 -3
  62. package/vendor/docs/multi-domain-relay-design.md +836 -0
  63. package/vendor/docs/package.json +1 -1
  64. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  65. package/vendor/gatekeeper/dist/index.d.ts +3 -3
  66. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/index.js +2 -2
  68. package/vendor/gatekeeper/dist/index.js.map +1 -1
  69. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  70. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  71. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  72. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  73. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  74. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  75. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  76. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  77. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/grants.js +20 -13
  79. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
  81. package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
  82. package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
  83. package/vendor/gatekeeper/dist/lib/secret.js +12 -0
  84. package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
  85. package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
  86. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
  87. package/vendor/gatekeeper/dist/lib/templates.js +80 -14
  88. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
  89. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
  90. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
  91. package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
  92. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
  93. package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
  94. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  95. package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
  96. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/index.js +56 -11
  98. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  100. package/vendor/gatekeeper/dist/server/routes/authz.js +325 -55
  101. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  102. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  103. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
  104. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  105. package/vendor/gatekeeper/package.json +2 -2
  106. package/vendor/server/bin/lamaste-server.js +7 -0
  107. package/vendor/server/package.json +2 -2
  108. package/vendor/server/src/commands/domains.js +214 -0
  109. package/vendor/server/src/commands/plugin-identity.js +2 -16
  110. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  111. package/vendor/server/src/commands/plugins.js +175 -14
  112. package/vendor/server/src/commands/registry-credential.js +10 -3
  113. package/vendor/server/src/commands/sites.js +18 -1
  114. package/vendor/server/src/commands/status.js +19 -0
  115. package/vendor/server/src/commands/tunnels.js +4 -1
  116. package/vendor/server/src/commands/users.js +8 -19
  117. package/vendor/server/src/domains.js +92 -0
  118. package/vendor/server/src/fail.js +21 -0
  119. package/vendor/server/src/oidc-client-grants.js +63 -0
  120. package/vendor/server/src/panel-api.js +15 -0
  121. package/vendor/server/src/plugin-approval.js +80 -1
  122. package/vendor/server/src/plugin-web-surface.js +307 -0
  123. package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
  124. package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
  125. package/vendor/server-ui/dist/index.html +2 -2
  126. package/vendor/serverd/package.json +2 -2
  127. package/vendor/serverd/src/index.js +50 -19
  128. package/vendor/serverd/src/lib/app-error.js +24 -0
  129. package/vendor/serverd/src/lib/audience.js +12 -3
  130. package/vendor/serverd/src/lib/authelia.js +341 -35
  131. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  132. package/vendor/serverd/src/lib/chisel.js +10 -2
  133. package/vendor/serverd/src/lib/directory.js +67 -52
  134. package/vendor/serverd/src/lib/domains.js +1792 -0
  135. package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
  136. package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
  137. package/vendor/serverd/src/lib/identity-sync.js +35 -0
  138. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  139. package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
  140. package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
  141. package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
  142. package/vendor/serverd/src/lib/nginx.js +576 -131
  143. package/vendor/serverd/src/lib/owned-groups.js +326 -0
  144. package/vendor/serverd/src/lib/plugin-domain.js +86 -0
  145. package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
  146. package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
  147. package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
  148. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  149. package/vendor/serverd/src/lib/plugins.js +82 -9
  150. package/vendor/serverd/src/lib/registry-credential.js +39 -6
  151. package/vendor/serverd/src/lib/services.js +9 -1
  152. package/vendor/serverd/src/lib/state.js +29 -0
  153. package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
  154. package/vendor/serverd/src/lib/user-surface.js +77 -31
  155. package/vendor/serverd/src/lib/users-lock.js +23 -0
  156. package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
  157. package/vendor/serverd/src/lib/web-surface.js +783 -0
  158. package/vendor/serverd/src/middleware/user-access-session.js +11 -4
  159. package/vendor/serverd/src/routes/invite.js +15 -5
  160. package/vendor/serverd/src/routes/management/domains.js +154 -0
  161. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
  162. package/vendor/serverd/src/routes/management/identity.js +21 -7
  163. package/vendor/serverd/src/routes/management/invitations.js +33 -4
  164. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  165. package/vendor/serverd/src/routes/management/plugins.js +6 -2
  166. package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
  167. package/vendor/serverd/src/routes/management/sites.js +188 -71
  168. package/vendor/serverd/src/routes/management/tunnels.js +37 -22
  169. package/vendor/serverd/src/routes/management/users.js +184 -64
  170. package/vendor/serverd/src/routes/management.js +4 -0
  171. package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
  172. package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
  173. package/vendor/serverd/src/routes/user-access.js +13 -1
  174. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
  175. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
  176. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
  177. package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
  178. package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
@@ -10,7 +10,12 @@
10
10
  * Remove a plugin (must be disabled first); a hosted one
11
11
  * through its uninstall pipeline (an operation); --purge also
12
12
  * deletes the data of the agent half Lamaste installed
13
- * oidc-clients <name> The OIDC clients a plugin declares on Authelia
13
+ * oidc-clients <name> [grants]
14
+ * The OIDC clients a plugin declares on Authelia, and who is
15
+ * granted each user-facing one
16
+ * oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>
17
+ * Grant (or revoke) a user-facing client to a user or a Lamaste
18
+ * group: what admits an external member to its sign-in
14
19
  * rotate-secret <name> <clientId>
15
20
  * New secret for a confidential OIDC client (shown once)
16
21
  * hosting <name> Which agent hosts an agent-hosted plugin's agent half
@@ -59,6 +64,11 @@
59
64
  * Who holds which of its roles; give or take them away
60
65
  * identity-sync <name> [--reconcile]
61
66
  * Sync its audience to its agent half now
67
+ * domain <name> [<domain>] The domain a plugin belongs to; move it to another active one
68
+ * web-surface <name> [create [--subdomain <s>] [--access-mode <m>] | move --tunnel <t> | delete [--yes]]
69
+ * The site an agent-hosted plugin owns for its web surface: what it
70
+ * declares, whether it is served, its trees; create, move a bound
71
+ * tunnel's host to it, delete it
62
72
  *
63
73
  * Every subcommand goes through the running panel (`/api/plugins/*`, admin
64
74
  * certificate): the panel owns the plugin registry (SQLite `state.db`), runs
@@ -77,6 +87,15 @@ import { panelRequest } from '../panel-api.js';
77
87
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
78
88
  import { configureCmd, settingsCmd } from './plugin-settings.js';
79
89
  import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
90
+ import { webSurfaceCmd } from './plugin-web-surface.js';
91
+ import { pluginDomainCmd } from './domains.js';
92
+ import { fail as failPanel } from '../fail.js';
93
+ import {
94
+ OIDC_CLIENT_RESOURCE,
95
+ findClientGrants,
96
+ parsePrincipal,
97
+ principalText,
98
+ } from '../oidc-client-grants.js';
80
99
  import { settingsDiffLines, terminalText } from '../plugin-settings.js';
81
100
  import {
82
101
  APPROVE_HOOKS_USAGE,
@@ -85,6 +104,8 @@ import {
85
104
  identityDiffLines,
86
105
  parseApproveArgs,
87
106
  pendingUpdateReasons,
107
+ webSurfaceBlockLines,
108
+ webTreeDiffLines,
88
109
  } from '../plugin-approval.js';
89
110
 
90
111
  /** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
@@ -109,7 +130,7 @@ export async function runPlugins(args, { json }) {
109
130
  case 'uninstall':
110
131
  return uninstallPluginCmd(args[1], args.slice(2), { json });
111
132
  case 'oidc-clients':
112
- return oidcClientsCmd(args[1], { json });
133
+ return oidcClientsCmd(args[1], args.slice(2), { json });
113
134
  case 'rotate-secret':
114
135
  return rotateSecretCmd(args[1], args[2], { json });
115
136
  case 'hosting':
@@ -152,6 +173,10 @@ export async function runPlugins(args, { json }) {
152
173
  return entitlementsCmd(args.slice(1), { json });
153
174
  case 'identity-sync':
154
175
  return identitySyncCmd(args.slice(1), { json });
176
+ case 'web-surface':
177
+ return webSurfaceCmd(args.slice(1), { json });
178
+ case 'domain':
179
+ return pluginDomainCmd(args.slice(1), { json });
155
180
  default:
156
181
  printPluginUsage();
157
182
  process.exit(sub ? 1 : 0);
@@ -172,7 +197,11 @@ ${b('Subcommands:')}
172
197
  ${c('uninstall <name> [--force] [--purge]')}
173
198
  Remove a disabled plugin (a hosted one through its uninstall pipeline;
174
199
  --purge deletes the data of the agent half Lamaste installed)
175
- ${c('oidc-clients <name>')} OIDC clients the plugin declares on Authelia (no secrets)
200
+ ${c('oidc-clients <name> [grants]')}
201
+ OIDC clients the plugin declares on Authelia (no secrets), with grants
202
+ ${c('oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>')}
203
+ Admit (or no longer) an external member to a client's sign-in —
204
+ members of the plugin's domain need no grant
176
205
  ${c('rotate-secret <name> <clientId>')}
177
206
  Replace a confidential client's secret — printed once
178
207
  ${c('hosting <name>')} Which agent hosts the plugin's agent half, port, health
@@ -219,6 +248,16 @@ ${b('Subcommands:')}
219
248
  Take every role away
220
249
  ${c('identity-sync <name> [--reconcile]')}
221
250
  Sync its audience to its agent half now; --reconcile replays all of it
251
+ ${c('domain <name> [<d>]')} The domain a plugin belongs to (its OIDC sign-in, surfaces, audience);
252
+ with <d>, move it to that active domain (refused while a surface is served)
253
+ ${c('web-surface <name>')} The site a plugin owns for its web surface: what it declares, its site,
254
+ whether it is served (and why not), its trees, the bound tunnel, the offer
255
+ ${c('web-surface <name> create [--subdomain <label>] [--access-mode authenticated|restricted]')}
256
+ Give it its site (default: the subdomain and access mode it declares)
257
+ ${c('web-surface <name> move --tunnel <id | subdomain | fqdn>')}
258
+ Hand the host of the tunnel bound as its user surface to its site
259
+ ${c('web-surface <name> delete [--yes]')}
260
+ Delete its site, its trees and the grants on it (typed confirmation)
222
261
  `);
223
262
  }
224
263
 
@@ -796,16 +835,25 @@ function printIdentityBlock(block, title) {
796
835
  for (const line of identityBlockLines(block)) console.log(` ${line}`);
797
836
  }
798
837
 
838
+ /**
839
+ * Print a web surface — the web tree an approval covers and the site
840
+ * Lamaste offers for it (the hook status's `web`).
841
+ */
842
+ function printWebSurfaceBlock(web, title) {
843
+ console.log(chalk.bold(` ${title}`));
844
+ for (const line of webSurfaceBlockLines(web)) console.log(` ${line}`);
845
+ }
846
+
799
847
  /**
800
848
  * Print what a version brings that needs approval — the approval digest
801
849
  * (what `approve-hooks` takes; none for an update that brings neither
802
- * relay hooks, nor an agent half Lamaste installs, nor an identity block),
803
- * its relay hook set, the agent half Lamaste installs and its identity
804
- * block.
850
+ * relay hooks, nor an agent half Lamaste installs, nor an identity block,
851
+ * nor a web surface), its relay hook set, the agent half Lamaste installs,
852
+ * its identity block and its web tree.
805
853
  */
806
- function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
854
+ function printApprovalContents({ digest, relay, agentHalf, identity = null, web = null }) {
807
855
  console.log(
808
- ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block')}`,
856
+ ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block, no web surface')}`,
809
857
  );
810
858
  if (relay) {
811
859
  console.log('');
@@ -822,6 +870,27 @@ function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
822
870
  'Identity — Lamaste sends its agent half the users entitled to these roles',
823
871
  );
824
872
  }
873
+ if (web?.declared) {
874
+ console.log('');
875
+ printWebSurfaceBlock(
876
+ web,
877
+ 'Web surface — the relay serves this tree on the site the plugin owns',
878
+ );
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Print how an update changes the plugin's web tree (part of the digest):
884
+ * the tree before and after, its file count and size.
885
+ */
886
+ function printWebTreeChanges(web) {
887
+ const shown = webTreeDiffLines(web);
888
+ if (!shown) return;
889
+ console.log('');
890
+ console.log(chalk.bold(` Web surface — this version ${shown.headline}`));
891
+ for (const line of shown.lines) {
892
+ console.log(` ${shown.changed ? line : chalk.dim(line)}`);
893
+ }
825
894
  }
826
895
 
827
896
  /**
@@ -874,6 +943,7 @@ function printPendingUpdate(name, update, title) {
874
943
  agentHalf: update.agentHalf,
875
944
  });
876
945
  printIdentityChanges(update.identity);
946
+ printWebTreeChanges(update.web);
877
947
  const lines = settingsDiffLines(update.settings);
878
948
  if (lines.length > 0) {
879
949
  console.log('');
@@ -916,7 +986,7 @@ async function hooksCmd(name, { json }) {
916
986
  console.log('');
917
987
  if (!approval.required && !hooks.agent && !hooks.pendingUpdate) {
918
988
  console.log(
919
- ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install and no identity.\n`,
989
+ ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install, no identity and no web surface.\n`,
920
990
  );
921
991
  return;
922
992
  }
@@ -933,6 +1003,7 @@ async function hooksCmd(name, { json }) {
933
1003
  relay: hooks.relay,
934
1004
  agentHalf: hooks.agentHalf,
935
1005
  identity: hooks.identity?.declared ? hooks.identity.block : null,
1006
+ web: hooks.web ?? null,
936
1007
  });
937
1008
  console.log(
938
1009
  `\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
@@ -947,6 +1018,13 @@ async function hooksCmd(name, { json }) {
947
1018
  ),
948
1019
  );
949
1020
  }
1021
+ if (hooks.web?.declared) {
1022
+ console.log(
1023
+ chalk.dim(
1024
+ ` Its site (served, trees, offer): lamaste-server plugins web-surface ${terminalText(name)}`,
1025
+ ),
1026
+ );
1027
+ }
950
1028
  }
951
1029
  if (hooks.agent) {
952
1030
  console.log(chalk.bold(`\n Agent hooks (run by the agent half; no approval)`));
@@ -1262,6 +1340,7 @@ async function installPluginCmd(packageName, { json }) {
1262
1340
  relay: result.hooks.relay,
1263
1341
  agentHalf: result.hooks.agentHalf,
1264
1342
  identity: result.hooks.identity?.declared ? result.hooks.identity.block : null,
1343
+ web: result.hooks.web ?? null,
1265
1344
  });
1266
1345
  if (approval.state === 'pending') {
1267
1346
  console.log(
@@ -1376,13 +1455,25 @@ async function uninstallPluginCmd(name, rest, { json }) {
1376
1455
  * @param {string | undefined} name
1377
1456
  * @param {{ json: boolean }} options
1378
1457
  */
1379
- async function oidcClientsCmd(name, { json }) {
1380
- if (!name) {
1381
- const msg = 'Usage: lamaste-server plugins oidc-clients <name>';
1458
+ async function oidcClientsCmd(name, rest, { json }) {
1459
+ const usage = () => {
1460
+ const msg =
1461
+ 'Usage: lamaste-server plugins oidc-clients <name> [grants | grant|revoke <clientId> --user <u>|--group <g>]';
1382
1462
  if (json) emitError(msg);
1383
1463
  else console.error(`\n ${msg}\n`);
1384
1464
  process.exit(1);
1385
- }
1465
+ };
1466
+ if (!name) return usage();
1467
+ const [action, clientId, ...principalArgs] = rest;
1468
+ if (action === 'grant' || action === 'revoke') {
1469
+ const principal = parsePrincipal(principalArgs);
1470
+ if (!clientId || !principal) return usage();
1471
+ return action === 'grant'
1472
+ ? grantOidcClientCmd(clientId, principal, { json })
1473
+ : revokeOidcClientCmd(name, clientId, principal, { json });
1474
+ }
1475
+ if (action !== undefined && (action !== 'grants' || clientId !== undefined)) return usage();
1476
+
1386
1477
  const result = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}/oidc-clients`);
1387
1478
  if (json) {
1388
1479
  emit(result);
@@ -1402,7 +1493,7 @@ async function oidcClientsCmd(name, { json }) {
1402
1493
  const kind = c.confidential ? chalk.yellow('confidential') : chalk.green('public');
1403
1494
  const served = c.enabled ? '' : d(' (plugin disabled — not served)');
1404
1495
  console.log(` ${chalk.cyan(c.id)} ${kind}${served}`);
1405
- console.log(` grants: ${c.grantTypes.join(', ')}`);
1496
+ console.log(` flows: ${c.grantTypes.join(', ')}`);
1406
1497
  console.log(` scopes: ${c.scopes.join(', ')}`);
1407
1498
  if (c.audience.length > 0) console.log(` audience: ${c.audience.join(', ')}`);
1408
1499
  if (c.accessTokenClaims.length > 0) {
@@ -1415,10 +1506,80 @@ async function oidcClientsCmd(name, { json }) {
1415
1506
  }`,
1416
1507
  );
1417
1508
  }
1509
+ if (c.userFacing) {
1510
+ const granted = Array.isArray(c.grants)
1511
+ ? c.grants.length > 0
1512
+ ? c.grants.map(principalText).join(', ')
1513
+ : d('none — external members do not sign in')
1514
+ : chalk.yellow('unknown (Gatekeeper could not be read)');
1515
+ console.log(` granted: ${granted}`);
1516
+ }
1418
1517
  }
1419
1518
  console.log('');
1420
1519
  }
1421
1520
 
1521
+ /** Grant a user-facing client to a user or a Lamaste group (a Gatekeeper grant the panel checks). */
1522
+ async function grantOidcClientCmd(clientId, principal, { json }) {
1523
+ let result;
1524
+ try {
1525
+ result = await panelRequest('POST', '/api/gatekeeper/grants', {
1526
+ ...principal,
1527
+ resourceType: OIDC_CLIENT_RESOURCE,
1528
+ resourceId: clientId,
1529
+ });
1530
+ } catch (err) {
1531
+ failPanel(err.message, json, err);
1532
+ }
1533
+ if (json) {
1534
+ emitComplete(result);
1535
+ return;
1536
+ }
1537
+ console.log(
1538
+ `\n ${chalk.green('✓')} ${principalText(principal)} may sign in with ${terminalText(clientId)}\n`,
1539
+ );
1540
+ }
1541
+
1542
+ /** Revoke the grant of a user-facing client to a user or a Lamaste group. */
1543
+ async function revokeOidcClientCmd(name, clientId, principal, { json }) {
1544
+ let found;
1545
+ try {
1546
+ const { clients } = await panelRequest(
1547
+ 'GET',
1548
+ `/api/plugins/${encodeURIComponent(name)}/oidc-clients`,
1549
+ );
1550
+ found = findClientGrants(clients ?? [], clientId, principal);
1551
+ } catch (err) {
1552
+ failPanel(err.message, json, err);
1553
+ }
1554
+ if ('error' in found) {
1555
+ const reason = {
1556
+ no_client: `Plugin ${name} declares no user-facing OIDC client ${clientId}`,
1557
+ unreadable: "Gatekeeper's grants could not be read — try again",
1558
+ no_grant: `${clientId} is not granted to ${principalText(principal)}`,
1559
+ }[found.error];
1560
+ failPanel(reason, json);
1561
+ }
1562
+ // Every grant naming the principal goes: one left behind would still let
1563
+ // them sign in.
1564
+ const results = [];
1565
+ try {
1566
+ for (const grantId of found.grantIds) {
1567
+ results.push(
1568
+ await panelRequest('DELETE', `/api/gatekeeper/grants/${encodeURIComponent(grantId)}`),
1569
+ );
1570
+ }
1571
+ } catch (err) {
1572
+ failPanel(err.message, json, err);
1573
+ }
1574
+ if (json) {
1575
+ emitComplete({ revoked: found.grantIds.length, results });
1576
+ return;
1577
+ }
1578
+ console.log(
1579
+ `\n ${chalk.green('✓')} ${principalText(principal)} no longer signs in with ${terminalText(clientId)}\n`,
1580
+ );
1581
+ }
1582
+
1422
1583
  /**
1423
1584
  * @param {string | undefined} name
1424
1585
  * @param {string | undefined} clientId
@@ -4,7 +4,7 @@
4
4
  * for agents.
5
5
  *
6
6
  * registry-credential show
7
- * registry-credential set --url <registry> --oidc
7
+ * registry-credential set --url <registry> --oidc [--issuer-domain <domain>]
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
@@ -35,7 +35,7 @@ export async function runRegistryCredential(args, { json }) {
35
35
  return clear({ json });
36
36
  default:
37
37
  process.stderr.write(
38
- `Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc | --token-stdin) | clear>\n`,
38
+ `Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc [--issuer-domain <domain>] | --token-stdin) | clear>\n`,
39
39
  );
40
40
  process.exit(1);
41
41
  }
@@ -68,6 +68,7 @@ async function set(args, { json }) {
68
68
  let url;
69
69
  let tokenFromStdin = false;
70
70
  let oidc = false;
71
+ let issuerDomain;
71
72
  for (let i = 0; i < args.length; i += 1) {
72
73
  const arg = args[i];
73
74
  if (arg === '--url') {
@@ -78,6 +79,8 @@ async function set(args, { json }) {
78
79
  tokenFromStdin = true;
79
80
  } else if (arg === '--oidc') {
80
81
  oidc = true;
82
+ } else if (arg === '--issuer-domain') {
83
+ issuerDomain = args[++i]?.toLowerCase();
81
84
  } else if (arg === '--token' || arg.startsWith('--token=')) {
82
85
  throw new Error(
83
86
  'The token is never accepted on the command line — set LAMASTE_REGISTRY_TOKEN or use --token-stdin',
@@ -87,13 +90,17 @@ async function set(args, { json }) {
87
90
  }
88
91
  }
89
92
  if (!url) throw new Error('registry-credential set: --url <registry> is required');
93
+ if (issuerDomain !== undefined && !oidc) {
94
+ throw new Error('registry-credential set: --issuer-domain goes with --oidc');
95
+ }
90
96
  if (oidc && tokenFromStdin) {
91
97
  throw new Error('registry-credential set: choose --oidc or --token-stdin, not both');
92
98
  }
93
99
 
94
100
  let auth;
95
101
  if (oidc) {
96
- auth = { kind: 'oidc' };
102
+ // The domain whose issuer (https://auth.<domain>) the registry trusts; the primary when absent.
103
+ auth = { kind: 'oidc', ...(issuerDomain ? { issuerDomain } : {}) };
97
104
  } else {
98
105
  let token = tokenFromStdin ? (await readStdin()).trim() : process.env.LAMASTE_REGISTRY_TOKEN;
99
106
  if (!token) {
@@ -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
@@ -56,6 +61,7 @@ ${b('Create options:')}
56
61
  --type <type> Site type: managed (default) or custom
57
62
  --custom-domain <d> Custom domain (required if type is custom)
58
63
  --aliases <a,b> Hostnames that redirect to the custom domain (e.g. www.<domain>)
64
+ --domain <domain> A managed site's base domain, an active one (default: the primary)
59
65
  --spa Enable SPA mode (single-page application routing)
60
66
  --authelia Enable Authelia protection
61
67
  `);
@@ -92,12 +98,21 @@ async function listSites({ json }) {
92
98
  const certStatus = s.certIssued ? g('cert issued') : y('no cert');
93
99
  const spaTag = s.spaMode ? d('[SPA]') : '';
94
100
  const authTag = s.autheliaProtected ? d('[protected]') : '';
101
+ const owner = siteOwnerTag(s);
102
+ const ownerTag = owner ? c(owner) : '';
95
103
 
96
104
  console.log(
97
- ` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag}`.trimEnd(),
105
+ ` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag} ${ownerTag}`.trimEnd(),
98
106
  );
99
107
  console.log(` ${d(`id: ${s.id}`)}`);
100
108
  }
109
+ if (sites.some((s) => siteOwnerTag(s) !== null)) {
110
+ console.log(
111
+ d(
112
+ '\n A [plugin …] site is the web surface of that plugin: manage it with lamaste-server plugins web-surface <plugin>',
113
+ ),
114
+ );
115
+ }
101
116
  console.log('');
102
117
  }
103
118
 
@@ -110,6 +125,7 @@ async function createSite(args, { json }) {
110
125
  const type = getArg(args, 'type') || 'managed';
111
126
  const customDomain = getArg(args, 'custom-domain') || undefined;
112
127
  const aliasArg = getArg(args, 'aliases');
128
+ const domain = getArg(args, 'domain');
113
129
  const spaMode = args.includes('--spa');
114
130
  const autheliaProtected = args.includes('--authelia');
115
131
 
@@ -138,6 +154,7 @@ async function createSite(args, { json }) {
138
154
  const body = { name, type, spaMode, autheliaProtected };
139
155
  if (customDomain) body.customDomain = customDomain;
140
156
  if (aliasArg) body.aliases = splitAliases(aliasArg);
157
+ if (domain) body.domain = domain.toLowerCase();
141
158
 
142
159
  if (!json) process.stderr.write(` Creating site ${chalk.cyan(name)}...`);
143
160
 
@@ -10,6 +10,8 @@ import { readConfigSafe, PANEL_SERVICE, GATEKEEPER_SERVICE, PKI_DIR } from '../c
10
10
  import { getMtlsCerts } from '@lamalibre/lamaste/server';
11
11
  import { exec } from '../exec.js';
12
12
  import { emit } from '../ndjson.js';
13
+ import { panelRequest } from '../panel-api.js';
14
+ import { domainRows } from '../domains.js';
13
15
 
14
16
  /**
15
17
  * @param {{ json: boolean }} options
@@ -20,6 +22,15 @@ export async function runStatus({ json }) {
20
22
 
21
23
  // Collect service statuses
22
24
  const panelStatus = await getServiceStatus(PANEL_SERVICE);
25
+ // The relay's domains, from the running panel — asked while the other
26
+ // checks run, briefly; none while it is down or not onboarded.
27
+ const domainsRequest =
28
+ domain && panelStatus.active
29
+ ? panelRequest('GET', '/api/domains', null, { timeoutMs: 4000 }).then(
30
+ (r) => r.domains ?? [],
31
+ () => null,
32
+ )
33
+ : Promise.resolve(null);
23
34
  const gatekeeperStatus = await getServiceStatus(GATEKEEPER_SERVICE);
24
35
  const nginxStatus = await getServiceStatus('nginx');
25
36
  const autheliaStatus = await getServiceStatus('authelia');
@@ -35,10 +46,12 @@ export async function runStatus({ json }) {
35
46
  // Collect system info
36
47
  const uptime = await getUptime();
37
48
  const memory = await getMemoryUsage();
49
+ const domains = await domainsRequest;
38
50
 
39
51
  if (json) {
40
52
  emit({
41
53
  domain,
54
+ domains,
42
55
  services: {
43
56
  panel: panelStatus,
44
57
  gatekeeper: gatekeeperStatus,
@@ -66,6 +79,12 @@ export async function runStatus({ json }) {
66
79
  if (domain) {
67
80
  console.log(` ${b('Domain:')} ${c(String(domain))}`);
68
81
  }
82
+ if (domains && domains.length > 1) {
83
+ console.log(` ${b('Domains:')}`);
84
+ for (const row of domainRows(domains)) {
85
+ console.log(` ${c(row.name)} ${d(`${row.state}, ${row.members} member(s)`)}`);
86
+ }
87
+ }
69
88
 
70
89
  if (uptime) {
71
90
  console.log(` ${b('Uptime:')} ${d(uptime)}`);
@@ -89,6 +89,7 @@ ${b('Create options:')}
89
89
  --description <text> Optional description
90
90
  --access-mode <mode> Access mode: public, authenticated, restricted (default: restricted)
91
91
  --max-body-mb <n> Largest request body in MiB, 1-10240 (default: 10)
92
+ --domain <domain> Base domain of the hostname, an active one (default: the primary)
92
93
  `);
93
94
  }
94
95
 
@@ -125,7 +126,7 @@ async function listTunnels({ json }) {
125
126
  const carrier = t.agentLabel ? d(`via ${t.agentLabel}`) : r('no agent');
126
127
  const bodyLimit = t.type !== 'panel' ? d(`${t.maxBodySizeMb ?? 1} MiB`) : '';
127
128
  console.log(
128
- ` ${c(t.subdomain)} \u2192 :${t.port} ${enabled} ${carrier} ${accessMode} ${bodyLimit} ${type}`.trimEnd(),
129
+ ` ${c(t.fqdn ?? t.subdomain)} \u2192 :${t.port} ${enabled} ${carrier} ${accessMode} ${bodyLimit} ${type}`.trimEnd(),
129
130
  );
130
131
  if (t.description) {
131
132
  console.log(` ${d(t.description)}`);
@@ -150,6 +151,7 @@ async function createTunnel(args, { json }) {
150
151
  const accessMode = getArg(args, 'access-mode') || undefined;
151
152
  const agentLabel = getArg(args, 'agent');
152
153
  const maxBodyStr = getArg(args, 'max-body-mb');
154
+ const domain = getArg(args, 'domain');
153
155
 
154
156
  if (!subdomain || !portStr || !agentLabel) {
155
157
  const msg = 'Error: --subdomain, --port and --agent are required';
@@ -180,6 +182,7 @@ async function createTunnel(args, { json }) {
180
182
  if (description) body.description = description;
181
183
  if (accessMode) body.accessMode = accessMode;
182
184
  if (maxBodySizeMb !== undefined) body.maxBodySizeMb = maxBodySizeMb;
185
+ if (domain) body.domain = domain.toLowerCase();
183
186
 
184
187
  if (!json) process.stderr.write(` Creating tunnel ${chalk.cyan(subdomain)} \u2192 :${port}...`);
185
188
 
@@ -31,9 +31,11 @@
31
31
 
32
32
  import chalk from 'chalk';
33
33
  import { panelRequest } from '../panel-api.js';
34
- import { emit, emitComplete, emitError } from '../ndjson.js';
34
+ import { emit, emitComplete } from '../ndjson.js';
35
+ import { fail } from '../fail.js';
35
36
  import { parseUserArgs, warningLine } from '../plugin-identity.js';
36
37
  import { formatMoment, terminalText } from '../plugin-settings.js';
38
+ import { membershipsText } from '../domains.js';
37
39
 
38
40
  /**
39
41
  * @param {string[]} args Subcommand args: ['list'] | ['disable', '<u>'] | ...
@@ -78,21 +80,6 @@ ${b('Subcommands:')}
78
80
  `);
79
81
  }
80
82
 
81
- /**
82
- * Exit with a usage error or a refused request, as text or NDJSON (with
83
- * the refusal's `code`, e.g. `last_admin`, `user_not_removed`).
84
- *
85
- * @param {string} message
86
- * @param {boolean} json
87
- * @param {{ code?: string }} [err]
88
- * @returns {never}
89
- */
90
- function fail(message, json, err = {}) {
91
- if (json) emitError(message, false, typeof err.code === 'string' ? { code: err.code } : {});
92
- else console.error(`\n ${chalk.red(terminalText(message))}\n`);
93
- process.exit(1);
94
- }
95
-
96
83
  /** Print a change's warnings: what the panel could not complete beside it. */
97
84
  function printWarnings(warnings) {
98
85
  for (const warning of warnings ?? []) {
@@ -107,7 +94,8 @@ function statusCell(status) {
107
94
 
108
95
  /**
109
96
  * The rows `users list` prints: each user's username, status, directory id
110
- * (a dash until the directory has seen them) and groups, as terminal text.
97
+ * (a dash until the directory has seen them), groups and domain memberships
98
+ * (external ones marked), as terminal text.
111
99
  * Pure, exported for tests.
112
100
  *
113
101
  * @param {Array<Record<string, any>>} users as `GET /api/users` lists them
@@ -118,6 +106,7 @@ export function userRows(users) {
118
106
  status: u.status === 'disabled' ? 'disabled' : 'active',
119
107
  userId: u.userId ? terminalText(u.userId) : '—',
120
108
  groups: terminalText((u.groups ?? []).join(', ')),
109
+ domains: membershipsText(u.domains),
121
110
  }));
122
111
  }
123
112
 
@@ -142,11 +131,11 @@ async function listUsers({ json }) {
142
131
  console.log('');
143
132
  console.log(chalk.bold(' Users'));
144
133
  console.log(
145
- chalk.dim(` ${'USERNAME'.padEnd(width)} STATUS ${'USER ID'.padEnd(36)} GROUPS`),
134
+ chalk.dim(` ${'USERNAME'.padEnd(width)} STATUS ${'USER ID'.padEnd(36)} DOMAINS GROUPS`),
146
135
  );
147
136
  for (const r of rows) {
148
137
  console.log(
149
- ` ${chalk.cyan(r.username.padEnd(width))} ${statusCell(r.status)} ${r.userId.padEnd(36)} ${chalk.dim(r.groups)}`,
138
+ ` ${chalk.cyan(r.username.padEnd(width))} ${statusCell(r.status)} ${r.userId.padEnd(36)} ${r.domains} ${chalk.dim(r.groups)}`,
150
139
  );
151
140
  }
152
141
  if (rows.some((r) => r.userId === '—')) {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The relay's domains as `lamaste-server domains` prints them — pure
3
+ * helpers, exported for tests (the command is src/commands/domains.js).
4
+ */
5
+
6
+ import { terminalText } from './plugin-settings.js';
7
+
8
+ /**
9
+ * One row per domain: name, state, members, what uses it — as terminal text.
10
+ *
11
+ * @param {Array<Record<string, any>>} domains as `GET /api/domains` lists them
12
+ */
13
+ export function domainRows(domains) {
14
+ return domains.map((d) => ({
15
+ name: terminalText(d.name),
16
+ state: d.status === 'active' ? (d.primary ? 'primary' : 'active') : 'pending',
17
+ members: Number.isInteger(d.members) ? d.members : 0,
18
+ externalMembers: Number.isInteger(d.externalMembers) ? d.externalMembers : 0,
19
+ usedBy: usageText(d.usedBy),
20
+ }));
21
+ }
22
+
23
+ /** What uses a domain, in one line (empty when nothing does). */
24
+ export function usageText(usedBy) {
25
+ if (!usedBy) return '';
26
+ const parts = [];
27
+ const count = (label, items) => {
28
+ if (Array.isArray(items) && items.length > 0) parts.push(`${items.length} ${label}`);
29
+ };
30
+ count('tunnel(s)', usedBy.tunnels);
31
+ count('site(s)', usedBy.sites);
32
+ count('plugin(s)', usedBy.plugins);
33
+ if (usedBy.registry) parts.push('the registry credential');
34
+ return parts.join(', ');
35
+ }
36
+
37
+ /** `/api/domains/<name><rest>`. */
38
+ export function domainPath(name, rest = '') {
39
+ return `/api/domains/${encodeURIComponent(name)}${rest}`;
40
+ }
41
+
42
+ /**
43
+ * A user's memberships in one line — `a.example, b.example (external)` —
44
+ * as terminal text; `—` for none. A bare name (an older panel's answer) is
45
+ * a member.
46
+ *
47
+ * @param {ReadonlyArray<string | { name: string, kind?: string }> | undefined} domains
48
+ */
49
+ export function membershipsText(domains) {
50
+ const parts = (domains ?? []).flatMap((d) => {
51
+ const name = typeof d === 'string' ? d : d?.name;
52
+ if (typeof name !== 'string') return [];
53
+ return [terminalText(d?.kind === 'external' ? `${name} (external)` : name)];
54
+ });
55
+ return parts.length > 0 ? parts.join(', ') : '—';
56
+ }
57
+
58
+ /** Whether any of `domains` is an external membership. */
59
+ export function isExternalAnywhere(domains) {
60
+ return (domains ?? []).some((d) => d?.kind === 'external');
61
+ }
62
+
63
+ /**
64
+ * What `domains members <domain> <action> <user> …` asks the panel, or
65
+ * null for a command line it does not take:
66
+ *
67
+ * - `add <user> [--external]` — a member (an external one with the flag);
68
+ * - `kind <user> member|external` — change the kind of a membership;
69
+ * - `remove <user>` — end it.
70
+ *
71
+ * `user` is a directory id or a username.
72
+ *
73
+ * @param {string} domain
74
+ * @param {string[]} rest what follows `members <domain>`
75
+ * @returns {{ method: 'PUT' | 'DELETE', path: string, body: object | null, user: string, kind: 'member' | 'external' | null } | null}
76
+ */
77
+ export function memberRequest(domain, rest) {
78
+ const [action, user, ...more] = rest;
79
+ if (!user) return null;
80
+ const path = domainPath(domain, `/members/${encodeURIComponent(user)}`);
81
+ if (action === 'remove' && more.length === 0) {
82
+ return { method: 'DELETE', path, body: null, user, kind: null };
83
+ }
84
+ if (action === 'add' && more.every((a) => a === '--external')) {
85
+ const kind = more.length > 0 ? 'external' : 'member';
86
+ return { method: 'PUT', path, body: { kind }, user, kind };
87
+ }
88
+ if (action === 'kind' && more.length === 1 && (more[0] === 'member' || more[0] === 'external')) {
89
+ return { method: 'PUT', path, body: { kind: more[0] }, user, kind: more[0] };
90
+ }
91
+ return null;
92
+ }