[object Object]

← back to Cli Printing Press

feat(websniff): add auth session capture with domain binding and security hardening

91e04cca82c5701c5967646a860e8fac9ba57b96 · 2026-03-28 22:23:24 -0700 · Matt Van Horn

- AuthCapture struct with Headers, Cookies, Type, BoundDomain, ExpiresAt
- detectAuth() prefers explicitly captured auth over header inference
- --auth-from flag on sniff command injects pre-captured auth
- Domain binding: refuses cross-origin auth replay with clear error
- Cookie auth scoped to informational only (no template support)
- Test fixtures for Spotify-like authenticated capture scenarios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 91e04cca82c5701c5967646a860e8fac9ba57b96
Author: Matt Van Horn <mvanhorn@MacBook-Pro.local>
Date:   Sat Mar 28 22:23:24 2026 -0700

    feat(websniff): add auth session capture with domain binding and security hardening
    
    - AuthCapture struct with Headers, Cookies, Type, BoundDomain, ExpiresAt
    - detectAuth() prefers explicitly captured auth over header inference
    - --auth-from flag on sniff command injects pre-captured auth
    - Domain binding: refuses cross-origin auth replay with clear error
    - Cookie auth scoped to informational only (no template support)
    - Test fixtures for Spotify-like authenticated capture scenarios
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/cli/sniff.go                            | 85 +++++++++++++++++++++-
 internal/cli/sniff_test.go                       | 25 +++++++
 internal/spec/spec.go                            |  2 +-
 internal/websniff/capture.go                     | 73 +++++++++++++++++++
 internal/websniff/parser.go                      | 46 ++----------
 internal/websniff/specgen.go                     | 91 ++++++++++++++++++++++--
 internal/websniff/specgen_test.go                | 91 ++++++++++++++++++++++++
 internal/websniff/types.go                       |  9 +++
 testdata/sniff/sample-auth-capture-mismatch.json | 29 ++++++++
 testdata/sniff/sample-auth-capture.json          | 30 ++++++++
 10 files changed, 432 insertions(+), 49 deletions(-)

