@crvouga/dev-fleet 0.0.0-stage → 0.2.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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
2
|
+
import { convergeManagerHub } from './manager-hub';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fleet } from '../fleet-config';
|
|
5
|
+
import { hubUrl, roleOf } from '../topology';
|
|
6
|
+
import { record } from '../config';
|
|
7
|
+
import { isRevision } from '../machine/state';
|
|
8
|
+
import { privateHub } from './beszel';
|
|
9
|
+
import { agentConnection, agentDisk } from './agent-evidence';
|
|
10
|
+
import { runLockedCommand as runCommand } from '../runner';
|
|
11
|
+
import type { Check, Runner } from '../types';
|
|
12
|
+
|
|
13
|
+
export async function applyAgent(
|
|
14
|
+
value: unknown,
|
|
15
|
+
root: string,
|
|
16
|
+
run: Runner,
|
|
17
|
+
options: { receipts?: string } = {}
|
|
18
|
+
): Promise<Check[]> {
|
|
19
|
+
const request = validateRequest(value);
|
|
20
|
+
const definition = fleet;
|
|
21
|
+
await validateSource(request, root, run);
|
|
22
|
+
const role = roleOf(definition, request.node);
|
|
23
|
+
if (role === null) throw new Error('BESZEL_NOT_ENROLLED');
|
|
24
|
+
if (request.mode === 'hub') {
|
|
25
|
+
// The hub runs only on the manager; workers only ever run the agent.
|
|
26
|
+
if (role !== 'manager') throw new Error('BESZEL_NOT_HUB');
|
|
27
|
+
return convergeManagerHub(root, definition, run);
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
typeof request.publicKey !== 'string' ||
|
|
31
|
+
typeof request.token !== 'string'
|
|
32
|
+
)
|
|
33
|
+
throw new Error('BESZEL_CREDENTIALS_REQUIRED');
|
|
34
|
+
// Agents connect only to the manager's private hub URL.
|
|
35
|
+
if (privateHub(request.hubUrl) !== privateHub(hubUrl(definition)))
|
|
36
|
+
throw new Error('BESZEL_HUB_MISMATCH');
|
|
37
|
+
return installAgent(request, root, run, options.receipts);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Workers (Linux) install a hardened systemd unit. The macOS manager's own
|
|
41
|
+
* agent is a per-user launch agent installed locally by fleet-sync
|
|
42
|
+
* (installManagerAgent), never through this remote entry point.
|
|
43
|
+
*/
|
|
44
|
+
async function installAgent(
|
|
45
|
+
request: Request,
|
|
46
|
+
root: string,
|
|
47
|
+
run: Runner,
|
|
48
|
+
receipts: string | undefined
|
|
49
|
+
): Promise<Check[]> {
|
|
50
|
+
// Measured before anything changes: a full disk is named, not guessed at.
|
|
51
|
+
const disk = await agentDisk(run);
|
|
52
|
+
if (disk.status !== 'pass') return [disk];
|
|
53
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
54
|
+
const result = await runAnsibleWithReceipt(
|
|
55
|
+
run,
|
|
56
|
+
'agent.yml',
|
|
57
|
+
{
|
|
58
|
+
cwd: monitoring,
|
|
59
|
+
timeoutMs: 300_000,
|
|
60
|
+
input: JSON.stringify({
|
|
61
|
+
beszel_node: request.node,
|
|
62
|
+
beszel_hub_url: privateHub(request.hubUrl),
|
|
63
|
+
beszel_public_key: request.publicKey,
|
|
64
|
+
beszel_agent_token: request.token,
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'beszel-agent',
|
|
69
|
+
sources: [monitoring],
|
|
70
|
+
verify: () => agentActive(run),
|
|
71
|
+
...(receipts ? { directory: receipts } : {}),
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
if (!result.ok)
|
|
75
|
+
return [
|
|
76
|
+
disk,
|
|
77
|
+
{
|
|
78
|
+
id: 'beszel:installation',
|
|
79
|
+
status: 'fail',
|
|
80
|
+
summary: 'BESZEL_INSTALLATION_INCOMPLETE',
|
|
81
|
+
remedy: {
|
|
82
|
+
action:
|
|
83
|
+
'Inspect sudo, signed artifact access, free disk and existing service ownership. Preserve the native agent DATA_DIR and rerun the manager.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const ready = await agentActive(run);
|
|
88
|
+
return [
|
|
89
|
+
disk,
|
|
90
|
+
{
|
|
91
|
+
id: 'beszel:service',
|
|
92
|
+
status: ready ? 'pass' : 'fail',
|
|
93
|
+
summary: ready
|
|
94
|
+
? 'Native Beszel service is enabled and active; hub metrics remain a separate gate.'
|
|
95
|
+
: 'BESZEL_SERVICE_STOPPED',
|
|
96
|
+
},
|
|
97
|
+
// How the hub answered the agent, from the agent's side: a rejected token
|
|
98
|
+
// or key is told apart from a hub that is merely down.
|
|
99
|
+
...(ready ? [await agentConnection(run)] : []),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
async function agentActive(run: Runner): Promise<boolean> {
|
|
103
|
+
return (
|
|
104
|
+
(await run(['systemctl', 'is-enabled', 'beszel-agent.service'])).ok &&
|
|
105
|
+
(await run(['systemctl', 'is-active', 'beszel-agent.service'])).ok
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
interface Request {
|
|
109
|
+
node: string;
|
|
110
|
+
revision: string;
|
|
111
|
+
hubUrl: string;
|
|
112
|
+
mode?: unknown;
|
|
113
|
+
publicKey?: unknown;
|
|
114
|
+
token?: unknown;
|
|
115
|
+
}
|
|
116
|
+
function validateRequest(value: unknown): Request {
|
|
117
|
+
if (
|
|
118
|
+
!record(value) ||
|
|
119
|
+
typeof value['node'] !== 'string' ||
|
|
120
|
+
!isRevision(value['revision']) ||
|
|
121
|
+
typeof value['hubUrl'] !== 'string'
|
|
122
|
+
)
|
|
123
|
+
throw new Error('BESZEL_INPUT_INVALID');
|
|
124
|
+
return value as unknown as Request;
|
|
125
|
+
}
|
|
126
|
+
async function validateSource(
|
|
127
|
+
request: Request,
|
|
128
|
+
root: string,
|
|
129
|
+
run: Runner
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
for (const [args, expected] of [
|
|
132
|
+
[['git', 'rev-parse', 'HEAD'], request.revision],
|
|
133
|
+
[['git', 'rev-parse', '--show-toplevel'], resolve(root)],
|
|
134
|
+
[['git', 'status', '--porcelain'], ''],
|
|
135
|
+
] as const) {
|
|
136
|
+
const observed = await run([...args], { cwd: root });
|
|
137
|
+
if (!observed.ok || observed.stdout.trim() !== expected)
|
|
138
|
+
throw new Error('BESZEL_SOURCE_MISMATCH');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function main(): Promise<Check[]> {
|
|
142
|
+
let raw = '';
|
|
143
|
+
for await (const chunk of process.stdin) {
|
|
144
|
+
raw += String(chunk);
|
|
145
|
+
if (raw.length > 16_384) throw new Error('BESZEL_INPUT_LIMIT');
|
|
146
|
+
}
|
|
147
|
+
return applyAgent(JSON.parse(raw), process.cwd(), runCommand);
|
|
148
|
+
}
|
|
149
|
+
/** Turns a known refusal into structured evidence; anything else is redacted. */
|
|
150
|
+
export function refusal(error: unknown): Check[] {
|
|
151
|
+
const code = error instanceof Error ? error.message : '';
|
|
152
|
+
const action = REFUSALS[code];
|
|
153
|
+
return [
|
|
154
|
+
action
|
|
155
|
+
? {
|
|
156
|
+
id: 'beszel:operation',
|
|
157
|
+
status: 'fail',
|
|
158
|
+
summary: code,
|
|
159
|
+
remedy: { action },
|
|
160
|
+
}
|
|
161
|
+
: {
|
|
162
|
+
id: 'beszel:operation',
|
|
163
|
+
status: 'fail',
|
|
164
|
+
summary: 'BESZEL_OPERATION_INCOMPLETE',
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
const REFUSALS: Record<string, string> = {
|
|
169
|
+
BESZEL_NOT_HUB:
|
|
170
|
+
'The Beszel hub runs only on the manager. Workers run only the agent; converge the hub from the manager with fleet-sync.',
|
|
171
|
+
BESZEL_NOT_ENROLLED:
|
|
172
|
+
'Declare this machine in fleet.json (the manager or a worker) before enrolling monitoring.',
|
|
173
|
+
BESZEL_SOURCE_MISMATCH:
|
|
174
|
+
'The checkout is not the clean, expected revision. Rerun fleet-sync so the reviewed source is installed first.',
|
|
175
|
+
BESZEL_HUB_MISMATCH:
|
|
176
|
+
"Agents connect only to the manager's private hub URL from fleet.json. Rerun fleet-sync from an up-to-date checkout.",
|
|
177
|
+
BESZEL_CREDENTIALS_REQUIRED:
|
|
178
|
+
'Agent enrollment credentials were not delivered. Rerun fleet-sync from the manager.',
|
|
179
|
+
};
|
|
180
|
+
if (import.meta.main) {
|
|
181
|
+
const checks = await main().catch(refusal);
|
|
182
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
183
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { fleet } from '../fleet-config';
|
|
2
|
+
import lock from '../../ansible/monitoring/lock.json';
|
|
3
|
+
import type { Check } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface BeszelEvidence {
|
|
6
|
+
dashboardUrl: string;
|
|
7
|
+
checks: Check[];
|
|
8
|
+
receivedAt: string | null;
|
|
9
|
+
}
|
|
10
|
+
export type BeszelRequest = (path: string) => Promise<unknown>;
|
|
11
|
+
type Row = Record<string, unknown>;
|
|
12
|
+
function object(value: unknown): value is Row {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function items(value: unknown): Row[] {
|
|
16
|
+
if (
|
|
17
|
+
!object(value) ||
|
|
18
|
+
!Array.isArray(value['items']) ||
|
|
19
|
+
!value['items'].every(object)
|
|
20
|
+
)
|
|
21
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
22
|
+
return value['items'];
|
|
23
|
+
}
|
|
24
|
+
function check(
|
|
25
|
+
id: string,
|
|
26
|
+
status: Check['status'],
|
|
27
|
+
summary: string,
|
|
28
|
+
action: string
|
|
29
|
+
): Check {
|
|
30
|
+
return { id: `beszel:${id}`, status, summary, remedy: { action } };
|
|
31
|
+
}
|
|
32
|
+
/** Public dashboard hostname served through the Cloudflare
|
|
33
|
+
* tunnel `beszel`. Agents and the manager keep using the private hub URL.
|
|
34
|
+
*/
|
|
35
|
+
export const publicHubUrl = (): string => fleet.services.monitoring.publicUrl;
|
|
36
|
+
|
|
37
|
+
export function privateHub(value: string): string {
|
|
38
|
+
const url = new URL(value);
|
|
39
|
+
if (
|
|
40
|
+
url.protocol !== 'https:' ||
|
|
41
|
+
!url.hostname.endsWith('.ts.net') ||
|
|
42
|
+
url.port !== '8443' ||
|
|
43
|
+
url.username ||
|
|
44
|
+
url.password ||
|
|
45
|
+
url.search ||
|
|
46
|
+
url.hash ||
|
|
47
|
+
url.pathname !== '/'
|
|
48
|
+
)
|
|
49
|
+
throw new Error('BESZEL_PRIVATE_URL');
|
|
50
|
+
return url.origin;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read-only PocketBase endpoints, verified against the upstream Beszel
|
|
54
|
+
* release `ansible/monitoring/lock.json` pins.
|
|
55
|
+
* Never return raw records: they may contain tokens, hosts or user information.
|
|
56
|
+
*/
|
|
57
|
+
export async function verifyBeszel(
|
|
58
|
+
hub: string,
|
|
59
|
+
node: string,
|
|
60
|
+
request: BeszelRequest,
|
|
61
|
+
now = Date.now()
|
|
62
|
+
): Promise<BeszelEvidence> {
|
|
63
|
+
const dashboardUrl = privateHub(hub);
|
|
64
|
+
if (!/^[a-z][a-z0-9-]{0,62}$/.test(node)) throw new Error('BESZEL_NODE_ID');
|
|
65
|
+
const result: BeszelEvidence = { dashboardUrl, checks: [], receivedAt: null };
|
|
66
|
+
try {
|
|
67
|
+
const info = await request('/api/beszel/info');
|
|
68
|
+
if (!object(info) || info['v'] !== lock.version) {
|
|
69
|
+
result.checks.push(
|
|
70
|
+
check(
|
|
71
|
+
'version',
|
|
72
|
+
'fail',
|
|
73
|
+
'BESZEL_VERSION_MISMATCH',
|
|
74
|
+
'Apply the pinned hub release before using its versioned API contract.'
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
result.checks.push(
|
|
80
|
+
check(
|
|
81
|
+
'version',
|
|
82
|
+
'pass',
|
|
83
|
+
`Authenticated hub reports ${lock.version}.`,
|
|
84
|
+
'None.'
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
const filter = encodeURIComponent(`name="${node}"`);
|
|
88
|
+
const records = items(
|
|
89
|
+
await request(
|
|
90
|
+
`/api/collections/systems/records?filter=${filter}&perPage=2`
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
if (records.length !== 1) {
|
|
94
|
+
result.checks.push(
|
|
95
|
+
check(
|
|
96
|
+
'registration',
|
|
97
|
+
'fail',
|
|
98
|
+
records.length ? 'BESZEL_DUPLICATE_NODE' : 'BESZEL_NODE_MISSING',
|
|
99
|
+
'Review the canonical node in the hub. Preserve its fingerprint and history; do not create a replacement identity.'
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
await verifySample(records[0]!, result, request, now);
|
|
105
|
+
} catch {
|
|
106
|
+
result.checks.push(
|
|
107
|
+
check(
|
|
108
|
+
'api',
|
|
109
|
+
'fail',
|
|
110
|
+
'BESZEL_API_UNVERIFIED',
|
|
111
|
+
'Verify private reachability and a valid hub account with access to this node. Raw HTTP bodies are withheld.'
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** No redirects, browser, token minting, retries or response logging. */
|
|
119
|
+
export function beszelReader(
|
|
120
|
+
hub: string,
|
|
121
|
+
token: string,
|
|
122
|
+
fetcher: typeof fetch = fetch
|
|
123
|
+
): BeszelRequest {
|
|
124
|
+
const origin = privateHub(hub);
|
|
125
|
+
return async (path) => {
|
|
126
|
+
if (!path.startsWith('/api/')) throw new Error('BESZEL_API_PATH');
|
|
127
|
+
const response = await fetcher(`${origin}${path}`, {
|
|
128
|
+
headers: { Authorization: token },
|
|
129
|
+
redirect: 'error',
|
|
130
|
+
signal: AbortSignal.timeout(10_000),
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) throw new Error('BESZEL_API_UNVERIFIED');
|
|
133
|
+
const text = await response.text();
|
|
134
|
+
if (text.length > 256_000) throw new Error('BESZEL_RESPONSE_LIMIT');
|
|
135
|
+
return JSON.parse(text) as unknown;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function verifySample(
|
|
140
|
+
record: Row,
|
|
141
|
+
result: BeszelEvidence,
|
|
142
|
+
request: BeszelRequest,
|
|
143
|
+
now: number
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const id = record['id'];
|
|
146
|
+
if (typeof id !== 'string' || !/^[a-z0-9]{15}$/.test(id))
|
|
147
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
148
|
+
result.checks.push(
|
|
149
|
+
check(
|
|
150
|
+
'registration',
|
|
151
|
+
'pass',
|
|
152
|
+
'Exactly one canonical node record is visible to this account.',
|
|
153
|
+
'None.'
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
result.checks.push(
|
|
157
|
+
check(
|
|
158
|
+
'connection',
|
|
159
|
+
record['status'] === 'up' ? 'pass' : 'fail',
|
|
160
|
+
record['status'] === 'up'
|
|
161
|
+
? 'Hub reports node connected.'
|
|
162
|
+
: 'BESZEL_AGENT_DISCONNECTED',
|
|
163
|
+
'Inspect agent service, private hub connectivity and node-specific token; preserve its DATA_DIR.'
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
await checkFreshness(id, result, request, now);
|
|
167
|
+
}
|
|
168
|
+
async function checkFreshness(
|
|
169
|
+
id: string,
|
|
170
|
+
result: BeszelEvidence,
|
|
171
|
+
request: BeszelRequest,
|
|
172
|
+
now: number
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const stats = items(
|
|
175
|
+
await request(
|
|
176
|
+
`/api/collections/system_stats/records?filter=${encodeURIComponent(`system="${id}" && type="1m"`)}&sort=-created&perPage=1&fields=created`
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
const created = stats[0]?.['created'];
|
|
180
|
+
const timestamp = typeof created === 'string' ? Date.parse(created) : NaN;
|
|
181
|
+
const fresh =
|
|
182
|
+
Number.isFinite(timestamp) &&
|
|
183
|
+
now - timestamp <= 180_000 &&
|
|
184
|
+
now >= timestamp - 30_000;
|
|
185
|
+
result.receivedAt = Number.isFinite(timestamp)
|
|
186
|
+
? new Date(timestamp).toISOString()
|
|
187
|
+
: null;
|
|
188
|
+
result.checks.push(
|
|
189
|
+
check(
|
|
190
|
+
'freshness',
|
|
191
|
+
fresh ? 'pass' : 'fail',
|
|
192
|
+
fresh
|
|
193
|
+
? `Hub received sample ${result.receivedAt}; within 180 seconds.`
|
|
194
|
+
: 'BESZEL_METRICS_STALE',
|
|
195
|
+
'Check hub and node clocks, agent connection and disk capacity. Rerun after the next sampling interval.'
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { record } from '../config';
|
|
3
|
+
import { privateHub } from './beszel';
|
|
4
|
+
import lock from '../../ansible/monitoring/lock.json';
|
|
5
|
+
|
|
6
|
+
export type HubRequest = (path: string, body?: object) => Promise<unknown>;
|
|
7
|
+
export interface AgentEnrollment {
|
|
8
|
+
systemId: string;
|
|
9
|
+
hubUrl: string;
|
|
10
|
+
publicKey: string;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
function rows(value: unknown): Record<string, unknown>[] {
|
|
14
|
+
if (
|
|
15
|
+
!record(value) ||
|
|
16
|
+
!Array.isArray(value['items']) ||
|
|
17
|
+
!value['items'].every(record)
|
|
18
|
+
)
|
|
19
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
20
|
+
return value['items'];
|
|
21
|
+
}
|
|
22
|
+
function identifier(value: unknown): string {
|
|
23
|
+
if (typeof value !== 'string' || !/^[a-z0-9]{15}$/.test(value))
|
|
24
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/** Mutating API adapter: call only after fleet approval and under manager lock.
|
|
28
|
+
* Uses the same systems/fingerprints API as upstream Add System, never its DB.
|
|
29
|
+
* The returned token is secret material: pass only through private SSH stdin.
|
|
30
|
+
*/
|
|
31
|
+
export async function enrollBeszel(
|
|
32
|
+
input: { node: string; hub: string; userId: string; host: string },
|
|
33
|
+
request: HubRequest
|
|
34
|
+
): Promise<AgentEnrollment> {
|
|
35
|
+
const hubUrl = privateHub(input.hub);
|
|
36
|
+
if (
|
|
37
|
+
!/^[a-z][a-z0-9-]{0,62}$/.test(input.node) ||
|
|
38
|
+
!/^[a-z0-9.-]+\.ts\.net$/.test(input.host)
|
|
39
|
+
)
|
|
40
|
+
throw new Error('BESZEL_NODE_INVALID');
|
|
41
|
+
const info = await hubInfo(request);
|
|
42
|
+
const systemId = await ensureSystem(input, request);
|
|
43
|
+
const fingerprints = rows(
|
|
44
|
+
await request(
|
|
45
|
+
`/api/collections/fingerprints/records?filter=${encodeURIComponent(`system="${systemId}"`)}&perPage=2`
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
if (fingerprints.length > 1) throw new Error('BESZEL_DUPLICATE_FINGERPRINT');
|
|
49
|
+
let token = fingerprints[0]?.['token'];
|
|
50
|
+
if (fingerprints.length === 0) {
|
|
51
|
+
token = randomUUID();
|
|
52
|
+
await request('/api/collections/fingerprints/records', {
|
|
53
|
+
id: createHash('sha256')
|
|
54
|
+
.update(`personal:fingerprint:${systemId}`)
|
|
55
|
+
.digest('hex')
|
|
56
|
+
.slice(0, 15),
|
|
57
|
+
system: systemId,
|
|
58
|
+
token,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
typeof token !== 'string' ||
|
|
63
|
+
!/^[a-zA-Z0-9_-][a-zA-Z0-9_-]{15,63}$/.test(token)
|
|
64
|
+
)
|
|
65
|
+
throw new Error('BESZEL_TOKEN_INVALID');
|
|
66
|
+
return { systemId, hubUrl, publicKey: info['key'], token };
|
|
67
|
+
}
|
|
68
|
+
async function ensureSystem(
|
|
69
|
+
input: { node: string; userId: string; host: string },
|
|
70
|
+
request: HubRequest
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const systems = rows(
|
|
73
|
+
await request(
|
|
74
|
+
`/api/collections/systems/records?filter=${encodeURIComponent(`name="${input.node}"`)}&perPage=2`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
if (systems.length > 1) throw new Error('BESZEL_DUPLICATE_NODE');
|
|
78
|
+
if (systems[0]) {
|
|
79
|
+
if (systems[0]['host'] !== input.host)
|
|
80
|
+
throw new Error('BESZEL_HOST_CONFLICT');
|
|
81
|
+
return identifier(systems[0]['id']);
|
|
82
|
+
}
|
|
83
|
+
const id = createHash('sha256')
|
|
84
|
+
.update(`personal:system:${input.node}`)
|
|
85
|
+
.digest('hex')
|
|
86
|
+
.slice(0, 15);
|
|
87
|
+
const created = await request('/api/collections/systems/records', {
|
|
88
|
+
id,
|
|
89
|
+
name: input.node,
|
|
90
|
+
host: input.host,
|
|
91
|
+
port: '45876',
|
|
92
|
+
users: [identifier(input.userId)],
|
|
93
|
+
});
|
|
94
|
+
if (!record(created) || created['id'] !== id)
|
|
95
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function hubInfo(
|
|
100
|
+
request: HubRequest
|
|
101
|
+
): Promise<Record<string, unknown> & { key: string }> {
|
|
102
|
+
const info = await request('/api/beszel/info');
|
|
103
|
+
if (
|
|
104
|
+
!record(info) ||
|
|
105
|
+
info['v'] !== lock.version ||
|
|
106
|
+
typeof info['key'] !== 'string' ||
|
|
107
|
+
!info['key'].startsWith('ssh-ed25519 ')
|
|
108
|
+
)
|
|
109
|
+
throw new Error('BESZEL_VERSION_OR_KEY');
|
|
110
|
+
return info as Record<string, unknown> & { key: string };
|
|
111
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { privateHub } from './beszel';
|
|
2
|
+
import type { HubRequest } from './enroll';
|
|
3
|
+
import { record } from '../config';
|
|
4
|
+
import { fleet, secret } from '../fleet-config';
|
|
5
|
+
|
|
6
|
+
/** The normal (non-superuser) Beszel account the manager enrolls nodes with.
|
|
7
|
+
* Its password is the fleet's monitoring-account-password secret. A fresh hub on the
|
|
8
|
+
* manager creates the account itself on first start (hub-darwin.yml).
|
|
9
|
+
*/
|
|
10
|
+
export const fleetIdentity = (): string =>
|
|
11
|
+
fleet.services.monitoring.managerAccount;
|
|
12
|
+
|
|
13
|
+
/** Read-only Beszel user the hub signs every visitor in as (AUTO_LOGIN), so the
|
|
14
|
+
* dashboard needs no login. Create it once in the hub (Settings > Users, role
|
|
15
|
+
* "Read-only"); it holds no secret and must never be an admin. If the account
|
|
16
|
+
* does not exist the hub simply shows its normal login page.
|
|
17
|
+
*/
|
|
18
|
+
export const publicViewerIdentity = (): string =>
|
|
19
|
+
fleet.services.monitoring.viewerAccount;
|
|
20
|
+
|
|
21
|
+
/** Accept the plain password, or a legacy JSON identity/password pair. */
|
|
22
|
+
export function fleetCredentials(secret: string): {
|
|
23
|
+
identity: string;
|
|
24
|
+
password: string;
|
|
25
|
+
} {
|
|
26
|
+
const value = secret.trim();
|
|
27
|
+
if (!value) throw new Error('BESZEL_ACCOUNT_INVALID');
|
|
28
|
+
if (value.startsWith('{')) {
|
|
29
|
+
let parsed: unknown;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(value);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error('BESZEL_ACCOUNT_INVALID');
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
!record(parsed) ||
|
|
37
|
+
typeof parsed['identity'] !== 'string' ||
|
|
38
|
+
typeof parsed['password'] !== 'string'
|
|
39
|
+
)
|
|
40
|
+
throw new Error('BESZEL_ACCOUNT_INVALID');
|
|
41
|
+
return { identity: parsed['identity'], password: parsed['password'] };
|
|
42
|
+
}
|
|
43
|
+
return { identity: fleetIdentity(), password: value };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The fleet login from the manager's Vault session, or null when absent.
|
|
47
|
+
* Used to create the account on a hub's first start; never printed.
|
|
48
|
+
*/
|
|
49
|
+
export async function fleetAccount(): Promise<{
|
|
50
|
+
identity: string;
|
|
51
|
+
password: string;
|
|
52
|
+
} | null> {
|
|
53
|
+
const password = await secret('monitoring-account-password');
|
|
54
|
+
try {
|
|
55
|
+
return password ? fleetCredentials(password) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reads the existing account password only. This never creates credentials. The normal Beszel account must have fleet-node access.
|
|
62
|
+
*/
|
|
63
|
+
export async function hubAccess(
|
|
64
|
+
hub: string
|
|
65
|
+
): Promise<{ userId: string; request: HubRequest }> {
|
|
66
|
+
const url = privateHub(hub);
|
|
67
|
+
const password = await secret('monitoring-account-password');
|
|
68
|
+
if (!password) throw new Error('BESZEL_ACCOUNT_REQUIRED');
|
|
69
|
+
const credentials = fleetCredentials(password);
|
|
70
|
+
const login = await call(
|
|
71
|
+
url,
|
|
72
|
+
'/api/collections/users/auth-with-password',
|
|
73
|
+
'',
|
|
74
|
+
credentials
|
|
75
|
+
);
|
|
76
|
+
if (
|
|
77
|
+
!record(login) ||
|
|
78
|
+
typeof login['token'] !== 'string' ||
|
|
79
|
+
!record(login['record']) ||
|
|
80
|
+
typeof login['record']['id'] !== 'string'
|
|
81
|
+
)
|
|
82
|
+
throw new Error('BESZEL_AUTH_REQUIRED');
|
|
83
|
+
const token = login['token'];
|
|
84
|
+
return {
|
|
85
|
+
userId: login['record']['id'],
|
|
86
|
+
request: (path, body) => call(url, path, token, body),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function call(
|
|
90
|
+
url: string,
|
|
91
|
+
path: string,
|
|
92
|
+
token: string,
|
|
93
|
+
body?: object
|
|
94
|
+
): Promise<unknown> {
|
|
95
|
+
if (!path.startsWith('/api/')) throw new Error('BESZEL_API_PATH');
|
|
96
|
+
const response = await fetch(`${url}${path}`, {
|
|
97
|
+
method: body ? 'POST' : 'GET',
|
|
98
|
+
headers: { Authorization: token, 'Content-Type': 'application/json' },
|
|
99
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
100
|
+
redirect: 'error',
|
|
101
|
+
signal: AbortSignal.timeout(10_000),
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) throw new Error(failure(response.status, path));
|
|
104
|
+
return response.json() as Promise<unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** PocketBase answers a rejected password login with 400, not 401. */
|
|
108
|
+
export function failure(status: number, path: string): string {
|
|
109
|
+
if (status === 400 && path.endsWith('/auth-with-password'))
|
|
110
|
+
return 'BESZEL_ACCOUNT_REQUIRED';
|
|
111
|
+
return status === 401 || status === 403
|
|
112
|
+
? 'BESZEL_AUTH_REQUIRED'
|
|
113
|
+
: 'BESZEL_API_UNAVAILABLE';
|
|
114
|
+
}
|