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 +32 -0
- package/README.md +54 -5
- package/package.json +1 -1
- package/src/commands/compare.js +13 -0
- package/src/commands/history.js +3 -2
- package/src/commands/share.js +16 -7
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)
|
|
6
6
|
[](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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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": {
|
package/src/commands/compare.js
CHANGED
|
@@ -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,
|
package/src/commands/history.js
CHANGED
|
@@ -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}/
|
|
45
|
-
// Backend
|
|
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,
|
package/src/commands/share.js
CHANGED
|
@@ -57,17 +57,17 @@ share
|
|
|
57
57
|
|
|
58
58
|
let reportId;
|
|
59
59
|
if (target) {
|
|
60
|
-
// Inline-compare path: run a /compare
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
|
70
|
-
//
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|