sealkeep 0.9.0 → 0.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/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +252 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +332 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5940 -297
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/src/secrets.d.ts
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import type { GdriveCredentials } from "./providers/gdrive.js";
|
|
2
|
+
import type { StorageDestinationIdentity } from "./types.js";
|
|
3
|
+
declare const nativeSecretOptions: {
|
|
4
|
+
timeout: number;
|
|
5
|
+
killSignal: "SIGKILL";
|
|
6
|
+
};
|
|
7
|
+
export type MacosSecurityInvocation = {
|
|
8
|
+
file: "/usr/sbin/taskpolicy";
|
|
9
|
+
args: string[];
|
|
10
|
+
options: typeof nativeSecretOptions;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* launchd releases before v0.10 marked the complete daemon job Background.
|
|
14
|
+
* Even after the daemon clears its own Darwin background bit, launchd applies
|
|
15
|
+
* the old job policy to helpers it spawns; a normally instant Keychain lookup
|
|
16
|
+
* can then consume the whole timeout while the machine is busy. `-a` gives
|
|
17
|
+
* only this Apple-owned helper ordinary application resource policy. The
|
|
18
|
+
* daemon's Nice=5 and LowPriorityIO settings, and every archive worker, remain
|
|
19
|
+
* deferential.
|
|
20
|
+
*/
|
|
21
|
+
export declare function macosSecurityInvocation(args: readonly string[]): MacosSecurityInvocation;
|
|
22
|
+
/** `security` returns errSecItemNotFound (-25300) as its low-byte exit status. */
|
|
23
|
+
export declare function isMacosSecurityItemNotFound(error: unknown): boolean;
|
|
2
24
|
export type SecretRef = {
|
|
3
25
|
service: string;
|
|
4
26
|
account: string;
|
|
5
27
|
};
|
|
6
|
-
export type BackendName = "macos-keychain" | "secret-service" | "windows-dpapi" | "file";
|
|
28
|
+
export type BackendName = "macos-keychain" | "secret-service" | "linux-systemd-creds" | "windows-dpapi" | "file";
|
|
7
29
|
export interface SecretBackend {
|
|
8
30
|
readonly name: BackendName;
|
|
9
31
|
/** True when this backend can actually be used on this machine right now. */
|
|
@@ -27,11 +49,6 @@ export declare class FileSecretBackend implements SecretBackend {
|
|
|
27
49
|
delete(ref: SecretRef): Promise<void>;
|
|
28
50
|
}
|
|
29
51
|
export declare function secretBackends(dataDir: string): SecretBackend[];
|
|
30
|
-
/**
|
|
31
|
-
* Picks the strongest backend this machine supports. The file fallback always
|
|
32
|
-
* qualifies. `SEALKEEP_SECRET_BACKEND` pins the choice, which is how a test suite
|
|
33
|
-
* or a sandboxed environment stays out of the real OS keystore.
|
|
34
|
-
*/
|
|
35
52
|
export declare function chooseBackend(dataDir: string, preferred?: BackendName, env?: NodeJS.ProcessEnv): Promise<SecretBackend>;
|
|
36
53
|
export type ProviderCredentials = {
|
|
37
54
|
accessKeyId: string;
|
|
@@ -41,6 +58,24 @@ export type ProviderCredentials = {
|
|
|
41
58
|
clientEmail: string;
|
|
42
59
|
privateKey: string;
|
|
43
60
|
} | GdriveCredentials;
|
|
61
|
+
/**
|
|
62
|
+
* Non-secret evidence attached to a provider credential after the connection
|
|
63
|
+
* probe succeeds. The routing label is intentionally absent: labels can be
|
|
64
|
+
* edited account-wide, while this snapshot identifies the destination whose
|
|
65
|
+
* write/read/delete permissions were actually proved on this machine.
|
|
66
|
+
*/
|
|
67
|
+
export type ProviderCredentialBinding = {
|
|
68
|
+
destination: StorageDestinationIdentity;
|
|
69
|
+
destinationFingerprint: string;
|
|
70
|
+
/** Null only for the narrowly-scoped local legacy migration. */
|
|
71
|
+
verifiedAt: string | null;
|
|
72
|
+
};
|
|
73
|
+
export type ProviderCredentialRecord = {
|
|
74
|
+
credentials: ProviderCredentials;
|
|
75
|
+
binding: ProviderCredentialBinding | null;
|
|
76
|
+
/** Raw credentials written by releases which predate destination binding. */
|
|
77
|
+
legacy: boolean;
|
|
78
|
+
};
|
|
44
79
|
/**
|
|
45
80
|
* Remembering the recovery phrase.
|
|
46
81
|
*
|
|
@@ -77,10 +112,19 @@ export declare function resolveRecoveryPhrase(dataDir: string, vaultId: string,
|
|
|
77
112
|
* Provider credentials are addressed by storage config id, never by bucket name,
|
|
78
113
|
* so rotating a config cannot silently reuse an old credential.
|
|
79
114
|
*/
|
|
80
|
-
export declare function storeProviderCredentials(dataDir: string, storageConfigId: string, credentials: ProviderCredentials, preferred?: BackendName): Promise<{
|
|
115
|
+
export declare function storeProviderCredentials(dataDir: string, storageConfigId: string, credentials: ProviderCredentials, preferred?: BackendName, binding?: ProviderCredentialBinding): Promise<{
|
|
81
116
|
backend: BackendName;
|
|
82
117
|
}>;
|
|
118
|
+
export declare function loadProviderCredentialRecord(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<ProviderCredentialRecord>;
|
|
83
119
|
export declare function loadProviderCredentials(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<ProviderCredentials>;
|
|
120
|
+
/**
|
|
121
|
+
* Raw slot presence for disconnect. Parsing is deliberately skipped: a
|
|
122
|
+
* truncated or obsolete secret still belongs to the requested target and must
|
|
123
|
+
* remain removable. Backend failures propagate instead of being reported as
|
|
124
|
+
* an already-disconnected success.
|
|
125
|
+
*/
|
|
126
|
+
export declare function providerCredentialSlotPresent(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<boolean>;
|
|
84
127
|
export declare function deleteProviderCredentials(dataDir: string, storageConfigId: string, preferred?: BackendName): Promise<{
|
|
85
128
|
backend: BackendName;
|
|
86
129
|
}>;
|
|
130
|
+
export {};
|
package/dist/src/secrets.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { platform } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -6,6 +7,36 @@ import { promisify } from "node:util";
|
|
|
6
7
|
import { fail } from "./errors.js";
|
|
7
8
|
import { envVar } from "./env.js";
|
|
8
9
|
const run = promisify(execFile);
|
|
10
|
+
// Native secret helpers can wait for a locked/login keychain prompt forever.
|
|
11
|
+
// An unattended daemon has nobody to answer that prompt, so every call must
|
|
12
|
+
// fail closed on a short bound and let the durable queue retry later.
|
|
13
|
+
const NATIVE_SECRET_TIMEOUT_MS = 5_000;
|
|
14
|
+
const nativeSecretOptions = { timeout: NATIVE_SECRET_TIMEOUT_MS, killSignal: "SIGKILL" };
|
|
15
|
+
/**
|
|
16
|
+
* launchd releases before v0.10 marked the complete daemon job Background.
|
|
17
|
+
* Even after the daemon clears its own Darwin background bit, launchd applies
|
|
18
|
+
* the old job policy to helpers it spawns; a normally instant Keychain lookup
|
|
19
|
+
* can then consume the whole timeout while the machine is busy. `-a` gives
|
|
20
|
+
* only this Apple-owned helper ordinary application resource policy. The
|
|
21
|
+
* daemon's Nice=5 and LowPriorityIO settings, and every archive worker, remain
|
|
22
|
+
* deferential.
|
|
23
|
+
*/
|
|
24
|
+
export function macosSecurityInvocation(args) {
|
|
25
|
+
return {
|
|
26
|
+
file: "/usr/sbin/taskpolicy",
|
|
27
|
+
args: ["-a", "/usr/bin/security", ...args],
|
|
28
|
+
options: { ...nativeSecretOptions },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const runMacosSecurity = (args) => {
|
|
32
|
+
const invocation = macosSecurityInvocation(args);
|
|
33
|
+
return run(invocation.file, invocation.args, invocation.options);
|
|
34
|
+
};
|
|
35
|
+
/** `security` returns errSecItemNotFound (-25300) as its low-byte exit status. */
|
|
36
|
+
export function isMacosSecurityItemNotFound(error) {
|
|
37
|
+
return typeof error === "object" && error !== null
|
|
38
|
+
&& "code" in error && error.code === 44;
|
|
39
|
+
}
|
|
9
40
|
async function commandExists(command) {
|
|
10
41
|
try {
|
|
11
42
|
await run(platform() === "win32" ? "where" : "which", [command]);
|
|
@@ -15,6 +46,17 @@ async function commandExists(command) {
|
|
|
15
46
|
return false;
|
|
16
47
|
}
|
|
17
48
|
}
|
|
49
|
+
async function commandWithInput(command, args, input, timeout = 5_000) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const child = execFile(command, args, { encoding: "utf8", timeout, maxBuffer: 2 * 1024 * 1024 }, (error, stdout) => {
|
|
52
|
+
if (error)
|
|
53
|
+
reject(error);
|
|
54
|
+
else
|
|
55
|
+
resolve(stdout);
|
|
56
|
+
});
|
|
57
|
+
child.stdin?.end(input);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
18
60
|
/**
|
|
19
61
|
* macOS Keychain.
|
|
20
62
|
*
|
|
@@ -25,29 +67,81 @@ async function commandExists(command) {
|
|
|
25
67
|
*/
|
|
26
68
|
class MacosKeychain {
|
|
27
69
|
name = "macos-keychain";
|
|
28
|
-
|
|
29
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Having the `security` binary is not the same as having a keychain.
|
|
72
|
+
*
|
|
73
|
+
* This used to answer yes on any Mac, so a process whose HOME does not hold a
|
|
74
|
+
* login keychain — a test with a throwaway HOME, CI, a container — was told
|
|
75
|
+
* the keychain was usable, tried to write, and macOS put a MODAL DIALOG in
|
|
76
|
+
* front of a human offering to "Reset To Defaults" on their real keychain.
|
|
77
|
+
* A storage backend that cannot store must say so and let the file backend
|
|
78
|
+
* take over; `security list-keychains` is the cheapest honest answer.
|
|
79
|
+
*/
|
|
80
|
+
async available() {
|
|
81
|
+
if (platform() !== "darwin")
|
|
82
|
+
return false;
|
|
30
83
|
try {
|
|
31
|
-
|
|
84
|
+
const { stdout } = await runMacosSecurity(["list-keychains", "-d", "user"]);
|
|
85
|
+
return stdout.trim().length > 0;
|
|
32
86
|
}
|
|
33
87
|
catch {
|
|
34
|
-
return
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async get(ref) {
|
|
92
|
+
try {
|
|
93
|
+
return (await runMacosSecurity(["find-generic-password", "-s", ref.service, "-a", ref.account, "-w"])).stdout.trim();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (isMacosSecurityItemNotFound(error))
|
|
97
|
+
return null;
|
|
98
|
+
throw error;
|
|
35
99
|
}
|
|
36
100
|
}
|
|
37
101
|
async set(ref, value) {
|
|
38
|
-
await
|
|
102
|
+
await runMacosSecurity(["add-generic-password", "-U", "-s", ref.service, "-a", ref.account, "-w", value]);
|
|
39
103
|
}
|
|
40
104
|
async delete(ref) {
|
|
41
105
|
try {
|
|
42
|
-
await
|
|
106
|
+
await runMacosSecurity(["delete-generic-password", "-s", ref.service, "-a", ref.account]);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!isMacosSecurityItemNotFound(error))
|
|
110
|
+
throw error;
|
|
43
111
|
}
|
|
44
|
-
catch { /* already absent */ }
|
|
45
112
|
}
|
|
46
113
|
}
|
|
47
114
|
/** Freedesktop Secret Service via `secret-tool`, which reads the secret from stdin. */
|
|
48
115
|
class SecretService {
|
|
49
116
|
name = "secret-service";
|
|
50
|
-
async available() {
|
|
117
|
+
async available() {
|
|
118
|
+
if (platform() !== "linux" || !(await commandExists("secret-tool")))
|
|
119
|
+
return false;
|
|
120
|
+
// `secret-tool` is only a client. Minimal servers and containers often
|
|
121
|
+
// install the binary without a D-Bus Secret Service; choosing it there
|
|
122
|
+
// makes the first unattended write fail instead of falling back safely.
|
|
123
|
+
// A read-only search is insufficient: a headless VPS can expose the D-Bus
|
|
124
|
+
// API and a volatile session collection while its persistent login
|
|
125
|
+
// collection is missing/locked. Prove the exact operation Sealkeep needs
|
|
126
|
+
// with a random reversible store/read/clear, each time-bounded so a prompt
|
|
127
|
+
// that nobody can answer cannot hang every hook.
|
|
128
|
+
const account = `probe-${process.pid}-${randomUUID()}`;
|
|
129
|
+
const value = randomUUID();
|
|
130
|
+
try {
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
const child = execFile("secret-tool", ["store", "--label", "Sealkeep native-keystore availability probe", "service", "sealkeep-availability-probe", "account", account], { timeout: 2_000 }, (error) => error ? reject(error) : resolve());
|
|
133
|
+
child.stdin?.end(value);
|
|
134
|
+
});
|
|
135
|
+
const found = (await run("secret-tool", ["lookup", "service", "sealkeep-availability-probe", "account", account], { timeout: 2_000 })).stdout.trim();
|
|
136
|
+
return found === value;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await run("secret-tool", ["clear", "service", "sealkeep-availability-probe", "account", account], { timeout: 2_000 }).catch(() => undefined);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
51
145
|
async get(ref) {
|
|
52
146
|
try {
|
|
53
147
|
return (await run("secret-tool", ["lookup", "service", ref.service, "account", ref.account])).stdout.trim() || null;
|
|
@@ -69,6 +163,64 @@ class SecretService {
|
|
|
69
163
|
catch { /* already absent */ }
|
|
70
164
|
}
|
|
71
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* systemd encrypted credentials for headless Linux.
|
|
168
|
+
*
|
|
169
|
+
* Secret Service is the preferred desktop backend, where PAM unlocks the
|
|
170
|
+
* login collection. A key-only server session often has no unlock password at
|
|
171
|
+
* all. On modern systemd, root (and environments granted access to a host/TPM
|
|
172
|
+
* credential key) can instead seal a user-scoped credential to this OS
|
|
173
|
+
* installation. The plaintext travels over stdin/stdout only; the vault keeps
|
|
174
|
+
* the authenticated Base64 ciphertext. A reversible probe prevents merely
|
|
175
|
+
* having the binary from selecting an unusable backend.
|
|
176
|
+
*/
|
|
177
|
+
class LinuxSystemdCredentials {
|
|
178
|
+
root;
|
|
179
|
+
name = "linux-systemd-creds";
|
|
180
|
+
constructor(root) {
|
|
181
|
+
this.root = root;
|
|
182
|
+
}
|
|
183
|
+
nameFor(ref) {
|
|
184
|
+
return `sealkeep-${createHash("sha256").update(`${ref.service}\0${ref.account}`).digest("hex")}`;
|
|
185
|
+
}
|
|
186
|
+
path(ref) { return join(this.root, `${this.nameFor(ref)}.credential`); }
|
|
187
|
+
async encrypt(name, value) {
|
|
188
|
+
return commandWithInput("systemd-creds", ["--user", "--no-ask-password", "--with-key=host", `--name=${name}`, "encrypt", "-", "-"], value);
|
|
189
|
+
}
|
|
190
|
+
async decrypt(name, value) {
|
|
191
|
+
return commandWithInput("systemd-creds", ["--user", "--no-ask-password", `--name=${name}`, "decrypt", "-", "-"], value);
|
|
192
|
+
}
|
|
193
|
+
async available() {
|
|
194
|
+
if (platform() !== "linux" || !(await commandExists("systemd-creds")))
|
|
195
|
+
return false;
|
|
196
|
+
const name = `sealkeep-probe-${process.pid}-${randomUUID()}`;
|
|
197
|
+
const value = randomUUID();
|
|
198
|
+
try {
|
|
199
|
+
return (await this.decrypt(name, await this.encrypt(name, value))).trim() === value;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async get(ref) {
|
|
206
|
+
const encrypted = await readFile(this.path(ref), "utf8").catch(() => null);
|
|
207
|
+
if (!encrypted)
|
|
208
|
+
return null;
|
|
209
|
+
try {
|
|
210
|
+
return (await this.decrypt(this.nameFor(ref), encrypted)).trim() || null;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async set(ref, value) {
|
|
217
|
+
const encrypted = await this.encrypt(this.nameFor(ref), value);
|
|
218
|
+
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
219
|
+
await writeFile(this.path(ref), encrypted.trim() + "\n", { mode: 0o600 });
|
|
220
|
+
await chmod(this.path(ref), 0o600);
|
|
221
|
+
}
|
|
222
|
+
async delete(ref) { await rm(this.path(ref), { force: true }); }
|
|
223
|
+
}
|
|
72
224
|
/**
|
|
73
225
|
* Windows DPAPI through PowerShell. The ciphertext is bound to the current user
|
|
74
226
|
* account, so the stored file is useless to any other user on the machine.
|
|
@@ -127,13 +279,111 @@ export class FileSecretBackend {
|
|
|
127
279
|
async delete(ref) { await rm(this.path(ref), { force: true }); }
|
|
128
280
|
}
|
|
129
281
|
export function secretBackends(dataDir) {
|
|
130
|
-
|
|
282
|
+
const root = join(dataDir, "secrets");
|
|
283
|
+
return [new MacosKeychain(), new SecretService(), new LinuxSystemdCredentials(root), new WindowsDpapi(root), new FileSecretBackend(root)];
|
|
131
284
|
}
|
|
132
285
|
/**
|
|
133
286
|
* Picks the strongest backend this machine supports. The file fallback always
|
|
134
287
|
* qualifies. `SEALKEEP_SECRET_BACKEND` pins the choice, which is how a test suite
|
|
135
288
|
* or a sandboxed environment stays out of the real OS keystore.
|
|
136
289
|
*/
|
|
290
|
+
/**
|
|
291
|
+
* The service ids these secrets were stored under before the rename.
|
|
292
|
+
*
|
|
293
|
+
* A keystore entry is not ours to orphan. Renaming the id without moving the
|
|
294
|
+
* entry does not fail loudly — the lookup simply misses, and the product
|
|
295
|
+
* quietly behaves as though the user had never signed in, never stored a
|
|
296
|
+
* provider credential, never chose to remember a phrase. The secret is still
|
|
297
|
+
* sitting in their keychain under a name nothing looks for any more.
|
|
298
|
+
*/
|
|
299
|
+
const LEGACY_SERVICE = new Map([
|
|
300
|
+
["sealkeep-provider", "vaultline-provider"],
|
|
301
|
+
["sealkeep-recovery", "vaultline-recovery"],
|
|
302
|
+
["sealkeep-cloud", "vaultline-cloud"]
|
|
303
|
+
]);
|
|
304
|
+
/**
|
|
305
|
+
* A backend whose reads heal. A miss under the current id retries the old one,
|
|
306
|
+
* and a hit is rewritten under the new id and the old entry removed — so the
|
|
307
|
+
* move happens once, on first use, without a migration step anyone has to run
|
|
308
|
+
* and without a second copy of a secret left behind.
|
|
309
|
+
*/
|
|
310
|
+
function migrating(backend) {
|
|
311
|
+
return {
|
|
312
|
+
name: backend.name,
|
|
313
|
+
available: () => backend.available(),
|
|
314
|
+
async get(ref) {
|
|
315
|
+
const current = await backend.get(ref);
|
|
316
|
+
if (current !== null)
|
|
317
|
+
return current;
|
|
318
|
+
const legacy = LEGACY_SERVICE.get(ref.service);
|
|
319
|
+
if (!legacy)
|
|
320
|
+
return null;
|
|
321
|
+
const found = await backend.get({ ...ref, service: legacy });
|
|
322
|
+
if (found === null)
|
|
323
|
+
return null;
|
|
324
|
+
await backend.set(ref, found);
|
|
325
|
+
// Read it back before removing the only other copy. Without this, a
|
|
326
|
+
// write that reported success but did not land — a locked keychain, a
|
|
327
|
+
// full disk on the file backend — took the legacy entry with it and the
|
|
328
|
+
// secret was gone. And if another process rotated or forgot this secret
|
|
329
|
+
// between our read and our write, the value under the new id is theirs,
|
|
330
|
+
// not ours: leave it, and leave the legacy entry alone too rather than
|
|
331
|
+
// resurrecting something a user explicitly forgot.
|
|
332
|
+
const settled = await backend.get(ref);
|
|
333
|
+
if (settled !== found)
|
|
334
|
+
return settled;
|
|
335
|
+
await backend.delete({ ...ref, service: legacy });
|
|
336
|
+
return found;
|
|
337
|
+
},
|
|
338
|
+
set: (ref, value) => backend.set(ref, value),
|
|
339
|
+
// Forgetting must forget both, or a "forgotten" phrase comes back.
|
|
340
|
+
async delete(ref) {
|
|
341
|
+
await backend.delete(ref);
|
|
342
|
+
const legacy = LEGACY_SERVICE.get(ref.service);
|
|
343
|
+
if (legacy)
|
|
344
|
+
await backend.delete({ ...ref, service: legacy });
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A stronger backend can become available after an upgrade (for example,
|
|
350
|
+
* systemd-creds arrives on a headless server that previously used the
|
|
351
|
+
* owner-only file fallback). Selecting it must not make the existing secret
|
|
352
|
+
* disappear. On a primary miss, read lower-priority usable backends, copy the
|
|
353
|
+
* value into the primary, verify the copy, and only then remove the old one.
|
|
354
|
+
*/
|
|
355
|
+
function migratingBackends(primary, fallbacks) {
|
|
356
|
+
return {
|
|
357
|
+
name: primary.name,
|
|
358
|
+
available: () => primary.available(),
|
|
359
|
+
async get(ref) {
|
|
360
|
+
const current = await primary.get(ref);
|
|
361
|
+
if (current !== null)
|
|
362
|
+
return current;
|
|
363
|
+
for (const fallback of fallbacks) {
|
|
364
|
+
if (!(await fallback.available()))
|
|
365
|
+
continue;
|
|
366
|
+
const found = await fallback.get(ref);
|
|
367
|
+
if (found === null)
|
|
368
|
+
continue;
|
|
369
|
+
await primary.set(ref, found);
|
|
370
|
+
const settled = await primary.get(ref);
|
|
371
|
+
if (settled !== found)
|
|
372
|
+
return settled;
|
|
373
|
+
await fallback.delete(ref);
|
|
374
|
+
return found;
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
},
|
|
378
|
+
set: (ref, value) => primary.set(ref, value),
|
|
379
|
+
async delete(ref) {
|
|
380
|
+
await primary.delete(ref);
|
|
381
|
+
for (const fallback of fallbacks)
|
|
382
|
+
if (await fallback.available())
|
|
383
|
+
await fallback.delete(ref);
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
137
387
|
export async function chooseBackend(dataDir, preferred, env = process.env) {
|
|
138
388
|
preferred = preferred ?? envVar("SECRET_BACKEND", env);
|
|
139
389
|
const backends = secretBackends(dataDir);
|
|
@@ -143,15 +393,17 @@ export async function chooseBackend(dataDir, preferred, env = process.env) {
|
|
|
143
393
|
fail("invalid_argument", `Unknown secret backend: ${preferred}`);
|
|
144
394
|
if (!(await chosen.available()))
|
|
145
395
|
fail("invalid_argument", `Secret backend ${preferred} is not available on this machine`);
|
|
146
|
-
return chosen;
|
|
396
|
+
return migrating(chosen);
|
|
397
|
+
}
|
|
398
|
+
for (let index = 0; index < backends.length; index += 1) {
|
|
399
|
+
if (!(await backends[index].available()))
|
|
400
|
+
continue;
|
|
401
|
+
return migratingBackends(migrating(backends[index]), backends.slice(index + 1).map(migrating));
|
|
147
402
|
}
|
|
148
|
-
|
|
149
|
-
if (await backend.available())
|
|
150
|
-
return backend;
|
|
151
|
-
return new FileSecretBackend(join(dataDir, "secrets"));
|
|
403
|
+
return migrating(new FileSecretBackend(join(dataDir, "secrets")));
|
|
152
404
|
}
|
|
153
|
-
const SERVICE = "
|
|
154
|
-
const PHRASE_SERVICE = "
|
|
405
|
+
const SERVICE = "sealkeep-provider";
|
|
406
|
+
const PHRASE_SERVICE = "sealkeep-recovery";
|
|
155
407
|
/**
|
|
156
408
|
* Remembering the recovery phrase.
|
|
157
409
|
*
|
|
@@ -197,22 +449,62 @@ export async function resolveRecoveryPhrase(dataDir, vaultId, explicit, env = pr
|
|
|
197
449
|
* Provider credentials are addressed by storage config id, never by bucket name,
|
|
198
450
|
* so rotating a config cannot silently reuse an old credential.
|
|
199
451
|
*/
|
|
200
|
-
export async function storeProviderCredentials(dataDir, storageConfigId, credentials, preferred) {
|
|
452
|
+
export async function storeProviderCredentials(dataDir, storageConfigId, credentials, preferred, binding) {
|
|
201
453
|
const backend = await chooseBackend(dataDir, preferred);
|
|
202
|
-
|
|
454
|
+
const stored = binding
|
|
455
|
+
? { version: 2, credentials, binding }
|
|
456
|
+
: credentials;
|
|
457
|
+
await backend.set({ service: SERVICE, account: storageConfigId }, JSON.stringify(stored));
|
|
203
458
|
return { backend: backend.name };
|
|
204
459
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
460
|
+
function isObject(value) {
|
|
461
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
462
|
+
}
|
|
463
|
+
function providerCredentialRecord(raw, storageConfigId) {
|
|
464
|
+
let parsed;
|
|
210
465
|
try {
|
|
211
|
-
|
|
466
|
+
parsed = JSON.parse(raw);
|
|
212
467
|
}
|
|
213
468
|
catch {
|
|
214
469
|
return fail("invalid_argument", `Stored credentials for ${storageConfigId} are not readable JSON`, { storageConfigId });
|
|
215
470
|
}
|
|
471
|
+
if (isObject(parsed) && parsed.version === 2) {
|
|
472
|
+
if (!isObject(parsed.credentials) || !isObject(parsed.binding)
|
|
473
|
+
|| !isObject(parsed.binding.destination)
|
|
474
|
+
|| typeof parsed.binding.destinationFingerprint !== "string"
|
|
475
|
+
|| (parsed.binding.verifiedAt !== null && typeof parsed.binding.verifiedAt !== "string")) {
|
|
476
|
+
return fail("invalid_argument", `Stored credentials for ${storageConfigId} have an invalid destination binding`, { storageConfigId });
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
credentials: parsed.credentials,
|
|
480
|
+
binding: parsed.binding,
|
|
481
|
+
legacy: false,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (!isObject(parsed)) {
|
|
485
|
+
return fail("invalid_argument", `Stored credentials for ${storageConfigId} have an invalid shape`, { storageConfigId });
|
|
486
|
+
}
|
|
487
|
+
return { credentials: parsed, binding: null, legacy: true };
|
|
488
|
+
}
|
|
489
|
+
export async function loadProviderCredentialRecord(dataDir, storageConfigId, preferred) {
|
|
490
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
491
|
+
const raw = await backend.get({ service: SERVICE, account: storageConfigId });
|
|
492
|
+
if (!raw)
|
|
493
|
+
fail("signer_not_configured", `No stored credentials for storage config ${storageConfigId}. Run: sealkeep storage credentials set`, { storageConfigId, backend: backend.name });
|
|
494
|
+
return providerCredentialRecord(raw, storageConfigId);
|
|
495
|
+
}
|
|
496
|
+
export async function loadProviderCredentials(dataDir, storageConfigId, preferred) {
|
|
497
|
+
return (await loadProviderCredentialRecord(dataDir, storageConfigId, preferred)).credentials;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Raw slot presence for disconnect. Parsing is deliberately skipped: a
|
|
501
|
+
* truncated or obsolete secret still belongs to the requested target and must
|
|
502
|
+
* remain removable. Backend failures propagate instead of being reported as
|
|
503
|
+
* an already-disconnected success.
|
|
504
|
+
*/
|
|
505
|
+
export async function providerCredentialSlotPresent(dataDir, storageConfigId, preferred) {
|
|
506
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
507
|
+
return (await backend.get({ service: SERVICE, account: storageConfigId })) !== null;
|
|
216
508
|
}
|
|
217
509
|
export async function deleteProviderCredentials(dataDir, storageConfigId, preferred) {
|
|
218
510
|
const backend = await chooseBackend(dataDir, preferred);
|
package/dist/src/service.d.ts
CHANGED
|
@@ -10,16 +10,20 @@ export type ServiceOptions = {
|
|
|
10
10
|
* Environment the service runs with.
|
|
11
11
|
*
|
|
12
12
|
* A service manager starts the daemon with a bare environment — it does not
|
|
13
|
-
* inherit the shell that installed it.
|
|
14
|
-
* `SEALKEEP_ENABLE_SIGNER
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* reads. Whatever the installer decides uploads need, it has to be written
|
|
18
|
-
* into the unit.
|
|
13
|
+
* inherit the shell that installed it. Upload-capable service definitions
|
|
14
|
+
* carry `SEALKEEP_ENABLE_SIGNER`; configured targets and machine-local
|
|
15
|
+
* credentials remain the separate gates which decide whether any write can
|
|
16
|
+
* actually happen.
|
|
19
17
|
*/
|
|
20
18
|
environment?: Record<string, string>;
|
|
19
|
+
/** Grace distinguishes a package upgrade's brief gap from a real uninstall. */
|
|
20
|
+
cleanupGraceMs?: number;
|
|
21
|
+
/** Shortened only by isolated lifecycle rehearsals. */
|
|
22
|
+
cleanupMonitorMs?: number;
|
|
21
23
|
/** Injected in tests so no real service manager is touched. */
|
|
22
24
|
exec?: (file: string, args: string[]) => Promise<unknown>;
|
|
25
|
+
/** Current numeric user. Injected only by hermetic platform rehearsals. */
|
|
26
|
+
uid?: number;
|
|
23
27
|
};
|
|
24
28
|
export type ServicePlan = {
|
|
25
29
|
kind: ServiceKind;
|
|
@@ -28,8 +32,22 @@ export type ServicePlan = {
|
|
|
28
32
|
contents: string;
|
|
29
33
|
enable: [string, string[]][];
|
|
30
34
|
disable: [string, string[]][];
|
|
35
|
+
managerPath: string;
|
|
36
|
+
receiptPath: string;
|
|
31
37
|
};
|
|
32
|
-
export declare const SERVICE_LABEL = "ai.
|
|
38
|
+
export declare const SERVICE_LABEL = "ai.sealkeep.agent";
|
|
39
|
+
/**
|
|
40
|
+
* What this service was called before the rename, and why that is not just
|
|
41
|
+
* trivia.
|
|
42
|
+
*
|
|
43
|
+
* A launchd agent is not owned by the file that wrote it — it is owned by the
|
|
44
|
+
* OS, under a label, until something tells the OS to let go. Rename the label
|
|
45
|
+
* and install, and the old agent does not disappear: it keeps loading at every
|
|
46
|
+
* login, running an executable that may no longer exist, next to the new one.
|
|
47
|
+
* So every install and every uninstall tears the old label down first.
|
|
48
|
+
*/
|
|
49
|
+
export declare const LEGACY_SERVICE_LABEL = "ai.vaultline.agent";
|
|
50
|
+
export declare const LEGACY_SYSTEMD_UNIT = "vaultline.service";
|
|
33
51
|
/**
|
|
34
52
|
* Describes the service this machine would install, without touching anything.
|
|
35
53
|
*
|
|
@@ -37,22 +55,36 @@ export declare const SERVICE_LABEL = "ai.vaultline.agent";
|
|
|
37
55
|
* exact file and commands, and tests can assert them without a service manager.
|
|
38
56
|
*/
|
|
39
57
|
export declare function servicePlan(options: ServiceOptions): ServicePlan;
|
|
58
|
+
export type ServicePersistence = "boot" | "login";
|
|
40
59
|
export type ServiceOutcome = {
|
|
41
60
|
kind: ServiceKind;
|
|
42
61
|
installed: boolean;
|
|
43
62
|
path: string;
|
|
44
63
|
ranCommands: string[];
|
|
64
|
+
managerPath?: string;
|
|
65
|
+
receiptPath?: string;
|
|
66
|
+
/** `boot` is verified systemd lingering; other native services start at login. */
|
|
67
|
+
persistence?: ServicePersistence;
|
|
45
68
|
note?: string;
|
|
46
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Upgrade path for units installed before the package-independent manager
|
|
72
|
+
* existed. No new service is installed on a machine that did not already
|
|
73
|
+
* consent to one; an existing stale or inactive registration is repaired and
|
|
74
|
+
* activated immediately instead of waiting for the next login/reboot.
|
|
75
|
+
*/
|
|
76
|
+
export declare function reconcileInstalledService(options: ServiceOptions): Promise<boolean>;
|
|
47
77
|
/** Writes the unit file and asks the platform's service manager to start it at login. */
|
|
48
78
|
export declare function installService(options: ServiceOptions): Promise<ServiceOutcome>;
|
|
49
79
|
export declare function uninstallService(options: ServiceOptions): Promise<ServiceOutcome>;
|
|
50
|
-
/** Reports
|
|
80
|
+
/** Reports durable installation separately from whether the manager has it live now. */
|
|
51
81
|
export declare function serviceStatus(options: ServiceOptions): Promise<{
|
|
52
82
|
kind: ServiceKind;
|
|
83
|
+
installed: boolean;
|
|
53
84
|
present: boolean;
|
|
54
85
|
path: string;
|
|
55
86
|
runs?: string;
|
|
87
|
+
persistence?: ServicePersistence;
|
|
56
88
|
}>;
|
|
57
89
|
/**
|
|
58
90
|
* The environment a service unit needs beyond its flags. A service manager
|
|
@@ -60,14 +92,20 @@ export declare function serviceStatus(options: ServiceOptions): Promise<{
|
|
|
60
92
|
* where secrets live has to ride along in the unit — a vault set up with
|
|
61
93
|
* SEALKEEP_SECRET_BACKEND=file kept its phrase in a keystore the daemon then
|
|
62
94
|
* never looked in, and every tick died on "No recovery phrase available".
|
|
95
|
+
* Upload capability is included by default because storage may be connected
|
|
96
|
+
* after installation; target configuration and credentials remain mandatory.
|
|
97
|
+
* Callers representing an explicit `--no-upload` omit it with `uploads:false`.
|
|
63
98
|
*/
|
|
64
|
-
export declare function serviceUnitEnvironment(base?: Record<string, string>, env?: NodeJS.ProcessEnv
|
|
65
|
-
|
|
99
|
+
export declare function serviceUnitEnvironment(base?: Record<string, string>, env?: NodeJS.ProcessEnv, options?: {
|
|
100
|
+
uploads?: boolean;
|
|
101
|
+
}): Record<string, string>;
|
|
102
|
+
/** The argv the service should run: this package's daemon plus its loopback UI lane. */
|
|
66
103
|
export declare function daemonInvocation(dataDir: string, options?: {
|
|
67
104
|
reclaim?: boolean;
|
|
105
|
+
upload?: boolean;
|
|
68
106
|
cliPath?: string;
|
|
107
|
+
intervalSeconds?: number;
|
|
69
108
|
}): {
|
|
70
109
|
executable: string;
|
|
71
110
|
args: string[];
|
|
72
111
|
};
|
|
73
|
-
export declare function serviceKind(target?: NodeJS.Platform): ServiceKind;
|