diff --git a/internal/cli/sniff.go b/internal/cli/sniff.go
index 14920cb6..4ae44ea6 100644
--- a/internal/cli/sniff.go
+++ b/internal/cli/sniff.go
@@ -2,6 +2,7 @@ package cli
 
 import (
 	"fmt"
+	"net/url"
 	"strings"
 
 	"github.com/mvanhorn/cli-printing-press/internal/websniff"
@@ -13,6 +14,7 @@ func newSniffCmd() *cobra.Command {
 	var outputPath string
 	var name string
 	var blocklist string
+	var authFrom string
 
 	cmd := &cobra.Command{
 		Use:   "sniff",
@@ -20,7 +22,23 @@ func newSniffCmd() *cobra.Command {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			websniff.SetAdditionalBlocklist(splitCSV(blocklist))
 
-			apiSpec, err := websniff.Analyze(harPath)
+			capture, err := websniff.LoadCapture(harPath)
+			if err != nil {
+				return fmt.Errorf("loading capture: %w", err)
+			}
+
+			if authFrom != "" {
+				authCapture, err := websniff.ParseEnriched(authFrom)
+				if err != nil {
+					return fmt.Errorf("reading auth capture: %w", err)
+				}
+				if err := validateAuthDomainBinding(authCapture, capture); err != nil {
+					return err
+				}
+				capture.Auth = authCapture.Auth
+			}
+
+			apiSpec, err := websniff.AnalyzeCapture(capture)
 			if err != nil {
 				return fmt.Errorf("analyzing capture: %w", err)
 			}
@@ -53,6 +71,7 @@ func newSniffCmd() *cobra.Command {
 	cmd.Flags().StringVar(&outputPath, "output", "", "Output path for generated spec YAML")
 	cmd.Flags().StringVar(&name, "name", "", "Override the auto-detected API name")
 	cmd.Flags().StringVar(&blocklist, "blocklist", "", "Comma-separated additional domains to filter")
+	cmd.Flags().StringVar(&authFrom, "auth-from", "", "Path to an enriched capture file to import auth from")
 	_ = cmd.MarkFlagRequired("har")
 
 	return cmd
@@ -75,3 +94,67 @@ func splitCSV(value string) []string {
 
 	return out
 }
+
+func validateAuthDomainBinding(authCapture *websniff.EnrichedCapture, targetCapture *websniff.EnrichedCapture) error {
+	if authCapture == nil || authCapture.Auth == nil || strings.TrimSpace(authCapture.Auth.BoundDomain) == "" {
+		return nil
+	}
+
+	targetDomain := captureDomain(targetCapture)
+	boundDomain := normalizeDomain(authCapture.Auth.BoundDomain)
+	if targetDomain == "" || boundDomain == "" {
+		return nil
+	}
+	if targetDomain == boundDomain || strings.HasSuffix(targetDomain, "."+boundDomain) {
+		return nil
+	}
+
+	return fmt.Errorf("auth captured for %s cannot be used with %s (domain mismatch)", authCapture.Auth.BoundDomain, targetDomain)
+}
+
+func captureDomain(capture *websniff.EnrichedCapture) string {
+	if capture == nil {
+		return ""
+	}
+
+	if capture.TargetURL != "" {
+		parsed, err := url.Parse(capture.TargetURL)
+		if err == nil && parsed.Hostname() != "" {
+			return normalizeDomain(parsed.Hostname())
+		}
+	}
+
+	baseURL := commonCaptureBaseURL(capture)
+	parsed, err := url.Parse(baseURL)
+	if err != nil {
+		return ""
+	}
+
+	return normalizeDomain(parsed.Hostname())
+}
+
+func commonCaptureBaseURL(capture *websniff.EnrichedCapture) string {
+	counts := make(map[string]int)
+	best := ""
+	bestCount := 0
+
+	for _, entry := range capture.Entries {
+		parsed, err := url.Parse(entry.URL)
+		if err != nil || parsed.Scheme == "" || parsed.Host == "" {
+			continue
+		}
+
+		base := parsed.Scheme + "://" + parsed.Host
+		counts[base]++
+		if counts[base] > bestCount {
+			best = base
+			bestCount = counts[base]
+		}
+	}
+
+	return best
+}
+
+func normalizeDomain(domain string) string {
+	return strings.ToLower(strings.TrimSpace(domain))
+}
diff --git a/internal/cli/sniff_test.go b/internal/cli/sniff_test.go
new file mode 100644
index 00000000..1ef6169b
--- /dev/null
+++ b/internal/cli/sniff_test.go
@@ -0,0 +1,25 @@
+package cli
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSniffCmdRejectsDomainMismatchOnAuthFrom(t *testing.T) {
+	t.Parallel()
+
+	cmd := newSniffCmd()
+	outputPath := filepath.Join(t.TempDir(), "spec.yaml")
+	cmd.SetArgs([]string{
+		"--har", filepath.Join("..", "..", "testdata", "sniff", "sample-enriched.json"),
+		"--auth-from", filepath.Join("..", "..", "testdata", "sniff", "sample-auth-capture-mismatch.json"),
+		"--output", outputPath,
+	})
+
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.EqualError(t, err, "auth captured for other.example.com cannot be used with hn.algolia.com (domain mismatch)")
+}
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index f1085fdd..7eb73cca 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -21,7 +21,7 @@ type APISpec struct {
 }
 
 type AuthConfig struct {
-	Type             string   `yaml:"type"` // api_key, oauth2, bearer_token, none
+	Type             string   `yaml:"type"` // api_key, oauth2, bearer_token, cookie, none
 	Header           string   `yaml:"header"`
 	Format           string   `yaml:"format"`
 	EnvVars          []string `yaml:"env_vars"`
diff --git a/internal/websniff/capture.go b/internal/websniff/capture.go
new file mode 100644
index 00000000..7635be74
--- /dev/null
+++ b/internal/websniff/capture.go
@@ -0,0 +1,73 @@
+package websniff
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+func LoadCapture(path string) (*EnrichedCapture, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("reading file: %w", err)
+	}
+
+	if bytes.Contains(data, []byte(`"log"`)) {
+		var har HAR
+		if err := json.Unmarshal(data, &har); err != nil {
+			return nil, fmt.Errorf("parsing har json: %w", err)
+		}
+
+		capture := &EnrichedCapture{
+			Entries: make([]EnrichedEntry, 0, len(har.Log.Entries)),
+		}
+		for _, entry := range har.Log.Entries {
+			capture.Entries = append(capture.Entries, convertHAREntry(entry))
+		}
+		if len(har.Log.Entries) > 0 {
+			capture.TargetURL = har.Log.Entries[0].Request.URL
+		}
+
+		return capture, nil
+	}
+
+	if bytes.Contains(data, []byte(`"target_url"`)) {
+		var capture EnrichedCapture
+		if err := json.Unmarshal(data, &capture); err != nil {
+			return nil, fmt.Errorf("parsing enriched json: %w", err)
+		}
+
+		return &capture, nil
+	}
+
+	return nil, fmt.Errorf("unknown capture format")
+}
+
+func WriteEnrichedCapture(capture *EnrichedCapture, outputPath string) error {
+	if capture == nil {
+		return fmt.Errorf("capture is required")
+	}
+
+	data, err := json.MarshalIndent(capture, "", "  ")
+	if err != nil {
+		return fmt.Errorf("marshaling enriched json: %w", err)
+	}
+
+	if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
+		return fmt.Errorf("creating output directory: %w", err)
+	}
+
+	file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
+	if err != nil {
+		return fmt.Errorf("opening output file: %w", err)
+	}
+	defer file.Close()
+
+	if _, err := file.Write(data); err != nil {
+		return fmt.Errorf("writing enriched json: %w", err)
+	}
+
+	return nil
+}
diff --git a/internal/websniff/parser.go b/internal/websniff/parser.go
index 7d1ba2f2..f025cc7c 100644
--- a/internal/websniff/parser.go
+++ b/internal/websniff/parser.go
@@ -1,7 +1,6 @@
 package websniff
 
 import (
-	"bytes"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -22,54 +21,21 @@ func ParseHAR(path string) (*HAR, error) {
 }
 
 func ParseEnriched(path string) (*EnrichedCapture, error) {
-	data, err := os.ReadFile(path)
+	capture, err := LoadCapture(path)
 	if err != nil {
-		return nil, fmt.Errorf("reading file: %w", err)
-	}
-
-	var capture EnrichedCapture
-	if err := json.Unmarshal(data, &capture); err != nil {
-		return nil, fmt.Errorf("parsing enriched json: %w", err)
+		return nil, err
 	}
 
-	return &capture, nil
+	return capture, nil
 }
 
 func ParseCapture(path string) ([]EnrichedEntry, string, error) {
-	data, err := os.ReadFile(path)
+	capture, err := LoadCapture(path)
 	if err != nil {
-		return nil, "", fmt.Errorf("reading file: %w", err)
-	}
-
-	if bytes.Contains(data, []byte(`"log"`)) {
-		var har HAR
-		if err := json.Unmarshal(data, &har); err != nil {
-			return nil, "", fmt.Errorf("parsing har json: %w", err)
-		}
-
-		entries := make([]EnrichedEntry, 0, len(har.Log.Entries))
-		for _, entry := range har.Log.Entries {
-			entries = append(entries, convertHAREntry(entry))
-		}
-
-		targetURL := ""
-		if len(har.Log.Entries) > 0 {
-			targetURL = har.Log.Entries[0].Request.URL
-		}
-
-		return entries, targetURL, nil
-	}
-
-	if bytes.Contains(data, []byte(`"target_url"`)) {
-		var capture EnrichedCapture
-		if err := json.Unmarshal(data, &capture); err != nil {
-			return nil, "", fmt.Errorf("parsing enriched json: %w", err)
-		}
-
-		return capture.Entries, capture.TargetURL, nil
+		return nil, "", err
 	}
 
-	return nil, "", fmt.Errorf("unknown capture format")
+	return capture.Entries, capture.TargetURL, nil
 }
 
 func convertHAREntry(entry HAREntry) EnrichedEntry {
diff --git a/internal/websniff/specgen.go b/internal/websniff/specgen.go
index bc0ddfa8..441cbb84 100644
--- a/internal/websniff/specgen.go
+++ b/internal/websniff/specgen.go
@@ -14,12 +14,20 @@ import (
 )
 
 func Analyze(capturePath string) (*spec.APISpec, error) {
-	entries, targetURL, err := ParseCapture(capturePath)
+	capture, err := LoadCapture(capturePath)
 	if err != nil {
 		return nil, err
 	}
 
-	apiEntries, _ := ClassifyEntries(entries)
+	return AnalyzeCapture(capture)
+}
+
+func AnalyzeCapture(capture *EnrichedCapture) (*spec.APISpec, error) {
+	if capture == nil {
+		return nil, fmt.Errorf("capture is required")
+	}
+
+	apiEntries, _ := ClassifyEntries(capture.Entries)
 	groups := DeduplicateEndpoints(apiEntries)
 
 	resources := make(map[string]spec.Resource)
@@ -49,10 +57,10 @@ func Analyze(capturePath string) (*spec.APISpec, error) {
 
 	baseURL := mostCommonBaseURL(apiEntries)
 	if baseURL == "" {
-		baseURL = normalizeBaseURL(targetURL)
+		baseURL = normalizeBaseURL(capture.TargetURL)
 	}
 
-	nameSource := targetURL
+	nameSource := capture.TargetURL
 	if nameSource == "" {
 		nameSource = baseURL
 	}
@@ -63,7 +71,7 @@ func Analyze(capturePath string) (*spec.APISpec, error) {
 		Description: fmt.Sprintf("Discovered API spec for %s", name),
 		Version:     "0.1.0",
 		BaseURL:     baseURL,
-		Auth:        detectAuth(apiEntries, name),
+		Auth:        detectAuth(capture, apiEntries, name),
 		Config: spec.ConfigSpec{
 			Format: "toml",
 			Path:   fmt.Sprintf("~/.config/%s-pp-cli/config.toml", name),
@@ -130,7 +138,7 @@ func buildEndpoint(group EndpointGroup) spec.Endpoint {
 
 	body := inferRequestBody(group.Entries)
 	params := inferURLParams(group.Entries, group.NormalizedPath)
-	auth := detectAuth(group.Entries, "")
+	auth := detectAuth(nil, group.Entries, "")
 	if auth.Type == "api_key" && strings.EqualFold(auth.In, "query") && auth.Header != "" {
 		params = filterAuthQueryParam(params, auth.Header)
 	}
@@ -232,8 +240,15 @@ func inferURLParams(entries []EnrichedEntry, normalizedPath string) []spec.Param
 	return params
 }
 
-func detectAuth(entries []EnrichedEntry, name string) spec.AuthConfig {
+func detectAuth(capture *EnrichedCapture, entries []EnrichedEntry, name string) spec.AuthConfig {
 	envPrefix := strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
+	if capture != nil && capture.Auth != nil {
+		auth := detectCapturedAuth(capture.Auth, envPrefix)
+		if auth.Type != "" {
+			return auth
+		}
+	}
+
 	for _, entry := range entries {
 		for headerName, value := range entry.RequestHeaders {
 			lowerHeader := strings.ToLower(headerName)
@@ -274,6 +289,68 @@ func detectAuth(entries []EnrichedEntry, name string) spec.AuthConfig {
 	return spec.AuthConfig{Type: "none"}
 }
 
+func detectCapturedAuth(capture *AuthCapture, envPrefix string) spec.AuthConfig {
+	if capture == nil {
+		return spec.AuthConfig{}
+	}
+
+	captureType := strings.ToLower(strings.TrimSpace(capture.Type))
+	switch {
+	case len(capture.Headers) > 0:
+		switch captureType {
+		case "bearer":
+			return spec.AuthConfig{
+				Type:    "bearer_token",
+				Header:  "Authorization",
+				EnvVars: envVarsOrNil(envPrefix, "TOKEN"),
+			}
+		case "api_key":
+			headerName := firstAuthHeader(capture.Headers)
+			if headerName == "" {
+				headerName = "X-API-Key"
+			}
+			return spec.AuthConfig{
+				Type:    "api_key",
+				Header:  headerName,
+				In:      "header",
+				EnvVars: envVarsOrNil(envPrefix, "API_KEY"),
+			}
+		case "cookie":
+			return spec.AuthConfig{
+				Type:   "cookie",
+				Header: "Cookie",
+				In:     "cookie",
+				Format: "informational only; no template support",
+			}
+		}
+	case captureType == "cookie" && len(capture.Cookies) > 0:
+		return spec.AuthConfig{
+			Type:   "cookie",
+			Header: "Cookie",
+			In:     "cookie",
+			Format: "informational only; no template support",
+		}
+	}
+
+	return spec.AuthConfig{}
+}
+
+func firstAuthHeader(headers map[string]string) string {
+	for _, preferred := range []string{"Authorization", "X-API-Key", "Api-Key", "X-Auth-Token"} {
+		for name := range headers {
+			if strings.EqualFold(name, preferred) {
+				return name
+			}
+		}
+	}
+
+	for name := range headers {
+		return name
+	}
+
+	return ""
+}
+
 func envVarsOrNil(prefix string, suffix string) []string {
 	if prefix == "" {
 		return nil
diff --git a/internal/websniff/specgen_test.go b/internal/websniff/specgen_test.go
index 797597b6..c3cccd08 100644
--- a/internal/websniff/specgen_test.go
+++ b/internal/websniff/specgen_test.go
@@ -110,3 +110,94 @@ func TestDeriveNameFromURL(t *testing.T) {
 		})
 	}
 }
+
+func TestAnalyzeCapture_UsesCapturedBearerAuth(t *testing.T) {
+	t.Parallel()
+
+	capture, err := ParseEnriched(filepath.Join("..", "..", "testdata", "sniff", "sample-auth-capture.json"))
+	require.NoError(t, err)
+
+	apiSpec, err := AnalyzeCapture(capture)
+	require.NoError(t, err)
+
+	assert.Equal(t, "bearer_token", apiSpec.Auth.Type)
+	assert.Equal(t, "Authorization", apiSpec.Auth.Header)
+}
+
+func TestAnalyzeCapture_UsesCapturedCookieAuth(t *testing.T) {
+	t.Parallel()
+
+	capture := &EnrichedCapture{
+		TargetURL: "https://api.spotify.com",
+		Auth: &AuthCapture{
+			Cookies:     []string{"_session=abc"},
+			Type:        "cookie",
+			BoundDomain: "spotify.com",
+		},
+		Entries: []EnrichedEntry{
+			{
+				Method:         "GET",
+				URL:            "https://api.spotify.com/v1/me",
+				RequestHeaders: map[string]string{"Content-Type": "application/json"},
+			},
+		},
+	}
+
+	apiSpec, err := AnalyzeCapture(capture)
+	require.NoError(t, err)
+
+	assert.Equal(t, "cookie", apiSpec.Auth.Type)
+	assert.Equal(t, "Cookie", apiSpec.Auth.Header)
+	assert.Equal(t, "cookie", apiSpec.Auth.In)
+	assert.Equal(t, "informational only; no template support", apiSpec.Auth.Format)
+}
+
+func TestDetectAuth_PrefersCapturedAuthOverHeaders(t *testing.T) {
+	t.Parallel()
+
+	auth := detectAuth(&EnrichedCapture{
+		Auth: &AuthCapture{
+			Headers: map[string]string{"X-API-Key": "key-123"},
+			Type:    "api_key",
+		},
+	}, []EnrichedEntry{
+		{
+			RequestHeaders: map[string]string{"Authorization": "Bearer tok123"},
+		},
+	}, "spotify")
+
+	assert.Equal(t, "api_key", auth.Type)
+	assert.Equal(t, "X-API-Key", auth.Header)
+	assert.Equal(t, "header", auth.In)
+}
+
+func TestDetectAuth_FallsBackToHeaderInference(t *testing.T) {
+	t.Parallel()
+
+	auth := detectAuth(nil, []EnrichedEntry{
+		{
+			RequestHeaders: map[string]string{"Authorization": "Bearer tok123"},
+		},
+	}, "spotify")
+
+	assert.Equal(t, "bearer_token", auth.Type)
+	assert.Equal(t, "Authorization", auth.Header)
+}
+
+func TestWriteEnrichedCaptureUsesPrivatePermissions(t *testing.T) {
+	t.Parallel()
+
+	outputPath := filepath.Join(t.TempDir(), "capture.json")
+	err := WriteEnrichedCapture(&EnrichedCapture{
+		TargetURL: "https://api.spotify.com",
+		Auth: &AuthCapture{
+			Headers: map[string]string{"Authorization": "Bearer tok123"},
+			Type:    "bearer",
+		},
+	}, outputPath)
+	require.NoError(t, err)
+
+	info, err := os.Stat(outputPath)
+	require.NoError(t, err)
+	assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+}
diff --git a/internal/websniff/types.go b/internal/websniff/types.go
index 48597dc2..cc8edd9a 100644
--- a/internal/websniff/types.go
+++ b/internal/websniff/types.go
@@ -45,9 +45,18 @@ type EnrichedCapture struct {
 	TargetURL         string          `json:"target_url"`
 	CapturedAt        string          `json:"captured_at"`
 	InteractionRounds int             `json:"interaction_rounds"`
+	Auth              *AuthCapture    `json:"auth,omitempty"`
 	Entries           []EnrichedEntry `json:"entries"`
 }
 
+type AuthCapture struct {
+	Headers     map[string]string `json:"headers"`
+	Cookies     []string          `json:"cookies"`
+	Type        string            `json:"type"`
+	BoundDomain string            `json:"bound_domain"`
+	ExpiresAt   string            `json:"expires_at"`
+}
+
 type EnrichedEntry struct {
 	Method              string            `json:"method"`
 	URL                 string            `json:"url"`
diff --git a/testdata/sniff/sample-auth-capture-mismatch.json b/testdata/sniff/sample-auth-capture-mismatch.json
new file mode 100644
index 00000000..439b2b39
--- /dev/null
+++ b/testdata/sniff/sample-auth-capture-mismatch.json
@@ -0,0 +1,29 @@
+{
+  "target_url": "https://other.example.com",
+  "captured_at": "2026-03-28T20:00:00Z",
+  "interaction_rounds": 1,
+  "auth": {
+    "headers": {
+      "Authorization": "Bearer test-token-123"
+    },
+    "cookies": [],
+    "type": "bearer",
+    "bound_domain": "other.example.com",
+    "expires_at": "2026-03-28T21:00:00Z"
+  },
+  "entries": [
+    {
+      "method": "GET",
+      "url": "https://other.example.com/v1/me",
+      "request_headers": {
+        "Authorization": "Bearer test-token-123"
+      },
+      "request_body": "",
+      "response_status": 200,
+      "response_content_type": "application/json",
+      "response_body": "{\"ok\":true}",
+      "classification": "api",
+      "is_noise": false
+    }
+  ]
+}
diff --git a/testdata/sniff/sample-auth-capture.json b/testdata/sniff/sample-auth-capture.json
new file mode 100644
index 00000000..09da1f65
--- /dev/null
+++ b/testdata/sniff/sample-auth-capture.json
@@ -0,0 +1,30 @@
+{
+  "target_url": "https://api.spotify.com",
+  "captured_at": "2026-03-28T20:00:00Z",
+  "interaction_rounds": 1,
+  "auth": {
+    "headers": {
+      "Authorization": "Bearer test-token-123"
+    },
+    "cookies": [],
+    "type": "bearer",
+    "bound_domain": "api.spotify.com",
+    "expires_at": "2026-03-28T21:00:00Z"
+  },
+  "entries": [
+    {
+      "method": "GET",
+      "url": "https://api.spotify.com/v1/me/playlists",
+      "request_headers": {
+        "Authorization": "Bearer test-token-123",
+        "Content-Type": "application/json"
+      },
+      "request_body": "",
+      "response_status": 200,
+      "response_content_type": "application/json",
+      "response_body": "{\"items\":[{\"id\":\"abc\",\"name\":\"My Playlist\"}],\"total\":1}",
+      "classification": "api",
+      "is_noise": false
+    }
+  ]
+}

← 7df1537a docs(cli): fix Trevin article link in credits (#36)  ·  back to Cli Printing Press  ·  feat(websniff): add captured traffic test fixture generator 96bc65df →