@lamalibre/create-lamaste 4.5.0 → 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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +34 -2
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +2 -2
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/02-guides/static-sites.md +4 -0
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +65 -7
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +112 -37
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +19 -1
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +21 -19
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +29 -10
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +103 -22
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +63 -6
- package/vendor/server/src/commands/sites.js +15 -1
- package/vendor/server/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +9 -4
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/gatekeeper-client.js +18 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/nginx.js +269 -2
- package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +128 -5
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +39 -6
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/user-surface.js +48 -27
- package/vendor/serverd/src/lib/vhost-reconcile.js +5 -3
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +11 -20
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
* Who holds which of its roles; give or take them away
|
|
60
60
|
* identity-sync <name> [--reconcile]
|
|
61
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
|
|
62
66
|
*
|
|
63
67
|
* Every subcommand goes through the running panel (`/api/plugins/*`, admin
|
|
64
68
|
* certificate): the panel owns the plugin registry (SQLite `state.db`), runs
|
|
@@ -77,6 +81,7 @@ import { panelRequest } from '../panel-api.js';
|
|
|
77
81
|
import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
|
|
78
82
|
import { configureCmd, settingsCmd } from './plugin-settings.js';
|
|
79
83
|
import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
|
|
84
|
+
import { webSurfaceCmd } from './plugin-web-surface.js';
|
|
80
85
|
import { settingsDiffLines, terminalText } from '../plugin-settings.js';
|
|
81
86
|
import {
|
|
82
87
|
APPROVE_HOOKS_USAGE,
|
|
@@ -85,6 +90,8 @@ import {
|
|
|
85
90
|
identityDiffLines,
|
|
86
91
|
parseApproveArgs,
|
|
87
92
|
pendingUpdateReasons,
|
|
93
|
+
webSurfaceBlockLines,
|
|
94
|
+
webTreeDiffLines,
|
|
88
95
|
} from '../plugin-approval.js';
|
|
89
96
|
|
|
90
97
|
/** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
|
|
@@ -152,6 +159,8 @@ export async function runPlugins(args, { json }) {
|
|
|
152
159
|
return entitlementsCmd(args.slice(1), { json });
|
|
153
160
|
case 'identity-sync':
|
|
154
161
|
return identitySyncCmd(args.slice(1), { json });
|
|
162
|
+
case 'web-surface':
|
|
163
|
+
return webSurfaceCmd(args.slice(1), { json });
|
|
155
164
|
default:
|
|
156
165
|
printPluginUsage();
|
|
157
166
|
process.exit(sub ? 1 : 0);
|
|
@@ -219,6 +228,14 @@ ${b('Subcommands:')}
|
|
|
219
228
|
Take every role away
|
|
220
229
|
${c('identity-sync <name> [--reconcile]')}
|
|
221
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)
|
|
222
239
|
`);
|
|
223
240
|
}
|
|
224
241
|
|
|
@@ -796,16 +813,25 @@ function printIdentityBlock(block, title) {
|
|
|
796
813
|
for (const line of identityBlockLines(block)) console.log(` ${line}`);
|
|
797
814
|
}
|
|
798
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
|
+
|
|
799
825
|
/**
|
|
800
826
|
* Print what a version brings that needs approval — the approval digest
|
|
801
827
|
* (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
|
|
804
|
-
* block.
|
|
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.
|
|
805
831
|
*/
|
|
806
|
-
function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
|
|
832
|
+
function printApprovalContents({ digest, relay, agentHalf, identity = null, web = null }) {
|
|
807
833
|
console.log(
|
|
808
|
-
` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block')}`,
|
|
834
|
+
` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block, no web surface')}`,
|
|
809
835
|
);
|
|
810
836
|
if (relay) {
|
|
811
837
|
console.log('');
|
|
@@ -822,6 +848,27 @@ function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
|
|
|
822
848
|
'Identity — Lamaste sends its agent half the users entitled to these roles',
|
|
823
849
|
);
|
|
824
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
|
+
}
|
|
825
872
|
}
|
|
826
873
|
|
|
827
874
|
/**
|
|
@@ -874,6 +921,7 @@ function printPendingUpdate(name, update, title) {
|
|
|
874
921
|
agentHalf: update.agentHalf,
|
|
875
922
|
});
|
|
876
923
|
printIdentityChanges(update.identity);
|
|
924
|
+
printWebTreeChanges(update.web);
|
|
877
925
|
const lines = settingsDiffLines(update.settings);
|
|
878
926
|
if (lines.length > 0) {
|
|
879
927
|
console.log('');
|
|
@@ -916,7 +964,7 @@ async function hooksCmd(name, { json }) {
|
|
|
916
964
|
console.log('');
|
|
917
965
|
if (!approval.required && !hooks.agent && !hooks.pendingUpdate) {
|
|
918
966
|
console.log(
|
|
919
|
-
` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install and no
|
|
967
|
+
` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install, no identity and no web surface.\n`,
|
|
920
968
|
);
|
|
921
969
|
return;
|
|
922
970
|
}
|
|
@@ -933,6 +981,7 @@ async function hooksCmd(name, { json }) {
|
|
|
933
981
|
relay: hooks.relay,
|
|
934
982
|
agentHalf: hooks.agentHalf,
|
|
935
983
|
identity: hooks.identity?.declared ? hooks.identity.block : null,
|
|
984
|
+
web: hooks.web ?? null,
|
|
936
985
|
});
|
|
937
986
|
console.log(
|
|
938
987
|
`\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
|
|
@@ -947,6 +996,13 @@ async function hooksCmd(name, { json }) {
|
|
|
947
996
|
),
|
|
948
997
|
);
|
|
949
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
|
+
}
|
|
950
1006
|
}
|
|
951
1007
|
if (hooks.agent) {
|
|
952
1008
|
console.log(chalk.bold(`\n Agent hooks (run by the agent half; no approval)`));
|
|
@@ -1262,6 +1318,7 @@ async function installPluginCmd(packageName, { json }) {
|
|
|
1262
1318
|
relay: result.hooks.relay,
|
|
1263
1319
|
agentHalf: result.hooks.agentHalf,
|
|
1264
1320
|
identity: result.hooks.identity?.declared ? result.hooks.identity.block : null,
|
|
1321
|
+
web: result.hooks.web ?? null,
|
|
1265
1322
|
});
|
|
1266
1323
|
if (approval.state === 'pending') {
|
|
1267
1324
|
console.log(
|
|
@@ -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
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
|
|
3
3
|
* for approval, what an identity block lets a plugin learn and how an
|
|
4
|
-
* update changes it, the
|
|
4
|
+
* update changes it, the web tree a version brings and how an update
|
|
5
|
+
* changes it, the command that approves it, and the arguments that
|
|
5
6
|
* command takes. Pure — no output — so they are tested as they are.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { terminalText } from './plugin-settings.js';
|
|
10
|
+
import { bytesText, shortDigest } from './plugin-web-surface.js';
|
|
9
11
|
|
|
10
12
|
const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
11
13
|
const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
|
|
@@ -141,6 +143,83 @@ export function identityDiffLines(diff) {
|
|
|
141
143
|
return { headline, lines };
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* What the installed version's web surface brings (the hook status's
|
|
148
|
+
* `web`: `{ declared, approved, treeSha256, surface }`) as lines — the tree
|
|
149
|
+
* the approval covers, and the site Lamaste offers for it. Empty when it
|
|
150
|
+
* declares none.
|
|
151
|
+
*
|
|
152
|
+
* @param {unknown} web
|
|
153
|
+
* @returns {string[]}
|
|
154
|
+
*/
|
|
155
|
+
export function webSurfaceBlockLines(web) {
|
|
156
|
+
if (web === null || typeof web !== 'object' || !web.declared) return [];
|
|
157
|
+
const surface = web.surface !== null && typeof web.surface === 'object' ? web.surface : null;
|
|
158
|
+
const lines = [terminalText(`tree: ${web.treeSha256 ?? 'unknown'}`)];
|
|
159
|
+
if (surface) {
|
|
160
|
+
lines.push(
|
|
161
|
+
terminalText(
|
|
162
|
+
`site: offered on ${surface.defaultSubdomain} (${surface.accessMode}), the shell from ${surface.root}/ of its server package`,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* How an update changes the plugin's web tree (`pendingUpdate.web`:
|
|
171
|
+
* `{ before: sha256 | null, after: { sha256, files, totalBytes } | null }`
|
|
172
|
+
* — part of the digest): a headline and its lines. Null when neither
|
|
173
|
+
* version declares a web surface.
|
|
174
|
+
*
|
|
175
|
+
* @param {unknown} web
|
|
176
|
+
* @returns {{ headline: string, lines: string[], changed: boolean } | null}
|
|
177
|
+
*/
|
|
178
|
+
export function webTreeDiffLines(web) {
|
|
179
|
+
if (web === null || typeof web !== 'object') return null;
|
|
180
|
+
const before = typeof web.before === 'string' ? web.before : null;
|
|
181
|
+
const after =
|
|
182
|
+
web.after !== null && typeof web.after === 'object' && typeof web.after.sha256 === 'string'
|
|
183
|
+
? web.after
|
|
184
|
+
: null;
|
|
185
|
+
if (before === null && after === null) return null;
|
|
186
|
+
const contents = (tree) => `${tree.files} file(s), ${bytesText(tree.totalBytes)}`;
|
|
187
|
+
if (after === null) {
|
|
188
|
+
return {
|
|
189
|
+
headline:
|
|
190
|
+
'no longer declares a web surface — its site answers 503 until a version that does is approved',
|
|
191
|
+
lines: [terminalText(`tree: ${shortDigest(before)} → none`)],
|
|
192
|
+
changed: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (before === null) {
|
|
196
|
+
return {
|
|
197
|
+
headline: 'declares a web surface for the first time',
|
|
198
|
+
lines: [
|
|
199
|
+
terminalText(`tree: ${after.sha256}`),
|
|
200
|
+
terminalText(`contents: ${contents(after)}`),
|
|
201
|
+
],
|
|
202
|
+
changed: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (before === after.sha256) {
|
|
206
|
+
return {
|
|
207
|
+
headline: 'keeps its web tree',
|
|
208
|
+
lines: [terminalText(`tree: ${after.sha256} (${contents(after)})`)],
|
|
209
|
+
changed: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
headline: 'changes its web tree — its site serves the new shell once approved',
|
|
214
|
+
lines: [
|
|
215
|
+
terminalText(`tree: ${shortDigest(before)} → ${shortDigest(after.sha256)}`),
|
|
216
|
+
terminalText(`new tree: ${after.sha256}`),
|
|
217
|
+
terminalText(`contents: ${contents(after)}`),
|
|
218
|
+
],
|
|
219
|
+
changed: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
144
223
|
/**
|
|
145
224
|
* The command that approves what plugin `name` brings: the installed
|
|
146
225
|
* version by its digest (`{ digest }`), or a pending update by its version
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's web surface at the terminal — the arguments of
|
|
3
|
+
* `lamaste-server plugins web-surface <name> [create|move|delete]`, the
|
|
4
|
+
* words its output is made of, and how `sites list` marks a site a plugin
|
|
5
|
+
* owns (what an approval shows about a web tree is in `plugin-approval.js`). Pure — no output, no
|
|
6
|
+
* panel — so they are tested as they are. Every string from the panel goes
|
|
7
|
+
* through `terminalText` before it is printed; `--json` carries the data as
|
|
8
|
+
* it came.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { WEB_SURFACE_ACCESS_MODES, isWebSurfaceSubdomain } from '@lamalibre/lamaste';
|
|
12
|
+
import { terminalText } from './plugin-settings.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Arguments
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** How `plugins web-surface` is called. */
|
|
19
|
+
export const WEB_SURFACE_USAGE =
|
|
20
|
+
'Usage: lamaste-server plugins web-surface <name> [create [--subdomain <label>] [--access-mode authenticated|restricted] | move --tunnel <id | subdomain | fqdn> | delete [--yes]]';
|
|
21
|
+
|
|
22
|
+
/** The flags each action takes, and whether each takes a value. */
|
|
23
|
+
const ACTION_FLAGS = {
|
|
24
|
+
status: {},
|
|
25
|
+
create: { '--subdomain': true, '--access-mode': true },
|
|
26
|
+
move: { '--tunnel': true },
|
|
27
|
+
delete: { '--yes': false },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Every flag that takes a value, whichever action it belongs to. */
|
|
31
|
+
const VALUE_FLAGS = new Set(
|
|
32
|
+
Object.values(ACTION_FLAGS).flatMap((flags) =>
|
|
33
|
+
Object.entries(flags)
|
|
34
|
+
.filter(([, takesValue]) => takesValue)
|
|
35
|
+
.map(([flag]) => flag),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The arguments of `plugins web-surface …` after the subcommand: the
|
|
41
|
+
* plugin, then an action — none (its status), `create` (with `--subdomain`
|
|
42
|
+
* and `--access-mode`, each defaulting to what the plugin declares),
|
|
43
|
+
* `move --tunnel <ref>` (a tunnel's id, subdomain or FQDN), or `delete`
|
|
44
|
+
* (`--yes` skips the typed confirmation).
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} args
|
|
47
|
+
* @returns {{ ok: true, name: string, action: 'status' }
|
|
48
|
+
* | { ok: true, name: string, action: 'create', subdomain: string | null, accessMode: string | null }
|
|
49
|
+
* | { ok: true, name: string, action: 'move', tunnel: string }
|
|
50
|
+
* | { ok: true, name: string, action: 'delete', yes: boolean }
|
|
51
|
+
* | { ok: false, error: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function parseWebSurfaceArgs(args) {
|
|
54
|
+
const usage = WEB_SURFACE_USAGE;
|
|
55
|
+
const positional = [];
|
|
56
|
+
/** @type {Map<string, string | true>} */
|
|
57
|
+
const flags = new Map();
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
if (!arg.startsWith('--')) {
|
|
61
|
+
positional.push(arg);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (flags.has(arg)) return { ok: false, error: `${arg} is given twice — ${usage}` };
|
|
65
|
+
if (!VALUE_FLAGS.has(arg)) {
|
|
66
|
+
flags.set(arg, true);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const value = args[i + 1];
|
|
70
|
+
if (value === undefined || value.startsWith('--')) {
|
|
71
|
+
return { ok: false, error: `${arg} needs a value — ${usage}` };
|
|
72
|
+
}
|
|
73
|
+
flags.set(arg, value);
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
if (positional.length === 0) return { ok: false, error: usage };
|
|
77
|
+
if (positional.length > 2) {
|
|
78
|
+
return { ok: false, error: `Unexpected argument "${positional[2]}" — ${usage}` };
|
|
79
|
+
}
|
|
80
|
+
const [name, action = 'status'] = positional;
|
|
81
|
+
if (positional.length === 2 && !['create', 'move', 'delete'].includes(action)) {
|
|
82
|
+
return { ok: false, error: `Unknown action "${action}" — ${usage}` };
|
|
83
|
+
}
|
|
84
|
+
const allowed = ACTION_FLAGS[action];
|
|
85
|
+
for (const flag of flags.keys()) {
|
|
86
|
+
if (!Object.hasOwn(allowed, flag)) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error:
|
|
90
|
+
action === 'status'
|
|
91
|
+
? `Unknown option ${flag} — ${usage}`
|
|
92
|
+
: `${action} takes no ${flag} — ${usage}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (action) {
|
|
98
|
+
case 'create': {
|
|
99
|
+
const subdomain = flags.get('--subdomain') ?? null;
|
|
100
|
+
const accessMode = flags.get('--access-mode') ?? null;
|
|
101
|
+
if (subdomain !== null && !isWebSurfaceSubdomain(subdomain)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: `"${subdomain}" cannot be a web surface's subdomain: a DNS label, not reserved (panel, auth, tunnel, www, mail, ftp, api) and not agent-*`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (accessMode !== null && !WEB_SURFACE_ACCESS_MODES.includes(accessMode)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: `--access-mode is ${WEB_SURFACE_ACCESS_MODES.join(' or ')} — a web surface is never public`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { ok: true, name, action, subdomain, accessMode };
|
|
114
|
+
}
|
|
115
|
+
case 'move': {
|
|
116
|
+
const tunnel = flags.get('--tunnel');
|
|
117
|
+
if (tunnel === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: `Name the tunnel to move: --tunnel <id | subdomain | fqdn> — ${usage}`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, name, action, tunnel };
|
|
124
|
+
}
|
|
125
|
+
case 'delete':
|
|
126
|
+
return { ok: true, name, action, yes: flags.has('--yes') };
|
|
127
|
+
default:
|
|
128
|
+
return { ok: true, name, action: 'status' };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The body of `POST /api/plugins/:name/web-surface`: only what was given —
|
|
134
|
+
* the panel falls back to what the plugin declares.
|
|
135
|
+
*
|
|
136
|
+
* @param {{ subdomain: string | null, accessMode: string | null }} parsed
|
|
137
|
+
* @returns {{ subdomain?: string, accessMode?: string }}
|
|
138
|
+
*/
|
|
139
|
+
export function createBody({ subdomain, accessMode }) {
|
|
140
|
+
return {
|
|
141
|
+
...(subdomain !== null ? { subdomain } : {}),
|
|
142
|
+
...(accessMode !== null ? { accessMode } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Output
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
/** A tree digest as the terminal shows it beside others: its first 12 hex digits. */
|
|
151
|
+
export function shortDigest(sha256) {
|
|
152
|
+
if (typeof sha256 !== 'string' || sha256.length === 0) return '—';
|
|
153
|
+
return terminalText(sha256.slice(0, 12));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A size in bytes, in words (`1.5 MiB`). */
|
|
157
|
+
export function bytesText(bytes) {
|
|
158
|
+
if (!Number.isFinite(bytes) || bytes < 0) return 'an unknown size';
|
|
159
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
160
|
+
const units = ['KiB', 'MiB', 'GiB'];
|
|
161
|
+
let value = bytes / 1024;
|
|
162
|
+
let unit = 0;
|
|
163
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
164
|
+
value /= 1024;
|
|
165
|
+
unit++;
|
|
166
|
+
}
|
|
167
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether the web surface's site serves the shell and the API, and why not
|
|
172
|
+
* — the status's `served`/`reason` (and whether it owns a site at all) in
|
|
173
|
+
* a sentence.
|
|
174
|
+
*
|
|
175
|
+
* @param {{ served?: boolean, reason?: string | null, site?: unknown }} view
|
|
176
|
+
* @param {string} name the plugin
|
|
177
|
+
* @returns {{ served: boolean, text: string }}
|
|
178
|
+
*/
|
|
179
|
+
export function webSurfaceServedText(view, name) {
|
|
180
|
+
const plugin = terminalText(name);
|
|
181
|
+
const hasSite = view?.site !== null && view?.site !== undefined;
|
|
182
|
+
if (view?.served) {
|
|
183
|
+
return hasSite
|
|
184
|
+
? { served: true, text: 'served — its site serves the shell and the API' }
|
|
185
|
+
: {
|
|
186
|
+
served: false,
|
|
187
|
+
text: 'ready to be served, but it owns no site yet — create one, or move a bound tunnel to it',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const where = hasSite ? 'its site answers 503' : 'it owns no site yet';
|
|
191
|
+
const notServed = (why) => ({ served: false, text: `${why} (${where})` });
|
|
192
|
+
switch (view?.reason) {
|
|
193
|
+
case 'not_installed':
|
|
194
|
+
return notServed('not served — the plugin is not installed');
|
|
195
|
+
case 'no_identity':
|
|
196
|
+
return notServed(
|
|
197
|
+
'not served — its installed version declares no identity, which a web surface fronts',
|
|
198
|
+
);
|
|
199
|
+
case 'no_web_surface':
|
|
200
|
+
return notServed('not served — its installed version declares no web surface');
|
|
201
|
+
case 'not_owner':
|
|
202
|
+
return notServed(
|
|
203
|
+
`not served — its site was created for another package under the same route: delete it (lamaste-server plugins web-surface ${plugin} delete --yes), then create one`,
|
|
204
|
+
);
|
|
205
|
+
case 'not_approved':
|
|
206
|
+
return notServed(
|
|
207
|
+
`not served — its installed version waits for approval: lamaste-server plugins hooks ${plugin}`,
|
|
208
|
+
);
|
|
209
|
+
case 'not_hosted':
|
|
210
|
+
return notServed(
|
|
211
|
+
`not served — it is not hosted on an agent, or its hosting is not active: lamaste-server plugins hosting ${plugin}`,
|
|
212
|
+
);
|
|
213
|
+
case 'tree_not_current':
|
|
214
|
+
return notServed(
|
|
215
|
+
'not served yet — the relay does not serve its approved web tree yet; it switches when the approved version is installed',
|
|
216
|
+
);
|
|
217
|
+
default:
|
|
218
|
+
return {
|
|
219
|
+
served: false,
|
|
220
|
+
text: `not served (${terminalText(view?.reason ?? 'unknown')}; ${where})`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The trees the relay holds for a web surface (the status's `tree`:
|
|
227
|
+
* `{ approved, current, previous }`) as `[label, value]` rows — short
|
|
228
|
+
* digests, with whether the one served is the approved one.
|
|
229
|
+
*
|
|
230
|
+
* @param {{ approved?: string | null, current?: string | null, previous?: string | null } | null | undefined} tree
|
|
231
|
+
* @returns {Array<[string, string]>}
|
|
232
|
+
*/
|
|
233
|
+
export function webTreeRows(tree) {
|
|
234
|
+
const approved = tree?.approved ?? null;
|
|
235
|
+
const current = tree?.current ?? null;
|
|
236
|
+
const previous = tree?.previous ?? null;
|
|
237
|
+
const serving =
|
|
238
|
+
current === null
|
|
239
|
+
? 'none'
|
|
240
|
+
: approved !== null && current === approved
|
|
241
|
+
? `${shortDigest(current)} (the approved tree)`
|
|
242
|
+
: `${shortDigest(current)} (not the approved tree)`;
|
|
243
|
+
return [
|
|
244
|
+
['approved tree', approved === null ? 'none' : shortDigest(approved)],
|
|
245
|
+
['serving', serving],
|
|
246
|
+
[
|
|
247
|
+
'previous',
|
|
248
|
+
previous === null ? 'none' : `${shortDigest(previous)} (fallback for pages still open)`,
|
|
249
|
+
],
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** What each refusal of the web-surface routes asks of the administrator, beside its message. */
|
|
254
|
+
const REFUSAL_HINTS = {
|
|
255
|
+
no_web_surface: () =>
|
|
256
|
+
'Its installed version declares no userSurface.web; update to a version that does.',
|
|
257
|
+
no_identity: () =>
|
|
258
|
+
'A web surface is the front of its user surface, which needs an identity block.',
|
|
259
|
+
exists: (name) => `It owns a site already: lamaste-server plugins web-surface ${name}`,
|
|
260
|
+
bound_to_tunnel: (name) =>
|
|
261
|
+
`Move that tunnel's host to it instead: lamaste-server plugins web-surface ${name} move --tunnel <id | fqdn>`,
|
|
262
|
+
hostname_taken: (name) =>
|
|
263
|
+
`Choose another subdomain: lamaste-server plugins web-surface ${name} create --subdomain <label>`,
|
|
264
|
+
not_bound: () =>
|
|
265
|
+
'Only a tunnel bound as its user surface moves (lamaste-server tunnels bind-user-surface).',
|
|
266
|
+
shared_tunnel: () =>
|
|
267
|
+
'Unbind the other plugins first: lamaste-server tunnels unbind-user-surface <tunnel> <plugin>',
|
|
268
|
+
tunnel_disabled: () => 'Enable the tunnel first: lamaste-server tunnels toggle <id> --enable',
|
|
269
|
+
not_hosted: (name) =>
|
|
270
|
+
`The agent carrying the tunnel must host it: lamaste-server plugins hosting ${name}`,
|
|
271
|
+
not_approved: (name) =>
|
|
272
|
+
`Approve its installed version first: lamaste-server plugins hooks ${name}`,
|
|
273
|
+
web_tree: () =>
|
|
274
|
+
"The approved web tree could not be put in place; nothing changed. The panel's log names the file.",
|
|
275
|
+
gatekeeper: () => 'Gatekeeper could not be reached; nothing changed. Try again once it runs.',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The next step a refusal (`code`) of the web-surface routes points to, or
|
|
280
|
+
* null when its message says all there is.
|
|
281
|
+
*
|
|
282
|
+
* @param {unknown} code
|
|
283
|
+
* @param {string} name the plugin
|
|
284
|
+
* @returns {string | null}
|
|
285
|
+
*/
|
|
286
|
+
export function webSurfaceRefusalHint(code, name) {
|
|
287
|
+
if (typeof code !== 'string' || !Object.hasOwn(REFUSAL_HINTS, code)) return null;
|
|
288
|
+
return terminalText(REFUSAL_HINTS[code](terminalText(name)));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Sites
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* How `sites list` marks a site: `[plugin <route>, <access mode>]` for a
|
|
297
|
+
* site a plugin owns as its web surface, null for any other.
|
|
298
|
+
*
|
|
299
|
+
* @param {Record<string, any>} site
|
|
300
|
+
* @returns {string | null}
|
|
301
|
+
*/
|
|
302
|
+
export function siteOwnerTag(site) {
|
|
303
|
+
const owner = site?.owner;
|
|
304
|
+
if (owner === null || typeof owner !== 'object' || typeof owner.plugin !== 'string') return null;
|
|
305
|
+
const mode = typeof site.accessMode === 'string' ? `, ${site.accessMode}` : '';
|
|
306
|
+
return terminalText(`[plugin ${owner.plugin}${mode}]`);
|
|
307
|
+
}
|