@syntesseraai/opencode-feature-factory 0.2.7 → 0.2.8
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/discovery.js +19 -16
- package/dist/discovery.test.js +2 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +53 -50
- package/dist/output.js +6 -2
- package/dist/output.test.js +30 -28
- package/dist/quality-gate-config.js +14 -6
- package/dist/quality-gate-config.test.js +24 -22
- package/dist/stop-quality-gate.d.ts +1 -1
- package/dist/stop-quality-gate.js +11 -5
- package/dist/stop-quality-gate.test.js +84 -82
- package/dist/types.js +2 -1
- package/package.json +1 -10
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Unit tests for stop-quality-gate module
|
|
3
4
|
*
|
|
@@ -5,7 +6,8 @@
|
|
|
5
6
|
* - sanitizeOutput: redacts secrets from CI output
|
|
6
7
|
* - isSessionReadOnly: determines if session has write permissions
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const stop_quality_gate_1 = require("./stop-quality-gate");
|
|
9
11
|
function isSessionReadOnly(permission) {
|
|
10
12
|
if (!permission)
|
|
11
13
|
return false;
|
|
@@ -15,178 +17,178 @@ describe('sanitizeOutput', () => {
|
|
|
15
17
|
describe('AWS credentials', () => {
|
|
16
18
|
it('should redact AWS Access Key IDs', () => {
|
|
17
19
|
const input = 'Found credentials: AKIAIOSFODNN7EXAMPLE in config';
|
|
18
|
-
const result = sanitizeOutput(input);
|
|
20
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
19
21
|
expect(result).toBe('Found credentials: [REDACTED_AWS_KEY] in config');
|
|
20
22
|
});
|
|
21
23
|
it('should redact multiple AWS keys', () => {
|
|
22
24
|
const input = 'Key1: AKIAIOSFODNN7EXAMPLE Key2: AKIAI44QH8DHBEXAMPLE';
|
|
23
|
-
const result = sanitizeOutput(input);
|
|
25
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
24
26
|
expect(result).toBe('Key1: [REDACTED_AWS_KEY] Key2: [REDACTED_AWS_KEY]');
|
|
25
27
|
});
|
|
26
28
|
it('should not redact partial AWS key patterns', () => {
|
|
27
29
|
const input = 'AKIA123'; // Too short
|
|
28
|
-
const result = sanitizeOutput(input);
|
|
30
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
29
31
|
expect(result).toBe('AKIA123');
|
|
30
32
|
});
|
|
31
33
|
});
|
|
32
34
|
describe('GitHub tokens', () => {
|
|
33
35
|
it('should redact GitHub Personal Access Tokens (classic)', () => {
|
|
34
36
|
const input = 'Using ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
35
|
-
const result = sanitizeOutput(input);
|
|
37
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
36
38
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
37
39
|
});
|
|
38
40
|
it('should redact GitHub Personal Access Tokens (fine-grained)', () => {
|
|
39
41
|
const input = 'Using github_pat_11ABCDEFG_abcdefghijklmnopqrstuvwxyz';
|
|
40
|
-
const result = sanitizeOutput(input);
|
|
42
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
41
43
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
42
44
|
});
|
|
43
45
|
it('should redact GitHub OAuth tokens', () => {
|
|
44
46
|
const input = 'oauth=gho_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
45
|
-
const result = sanitizeOutput(input);
|
|
47
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
46
48
|
expect(result).toBe('oauth=[REDACTED_GH_TOKEN]');
|
|
47
49
|
});
|
|
48
50
|
it('should redact GitHub App user-to-server tokens', () => {
|
|
49
51
|
const input = 'Using ghu_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
50
|
-
const result = sanitizeOutput(input);
|
|
52
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
51
53
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
52
54
|
});
|
|
53
55
|
it('should redact GitHub App server-to-server tokens', () => {
|
|
54
56
|
const input = 'Using ghs_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
55
|
-
const result = sanitizeOutput(input);
|
|
57
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
56
58
|
expect(result).toBe('Using [REDACTED_GH_TOKEN]');
|
|
57
59
|
});
|
|
58
60
|
it('should not redact partial GitHub token patterns', () => {
|
|
59
61
|
const input = 'ghp_abc'; // Too short
|
|
60
|
-
const result = sanitizeOutput(input);
|
|
62
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
61
63
|
expect(result).toBe('ghp_abc');
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
describe('GitLab tokens', () => {
|
|
65
67
|
it('should redact GitLab Personal Access Tokens', () => {
|
|
66
68
|
const input = 'Using glpat-abcdefghij1234567890';
|
|
67
|
-
const result = sanitizeOutput(input);
|
|
69
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
68
70
|
expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
|
|
69
71
|
});
|
|
70
72
|
it('should redact GitLab tokens with hyphens', () => {
|
|
71
73
|
const input = 'Using glpat-abc-def-ghi-jkl-mnop-qrs';
|
|
72
|
-
const result = sanitizeOutput(input);
|
|
74
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
73
75
|
expect(result).toBe('Using [REDACTED_GITLAB_TOKEN]');
|
|
74
76
|
});
|
|
75
77
|
it('should not redact partial GitLab token patterns', () => {
|
|
76
78
|
const input = 'glpat-short'; // Too short
|
|
77
|
-
const result = sanitizeOutput(input);
|
|
79
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
78
80
|
expect(result).toBe('glpat-short');
|
|
79
81
|
});
|
|
80
82
|
});
|
|
81
83
|
describe('npm tokens', () => {
|
|
82
84
|
it('should redact npm tokens', () => {
|
|
83
85
|
const input = 'Using npm_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789';
|
|
84
|
-
const result = sanitizeOutput(input);
|
|
86
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
85
87
|
expect(result).toBe('Using [REDACTED_NPM_TOKEN]');
|
|
86
88
|
});
|
|
87
89
|
it('should not redact partial npm token patterns', () => {
|
|
88
90
|
const input = 'npm_abc'; // Too short
|
|
89
|
-
const result = sanitizeOutput(input);
|
|
91
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
90
92
|
expect(result).toBe('npm_abc');
|
|
91
93
|
});
|
|
92
94
|
});
|
|
93
95
|
describe('Bearer tokens', () => {
|
|
94
96
|
it('should redact Bearer tokens', () => {
|
|
95
97
|
const input = 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
96
|
-
const result = sanitizeOutput(input);
|
|
98
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
97
99
|
expect(result).toBe('Authorization: Bearer [REDACTED]');
|
|
98
100
|
});
|
|
99
101
|
it('should handle case-insensitive Bearer', () => {
|
|
100
102
|
const input = 'bearer abc123-token.value';
|
|
101
|
-
const result = sanitizeOutput(input);
|
|
103
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
102
104
|
expect(result).toBe('Bearer [REDACTED]');
|
|
103
105
|
});
|
|
104
106
|
});
|
|
105
107
|
describe('API keys', () => {
|
|
106
108
|
it('should redact api_key assignments', () => {
|
|
107
109
|
const input = 'api_key=sk-1234567890abcdef';
|
|
108
|
-
const result = sanitizeOutput(input);
|
|
110
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
109
111
|
expect(result).toBe('api_key=[REDACTED]');
|
|
110
112
|
});
|
|
111
113
|
it('should redact api-key with hyphen', () => {
|
|
112
114
|
const input = 'api-key: my-secret-key';
|
|
113
|
-
const result = sanitizeOutput(input);
|
|
115
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
114
116
|
expect(result).toBe('api_key=[REDACTED]');
|
|
115
117
|
});
|
|
116
118
|
it('should redact apikey without separator', () => {
|
|
117
119
|
const input = 'apikey=abc123';
|
|
118
|
-
const result = sanitizeOutput(input);
|
|
120
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
119
121
|
expect(result).toBe('api_key=[REDACTED]');
|
|
120
122
|
});
|
|
121
123
|
it('should redact quoted api keys', () => {
|
|
122
124
|
const input = "api_key='secret-value'";
|
|
123
|
-
const result = sanitizeOutput(input);
|
|
125
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
124
126
|
expect(result).toBe('api_key=[REDACTED]');
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
129
|
describe('tokens', () => {
|
|
128
130
|
it('should redact token assignments with 8+ char values', () => {
|
|
129
131
|
const input = 'token=ghp_xxxxxxxxxxxxxxxxxxxx';
|
|
130
|
-
const result = sanitizeOutput(input);
|
|
132
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
131
133
|
expect(result).toBe('token=[REDACTED]');
|
|
132
134
|
});
|
|
133
135
|
it('should redact tokens plural with 8+ char values', () => {
|
|
134
136
|
const input = 'tokens: secret12345';
|
|
135
|
-
const result = sanitizeOutput(input);
|
|
137
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
136
138
|
expect(result).toBe('token=[REDACTED]');
|
|
137
139
|
});
|
|
138
140
|
it('should NOT redact token with short values (less than 8 chars)', () => {
|
|
139
141
|
const input = 'token=abc123';
|
|
140
|
-
const result = sanitizeOutput(input);
|
|
142
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
141
143
|
expect(result).toBe('token=abc123');
|
|
142
144
|
});
|
|
143
145
|
it('should NOT redact phrases like "token count: 5" (value too short)', () => {
|
|
144
146
|
const input = 'token count: 5';
|
|
145
|
-
const result = sanitizeOutput(input);
|
|
147
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
146
148
|
expect(result).toBe('token count: 5');
|
|
147
149
|
});
|
|
148
150
|
it('should NOT redact "token: abc" (value too short)', () => {
|
|
149
151
|
const input = 'token: abc';
|
|
150
|
-
const result = sanitizeOutput(input);
|
|
152
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
151
153
|
expect(result).toBe('token: abc');
|
|
152
154
|
});
|
|
153
155
|
it('should redact actual token values that are 8+ chars', () => {
|
|
154
156
|
const input = 'token=abcd1234efgh';
|
|
155
|
-
const result = sanitizeOutput(input);
|
|
157
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
156
158
|
expect(result).toBe('token=[REDACTED]');
|
|
157
159
|
});
|
|
158
160
|
it('should redact quoted tokens with 8+ char values', () => {
|
|
159
161
|
const input = 'token="my-secret-token-value"';
|
|
160
|
-
const result = sanitizeOutput(input);
|
|
162
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
161
163
|
expect(result).toBe('token=[REDACTED]');
|
|
162
164
|
});
|
|
163
165
|
});
|
|
164
166
|
describe('passwords', () => {
|
|
165
167
|
it('should redact password assignments', () => {
|
|
166
168
|
const input = 'password=super-secret-123!';
|
|
167
|
-
const result = sanitizeOutput(input);
|
|
169
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
168
170
|
expect(result).toBe('password=[REDACTED]');
|
|
169
171
|
});
|
|
170
172
|
it('should redact passwords plural', () => {
|
|
171
173
|
const input = 'passwords: "admin123"';
|
|
172
|
-
const result = sanitizeOutput(input);
|
|
174
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
173
175
|
expect(result).toBe('password=[REDACTED]');
|
|
174
176
|
});
|
|
175
177
|
it('should handle special characters in passwords', () => {
|
|
176
178
|
const input = 'password=P@$$w0rd!#%';
|
|
177
|
-
const result = sanitizeOutput(input);
|
|
179
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
178
180
|
expect(result).toBe('password=[REDACTED]');
|
|
179
181
|
});
|
|
180
182
|
});
|
|
181
183
|
describe('generic secrets', () => {
|
|
182
184
|
it('should redact secret assignments', () => {
|
|
183
185
|
const input = 'secret=my-app-secret-key';
|
|
184
|
-
const result = sanitizeOutput(input);
|
|
186
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
185
187
|
expect(result).toBe('secret=[REDACTED]');
|
|
186
188
|
});
|
|
187
189
|
it('should redact secrets plural', () => {
|
|
188
190
|
const input = 'secrets: "confidential-data"';
|
|
189
|
-
const result = sanitizeOutput(input);
|
|
191
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
190
192
|
expect(result).toBe('secret=[REDACTED]');
|
|
191
193
|
});
|
|
192
194
|
});
|
|
@@ -194,40 +196,40 @@ describe('sanitizeOutput', () => {
|
|
|
194
196
|
it('should redact long base64-like strings', () => {
|
|
195
197
|
const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODk=';
|
|
196
198
|
const input = `Encoded value: ${base64}`;
|
|
197
|
-
const result = sanitizeOutput(input);
|
|
199
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
198
200
|
expect(result).toBe('Encoded value: [REDACTED_BASE64]');
|
|
199
201
|
});
|
|
200
202
|
it('should not redact short base64 strings', () => {
|
|
201
203
|
const input = 'Short: abc123';
|
|
202
|
-
const result = sanitizeOutput(input);
|
|
204
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
203
205
|
expect(result).toBe('Short: abc123');
|
|
204
206
|
});
|
|
205
207
|
it('should redact base64 without padding', () => {
|
|
206
208
|
const base64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw';
|
|
207
|
-
const result = sanitizeOutput(base64);
|
|
209
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
208
210
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
209
211
|
});
|
|
210
212
|
it('should redact base64 strings up to 500 chars (ReDoS prevention)', () => {
|
|
211
213
|
// Generate a 500-char base64 string
|
|
212
214
|
const base64 = 'A'.repeat(500);
|
|
213
|
-
const result = sanitizeOutput(base64);
|
|
215
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
214
216
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
215
217
|
});
|
|
216
218
|
it('should handle base64 strings over 500 chars by matching only first 500 (ReDoS prevention)', () => {
|
|
217
219
|
// Generate a 501-char base64 string - only first 500 chars match
|
|
218
220
|
const base64 = 'A'.repeat(501);
|
|
219
|
-
const result = sanitizeOutput(base64);
|
|
221
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
220
222
|
// Pattern matches the first 500 chars, leaving 1 char behind
|
|
221
223
|
expect(result).toBe('[REDACTED_BASE64]A');
|
|
222
224
|
});
|
|
223
225
|
it('should handle base64 at exactly 40 chars (minimum threshold)', () => {
|
|
224
226
|
const base64 = 'A'.repeat(40);
|
|
225
|
-
const result = sanitizeOutput(base64);
|
|
227
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
226
228
|
expect(result).toBe('[REDACTED_BASE64]');
|
|
227
229
|
});
|
|
228
230
|
it('should NOT redact base64 strings under 40 chars', () => {
|
|
229
231
|
const base64 = 'A'.repeat(39);
|
|
230
|
-
const result = sanitizeOutput(base64);
|
|
232
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(base64);
|
|
231
233
|
expect(result).toBe(base64);
|
|
232
234
|
});
|
|
233
235
|
});
|
|
@@ -238,7 +240,7 @@ describe('sanitizeOutput', () => {
|
|
|
238
240
|
Auth: Bearer my-jwt-token
|
|
239
241
|
password=admin123
|
|
240
242
|
`;
|
|
241
|
-
const result = sanitizeOutput(input);
|
|
243
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
242
244
|
expect(result).toContain('[REDACTED_AWS_KEY]');
|
|
243
245
|
expect(result).toContain('Bearer [REDACTED]');
|
|
244
246
|
expect(result).toContain('password=[REDACTED]');
|
|
@@ -252,28 +254,28 @@ describe('sanitizeOutput', () => {
|
|
|
252
254
|
const input = `-----BEGIN RSA PRIVATE KEY-----
|
|
253
255
|
MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
|
|
254
256
|
-----END RSA PRIVATE KEY-----`;
|
|
255
|
-
const result = sanitizeOutput(input);
|
|
257
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
256
258
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
257
259
|
});
|
|
258
260
|
it('should redact OpenSSH private keys', () => {
|
|
259
261
|
const input = `-----BEGIN OPENSSH PRIVATE KEY-----
|
|
260
262
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAA
|
|
261
263
|
-----END OPENSSH PRIVATE KEY-----`;
|
|
262
|
-
const result = sanitizeOutput(input);
|
|
264
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
263
265
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
264
266
|
});
|
|
265
267
|
it('should redact EC private keys', () => {
|
|
266
268
|
const input = `-----BEGIN EC PRIVATE KEY-----
|
|
267
269
|
MHQCAQEEICg7E4NN6YPWoU6/FXa5ON6Pt6LKBfA8WL
|
|
268
270
|
-----END EC PRIVATE KEY-----`;
|
|
269
|
-
const result = sanitizeOutput(input);
|
|
271
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
270
272
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
271
273
|
});
|
|
272
274
|
it('should redact generic private keys', () => {
|
|
273
275
|
const input = `-----BEGIN PRIVATE KEY-----
|
|
274
276
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
|
|
275
277
|
-----END PRIVATE KEY-----`;
|
|
276
|
-
const result = sanitizeOutput(input);
|
|
278
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
277
279
|
expect(result).toBe('[REDACTED_PRIVATE_KEY]');
|
|
278
280
|
});
|
|
279
281
|
it('should redact private keys embedded in output', () => {
|
|
@@ -282,7 +284,7 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASC
|
|
|
282
284
|
MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
|
|
283
285
|
-----END RSA PRIVATE KEY-----
|
|
284
286
|
Done loading.`;
|
|
285
|
-
const result = sanitizeOutput(input);
|
|
287
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
286
288
|
expect(result).toBe(`Loading configuration...
|
|
287
289
|
[REDACTED_PRIVATE_KEY]
|
|
288
290
|
Done loading.`);
|
|
@@ -291,49 +293,49 @@ Done loading.`);
|
|
|
291
293
|
describe('GCP credentials', () => {
|
|
292
294
|
it('should redact GCP API keys', () => {
|
|
293
295
|
const input = 'Using GCP key: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe';
|
|
294
|
-
const result = sanitizeOutput(input);
|
|
296
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
295
297
|
expect(result).toBe('Using GCP key: [REDACTED_GCP_KEY]');
|
|
296
298
|
});
|
|
297
299
|
it('should redact multiple GCP API keys', () => {
|
|
298
300
|
const input = 'Key1: AIzaSyDaGmWKa4JsXZ-HjGw7ISLn_3namBGewQe Key2: AIzaSyB-1234567890abcdefghijklmnopqrstu';
|
|
299
|
-
const result = sanitizeOutput(input);
|
|
301
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
300
302
|
expect(result).toBe('Key1: [REDACTED_GCP_KEY] Key2: [REDACTED_GCP_KEY]');
|
|
301
303
|
});
|
|
302
304
|
it('should not redact partial GCP API key patterns', () => {
|
|
303
305
|
const input = 'AIza123'; // Too short
|
|
304
|
-
const result = sanitizeOutput(input);
|
|
306
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
305
307
|
expect(result).toBe('AIza123');
|
|
306
308
|
});
|
|
307
309
|
it('should redact GCP OAuth tokens', () => {
|
|
308
310
|
const input = 'Authorization: ya29.a0AfH6SMBx-example-token-value_123';
|
|
309
|
-
const result = sanitizeOutput(input);
|
|
311
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
310
312
|
expect(result).toBe('Authorization: [REDACTED_GCP_TOKEN]');
|
|
311
313
|
});
|
|
312
314
|
it('should redact GCP OAuth tokens with various characters', () => {
|
|
313
315
|
const input = 'token=ya29.Gl-abc_XYZ-123';
|
|
314
|
-
const result = sanitizeOutput(input);
|
|
316
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
315
317
|
expect(result).toBe('token=[REDACTED_GCP_TOKEN]');
|
|
316
318
|
});
|
|
317
319
|
});
|
|
318
320
|
describe('Slack tokens', () => {
|
|
319
321
|
it('should redact Slack bot tokens', () => {
|
|
320
322
|
const input = 'SLACK_BOT_TOKEN=xoxb-123456789012-1234567890123-AbCdEfGhIjKlMnOpQrStUvWx';
|
|
321
|
-
const result = sanitizeOutput(input);
|
|
323
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
322
324
|
expect(result).toBe('SLACK_BOT_TOKEN=[REDACTED_SLACK_TOKEN]');
|
|
323
325
|
});
|
|
324
326
|
it('should redact Slack user tokens', () => {
|
|
325
327
|
const input = 'Using xoxp-123456789012-123456789012-123456789012-abcdef1234567890abcdef1234567890';
|
|
326
|
-
const result = sanitizeOutput(input);
|
|
328
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
327
329
|
expect(result).toBe('Using [REDACTED_SLACK_TOKEN]');
|
|
328
330
|
});
|
|
329
331
|
it('should redact Slack app tokens', () => {
|
|
330
332
|
const input = 'APP_TOKEN=xapp-1-A0123BCDEFG-1234567890123-abcdefghijklmnopqrstuvwxyz0123456789';
|
|
331
|
-
const result = sanitizeOutput(input);
|
|
333
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
332
334
|
expect(result).toBe('APP_TOKEN=[REDACTED_SLACK_TOKEN]');
|
|
333
335
|
});
|
|
334
336
|
it('should redact Slack webhook URLs', () => {
|
|
335
337
|
const input = 'Webhook: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX';
|
|
336
|
-
const result = sanitizeOutput(input);
|
|
338
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
337
339
|
expect(result).toBe('Webhook: https://[REDACTED_SLACK_WEBHOOK]');
|
|
338
340
|
});
|
|
339
341
|
it('should redact multiple different Slack token types', () => {
|
|
@@ -342,7 +344,7 @@ Done loading.`);
|
|
|
342
344
|
User: xoxp-789-012-def
|
|
343
345
|
App: xapp-345-ghi
|
|
344
346
|
`;
|
|
345
|
-
const result = sanitizeOutput(input);
|
|
347
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
346
348
|
expect(result).toContain('[REDACTED_SLACK_TOKEN]');
|
|
347
349
|
expect(result).not.toContain('xoxb-');
|
|
348
350
|
expect(result).not.toContain('xoxp-');
|
|
@@ -353,100 +355,100 @@ Done loading.`);
|
|
|
353
355
|
it('should redact Stripe live secret keys', () => {
|
|
354
356
|
// 24 chars after sk_live_: 51ABC123DEF456GHI789JKLM
|
|
355
357
|
const input = 'STRIPE_SECRET_KEY=sk_live_51ABC123DEF456GHI789JKLM';
|
|
356
|
-
const result = sanitizeOutput(input);
|
|
358
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
357
359
|
expect(result).toBe('STRIPE_SECRET_KEY=[REDACTED_STRIPE_KEY]');
|
|
358
360
|
});
|
|
359
361
|
it('should redact Stripe test secret keys', () => {
|
|
360
362
|
// 24 chars after sk_test_: 51ABC123DEF456GHI789JKLM
|
|
361
363
|
const input = 'Using sk_test_51ABC123DEF456GHI789JKLM for testing';
|
|
362
|
-
const result = sanitizeOutput(input);
|
|
364
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
363
365
|
expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
|
|
364
366
|
});
|
|
365
367
|
it('should redact Stripe live restricted keys', () => {
|
|
366
368
|
// 24 chars after rk_live_: 51ABC123DEF456GHI789JKLM
|
|
367
369
|
const input = 'RESTRICTED_KEY=rk_live_51ABC123DEF456GHI789JKLM';
|
|
368
|
-
const result = sanitizeOutput(input);
|
|
370
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
369
371
|
expect(result).toBe('RESTRICTED_KEY=[REDACTED_STRIPE_KEY]');
|
|
370
372
|
});
|
|
371
373
|
it('should redact Stripe test restricted keys', () => {
|
|
372
374
|
// 24 chars after rk_test_: 51ABC123DEF456GHI789JKLM
|
|
373
375
|
const input = 'Using rk_test_51ABC123DEF456GHI789JKLM for testing';
|
|
374
|
-
const result = sanitizeOutput(input);
|
|
376
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
375
377
|
expect(result).toBe('Using [REDACTED_STRIPE_KEY] for testing');
|
|
376
378
|
});
|
|
377
379
|
it('should not redact Stripe keys that are too short', () => {
|
|
378
380
|
const input = 'sk_live_short'; // Less than 24 characters after prefix
|
|
379
|
-
const result = sanitizeOutput(input);
|
|
381
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
380
382
|
expect(result).toBe('sk_live_short');
|
|
381
383
|
});
|
|
382
384
|
it('should redact multiple Stripe keys', () => {
|
|
383
385
|
// 24 chars after each prefix
|
|
384
386
|
const input = 'Live: sk_live_51ABC123DEF456GHI789JKLM Test: sk_test_51XYZ789ABC123DEF456GHIJ';
|
|
385
|
-
const result = sanitizeOutput(input);
|
|
387
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
386
388
|
expect(result).toBe('Live: [REDACTED_STRIPE_KEY] Test: [REDACTED_STRIPE_KEY]');
|
|
387
389
|
});
|
|
388
390
|
it('should redact long Stripe keys', () => {
|
|
389
391
|
const input = 'sk_live_51ABC123DEF456GHI789JKLmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOP';
|
|
390
|
-
const result = sanitizeOutput(input);
|
|
392
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
391
393
|
expect(result).toBe('[REDACTED_STRIPE_KEY]');
|
|
392
394
|
});
|
|
393
395
|
});
|
|
394
396
|
describe('database connection strings', () => {
|
|
395
397
|
it('should redact PostgreSQL connection strings', () => {
|
|
396
398
|
const input = 'DATABASE_URL=postgres://admin:secretpass123@db.example.com:5432/mydb';
|
|
397
|
-
const result = sanitizeOutput(input);
|
|
399
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
398
400
|
expect(result).toBe('DATABASE_URL=[REDACTED_CONNECTION_STRING]');
|
|
399
401
|
});
|
|
400
402
|
it('should redact MongoDB connection strings with +srv', () => {
|
|
401
403
|
const input = 'Connecting to mongodb+srv://user:p@ssw0rd@cluster.mongodb.net/database';
|
|
402
|
-
const result = sanitizeOutput(input);
|
|
404
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
403
405
|
expect(result).toBe('Connecting to [REDACTED_CONNECTION_STRING]');
|
|
404
406
|
});
|
|
405
407
|
it('should redact Redis connection strings', () => {
|
|
406
408
|
const input = 'REDIS_URL=redis://default:myredispassword@redis.example.com:6379';
|
|
407
|
-
const result = sanitizeOutput(input);
|
|
409
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
408
410
|
expect(result).toBe('REDIS_URL=[REDACTED_CONNECTION_STRING]');
|
|
409
411
|
});
|
|
410
412
|
it('should redact MySQL connection strings', () => {
|
|
411
413
|
const input = 'mysql://root:rootpassword@localhost:3306/testdb';
|
|
412
|
-
const result = sanitizeOutput(input);
|
|
414
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
413
415
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
414
416
|
});
|
|
415
417
|
it('should redact rediss (TLS) connection strings', () => {
|
|
416
418
|
const input = 'rediss://user:password@secure-redis.example.com:6380';
|
|
417
|
-
const result = sanitizeOutput(input);
|
|
419
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
418
420
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
419
421
|
});
|
|
420
422
|
it('should redact connection strings with URL-encoded passwords', () => {
|
|
421
423
|
const input = 'mongodb://user:p%40ss%23word@host/db';
|
|
422
|
-
const result = sanitizeOutput(input);
|
|
424
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
423
425
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
424
426
|
});
|
|
425
427
|
it('should redact PostgreSQL with URL-encoded @ in password', () => {
|
|
426
428
|
const input = 'postgres://admin:secret%40pass@db.example.com:5432/mydb';
|
|
427
|
-
const result = sanitizeOutput(input);
|
|
429
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
428
430
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
429
431
|
});
|
|
430
432
|
it('should redact connection strings with multiple URL-encoded characters', () => {
|
|
431
433
|
const input = 'mysql://root:p%40ss%3Dw%26rd%21@localhost:3306/testdb';
|
|
432
|
-
const result = sanitizeOutput(input);
|
|
434
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
433
435
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
434
436
|
});
|
|
435
437
|
it('should redact MongoDB+srv with URL-encoded password', () => {
|
|
436
438
|
const input = 'mongodb+srv://user:my%40complex%23pass@cluster.mongodb.net/database';
|
|
437
|
-
const result = sanitizeOutput(input);
|
|
439
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
438
440
|
expect(result).toBe('[REDACTED_CONNECTION_STRING]');
|
|
439
441
|
});
|
|
440
442
|
});
|
|
441
443
|
describe('non-secret content', () => {
|
|
442
444
|
it('should preserve normal log output', () => {
|
|
443
445
|
const input = 'Build completed successfully in 2.5s';
|
|
444
|
-
const result = sanitizeOutput(input);
|
|
446
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
445
447
|
expect(result).toBe(input);
|
|
446
448
|
});
|
|
447
449
|
it('should preserve error messages without secrets', () => {
|
|
448
450
|
const input = 'Error: Cannot find module "./missing-file"';
|
|
449
|
-
const result = sanitizeOutput(input);
|
|
451
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
450
452
|
expect(result).toBe(input);
|
|
451
453
|
});
|
|
452
454
|
it('should preserve stack traces', () => {
|
|
@@ -455,7 +457,7 @@ Done loading.`);
|
|
|
455
457
|
at Object.<anonymous> (/app/test.js:10:5)
|
|
456
458
|
at Module._compile (internal/modules/cjs/loader.js:1085:14)
|
|
457
459
|
`;
|
|
458
|
-
const result = sanitizeOutput(input);
|
|
460
|
+
const result = (0, stop_quality_gate_1.sanitizeOutput)(input);
|
|
459
461
|
expect(result).toBe(input);
|
|
460
462
|
});
|
|
461
463
|
});
|
|
@@ -506,17 +508,17 @@ describe('isSessionReadOnly', () => {
|
|
|
506
508
|
describe('truncateOutput', () => {
|
|
507
509
|
it('should return output unchanged if under maxLines', () => {
|
|
508
510
|
const input = 'line1\nline2\nline3';
|
|
509
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
511
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
510
512
|
});
|
|
511
513
|
it('should return output unchanged if exactly at maxLines', () => {
|
|
512
514
|
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
|
|
513
515
|
const input = lines.join('\n');
|
|
514
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
516
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
515
517
|
});
|
|
516
518
|
it('should truncate to last 20 lines by default', () => {
|
|
517
519
|
const lines = Array.from({ length: 30 }, (_, i) => `line${i + 1}`);
|
|
518
520
|
const input = lines.join('\n');
|
|
519
|
-
const result = truncateOutput(input);
|
|
521
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input);
|
|
520
522
|
expect(result).toContain('... (10 lines omitted)');
|
|
521
523
|
expect(result).toContain('line11');
|
|
522
524
|
expect(result).toContain('line30');
|
|
@@ -526,7 +528,7 @@ describe('truncateOutput', () => {
|
|
|
526
528
|
it('should truncate to custom maxLines', () => {
|
|
527
529
|
const lines = Array.from({ length: 15 }, (_, i) => `line${i + 1}`);
|
|
528
530
|
const input = lines.join('\n');
|
|
529
|
-
const result = truncateOutput(input, 5);
|
|
531
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input, 5);
|
|
530
532
|
expect(result).toContain('... (10 lines omitted)');
|
|
531
533
|
expect(result).toContain('line11');
|
|
532
534
|
expect(result).toContain('line15');
|
|
@@ -534,15 +536,15 @@ describe('truncateOutput', () => {
|
|
|
534
536
|
});
|
|
535
537
|
it('should handle single line output', () => {
|
|
536
538
|
const input = 'single line';
|
|
537
|
-
expect(truncateOutput(input, 20)).toBe(input);
|
|
539
|
+
expect((0, stop_quality_gate_1.truncateOutput)(input, 20)).toBe(input);
|
|
538
540
|
});
|
|
539
541
|
it('should handle empty output', () => {
|
|
540
|
-
expect(truncateOutput('', 20)).toBe('');
|
|
542
|
+
expect((0, stop_quality_gate_1.truncateOutput)('', 20)).toBe('');
|
|
541
543
|
});
|
|
542
544
|
it('should preserve line content exactly', () => {
|
|
543
545
|
const lines = Array.from({ length: 25 }, (_, i) => `Error at line ${i + 1}: something failed`);
|
|
544
546
|
const input = lines.join('\n');
|
|
545
|
-
const result = truncateOutput(input, 10);
|
|
547
|
+
const result = (0, stop_quality_gate_1.truncateOutput)(input, 10);
|
|
546
548
|
expect(result).toContain('Error at line 16: something failed');
|
|
547
549
|
expect(result).toContain('Error at line 25: something failed');
|
|
548
550
|
});
|
package/dist/types.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@syntesseraai/opencode-feature-factory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.8",
|
|
5
5
|
"description": "OpenCode plugin for Feature Factory agents - provides sub-agents and skills for validation, review, security, and architecture assessment",
|
|
6
|
-
"type": "module",
|
|
7
6
|
"license": "MIT",
|
|
8
7
|
"main": "./dist/index.js",
|
|
9
|
-
"module": "./dist/index.js",
|
|
10
8
|
"types": "./dist/index.d.ts",
|
|
11
9
|
"bin": {
|
|
12
10
|
"ff-deploy": "./bin/ff-deploy.js"
|
|
13
11
|
},
|
|
14
|
-
"exports": {
|
|
15
|
-
".": {
|
|
16
|
-
"types": "./dist/index.d.ts",
|
|
17
|
-
"import": "./dist/index.js",
|
|
18
|
-
"default": "./dist/index.js"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
12
|
"files": [
|
|
22
13
|
"dist",
|
|
23
14
|
"assets",
|