specshield 2.0.0 → 3.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 +11 -201
- package/package.json +7 -8
- package/src/api/bdctClient.js +28 -23
- package/src/cli.js +5 -3
- package/src/commands/bdct.js +81 -50
- package/src/api/contractsClient.js +0 -88
- package/src/commands/contracts.js +0 -561
package/src/commands/bdct.js
CHANGED
|
@@ -51,6 +51,26 @@ function hr() {
|
|
|
51
51
|
return chalk.gray(' ─────────────────────────────────────────────────────');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Flatten a verification result's mismatches.
|
|
56
|
+
* Current backend returns `resultJson` (JSON string of [{endpoint,status,mismatches:[...]}]).
|
|
57
|
+
* Tolerate older shapes that put a flat array on `issues` or `mismatches`.
|
|
58
|
+
*/
|
|
59
|
+
function flattenMismatches(result) {
|
|
60
|
+
if (!result) return [];
|
|
61
|
+
if (Array.isArray(result.issues)) return result.issues;
|
|
62
|
+
if (Array.isArray(result.mismatches)) return result.mismatches;
|
|
63
|
+
if (typeof result.resultJson === 'string' && result.resultJson.length > 0) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(result.resultJson);
|
|
66
|
+
if (Array.isArray(parsed)) {
|
|
67
|
+
return parsed.flatMap(ep => Array.isArray(ep.mismatches) ? ep.mismatches : []);
|
|
68
|
+
}
|
|
69
|
+
} catch { /* fall through */ }
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/** Strip ANSI escape codes for length measurement */
|
|
55
75
|
function stripAnsi(str) {
|
|
56
76
|
return str.replace(/\[[0-9;]*m/g, '');
|
|
@@ -82,8 +102,8 @@ const publishProviderCommand = new Command('publish-provider')
|
|
|
82
102
|
.requiredOption('--spec <path>', 'Path to provider spec file (YAML or JSON)')
|
|
83
103
|
.requiredOption('--provider <name>', 'Provider service name')
|
|
84
104
|
.requiredOption('--version <ver>', 'Provider version tag')
|
|
105
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
85
106
|
.option('--env <environment>', 'Environment label (e.g. staging, production)')
|
|
86
|
-
.option('--org <key>', 'Organization key')
|
|
87
107
|
.option('--branch <branch>', 'Git branch name')
|
|
88
108
|
.option('--json', 'Output raw JSON')
|
|
89
109
|
.option('--server <url>', 'SpecShield server URL')
|
|
@@ -110,12 +130,12 @@ const publishProviderCommand = new Command('publish-provider')
|
|
|
110
130
|
|
|
111
131
|
try {
|
|
112
132
|
const result = await publishProviderSpec(opts.server, token, {
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
orgKey: opts.org,
|
|
134
|
+
providerName: opts.provider,
|
|
135
|
+
version: opts.version,
|
|
115
136
|
specContent,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
branch: opts.branch || null,
|
|
137
|
+
environment: opts.env || null,
|
|
138
|
+
branch: opts.branch || null,
|
|
119
139
|
});
|
|
120
140
|
if (spinner) spinner.stop();
|
|
121
141
|
|
|
@@ -131,7 +151,11 @@ const publishProviderCommand = new Command('publish-provider')
|
|
|
131
151
|
process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
|
|
132
152
|
process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
|
|
133
153
|
if (opts.env) process.stdout.write(` Environment : ${opts.env}\n`);
|
|
134
|
-
|
|
154
|
+
const publishedAt = result.createdAt || result.publishedAt;
|
|
155
|
+
if (publishedAt) process.stdout.write(` Published At: ${fmtDate(publishedAt)}\n`);
|
|
156
|
+
if (typeof result.verificationsTriggered === 'number') {
|
|
157
|
+
process.stdout.write(` Verifications triggered: ${chalk.cyan(result.verificationsTriggered)}\n`);
|
|
158
|
+
}
|
|
135
159
|
process.stdout.write('\n');
|
|
136
160
|
} catch (err) {
|
|
137
161
|
if (spinner) spinner.fail('Publish failed');
|
|
@@ -148,7 +172,7 @@ const publishConsumerCommand = new Command('publish-consumer')
|
|
|
148
172
|
.requiredOption('--consumer <name>', 'Consumer service name')
|
|
149
173
|
.requiredOption('--provider <name>', 'Provider service name')
|
|
150
174
|
.requiredOption('--version <ver>', 'Consumer version tag')
|
|
151
|
-
.
|
|
175
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
152
176
|
.option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
|
|
153
177
|
.option('--json', 'Output raw JSON')
|
|
154
178
|
.option('--server <url>', 'SpecShield server URL')
|
|
@@ -175,12 +199,12 @@ const publishConsumerCommand = new Command('publish-consumer')
|
|
|
175
199
|
|
|
176
200
|
try {
|
|
177
201
|
const result = await publishConsumerContract(opts.server, token, {
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
orgKey: opts.org,
|
|
203
|
+
consumerName: opts.consumer,
|
|
204
|
+
providerName: opts.provider,
|
|
180
205
|
version: opts.version,
|
|
181
206
|
contractContent,
|
|
182
|
-
|
|
183
|
-
format: opts.format || 'OPENAPI',
|
|
207
|
+
contractFormat: opts.format || 'OPENAPI',
|
|
184
208
|
});
|
|
185
209
|
if (spinner) spinner.stop();
|
|
186
210
|
|
|
@@ -197,7 +221,11 @@ const publishConsumerCommand = new Command('publish-consumer')
|
|
|
197
221
|
process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
|
|
198
222
|
process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
|
|
199
223
|
process.stdout.write(` Format : ${opts.format || 'OPENAPI'}\n`);
|
|
200
|
-
|
|
224
|
+
const consumerPublishedAt = result.createdAt || result.publishedAt;
|
|
225
|
+
if (consumerPublishedAt) process.stdout.write(` Published At: ${fmtDate(consumerPublishedAt)}\n`);
|
|
226
|
+
if (typeof result.verificationsTriggered === 'number') {
|
|
227
|
+
process.stdout.write(` Verifications triggered: ${chalk.cyan(result.verificationsTriggered)}\n`);
|
|
228
|
+
}
|
|
201
229
|
process.stdout.write('\n');
|
|
202
230
|
process.stdout.write(chalk.gray(` ➜ Run: specshield bdct verify --consumer ${opts.consumer} --provider ${opts.provider}\n`));
|
|
203
231
|
process.stdout.write('\n');
|
|
@@ -214,10 +242,10 @@ const verifyCommand = new Command('verify')
|
|
|
214
242
|
.description('Verify consumer-provider contract compatibility')
|
|
215
243
|
.requiredOption('--consumer <name>', 'Consumer service name')
|
|
216
244
|
.requiredOption('--provider <name>', 'Provider service name')
|
|
217
|
-
.
|
|
218
|
-
.
|
|
245
|
+
.requiredOption('--consumer-version <ver>', 'Consumer version to verify')
|
|
246
|
+
.requiredOption('--provider-version <ver>', 'Provider version to verify against')
|
|
247
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
219
248
|
.option('--env <environment>', 'Environment label')
|
|
220
|
-
.option('--org <key>', 'Organization key')
|
|
221
249
|
.option('--json', 'Output raw JSON')
|
|
222
250
|
.option('--server <url>', 'SpecShield server URL')
|
|
223
251
|
.option('--api-token <token>', 'API token')
|
|
@@ -229,12 +257,12 @@ const verifyCommand = new Command('verify')
|
|
|
229
257
|
|
|
230
258
|
try {
|
|
231
259
|
const result = await verify(opts.server, token, {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
consumerVersion: opts.consumerVersion
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
260
|
+
orgKey: opts.org,
|
|
261
|
+
consumerName: opts.consumer,
|
|
262
|
+
consumerVersion: opts.consumerVersion,
|
|
263
|
+
providerName: opts.provider,
|
|
264
|
+
providerVersion: opts.providerVersion,
|
|
265
|
+
environment: opts.env || null,
|
|
238
266
|
});
|
|
239
267
|
if (spinner) spinner.stop();
|
|
240
268
|
|
|
@@ -262,14 +290,26 @@ const verifyCommand = new Command('verify')
|
|
|
262
290
|
process.stdout.write(` Verified At : ${fmtDate(result.verifiedAt || result.completedAt)}\n`);
|
|
263
291
|
}
|
|
264
292
|
|
|
265
|
-
|
|
293
|
+
// Backend returns resultJson as a JSON string of [{endpoint, status, mismatches:[...]}].
|
|
294
|
+
// Older shapes also accepted: top-level issues / mismatches arrays.
|
|
295
|
+
const issues = flattenMismatches(result);
|
|
266
296
|
if (issues.length > 0) {
|
|
267
297
|
process.stdout.write('\n');
|
|
268
298
|
process.stdout.write(chalk.red.bold(' Issues') + '\n');
|
|
269
299
|
process.stdout.write(hr() + '\n');
|
|
270
300
|
for (const issue of issues) {
|
|
271
|
-
|
|
272
|
-
|
|
301
|
+
const sev = (issue.severity || 'ERROR').toUpperCase();
|
|
302
|
+
const marker = sev === 'WARNING' ? chalk.yellow('⚠') : chalk.red('●');
|
|
303
|
+
const type = issue.type || issue.mismatchType || 'MISMATCH';
|
|
304
|
+
const loc = issue.field || issue.path || issue.endpoint || '$';
|
|
305
|
+
process.stdout.write(` ${marker} ${chalk.bold(type)} at ${chalk.gray(loc)}\n`);
|
|
306
|
+
if (issue.consumerExpects && issue.providerProvides) {
|
|
307
|
+
process.stdout.write(` ${chalk.gray(`consumer: ${issue.consumerExpects}, provider: ${issue.providerProvides}`)}\n`);
|
|
308
|
+
} else if (issue.consumerExpects) {
|
|
309
|
+
process.stdout.write(` ${chalk.gray(`expected: ${issue.consumerExpects}`)}\n`);
|
|
310
|
+
} else if (issue.message) {
|
|
311
|
+
process.stdout.write(` ${chalk.gray(issue.message)}\n`);
|
|
312
|
+
}
|
|
273
313
|
}
|
|
274
314
|
process.stdout.write('\n');
|
|
275
315
|
}
|
|
@@ -289,8 +329,8 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
289
329
|
.description('Check if a service version is safe to deploy')
|
|
290
330
|
.requiredOption('--service <name>', 'Service name (consumer or provider)')
|
|
291
331
|
.requiredOption('--version <ver>', 'Service version to check')
|
|
332
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
292
333
|
.option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
|
|
293
|
-
.option('--org <key>', 'Organization key')
|
|
294
334
|
.option('--json', 'Output raw JSON')
|
|
295
335
|
.option('--server <url>', 'SpecShield server URL')
|
|
296
336
|
.option('--api-token <token>', 'API token')
|
|
@@ -302,10 +342,10 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
302
342
|
|
|
303
343
|
try {
|
|
304
344
|
const result = await canIDeploy(opts.server, token, {
|
|
345
|
+
org: opts.org,
|
|
305
346
|
service: opts.service,
|
|
306
347
|
version: opts.version,
|
|
307
348
|
env: opts.env || null,
|
|
308
|
-
org: opts.org || null,
|
|
309
349
|
});
|
|
310
350
|
if (spinner) spinner.stop();
|
|
311
351
|
|
|
@@ -360,7 +400,7 @@ const canIDeployCommand = new Command('can-i-deploy')
|
|
|
360
400
|
|
|
361
401
|
const listCommand = new Command('list')
|
|
362
402
|
.description('List BDCT verification history')
|
|
363
|
-
.
|
|
403
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
364
404
|
.option('--consumer <name>', 'Filter by consumer service name')
|
|
365
405
|
.option('--provider <name>', 'Filter by provider service name')
|
|
366
406
|
.option('--env <environment>', 'Filter by environment')
|
|
@@ -433,7 +473,7 @@ const listCommand = new Command('list')
|
|
|
433
473
|
|
|
434
474
|
const matrixCommand = new Command('matrix')
|
|
435
475
|
.description('Show ASCII compatibility matrix of consumers vs providers')
|
|
436
|
-
.
|
|
476
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
437
477
|
.option('--env <environment>', 'Environment label')
|
|
438
478
|
.option('--json', 'Output raw JSON')
|
|
439
479
|
.option('--server <url>', 'SpecShield server URL')
|
|
@@ -456,7 +496,8 @@ const matrixCommand = new Command('matrix')
|
|
|
456
496
|
return;
|
|
457
497
|
}
|
|
458
498
|
|
|
459
|
-
//
|
|
499
|
+
// Backend matrix shape: { consumers: string[], providers: string[],
|
|
500
|
+
// cells: { "<consumer>__<provider>": "<STATUS>" } }
|
|
460
501
|
const consumers = matrix.consumers || [];
|
|
461
502
|
const providers = matrix.providers || [];
|
|
462
503
|
const cells = matrix.cells || {};
|
|
@@ -475,7 +516,7 @@ const matrixCommand = new Command('matrix')
|
|
|
475
516
|
const headers = ['Consumer \\ Provider', ...providers];
|
|
476
517
|
const rows = consumers.map(consumer => {
|
|
477
518
|
const providerCells = providers.map(provider => {
|
|
478
|
-
const status =
|
|
519
|
+
const status = cells[`${consumer}__${provider}`] || 'UNKNOWN';
|
|
479
520
|
return compatBadge(status);
|
|
480
521
|
});
|
|
481
522
|
return [chalk.white(consumer), ...providerCells];
|
|
@@ -497,11 +538,8 @@ const matrixCommand = new Command('matrix')
|
|
|
497
538
|
|
|
498
539
|
const listProvidersCommand = new Command('list-providers')
|
|
499
540
|
.description('List published provider specs')
|
|
500
|
-
.
|
|
541
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
501
542
|
.option('--provider <name>', 'Filter by provider service name')
|
|
502
|
-
.option('--env <environment>', 'Filter by environment')
|
|
503
|
-
.option('--page <n>', 'Page number (0-based)', '0')
|
|
504
|
-
.option('--size <n>', 'Page size', '20')
|
|
505
543
|
.option('--json', 'Output raw JSON')
|
|
506
544
|
.option('--server <url>', 'SpecShield server URL')
|
|
507
545
|
.option('--api-token <token>', 'API token')
|
|
@@ -515,9 +553,6 @@ const listProvidersCommand = new Command('list-providers')
|
|
|
515
553
|
const page = await listProviderSpecs(opts.server, token, {
|
|
516
554
|
org: opts.org,
|
|
517
555
|
provider: opts.provider,
|
|
518
|
-
env: opts.env,
|
|
519
|
-
page: parseInt(opts.page, 10) || 0,
|
|
520
|
-
size: parseInt(opts.size, 10) || 20,
|
|
521
556
|
});
|
|
522
557
|
if (spinner) spinner.stop();
|
|
523
558
|
|
|
@@ -543,11 +578,11 @@ const listProvidersCommand = new Command('list-providers')
|
|
|
543
578
|
['ID', 'Provider', 'Version', 'Environment', 'Branch', 'Published At'],
|
|
544
579
|
items.map(s => [
|
|
545
580
|
chalk.cyan(String(s.id ?? '—')),
|
|
546
|
-
s.
|
|
581
|
+
s.providerName || s.provider || '—',
|
|
547
582
|
chalk.cyan(s.version || '—'),
|
|
548
|
-
s.
|
|
583
|
+
s.environment || s.env || '—',
|
|
549
584
|
chalk.gray(s.branch || '—'),
|
|
550
|
-
fmtDate(s.publishedAt),
|
|
585
|
+
fmtDate(s.createdAt || s.publishedAt),
|
|
551
586
|
])
|
|
552
587
|
);
|
|
553
588
|
|
|
@@ -566,11 +601,9 @@ const listProvidersCommand = new Command('list-providers')
|
|
|
566
601
|
|
|
567
602
|
const listConsumersCommand = new Command('list-consumers')
|
|
568
603
|
.description('List published consumer contracts')
|
|
569
|
-
.
|
|
604
|
+
.requiredOption('--org <key>', 'Organization key')
|
|
570
605
|
.option('--consumer <name>', 'Filter by consumer service name')
|
|
571
606
|
.option('--provider <name>', 'Filter by provider service name')
|
|
572
|
-
.option('--page <n>', 'Page number (0-based)', '0')
|
|
573
|
-
.option('--size <n>', 'Page size', '20')
|
|
574
607
|
.option('--json', 'Output raw JSON')
|
|
575
608
|
.option('--server <url>', 'SpecShield server URL')
|
|
576
609
|
.option('--api-token <token>', 'API token')
|
|
@@ -585,8 +618,6 @@ const listConsumersCommand = new Command('list-consumers')
|
|
|
585
618
|
org: opts.org,
|
|
586
619
|
consumer: opts.consumer,
|
|
587
620
|
provider: opts.provider,
|
|
588
|
-
page: parseInt(opts.page, 10) || 0,
|
|
589
|
-
size: parseInt(opts.size, 10) || 20,
|
|
590
621
|
});
|
|
591
622
|
if (spinner) spinner.stop();
|
|
592
623
|
|
|
@@ -612,11 +643,11 @@ const listConsumersCommand = new Command('list-consumers')
|
|
|
612
643
|
['ID', 'Consumer', 'Provider', 'Version', 'Format', 'Published At'],
|
|
613
644
|
items.map(c => [
|
|
614
645
|
chalk.cyan(String(c.id ?? '—')),
|
|
615
|
-
c.
|
|
616
|
-
c.
|
|
646
|
+
c.consumerName || c.consumer || '—',
|
|
647
|
+
c.providerName || c.provider || '—',
|
|
617
648
|
chalk.cyan(c.version || '—'),
|
|
618
|
-
c.format || '—',
|
|
619
|
-
fmtDate(c.publishedAt),
|
|
649
|
+
c.contractFormat || c.format || '—',
|
|
650
|
+
fmtDate(c.createdAt || c.publishedAt),
|
|
620
651
|
])
|
|
621
652
|
);
|
|
622
653
|
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const axios = require('axios');
|
|
4
|
-
const { version } = require('../../package.json');
|
|
5
|
-
|
|
6
|
-
const DEFAULT_SERVER = 'https://specshield.io';
|
|
7
|
-
const TIMEOUT = 15000;
|
|
8
|
-
|
|
9
|
-
function buildClient(server, apiToken) {
|
|
10
|
-
const baseURL = (server || DEFAULT_SERVER).replace(/\/$/, '');
|
|
11
|
-
const headers = {
|
|
12
|
-
'Content-Type': 'application/json',
|
|
13
|
-
'X-SpecShield-Client': 'cli',
|
|
14
|
-
'X-SpecShield-Version': version,
|
|
15
|
-
};
|
|
16
|
-
if (apiToken) headers['X-Api-Key'] = apiToken;
|
|
17
|
-
return axios.create({ baseURL, timeout: TIMEOUT, headers });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function apiError(err) {
|
|
21
|
-
if (err.response) {
|
|
22
|
-
const data = err.response.data;
|
|
23
|
-
const msg = (data && (data.message || data.error || data.title))
|
|
24
|
-
|| `HTTP ${err.response.status}`;
|
|
25
|
-
return new Error(`API error (${err.response.status}): ${msg}`);
|
|
26
|
-
}
|
|
27
|
-
if (err.request) return new Error(`No response from server: ${err.message}`);
|
|
28
|
-
return new Error(`Request failed: ${err.message}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function publishContract(server, apiToken, payload) {
|
|
32
|
-
try {
|
|
33
|
-
const res = await buildClient(server, apiToken).post('/api/contracts/publish', payload);
|
|
34
|
-
return res.data;
|
|
35
|
-
} catch (err) { throw apiError(err); }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function listContracts(server, apiToken, { org, consumer, provider, contractName, status, page = 0, size = 20 } = {}) {
|
|
39
|
-
try {
|
|
40
|
-
const params = { page, size };
|
|
41
|
-
if (org) params.orgKey = org;
|
|
42
|
-
if (consumer) params.consumerServiceKey = consumer;
|
|
43
|
-
if (provider) params.providerServiceKey = provider;
|
|
44
|
-
if (contractName) params.contractName = contractName;
|
|
45
|
-
if (status) params.status = status;
|
|
46
|
-
const res = await buildClient(server, apiToken).get('/api/contracts', { params });
|
|
47
|
-
return res.data;
|
|
48
|
-
} catch (err) { throw apiError(err); }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function getLatestContract(server, apiToken, { org, consumer, provider, contractName } = {}) {
|
|
52
|
-
try {
|
|
53
|
-
const params = {};
|
|
54
|
-
if (org) params.orgKey = org;
|
|
55
|
-
if (consumer) params.consumerServiceKey = consumer;
|
|
56
|
-
if (provider) params.providerServiceKey = provider;
|
|
57
|
-
if (contractName) params.contractName = contractName;
|
|
58
|
-
const res = await buildClient(server, apiToken).get('/api/contracts/latest', { params });
|
|
59
|
-
return res.data;
|
|
60
|
-
} catch (err) { throw apiError(err); }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function verifyContract(server, apiToken, contractId, payload) {
|
|
64
|
-
try {
|
|
65
|
-
const res = await buildClient(server, apiToken).post(`/api/contracts/${contractId}/verify`, payload);
|
|
66
|
-
return res.data;
|
|
67
|
-
} catch (err) { throw apiError(err); }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function getVerificationHistory(server, apiToken, contractId) {
|
|
71
|
-
try {
|
|
72
|
-
const res = await buildClient(server, apiToken).get(`/api/contracts/${contractId}/verifications`);
|
|
73
|
-
return res.data;
|
|
74
|
-
} catch (err) { throw apiError(err); }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function canIDeploy(server, apiToken, { provider, version: ver, environment } = {}) {
|
|
78
|
-
try {
|
|
79
|
-
const params = { version: ver };
|
|
80
|
-
if (environment) params.environment = environment;
|
|
81
|
-
const res = await buildClient(server, apiToken).get(
|
|
82
|
-
`/api/providers/${encodeURIComponent(provider)}/can-i-deploy`, { params }
|
|
83
|
-
);
|
|
84
|
-
return res.data;
|
|
85
|
-
} catch (err) { throw apiError(err); }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
module.exports = { publishContract, listContracts, getLatestContract, verifyContract, getVerificationHistory, canIDeploy };
|