@outputai/cli 0.3.3-next.33928d3.0 → 0.3.3-next.6137ea6.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/assets/docker/docker-compose-dev.yml +1 -1
- package/dist/commands/credentials/edit.js +7 -0
- package/dist/commands/credentials/edit.spec.js +23 -0
- package/dist/generated/framework_version.json +1 -1
- package/dist/services/credentials_service.integration.test.js +60 -0
- package/dist/templates/project/package.json.template +1 -1
- package/package.json +4 -4
|
@@ -48,6 +48,13 @@ export default class CredentialsEdit extends Command {
|
|
|
48
48
|
const edited = fs.readFileSync(tmpFile, 'utf8');
|
|
49
49
|
// Validate YAML before saving
|
|
50
50
|
parseYaml(edited);
|
|
51
|
+
// AES-GCM uses a fresh nonce per encrypt, so re-encrypting unchanged
|
|
52
|
+
// plaintext produces a new ciphertext. Skip the write when nothing
|
|
53
|
+
// changed to avoid leaving the file dirty in git.
|
|
54
|
+
if (edited === plaintext) {
|
|
55
|
+
this.log('No changes detected. Credentials unchanged.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
51
58
|
writeEncrypted(environment, edited, workflow);
|
|
52
59
|
this.log('Credentials saved successfully.');
|
|
53
60
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import * as credentialsService from '#services/credentials_service.js';
|
|
4
5
|
import CredentialsEdit from './edit.js';
|
|
5
6
|
vi.mock('#services/credentials_service.js');
|
|
@@ -70,4 +71,26 @@ describe('credentials edit command', () => {
|
|
|
70
71
|
await expect(cmd.run()).rejects.toThrow('No credentials file found');
|
|
71
72
|
});
|
|
72
73
|
});
|
|
74
|
+
// Regression: OUT-441 — editing without making changes must not re-encrypt
|
|
75
|
+
// the file. AES-GCM uses a fresh nonce per encrypt, so an unconditional
|
|
76
|
+
// re-write produces a new ciphertext and leaves the file dirty in git.
|
|
77
|
+
describe('no-op edit (OUT-441)', () => {
|
|
78
|
+
it('should NOT call writeEncrypted when the editor returns unchanged plaintext', async () => {
|
|
79
|
+
const original = 'anthropic:\n api_key: sk-test\n';
|
|
80
|
+
vi.mocked(credentialsService.decryptCredentials).mockReturnValue(original);
|
|
81
|
+
vi.mocked(fs.readFileSync).mockReturnValue(original);
|
|
82
|
+
const cmd = createTestCommand();
|
|
83
|
+
await cmd.run();
|
|
84
|
+
expect(credentialsService.writeEncrypted).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
it('should still call writeEncrypted when the editor returns modified plaintext', async () => {
|
|
87
|
+
const original = 'anthropic:\n api_key: sk-test\n';
|
|
88
|
+
const modified = 'anthropic:\n api_key: sk-NEW\n';
|
|
89
|
+
vi.mocked(credentialsService.decryptCredentials).mockReturnValue(original);
|
|
90
|
+
vi.mocked(fs.readFileSync).mockReturnValue(modified);
|
|
91
|
+
const cmd = createTestCommand();
|
|
92
|
+
await cmd.run();
|
|
93
|
+
expect(credentialsService.writeEncrypted).toHaveBeenCalledWith(undefined, modified, undefined);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
73
96
|
});
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { load as parseYaml } from 'js-yaml';
|
|
6
6
|
import { encrypt, decrypt, generateKey } from '@outputai/credentials';
|
|
7
|
+
import { initCredentials, decryptCredentials, writeEncrypted } from './credentials_service.js';
|
|
7
8
|
describe('credentials service integration', () => {
|
|
8
9
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'output-creds-'));
|
|
9
10
|
afterAll(() => {
|
|
@@ -63,4 +64,63 @@ describe('credentials service integration', () => {
|
|
|
63
64
|
expect(parsed.openai.api_key).toBe('sk-openai-789');
|
|
64
65
|
});
|
|
65
66
|
});
|
|
67
|
+
// Reproduction: editing the dev credential must not touch the production
|
|
68
|
+
// credential. Reported as a partial bug report — these tests pin the
|
|
69
|
+
// cross-environment isolation invariant.
|
|
70
|
+
describe('multi-environment isolation', () => {
|
|
71
|
+
const withIsolatedProject = (body) => {
|
|
72
|
+
const originalCwd = process.cwd();
|
|
73
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'output-creds-multi-env-'));
|
|
74
|
+
process.chdir(projectDir);
|
|
75
|
+
try {
|
|
76
|
+
body();
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
process.chdir(originalCwd);
|
|
80
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
it('editing the development credential does not modify the production credential', () => withIsolatedProject(() => {
|
|
84
|
+
const prod = initCredentials('production');
|
|
85
|
+
const dev = initCredentials('development');
|
|
86
|
+
const prodKeyBefore = fs.readFileSync(prod.keyPath);
|
|
87
|
+
const prodCredBefore = fs.readFileSync(prod.credPath);
|
|
88
|
+
const devKeyBefore = fs.readFileSync(dev.keyPath);
|
|
89
|
+
writeEncrypted('development', 'anthropic:\n api_key: sk-DEV-EDITED\nopenai:\n api_key: sk-DEV-EDITED-2\n');
|
|
90
|
+
// Production must be byte-for-byte identical
|
|
91
|
+
expect(fs.readFileSync(prod.keyPath).equals(prodKeyBefore)).toBe(true);
|
|
92
|
+
expect(fs.readFileSync(prod.credPath).equals(prodCredBefore)).toBe(true);
|
|
93
|
+
// Dev key must not have been regenerated
|
|
94
|
+
expect(fs.readFileSync(dev.keyPath).equals(devKeyBefore)).toBe(true);
|
|
95
|
+
// Dev credential should now contain the edited plaintext
|
|
96
|
+
expect(decryptCredentials('development')).toContain('sk-DEV-EDITED');
|
|
97
|
+
// Production credential should still be the original template (empty values)
|
|
98
|
+
const prodPlain = decryptCredentials('production');
|
|
99
|
+
const prodParsed = parseYaml(prodPlain);
|
|
100
|
+
expect(prodParsed.anthropic.api_key).toBe('');
|
|
101
|
+
expect(prodParsed.openai.api_key).toBe('');
|
|
102
|
+
}));
|
|
103
|
+
it('editing production does not bleed into development', () => withIsolatedProject(() => {
|
|
104
|
+
const prod = initCredentials('production');
|
|
105
|
+
const dev = initCredentials('development');
|
|
106
|
+
const devKeyBefore = fs.readFileSync(dev.keyPath);
|
|
107
|
+
const devCredBefore = fs.readFileSync(dev.credPath);
|
|
108
|
+
writeEncrypted('production', 'anthropic:\n api_key: sk-PROD-EDITED\nopenai:\n api_key: sk-PROD-EDITED-2\n');
|
|
109
|
+
expect(fs.readFileSync(dev.keyPath).equals(devKeyBefore)).toBe(true);
|
|
110
|
+
expect(fs.readFileSync(dev.credPath).equals(devCredBefore)).toBe(true);
|
|
111
|
+
expect(decryptCredentials('production')).toContain('sk-PROD-EDITED');
|
|
112
|
+
expect(decryptCredentials('development')).not.toContain('sk-PROD-EDITED');
|
|
113
|
+
// Sanity: prod and dev still resolved to different paths/keys
|
|
114
|
+
expect(prod.keyPath).not.toBe(dev.keyPath);
|
|
115
|
+
expect(prod.credPath).not.toBe(dev.credPath);
|
|
116
|
+
}));
|
|
117
|
+
it('initializing a second environment does not mutate the first', () => withIsolatedProject(() => {
|
|
118
|
+
const prod = initCredentials('production');
|
|
119
|
+
const prodKeyBytes = fs.readFileSync(prod.keyPath);
|
|
120
|
+
const prodCredBytes = fs.readFileSync(prod.credPath);
|
|
121
|
+
initCredentials('development');
|
|
122
|
+
expect(fs.readFileSync(prod.keyPath).equals(prodKeyBytes)).toBe(true);
|
|
123
|
+
expect(fs.readFileSync(prod.credPath).equals(prodCredBytes)).toBe(true);
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
66
126
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/cli",
|
|
3
|
-
"version": "0.3.3-next.
|
|
3
|
+
"version": "0.3.3-next.6137ea6.0",
|
|
4
4
|
"description": "CLI for Output.ai workflow generation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"semver": "7.7.4",
|
|
37
37
|
"undici": "8.1.0",
|
|
38
38
|
"yaml": "^2.8.3",
|
|
39
|
-
"@outputai/
|
|
40
|
-
"@outputai/
|
|
41
|
-
"@outputai/llm": "0.3.3-next.
|
|
39
|
+
"@outputai/evals": "0.3.3-next.6137ea6.0",
|
|
40
|
+
"@outputai/credentials": "0.3.3-next.6137ea6.0",
|
|
41
|
+
"@outputai/llm": "0.3.3-next.6137ea6.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/cli-progress": "3.11.6",
|