specshield 2.0.1 → 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.
@@ -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
- provider: opts.provider,
114
- version: opts.version,
133
+ orgKey: opts.org,
134
+ providerName: opts.provider,
135
+ version: opts.version,
115
136
  specContent,
116
- env: opts.env || null,
117
- orgKey: opts.org || null,
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
- if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
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
- .option('--org <key>', 'Organization key')
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
- consumer: opts.consumer,
179
- provider: opts.provider,
202
+ orgKey: opts.org,
203
+ consumerName: opts.consumer,
204
+ providerName: opts.provider,
180
205
  version: opts.version,
181
206
  contractContent,
182
- orgKey: opts.org || null,
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
- if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
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
- .option('--consumer-version <ver>', 'Consumer version to verify')
218
- .option('--provider-version <ver>', 'Provider version to verify against')
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
- consumer: opts.consumer,
233
- provider: opts.provider,
234
- consumerVersion: opts.consumerVersion || null,
235
- providerVersion: opts.providerVersion || null,
236
- env: opts.env || null,
237
- orgKey: opts.org || null,
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
- const issues = result.issues || result.mismatches || [];
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
- process.stdout.write(` ${chalk.red('●')} ${chalk.bold(issue.type || issue.mismatchType || 'MISMATCH')} at ${chalk.gray(issue.path || '$')}\n`);
272
- if (issue.message) process.stdout.write(` ${chalk.gray(issue.message)}\n`);
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
- .option('--org <key>', 'Filter by organization key')
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
- .option('--org <key>', 'Organization key')
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
- // Expected matrix shape: { consumers: string[], providers: string[], cells: { [consumer]: { [provider]: string } } }
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 = (cells[consumer] && cells[consumer][provider]) || 'UNKNOWN';
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
- .option('--org <key>', 'Filter by organization key')
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.provider || s.providerName || '—',
581
+ s.providerName || s.provider || '—',
547
582
  chalk.cyan(s.version || '—'),
548
- s.env || s.environment || '—',
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
- .option('--org <key>', 'Filter by organization key')
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.consumer || c.consumerName || '—',
616
- c.provider || c.providerName || '—',
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 };