@caupulican/pi-adaptative 0.85.8 → 0.86.2
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/CHANGELOG.md +42 -0
- package/dist/core/agent-paths.d.ts +6 -3
- package/dist/core/agent-paths.d.ts.map +1 -1
- package/dist/core/agent-paths.js +10 -3
- package/dist/core/agent-paths.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +19 -8
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +11 -5
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +34 -8
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/autonomy/subagent-prompt.d.ts +3 -3
- package/dist/core/autonomy/subagent-prompt.js +9 -9
- package/dist/core/autonomy/subagent-prompt.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +10 -8
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +23 -13
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/bash-execution-controller.d.ts +4 -0
- package/dist/core/bash-execution-controller.d.ts.map +1 -1
- package/dist/core/bash-execution-controller.js +11 -4
- package/dist/core/bash-execution-controller.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/capability-contract.d.ts +1 -1
- package/dist/core/capability-contract.d.ts.map +1 -1
- package/dist/core/capability-contract.js +0 -1
- package/dist/core/capability-contract.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +1 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -1
- package/dist/core/default-tool-surface.js +3 -2
- package/dist/core/default-tool-surface.js.map +1 -1
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts +27 -8
- package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control-coordinator.js +191 -33
- package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-agent-control.d.ts +45 -6
- package/dist/core/delegation/worker-agent-control.d.ts.map +1 -1
- package/dist/core/delegation/worker-agent-control.js +110 -13
- package/dist/core/delegation/worker-agent-control.js.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -1
- package/dist/core/delegation/worker-attempt-executor.js +177 -158
- package/dist/core/delegation/worker-attempt-executor.js.map +1 -1
- package/dist/core/delegation/worker-authority-resolver.d.ts +23 -0
- package/dist/core/delegation/worker-authority-resolver.d.ts.map +1 -0
- package/dist/core/delegation/worker-authority-resolver.js +128 -0
- package/dist/core/delegation/worker-authority-resolver.js.map +1 -0
- package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -1
- package/dist/core/delegation/worker-conversation-store.js +1 -1
- package/dist/core/delegation/worker-conversation-store.js.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.d.ts +7 -1
- package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.js +260 -87
- package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
- package/dist/core/delegation/worker-delegation-request.d.ts +25 -4
- package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-request.js +90 -1
- package/dist/core/delegation/worker-delegation-request.js.map +1 -1
- package/dist/core/delegation/worker-execution-policy.d.ts +2 -0
- package/dist/core/delegation/worker-execution-policy.d.ts.map +1 -1
- package/dist/core/delegation/worker-execution-policy.js +22 -12
- package/dist/core/delegation/worker-execution-policy.js.map +1 -1
- package/dist/core/delegation/worker-lifecycle.d.ts +1 -0
- package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
- package/dist/core/delegation/worker-lifecycle.js +4 -2
- package/dist/core/delegation/worker-lifecycle.js.map +1 -1
- package/dist/core/delegation/worker-profile-resolver.d.ts +0 -2
- package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -1
- package/dist/core/delegation/worker-profile-resolver.js +8 -35
- package/dist/core/delegation/worker-profile-resolver.js.map +1 -1
- package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -1
- package/dist/core/delegation/worker-recovery-coordinator.js +1 -0
- package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts +6 -1
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +20 -6
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts +47 -0
- package/dist/core/delegation/worker-tree-budget-coordinator.d.ts.map +1 -0
- package/dist/core/delegation/worker-tree-budget-coordinator.js +210 -0
- package/dist/core/delegation/worker-tree-budget-coordinator.js.map +1 -0
- package/dist/core/memory/providers/file-store.d.ts +2 -0
- package/dist/core/memory/providers/file-store.d.ts.map +1 -1
- package/dist/core/memory/providers/file-store.js +22 -20
- package/dist/core/memory/providers/file-store.js.map +1 -1
- package/dist/core/models/runtime-process.d.ts.map +1 -1
- package/dist/core/models/runtime-process.js +5 -18
- package/dist/core/models/runtime-process.js.map +1 -1
- package/dist/core/orchestration/bounded-string-array.d.ts +11 -0
- package/dist/core/orchestration/bounded-string-array.d.ts.map +1 -0
- package/dist/core/orchestration/bounded-string-array.js +17 -0
- package/dist/core/orchestration/bounded-string-array.js.map +1 -0
- package/dist/core/orchestration/capability-gateway.d.ts +16 -0
- package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
- package/dist/core/orchestration/capability-gateway.js +38 -0
- package/dist/core/orchestration/capability-gateway.js.map +1 -1
- package/dist/core/orchestration/contracts.d.ts +11 -3
- package/dist/core/orchestration/contracts.d.ts.map +1 -1
- package/dist/core/orchestration/contracts.js +0 -1
- package/dist/core/orchestration/contracts.js.map +1 -1
- package/dist/core/orchestration/delegation-ledger.d.ts +1 -0
- package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
- package/dist/core/orchestration/delegation-ledger.js +3 -0
- package/dist/core/orchestration/delegation-ledger.js.map +1 -1
- package/dist/core/orchestration/event-store.d.ts +12 -10
- package/dist/core/orchestration/event-store.d.ts.map +1 -1
- package/dist/core/orchestration/event-store.js +94 -104
- package/dist/core/orchestration/event-store.js.map +1 -1
- package/dist/core/orchestration/lane-tool-manifests.d.ts +2 -2
- package/dist/core/orchestration/lane-tool-manifests.d.ts.map +1 -1
- package/dist/core/orchestration/lane-tool-manifests.js +4 -1
- package/dist/core/orchestration/lane-tool-manifests.js.map +1 -1
- package/dist/core/orchestration/policy-compiler.d.ts.map +1 -1
- package/dist/core/orchestration/policy-compiler.js +8 -9
- package/dist/core/orchestration/policy-compiler.js.map +1 -1
- package/dist/core/orchestration/profile-registry.d.ts.map +1 -1
- package/dist/core/orchestration/profile-registry.js +4 -10
- package/dist/core/orchestration/profile-registry.js.map +1 -1
- package/dist/core/orchestration/task-runtime.d.ts +1 -0
- package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
- package/dist/core/orchestration/task-runtime.js +58 -3
- package/dist/core/orchestration/task-runtime.js.map +1 -1
- package/dist/core/orchestration/worker-execution-contract.d.ts.map +1 -1
- package/dist/core/orchestration/worker-execution-contract.js +10 -17
- package/dist/core/orchestration/worker-execution-contract.js.map +1 -1
- package/dist/core/risk-classifier.js +3 -3
- package/dist/core/risk-classifier.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +4 -1
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +34 -15
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/secrets/bitwarden-credential-storage.d.ts +51 -0
- package/dist/core/secrets/bitwarden-credential-storage.d.ts.map +1 -0
- package/dist/core/secrets/bitwarden-credential-storage.js +345 -0
- package/dist/core/secrets/bitwarden-credential-storage.js.map +1 -0
- package/dist/core/secrets/credential-exposure-guard.d.ts +8 -4
- package/dist/core/secrets/credential-exposure-guard.d.ts.map +1 -1
- package/dist/core/secrets/credential-exposure-guard.js +30 -31
- package/dist/core/secrets/credential-exposure-guard.js.map +1 -1
- package/dist/core/secrets/credential-manager.d.ts +109 -0
- package/dist/core/secrets/credential-manager.d.ts.map +1 -0
- package/dist/core/secrets/credential-manager.js +384 -0
- package/dist/core/secrets/credential-manager.js.map +1 -0
- package/dist/core/secrets/credential-project.d.ts +4 -0
- package/dist/core/secrets/credential-project.d.ts.map +1 -0
- package/dist/core/secrets/credential-project.js +145 -0
- package/dist/core/secrets/credential-project.js.map +1 -0
- package/dist/core/secrets/secret-dotenv.d.ts +1 -1
- package/dist/core/secrets/secret-dotenv.js +1 -1
- package/dist/core/secrets/secret-dotenv.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -2
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +9 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tool-capability-policy.js +2 -2
- package/dist/core/tool-capability-policy.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +29 -1
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +179 -28
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/git-filter.d.ts +1 -0
- package/dist/core/tools/git-filter.d.ts.map +1 -1
- package/dist/core/tools/git-filter.js +1 -1
- package/dist/core/tools/git-filter.js.map +1 -1
- package/dist/core/tools/profile-writer.js +3 -3
- package/dist/core/tools/profile-writer.js.map +1 -1
- package/dist/core/tools/python.d.ts +2 -0
- package/dist/core/tools/python.d.ts.map +1 -1
- package/dist/core/tools/python.js +17 -12
- package/dist/core/tools/python.js.map +1 -1
- package/dist/core/tools/run-process.d.ts +2 -0
- package/dist/core/tools/run-process.d.ts.map +1 -1
- package/dist/core/tools/run-process.js +7 -3
- package/dist/core/tools/run-process.js.map +1 -1
- package/dist/core/tools/secret-store.d.ts +8 -18
- package/dist/core/tools/secret-store.d.ts.map +1 -1
- package/dist/core/tools/secret-store.js +83 -348
- package/dist/core/tools/secret-store.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +38 -1
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +35 -14
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +26 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/secret-menu-controller.d.ts +21 -0
- package/dist/modes/interactive/secret-menu-controller.d.ts.map +1 -0
- package/dist/modes/interactive/secret-menu-controller.js +276 -0
- package/dist/modes/interactive/secret-menu-controller.js.map +1 -0
- package/dist/utils/tools-manager.d.ts +2 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +43 -48
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/zip-extractor.d.ts +8 -0
- package/dist/utils/zip-extractor.d.ts.map +1 -0
- package/dist/utils/zip-extractor.js +275 -0
- package/dist/utils/zip-extractor.js.map +1 -0
- package/docs/harness-architecture.md +34 -22
- package/docs/index.md +1 -1
- package/docs/resources.md +1 -1
- package/docs/settings.md +39 -103
- package/docs/usage.md +7 -5
- package/docs/worker-profiles.md +9 -7
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
- package/dist/core/secrets/secret-vault-format.d.ts +0 -112
- package/dist/core/secrets/secret-vault-format.d.ts.map +0 -1
- package/dist/core/secrets/secret-vault-format.js +0 -396
- package/dist/core/secrets/secret-vault-format.js.map +0 -1
- package/dist/core/secrets/secret-vault.d.ts +0 -72
- package/dist/core/secrets/secret-vault.d.ts.map +0 -1
- package/dist/core/secrets/secret-vault.js +0 -663
- package/dist/core/secrets/secret-vault.js.map +0 -1
|
@@ -1,663 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { constants, promises as fs } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { managedSecretEnvDir, secretsDir, secretVaultFile } from "../agent-paths.js";
|
|
5
|
-
import { redactKnownSecrets } from "../security/secret-text.js";
|
|
6
|
-
import { withFileLock, writeFileAtomic } from "../util/atomic-file.js";
|
|
7
|
-
import { MAX_DOTENV_DOCUMENT_BYTES, parseDotenvDocument } from "./secret-dotenv.js";
|
|
8
|
-
import { assertPassphrase, createVaultKdf, DEFAULT_SCRYPT_N, decryptPayload, deriveKey, MANAGED_ENV_HEADER_PREFIX, MAX_BINDINGS_PER_PROFILE, MAX_PROFILES, MAX_VARIABLES_PER_PROFILE, MAX_VAULT_FILE_BYTES, parseEnvelope, profileSummary, SecretVaultError, sameKdf, scrubPayload, serializeEnvelope, serializeVariableValues, VAULT_MARKER, validateBindingEnvFile, validateBindingWorkspace, validateDescription, validateDotenvProfileDocument, validateProfileId, validateScryptN, validateSecretValue, validateVariableName, } from "./secret-vault-format.js";
|
|
9
|
-
export { MAX_BINDINGS_PER_PROFILE, MAX_VARIABLES_PER_PROFILE, SECRET_DESCRIPTION_MAX_CHARS, SECRET_PRINTABLE_METADATA_PATTERN, SECRET_PROFILE_ID_MAX_CHARS, SECRET_PROFILE_ID_PATTERN, SECRET_VARIABLE_NAME_MAX_CHARS, SECRET_VARIABLE_NAME_PATTERN, SecretVaultError, } from "./secret-vault-format.js";
|
|
10
|
-
function nodeErrorCode(error) {
|
|
11
|
-
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
12
|
-
? error.code
|
|
13
|
-
: undefined;
|
|
14
|
-
}
|
|
15
|
-
async function lstatIfExists(path) {
|
|
16
|
-
try {
|
|
17
|
-
return await fs.lstat(path);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (nodeErrorCode(error) === "ENOENT")
|
|
21
|
-
return undefined;
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
async function readBoundedRegularFile(filePath, maxBytes, label) {
|
|
26
|
-
const before = await fs.lstat(filePath);
|
|
27
|
-
if (before.isSymbolicLink())
|
|
28
|
-
throw new SecretVaultError("symlink_refused", `${label} symlinks are refused.`);
|
|
29
|
-
if (!before.isFile()) {
|
|
30
|
-
throw new SecretVaultError(label === "Secret vault" ? "vault_corrupt" : "destination_invalid", `${label} path is not a regular file.`);
|
|
31
|
-
}
|
|
32
|
-
if (before.size > maxBytes) {
|
|
33
|
-
throw new SecretVaultError(label === "Secret vault" ? "vault_too_large" : "invalid_dotenv", `${label} exceeds its ${label === "Secret vault" ? "4 MiB" : "512 KiB"} limit.`);
|
|
34
|
-
}
|
|
35
|
-
const handle = await fs.open(filePath, constants.O_RDONLY);
|
|
36
|
-
try {
|
|
37
|
-
const opened = await handle.stat();
|
|
38
|
-
if ((before.dev !== opened.dev || before.ino !== opened.ino) && before.ino !== 0 && opened.ino !== 0) {
|
|
39
|
-
throw new SecretVaultError("symlink_refused", "Secret vault changed while it was being opened.");
|
|
40
|
-
}
|
|
41
|
-
const output = Buffer.alloc(Math.min(maxBytes + 1, before.size + 1));
|
|
42
|
-
let offset = 0;
|
|
43
|
-
while (offset < output.byteLength) {
|
|
44
|
-
const { bytesRead } = await handle.read(output, offset, output.byteLength - offset, null);
|
|
45
|
-
if (bytesRead === 0)
|
|
46
|
-
break;
|
|
47
|
-
offset += bytesRead;
|
|
48
|
-
}
|
|
49
|
-
if (offset > before.size || offset > maxBytes) {
|
|
50
|
-
output.fill(0);
|
|
51
|
-
throw new SecretVaultError(label === "Secret vault" ? "vault_too_large" : "invalid_dotenv", `${label} grew beyond its limit while being read.`);
|
|
52
|
-
}
|
|
53
|
-
const text = output.toString("utf8", 0, offset);
|
|
54
|
-
output.fill(0);
|
|
55
|
-
return text;
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
await handle.close();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function readBoundedFile(filePath) {
|
|
62
|
-
return readBoundedRegularFile(filePath, MAX_VAULT_FILE_BYTES, "Secret vault");
|
|
63
|
-
}
|
|
64
|
-
async function readFilePrefix(filePath, maxBytes) {
|
|
65
|
-
const handle = await fs.open(filePath, constants.O_RDONLY);
|
|
66
|
-
try {
|
|
67
|
-
const output = Buffer.alloc(maxBytes);
|
|
68
|
-
const { bytesRead } = await handle.read(output, 0, output.byteLength, 0);
|
|
69
|
-
const text = output.toString("utf8", 0, bytesRead);
|
|
70
|
-
output.fill(0);
|
|
71
|
-
return text;
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
await handle.close();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async function ensurePrivateDirectory(path) {
|
|
78
|
-
const existing = await lstatIfExists(path);
|
|
79
|
-
if (existing?.isSymbolicLink()) {
|
|
80
|
-
throw new SecretVaultError("symlink_refused", "Secret storage directory symlinks are refused.");
|
|
81
|
-
}
|
|
82
|
-
if (existing && !existing.isDirectory()) {
|
|
83
|
-
throw new SecretVaultError("destination_invalid", "Secret storage path is not a directory.");
|
|
84
|
-
}
|
|
85
|
-
await fs.mkdir(path, { recursive: true, mode: 0o700 });
|
|
86
|
-
if (process.platform !== "win32")
|
|
87
|
-
await fs.chmod(path, 0o700);
|
|
88
|
-
}
|
|
89
|
-
async function enforcePrivateFileMode(path) {
|
|
90
|
-
if (process.platform !== "win32")
|
|
91
|
-
await fs.chmod(path, 0o600);
|
|
92
|
-
}
|
|
93
|
-
function isPathInside(root, target) {
|
|
94
|
-
const fromRoot = relative(root, target);
|
|
95
|
-
return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
|
|
96
|
-
}
|
|
97
|
-
function escapeRegExp(value) {
|
|
98
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Encrypted generic credential vault. Only the derived key is retained in memory, until this
|
|
102
|
-
* process explicitly locks the vault or exits. The native model-facing tool receives summaries,
|
|
103
|
-
* never decrypted values or materialization paths.
|
|
104
|
-
*/
|
|
105
|
-
export class SecretVault {
|
|
106
|
-
constructor(options) {
|
|
107
|
-
this.cachedProfiles = [];
|
|
108
|
-
this.activeProfilesByWorkspace = new Map();
|
|
109
|
-
this.rootDir = secretsDir(options.agentDir);
|
|
110
|
-
this.vaultPath = secretVaultFile(options.agentDir);
|
|
111
|
-
this.materializedDir = managedSecretEnvDir(options.agentDir);
|
|
112
|
-
this.scryptN = validateScryptN(options.scryptN ?? DEFAULT_SCRYPT_N);
|
|
113
|
-
this.now = options.now ?? (() => new Date());
|
|
114
|
-
this.onEnvironmentChanged = options.onEnvironmentChanged;
|
|
115
|
-
}
|
|
116
|
-
static forAgentDir(agentDir) {
|
|
117
|
-
return new SecretVault({ agentDir });
|
|
118
|
-
}
|
|
119
|
-
get isUnlocked() {
|
|
120
|
-
return this.cachedKey !== undefined;
|
|
121
|
-
}
|
|
122
|
-
async exists() {
|
|
123
|
-
const existing = await lstatIfExists(this.vaultPath);
|
|
124
|
-
if (existing?.isSymbolicLink()) {
|
|
125
|
-
throw new SecretVaultError("symlink_refused", "Secret vault symlinks are refused.");
|
|
126
|
-
}
|
|
127
|
-
return existing !== undefined;
|
|
128
|
-
}
|
|
129
|
-
lock() {
|
|
130
|
-
this.cachedKey?.fill(0);
|
|
131
|
-
this.cachedKey = undefined;
|
|
132
|
-
this.cachedKdf = undefined;
|
|
133
|
-
this.clearSecretCache();
|
|
134
|
-
this.activeProfilesByWorkspace.clear();
|
|
135
|
-
this.notifyEnvironmentChanged();
|
|
136
|
-
}
|
|
137
|
-
/** Environment for the nearest explicitly activated profile covering cwd. */
|
|
138
|
-
getEnvironmentForCwd(cwd) {
|
|
139
|
-
const match = this.resolveCachedProfile(cwd);
|
|
140
|
-
if (!match)
|
|
141
|
-
return {};
|
|
142
|
-
return Object.fromEntries(match.profile.variables.map((variable) => [variable.name, variable.value]));
|
|
143
|
-
}
|
|
144
|
-
hasEnvironmentForCwd(cwd) {
|
|
145
|
-
return this.resolveCachedProfile(cwd) !== undefined;
|
|
146
|
-
}
|
|
147
|
-
/** Remove exact unlocked vault values and high-confidence credential shapes from model-facing text. */
|
|
148
|
-
redactSensitiveText(text) {
|
|
149
|
-
let redacted = redactKnownSecrets(text);
|
|
150
|
-
const values = [
|
|
151
|
-
...new Set(this.cachedProfiles
|
|
152
|
-
.flatMap((profile) => profile.variables.map((variable) => variable.value))
|
|
153
|
-
.filter(Boolean)),
|
|
154
|
-
].sort((left, right) => right.length - left.length);
|
|
155
|
-
for (const value of values) {
|
|
156
|
-
if (value.length >= 4) {
|
|
157
|
-
redacted = redacted.split(value).join("[REDACTED]");
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const bounded = new RegExp(`(^|[\\s=:;,])${escapeRegExp(value)}(?=$|[\\s,;])`, "g");
|
|
161
|
-
redacted = redacted.replace(bounded, (_match, prefix) => `${prefix}[REDACTED]`);
|
|
162
|
-
}
|
|
163
|
-
return redacted;
|
|
164
|
-
}
|
|
165
|
-
async initialize(passphrase) {
|
|
166
|
-
assertPassphrase(passphrase, true);
|
|
167
|
-
await ensurePrivateDirectory(this.rootDir);
|
|
168
|
-
await withFileLock(this.vaultPath, async () => {
|
|
169
|
-
if (await lstatIfExists(this.vaultPath)) {
|
|
170
|
-
throw new SecretVaultError("already_initialized", "Secret vault is already initialized.");
|
|
171
|
-
}
|
|
172
|
-
const kdf = createVaultKdf(this.scryptN);
|
|
173
|
-
const key = await deriveKey(passphrase, kdf);
|
|
174
|
-
const payload = { marker: VAULT_MARKER, profiles: [] };
|
|
175
|
-
try {
|
|
176
|
-
await this.writeEnvelope(kdf, payload, key);
|
|
177
|
-
this.replaceCachedKey(key, kdf);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
key.fill(0);
|
|
181
|
-
throw error;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
async unlock(passphrase) {
|
|
186
|
-
assertPassphrase(passphrase, false);
|
|
187
|
-
if (!(await this.exists()))
|
|
188
|
-
throw new SecretVaultError("not_initialized", "Secret vault is not initialized.");
|
|
189
|
-
const envelope = parseEnvelope(await readBoundedFile(this.vaultPath));
|
|
190
|
-
const key = await deriveKey(passphrase, envelope.kdf);
|
|
191
|
-
let payload;
|
|
192
|
-
try {
|
|
193
|
-
const environmentBefore = this.activeEnvironmentFingerprint();
|
|
194
|
-
payload = decryptPayload(envelope.payload, key);
|
|
195
|
-
this.replaceCachedKey(key, envelope.kdf);
|
|
196
|
-
this.refreshSecretCache(payload);
|
|
197
|
-
if (environmentBefore !== this.activeEnvironmentFingerprint())
|
|
198
|
-
this.notifyEnvironmentChanged();
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
key.fill(0);
|
|
202
|
-
throw error;
|
|
203
|
-
}
|
|
204
|
-
finally {
|
|
205
|
-
if (payload)
|
|
206
|
-
scrubPayload(payload);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
async listProfiles() {
|
|
210
|
-
return this.withPayload((payload) => payload.profiles.map(profileSummary), false);
|
|
211
|
-
}
|
|
212
|
-
async upsertProfile(profile, description, values) {
|
|
213
|
-
if (values.length === 0) {
|
|
214
|
-
throw new SecretVaultError("invalid_variable", "At least one environment variable is required.");
|
|
215
|
-
}
|
|
216
|
-
const seenNames = new Set();
|
|
217
|
-
const normalizedValues = values.map((entry) => {
|
|
218
|
-
const name = validateVariableName(entry.name);
|
|
219
|
-
if (seenNames.has(name)) {
|
|
220
|
-
throw new SecretVaultError("invalid_variable", `Environment variable ${name} is duplicated.`);
|
|
221
|
-
}
|
|
222
|
-
seenNames.add(name);
|
|
223
|
-
return { name, value: validateSecretValue(entry.value) };
|
|
224
|
-
});
|
|
225
|
-
return this.storeProfileDocument(profile, description, serializeVariableValues(normalizedValues), undefined, true);
|
|
226
|
-
}
|
|
227
|
-
async replaceProfileDocument(profile, description, document, binding) {
|
|
228
|
-
return this.storeProfileDocument(profile, description, document, binding, false);
|
|
229
|
-
}
|
|
230
|
-
async getProfileDocument(profile) {
|
|
231
|
-
const normalizedProfile = validateProfileId(profile);
|
|
232
|
-
return this.withPayload((payload) => payload.profiles.find((candidate) => candidate.profile === normalizedProfile)?.dotenv, false);
|
|
233
|
-
}
|
|
234
|
-
async readOwnerDotenv(destination) {
|
|
235
|
-
this.assertDestination(destination);
|
|
236
|
-
return readBoundedRegularFile(destination, MAX_DOTENV_DOCUMENT_BYTES, "Dotenv file");
|
|
237
|
-
}
|
|
238
|
-
async removeProfile(profile, variableNames) {
|
|
239
|
-
const normalizedProfile = validateProfileId(profile);
|
|
240
|
-
const normalizedNames = variableNames?.map(validateVariableName);
|
|
241
|
-
if (normalizedNames && new Set(normalizedNames).size !== normalizedNames.length) {
|
|
242
|
-
throw new SecretVaultError("invalid_variable", "Environment variable removals are duplicated.");
|
|
243
|
-
}
|
|
244
|
-
let managedMaterializationRemoved = false;
|
|
245
|
-
let boundMaterializationsRemoved = 0;
|
|
246
|
-
const result = await this.withPayload(async (payload) => {
|
|
247
|
-
const index = payload.profiles.findIndex((candidate) => candidate.profile === normalizedProfile);
|
|
248
|
-
const record = payload.profiles[index];
|
|
249
|
-
if (!record)
|
|
250
|
-
throw new SecretVaultError("profile_missing", "Secret profile does not exist.");
|
|
251
|
-
const variables = validateDotenvProfileDocument(record.dotenv).variables;
|
|
252
|
-
let removedProfile;
|
|
253
|
-
let removedVariableNames;
|
|
254
|
-
let remaining = [];
|
|
255
|
-
if (normalizedNames === undefined) {
|
|
256
|
-
removedProfile = true;
|
|
257
|
-
removedVariableNames = variables.map((variable) => variable.name);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
if (normalizedNames.length === 0) {
|
|
261
|
-
throw new SecretVaultError("invalid_variable", "Select at least one environment variable to remove.");
|
|
262
|
-
}
|
|
263
|
-
const requested = new Set(normalizedNames);
|
|
264
|
-
removedVariableNames = variables
|
|
265
|
-
.filter((variable) => requested.has(variable.name))
|
|
266
|
-
.map((variable) => variable.name);
|
|
267
|
-
if (removedVariableNames.length !== requested.size) {
|
|
268
|
-
throw new SecretVaultError("invalid_variable", "One or more requested environment variables do not exist.");
|
|
269
|
-
}
|
|
270
|
-
remaining = variables.filter((variable) => !requested.has(variable.name));
|
|
271
|
-
removedProfile = remaining.length === 0;
|
|
272
|
-
}
|
|
273
|
-
// Validate the complete mutation before removing any recoverable materialization.
|
|
274
|
-
managedMaterializationRemoved = await this.removeManagedMaterialization(normalizedProfile);
|
|
275
|
-
boundMaterializationsRemoved = await this.removeBoundMaterializations(record);
|
|
276
|
-
if (removedProfile)
|
|
277
|
-
payload.profiles.splice(index, 1);
|
|
278
|
-
else {
|
|
279
|
-
record.dotenv = serializeVariableValues(remaining);
|
|
280
|
-
record.updatedAt = this.now().toISOString();
|
|
281
|
-
}
|
|
282
|
-
return { removedProfile, removedVariableNames };
|
|
283
|
-
}, true);
|
|
284
|
-
this.deactivateProfile(normalizedProfile);
|
|
285
|
-
return {
|
|
286
|
-
profile: normalizedProfile,
|
|
287
|
-
...result,
|
|
288
|
-
managedMaterializationRemoved,
|
|
289
|
-
boundMaterializationsRemoved,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
getManagedEnvPath(profile) {
|
|
293
|
-
return join(this.materializedDir, `${validateProfileId(profile)}.env`);
|
|
294
|
-
}
|
|
295
|
-
resolveBindingTarget(workspace, envFile) {
|
|
296
|
-
const normalizedWorkspace = validateBindingWorkspace(resolve(workspace));
|
|
297
|
-
const normalizedEnvFile = validateBindingEnvFile(envFile);
|
|
298
|
-
const destination = resolve(normalizedWorkspace, normalizedEnvFile);
|
|
299
|
-
if (!isPathInside(normalizedWorkspace, destination)) {
|
|
300
|
-
throw new SecretVaultError("destination_invalid", "Credential target must stay inside its workspace.");
|
|
301
|
-
}
|
|
302
|
-
return { workspace: normalizedWorkspace, envFile: normalizedEnvFile, destination };
|
|
303
|
-
}
|
|
304
|
-
async resolveBindingForWorkspace(cwd, profile) {
|
|
305
|
-
const normalizedCwd = resolve(cwd);
|
|
306
|
-
const normalizedProfile = profile === undefined ? undefined : validateProfileId(profile);
|
|
307
|
-
return this.withPayload((payload) => {
|
|
308
|
-
const matches = payload.profiles.flatMap((candidate) => {
|
|
309
|
-
if (normalizedProfile !== undefined && candidate.profile !== normalizedProfile)
|
|
310
|
-
return [];
|
|
311
|
-
const binding = candidate.bindings
|
|
312
|
-
.filter((entry) => isPathInside(entry.workspace, normalizedCwd))
|
|
313
|
-
.sort((left, right) => right.workspace.length - left.workspace.length)[0];
|
|
314
|
-
if (!binding)
|
|
315
|
-
return [];
|
|
316
|
-
return [{ candidate, binding }];
|
|
317
|
-
});
|
|
318
|
-
if (matches.length === 0) {
|
|
319
|
-
throw new SecretVaultError("binding_missing", normalizedProfile
|
|
320
|
-
? `Secret profile ${normalizedProfile} is not bound to this workspace.`
|
|
321
|
-
: "No secret profile is bound to this workspace.");
|
|
322
|
-
}
|
|
323
|
-
const longestRoot = Math.max(...matches.map((match) => match.binding.workspace.length));
|
|
324
|
-
const nearest = matches.filter((match) => match.binding.workspace.length === longestRoot);
|
|
325
|
-
let selected = nearest[0];
|
|
326
|
-
if (nearest.length > 1) {
|
|
327
|
-
const active = nearest.find((match) => this.activeProfilesByWorkspace.get(match.binding.workspace) === match.candidate.profile);
|
|
328
|
-
if (!active) {
|
|
329
|
-
throw new SecretVaultError("ambiguous_binding", "Multiple secret profiles apply to this workspace; select one by profile name.");
|
|
330
|
-
}
|
|
331
|
-
selected = active;
|
|
332
|
-
}
|
|
333
|
-
const parsed = validateDotenvProfileDocument(selected.candidate.dotenv);
|
|
334
|
-
return {
|
|
335
|
-
profile: selected.candidate.profile,
|
|
336
|
-
...selected.binding,
|
|
337
|
-
destination: resolve(selected.binding.workspace, selected.binding.envFile),
|
|
338
|
-
variableNames: parsed.variables.map((variable) => variable.name),
|
|
339
|
-
};
|
|
340
|
-
}, false);
|
|
341
|
-
}
|
|
342
|
-
async inspectEnvDestination(destination, profile) {
|
|
343
|
-
validateProfileId(profile);
|
|
344
|
-
this.assertDestination(destination);
|
|
345
|
-
const existing = await lstatIfExists(destination);
|
|
346
|
-
if (!existing)
|
|
347
|
-
return "missing";
|
|
348
|
-
if (existing.isSymbolicLink()) {
|
|
349
|
-
throw new SecretVaultError("symlink_refused", "Secret dotenv destination symlinks are refused.");
|
|
350
|
-
}
|
|
351
|
-
if (!existing.isFile()) {
|
|
352
|
-
throw new SecretVaultError("destination_invalid", "Secret dotenv destination is not a regular file.");
|
|
353
|
-
}
|
|
354
|
-
const firstLine = (await readFilePrefix(destination, 512)).split(/\r?\n/, 1)[0] ?? "";
|
|
355
|
-
if (firstLine === `${MANAGED_ENV_HEADER_PREFIX}${profile}`)
|
|
356
|
-
return "managed-profile";
|
|
357
|
-
if (firstLine.startsWith(MANAGED_ENV_HEADER_PREFIX))
|
|
358
|
-
return "managed-other";
|
|
359
|
-
return "unmanaged";
|
|
360
|
-
}
|
|
361
|
-
async materializeEnv(profile, destination, options = {}) {
|
|
362
|
-
const normalizedProfile = validateProfileId(profile);
|
|
363
|
-
this.assertDestination(destination);
|
|
364
|
-
const previousState = await this.inspectEnvDestination(destination, normalizedProfile);
|
|
365
|
-
await this.assertReplaceableDestination(destination, previousState, options);
|
|
366
|
-
if (options.managed) {
|
|
367
|
-
if (resolve(destination) !== resolve(this.getManagedEnvPath(normalizedProfile))) {
|
|
368
|
-
throw new SecretVaultError("destination_invalid", "Managed dotenv destination is not canonical.");
|
|
369
|
-
}
|
|
370
|
-
await ensurePrivateDirectory(this.materializedDir);
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
const parent = await lstatIfExists(dirname(destination));
|
|
374
|
-
if (!parent?.isDirectory()) {
|
|
375
|
-
throw new SecretVaultError("destination_invalid", "Dotenv destination directory does not exist.");
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return this.withPayload(async (payload) => {
|
|
379
|
-
const record = payload.profiles.find((candidate) => candidate.profile === normalizedProfile);
|
|
380
|
-
if (!record)
|
|
381
|
-
throw new SecretVaultError("profile_missing", "Secret profile does not exist.");
|
|
382
|
-
const binding = record.bindings.find((candidate) => resolve(candidate.workspace, candidate.envFile) === resolve(destination));
|
|
383
|
-
const activationCwd = options.activationWorkspace ? resolve(options.activationWorkspace) : undefined;
|
|
384
|
-
const activationBinding = activationCwd
|
|
385
|
-
? record.bindings
|
|
386
|
-
.filter((candidate) => isPathInside(candidate.workspace, activationCwd))
|
|
387
|
-
.sort((left, right) => right.workspace.length - left.workspace.length)[0]
|
|
388
|
-
: binding;
|
|
389
|
-
if (activationCwd && !activationBinding) {
|
|
390
|
-
throw new SecretVaultError("binding_missing", `Secret profile ${normalizedProfile} is not bound to this workspace.`);
|
|
391
|
-
}
|
|
392
|
-
if (binding && !options.managed)
|
|
393
|
-
await this.assertBoundDestinationParent(binding, destination);
|
|
394
|
-
const currentState = await this.inspectEnvDestination(destination, normalizedProfile);
|
|
395
|
-
await this.assertReplaceableDestination(destination, currentState, options);
|
|
396
|
-
const variableNames = validateDotenvProfileDocument(record.dotenv).variables.map((variable) => variable.name);
|
|
397
|
-
const body = record.dotenv.endsWith("\n") ? record.dotenv : `${record.dotenv}\n`;
|
|
398
|
-
let dotenv = `${MANAGED_ENV_HEADER_PREFIX}${normalizedProfile}\n# Generated by Pi. Do not edit, inspect, source as shell code, or commit.\n${body}`;
|
|
399
|
-
try {
|
|
400
|
-
await writeFileAtomic(destination, dotenv, { mode: 0o600 });
|
|
401
|
-
await enforcePrivateFileMode(destination);
|
|
402
|
-
if (activationBinding) {
|
|
403
|
-
this.activeProfilesByWorkspace.set(activationBinding.workspace, normalizedProfile);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
finally {
|
|
407
|
-
dotenv = "";
|
|
408
|
-
}
|
|
409
|
-
return { profile: normalizedProfile, variableNames, previousState: currentState };
|
|
410
|
-
}, false);
|
|
411
|
-
}
|
|
412
|
-
async storeProfileDocument(profile, description, document, binding, merge) {
|
|
413
|
-
const normalizedProfile = validateProfileId(profile);
|
|
414
|
-
const normalizedDescription = validateDescription(description);
|
|
415
|
-
const parsedDocument = validateDotenvProfileDocument(document);
|
|
416
|
-
const target = binding ? this.resolveBindingTarget(binding.workspace, binding.envFile) : undefined;
|
|
417
|
-
const normalizedBinding = target ? { workspace: target.workspace, envFile: target.envFile } : undefined;
|
|
418
|
-
const summary = await this.withPayload(async (payload) => {
|
|
419
|
-
let record = payload.profiles.find((candidate) => candidate.profile === normalizedProfile);
|
|
420
|
-
const timestamp = this.now().toISOString();
|
|
421
|
-
if (!record && payload.profiles.length >= MAX_PROFILES) {
|
|
422
|
-
throw new SecretVaultError("profile_limit", `Secret vault supports at most ${MAX_PROFILES} profiles.`);
|
|
423
|
-
}
|
|
424
|
-
const nextBindings = record?.bindings.map((entry) => ({ ...entry })) ?? [];
|
|
425
|
-
if (normalizedBinding) {
|
|
426
|
-
const next = { ...normalizedBinding, updatedAt: timestamp };
|
|
427
|
-
const existingIndex = nextBindings.findIndex((entry) => entry.workspace === normalizedBinding.workspace);
|
|
428
|
-
if (existingIndex === -1)
|
|
429
|
-
nextBindings.push(next);
|
|
430
|
-
else
|
|
431
|
-
nextBindings[existingIndex] = next;
|
|
432
|
-
if (nextBindings.length > MAX_BINDINGS_PER_PROFILE) {
|
|
433
|
-
throw new SecretVaultError("profile_limit", `A secret profile supports at most ${MAX_BINDINGS_PER_PROFILE} workspace bindings.`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
let nextDocument = parsedDocument.document;
|
|
437
|
-
if (merge && record) {
|
|
438
|
-
const merged = new Map(validateDotenvProfileDocument(record.dotenv).variables.map((variable) => [
|
|
439
|
-
variable.name,
|
|
440
|
-
variable.value,
|
|
441
|
-
]));
|
|
442
|
-
for (const variable of parsedDocument.variables)
|
|
443
|
-
merged.set(variable.name, variable.value);
|
|
444
|
-
if (merged.size > MAX_VARIABLES_PER_PROFILE) {
|
|
445
|
-
throw new SecretVaultError("variable_limit", `A secret profile supports at most ${MAX_VARIABLES_PER_PROFILE} variables.`);
|
|
446
|
-
}
|
|
447
|
-
nextDocument = serializeVariableValues([...merged].map(([name, value]) => ({ name, value })));
|
|
448
|
-
}
|
|
449
|
-
if (record) {
|
|
450
|
-
await this.removeManagedMaterialization(normalizedProfile);
|
|
451
|
-
await this.removeBoundMaterializations(record);
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
record = {
|
|
455
|
-
profile: normalizedProfile,
|
|
456
|
-
createdAt: timestamp,
|
|
457
|
-
updatedAt: timestamp,
|
|
458
|
-
dotenv: nextDocument,
|
|
459
|
-
bindings: [],
|
|
460
|
-
};
|
|
461
|
-
payload.profiles.push(record);
|
|
462
|
-
}
|
|
463
|
-
if (normalizedDescription !== undefined)
|
|
464
|
-
record.description = normalizedDescription;
|
|
465
|
-
record.dotenv = nextDocument;
|
|
466
|
-
record.bindings = nextBindings.sort((left, right) => left.workspace.localeCompare(right.workspace) || left.envFile.localeCompare(right.envFile));
|
|
467
|
-
record.updatedAt = timestamp;
|
|
468
|
-
payload.profiles.sort((left, right) => left.profile.localeCompare(right.profile));
|
|
469
|
-
return profileSummary(record);
|
|
470
|
-
}, true);
|
|
471
|
-
this.deactivateProfile(normalizedProfile);
|
|
472
|
-
return summary;
|
|
473
|
-
}
|
|
474
|
-
async assertReplaceableDestination(destination, state, options) {
|
|
475
|
-
if (state === "managed-other") {
|
|
476
|
-
throw new SecretVaultError("destination_conflict", "Destination already contains a file managed for another secret profile.");
|
|
477
|
-
}
|
|
478
|
-
if (state !== "unmanaged")
|
|
479
|
-
return;
|
|
480
|
-
if (!options.allowReplaceUnmanaged || options.expectedUnmanagedContent === undefined) {
|
|
481
|
-
throw new SecretVaultError("destination_conflict", "Destination already contains an unmanaged file that was not reviewed for replacement.");
|
|
482
|
-
}
|
|
483
|
-
if ((await this.readOwnerDotenv(destination)) !== options.expectedUnmanagedContent) {
|
|
484
|
-
throw new SecretVaultError("destination_conflict", "The reviewed dotenv file changed before replacement and was preserved.");
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
async assertBoundDestinationParent(binding, destination) {
|
|
488
|
-
try {
|
|
489
|
-
const [workspace, parent] = await Promise.all([
|
|
490
|
-
fs.realpath(binding.workspace),
|
|
491
|
-
fs.realpath(dirname(destination)),
|
|
492
|
-
]);
|
|
493
|
-
if (isPathInside(workspace, parent))
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
catch {
|
|
497
|
-
// Collapse filesystem detail so a model-facing failure cannot disclose unrelated paths.
|
|
498
|
-
}
|
|
499
|
-
throw new SecretVaultError("destination_invalid", "Credential destination resolves outside its bound workspace or cannot be verified.");
|
|
500
|
-
}
|
|
501
|
-
assertDestination(destination) {
|
|
502
|
-
if (!isAbsolute(destination) || resolve(destination) === resolve(this.vaultPath)) {
|
|
503
|
-
throw new SecretVaultError("destination_invalid", "Secret dotenv destination must be an absolute non-vault path.");
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
replaceCachedKey(key, kdf) {
|
|
507
|
-
this.cachedKey?.fill(0);
|
|
508
|
-
this.cachedKey = key;
|
|
509
|
-
this.cachedKdf = { ...kdf };
|
|
510
|
-
}
|
|
511
|
-
clearSecretCache() {
|
|
512
|
-
for (const profile of this.cachedProfiles) {
|
|
513
|
-
for (const variable of profile.variables)
|
|
514
|
-
variable.value = "";
|
|
515
|
-
}
|
|
516
|
-
this.cachedProfiles = [];
|
|
517
|
-
}
|
|
518
|
-
deactivateProfile(profile) {
|
|
519
|
-
let changed = false;
|
|
520
|
-
for (const [workspace, activeProfile] of this.activeProfilesByWorkspace) {
|
|
521
|
-
if (activeProfile !== profile)
|
|
522
|
-
continue;
|
|
523
|
-
this.activeProfilesByWorkspace.delete(workspace);
|
|
524
|
-
changed = true;
|
|
525
|
-
}
|
|
526
|
-
const cached = this.cachedProfiles.find((candidate) => candidate.profile === profile);
|
|
527
|
-
if (cached) {
|
|
528
|
-
for (const variable of cached.variables)
|
|
529
|
-
variable.value = "";
|
|
530
|
-
cached.variables = [];
|
|
531
|
-
}
|
|
532
|
-
if (changed)
|
|
533
|
-
this.notifyEnvironmentChanged();
|
|
534
|
-
}
|
|
535
|
-
notifyEnvironmentChanged() {
|
|
536
|
-
try {
|
|
537
|
-
this.onEnvironmentChanged?.();
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
// Credential state changes remain authoritative even if host cleanup is best-effort.
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
activeEnvironmentFingerprint() {
|
|
544
|
-
const hash = createHash("sha256");
|
|
545
|
-
for (const profile of this.cachedProfiles) {
|
|
546
|
-
const activeBindings = profile.bindings
|
|
547
|
-
.filter((binding) => this.activeProfilesByWorkspace.get(binding.workspace) === profile.profile)
|
|
548
|
-
.sort((left, right) => left.workspace.localeCompare(right.workspace));
|
|
549
|
-
if (activeBindings.length === 0)
|
|
550
|
-
continue;
|
|
551
|
-
hash.update(`${profile.profile.length}:${profile.profile}`);
|
|
552
|
-
for (const binding of activeBindings)
|
|
553
|
-
hash.update(`${binding.workspace.length}:${binding.workspace}`);
|
|
554
|
-
for (const variable of profile.variables) {
|
|
555
|
-
hash.update(`${variable.name.length}:${variable.name}${variable.value.length}:${variable.value}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return hash.digest("hex");
|
|
559
|
-
}
|
|
560
|
-
refreshSecretCache(payload) {
|
|
561
|
-
this.clearSecretCache();
|
|
562
|
-
this.cachedProfiles = payload.profiles.map((profile) => ({
|
|
563
|
-
profile: profile.profile,
|
|
564
|
-
bindings: profile.bindings.map((binding) => ({ ...binding })),
|
|
565
|
-
variables: profile.bindings.some((binding) => this.activeProfilesByWorkspace.get(binding.workspace) === profile.profile)
|
|
566
|
-
? parseDotenvDocument(profile.dotenv).variables.map((variable) => ({ ...variable }))
|
|
567
|
-
: [],
|
|
568
|
-
}));
|
|
569
|
-
for (const [workspace, profile] of this.activeProfilesByWorkspace) {
|
|
570
|
-
const stillBound = this.cachedProfiles.some((candidate) => candidate.profile === profile && candidate.bindings.some((binding) => binding.workspace === workspace));
|
|
571
|
-
if (!stillBound)
|
|
572
|
-
this.activeProfilesByWorkspace.delete(workspace);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
resolveCachedProfile(cwd) {
|
|
576
|
-
const resolvedCwd = resolve(cwd);
|
|
577
|
-
return this.cachedProfiles
|
|
578
|
-
.flatMap((profile) => {
|
|
579
|
-
const binding = profile.bindings
|
|
580
|
-
.filter((candidate) => isPathInside(candidate.workspace, resolvedCwd))
|
|
581
|
-
.sort((left, right) => right.workspace.length - left.workspace.length)[0];
|
|
582
|
-
return binding ? [{ profile, binding }] : [];
|
|
583
|
-
})
|
|
584
|
-
.filter((match) => this.activeProfilesByWorkspace.get(match.binding.workspace) === match.profile.profile)
|
|
585
|
-
.sort((left, right) => right.binding.workspace.length - left.binding.workspace.length)[0];
|
|
586
|
-
}
|
|
587
|
-
requireUnlocked() {
|
|
588
|
-
if (!this.cachedKey || !this.cachedKdf) {
|
|
589
|
-
throw new SecretVaultError("not_unlocked", "Secret vault is locked.");
|
|
590
|
-
}
|
|
591
|
-
return { key: this.cachedKey, kdf: this.cachedKdf };
|
|
592
|
-
}
|
|
593
|
-
async withPayload(operation, write) {
|
|
594
|
-
return withFileLock(this.vaultPath, async () => {
|
|
595
|
-
const environmentBefore = this.activeEnvironmentFingerprint();
|
|
596
|
-
const { key, kdf } = this.requireUnlocked();
|
|
597
|
-
if (!(await lstatIfExists(this.vaultPath))) {
|
|
598
|
-
throw new SecretVaultError("not_initialized", "Secret vault is not initialized.");
|
|
599
|
-
}
|
|
600
|
-
const envelope = parseEnvelope(await readBoundedFile(this.vaultPath));
|
|
601
|
-
if (!sameKdf(kdf, envelope.kdf)) {
|
|
602
|
-
this.lock();
|
|
603
|
-
throw new SecretVaultError("unlock_failed", "Secret vault changed and must be unlocked again.");
|
|
604
|
-
}
|
|
605
|
-
const payload = decryptPayload(envelope.payload, key);
|
|
606
|
-
try {
|
|
607
|
-
const result = await operation(payload);
|
|
608
|
-
if (write)
|
|
609
|
-
await this.writeEnvelope(kdf, payload, key);
|
|
610
|
-
this.refreshSecretCache(payload);
|
|
611
|
-
if (environmentBefore !== this.activeEnvironmentFingerprint())
|
|
612
|
-
this.notifyEnvironmentChanged();
|
|
613
|
-
return result;
|
|
614
|
-
}
|
|
615
|
-
finally {
|
|
616
|
-
scrubPayload(payload);
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
async writeEnvelope(kdf, payload, key) {
|
|
621
|
-
let serialized = serializeEnvelope(kdf, payload, key);
|
|
622
|
-
try {
|
|
623
|
-
await writeFileAtomic(this.vaultPath, serialized, { mode: 0o600 });
|
|
624
|
-
await enforcePrivateFileMode(this.vaultPath);
|
|
625
|
-
}
|
|
626
|
-
finally {
|
|
627
|
-
serialized = "";
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
async removeBoundMaterializations(record) {
|
|
631
|
-
let removed = 0;
|
|
632
|
-
for (const binding of record.bindings) {
|
|
633
|
-
const destination = resolve(binding.workspace, binding.envFile);
|
|
634
|
-
const existing = await lstatIfExists(destination);
|
|
635
|
-
if (!existing)
|
|
636
|
-
continue;
|
|
637
|
-
const state = await this.inspectEnvDestination(destination, record.profile);
|
|
638
|
-
if (state !== "managed-profile")
|
|
639
|
-
continue;
|
|
640
|
-
await fs.unlink(destination);
|
|
641
|
-
removed++;
|
|
642
|
-
}
|
|
643
|
-
return removed;
|
|
644
|
-
}
|
|
645
|
-
async removeManagedMaterialization(profile) {
|
|
646
|
-
const path = this.getManagedEnvPath(profile);
|
|
647
|
-
const existing = await lstatIfExists(path);
|
|
648
|
-
if (!existing)
|
|
649
|
-
return false;
|
|
650
|
-
if (existing.isSymbolicLink()) {
|
|
651
|
-
throw new SecretVaultError("symlink_refused", "Managed dotenv symlinks are refused.");
|
|
652
|
-
}
|
|
653
|
-
if (!existing.isFile()) {
|
|
654
|
-
throw new SecretVaultError("destination_invalid", "Managed dotenv path is not a regular file.");
|
|
655
|
-
}
|
|
656
|
-
const state = await this.inspectEnvDestination(path, profile);
|
|
657
|
-
if (state !== "managed-profile")
|
|
658
|
-
return false;
|
|
659
|
-
await fs.unlink(path);
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
//# sourceMappingURL=secret-vault.js.map
|