@lamalibre/create-lamaste 4.2.0 → 4.3.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 +205 -2
- package/src/index.js +4 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- 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 +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +98 -34
- 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 +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +7 -3
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +190 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +18 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +32 -4
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.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 +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Lamaste's audit log and panel notifications (4.3, slice 2b).
|
|
2
|
+
--
|
|
3
|
+
-- audit_events: append-only record of administrative decisions and of what
|
|
4
|
+
-- plugins' lifecycle hooks report through the `audit` effect. `details` is
|
|
5
|
+
-- JSON metadata — never a request or response body, a secret or personal
|
|
6
|
+
-- data. Pruned by age and count (lib/audit.js), never edited.
|
|
7
|
+
CREATE TABLE audit_events (
|
|
8
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
|
+
at TEXT NOT NULL,
|
|
10
|
+
actor TEXT NOT NULL,
|
|
11
|
+
action TEXT NOT NULL,
|
|
12
|
+
plugin TEXT,
|
|
13
|
+
details TEXT NOT NULL DEFAULT '{}'
|
|
14
|
+
);
|
|
15
|
+
CREATE INDEX audit_events_plugin ON audit_events (plugin, id);
|
|
16
|
+
CREATE INDEX audit_events_action ON audit_events (action, id);
|
|
17
|
+
|
|
18
|
+
-- notifications: messages for the panel's administrators — the
|
|
19
|
+
-- `notify-admins` hook effect and Lamaste's own events. Read state is
|
|
20
|
+
-- shared: the panel has one administrator identity.
|
|
21
|
+
CREATE TABLE notifications (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
at TEXT NOT NULL,
|
|
24
|
+
source TEXT NOT NULL,
|
|
25
|
+
level TEXT NOT NULL,
|
|
26
|
+
message TEXT NOT NULL,
|
|
27
|
+
read_at TEXT
|
|
28
|
+
);
|
|
29
|
+
CREATE INDEX notifications_unread ON notifications (read_at, id);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
-- Lifecycle hooks of agent-hosted plugins (4.3, slice 2b).
|
|
2
|
+
|
|
3
|
+
-- The manifest's `hooks` block (JSON; NULL when the plugin declares none)
|
|
4
|
+
-- and the SHA-256 of its relay entry module, read when the package was
|
|
5
|
+
-- installed. Together they give the relay hook set's digest without
|
|
6
|
+
-- touching the package again.
|
|
7
|
+
ALTER TABLE plugins ADD COLUMN hooks TEXT;
|
|
8
|
+
ALTER TABLE plugins ADD COLUMN hooks_entry_sha256 TEXT;
|
|
9
|
+
|
|
10
|
+
-- Approval of a plugin's relay hook set, and what the hook runner holds.
|
|
11
|
+
-- approved_digest the digest an administrator (or auto-approve) approved
|
|
12
|
+
-- auto_approve approve later hook-set changes automatically (audited)
|
|
13
|
+
-- installed_digest the digest lamaste-priv last installed into the runner
|
|
14
|
+
-- pack_versions JSON {pack: version} of the shared-hook rule packs last
|
|
15
|
+
-- recorded, to audit a rule update once per plugin
|
|
16
|
+
-- pending_update JSON {version, digest, ...} of a staged update waiting
|
|
17
|
+
-- for approval, or NULL
|
|
18
|
+
-- tunnel_fallback JSON fallback response the plugin's tunnel:down hook
|
|
19
|
+
-- returned, served while it is down; NULL otherwise
|
|
20
|
+
-- Kept per plugin name; removed when the plugin is uninstalled.
|
|
21
|
+
CREATE TABLE plugin_hook_sets (
|
|
22
|
+
plugin TEXT PRIMARY KEY,
|
|
23
|
+
approved_digest TEXT,
|
|
24
|
+
approved_version TEXT,
|
|
25
|
+
approved_at TEXT,
|
|
26
|
+
approved_by TEXT,
|
|
27
|
+
auto_approve INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
installed_digest TEXT,
|
|
29
|
+
pack_versions TEXT NOT NULL DEFAULT '{}',
|
|
30
|
+
pending_update TEXT,
|
|
31
|
+
tunnel_fallback TEXT
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- Long-running plugin operations (hosting, unhosting, update, uninstall):
|
|
35
|
+
-- one running per plugin; steps and outcome kept for the panel. `steps` is
|
|
36
|
+
-- JSON [{name, status, startedAt, finishedAt, error?}] — metadata only.
|
|
37
|
+
CREATE TABLE plugin_operations (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
plugin TEXT NOT NULL,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
actor TEXT NOT NULL,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
44
|
+
error TEXT,
|
|
45
|
+
params TEXT NOT NULL DEFAULT '{}',
|
|
46
|
+
started_at TEXT NOT NULL,
|
|
47
|
+
finished_at TEXT
|
|
48
|
+
);
|
|
49
|
+
CREATE INDEX plugin_operations_plugin ON plugin_operations (plugin, started_at);
|
|
50
|
+
|
|
51
|
+
-- Work for an agent's sync (agent-config `agentJobs`): calls the agent
|
|
52
|
+
-- makes to its agent half on its own loopback (hook stages outside the
|
|
53
|
+
-- tunnel), reported back with a result. `input` is the hook input (JSON
|
|
54
|
+
-- metadata, no secrets); the agent signs the call with the installation
|
|
55
|
+
-- secret it already holds.
|
|
56
|
+
CREATE TABLE agent_jobs (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
agent_label TEXT NOT NULL,
|
|
59
|
+
plugin TEXT NOT NULL,
|
|
60
|
+
kind TEXT NOT NULL,
|
|
61
|
+
stage TEXT NOT NULL,
|
|
62
|
+
input TEXT NOT NULL,
|
|
63
|
+
timeout_ms INTEGER NOT NULL,
|
|
64
|
+
status TEXT NOT NULL,
|
|
65
|
+
result TEXT,
|
|
66
|
+
created_at TEXT NOT NULL,
|
|
67
|
+
deadline_at TEXT NOT NULL,
|
|
68
|
+
completed_at TEXT
|
|
69
|
+
);
|
|
70
|
+
CREATE INDEX agent_jobs_agent ON agent_jobs (agent_label, status);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Agent halves Lamaste installs onto an agent (4.3, slice 3).
|
|
2
|
+
|
|
3
|
+
-- The agent half pinned when the plugin's version was staged (install or
|
|
4
|
+
-- update): JSON {package, version, integrity}, where `integrity` is the npm
|
|
5
|
+
-- Subresource Integrity (sha512) of the agent package's tarball as the
|
|
6
|
+
-- registry published it. The approval digest covers it and the agent
|
|
7
|
+
-- installs only a tarball that matches it. NULL when Lamaste installs no
|
|
8
|
+
-- agent half (no `agent.package` in the manifest).
|
|
9
|
+
ALTER TABLE plugins ADD COLUMN agent_half_pin TEXT;
|
|
10
|
+
|
|
11
|
+
-- Agent jobs gain kinds besides `hook`: `app-install`, `app-activate` and
|
|
12
|
+
-- `app-remove` run the agent's root helper. Their parameters are `payload`
|
|
13
|
+
-- (JSON: {spec} | {version, package} | {purge, package}); `stage` and
|
|
14
|
+
-- `input` belong to hook jobs only, so they become nullable. SQLite cannot
|
|
15
|
+
-- drop a NOT NULL constraint in place: the table is rebuilt with its rows.
|
|
16
|
+
CREATE TABLE agent_jobs_v2 (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
agent_label TEXT NOT NULL,
|
|
19
|
+
plugin TEXT NOT NULL,
|
|
20
|
+
kind TEXT NOT NULL,
|
|
21
|
+
stage TEXT,
|
|
22
|
+
input TEXT,
|
|
23
|
+
payload TEXT,
|
|
24
|
+
timeout_ms INTEGER NOT NULL,
|
|
25
|
+
status TEXT NOT NULL,
|
|
26
|
+
result TEXT,
|
|
27
|
+
created_at TEXT NOT NULL,
|
|
28
|
+
deadline_at TEXT NOT NULL,
|
|
29
|
+
completed_at TEXT
|
|
30
|
+
);
|
|
31
|
+
INSERT INTO agent_jobs_v2
|
|
32
|
+
(id, agent_label, plugin, kind, stage, input, payload, timeout_ms, status, result,
|
|
33
|
+
created_at, deadline_at, completed_at)
|
|
34
|
+
SELECT id, agent_label, plugin, kind, stage, input, NULL, timeout_ms, status, result,
|
|
35
|
+
created_at, deadline_at, completed_at
|
|
36
|
+
FROM agent_jobs;
|
|
37
|
+
DROP TABLE agent_jobs;
|
|
38
|
+
ALTER TABLE agent_jobs_v2 RENAME TO agent_jobs;
|
|
39
|
+
CREATE INDEX agent_jobs_agent ON agent_jobs (agent_label, status);
|
|
40
|
+
|
|
41
|
+
-- What an agent reports about its host (JSON HostFacts: OS, distribution,
|
|
42
|
+
-- architecture, Node.js, systemd, the app host helper) and when it last
|
|
43
|
+
-- did — checked against a plugin's `agent.requires` when it is hosted.
|
|
44
|
+
ALTER TABLE agents ADD COLUMN host_facts TEXT;
|
|
45
|
+
ALTER TABLE agents ADD COLUMN host_facts_at TEXT;
|
|
46
|
+
|
|
47
|
+
-- An agent's push-install window and policy. The registry code has set both
|
|
48
|
+
-- fields since push install exists, but the SQLite table (0004) had no
|
|
49
|
+
-- columns for them, so an enabled window was never stored and the
|
|
50
|
+
-- push-install gates could never pass. Hosting, updating and removing an
|
|
51
|
+
-- agent half are gated on them.
|
|
52
|
+
ALTER TABLE agents ADD COLUMN push_install_enabled_until TEXT;
|
|
53
|
+
ALTER TABLE agents ADD COLUMN push_install_policy TEXT;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Plugin routes are unique; data an agent half leaves on an agent is kept
|
|
2
|
+
-- on record (4.3, slice 3).
|
|
3
|
+
|
|
4
|
+
-- A plugin's route — its name without a trailing "-server" or "-agent"
|
|
5
|
+
-- (`derivePluginRoute`) — is what its API (`/api/<route>`), capabilities
|
|
6
|
+
-- (`plugin:<route>:*`), OIDC client ids, hosting record and, on the agent,
|
|
7
|
+
-- its agent half's account and directories are named after. Two installed
|
|
8
|
+
-- plugins sharing one ("fx" and "fx-server") would act on each other's
|
|
9
|
+
-- hosting and capabilities, so each route belongs to one installed plugin.
|
|
10
|
+
ALTER TABLE plugins ADD COLUMN route TEXT;
|
|
11
|
+
UPDATE plugins SET route = CASE
|
|
12
|
+
WHEN substr(name, -7) = '-server' THEN substr(name, 1, length(name) - 7)
|
|
13
|
+
WHEN substr(name, -6) = '-agent' THEN substr(name, 1, length(name) - 6)
|
|
14
|
+
ELSE name
|
|
15
|
+
END;
|
|
16
|
+
-- Plugins installed before the rule may already share a route: the one
|
|
17
|
+
-- installed first keeps it; the others hold none (NULL) and can only be
|
|
18
|
+
-- uninstalled — never hosted — until they are reinstalled under a route
|
|
19
|
+
-- of their own.
|
|
20
|
+
UPDATE plugins SET route = NULL WHERE EXISTS (
|
|
21
|
+
SELECT 1 FROM plugins AS other
|
|
22
|
+
WHERE other.route = plugins.route
|
|
23
|
+
AND other.name <> plugins.name
|
|
24
|
+
AND (other.installed_at < plugins.installed_at
|
|
25
|
+
OR (other.installed_at = plugins.installed_at AND other.name < plugins.name))
|
|
26
|
+
);
|
|
27
|
+
CREATE UNIQUE INDEX plugins_route ON plugins (route);
|
|
28
|
+
|
|
29
|
+
-- The data an agent half leaves on an agent when it is removed without
|
|
30
|
+
-- purge (unhost, uninstall, a rolled-back install): its data directory and
|
|
31
|
+
-- service account, both named after the route. Kept here — not on the
|
|
32
|
+
-- plugin row — because it outlives the plugin's uninstall and is keyed the
|
|
33
|
+
-- way it exists on the agent: one record per agent and route. `package` is
|
|
34
|
+
-- the agent package that owns it (the agent's helper refuses to install
|
|
35
|
+
-- another package over it); `plugin` the plugin it belonged to. Removed when
|
|
36
|
+
-- it is purged, or when the same agent half is installed there again and
|
|
37
|
+
-- takes the data back.
|
|
38
|
+
CREATE TABLE retained_agent_data (
|
|
39
|
+
agent_label TEXT NOT NULL,
|
|
40
|
+
route TEXT NOT NULL,
|
|
41
|
+
plugin TEXT NOT NULL,
|
|
42
|
+
package TEXT NOT NULL,
|
|
43
|
+
since TEXT NOT NULL,
|
|
44
|
+
operation TEXT,
|
|
45
|
+
PRIMARY KEY (agent_label, route)
|
|
46
|
+
);
|
|
47
|
+
CREATE INDEX retained_agent_data_plugin ON retained_agent_data (plugin);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- App jobs are signed by the relay (4.3, slice 3).
|
|
2
|
+
|
|
3
|
+
-- The agent's root helper runs an app job (`app-install`, `app-activate`,
|
|
4
|
+
-- `app-remove`) only when the relay signed it, with the key the agent's
|
|
5
|
+
-- operator pinned: JSON {envelope, sig} (core `app-job-signature.ts`),
|
|
6
|
+
-- made with the relay's signing key when the job is created and listed with
|
|
7
|
+
-- it in agent-config. NULL for hook jobs, which the helper never sees.
|
|
8
|
+
ALTER TABLE agent_jobs ADD COLUMN signature TEXT;
|
|
9
|
+
|
|
10
|
+
-- An app job left pending from before has no signature, and no agent would
|
|
11
|
+
-- run it: it is cancelled rather than listed.
|
|
12
|
+
UPDATE agent_jobs
|
|
13
|
+
SET status = 'cancelled', completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
14
|
+
WHERE status = 'pending' AND kind != 'hook';
|
|
@@ -15,10 +15,11 @@ import crypto from 'node:crypto';
|
|
|
15
15
|
import { access, constants, mkdir } from 'node:fs/promises';
|
|
16
16
|
import {
|
|
17
17
|
BASE_CAPABILITIES as CORE_BASE_CAPABILITIES,
|
|
18
|
+
HostFactsSchema,
|
|
18
19
|
PLUGIN_AGENT_CN_PREFIX as CORE_PLUGIN_AGENT_CN_PREFIX,
|
|
19
20
|
} from '@lamalibre/lamaste';
|
|
20
21
|
import { addToRevocationList } from './revocation.js';
|
|
21
|
-
import { releaseAgentTunnels } from '@lamalibre/lamaste/server';
|
|
22
|
+
import { releaseAgentTunnels, stripPluginCapabilities } from '@lamalibre/lamaste/server';
|
|
22
23
|
import { tunnelNginxDeps, tunnelChiselDeps, tunnelStateDeps } from './tunnel-deps.js';
|
|
23
24
|
import {
|
|
24
25
|
addChiselCredential,
|
|
@@ -241,8 +242,9 @@ async function getStmts() {
|
|
|
241
242
|
insert: db.prepare(`
|
|
242
243
|
INSERT INTO agents
|
|
243
244
|
(label, serial, capabilities, allowed_sites, enrollment_method,
|
|
244
|
-
delegated_by, created_at, expires_at, revoked, revoked_at
|
|
245
|
-
|
|
245
|
+
delegated_by, created_at, expires_at, revoked, revoked_at,
|
|
246
|
+
host_facts, host_facts_at, push_install_enabled_until, push_install_policy)
|
|
247
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
246
248
|
`),
|
|
247
249
|
|
|
248
250
|
deleteAll: db.prepare('DELETE FROM agents'),
|
|
@@ -276,13 +278,51 @@ function rowToAgent(row) {
|
|
|
276
278
|
};
|
|
277
279
|
if (row.revoked_at != null) agent.revokedAt = row.revoked_at;
|
|
278
280
|
if (row.delegated_by != null) agent.delegatedBy = row.delegated_by;
|
|
281
|
+
if (row.host_facts != null) agent.hostFacts = parseHostFacts(row.host_facts);
|
|
282
|
+
if (row.host_facts_at != null) agent.hostFactsAt = row.host_facts_at;
|
|
283
|
+
if (row.push_install_enabled_until != null) {
|
|
284
|
+
agent.pushInstallEnabledUntil = row.push_install_enabled_until;
|
|
285
|
+
}
|
|
286
|
+
if (row.push_install_policy != null) agent.pushInstallPolicy = row.push_install_policy;
|
|
279
287
|
return agent;
|
|
280
288
|
}
|
|
281
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Stored host facts, re-validated on the way out: a row that no longer
|
|
292
|
+
* parses as HostFacts (written by another version) reads as none, so
|
|
293
|
+
* eligibility asks the agent to report again instead of trusting it.
|
|
294
|
+
*/
|
|
295
|
+
function parseHostFacts(text) {
|
|
296
|
+
try {
|
|
297
|
+
const parsed = HostFactsSchema.safeParse(JSON.parse(text));
|
|
298
|
+
return parsed.success ? parsed.data : null;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
282
304
|
function nullableString(value) {
|
|
283
305
|
return value === undefined || value === null ? null : value;
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Remove every `plugin:<route>:*` capability from every agent's stored
|
|
310
|
+
* record, under the registry lock — run when a plugin is uninstalled, so
|
|
311
|
+
* installing it again later does not silently re-grant what an admin gave
|
|
312
|
+
* the old installation. Returns the per-agent diff for the audit log.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} route
|
|
315
|
+
* @returns {Promise<Array<{ label: string, removed: string[] }>>}
|
|
316
|
+
*/
|
|
317
|
+
export function revokePluginCapabilitiesFromAgents(route) {
|
|
318
|
+
return withRegistryLock(async () => {
|
|
319
|
+
const registry = await loadAgentRegistry();
|
|
320
|
+
const diffs = stripPluginCapabilities(registry.agents, route);
|
|
321
|
+
if (diffs.length > 0) await saveAgentRegistry(registry);
|
|
322
|
+
return diffs;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
286
326
|
/**
|
|
287
327
|
* Load the agent registry.
|
|
288
328
|
* Returns `{ agents: [] }` if the table is empty.
|
|
@@ -340,6 +380,10 @@ export async function saveAgentRegistry(data) {
|
|
|
340
380
|
a.expiresAt,
|
|
341
381
|
a.revoked ? 1 : 0,
|
|
342
382
|
nullableString(a.revokedAt),
|
|
383
|
+
a.hostFacts ? JSON.stringify(a.hostFacts) : null,
|
|
384
|
+
nullableString(a.hostFactsAt),
|
|
385
|
+
nullableString(a.pushInstallEnabledUntil),
|
|
386
|
+
nullableString(a.pushInstallPolicy),
|
|
343
387
|
);
|
|
344
388
|
}
|
|
345
389
|
s.commit.run();
|
|
@@ -533,20 +577,24 @@ export async function generateAgentCert(label, logger, capabilities, allowedSite
|
|
|
533
577
|
}
|
|
534
578
|
|
|
535
579
|
/**
|
|
536
|
-
* List all agent certificates with expiry status
|
|
580
|
+
* List all agent certificates with expiry status and the host facts each
|
|
581
|
+
* agent last reported. `status` sums a certificate up for the panel:
|
|
582
|
+
* `revoked`, `expired`, `expiring-soon` (within 30 days) or `active`.
|
|
537
583
|
*
|
|
538
|
-
* @returns {Promise<Array<{ label: string, serial: string, createdAt: string, expiresAt: string, revoked: boolean, expiringSoon: boolean }>>}
|
|
584
|
+
* @returns {Promise<Array<{ label: string, serial: string, createdAt: string, expiresAt: string, revoked: boolean, expiringSoon: boolean, status: 'active' | 'expiring-soon' | 'expired' | 'revoked', hostFacts: object | null, hostFactsAt: string | null }>>}
|
|
539
585
|
*/
|
|
540
586
|
export async function listAgentCerts() {
|
|
541
587
|
const registry = await loadAgentRegistry();
|
|
588
|
+
const now = Date.now();
|
|
542
589
|
|
|
543
590
|
return registry.agents.map((agent) => {
|
|
544
591
|
let expiringSoon = false;
|
|
592
|
+
let expired = false;
|
|
545
593
|
if (!agent.revoked && agent.expiresAt) {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
);
|
|
594
|
+
const expiresAt = new Date(agent.expiresAt).getTime();
|
|
595
|
+
const daysUntilExpiry = Math.floor((expiresAt - now) / (1000 * 60 * 60 * 24));
|
|
549
596
|
expiringSoon = daysUntilExpiry <= 30;
|
|
597
|
+
expired = expiresAt <= now;
|
|
550
598
|
}
|
|
551
599
|
const isPluginAgent = agent.label.startsWith(PLUGIN_AGENT_CN_PREFIX);
|
|
552
600
|
const entry = {
|
|
@@ -554,6 +602,16 @@ export async function listAgentCerts() {
|
|
|
554
602
|
capabilities: agent.capabilities || (isPluginAgent ? [] : ['tunnels:read']),
|
|
555
603
|
enrollmentMethod: agent.enrollmentMethod || 'p12',
|
|
556
604
|
expiringSoon,
|
|
605
|
+
status: agent.revoked
|
|
606
|
+
? 'revoked'
|
|
607
|
+
: expired
|
|
608
|
+
? 'expired'
|
|
609
|
+
: expiringSoon
|
|
610
|
+
? 'expiring-soon'
|
|
611
|
+
: 'active',
|
|
612
|
+
// What the agent last reported about its host (null until it has).
|
|
613
|
+
hostFacts: agent.hostFacts ?? null,
|
|
614
|
+
hostFactsAt: agent.hostFactsAt ?? null,
|
|
557
615
|
};
|
|
558
616
|
if (isPluginAgent) {
|
|
559
617
|
entry.certType = 'plugin-agent';
|
|
@@ -657,6 +715,29 @@ export async function updateAgentAllowedSites(label, allowedSites) {
|
|
|
657
715
|
});
|
|
658
716
|
}
|
|
659
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Record what agent `label` reports about its host (validated
|
|
720
|
+
* `HostFacts`), under the registry lock. Only an enrolled, unrevoked
|
|
721
|
+
* agent has a record to keep them on.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} label
|
|
724
|
+
* @param {import('@lamalibre/lamaste').HostFacts} hostFacts
|
|
725
|
+
* @returns {Promise<{ hostFactsAt: string }>}
|
|
726
|
+
*/
|
|
727
|
+
export function setAgentHostFacts(label, hostFacts) {
|
|
728
|
+
return withRegistryLock(async () => {
|
|
729
|
+
const registry = await loadAgentRegistry();
|
|
730
|
+
const agent = registry.agents.find((a) => a.label === label && !a.revoked);
|
|
731
|
+
if (!agent) {
|
|
732
|
+
throw Object.assign(new Error(`Agent certificate "${label}" not found`), { statusCode: 404 });
|
|
733
|
+
}
|
|
734
|
+
agent.hostFacts = hostFacts;
|
|
735
|
+
agent.hostFactsAt = new Date().toISOString();
|
|
736
|
+
await saveAgentRegistry(registry);
|
|
737
|
+
return { hostFactsAt: agent.hostFactsAt };
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
660
741
|
/**
|
|
661
742
|
* Revoke an agent certificate by label.
|
|
662
743
|
*
|