specshield 3.2.4 → 3.2.6
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 +398 -39
- package/package.json +1 -1
- package/src/commands/compare.js +15 -0
- package/src/commands/history.js +38 -0
- package/src/commands/share.js +27 -0
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,29 @@ 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:** `--remote` runs the diff on our servers but currently
|
|
256
|
+
> does **not** persist the result to your comparison history (saving is
|
|
257
|
+
> UI-only right now — see the
|
|
258
|
+
> [Comparison History](#comparison-history) section below). To save a
|
|
259
|
+
> comparison, run it from the dashboard at
|
|
260
|
+
> [specshield.io/account/compare](https://specshield.io/account/compare).
|
|
261
|
+
> A CLI-driven save is on the roadmap.
|
|
262
|
+
|
|
226
263
|
## Comparison History
|
|
227
264
|
|
|
228
|
-
|
|
265
|
+
> **Status: currently UI-only.** Comparison history is available in the
|
|
266
|
+
> dashboard at [specshield.io/account/history](https://specshield.io/account/history).
|
|
267
|
+
> The `specshield history` CLI command exists but currently exits with a
|
|
268
|
+
> "use the dashboard" message — a CLI-callable API endpoint is on the
|
|
269
|
+
> roadmap (the backend's `/me/*` routes require browser-session JWT auth,
|
|
270
|
+
> not the CLI's API key). The dashboard shows the same data and adds
|
|
271
|
+
> per-comparison drill-down.
|
|
272
|
+
|
|
273
|
+
Every comparison you run **in the dashboard's `/account/compare` page** is
|
|
274
|
+
saved to your account. Useful for tracking API drift over time across team
|
|
275
|
+
members + machines.
|
|
276
|
+
|
|
277
|
+
The CLI command (once the backend endpoint ships):
|
|
229
278
|
|
|
230
279
|
```bash
|
|
231
280
|
specshield history # last 20 comparisons
|
|
@@ -233,6 +282,8 @@ specshield history --limit 50
|
|
|
233
282
|
specshield history --json
|
|
234
283
|
```
|
|
235
284
|
|
|
285
|
+
Expected output (once enabled):
|
|
286
|
+
|
|
236
287
|
```
|
|
237
288
|
Your recent comparisons
|
|
238
289
|
─────────────────────────────────────────────────────
|
|
@@ -243,7 +294,15 @@ specshield history --json
|
|
|
243
294
|
|
|
244
295
|
## Share a Comparison
|
|
245
296
|
|
|
246
|
-
|
|
297
|
+
> **Status: currently UI-only.** Share links are generated from the
|
|
298
|
+
> dashboard — open any comparison at
|
|
299
|
+
> [specshield.io/account/history](https://specshield.io/account/history)
|
|
300
|
+
> and click the "Share" button. Same `/r/<token>` URL shape. The
|
|
301
|
+
> `specshield share` CLI command currently exits with a "use the dashboard"
|
|
302
|
+
> message; CLI integration ships when the backend exposes a CLI-callable
|
|
303
|
+
> share-link endpoint.
|
|
304
|
+
|
|
305
|
+
Once enabled, the CLI usage will be:
|
|
247
306
|
|
|
248
307
|
```bash
|
|
249
308
|
# Share an existing report by ID (from `specshield history`)
|
|
@@ -262,7 +321,8 @@ specshield share 482 --expires 30
|
|
|
262
321
|
Expires: 2026-06-16T12:34:56Z
|
|
263
322
|
```
|
|
264
323
|
|
|
265
|
-
Links use a 256-bit random token (unguessable by enumeration). Revoke any
|
|
324
|
+
Links use a 256-bit random token (unguessable by enumeration). Revoke any
|
|
325
|
+
time from the dashboard.
|
|
266
326
|
|
|
267
327
|
## GitHub App — PR Checks
|
|
268
328
|
|
|
@@ -486,63 +546,362 @@ specshield bdct can-i-deploy --org acme-store --service payment-service --versio
|
|
|
486
546
|
|
|
487
547
|
---
|
|
488
548
|
|
|
489
|
-
# `bdct capture from-har` —
|
|
549
|
+
# `bdct capture from-har` — turn real traffic into a consumer contract
|
|
550
|
+
|
|
551
|
+
**This is the feature that makes BDCT actually work for normal teams.**
|
|
552
|
+
Pact requires every consumer team to learn a DSL, instrument their tests,
|
|
553
|
+
and run a broker. SpecShield asks for something they already have: a
|
|
554
|
+
recording of an HTTP test run, in the universal HAR format that every
|
|
555
|
+
browser, every test framework, and every recording proxy already produces.
|
|
556
|
+
|
|
557
|
+
A hand-written consumer contract drifts the moment you forget a field. A
|
|
558
|
+
HAR-derived contract reflects what your code **actually called**, every
|
|
559
|
+
time you re-record. It's the difference between "we documented the
|
|
560
|
+
integration" and "we proved the integration."
|
|
490
561
|
|
|
491
|
-
|
|
562
|
+
### The 3-step mental model
|
|
492
563
|
|
|
493
|
-
|
|
564
|
+
```
|
|
565
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
566
|
+
│ 1. RECORD 2. CAPTURE 3. GATE │
|
|
567
|
+
│ │
|
|
568
|
+
│ Your existing test run → specshield bdct → publish-consumer │
|
|
569
|
+
│ → traffic.har capture from-har verify │
|
|
570
|
+
│ (browser / Playwright / → consumer-contract.yaml can-i-deploy │
|
|
571
|
+
│ Cypress / mitmproxy / │
|
|
572
|
+
│ Postman / k6 / …) ← language-agnostic ← every provider │
|
|
573
|
+
│ CLI does the OpenAPI PR re-checks │
|
|
574
|
+
│ inference every consumer │
|
|
575
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Step 1 already happens in your team — you're just saving the file. Step 2
|
|
579
|
+
is one CLI command. Step 3 is the BDCT registry you publish to.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Step 1 — record a HAR file
|
|
494
584
|
|
|
495
|
-
|
|
585
|
+
Pick the recorder that matches **how the consumer is exercised** in your
|
|
586
|
+
project. You almost never have to write recorder code yourself.
|
|
496
587
|
|
|
497
|
-
|
|
588
|
+
| Recorder | Use when the consumer is… | Effort |
|
|
589
|
+
|---|---|---|
|
|
590
|
+
| **[Browser DevTools](https://developer.chrome.com/docs/devtools/network/reference#save)** (Chrome / Firefox / Edge / Safari) | A frontend SPA, a Swagger UI, or any browser-driven app | Zero code |
|
|
591
|
+
| **[Playwright](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har)** with `recordHar` | Exercised by Playwright UI/integration tests | **One config field** |
|
|
592
|
+
| **[Cypress](https://github.com/NeuraLegion/cypress-har-generator)** + `cypress-har-generator` | Exercised by Cypress UI tests | One plugin |
|
|
593
|
+
| **[mitmproxy](https://docs.mitmproxy.org/stable/addons-examples/#har-dump)** | A backend service-to-service caller in a test env | One brew install, no consumer changes |
|
|
594
|
+
| **[Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting/exporting-postman-data/#exporting-as-har)** / Insomnia / Bruno | Manually exercised during exploratory testing | Right-click → export |
|
|
595
|
+
| **[k6](https://grafana.com/docs/k6/latest/results-output/real-time/json/#har)** load tests | Already running k6 against the provider | One `--out har=…` flag |
|
|
596
|
+
|
|
597
|
+
**Recipes follow** — keep reading for the exact commands per tool, then jump
|
|
598
|
+
to Step 2 (the capture command, which is identical regardless of recorder).
|
|
599
|
+
|
|
600
|
+
### 1a. Chrome / Firefox / Edge DevTools (zero code — best for frontend)
|
|
601
|
+
|
|
602
|
+
1. Open the page that calls your API in the browser.
|
|
603
|
+
2. Open DevTools (`F12` / `Cmd+Opt+I`) → **Network** tab.
|
|
604
|
+
3. Click the 🚫 button to clear, then reload the page and click through the
|
|
605
|
+
flows you want to record.
|
|
606
|
+
4. Right-click anywhere in the request list → **Save all as HAR with
|
|
607
|
+
content** → save as `traffic.har`.
|
|
608
|
+
|
|
609
|
+
That's the entire recording. The file is byte-compatible with everything
|
|
610
|
+
SpecShield does.
|
|
611
|
+
|
|
612
|
+
> **Safari note:** Safari's "Export HAR" is under the Develop menu →
|
|
613
|
+
> *Export…* in the Network tab; it produces the same format.
|
|
614
|
+
|
|
615
|
+
### 1b. Playwright (best for teams with UI tests)
|
|
616
|
+
|
|
617
|
+
Add one option to your `playwright.config.js`:
|
|
618
|
+
|
|
619
|
+
```js
|
|
620
|
+
// playwright.config.js
|
|
621
|
+
const path = require('path');
|
|
622
|
+
module.exports = {
|
|
623
|
+
testDir: './tests',
|
|
624
|
+
use: {
|
|
625
|
+
baseURL: 'https://staging.acme.com',
|
|
626
|
+
recordHar: { path: path.resolve(__dirname, 'traffic.har'), mode: 'full', content: 'embed' },
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Run your tests as usual:
|
|
498
632
|
|
|
499
633
|
```bash
|
|
500
|
-
|
|
501
|
-
#
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
634
|
+
npx playwright test
|
|
635
|
+
# → writes traffic.har on context close
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
The HAR will contain every HTTP call the browser made during the test —
|
|
639
|
+
HTML, JS, images, and the API JSON. The `--onlyJson` default in Step 2
|
|
640
|
+
filters out the noise automatically.
|
|
641
|
+
|
|
642
|
+
> Need the HAR file flushed **per test**? Create the context explicitly in
|
|
643
|
+
> your test and call `await context.close()` at the end. A runnable
|
|
644
|
+
> copy-paste starter lives in [`examples/playwright-har/`](examples/playwright-har/)
|
|
645
|
+
> of this repo — clone, `npm install`, `npx playwright install chromium`,
|
|
646
|
+
> `npm test` produces `traffic.har` against the public JSONPlaceholder
|
|
647
|
+
> sandbox so you can verify the toolchain works before pointing it at your
|
|
648
|
+
> own provider.
|
|
505
649
|
|
|
650
|
+
### 1c. Cypress
|
|
651
|
+
|
|
652
|
+
```bash
|
|
653
|
+
npm i -D @neuralegion/cypress-har-generator
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
```js
|
|
657
|
+
// cypress.config.js
|
|
658
|
+
const { install } = require('@neuralegion/cypress-har-generator');
|
|
659
|
+
module.exports = { e2e: { setupNodeEvents(on, config) { install(on); return config; } } };
|
|
660
|
+
|
|
661
|
+
// cypress/support/e2e.js
|
|
662
|
+
beforeEach(() => cy.recordHar());
|
|
663
|
+
afterEach(() => cy.saveHar({ outDir: './har-out' }));
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Run `npx cypress run` and the HAR for each spec lands in `har-out/`.
|
|
667
|
+
|
|
668
|
+
### 1d. mitmproxy (best for backend service-to-service)
|
|
669
|
+
|
|
670
|
+
When the consumer is a backend service (no browser), point it through
|
|
671
|
+
mitmproxy in your integration-test env. No consumer code change.
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
# Install once
|
|
675
|
+
brew install mitmproxy # macOS — also available via pip and apt
|
|
676
|
+
|
|
677
|
+
# Start the proxy and tell it to dump HAR
|
|
678
|
+
mitmdump --set hardump=traffic.har --listen-port 8080
|
|
679
|
+
|
|
680
|
+
# In another terminal: run your tests with HTTPS_PROXY pointing at it
|
|
681
|
+
HTTPS_PROXY=http://localhost:8080 HTTP_PROXY=http://localhost:8080 \
|
|
682
|
+
./run-integration-tests.sh
|
|
683
|
+
|
|
684
|
+
# Ctrl-C mitmdump when done — traffic.har is on disk
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
The `hardump` add-on is built into mitmproxy ≥ 9.0. For TLS hosts you'll
|
|
688
|
+
need to trust mitmproxy's CA in your test JVM/runtime (see the
|
|
689
|
+
[mitmproxy CA docs](https://docs.mitmproxy.org/stable/concepts-certificates/));
|
|
690
|
+
in many test environments it's a single env var
|
|
691
|
+
(`NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, or a
|
|
692
|
+
JVM `cacerts` import).
|
|
693
|
+
|
|
694
|
+
### 1e. Postman / Insomnia / Bruno
|
|
695
|
+
|
|
696
|
+
Run the request (or a whole collection runner). Right-click the response
|
|
697
|
+
→ **Save Response as HAR**. Repeat for each request you want included,
|
|
698
|
+
then either keep them as separate `.har` files (pass `--in` once per file
|
|
699
|
+
via a loop in CI) or merge them with any HAR-merge tool.
|
|
700
|
+
|
|
701
|
+
### 1f. k6 load test (bonus — recording while load-testing)
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
k6 run --out har=traffic.har script.js
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
k6 emits a HAR alongside its load metrics. Same file works with
|
|
708
|
+
`bdct capture from-har`.
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Step 2 — turn the HAR into a consumer contract
|
|
713
|
+
|
|
714
|
+
This is the one CLI command. The same command works regardless of which
|
|
715
|
+
recorder produced the HAR.
|
|
716
|
+
|
|
717
|
+
```bash
|
|
506
718
|
specshield bdct capture from-har \
|
|
507
|
-
--in
|
|
719
|
+
--in traffic.har \
|
|
508
720
|
--base-url https://api.acme.com \
|
|
509
|
-
--out
|
|
721
|
+
--out consumer-contract.yaml
|
|
510
722
|
```
|
|
511
723
|
|
|
724
|
+
Expected output:
|
|
725
|
+
|
|
512
726
|
```
|
|
513
|
-
✔ Wrote
|
|
727
|
+
✔ Wrote consumer-contract.yaml (4 endpoints, 6 ops from 23/41 entries)
|
|
514
728
|
```
|
|
515
729
|
|
|
516
|
-
|
|
730
|
+
The `23/41` summary means **23 entries** survived the filters (right host,
|
|
731
|
+
JSON body) out of **41 total** the recorder captured. Open the YAML to see
|
|
732
|
+
exactly what your code talks to — endpoint by endpoint, field by field.
|
|
517
733
|
|
|
518
|
-
|
|
519
|
-
|---|---|
|
|
520
|
-
| `--in <path>` | **Required.** Input HAR file (HTTP Archive 1.2). |
|
|
521
|
-
| `--out <path>` | Output file (default: stdout). |
|
|
522
|
-
| `--base-url <url>` | Keep only entries matching this URL prefix (e.g. `https://api.acme.com` or `https://api.acme.com/v1`). |
|
|
523
|
-
| `--method <verbs>` | Comma-separated methods to include (e.g. `GET,POST`). Default: all. |
|
|
524
|
-
| `--title <title>` | OpenAPI `info.title`. Default: `Captured consumer contract`. |
|
|
525
|
-
| `--version <ver>` | OpenAPI `info.version`. Default: `0.1.0`. |
|
|
526
|
-
| `--format <fmt>` | Output format: `yaml` (default) or `json`. |
|
|
527
|
-
| `--include-non-json` | Keep entries with non-JSON bodies (default: drop them — schemas can't be inferred). |
|
|
734
|
+
### All options
|
|
528
735
|
|
|
529
|
-
|
|
736
|
+
| Flag | Purpose | Default |
|
|
737
|
+
|---|---|---|
|
|
738
|
+
| `--in <path>` | **Required.** Input HAR file (HAR 1.2). | — |
|
|
739
|
+
| `--out <path>` | Output file. If omitted, writes to stdout. | stdout |
|
|
740
|
+
| `--base-url <url>` | Keep only entries matching this URL prefix. Critical when the consumer talks to multiple backends — see "multi-host" below. | (no filter — keeps every host) |
|
|
741
|
+
| `--method <verbs>` | Comma-separated methods to include, e.g. `GET,POST`. | all methods |
|
|
742
|
+
| `--title <title>` | OpenAPI `info.title`. | `Captured consumer contract` |
|
|
743
|
+
| `--version <ver>` | OpenAPI `info.version` — usually your git SHA in CI. | `0.1.0` |
|
|
744
|
+
| `--format <fmt>` | `yaml` or `json`. | `yaml` |
|
|
745
|
+
| `--include-non-json` | Keep entries with non-JSON bodies (HTML, images, binary). Schemas can't be inferred but the endpoints will appear. | off |
|
|
746
|
+
|
|
747
|
+
### What the engine actually does
|
|
748
|
+
|
|
749
|
+
For every HAR entry that passes the filters:
|
|
750
|
+
|
|
751
|
+
- **Path templating.** `/users/123/orders/abc-2026` becomes
|
|
752
|
+
`/users/{userId}/orders/{orderId}`. Parameter names come from the
|
|
753
|
+
preceding noun in the URL — *not* a generic `{id}` — so the resulting
|
|
754
|
+
OpenAPI matches the parameter names your provider likely already uses
|
|
755
|
+
(`{userId}`, `{orderId}`, `{accountId}`, …).
|
|
756
|
+
- **Per-status schema merging.** If three `GET /users/{userId}` samples
|
|
757
|
+
return slightly different shapes, the emitted schema is the *merger*:
|
|
758
|
+
fields seen in **every** sample stay `required`; fields seen in **some**
|
|
759
|
+
samples become optional; `integer` + `number` widens to `number`; a
|
|
760
|
+
type conflict falls back to `string`.
|
|
761
|
+
- **Format detection.** UUIDs, RFC 3339 date-times, and email addresses
|
|
762
|
+
get `format: uuid` / `format: date-time` / `format: email`.
|
|
763
|
+
- **Status-code coverage.** A 404 sample produces its own response schema
|
|
764
|
+
alongside the 200 — so the contract documents the error shapes your code
|
|
765
|
+
handles, not just the happy path.
|
|
766
|
+
- **JSON-only by default.** Entries with non-JSON bodies are dropped; the
|
|
767
|
+
HTML page load from a Playwright test never ends up in the contract.
|
|
530
768
|
|
|
531
|
-
|
|
532
|
-
- **Per-status schema merging:** if two recorded GETs return slightly different shapes, the emitted schema is the merger — fields seen in EVERY sample stay `required`, fields seen in only SOME become optional, integer + number widens to number.
|
|
533
|
-
- **Format detection:** UUIDs, RFC 3339 date-times, and emails get `format: uuid|date-time|email`.
|
|
534
|
-
- **JSON-only by default:** non-JSON bodies are dropped (schemas can't be inferred from binary/HTML); override with `--include-non-json` for debugging.
|
|
769
|
+
---
|
|
535
770
|
|
|
536
|
-
|
|
771
|
+
## Step 3 — publish + gate
|
|
537
772
|
|
|
538
773
|
```bash
|
|
774
|
+
# 1. Publish the captured contract (run on every consumer PR)
|
|
539
775
|
specshield bdct publish-consumer \
|
|
540
|
-
--org
|
|
541
|
-
--
|
|
542
|
-
--
|
|
543
|
-
--
|
|
776
|
+
--org acme-store \
|
|
777
|
+
--consumer checkout-ui \
|
|
778
|
+
--provider payment-service \
|
|
779
|
+
--version "$GIT_SHA" \
|
|
780
|
+
--format OPENAPI \
|
|
781
|
+
--contract consumer-contract.yaml
|
|
782
|
+
|
|
783
|
+
# 2. Gate the deploy (run before promoting the consumer)
|
|
784
|
+
specshield bdct can-i-deploy \
|
|
785
|
+
--org acme-store \
|
|
786
|
+
--service checkout-ui --version "$GIT_SHA" --env staging
|
|
787
|
+
# exit 0 = safe; exit 1 = a verification says NO and the deploy is blocked
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
Full BDCT command reference is in the [Bi-Directional Contract Testing](#bi-directional-contract-testing-bdct)
|
|
791
|
+
section above.
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
## Operational concerns (read this before going live)
|
|
796
|
+
|
|
797
|
+
### Scrubbing auth tokens, cookies, and PII
|
|
798
|
+
|
|
799
|
+
A raw HAR can contain `Authorization` headers, session cookies, and real
|
|
800
|
+
PII in request/response bodies. **Scrub before publishing.** Two patterns:
|
|
801
|
+
|
|
802
|
+
**Quick scrub with jq** (zero deps if you already have jq):
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
jq 'del(.. | .headers? | .[]? | select(.name | ascii_downcase | IN("authorization","cookie","set-cookie","x-api-key")))' \
|
|
806
|
+
traffic.har > scrubbed.har
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**Heavier scrub via `har-sanitizer`** (npm tool — supports body redaction
|
|
810
|
+
patterns, allowlists, etc.):
|
|
811
|
+
|
|
812
|
+
```bash
|
|
813
|
+
npx har-sanitizer --input traffic.har --output scrubbed.har --scrub-words 'email,ssn,creditCard'
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
The contract that `bdct capture from-har` emits only carries field *names
|
|
817
|
+
and types* — not values — so once the HAR is scrubbed of secrets, the
|
|
818
|
+
published contract is safe to share with the provider team.
|
|
819
|
+
|
|
820
|
+
### Multi-host filtering (consumer talks to several backends)
|
|
821
|
+
|
|
822
|
+
`--base-url` is a prefix match, so it doubles as a host filter:
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
# Keep only calls to the payment service
|
|
826
|
+
specshield bdct capture from-har \
|
|
827
|
+
--in traffic.har --base-url https://payment.acme.com \
|
|
828
|
+
--out contracts/checkout-ui-payment.yaml
|
|
829
|
+
|
|
830
|
+
# Same HAR, different provider — keep only calls to inventory
|
|
831
|
+
specshield bdct capture from-har \
|
|
832
|
+
--in traffic.har --base-url https://inventory.acme.com \
|
|
833
|
+
--out contracts/checkout-ui-inventory.yaml
|
|
544
834
|
```
|
|
545
835
|
|
|
836
|
+
One test run → one HAR → N consumer contracts, one per provider.
|
|
837
|
+
|
|
838
|
+
### Keeping contracts fresh in CI
|
|
839
|
+
|
|
840
|
+
The whole pattern is "record from your existing tests, publish from CI." A
|
|
841
|
+
typical `.github/workflows/contract-test.yml`:
|
|
842
|
+
|
|
843
|
+
```yaml
|
|
844
|
+
- name: Run integration tests (produces traffic.har)
|
|
845
|
+
run: npx playwright test # or ./mvnw verify, or whatever you use
|
|
846
|
+
|
|
847
|
+
- name: HAR → consumer contract
|
|
848
|
+
run: |
|
|
849
|
+
specshield bdct capture from-har \
|
|
850
|
+
--in traffic.har \
|
|
851
|
+
--base-url ${{ vars.PROVIDER_BASE_URL }} \
|
|
852
|
+
--out consumer-contract.yaml
|
|
853
|
+
|
|
854
|
+
- name: Publish contract
|
|
855
|
+
env:
|
|
856
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
857
|
+
run: |
|
|
858
|
+
specshield bdct publish-consumer \
|
|
859
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
860
|
+
--consumer ${{ vars.SERVICE_NAME }} \
|
|
861
|
+
--provider ${{ vars.PROVIDER_NAME }} \
|
|
862
|
+
--version ${{ github.sha }} \
|
|
863
|
+
--format OPENAPI \
|
|
864
|
+
--contract consumer-contract.yaml
|
|
865
|
+
|
|
866
|
+
- name: Gate the deploy
|
|
867
|
+
env:
|
|
868
|
+
SPECSHIELD_API_KEY: ${{ secrets.SPECSHIELD_API_KEY }}
|
|
869
|
+
run: |
|
|
870
|
+
specshield bdct can-i-deploy \
|
|
871
|
+
--org ${{ vars.SPECSHIELD_ORG }} \
|
|
872
|
+
--service ${{ vars.SERVICE_NAME }} \
|
|
873
|
+
--version ${{ github.sha }} \
|
|
874
|
+
--env staging
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Identical pattern on GitLab, CircleCI, Jenkins — only the secret-injection
|
|
878
|
+
syntax changes.
|
|
879
|
+
|
|
880
|
+
### Common pitfalls
|
|
881
|
+
|
|
882
|
+
| Symptom | Cause | Fix |
|
|
883
|
+
|---|---|---|
|
|
884
|
+
| `0 endpoints, 0 ops from 0/N entries` | `--base-url` doesn't match any host in the HAR | Drop `--base-url` to see what hosts ARE in the HAR; re-run with the right prefix. |
|
|
885
|
+
| Contract is missing your POST | Recorder captured a 4xx for that POST | Fix the test so the POST succeeds, OR keep the 4xx (it'll document the error path). |
|
|
886
|
+
| Path got templated when you didn't want it to | A literal path segment looked like a numeric/UUID id | Path segments are templated when **multiple** entries share a prefix but differ on that segment. A single entry stays literal. |
|
|
887
|
+
| Playwright HAR file is empty / not written | `context.close()` didn't run | Create the context explicitly with `chromium.launch()` → `browser.newContext({ recordHar })` and `await context.close()` at the end of the test. |
|
|
888
|
+
| Backend rejects POST when mitmproxy is in the path | TLS cert not trusted by the consumer | See the mitmproxy CA-cert link above; one env var or one JKS import. |
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## Why this is the most valuable piece of the CLI
|
|
893
|
+
|
|
894
|
+
Every other contract-testing product on the market asks the consumer team
|
|
895
|
+
to **change their code** — adopt a DSL, instrument their tests, run a
|
|
896
|
+
broker. That tax is why most teams that "should" be doing contract testing
|
|
897
|
+
aren't.
|
|
898
|
+
|
|
899
|
+
`bdct capture from-har` removes the tax. Your team's existing test run is
|
|
900
|
+
already the contract; the CLI just converts the format. The HAR-capture →
|
|
901
|
+
publish → can-i-deploy loop is what turns "we have OpenAPI specs in a
|
|
902
|
+
folder" into "no provider PR merges if it would break a deployed consumer"
|
|
903
|
+
— with measurable enforcement, in one CI step, and no per-language SDK.
|
|
904
|
+
|
|
546
905
|
---
|
|
547
906
|
|
|
548
907
|
# `bdct verify-provider` — does your live provider actually match its spec?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.6",
|
|
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,21 @@ 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
|
+
// Honest note for --remote when the backend didn't return a history
|
|
101
|
+
// ID. The `/compare` endpoint runs the diff but does NOT persist to
|
|
102
|
+
// the user's comparison history when called with X-Api-Key auth —
|
|
103
|
+
// that's a UI-only path right now (see the share/history commands).
|
|
104
|
+
// When/if the backend starts returning historyId, this note auto-hides.
|
|
105
|
+
if ((options.remote || options.remoteUrl)
|
|
106
|
+
&& !result.historyId && !result.reportId && !result.id) {
|
|
107
|
+
const chalk = require('chalk');
|
|
108
|
+
process.stdout.write('\n' + chalk.gray(
|
|
109
|
+
' Note: --remote ran the diff on our servers but did NOT save it to your\n' +
|
|
110
|
+
' comparison history (history save is currently UI-only). For history, run\n' +
|
|
111
|
+
' the compare in the dashboard at https://specshield.io/account/compare.\n'
|
|
112
|
+
));
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
// Contextual signup nudge — runs only when the user isn't logged in,
|
package/src/commands/history.js
CHANGED
|
@@ -49,6 +49,17 @@ history
|
|
|
49
49
|
});
|
|
50
50
|
if (spinner) spinner.stop();
|
|
51
51
|
|
|
52
|
+
// The /me/* routes currently require JWT (browser session) auth — the
|
|
53
|
+
// CLI's X-Api-Key isn't accepted, so the backend redirects to /login
|
|
54
|
+
// and axios follows the 302 to the SPA's HTML shell. Detect that and
|
|
55
|
+
// tell the user where the feature really lives, instead of silently
|
|
56
|
+
// returning an empty list.
|
|
57
|
+
if (typeof response.data === 'string'
|
|
58
|
+
&& (response.data.includes('<!doctype') || response.data.includes('<html'))) {
|
|
59
|
+
printWebOnlyMessage(opts);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
const items = response.data?.content
|
|
53
64
|
|| response.data?.items
|
|
54
65
|
|| (Array.isArray(response.data) ? response.data : []);
|
|
@@ -107,4 +118,31 @@ function printSignupNudge() {
|
|
|
107
118
|
console.log('');
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Surfaced when the backend redirects our API-key request to /login (HTML
|
|
123
|
+
* response). The /me/compare-history endpoint is currently JWT-only — the
|
|
124
|
+
* web UI signs in via OAuth and uses a browser-session JWT. A CLI-callable
|
|
125
|
+
* API endpoint is on the roadmap; until then, the dashboard is the way.
|
|
126
|
+
*/
|
|
127
|
+
function printWebOnlyMessage(opts) {
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
process.stdout.write(JSON.stringify({
|
|
130
|
+
error: 'CLI_UNAVAILABLE',
|
|
131
|
+
message: 'Comparison history is currently UI-only. The CLI API endpoint is on the roadmap.',
|
|
132
|
+
dashboard: 'https://specshield.io/account/history',
|
|
133
|
+
}, null, 2) + '\n');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log(chalk.bold(' Comparison history is currently UI-only.'));
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(' View your comparisons at:');
|
|
140
|
+
console.log(' ' + chalk.cyan('https://specshield.io/account/history'));
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(chalk.gray(' CLI access is on the roadmap — needs a backend API endpoint that'));
|
|
143
|
+
console.log(chalk.gray(' accepts the CLI\'s X-Api-Key auth (currently `/me/*` routes require'));
|
|
144
|
+
console.log(chalk.gray(' the browser-session JWT). Until then, use the dashboard.'));
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
|
|
110
148
|
module.exports = history;
|
package/src/commands/share.js
CHANGED
|
@@ -50,6 +50,14 @@ share
|
|
|
50
50
|
process.exit(2);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// The share-link endpoint lives on `/me/*` which currently requires
|
|
54
|
+
// browser-session JWT auth — the CLI's X-Api-Key gets redirected to
|
|
55
|
+
// /login. Until the backend exposes a CLI-callable endpoint, point
|
|
56
|
+
// users at the dashboard rather than letting the request fail opaquely.
|
|
57
|
+
printWebOnlyMessage();
|
|
58
|
+
process.exit(2);
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line no-unreachable
|
|
53
61
|
const spinner = ora('Generating share link...').start();
|
|
54
62
|
try {
|
|
55
63
|
const headers = { 'X-Api-Key': apiKey, 'X-SpecShield-Client': 'cli' };
|
|
@@ -118,4 +126,23 @@ function printSignupNudge() {
|
|
|
118
126
|
console.log('');
|
|
119
127
|
}
|
|
120
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Surfaced because `/me/share-links` currently requires JWT (web sign-in).
|
|
131
|
+
* The CLI X-Api-Key gets redirected to /login. Until a CLI-callable endpoint
|
|
132
|
+
* exists, the dashboard is the way.
|
|
133
|
+
*/
|
|
134
|
+
function printWebOnlyMessage() {
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(chalk.bold(' Share links are currently UI-only.'));
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log(' Generate a share link at:');
|
|
139
|
+
console.log(' ' + chalk.cyan('https://specshield.io/account/history'));
|
|
140
|
+
console.log(' (open any comparison → click "Share").');
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(chalk.gray(' CLI access is on the roadmap — needs a backend API endpoint that'));
|
|
143
|
+
console.log(chalk.gray(' accepts the CLI\'s X-Api-Key auth (currently `/me/*` routes require'));
|
|
144
|
+
console.log(chalk.gray(' the browser-session JWT). Until then, use the dashboard.'));
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
module.exports = share;
|