diff --git a/cmd/optiqor/main.go b/cmd/optiqor/main.go index 477d675..95b331d 100644 --- a/cmd/optiqor/main.go +++ b/cmd/optiqor/main.go @@ -126,15 +126,16 @@ func versionTemplate() string { func newAnalyzeCmd() *cobra.Command { var ( - jsonOut bool - htmlPath string - offline bool - shareFlag bool - roast bool - minSev string - detectors []string - failOn string // severity threshold that triggers exit code 1 - outputPath string + jsonOut bool + htmlPath string + offline bool + shareFlag bool + privateFlag bool + roast bool + minSev string + detectors []string + failOn string // severity threshold that triggers exit code 1 + outputPath string ) cmd := &cobra.Command{ Use: "analyze [chart]", @@ -148,7 +149,8 @@ side-effect of parsing — they are not the headline feature. optiqor analyze ./values.yaml --json optiqor analyze ./chart --severity=med --fail-on=high optiqor analyze ./chart --detector cpu-overprovisioned --detector missing-memory-limit - optiqor analyze ./my-chart --roast # same findings, snarkier titles`, + optiqor analyze ./my-chart --roast # same findings, snarkier titles + optiqor analyze ./my-chart --share --private # login-gated share link`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { path := "." @@ -159,6 +161,11 @@ side-effect of parsing — they are not the headline feature. if err != nil { return err } + + if privateFlag && !shareFlag { + return fmt.Errorf("--private requires --share: pass both flags together") + } + rep, err := analyze.RunPath(abs) if err != nil { return err @@ -196,7 +203,7 @@ side-effect of parsing — they are not the headline feature. return err } if shareFlag { - emitShareURL(cmd, rep) + emitShareURL(cmd, rep, privateFlag) } _ = offline return checkFailOn(rep, effFailOn) @@ -206,6 +213,7 @@ side-effect of parsing — they are not the headline feature. cmd.Flags().StringVar(&htmlPath, "html", "", "also write a self-contained HTML report to this path") cmd.Flags().BoolVar(&offline, "offline", true, "do not perform any network calls (always true in Phase 1)") cmd.Flags().BoolVar(&shareFlag, "share", false, "print optiqor.dev/r/ for the sanitised analysis (no upload in Phase 1)") + cmd.Flags().BoolVar(&privateFlag, "private", false, "make the share link login-gated (requires --share; sends X-Optiqor-Private: 1)") cmd.Flags().BoolVar(&roast, "roast", false, "humorous output (findings stay accurate)") cmd.Flags().StringVar(&minSev, "severity", "", "drop findings below this severity (low|med|high)") cmd.Flags().StringArrayVar(&detectors, "detector", nil, "only run findings from these detector IDs (repeatable)") @@ -264,20 +272,36 @@ func openOutput(cmd *cobra.Command, path string) (io.Writer, func(), error) { return f, func() { _ = f.Close() }, nil } -// emitShareURL handles --share end-to-end. Prints to stderr so -// stdout (JSON/text) stays clean. Never blocks the success path — on -// upload failure we still print the URL so the user has a stable -// identifier to re-share later. OPTIQOR_SHARE_URL overrides the -// endpoint for self-hosted deploys. -func emitShareURL(cmd *cobra.Command, rep any) { +// emitShareURL handles the `--share` flag end-to-end. +// +// It computes the local content-addressable hash, attempts to upload +// the sanitised payload to the sandbox endpoint, and prints the +// resulting `optiqor.dev/r/` URL to stderr (so JSON/text output on +// stdout stays clean). +// +// When private is true the upload request carries X-Optiqor-Private: 1 +// so the sandbox receiver returns a token-gated URL (Phase 2 backend +// work). The CLI side only sets the header; the receiver handles the +// rest. +// +// The function never blocks the caller's success path — if the upload +// fails (offline, sandbox down, 5xx), we still print the URL so the +// user has a stable identifier they can re-share later. The endpoint +// is overridable via OPTIQOR_SHARE_URL for self-hosted deploys. + +func emitShareURL(cmd *cobra.Command, rep any, private bool) { endpoint := os.Getenv("OPTIQOR_SHARE_URL") - res := share.Upload(rep, endpoint) + res := share.Upload(rep, endpoint, private) if res.Hash == "" { return } suffix := "" if res.Posted { - suffix = " (uploaded)" + if private { + suffix = " (uploaded · login-gated)" + } else { + suffix = " (uploaded)" + } } else if res.Error != "" { suffix = " (offline / not uploaded — " + res.Error + ")" } diff --git a/internal/share/share.go b/internal/share/share.go index 1c1b910..e325425 100644 --- a/internal/share/share.go +++ b/internal/share/share.go @@ -111,10 +111,19 @@ type UploadResult struct { Error string } -// Upload POSTs the sanitised report JSON. Hard rule: the only -// outbound call the CLI makes, and only on --share. Never retries, -// never logs bodies, never sends anything but the sanitised payload. -func Upload(report any, endpoint string) UploadResult { +// Upload attempts to POST the sanitised report JSON to endpoint and +// returns the resulting (hash, URL, posted) tuple. +// +// When private is true, the request carries X-Optiqor-Private: 1 so +// the sandbox receiver returns a token-gated URL (Phase 2 backend work). +// The CLI side only sets the header; the token arrives in the response +// body once the receiver is live. +// +// CLI hard rule: this is the only outbound network call optiqor makes, +// and only when the user explicitly passes --share. The function never +// retries; never logs request bodies; never sends anything but the +// sanitised payload. +func Upload(report any, endpoint string, private bool) UploadResult { hash, err := Hash(report) if err != nil { return UploadResult{Error: err.Error()} @@ -142,6 +151,9 @@ func Upload(report any, endpoint string) UploadResult { req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Optiqor-Hash", hash) req.Header.Set("User-Agent", "optiqor-cli") + if private { + req.Header.Set("X-Optiqor-Private", "1") + } client := &http.Client{Timeout: uploadTimeout} resp, err := client.Do(req) diff --git a/internal/share/share_test.go b/internal/share/share_test.go index faef547..f5e0b9f 100644 --- a/internal/share/share_test.go +++ b/internal/share/share_test.go @@ -175,7 +175,7 @@ func TestUpload(t *testing.T) { defer srv.Close() } - res := Upload(tc.input, srv.URL) + res := Upload(tc.input, srv.URL, false) if res.Posted != tc.wantPost { t.Fatalf("Posted = %v want %v; Error=%q", res.Posted, tc.wantPost, res.Error) @@ -216,3 +216,34 @@ func TestIsHash(t *testing.T) { }) } } + +func TestUpload_PrivateHeaderSent(t *testing.T) { + var gotPrivate string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPrivate = r.Header.Get("X-Optiqor-Private") + w.WriteHeader(http.StatusAccepted) + })) + defer srv.Close() + + res := Upload(sample{Workloads: 1}, srv.URL, true) + if !res.Posted { + t.Fatalf("Posted = false, error = %q", res.Error) + } + if gotPrivate != "1" { + t.Errorf("X-Optiqor-Private = %q, want %q", gotPrivate, "1") + } +} + +func TestUpload_NoPrivateHeaderByDefault(t *testing.T) { + var gotPrivate string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPrivate = r.Header.Get("X-Optiqor-Private") + w.WriteHeader(http.StatusAccepted) + })) + defer srv.Close() + + Upload(sample{Workloads: 1}, srv.URL, false) + if gotPrivate != "" { + t.Errorf("X-Optiqor-Private should be absent, got %q", gotPrivate) + } +}