// Package tlog is the attest-reconciler's HTTP client for the Transparency Log // (ADR-0005). Mirrors admin-api/internal/tlog.HTTPSubmitter byte-for-byte so // resubmits from the reconciler collide with the admin-api's original log // entry — the server's idempotency key covers both paths. package tlog import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "time" "basicstack.de/proofedmesh/services/attest-reconciler/internal/attest" ) type HTTPSubmitter struct { BaseURL string Client *http.Client TokenSource attest.TokenSource } type submitRequest struct { Envelope attest.Envelope `json:"envelope"` ContentHash string `json:"content_hash"` } func (s HTTPSubmitter) Submit(ctx context.Context, env attest.Envelope, contentHash string) (attest.LogReceipt, error) { if s.BaseURL == "" { return attest.LogReceipt{}, errors.New("tlog.HTTPSubmitter: BaseURL is empty") } cli := s.Client if cli == nil { cli = &http.Client{Timeout: 10 * time.Second} } body, err := json.Marshal(submitRequest{Envelope: env, ContentHash: contentHash}) if err != nil { return attest.LogReceipt{}, fmt.Errorf("marshal: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.BaseURL+"/api/v1/log/entries", bytes.NewReader(body)) if err != nil { return attest.LogReceipt{}, err } req.Header.Set("Content-Type", "application/json") if s.TokenSource != nil { tok, err := s.TokenSource.Token(ctx) if err != nil { return attest.LogReceipt{}, fmt.Errorf("token: %w", err) } req.Header.Set("Authorization", "Bearer "+tok) } resp, err := cli.Do(req) if err != nil { return attest.LogReceipt{}, err } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) switch resp.StatusCode { case http.StatusOK, http.StatusCreated: default: return attest.LogReceipt{}, fmt.Errorf("tlog: status %d: %s", resp.StatusCode, string(raw)) } var out attest.LogReceipt if err := json.Unmarshal(raw, &out); err != nil { return attest.LogReceipt{}, fmt.Errorf("decode: %w", err) } if out.ContentHash != contentHash { return out, fmt.Errorf("tlog response content_hash %q != sent %q", out.ContentHash, contentHash) } if out.LogIndex < 0 { return out, fmt.Errorf("tlog response has negative log_index %d", out.LogIndex) } return out, nil }