@outputai/credentials 0.2.1-next.fd72d95.0 → 0.3.0
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/encrypted_yaml_provider.js +20 -2
- package/dist/encrypted_yaml_provider.spec.js +64 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +22 -0
- package/dist/hooks.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { load as parseYaml } from 'js-yaml';
|
|
3
3
|
import { decrypt } from './encryption.js';
|
|
4
|
-
import { MissingKeyError } from './errors.js';
|
|
4
|
+
import { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingKeyError } from './errors.js';
|
|
5
5
|
import { resolveKeyEnvVar, resolveCredentialsPath, resolveKeyPath, resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar } from './paths.js';
|
|
6
6
|
const resolveBaseDir = () => {
|
|
7
7
|
const arg = process.argv[2];
|
|
@@ -29,7 +29,25 @@ const resolveWorkflowKey = (workflowName, workflowDir) => {
|
|
|
29
29
|
}
|
|
30
30
|
return resolveKey(undefined);
|
|
31
31
|
};
|
|
32
|
-
const
|
|
32
|
+
const safeDecrypt = (ciphertext, key, credPath) => {
|
|
33
|
+
try {
|
|
34
|
+
return decrypt(ciphertext, key);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof RangeError) {
|
|
38
|
+
throw new MalformedCredentialsKeyError(credPath, error.message);
|
|
39
|
+
}
|
|
40
|
+
if (error instanceof Error && error.message.includes('aes/gcm')) {
|
|
41
|
+
throw new InvalidCredentialsKeyError(credPath);
|
|
42
|
+
}
|
|
43
|
+
throw new InvalidCredentialsKeyError(credPath, error instanceof Error ? error.message : String(error));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const decryptYaml = (credPath, key) => {
|
|
47
|
+
const ciphertext = readFileSync(credPath, 'utf8').trim();
|
|
48
|
+
const plaintext = safeDecrypt(ciphertext, key, credPath);
|
|
49
|
+
return parseYaml(plaintext) || {};
|
|
50
|
+
};
|
|
33
51
|
export const encryptedYamlProvider = {
|
|
34
52
|
loadGlobal: ({ environment }) => {
|
|
35
53
|
const baseDir = resolveBaseDir();
|
|
@@ -81,6 +81,70 @@ describe('encrypted YAML provider', () => {
|
|
|
81
81
|
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
82
82
|
.toThrow('No credentials key found');
|
|
83
83
|
});
|
|
84
|
+
it('should throw InvalidCredentialsKeyError when the key does not match the encrypted file', async () => {
|
|
85
|
+
process.env.OUTPUT_CREDENTIALS_KEY = generateKey();
|
|
86
|
+
vi.doMock('node:fs', () => ({
|
|
87
|
+
readFileSync: () => ciphertext,
|
|
88
|
+
existsSync: (path) => path.endsWith('credentials.yml.enc')
|
|
89
|
+
}));
|
|
90
|
+
const provider = await loadProvider();
|
|
91
|
+
const { InvalidCredentialsKeyError } = await import('./errors.js');
|
|
92
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
93
|
+
.toThrow(InvalidCredentialsKeyError);
|
|
94
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
95
|
+
.toThrow(/Failed to decrypt .*credentials\.yml\.enc/);
|
|
96
|
+
});
|
|
97
|
+
it('should throw MalformedCredentialsKeyError when the key has the wrong length', async () => {
|
|
98
|
+
process.env.OUTPUT_CREDENTIALS_KEY = key.slice(0, 55);
|
|
99
|
+
vi.doMock('node:fs', () => ({
|
|
100
|
+
readFileSync: () => ciphertext,
|
|
101
|
+
existsSync: (path) => path.endsWith('credentials.yml.enc')
|
|
102
|
+
}));
|
|
103
|
+
const provider = await loadProvider();
|
|
104
|
+
const { MalformedCredentialsKeyError } = await import('./errors.js');
|
|
105
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
106
|
+
.toThrow(MalformedCredentialsKeyError);
|
|
107
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
108
|
+
.toThrow(/must be exactly 64 hex characters/);
|
|
109
|
+
});
|
|
110
|
+
it('should throw MalformedCredentialsKeyError when the key contains non-hex characters', async () => {
|
|
111
|
+
process.env.OUTPUT_CREDENTIALS_KEY = `${key.slice(0, 10)} ${key.slice(11)}`;
|
|
112
|
+
vi.doMock('node:fs', () => ({
|
|
113
|
+
readFileSync: () => ciphertext,
|
|
114
|
+
existsSync: (path) => path.endsWith('credentials.yml.enc')
|
|
115
|
+
}));
|
|
116
|
+
const provider = await loadProvider();
|
|
117
|
+
const { MalformedCredentialsKeyError } = await import('./errors.js');
|
|
118
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
119
|
+
.toThrow(MalformedCredentialsKeyError);
|
|
120
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
121
|
+
.toThrow(/typos, whitespace, or truncation/);
|
|
122
|
+
});
|
|
123
|
+
it('should throw InvalidCredentialsKeyError with the underlying message for unknown decrypt failures', async () => {
|
|
124
|
+
process.env.OUTPUT_CREDENTIALS_KEY = key;
|
|
125
|
+
vi.doMock('node:fs', () => ({
|
|
126
|
+
readFileSync: () => ciphertext,
|
|
127
|
+
existsSync: (path) => path.endsWith('credentials.yml.enc')
|
|
128
|
+
}));
|
|
129
|
+
vi.doMock('./encryption.js', () => ({
|
|
130
|
+
decrypt: () => {
|
|
131
|
+
throw new Error('unexpected internal failure');
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
try {
|
|
135
|
+
const provider = await loadProvider();
|
|
136
|
+
const { InvalidCredentialsKeyError } = await import('./errors.js');
|
|
137
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
138
|
+
.toThrow(InvalidCredentialsKeyError);
|
|
139
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
140
|
+
.toThrow(/unexpected internal failure/);
|
|
141
|
+
expect(() => provider.loadGlobal({ environment: undefined }))
|
|
142
|
+
.toThrow(/may not match.*or the credentials file may be corrupted/);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
vi.doUnmock('./encryption.js');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
84
148
|
});
|
|
85
149
|
describe('loadForWorkflow', () => {
|
|
86
150
|
it('should return null when workflowDir is undefined', async () => {
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,3 +4,9 @@ export declare class MissingKeyError extends Error {
|
|
|
4
4
|
export declare class MissingCredentialError extends Error {
|
|
5
5
|
constructor(path: string);
|
|
6
6
|
}
|
|
7
|
+
export declare class InvalidCredentialsKeyError extends Error {
|
|
8
|
+
constructor(credentialsPath: string, underlyingError?: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class MalformedCredentialsKeyError extends Error {
|
|
11
|
+
constructor(credentialsPath: string, detail: string);
|
|
12
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -7,10 +7,32 @@ export class MissingKeyError extends Error {
|
|
|
7
7
|
`config/credentials/${environment}.key` :
|
|
8
8
|
'config/credentials.key';
|
|
9
9
|
super(`No credentials key found. Set ${envVar} env var or create ${keyFile}.`);
|
|
10
|
+
this.name = 'MissingKeyError';
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
export class MissingCredentialError extends Error {
|
|
13
14
|
constructor(path) {
|
|
14
15
|
super(`Required credential not found: "${path}".`);
|
|
16
|
+
this.name = 'MissingCredentialError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class InvalidCredentialsKeyError extends Error {
|
|
20
|
+
constructor(credentialsPath, underlyingError) {
|
|
21
|
+
const message = underlyingError ?
|
|
22
|
+
`Failed to decrypt ${credentialsPath}: ${underlyingError}. ` +
|
|
23
|
+
'The credentials key may not match the one used to encrypt this file, or the credentials file may be corrupted. ' +
|
|
24
|
+
'Check OUTPUT_CREDENTIALS_KEY env var or config/credentials.key.' :
|
|
25
|
+
`Failed to decrypt ${credentialsPath}. The credentials key does not match the one used to encrypt this file. ` +
|
|
26
|
+
'Check OUTPUT_CREDENTIALS_KEY env var or config/credentials.key.';
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'InvalidCredentialsKeyError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class MalformedCredentialsKeyError extends Error {
|
|
32
|
+
constructor(credentialsPath, detail) {
|
|
33
|
+
super(`Credentials key for ${credentialsPath} is malformed (${detail}). ` +
|
|
34
|
+
'The key must be exactly 64 hex characters. ' +
|
|
35
|
+
'Check OUTPUT_CREDENTIALS_KEY env var or config/credentials.key for typos, whitespace, or truncation.');
|
|
36
|
+
this.name = 'MalformedCredentialsKeyError';
|
|
15
37
|
}
|
|
16
38
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setProvider } from './provider_registry.js';
|
|
2
2
|
import { encryptedYamlProvider } from './encrypted_yaml_provider.js';
|
|
3
|
-
import {
|
|
3
|
+
import { onBeforeWorkerStart } from '@outputai/core/hooks';
|
|
4
4
|
import { resolveCredentialRefs } from './credentials.js';
|
|
5
5
|
setProvider(encryptedYamlProvider);
|
|
6
|
-
|
|
6
|
+
onBeforeWorkerStart(resolveCredentialRefs);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export { credentials, resolveCredentialRefs } from './credentials.js';
|
|
|
3
3
|
export { setProvider, getProvider } from './provider_registry.js';
|
|
4
4
|
export { encryptedYamlProvider } from './encrypted_yaml_provider.js';
|
|
5
5
|
export { encrypt, decrypt, generateKey } from './encryption.js';
|
|
6
|
-
export { MissingCredentialError, MissingKeyError } from './errors.js';
|
|
6
|
+
export { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingCredentialError, MissingKeyError } from './errors.js';
|
|
7
7
|
export { getNestedValue, resolveCredentialsPath, resolveKeyPath, resolveKeyEnvVar, resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar } from './paths.js';
|
|
8
8
|
export type { CredentialsProvider, GlobalContext, WorkflowContext } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -3,5 +3,5 @@ export { credentials, resolveCredentialRefs } from './credentials.js';
|
|
|
3
3
|
export { setProvider, getProvider } from './provider_registry.js';
|
|
4
4
|
export { encryptedYamlProvider } from './encrypted_yaml_provider.js';
|
|
5
5
|
export { encrypt, decrypt, generateKey } from './encryption.js';
|
|
6
|
-
export { MissingCredentialError, MissingKeyError } from './errors.js';
|
|
6
|
+
export { InvalidCredentialsKeyError, MalformedCredentialsKeyError, MissingCredentialError, MissingKeyError } from './errors.js';
|
|
7
7
|
export { getNestedValue, resolveCredentialsPath, resolveKeyPath, resolveKeyEnvVar, resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar } from './paths.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/credentials",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Encrypted credentials management for Output.ai workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@noble/ciphers": "2.2.0",
|
|
21
21
|
"js-yaml": "4.1.1",
|
|
22
|
-
"@outputai/core": "0.
|
|
22
|
+
"@outputai/core": "0.3.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/js-yaml": "4.0.9"
|