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.
@@ -1,561 +0,0 @@
1
- 'use strict';
2
-
3
- const { Command } = require('commander');
4
- const chalk = require('chalk');
5
- const ora = require('ora');
6
- const path = require('path');
7
- const fsExtra = require('fs-extra');
8
- const logger = require('../utils/logger');
9
- const { getStoredApiKey } = require('../config/localConfig');
10
- const {
11
- publishContract,
12
- listContracts,
13
- getLatestContract,
14
- verifyContract,
15
- getVerificationHistory,
16
- canIDeploy,
17
- } = require('../api/contractsClient');
18
-
19
- // ─── Helpers ────────────────────────────────────────────────────────────────
20
-
21
- async function resolveApiToken(opts) {
22
- return opts.apiToken || process.env.SPECSHIELD_API_KEY || (await getStoredApiKey()) || null;
23
- }
24
-
25
- function requireToken(token) {
26
- if (!token) {
27
- logger.error('No API token found. Pass --api-token, set SPECSHIELD_API_KEY, or run: specshield login --api-key <KEY>');
28
- process.exit(2);
29
- }
30
- }
31
-
32
- function fmtDate(iso) {
33
- if (!iso) return chalk.gray('—');
34
- try {
35
- return new Date(iso).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', hour12: false })
36
- .replace(',', '');
37
- } catch { return iso; }
38
- }
39
-
40
- function statusBadge(status) {
41
- if (!status) return chalk.gray('—');
42
- const s = String(status).toUpperCase();
43
- if (s === 'PUBLISHED') return chalk.green(s);
44
- if (s === 'DEPRECATED') return chalk.yellow(s);
45
- return chalk.gray(s);
46
- }
47
-
48
- function verifyBadge(status) {
49
- if (!status) return chalk.gray('—');
50
- const s = String(status).toUpperCase();
51
- if (s === 'SUCCESS') return chalk.green(s);
52
- if (s === 'FAILED') return chalk.red(s);
53
- if (s === 'PENDING') return chalk.yellow(s);
54
- return chalk.gray(s);
55
- }
56
-
57
- function hr() {
58
- return chalk.gray(' ─────────────────────────────────────────────────────');
59
- }
60
-
61
- /** Simple padded column table */
62
- function printTable(headers, rows) {
63
- const widths = headers.map((h, i) =>
64
- Math.max(h.length, ...rows.map(r => stripAnsi(String(r[i] ?? '')).length))
65
- );
66
- const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(widths[i]))).join(' ');
67
- process.stdout.write('\n ' + headerLine + '\n');
68
- process.stdout.write(' ' + widths.map(w => '─'.repeat(w)).join(' ') + '\n');
69
- for (const row of rows) {
70
- const line = row.map((cell, i) => {
71
- const raw = String(cell ?? '');
72
- const pad = widths[i] - stripAnsi(raw).length;
73
- return raw + ' '.repeat(Math.max(0, pad));
74
- }).join(' ');
75
- process.stdout.write(' ' + line + '\n');
76
- }
77
- process.stdout.write('\n');
78
- }
79
-
80
- /** Strip ANSI escape codes for length measurement */
81
- function stripAnsi(str) {
82
- return str.replace(/\u001b\[[0-9;]*m/g, '');
83
- }
84
-
85
- // ─── Publish ─────────────────────────────────────────────────────────────────
86
-
87
- const publishCommand = new Command('publish')
88
- .description('Publish a consumer contract to the registry')
89
- .requiredOption('--file <path>', 'Path to contract JSON file')
90
- .option('--org <key>', 'Organization key (overrides file value)')
91
- .option('--consumer <key>', 'Consumer service key (overrides file value)')
92
- .option('--provider <key>', 'Provider service key (overrides file value)')
93
- .option('--consumer-version <ver>', 'Consumer version tag')
94
- .option('--contract-name <name>', 'Contract name (overrides file value)')
95
- .option('--tag <tag>', 'Tag / git branch')
96
- .option('--server <url>', 'SpecShield server URL')
97
- .option('--api-token <token>', 'API token (overrides env / stored config)')
98
- .action(async (opts) => {
99
- const token = await resolveApiToken(opts);
100
- requireToken(token);
101
-
102
- // Read and validate file
103
- const filePath = path.resolve(opts.file);
104
- if (!(await fsExtra.pathExists(filePath))) {
105
- logger.error(`Contract file not found: ${filePath}`);
106
- process.exit(2);
107
- }
108
-
109
- let contractDoc;
110
- try {
111
- const raw = await fsExtra.readFile(filePath, 'utf8');
112
- contractDoc = JSON.parse(raw);
113
- } catch (err) {
114
- logger.error(`Invalid JSON in contract file: ${err.message}`);
115
- process.exit(2);
116
- }
117
-
118
- // Basic schema validation
119
- if (!contractDoc.interactions || !Array.isArray(contractDoc.interactions)) {
120
- logger.error('Contract file must have an "interactions" array.');
121
- process.exit(2);
122
- }
123
-
124
- // Resolve metadata (CLI flags override file values)
125
- const orgKey = opts.org || contractDoc.orgKey || contractDoc.org;
126
- const consumerKey = opts.consumer || contractDoc.consumer?.name;
127
- const providerKey = opts.provider || contractDoc.provider?.name;
128
- const contractName = opts.contractName || contractDoc.contractName;
129
- const contractType = contractDoc.contractType || 'HTTP';
130
-
131
- const missing = [];
132
- if (!orgKey) missing.push('--org (or "org" in contract file)');
133
- if (!consumerKey) missing.push('--consumer (or consumer.name in contract file)');
134
- if (!providerKey) missing.push('--provider (or provider.name in contract file)');
135
- if (!contractName) missing.push('--contract-name (or contractName in contract file)');
136
- if (missing.length) {
137
- logger.error(`Missing required fields:\n ${missing.join('\n ')}`);
138
- process.exit(2);
139
- }
140
-
141
- const spinner = ora('Publishing contract...').start();
142
-
143
- try {
144
- const result = await publishContract(opts.server, token, {
145
- orgKey,
146
- consumerServiceKey: consumerKey,
147
- providerServiceKey: providerKey,
148
- consumerVersion: opts.consumerVersion || contractDoc.consumer?.version || null,
149
- contractName,
150
- contractType,
151
- gitBranch: opts.tag || null,
152
- verifierName: 'specshield-cli',
153
- contentJson: contractDoc,
154
- });
155
- spinner.stop();
156
-
157
- process.stdout.write('\n');
158
- process.stdout.write(chalk.green.bold(' ✔ Contract Published Successfully') + '\n');
159
- process.stdout.write(hr() + '\n');
160
- process.stdout.write(` Contract ID : ${chalk.cyan(result.contractId)}\n`);
161
- process.stdout.write(` Contract Name : ${chalk.white(contractName)}\n`);
162
- process.stdout.write(` Consumer : ${consumerKey}\n`);
163
- process.stdout.write(` Provider : ${providerKey}\n`);
164
- process.stdout.write(` Version : ${chalk.cyan(result.contractVersion)}\n`);
165
- process.stdout.write(` Status : ${statusBadge(result.status)}\n`);
166
- if (result.contentHash) {
167
- process.stdout.write(` Content Hash : ${chalk.gray(result.contentHash.substring(0, 16) + '...')}\n`);
168
- }
169
- process.stdout.write(` Published At : ${fmtDate(result.publishedAt)}\n`);
170
- process.stdout.write('\n');
171
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts verify --contract-id ${result.contractId} --base-url <URL>\n`));
172
- process.stdout.write('\n');
173
- } catch (err) {
174
- spinner.fail('Publish failed');
175
- logger.error(err.message);
176
- process.exit(1);
177
- }
178
- });
179
-
180
- // ─── List ─────────────────────────────────────────────────────────────────────
181
-
182
- const listCommand = new Command('list')
183
- .description('List contracts in the registry')
184
- .option('--consumer <key>', 'Filter by consumer service key')
185
- .option('--provider <key>', 'Filter by provider service key')
186
- .option('--org <key>', 'Filter by organization key')
187
- .option('--status <status>', 'Filter by status (PUBLISHED | DEPRECATED)')
188
- .option('--contract-name <name>', 'Filter by contract name')
189
- .option('--page <n>', 'Page number (0-based)', '0')
190
- .option('--size <n>', 'Page size', '20')
191
- .option('--json', 'Output raw JSON')
192
- .option('--server <url>', 'SpecShield server URL')
193
- .option('--api-token <token>', 'API token')
194
- .action(async (opts) => {
195
- const token = await resolveApiToken(opts);
196
- requireToken(token);
197
-
198
- const spinner = opts.json ? null : ora('Fetching contracts...').start();
199
-
200
- try {
201
- const page = await listContracts(opts.server, token, {
202
- org: opts.org,
203
- consumer: opts.consumer,
204
- provider: opts.provider,
205
- contractName: opts.contractName,
206
- status: opts.status,
207
- page: parseInt(opts.page, 10) || 0,
208
- size: parseInt(opts.size, 10) || 20,
209
- });
210
- if (spinner) spinner.stop();
211
-
212
- if (opts.json) {
213
- process.stdout.write(JSON.stringify(page, null, 2) + '\n');
214
- return;
215
- }
216
-
217
- const items = page.content || [];
218
- const total = page.totalElements ?? items.length;
219
-
220
- process.stdout.write('\n');
221
- process.stdout.write(chalk.bold(' SpecShield Contract Registry') + '\n');
222
- process.stdout.write(hr() + '\n');
223
- process.stdout.write(` Showing ${items.length} of ${total} contracts\n`);
224
-
225
- if (items.length === 0) {
226
- process.stdout.write(chalk.gray('\n No contracts found matching filters.\n\n'));
227
- return;
228
- }
229
-
230
- printTable(
231
- ['ID', 'Contract Name', 'Consumer', 'Provider', 'Ver', 'Status', 'Last Verify', 'Published'],
232
- items.map(c => [
233
- chalk.cyan(String(c.contractId)),
234
- c.contractName,
235
- c.consumerServiceKey,
236
- c.providerServiceKey,
237
- c.contractVersion,
238
- statusBadge(c.status),
239
- verifyBadge(c.lastVerificationStatus),
240
- fmtDate(c.publishedAt),
241
- ])
242
- );
243
-
244
- if (page.totalPages > 1) {
245
- const cur = (page.number ?? 0) + 1;
246
- process.stdout.write(chalk.gray(` Page ${cur} of ${page.totalPages} · Use --page and --size to navigate\n\n`));
247
- }
248
- } catch (err) {
249
- if (spinner) spinner.fail('List failed');
250
- logger.error(err.message);
251
- process.exit(1);
252
- }
253
- });
254
-
255
- // ─── Latest ───────────────────────────────────────────────────────────────────
256
-
257
- const latestCommand = new Command('latest')
258
- .description('Get the latest version of a contract')
259
- .option('--consumer <key>', 'Consumer service key')
260
- .option('--provider <key>', 'Provider service key')
261
- .option('--org <key>', 'Organization key')
262
- .option('--contract-name <name>', 'Contract name')
263
- .option('--json', 'Print full contract JSON')
264
- .option('--server <url>', 'SpecShield server URL')
265
- .option('--api-token <token>', 'API token')
266
- .action(async (opts) => {
267
- const token = await resolveApiToken(opts);
268
- requireToken(token);
269
-
270
- const spinner = opts.json ? null : ora('Fetching latest contract...').start();
271
-
272
- try {
273
- const c = await getLatestContract(opts.server, token, {
274
- org: opts.org,
275
- consumer: opts.consumer,
276
- provider: opts.provider,
277
- contractName: opts.contractName,
278
- });
279
- if (spinner) spinner.stop();
280
-
281
- if (opts.json) {
282
- process.stdout.write(JSON.stringify(c, null, 2) + '\n');
283
- return;
284
- }
285
-
286
- process.stdout.write('\n');
287
- process.stdout.write(chalk.bold(' Latest Contract') + '\n');
288
- process.stdout.write(hr() + '\n');
289
- process.stdout.write(` Contract ID : ${chalk.cyan(c.contractId)}\n`);
290
- process.stdout.write(` Contract Name : ${chalk.white(c.contractName)}\n`);
291
- process.stdout.write(` Consumer : ${c.consumerServiceKey}\n`);
292
- process.stdout.write(` Provider : ${c.providerServiceKey}\n`);
293
- process.stdout.write(` Version : ${chalk.cyan(c.contractVersion)}\n`);
294
- process.stdout.write(` Type : ${c.contractType || '—'}\n`);
295
- process.stdout.write(` Status : ${statusBadge(c.status)}\n`);
296
- if (c.contentHash) {
297
- process.stdout.write(` Content Hash : ${chalk.gray(c.contentHash.substring(0, 16) + '...')}\n`);
298
- }
299
- process.stdout.write(` Published At : ${fmtDate(c.publishedAt)}\n`);
300
-
301
- if (c.verificationHistory && c.verificationHistory.length) {
302
- const last = c.verificationHistory[0];
303
- process.stdout.write('\n');
304
- process.stdout.write(chalk.bold(' Last Verification') + '\n');
305
- process.stdout.write(hr() + '\n');
306
- process.stdout.write(` Verification ID : ${chalk.cyan(last.verificationId)}\n`);
307
- process.stdout.write(` Status : ${verifyBadge(last.verificationStatus)}\n`);
308
- process.stdout.write(` Environment : ${last.environment || '—'}\n`);
309
- process.stdout.write(` Provider Ver : ${last.providerVersion || '—'}\n`);
310
- process.stdout.write(` Completed At : ${fmtDate(last.completedAt)}\n`);
311
- }
312
-
313
- process.stdout.write('\n');
314
- process.stdout.write(chalk.gray(` ➜ Use --json to see full contract content\n`));
315
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts verify --contract-id ${c.contractId} --base-url <URL>\n`));
316
- process.stdout.write('\n');
317
- } catch (err) {
318
- if (spinner) spinner.fail('Fetch failed');
319
- logger.error(err.message);
320
- process.exit(1);
321
- }
322
- });
323
-
324
- // ─── Verify ───────────────────────────────────────────────────────────────────
325
-
326
- const verifyCommand = new Command('verify')
327
- .description('Verify a contract against a live provider')
328
- .requiredOption('--contract-id <id>', 'Contract ID to verify')
329
- .requiredOption('--base-url <url>', 'Provider base URL (e.g. http://localhost:8080)')
330
- .option('--provider-version <ver>', 'Provider version tag')
331
- .option('--env <environment>', 'Environment label (e.g. staging, qa)')
332
- .option('--mode <mode>', 'Verification mode: LIVE | REPLAY', 'LIVE')
333
- .option('--json', 'Output raw JSON')
334
- .option('--server <url>', 'SpecShield server URL')
335
- .option('--api-token <token>', 'API token')
336
- .action(async (opts) => {
337
- const token = await resolveApiToken(opts);
338
- requireToken(token);
339
-
340
- // Validate base URL
341
- try { new URL(opts.baseUrl); } catch {
342
- logger.error(`Invalid base URL: ${opts.baseUrl}`);
343
- process.exit(2);
344
- }
345
-
346
- const contractId = parseInt(opts.contractId, 10);
347
- if (isNaN(contractId)) {
348
- logger.error('--contract-id must be a number');
349
- process.exit(2);
350
- }
351
-
352
- const spinner = opts.json ? null : ora(`Verifying contract ${contractId}...`).start();
353
-
354
- try {
355
- const result = await verifyContract(opts.server, token, contractId, {
356
- baseUrl: opts.baseUrl.replace(/\/$/, ''),
357
- providerVersion: opts.providerVersion || null,
358
- verificationMode: opts.mode || 'LIVE',
359
- environment: opts.env || null,
360
- verifierName: 'specshield-cli',
361
- });
362
- if (spinner) spinner.stop();
363
-
364
- if (opts.json) {
365
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
366
- return;
367
- }
368
-
369
- const summary = result.resultSummary || result.summary || {};
370
- const total = summary.total ?? 0;
371
- const passed = summary.passed ?? 0;
372
- const failed = summary.failed ?? 0;
373
- const mismatches = result.mismatches || [];
374
- const success = result.verificationStatus === 'SUCCESS';
375
-
376
- process.stdout.write('\n');
377
- if (success) {
378
- process.stdout.write(chalk.green.bold(` ✔ Verification PASSED`) + chalk.gray(` (${passed}/${total} interactions)\n`));
379
- } else {
380
- process.stdout.write(chalk.red.bold(` ✖ Verification FAILED`) + chalk.gray(` (${passed}/${total} interactions passed, ${failed} failed)\n`));
381
- }
382
- process.stdout.write(hr() + '\n');
383
- process.stdout.write(` Verification ID : ${chalk.cyan(result.verificationId)}\n`);
384
- process.stdout.write(` Contract ID : ${chalk.cyan(contractId)}\n`);
385
- process.stdout.write(` Status : ${verifyBadge(result.verificationStatus)}\n`);
386
- process.stdout.write(` Started At : ${fmtDate(result.startedAt)}\n`);
387
- process.stdout.write(` Completed At : ${fmtDate(result.completedAt)}\n`);
388
-
389
- if (mismatches.length > 0) {
390
- process.stdout.write('\n');
391
- process.stdout.write(chalk.red.bold(' Mismatches') + '\n');
392
- process.stdout.write(hr() + '\n');
393
- for (const m of mismatches) {
394
- process.stdout.write(` ${chalk.red('●')} ${chalk.bold('[' + (m.interactionKey || '?') + ']')} ${chalk.yellow(m.mismatchType)} at ${chalk.gray(m.path || '$')}\n`);
395
- if (m.expectedValue !== null && m.expectedValue !== undefined) {
396
- process.stdout.write(` ${chalk.gray('expected:')} ${chalk.green(m.expectedValue)} ${chalk.gray('→')} ${chalk.red(m.actualValue ?? 'null')}\n`);
397
- }
398
- process.stdout.write(` ${chalk.gray(m.message || '')}\n`);
399
- }
400
- process.stdout.write('\n');
401
- }
402
-
403
- if (success) {
404
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts can-i-deploy --provider <NAME> --version <VER>\n`));
405
- } else {
406
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts history --contract-id ${contractId} to inspect past runs\n`));
407
- }
408
- process.stdout.write('\n');
409
-
410
- process.exit(success ? 0 : 1);
411
- } catch (err) {
412
- if (spinner) spinner.fail('Verification failed');
413
- logger.error(err.message);
414
- process.exit(2);
415
- }
416
- });
417
-
418
- // ─── History ──────────────────────────────────────────────────────────────────
419
-
420
- const historyCommand = new Command('history')
421
- .description('Show verification history for a contract')
422
- .requiredOption('--contract-id <id>', 'Contract ID')
423
- .option('--json', 'Output raw JSON')
424
- .option('--server <url>', 'SpecShield server URL')
425
- .option('--api-token <token>', 'API token')
426
- .action(async (opts) => {
427
- const token = await resolveApiToken(opts);
428
- requireToken(token);
429
-
430
- const contractId = parseInt(opts.contractId, 10);
431
- if (isNaN(contractId)) {
432
- logger.error('--contract-id must be a number');
433
- process.exit(2);
434
- }
435
-
436
- const spinner = opts.json ? null : ora('Fetching verification history...').start();
437
-
438
- try {
439
- const history = await getVerificationHistory(opts.server, token, contractId);
440
- if (spinner) spinner.stop();
441
-
442
- if (opts.json) {
443
- process.stdout.write(JSON.stringify(history, null, 2) + '\n');
444
- return;
445
- }
446
-
447
- const items = Array.isArray(history) ? history : (history.content || []);
448
-
449
- process.stdout.write('\n');
450
- process.stdout.write(chalk.bold(` Verification History — Contract ${contractId}`) + '\n');
451
- process.stdout.write(hr() + '\n');
452
-
453
- if (items.length === 0) {
454
- process.stdout.write(chalk.gray('\n No verifications found for this contract.\n\n'));
455
- return;
456
- }
457
-
458
- printTable(
459
- ['ID', 'Status', 'Environment', 'Provider Version', 'Mode', 'Completed At'],
460
- items.map(v => [
461
- chalk.cyan(String(v.verificationId ?? v.id ?? '—')),
462
- verifyBadge(v.verificationStatus),
463
- v.environment || '—',
464
- v.providerVersion || '—',
465
- v.verificationMode || '—',
466
- fmtDate(v.completedAt),
467
- ])
468
- );
469
-
470
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts verify --contract-id ${contractId} --base-url <URL> to re-verify\n\n`));
471
- } catch (err) {
472
- if (spinner) spinner.fail('Fetch failed');
473
- logger.error(err.message);
474
- process.exit(1);
475
- }
476
- });
477
-
478
- // ─── Can-I-Deploy ─────────────────────────────────────────────────────────────
479
-
480
- const canIDeployCommand = new Command('can-i-deploy')
481
- .description('Check if a provider version is safe to deploy')
482
- .requiredOption('--provider <key>', 'Provider service key')
483
- .requiredOption('--version <ver>', 'Provider version to check')
484
- .option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
485
- .option('--json', 'Output raw JSON')
486
- .option('--server <url>', 'SpecShield server URL')
487
- .option('--api-token <token>', 'API token')
488
- .action(async (opts) => {
489
- const token = await resolveApiToken(opts);
490
- requireToken(token);
491
-
492
- const spinner = opts.json ? null : ora(`Checking deployment safety for ${opts.provider}@${opts.version}...`).start();
493
-
494
- try {
495
- const results = await canIDeploy(opts.server, token, {
496
- provider: opts.provider,
497
- version: opts.version,
498
- environment: opts.env || null,
499
- });
500
- if (spinner) spinner.stop();
501
-
502
- if (opts.json) {
503
- process.stdout.write(JSON.stringify(results, null, 2) + '\n');
504
- return;
505
- }
506
-
507
- const items = Array.isArray(results) ? results : [results];
508
- const allAllowed = items.every(r => r.allowed);
509
- const envLabel = opts.env ? ` in ${opts.env}` : '';
510
-
511
- process.stdout.write('\n');
512
- if (allAllowed) {
513
- process.stdout.write(chalk.green.bold(' ✔ PASS') + chalk.white(`: ${opts.provider} v${opts.version} is deployable${envLabel}\n`));
514
- } else {
515
- process.stdout.write(chalk.red.bold(' ✖ FAIL') + chalk.white(`: ${opts.provider} v${opts.version} is NOT deployable${envLabel}\n`));
516
- }
517
- process.stdout.write(hr() + '\n');
518
-
519
- if (items.length > 0) {
520
- process.stdout.write('\n');
521
- process.stdout.write(chalk.bold(' Contract Decisions') + '\n');
522
- for (const r of items) {
523
- const icon = r.allowed ? chalk.green('✔') : chalk.red('✖');
524
- const status = r.verificationStatus
525
- ? ` — ${verifyBadge(r.verificationStatus)}`
526
- : '';
527
- process.stdout.write(` ${icon} Contract ID ${chalk.cyan(r.contractId)}${status}\n`);
528
- if (r.reason) {
529
- process.stdout.write(` ${chalk.gray(r.reason)}\n`);
530
- }
531
- }
532
- process.stdout.write('\n');
533
- }
534
-
535
- if (!allAllowed) {
536
- process.stdout.write(chalk.gray(` ➜ Run: specshield contracts verify --contract-id <ID> --base-url <URL>\n`));
537
- process.stdout.write(chalk.gray(` ➜ to verify pending contracts before deploying\n`));
538
- }
539
- process.stdout.write('\n');
540
-
541
- process.exit(allAllowed ? 0 : 1);
542
- } catch (err) {
543
- if (spinner) spinner.fail('Check failed');
544
- logger.error(err.message);
545
- process.exit(2);
546
- }
547
- });
548
-
549
- // ─── Parent contracts command ─────────────────────────────────────────────────
550
-
551
- const contracts = new Command('contracts')
552
- .description('Manage and verify consumer-driven contracts');
553
-
554
- contracts.addCommand(publishCommand);
555
- contracts.addCommand(listCommand);
556
- contracts.addCommand(latestCommand);
557
- contracts.addCommand(verifyCommand);
558
- contracts.addCommand(historyCommand);
559
- contracts.addCommand(canIDeployCommand);
560
-
561
- module.exports = contracts;