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 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,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
- 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.
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
- Generate a public, tokenized URL for any comparison report. Great for Slack threads, PR comments, Jira tickets.
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 time from the dashboard.
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` — record real traffic, get a consumer contract
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
- **The Pact alternative for teams that don't want to adopt a DSL.**
562
+ ### The 3-step mental model
492
563
 
493
- `from-har` reads a [HAR file](https://en.wikipedia.org/wiki/HAR_(file_format)) (HTTP Archive — any browser, Cypress, Playwright, k6, Insomnia, or Charles Proxy can export one), filters to your provider's host, infers an OpenAPI 3.0 consumer-contract subset from what your tests actually called, and writes it to a file you can publish with `bdct publish-consumer`.
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
- Why this matters: a hand-written consumer subset can be wrong (you forget the `currency` field your code reads, and the provider can remove it without anyone noticing). A HAR recording **captures what your code actually does** — without the Pact DSL, language-agnostic, runs anywhere.
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
- ### Generate a contract from a recorded test run
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
- # Record a HAR (one of many ways)
501
- # - Chrome DevTools Network right-click "Save all as HAR"
502
- # - Playwright: page.on('request')... or use BROWSER_TOOLS_HAR
503
- # - Cypress: cy.intercept(...) + plugins like cypress-har-generator
504
- # - k6: k6 run --out har=run.har script.js
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 tests/checkout-run.har \
719
+ --in traffic.har \
508
720
  --base-url https://api.acme.com \
509
- --out contracts/checkout-ui-payment.yaml
721
+ --out consumer-contract.yaml
510
722
  ```
511
723
 
724
+ Expected output:
725
+
512
726
  ```
513
- ✔ Wrote contracts/checkout-ui-payment.yaml (2 endpoints, 3 ops from 17/24 entries)
727
+ ✔ Wrote consumer-contract.yaml (4 endpoints, 6 ops from 23/41 entries)
514
728
  ```
515
729
 
516
- ### Options
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
- | Flag | Purpose |
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
- ### What you get
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
- - **Path templating:** concrete paths like `/users/123/orders/abc-2026` are turned into `/users/{userId}/orders/{orderId}` (the param is named from the preceding noun, not a generic `{id}`).
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
- ### Use the captured contract in BDCT
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 acme-store --consumer checkout-ui \
541
- --provider payment-service --version 2.0.0 \
542
- --contract contracts/checkout-ui-payment.yaml \
543
- --format OPENAPI
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.4",
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": {
@@ -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,
@@ -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;
@@ -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;