@lamalibre/create-lamaste 4.4.1 → 4.5.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 (149) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +200 -16
  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/03-architecture/installer-flow.md +2 -2
  28. package/vendor/docs/03-architecture/installer.md +4 -4
  29. package/vendor/docs/03-architecture/management-flow.md +3 -1
  30. package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
  31. package/vendor/docs/03-architecture/overview.md +13 -17
  32. package/vendor/docs/03-architecture/panel-client.md +6 -6
  33. package/vendor/docs/03-architecture/panel-server.md +3 -2
  34. package/vendor/docs/03-architecture/state-management.md +4 -8
  35. package/vendor/docs/03-architecture/system-overview.md +1 -1
  36. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +291 -22
  37. package/vendor/docs/04-api-reference/app-host.md +3 -3
  38. package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
  39. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  40. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  41. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  42. package/vendor/docs/04-api-reference/tunnels.md +67 -29
  43. package/vendor/docs/04-api-reference/users.md +241 -45
  44. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  45. package/vendor/docs/05-operations/monitoring.md +39 -0
  46. package/vendor/docs/05-operations/uninstalling.md +2 -1
  47. package/vendor/docs/05-operations/upgrades.md +15 -6
  48. package/vendor/docs/06-reference/config-files.md +82 -188
  49. package/vendor/docs/06-reference/glossary.md +3 -3
  50. package/vendor/docs/06-reference/installer-flags.md +13 -13
  51. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  52. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  53. package/vendor/docs/gatekeeper-design.md +10 -8
  54. package/vendor/docs/identity-directory-design.md +48 -14
  55. package/vendor/docs/package.json +1 -1
  56. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  57. package/vendor/gatekeeper/dist/index.d.ts +2 -1
  58. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  59. package/vendor/gatekeeper/dist/index.js +2 -0
  60. package/vendor/gatekeeper/dist/index.js.map +1 -1
  61. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  62. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  63. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  64. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  65. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  66. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  68. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  69. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  70. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  71. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  72. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  73. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
  74. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  75. package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
  76. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  77. package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
  78. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  79. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  80. package/vendor/gatekeeper/dist/server/index.js +22 -48
  81. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  82. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  83. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  84. package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
  85. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  86. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  87. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  88. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  89. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  90. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  91. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  92. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  93. package/vendor/gatekeeper/package.json +2 -2
  94. package/vendor/server/bin/lamaste-server.js +15 -1
  95. package/vendor/server/package.json +2 -2
  96. package/vendor/server/src/commands/plugin-identity.js +335 -0
  97. package/vendor/server/src/commands/plugins.js +99 -11
  98. package/vendor/server/src/commands/registry-credential.js +2 -1
  99. package/vendor/server/src/commands/tunnels.js +102 -2
  100. package/vendor/server/src/commands/uninstall.js +3 -1
  101. package/vendor/server/src/commands/users.js +358 -0
  102. package/vendor/server/src/plugin-approval.js +107 -5
  103. package/vendor/server/src/plugin-identity.js +382 -0
  104. package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
  105. package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +84 -0
  106. package/vendor/server-ui/dist/index.html +2 -2
  107. package/vendor/serverd/package.json +2 -2
  108. package/vendor/serverd/src/index.js +25 -1
  109. package/vendor/serverd/src/lib/audience.js +576 -0
  110. package/vendor/serverd/src/lib/authelia.js +78 -7
  111. package/vendor/serverd/src/lib/config.js +68 -43
  112. package/vendor/serverd/src/lib/directory.js +1113 -0
  113. package/vendor/serverd/src/lib/entitlements.js +517 -0
  114. package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
  115. package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
  116. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  117. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  118. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  119. package/vendor/serverd/src/lib/mtls.js +6 -2
  120. package/vendor/serverd/src/lib/nginx.js +279 -48
  121. package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
  122. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  123. package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
  124. package/vendor/serverd/src/lib/plugins.js +59 -4
  125. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  126. package/vendor/serverd/src/lib/request-logging.js +19 -0
  127. package/vendor/serverd/src/lib/retained-data.js +50 -4
  128. package/vendor/serverd/src/lib/state.js +18 -0
  129. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  130. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  131. package/vendor/serverd/src/lib/user-caller.js +67 -0
  132. package/vendor/serverd/src/lib/user-surface.js +733 -0
  133. package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
  134. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  135. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  136. package/vendor/serverd/src/routes/invite.js +53 -12
  137. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  138. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  139. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  140. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  141. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  142. package/vendor/serverd/src/routes/management/tunnels.js +98 -2
  143. package/vendor/serverd/src/routes/management/users.js +290 -208
  144. package/vendor/serverd/src/routes/management.js +4 -0
  145. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  146. package/vendor/serverd/src/routes/user-access.js +17 -4
  147. package/vendor/serverd/src/routes/user-surface.js +69 -0
  148. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  149. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -52,6 +52,13 @@
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
55
62
  *
