@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface SnapshotInput {
|
|
2
|
+
app: string;
|
|
3
|
+
backupRoot: string;
|
|
4
|
+
vaultDir: string;
|
|
5
|
+
encryptedFile: string;
|
|
6
|
+
composeDir: string;
|
|
7
|
+
composeFile: string;
|
|
8
|
+
appUnitFile: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Snapshot {
|
|
11
|
+
app: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
dir: string;
|
|
14
|
+
manifestEntry: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* capture a timestamped backup of all four artefacts for an app:
|
|
18
|
+
* - encrypted vault blob
|
|
19
|
+
* - manifest entry (as manifest.json in the snapshot dir)
|
|
20
|
+
* - compose file
|
|
21
|
+
* - systemd unit (if it exists)
|
|
22
|
+
*/
|
|
23
|
+
export declare function snapshotApp(input: SnapshotInput): Snapshot;
|
|
24
|
+
/**
|
|
25
|
+
* restore all four artefacts from a snapshot back to their original locations.
|
|
26
|
+
* the manifest is merged: only apps[app] is replaced, other apps are untouched.
|
|
27
|
+
* if the unit file wasn't captured (didn't exist at snapshot time), no unit is written.
|
|
28
|
+
*/
|
|
29
|
+
export declare function restoreSnapshot(input: SnapshotInput, snap: Snapshot): void;
|
|
30
|
+
/**
|
|
31
|
+
* walk backupRoot looking for <timestamp>/<app>/ directories.
|
|
32
|
+
* returns snapshots sorted newest-first by timestamp string (lexicographic, which
|
|
33
|
+
* works correctly for the ISO-safe format with dashes instead of colons/dots).
|
|
34
|
+
* returns [] if backupRoot doesn't exist.
|
|
35
|
+
*/
|
|
36
|
+
export declare function listSnapshots(backupRoot: string, app: string): Snapshot[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
// callers must serialise migration operations: same-millisecond invocations
|
|
4
|
+
// would write to the same backup dir and silently overwrite each other.
|
|
5
|
+
// migrate-v2 (Task 21) is the only caller and is invoked one app at a time.
|
|
6
|
+
function makeTimestamp() {
|
|
7
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* capture a timestamped backup of all four artefacts for an app:
|
|
11
|
+
* - encrypted vault blob
|
|
12
|
+
* - manifest entry (as manifest.json in the snapshot dir)
|
|
13
|
+
* - compose file
|
|
14
|
+
* - systemd unit (if it exists)
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotApp(input) {
|
|
17
|
+
const { app, backupRoot, vaultDir, encryptedFile, composeDir, composeFile, appUnitFile } = input;
|
|
18
|
+
// ensure backupRoot exists at 0700 — mkdirSync only sets mode on newly
|
|
19
|
+
// created dirs; if it pre-existed with a looser mode, chmod it explicitly.
|
|
20
|
+
if (!existsSync(backupRoot)) {
|
|
21
|
+
mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
chmodSync(backupRoot, 0o700);
|
|
25
|
+
}
|
|
26
|
+
const timestamp = makeTimestamp();
|
|
27
|
+
const snapDir = join(backupRoot, timestamp, app);
|
|
28
|
+
mkdirSync(snapDir, { recursive: true, mode: 0o700 });
|
|
29
|
+
// 1. encrypted vault blob
|
|
30
|
+
copyFileSync(join(vaultDir, encryptedFile), join(snapDir, encryptedFile));
|
|
31
|
+
// 2. manifest entry for this app only
|
|
32
|
+
const manifestPath = join(vaultDir, 'manifest.json');
|
|
33
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
34
|
+
const manifestEntry = manifest.apps[app] ?? {};
|
|
35
|
+
writeFileSync(join(snapDir, 'manifest.json'), JSON.stringify(manifestEntry, null, 2));
|
|
36
|
+
// 3. compose file
|
|
37
|
+
copyFileSync(join(composeDir, composeFile), join(snapDir, composeFile));
|
|
38
|
+
// 4. systemd unit (omitted if it doesn't exist yet)
|
|
39
|
+
if (existsSync(appUnitFile)) {
|
|
40
|
+
copyFileSync(appUnitFile, join(snapDir, basename(appUnitFile)));
|
|
41
|
+
}
|
|
42
|
+
return { app, timestamp, dir: snapDir, manifestEntry };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* restore all four artefacts from a snapshot back to their original locations.
|
|
46
|
+
* the manifest is merged: only apps[app] is replaced, other apps are untouched.
|
|
47
|
+
* if the unit file wasn't captured (didn't exist at snapshot time), no unit is written.
|
|
48
|
+
*/
|
|
49
|
+
export function restoreSnapshot(input, snap) {
|
|
50
|
+
const { app, vaultDir, encryptedFile, composeDir, composeFile, appUnitFile } = input;
|
|
51
|
+
const { dir: snapDir } = snap;
|
|
52
|
+
// 1. encrypted vault blob
|
|
53
|
+
copyFileSync(join(snapDir, encryptedFile), join(vaultDir, encryptedFile));
|
|
54
|
+
// 2. manifest entry — merge back into live manifest, leaving other apps untouched
|
|
55
|
+
const snapEntryRaw = readFileSync(join(snapDir, 'manifest.json'), 'utf-8');
|
|
56
|
+
const snapEntry = JSON.parse(snapEntryRaw);
|
|
57
|
+
const manifestPath = join(vaultDir, 'manifest.json');
|
|
58
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
59
|
+
manifest.apps[app] = snapEntry;
|
|
60
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
61
|
+
// 3. compose file
|
|
62
|
+
copyFileSync(join(snapDir, composeFile), join(composeDir, composeFile));
|
|
63
|
+
// 4. systemd unit — only write back if it was captured in the snapshot
|
|
64
|
+
const unitBasename = basename(appUnitFile);
|
|
65
|
+
const snapUnit = join(snapDir, unitBasename);
|
|
66
|
+
if (existsSync(snapUnit)) {
|
|
67
|
+
copyFileSync(snapUnit, appUnitFile);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* walk backupRoot looking for <timestamp>/<app>/ directories.
|
|
72
|
+
* returns snapshots sorted newest-first by timestamp string (lexicographic, which
|
|
73
|
+
* works correctly for the ISO-safe format with dashes instead of colons/dots).
|
|
74
|
+
* returns [] if backupRoot doesn't exist.
|
|
75
|
+
*/
|
|
76
|
+
export function listSnapshots(backupRoot, app) {
|
|
77
|
+
if (!existsSync(backupRoot))
|
|
78
|
+
return [];
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const entry of readdirSync(backupRoot)) {
|
|
81
|
+
const tsDir = join(backupRoot, entry);
|
|
82
|
+
try {
|
|
83
|
+
if (!statSync(tsDir).isDirectory())
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const appDir = join(tsDir, app);
|
|
90
|
+
try {
|
|
91
|
+
if (!statSync(appDir).isDirectory())
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let manifestEntry = null;
|
|
98
|
+
const mPath = join(appDir, 'manifest.json');
|
|
99
|
+
if (existsSync(mPath)) {
|
|
100
|
+
try {
|
|
101
|
+
manifestEntry = JSON.parse(readFileSync(mPath, 'utf-8'));
|
|
102
|
+
}
|
|
103
|
+
catch { /* leave null */ }
|
|
104
|
+
}
|
|
105
|
+
results.push({
|
|
106
|
+
app,
|
|
107
|
+
timestamp: entry,
|
|
108
|
+
dir: appDir,
|
|
109
|
+
manifestEntry,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Sort newest-first — the timestamp format is lexicographically sortable
|
|
113
|
+
results.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const IDLE_TIMEOUT_MS = 30000;
|
|
2
|
+
export declare function _resetRateLimit(initialTokens?: number): void;
|
|
3
|
+
export declare function decryptVaultBlob(privateKeyPath: string, blobPath: string): Record<string, string>;
|
|
4
|
+
export interface AgentDeps {
|
|
5
|
+
app: string;
|
|
6
|
+
getSecrets: () => Record<string, string>;
|
|
7
|
+
refresh: () => void;
|
|
8
|
+
}
|
|
9
|
+
export interface Server {
|
|
10
|
+
listen(path: string): Promise<void>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createServer(deps: AgentDeps): Server;
|
|
14
|
+
export interface AgentArgs {
|
|
15
|
+
app: string;
|
|
16
|
+
vault: string;
|
|
17
|
+
socket: string;
|
|
18
|
+
credential?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseArgs(argv: string[]): AgentArgs;
|
|
21
|
+
export declare function main(argv: string[]): Promise<void>;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createServer as netCreateServer } from 'node:net';
|
|
2
|
+
import { existsSync, unlinkSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { execSafe } from './exec.js';
|
|
4
|
+
import { SecretsError } from './errors.js';
|
|
5
|
+
import { parseRequest, writeResponse, ProtocolError } from './secrets-v2-protocol.js';
|
|
6
|
+
export const IDLE_TIMEOUT_MS = 30_000;
|
|
7
|
+
const TERM = Buffer.from('\r\n\r\n');
|
|
8
|
+
// module-level token bucket — limits total throughput to 100 req/sec across all connections
|
|
9
|
+
let _tokens = 100;
|
|
10
|
+
let _lastRefill = Date.now();
|
|
11
|
+
function takeToken() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const elapsed = (now - _lastRefill) / 1000;
|
|
14
|
+
if (elapsed > 0) {
|
|
15
|
+
_tokens = Math.min(100, _tokens + elapsed * 100);
|
|
16
|
+
_lastRefill = now;
|
|
17
|
+
}
|
|
18
|
+
if (_tokens < 1)
|
|
19
|
+
return false;
|
|
20
|
+
_tokens -= 1;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
export function _resetRateLimit(initialTokens = 100) {
|
|
24
|
+
_tokens = initialTokens;
|
|
25
|
+
_lastRefill = Date.now();
|
|
26
|
+
}
|
|
27
|
+
export function decryptVaultBlob(privateKeyPath, blobPath) {
|
|
28
|
+
if (!existsSync(blobPath))
|
|
29
|
+
throw new SecretsError(`vault blob not found: ${blobPath}`);
|
|
30
|
+
if (!existsSync(privateKeyPath))
|
|
31
|
+
throw new SecretsError(`private key not found: ${privateKeyPath}`);
|
|
32
|
+
const r = execSafe('age', ['-d', '-i', privateKeyPath, blobPath]);
|
|
33
|
+
if (!r.ok)
|
|
34
|
+
throw new SecretsError(`age decrypt failed: ${r.stderr}`);
|
|
35
|
+
return parseEnvFormat(r.stdout);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse plaintext env content into a key/value map.
|
|
39
|
+
*
|
|
40
|
+
* Note: callers receive `content` after `execSafe` has applied `.trim()` to
|
|
41
|
+
* the full stdout. Leading/trailing whitespace at the start of the first
|
|
42
|
+
* line and end of the last line is consumed before parsing. Secret values
|
|
43
|
+
* with deliberate edge whitespace will be subtly altered. This is a known
|
|
44
|
+
* project-wide gotcha (also affects v1 secrets); see brain note q5YkhSmRVx9m.
|
|
45
|
+
*/
|
|
46
|
+
function parseEnvFormat(content) {
|
|
47
|
+
const map = {};
|
|
48
|
+
for (const rawLine of content.split('\n')) {
|
|
49
|
+
if (!rawLine.trim() || rawLine.startsWith('#'))
|
|
50
|
+
continue;
|
|
51
|
+
const i = rawLine.indexOf('=');
|
|
52
|
+
if (i > 0) {
|
|
53
|
+
map[rawLine.slice(0, i)] = rawLine.slice(i + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
const MAX_REQUEST_BYTES = 8192;
|
|
59
|
+
export function createServer(deps) {
|
|
60
|
+
const server = netCreateServer((sock) => handleConnection(sock, deps));
|
|
61
|
+
let socketPath = '';
|
|
62
|
+
return {
|
|
63
|
+
listen: (path) => new Promise((resolve, reject) => {
|
|
64
|
+
socketPath = path;
|
|
65
|
+
if (existsSync(path)) {
|
|
66
|
+
try {
|
|
67
|
+
unlinkSync(path);
|
|
68
|
+
}
|
|
69
|
+
catch { /* race; let listen fail naturally */ }
|
|
70
|
+
}
|
|
71
|
+
const onError = (err) => reject(err);
|
|
72
|
+
server.once('error', onError);
|
|
73
|
+
server.listen(path, () => {
|
|
74
|
+
server.off('error', onError);
|
|
75
|
+
// 0o660 (not 0o600) is deliberate: the systemd unit runs the agent
|
|
76
|
+
// as DynamicUser, and the consumer container bind-mounts this
|
|
77
|
+
// socket and connects as a separate uid sharing the systemd-
|
|
78
|
+
// allocated group. tightening to 0o600 would break that. if the
|
|
79
|
+
// consumer pattern ever changes, drop to 0o600.
|
|
80
|
+
try {
|
|
81
|
+
chmodSync(path, 0o660);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
process.stderr.write(`[fleet-agent] WARNING: chmod 0660 on ${path} failed: ${err.message}\n`);
|
|
85
|
+
}
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
89
|
+
close: () => new Promise((resolve) => {
|
|
90
|
+
server.close(() => {
|
|
91
|
+
if (socketPath && existsSync(socketPath)) {
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(socketPath);
|
|
94
|
+
}
|
|
95
|
+
catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function handleConnection(sock, deps) {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let totalBytes = 0;
|
|
105
|
+
let handled = false;
|
|
106
|
+
let searchedUpTo = 0;
|
|
107
|
+
sock.setTimeout(IDLE_TIMEOUT_MS, () => sock.destroy());
|
|
108
|
+
const handle = () => {
|
|
109
|
+
if (handled)
|
|
110
|
+
return;
|
|
111
|
+
handled = true;
|
|
112
|
+
if (!takeToken()) {
|
|
113
|
+
sock.end(writeResponse(429, { error: 'rate_limited' }));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const buf = Buffer.concat(chunks);
|
|
117
|
+
try {
|
|
118
|
+
const req = parseRequest(buf);
|
|
119
|
+
const resp = dispatch(req, deps);
|
|
120
|
+
sock.end(resp);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const isProto = err instanceof ProtocolError;
|
|
124
|
+
const status = isProto ? 400 : 500;
|
|
125
|
+
const message = isProto ? err.message : 'internal';
|
|
126
|
+
sock.end(writeResponse(status, { error: message }));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
sock.on('data', (chunk) => {
|
|
130
|
+
chunks.push(chunk);
|
|
131
|
+
totalBytes += chunk.length;
|
|
132
|
+
if (totalBytes > MAX_REQUEST_BYTES) {
|
|
133
|
+
handled = true;
|
|
134
|
+
sock.end(writeResponse(413, { error: 'request too large' }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const buf = Buffer.concat(chunks);
|
|
138
|
+
const idx = buf.indexOf(TERM, Math.max(0, searchedUpTo - 3));
|
|
139
|
+
if (idx >= 0) {
|
|
140
|
+
handle();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
searchedUpTo = buf.length;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
sock.on('end', () => { if (!handled)
|
|
147
|
+
handle(); });
|
|
148
|
+
sock.on('error', () => { });
|
|
149
|
+
}
|
|
150
|
+
const KNOWN_FLAGS = new Set(['--app', '--vault', '--socket', '--credential']);
|
|
151
|
+
const USAGE = 'Usage: fleet-agent --app <name> --vault <dir> --socket <path> [--credential <path>]';
|
|
152
|
+
export function parseArgs(argv) {
|
|
153
|
+
const parsed = {};
|
|
154
|
+
let i = 0;
|
|
155
|
+
while (i < argv.length) {
|
|
156
|
+
const flag = argv[i];
|
|
157
|
+
if (!flag.startsWith('--')) {
|
|
158
|
+
throw new SecretsError(`unexpected argument: ${flag}\n${USAGE}`);
|
|
159
|
+
}
|
|
160
|
+
if (!KNOWN_FLAGS.has(flag)) {
|
|
161
|
+
throw new SecretsError(`unknown flag: ${flag}\n${USAGE}`);
|
|
162
|
+
}
|
|
163
|
+
i++;
|
|
164
|
+
if (i >= argv.length || argv[i].startsWith('--')) {
|
|
165
|
+
throw new SecretsError(`flag ${flag} requires a value\n${USAGE}`);
|
|
166
|
+
}
|
|
167
|
+
parsed[flag.slice(2)] = argv[i];
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
for (const required of ['app', 'vault', 'socket']) {
|
|
171
|
+
if (!parsed[required]) {
|
|
172
|
+
throw new SecretsError(`missing required flag --${required}\n${USAGE}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
app: parsed['app'],
|
|
177
|
+
vault: parsed['vault'],
|
|
178
|
+
socket: parsed['socket'],
|
|
179
|
+
...(parsed['credential'] !== undefined ? { credential: parsed['credential'] } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export async function main(argv) {
|
|
183
|
+
const args = parseArgs(argv);
|
|
184
|
+
const credentialPath = args.credential
|
|
185
|
+
?? (process.env.CREDENTIALS_DIRECTORY
|
|
186
|
+
? `${process.env.CREDENTIALS_DIRECTORY}/age-key`
|
|
187
|
+
: undefined);
|
|
188
|
+
if (!credentialPath) {
|
|
189
|
+
throw new SecretsError('no credential path: pass --credential or run under systemd with LoadCredential');
|
|
190
|
+
}
|
|
191
|
+
const vaultBlobPath = `${args.vault}/${args.app}.env.age`;
|
|
192
|
+
let secrets = decryptVaultBlob(credentialPath, vaultBlobPath);
|
|
193
|
+
const deps = {
|
|
194
|
+
app: args.app,
|
|
195
|
+
getSecrets: () => secrets,
|
|
196
|
+
refresh: () => {
|
|
197
|
+
secrets = decryptVaultBlob(credentialPath, vaultBlobPath);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const server = createServer(deps);
|
|
201
|
+
await server.listen(args.socket);
|
|
202
|
+
const notify = process.env.NOTIFY_SOCKET;
|
|
203
|
+
if (notify) {
|
|
204
|
+
const r = execSafe('systemd-notify', ['--ready'], {});
|
|
205
|
+
if (!r.ok) {
|
|
206
|
+
process.stderr.write(`[fleet-agent ${args.app}] systemd-notify failed: ${r.stderr}\n`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
let shuttingDown = false;
|
|
211
|
+
const handleSignal = async (sig) => {
|
|
212
|
+
if (shuttingDown)
|
|
213
|
+
return;
|
|
214
|
+
shuttingDown = true;
|
|
215
|
+
process.stderr.write(`[fleet-agent ${args.app}] ${sig}, shutting down\n`);
|
|
216
|
+
try {
|
|
217
|
+
await server.close();
|
|
218
|
+
}
|
|
219
|
+
catch { /* ignore */ }
|
|
220
|
+
resolve();
|
|
221
|
+
};
|
|
222
|
+
process.once('SIGTERM', () => handleSignal('SIGTERM'));
|
|
223
|
+
process.once('SIGINT', () => handleSignal('SIGINT'));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function dispatch(req, deps) {
|
|
227
|
+
if (req.method === 'GET' && req.path === '/health') {
|
|
228
|
+
const m = deps.getSecrets();
|
|
229
|
+
return writeResponse(200, { app: deps.app, secrets: Object.keys(m).length });
|
|
230
|
+
}
|
|
231
|
+
if (req.method === 'POST' && req.path === '/refresh') {
|
|
232
|
+
deps.refresh();
|
|
233
|
+
return writeResponse(200, { reloaded: true });
|
|
234
|
+
}
|
|
235
|
+
if (req.method === 'GET' && req.path === '/secrets') {
|
|
236
|
+
return writeResponse(200, deps.getSecrets());
|
|
237
|
+
}
|
|
238
|
+
if (req.method === 'GET' && req.path.startsWith('/secrets/')) {
|
|
239
|
+
const key = req.path.slice('/secrets/'.length);
|
|
240
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
241
|
+
return writeResponse(400, { error: 'invalid_key' });
|
|
242
|
+
}
|
|
243
|
+
const m = deps.getSecrets();
|
|
244
|
+
if (key in m)
|
|
245
|
+
return writeResponse(200, { value: m[key] });
|
|
246
|
+
return writeResponse(404, { error: 'not_found' });
|
|
247
|
+
}
|
|
248
|
+
return writeResponse(404, { error: 'not_found' });
|
|
249
|
+
}
|
package/dist/core/secrets.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const VAULT_DIR: string;
|
|
2
2
|
export declare const KEY_PATH = "/etc/fleet/age.key";
|
|
3
3
|
export declare const RUNTIME_DIR = "/run/fleet-secrets";
|
|
4
|
+
export declare const MANIFEST_PATH: string;
|
|
4
5
|
export interface SecretMetadata {
|
|
5
6
|
lastRotated: string;
|
|
6
7
|
provider?: string;
|
|
@@ -17,8 +18,10 @@ export interface ManifestEntry {
|
|
|
17
18
|
/** Per-secret metadata, keyed by secret name. Backwards-compatible: missing means
|
|
18
19
|
* lastRotated falls back to lastSealedAt and provider is auto-classified at read time. */
|
|
19
20
|
secrets?: Record<string, SecretMetadata>;
|
|
20
|
-
/**
|
|
21
|
+
/** per-app age recipient public key. required when mode === 'socket'. */
|
|
21
22
|
recipient?: string;
|
|
23
|
+
/** delivery mode. defaults to 'unseal' (v1 tmpfs path) when undefined for backward compat. */
|
|
24
|
+
mode?: 'unseal' | 'socket';
|
|
22
25
|
}
|
|
23
26
|
export interface Manifest {
|
|
24
27
|
version: number;
|
|
@@ -31,9 +34,41 @@ export declare function getPublicKey(): string;
|
|
|
31
34
|
export declare function initVault(): string;
|
|
32
35
|
export declare function loadManifest(): Manifest;
|
|
33
36
|
export declare function saveManifest(manifest: Manifest): void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Run an arbitrary transaction under the manifest's inter-process lock.
|
|
39
|
+
* The callback receives no manifest — it's responsible for any load / save
|
|
40
|
+
* it needs (the seal flows do their own validate → backup → seal → cleanup
|
|
41
|
+
* dance and don't fit a simple load/mutate/save shape).
|
|
42
|
+
*
|
|
43
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
44
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lockManifest<T>(fn: () => Promise<T> | T): Promise<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Run a simple load → mutate → save transaction against the secrets
|
|
49
|
+
* manifest under an inter-process lock. Used by callers that need a
|
|
50
|
+
* straightforward RMW with no extra side-effects.
|
|
51
|
+
*
|
|
52
|
+
* The mutator may return a different Manifest object or mutate the input
|
|
53
|
+
* and return it. The returned value is persisted on a successful run; if
|
|
54
|
+
* the mutator throws, no save happens and the lock is released.
|
|
55
|
+
*
|
|
56
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
57
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function withManifest(fn: (m: Manifest) => Manifest | Promise<Manifest>): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Create a per-op backup of an app's encrypted vault file.
|
|
62
|
+
*
|
|
63
|
+
* Each call produces a unique `.bak-<tag>` path so concurrent operations do
|
|
64
|
+
* not silently overwrite each other's recovery point. If you don't supply a
|
|
65
|
+
* tag, one is generated from PID + timestamp + an in-process monotonic
|
|
66
|
+
* counter. Callers MUST keep the returned path and pass it to
|
|
67
|
+
* `restoreVaultFile` / `removeBackup`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function backupVaultFile(app: string, tag?: string): string | null;
|
|
70
|
+
export declare function restoreVaultFile(app: string, bakPath?: string): boolean;
|
|
71
|
+
export declare function removeBackup(app: string, bakPath?: string): void;
|
|
37
72
|
export declare function ageEncrypt(plaintext: string): string;
|
|
38
73
|
export declare function ageDecrypt(ciphertext: string | Buffer): string;
|
|
39
74
|
export declare function ageDecryptFile(filePath: string): string;
|
package/dist/core/secrets.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, chmodSync, rmSync, copyFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, chmodSync, statSync, rmSync, copyFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { SecretsError, VaultNotInitializedError } from './errors.js';
|
|
5
5
|
import { execSafe } from './exec.js';
|
|
6
6
|
import { assertAppName, assertFilePath } from './validate.js';
|
|
7
|
+
import { withFileLock } from './file-lock.js';
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
+
// vault dir resolves from FLEET_VAULT_DIR if set; otherwise falls back to
|
|
10
|
+
// the repo-local vault dir for dev. captures at module load — tests that
|
|
11
|
+
// want to override need to set the env var before importing. matches the
|
|
12
|
+
// pattern used in secrets-v2-cleanup.ts.
|
|
13
|
+
export const VAULT_DIR = process.env.FLEET_VAULT_DIR
|
|
14
|
+
?? join(__dirname, '..', '..', 'vault');
|
|
9
15
|
export const KEY_PATH = '/etc/fleet/age.key';
|
|
10
16
|
export const RUNTIME_DIR = '/run/fleet-secrets';
|
|
11
|
-
const MANIFEST_PATH = join(VAULT_DIR, 'manifest.json');
|
|
17
|
+
export const MANIFEST_PATH = join(VAULT_DIR, 'manifest.json');
|
|
12
18
|
const SECRET_DELIMITER = '---SECRET:';
|
|
13
19
|
export function ensureAge() {
|
|
14
20
|
if (!execSafe('which', ['age']).ok) {
|
|
@@ -64,7 +70,50 @@ export function loadManifest() {
|
|
|
64
70
|
export function saveManifest(manifest) {
|
|
65
71
|
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
66
72
|
}
|
|
67
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Run an arbitrary transaction under the manifest's inter-process lock.
|
|
75
|
+
* The callback receives no manifest — it's responsible for any load / save
|
|
76
|
+
* it needs (the seal flows do their own validate → backup → seal → cleanup
|
|
77
|
+
* dance and don't fit a simple load/mutate/save shape).
|
|
78
|
+
*
|
|
79
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
80
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
81
|
+
*/
|
|
82
|
+
export async function lockManifest(fn) {
|
|
83
|
+
return withFileLock(MANIFEST_PATH, fn);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Run a simple load → mutate → save transaction against the secrets
|
|
87
|
+
* manifest under an inter-process lock. Used by callers that need a
|
|
88
|
+
* straightforward RMW with no extra side-effects.
|
|
89
|
+
*
|
|
90
|
+
* The mutator may return a different Manifest object or mutate the input
|
|
91
|
+
* and return it. The returned value is persisted on a successful run; if
|
|
92
|
+
* the mutator throws, no save happens and the lock is released.
|
|
93
|
+
*
|
|
94
|
+
* Important: NOT reentrant. Wrap only the outermost RMW boundary; inner
|
|
95
|
+
* helpers should call plain `loadManifest` / `saveManifest`.
|
|
96
|
+
*/
|
|
97
|
+
export async function withManifest(fn) {
|
|
98
|
+
await lockManifest(async () => {
|
|
99
|
+
const manifest = loadManifest();
|
|
100
|
+
const next = await fn(manifest);
|
|
101
|
+
saveManifest(next);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// monotonic counter so two backupVaultFile calls in the same millisecond
|
|
105
|
+
// (same process, same default-tag path) still produce distinct bak filenames.
|
|
106
|
+
let backupSeq = 0;
|
|
107
|
+
/**
|
|
108
|
+
* Create a per-op backup of an app's encrypted vault file.
|
|
109
|
+
*
|
|
110
|
+
* Each call produces a unique `.bak-<tag>` path so concurrent operations do
|
|
111
|
+
* not silently overwrite each other's recovery point. If you don't supply a
|
|
112
|
+
* tag, one is generated from PID + timestamp + an in-process monotonic
|
|
113
|
+
* counter. Callers MUST keep the returned path and pass it to
|
|
114
|
+
* `restoreVaultFile` / `removeBackup`.
|
|
115
|
+
*/
|
|
116
|
+
export function backupVaultFile(app, tag) {
|
|
68
117
|
const manifest = loadManifest();
|
|
69
118
|
const entry = manifest.apps[app];
|
|
70
119
|
if (!entry)
|
|
@@ -72,30 +121,61 @@ export function backupVaultFile(app) {
|
|
|
72
121
|
const src = join(VAULT_DIR, entry.encryptedFile);
|
|
73
122
|
if (!existsSync(src))
|
|
74
123
|
return null;
|
|
75
|
-
const
|
|
124
|
+
const t = tag ?? `${process.pid}-${Date.now()}-${++backupSeq}`;
|
|
125
|
+
const bak = `${src}.bak-${t}`;
|
|
76
126
|
copyFileSync(src, bak);
|
|
77
127
|
return bak;
|
|
78
128
|
}
|
|
79
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Find the newest `<encryptedFile>.bak-*` for an app, if any. Returns the full
|
|
131
|
+
* path (in VAULT_DIR) or null. Used as a best-effort fallback when no specific
|
|
132
|
+
* backup path was supplied (e.g. the simple CLI / MCP `secrets restore` flow).
|
|
133
|
+
*/
|
|
134
|
+
function findNewestBackup(encryptedFile) {
|
|
135
|
+
if (!existsSync(VAULT_DIR))
|
|
136
|
+
return null;
|
|
137
|
+
const prefix = `${encryptedFile}.bak-`;
|
|
138
|
+
let newest = null;
|
|
139
|
+
for (const name of readdirSync(VAULT_DIR)) {
|
|
140
|
+
if (!name.startsWith(prefix))
|
|
141
|
+
continue;
|
|
142
|
+
const full = join(VAULT_DIR, name);
|
|
143
|
+
try {
|
|
144
|
+
const m = statSync(full).mtimeMs;
|
|
145
|
+
if (!newest || m > newest.mtime)
|
|
146
|
+
newest = { path: full, mtime: m };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// ignore stat failures — file may have been removed mid-scan
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return newest ? newest.path : null;
|
|
153
|
+
}
|
|
154
|
+
export function restoreVaultFile(app, bakPath) {
|
|
80
155
|
const manifest = loadManifest();
|
|
81
156
|
const entry = manifest.apps[app];
|
|
82
157
|
if (!entry)
|
|
83
158
|
return false;
|
|
84
159
|
const src = join(VAULT_DIR, entry.encryptedFile);
|
|
85
|
-
const bak =
|
|
86
|
-
if (!existsSync(bak))
|
|
160
|
+
const bak = bakPath ?? findNewestBackup(entry.encryptedFile);
|
|
161
|
+
if (!bak || !existsSync(bak))
|
|
87
162
|
return false;
|
|
88
163
|
copyFileSync(bak, src);
|
|
89
164
|
rmSync(bak, { force: true });
|
|
90
165
|
return true;
|
|
91
166
|
}
|
|
92
|
-
export function removeBackup(app) {
|
|
167
|
+
export function removeBackup(app, bakPath) {
|
|
168
|
+
if (bakPath) {
|
|
169
|
+
rmSync(bakPath, { force: true });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
93
172
|
const manifest = loadManifest();
|
|
94
173
|
const entry = manifest.apps[app];
|
|
95
174
|
if (!entry)
|
|
96
175
|
return;
|
|
97
|
-
const bak =
|
|
98
|
-
|
|
176
|
+
const bak = findNewestBackup(entry.encryptedFile);
|
|
177
|
+
if (bak)
|
|
178
|
+
rmSync(bak, { force: true });
|
|
99
179
|
}
|
|
100
180
|
export function ageEncrypt(plaintext) {
|
|
101
181
|
const pubkey = getPublicKey();
|