@pixlcore/xycli 1.0.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/README.md +543 -0
- package/docs/help.md +2587 -0
- package/docs/screenshots.md +219 -0
- package/docs/sync.md +920 -0
- package/docs/tutorial.md +204 -0
- package/index.js +444 -0
- package/lib/alerts.js +735 -0
- package/lib/apikey.js +425 -0
- package/lib/buckets.js +607 -0
- package/lib/categories.js +323 -0
- package/lib/channels.js +290 -0
- package/lib/config.js +86 -0
- package/lib/dashboard.js +237 -0
- package/lib/doc.js +131 -0
- package/lib/events.js +1314 -0
- package/lib/groups.js +1038 -0
- package/lib/jobs.js +1254 -0
- package/lib/log.js +125 -0
- package/lib/marketplace.js +386 -0
- package/lib/monitors.js +412 -0
- package/lib/plugins.js +491 -0
- package/lib/secrets.js +441 -0
- package/lib/servers.js +1502 -0
- package/lib/snapshots.js +498 -0
- package/lib/sync.js +963 -0
- package/lib/system.js +612 -0
- package/lib/tags.js +250 -0
- package/lib/tickets.js +681 -0
- package/lib/transfer.js +447 -0
- package/lib/utils.js +2745 -0
- package/lib/webhook.js +414 -0
- package/logo.webp +0 -0
- package/package.json +38 -0
- package/test/README.md +97 -0
- package/test/api-validation.test.js +170 -0
- package/test/categories.test.js +246 -0
- package/test/channels.test.js +209 -0
- package/test/config.test.js +96 -0
- package/test/doc.test.js +84 -0
- package/test/groups.test.js +211 -0
- package/test/help.test.js +26 -0
- package/test/helpers/common.js +127 -0
- package/test/helpers/pagination-preload.cjs +18 -0
- package/test/log.test.js +103 -0
- package/test/markdown.test.js +61 -0
- package/test/marketplace.test.js +229 -0
- package/test/monitors.test.js +227 -0
- package/test/pagination.test.js +138 -0
- package/test/plugins.test.js +351 -0
- package/test/secrets.test.js +271 -0
- package/test/servers.test.js +303 -0
- package/test/snapshots.test.js +167 -0
- package/test/sync-unit.test.js +446 -0
- package/test/sync.test.js +542 -0
- package/test/system.test.js +94 -0
- package/test/tags.test.js +230 -0
- package/test/tickets.test.js +261 -0
- package/test/toast.test.js +73 -0
- package/test/transfer-apikey.test.js +96 -0
- package/test/transfer-workflow.test.js +122 -0
- package/test/transfer.test.js +314 -0
- package/test/webhook.test.js +332 -0
- package/test.js +49 -0
package/lib/secrets.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// Secret Vault Management Layer
|
|
2
|
+
|
|
3
|
+
const cli = require('pixl-cli');
|
|
4
|
+
const Tools = cli.Tools;
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
|
|
8
|
+
async cmd_secrets() {
|
|
9
|
+
// The plural command lists Secret Vault metadata only. Decrypted values are
|
|
10
|
+
// available exclusively through the explicit, confirmed decrypt command.
|
|
11
|
+
await this.cmd_get_secrets();
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async cmd_secret() {
|
|
15
|
+
var cmd = this.args.other.shift();
|
|
16
|
+
if (!cmd) return this.dieUsage('secret');
|
|
17
|
+
|
|
18
|
+
switch (cmd) {
|
|
19
|
+
case 'list': await this.cmd_get_secrets(); break;
|
|
20
|
+
case 'get': await this.cmd_get_secret(); break;
|
|
21
|
+
case 'create': await this.cmd_create_secret(); break;
|
|
22
|
+
case 'update': await this.cmd_update_secret(); break;
|
|
23
|
+
case 'decrypt': await this.cmd_decrypt_secret(); break;
|
|
24
|
+
case 'delete': await this.cmd_delete_secret(); break;
|
|
25
|
+
|
|
26
|
+
default:
|
|
27
|
+
// A bare ID or title opens the safe metadata view.
|
|
28
|
+
this.args.other.unshift(cmd);
|
|
29
|
+
await this.cmd_get_secret();
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async cmd_get_secrets() {
|
|
35
|
+
// Secret values are never present in getMultiple(), so all listing and
|
|
36
|
+
// filtering below operates exclusively on plaintext vault metadata.
|
|
37
|
+
this.prepSearchArgs();
|
|
38
|
+
await this.getMultiple();
|
|
39
|
+
|
|
40
|
+
var secrets = this.secrets.slice(0);
|
|
41
|
+
var is_filtered = !!this.args.other.length;
|
|
42
|
+
if (is_filtered) {
|
|
43
|
+
var search = this.args.other.join(' ');
|
|
44
|
+
secrets = this.findObjectsFuzzy(secrets, {
|
|
45
|
+
id: search,
|
|
46
|
+
title: search,
|
|
47
|
+
notes: search,
|
|
48
|
+
names: search
|
|
49
|
+
}, 1);
|
|
50
|
+
}
|
|
51
|
+
delete this.args.other;
|
|
52
|
+
|
|
53
|
+
if ('enabled' in this.args) {
|
|
54
|
+
var enabled = this.parseSecretBoolean(this.args.enabled, 'enabled');
|
|
55
|
+
secrets = secrets.filter( secret => !!secret.enabled === enabled );
|
|
56
|
+
delete this.args.enabled;
|
|
57
|
+
is_filtered = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
var filters = {
|
|
61
|
+
name: 'names',
|
|
62
|
+
event: 'events',
|
|
63
|
+
category: 'categories',
|
|
64
|
+
plugin: 'plugins',
|
|
65
|
+
web_hook: 'web_hooks',
|
|
66
|
+
hook: 'web_hooks'
|
|
67
|
+
};
|
|
68
|
+
Object.keys(filters).forEach( key => {
|
|
69
|
+
if (!(key in this.args)) return;
|
|
70
|
+
var values = this.parseSecretList(this.args[key], key);
|
|
71
|
+
var property = filters[key];
|
|
72
|
+
secrets = secrets.filter( secret => values.every( value => (secret[property] || []).includes(value) ) );
|
|
73
|
+
delete this.args[key];
|
|
74
|
+
is_filtered = true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (Tools.numKeys(this.args)) {
|
|
78
|
+
secrets = this.findObjectsFuzzy(secrets, this.args);
|
|
79
|
+
is_filtered = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
secrets.sort( (a, b) => String(a.title).toLowerCase().localeCompare(String(b.title).toLowerCase()) );
|
|
83
|
+
if (this.format.match(/json/)) return this.jsonOutput(secrets);
|
|
84
|
+
|
|
85
|
+
this.printPaginatedBoxTable({
|
|
86
|
+
title: is_filtered ? 'Filtered Secret Vaults' : 'All Secret Vaults',
|
|
87
|
+
header: ['Vault ID', 'Title', 'Status', 'Variable Names', 'Assignments', 'Modified'],
|
|
88
|
+
rows: secrets.slice(this.offset, this.offset + this.limit),
|
|
89
|
+
list: { length: secrets.length },
|
|
90
|
+
offset: this.offset,
|
|
91
|
+
limit: this.limit
|
|
92
|
+
}, secret => [
|
|
93
|
+
this.color('theme').bold(secret.id),
|
|
94
|
+
bold(secret.title),
|
|
95
|
+
this.getNiceEnabled(secret.enabled),
|
|
96
|
+
(secret.names || []).join(', ') || gray('(None)'),
|
|
97
|
+
Tools.commify(['events', 'categories', 'plugins', 'web_hooks'].reduce( (total, key) => total + (secret[key] || []).length, 0 )),
|
|
98
|
+
this.getRelativeDateTime(secret.modified, true)
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
this.printSuggestedCommands({
|
|
102
|
+
"View vault details": "xy secret SECRET_VAULT_ID_OR_TITLE",
|
|
103
|
+
"Create a Secret Vault": 'xy secret create --title "My Vault" --fields @secrets.json',
|
|
104
|
+
"List disabled vaults": "xy secrets --enabled false",
|
|
105
|
+
"Find vaults containing a name": "xy secrets --name API_TOKEN"
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async cmd_get_secret() {
|
|
110
|
+
await this.getMultiple();
|
|
111
|
+
var selector = this.args.other.join(' ') || this.args.id || this.args.title;
|
|
112
|
+
if (!selector) return this.dieUsage('secret get');
|
|
113
|
+
|
|
114
|
+
delete this.args.other;
|
|
115
|
+
delete this.args.id;
|
|
116
|
+
delete this.args.title;
|
|
117
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Secret Vault get option: --" + Tools.firstKey(this.args));
|
|
118
|
+
|
|
119
|
+
var match = Tools.findObject(this.secrets, { id: selector }) || this.findObjectFuzzy(this.secrets, { title: selector });
|
|
120
|
+
if (!match) return this.die("Could not find Secret Vault based on your criteria: " + selector);
|
|
121
|
+
var secret = await this.fetchSecret(match.id);
|
|
122
|
+
if (this.format.match(/json/)) return this.jsonOutput(secret);
|
|
123
|
+
|
|
124
|
+
this.printBoxList({
|
|
125
|
+
title: 'Secret Vault Summary',
|
|
126
|
+
rows: [
|
|
127
|
+
[ 'Vault ID', gray(secret.id) ],
|
|
128
|
+
[ 'Title', this.color('theme').bold(secret.title) ],
|
|
129
|
+
[ 'Status', this.getNiceEnabled(secret.enabled) ],
|
|
130
|
+
[ 'Icon', secret.icon || gray('(None)') ],
|
|
131
|
+
[ 'Variable Names', (secret.names || []).join(', ') || gray('(None)') ],
|
|
132
|
+
[ 'Events', this.getNiceSecretAssignments('events', secret.events) ],
|
|
133
|
+
[ 'Categories', this.getNiceSecretAssignments('categories', secret.categories) ],
|
|
134
|
+
[ 'Plugins', this.getNiceSecretAssignments('plugins', secret.plugins) ],
|
|
135
|
+
[ 'Web Hooks', this.getNiceSecretAssignments('web_hooks', secret.web_hooks) ],
|
|
136
|
+
[ 'Author', secret.username || gray('(Unknown)') ],
|
|
137
|
+
[ 'Created', this.getNiceDateTime(secret.created, true, true) ],
|
|
138
|
+
[ 'Modified', this.getNiceDateTime(secret.modified, true, true) ],
|
|
139
|
+
[ 'Revision', secret.revision || 1 ]
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Notes are plaintext metadata and may contain multiple lines.
|
|
144
|
+
if (secret.notes) this.printUserNotes('SECRET VAULT NOTES', secret.notes);
|
|
145
|
+
|
|
146
|
+
this.printSuggestedCommands({
|
|
147
|
+
"Decrypt variables": `xy secret decrypt ${secret.id}`,
|
|
148
|
+
"Update vault metadata": `xy secret update ${secret.id} --notes "Updated notes"`,
|
|
149
|
+
"Replace all variables": `xy secret update ${secret.id} --fields @secrets.json`,
|
|
150
|
+
"Disable vault": secret.enabled ? `xy secret update ${secret.id} --enabled false` : '',
|
|
151
|
+
"Delete vault": `xy secret delete ${secret.id}`
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async cmd_create_secret() {
|
|
156
|
+
// Load assignment targets for exact-ID validation before accepting plaintext.
|
|
157
|
+
await this.getMultiple();
|
|
158
|
+
if (this.args.other.length) return this.dieUsage('secret create');
|
|
159
|
+
delete this.args.other;
|
|
160
|
+
|
|
161
|
+
var params = this.prepareSecretParams({
|
|
162
|
+
enabled: true,
|
|
163
|
+
icon: '',
|
|
164
|
+
notes: '',
|
|
165
|
+
events: [],
|
|
166
|
+
categories: [],
|
|
167
|
+
plugins: [],
|
|
168
|
+
web_hooks: [],
|
|
169
|
+
fields: []
|
|
170
|
+
}, this.args, true);
|
|
171
|
+
if (!params.title) return this.dieUsage('secret create');
|
|
172
|
+
|
|
173
|
+
var data = await this.callStandardAPI('createSecret', params, {
|
|
174
|
+
text: 'Creating Secret Vault...',
|
|
175
|
+
filterRequest: request => this.redactSecretRequest(request)
|
|
176
|
+
});
|
|
177
|
+
if (this.dry) return;
|
|
178
|
+
if (this.format.match(/json/)) return this.jsonOutput(data.secret);
|
|
179
|
+
|
|
180
|
+
this.toast('✅', 'green', "Successfully created Secret Vault: #" + data.secret.id);
|
|
181
|
+
this.printSuggestedCommands({
|
|
182
|
+
"View vault details": `xy secret ${data.secret.id}`,
|
|
183
|
+
"Decrypt variables": `xy secret decrypt ${data.secret.id}`,
|
|
184
|
+
"Replace all variables": `xy secret update ${data.secret.id} --fields @secrets.json`,
|
|
185
|
+
"List all Secret Vaults": "xy secrets"
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async cmd_update_secret() {
|
|
190
|
+
await this.getMultiple();
|
|
191
|
+
var id = this.consumeSecretID();
|
|
192
|
+
var secret = await this.fetchSecret(id);
|
|
193
|
+
|
|
194
|
+
this.printSecretMutationSummary('Update Secret Vault', secret);
|
|
195
|
+
if (!Tools.numKeys(this.args)) return this.die("No updates specified for Secret Vault.");
|
|
196
|
+
|
|
197
|
+
// Validate first, so even an unsupported typo cannot cause a secret-shaped
|
|
198
|
+
// value to be echoed by the friendly update preview.
|
|
199
|
+
var params = this.prepareSecretParams(secret, this.args, false);
|
|
200
|
+
this.printUpdateData(this.redactSecretRequest(this.args));
|
|
201
|
+
params.id = id;
|
|
202
|
+
var data = await this.callStandardAPI('updateSecret', params, {
|
|
203
|
+
text: 'Updating Secret Vault...',
|
|
204
|
+
filterRequest: request => this.redactSecretRequest(request)
|
|
205
|
+
});
|
|
206
|
+
if (this.dry) return;
|
|
207
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
208
|
+
this.toast('✅', 'green', "Successfully updated Secret Vault: #" + id);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async cmd_decrypt_secret() {
|
|
212
|
+
// Fetching metadata is safe and does not create a secret_access audit event.
|
|
213
|
+
// Only the confirmed decrypt_secret API call below records the access.
|
|
214
|
+
await this.getMultiple();
|
|
215
|
+
var id = this.consumeSecretID();
|
|
216
|
+
var secret = await this.fetchSecret(id);
|
|
217
|
+
this.printSecretMutationSummary('Decrypt Secret Vault', secret);
|
|
218
|
+
|
|
219
|
+
if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
|
|
220
|
+
return this.die("Secret Vault decrypt --confirm must be true or false.");
|
|
221
|
+
}
|
|
222
|
+
if (this.args.confirm !== true) {
|
|
223
|
+
this.toast('⚠️', 'orange', "Please confirm decryption by adding '--confirm'. This access will be recorded in the xyOps activity log.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
delete this.args.confirm;
|
|
227
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Secret Vault decrypt option: --" + Tools.firstKey(this.args));
|
|
228
|
+
|
|
229
|
+
var data = await this.callStandardAPI('decryptSecret', { id: id }, {
|
|
230
|
+
text: 'Decrypting Secret Vault...',
|
|
231
|
+
filterResponse: response => ({
|
|
232
|
+
code: response.code,
|
|
233
|
+
fields: this.redactSecretFields(response.fields)
|
|
234
|
+
})
|
|
235
|
+
});
|
|
236
|
+
if (this.dry) return;
|
|
237
|
+
if (this.format.match(/json/)) return this.jsonOutput(data.fields);
|
|
238
|
+
|
|
239
|
+
if (!data.fields.length) {
|
|
240
|
+
println( "\n " + this.color('theme').bold('SECRET VARIABLES') );
|
|
241
|
+
println( " " + gray('(None)') );
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Values are deliberately not trimmed, wrapped, indented or boxed. This
|
|
246
|
+
// preserves multiline data exactly so it can be selected and copied.
|
|
247
|
+
data.fields.forEach( field => {
|
|
248
|
+
println( "\n" + this.color('orange').bold('SECRET VARIABLE: ' + field.name) );
|
|
249
|
+
var value = String(field.value);
|
|
250
|
+
print(value);
|
|
251
|
+
if (!value.endsWith("\n")) print("\n");
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
async cmd_delete_secret() {
|
|
256
|
+
await this.getMultiple();
|
|
257
|
+
var id = this.consumeSecretID();
|
|
258
|
+
var secret = await this.fetchSecret(id);
|
|
259
|
+
this.printSecretMutationSummary('Delete Secret Vault', secret);
|
|
260
|
+
|
|
261
|
+
if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) {
|
|
262
|
+
return this.die("Secret Vault delete --confirm must be true or false.");
|
|
263
|
+
}
|
|
264
|
+
if (this.args.confirm !== true) {
|
|
265
|
+
this.toast('⚠️', 'orange', "Please confirm the Secret Vault delete by adding '--confirm'.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
delete this.args.confirm;
|
|
269
|
+
if (Tools.numKeys(this.args)) return this.die("Unsupported Secret Vault delete option: --" + Tools.firstKey(this.args));
|
|
270
|
+
|
|
271
|
+
var data = await this.callStandardAPI('deleteSecret', { id: id }, {
|
|
272
|
+
text: 'Deleting Secret Vault...'
|
|
273
|
+
});
|
|
274
|
+
if (this.dry) return;
|
|
275
|
+
if (this.format.match(/json/)) return this.jsonOutput(data);
|
|
276
|
+
this.toast('✅', 'green', "Successfully deleted Secret Vault: #" + id);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async fetchSecret(id) {
|
|
280
|
+
// get_secret returns metadata only and cannot expose encrypted values.
|
|
281
|
+
cli.progress.start({ amount: 1, pct: false, text: gray('→ Loading Secret Vault...') });
|
|
282
|
+
var { err, data } = await this.api.getSecret({ id: id });
|
|
283
|
+
cli.progress.end();
|
|
284
|
+
if (err) this.die(err);
|
|
285
|
+
return data.secret;
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
consumeSecretID() {
|
|
289
|
+
// Mutations and decryption require an exact internal ID. A fuzzy title must
|
|
290
|
+
// never select the destination for either a destructive or sensitive action.
|
|
291
|
+
var id = this.args.other.shift() || this.args.id;
|
|
292
|
+
if (!id) this.die("Missing required Secret Vault ID argument.");
|
|
293
|
+
if (this.args.other.length) this.die("Unexpected argument after Secret Vault ID: " + this.args.other[0]);
|
|
294
|
+
if (this.args.id && (this.args.id !== id)) this.die("Conflicting Secret Vault ID arguments.");
|
|
295
|
+
if ((typeof(id) != 'string') || !id.match(/^[a-z0-9_]+$/)) this.die("Invalid Secret Vault ID: " + id);
|
|
296
|
+
delete this.args.id;
|
|
297
|
+
delete this.args.other;
|
|
298
|
+
return id;
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
prepareSecretParams(secret, input, creating) {
|
|
302
|
+
// Metadata updates are sparse. Assignment aliases append to their complete
|
|
303
|
+
// lists, while fields is accepted only as one complete replacement array.
|
|
304
|
+
var params = creating ? Tools.copyHash(secret, true) : {};
|
|
305
|
+
var allowed = [
|
|
306
|
+
'id', 'title', 'enabled', 'icon', 'notes', 'fields',
|
|
307
|
+
'events', 'event', 'categories', 'category', 'plugins', 'plugin',
|
|
308
|
+
'web_hooks', 'web_hook', 'hook'
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
Object.keys(input).forEach( key => {
|
|
312
|
+
if (key.includes('.')) this.die("Secret Vault options do not support dotted paths: --" + key);
|
|
313
|
+
if (!allowed.includes(key)) this.die("Unsupported Secret Vault option: --" + key);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
['id', 'title', 'enabled', 'icon', 'notes', 'fields'].forEach( key => {
|
|
317
|
+
if (key in input) params[key] = input[key];
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
var assignments = [
|
|
321
|
+
{ property: 'events', aliases: ['event'], collection: 'events', label: 'event' },
|
|
322
|
+
{ property: 'categories', aliases: ['category'], collection: 'categories', label: 'category' },
|
|
323
|
+
{ property: 'plugins', aliases: ['plugin'], collection: 'plugins', label: 'Plugin' },
|
|
324
|
+
{ property: 'web_hooks', aliases: ['web_hook', 'hook'], collection: 'web_hooks', label: 'web hook' }
|
|
325
|
+
];
|
|
326
|
+
assignments.forEach( assignment => {
|
|
327
|
+
var has_list = Object.prototype.hasOwnProperty.call(input, assignment.property);
|
|
328
|
+
var aliases = assignment.aliases.filter( alias => Object.prototype.hasOwnProperty.call(input, alias) );
|
|
329
|
+
if (!creating && !has_list && !aliases.length) return;
|
|
330
|
+
|
|
331
|
+
var value = has_list ? input[assignment.property] : (secret[assignment.property] || []);
|
|
332
|
+
var list = this.parseSecretList(value, assignment.property);
|
|
333
|
+
aliases.forEach( alias => {
|
|
334
|
+
list = list.concat(this.parseSecretList(input[alias], alias));
|
|
335
|
+
});
|
|
336
|
+
params[assignment.property] = Array.from(new Set(list));
|
|
337
|
+
this.validateSecretAssignments(params[assignment.property], assignment.collection, assignment.label);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if ('fields' in params) params.fields = this.validateSecretFields(params.fields);
|
|
341
|
+
if ('title' in params) {
|
|
342
|
+
if ((typeof(params.title) != 'string') || !params.title.trim()) this.die("Secret Vault title cannot be empty and must be a string.");
|
|
343
|
+
params.title = params.title.trim();
|
|
344
|
+
}
|
|
345
|
+
if ('enabled' in params) params.enabled = this.parseSecretBoolean(params.enabled, 'enabled');
|
|
346
|
+
['icon', 'notes'].forEach( key => {
|
|
347
|
+
if ((key in params) && (typeof(params[key]) != 'string')) this.die("Secret Vault " + key + " must be a string.");
|
|
348
|
+
});
|
|
349
|
+
if (('id' in params) && ((typeof(params.id) != 'string') || !params.id.match(/^[a-z0-9_]+$/))) {
|
|
350
|
+
this.die("Invalid Secret Vault ID: " + params.id);
|
|
351
|
+
}
|
|
352
|
+
return params;
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
parseSecretList(value, name) {
|
|
356
|
+
// Accept JSON arrays, repeated options and comma-separated strings. Empty
|
|
357
|
+
// input intentionally becomes an empty list so assignments can be cleared.
|
|
358
|
+
if ((value === undefined) || (value === null) || (value === '')) return [];
|
|
359
|
+
var list = [];
|
|
360
|
+
Tools.alwaysArray(value).forEach( item => {
|
|
361
|
+
if (typeof(item) != 'string') this.die("Secret Vault --" + name + " must be a comma-separated string or JSON array of strings.");
|
|
362
|
+
list = list.concat(item.split(/\s*,\s*/));
|
|
363
|
+
});
|
|
364
|
+
return Array.from(new Set(list.map( item => item.trim() ).filter( item => !!item )));
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
validateSecretAssignments(ids, collection, label) {
|
|
368
|
+
ids.forEach( id => {
|
|
369
|
+
if (!id.match(/^[a-z0-9_]+$/)) this.die("Invalid " + label + " ID for Secret Vault assignment: " + id);
|
|
370
|
+
if (!Tools.findObject(this[collection] || [], { id: id })) {
|
|
371
|
+
this.die("Could not find " + label + " for Secret Vault assignment: " + id);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
validateSecretFields(fields) {
|
|
377
|
+
// Values stay opaque strings. Validate structure and names without ever
|
|
378
|
+
// including a plaintext value in an error message.
|
|
379
|
+
if (!Array.isArray(fields)) this.die("Secret Vault fields must be a complete JSON array.");
|
|
380
|
+
var names = new Set();
|
|
381
|
+
return fields.map( (field, idx) => {
|
|
382
|
+
if (!Tools.isaHash(field)) this.die("Secret Vault fields." + idx + " must be a JSON object.");
|
|
383
|
+
var extra = Object.keys(field).find( key => !['name', 'value'].includes(key) );
|
|
384
|
+
if (extra) this.die("Unsupported property in Secret Vault fields." + idx + ': "' + extra + '".');
|
|
385
|
+
if ((typeof(field.name) != 'string') || !field.name.match(/^[A-Za-z_][A-Za-z0-9_]*$/) || field.name.match(Tools.MATCH_BAD_KEY)) {
|
|
386
|
+
this.die("Invalid variable name in Secret Vault fields." + idx + ".");
|
|
387
|
+
}
|
|
388
|
+
if (names.has(field.name)) this.die("Duplicate variable name in Secret Vault fields: " + field.name);
|
|
389
|
+
if (typeof(field.value) != 'string') this.die("Secret Vault fields." + idx + ".value must be a string.");
|
|
390
|
+
names.add(field.name);
|
|
391
|
+
return { name: field.name, value: field.value };
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
parseSecretBoolean(value, name) {
|
|
396
|
+
if ([true, 1, '1', 'true', 'yes', 'on'].includes(value)) return true;
|
|
397
|
+
if ([false, 0, '0', 'false', 'no', 'off'].includes(value)) return false;
|
|
398
|
+
this.die("Invalid boolean value for '" + name + "': " + value);
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
redactSecretFields(fields) {
|
|
402
|
+
// Invalid input can reach an update preview before structural validation.
|
|
403
|
+
// In that case, redact the complete value instead of inspecting or echoing it.
|
|
404
|
+
if (!Array.isArray(fields)) return '[REDACTED]';
|
|
405
|
+
return fields.map( field => ({
|
|
406
|
+
name: Tools.isaHash(field) && (typeof(field.name) == 'string') ? field.name : '(Invalid)',
|
|
407
|
+
value: '[REDACTED]'
|
|
408
|
+
}));
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
redactSecretRequest(request) {
|
|
412
|
+
// Construct the display object without first cloning plaintext field values.
|
|
413
|
+
var display = {};
|
|
414
|
+
Object.keys(request || {}).forEach( key => {
|
|
415
|
+
if (key == 'fields') display.fields = this.redactSecretFields(request.fields);
|
|
416
|
+
else display[key] = Tools.copyHash({ value: request[key] }, true).value;
|
|
417
|
+
});
|
|
418
|
+
return display;
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
printSecretMutationSummary(title, secret) {
|
|
422
|
+
this.printMutationSummary({
|
|
423
|
+
title: title,
|
|
424
|
+
rows: [
|
|
425
|
+
[ 'Vault ID', gray(secret.id) ],
|
|
426
|
+
[ 'Title', this.color('theme').bold(secret.title) ]
|
|
427
|
+
]
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
getNiceSecretAssignments(type, ids) {
|
|
432
|
+
ids = ids || [];
|
|
433
|
+
if (!ids.length) return gray('(None)');
|
|
434
|
+
var collection = this[type] || [];
|
|
435
|
+
return ids.map( id => {
|
|
436
|
+
var item = Tools.findObject(collection, { id: id });
|
|
437
|
+
return item ? item.title + gray(' (' + id + ')') : gray(id + ' (Not Found)');
|
|
438
|
+
}).join(', ');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
}; // module.exports
|