@lamalibre/create-lamaste 4.0.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/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { readFile, writeFile, rename, open, mkdir } from 'node:fs/promises';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { getSystemStats } from '../../lib/system-stats.js';
|
|
7
|
+
import { getConfig } from '../../lib/config.js';
|
|
8
|
+
import { setPluginCapabilities } from '../../lib/mtls.js';
|
|
9
|
+
import { getPluginCapabilities } from '../../lib/plugins.js';
|
|
10
|
+
import { PLUGIN_CAPABILITY_REGEX } from '@lamalibre/lamaste';
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
/** Root helper that starts create-lamaste in a transient unit (see lamaste-priv). */
|
|
15
|
+
const PRIV = '/usr/local/sbin/lamaste-priv';
|
|
16
|
+
|
|
17
|
+
// Persist agent-reported capabilities so they survive server restarts
|
|
18
|
+
const AGENT_CAPS_FILE = () => join(getConfig().dataDir, 'agent-plugin-caps.json');
|
|
19
|
+
|
|
20
|
+
async function loadAgentReportedCaps() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(AGENT_CAPS_FILE(), 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
return Array.isArray(parsed.capabilities) ? parsed.capabilities : [];
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err.code === 'ENOENT') return [];
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function saveAgentReportedCaps(caps) {
|
|
32
|
+
const filePath = AGENT_CAPS_FILE();
|
|
33
|
+
const tmpPath = `${filePath}.tmp`;
|
|
34
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
35
|
+
const content = JSON.stringify({ capabilities: caps }, null, 2) + '\n';
|
|
36
|
+
await writeFile(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
37
|
+
const fd = await open(tmpPath, 'r');
|
|
38
|
+
await fd.sync();
|
|
39
|
+
await fd.close();
|
|
40
|
+
await rename(tmpPath, filePath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const UpdateBodySchema = z.object({
|
|
44
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver (e.g. 1.0.43)'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default async function systemRoutes(fastify, _opts) {
|
|
48
|
+
// Load agent-reported capabilities from disk on startup
|
|
49
|
+
try {
|
|
50
|
+
const persistedCaps = await loadAgentReportedCaps();
|
|
51
|
+
if (persistedCaps.length > 0) {
|
|
52
|
+
const serverCaps = await getPluginCapabilities();
|
|
53
|
+
const merged = [...new Set([...serverCaps, ...persistedCaps])];
|
|
54
|
+
setPluginCapabilities(merged);
|
|
55
|
+
fastify.log.info(
|
|
56
|
+
{ count: persistedCaps.length },
|
|
57
|
+
'Loaded persisted agent plugin capabilities',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
fastify.log.warn({ err: err.message }, 'Failed to load agent-reported capabilities');
|
|
62
|
+
}
|
|
63
|
+
fastify.get(
|
|
64
|
+
'/system/stats',
|
|
65
|
+
{
|
|
66
|
+
preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'system:read' }),
|
|
67
|
+
},
|
|
68
|
+
async (request, reply) => {
|
|
69
|
+
try {
|
|
70
|
+
const stats = await getSystemStats(request.log);
|
|
71
|
+
return stats;
|
|
72
|
+
} catch {
|
|
73
|
+
return reply.code(500).send({ error: 'Failed to retrieve system stats' });
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ------------------------------------------------------------------
|
|
79
|
+
// POST /system/update — trigger a panel server update
|
|
80
|
+
//
|
|
81
|
+
// The root helper `lamaste-priv self-update <version>` starts
|
|
82
|
+
// `create-lamaste@<version> --yes` (the redeploy path) in a transient
|
|
83
|
+
// systemd unit, two seconds from now. A transient unit is critical: the
|
|
84
|
+
// panel runs as the lamalibre-lamaste-serverd service, and systemd's
|
|
85
|
+
// default KillMode (control-group) kills ALL processes in the cgroup
|
|
86
|
+
// when the installer stops the service. The helper takes only the
|
|
87
|
+
// version — the panel never supplies a script for root to run.
|
|
88
|
+
//
|
|
89
|
+
// Returns 202 immediately — caller should poll /api/health until
|
|
90
|
+
// the server comes back with the new version.
|
|
91
|
+
// ------------------------------------------------------------------
|
|
92
|
+
fastify.post(
|
|
93
|
+
'/system/update',
|
|
94
|
+
{
|
|
95
|
+
preHandler: fastify.requireRole(['admin']),
|
|
96
|
+
// Moderate tier — starts a privileged update via lamaste-priv;
|
|
97
|
+
// the global 100/min default is too generous for an operation that
|
|
98
|
+
// restarts the panel daemon.
|
|
99
|
+
config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
|
|
100
|
+
},
|
|
101
|
+
async (request, reply) => {
|
|
102
|
+
const body = UpdateBodySchema.parse(request.body);
|
|
103
|
+
const { version } = body;
|
|
104
|
+
|
|
105
|
+
let unitName;
|
|
106
|
+
try {
|
|
107
|
+
const { stdout } = await execFileAsync('sudo', [PRIV, 'self-update', version], {
|
|
108
|
+
timeout: 30_000,
|
|
109
|
+
});
|
|
110
|
+
unitName = stdout.trim();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
request.log.error({ err, version }, 'Failed to launch update unit');
|
|
113
|
+
return reply.code(500).send({
|
|
114
|
+
error: `Could not start the update: ${err.stderr?.trim() || err.message}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
request.log.info({ version, unit: unitName }, 'Panel update initiated via systemd-run');
|
|
119
|
+
|
|
120
|
+
return reply.code(202).send({
|
|
121
|
+
ok: true,
|
|
122
|
+
message: `Update to create-lamaste@${version} initiated. The panel will restart shortly.`,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// ------------------------------------------------------------------
|
|
128
|
+
// POST /agents/plugins/report — accept agent plugin capability report
|
|
129
|
+
//
|
|
130
|
+
// Agents report their enabled plugins during `lamaste-agent update`.
|
|
131
|
+
// The server merges the reported plugin capabilities into the valid
|
|
132
|
+
// capabilities set so they can be assigned to agents.
|
|
133
|
+
// ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
// Capability strings must be in the namespaced plugin form
|
|
136
|
+
// `plugin:<short-name>:<action>` (see PLUGIN_CAPABILITY_REGEX). The
|
|
137
|
+
// route-side prefix-scoping below additionally requires the `<short-name>`
|
|
138
|
+
// segment to match the reporting plugin's own derived route.
|
|
139
|
+
const PluginReportSchema = z.object({
|
|
140
|
+
plugins: z.array(
|
|
141
|
+
z.object({
|
|
142
|
+
name: z.string().regex(/^[a-z0-9-]+$/),
|
|
143
|
+
version: z.string(),
|
|
144
|
+
capabilities: z.array(z.string().regex(PLUGIN_CAPABILITY_REGEX)).default([]),
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
fastify.post(
|
|
150
|
+
'/agents/plugins/report',
|
|
151
|
+
{
|
|
152
|
+
preHandler: fastify.requireRole(['admin', 'agent']),
|
|
153
|
+
},
|
|
154
|
+
async (request, reply) => {
|
|
155
|
+
const result = PluginReportSchema.safeParse(request.body);
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
return reply
|
|
158
|
+
.code(400)
|
|
159
|
+
.send({ error: 'Invalid plugin report', details: result.error.issues });
|
|
160
|
+
}
|
|
161
|
+
const { plugins } = result.data;
|
|
162
|
+
|
|
163
|
+
// Collect capabilities, scoped to the reporting plugin's namespace.
|
|
164
|
+
// Only accept caps whose `<short-name>` segment matches the plugin's
|
|
165
|
+
// own name — this stops a compromised plugin from contributing
|
|
166
|
+
// capabilities that another plugin has claimed (or from inventing new
|
|
167
|
+
// top-level namespaces; the regex above already blocks anything
|
|
168
|
+
// outside `plugin:*`).
|
|
169
|
+
const reportedCaps = new Set();
|
|
170
|
+
for (const plugin of plugins) {
|
|
171
|
+
const expectedPrefix = `plugin:${plugin.name}:`;
|
|
172
|
+
for (const cap of plugin.capabilities) {
|
|
173
|
+
if (cap.startsWith(expectedPrefix)) {
|
|
174
|
+
reportedCaps.add(cap);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (reportedCaps.size === 0) {
|
|
180
|
+
return { ok: true, merged: 0 };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Merge with existing server-side plugin capabilities
|
|
184
|
+
const serverCaps = await getPluginCapabilities();
|
|
185
|
+
const allPluginCaps = [...new Set([...serverCaps, ...reportedCaps])];
|
|
186
|
+
setPluginCapabilities(allPluginCaps);
|
|
187
|
+
|
|
188
|
+
// Persist agent-reported capabilities so they survive server restarts
|
|
189
|
+
const existingAgentCaps = await loadAgentReportedCaps();
|
|
190
|
+
const allAgentCaps = [...new Set([...existingAgentCaps, ...reportedCaps])];
|
|
191
|
+
await saveAgentReportedCaps(allAgentCaps);
|
|
192
|
+
|
|
193
|
+
request.log.info(
|
|
194
|
+
{ agentPlugins: plugins.length, newCaps: reportedCaps.size },
|
|
195
|
+
'Agent plugin capabilities reported',
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return { ok: true, merged: reportedCaps.size };
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
RegisterScopeSchema,
|
|
4
|
+
RegisterInstanceSchema,
|
|
5
|
+
RequestTicketSchema,
|
|
6
|
+
ValidateTicketSchema,
|
|
7
|
+
CreateSessionSchema,
|
|
8
|
+
UpdateSessionSchema,
|
|
9
|
+
AssignmentSchema,
|
|
10
|
+
TicketScopeNameSchema,
|
|
11
|
+
registerScope,
|
|
12
|
+
listScopes,
|
|
13
|
+
unregisterScope,
|
|
14
|
+
registerInstance,
|
|
15
|
+
deregisterInstance,
|
|
16
|
+
instanceHeartbeat,
|
|
17
|
+
createAssignment,
|
|
18
|
+
removeAssignment,
|
|
19
|
+
listAssignments,
|
|
20
|
+
requestTicket,
|
|
21
|
+
getTicketInbox,
|
|
22
|
+
validateTicket,
|
|
23
|
+
listTickets,
|
|
24
|
+
revokeTicket,
|
|
25
|
+
createSession,
|
|
26
|
+
sessionHeartbeat,
|
|
27
|
+
updateSession,
|
|
28
|
+
killSession,
|
|
29
|
+
listSessions,
|
|
30
|
+
} from '../../lib/tickets.js';
|
|
31
|
+
|
|
32
|
+
// Param validation schemas
|
|
33
|
+
const HexIdSchema = z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(128)
|
|
37
|
+
.regex(/^[a-f0-9]+$/);
|
|
38
|
+
const SessionIdSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(128)
|
|
42
|
+
.regex(/^[a-zA-Z0-9_-]+$/);
|
|
43
|
+
// Accepts regular agent labels (e.g., "macbook-pro") and plugin-agent labels
|
|
44
|
+
// (e.g., "plugin-agent:macbook-pro:raspi-sync")
|
|
45
|
+
const AgentLabelSchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(150)
|
|
49
|
+
.regex(/^[a-zA-Z0-9_:-]+$/);
|
|
50
|
+
const InstanceScopeSchema = z
|
|
51
|
+
.string()
|
|
52
|
+
.min(1)
|
|
53
|
+
.max(200)
|
|
54
|
+
.regex(/^plugin:[a-z0-9-]+:[a-z0-9-]+:[a-f0-9]+$/);
|
|
55
|
+
|
|
56
|
+
export default async function ticketRoutes(fastify, _opts) {
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
// Ticket scope management (admin only)
|
|
59
|
+
// -----------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
// POST /api/tickets/scopes — Register ticket scopes
|
|
62
|
+
fastify.post(
|
|
63
|
+
'/tickets/scopes',
|
|
64
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
65
|
+
async (request, reply) => {
|
|
66
|
+
const body = RegisterScopeSchema.parse(request.body);
|
|
67
|
+
try {
|
|
68
|
+
const result = await registerScope(body, request.log);
|
|
69
|
+
return reply.code(201).send(result);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const statusCode = err.statusCode || 500;
|
|
72
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// GET /api/tickets/scopes — List registered scopes
|
|
78
|
+
fastify.get(
|
|
79
|
+
'/tickets/scopes',
|
|
80
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
81
|
+
async (request, reply) => {
|
|
82
|
+
try {
|
|
83
|
+
const result = await listScopes();
|
|
84
|
+
return result;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const statusCode = err.statusCode || 500;
|
|
87
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// DELETE /api/tickets/scopes/:name — Unregister ticket scopes
|
|
93
|
+
fastify.delete(
|
|
94
|
+
'/tickets/scopes/:name',
|
|
95
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
96
|
+
async (request, reply) => {
|
|
97
|
+
const name = TicketScopeNameSchema.parse(request.params.name);
|
|
98
|
+
try {
|
|
99
|
+
const result = await unregisterScope(name, request.log);
|
|
100
|
+
return result;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const statusCode = err.statusCode || 500;
|
|
103
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// -----------------------------------------------------------------------
|
|
109
|
+
// Instance registration (agent, scoped)
|
|
110
|
+
// -----------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
// POST /api/tickets/instances — Register or re-register as scope host
|
|
113
|
+
fastify.post(
|
|
114
|
+
'/tickets/instances',
|
|
115
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
116
|
+
async (request, reply) => {
|
|
117
|
+
const body = RegisterInstanceSchema.parse(request.body);
|
|
118
|
+
try {
|
|
119
|
+
// Verify caller has the base scope capability
|
|
120
|
+
if (request.certRole === 'agent') {
|
|
121
|
+
const caps = request.certCapabilities || [];
|
|
122
|
+
if (!caps.includes(body.scope)) {
|
|
123
|
+
return reply.code(403).send({ error: 'Insufficient capability' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const agentLabel = request.certLabel;
|
|
128
|
+
if (!agentLabel) {
|
|
129
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
130
|
+
}
|
|
131
|
+
const result = await registerInstance(body.scope, body.transport, agentLabel, request.log);
|
|
132
|
+
return reply.code(result.isReregistration ? 200 : 201).send({
|
|
133
|
+
ok: result.ok,
|
|
134
|
+
instanceId: result.instanceId,
|
|
135
|
+
instanceScope: result.instanceScope,
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const statusCode = err.statusCode || 500;
|
|
139
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// DELETE /api/tickets/instances/:instanceId — Deregister instance
|
|
145
|
+
fastify.delete(
|
|
146
|
+
'/tickets/instances/:instanceId',
|
|
147
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
148
|
+
async (request, reply) => {
|
|
149
|
+
const instanceId = HexIdSchema.parse(request.params.instanceId);
|
|
150
|
+
try {
|
|
151
|
+
const result = await deregisterInstance(
|
|
152
|
+
instanceId,
|
|
153
|
+
request.certLabel,
|
|
154
|
+
request.certRole,
|
|
155
|
+
request.log,
|
|
156
|
+
);
|
|
157
|
+
return result;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const statusCode = err.statusCode || 500;
|
|
160
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// POST /api/tickets/instances/:instanceId/heartbeat — Instance liveness
|
|
166
|
+
fastify.post(
|
|
167
|
+
'/tickets/instances/:instanceId/heartbeat',
|
|
168
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
169
|
+
async (request, reply) => {
|
|
170
|
+
const instanceId = HexIdSchema.parse(request.params.instanceId);
|
|
171
|
+
try {
|
|
172
|
+
const agentLabel = request.certLabel;
|
|
173
|
+
if (!agentLabel) {
|
|
174
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
175
|
+
}
|
|
176
|
+
// Defense-in-depth: reject agents with no capabilities before acquiring
|
|
177
|
+
// the lock. The library's instanceHeartbeat verifies the specific scope.
|
|
178
|
+
if (request.certRole === 'agent') {
|
|
179
|
+
const caps = request.certCapabilities || [];
|
|
180
|
+
if (caps.length === 0) {
|
|
181
|
+
return reply.code(404).send({ error: 'Not found' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const result = await instanceHeartbeat(instanceId, agentLabel);
|
|
185
|
+
return result;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const statusCode = err.statusCode || 500;
|
|
188
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// -----------------------------------------------------------------------
|
|
194
|
+
// Instance assignment (admin only)
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
// POST /api/tickets/assignments — Assign agent to instance
|
|
198
|
+
fastify.post(
|
|
199
|
+
'/tickets/assignments',
|
|
200
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
201
|
+
async (request, reply) => {
|
|
202
|
+
const body = AssignmentSchema.parse(request.body);
|
|
203
|
+
try {
|
|
204
|
+
const { isExisting, ...response } = await createAssignment(
|
|
205
|
+
body.agentLabel,
|
|
206
|
+
body.instanceScope,
|
|
207
|
+
request.log,
|
|
208
|
+
);
|
|
209
|
+
return reply.code(isExisting ? 200 : 201).send(response);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const statusCode = err.statusCode || 500;
|
|
212
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// DELETE /api/tickets/assignments/:agentLabel/:instanceScope — Remove assignment
|
|
218
|
+
fastify.delete(
|
|
219
|
+
'/tickets/assignments/:agentLabel/:instanceScope',
|
|
220
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
221
|
+
async (request, reply) => {
|
|
222
|
+
const agentLabel = AgentLabelSchema.parse(request.params.agentLabel);
|
|
223
|
+
const instanceScope = InstanceScopeSchema.parse(request.params.instanceScope);
|
|
224
|
+
try {
|
|
225
|
+
const result = await removeAssignment(agentLabel, instanceScope, request.log);
|
|
226
|
+
return result;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const statusCode = err.statusCode || 500;
|
|
229
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// GET /api/tickets/assignments — List all assignments
|
|
235
|
+
fastify.get(
|
|
236
|
+
'/tickets/assignments',
|
|
237
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
238
|
+
async (request, reply) => {
|
|
239
|
+
const filters = {};
|
|
240
|
+
if (request.query.agentLabel)
|
|
241
|
+
filters.agentLabel = AgentLabelSchema.parse(request.query.agentLabel);
|
|
242
|
+
if (request.query.instanceScope)
|
|
243
|
+
filters.instanceScope = InstanceScopeSchema.parse(request.query.instanceScope);
|
|
244
|
+
try {
|
|
245
|
+
const result = await listAssignments(filters);
|
|
246
|
+
return result;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const statusCode = err.statusCode || 500;
|
|
249
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// -----------------------------------------------------------------------
|
|
255
|
+
// Ticket operations (agent, scoped)
|
|
256
|
+
// -----------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
// POST /api/tickets — Request a ticket
|
|
259
|
+
fastify.post(
|
|
260
|
+
'/tickets',
|
|
261
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
262
|
+
async (request, reply) => {
|
|
263
|
+
const body = RequestTicketSchema.parse(request.body);
|
|
264
|
+
try {
|
|
265
|
+
// Verify caller has the base scope capability
|
|
266
|
+
if (request.certRole === 'agent') {
|
|
267
|
+
const caps = request.certCapabilities || [];
|
|
268
|
+
if (!caps.includes(body.scope)) {
|
|
269
|
+
return reply.code(404).send({ error: 'Not found' });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const sourceLabel = request.certLabel;
|
|
274
|
+
if (!sourceLabel) {
|
|
275
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
276
|
+
}
|
|
277
|
+
const result = await requestTicket(
|
|
278
|
+
body.scope,
|
|
279
|
+
body.instanceId,
|
|
280
|
+
body.target,
|
|
281
|
+
sourceLabel,
|
|
282
|
+
request.log,
|
|
283
|
+
);
|
|
284
|
+
return reply.code(201).send(result);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const statusCode = err.statusCode || 500;
|
|
287
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// GET /api/tickets/inbox — Retrieve pending tickets
|
|
293
|
+
fastify.get(
|
|
294
|
+
'/tickets/inbox',
|
|
295
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
296
|
+
async (request, reply) => {
|
|
297
|
+
try {
|
|
298
|
+
const agentLabel = request.certLabel;
|
|
299
|
+
if (!agentLabel) {
|
|
300
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
301
|
+
}
|
|
302
|
+
const result = await getTicketInbox(agentLabel);
|
|
303
|
+
return result;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const statusCode = err.statusCode || 500;
|
|
306
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// POST /api/tickets/validate — Validate a ticket
|
|
312
|
+
fastify.post(
|
|
313
|
+
'/tickets/validate',
|
|
314
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
315
|
+
async (request, reply) => {
|
|
316
|
+
const body = ValidateTicketSchema.parse(request.body);
|
|
317
|
+
try {
|
|
318
|
+
const callerLabel = request.certLabel;
|
|
319
|
+
if (!callerLabel) {
|
|
320
|
+
return reply.code(401).send({ error: 'Invalid ticket' });
|
|
321
|
+
}
|
|
322
|
+
const result = await validateTicket(body.ticketId, callerLabel, request.log);
|
|
323
|
+
return result;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const statusCode = err.statusCode || 500;
|
|
326
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// GET /api/tickets — List active tickets (admin)
|
|
332
|
+
fastify.get(
|
|
333
|
+
'/tickets',
|
|
334
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
335
|
+
async (request, reply) => {
|
|
336
|
+
try {
|
|
337
|
+
const result = await listTickets();
|
|
338
|
+
return result;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const statusCode = err.statusCode || 500;
|
|
341
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// DELETE /api/tickets/:ticketId — Revoke a ticket (admin)
|
|
347
|
+
fastify.delete(
|
|
348
|
+
'/tickets/:ticketId',
|
|
349
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
350
|
+
async (request, reply) => {
|
|
351
|
+
const ticketId = HexIdSchema.parse(request.params.ticketId);
|
|
352
|
+
try {
|
|
353
|
+
const result = await revokeTicket(ticketId, request.log);
|
|
354
|
+
return result;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const statusCode = err.statusCode || 500;
|
|
357
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// -----------------------------------------------------------------------
|
|
363
|
+
// Session management (agent + admin)
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
// POST /api/tickets/sessions — Report session creation
|
|
367
|
+
fastify.post(
|
|
368
|
+
'/tickets/sessions',
|
|
369
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
370
|
+
async (request, reply) => {
|
|
371
|
+
const body = CreateSessionSchema.parse(request.body);
|
|
372
|
+
try {
|
|
373
|
+
const callerLabel = request.certLabel;
|
|
374
|
+
if (!callerLabel) {
|
|
375
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
376
|
+
}
|
|
377
|
+
const result = await createSession(body.ticketId, callerLabel, request.log);
|
|
378
|
+
return reply.code(201).send(result);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const statusCode = err.statusCode || 500;
|
|
381
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// POST /api/tickets/sessions/:sessionId/heartbeat — Session heartbeat
|
|
387
|
+
fastify.post(
|
|
388
|
+
'/tickets/sessions/:sessionId/heartbeat',
|
|
389
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
390
|
+
async (request, reply) => {
|
|
391
|
+
const sessionId = SessionIdSchema.parse(request.params.sessionId);
|
|
392
|
+
try {
|
|
393
|
+
const callerLabel = request.certLabel;
|
|
394
|
+
if (!callerLabel) {
|
|
395
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
396
|
+
}
|
|
397
|
+
const result = await sessionHeartbeat(sessionId, callerLabel);
|
|
398
|
+
return result;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const statusCode = err.statusCode || 500;
|
|
401
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// PATCH /api/tickets/sessions/:sessionId — Update session status
|
|
407
|
+
fastify.patch(
|
|
408
|
+
'/tickets/sessions/:sessionId',
|
|
409
|
+
{ preHandler: fastify.requireRole(['admin', 'agent']) },
|
|
410
|
+
async (request, reply) => {
|
|
411
|
+
const sessionId = SessionIdSchema.parse(request.params.sessionId);
|
|
412
|
+
const body = UpdateSessionSchema.parse(request.body);
|
|
413
|
+
try {
|
|
414
|
+
const callerLabel = request.certLabel;
|
|
415
|
+
if (!callerLabel) {
|
|
416
|
+
return reply.code(400).send({ error: 'Agent label required' });
|
|
417
|
+
}
|
|
418
|
+
const result = await updateSession(sessionId, body.status, callerLabel);
|
|
419
|
+
return result;
|
|
420
|
+
} catch (err) {
|
|
421
|
+
const statusCode = err.statusCode || 500;
|
|
422
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// DELETE /api/tickets/sessions/:sessionId — Admin kill session
|
|
428
|
+
fastify.delete(
|
|
429
|
+
'/tickets/sessions/:sessionId',
|
|
430
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
431
|
+
async (request, reply) => {
|
|
432
|
+
const sessionId = SessionIdSchema.parse(request.params.sessionId);
|
|
433
|
+
try {
|
|
434
|
+
const result = await killSession(sessionId, request.log);
|
|
435
|
+
return result;
|
|
436
|
+
} catch (err) {
|
|
437
|
+
const statusCode = err.statusCode || 500;
|
|
438
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// GET /api/tickets/sessions — List active sessions (admin)
|
|
444
|
+
fastify.get(
|
|
445
|
+
'/tickets/sessions',
|
|
446
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
447
|
+
async (request, reply) => {
|
|
448
|
+
try {
|
|
449
|
+
const result = await listSessions();
|
|
450
|
+
return result;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
const statusCode = err.statusCode || 500;
|
|
453
|
+
return reply.code(statusCode).send({ error: err.message });
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
}
|