specshield 3.2.5 → 3.2.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # SpecShield CLI changelog
2
2
 
3
+ ## 3.2.7 — 2026-06-12 — `history` + `share` now work from the CLI
4
+
5
+ `specshield history` and `specshield share` were previously "UI-only" stubs —
6
+ the backend's comparison-history and share-link endpoints lived on JWT-only
7
+ routes that rejected the CLI's API key. The backend now exposes API-key-callable
8
+ endpoints (`/api/compare-history`, `/api/share-links`) and `/compare` returns the
9
+ saved comparison's id, so the full CLI/CI flow works end to end.
10
+
11
+ ### Added
12
+
13
+ - **`specshield history`** lists your recent comparisons (id, breaking count,
14
+ date, `base → target`). Honors `--limit` and `--json`. Reads the new
15
+ `/api/compare-history` endpoint.
16
+ - **`specshield share <id>`** generates a public `/r/<token>` link for a saved
17
+ comparison via the new `/api/share-links` endpoint. `--expires <days>` sets an
18
+ expiry. `specshield share <base> <target>` still compares-and-shares in one step.
19
+ - **`compare --remote` now returns a `historyId`** when run with an API key —
20
+ feed it straight into `specshield share` (the CI "compare → share link in PR
21
+ comment" flow).
22
+
23
+ ### Changed
24
+
25
+ - `compare --remote`'s footer is accurate again: with an API key the comparison
26
+ is saved (no note); without one it notes the diff ran but wasn't saved.
27
+ - Removed the "currently UI-only" notices from `history` / `share` (and the
28
+ matching README callouts) now that both work.
29
+
30
+ ### Fixed
31
+
32
+ - `specshield share` validates the report id is a positive number and gives a
33
+ clear message (pointing at `specshield history`) instead of a server 400.
34
+
3
35
  ## 3.2.3 — 2026-05-28 — BDCT fidelity: HAR capture + provider conformance
4
36
 
5
37
  Two new `bdct` sub-commands that close the contract-fidelity gap PactFlow
package/README.md CHANGED
@@ -5,13 +5,40 @@
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue)](#license)
6
6
  [![node](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org)
7
7
 
8
+ ## Contents
9
+
10
+ **Get started**
11
+ - [Quick Start](#quick-start) · [`specshield init` wizard](#specshield-init--first-run-setup-wizard) · [Login & Authentication](#login--authentication)
12
+
13
+ **Why SpecShield**
14
+ - [The Problem](#the-problem) · [The Solution](#the-solution) · [See It in Action](#see-it-in-action) · [Use Cases](#use-cases)
15
+ - [Local vs Cloud](#local-vs-cloud) · [vs. Alternatives](#vs-alternatives) · [Pricing](#pricing)
16
+
17
+ **Commands — local & hosted compare**
18
+ - [Local Compare](#local-compare) · [Remote Compare](#remote-compare) · [Comparison History](#comparison-history) · [Share a Comparison](#share-a-comparison) · [GitHub App PR Checks](#github-app--pr-checks)
19
+
20
+ **Bi-Directional Contract Testing (BDCT)**
21
+ - [BDCT overview & full command reference](#bi-directional-contract-testing-bdct)
22
+ - [`bdct capture from-har` — HAR → consumer contract](#bdct-capture-from-har--turn-real-traffic-into-a-consumer-contract)
23
+ - [Step 1 — record a HAR](#step-1--record-a-har-file) · [Step 2 — generate contract](#step-2--turn-the-har-into-a-consumer-contract) · [Step 3 — publish + gate](#step-3--publish--gate) · [Operational concerns](#operational-concerns-read-this-before-going-live)
24
+ - [`bdct verify-provider` — spec-vs-production conformance](#bdct-verify-provider--does-your-live-provider-actually-match-its-spec)
25
+
26
+ **CI/CD**
27
+ - [GitHub Actions workflows](#cicd--github-actions)
28
+
29
+ **Reference**
30
+ - [Config File](#config-file) · [All Options](#all-options) · [Exit Codes](#exit-codes)
31
+
32
+ **Project**
33
+ - [License](#license) · [Support](#support)
34
+
8
35
  ---
9
36
 
10
37
  > **OpenAPI Diff · Breaking-Change Detection · Bi-Directional Contract Testing · `can-i-deploy` Gate · Pact-File Ingest · Live-Traffic Capture · Spec-vs-Production Conformance · GitHub PR Checks**
11
38
 
12
39
  ---
13
40
 
14
- ## Never ship a breaking change to your API consumers.
41
+ ## Never ship a breaking change to your API consumers
15
42
 
16
43
  **SpecShield** is the one CLI that does four things to keep your API safe:
17
44
 
@@ -215,7 +242,9 @@ specshield compare base.yaml target.yaml --ignore "DELETE /admin removed" --fail
215
242
 
216
243
  ## Remote Compare
217
244
 
218
- Sends your specs to SpecShield and stores results in your dashboard. Requires a free account.
245
+ Sends your specs to SpecShield's hosted diff engine. Useful for keeping the
246
+ heavy comparison off your CI runner and for cross-validating against the
247
+ exact engine the dashboard uses. Requires a free account.
219
248
 
220
249
  ```bash
221
250
  specshield compare base.yaml target.yaml --remote
@@ -223,9 +252,16 @@ specshield compare base.yaml target.yaml --remote --fail-on-breaking
223
252
  specshield compare base.yaml target.yaml --remote --json --output result.json
224
253
  ```
225
254
 
255
+ > **History note:** with an API key set (run `specshield login`, or pass
256
+ > `--api-key` / `SPECSHIELD_API_KEY`), `--remote` saves each comparison to
257
+ > your history and the response includes a `historyId` you can hand to
258
+ > `specshield share`. Without a key the diff still runs — it just isn't saved.
259
+
226
260
  ## Comparison History
227
261
 
228
- Every `specshield compare --remote` is saved to your account. List recent comparisons from any machine — useful for tracking API drift over time across CI pipelines + local runs.
262
+ Every comparison you run with `--remote` (and every one from the dashboard) is
263
+ saved to your account — useful for tracking API drift over time across team
264
+ members and machines. List them straight from the CLI:
229
265
 
230
266
  ```bash
231
267
  specshield history # last 20 comparisons
@@ -233,6 +269,12 @@ specshield history --limit 50
233
269
  specshield history --json
234
270
  ```
235
271
 
272
+ The dashboard at
273
+ [specshield.io/account/history](https://specshield.io/account/history) shows
274
+ the same data with per-comparison drill-down.
275
+
276
+ Example output:
277
+
236
278
  ```
237
279
  Your recent comparisons
238
280
  ─────────────────────────────────────────────────────
@@ -243,7 +285,13 @@ specshield history --json
243
285
 
244
286
  ## Share a Comparison
245
287
 
246
- Generate a public, tokenized URL for any comparison report. Great for Slack threads, PR comments, Jira tickets.
288
+ Generate a public `/r/<token>` link for any comparison paste it into Slack, a
289
+ GitHub PR comment, or a Jira ticket and anyone can view the diff without a
290
+ SpecShield account. You can also create links from the dashboard (open a
291
+ comparison at [specshield.io/account/history](https://specshield.io/account/history)
292
+ → "Share").
293
+
294
+ Usage:
247
295
 
248
296
  ```bash
249
297
  # Share an existing report by ID (from `specshield history`)
@@ -262,7 +310,8 @@ specshield share 482 --expires 30
262
310
  Expires: 2026-06-16T12:34:56Z
263
311
  ```
264
312
 
265
- Links use a 256-bit random token (unguessable by enumeration). Revoke any time from the dashboard.
313
+ Links use a 256-bit random token (unguessable by enumeration). Revoke any
314
+ time from the dashboard.
266
315
 
267
316
  ## GitHub App — PR Checks
268
317
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "description": "CLI for OpenAPI breaking change detection and bi-directional contract verification — with can-i-deploy gating, GitHub PR checks, and a first-run setup wizard.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -96,6 +96,19 @@ compare
96
96
  await fsExtra.outputFile(options.output, JSON.stringify(jsonOutput, null, 2));
97
97
  logger.info(`Results saved to ${options.output}`);
98
98
  }
99
+
100
+ // Note for an anonymous --remote: the diff ran but wasn't saved
101
+ // because no API key was sent. An authenticated --remote persists the
102
+ // comparison and the backend returns a historyId, so this note hides.
103
+ if ((options.remote || options.remoteUrl)
104
+ && !result.historyId && !result.reportId && !result.id) {
105
+ const chalk = require('chalk');
106
+ process.stdout.write('\n' + chalk.gray(
107
+ ' Note: --remote ran the diff but did NOT save it to your history\n' +
108
+ ' (no API key). Run "specshield login" (or set SPECSHIELD_API_KEY) to\n' +
109
+ ' save comparisons and unlock "specshield history" / "specshield share".\n'
110
+ ));
111
+ }
99
112
  }
100
113
 
101
114
  // Contextual signup nudge — runs only when the user isn't logged in,
@@ -41,8 +41,9 @@ history
41
41
  const spinner = opts.json ? null : ora('Fetching your comparison history...').start();
42
42
  try {
43
43
  const size = parseInt(opts.limit, 10) || 20;
44
- const response = await axios.get(`${opts.apiUrl}/me/compare-history`, {
45
- // Backend uses Spring Data Page conventions: page (0-indexed) + size.
44
+ const response = await axios.get(`${opts.apiUrl}/api/compare-history`, {
45
+ // Backend paginates with page (0-indexed) + size; the /api endpoint
46
+ // returns a PagedResponse envelope ({ content, page, size, ... }).
46
47
  params: { page: 0, size },
47
48
  headers: { 'X-Api-Key': apiKey, 'X-SpecShield-Client': 'cli' },
48
49
  timeout: 10000,
@@ -57,17 +57,17 @@ share
57
57
 
58
58
  let reportId;
59
59
  if (target) {
60
- // Inline-compare path: run a /compare with --remote semantics to
61
- // persist the result, then immediately share the resulting history
62
- // row. Two API calls but keeps the backend share endpoint single-purpose.
60
+ // Inline-compare path: run a /compare (which persists when called with
61
+ // a valid X-Api-Key), then immediately share the resulting history row.
62
+ // Two API calls but keeps the backend share endpoint single-purpose.
63
63
  spinner.text = 'Running remote comparison...';
64
64
  const baseSpec = await loadSpec(reportOrBase);
65
65
  const targetSpec = await loadSpec(target);
66
66
  const compareResp = await axios.post(`${opts.apiUrl}/compare`,
67
67
  { baseSpec, targetSpec },
68
68
  { headers, timeout: 30000 });
69
- // The /compare endpoint returns the diff; the persisted history ID is
70
- // on the response (added by CompareHistoryService).
69
+ // The /compare endpoint returns the diff plus the persisted row's
70
+ // historyId (present because we authenticated with X-Api-Key).
71
71
  reportId = compareResp.data?.historyId || compareResp.data?.reportId;
72
72
  if (!reportId) {
73
73
  throw new Error('Comparison succeeded but no history ID was returned — cannot create share link.');
@@ -77,8 +77,17 @@ share
77
77
  reportId = reportOrBase;
78
78
  }
79
79
 
80
- const response = await axios.post(`${opts.apiUrl}/me/share-links`,
81
- { reportId, expiresInDays },
80
+ // The backend's reportId is a numeric history id. Validate before the
81
+ // call so a typo gets a clear message instead of a 400 from the server.
82
+ const numericReportId = Number(reportId);
83
+ if (!Number.isInteger(numericReportId) || numericReportId <= 0) {
84
+ spinner.stop();
85
+ logger.error(`Invalid report ID "${reportId}". Expected a numeric comparison ID — see "specshield history".`);
86
+ process.exit(2);
87
+ }
88
+
89
+ const response = await axios.post(`${opts.apiUrl}/api/share-links`,
90
+ { reportId: numericReportId, expiresInDays },
82
91
  { headers, timeout: 15000 });
83
92
  spinner.stop();
84
93