56
63
  * Every subcommand goes through the running panel (`/api/plugins/*`, admin
57
64
  * certificate): the panel owns the plugin registry (SQLite `state.db`), runs
@@ -69,10 +76,13 @@ import { PANEL_SERVICE } from '../config.js';
69
76
  import { panelRequest } from '../panel-api.js';
70
77
  import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
71
78
  import { configureCmd, settingsCmd } from './plugin-settings.js';
79
+ import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
72
80
  import { settingsDiffLines, terminalText } from '../plugin-settings.js';
73
81
  import {
74
82
  APPROVE_HOOKS_USAGE,
75
83
  approveHooksCommand,
84
+ identityBlockLines,
85
+ identityDiffLines,
76
86
  parseApproveArgs,
77
87
  pendingUpdateReasons,
78
88
  } from '../plugin-approval.js';
@@ -134,6 +144,14 @@ export async function runPlugins(args, { json }) {
134
144
  return settingsCmd(args.slice(1), { json });
135
145
  case 'configure':
136
146
  return configureCmd(args.slice(1), { json });
147
+ case 'identity':
148
+ return identityCmd(args[1], { json });
149
+ case 'audience':
150
+ return audienceCmd(args[1], { json });
151
+ case 'entitlements':
152
+ return entitlementsCmd(args.slice(1), { json });
153
+ case 'identity-sync':
154
+ return identitySyncCmd(args.slice(1), { json });
137
155
  default:
138
156
  printPluginUsage();
139
157
  process.exit(sub ? 1 : 0);
@@ -191,6 +209,16 @@ ${b('Subcommands:')}
191
209
  --clear puts a field back to its default or clears a secret,
192
210
  --secrets-stdin reads {"key": "value" | null} from stdin;
193
211
  in a terminal without flags, asked field by field
212
+ ${c('identity <name>')} People of a plugin that declares identity: its roles and attributes,
213
+ approval, whether users reach it (and why not), the feed, erasures
214
+ ${c('audience <name>')} Its members and the users who left, as its agent half is told
215
+ ${c('entitlements <name>')} Who holds which of its roles (Gatekeeper grants with roles)
216
+ ${c('entitlements <name> --user <u> | --group <g> --roles a,b')}
217
+ Give a user or a Lamaste group exactly these roles
218
+ ${c('entitlements <name> --revoke --user <u> | --group <g>')}
219
+ Take every role away
220
+ ${c('identity-sync <name> [--reconcile]')}
221
+ Sync its audience to its agent half now; --reconcile replays all of it
194
222
  `);
195
223
  }
196
224
 
@@ -725,9 +753,11 @@ function printRelayHookSet(relay, title) {
725
753
  ` request: ${hook.stage} ${who} on ${hook.routes.join(', ')}${failOpen}` +
726
754
  d(` (body ${hook.bodyMaxBytes ?? 0} B, ${hook.timeoutMs} ms, onError ${hook.onError})`),
727
755
  );
728
- if (hook.inactiveRoutes.length > 0) {
756
+ if (hook.userSurfaceRoutes.length > 0) {
729
757
  console.log(
730
- d(` not active until the user surface: ${hook.inactiveRoutes.join(', ')}`),
758
+ d(
759
+ ` runs on user-surface requests (plugins that declare identity): ${hook.userSurfaceRoutes.join(', ')}`,
760
+ ),
731
761
  );
732
762
  }
733
763
  }
@@ -757,15 +787,25 @@ function printAgentHalf(agentHalf, title) {
757
787
  );
758
788
  }
759
789
 
790
+ /**
791
+ * Print an identity block — the roles a plugin's users hold and what it
792
+ * receives about each member.
793
+ */
794
+ function printIdentityBlock(block, title) {
795
+ console.log(chalk.bold(` ${title}`));
796
+ for (const line of identityBlockLines(block)) console.log(` ${line}`);
797
+ }
798
+
760
799
  /**
761
800
  * Print what a version brings that needs approval — the approval digest
762
801
  * (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.
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.
765
805
  */
766
- function printApprovalContents({ digest, relay, agentHalf }) {
806
+ function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
767
807
  console.log(
768
- ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs')}`,
808
+ ` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block')}`,
769
809
  );
