@agent-vm/secret-management 0.0.71
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/LICENSE +21 -0
- package/dist/contracts.d.ts +18 -0
- package/dist/contracts.js +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +500 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.js +16 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Shravan Sunder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/contracts.d.ts
|
|
2
|
+
interface MediatedSecretSpec {
|
|
3
|
+
readonly hosts: readonly string[];
|
|
4
|
+
readonly value: string;
|
|
5
|
+
}
|
|
6
|
+
type SecretRef = {
|
|
7
|
+
readonly source: '1password';
|
|
8
|
+
readonly ref: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly source: 'environment';
|
|
11
|
+
readonly ref: string;
|
|
12
|
+
};
|
|
13
|
+
interface SecretResolver {
|
|
14
|
+
resolve(ref: SecretRef): Promise<string>;
|
|
15
|
+
resolveAll(refs: Record<string, SecretRef>): Promise<Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { MediatedSecretSpec, SecretRef, SecretResolver };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MediatedSecretSpec, SecretRef, SecretResolver } from "./contracts.js";
|
|
2
|
+
import { createStaticSecretResolver } from "./testing.js";
|
|
3
|
+
import { ResolveAllResponse } from "@1password/sdk";
|
|
4
|
+
|
|
5
|
+
//#region src/composite-secret-resolver.d.ts
|
|
6
|
+
type SecretEnvironment = Readonly<Record<string, string | undefined>>;
|
|
7
|
+
declare function createCompositeSecretResolver(onePasswordResolver: SecretResolver | null, env?: SecretEnvironment): SecretResolver;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/onepassword-secret-resolver.d.ts
|
|
10
|
+
interface SecretResolverClient {
|
|
11
|
+
readonly secrets: {
|
|
12
|
+
resolve(secretReference: string): Promise<string>;
|
|
13
|
+
resolveAll(secretReferences: readonly string[]): Promise<ResolveAllResponse>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
type TokenSource = {
|
|
17
|
+
readonly type: 'op-cli';
|
|
18
|
+
readonly ref: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: 'env';
|
|
21
|
+
readonly envVar?: string | undefined;
|
|
22
|
+
} | {
|
|
23
|
+
readonly type: 'keychain';
|
|
24
|
+
readonly service: string;
|
|
25
|
+
readonly account: string;
|
|
26
|
+
};
|
|
27
|
+
interface ExecFileOptions {
|
|
28
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
readonly input?: string | undefined;
|
|
30
|
+
readonly redactErrorOutput?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
interface ExecFileResult {
|
|
33
|
+
readonly stdout: string;
|
|
34
|
+
readonly stderr: string;
|
|
35
|
+
}
|
|
36
|
+
declare function resolveServiceAccountToken(source: TokenSource, dependencies?: {
|
|
37
|
+
readonly execFileAsync?: (command: string, args: readonly string[], options?: ExecFileOptions) => Promise<ExecFileResult>;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
interface CreateSecretResolverDependencies {
|
|
40
|
+
readonly createClient?: (config: {
|
|
41
|
+
auth: string;
|
|
42
|
+
integrationName: string;
|
|
43
|
+
integrationVersion: string;
|
|
44
|
+
}) => Promise<SecretResolverClient>;
|
|
45
|
+
readonly execFileAsync?: (command: string, args: readonly string[], options?: ExecFileOptions) => Promise<ExecFileResult>;
|
|
46
|
+
readonly integrationName?: string;
|
|
47
|
+
readonly integrationVersion?: string;
|
|
48
|
+
}
|
|
49
|
+
declare function createSecretResolver(options: {
|
|
50
|
+
readonly serviceAccountToken: string;
|
|
51
|
+
}, dependencies?: CreateSecretResolverDependencies): Promise<SecretResolver>;
|
|
52
|
+
declare function createOpCliSecretResolver(options: {
|
|
53
|
+
readonly serviceAccountToken: string;
|
|
54
|
+
}, dependencies?: Pick<CreateSecretResolverDependencies, 'execFileAsync'>): Promise<SecretResolver>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { CreateSecretResolverDependencies, ExecFileOptions, ExecFileResult, MediatedSecretSpec, SecretRef, SecretResolver, SecretResolverClient, type TokenSource, createCompositeSecretResolver, createOpCliSecretResolver, createSecretResolver, createStaticSecretResolver, resolveServiceAccountToken };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import "./contracts.js";
|
|
2
|
+
import { createStaticSecretResolver } from "./testing.js";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createClient } from "@1password/sdk";
|
|
6
|
+
//#region src/composite-secret-resolver.ts
|
|
7
|
+
function resolveEnvironmentSecret(ref, env) {
|
|
8
|
+
const value = env[ref.ref];
|
|
9
|
+
if (value === void 0) throw new Error(`Environment variable '${ref.ref}' is not set.`);
|
|
10
|
+
if (value.trim().length === 0) throw new Error(`Environment variable '${ref.ref}' is set but empty.`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function formatUnknownError$1(error) {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
}
|
|
16
|
+
function createSecretResolutionError(options) {
|
|
17
|
+
return new Error(`Failed to resolve secret '${options.secretName}' from '${options.ref.ref}': ${formatUnknownError$1(options.cause)}`, { cause: options.cause });
|
|
18
|
+
}
|
|
19
|
+
function throwAggregateSecretResolutionError(failures) {
|
|
20
|
+
if (failures.length > 0) throw new AggregateError(failures, `Failed to resolve ${String(failures.length)} secret(s).`);
|
|
21
|
+
}
|
|
22
|
+
function extractAggregateErrors(error) {
|
|
23
|
+
return (Array.isArray(error.errors) ? error.errors : [error]).map((failure) => failure instanceof Error ? failure : new Error(formatUnknownError$1(failure), { cause: failure }));
|
|
24
|
+
}
|
|
25
|
+
function createCompositeSecretResolver(onePasswordResolver, env = process.env) {
|
|
26
|
+
return {
|
|
27
|
+
async resolve(ref) {
|
|
28
|
+
switch (ref.source) {
|
|
29
|
+
case "environment": return resolveEnvironmentSecret(ref, env);
|
|
30
|
+
case "1password":
|
|
31
|
+
if (!onePasswordResolver) throw new Error("Secret with source '1password' requires host.secretsProvider to be configured.");
|
|
32
|
+
return await onePasswordResolver.resolve(ref);
|
|
33
|
+
default: throw new Error(`Unsupported secret source: ${JSON.stringify(ref)}`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
async resolveAll(refs) {
|
|
37
|
+
const resolved = {};
|
|
38
|
+
const onePasswordRefs = {};
|
|
39
|
+
const failures = [];
|
|
40
|
+
for (const [name, ref] of Object.entries(refs)) switch (ref.source) {
|
|
41
|
+
case "environment":
|
|
42
|
+
try {
|
|
43
|
+
resolved[name] = resolveEnvironmentSecret(ref, env);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
failures.push(createSecretResolutionError({
|
|
46
|
+
cause: error,
|
|
47
|
+
ref,
|
|
48
|
+
secretName: name
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "1password":
|
|
53
|
+
onePasswordRefs[name] = ref;
|
|
54
|
+
break;
|
|
55
|
+
default: throw new Error(`Unsupported secret source: ${JSON.stringify(ref)}`);
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(onePasswordRefs).length > 0) if (!onePasswordResolver) failures.push(...Object.entries(onePasswordRefs).map(([name, ref]) => createSecretResolutionError({
|
|
58
|
+
cause: /* @__PURE__ */ new Error("Secret with source '1password' requires host.secretsProvider to be configured."),
|
|
59
|
+
ref,
|
|
60
|
+
secretName: name
|
|
61
|
+
})));
|
|
62
|
+
else {
|
|
63
|
+
const resolver = onePasswordResolver;
|
|
64
|
+
try {
|
|
65
|
+
Object.assign(resolved, await resolver.resolveAll(onePasswordRefs));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof AggregateError) failures.push(...extractAggregateErrors(error));
|
|
68
|
+
else failures.push(...Object.entries(onePasswordRefs).map(([name, ref]) => createSecretResolutionError({
|
|
69
|
+
cause: error,
|
|
70
|
+
ref,
|
|
71
|
+
secretName: name
|
|
72
|
+
})));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throwAggregateSecretResolutionError(failures);
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/onepassword-secret-resolver.ts
|
|
82
|
+
function formatUnknownError(error) {
|
|
83
|
+
if (error instanceof AggregateError) {
|
|
84
|
+
const childMessages = readAggregateErrorChildren(error).map(formatUnknownError);
|
|
85
|
+
if (childMessages.length === 0) return error.message;
|
|
86
|
+
const separator = error.message.endsWith(".") ? "" : ".";
|
|
87
|
+
return `${error.message}${separator} Details: ${childMessages.join("; ")}`;
|
|
88
|
+
}
|
|
89
|
+
return error instanceof Error ? error.message : String(error);
|
|
90
|
+
}
|
|
91
|
+
var RedactedExecFileError = class extends Error {
|
|
92
|
+
safeDetail;
|
|
93
|
+
constructor(message, safeDetail, options) {
|
|
94
|
+
super(message, options);
|
|
95
|
+
this.safeDetail = safeDetail;
|
|
96
|
+
this.name = "RedactedExecFileError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var OpInjectOutputError = class extends Error {
|
|
100
|
+
constructor(message) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "OpInjectOutputError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function formatErrorMetadataValue(value) {
|
|
106
|
+
if (typeof value === "number" || typeof value === "string") return String(value);
|
|
107
|
+
}
|
|
108
|
+
function readErrorCode(error) {
|
|
109
|
+
if (!("code" in error)) return;
|
|
110
|
+
return formatErrorMetadataValue(error.code);
|
|
111
|
+
}
|
|
112
|
+
function readErrorSignal(error) {
|
|
113
|
+
if (!("signal" in error)) return;
|
|
114
|
+
return formatErrorMetadataValue(error.signal);
|
|
115
|
+
}
|
|
116
|
+
function formatRedactedExecErrorDetail(error) {
|
|
117
|
+
const exitCode = readErrorCode(error) ?? "unknown";
|
|
118
|
+
const signal = readErrorSignal(error);
|
|
119
|
+
return signal === void 0 ? `exit code ${exitCode}` : `exit code ${exitCode}, signal ${signal}`;
|
|
120
|
+
}
|
|
121
|
+
function createExecFileError(options) {
|
|
122
|
+
if (options.redactErrorOutput) {
|
|
123
|
+
const safeDetail = formatRedactedExecErrorDetail(options.error);
|
|
124
|
+
return new RedactedExecFileError(`${options.command} failed: ${safeDetail}`, safeDetail);
|
|
125
|
+
}
|
|
126
|
+
const errorDetail = options.stderr.trim() || options.error.message;
|
|
127
|
+
return /* @__PURE__ */ new Error(`${options.command} failed: ${errorDetail}`);
|
|
128
|
+
}
|
|
129
|
+
function formatStdinWriteErrorDetail(error) {
|
|
130
|
+
const errorCode = readErrorCode(error);
|
|
131
|
+
return errorCode === void 0 ? "stdin write failed" : `stdin write failed: ${errorCode}`;
|
|
132
|
+
}
|
|
133
|
+
function createStdinWriteError(command, error, redactErrorOutput) {
|
|
134
|
+
if (redactErrorOutput) {
|
|
135
|
+
const safeDetail = formatStdinWriteErrorDetail(error);
|
|
136
|
+
return new RedactedExecFileError(`${command} failed writing stdin: ${safeDetail}`, safeDetail, { cause: error });
|
|
137
|
+
}
|
|
138
|
+
return new Error(`${command} failed writing stdin: ${formatUnknownError(error)}`, { cause: error });
|
|
139
|
+
}
|
|
140
|
+
function ensureMacOsForKeychain() {
|
|
141
|
+
if (process.platform !== "darwin") throw new Error("Keychain token source is only supported on macOS. Use an env or op-cli token source on this platform so cmd-ts can surface a clear startup error.");
|
|
142
|
+
}
|
|
143
|
+
function execFileAsync(command, args, options) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let hasSettled = false;
|
|
146
|
+
const rejectOnce = (error) => {
|
|
147
|
+
if (hasSettled) return;
|
|
148
|
+
hasSettled = true;
|
|
149
|
+
reject(error);
|
|
150
|
+
};
|
|
151
|
+
const resolveOnce = (result) => {
|
|
152
|
+
if (hasSettled) return;
|
|
153
|
+
hasSettled = true;
|
|
154
|
+
resolve(result);
|
|
155
|
+
};
|
|
156
|
+
const child = execFile(command, [...args], {
|
|
157
|
+
env: options?.env,
|
|
158
|
+
timeout: 3e4
|
|
159
|
+
}, (error, stdout, stderr) => {
|
|
160
|
+
if (error) {
|
|
161
|
+
rejectOnce(createExecFileError({
|
|
162
|
+
command,
|
|
163
|
+
error,
|
|
164
|
+
redactErrorOutput: options?.redactErrorOutput,
|
|
165
|
+
stderr
|
|
166
|
+
}));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
resolveOnce({
|
|
170
|
+
stdout,
|
|
171
|
+
stderr
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
if (options?.input !== void 0) {
|
|
175
|
+
if (!child.stdin) {
|
|
176
|
+
child.kill();
|
|
177
|
+
rejectOnce(/* @__PURE__ */ new Error(`${command} did not expose stdin for input`));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
child.stdin.once("error", (error) => {
|
|
181
|
+
child.kill();
|
|
182
|
+
rejectOnce(createStdinWriteError(command, error, options.redactErrorOutput));
|
|
183
|
+
});
|
|
184
|
+
child.stdin.end(options.input);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const SAFE_IDENTIFIER_PATTERN = /^[\w.@-]+$/u;
|
|
189
|
+
async function resolveServiceAccountToken(source, dependencies) {
|
|
190
|
+
const exec = dependencies?.execFileAsync ?? execFileAsync;
|
|
191
|
+
switch (source.type) {
|
|
192
|
+
case "op-cli": {
|
|
193
|
+
const token = (await exec("op", ["read", source.ref], { redactErrorOutput: true })).stdout.trim();
|
|
194
|
+
if (token.length === 0) throw new Error("op-cli token resolution returned empty value");
|
|
195
|
+
return token;
|
|
196
|
+
}
|
|
197
|
+
case "env": {
|
|
198
|
+
const envVar = source.envVar ?? "OP_SERVICE_ACCOUNT_TOKEN";
|
|
199
|
+
const token = process.env[envVar]?.trim();
|
|
200
|
+
if (!token) throw new Error(`Environment variable ${envVar} is not set`);
|
|
201
|
+
return token;
|
|
202
|
+
}
|
|
203
|
+
case "keychain": {
|
|
204
|
+
ensureMacOsForKeychain();
|
|
205
|
+
if (!SAFE_IDENTIFIER_PATTERN.test(source.service)) throw new Error("Keychain service name contains invalid characters");
|
|
206
|
+
if (!SAFE_IDENTIFIER_PATTERN.test(source.account)) throw new Error("Keychain account name contains invalid characters");
|
|
207
|
+
const token = (await exec("security", [
|
|
208
|
+
"find-generic-password",
|
|
209
|
+
"-s",
|
|
210
|
+
source.service,
|
|
211
|
+
"-a",
|
|
212
|
+
source.account,
|
|
213
|
+
"-w"
|
|
214
|
+
])).stdout.trim();
|
|
215
|
+
if (token.length === 0) throw new Error("Keychain token resolution returned empty value");
|
|
216
|
+
return token;
|
|
217
|
+
}
|
|
218
|
+
default: throw new Error(`Unsupported token source: ${JSON.stringify(source)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function resolveSecretWithOpCli(serviceAccountToken, secretReference, exec) {
|
|
222
|
+
return stripOpReadStdoutTerminator((await exec("op", ["read", secretReference], {
|
|
223
|
+
env: createOpCliServiceAccountEnv(serviceAccountToken),
|
|
224
|
+
redactErrorOutput: true
|
|
225
|
+
})).stdout);
|
|
226
|
+
}
|
|
227
|
+
function stripOpReadStdoutTerminator(stdout) {
|
|
228
|
+
if (stdout.endsWith("\r\n")) return stdout.slice(0, -2);
|
|
229
|
+
if (stdout.endsWith("\n")) return stdout.slice(0, -1);
|
|
230
|
+
return stdout;
|
|
231
|
+
}
|
|
232
|
+
const opCliProcessPlumbingEnvNames = [
|
|
233
|
+
"APPDATA",
|
|
234
|
+
"ALL_PROXY",
|
|
235
|
+
"all_proxy",
|
|
236
|
+
"COMSPEC",
|
|
237
|
+
"HOME",
|
|
238
|
+
"HTTP_PROXY",
|
|
239
|
+
"http_proxy",
|
|
240
|
+
"HTTPS_PROXY",
|
|
241
|
+
"https_proxy",
|
|
242
|
+
"LANG",
|
|
243
|
+
"LC_ALL",
|
|
244
|
+
"LC_CTYPE",
|
|
245
|
+
"LOCALAPPDATA",
|
|
246
|
+
"NO_PROXY",
|
|
247
|
+
"no_proxy",
|
|
248
|
+
"PATH",
|
|
249
|
+
"SSL_CERT_DIR",
|
|
250
|
+
"SSL_CERT_FILE",
|
|
251
|
+
"TEMP",
|
|
252
|
+
"TMP",
|
|
253
|
+
"TMPDIR",
|
|
254
|
+
"TZ",
|
|
255
|
+
"USERPROFILE",
|
|
256
|
+
"WINDIR",
|
|
257
|
+
"XDG_CACHE_HOME",
|
|
258
|
+
"XDG_CONFIG_HOME",
|
|
259
|
+
"XDG_DATA_HOME",
|
|
260
|
+
"XDG_RUNTIME_DIR"
|
|
261
|
+
];
|
|
262
|
+
function createOpCliServiceAccountEnv(serviceAccountToken) {
|
|
263
|
+
const env = {};
|
|
264
|
+
for (const envName of opCliProcessPlumbingEnvNames) {
|
|
265
|
+
const envValue = process.env[envName];
|
|
266
|
+
if (envValue !== void 0) env[envName] = envValue;
|
|
267
|
+
}
|
|
268
|
+
env.OP_SERVICE_ACCOUNT_TOKEN = serviceAccountToken;
|
|
269
|
+
return env;
|
|
270
|
+
}
|
|
271
|
+
const opInjectTemplateDelimiterPattern = /(?:\{\{|\}\})/u;
|
|
272
|
+
function assertOpInjectTemplateSafeReference(entry) {
|
|
273
|
+
if (!opInjectTemplateDelimiterPattern.test(entry.secretRef.ref) && !entry.secretRef.ref.includes("\0") && !entry.secretRef.ref.includes("\r") && !entry.secretRef.ref.includes("\n")) return;
|
|
274
|
+
throw new OpInjectOutputError(`op inject template rejected unsafe 1Password reference for secret '${entry.secretName}'.`);
|
|
275
|
+
}
|
|
276
|
+
async function resolveAllSecretsWithOpCli(serviceAccountToken, refs, exec) {
|
|
277
|
+
try {
|
|
278
|
+
return await resolveAllSecretsWithOpInject(serviceAccountToken, refs, exec);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
const sanitizedInjectError = sanitizeOpInjectError(error);
|
|
281
|
+
try {
|
|
282
|
+
return await resolveAllSecretsWithSerialOpReads(serviceAccountToken, refs, exec);
|
|
283
|
+
} catch (readError) {
|
|
284
|
+
if (readError instanceof AggregateError) throw createAggregateErrorWithCause({
|
|
285
|
+
cause: readError,
|
|
286
|
+
errors: [sanitizedInjectError, ...readAggregateErrorChildren(readError)],
|
|
287
|
+
message: readError.message
|
|
288
|
+
});
|
|
289
|
+
throw createAggregateErrorWithCause({
|
|
290
|
+
cause: readError,
|
|
291
|
+
errors: [sanitizedInjectError, readError],
|
|
292
|
+
message: "op inject and serial op read both failed."
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function sanitizeOpInjectError(error) {
|
|
298
|
+
if (error instanceof RedactedExecFileError) return /* @__PURE__ */ new Error(`op inject failed before serial op read: ${error.safeDetail}`);
|
|
299
|
+
if (error instanceof OpInjectOutputError) return /* @__PURE__ */ new Error(`op inject failed before serial op read: ${error.message}`);
|
|
300
|
+
const errorType = error instanceof Error ? error.name : typeof error;
|
|
301
|
+
return /* @__PURE__ */ new Error(`op inject failed before serial op read: ${errorType}`);
|
|
302
|
+
}
|
|
303
|
+
function readAggregateErrorChildren(error) {
|
|
304
|
+
const errorChildren = error.errors;
|
|
305
|
+
return Array.isArray(errorChildren) ? errorChildren : [];
|
|
306
|
+
}
|
|
307
|
+
function createAggregateErrorWithCause(options) {
|
|
308
|
+
const aggregateError = new AggregateError(options.errors, options.message);
|
|
309
|
+
aggregateError.cause = options.cause;
|
|
310
|
+
return aggregateError;
|
|
311
|
+
}
|
|
312
|
+
function createFallbackStageError(stage, error) {
|
|
313
|
+
return new Error(`${stage} failed before op CLI fallback: ${formatUnknownError(error)}`, { cause: error });
|
|
314
|
+
}
|
|
315
|
+
function createFallbackFailureError(options) {
|
|
316
|
+
if (options.fallbackError instanceof AggregateError) return createAggregateErrorWithCause({
|
|
317
|
+
cause: options.fallbackError,
|
|
318
|
+
errors: [options.stageError, ...readAggregateErrorChildren(options.fallbackError)],
|
|
319
|
+
message: options.fallbackError.message
|
|
320
|
+
});
|
|
321
|
+
return createAggregateErrorWithCause({
|
|
322
|
+
cause: options.fallbackError,
|
|
323
|
+
errors: [options.stageError, options.fallbackError],
|
|
324
|
+
message: options.message
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function opInjectStartMarker(markerId) {
|
|
328
|
+
return `agent-vm-op-inject-start:${markerId}`;
|
|
329
|
+
}
|
|
330
|
+
function opInjectEndMarker(markerId) {
|
|
331
|
+
return `agent-vm-op-inject-end:${markerId}`;
|
|
332
|
+
}
|
|
333
|
+
function createOpInjectEntries(refs) {
|
|
334
|
+
return Object.entries(refs).map(([secretName, secretRef]) => ({
|
|
335
|
+
markerId: randomUUID(),
|
|
336
|
+
secretName,
|
|
337
|
+
secretRef
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
function buildOpInjectTemplate(entries) {
|
|
341
|
+
return entries.map((entry) => {
|
|
342
|
+
assertOpInjectTemplateSafeReference(entry);
|
|
343
|
+
return [
|
|
344
|
+
opInjectStartMarker(entry.markerId),
|
|
345
|
+
`{{ ${entry.secretRef.ref} }}`,
|
|
346
|
+
opInjectEndMarker(entry.markerId)
|
|
347
|
+
].join("\n");
|
|
348
|
+
}).join("\n");
|
|
349
|
+
}
|
|
350
|
+
function findUniqueOpInjectMarker(options) {
|
|
351
|
+
const markerIndex = options.output.indexOf(options.marker);
|
|
352
|
+
if (markerIndex === -1) throw new OpInjectOutputError(`op inject output omitted ${options.markerDescription} marker for secret '${options.secretName}' (${options.secretReference}).`);
|
|
353
|
+
if (options.output.indexOf(options.marker, markerIndex + options.marker.length) !== -1) throw new OpInjectOutputError(`op inject output for secret '${options.secretName}' (${options.secretReference}) contained repeated ${options.markerDescription} marker.`);
|
|
354
|
+
return markerIndex;
|
|
355
|
+
}
|
|
356
|
+
function extractInjectedSecret(options) {
|
|
357
|
+
const startToken = `${opInjectStartMarker(options.entry.markerId)}\n`;
|
|
358
|
+
const endToken = `\n${opInjectEndMarker(options.entry.markerId)}`;
|
|
359
|
+
const secretStartIndex = findUniqueOpInjectMarker({
|
|
360
|
+
marker: startToken,
|
|
361
|
+
markerDescription: "start",
|
|
362
|
+
output: options.output,
|
|
363
|
+
secretName: options.entry.secretName,
|
|
364
|
+
secretReference: options.entry.secretRef.ref
|
|
365
|
+
}) + startToken.length;
|
|
366
|
+
const secretEndIndex = findUniqueOpInjectMarker({
|
|
367
|
+
marker: endToken,
|
|
368
|
+
markerDescription: "end",
|
|
369
|
+
output: options.output,
|
|
370
|
+
secretName: options.entry.secretName,
|
|
371
|
+
secretReference: options.entry.secretRef.ref
|
|
372
|
+
});
|
|
373
|
+
return options.output.slice(secretStartIndex, secretEndIndex);
|
|
374
|
+
}
|
|
375
|
+
function mapOpInjectOutput(entries, output) {
|
|
376
|
+
return Object.fromEntries(entries.map((entry) => [entry.secretName, extractInjectedSecret({
|
|
377
|
+
entry,
|
|
378
|
+
output
|
|
379
|
+
})]));
|
|
380
|
+
}
|
|
381
|
+
async function resolveAllSecretsWithOpInject(serviceAccountToken, refs, exec) {
|
|
382
|
+
const entries = createOpInjectEntries(refs);
|
|
383
|
+
if (entries.length === 0) return {};
|
|
384
|
+
return mapOpInjectOutput(entries, (await exec("op", [
|
|
385
|
+
"inject",
|
|
386
|
+
"--in-file",
|
|
387
|
+
"/dev/stdin"
|
|
388
|
+
], {
|
|
389
|
+
env: createOpCliServiceAccountEnv(serviceAccountToken),
|
|
390
|
+
input: buildOpInjectTemplate(entries),
|
|
391
|
+
redactErrorOutput: true
|
|
392
|
+
})).stdout);
|
|
393
|
+
}
|
|
394
|
+
async function resolveAllSecretsWithSerialOpReads(serviceAccountToken, refs, exec) {
|
|
395
|
+
const resolvedSecrets = {};
|
|
396
|
+
const failures = [];
|
|
397
|
+
for (const [secretName, secretRef] of Object.entries(refs)) try {
|
|
398
|
+
resolvedSecrets[secretName] = await resolveSecretWithOpCli(serviceAccountToken, secretRef.ref, exec);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
failures.push(new Error(`Failed to resolve secret '${secretName}' from '${secretRef.ref}' via op read: ${formatUnknownError(error)}`, { cause: error }));
|
|
401
|
+
}
|
|
402
|
+
if (failures.length > 0) throw new AggregateError(failures, `Failed to resolve ${String(failures.length)} secret(s) via op read.`);
|
|
403
|
+
return resolvedSecrets;
|
|
404
|
+
}
|
|
405
|
+
function formatResolveReferenceError(error) {
|
|
406
|
+
return "message" in error && typeof error.message === "string" ? `${error.type}: ${error.message}` : error.type;
|
|
407
|
+
}
|
|
408
|
+
function readSdkBatchSecret(options) {
|
|
409
|
+
const individualResponse = options.response.individualResponses[options.secretReference];
|
|
410
|
+
if (!individualResponse) throw new Error(`1Password SDK resolveAll response omitted '${options.secretName}' (${options.secretReference}).`);
|
|
411
|
+
if (individualResponse.content !== void 0) return individualResponse.content.secret;
|
|
412
|
+
if (individualResponse.error !== void 0) throw new Error(`1Password SDK resolveAll failed for '${options.secretName}' (${options.secretReference}): ${formatResolveReferenceError(individualResponse.error)}`);
|
|
413
|
+
throw new Error(`1Password SDK resolveAll returned neither content nor error for '${options.secretName}' (${options.secretReference}).`);
|
|
414
|
+
}
|
|
415
|
+
function mapSdkResolveAllResponse(refs, response) {
|
|
416
|
+
return Object.fromEntries(Object.entries(refs).map(([secretName, secretRef]) => [secretName, readSdkBatchSecret({
|
|
417
|
+
response,
|
|
418
|
+
secretName,
|
|
419
|
+
secretReference: secretRef.ref
|
|
420
|
+
})]));
|
|
421
|
+
}
|
|
422
|
+
async function createSecretResolver(options, dependencies = {}) {
|
|
423
|
+
const exec = dependencies.execFileAsync ?? execFileAsync;
|
|
424
|
+
try {
|
|
425
|
+
const client = await (dependencies.createClient ?? createClient)({
|
|
426
|
+
auth: options.serviceAccountToken,
|
|
427
|
+
integrationName: dependencies.integrationName ?? "agent-vm",
|
|
428
|
+
integrationVersion: dependencies.integrationVersion ?? "0.0.1"
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
resolve: async (ref) => {
|
|
432
|
+
try {
|
|
433
|
+
return await client.secrets.resolve(ref.ref);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const sdkResolveError = createFallbackStageError("1Password SDK resolve", error);
|
|
436
|
+
try {
|
|
437
|
+
return await resolveSecretWithOpCli(options.serviceAccountToken, ref.ref, exec);
|
|
438
|
+
} catch (fallbackError) {
|
|
439
|
+
throw createFallbackFailureError({
|
|
440
|
+
fallbackError,
|
|
441
|
+
message: "1Password SDK resolve and op CLI fallback both failed.",
|
|
442
|
+
stageError: sdkResolveError
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
resolveAll: async (refs) => {
|
|
448
|
+
try {
|
|
449
|
+
return mapSdkResolveAllResponse(refs, await client.secrets.resolveAll(Object.values(refs).map((secretRef) => secretRef.ref)));
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const sdkResolveAllError = createFallbackStageError("1Password SDK resolveAll", error);
|
|
452
|
+
try {
|
|
453
|
+
return await resolveAllSecretsWithOpCli(options.serviceAccountToken, refs, exec);
|
|
454
|
+
} catch (fallbackError) {
|
|
455
|
+
throw createFallbackFailureError({
|
|
456
|
+
fallbackError,
|
|
457
|
+
message: "1Password SDK resolveAll and op CLI fallback both failed.",
|
|
458
|
+
stageError: sdkResolveAllError
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
const sdkClientCreationError = createFallbackStageError("1Password SDK client creation", error);
|
|
466
|
+
return {
|
|
467
|
+
resolve: async (ref) => {
|
|
468
|
+
try {
|
|
469
|
+
return await resolveSecretWithOpCli(options.serviceAccountToken, ref.ref, exec);
|
|
470
|
+
} catch (fallbackError) {
|
|
471
|
+
throw createFallbackFailureError({
|
|
472
|
+
fallbackError,
|
|
473
|
+
message: "1Password SDK client creation and op CLI fallback both failed.",
|
|
474
|
+
stageError: sdkClientCreationError
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
resolveAll: async (refs) => {
|
|
479
|
+
try {
|
|
480
|
+
return await resolveAllSecretsWithOpCli(options.serviceAccountToken, refs, exec);
|
|
481
|
+
} catch (fallbackError) {
|
|
482
|
+
throw createFallbackFailureError({
|
|
483
|
+
fallbackError,
|
|
484
|
+
message: "1Password SDK client creation and op CLI fallback both failed.",
|
|
485
|
+
stageError: sdkClientCreationError
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function createOpCliSecretResolver(options, dependencies = {}) {
|
|
493
|
+
const exec = dependencies.execFileAsync ?? execFileAsync;
|
|
494
|
+
return {
|
|
495
|
+
resolve: async (ref) => await resolveSecretWithOpCli(options.serviceAccountToken, ref.ref, exec),
|
|
496
|
+
resolveAll: async (refs) => await resolveAllSecretsWithOpCli(options.serviceAccountToken, refs, exec)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
export { createCompositeSecretResolver, createOpCliSecretResolver, createSecretResolver, createStaticSecretResolver, resolveServiceAccountToken };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/testing.ts
|
|
2
|
+
function createStaticSecretResolver(values) {
|
|
3
|
+
const resolve = async (ref) => {
|
|
4
|
+
const value = values[ref.ref];
|
|
5
|
+
if (value === void 0) throw new Error(`No test secret value configured for '${ref.ref}'.`);
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
8
|
+
return {
|
|
9
|
+
resolve,
|
|
10
|
+
resolveAll: async (refs) => {
|
|
11
|
+
return Object.fromEntries(await Promise.all(Object.entries(refs).map(async ([name, ref]) => [name, await resolve(ref)])));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { createStaticSecretResolver };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-vm/secret-management",
|
|
3
|
+
"version": "0.0.71",
|
|
4
|
+
"description": "Shared secret reference contracts and resolvers for agent-vm packages.",
|
|
5
|
+
"homepage": "https://github.com/ShravanSunder/agent-vm#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/ShravanSunder/agent-vm/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Shravan Sunder <ShravanSunder@users.noreply.github.com>",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ShravanSunder/agent-vm.git",
|
|
14
|
+
"directory": "packages/secret-management"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./contracts": {
|
|
28
|
+
"types": "./dist/contracts.d.ts",
|
|
29
|
+
"import": "./dist/contracts.js"
|
|
30
|
+
},
|
|
31
|
+
"./testing": {
|
|
32
|
+
"types": "./dist/testing.d.ts",
|
|
33
|
+
"import": "./dist/testing.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@1password/sdk": "^0.4.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|