@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
|
@@ -11,9 +11,12 @@ import {
|
|
|
11
11
|
updateSite,
|
|
12
12
|
verifyDns,
|
|
13
13
|
SiteError,
|
|
14
|
+
withTunnelLock,
|
|
14
15
|
} from '@lamalibre/lamaste/server';
|
|
16
|
+
import { AppError } from '../../lib/app-error.js';
|
|
17
|
+
import { webSurfaceServing } from '../../lib/web-surface.js';
|
|
15
18
|
import { getConfig } from '../../lib/config.js';
|
|
16
|
-
import { readSites, writeSites, readTunnels } from '../../lib/state.js';
|
|
19
|
+
import { readSites, writeSites, readTunnels, withSitesLock } from '../../lib/state.js';
|
|
17
20
|
import { writeStaticSiteVhost, removeStaticSiteVhost } from '../../lib/nginx.js';
|
|
18
21
|
import { updateAccessControl } from '../../lib/authelia.js';
|
|
19
22
|
import { issueTunnelCert, getCertPath, issueSiteCert } from '../../lib/certbot.js';
|
|
@@ -134,6 +137,79 @@ function siteErrorStatus(code) {
|
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Run a site workflow that may give a site a hostname: under core's tunnel
|
|
142
|
+
* lock, then the sites lock — the order every such workflow takes them in
|
|
143
|
+
* (`withSitesLock`) — so no tunnel takes the name meanwhile.
|
|
144
|
+
*
|
|
145
|
+
* @template T
|
|
146
|
+
* @param {() => Promise<T>} fn
|
|
147
|
+
* @returns {Promise<T>}
|
|
148
|
+
*/
|
|
149
|
+
function withHostnameLocks(fn) {
|
|
150
|
+
return withTunnelLock(() => withSitesLock(fn));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Refuse (409) a site a plugin owns: its files are the plugin's approved
|
|
155
|
+
* web tree and its settings follow the plugin — it is managed through the
|
|
156
|
+
* plugin's web surface, never here (and never by `lamaste-agent deploy`).
|
|
157
|
+
*/
|
|
158
|
+
function assertNotPluginOwned(site) {
|
|
159
|
+
if (site?.owner) {
|
|
160
|
+
throw new AppError(
|
|
161
|
+
`Site '${site.name}' is the web surface of plugin '${site.owner.plugin}' — manage it through the plugin`,
|
|
162
|
+
409,
|
|
163
|
+
{ code: 'plugin_owned_site' },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The site `id`, refused when a plugin owns it; null when there is none. */
|
|
169
|
+
async function siteForManagement(id) {
|
|
170
|
+
const site = (await readSites()).find((s) => s.id === id) ?? null;
|
|
171
|
+
assertNotPluginOwned(site);
|
|
172
|
+
return site;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Record the size of site `id` on disk in sites.json, re-reading the file
|
|
177
|
+
* under the sites lock — never a copy read before an upload, which would
|
|
178
|
+
* undo every change made meanwhile.
|
|
179
|
+
*
|
|
180
|
+
* @returns {Promise<number>} the size
|
|
181
|
+
*/
|
|
182
|
+
async function recordSiteSize(id) {
|
|
183
|
+
const totalSize = await getSiteSize(id);
|
|
184
|
+
await withSitesLock(async () => {
|
|
185
|
+
const sites = await readSites();
|
|
186
|
+
const site = sites.find((s) => s.id === id);
|
|
187
|
+
if (!site) return;
|
|
188
|
+
site.totalSize = totalSize;
|
|
189
|
+
await writeSites(sites);
|
|
190
|
+
});
|
|
191
|
+
return totalSize;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sites as listed: a plugin-owned one with `webSurface: { served, reason }`
|
|
196
|
+
* — whether it serves its plugin's shell and API now, or answers 503 and why.
|
|
197
|
+
*/
|
|
198
|
+
async function withWebSurfaceState(sites) {
|
|
199
|
+
if (!sites.some((site) => site.owner)) return sites;
|
|
200
|
+
const tunnels = await readTunnels();
|
|
201
|
+
return Promise.all(
|
|
202
|
+
sites.map(async (site) => {
|
|
203
|
+
if (!site.owner) return site;
|
|
204
|
+
const serving = await webSurfaceServing(site.owner.plugin, tunnels);
|
|
205
|
+
return {
|
|
206
|
+
...site,
|
|
207
|
+
webSurface: { served: serving.served, reason: serving.served ? null : serving.reason },
|
|
208
|
+
};
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
function assertAgentSiteAccess(request, site) {
|
|
138
214
|
if (request.certRole === 'agent') {
|
|
139
215
|
const allowed = request.certAllowedSites || [];
|
|
@@ -191,7 +267,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
191
267
|
}
|
|
192
268
|
|
|
193
269
|
const total = sites.length;
|
|
194
|
-
const windowed = sites.slice(offset, offset + limit);
|
|
270
|
+
const windowed = await withWebSurfaceState(sites.slice(offset, offset + limit));
|
|
195
271
|
|
|
196
272
|
return { sites: windowed, total, limit, offset };
|
|
197
273
|
},
|
|
@@ -214,22 +290,24 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
214
290
|
}
|
|
215
291
|
|
|
216
292
|
try {
|
|
217
|
-
const result = await
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
293
|
+
const result = await withHostnameLocks(() =>
|
|
294
|
+
createSite({
|
|
295
|
+
name: body.name,
|
|
296
|
+
type: body.type,
|
|
297
|
+
customDomain: body.customDomain,
|
|
298
|
+
aliases: body.aliases,
|
|
299
|
+
spaMode: body.spaMode,
|
|
300
|
+
autheliaProtected: body.autheliaProtected,
|
|
301
|
+
domain: config.domain,
|
|
302
|
+
email: config.email,
|
|
303
|
+
nginx: nginxDeps,
|
|
304
|
+
certbot: certbotDeps,
|
|
305
|
+
files: filesDeps,
|
|
306
|
+
siteState: siteStateDeps,
|
|
307
|
+
tunnelState: tunnelReadDeps,
|
|
308
|
+
logger: request.log,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
233
311
|
|
|
234
312
|
const response = { ok: true, site: result.site };
|
|
235
313
|
if (result.message) response.message = result.message;
|
|
@@ -261,12 +339,15 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
261
339
|
const { id } = IdParamSchema.parse(request.params);
|
|
262
340
|
|
|
263
341
|
try {
|
|
264
|
-
await
|
|
265
|
-
id
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
await withSitesLock(async () => {
|
|
343
|
+
await siteForManagement(id);
|
|
344
|
+
await deleteSite({
|
|
345
|
+
id,
|
|
346
|
+
nginx: nginxDeps,
|
|
347
|
+
files: filesDeps,
|
|
348
|
+
siteState: siteStateDeps,
|
|
349
|
+
logger: request.log,
|
|
350
|
+
});
|
|
270
351
|
});
|
|
271
352
|
return { ok: true };
|
|
272
353
|
} catch (err) {
|
|
@@ -277,6 +358,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
277
358
|
details: err.message,
|
|
278
359
|
});
|
|
279
360
|
}
|
|
361
|
+
if (err instanceof AppError) throw err;
|
|
280
362
|
request.log.error(err, 'Failed to delete site');
|
|
281
363
|
return reply.code(500).send({
|
|
282
364
|
error: 'Failed to delete site',
|
|
@@ -298,21 +380,24 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
298
380
|
const config = getConfig();
|
|
299
381
|
|
|
300
382
|
try {
|
|
301
|
-
const result = await
|
|
302
|
-
id
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
383
|
+
const result = await withHostnameLocks(async () => {
|
|
384
|
+
await siteForManagement(id);
|
|
385
|
+
return updateSite({
|
|
386
|
+
id,
|
|
387
|
+
spaMode: body.spaMode,
|
|
388
|
+
autheliaProtected: body.autheliaProtected,
|
|
389
|
+
allowedUsers: body.allowedUsers,
|
|
390
|
+
aliases: body.aliases,
|
|
391
|
+
domain: config.domain,
|
|
392
|
+
email: config.email,
|
|
393
|
+
serverIp: config.ip,
|
|
394
|
+
nginx: nginxDeps,
|
|
395
|
+
certbot: certbotDeps,
|
|
396
|
+
siteState: siteStateDeps,
|
|
397
|
+
tunnelState: tunnelReadDeps,
|
|
398
|
+
authelia: autheliaDeps,
|
|
399
|
+
logger: request.log,
|
|
400
|
+
});
|
|
316
401
|
});
|
|
317
402
|
return result;
|
|
318
403
|
} catch (err) {
|
|
@@ -330,6 +415,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
330
415
|
details: err.message,
|
|
331
416
|
});
|
|
332
417
|
}
|
|
418
|
+
if (err instanceof AppError) throw err;
|
|
333
419
|
request.log.error(err, 'Failed to update site');
|
|
334
420
|
return reply.code(500).send({
|
|
335
421
|
error: 'Failed to update site configuration',
|
|
@@ -353,15 +439,18 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
353
439
|
const config = getConfig();
|
|
354
440
|
|
|
355
441
|
try {
|
|
356
|
-
const result = await
|
|
357
|
-
id
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
442
|
+
const result = await withHostnameLocks(async () => {
|
|
443
|
+
await siteForManagement(id);
|
|
444
|
+
return verifyDns({
|
|
445
|
+
id,
|
|
446
|
+
serverIp: config.ip,
|
|
447
|
+
domain: config.domain,
|
|
448
|
+
email: config.email,
|
|
449
|
+
nginx: nginxDeps,
|
|
450
|
+
certbot: certbotDeps,
|
|
451
|
+
siteState: siteStateDeps,
|
|
452
|
+
logger: request.log,
|
|
453
|
+
});
|
|
365
454
|
});
|
|
366
455
|
return result;
|
|
367
456
|
} catch (err) {
|
|
@@ -371,6 +460,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
371
460
|
error: err.message,
|
|
372
461
|
});
|
|
373
462
|
}
|
|
463
|
+
if (err instanceof AppError) throw err;
|
|
374
464
|
request.log.error(err, 'Failed to verify DNS');
|
|
375
465
|
return reply.code(500).send({
|
|
376
466
|
error: 'DNS verification failed',
|
|
@@ -390,8 +480,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
390
480
|
const { id } = IdParamSchema.parse(request.params);
|
|
391
481
|
const { path: relativePath } = PathQuerySchema.parse(request.query);
|
|
392
482
|
|
|
393
|
-
const
|
|
394
|
-
const site = sites.find((s) => s.id === id);
|
|
483
|
+
const site = await siteForManagement(id);
|
|
395
484
|
if (!site) {
|
|
396
485
|
return reply.code(404).send({ error: 'Site not found' });
|
|
397
486
|
}
|
|
@@ -417,8 +506,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
417
506
|
const { id } = IdParamSchema.parse(request.params);
|
|
418
507
|
const config = getConfig();
|
|
419
508
|
|
|
420
|
-
const
|
|
421
|
-
const site = sites.find((s) => s.id === id);
|
|
509
|
+
const site = await siteForManagement(id);
|
|
422
510
|
if (!site) {
|
|
423
511
|
return reply.code(404).send({ error: 'Site not found' });
|
|
424
512
|
}
|
|
@@ -450,13 +538,9 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
450
538
|
|
|
451
539
|
// Update site size
|
|
452
540
|
try {
|
|
453
|
-
const totalSize = await
|
|
454
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
455
|
-
sites[siteIndex].totalSize = totalSize;
|
|
456
|
-
|
|
541
|
+
const totalSize = await recordSiteSize(id);
|
|
457
542
|
if (totalSize > config.maxSiteSize) {
|
|
458
543
|
// Don't block but warn
|
|
459
|
-
await writeSites(sites);
|
|
460
544
|
return reply.code(200).send({
|
|
461
545
|
ok: true,
|
|
462
546
|
files: uploadedFiles,
|
|
@@ -464,8 +548,6 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
464
548
|
totalSize,
|
|
465
549
|
});
|
|
466
550
|
}
|
|
467
|
-
|
|
468
|
-
await writeSites(sites);
|
|
469
551
|
} catch {
|
|
470
552
|
// Non-critical — don't fail the upload
|
|
471
553
|
}
|
|
@@ -483,8 +565,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
483
565
|
async (request, reply) => {
|
|
484
566
|
const { id } = IdParamSchema.parse(request.params);
|
|
485
567
|
|
|
486
|
-
const
|
|
487
|
-
const site = sites.find((s) => s.id === id);
|
|
568
|
+
const site = await siteForManagement(id);
|
|
488
569
|
if (!site) {
|
|
489
570
|
return reply.code(404).send({ error: 'Site not found' });
|
|
490
571
|
}
|
|
@@ -501,10 +582,7 @@ export default async function sitesRoutes(fastify, _opts) {
|
|
|
501
582
|
|
|
502
583
|
// Update site size
|
|
503
584
|
try {
|
|
504
|
-
|
|
505
|
-
const siteIndex = sites.findIndex((s) => s.id === id);
|
|
506
|
-
sites[siteIndex].totalSize = totalSize;
|
|
507
|
-
await writeSites(sites);
|
|
585
|
+
await recordSiteSize(id);
|
|
508
586
|
} catch {
|
|
509
587
|
// Non-critical
|
|
510
588
|
}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* under the tunnel-state lock. Read-only endpoints and HTTP-specific auth
|
|
6
6
|
* checks remain here; checks that depend on the tunnel's current state run
|
|
7
7
|
* inside the lock through the workflows' `authorize` hook.
|
|
8
|
+
*
|
|
9
|
+
* A tunnel may serve agent-hosted plugins' user surfaces (slice 6, admin
|
|
10
|
+
* only; `lib/user-surface.js`):
|
|
11
|
+
*
|
|
12
|
+
* PUT /tunnels/:id/user-surface { route } — bind
|
|
13
|
+
* DELETE /tunnels/:id/user-surface/:route unbind
|
|
14
|
+
*
|
|
15
|
+
* Listings carry the binding (`userSurface: { routes }`) and how each bound
|
|
16
|
+
* route is served (`userSurfaceStatus`).
|
|
8
17
|
*/
|
|
9
18
|
import { z } from 'zod';
|
|
10
19
|
import {
|
|
@@ -30,6 +39,14 @@ import { buildChiselArgs } from '../../lib/chisel-args.js';
|
|
|
30
39
|
import { listAgentPluginAssignments } from '../../lib/plugin-hosting.js';
|
|
31
40
|
import { pendingAgentJobs } from '../../lib/agent-jobs.js';
|
|
32
41
|
import { recordAgentContact } from '../../lib/agent-presence.js';
|
|
42
|
+
import { actorFromRequest } from '../../lib/actor.js';
|
|
43
|
+
import {
|
|
44
|
+
auditDeletedTunnelUserSurface,
|
|
45
|
+
bindTunnelUserSurface,
|
|
46
|
+
unbindTunnelUserSurface,
|
|
47
|
+
userSurfaceDeps,
|
|
48
|
+
withUserSurfaceStatus,
|
|
49
|
+
} from '../../lib/user-surface.js';
|
|
33
50
|
import {
|
|
34
51
|
tunnelNginxDeps,
|
|
35
52
|
tunnelCertbotDeps,
|
|
@@ -39,6 +56,20 @@ import {
|
|
|
39
56
|
|
|
40
57
|
const IdParamSchema = z.object({ id: z.string().uuid() });
|
|
41
58
|
|
|
59
|
+
/** A plugin route, as a user surface is bound for it (core `isUserSurfaceRoute` decides the rest). */
|
|
60
|
+
const UserSurfaceRouteSchema = z
|
|
61
|
+
.string()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(64)
|
|
64
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'route must be a plugin route');
|
|
65
|
+
|
|
66
|
+
const UserSurfaceBodySchema = z.object({ route: UserSurfaceRouteSchema }).strict();
|
|
67
|
+
|
|
68
|
+
const UserSurfaceParamsSchema = z.object({ id: z.string().uuid(), route: UserSurfaceRouteSchema });
|
|
69
|
+
|
|
70
|
+
/** Binding and unbinding rewrite a vhost and reload nginx. */
|
|
71
|
+
const USER_SURFACE_WRITE_RATE_LIMIT = { rateLimit: { max: 30, timeWindow: '1 minute' } };
|
|
72
|
+
|
|
42
73
|
const TunnelPortSchema = z
|
|
43
74
|
.number()
|
|
44
75
|
.int('Port must be an integer')
|
|
@@ -189,10 +220,16 @@ function maxBodyCeiling(request) {
|
|
|
189
220
|
return request.certRole === 'admin' ? TUNNEL_MAX_BODY_MB.max : TUNNEL_MAX_BODY_MB.agentMax;
|
|
190
221
|
}
|
|
191
222
|
|
|
192
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Core `createTunnel`'s `hostTaken`: the static site (or one of its
|
|
225
|
+
* aliases) that already answers on `fqdn` — two vhosts with one
|
|
226
|
+
* server_name make nginx pick silently. Asked under the tunnel lock, which
|
|
227
|
+
* every workflow giving a site a hostname holds too.
|
|
228
|
+
*/
|
|
193
229
|
async function siteServing(fqdn) {
|
|
194
230
|
const sites = await readSites();
|
|
195
|
-
|
|
231
|
+
const site = sites.find((s) => s.fqdn === fqdn || (s.aliases ?? []).includes(fqdn));
|
|
232
|
+
return site ? `by a static site ('${site.name}')` : null;
|
|
196
233
|
}
|
|
197
234
|
|
|
198
235
|
/**
|
|
@@ -215,11 +252,30 @@ function tunnelErrorStatus(code) {
|
|
|
215
252
|
return 403;
|
|
216
253
|
case 'NOT_FOUND':
|
|
217
254
|
return 404;
|
|
255
|
+
case 'USER_SURFACE_CONFLICT':
|
|
256
|
+
return 409;
|
|
218
257
|
default:
|
|
219
258
|
return 500;
|
|
220
259
|
}
|
|
221
260
|
}
|
|
222
261
|
|
|
262
|
+
/** A refused or failed user-surface change: the workflow's status, or a 500. */
|
|
263
|
+
function sendUserSurfaceFailure(request, reply, err, fallback) {
|
|
264
|
+
if (err instanceof TunnelError) {
|
|
265
|
+
const status = tunnelErrorStatus(err.code);
|
|
266
|
+
return reply.code(status).send({
|
|
267
|
+
error: status === 500 ? fallback : err.message,
|
|
268
|
+
...(status === 500 ? { details: err.message } : {}),
|
|
269
|
+
...(err.code === 'USER_SURFACE_CONFLICT' ? { code: 'user_surface_conflict' } : {}),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (Number.isInteger(err.statusCode)) {
|
|
273
|
+
return reply.code(err.statusCode).send({ error: err.message });
|
|
274
|
+
}
|
|
275
|
+
request.log.error(err, fallback);
|
|
276
|
+
return reply.code(500).send({ error: fallback, details: err.message });
|
|
277
|
+
}
|
|
278
|
+
|
|
223
279
|
export default async function tunnelRoutes(fastify, _opts) {
|
|
224
280
|
const nginxDeps = tunnelNginxDeps;
|
|
225
281
|
const certbotDeps = tunnelCertbotDeps;
|
|
@@ -348,7 +404,9 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
348
404
|
});
|
|
349
405
|
|
|
350
406
|
const total = tunnels.length;
|
|
351
|
-
const windowed =
|
|
407
|
+
const windowed = await Promise.all(
|
|
408
|
+
tunnels.slice(offset, offset + limit).map((t) => withUserSurfaceStatus(t)),
|
|
409
|
+
);
|
|
352
410
|
|
|
353
411
|
return { tunnels: windowed, total, limit, offset };
|
|
354
412
|
},
|
|
@@ -444,16 +502,6 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
444
502
|
});
|
|
445
503
|
}
|
|
446
504
|
|
|
447
|
-
// A static site (or one of its aliases) may already answer on this
|
|
448
|
-
// hostname; two vhosts with one server_name make nginx pick silently.
|
|
449
|
-
const fqdn = `${subdomain}.${config.domain}`;
|
|
450
|
-
if (await siteServing(fqdn)) {
|
|
451
|
-
return reply.code(400).send({
|
|
452
|
-
error: 'Failed to create tunnel',
|
|
453
|
-
details: `Domain '${fqdn}' is already served by a static site`,
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
505
|
try {
|
|
458
506
|
const tunnel = await createTunnel({
|
|
459
507
|
subdomain,
|
|
@@ -471,6 +519,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
471
519
|
chisel: chiselDeps,
|
|
472
520
|
state: stateDeps,
|
|
473
521
|
agents: agentDeps,
|
|
522
|
+
hostTaken: siteServing,
|
|
474
523
|
logger: request.log,
|
|
475
524
|
});
|
|
476
525
|
return reply.code(201).send({ ok: true, tunnel });
|
|
@@ -547,6 +596,8 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
547
596
|
chisel: chiselDeps,
|
|
548
597
|
state: stateDeps,
|
|
549
598
|
agents: agentDeps,
|
|
599
|
+
userSurface: userSurfaceDeps,
|
|
600
|
+
hostTaken: siteServing,
|
|
550
601
|
logger: request.log,
|
|
551
602
|
authorize: authorizeFor(request, 'change'),
|
|
552
603
|
});
|
|
@@ -577,7 +628,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
577
628
|
const { id } = IdParamSchema.parse(request.params);
|
|
578
629
|
|
|
579
630
|
try {
|
|
580
|
-
await deleteTunnel({
|
|
631
|
+
const { tunnel } = await deleteTunnel({
|
|
581
632
|
id,
|
|
582
633
|
nginx: nginxDeps,
|
|
583
634
|
chisel: chiselDeps,
|
|
@@ -585,6 +636,11 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
585
636
|
logger: request.log,
|
|
586
637
|
authorize: authorizeFor(request, 'delete'),
|
|
587
638
|
});
|
|
639
|
+
// Its user-surface bindings went with it.
|
|
640
|
+
await auditDeletedTunnelUserSurface(tunnel, {
|
|
641
|
+
actor: actorFromRequest(request),
|
|
642
|
+
logger: request.log,
|
|
643
|
+
});
|
|
588
644
|
return { ok: true };
|
|
589
645
|
} catch (err) {
|
|
590
646
|
if (err instanceof TunnelError) {
|
|
@@ -603,6 +659,44 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
603
659
|
},
|
|
604
660
|
);
|
|
605
661
|
|
|
662
|
+
// PUT /api/tunnels/:id/user-surface — serve a plugin's user surface on this
|
|
663
|
+
// tunnel's host (from when its installed, approved version declares identity)
|
|
664
|
+
fastify.put(
|
|
665
|
+
'/tunnels/:id/user-surface',
|
|
666
|
+
{ preHandler: fastify.requireRole(['admin']), config: USER_SURFACE_WRITE_RATE_LIMIT },
|
|
667
|
+
async (request, reply) => {
|
|
668
|
+
const { id } = IdParamSchema.parse(request.params);
|
|
669
|
+
const { route } = UserSurfaceBodySchema.parse(request.body);
|
|
670
|
+
try {
|
|
671
|
+
const { changed, tunnel } = await bindTunnelUserSurface(id, route, {
|
|
672
|
+
actor: actorFromRequest(request),
|
|
673
|
+
logger: request.log,
|
|
674
|
+
});
|
|
675
|
+
return { ok: true, changed, tunnel };
|
|
676
|
+
} catch (err) {
|
|
677
|
+
return sendUserSurfaceFailure(request, reply, err, 'Failed to bind the user surface');
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// DELETE /api/tunnels/:id/user-surface/:route — stop serving it there
|
|
683
|
+
fastify.delete(
|
|
684
|
+
'/tunnels/:id/user-surface/:route',
|
|
685
|
+
{ preHandler: fastify.requireRole(['admin']), config: USER_SURFACE_WRITE_RATE_LIMIT },
|
|
686
|
+
async (request, reply) => {
|
|
687
|
+
const { id, route } = UserSurfaceParamsSchema.parse(request.params);
|
|
688
|
+
try {
|
|
689
|
+
const { tunnel } = await unbindTunnelUserSurface(id, route, {
|
|
690
|
+
actor: actorFromRequest(request),
|
|
691
|
+
logger: request.log,
|
|
692
|
+
});
|
|
693
|
+
return { ok: true, tunnel };
|
|
694
|
+
} catch (err) {
|
|
695
|
+
return sendUserSurfaceFailure(request, reply, err, 'Failed to unbind the user surface');
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
);
|
|
699
|
+
|
|
606
700
|
// GET /api/tunnels/agent-panel-status — check if agent has a panel tunnel
|
|
607
701
|
fastify.get(
|
|
608
702
|
'/tunnels/agent-panel-status',
|
|
@@ -652,14 +746,6 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
652
746
|
});
|
|
653
747
|
}
|
|
654
748
|
|
|
655
|
-
const fqdn = `${subdomain}.${config.domain}`;
|
|
656
|
-
if (await siteServing(fqdn)) {
|
|
657
|
-
return reply.code(400).send({
|
|
658
|
-
error: 'Failed to expose agent panel',
|
|
659
|
-
details: `Domain '${fqdn}' is already served by a static site`,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
749
|
// Check if a panel tunnel already exists for this agent
|
|
664
750
|
const existing = await readTunnels();
|
|
665
751
|
const existingPanel = existing.find((t) => t.type === 'panel' && t.subdomain === subdomain);
|
|
@@ -684,6 +770,7 @@ export default async function tunnelRoutes(fastify, _opts) {
|
|
|
684
770
|
chisel: chiselDeps,
|
|
685
771
|
state: stateDeps,
|
|
686
772
|
agents: agentDeps,
|
|
773
|
+
hostTaken: siteServing,
|
|
687
774
|
logger: request.log,
|
|
688
775
|
});
|
|
689
776
|
return reply.code(201).send({ ok: true, tunnel });
|