@catladder/cli 0.0.0-main-2355fd7c → 0.0.0-main-018e2dee
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/dist/apps/cli/src/apps/catenv/catenv.js +3 -0
- package/dist/apps/cli/src/apps/catenv/catenv.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.d.ts +4 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js +12 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.d.ts +2 -2
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js +3 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.d.ts +10 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js +98 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js.map +1 -0
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupContext.js +2 -0
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupContext.js.map +1 -1
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.d.ts +9 -0
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js +31 -0
- package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js.map +1 -0
- package/dist/apps/cli/src/config/getProjectConfig.js +9 -1
- package/dist/apps/cli/src/config/getProjectConfig.js.map +1 -1
- package/dist/apps/cli/src/gcloud/localAccess.d.ts +53 -0
- package/dist/apps/cli/src/gcloud/localAccess.js +176 -0
- package/dist/apps/cli/src/gcloud/localAccess.js.map +1 -0
- package/dist/apps/cli/src/gcloud/serviceAccounts.js +9 -2
- package/dist/apps/cli/src/gcloud/serviceAccounts.js.map +1 -1
- package/dist/apps/cli/src/localAccess/declared.d.ts +14 -0
- package/dist/apps/cli/src/localAccess/declared.js +34 -0
- package/dist/apps/cli/src/localAccess/declared.js.map +1 -0
- package/dist/apps/cli/src/localAccess/keyFiles.d.ts +28 -0
- package/dist/apps/cli/src/localAccess/keyFiles.js +91 -0
- package/dist/apps/cli/src/localAccess/keyFiles.js.map +1 -0
- package/dist/apps/cli/src/utils/writePrivateFile.d.ts +7 -0
- package/dist/apps/cli/src/utils/writePrivateFile.js +25 -0
- package/dist/apps/cli/src/utils/writePrivateFile.js.map +1 -0
- package/dist/apps/cli/src/vault/BitwardenVault.js +13 -22
- package/dist/apps/cli/src/vault/BitwardenVault.js.map +1 -1
- package/dist/apps/cli/src/vault/VaultManager.d.ts +6 -0
- package/dist/apps/cli/src/vault/VaultManager.js +18 -0
- package/dist/apps/cli/src/vault/VaultManager.js.map +1 -1
- package/dist/apps/cli/src/vault/bw.d.ts +6 -2
- package/dist/apps/cli/src/vault/bw.js +32 -6
- package/dist/apps/cli/src/vault/bw.js.map +1 -1
- package/dist/apps/cli/src/vault/cache.js +5 -2
- package/dist/apps/cli/src/vault/cache.js.map +1 -1
- package/dist/bundles/catenv/index.js +436 -31
- package/dist/bundles/cli/index.js +786 -35
- package/dist/bundles/skills/catladder-secrets/SKILL.md +35 -3
- package/dist/packages/pipeline/src/index.d.ts +1 -0
- package/dist/packages/pipeline/src/index.js +1 -0
- package/dist/packages/pipeline/src/index.js.map +1 -1
- package/dist/packages/pipeline/src/localAccess/index.d.ts +53 -0
- package/dist/packages/pipeline/src/localAccess/index.js +177 -0
- package/dist/packages/pipeline/src/localAccess/index.js.map +1 -0
- package/dist/packages/pipeline/src/types/config.d.ts +75 -0
- package/dist/packages/pipeline/src/types/config.js.map +1 -1
- package/dist/skills/catladder-secrets/SKILL.md +35 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/apps/catenv/catenv.ts +4 -0
- package/src/apps/cli/commands/project/doctor/GcloudProjectInspector.ts +25 -1
- package/src/apps/cli/commands/project/doctor/__tests__/checkLocalAccess.test.ts +127 -0
- package/src/apps/cli/commands/project/doctor/checkCloudRun.ts +5 -1
- package/src/apps/cli/commands/project/doctor/checkLocalAccess.ts +146 -0
- package/src/apps/cli/commands/project/doctor/index.ts +1 -1
- package/src/apps/cli/commands/project/setup/setupContext.ts +2 -0
- package/src/apps/cli/commands/project/setup/setupLocalAccess.ts +44 -0
- package/src/config/__tests__/getEnvVarsResolved.test.ts +45 -0
- package/src/config/getProjectConfig.ts +11 -1
- package/src/gcloud/__tests__/localAccess.test.ts +82 -0
- package/src/gcloud/__tests__/syncLocalAccess.test.ts +196 -0
- package/src/gcloud/localAccess.ts +257 -0
- package/src/gcloud/serviceAccounts.ts +8 -2
- package/src/localAccess/__tests__/keyFiles.test.ts +166 -0
- package/src/localAccess/declared.ts +38 -0
- package/src/localAccess/keyFiles.ts +126 -0
- package/src/utils/__tests__/writePrivateFile.test.ts +40 -0
- package/src/utils/writePrivateFile.ts +20 -0
- package/src/vault/BitwardenVault.ts +8 -9
- package/src/vault/VaultManager.ts +28 -0
- package/src/vault/__tests__/BitwardenVault.test.ts +4 -4
- package/src/vault/__tests__/cache.test.ts +37 -0
- package/src/vault/bw.ts +34 -9
- package/src/vault/cache.ts +5 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Config } from "@catladder/pipeline";
|
|
2
|
+
import {
|
|
3
|
+
getLocalAccessGcpSecretName,
|
|
4
|
+
getSecretVarName,
|
|
5
|
+
} from "@catladder/pipeline";
|
|
6
|
+
import { mkdir, readdir, rm, writeFile } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import type { IO } from "../core/types";
|
|
10
|
+
import { writePrivateFile } from "../utils/writePrivateFile";
|
|
11
|
+
import {
|
|
12
|
+
getDeclaredLocalAccessCredentialsEnvVars,
|
|
13
|
+
getDeclaredLocalAccessEnvs,
|
|
14
|
+
} from "./declared";
|
|
15
|
+
import { getProjectRootPath } from "../git/gitProjectInformation";
|
|
16
|
+
|
|
17
|
+
const sanitize = (s: string) => s.replace(/[^A-Za-z0-9]+/g, "_");
|
|
18
|
+
|
|
19
|
+
/** file name of a component/env key, relative to the local access folder */
|
|
20
|
+
export const getLocalAccessKeyFileName = (env: string, componentName: string) =>
|
|
21
|
+
`gcp-${sanitize(env)}-${sanitize(componentName)}.json`;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* env var exposing the key file path. Deliberately NOT
|
|
25
|
+
* GOOGLE_APPLICATION_CREDENTIALS: the key must only be used where a
|
|
26
|
+
* command opts in (isolated gcloud config, or a declared
|
|
27
|
+
* `credentialsEnvVar`), never by ADC implicitly.
|
|
28
|
+
*/
|
|
29
|
+
export const getLocalAccessKeyFileVarName = (env: string) =>
|
|
30
|
+
`CATLADDER_LOCAL_ACCESS_GCP_${sanitize(env).toUpperCase()}_KEY_FILE`;
|
|
31
|
+
|
|
32
|
+
// inside .catladder/, which ignores itself (see vault/cache.ts)
|
|
33
|
+
const getFolder = async () =>
|
|
34
|
+
join(await getProjectRootPath(), ".catladder", "local-access");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* env vars pointing at the key files that exist for the component
|
|
38
|
+
* (only paths, no secrets): one per env, plus the declared
|
|
39
|
+
* `credentialsEnvVar` names
|
|
40
|
+
*/
|
|
41
|
+
export const getLocalAccessKeyFileVars = async (
|
|
42
|
+
config: Config,
|
|
43
|
+
componentName: string,
|
|
44
|
+
): Promise<Record<string, string>> => {
|
|
45
|
+
const folder = await getFolder();
|
|
46
|
+
const getPath = (env: string) =>
|
|
47
|
+
join(folder, getLocalAccessKeyFileName(env, componentName));
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
[
|
|
50
|
+
...getDeclaredLocalAccessEnvs(config, componentName).map(
|
|
51
|
+
(env): [string, string] => [
|
|
52
|
+
getLocalAccessKeyFileVarName(env),
|
|
53
|
+
getPath(env),
|
|
54
|
+
],
|
|
55
|
+
),
|
|
56
|
+
...Object.entries(
|
|
57
|
+
getDeclaredLocalAccessCredentialsEnvVars(config, componentName),
|
|
58
|
+
).map(([name, env]): [string, string] => [name, getPath(env)]),
|
|
59
|
+
].filter(([, path]) => existsSync(path)),
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* writes the stored local access service account keys to gitignored
|
|
65
|
+
* files (mode 0600) and removes files of declarations that are gone.
|
|
66
|
+
* A no-op without any declaration (nothing is created). Never throws:
|
|
67
|
+
* a vault that cannot be reached or an invalid declaration (reported by
|
|
68
|
+
* `project doctor`) must not break catenv.
|
|
69
|
+
*
|
|
70
|
+
* The key is only read through the cache; if setup rotated it on another
|
|
71
|
+
* machine, refresh with `catenv --vault-mode refresh`.
|
|
72
|
+
*/
|
|
73
|
+
export const writeLocalAccessKeyFiles = async (io: IO, config: Config) => {
|
|
74
|
+
const wanted = Object.keys(config.components).flatMap((componentName) =>
|
|
75
|
+
getDeclaredLocalAccessEnvs(config, componentName).map((env) => ({
|
|
76
|
+
env,
|
|
77
|
+
componentName,
|
|
78
|
+
secretName: getSecretVarName(
|
|
79
|
+
"local",
|
|
80
|
+
componentName,
|
|
81
|
+
getLocalAccessGcpSecretName(env),
|
|
82
|
+
),
|
|
83
|
+
fileName: getLocalAccessKeyFileName(env, componentName),
|
|
84
|
+
})),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const folder = await getFolder();
|
|
88
|
+
if (wanted.length === 0 && !existsSync(folder)) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const secrets =
|
|
92
|
+
wanted.length > 0
|
|
93
|
+
? await (
|
|
94
|
+
await io.getVaultManager()
|
|
95
|
+
).readSecrets(
|
|
96
|
+
wanted.map((w) => w.secretName),
|
|
97
|
+
io,
|
|
98
|
+
)
|
|
99
|
+
: {};
|
|
100
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
101
|
+
// the parent folder ignores itself
|
|
102
|
+
await writeFile(join(folder, "..", ".gitignore"), "*\n");
|
|
103
|
+
|
|
104
|
+
const written = new Set<string>();
|
|
105
|
+
for (const { secretName, fileName, env, componentName } of wanted) {
|
|
106
|
+
const key = secrets[secretName];
|
|
107
|
+
if (!key) {
|
|
108
|
+
io.log(
|
|
109
|
+
`⚠️ no local access key for ${env}:${componentName} in the secret store — run \`catladder project setup\` (or \`catenv --vault-mode refresh\` if it was set up on another machine)`,
|
|
110
|
+
);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const path = join(folder, fileName);
|
|
114
|
+
await writePrivateFile(path, key);
|
|
115
|
+
written.add(fileName);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const file of await readdir(folder)) {
|
|
119
|
+
if (!written.has(file)) {
|
|
120
|
+
await rm(join(folder, file), { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
io.log(`⚠️ could not write local access keys: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmod,
|
|
3
|
+
mkdtemp,
|
|
4
|
+
readFile,
|
|
5
|
+
readdir,
|
|
6
|
+
rm,
|
|
7
|
+
stat,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "fs/promises";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
import { writePrivateFile } from "../writePrivateFile";
|
|
14
|
+
|
|
15
|
+
let dir: string;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
dir = await mkdtemp(join(tmpdir(), "private-file-test-"));
|
|
18
|
+
});
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(dir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("writePrivateFile", () => {
|
|
24
|
+
it("creates the file with mode 0600", async () => {
|
|
25
|
+
const path = join(dir, "a.json");
|
|
26
|
+
await writePrivateFile(path, "secret");
|
|
27
|
+
expect(await readFile(path, "utf-8")).toBe("secret");
|
|
28
|
+
expect((await stat(path)).mode & 0o777).toBe(0o600);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("tightens an existing 0644 file and leaves no temp file behind", async () => {
|
|
32
|
+
const path = join(dir, "a.json");
|
|
33
|
+
await writeFile(path, "old");
|
|
34
|
+
await chmod(path, 0o644);
|
|
35
|
+
await writePrivateFile(path, "new");
|
|
36
|
+
expect(await readFile(path, "utf-8")).toBe("new");
|
|
37
|
+
expect((await stat(path)).mode & 0o777).toBe(0o600);
|
|
38
|
+
expect(await readdir(dir)).toEqual(["a.json"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { chmod, rename, rm, writeFile } from "fs/promises";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* writes a file readable by the owner only. Goes through a 0600 temp
|
|
5
|
+
* file and a rename, so the content is never visible with looser
|
|
6
|
+
* permissions (writing first and chmod-ing afterwards would leave an
|
|
7
|
+
* existing 0644 file readable until the chmod).
|
|
8
|
+
*/
|
|
9
|
+
export const writePrivateFile = async (path: string, content: string) => {
|
|
10
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
11
|
+
try {
|
|
12
|
+
await writeFile(tmp, content, { mode: 0o600 });
|
|
13
|
+
// the mode is only applied when the file is created
|
|
14
|
+
await chmod(tmp, 0o600);
|
|
15
|
+
await rename(tmp, path);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
await rm(tmp, { force: true });
|
|
18
|
+
throw e;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -142,16 +142,15 @@ export class BitwardenVault implements SecretsVault {
|
|
|
142
142
|
const notes = stringify({ ...previous, ...secrets });
|
|
143
143
|
|
|
144
144
|
if (existing) {
|
|
145
|
-
await runBw(
|
|
146
|
-
|
|
147
|
-
"item",
|
|
148
|
-
existing.id,
|
|
145
|
+
await runBw(
|
|
146
|
+
bwSession,
|
|
147
|
+
["edit", "item", existing.id],
|
|
149
148
|
encodePayload({ ...existing, notes }),
|
|
150
|
-
|
|
149
|
+
);
|
|
151
150
|
} else {
|
|
152
|
-
await runBw(
|
|
153
|
-
|
|
154
|
-
"item",
|
|
151
|
+
await runBw(
|
|
152
|
+
bwSession,
|
|
153
|
+
["create", "item"],
|
|
155
154
|
encodePayload({
|
|
156
155
|
type: SECURE_NOTE_TYPE,
|
|
157
156
|
secureNote: { type: 0 },
|
|
@@ -164,7 +163,7 @@ export class BitwardenVault implements SecretsVault {
|
|
|
164
163
|
}
|
|
165
164
|
: {}),
|
|
166
165
|
}),
|
|
167
|
-
|
|
166
|
+
);
|
|
168
167
|
}
|
|
169
168
|
}
|
|
170
169
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Config } from "@catladder/pipeline";
|
|
2
2
|
import {
|
|
3
3
|
getEnvironment,
|
|
4
|
+
getLocalAccessGcpSecretName,
|
|
4
5
|
getSecretVarName,
|
|
5
6
|
getVaultConfig,
|
|
6
7
|
} from "@catladder/pipeline";
|
|
7
8
|
import type { IO } from "../core/types";
|
|
9
|
+
import { getDeclaredLocalAccessEnvs } from "../localAccess/declared";
|
|
8
10
|
import { BitwardenVault } from "./BitwardenVault";
|
|
9
11
|
import type { CachedSecrets } from "./cache";
|
|
10
12
|
import {
|
|
@@ -140,6 +142,19 @@ export class VaultManager {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/**
|
|
146
|
+
* updates the local cache only (the vault already holds the values),
|
|
147
|
+
* "local" env only
|
|
148
|
+
*/
|
|
149
|
+
async cacheSecrets(
|
|
150
|
+
env: string,
|
|
151
|
+
componentName: string,
|
|
152
|
+
secrets: Record<string, string>,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
if (env !== CACHED_ENV) return;
|
|
155
|
+
await updateSecretsCache(this.vault.id, env, componentName, secrets);
|
|
156
|
+
}
|
|
157
|
+
|
|
143
158
|
/**
|
|
144
159
|
* the subset of the vault secrets that may be cached: the declared
|
|
145
160
|
* secrets of the "local" env, per component, as a hierarchy
|
|
@@ -165,6 +180,19 @@ export class VaultManager {
|
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
private async declaredSecretKeys(componentName: string): Promise<string[]> {
|
|
183
|
+
return [
|
|
184
|
+
...(await this.declaredEnvSecretKeys(componentName)),
|
|
185
|
+
// the local access keys `project setup` provisions are not part
|
|
186
|
+
// of any env's vars, but belong to the cached local secrets
|
|
187
|
+
...getDeclaredLocalAccessEnvs(this.config, componentName).map(
|
|
188
|
+
getLocalAccessGcpSecretName,
|
|
189
|
+
),
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async declaredEnvSecretKeys(
|
|
194
|
+
componentName: string,
|
|
195
|
+
): Promise<string[]> {
|
|
168
196
|
try {
|
|
169
197
|
const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment({
|
|
170
198
|
config: this.config,
|
|
@@ -27,10 +27,10 @@ const bwContains = (item?: Record<string, unknown>) =>
|
|
|
27
27
|
|
|
28
28
|
/** the notes of the `bw edit|create item` payload, parsed back */
|
|
29
29
|
const writtenNotes = () => {
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
);
|
|
30
|
+
// the payload goes through stdin (not argv: visible in `ps`)
|
|
31
|
+
const [, args, input] = runBw.mock.calls[0];
|
|
32
|
+
expect(args).not.toContain(input);
|
|
33
|
+
const payload = JSON.parse(Buffer.from(input, "base64").toString("utf-8"));
|
|
34
34
|
return parse(payload.notes);
|
|
35
35
|
};
|
|
36
36
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { chmod, mkdtemp, mkdir, rm, stat, writeFile } from "fs/promises";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
let root: string;
|
|
7
|
+
vi.mock("../../git/gitProjectInformation", () => ({
|
|
8
|
+
getProjectRootPath: async () => root,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { readSecretsCache, writeSecretsCache } from "../cache";
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
root = await mkdtemp(join(tmpdir(), "cache-test-"));
|
|
15
|
+
});
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(root, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("secrets cache", () => {
|
|
21
|
+
it("is written with mode 0600", async () => {
|
|
22
|
+
await writeSecretsCache("v", { local: { web: { KEY: "value" } } });
|
|
23
|
+
const mode = (await stat(join(root, ".catladder", "secrets.json"))).mode;
|
|
24
|
+
expect(mode & 0o777).toBe(0o600);
|
|
25
|
+
expect((await readSecretsCache())?.secrets.local.web.KEY).toBe("value");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("tightens an existing world-readable cache", async () => {
|
|
29
|
+
const folder = join(root, ".catladder");
|
|
30
|
+
await mkdir(folder);
|
|
31
|
+
const path = join(folder, "secrets.json");
|
|
32
|
+
await writeFile(path, "{}");
|
|
33
|
+
await chmod(path, 0o644);
|
|
34
|
+
await writeSecretsCache("v", { local: { web: { KEY: "value" } } });
|
|
35
|
+
expect((await stat(path)).mode & 0o777).toBe(0o600);
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/vault/bw.ts
CHANGED
|
@@ -87,21 +87,46 @@ export const getBwSession = async (
|
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* runs a bw command with the given session and returns stdout
|
|
90
|
+
* runs a bw command with the given session and returns stdout.
|
|
91
|
+
*
|
|
92
|
+
* `input` is written to stdin: use it for payloads (e.g. the secrets of
|
|
93
|
+
* `bw create item`), which would otherwise be visible in `ps` and be
|
|
94
|
+
* echoed by node's "Command failed" error.
|
|
91
95
|
*/
|
|
92
96
|
export const runBw = async (
|
|
93
97
|
session: string,
|
|
94
98
|
args: string[],
|
|
99
|
+
input?: string,
|
|
95
100
|
): Promise<string> => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
env
|
|
101
|
+
const fullArgs = [...args, "--raw", "--nointeraction"];
|
|
102
|
+
const env = { ...process.env, BW_SESSION: session };
|
|
103
|
+
if (input === undefined) {
|
|
104
|
+
const { stdout } = await execFile("bw", fullArgs, {
|
|
105
|
+
env,
|
|
101
106
|
maxBuffer: 50 * 1024 * 1024,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
});
|
|
108
|
+
return stdout;
|
|
109
|
+
}
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const child = spawn("bw", fullArgs, {
|
|
112
|
+
env,
|
|
113
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
let stdout = "";
|
|
116
|
+
let stderr = "";
|
|
117
|
+
child.stdout.on("data", (data) => (stdout += data.toString("utf-8")));
|
|
118
|
+
child.stderr.on("data", (data) => (stderr += data.toString("utf-8")));
|
|
119
|
+
child.on("error", reject);
|
|
120
|
+
child.on("close", (code) =>
|
|
121
|
+
code === 0
|
|
122
|
+
? resolve(stdout)
|
|
123
|
+
: reject(new Error(`bw ${args[0]} failed: ${stderr.trim()}`)),
|
|
124
|
+
);
|
|
125
|
+
child.stdin.on("error", () => {
|
|
126
|
+
// bw exiting early closes the pipe, the close handler reports why
|
|
127
|
+
});
|
|
128
|
+
child.stdin.end(input);
|
|
129
|
+
});
|
|
105
130
|
};
|
|
106
131
|
|
|
107
132
|
export const runBwJson = async <T>(
|
package/src/vault/cache.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { writePrivateFile } from "../utils/writePrivateFile";
|
|
3
4
|
import { getProjectRootPath } from "../git/gitProjectInformation";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -47,7 +48,7 @@ export const writeSecretsCache = async (
|
|
|
47
48
|
secrets: CachedSecrets,
|
|
48
49
|
): Promise<void> => {
|
|
49
50
|
const folder = await getCacheFolder();
|
|
50
|
-
await mkdir(folder, { recursive: true });
|
|
51
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
51
52
|
// the folder ignores itself, so the cache can never be committed
|
|
52
53
|
await writeFile(join(folder, ".gitignore"), "*\n");
|
|
53
54
|
const cache: SecretsCache = {
|
|
@@ -55,7 +56,9 @@ export const writeSecretsCache = async (
|
|
|
55
56
|
updatedAt: new Date().toISOString(),
|
|
56
57
|
secrets,
|
|
57
58
|
};
|
|
58
|
-
|
|
59
|
+
const path = await getCachePath();
|
|
60
|
+
// may hold key material (local access keys): owner only
|
|
61
|
+
await writePrivateFile(path, JSON.stringify(cache, null, 2));
|
|
59
62
|
};
|
|
60
63
|
|
|
61
64
|
/**
|