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