From 0aa55b3b1c44470bd6c6026e586ee310aba4ac20 Mon Sep 17 00:00:00 2001 From: sofq Date: Sun, 29 Mar 2026 02:00:48 +0700 Subject: [PATCH] fix: correct OAuth2 base URL, harden OAuth callback, thread context, tighten permissions - Fix OAuth2 base URL from /wiki/rest/api/v2 (invalid) to /wiki/api/v2 - Validate OAuth2 callback code parameter before reporting success - Thread context.Context through FetchUserPages for cancellation support - Change template file permissions from 0644 to 0600 (consistent with codebase) - Add io.LimitReader to search response body reads (10 MiB cap) --- .planning/STATE.md | 12 ++++++------ cmd/avatar.go | 2 +- cmd/root.go | 2 +- cmd/search.go | 2 +- internal/avatar/fetch.go | 4 ++-- internal/avatar/fetch_test.go | 7 ++++--- internal/oauth2/threelo.go | 5 +++++ internal/template/template.go | 2 +- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 9150647..29d5ba2 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,15 @@ gsd_state_version: 1.0 milestone: v1.2 milestone_name: Workflow, Parity & Release Infrastructure -status: completed +status: executing stopped_at: Completed 18-02-PLAN.md -last_updated: "2026-03-28T18:26:28.046Z" +last_updated: "2026-03-28T18:40:40.131Z" last_activity: 2026-03-28 progress: total_phases: 7 - completed_phases: 6 + completed_phases: 7 total_plans: 19 - completed_plans: 17 + completed_plans: 19 percent: 100 --- @@ -21,13 +21,13 @@ progress: See: .planning/PROJECT.md (updated 2026-03-28) **Core value:** Give AI agents reliable, structured JSON access to Confluence content through a CLI -**Current focus:** Phase 16 — schema-gendocs +**Current focus:** Phase 18 — documentation-site ## Current Position Phase: 18 Plan: Not started -Status: Phase 16 complete +Status: Executing Phase 18 Last activity: 2026-03-28 Progress: [██████████] 100% (2/2 plans in phase 16) diff --git a/cmd/avatar.go b/cmd/avatar.go index 37779e6..30928c5 100644 --- a/cmd/avatar.go +++ b/cmd/avatar.go @@ -40,7 +40,7 @@ func runAvatarAnalyze(cmd *cobra.Command, args []string) error { return &cferrors.AlreadyWrittenError{Code: cferrors.ExitValidation} } - pages, err := avatar.FetchUserPages(c, userFlag) + pages, err := avatar.FetchUserPages(cmd.Context(), c, userFlag) if err != nil { // Classify error: 401/unauthorized/auth → ExitAuth, else ExitError. errStr := strings.ToLower(err.Error()) diff --git a/cmd/root.go b/cmd/root.go index 8d192d8..e722fb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -155,7 +155,7 @@ var rootCmd = &cobra.Command{ // Switch base URL to Atlassian API proxy. resolved.BaseURL = fmt.Sprintf( - "https://api.atlassian.com/ex/confluence/%s/wiki/rest/api/v2", + "https://api.atlassian.com/ex/confluence/%s/wiki/api/v2", effectiveCloudID, ) } diff --git a/cmd/search.go b/cmd/search.go index d30e8c1..63fb413 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -43,7 +43,7 @@ func fetchV1(cmd *cobra.Command, c *client.Client, fullURL string) ([]byte, int) } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) if err != nil { apiErr := &cferrors.APIError{ErrorType: "connection_error", Message: "reading response body: " + err.Error()} apiErr.WriteJSON(c.Stderr) diff --git a/internal/avatar/fetch.go b/internal/avatar/fetch.go index 14b3a32..1d19bce 100644 --- a/internal/avatar/fetch.go +++ b/internal/avatar/fetch.go @@ -61,7 +61,7 @@ type contentPage struct { // FetchUserPages fetches Confluence pages created by accountID. // It uses the v1 content API with CQL search and returns up to 200 pages. // The Body field of each PageRecord contains plain text (HTML stripped). -func FetchUserPages(c *client.Client, accountID string) ([]PageRecord, error) { +func FetchUserPages(ctx context.Context, c *client.Client, accountID string) ([]PageRecord, error) { cql := fmt.Sprintf(`creator = "%s" AND type = page ORDER BY lastModified DESC`, escapeCQLString(accountID)) @@ -77,7 +77,7 @@ func FetchUserPages(c *client.Client, accountID string) ([]PageRecord, error) { const maxPages = 200 for nextURL != "" && len(records) < maxPages { - body, err := fetchContentV1(context.TODO(), c, nextURL) + body, err := fetchContentV1(ctx, c, nextURL) if err != nil { return nil, err } diff --git a/internal/avatar/fetch_test.go b/internal/avatar/fetch_test.go index 68fea1b..d53cd62 100644 --- a/internal/avatar/fetch_test.go +++ b/internal/avatar/fetch_test.go @@ -1,6 +1,7 @@ package avatar_test import ( + "context" "encoding/json" "fmt" "io" @@ -144,7 +145,7 @@ func TestFetchUserPages_HappyPath(t *testing.T) { defer srv.Close() c := newTestClient(srv.URL + "/wiki/api/v2") - records, err := avatar.FetchUserPages(c, accountID) + records, err := avatar.FetchUserPages(context.Background(), c, accountID) if err != nil { t.Fatalf("FetchUserPages returned error: %v", err) } @@ -191,7 +192,7 @@ func TestFetchUserPages_EmptyResults(t *testing.T) { defer srv.Close() c := newTestClient(srv.URL + "/wiki/api/v2") - records, err := avatar.FetchUserPages(c, "user123") + records, err := avatar.FetchUserPages(context.Background(), c, "user123") if err != nil { t.Fatalf("FetchUserPages returned error: %v", err) } @@ -210,7 +211,7 @@ func TestFetchUserPages_HTTP401(t *testing.T) { defer srv.Close() c := newTestClient(srv.URL + "/wiki/api/v2") - records, err := avatar.FetchUserPages(c, "user123") + records, err := avatar.FetchUserPages(context.Background(), c, "user123") if err == nil { t.Fatalf("expected error on 401, got nil (records: %v)", records) } diff --git a/internal/oauth2/threelo.go b/internal/oauth2/threelo.go index dcd00a9..74da3cd 100644 --- a/internal/oauth2/threelo.go +++ b/internal/oauth2/threelo.go @@ -175,6 +175,11 @@ func waitForCallback(listener net.Listener, expectedState string, timeout time.D return } code := r.URL.Query().Get("code") + if code == "" { + errCh <- fmt.Errorf("missing code parameter in callback") + http.Error(w, "Authorization failed: missing code parameter.", http.StatusBadRequest) + return + } fmt.Fprint(w, "Authorization successful! You may close this window.") codeCh <- code }) diff --git a/internal/template/template.go b/internal/template/template.go index 606816a..5501ad0 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -183,7 +183,7 @@ func Save(name string, tmpl *Template) error { return fmt.Errorf("marshal template: %w", err) } - if err := os.WriteFile(path, data, 0o644); err != nil { + if err := os.WriteFile(path, data, 0o600); err != nil { return fmt.Errorf("write template: %w", err) } return nil