@matthesketh/fleet 1.8.1 → 1.11.1
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/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/index.js +0 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +9 -6
- package/dist/tui/views/HealthView.js +9 -4
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +8 -5
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* gate, and rolls back on failure. Pure functions where possible — I/O isolated
|
|
5
5
|
* to thin wrappers so tests can run without a real vault.
|
|
6
6
|
*/
|
|
7
|
-
import { decryptApp, sealApp, loadManifest } from './secrets.js';
|
|
7
|
+
import { decryptApp, sealApp, loadManifest, lockManifest } from './secrets.js';
|
|
8
8
|
import { snapshotApp, restoreSnapshot } from './secrets-snapshots.js';
|
|
9
9
|
import { auditLog } from './secrets-audit.js';
|
|
10
10
|
import { markRotated } from './secrets-metadata.js';
|
|
@@ -108,58 +108,64 @@ export function applyRotation(plaintext, key, newValue, strategy) {
|
|
|
108
108
|
* snapshot → seal → audit. Restart + health-gate are caller's responsibility
|
|
109
109
|
* (we want the engine pure-ish so it's easy to test).
|
|
110
110
|
*/
|
|
111
|
-
export function performRotation(app, key, newValue, opts = {}) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
// 3. Stamp metadata.
|
|
142
|
-
markRotated(app, key, { strategy, notes: opts.notes });
|
|
143
|
-
auditLog({ op: 'rotate', app, secret: key, ok: true, details: `strategy=${strategy}` });
|
|
144
|
-
return { app, key, strategy, snapshot, rolledBack: false };
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
148
|
-
// Restore from snapshot on any failure.
|
|
111
|
+
export async function performRotation(app, key, newValue, opts = {}) {
|
|
112
|
+
// Hold the manifest lock for the whole snapshot → seal → markRotated cycle
|
|
113
|
+
// so a concurrent CLI/cron writer can't slip a stale write between our
|
|
114
|
+
// seal and our metadata stamp. markRotated calls saveManifest internally;
|
|
115
|
+
// that write happens under our lock.
|
|
116
|
+
return await lockManifest(() => {
|
|
117
|
+
const manifest = loadManifest();
|
|
118
|
+
const entry = manifest.apps[app];
|
|
119
|
+
if (!entry)
|
|
120
|
+
throw new SecretsError(`No app in manifest: ${app}`);
|
|
121
|
+
if (entry.type !== 'env') {
|
|
122
|
+
throw new SecretsError(`Rotation only supports env-type apps, got ${entry.type}`);
|
|
123
|
+
}
|
|
124
|
+
const provider = classifySecret(key);
|
|
125
|
+
const strategy = provider?.strategy ?? 'immediate';
|
|
126
|
+
if (strategy === 'user-issued') {
|
|
127
|
+
throw new SecretsError(`${key} is a user-issued token. Rotating yours doesn't help — invalidate per-user instead.`);
|
|
128
|
+
}
|
|
129
|
+
// Strict typed opt — was previously a substring match on opts.notes which
|
|
130
|
+
// could be bypassed by any caller embedding the flag in free-text notes.
|
|
131
|
+
if (strategy === 'at-rest-key' && !opts.dataMigrated) {
|
|
132
|
+
throw new SecretsError(`${key} encrypts data at rest. Re-encrypt your data first, then pass --data-migrated.`);
|
|
133
|
+
}
|
|
134
|
+
if (opts.dryRun) {
|
|
135
|
+
auditLog({ op: 'rotate-attempted', app, secret: key, ok: true, details: 'dry-run' });
|
|
136
|
+
return { app, key, strategy, snapshot: '(dry-run)', rolledBack: false };
|
|
137
|
+
}
|
|
138
|
+
// 1. Snapshot before any change.
|
|
139
|
+
const snapshot = snapshotApp(app);
|
|
140
|
+
auditLog({ op: 'snapshot', app, secret: key, ok: true, details: snapshot });
|
|
149
141
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
// 2. Decrypt, apply rotation, re-encrypt.
|
|
143
|
+
const plaintext = decryptApp(app);
|
|
144
|
+
const updated = applyRotation(plaintext, key, newValue, strategy);
|
|
145
|
+
sealApp(app, updated, entry.sourceFile);
|
|
146
|
+
// 3. Stamp metadata.
|
|
147
|
+
markRotated(app, key, { strategy, notes: opts.notes });
|
|
148
|
+
auditLog({ op: 'rotate', app, secret: key, ok: true, details: `strategy=${strategy}` });
|
|
149
|
+
return { app, key, strategy, snapshot, rolledBack: false };
|
|
152
150
|
}
|
|
153
|
-
catch (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
ok:
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
catch (err) {
|
|
152
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
153
|
+
// Restore from snapshot on any failure.
|
|
154
|
+
try {
|
|
155
|
+
restoreSnapshot(app);
|
|
156
|
+
auditLog({ op: 'rollback', app, secret: key, ok: true, details: `auto: ${reason}` });
|
|
157
|
+
}
|
|
158
|
+
catch (rollbackErr) {
|
|
159
|
+
auditLog({
|
|
160
|
+
op: 'rollback',
|
|
161
|
+
app,
|
|
162
|
+
secret: key,
|
|
163
|
+
ok: false,
|
|
164
|
+
details: `auto rollback also failed: ${rollbackErr}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
auditLog({ op: 'rotate-failed', app, secret: key, ok: false, details: reason });
|
|
168
|
+
return { app, key, strategy, snapshot, rolledBack: true, reason };
|
|
161
169
|
}
|
|
162
|
-
|
|
163
|
-
return { app, key, strategy, snapshot, rolledBack: true, reason };
|
|
164
|
-
}
|
|
170
|
+
});
|
|
165
171
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface CleanupOpts {
|
|
2
|
+
app: string;
|
|
3
|
+
retentionDays?: number;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface CleanupResult {
|
|
7
|
+
app: string;
|
|
8
|
+
removedBak: boolean;
|
|
9
|
+
removedSnapshots: string[];
|
|
10
|
+
keptSnapshots: string[];
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* parse a filesystem-safe snapshot timestamp back to a Date.
|
|
15
|
+
* input: '2026-05-06T12-00-00-000Z' (colons and dots replaced with dashes)
|
|
16
|
+
* output: Date('2026-05-06T12:00:00.000Z'), or null if unparseable
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseSnapshotTimestamp(ts: string): Date | null;
|
|
19
|
+
export declare function cleanupV2Backups(opts: CleanupOpts): Promise<CleanupResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { existsSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { findApp, load } from './registry.js';
|
|
6
|
+
import { listSnapshots } from './secrets-v2-snapshot.js';
|
|
7
|
+
import { SecretsError } from './errors.js';
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
/** resolve vault dir: env override (used in tests) or the default computed path */
|
|
10
|
+
function resolveVaultDir() {
|
|
11
|
+
return process.env.FLEET_VAULT_DIR ?? join(__dirname, '..', '..', 'vault');
|
|
12
|
+
}
|
|
13
|
+
/** read manifest directly without calling requireInit() */
|
|
14
|
+
function readManifest(vaultDir) {
|
|
15
|
+
const p = join(vaultDir, 'manifest.json');
|
|
16
|
+
if (!existsSync(p))
|
|
17
|
+
return { version: 1, apps: {} };
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { version: 1, apps: {} };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* parse a filesystem-safe snapshot timestamp back to a Date.
|
|
27
|
+
* input: '2026-05-06T12-00-00-000Z' (colons and dots replaced with dashes)
|
|
28
|
+
* output: Date('2026-05-06T12:00:00.000Z'), or null if unparseable
|
|
29
|
+
*/
|
|
30
|
+
export function parseSnapshotTimestamp(ts) {
|
|
31
|
+
const m = ts.match(/^(\d{4}-\d{2}-\d{2}T)(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/);
|
|
32
|
+
if (!m)
|
|
33
|
+
return null;
|
|
34
|
+
return new Date(`${m[1]}${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`);
|
|
35
|
+
}
|
|
36
|
+
export async function cleanupV2Backups(opts) {
|
|
37
|
+
const { app, retentionDays = 30, dryRun = false } = opts;
|
|
38
|
+
const registry = load();
|
|
39
|
+
const appEntry = findApp(registry, app);
|
|
40
|
+
if (!appEntry) {
|
|
41
|
+
throw new SecretsError(`app '${app}' not found in fleet registry`);
|
|
42
|
+
}
|
|
43
|
+
const vaultDir = resolveVaultDir();
|
|
44
|
+
const manifest = readManifest(vaultDir);
|
|
45
|
+
const entry = manifest.apps[app];
|
|
46
|
+
if (!entry || entry.mode !== 'socket') {
|
|
47
|
+
throw new SecretsError(`app '${app}' is not in v2 mode; cleanup is for post-v2-migration apps only`);
|
|
48
|
+
}
|
|
49
|
+
const cutoff = Date.now() - retentionDays * 86_400_000;
|
|
50
|
+
const backupRoot = join(vaultDir, 'backups');
|
|
51
|
+
const snapshots = listSnapshots(backupRoot, app);
|
|
52
|
+
const removedSnapshots = [];
|
|
53
|
+
const keptSnapshots = [];
|
|
54
|
+
for (const snap of snapshots) {
|
|
55
|
+
const ts = parseSnapshotTimestamp(snap.timestamp);
|
|
56
|
+
if (!ts) {
|
|
57
|
+
// unparseable timestamp — keep rather than risk destroying unknown content
|
|
58
|
+
keptSnapshots.push(snap.timestamp);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ts.getTime() < cutoff) {
|
|
62
|
+
removedSnapshots.push(snap.timestamp);
|
|
63
|
+
if (!dryRun) {
|
|
64
|
+
rmSync(snap.dir, { recursive: true, force: true });
|
|
65
|
+
// best-effort: remove the parent timestamp dir if it's now empty
|
|
66
|
+
const parentDir = dirname(snap.dir);
|
|
67
|
+
try {
|
|
68
|
+
const remaining = readdirSync(parentDir);
|
|
69
|
+
if (remaining.length === 0)
|
|
70
|
+
rmSync(parentDir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
keptSnapshots.push(snap.timestamp);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let removedBak = false;
|
|
80
|
+
const bakPath = join(vaultDir, `${app}.env.age.v1.bak`);
|
|
81
|
+
if (existsSync(bakPath)) {
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(bakPath);
|
|
85
|
+
removedBak = true;
|
|
86
|
+
}
|
|
87
|
+
catch { /* best-effort */ }
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
removedBak = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { app, removedBak, removedSnapshots, keptSnapshots, dryRun };
|
|
94
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const CRED_DIR = "/etc/fleet/credentials";
|
|
2
|
+
export declare function credentialPathFor(app: string): string;
|
|
3
|
+
export declare function encryptCredential(args: {
|
|
4
|
+
name: string;
|
|
5
|
+
plaintext: string;
|
|
6
|
+
outputPath: string;
|
|
7
|
+
}): void;
|
|
8
|
+
export declare function credentialExists(app: string): boolean;
|
|
9
|
+
export declare function removeCredential(app: string): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { execSafe } from './exec.js';
|
|
4
|
+
import { SecretsError } from './errors.js';
|
|
5
|
+
export const CRED_DIR = '/etc/fleet/credentials';
|
|
6
|
+
export function credentialPathFor(app) {
|
|
7
|
+
const p = join(CRED_DIR, `${app}.cred`);
|
|
8
|
+
if (!p.startsWith(CRED_DIR + '/')) {
|
|
9
|
+
throw new SecretsError(`invalid app name: ${app}`);
|
|
10
|
+
}
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
export function encryptCredential(args) {
|
|
14
|
+
const dir = dirname(args.outputPath);
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
const r = execSafe('systemd-creds', ['encrypt', '--name', args.name, '-', args.outputPath], { input: args.plaintext });
|
|
19
|
+
if (!r.ok) {
|
|
20
|
+
const safeStderr = args.plaintext.length > 0
|
|
21
|
+
? r.stderr.split(args.plaintext).join('[redacted]')
|
|
22
|
+
: r.stderr;
|
|
23
|
+
throw new SecretsError(`systemd-creds encrypt failed: ${safeStderr}`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
chmodSync(args.outputPath, 0o600);
|
|
27
|
+
}
|
|
28
|
+
catch (chmodErr) {
|
|
29
|
+
try {
|
|
30
|
+
unlinkSync(args.outputPath);
|
|
31
|
+
}
|
|
32
|
+
catch { /* ignore */ }
|
|
33
|
+
throw new SecretsError(`chmod failed for ${args.outputPath}: ${chmodErr.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function credentialExists(app) {
|
|
37
|
+
return existsSync(credentialPathFor(app));
|
|
38
|
+
}
|
|
39
|
+
export function removeCredential(app) {
|
|
40
|
+
const p = credentialPathFor(app);
|
|
41
|
+
if (existsSync(p)) {
|
|
42
|
+
unlinkSync(p);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface InstallResult {
|
|
2
|
+
agentBinaryInstalled: boolean;
|
|
3
|
+
unitFileInstalled: boolean;
|
|
4
|
+
daemonReloaded: boolean;
|
|
5
|
+
templateParseable: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function installV2(opts?: {
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
agentSourcePath?: string;
|
|
10
|
+
destBinaryPath?: string;
|
|
11
|
+
unitFilePath?: string;
|
|
12
|
+
vaultPath?: string;
|
|
13
|
+
}): Promise<InstallResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execSafe } from './exec.js';
|
|
5
|
+
import { SecretsError } from './errors.js';
|
|
6
|
+
import { generateAgentUnit } from '../templates/agent-unit.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DEFAULT_AGENT_SOURCE = join(__dirname, '..', 'bin', 'fleet-agent.js');
|
|
9
|
+
const DEFAULT_BINARY_DEST = '/usr/local/bin/fleet-agent';
|
|
10
|
+
const DEFAULT_UNIT_PATH = '/etc/systemd/system/fleet-secrets-agent@.service';
|
|
11
|
+
/** vault dir resolution. mirrors secrets-v2-cleanup.ts — env override
|
|
12
|
+
* takes precedence so tests and bespoke deploys both work without
|
|
13
|
+
* hardcoding an operator-specific path into the systemd unit. */
|
|
14
|
+
function resolveVaultDir() {
|
|
15
|
+
return process.env.FLEET_VAULT_DIR ?? join(__dirname, '..', '..', 'vault');
|
|
16
|
+
}
|
|
17
|
+
export async function installV2(opts = {}) {
|
|
18
|
+
const sourcePath = opts.agentSourcePath ?? DEFAULT_AGENT_SOURCE;
|
|
19
|
+
const destPath = opts.destBinaryPath ?? DEFAULT_BINARY_DEST;
|
|
20
|
+
const unitPath = opts.unitFilePath ?? DEFAULT_UNIT_PATH;
|
|
21
|
+
const vaultPath = opts.vaultPath ?? resolveVaultDir();
|
|
22
|
+
const dryRun = opts.dryRun ?? false;
|
|
23
|
+
if (!existsSync(sourcePath)) {
|
|
24
|
+
throw new SecretsError(`agent binary source not found at ${sourcePath} — run 'npm run build' first`);
|
|
25
|
+
}
|
|
26
|
+
const sourceContent = readFileSync(sourcePath);
|
|
27
|
+
const result = {
|
|
28
|
+
agentBinaryInstalled: false,
|
|
29
|
+
unitFileInstalled: false,
|
|
30
|
+
daemonReloaded: false,
|
|
31
|
+
templateParseable: false,
|
|
32
|
+
};
|
|
33
|
+
// install binary if changed (byte-equal comparison)
|
|
34
|
+
let needBinaryWrite = !existsSync(destPath);
|
|
35
|
+
if (!needBinaryWrite) {
|
|
36
|
+
const existingContent = readFileSync(destPath);
|
|
37
|
+
needBinaryWrite = !existingContent.equals(sourceContent);
|
|
38
|
+
}
|
|
39
|
+
if (needBinaryWrite) {
|
|
40
|
+
if (!dryRun) {
|
|
41
|
+
copyFileSync(sourcePath, destPath);
|
|
42
|
+
chmodSync(destPath, 0o755);
|
|
43
|
+
}
|
|
44
|
+
result.agentBinaryInstalled = true;
|
|
45
|
+
}
|
|
46
|
+
// install unit file if changed (text-equal comparison)
|
|
47
|
+
const unitContent = generateAgentUnit(vaultPath);
|
|
48
|
+
let needUnitWrite = !existsSync(unitPath);
|
|
49
|
+
if (!needUnitWrite) {
|
|
50
|
+
const existing = readFileSync(unitPath, 'utf-8');
|
|
51
|
+
needUnitWrite = existing !== unitContent;
|
|
52
|
+
}
|
|
53
|
+
if (needUnitWrite) {
|
|
54
|
+
if (!dryRun) {
|
|
55
|
+
writeFileSync(unitPath, unitContent);
|
|
56
|
+
chmodSync(unitPath, 0o644);
|
|
57
|
+
}
|
|
58
|
+
result.unitFileInstalled = true;
|
|
59
|
+
}
|
|
60
|
+
// daemon-reload only if we wrote something
|
|
61
|
+
if ((result.agentBinaryInstalled || result.unitFileInstalled) && !dryRun) {
|
|
62
|
+
const r = execSafe('systemctl', ['daemon-reload']);
|
|
63
|
+
if (!r.ok)
|
|
64
|
+
throw new SecretsError(`systemctl daemon-reload failed: ${r.stderr}`);
|
|
65
|
+
result.daemonReloaded = true;
|
|
66
|
+
}
|
|
67
|
+
// verify template is parseable (soft check — not thrown on failure)
|
|
68
|
+
if (!dryRun) {
|
|
69
|
+
const r = execSafe('systemctl', ['cat', 'fleet-secrets-agent@verify.service']);
|
|
70
|
+
result.templateParseable = r.ok;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
result.templateParseable = true;
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Keypair {
|
|
2
|
+
publicKey: string;
|
|
3
|
+
privateKey: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function reencryptForRecipient(args: {
|
|
6
|
+
ciphertext: string;
|
|
7
|
+
oldKeyPath: string;
|
|
8
|
+
newRecipient: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function generateKeypair(): Keypair;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSafe } from './exec.js';
|
|
2
|
+
import { SecretsError } from './errors.js';
|
|
3
|
+
export function reencryptForRecipient(args) {
|
|
4
|
+
const dec = execSafe('age', ['-d', '-i', args.oldKeyPath], { input: args.ciphertext });
|
|
5
|
+
if (!dec.ok) {
|
|
6
|
+
throw new SecretsError(`decrypt failed: ${dec.stderr}`);
|
|
7
|
+
}
|
|
8
|
+
const plaintext = dec.stdout;
|
|
9
|
+
const enc = execSafe('age', ['-r', args.newRecipient, '--armor'], { input: plaintext });
|
|
10
|
+
if (!enc.ok) {
|
|
11
|
+
throw new SecretsError(`encrypt failed: ${enc.stderr}`);
|
|
12
|
+
}
|
|
13
|
+
return enc.stdout;
|
|
14
|
+
}
|
|
15
|
+
export function generateKeypair() {
|
|
16
|
+
const r = execSafe('age-keygen', []);
|
|
17
|
+
if (!r.ok)
|
|
18
|
+
throw new SecretsError(`age-keygen failed: ${r.stderr}`);
|
|
19
|
+
const lines = r.stdout.split('\n');
|
|
20
|
+
const pub = lines.find(l => l.startsWith('# public key: '))?.slice('# public key: '.length).trim();
|
|
21
|
+
const priv = lines.find(l => l.startsWith('AGE-SECRET-KEY-'))?.trim();
|
|
22
|
+
if (!pub || !priv) {
|
|
23
|
+
const safeOut = r.stdout
|
|
24
|
+
.split('\n')
|
|
25
|
+
.filter(l => !l.includes('AGE-SECRET-KEY-'))
|
|
26
|
+
.join('\n')
|
|
27
|
+
.slice(0, 200);
|
|
28
|
+
throw new SecretsError(`could not parse age-keygen output: ${safeOut}`);
|
|
29
|
+
}
|
|
30
|
+
return { publicKey: pub, privateKey: priv };
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface MigrateOpts {
|
|
2
|
+
app: string;
|
|
3
|
+
noRestartApp?: boolean;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface MigrateStep {
|
|
7
|
+
step: number;
|
|
8
|
+
name: string;
|
|
9
|
+
ok: boolean;
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MigrateResult {
|
|
13
|
+
app: string;
|
|
14
|
+
snapshotDir: string | null;
|
|
15
|
+
steps: MigrateStep[];
|
|
16
|
+
rolledBack: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function migrateAppToV2(opts: MigrateOpts): Promise<MigrateResult>;
|
|
19
|
+
export interface RevertOpts {
|
|
20
|
+
app: string;
|
|
21
|
+
snapshotTimestamp?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RevertResult {
|
|
24
|
+
app: string;
|
|
25
|
+
snapshotUsed: string;
|
|
26
|
+
steps: MigrateStep[];
|
|
27
|
+
ok: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function revertAppFromV2(opts: RevertOpts): Promise<RevertResult>;
|