770
810
  if (relay) {
771
811
  console.log('');
@@ -775,12 +815,45 @@ function printApprovalContents({ digest, relay, agentHalf }) {
775
815
  console.log('');
776
816
  printAgentHalf(agentHalf, 'Agent half — Lamaste installs it on the hosting agent');
777
817
  }
818
+ if (identity) {
819
+ console.log('');
820
+ printIdentityBlock(
821
+ identity,
822
+ 'Identity — Lamaste sends its agent half the users entitled to these roles',
823
+ );
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Print how an update changes the plugin's identity block (part of the
829
+ * digest): roles added or removed and attributes widened wait for an
830
+ * administrator whatever auto-approve says; the block it brings.
831
+ */
832
+ function printIdentityChanges(diff) {
833
+ const shown = identityDiffLines(diff);
834
+ if (!shown) {
835
+ // Declared before and after, unchanged: still part of what is approved.
836
+ if (diff?.after) {
837
+ console.log('');
838
+ printIdentityBlock(diff.after, 'Identity — unchanged');
839
+ }
840
+ return;
841
+ }
842
+ console.log('');
843
+ console.log(chalk.bold(` Identity — this version ${shown.headline}`));
844
+ for (const line of shown.lines) {
845
+ console.log(` ${line.approval ? chalk.yellow(line.text) : line.text}`);
846
+ }
847
+ if (diff.after) {
848
+ for (const line of identityBlockLines(diff.after)) console.log(chalk.dim(` ${line}`));
849
+ }
778
850
  }
779
851
 
780
852
  /**
781
853
  * 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
854
+ * (`approvalRequired` — a secret turned plain in red, an identity change
855
+ * with its note), what it brings (its digest, relay hook set and agent
856
+ * half), how it changes the identity block, how it changes the settings
784
857
  * schema (outside the digest) — fields added and removed with their types,
785
858
  * retyped ones, newly required ones, and a warning for secrets that become
786
859
  * plain values — what auto-approve could not cover beyond the reasons, and
@@ -791,10 +864,16 @@ function printPendingUpdate(name, update, title) {
791
864
  console.log(chalk.bold(` ${title}`));
792
865
  const { required, notCovered } = pendingUpdateReasons(update);
793
866
  for (const line of required) {
794
- console.log(line.warning ? chalk.red(` - ${line.text}`) : ` - ${line.text}`);
867
+ const text = line.warning ? chalk.red(` - ${line.text}`) : ` - ${line.text}`;
868
+ console.log(line.note ? `${text}${chalk.dim(` (${line.note})`)}` : text);
795
869
  }
796
870
  console.log('');
797
- printApprovalContents(update);
871
+ printApprovalContents({
872
+ digest: update.digest,
873
+ relay: update.relay,
874
+ agentHalf: update.agentHalf,
875
+ });
876
+ printIdentityChanges(update.identity);
798
877
  const lines = settingsDiffLines(update.settings);
799
878
  if (lines.length > 0) {
800
879
  console.log('');
@@ -837,7 +916,7 @@ async function hooksCmd(name, { json }) {
837
916
  console.log('');
838
917
  if (!approval.required && !hooks.agent && !hooks.pendingUpdate) {
839
918
  console.log(
840
- ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks and no agent half for Lamaste to install.\n`,
919
+ ` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install and no identity.\n`,
841
920
  );
842
921
  return;
843
922
  }
@@ -853,6 +932,7 @@ async function hooksCmd(name, { json }) {
853
932
  digest: approval.digest,
854
933
  relay: hooks.relay,
855
934
  agentHalf: hooks.agentHalf,
935
+ identity: hooks.identity?.declared ? hooks.identity.block : null,
856
936
  });
857
937
  console.log(
858
938
  `\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
@@ -860,6 +940,13 @@ async function hooksCmd(name, { json }) {
860
940
  if (approval.state !== 'approved') {
861
941
  console.log(`\n Approve with: ${approveCommand(name, { digest: approval.digest })}`);
862
942
  }
943
+ if (hooks.identity?.declared) {
944
+ console.log(
945
+ chalk.dim(
946
+ ` Its people (entitlements, audience, feed): lamaste-server plugins identity ${terminalText(name)}`,
947
+ ),
948
+ );
949
+ }
863
950
  }
864
951
  if (hooks.agent) {
865
952
  console.log(chalk.bold(`\n Agent hooks (run by the agent half; no approval)`));
@@ -1174,6 +1261,7 @@ async function installPluginCmd(packageName, { json }) {
1174
1261
  digest: approval.digest,
1175
1262
  relay: result.hooks.relay,
1176
1263
  agentHalf: result.hooks.agentHalf,
1264
+ identity: result.hooks.identity?.declared ? result.hooks.identity.block : null,
1177
1265
  });
1178
1266
  if (approval.state === 'pending') {
1179
1267
  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.
@@ -9,16 +9,28 @@
9
9
  * assign <id> --agent <label> Move a tunnel to another agent
10
10
  * configure <id> [--access-mode <m>] [--max-body-mb <n>]
11
11
  * Change access mode / body limit
12
+ * bind-user-surface <tunnel> <plugin> Serve an agent-hosted plugin's user
13
+ * surface (/api/<route>/app/*) on the
14
+ * tunnel's host
15
+ * unbind-user-surface <tunnel> <plugin> Stop serving it there
12
16
  *
13
17
  * List reads directly from the state file.
14
- * Create/delete/toggle use the panel REST API (which has the full
15
- * nginx/certbot/chisel dependency stack).
18
+ * Create/delete/toggle and the user-surface binding use the panel REST API
19
+ * (which has the full nginx/certbot/chisel dependency stack); a tunnel is
20
+ * named by its id, subdomain or FQDN there, a plugin by its name or route.
16
21
  */
17
22
 
18
23
  import chalk from 'chalk';
19
24
  import { readTunnels } from '../state.js';
20
25
  import { panelRequest } from '../panel-api.js';
21
26
  import { emit, emitError, emitComplete } from '../ndjson.js';
27
+ import {
28
+ findHostTunnel,
29
+ findPluginByNameOrRoute,
30
+ parseUserSurfaceArgs,
31
+ userSurfaceStatusText,
32
+ } from '../plugin-identity.js';
33
+ import { terminalText } from '../plugin-settings.js';
22
34
 
23
35
  /**
24
36
  * @param {string[]} args
@@ -40,6 +52,9 @@ export async function runTunnels(args, { json }) {
40
52
  return assignTunnel(args.slice(1), { json });
41
53
  case 'configure':
42
54
  return configureTunnel(args.slice(1), { json });
55
+ case 'bind-user-surface':
56
+ case 'unbind-user-surface':
57
+ return userSurfaceCmd(sub, args.slice(1), { json });
43
58
  default:
44
59
  printTunnelUsage();
45
60
  process.exit(sub ? 1 : 0);
@@ -59,6 +74,13 @@ ${b('Subcommands:')}
59
74
  ${c('toggle')} <id> --enable|--disable Enable or disable a tunnel
60
75
  ${c('assign')} <id> --agent <label> Move a tunnel to another agent
61
76
  ${c('configure')} <id> [--access-mode <m>] [--max-body-mb <n>] Change access mode / body limit
77
+ ${c('bind-user-surface')} <tunnel> <plugin> Serve an agent-hosted plugin's user surface
78
+ (/api/<route>/app/*) on the tunnel's host
79
+ ${c('unbind-user-surface')} <tunnel> <plugin> Stop serving it there
80
+
81
+ A tunnel is named by its id, subdomain or FQDN; a plugin by its name or route. A user
82
+ surface needs an enabled tunnel that is authenticated or restricted, carried by the
83
+ agent that hosts the plugin — it is served only while that agent carries the tunnel.
62
84
 
63
85
  ${b('Create options:')}
64
86
  --subdomain <name> Tunnel subdomain (required)
@@ -108,6 +130,10 @@ async function listTunnels({ json }) {
108
130
  if (t.description) {
109
131
  console.log(` ${d(t.description)}`);
110
132
  }
133
+ const userSurface = Array.isArray(t.userSurface?.routes) ? t.userSurface.routes : [];
134
+ if (userSurface.length > 0) {
135
+ console.log(` ${d('user surface of:')} ${terminalText(userSurface.join(', '))}`);
136
+ }
111
137
  console.log(` ${d(`id: ${t.id} fqdn: ${t.fqdn}`)}`);
112
138
  }
113
139
  console.log('');
@@ -363,3 +389,77 @@ function getArg(args, name) {
363
389
  if (idx === -1 || idx + 1 >= args.length) return null;
364
390
  return args[idx + 1];
365
391
  }
392
+
393
+ /**
394
+ * `bind-user-surface|unbind-user-surface <tunnel> <plugin>`: the tunnel's
395
+ * host serves (or stops serving) the plugin's `/api/<route>/app/*` — from
396
+ * when its installed, approved version declares identity; until then its
397
+ * requests go down the tunnel. Prints how each route bound to the tunnel is
398
+ * served afterwards.
399
+ *
400
+ * @param {'bind-user-surface' | 'unbind-user-surface'} sub
401
+ * @param {string[]} args
402
+ * @param {{ json: boolean }} options
403
+ */
404
+ async function userSurfaceCmd(sub, args, { json }) {
405
+ const failWith = (message, code) => {
406
+ if (json) emitError(message, false, code ? { code } : {});
407
+ else console.error(`\n ${chalk.red(terminalText(message))}\n`);
408
+ process.exit(1);
409
+ };
410
+ const parsed = parseUserSurfaceArgs(sub, args);
411
+ if (!parsed.ok) failWith(parsed.error);
412
+
413
+ const found = findHostTunnel(await readTunnels(), parsed.tunnel);
414
+ if (found.error) failWith(found.error);
415
+ const { tunnel } = found;
416
+
417
+ let plugins;
418
+ try {
419
+ ({ plugins = [] } = await panelRequest('GET', '/api/plugins'));
420
+ } catch (err) {
421
+ failWith(err.message, err.code);
422
+ }
423
+ const named = findPluginByNameOrRoute(plugins, parsed.plugin);
424
+ if (named.error) failWith(named.error);
425
+ const { route } = named.plugin;
426
+
427
+ const bind = sub === 'bind-user-surface';
428
+ const surfacePath = `/api/tunnels/${encodeURIComponent(tunnel.id)}/user-surface`;
429
+ let result;
430
+ try {
431
+ result = bind
432
+ ? await panelRequest('PUT', surfacePath, { route })
433
+ : await panelRequest('DELETE', `${surfacePath}/${encodeURIComponent(route)}`);
434
+ } catch (err) {
435
+ failWith(err.message, err.code);
436
+ }
437
+ if (json) {
438
+ emitComplete({ id: tunnel.id, route, ...result });
439
+ return;
440
+ }
441
+ const host = terminalText(result.tunnel?.fqdn ?? tunnel.fqdn);
442
+ if (bind) {
443
+ console.log(
444
+ result.changed
445
+ ? `\n ${chalk.green('✓')} ${chalk.cyan(host)} serves the user surface of ${chalk.cyan(route)}`
446
+ : `\n ${chalk.cyan(host)} already serves the user surface of ${chalk.cyan(route)}`,
447
+ );
448
+ } else {
449
+ console.log(
450
+ `\n ${chalk.green('✓')} ${chalk.cyan(host)} no longer serves the user surface of ${chalk.cyan(route)} — its requests go down the tunnel`,
451
+ );
452
+ }
453
+ const statuses = Array.isArray(result.tunnel?.userSurfaceStatus)
454
+ ? result.tunnel.userSurfaceStatus
455
+ : [];
456
+ for (const status of statuses) {
457
+ const served = status.served === true;
458
+ const word = userSurfaceStatusText(status);
459
+ console.log(
460
+ ` ${terminalText(status.route)}: ${served ? chalk.green(word) : chalk.yellow(word)}` +
461
+ (served ? chalk.dim(` https://${host}/api/${terminalText(status.route)}/app/`) : ''),
462
+ );
463
+ }
464
+ console.log('');
465
+ }
@@ -190,11 +190,13 @@ async function removeServices() {
190
190
  * @param {Record<string, unknown> | null} _config
191
191
  */
192
192
  async function removeNginxConfigs(_config) {
193
- // Remove lamaste-specific sites
193
+ // Remove lamaste-specific sites, then the snippets and http-level maps
194
+ // they use (the sites go first: a site naming a removed map fails nginx)
194
195
  const patterns = [
195
196
  '/etc/nginx/sites-enabled/lamalibre-lamaste-*',
196
197
  '/etc/nginx/sites-available/lamalibre-lamaste-*',
197
198
  '/etc/nginx/snippets/lamalibre-lamaste-*',
199
+ '/etc/nginx/conf.d/lamalibre-lamaste-*',
198
200
  ];
199
201
 
200
202
  for (const pattern of patterns) {