feat(14-01): record vendor calls with summer parity:upstream and replay them offline

- WriteUpstream masks vars, hashes long base64 JSON strings and refuses unmasked Authorization/X-Api-Key
- multipart requests recorded as ordered parts; the fake compares parts and hashed payloads
- loopback CONNECT recording proxy with a local ECDSA parity CA, script and forward modes
- parity:upstream command, README and parity docs
This commit is contained in:
Jakub Zych
2026-10-03 19:55:42 +02:00
parent e6a67134d1
commit ee0004fb65
11 changed files with 1647 additions and 15 deletions

View File

@@ -2,7 +2,13 @@ package tide
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
@@ -148,3 +154,135 @@ func TestLoadUpstreamAndPath(t *testing.T) {
t.Fatalf("sidecar = %+v", s)
}
}
func TestWriteUpstreamRefusesUnmaskedCredential(t *testing.T) {
store := upstreamTestStore(t)
dir := t.TempDir()
base := func(h map[string]string) UpstreamSidecar {
return UpstreamSidecar{Version: 1, Exchanges: []UpstreamExchange{{
Request: UpstreamRequest{Method: "GET", URL: "https://api.example.test/v1/me", Headers: h},
Response: UpstreamResponse{Status: 200},
}}}
}
refused := []map[string]string{
{"Authorization": "Bearer live-unknown-token"},
{"X-Api-Key": "sk-live-unknown"},
{"x-api-key": "plainvalue"},
{"Authorization": "Bearer abc{{secret:example-token}}"},
{"Authorization": "Bearer example-token-value extra"},
}
for _, h := range refused {
path := filepath.Join(dir, "refused.upstream.yaml")
err := WriteUpstream(path, base(h), store)
if err == nil {
t.Fatalf("%v: unmasked credential written", h)
}
for name, v := range h {
if !strings.Contains(err.Error(), name) {
t.Fatalf("error %q does not name header %s", err, name)
}
if strings.Contains(err.Error(), v) {
t.Fatalf("error leaks the value: %v", err)
}
}
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
t.Fatalf("%v: file written despite refusal", h)
}
}
accepted := []map[string]string{
{"Authorization": "Bearer example-token-value"},
{"Authorization": "Discogs token=example-token-value"},
{"X-Api-Key": "example-token-value"},
{"Authorization": ""},
}
for _, h := range accepted {
path := filepath.Join(dir, "ok.upstream.yaml")
if err := WriteUpstream(path, base(h), store); err != nil {
t.Fatalf("%v: %v", h, err)
}
raw, _ := os.ReadFile(path)
if strings.Contains(string(raw), "example-token-value") {
t.Fatalf("written sidecar leaks the value:\n%s", raw)
}
if _, err := LoadUpstream(path); err != nil {
t.Fatalf("written sidecar does not load: %v", err)
}
}
// The masked URL query and response body round-trip through the fake.
s := base(map[string]string{"Authorization": "Bearer example-token-value"})
s.Exchanges[0].Request.URL = "https://api.example.test/v1/me?token=example-token-value"
s.Exchanges[0].Response.Body = `{"token":"example-token-value"}`
path := filepath.Join(dir, "query.upstream.yaml")
if err := WriteUpstream(path, s, store); err != nil {
t.Fatal(err)
}
raw, _ := os.ReadFile(path)
if strings.Contains(string(raw), "example-token-value") || !strings.Contains(string(raw), "token={{secret:example-token}}") {
t.Fatalf("query not masked:\n%s", raw)
}
if err := WriteUpstream(path, UpstreamSidecar{Exchanges: []UpstreamExchange{{Request: UpstreamRequest{Method: "GET", URL: "relative"}}}}, store); err == nil {
t.Fatal("invalid sidecar must not be written")
}
}
func TestUpstreamFakeHashesBase64Bodies(t *testing.T) {
img := make([]byte, 1536) // 2 KiB of base64
for i := range img {
img[i] = byte(i * 7)
}
b64 := base64.StdEncoding.EncodeToString(img)
if len(b64) < 2000 {
t.Fatalf("fixture too small: %d", len(b64))
}
sent := `{"model":"m","messages":[{"content":[{"type":"image","source":{"data":"` + b64 + `"}},{"type":"image_url","url":"data:image/png;base64,` + b64 + `"}]}]}`
s := UpstreamSidecar{Version: 1, Exchanges: []UpstreamExchange{{
Request: UpstreamRequest{
Method: "POST",
URL: "https://api.example.test/v1/messages",
Headers: map[string]string{"Content-Type": "application/json", "X-Api-Key": "example-token-value"},
// PHP escapes "/" in JSON strings; the stored form must still hash.
Body: strings.ReplaceAll(sent, "/", `\/`),
},
Response: UpstreamResponse{Status: 200, Body: `{"ok":true}`},
}}}
path := filepath.Join(t.TempDir(), "img.upstream.yaml")
store := upstreamTestStore(t)
if err := WriteUpstream(path, s, store); err != nil {
t.Fatal(err)
}
raw, _ := os.ReadFile(path)
if strings.Contains(string(raw), b64[:64]) {
t.Fatalf("base64 payload kept in the sidecar")
}
sum := sha256.Sum256(img)
ph := "{{sha256:" + hex.EncodeToString(sum[:]) + "}}"
if !strings.Contains(string(raw), ph) || !strings.Contains(string(raw), "data:image\\\\/png;base64,"+ph) && !strings.Contains(string(raw), "data:image/png;base64,"+ph) {
t.Fatalf("sidecar lacks the hash placeholders:\n%s", raw)
}
loaded, err := LoadUpstream(path)
if err != nil {
t.Fatal(err)
}
send := func(body string) error {
req, _ := http.NewRequest("POST", "https://api.example.test/v1/messages", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", "example-token-value")
f := NewUpstreamFake(loaded, store)
_, err := f.RoundTrip(req)
return err
}
if err := send(sent); err != nil {
t.Fatalf("same bytes rejected: %v", err)
}
changed := slices.Clone(img)
changed[100] ^= 1
b64c := base64.StdEncoding.EncodeToString(changed)
err = send(strings.Replace(sent, b64, b64c, 1))
if err == nil || !strings.Contains(err.Error(), "body $.messages[0].content[0].source.data") {
t.Fatalf("one changed byte accepted or misreported: %v", err)
}
if len(err.Error()) > 1000 {
t.Fatalf("mismatch message not clipped: %d bytes", len(err.Error()))
}
}