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

@@ -89,6 +89,67 @@ func parityBroadcastsCommand() bonfire.Command {
}
}
func parityUpstreamCommand() bonfire.Command {
return bonfire.Command{
Name: "parity:upstream",
Description: "Record the vendor HTTPS calls a reference backend sends through a loopback recording proxy into an upstream sidecar",
Flags: []bonfire.Flag{
{Name: "listen", Description: "Loopback address of the recording proxy", Default: tide.DefaultUpstreamProxyListen},
{Name: "ca-dir", Description: "Directory of the local parity CA, outside the fixtures tree (key kept mode 0600)"},
{Name: "out", Description: "Destination sidecar path (<fixture>.upstream.yaml)"},
{Name: "mode", Description: "script (answer from --script) or forward (send once to the real vendor)", Default: "script"},
{Name: "script", Description: "YAML file of scripted vendor responses (script mode)"},
{Name: "vars", Description: "Private mode-0600 variable store outside fixtures; its values are masked in the sidecar"},
},
Run: runParityUpstream,
}
}
func runParityUpstream(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
caDir, err := requireFlag(in, "ca-dir", "parity:upstream")
if err != nil {
return err
}
outPath, err := requireFlag(in, "out", "parity:upstream")
if err != nil {
return err
}
varsPath, err := requireFlag(in, "vars", "parity:upstream")
if err != nil {
return err
}
listen := flagValue(in, "listen")
if listen == "" {
listen = tide.DefaultUpstreamProxyListen
}
proxy, err := tide.NewUpstreamProxy(tide.UpstreamProxyConfig{
Listen: listen,
CADir: caDir,
Out: outPath,
Mode: flagValue(in, "mode"),
Script: flagValue(in, "script"),
VarsPath: varsPath,
})
if err != nil {
return err
}
certPath := filepath.Join(caDir, "parity-ca.pem")
if abs, err := filepath.Abs(certPath); err == nil {
certPath = abs
}
out.Info(fmt.Sprintf("upstream proxy listening on %s", listen))
out.Info(fmt.Sprintf("parity CA certificate: %s", certPath))
out.Info(fmt.Sprintf("point the reference backend at it: HTTPS_PROXY=http://%s, curl.cainfo and openssl.cafile=%s", listen, certPath))
if err := proxy.ListenAndServe(ctx); err != nil {
return err
}
if err := proxy.Flush(); err != nil {
return err
}
out.Success(fmt.Sprintf("wrote %s", outPath))
return nil
}
func runParityBroadcasts(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
flowPath, err := requireFlag(in, "flow", "parity:broadcasts")
if err != nil {